diff --git a/.gitattributes b/.gitattributes index 027012e27..769041a17 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,4 +17,12 @@ kernel/config-microvm-debug text eol=lf # Linux hosts read the setup tool manifest, including copies staged from Windows. scripts/setup/tool-versions.conf text eol=lf -.github/workflows/*.lock.yml linguist-generated=true \ No newline at end of file +.github/workflows/*.lock.yml linguist-generated=true +# Recorded from the Python code that nvx-rs replaced, by scripts/nvx_rs_fixtures.py, +# which microsoft/nvx#359 retired with that code; they are frozen golden data. +nvx-rs/src/host/python/unicode.rs linguist-generated=true +nvx-rs/tests/fixtures/cli.json linguist-generated=true +nvx-rs/tests/fixtures/egress-policy.json linguist-generated=true +nvx-rs/tests/fixtures/python.json linguist-generated=true +nvx-rs/tests/fixtures/sandbox.json linguist-generated=true +nvx-rs/tests/fixtures/sandbox_options.json linguist-generated=true \ No newline at end of file diff --git a/.github/actions/build-guest-artifacts/action.yml b/.github/actions/build-guest-artifacts/action.yml index c90dfeba2..1fb6039f2 100644 --- a/.github/actions/build-guest-artifacts/action.yml +++ b/.github/actions/build-guest-artifacts/action.yml @@ -73,21 +73,23 @@ runs: AZURELINUX_VERSION: ${{ steps.guest-versions.outputs.azurelinux-version }} KERNEL_INPUT_HASH: ${{ hashFiles('SOURCE-MANIFEST.json', 'kernel/config-microvm', 'kernel/patches/**', 'docker/Dockerfile', '.dockerignore', 'scripts/nvx_tools/build.py', 'scripts/nvx_tools/build_config.py', 'scripts/nvx_tools/build_constants.py') }} DEBUG_KERNEL_INPUT_HASH: ${{ hashFiles('SOURCE-MANIFEST.json', 'kernel/config-microvm', 'kernel/config-microvm-debug', 'kernel/patches/**', 'docker/Dockerfile', '.dockerignore', 'scripts/nvx_tools/build.py', 'scripts/nvx_tools/build_config.py', 'scripts/nvx_tools/build_constants.py') }} - ALPINE_INPUT_HASH: ${{ hashFiles('SOURCE-MANIFEST.json', 'guest/common/**', 'guest/alpine/**', 'docker/Dockerfile', '.dockerignore', 'scripts/nvx_tools/build.py', 'scripts/nvx_tools/build_config.py', 'scripts/nvx_tools/build_constants.py', 'scripts/nvx_tools/common.py', 'scripts/nvx_tools/guests.py') }} - UBUNTU_INPUT_HASH: ${{ hashFiles('SOURCE-MANIFEST.json', 'guest/common/**', 'guest/ubuntu/**', 'ubuntu/**', 'docker/Dockerfile', '.dockerignore', 'scripts/nvx_tools/build.py', 'scripts/nvx_tools/build_config.py', 'scripts/nvx_tools/build_constants.py', 'scripts/nvx_tools/common.py', 'scripts/nvx_tools/guests.py', 'scripts/nvx_tools/ubuntu.py') }} - AZURELINUX_INPUT_HASH: ${{ hashFiles('SOURCE-MANIFEST.json', 'guest/common/**', 'azurelinux/**', 'docker/Dockerfile', '.dockerignore', 'scripts/nvx_tools/azurelinux.py', 'scripts/nvx_tools/build.py', 'scripts/nvx_tools/build_config.py', 'scripts/nvx_tools/build_constants.py', 'scripts/nvx_tools/common.py', 'scripts/nvx_tools/guests.py') }} + ALPINE_INPUT_HASH: ${{ hashFiles('SOURCE-MANIFEST.json', 'guest/common/**', 'guest/alpine/**', 'nvx-rs/**', '!nvx-rs/target/**', 'rust-toolchain.toml', 'docker/Dockerfile', '.dockerignore', 'scripts/nvx_tools/build.py', 'scripts/nvx_tools/build_config.py', 'scripts/nvx_tools/build_constants.py', 'scripts/nvx_tools/common.py', 'scripts/nvx_tools/elf.py', 'scripts/nvx_tools/guest_binaries.py', 'scripts/nvx_tools/guests.py') }} + UBUNTU_INPUT_HASH: ${{ hashFiles('SOURCE-MANIFEST.json', 'guest/common/**', 'guest/ubuntu/**', 'ubuntu/**', 'nvx-rs/**', '!nvx-rs/target/**', 'rust-toolchain.toml', 'docker/Dockerfile', '.dockerignore', 'scripts/nvx_tools/build.py', 'scripts/nvx_tools/build_config.py', 'scripts/nvx_tools/build_constants.py', 'scripts/nvx_tools/common.py', 'scripts/nvx_tools/elf.py', 'scripts/nvx_tools/guest_binaries.py', 'scripts/nvx_tools/guests.py', 'scripts/nvx_tools/ubuntu.py') }} + AZURELINUX_INPUT_HASH: ${{ hashFiles('SOURCE-MANIFEST.json', 'guest/common/**', 'azurelinux/**', 'nvx-rs/**', '!nvx-rs/target/**', 'rust-toolchain.toml', 'docker/Dockerfile', '.dockerignore', 'scripts/nvx_tools/azurelinux.py', 'scripts/nvx_tools/build.py', 'scripts/nvx_tools/build_config.py', 'scripts/nvx_tools/build_constants.py', 'scripts/nvx_tools/common.py', 'scripts/nvx_tools/elf.py', 'scripts/nvx_tools/guest_binaries.py', 'scripts/nvx_tools/guests.py') }} run: | set -euo pipefail # hashFiles hashes contents only, so the kernel keys also name each patch - # path and its order. + # path and its order, and the guest image keys the path-aware digest of + # the Rust guest sources that their package manifests record. patch_identity=$(python3 scripts/nvx.py kernel-patch-identity) + rust_source_identity=$(python3 scripts/nvx.py guest-binary-source-identity) { echo "kernel=linux-kernel-v1-${KERNEL_VERSION}-${KERNEL_INPUT_HASH}-${patch_identity}" echo "kernel-debug=linux-kernel-debug-v1-${KERNEL_VERSION}-${DEBUG_KERNEL_INPUT_HASH}-${patch_identity}" - echo "alpine=alpine-image-v1-${ALPINE_VERSION}-${ALPINE_INPUT_HASH}" - echo "ubuntu=ubuntu-initramfs-v1-${UBUNTU_VERSION}-${UBUNTU_INPUT_HASH}" - echo "ubuntu-distro=ubuntu-distro-v1-${UBUNTU_VERSION}-${UBUNTU_INPUT_HASH}" - echo "azurelinux=azurelinux-initramfs-v1-${AZURELINUX_VERSION}-${AZURELINUX_INPUT_HASH}" + echo "alpine=alpine-image-v2-${ALPINE_VERSION}-${ALPINE_INPUT_HASH}-${rust_source_identity}" + echo "ubuntu=ubuntu-initramfs-v2-${UBUNTU_VERSION}-${UBUNTU_INPUT_HASH}-${rust_source_identity}" + echo "ubuntu-distro=ubuntu-distro-v2-${UBUNTU_VERSION}-${UBUNTU_INPUT_HASH}-${rust_source_identity}" + echo "azurelinux=azurelinux-initramfs-v2-${AZURELINUX_VERSION}-${AZURELINUX_INPUT_HASH}-${rust_source_identity}" } >> "${GITHUB_OUTPUT}" - name: Restore Linux kernel (zstd) diff --git a/.github/actions/check-nvx-rs/action.yml b/.github/actions/check-nvx-rs/action.yml index f4ff8af1c..de4540def 100644 --- a/.github/actions/check-nvx-rs/action.yml +++ b/.github/actions/check-nvx-rs/action.yml @@ -61,7 +61,8 @@ runs: cargo "+${{ steps.toolchains.outputs.toolchain }}" test --locked --all-features cargo "+${{ steps.toolchains.outputs.toolchain }}" test --locked - # Stages a synthetic release, which the bundled-artifacts test then requires to find. + # Stages a synthetic release in the current layout, with nvx-rs and its + # provenance, which the bundled-artifacts test then requires to find. - name: Check artifact bundling shell: bash working-directory: nvx-rs @@ -69,18 +70,23 @@ runs: set -euo pipefail bundle=$(mktemp -d) executable=openvmm + cli=nvx-rs bundle_dir="${bundle}" if [[ "${RUNNER_OS}" == Windows ]]; then executable=openvmm.exe + cli=nvx-rs.exe bundle_dir=$(cygpath -w "${bundle}") fi - mkdir -p "${bundle}/bin" "${bundle}/guest" + mkdir -p "${bundle}/bin" "${bundle}/guest" "${bundle}/provenance" printf openvmm > "${bundle}/bin/${executable}" + printf nvx-rs > "${bundle}/bin/${cli}" + printf '{}' > "${bundle}/provenance/nvx-rs.provenance.json" printf kernel > "${bundle}/guest/vmlinux" printf initramfs > "${bundle}/guest/initramfs.cpio.gz" cp ../SOURCE-MANIFEST.json "${bundle}/" - (cd "${bundle}" && sha256sum "bin/${executable}" guest/vmlinux \ - guest/initramfs.cpio.gz SOURCE-MANIFEST.json > SHA256SUMS) + (cd "${bundle}" && sha256sum "bin/${executable}" "bin/${cli}" guest/vmlinux \ + guest/initramfs.cpio.gz provenance/nvx-rs.provenance.json \ + SOURCE-MANIFEST.json > SHA256SUMS) NVX_BUNDLE_DIR="${bundle_dir}" cargo "+${{ steps.toolchains.outputs.toolchain }}" test --locked --lib \ --features bundled artifacts diff --git a/.github/actions/package-release/action.yml b/.github/actions/package-release/action.yml index 036485c4d..ec4650084 100644 --- a/.github/actions/package-release/action.yml +++ b/.github/actions/package-release/action.yml @@ -1,14 +1,40 @@ name: Package release -description: Build and upload an NVX binary release for one platform +description: Build an NVX binary release for one platform, and upload it for publication inputs: platform: description: Platform suffix used in the package and artifact names required: true + nvx-rs-artifact: + description: Base name of the nvx-rs artifacts for the runner's OS + required: true + upload: + description: Whether to upload the package for the development release + required: true runs: using: composite steps: + # Every package ships the nvx-rs that the NVX CLI job of its OS built and + # tested, with its provenance, which nvx.py download installs where + # nvx.py looks for them. + - name: Download nvx-rs executable + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.nvx-rs-artifact }}-executable + path: nvx-rs/target/release + + - name: Download nvx-rs provenance + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.nvx-rs-artifact }}-provenance + path: build + + - name: Make nvx-rs executable + if: runner.os != 'Windows' + shell: bash + run: chmod +x nvx-rs/target/release/nvx-rs + - name: Rematerialize canonical kernel inputs if: runner.os == 'Windows' shell: powershell @@ -52,7 +78,7 @@ runs: if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Upload Linux release - if: runner.os != 'Windows' + if: runner.os != 'Windows' && inputs.upload == 'true' uses: actions/upload-artifact@v7 with: name: release-${{ inputs.platform }} @@ -62,7 +88,7 @@ runs: retention-days: 1 - name: Upload Windows release - if: runner.os == 'Windows' + if: runner.os == 'Windows' && inputs.upload == 'true' uses: actions/upload-artifact@v7 with: name: release-${{ inputs.platform }} diff --git a/.github/actions/validate-nvx/action.yml b/.github/actions/validate-nvx/action.yml index 68ad94130..04debeddc 100644 --- a/.github/actions/validate-nvx/action.yml +++ b/.github/actions/validate-nvx/action.yml @@ -10,7 +10,7 @@ runs: run: | set -euo pipefail python3 -m compileall -q scripts - python3 -m unittest scripts/test_performance.py scripts/test_nvx_tools.py scripts/test_guest_binaries.py scripts/test_microvm_tests.py scripts/test_development_release.py scripts/test_egress_policy.py scripts/test_control_session.py scripts/test_managed_agent.py scripts/test_guest_time.py scripts/test_guest_ingress.py scripts/test_host_telemetry.py scripts/test_time_abi.py scripts/test_tool_versions.py scripts/test_markdown_links.py -v + python3 -m unittest scripts/test_performance.py scripts/test_nvx_tools.py scripts/test_guest_binaries.py scripts/test_microvm_tests.py scripts/test_development_release.py scripts/test_host_telemetry.py scripts/test_time_abi.py scripts/test_tool_versions.py scripts/test_markdown_links.py -v python3 scripts/test_adversarial.py -v python3 .github/skills/nvx-host-connect/scripts/test_hosts.py -v python3 scripts/nvx.py --help @@ -30,7 +30,7 @@ runs: $ErrorActionPreference = "Stop" python -m compileall -q scripts if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - python -m unittest scripts/test_performance.py scripts/test_nvx_tools.py scripts/test_guest_binaries.py scripts/test_microvm_tests.py scripts/test_development_release.py scripts/test_egress_policy.py scripts/test_control_session.py scripts/test_managed_agent.py scripts/test_guest_time.py scripts/test_guest_ingress.py scripts/test_host_telemetry.py scripts/test_time_abi.py scripts/test_tool_versions.py scripts/test_markdown_links.py -v + python -m unittest scripts/test_performance.py scripts/test_nvx_tools.py scripts/test_guest_binaries.py scripts/test_microvm_tests.py scripts/test_development_release.py scripts/test_host_telemetry.py scripts/test_time_abi.py scripts/test_tool_versions.py scripts/test_markdown_links.py -v if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } python scripts\test_adversarial.py -v if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c45fdd946..0007defa8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -50,9 +50,10 @@ [copilot-setup-steps](./workflows/copilot-setup-steps.yml) prepares. It initializes `openvmm/`; installs `requirements-dev.txt` into a Python virtual environment on `PATH`, the Rust release that `rust-toolchain.toml` pins with - the OpenVMM test targets, the minimum supported Rust versions of the - `aci_edge_sandboxes` and `nvx-rs` crates, cargo-nextest, and the native guest build - prerequisites; grants `/dev/kvm` access; pulls the pinned shell linter images; + the OpenVMM test targets and the `x86_64-unknown-linux-musl` target used by + the Rust guest binaries, the minimum supported Rust versions of the + `aci_edge_sandboxes` and `nvx-rs` crates, cargo-nextest, and the remaining + native guest build prerequisites; grants `/dev/kvm` access; pulls the pinned shell linter images; and stages the pinned Linux and Ubuntu Base archives in `.cache/downloads`. When CI has cached them for the checked-out inputs, it also restores the guest artifacts into `build/` and the KVM OpenVMM binary into @@ -65,6 +66,13 @@ and guest artifacts only when their inputs changed. Leave `test-openvmm-unit`, `test-openvmm`, `build-guest --guest all`, `verify-guest-determinism`, and `benchmark` to CI. +- `nvx.py sandbox`, `nvx.py run`, and the microVM scenarios that drive them run + `nvx-rs`, which `nvx.py` builds with Cargo into `nvx-rs/target/release/` the + first time that a command needs it, in about a minute. Rebuild it with + `cargo build --release --locked --features cli --manifest-path nvx-rs/Cargo.toml` + after changing `nvx-rs/`, or set `NVX_RS` to another build (see + [Usage](../doc/usage.md#nvx-rs)). The `managed-lifecycle` scenario runs an + `nvx-rs` test with Cargo. - Build trees are large. Check `df -h .` before a build, keep one OpenVMM build profile at a time, and delete trees you no longer need with `cargo clean` or `docker builder prune -af`. The setup disables incremental compilation and diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0964977e..2854f3104 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,9 +120,55 @@ jobs: with: python-version: "3.12" + # nvx.py runs nvx-rs for its sandbox command and for the egress and + # artifact steps of its run command. A static musl build runs on every + # self-hosted Linux runner, so the microVM jobs take this one, as they + # take OpenVMM from its build jobs. It goes where `cargo build + # --release` puts it, where nvx.py, and so the CLI tests, look for it, + # and NVX_RS names it for the tests that run it directly. + - name: Build nvx-rs + shell: bash + run: | + set -euo pipefail + toolchain=$(sed -n 's/^channel = "\([^"]*\)".*/\1/p' rust-toolchain.toml) + test -n "${toolchain}" + rustup toolchain install "${toolchain}" --profile minimal --no-self-update \ + --target x86_64-unknown-linux-musl + cargo "+${toolchain}" build --release --locked --features cli \ + --manifest-path nvx-rs/Cargo.toml --target x86_64-unknown-linux-musl + install -D -m 0755 nvx-rs/target/x86_64-unknown-linux-musl/release/nvx-rs \ + nvx-rs/target/release/nvx-rs + echo "NVX_RS=${GITHUB_WORKSPACE}/nvx-rs/target/release/nvx-rs" >> "${GITHUB_ENV}" + + # Packages ship the build with its provenance, which binds it to the + # nvx-rs sources, the pinned Rust release, the target, and the features. + - name: Record nvx-rs provenance + shell: bash + run: >- + python3 scripts/nvx.py record-nvx-rs-provenance + --target x86_64-unknown-linux-musl + - name: Validate NVX CLI uses: ./.github/actions/validate-nvx + - name: Upload nvx-rs executable + uses: actions/upload-artifact@v7 + with: + name: nvx-rs-linux-musl-executable + path: nvx-rs/target/release/nvx-rs + if-no-files-found: error + overwrite: true + retention-days: 1 + + - name: Upload nvx-rs provenance + uses: actions/upload-artifact@v7 + with: + name: nvx-rs-linux-musl-provenance + path: build/nvx-rs.provenance.json + if-no-files-found: error + overwrite: true + retention-days: 1 + nvx-cli-windows: name: NVX CLI / Windows if: >- @@ -155,9 +201,9 @@ jobs: with: python-version: "3.12" - # The curl shim test compiles the shim with the release that - # rust-toolchain.toml pins, which rustup would otherwise install with - # its default profile. + # The curl shim test compiles the shim, and the next step builds nvx-rs, + # with the release that rust-toolchain.toml pins, which rustup would + # otherwise install with its default profile. - name: Install the pinned Rust release shell: powershell run: | @@ -171,9 +217,46 @@ jobs: --profile minimal --no-self-update if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # As on Linux, for the microVM jobs and the CLI tests. The static C + # runtime spares the self-hosted runners the Visual C++ redistributable. + - name: Build nvx-rs + shell: powershell + env: + RUSTFLAGS: -C target-feature=+crt-static + run: | + $ErrorActionPreference = "Stop" + cargo build --release --locked --features cli --manifest-path nvx-rs/Cargo.toml + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + "NVX_RS=$env:GITHUB_WORKSPACE\nvx-rs\target\release\nvx-rs.exe" | + Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Record nvx-rs provenance + shell: powershell + run: | + python scripts\nvx.py record-nvx-rs-provenance --target x86_64-pc-windows-msvc + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Validate NVX CLI uses: ./.github/actions/validate-nvx + - name: Upload nvx-rs executable + uses: actions/upload-artifact@v7 + with: + name: nvx-rs-windows-msvc-executable + path: nvx-rs/target/release/nvx-rs.exe + if-no-files-found: error + overwrite: true + retention-days: 1 + + - name: Upload nvx-rs provenance + uses: actions/upload-artifact@v7 + with: + name: nvx-rs-windows-msvc-provenance + path: build/nvx-rs.provenance.json + if-no-files-found: error + overwrite: true + retention-days: 1 + openvmm-changes: name: Detect CI workload inputs if: github.event_name != 'pull_request' || github.event.pull_request.draft == false @@ -902,6 +985,7 @@ jobs: runner: '["self-hosted", "linux", "kvm", "virtual-machine"]' backend: kvm openvmm-artifact: openvmm-linux-gnu + nvx-rs-artifact: nvx-rs-linux-musl secrets: OPENVMM_DEPLOY_KEY: ${{ secrets.OPENVMM_DEPLOY_KEY }} @@ -934,6 +1018,7 @@ jobs: runner: '["self-hosted", "linux", "mshv", "virtual-machine"]' backend: mshv openvmm-artifact: openvmm-linux-gnu + nvx-rs-artifact: nvx-rs-linux-musl secrets: OPENVMM_DEPLOY_KEY: ${{ secrets.OPENVMM_DEPLOY_KEY }} @@ -966,6 +1051,7 @@ jobs: runner: '["self-hosted", "windows", "whp", "virtual-machine"]' backend: whp openvmm-artifact: openvmm-windows-msvc + nvx-rs-artifact: nvx-rs-windows-msvc secrets: OPENVMM_DEPLOY_KEY: ${{ secrets.OPENVMM_DEPLOY_KEY }} @@ -1001,6 +1087,7 @@ jobs: runner: '["self-hosted", "linux", "kvm", "virtual-machine"]' backend: kvm openvmm-artifact: openvmm-linux-gnu + nvx-rs-artifact: nvx-rs-linux-musl debug-kernel: true secrets: OPENVMM_DEPLOY_KEY: ${{ secrets.OPENVMM_DEPLOY_KEY }} @@ -1028,6 +1115,7 @@ jobs: runner: '["self-hosted", "linux", "mshv", "virtual-machine"]' backend: mshv openvmm-artifact: openvmm-linux-gnu + nvx-rs-artifact: nvx-rs-linux-musl debug-kernel: true secrets: OPENVMM_DEPLOY_KEY: ${{ secrets.OPENVMM_DEPLOY_KEY }} @@ -1055,6 +1143,7 @@ jobs: runner: '["self-hosted", "windows", "whp", "virtual-machine"]' backend: whp openvmm-artifact: openvmm-windows-msvc + nvx-rs-artifact: nvx-rs-windows-msvc debug-kernel: true secrets: OPENVMM_DEPLOY_KEY: ${{ secrets.OPENVMM_DEPLOY_KEY }} @@ -1095,6 +1184,7 @@ jobs: release-platform: linux-kvm benchmark-platform: linux-kvm-virtual-machine openvmm-artifact: openvmm-linux-gnu + nvx-rs-artifact: nvx-rs-linux-musl teardown-mode: guest-exit benchmark-cpus: 0-7 host-cpu-reserve: 0 @@ -1134,6 +1224,7 @@ jobs: release-platform: linux-mshv benchmark-platform: linux-mshv-virtual-machine openvmm-artifact: openvmm-linux-musl + nvx-rs-artifact: nvx-rs-linux-musl teardown-mode: guest-exit benchmark-cpus: 0-7 host-cpu-reserve: 0 @@ -1173,6 +1264,7 @@ jobs: release-platform: windows-whp benchmark-platform: windows-whp-virtual-machine openvmm-artifact: openvmm-windows-msvc + nvx-rs-artifact: nvx-rs-windows-msvc teardown-mode: guest-exit benchmark-cpus: 0-7 host-cpu-reserve: 0 diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index d5962e5da..3ff091f8a 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -22,7 +22,7 @@ on: - .gitmodules - SOURCE-MANIFEST.json - aci_edge_sandboxes/Cargo.toml - - nvx-rs/Cargo.toml + - nvx-rs/** - openvmm - requirements-dev.txt - rust-toolchain.toml diff --git a/.github/workflows/run-nvx-microvm-tests.yml b/.github/workflows/run-nvx-microvm-tests.yml index 45e843859..ea1dcf212 100644 --- a/.github/workflows/run-nvx-microvm-tests.yml +++ b/.github/workflows/run-nvx-microvm-tests.yml @@ -19,6 +19,10 @@ on: description: Base name of the compatible OpenVMM artifacts required: true type: string + nvx-rs-artifact: + description: Base name of the nvx-rs artifacts for the runner's OS + required: true + type: string debug-kernel: description: >- Run the same-host restore scenarios on the CI debug kernel instead @@ -84,6 +88,20 @@ jobs: shell: bash run: chmod +x openvmm/target/release/openvmm + # nvx.py runs nvx-rs for its sandbox command, which the scenarios and + # the steps below drive, and for the egress and artifact steps of its run + # command. It looks for nvx-rs where `cargo build --release` puts it. + - name: Download nvx-rs executable + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.nvx-rs-artifact }}-executable + path: nvx-rs/target/release + + - name: Make nvx-rs executable + if: runner.os != 'Windows' + shell: bash + run: chmod +x nvx-rs/target/release/nvx-rs + # Qualification verifies the downloaded OpenVMM's time ABI preflight. - name: Validate runner uses: ./.github/actions/validate-runner diff --git a/.github/workflows/run-platform.yml b/.github/workflows/run-platform.yml index d39a16817..2567aaf42 100644 --- a/.github/workflows/run-platform.yml +++ b/.github/workflows/run-platform.yml @@ -27,6 +27,10 @@ on: description: Base name of the compatible OpenVMM artifacts required: true type: string + nvx-rs-artifact: + description: Base name of the nvx-rs artifacts for the runner's OS + required: true + type: string teardown-mode: description: Guest teardown mode used by the benchmark required: true @@ -44,7 +48,9 @@ on: required: true type: number package-release: - description: Whether the platform produces a development release package + description: >- + Whether the platform packages a release, which dev pushes upload for + the development release required: true type: boolean secrets: @@ -126,11 +132,12 @@ jobs: host-cpu-reserve: ${{ inputs.host-cpu-reserve }} marker-timeout: ${{ inputs.marker-timeout }} + # Pull requests package and archive the release too, so that they prove + # that it builds, but only dev pushes upload it for publication. - name: Package release - if: >- - github.event_name == 'push' && - github.ref == 'refs/heads/dev' && - inputs.package-release + if: inputs.package-release uses: ./.github/actions/package-release with: platform: ${{ inputs.release-platform }} + nvx-rs-artifact: ${{ inputs.nvx-rs-artifact }} + upload: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' }} diff --git a/README.md b/README.md index 459cfc18a..c6b0ef2dc 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,20 @@ NVX was jointly developed by the [MSR Systems Research Group][msr-systems] and [msr-systems]: https://www.microsoft.com/en-us/research/group/systems-research-group-redmond/ [azure-systems]: https://www.microsoft.com/en-us/research/group/azure-research-systems/ -This repository includes version pins for Linux and OpenVMM, along with the patches, guest source -files, build tools, and benchmarks needed to use NVX. +This repository includes version pins for Linux, OpenVMM, and the Rust guest +toolchain, along with the patches, guest source files, build tools, and +benchmarks needed to use NVX. Alpine, Ubuntu, and Azure Linux ship the nine +static guest binaries from [`nvx-rs`](nvx-rs/README.md#guest-binaries) at +their common `/sbin/nvx-*` paths. ## Quick Start Python 3.10 or newer is required. The commands below download the prebuilt NVX package for the selected platform from the cloned revision's release when available, or from its nearest released -first-parent ancestor otherwise, so no local build is required. A successful boot prints +first-parent ancestor otherwise, so no build of the guest, OpenVMM, or +[`nvx-rs`](doc/usage.md#nvx-rs), which `nvx.py run` runs, is required. Should the release's `nvx-rs` +come from other sources than the checkout's, `nvx.py` builds it with Cargo, which +[rustup](https://rustup.rs/) installs. A successful boot prints `NVX-GUEST-BOOT-OK: alpine` and opens a root shell. Select Ubuntu or Azure Linux userland with the same NVX kernel by passing `--guest ubuntu` or `--guest azurelinux`. diff --git a/SOURCE-MANIFEST.json b/SOURCE-MANIFEST.json index a15f52395..86cb8a7d7 100644 --- a/SOURCE-MANIFEST.json +++ b/SOURCE-MANIFEST.json @@ -9,6 +9,10 @@ "control_session_protocol_version": 1, "control_contract_revision": "nvx-microvm-v2-control-v2" }, + "guest_rust": { + "toolchain": "1.95.0", + "target": "x86_64-unknown-linux-musl" + }, "linux": { "version": "6.18.38", "upstream_url": "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.18.38.tar.xz", @@ -31,7 +35,9 @@ "minirootfs_sha256": "41f73e3cf5fa919b8aa5ca6b30dc48f0da2720776d7423e2a7748211456fe081", "guest_sources": [ "guest/common", - "guest/alpine" + "guest/alpine", + "nvx-rs", + "rust-toolchain.toml" ], "package_manifests": [ "build/initramfs.cpio.gz.packages.json" @@ -52,7 +58,9 @@ "guest_sources": [ "guest/common", "guest/ubuntu", - "ubuntu/packages.lock.json" + "ubuntu/packages.lock.json", + "nvx-rs", + "rust-toolchain.toml" ], "package_manifests": [ "build/initramfs-ubuntu.cpio.gz.packages.json", @@ -70,7 +78,9 @@ "package_lock_sha256": "f2014a858f46482778f138b374f80c36df389206ae677870d58a7e919bec182b", "guest_sources": [ "guest/common", - "azurelinux/packages.lock.json" + "azurelinux/packages.lock.json", + "nvx-rs", + "rust-toolchain.toml" ], "package_manifests": [ "build/initramfs-azurelinux.cpio.gz.packages.json" diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 2990dad39..330e723d4 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -8,8 +8,38 @@ the distribution tooling. | --- | --- | --- | | Linux 6.18.38 | Generated under `.cache/linux` | GPL-2.0-only; the upstream archive and patch series are pinned in `SOURCE-MANIFEST.json` and `kernel/patches`. Each build exports its exact config, and `collect-sources` emits the complete patched tree. | | OpenVMM | `openvmm` | MIT; see `openvmm/LICENSE`. Source is a private Git submodule and is available only to authorized users. | -| NVX guest/build sources | `guest`, `ubuntu`, `kernel`, `scripts/nvx_tools` | MIT; see `LICENSE`. | -| musl libc (Debian 12 `musl`) | statically linked into `/sbin/nvx-time`, `/sbin/nvx-time-probe`, and `/sbin/nvx-exit` in generated initramfs images | MIT; Copyright © 2005-2020 Rich Felker, et al. The build image's `/usr/share/doc/musl/copyright` carries the full notice, and the source is Debian's `musl` source package. | +| NVX guest/build sources | `guest`, `nvx-rs`, `ubuntu`, `kernel`, `scripts/nvx_tools` | MIT; see `LICENSE`. | +| Rust core and standard libraries | linked into all nine Rust guest binaries listed in the musl row, and into `bin/nvx-rs`, or `bin/nvx-rs.exe`, of every release package | MIT OR Apache-2.0; the exact release is pinned by `rust-toolchain.toml`. Source and license files are under [`rust-lang/rust/library`](https://github.com/rust-lang/rust/tree/master/library), with `LICENSE-MIT` and `LICENSE-APACHE` at the repository root. | +| Rust `compiler_builtins` | Rust target runtime routines used by the guest binaries and `bin/nvx-rs[.exe]` | MIT OR Apache-2.0 for `rust-lang/compiler-builtins`; routines derived from LLVM compiler-rt are Apache-2.0 WITH LLVM-exception. Source is the revision bundled by the pinned Rust release under [`rust-lang/compiler-builtins`](https://github.com/rust-lang/compiler-builtins) and its [`llvm-project/compiler-rt`](https://github.com/llvm/llvm-project/tree/main/compiler-rt) sources. | +| Rust `libc` crate 0.2.189 | linked into the guest binaries and the Linux `bin/nvx-rs` through `nix` and `getrandom` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and source and license files are at [`rust-lang/libc` tag 0.2.189](https://github.com/rust-lang/libc/tree/0.2.189). | +| Rust `nix` crate 0.31.3 | system-call wrappers used by the guest binaries and the Linux `bin/nvx-rs` | MIT; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`nix-rust/nix`](https://crates.io/crates/nix/0.31.3). | +| Rust `getrandom` crate 0.4.3 | random bytes for the guest binaries, such as `nvx-reseed --sample`, and for `bin/nvx-rs[.exe]`, such as control capabilities | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`rust-random/getrandom`](https://crates.io/crates/getrandom/0.4.3). | +| Rust `bitflags` crate 2.13.2 | transitive `nix` dependency in the guest binaries and the Linux `bin/nvx-rs` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`bitflags/bitflags`](https://crates.io/crates/bitflags/2.13.2). | +| Rust `cfg-if` crate 1.0.5 | transitive `nix`, `getrandom`, and `sha2` dependency in the guest binaries and `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`rust-lang/cfg-if`](https://crates.io/crates/cfg-if/1.0.5). | +| Rust `memoffset` crate 0.9.1 | transitive `nix` dependency in the guest binaries and the Linux `bin/nvx-rs` | MIT; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`Gilnaa/memoffset`](https://crates.io/crates/memoffset/0.9.1). | +| Rust `serde` crate 1.0.229 | serialization framework of `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`serde-rs/serde`](https://crates.io/crates/serde/1.0.229). | +| Rust `serde_core` crate 1.0.229 | transitive `serde` and `serde_json` dependency in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`serde-rs/serde`](https://crates.io/crates/serde_core/1.0.229). | +| Rust `serde_derive` crate 1.0.229 | procedural macro that generates the `serde` derives of `nvx-rs` while it builds; the code that it generates, not the crate, is in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`serde-rs/serde`](https://crates.io/crates/serde_derive/1.0.229). | +| Rust `proc-macro2` crate 1.0.107 | build-time `serde_derive` dependency, not linked into `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`dtolnay/proc-macro2`](https://crates.io/crates/proc-macro2/1.0.107). | +| Rust `quote` crate 1.0.47 | build-time `serde_derive` dependency, not linked into `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`dtolnay/quote`](https://crates.io/crates/quote/1.0.47). | +| Rust `syn` crate 3.0.6 | build-time `serde_derive` dependency, not linked into `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`dtolnay/syn`](https://crates.io/crates/syn/3.0.6). | +| Rust `unicode-ident` crate 1.0.26 | build-time `proc-macro2` and `syn` dependency, not linked into `bin/nvx-rs[.exe]` | (MIT OR Apache-2.0) AND Unicode-3.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`dtolnay/unicode-ident`](https://crates.io/crates/unicode-ident/1.0.26). | +| Rust `serde_json` crate 1.0.151 | JSON reading and writing in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`serde-rs/json`](https://crates.io/crates/serde_json/1.0.151). | +| Rust `itoa` crate 1.0.18 | integer formatting for `serde_json` in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`dtolnay/itoa`](https://crates.io/crates/itoa/1.0.18). | +| Rust `memchr` crate 2.8.3 | byte search for `serde_json` in `bin/nvx-rs[.exe]` | Unlicense OR MIT; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`BurntSushi/memchr`](https://crates.io/crates/memchr/2.8.3). | +| Rust `zmij` crate 1.0.23 | floating-point formatting for `serde_json` in `bin/nvx-rs[.exe]` | MIT; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`dtolnay/zmij`](https://crates.io/crates/zmij/1.0.23). | +| Rust `sha2` crate 0.10.9 | SHA-256 in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`RustCrypto/hashes`](https://crates.io/crates/sha2/0.10.9). | +| Rust `digest` crate 0.10.7 | transitive `sha2` dependency in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`RustCrypto/traits`](https://crates.io/crates/digest/0.10.7). | +| Rust `crypto-common` crate 0.1.7 | transitive `sha2` dependency in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`RustCrypto/traits`](https://crates.io/crates/crypto-common/0.1.7). | +| Rust `block-buffer` crate 0.10.4 | transitive `sha2` dependency in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`RustCrypto/utils`](https://crates.io/crates/block-buffer/0.10.4). | +| Rust `cpufeatures` crate 0.2.17 | transitive `sha2` dependency in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`RustCrypto/utils`](https://crates.io/crates/cpufeatures/0.2.17). | +| Rust `generic-array` crate 0.14.7 | transitive `sha2` dependency in `bin/nvx-rs[.exe]` | MIT; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`fizyk20/generic-array`](https://crates.io/crates/generic-array/0.14.7). | +| Rust `typenum` crate 1.20.1 | transitive `sha2` dependency in `bin/nvx-rs[.exe]` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`paholg/typenum`](https://crates.io/crates/typenum/1.20.1). | +| Rust `windows-sys` crate 0.61.2 | Windows API bindings of `bin/nvx-rs.exe` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`microsoft/windows-rs`](https://crates.io/crates/windows-sys/0.61.2). | +| Rust `windows-link` crate 0.2.1 | transitive `windows-sys` dependency in `bin/nvx-rs.exe` | MIT OR Apache-2.0; the exact crate checksum is pinned in `nvx-rs/Cargo.lock`, and the source is published by [`microsoft/windows-rs`](https://crates.io/crates/windows-link/0.2.1). | +| musl libc | statically linked into `/sbin/nvx-console-pending`, `/sbin/nvx-mmio-write`, `/sbin/nvx-reseed`, `/sbin/nvx-exit`, `/sbin/nvx-device-io`, `/sbin/nvx-managed-agent`, `/sbin/nvx-ingress`, `/sbin/nvx-time`, and `/sbin/nvx-time-probe` in every generated initramfs, and into `bin/nvx-rs` of the `linux-kvm` and `linux-mshv` release packages | MIT; Copyright © 2005-2020 Rich Felker, et al. The exact implementation is supplied by the pinned Rust release's `x86_64-unknown-linux-musl` target. Upstream source and the full copyright notice are at [musl libc](https://musl.libc.org/). | +| LLVM `libunwind` | statically linked, with musl, into the nine guest binaries and the Linux `bin/nvx-rs`, which unwind and print backtraces through it | Apache-2.0 WITH LLVM-exception, whose exception lifts the attribution conditions for code embedded in a compiled program. The exact implementation is the `libunwind.a` that the pinned Rust release's `x86_64-unknown-linux-musl` target supplies, which the Rust project builds from its fork of [`llvm-project/libunwind`](https://github.com/llvm/llvm-project/tree/main/libunwind). | +| Microsoft C runtime: the Visual C++ runtime and the Universal C Runtime | statically linked into `bin/nvx-rs.exe` of the `windows-whp` release package, which CI builds with `-C target-feature=+crt-static`, so that it runs without the Visual C++ Redistributable | Proprietary Microsoft object code, linked in by the Visual Studio build tools and the Windows SDK of CI's Windows runner under their Microsoft Software License Terms, which permit distributing the programs built with them; see [Redistribute Visual C++ files](https://learn.microsoft.com/cpp/windows/redistributing-visual-cpp-files). No source is distributed. | | Alpine Linux packages | generated initramfs images | Per-package licenses. Every build emits `*.packages.json`; retain that manifest and collect the corresponding aports recipes and upstream sources before distributing an image. | | Azure Linux 3.0 packages | generated Azure Linux initramfs | Per-package licenses, including GPLv2 BusyBox. The base image is digest-pinned in `SOURCE-MANIFEST.json`, every supplemental RPM is SHA-256 pinned in `azurelinux/packages.lock.json`, and every build emits `initramfs-azurelinux.cpio.gz.packages.json`. `collect-sources` does not collect this corresponding source yet, so `--include-source` packages omit the Azure Linux initramfs; publish the matching source before distributing a binary-only package that contains it. | | Ubuntu Base 26.04.1 LTS and supplemental packages | generated Ubuntu initramfs and EROFS images | Per-package licenses. The base archive and every supplemental `.deb` are SHA-256 pinned. Retain the generated Ubuntu manifests and publish the matching `.dsc` plus all referenced source members before distributing an image. Ubuntu artifacts contain Ubuntu userland with the NVX kernel, not the Ubuntu kernel. | diff --git a/aci_edge_sandboxes/README.md b/aci_edge_sandboxes/README.md index fcc2cc1ff..9ce05ce14 100644 --- a/aci_edge_sandboxes/README.md +++ b/aci_edge_sandboxes/README.md @@ -866,10 +866,19 @@ through `endPort`, an `icmp` entry matches ICMP alone, or ICMPv6 for IPv6 networks, and `any` matches every protocol or, with a port or a port range, TCP and UDP on those ports. -Rules are expanded into OpenVMM rules exactly: exceptions are subtracted from -their networks, a port range becomes one OpenVMM range rule for each network, -and protocol `any` with a port or a port range becomes a TCP and a UDP rule. -Rules that would expand to more than 256 OpenVMM rules are rejected. Egress +Rules are lowered into OpenVMM rules exactly, by the lowering that compiles +NVX's egress policy files +([`nvx::host::network::policy`](../nvx-rs/src/host/network/policy/mod.rs)): +exceptions are subtracted from their networks, protocol `any` with a port or a +port range becomes a TCP and a UDP rule, the destinations of each action merge +into the fewest networks that cover them, a rule for every port of a protocol, +or for every protocol, subsumes the narrower rules that it covers, and +overlapping and adjacent port ranges of a network merge into one range rule. +The rules of each action are passed in order of family, address, prefix length, +protocol, and ports, and an action that still needs more than 256 OpenVMM rules +is rejected. Networks are read as policy files read them, so host bits are +cleared and IPv4 netmasks such as `/255.255.0.0` are accepted, but request +validation still refuses both before the backend sees them. Egress denied without allow rules, with ingress and host loopback denied, attaches no network device; otherwise the guest gets `10.0.0.2/24` behind the profile's NAT gateway @@ -1073,6 +1082,49 @@ affect Rust callers and builds: - A relative `process.cwd` is still rejected with `policy_validation`, and the message now points at `nvx::guest_path`. +The OpenVMM backend lowers egress rules with the lowering that compiles NVX's +egress policy files +([`nvx::host::network::policy`](../nvx-rs/src/host/network/policy/mod.rs)), +instead of expanding each rule on its own. What the rules match is unchanged, +and so are the error codes, but these change: + +- **Rule order.** The `--network-egress-allow` and `--network-egress-deny` + values follow family, address, prefix length, protocol, and ports, instead of + string order: `::/0` precedes `2001:db8::/32`, and `tcp:80` precedes + `tcp:443`. OpenVMM's matching does not depend on the order. +- **Rule count.** Each action's destinations merge into the fewest networks, a + rule for every port of a protocol, or for every protocol, subsumes the + narrower rules that it covers, and overlapping and adjacent port ranges of a + network merge. Policies usually need fewer OpenVMM rules, so some that + exceeded the limit of 256 are now accepted: 256 adjacent `/24` destinations + become one `/16` rule. Port ranges that overlap only partly across networks + can need a rule for their union, so a policy near the limit can also need + more rules than before. +- **Messages.** The backend's own rule checks, which `Backend` callers reach + without the request validation of `AciEdgeSandbox`, report the policy + compiler's messages, which name the field: + - `network.egress.allow[0].ports[0]: ICMP has no ports` became + `network.egress.allow[0].ports[0].port is not supported with icmp`; + - `....endPort must not be below port` became `....endPort 80 cannot be + below port 90`, and ports outside 1 through 65535 report `....port must be + between 1 and 65535`; + - `...to[0]: the destination is not a network` and `...to[0].except: an + exception is not a network within the destination` became + `...to[0].cidr is not a valid IPv4 or IPv6 CIDR: VALUE`, + `...to[0].cidr must not name an IPv6 scope: VALUE`, and + `...to[0].except[1] must be contained in NETWORK`; and + - `network.egress.allow expands to 300 OpenVMM rules; the openvmm backend + accepts at most 256`, and the rule-level `expands to more than 256 OpenVMM + rules` and `covers more than 256 networks after removing its exceptions`, + became `network.egress.allow emits at most 256 native rules`. This is the + one message that requests through `AciEdgeSandbox` can also reach. +- **Accepted rules.** Without request validation, the backend now reads a + network as policy files do: host bits are cleared and IPv4 netmasks are + accepted, so `10.0.0.7/24` is `10.0.0.0/24`, while port 0, which the backend + passed to OpenVMM before, is refused. Request validation still refuses such + networks with `malformed_request` first. A deny rule that covers the guest + network of host-loopback forwards is detected after the same normalization. + ## Limitations Live standard input requires guest control-protocol work and is rejected today. diff --git a/aci_edge_sandboxes/src/openvmm/mod.rs b/aci_edge_sandboxes/src/openvmm/mod.rs index eefd01b36..0bc983c8c 100644 --- a/aci_edge_sandboxes/src/openvmm/mod.rs +++ b/aci_edge_sandboxes/src/openvmm/mod.rs @@ -58,9 +58,10 @@ //! or read-write. OpenVMM enforces each mapping's access on the host, except that of a read-only //! path inside a read-write mapping, and hides denied paths and everything that is not mapped //! beside mapped files. A whole volume, or a file directly in a volume's root, is not mapped. -//! Egress rules are expanded -//! into OpenVMM's IPv4 and IPv6 rules exactly; rules that would need more than 256 OpenVMM rules -//! are rejected. Egress denied without allow rules, with ingress and host loopback denied, +//! Egress rules are lowered into OpenVMM's IPv4 and IPv6 rules exactly, by the lowering that +//! compiles NVX's egress policy files ([`nvx::host::network::policy`]): destinations and port +//! ranges merge into the fewest rules, and an action that still needs more than 256 OpenVMM rules +//! is rejected. Egress denied without allow rules, with ingress and host loopback denied, //! attaches no network device. With `network.ingress.default: allow`, OpenVMM listens on every //! host address at the TCP and UDP ports where the workload listens, and admits clients on the //! host's private networks, whose addresses are not globally reachable, but not clients inside @@ -924,7 +925,8 @@ mod tests { guest_port: 8080, }] ); - // Messages name the contract's fields. + // Messages name the contract's fields, as the shared lowering of NVX's egress policy + // files words them. let icmp_port = NetworkPolicy { egress: EgressPolicy::new(Access::Deny) .with_allow(NetworkRule::to("10.0.0.0/8").on_port(Protocol::Icmp, 8)), @@ -937,7 +939,43 @@ mod tests { assert_eq!(error.code(), ErrorCode::PolicyValidation); assert_eq!( error.message(), - "network.egress.allow[0].ports[0]: ICMP has no ports" + "network.egress.allow[0].ports[0].port is not supported with icmp" + ); + let scoped = NetworkPolicy { + egress: EgressPolicy::new(Access::Allow) + .with_deny(NetworkRule::to("192.0.2.0/24")) + .with_deny(NetworkRule::to("fe80::1%eth0/64")), + ..NetworkPolicy::deny_all() + }; + let error = Error::from( + network::openvmm_arguments(Some(&network_policy(&scoped, &[])), "10.0.0.2/24") + .unwrap_err(), + ); + assert_eq!(error.code(), ErrorCode::PolicyValidation); + assert_eq!( + error.message(), + "network.egress.deny[1].to[0].cidr must not name an IPv6 scope: fe80::1%eth0/64" + ); + let mut crowded = NetworkRule::to("192.0.2.0/24"); + crowded.ports = (0..257) + .map(|index| NetworkPort { + protocol: Protocol::Tcp, + port: Some(2 * index + 1), + end_port: None, + }) + .collect(); + let crowded = NetworkPolicy { + egress: EgressPolicy::new(Access::Deny).with_allow(crowded), + ..NetworkPolicy::deny_all() + }; + let error = Error::from( + network::openvmm_arguments(Some(&network_policy(&crowded, &[])), "10.0.0.2/24") + .unwrap_err(), + ); + assert_eq!(error.code(), ErrorCode::PolicyValidation); + assert_eq!( + error.message(), + "network.egress.allow emits at most 256 native rules" ); } diff --git a/doc/adversarial-testing.md b/doc/adversarial-testing.md index 077495811..b0a00fd0e 100644 --- a/doc/adversarial-testing.md +++ b/doc/adversarial-testing.md @@ -98,7 +98,8 @@ The executor: 1. requires the exact controller NVX commit; 2. runs `scripts/nvx.py verify`; 3. validates and hashes the kernel, initramfs, initramfs package manifest, - OpenVMM executable, and provenance documents before baseline boot; + OpenVMM executable, the [nvx-rs](#nvx-rs-on-the-target) that the scenarios + run, and their provenance documents before baseline boot; 4. starts filesystem and network canaries plus the watchdog; 5. refuses every scenario or canary dispatch after sticky oracle evidence records a filesystem or network violation; @@ -112,7 +113,12 @@ The executor: Child test processes receive a small environment allowlist. In particular, GitHub, Copilot, SSH-agent, token, and arbitrary controller environment -variables are not forwarded. +variables are not forwarded. The allowlist carries the paths that +[nvx-rs](#nvx-rs-on-the-target) needs: `NVX_RS`, `CARGO_HOME`, and +`RUSTUP_HOME`. It omits Cargo's other settings and credentials, such as +`CARGO_TARGET_DIR`, `RUSTFLAGS`, and registry tokens, and `RUSTUP_TOOLCHAIN`, +so that [`rust-toolchain.toml`](../rust-toolchain.toml) still selects the Rust +release. The controller may start the executor locally, but production campaigns must use `--executor-command`. This option names one trusted executable and accepts @@ -123,7 +129,8 @@ It must not evaluate protocol data as a shell command. The wrapper and target provisioner must: - create a fresh disposable target for every executor process; -- stage the exact NVX commit and provenance-bound artifacts before exposure; +- stage the exact NVX commit, provenance-bound artifacts, and + [nvx-rs](#nvx-rs-on-the-target) before exposure; - provide KVM, MSHV, or WHP without production or GitHub credentials; - deny production network access and bound target CPU, memory, disk, and time; - run only the executor entry point for the restricted principal; @@ -141,6 +148,44 @@ than nesting below `--output-dir`. This keeps the deepest deterministic microVM log paths below the legacy Windows `MAX_PATH` boundary. The controller metadata and summary record the absolute target artifact root. +### nvx-rs on the target + +Besides the guest and OpenVMM artifacts, a target needs nvx-rs, the Rust +command-line interface that `nvx.py` runs ([Usage](usage.md#nvx-rs)). Of the +catalogued scenarios, `managed-lifecycle` runs the nvx-rs test +`managed_lifecycle` with Cargo, and `l3-l4-egress-policy` compiles its egress +policy with nvx-rs and runs `nvx.py run`, which runs nvx-rs too. They find +nvx-rs where `nvx.py` does: at `NVX_RS`, then at +`nvx-rs/target/release/nvx-rs[.exe]`, and otherwise they build it there with +Cargo. + +A target therefore needs the Rust toolchain: rustup, with Cargo on the +executor's `PATH` and the Rust release that `rust-toolchain.toml` pins +installed. A prebuilt nvx-rs at `NVX_RS` or in `nvx-rs/target/release` saves +its build but not the toolchain, which `managed-lifecycle` needs. Because the +executor passes `NVX_RS`, `CARGO_HOME`, and `RUSTUP_HOME`, either may live +outside the home directory. + +A scenario's timeout includes any build, an existing nvx-rs is never rebuilt, +and a target may have no network access for Cargo. So, before exposure, build +both at the staged commit from the repository root, and record the provenance +of the nvx-rs build: + +```bash +cargo build --release --locked --features cli --manifest-path nvx-rs/Cargo.toml --target-dir nvx-rs/target +cargo test --no-run --locked --manifest-path nvx-rs/Cargo.toml --test managed_lifecycle +python3 scripts/nvx.py record-nvx-rs-provenance +``` + +Because `nvx.py` never rebuilds an nvx-rs that exists, the executor checks it +with the other artifacts before the baseline boot: the provenance in +`build/nvx-rs.provenance.json` must describe the nvx-rs that the scenarios +run, the one at `NVX_RS` or else in `nvx-rs/target/release`, as built from the +staged commit's nvx-rs sources. A stale build, or a missing one, fails the +initialization instead of running unnoticed. Run `record-nvx-rs-provenance` +with `NVX_RS` set when it names the build. On a target with the release built +from the staged commit, [`download`](usage.md#download) installs both. + ### Independent oracles The executor-side oracle watchdog runs without consulting Copilot. It @@ -169,8 +214,8 @@ targets rather than the quarantined machine. ## Running a campaign -First prepare the normal NVX guest and OpenVMM release artifacts. For a local -KVM virtual machine: +First prepare the normal NVX guest and OpenVMM release artifacts, and +[nvx-rs](#nvx-rs-on-the-target). For a local KVM virtual machine: ```bash python3 scripts/nvx.py test-adversarial \ diff --git a/doc/benchmarks.md b/doc/benchmarks.md index a7002fddd..d886326b3 100644 --- a/doc/benchmarks.md +++ b/doc/benchmarks.md @@ -214,8 +214,10 @@ MSHV, and WHP. They do not create TAP devices or require host firewall rules. Each workload directory includes `benchmark-metadata.json` with the platform, backend, ABI, processor count, host affinity set, memory sizes, artifact revisions, repository dirty state, warmups, measured run counts, and SHA-256 values for the VMM, kernel, initramfs, and coordinator. -Device metadata also records helper source and packaged-binary hashes. Collection rejects -mismatched lifecycle/workload metadata and duplicate topology rows. +Device metadata also records the aggregate Rust guest-source digest, pinned +toolchain and target, and the packaged `nvx-device-io` binary hash from the +initramfs manifest. Collection rejects mismatched lifecycle/workload metadata +and duplicate topology rows. Use a fixed affinity set containing one logical processor per physical core and at least `N+2` processors for an `N`-vCPU guest. The additional processors @@ -398,8 +400,9 @@ measures complete process wall time while host and guest exchange files through The dedicated suite always uses the canonical microVM and attaches the block backing object as the writable `scratch` role. There is no ABI selector or -legacy unroled `--virtio-blk` control. Every guest has 256 MiB RAM and runs the same static, dependency-free -x86-64 helper from the reproducible initramfs. Canonical runs execute five warmups followed by 30 +legacy unroled `--virtio-blk` control. Every guest has 256 MiB RAM and runs the +same static Rust x86-64 binary from the reproducible initramfs. +Canonical runs execute five warmups followed by 30 retained attempts for each device. Every operation window lasts ten seconds against a 512 MiB backing object. diff --git a/doc/build.md b/doc/build.md index ac30ffbca..71c087903 100644 --- a/doc/build.md +++ b/doc/build.md @@ -31,6 +31,22 @@ python3 scripts/nvx.py build-guest --native python3 scripts/nvx.py build-guest --native --guest ubuntu ``` +Both native image builds call the same guest-binary routine that Docker and +the `nvx-rs` CI job use. It can also be run directly: + +```bash +python3 scripts/nvx.py build-guest-binaries --output build/guest-binaries +``` + +The routine takes the exact toolchain from `rust-toolchain.toml`, the +`x86_64-unknown-linux-musl` target, and the `guest` profile and feature. It +uses `--locked`, a fresh target directory, disabled incremental compilation, +stable source and Cargo-home path remapping, and no linker build IDs. One +Cargo invocation builds all nine programs, and every result must pass +`check-static-elf`. The output's `manifest.json` binds the exact build pins, +one digest over the `nvx-rs` crate, the shared Python build recipe, and +`rust-toolchain.toml`, and each binary SHA-256. + The native build keeps the Linux archive, the Ubuntu Base archive, and the supplemental Ubuntu packages under `downloads/` in the cache directory, `.cache/` or `$NVX_CACHE_DIR`. It reuses a file there whose SHA-256 matches its @@ -95,8 +111,8 @@ recording its provenance. Host OS detection and backend validation live in Fixed build inputs and defaults live in [`build_constants.py`](../scripts/nvx_tools/build_constants.py). Its namespace -classes group kernel, OpenVMM, Alpine, Ubuntu, initramfs, Docker, cache-tool, -and release settings. For example, Python callers use +classes group kernel, OpenVMM, Rust guest-binary, Alpine, Ubuntu, initramfs, +Docker, cache-tool, and release settings. For example, Python callers use `KernelBuildConstants.VERSION` and `AlpineBuildConstants.MINIROOTFS_SHA256`; the previous module-level constants are not re-exported. Shared repository and artifact directories belong to `BuildConstants`. @@ -108,10 +124,14 @@ not frozen into the constants module. The provenance sidecars bind the kernel to its pinned archive, patch set, input configuration, generated configuration, and output hash; bind the -initramfs and package manifest to the pinned Alpine inputs and source files; -and bind OpenVMM to the exact clean gitlink revision and executable hash. -The Alpine source-file inputs include the constants module. Packaging rejects -missing, dirty, stale, or mismatched provenance. +initramfs and package manifest to the pinned Alpine inputs, Rust guest inputs, +and source files; and bind OpenVMM to the exact clean gitlink revision and +executable hash. Packaging rejects missing, dirty, stale, or mismatched +provenance. `build/nvx-rs.provenance.json` likewise binds the `nvx-rs` +executable that `nvx.py` runs to the digest of its sources and its build's +Rust release, target, and features; `nvx.py` writes it when it builds +`nvx-rs`, and [`record-nvx-rs-provenance`](usage.md#record-nvx-rs-provenance) +writes it for a build made otherwise. Ubuntu adds: @@ -164,12 +184,19 @@ Both commands need the standard guest build outputs above. The second writes complete per-scenario logs under `build/test-results/microvm` by default. -The Alpine initramfs includes the sandbox PID-1 bootstrap, its container namespace -helpers, the static `nvx-device-io` benchmark helper, the static `nvx-time-probe` -guest time probe, and the static `nvx-time` guest time component under `/sbin`. -The probe measures TSC and clock read latency, reports the guest's clock, -clocksource, TSC flags, and hypervisor identity, and runs the cross-vCPU warp -test that the +All three initramfses install the same nine Rust executables under `/sbin`: +`nvx-console-pending`, `nvx-device-io`, `nvx-exit`, `nvx-ingress`, +`nvx-managed-agent`, `nvx-mmio-write`, `nvx-reseed`, `nvx-time`, and +`nvx-time-probe`. Docker builds them once in a Rust +1.95.0 image pinned by digest and copies the identical bytes into Alpine, +Ubuntu, and Azure Linux; the kernel targets do not depend on that build. The +Ubuntu determinism target instead rebuilds them from fresh targets in both +attempts. Native image builds also rebuild them fresh. + +The Alpine initramfs additionally includes the sandbox PID-1 bootstrap and +its container namespace scripts. `nvx-time-probe` measures TSC and +clock read latency, reports the guest's clock, clocksource, TSC flags, and +hypervisor identity, and runs the cross-vCPU warp test that the [time ABI](design/3.2-clock-and-time.md#327-cross-processor-consistency) uses to enforce its 1 µs skew bound. `nvx-time` implements the time ABI's [guest obligations](design/3.2-clock-and-time.md#3210-guest-obligations): the conformance @@ -186,18 +213,18 @@ executable mapping. `nvx-time exhaustive` runs the CI-only exhaustive check boots never run it. The `--report-only` flag (or the `nvx_time_abi=report-only` kernel command-line token) reports violations without powering off, for runs against VMMs that -predate the ABI. Both time tools are linked statically against musl -(`musl-gcc`): with static glibc each would add about 700 KB to the initramfs, -and every unpacked kilobyte delays cold boot. The +predate the ABI. All nine executables link the musl supplied by the pinned +Rust target. Every package manifest records the shared Rust source digest and +each installed binary digest. The [kernel configuration checks](#kernel-configuration-checks) cover the kernel features that the sandbox bootstrap needs. The APK manifest records the -`blkid` and `util-linux` tools used by the bootstrap plus the device helper's -source and binary SHA-256 values. +`blkid` and `util-linux` tools used by the bootstrap along with every +guest-binary digest. The Ubuntu initramfs uses Ubuntu userland with the NVX kernel. It is not an Ubuntu-kernel or systemd VM. Its distribution-neutral package manifest records the Ubuntu Base and supplemental binary/source identities, license metadata, -rootfs SHA-256, and NVX helper provenance. +rootfs SHA-256, and NVX guest-binary provenance. The kernel build, `build-kernel`, which the Docker build also runs, caches the verified and patched source under `.cache/linux`, uses `O=build/linux`, runs @@ -303,13 +330,22 @@ and SHA-256. The debug kernel is a CI artifact and is never packaged. The Release packaging needs the OpenVMM binary, the guest artifacts in the layout below, and the OpenVMM, kernel, and Alpine initramfs provenance sidecars, so run -`build-openvmm` and `build-guest --guest all` first. A source-inclusive package +`build-openvmm` and `build-guest --guest all` first. It also needs the release +build of `nvx-rs` at `nvx-rs/target/release/nvx-rs[.exe]`, a static +`x86_64-unknown-linux-musl` executable on Linux and an +`x86_64-pc-windows-msvc` one on Windows, with the provenance that +[`record-nvx-rs-provenance`](usage.md#record-nvx-rs-provenance) writes to +`build/nvx-rs.provenance.json`. A source-inclusive package does not need the Azure Linux artifacts. Packaging stages and verifies a complete output before replacing an existing `dist/` version, which requires `--force`. Its `SOURCE-MANIFEST.json` records the package version and exact hashes for OpenVMM, Linux, the generated kernel config, each packaged guest initramfs and its package manifest, and the Ubuntu distro layer and its -manifest. The OpenVMM section advertises microVM ABI 2, +manifest. Its `nvx_rs` section records the `nvx-rs` executable's SHA-256, the +digest of the `nvx-rs` sources that built it, and the build's Rust release, +target, and features. Its `guest_rust` section pins Rust 1.95.0 and +`x86_64-unknown-linux-musl`, and each guest's `guest_sources` includes +`nvx-rs` and `rust-toolchain.toml`. The OpenVMM section advertises microVM ABI 2, control-session protocol 1, and contract `nvx-microvm-v2-control-v2`; product guest-agent metadata is intentionally not part of this platform manifest. @@ -318,6 +354,7 @@ A binary release directory has this layout: ```text bin/openvmm[.exe] +bin/nvx-rs[.exe] guest/vmlinux guest/vmlinux.config guest/initramfs.cpio.gz @@ -331,6 +368,7 @@ guest/initramfs-azurelinux.cpio.gz.packages.json provenance/openvmm.provenance.json provenance/initramfs.provenance.json provenance/vmlinux.provenance.json +provenance/nvx-rs.provenance.json licenses/LICENSE-OPENVMM licenses/COPYING-LINUX LICENSE @@ -342,6 +380,8 @@ SHA256SUMS Packages built with `--include-source` additionally contain `source/` and omit the Azure Linux guest artifacts, whose corresponding source is not collected. +The project source archive includes `nvx-rs` and excludes its generated +`target/` tree. `SHA256SUMS` has sorted `SHA256 relative/path` entries using POSIX separators for every packaged file, including `SOURCE-MANIFEST.json`, except `SHA256SUMS` itself. diff --git a/doc/ci.md b/doc/ci.md index a84061ff4..b454aa962 100644 --- a/doc/ci.md +++ b/doc/ci.md @@ -231,10 +231,15 @@ process exits, so delayed final output chunks cannot create a false failure. Shared guest artifacts are built with Docker on a GitHub-hosted Ubuntu runner. The kernel, Alpine initramfs, Ubuntu initramfs, and Ubuntu EROFS layer use -separate cache keys. Ubuntu keys include the Canonical archive pin, -supplemental package lock, common guest sources, shared download and guest -descriptor modules, converter implementation, Dockerfile, and `.dockerignore`. -Artifact upload +separate cache keys. Every guest key includes `nvx-rs/**` except its `target` +directory, `rust-toolchain.toml`, the shared binary-build modules, the +Dockerfile, and `.dockerignore`; the Rust builder image itself is pinned by +digest. Because `hashFiles` hashes contents only, every guest key also ends +with the path-aware Rust source digest that the guest package manifests +record, which `nvx.py guest-binary-source-identity` prints. Ubuntu keys also +include the Canonical archive pin, supplemental +package lock, common guest sources, shared download and guest descriptor +modules, and converter implementation. Artifact upload retains the Alpine filenames and adds the distinct Ubuntu filenames. Each backend also boots the Ubuntu initramfs and runs `/sbin/nvx-sandbox-smoke` from the Ubuntu EROFS layer as UID/GID 65534 over a @@ -357,7 +362,35 @@ hash. The producer handoff uses one-day workflow artifacts rather than caches. Each consumer downloads both the normalized executable and its build provenance, -then restores executable permissions on Linux. Once the required artifacts are +then restores executable permissions on Linux. + +`nvx.py` runs [`nvx-rs`](../nvx-rs/README.md#the-nvx-rs-cli) for its `sandbox` +command and for the egress-policy and artifact steps of its `run` command, so +the microVM test jobs take `nvx-rs` the same way, and every release package +ships it. Each NVX CLI job builds it for its operating system with +`--release --locked --features cli`, puts it at `nvx-rs/target/release/`, +where `nvx.py` looks for it, and runs the CLI tests with it, with `NVX_RS` +naming it for the tests that run it directly: `nvx-cli-linux` builds a static +`x86_64-unknown-linux-musl` executable, which runs on every self-hosted Linux +runner, and `nvx-cli-windows` an `x86_64-pc-windows-msvc` executable with a +static C runtime, which needs no Visual C++ redistributable. Each job records +the build's provenance with +[`record-nvx-rs-provenance`](usage.md#record-nvx-rs-provenance), and, once the +tests pass, uploads the executable and the provenance as the one-day +`nvx-rs-linux-musl-executable` and `nvx-rs-linux-musl-provenance` artifacts, or +the `nvx-rs-windows-msvc-*` pair. The workflows that take them name the pair by +its base name, `nvx-rs-linux-musl` or `nvx-rs-windows-msvc`, as they name the +OpenVMM artifacts. Each microVM test job of that operating system downloads +the executable into `nvx-rs/target/release/` and restores its executable +permission on Linux. The platform jobs run neither `nvx.py sandbox` nor +`nvx.py run`, but their +[`package-release`](../.github/actions/package-release/action.yml) step +downloads both, the provenance into `build/`, so that each package ships the +`nvx-rs` of its operating system. The `managed-lifecycle` scenario also builds +and runs the ignored `nvx-rs` test `tests/managed_lifecycle.rs` with Cargo on +the runner, as `test-aci-edge-sandboxes` builds the tests of its crate. + +Once the required artifacts are ready, benchmarks run in parallel with the NVX test layer, each on its backend's benchmark runner. All three use virtual-machine performance series and the constrained eight-CPU affinity policy. Development releases and @@ -411,7 +444,9 @@ system in `nvx-cli-linux` and `nvx-cli-windows` rather than in every hypervisor job. Each checks out OpenVMM, whose pinned CPU profiles a test reads, and uses Python 3.12, the release that the self-hosted runners provide; the Windows job also installs the pinned Rust release with the minimal profile -for the curl shim test. Fork pull requests lack the OpenVMM deploy key, so the +for the curl shim test and the `nvx-rs` build. Each job first builds the +`nvx-rs` executable that `nvx.py` runs, tests with it, and hands it to the +microVM test and platform jobs of its operating system, as described above. Fork pull requests lack the OpenVMM deploy key, so the CLI jobs run, like the hypervisor jobs, only for pushes and same-repository pull requests. Because the microVM and platform jobs of each operating system wait for its CLI job, a broken CLI does not occupy the hypervisor runners. @@ -590,12 +625,15 @@ the `guest` profile. The action delegates the musl lint and tests to path-remapped build, static-ELF check, and digest manifest to [`nvx.py build-guest-binaries`](usage.md#build-guest-binaries), and the prebuilt-binary integration run to -`nvx.py test-guest-binaries`. The guests ship the C programs in -`guest/common/` until their build switches to these binaries. +`nvx.py test-guest-binaries`. The same shared routine supplies every native +and Docker guest build, and Alpine, Ubuntu, and Azure Linux ship its nine +executables. The development release job depends on `aci-edge-sandboxes` and `nvx-rs` and requires their combined Linux/Windows results to be successful; failed, -cancelled, or skipped crate checks cannot publish a release. +cancelled, or skipped crate checks cannot publish a release. Before it uploads +anything, it checks that each platform's package ships `bin/openvmm` and +`bin/nvx-rs`, or their `.exe` forms on Windows, which `download` installs. Each `nvx-microvm-tests-{kvm,mshv,whp}` job then runs `nvx.py test-aci-edge-sandboxes` on its self-hosted runner; the debug-kernel @@ -622,7 +660,11 @@ Copilot or initiate login. The wrapper provisions a distinct disposable KVM, MSHV, or WHP target with no production or GitHub credentials and forwards only the typed executor -protocol. Existing persistent microVM and performance runners are not valid +protocol. Besides the guest and OpenVMM artifacts, the target needs the Rust +toolchain and a prebuilt nvx-rs with its provenance for the scenarios that run +it, as [nvx-rs on the target](adversarial-testing.md#nvx-rs-on-the-target) +explains. +Existing persistent microVM and performance runners are not valid adversarial targets. Loss of the target heartbeat, a policy oracle, or a teardown/post-campaign boot failure fails the job and requires quarantine and reimage. @@ -650,14 +692,17 @@ pin. It restores the shared guest artifacts through the `restore-only` input of [`build-guest-artifacts`](../.github/actions/build-guest-artifacts/action.yml) and the KVM OpenVMM binary that `build-openvmm` caches for the pinned revision, -so agents can run microVM tests without rebuilding either. The guest artifact +so agents can run microVM tests without rebuilding either. It doesn't build +`nvx-rs`: `nvx.py` builds it with Cargo, and records its provenance, the first +time that a command needs it. The guest artifact restore computes its keys with the same action that saves them. The binary restore repeats the key, paths, and gzip compression of the cache entry that `build-openvmm` saves, and a test in `scripts/test_nvx_tools.py` fails when they differ, because a mismatch would otherwise only cause a silent cache miss. Because the agent firewall blocks `cdn.kernel.org` and `cdimage.ubuntu.com`, even inside containers, the workflow also installs the native guest build -prerequisites, allows the unprivileged user namespaces that Alpine's `apk` uses +prerequisites, including the pinned Rust toolchain's +`x86_64-unknown-linux-musl` target, allows the unprivileged user namespaces that Alpine's `apk` uses for package triggers, and stages the pinned Linux and Ubuntu Base archives, so agents can rebuild guest artifacts with `build-guest --native`. When a runner has a separate `/mnt` disk with more free space than `/`, it mounts the @@ -669,7 +714,7 @@ Copilot always runs the version on `dev`, even for sessions based on other branches, so changes reach agent sessions only after they merge. Pushes run it as a normal workflow for validation when they change the workflow or a file that its steps take versions, requirements, metadata, or code from: the -`build-guest-artifacts` action, the files that pin its Rust, cargo-nextest, +`build-guest-artifacts` action, all `nvx-rs/**` files, the files that pin its Rust, cargo-nextest, and shell linter versions, `requirements-dev.txt`, `SOURCE-MANIFEST.json`, `.gitmodules`, the OpenVMM submodule pin, and the NVX CLI and its modules. diff --git a/doc/contribute.md b/doc/contribute.md index 4aeb0243c..30b90a8a7 100644 --- a/doc/contribute.md +++ b/doc/contribute.md @@ -92,7 +92,7 @@ and snapshot test, which boots NVX's kernel and Alpine initramfs when [`test-openvmm`](usage.md#test-openvmm) runs it. NVX owns behavior that depends on its patched Linux kernel, its Alpine, -Ubuntu, and Azure Linux userspaces, guest helpers, SMP behavior, or virtio +Ubuntu, and Azure Linux userspaces, guest binaries and scripts, SMP behavior, or virtio devices. Add those scenarios to `scripts/nvx_tools/microvm_tests.py` and run them with `scripts/nvx.py test-microvm --backend BACKEND`. Keep guest workloads in @@ -100,11 +100,18 @@ devices. Add those scenarios to The scenarios that drive the public `nvx.py sandbox` commands live in `scripts/nvx_tools/managed_exec_tests.py` and `scripts/nvx_tools/sandbox_lifecycle_tests.py`, and `microvm_tests.py` -registers them. +registers them. `nvx.py sandbox` and the control protocol belong to `nvx-rs` +([microsoft/nvx#359](https://github.com/microsoft/nvx/issues/359)), so the +scenarios drive them as commands, compile egress policies with +`nvx-rs egress compile`, and leave the control session to `nvx-rs`: the +`managed-lifecycle` scenario builds OpenVMM's command line and runs the ignored +`nvx-rs` test `tests/managed_lifecycle.rs`, which drives the guest. Add no +Python client of the control protocol, lifecycle, or egress logic. Unit tests for the host tooling live in `scripts/test_*.py`. The `aci_edge_sandboxes` and `nvx-rs` crates keep unit tests alongside their Rust -source and integration tests in their `tests` directories. Tests of the NVX +source and integration tests in their `tests` directories. The latter also +runs `tests/guest_tools.rs` against the exact guest-profile binaries. Tests of the NVX mechanics, such as the golden fixtures of the control protocol, belong in `nvx-rs`, and tests of the ACI Edge Sandboxes contract in `aci_edge_sandboxes`. The `scripts/nvx.py test-aci-edge-sandboxes --backend diff --git a/doc/distribution.md b/doc/distribution.md index da6ed1c74..a51c78c4b 100644 --- a/doc/distribution.md +++ b/doc/distribution.md @@ -1,9 +1,22 @@ # Package and source delivery `package` stages a release directory, `dist/` by default, from the -built OpenVMM binary and guest artifacts. [Build](build.md) lists the -artifacts and provenance it requires and the binary release layout. Every -package is either binary-only or source-inclusive, and you must choose one. +built OpenVMM binary, `nvx-rs` executable, and guest artifacts. [Build](build.md) +lists the artifacts and provenance it requires and the binary release layout. +Every package is either binary-only or source-inclusive, and you must choose +one. + +Every package ships the release build of `nvx-rs`, the Rust command-line +interface that `nvx.py` runs ([Usage](usage.md#nvx-rs)): `bin/nvx-rs`, a +static `x86_64-unknown-linux-musl` executable, in the Linux packages, and +`bin/nvx-rs.exe`, an `x86_64-pc-windows-msvc` executable with a static C +runtime, in the Windows package. `provenance/nvx-rs.provenance.json` binds it +to the digest of the `nvx-rs` sources that built it, the Rust release, the +target, and the `cli` feature, and the packaged `SOURCE-MANIFEST.json` records +the same in its `nvx_rs` section with the executable's SHA-256. `package` +requires `nvx-rs/target/release/nvx-rs[.exe]` and its provenance in `build/`, +which must describe it as such a build of the checkout's sources; see +[`record-nvx-rs-provenance`](usage.md#record-nvx-rs-provenance). ## Binary-only packages @@ -80,6 +93,12 @@ OpenVMM is MIT licensed: retain its notice, but its source does not have to be published merely because it is aggregated with Linux. See `THIRD_PARTY_NOTICES.md`. +The `nvx-rs` executables are MIT licensed NVX code that statically links the +Rust standard library and permissively licensed crates, with musl and LLVM's +`libunwind` on Linux and the Microsoft C runtime on Windows. +`THIRD_PARTY_NOTICES.md` lists each with its license; `nvx-rs/Cargo.lock` +pins every crate by checksum. + ## Release archives Turn a staged release directory into one deterministic archive: @@ -92,13 +111,31 @@ python3 scripts/nvx.py archive-release \ `archive-release` checks the staged files against their `SHA256SUMS` before it writes a `.tar.gz` or `.zip` archive outside the staged directory; see -[`archive-release`](usage.md#archive-release). - -CI follows the binary-only path. On a `dev` push that changes more than -documentation, the `linux-kvm`, `linux-mshv`, and `windows-whp` platform jobs -each stage `dist/nvx--` with `--binary-only`, then archive -it as `.tar.gz` on Linux and `.zip` on Windows through the -[`package-release`](../.github/actions/package-release/action.yml) action. +[`archive-release`](usage.md#archive-release). `SHA256SUMS` lists every +packaged file, `bin/nvx-rs[.exe]` and its provenance included, and every +archive marks the executables in `bin/` executable. + +[`download`](usage.md#download) verifies the package against its `SHA256SUMS` +and installs OpenVMM, the guest artifacts, their provenance, and `nvx-rs` into +the checkout, `nvx-rs` at `nvx-rs/target/release/nvx-rs[.exe]` with its +provenance in `build/`, so that `nvx.py run` and `nvx.py sandbox` need no +Cargo. It installs `nvx-rs` only when its provenance describes the build of +the checkout's `nvx-rs` sources that `nvx.py` runs. Releases from before +packages shipped `nvx-rs` remain installable: `download` installs the rest and +leaves `nvx.py` to build `nvx-rs` with Cargo, as it does for a fallback +release whose `nvx-rs` sources differ from the checkout's. + +CI follows the binary-only path. On a `dev` push, or a ready pull request from +a branch of this repository, that changes more than documentation, the +`linux-kvm`, `linux-mshv`, and `windows-whp` platform jobs each stage +`dist/nvx--` with `--binary-only`, then archive it as +`.tar.gz` on Linux and `.zip` on Windows through the +[`package-release`](../.github/actions/package-release/action.yml) action, +with the `nvx-rs` and provenance that the NVX CLI job of their operating +system built and tested. Only `dev` pushes upload the archives, so a pull +request proves that its packages build without publishing them. The +development release job checks that each package ships OpenVMM and `nvx-rs` +before it publishes them. [Continuous integration](ci.md) describes the jobs that gate and publish the resulting development release. diff --git a/doc/guests/azurelinux-guest.md b/doc/guests/azurelinux-guest.md index 111601a66..baa028f63 100644 --- a/doc/guests/azurelinux-guest.md +++ b/doc/guests/azurelinux-guest.md @@ -60,9 +60,9 @@ The build then: checks every RPM signature, installs the RPMs, and requires `ldd`, `setpriv`, and BusyBox to be present; 3. records the installed RPM inventory for the package manifest; -4. installs the common NVX `/init`, the guest scripts it needs, and - statically linked helpers built from `guest/common`, so the helpers do not - depend on Azure Linux libraries; +4. installs the common NVX `/init`, the guest scripts it needs, and the same + nine static Rust binaries as Alpine and Ubuntu, so they do not depend on + Azure Linux libraries; 5. links the BusyBox applets that the shared guest scripts expect, including `/bin/sh`; every other command resolves to the Azure Linux packages; 6. gives the `nobody` account, UID 65534, the empty home `/nonexistent` @@ -99,11 +99,12 @@ initramfs into a RAM filesystem capped at half of guest memory; see The package manifest `initramfs-azurelinux.cpio.gz.packages.json` records the release, architecture, base image, input digest, and artifact SHA-256. Each package entry names the RPM's version, release, architecture, license, and -source RPM. The manifest also records the source and binary digests of the -freestanding device-I/O helper. The input digest is a domain-separated SHA-256 -over the release, architecture, base image, and the checkout files that define -the build: the Dockerfile, the package lock, the build modules that download -the RPMs and compute the digest, and `guest/common`. +source RPM. The manifest also records the shared aggregate Rust source digest +and each of the nine installed guest-binary digests. The input digest is a +domain-separated SHA-256 over the release, architecture, base image, and the +checkout files that define the build: the Dockerfile, package lock, build +modules, guest scripts, complete `nvx-rs` guest input set, and +`rust-toolchain.toml`. Release staging rejects a manifest that does not match the artifact, the base image, or the current inputs. Only binary-only packages carry the two Azure diff --git a/doc/guests/ubuntu-guest.md b/doc/guests/ubuntu-guest.md index afebde1a5..169a8b474 100644 --- a/doc/guests/ubuntu-guest.md +++ b/doc/guests/ubuntu-guest.md @@ -85,7 +85,8 @@ The builder then: 6. requires one `root` account, a `nobody` account with UID 65534 and home `/nonexistent`, a `nogroup` group, and usr-merged `/bin`, `/sbin`, `/lib`, and `/lib64` links; and -7. installs the NVX guest files from `guest/common` and `guest/ubuntu`. +7. installs the NVX scripts from `guest/common` and `guest/ubuntu`, and the + nine Rust guest binaries from `nvx-rs`. The builder never executes a binary from the Ubuntu tree on the host. Both artifacts are packed with root ownership, fixed timestamps, and a stable file @@ -114,6 +115,12 @@ snapshots, including the guest obligations of the time ABI. The test harness rejects the `console-snapshot` scenario for Ubuntu instead of substituting Alpine. +It installs the same nine Rust guest binaries as Alpine and Azure Linux under +their `/sbin/nvx-*` names. Docker copies all nine from one digest-pinned Rust +builder, while native and determinism builds use the shared fresh-target +routine. The package manifest records one aggregate `nvx-rs` source digest +and each installed binary's digest. + ## Ubuntu sandbox layer [`build-distro-layer`](../usage.md#build-distro-layer) turns the prepared tree diff --git a/doc/project-structure.md b/doc/project-structure.md index 818ce554d..b002eab93 100644 --- a/doc/project-structure.md +++ b/doc/project-structure.md @@ -16,7 +16,7 @@ are build products or caches and are not part of the tracked source tree. The | `doc` | User and contributor documentation | | `docker` | Reproducible guest build environment | | `kernel` | Reproducible configs and complete Linux patch series | -| `guest` | Common guest sources plus Alpine-control and Ubuntu shell helpers | +| `guest` | Common and distribution-specific guest scripts | | `ubuntu` | Pinned Ubuntu supplemental binary-package lock | | `azurelinux` | Checksum-pinned Azure Linux supplemental RPM lock | | `aci_edge_sandboxes` | Rust crate `aci_edge_sandboxes`: state-aware sandbox API with an OpenVMM backend and an opt-in native-agent backend | @@ -52,9 +52,9 @@ nvx/ | |-- actionlint.yaml Self-hosted runner labels for actionlint | `-- copilot-instructions.md Repository-wide Copilot instructions |-- .vscode/settings.json Shared VS Code settings -|-- guest/ Guest-owned scripts and static helpers -| |-- common/ Shared init, agent, snapshot, time, and test helpers -| |-- alpine/ Alpine-control container entry helpers +|-- guest/ Guest-owned scripts +| |-- common/ Shared init, agent, snapshot, and probe scripts +| |-- alpine/ Alpine-control container entry scripts | `-- ubuntu/ Ubuntu interactive-shell startup policy |-- ubuntu/ | `-- packages.lock.json Exact supplemental Ubuntu binary package closure @@ -101,11 +101,11 @@ nvx/ | | |-- protocol/ Control-console codec shared by the host and the guest | | |-- contract.rs Control-contract versions and release compatibility | | |-- net/ CIDR parsing and subtraction, and hostname and guest-network forms -| | |-- host/ Artifacts, launch, policy lowering, sessions, and VM instances +| | |-- host/ Artifacts, launch, network and egress-policy lowering, sessions, VM instances, the sandbox launch profile, and the `nvx-rs` CLI (feature `cli`) | | |-- testing/ Fake OpenVMM (feature `testing`) | | |-- guest/ Programs that run in Linux guests (feature `guest`) -| | `-- bin/ nvx-fake-openvmm test double and the guest binaries -| |-- tests/ Fake-OpenVMM instance lifecycle and guest-binary tests +| | `-- bin/ nvx-rs CLI, nvx-fake-openvmm test double, and the guest binaries +| |-- tests/ Fake-OpenVMM instance lifecycle, CLI, guest-binary, and hypervisor managed-lifecycle tests, and the golden fixtures that Python recorded | |-- build.rs Stages the bundled artifacts (feature `bundled`) | `-- artifacts.json Release package pinned for the bundled artifacts |-- openvmm/ OpenVMM Git submodule @@ -128,11 +128,9 @@ nvx/ | | |-- collect_alpine_sources.py Alpine source collection | | |-- collect_ubuntu_sources.py Ubuntu source collection | | |-- common.py Shared process and configuration helpers -| | |-- control_session.py Control-session client for managed workloads | | |-- create_linux_source_archive.py Linux source packaging | | |-- development_release.py Resumable development release publication | | |-- doctor.py Time ABI host qualification -| | |-- egress_policy.py Egress policy compiler for OpenVMM's rule grammar | | |-- elf.py Static-executable check of ELF binaries | | |-- guests.py Typed guest descriptors | | |-- host_telemetry.py Benchmark host provenance and Windows capture storage telemetry @@ -140,12 +138,12 @@ nvx/ | | |-- managed_exec_tests.py Managed workload environment, directory, and timeout tests | | |-- microvm_test_scripts/ Guest workloads for the microVM tests | | |-- microvm_tests.py NVX microVM correctness tests +| | |-- nvx_rs.py Finds, builds, and runs nvx-rs, to which nvx.py delegates | | |-- openvmm_process.py Foreground OpenVMM process control for tests | | |-- performance.py CI performance collection, persistence, and gates | | |-- process_supervisor.py Linux supervisor for one owned process tree | | |-- release.py Release download, source collection, and packaging -| | |-- sandbox.py Host-side sandbox launch contract -| | |-- sandbox_lifecycle.py Managed sandbox lifecycle operations +| | |-- sandbox.py Live-share arguments of `nvx.py run` | | |-- sandbox_lifecycle_tests.py Managed sandbox lifecycle and security tests | | |-- time_abi.py Time ABI harness checks and CPU profile guidance | | `-- ubuntu.py Verified Ubuntu rootfs and EROFS preparation @@ -201,9 +199,10 @@ OpenVMM release. ### `guest/`, `ubuntu/`, and `azurelinux/` -`guest/common` contains scripts and static helper sources shared by the Alpine, -Ubuntu, and Azure Linux initramfs builds, plus the Azure Linux variants of the -container-entry helpers. `init` controls early boot and then starts an +`guest/common` contains scripts shared by the Alpine, Ubuntu, and Azure Linux +initramfs builds, plus the Azure Linux variants of the container-entry +scripts. The nine compiled `/sbin/nvx-*` programs live under `nvx-rs/src/guest` +and `nvx-rs/src/bin`. `init` controls early boot and then starts an initramfs program named by `nvx_exec=`, the Alpine-only `nvx-init-agent` sandbox profile, `nvx-managed-agent` for a managed workload, a virtio restore probe for benchmark profiling, or the interactive shell. On an interactive @@ -213,14 +212,16 @@ the container launch and entry scripts that only the Alpine initramfs installs; the entry script stages Alpine's musl loader and `setpriv` inside the workload's root. -The sandbox helpers resolve fixed virtio-blk roles through +The sandbox scripts resolve fixed virtio-blk roles through sysfs, assemble EROFS lower layers over ext4 scratch, place the workload in its cgroup before release, construct its mount/PID/UTS namespaces, enter its filesystem root after dropping capabilities, and retain the agent as the outer PID 1. In a managed sandbox, the agent supervises `nvx-managed-agent` and performs the ordered unmount teardown after a stop request. The remaining common helpers handle shutdown, virtio-fs mounting, snapshot preparation, and -the guest side of the time ABI, or provide device-access and diagnostic probes. +the guest side of the time ABI. The Rust guest binaries provide shutdown, +managed control, restore repair, device access, ingress announcements, and +diagnostic probes. `ubuntu/packages.lock.json` pins the complete supplemental `.deb` closure installed without maintainer-script execution. @@ -268,10 +269,14 @@ API, the policy honor matrix, and tests. The Rust crate `nvx-rs`, whose library `nvx` holds the NVX mechanics: the authenticated control protocol of the guest agent, artifact discovery and bundled-release staging, OpenVMM's command line, the lowering of network and -filesystem policies, process and platform primitives for Linux and Windows -hosts, the supervision of VM instances, and a fake OpenVMM for tests. Its -`guest` feature adds the programs that will replace the C programs of -`guest/common/`, built as static musl executables. It is a +filesystem policies, the compiler of `nvx.py`'s egress policy files, process +and platform primitives for Linux and Windows +hosts, the supervision of VM instances, the sandbox launch profile of +`nvx.py sandbox`, the `nvx-rs` command-line interface that ports +`nvx.py sandbox`, egress policy compilation, and artifact lookup, and a fake +OpenVMM for tests. Its +`guest` feature builds the nine static executables that every guest image +installs at `/sbin/nvx-*`. It is a standalone crate with its own `Cargo.lock`, like `aci_edge_sandboxes`, and builds independently of the OpenVMM submodule. See its [README](../nvx-rs/README.md). @@ -326,7 +331,7 @@ see [Automated environment bootstrap](setup.md#automated-environment-bootstrap). | `pyproject.toml` | Strict Pyright policy plus Ruff lint and format settings | | `requirements-dev.txt` | Pinned Python tools used by contributors and CI | | `rust-toolchain.toml` | Rust release that builds OpenVMM, its test guests, and the repository's Rust code | -| `SOURCE-MANIFEST.json` | Distribution version, OpenVMM microVM ABI and control-protocol versions, and exact Linux, Alpine, Ubuntu, and Azure Linux input identities and output locations | +| `SOURCE-MANIFEST.json` | Distribution version, OpenVMM microVM ABI and control-protocol versions, Rust guest toolchain and source roots, and exact Linux, Alpine, Ubuntu, and Azure Linux input identities and output locations | | `VERSION` | Distribution version consumed by packaging tools | | `.gitmodules` | OpenVMM repository URL, path, and tracking branch | | `.gitignore` | Excludes build products, caches, virtual environments, logs, and platform metadata | diff --git a/doc/run.md b/doc/run.md index a984dd70c..35d171c08 100644 --- a/doc/run.md +++ b/doc/run.md @@ -667,8 +667,10 @@ 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. `nvx.py` runs it with +[`nvx-rs`](usage.md#nvx-rs), which it finds, or builds with Cargo, as +[Usage](usage.md#nvx-rs) describes: ```bash python3 scripts/nvx.py build-distro-layer \ @@ -831,9 +833,11 @@ only by the sandbox's 1024-byte kernel command-line budget. A one-shot workload exit, a managed `stop`, and any failure after a share is mounted unmount the binds in reverse order, then the aggregate, before the overlay is unmounted or the VM powers off. Each target must be an absolute, canonical -path; `/` and `/etc`, and the `/etc/machine-id`, `/proc`, `/sys`, `/dev`, and -`/.nvx-agent` trees, are reserved for the container runtime, which replaces -`/etc/machine-id` with the workload's machine ID. +path; `/`, `/etc`, `/run`, and `/run/nvx`, and the `/etc/machine-id`, +`/proc`, `/sys`, `/dev`, `/.nvx-agent`, and `/run/nvx/time` trees, are +reserved for the container runtime, which replaces `/etc/machine-id` with the +workload's machine ID, creates `/run` and `/run/nvx`, and binds its time state +over `/run/nvx/time`. The guest refuses a target whose path crosses a symbolic link in a container layer, a target that a container layer holds as another kind of file than the share's, a repeated child, and overlapping targets, and any validation or mount diff --git a/doc/setup.md b/doc/setup.md index 40c15572d..a13674bf5 100644 --- a/doc/setup.md +++ b/doc/setup.md @@ -30,7 +30,11 @@ python3 scripts/nvx.py init Python 3.10 or newer and Git are required on every platform. Building OpenVMM or the Rust crates also requires [rustup](https://rustup.rs/), which installs the Rust release that [`rust-toolchain.toml`](../rust-toolchain.toml) pins on -first use. +first use. `nvx.py run` and `nvx.py sandbox` run `nvx-rs`, which +[`download`](usage.md#download) installs from a release built from the +checkout's `nvx-rs` sources. Otherwise, unless `NVX_RS` names an executable, +`nvx.py` builds it with that release the first time that a command needs it; +see [Usage](usage.md#nvx-rs). By default, a microVM boots on the built-in [CPU profile](usage.md#cpu-profiles) of its host's CPU: four Intel @@ -52,11 +56,21 @@ For Debian/Ubuntu hosts: sudo apt-get update sudo apt-get install -y \ bc binutils bison build-essential ca-certificates cpio curl erofs-utils flex \ - gzip libelf-dev libssl-dev musl-tools python3 rsync tar xz-utils zstd + gzip libelf-dev libssl-dev python3 rsync tar xz-utils zstd ``` -`musl-tools` provides `musl-gcc`, which native initramfs builds use for the -guest time tools; Docker builds install it in the build image. +Native guest builds use the exact Rust release in `rust-toolchain.toml` and +its musl standard-library target: + +```bash +toolchain=$(sed -n 's/^channel = "\([^"]*\)".*/\1/p' rust-toolchain.toml) +rustup toolchain install "$toolchain" --profile minimal +rustup target add --toolchain "$toolchain" x86_64-unknown-linux-musl +``` + +The Linux runner setup scripts and Copilot setup install this target +automatically. `build-essential` remains necessary for the kernel and as +Rust's linker driver; no separate guest libc compiler is required. ### Linux / KVM @@ -239,7 +253,8 @@ Production campaigns also require an administrator-owned, no-argument executor wrapper that provisions a separate disposable target and forwards the bounded executor protocol. See [Copilot-driven adversarial testing](adversarial-testing.md) -for the containment and credential requirements. +for the containment and credential requirements, and for the Rust toolchain +and nvx-rs that the target needs. Apply the configured Python and POSIX shell formatters with: diff --git a/doc/usage.md b/doc/usage.md index d25010063..aca1ca3d8 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -18,6 +18,45 @@ python3 scripts/nvx.py run --help python3 scripts/nvx.py performance gate --help ``` +## nvx-rs + +`nvx.py` runs [`nvx-rs`](../nvx-rs/README.md#the-nvx-rs-cli), the Rust +implementation of NVX's runtime steps, for the [`sandbox`](#sandbox) command, +which it runs entirely, and for the egress-policy and artifact steps of +[`run`](#run). It runs, in this order: + +1. the executable that the `NVX_RS` environment variable names; +2. `nvx-rs/target/release/nvx-rs`, or `nvx-rs.exe` on Windows, in the + checkout, where `cargo build --release` puts it, and where + [`download`](#download) installs the `nvx-rs` of a release; or else +3. that executable, after it builds it from the repository root with + `cargo build --release --locked --features cli --manifest-path + nvx-rs/Cargo.toml --target-dir nvx-rs/target`, the Rust release that + `rust-toolchain.toml` pins, and Cargo's output on standard error, so that + it never mixes with the output of the command. + +After `download`, `nvx.py` needs no Cargo: every release package ships +`bin/nvx-rs`, or `bin/nvx-rs.exe`, which `download` installs when the +release was built from the checkout's `nvx-rs` sources. Without `NVX_RS`, a +built or installed executable, or Cargo, such a command fails and names +these choices; [rustup](https://rustup.rs) installs Cargo. `nvx.py` rebuilds +nothing on its own once the executable exists, so run the build command again, +or delete the executable, after changing `nvx-rs`. The `managed-lifecycle` +scenario of [`test-microvm`](#test-microvm) also needs Cargo, which runs its +`nvx-rs` test. + +The provenance of the `nvx-rs` that `nvx.py` runs, +`build/nvx-rs.provenance.json`, binds the SHA-256 of its executable to a +digest of the `nvx-rs` sources that built it, the crate's manifest, lock +file, build script, and `src/` tree and `rust-toolchain.toml`, and to the +Rust release, target, and Cargo features of the build. `nvx.py` records it +when it builds `nvx-rs`, `download` installs a release's, and +[`record-nvx-rs-provenance`](#record-nvx-rs-provenance) records it for a +build made otherwise. [`package`](#package) and the +[adversarial executor](adversarial-testing.md#nvx-rs-on-the-target) refuse an +`nvx-rs` that it does not describe as built from the checkout's sources, so a +stale build never ships or runs unnoticed. + ## Commands | Command | Description | @@ -33,10 +72,12 @@ python3 scripts/nvx.py performance gate --help | `verify-guest-determinism` | Rebuild Ubuntu guest artifacts twice and compare SHA-256 values. | | `build-openvmm` | Build the OpenVMM release binary. | | `record-openvmm-provenance` | Bind an existing OpenVMM binary to the pinned source revision. | +| `record-nvx-rs-provenance` | Bind an existing `nvx-rs` executable to its sources, Rust release, target, and features. | | `materialize-kernel-provenance-inputs` | Write kernel provenance inputs from raw run-head blobs. | | `setup-cross-os-cache` | Install GNU tar and zstd for GitHub Actions cross-OS caches. | | `classify-ci-changes` | Select the CI suites that a list of changed files runs. | | `kernel-patch-identity` | Print the digest of the kernel patch paths and contents that keys the kernel cache. | +| `guest-binary-source-identity` | Print the digest of the Rust guest binary source paths and contents that keys the guest image caches. | | `check-required-ci` | Validate required GitHub Actions job results. | | `check-static-elf` | Check that executables are static ELF files without an interpreter. | | `test-openvmm-unit` | Run the OpenVMM workspace unit and documentation tests. | @@ -97,6 +138,20 @@ order, together with its content. The guest build appends it to both kernel cache keys, because `hashFiles` hashes contents only: a renamed patch changes the key even when its bytes do not. +### `guest-binary-source-identity` + +```text +python3 scripts/nvx.py guest-binary-source-identity [--root PATH] +``` + +Prints the source digest that guest package manifests record for the Rust guest +binaries: a digest of the relative path and content of every file under +`nvx-rs/src/`, the crate's manifest, lockfile, and build script, +`rust-toolchain.toml`, and the `build_constants.py` and `guest_binaries.py` +build modules. The guest build appends it to the Alpine, Ubuntu, and Azure +Linux cache keys, because `hashFiles` hashes contents only: moving a module +into its own `mod.rs` changes the key even when no bytes change. + ### `classify-ci-changes` ```text @@ -295,6 +350,26 @@ already exist at `openvmm/target/release/openvmm` on POSIX or `openvmm/target/release/openvmm.exe` on Windows. The command fails if either prerequisite is missing. +### `record-nvx-rs-provenance` + +```text +python3 scripts/nvx.py record-nvx-rs-provenance [--target TARGET] +``` + +| Option | Default | Description | +| --- | --- | --- | +| `--target TARGET` | the host target that `rustc -vV` reports | Rust target that built `nvx-rs`. | + +Records `build/nvx-rs.provenance.json` for the `nvx-rs` that `nvx.py` runs +without building it, the one that `NVX_RS` names or else +`nvx-rs/target/release/nvx-rs[.exe]`; see [nvx-rs](#nvx-rs). The record binds +the executable's SHA-256 to the digest of this checkout's `nvx-rs` sources, +the Rust release that `rust-toolchain.toml` pins, the target, and the `cli` +feature, so record it right after building `nvx-rs` from this checkout. For +`x86_64-unknown-linux-musl`, the target of the Linux release packages, the +executable must be static. CI's NVX CLI jobs record the builds that they hand +to the packages, and `nvx.py` records the builds that it makes itself. + ### `build` ```text @@ -369,9 +444,14 @@ only the same-host restore scenarios `smp`, `smp-snapshot`, test hook, which makes OpenVMM's default CPU profile selection, `auto`, fall back to a host profile on a host that a built-in profile serves, checks the fallback's warning, captures the guest, and restores it on the snapshot's host -profile. The command -requires `build/vmlinux` (with `--debug-kernel`, `build/vmlinux-debug` and its -`build/vmlinux-debug.config`), the selected initramfs, and +profile. The `managed-lifecycle` scenario boots a guest whose init hands +the boot to the managed agent, and runs the ignored `nvx-rs` test +`tests/managed_lifecycle.rs` with Cargo, which starts OpenVMM with the +scenario's command line and drives the guest's control session; the scenario +then checks OpenVMM's outcome report and the guest console. The scenarios that +drive `sandbox`, or that compile egress policies, run [`nvx-rs`](#nvx-rs). The +command requires `build/vmlinux` (with `--debug-kernel`, `build/vmlinux-debug` +and its `build/vmlinux-debug.config`), the selected initramfs, and `openvmm/target/release/openvmm[.exe]`. ### `doctor` @@ -547,7 +627,22 @@ checkout to install a newer release. Installing a release replaces the packaged guest artifacts under `build/` and removes any known guest artifact that the release does not declare, such as the -Azure Linux guest that source-inclusive packages omit. +Azure Linux guest that source-inclusive packages omit. It installs the +packaged OpenVMM at `openvmm/target/release/openvmm[.exe]` and the packaged +`nvx-rs` at `nvx-rs/target/release/nvx-rs[.exe]`, both executable, with their +provenance in `build/`, so that [`run`](#run) and [`sandbox`](#sandbox) need +no Cargo. + +`download` installs the packaged `nvx-rs` only when its provenance describes +the build of the checkout's `nvx-rs` sources that `nvx.py` runs, with the Rust +release that the checkout pins, the release target, and the `cli` feature, +because `nvx.py` runs that executable without rebuilding it. A fallback +release built from other `nvx-rs` sources, and a release from before packages +shipped `nvx-rs`, still install everything else: `download` then removes any +earlier `nvx-rs` and its provenance and reports why, and `nvx.py` builds +`nvx-rs` from the checkout with Cargo when a command needs it; see +[nvx-rs](#nvx-rs). A package whose `nvx-rs` does not match its own provenance +fails the download before it replaces anything. Windows release downloads support WHP. Linux release downloads support KVM and MSHV. `download` first uses `GH_TOKEN` or `GITHUB_TOKEN` when configured. @@ -624,9 +719,11 @@ python3 scripts/nvx.py run | `--dry-run` | off | Print the generated OpenVMM command without running it. | The command requires the OpenVMM release binary, `build/vmlinux`, and the -selected `build/initramfs*.cpio.gz`. Ubuntu selection never falls back to -Alpine. See [Run](run.md) for host setup, guest shutdown, networking, and -virtio-fs examples. +selected `build/initramfs*.cpio.gz`, which it looks up with `nvx-rs artifacts`, +and compiles `--network-egress-policy-file` with `nvx-rs egress compile`; see +[nvx-rs](#nvx-rs). It builds and runs the OpenVMM command itself. Ubuntu +selection never falls back to Alpine. See [Run](run.md) for host setup, guest +shutdown, networking, and virtio-fs examples. If no built-in CPU profile serves an Intel or AMD host's CPU, OpenVMM falls back to a host profile and warns with `NVX-CPU-PROFILE-FALLBACK:` at every @@ -688,10 +785,20 @@ python3 scripts/nvx.py sandbox Network policy and live-share options configure only the `run` and `provision` launches. +`sandbox` runs `nvx-rs sandbox` (see [nvx-rs](#nvx-rs)) with the operation and +the options given, and its exit status is the command's: 1 for an error, 2 for +a usage error, 130 after Ctrl-C, and otherwise the status that the workload or +OpenVMM returns. `nvx.py` parses the options only for its help and for the +usage errors of unknown options, invalid choices, and missing values, so it +still accepts abbreviated options, which it passes to `nvx-rs` in full. +`nvx-rs` checks their values, and its usage and messages name +`nvx.py sandbox`; its usage text wraps at 80 columns whatever the terminal's +width. + | 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. Only `distro` is accepted. | | `--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. | @@ -708,7 +815,7 @@ launches. | `--environment-file PATH` | omitted | Read the exact managed `exec` environment from a UTF-8 JSON array of `KEY=VALUE` strings, limited to 1 MiB of input. An empty array requests an empty environment. This option is mutually exclusive with `--environment`; omitting both preserves guest defaults. | | `--inherit-default-environment` | off | Layer the managed `exec` environment from `--environment` or `--environment-file` over the guest default environment instead of replacing it; an entry replaces the default variable of the same name. Without either option the workload already receives the defaults, so the flag has no effect. | | `--hypervisor {auto,whp,kvm,mshv}` | `auto` | Select the host hypervisor. | -| `--mount GUEST_TARGET,HOST_PATH[,ro\|rw]` | none | Live-share a host directory, or a single regular host file without its directory, at the absolute target inside the container rootfs for `run` or `provision`; defaults to `ro`. The mode follows the last comma, so `HOST_PATH` may contain commas when the mode is given. Repeat to attach more shares, each with its own target and mode, for example a read-write workspace and a read-only tool cache, as the children of an aggregate that the guest binds at their targets; a shared file always uses the aggregate. Targets and host paths must not overlap, and the shares' kernel command-line tokens count toward the sandbox's 1024-byte budget. An `rw` share accepts guest-created symbolic links, which the host never follows. `/` and `/etc`, and the `/etc/machine-id`, `/proc`, `/sys`, `/dev`, and `/.nvx-agent` trees, are reserved. | +| `--mount GUEST_TARGET,HOST_PATH[,ro\|rw]` | none | Live-share a host directory, or a single regular host file without its directory, at the absolute target inside the container rootfs for `run` or `provision`; defaults to `ro`. The mode follows the last comma, so `HOST_PATH` may contain commas when the mode is given. Repeat to attach more shares, each with its own target and mode, for example a read-write workspace and a read-only tool cache, as the children of an aggregate that the guest binds at their targets; a shared file always uses the aggregate. Targets and host paths must not overlap, and the shares' kernel command-line tokens count toward the sandbox's 1024-byte budget. An `rw` share accepts guest-created symbolic links, which the host never follows. `/`, `/etc`, `/run`, and `/run/nvx`, and the `/etc/machine-id`, `/proc`, `/sys`, `/dev`, `/.nvx-agent`, and `/run/nvx/time` trees, are reserved. | | `--mount-deny HOST_PATH` | none | Hide one existing file or directory inside a `--mount` host directory; relative paths are resolved inside it. It may name the directory itself when `--mount-allow` paths inside it remain. With several shares, it applies to the `--mount` before it, which must share a directory. Repeat to deny multiple paths. | | `--mount-allow HOST_PATH` | none | Expose one existing file or directory inside a `--mount-deny` path of the same share again, for example a readable `mcp-payloads` directory inside hidden logs. The hidden directories on the way list only the entries that lead to it, and the workload cannot modify them. Relative paths are resolved inside the share; with several shares, it applies to the `--mount` before it. Repeat to allow multiple paths. See [Access policy](run.md#access-policy). | | `--mount-write HOST_PATH` | none | Make one existing file or directory one of the only writable parts of an `rw` share; the workload's writes anywhere else in the share fail with `EROFS`. Relative paths are resolved inside the share; with several shares, it applies to the `--mount` before it, which must be `rw`. Repeat to declare multiple writable paths. `provision` persists the denied, allowed, and writable paths. | @@ -1112,7 +1219,13 @@ python3 scripts/nvx.py package | `--binary-only` | Stage binaries only; publish corresponding source separately. | | `--force` | Replace an existing staging destination. | -Exactly one of `--include-source` and `--binary-only` is required. See +Exactly one of `--include-source` and `--binary-only` is required. Every +package ships `nvx-rs` with its provenance, which must describe +`nvx-rs/target/release/nvx-rs[.exe]` as built from the checkout's sources +for the release target of the host's operating system: +`x86_64-unknown-linux-musl`, a static executable, on Linux, and +`x86_64-pc-windows-msvc` on Windows; see +[`record-nvx-rs-provenance`](#record-nvx-rs-provenance). See [Package and source delivery](distribution.md) for release procedures and source-publication requirements. diff --git a/docker/Dockerfile b/docker/Dockerfile index 030c3eb8d..8fdeac9a4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,26 @@ # syntax=docker/dockerfile:1.7 ARG DEBIAN_IMAGE=debian:12-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251 +ARG RUST_BUILDER_IMAGE=rust:1.95.0-slim-bookworm@sha256:d7482085ff5b415f84dba5647ae71606650bdef00db7aeb69f4b3d170c3e4082 ARG EROFS_UTILS_VERSION=1.5-1 ARG AZURELINUX_VERSION=3.0 ARG AZURELINUX_IMAGE=mcr.microsoft.com/azurelinux/base/core@sha256:c877612270d1ee2d6ab2bc1f64bfe38ab697ac50be325154ee5129fce89c17e4 +FROM ${RUST_BUILDER_IMAGE} AS guest-binaries +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + python3 \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /repo +COPY rust-toolchain.toml /repo/ +COPY nvx-rs/ /repo/nvx-rs/ +COPY scripts/ /repo/scripts/ +RUN set -eux; \ + toolchain="$(sed -n 's/^channel = "\([^"]*\)".*/\1/p' rust-toolchain.toml)"; \ + test -n "$toolchain"; \ + rustup target add --toolchain "$toolchain" x86_64-unknown-linux-musl; \ + python3 scripts/nvx.py build-guest-binaries --output /out/guest-binaries + FROM ${DEBIAN_IMAGE} AS base ARG EROFS_UTILS_VERSION RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -20,7 +36,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ gzip \ libelf-dev \ libssl-dev \ - musl-tools \ patch \ python3 \ rsync \ @@ -43,10 +58,18 @@ RUN python3 scripts/nvx.py build-kernel FROM base AS kernel-debug RUN python3 scripts/nvx.py build-kernel --debug -FROM base AS initramfs +# Only the guest images install the Rust guest binaries and record their sources, +# so the kernel and Linux source targets never build them. +FROM base AS guest-base +COPY nvx-rs/ /repo/nvx-rs/ +COPY rust-toolchain.toml /repo/ +COPY --from=guest-binaries /out/guest-binaries/ /repo/build/guest-binaries/ +ENV NVX_GUEST_BINARIES=/repo/build/guest-binaries + +FROM guest-base AS initramfs RUN python3 scripts/nvx.py build-initramfs --guest alpine -FROM base AS ubuntu-initramfs +FROM guest-base AS ubuntu-initramfs RUN python3 scripts/nvx.py build-initramfs --guest ubuntu FROM ubuntu-initramfs AS ubuntu-distro-layer @@ -54,26 +77,16 @@ RUN python3 scripts/nvx.py build-distro-layer \ --guest ubuntu \ --output build/ubuntu-distro.erofs -FROM base AS ubuntu-determinism -RUN python3 scripts/nvx.py verify-guest-determinism \ +FROM guest-base AS ubuntu-determinism +COPY --from=guest-binaries /usr/local/cargo/ /usr/local/cargo/ +COPY --from=guest-binaries /usr/local/rustup/ /usr/local/rustup/ +ENV CARGO_HOME=/usr/local/cargo +ENV RUSTUP_HOME=/usr/local/rustup +ENV PATH=/usr/local/cargo/bin:${PATH} +RUN NVX_GUEST_BINARIES= python3 scripts/nvx.py verify-guest-determinism \ --guest ubuntu \ --work-dir build/guest-determinism -FROM base AS azurelinux-helpers -RUN set -eux; \ - mkdir -p /out/helpers; \ - for helper in nvx-reseed nvx-mmio-write nvx-console-pending nvx-managed-agent; do \ - cc -static -Os -s -Wall -Wextra -Werror -o "/out/helpers/$helper" "/repo/guest/common/$helper.c"; \ - done; \ - for helper in nvx-time nvx-exit nvx-ingress; do \ - musl-gcc -static -Os -s -Wall -Wextra -Werror -o "/out/helpers/$helper" "/repo/guest/common/$helper.c"; \ - done; \ - musl-gcc -std=gnu11 -static -O2 -pthread -s -Wall -Wextra -Werror -o /out/helpers/nvx-time-probe \ - /repo/guest/common/nvx-time-probe.c; \ - cc -nostdlib -static -Os -fno-builtin -fno-pie -fno-stack-protector -no-pie \ - -Wl,--build-id=none -Wl,-z,noexecstack -s -o /out/helpers/nvx-device-io \ - /repo/guest/common/nvx-device-io.c - FROM base AS azurelinux-packages RUN python3 - <<'PY' import sys @@ -97,12 +110,11 @@ RUN rpm --import /etc/pki/rpm-gpg/MICROSOFT-RPM-GPG-KEY \ && rpm -qa --queryformat '%{NAME}\t%{VERSION}\t%{RELEASE}\t%{ARCH}\t%{LICENSE}\t%{SOURCERPM}\n' \ | sort > /tmp/azurelinux-rpm-packages.tsv -FROM base AS azurelinux-initramfs +FROM guest-base AS azurelinux-initramfs ARG AZURELINUX_IMAGE ARG AZURELINUX_VERSION COPY --from=azurelinux-rootfs / /rootfs/ COPY --from=azurelinux-rootfs /tmp/azurelinux-rpm-packages.tsv /tmp/azurelinux-rpm-packages.tsv -COPY --from=azurelinux-helpers /out/helpers/ /rootfs/sbin/ RUN <<'EOF' set -eux install -m 0755 /repo/guest/common/init /rootfs/init @@ -113,6 +125,22 @@ install -m 0755 /repo/guest/common/nvx-hostmount \ /repo/guest/common/nvx-container-enter-azurelinux /rootfs/sbin/ install -m 0755 /repo/guest/common/nvx-container-launch-azurelinux \ /rootfs/sbin/nvx-container-launch +python3 <<'PY' +import shutil +import sys +from pathlib import Path + +sys.path.insert(0, '/repo/scripts') +from nvx_tools.build_constants import GuestRustBuildConstants +from nvx_tools.guest_binaries import inspect_guest_binaries + +source = Path('/repo/build/guest-binaries') +inspect_guest_binaries(source) +for name in GuestRustBuildConstants.BINARIES: + destination = Path('/rootfs/sbin') / name + shutil.copyfile(source / name, destination) + destination.chmod(0o755) +PY # Link the BusyBox applets that the shared guest scripts expect; every other # command resolves through PATH to the Azure Linux packages. ln -sf ../sbin/busybox /rootfs/usr/bin/busybox @@ -171,10 +199,23 @@ from pathlib import Path sys.path.insert(0, '/repo/scripts') from nvx_tools.azurelinux import input_sha256 +from nvx_tools.build_constants import GuestRustBuildConstants +from nvx_tools.guest_binaries import inspect_guest_binaries def sha256(path: str) -> str: return hashlib.sha256(Path(path).read_bytes()).hexdigest() +guest_build = inspect_guest_binaries(Path('/repo/build/guest-binaries')) +helpers = {} +for name in GuestRustBuildConstants.BINARIES: + binary_sha256 = sha256(f'/rootfs/sbin/{name}') + if binary_sha256 != guest_build.binaries[name]: + raise ValueError(f'installed Rust guest binary changed: {name}') + helpers[name] = { + 'source_sha256': guest_build.source_sha256, + 'binary_sha256': binary_sha256, + } + manifest = { 'format': 1, 'guest': 'azurelinux', @@ -190,12 +231,7 @@ manifest = { ), 'package_manifest_format': 'azurelinux-v1', 'packages': json.loads(Path('/tmp/azurelinux-packages.json').read_text(encoding='utf-8')), - 'helpers': { - 'nvx-device-io': { - 'source_sha256': sha256('/repo/guest/common/nvx-device-io.c'), - 'binary_sha256': sha256('/rootfs/sbin/nvx-device-io'), - }, - }, + 'helpers': helpers, } Path('/tmp/initramfs-azurelinux.cpio.gz.packages.json').write_text( json.dumps(manifest, indent=2, sort_keys=True) + '\n', diff --git a/guest/common/nvx-console-pending.c b/guest/common/nvx-console-pending.c deleted file mode 100644 index 1c556d3a6..000000000 --- a/guest/common/nvx-console-pending.c +++ /dev/null @@ -1,40 +0,0 @@ -#include -#include -#include -#include -#include -#include - -int main(int argc, char **argv) -{ - int descriptor; - int pending; - - if (argc != 2) { - fprintf(stderr, "usage: nvx-console-pending DEVICE\n"); - return 2; - } - - descriptor = open(argv[1], O_RDONLY | O_CLOEXEC | O_NOCTTY | O_NONBLOCK); - if (descriptor < 0) { - fprintf(stderr, "nvx-console-pending: open %s: %s\n", argv[1], - strerror(errno)); - return 1; - } - if (ioctl(descriptor, FIONREAD, &pending) != 0) { - fprintf(stderr, "nvx-console-pending: ioctl %s: %s\n", argv[1], - strerror(errno)); - close(descriptor); - return 1; - } - if (close(descriptor) != 0) { - fprintf(stderr, "nvx-console-pending: close %s: %s\n", argv[1], - strerror(errno)); - return 1; - } - if (printf("%d\n", pending) < 0) { - fprintf(stderr, "nvx-console-pending: write: %s\n", strerror(errno)); - return 1; - } - return 0; -} \ No newline at end of file diff --git a/guest/common/nvx-device-io.c b/guest/common/nvx-device-io.c deleted file mode 100644 index 752b613be..000000000 --- a/guest/common/nvx-device-io.c +++ /dev/null @@ -1,376 +0,0 @@ -typedef unsigned long u64; - -#define SYS_READ 0 -#define SYS_WRITE 1 -#define SYS_CLOSE 3 -#define SYS_POLL 7 -#define SYS_PREAD64 17 -#define SYS_PWRITE64 18 -#define SYS_SOCKET 41 -#define SYS_CONNECT 42 -#define SYS_FSYNC 74 -#define SYS_FTRUNCATE 77 -#define SYS_CLOCK_GETTIME 228 -#define SYS_OPENAT 257 -#define SYS_EXIT_GROUP 231 - -#define AT_FDCWD -100 -#define O_RDONLY 0 -#define O_RDWR 2 -#define O_CREAT 64 -#define O_DIRECT 16384 -#define O_CLOEXEC 524288 -#define AF_INET 2 -#define SOCK_DGRAM 2 -#define SOCK_CLOEXEC 524288 -#define POLLIN 1 -#define CLOCK_MONOTONIC 1 - -struct timespec { - long seconds; - long nanoseconds; -}; - -struct sockaddr_in { - unsigned short family; - unsigned short port; - unsigned int address; - unsigned char padding[8]; -}; - -struct pollfd { - int fd; - short events; - short returned_events; -}; - -static unsigned char io_buffer[4096] __attribute__((aligned(4096))); -static char output[384]; -static u64 output_length; - -static long syscall6( - long number, - long argument1, - long argument2, - long argument3, - long argument4, - long argument5, - long argument6 -) { - register long register10 __asm__("r10") = argument4; - register long register8 __asm__("r8") = argument5; - register long register9 __asm__("r9") = argument6; - long result; - __asm__ volatile( - "syscall" - : "=a"(result) - : "a"(number), "D"(argument1), "S"(argument2), "d"(argument3), - "r"(register10), "r"(register8), "r"(register9) - : "rcx", "r11", "memory" - ); - return result; -} - -static long syscall4(long number, long a1, long a2, long a3, long a4) { - return syscall6(number, a1, a2, a3, a4, 0, 0); -} - -static long syscall3(long number, long a1, long a2, long a3) { - return syscall6(number, a1, a2, a3, 0, 0, 0); -} - -static long syscall2(long number, long a1, long a2) { - return syscall6(number, a1, a2, 0, 0, 0, 0); -} - -static long syscall1(long number, long a1) { - return syscall6(number, a1, 0, 0, 0, 0, 0); -} - -static int strings_equal(const char *left, const char *right) { - while (*left && *left == *right) { - left++; - right++; - } - return *left == *right; -} - -static int parse_u64(const char *text, u64 *value) { - u64 parsed = 0; - if (!*text) { - return -1; - } - while (*text) { - if (*text < '0' || *text > '9') { - return -1; - } - parsed = parsed * 10 + (u64)(*text - '0'); - text++; - } - *value = parsed; - return 0; -} - -static int parse_ipv4(const char *text, unsigned int *address) { - unsigned int octets[4] = {0, 0, 0, 0}; - int index = 0; - int digits = 0; - while (*text) { - if (*text == '.') { - if (!digits || index == 3) { - return -1; - } - index++; - digits = 0; - } else if (*text >= '0' && *text <= '9') { - octets[index] = octets[index] * 10 + (unsigned int)(*text - '0'); - if (octets[index] > 255) { - return -1; - } - digits++; - } else { - return -1; - } - text++; - } - if (index != 3 || !digits) { - return -1; - } - *address = octets[0] | (octets[1] << 8) | (octets[2] << 16) | - (octets[3] << 24); - return 0; -} - -static unsigned short network_u16(unsigned short value) { - return (unsigned short)((value << 8) | (value >> 8)); -} - -static u64 monotonic_ns(void) { - struct timespec value; - if (syscall2(SYS_CLOCK_GETTIME, CLOCK_MONOTONIC, (long)&value) < 0) { - return 0; - } - return (u64)value.seconds * 1000000000UL + (u64)value.nanoseconds; -} - -static void append_text(const char *text) { - while (*text && output_length < sizeof(output)) { - output[output_length++] = *text++; - } -} - -static void append_u64(u64 value) { - char digits[24]; - int length = 0; - do { - digits[length++] = (char)('0' + value % 10); - value /= 10; - } while (value); - while (length && output_length < sizeof(output)) { - output[output_length++] = digits[--length]; - } -} - -static int write_all(int descriptor, const void *data, u64 length) { - const char *current = data; - while (length) { - long written = syscall3(SYS_WRITE, descriptor, (long)current, (long)length); - if (written == -4) { - continue; - } - if (written <= 0) { - return -1; - } - current += written; - length -= (u64)written; - } - return 0; -} - -static int emit_result( - const char *device, - const char *operation, - u64 operations, - u64 bytes_per_operation, - u64 elapsed_ns -) { - output_length = 0; - append_text("{\"device\":\""); - append_text(device); - append_text("\",\"operation\":\""); - append_text(operation); - append_text("\",\"operations\":"); - append_u64(operations); - append_text(",\"bytes_per_operation\":"); - append_u64(bytes_per_operation); - append_text(",\"elapsed_ns\":"); - append_u64(elapsed_ns); - append_text("}\n"); - return write_all(1, output, output_length); -} - -static int run_file(int argc, char **argv) { - u64 duration_ms; - u64 size_bytes; - int writing; - int direct; - int create; - int flags; - long descriptor; - u64 started; - u64 ended; - u64 operations = 0; - u64 random = 0x9e3779b97f4a7c15UL; - u64 block_count; - int index; - - if (argc != 8 || parse_u64(argv[4], &duration_ms) || - parse_u64(argv[5], &size_bytes) || duration_ms == 0 || - size_bytes < sizeof(io_buffer)) { - return 2; - } - writing = strings_equal(argv[3], "write"); - if (!writing && !strings_equal(argv[3], "read")) { - return 2; - } - direct = strings_equal(argv[6], "direct"); - create = strings_equal(argv[7], "create"); - if ((!direct && !strings_equal(argv[6], "buffered")) || - (!create && !strings_equal(argv[7], "existing"))) { - return 2; - } - flags = (writing ? O_RDWR : O_RDONLY) | O_CLOEXEC; - if (direct) { - flags |= O_DIRECT; - } - if (create) { - flags |= O_CREAT; - } - descriptor = syscall4(SYS_OPENAT, AT_FDCWD, (long)argv[2], flags, 0600); - if (descriptor < 0) { - return 3; - } - if (create && syscall2(SYS_FTRUNCATE, descriptor, (long)size_bytes) < 0) { - syscall1(SYS_CLOSE, descriptor); - return 3; - } - for (index = 0; index < (int)sizeof(io_buffer); index++) { - io_buffer[index] = (unsigned char)(index * 31 + 17); - } - block_count = size_bytes / sizeof(io_buffer); - started = monotonic_ns(); - if (!started) { - syscall1(SYS_CLOSE, descriptor); - return 3; - } - ended = started; - while (ended - started < duration_ms * 1000000UL) { - u64 offset; - long transferred; - random = random * 6364136223846793005UL + 1442695040888963407UL; - offset = (random % block_count) * sizeof(io_buffer); - transferred = writing - ? syscall4(SYS_PWRITE64, descriptor, (long)io_buffer, sizeof(io_buffer), offset) - : syscall4(SYS_PREAD64, descriptor, (long)io_buffer, sizeof(io_buffer), offset); - if (transferred != (long)sizeof(io_buffer)) { - syscall1(SYS_CLOSE, descriptor); - return 4; - } - operations++; - if ((operations & 63) == 0) { - ended = monotonic_ns(); - } - } - ended = monotonic_ns(); - if (writing) { - syscall1(SYS_FSYNC, descriptor); - } - syscall1(SYS_CLOSE, descriptor); - return emit_result("file", argv[3], operations, sizeof(io_buffer), ended - started); -} - -static int run_network(int argc, char **argv) { - u64 port; - u64 duration_ms; - unsigned int address; - struct sockaddr_in peer = {0}; - struct pollfd poll_descriptor; - long descriptor; - u64 started; - u64 ended; - u64 operations = 0; - int index; - - if (argc != 5 || parse_ipv4(argv[2], &address) || - parse_u64(argv[3], &port) || port == 0 || port > 65535 || - parse_u64(argv[4], &duration_ms) || duration_ms == 0) { - return 2; - } - descriptor = syscall3(SYS_SOCKET, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); - if (descriptor < 0) { - return 3; - } - peer.family = AF_INET; - peer.port = network_u16((unsigned short)port); - peer.address = address; - if (syscall3(SYS_CONNECT, descriptor, (long)&peer, sizeof(peer)) < 0) { - syscall1(SYS_CLOSE, descriptor); - return 3; - } - for (index = 0; index < 64; index++) { - io_buffer[index] = (unsigned char)(index + 1); - } - poll_descriptor.fd = (int)descriptor; - poll_descriptor.events = POLLIN; - started = monotonic_ns(); - if (!started) { - syscall1(SYS_CLOSE, descriptor); - return 3; - } - ended = started; - while (ended - started < duration_ms * 1000000UL) { - long received; - if (syscall3(SYS_WRITE, descriptor, (long)io_buffer, 64) != 64) { - syscall1(SYS_CLOSE, descriptor); - return 4; - } - poll_descriptor.returned_events = 0; - if (syscall3(SYS_POLL, (long)&poll_descriptor, 1, 100) > 0 && - (poll_descriptor.returned_events & POLLIN)) { - received = syscall3(SYS_READ, descriptor, (long)io_buffer, 64); - if (received == 64) { - operations++; - } - } - ended = monotonic_ns(); - } - syscall1(SYS_CLOSE, descriptor); - return emit_result("network", "roundtrip", operations, 64, ended - started); -} - -static int benchmark_main(int argc, char **argv) { - if (argc > 1 && strings_equal(argv[1], "file")) { - return run_file(argc, argv); - } - if (argc > 1 && strings_equal(argv[1], "network")) { - return run_network(argc, argv); - } - return 2; -} - -void nvx_device_io_start(long *stack) { - int status = benchmark_main((int)stack[0], (char **)&stack[1]); - syscall1(SYS_EXIT_GROUP, status); - for (;;) { - } -} - -__asm__( - ".global _start\n" - ".type _start,@function\n" - "_start:\n" - "mov %rsp,%rdi\n" - "andq $-16,%rsp\n" - "call nvx_device_io_start\n" - "ud2\n" -); \ No newline at end of file diff --git a/guest/common/nvx-exit.c b/guest/common/nvx-exit.c deleted file mode 100644 index 6f581ad8a..000000000 --- a/guest/common/nvx-exit.c +++ /dev/null @@ -1,46 +0,0 @@ -/* - * nvx-exit [CODE]: stop the VM with an 8-bit exit code, by writing one byte - * to OpenVMM's control port 0x604 through /dev/port. CODE defaults to 1, and - * any CODE that isn't a decimal number from 0 to 255 becomes 1. It prints - * nothing; if the write fails, it exits with status 1. - * - * This is a single static exec rather than a shell script, because a guest's - * first command after a restore pays a cold first touch for every fork, exec - * and exit, and the script ran sh, a command substitution and a printf | dd - * pipeline. - */ -#include -#include -#include - -/* Tests build against a regular file; the guest always writes /dev/port. */ -#ifndef NVX_EXIT_DEVICE -#define NVX_EXIT_DEVICE "/dev/port" -#endif -#define NVX_EXIT_PORT 0x604 - -static unsigned char exit_code(const char *argument) -{ - unsigned int value = 0; - - if (argument == NULL || *argument == '\0') - return 1; - for (; *argument != '\0'; argument++) { - if (*argument < '0' || *argument > '9') - return 1; - value = value * 10 + (unsigned int)(*argument - '0'); - if (value > 255) - return 1; - } - return (unsigned char)value; -} - -int main(int argc, char **argv) -{ - unsigned char code = exit_code(argc > 1 ? argv[1] : NULL); - int port = open(NVX_EXIT_DEVICE, O_WRONLY); - - if (port < 0) - return 1; - return pwrite(port, &code, 1, NVX_EXIT_PORT) == 1 ? 0 : 1; -} diff --git a/guest/common/nvx-ingress.c b/guest/common/nvx-ingress.c deleted file mode 100644 index 2b021a0e2..000000000 --- a/guest/common/nvx-ingress.c +++ /dev/null @@ -1,661 +0,0 @@ -// nvx-ingress announces the guest's TCP and UDP listeners to the gateway of -// the portable network profile, which publishes them to the host's private -// networks or, for host-loopback ingress alone, on host loopback. The init -// system starts it, as root, when the kernel command line carries -// virtnet_ingress=allow or virtnet_ingress=host-loopback. -// -// Every 100 ms it reads the kernel's socket tables. A TCP socket in the LISTEN -// state is a listener, and so is an unconnected UDP socket that persists for a -// second, which skips short-lived client sockets, such as those of DNS queries. -// Each UDP socket settles on its own, identified by the kernel's inode for it, -// and stays announced while it exists, even once a server connects it to its -// client; another socket that reuses a port, such as a client's, settles anew, -// and a port announces the families of its announced sockets. Sockets bound to -// loopback addresses, or to IPv6 link-local ones, are unreachable through the -// gateway. A TCP listener on the unspecified IPv6 address also takes IPv4 -// connections unless the kernel's socket diagnostics report it IPv6-only; a UDP -// socket there, which the guest kernel's diagnostics do not cover, follows -// net.ipv6.bindv6only. It sends the canonical set of listeners to UDP port 1021 -// of the IPv4 gateway from a privileged port, which only root can bind, -// whenever the set changes and at least every 2 seconds. -// -// An announcement is the magic "NVXL", version 1, a flags byte whose bit 0 -// marks omitted listeners, a big-endian count, and that many entries in -// increasing order of protocol and port: the IP protocol number, a family -// mask (1 for IPv4, 2 for IPv6), and the big-endian port. -// -// "nvx-ingress test DIRECTORY" runs the numbered snapshots in DIRECTORY, from -// 0, as successive scans instead, and prints each scan's time and -// announcement in hexadecimal. A snapshot holds the socket tables under net/, -// optionally sys/net/ipv6/bindv6only, optionally a socket diagnostics dump in -// diag, and the scan time in milliseconds in time. - -#define _GNU_SOURCE - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define ANNOUNCEMENT_PORT 1021 -#define FIRST_SOURCE_PORT 1023 -#define LAST_SOURCE_PORT 512 -#define MAX_ANNOUNCED 64 -#define MAX_TRACKED 1024 -#define SCAN_INTERVAL_MS 100 -#define REFRESH_INTERVAL_MS 2000 -#define UDP_SETTLE_MS 1000 -#define FAMILY_IPV4 1 -#define FAMILY_IPV6 2 -#define PROTOCOL_TCP 6 -#define PROTOCOL_UDP 17 -#define TCP_LISTEN 0x0a -#define UDP_CONNECTED 0x01 -#define UDP_UNCONNECTED 0x07 -#define HEADER_LENGTH 8 -#define ENTRY_LENGTH 4 - -// The kernel's netlink socket diagnostics ABI (linux/netlink.h, -// linux/sock_diag.h, and linux/inet_diag.h), whose headers musl lacks. -#define NETLINK_SOCK_DIAG_PROTOCOL 4 -#define NETLINK_REQUEST 0x01 -#define NETLINK_DUMP 0x300 -#define NETLINK_ERROR 2 -#define NETLINK_DONE 3 -#define SOCK_DIAG_BY_FAMILY 20 -#define INET_DIAG_SKV6ONLY 11 - -struct netlink_header { - uint32_t length; - uint16_t type; - uint16_t flags; - uint32_t sequence; - uint32_t pid; -}; - -struct netlink_address { - sa_family_t family; - unsigned short pad; - uint32_t pid; - uint32_t groups; -}; - -struct netlink_attribute { - uint16_t length; - uint16_t type; -}; - -struct inet_diag_socket_id { - uint16_t source_port; - uint16_t destination_port; - uint32_t source[4]; - uint32_t destination[4]; - uint32_t interface; - uint32_t cookie[2]; -}; - -struct inet_diag_request { - uint8_t family; - uint8_t protocol; - uint8_t extensions; - uint8_t pad; - uint32_t states; - struct inet_diag_socket_id id; -}; - -struct inet_diag_message { - uint8_t family; - uint8_t state; - uint8_t timer; - uint8_t retransmits; - struct inet_diag_socket_id id; - uint32_t expires; - uint32_t receive_queue; - uint32_t write_queue; - uint32_t uid; - uint32_t inode; -}; - -// Whether the IPv6 TCP listeners on the unspecified address at one port take -// IPv4 connections, as the kernel's socket diagnostics report them. -struct wildcard_listener { - uint16_t port; - uint8_t dual_stack; - uint8_t ipv6_only; -}; - -struct listener { - uint8_t protocol; - uint8_t families; - uint16_t port; -}; - -struct table { - struct listener entries[MAX_TRACKED]; - size_t count; -}; - -// A reachable UDP socket, which the kernel's inode identifies while it exists. -struct udp_socket { - uint16_t port; - uint8_t families; - uint8_t connected; - unsigned long inode; -}; - -// A UDP socket that is settling or, once it has persisted unconnected for -// UDP_SETTLE_MS, announced. -struct udp_state { - uint16_t port; - uint8_t families; - uint8_t announced; - unsigned long inode; - uint64_t first_seen_ms; -}; - -static struct table found; -static struct udp_socket udp_sockets[MAX_TRACKED]; -static size_t udp_socket_count; -static struct udp_state udp_states[MAX_TRACKED]; -static size_t udp_state_count; -static struct udp_state next_udp_states[MAX_TRACKED]; -static uint8_t payload[HEADER_LENGTH + MAX_ANNOUNCED * ENTRY_LENGTH]; -static uint8_t last_payload[sizeof(payload)]; -static size_t last_length; -static int ipv6_dual_stack = 1; -static struct wildcard_listener wildcard_tcp[MAX_TRACKED]; -static size_t wildcard_tcp_count; -static uint8_t diag_buffer[32768] __attribute__((aligned(8))); -// The root of the socket tables and sysctls, which test mode replaces. -static const char *proc_root = "/proc"; -static int test_mode; - -static uint64_t now_ms(void) -{ - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - return (uint64_t)now.tv_sec * 1000 + (uint64_t)now.tv_nsec / 1000000; -} - -static void sleep_ms(unsigned int milliseconds) -{ - struct timespec delay = { - .tv_sec = milliseconds / 1000, - .tv_nsec = (long)(milliseconds % 1000) * 1000000, - }; - while (nanosleep(&delay, &delay) != 0 && errno == EINTR) { - } -} - -// Records a listener, merging the address families of one protocol and port. -static void add(struct table *table, uint8_t protocol, uint16_t port, uint8_t families) -{ - for (size_t index = 0; index < table->count; ++index) { - struct listener *entry = &table->entries[index]; - if (entry->protocol == protocol && entry->port == port) { - entry->families |= families; - return; - } - } - if (table->count < MAX_TRACKED) { - table->entries[table->count++] = (struct listener){ - .protocol = protocol, - .families = families, - .port = port, - }; - } -} - -// Returns the tracked state of the UDP socket with `inode` at `port`, if any. -static const struct udp_state *find_udp_state(uint16_t port, unsigned long inode) -{ - for (size_t index = 0; index < udp_state_count; ++index) { - if (udp_states[index].port == port && udp_states[index].inode == inode) { - return &udp_states[index]; - } - } - return NULL; -} - -static int parse_hex_u32(const char *text, uint32_t *value) -{ - char digits[9]; - char *end; - memcpy(digits, text, 8); - digits[8] = '\0'; - errno = 0; - unsigned long parsed = strtoul(digits, &end, 16); - if (errno != 0 || *end != '\0' || end != digits + 8) { - return 0; - } - *value = (uint32_t)parsed; - return 1; -} - -// Records the IPV6_V6ONLY state of an IPv6 TCP listener on the unspecified -// address from one socket diagnostics message. -static void note_wildcard_tcp(const uint8_t *message, size_t length) -{ - struct inet_diag_message diag; - if (length < sizeof(diag)) { - return; - } - memcpy(&diag, message, sizeof(diag)); - if (diag.family != AF_INET6 || diag.state != TCP_LISTEN) { - return; - } - for (int word = 0; word < 4; ++word) { - if (diag.id.source[word] != 0) { - return; - } - } - uint16_t port = ntohs(diag.id.source_port); - for (size_t offset = sizeof(diag); offset + sizeof(struct netlink_attribute) <= length;) { - struct netlink_attribute attribute; - memcpy(&attribute, message + offset, sizeof(attribute)); - if (attribute.length < sizeof(attribute) || attribute.length > length - offset) { - return; - } - if (attribute.type == INET_DIAG_SKV6ONLY && attribute.length > sizeof(attribute)) { - uint8_t ipv6_only = message[offset + sizeof(attribute)] != 0; - struct wildcard_listener *entry = NULL; - for (size_t index = 0; index < wildcard_tcp_count; ++index) { - if (wildcard_tcp[index].port == port) { - entry = &wildcard_tcp[index]; - } - } - if (entry == NULL && wildcard_tcp_count < MAX_TRACKED) { - entry = &wildcard_tcp[wildcard_tcp_count++]; - *entry = (struct wildcard_listener){.port = port}; - } - if (entry != NULL) { - entry->dual_stack |= (uint8_t)!ipv6_only; - entry->ipv6_only |= ipv6_only; - } - return; - } - offset += (attribute.length + 3u) & ~3u; - } -} - -// Records the IPv6 TCP listeners on the unspecified address of one part of a -// socket diagnostics dump, returning 0 once the dump ends or is malformed. -static int parse_wildcard_tcp(const uint8_t *buffer, size_t length) -{ - for (size_t offset = 0; offset + sizeof(struct netlink_header) <= length;) { - struct netlink_header header; - memcpy(&header, buffer + offset, sizeof(header)); - if (header.length < sizeof(header) || header.length > length - offset || - header.type == NETLINK_DONE || header.type == NETLINK_ERROR) { - return 0; - } - if (header.type == SOCK_DIAG_BY_FAMILY) { - note_wildcard_tcp(buffer + offset + sizeof(header), header.length - sizeof(header)); - } - offset += (header.length + 3u) & ~3u; - } - return 1; -} - -// Asks the kernel's socket diagnostics, or in test mode the snapshot's dump, -// which IPv6 TCP listeners on the unspecified address are IPv6-only. Without -// an answer, none is known. -static void query_wildcard_tcp(void) -{ - wildcard_tcp_count = 0; - if (test_mode) { - char path[4096]; - if (snprintf(path, sizeof(path), "%s/diag", proc_root) >= (int)sizeof(path)) { - return; - } - FILE *file = fopen(path, "rbe"); - if (file != NULL) { - size_t length = fread(diag_buffer, 1, sizeof(diag_buffer), file); - fclose(file); - parse_wildcard_tcp(diag_buffer, length); - } - return; - } - int fd = socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_SOCK_DIAG_PROTOCOL); - if (fd < 0) { - return; - } - // A dump that never ends must not stall the scans. - struct timeval timeout = {.tv_usec = SCAN_INTERVAL_MS * 1000}; - struct { - struct netlink_header header; - struct inet_diag_request request; - } query = { - .header = - { - .length = sizeof(query), - .type = SOCK_DIAG_BY_FAMILY, - .flags = NETLINK_REQUEST | NETLINK_DUMP, - }, - .request = - { - .family = AF_INET6, - .protocol = IPPROTO_TCP, - .states = 1u << TCP_LISTEN, - }, - }; - struct netlink_address kernel = {.family = AF_NETLINK}; - if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0 || - sendto(fd, &query, sizeof(query), 0, (struct sockaddr *)&kernel, sizeof(kernel)) != - (ssize_t)sizeof(query)) { - close(fd); - return; - } - for (;;) { - ssize_t received = recv(fd, diag_buffer, sizeof(diag_buffer), 0); - if (received <= 0 || !parse_wildcard_tcp(diag_buffer, (size_t)received)) { - break; - } - } - close(fd); -} - -// Returns the families through which an IPv6 socket of `protocol` on the -// unspecified address at `port` is reachable. -static uint8_t wildcard_families(uint8_t protocol, uint16_t port) -{ - if (protocol == PROTOCOL_TCP) { - for (size_t index = 0; index < wildcard_tcp_count; ++index) { - const struct wildcard_listener *entry = &wildcard_tcp[index]; - if (entry->port == port && (entry->dual_stack || entry->ipv6_only)) { - return entry->dual_stack ? FAMILY_IPV4 | FAMILY_IPV6 : FAMILY_IPV6; - } - } - } - return ipv6_dual_stack ? FAMILY_IPV4 | FAMILY_IPV6 : FAMILY_IPV6; -} - -// Returns the families through which a socket of `protocol` at `port`, bound -// to the kernel's hexadecimal rendering of an address, is reachable from the -// gateway, or 0. The kernel prints each 32-bit word of the address as it lies -// in memory. -static uint8_t reachable_families(const char *address, int ipv6, uint8_t protocol, uint16_t port) -{ - if (!ipv6) { - struct in_addr ipv4; - if (strlen(address) != 8 || !parse_hex_u32(address, &ipv4.s_addr)) { - return 0; - } - return (ntohl(ipv4.s_addr) >> 24) == 127 ? 0 : FAMILY_IPV4; - } - struct in6_addr ipv6_address; - if (strlen(address) != 32) { - return 0; - } - for (int word = 0; word < 4; ++word) { - uint32_t value; - if (!parse_hex_u32(address + word * 8, &value)) { - return 0; - } - memcpy(&ipv6_address.s6_addr[word * 4], &value, sizeof(value)); - } - if (IN6_IS_ADDR_UNSPECIFIED(&ipv6_address)) { - return wildcard_families(protocol, port); - } - if (IN6_IS_ADDR_LOOPBACK(&ipv6_address) || IN6_IS_ADDR_LINKLOCAL(&ipv6_address)) { - return 0; - } - if (IN6_IS_ADDR_V4MAPPED(&ipv6_address)) { - return ipv6_address.s6_addr[12] == 127 ? 0 : FAMILY_IPV4; - } - return FAMILY_IPV6; -} - -// Adds the TCP listeners of one of the kernel's socket tables, `name` under -// net/, to `found`, and its reachable UDP sockets, connected or not, to -// `udp_sockets`. -static void scan(const char *name, uint8_t protocol, int ipv6, uint16_t own_port) -{ - char line[512]; - char path[4096]; - if (snprintf(path, sizeof(path), "%s/net/%s", proc_root, name) >= (int)sizeof(path)) { - return; - } - FILE *file = fopen(path, "re"); - if (file == NULL) { - return; - } - if (fgets(line, sizeof(line), file) == NULL) { - fclose(file); - return; - } - while (fgets(line, sizeof(line), file) != NULL) { - char local[64]; - char remote[64]; - unsigned int state; - unsigned long inode; - if (sscanf(line, " %*u: %63s %63s %x %*s %*s %*s %*u %*u %lu", local, remote, &state, - &inode) != 4) { - continue; - } - char *local_port = strrchr(local, ':'); - char *remote_port = strrchr(remote, ':'); - if (local_port == NULL || remote_port == NULL) { - continue; - } - *local_port++ = '\0'; - *remote_port++ = '\0'; - unsigned long port = strtoul(local_port, NULL, 16); - if (port == 0 || port > UINT16_MAX) { - continue; - } - uint8_t connected = 0; - if (protocol == PROTOCOL_TCP) { - if (state != TCP_LISTEN) { - continue; - } - } else if (state == UDP_CONNECTED) { - connected = 1; - } else if (state != UDP_UNCONNECTED || strtoul(remote_port, NULL, 16) != 0 || - strspn(remote, "0") != strlen(remote) || port == own_port) { - continue; - } - uint8_t families = reachable_families(local, ipv6, protocol, (uint16_t)port); - if (families == 0) { - continue; - } - if (protocol == PROTOCOL_TCP) { - add(&found, protocol, (uint16_t)port, families); - } else if (udp_socket_count < MAX_TRACKED) { - udp_sockets[udp_socket_count++] = (struct udp_socket){ - .port = (uint16_t)port, - .families = families, - .connected = connected, - .inode = inode, - }; - } - } - fclose(file); -} - -static int compare_listeners(const void *left, const void *right) -{ - const struct listener *a = left; - const struct listener *b = right; - if (a->protocol != b->protocol) { - return a->protocol < b->protocol ? -1 : 1; - } - return a->port < b->port ? -1 : a->port > b->port; -} - -// Scans the socket tables and encodes the announcement into `payload`, -// returning its length. -static size_t collect(uint16_t own_port, uint64_t now) -{ - found.count = 0; - udp_socket_count = 0; - query_wildcard_tcp(); - scan("tcp", PROTOCOL_TCP, 0, own_port); - scan("tcp6", PROTOCOL_TCP, 1, own_port); - scan("udp", PROTOCOL_UDP, 0, own_port); - scan("udp6", PROTOCOL_UDP, 1, own_port); - - // An unconnected UDP socket is announced once it persists for - // UDP_SETTLE_MS, and stays announced, even once a server connects it to a - // client, while it exists. Each port announces the families of its - // announced sockets. - size_t next_count = 0; - for (size_t index = 0; index < udp_socket_count; ++index) { - const struct udp_socket *socket = &udp_sockets[index]; - const struct udp_state *previous = find_udp_state(socket->port, socket->inode); - struct udp_state next = { - .port = socket->port, - .families = socket->families, - .inode = socket->inode, - .first_seen_ms = previous != NULL ? previous->first_seen_ms : now, - }; - if (previous != NULL && previous->announced) { - next.announced = 1; - } else if (socket->connected) { - continue; - } else { - next.announced = now - next.first_seen_ms >= UDP_SETTLE_MS; - } - next_udp_states[next_count++] = next; - if (next.announced) { - add(&found, PROTOCOL_UDP, next.port, next.families); - } - } - memcpy(udp_states, next_udp_states, next_count * sizeof(udp_states[0])); - udp_state_count = next_count; - size_t announced = found.count; - - qsort(found.entries, announced, sizeof(found.entries[0]), compare_listeners); - size_t count = announced < MAX_ANNOUNCED ? announced : MAX_ANNOUNCED; - memcpy(payload, "NVXL", 4); - payload[4] = 1; - payload[5] = announced > MAX_ANNOUNCED; - payload[6] = (uint8_t)(count >> 8); - payload[7] = (uint8_t)count; - for (size_t index = 0; index < count; ++index) { - uint8_t *entry = &payload[HEADER_LENGTH + index * ENTRY_LENGTH]; - entry[0] = found.entries[index].protocol; - entry[1] = found.entries[index].families; - entry[2] = (uint8_t)(found.entries[index].port >> 8); - entry[3] = (uint8_t)found.entries[index].port; - } - return HEADER_LENGTH + count * ENTRY_LENGTH; -} - -static void read_bindv6only(void) -{ - char path[4096]; - if (snprintf(path, sizeof(path), "%s/sys/net/ipv6/bindv6only", proc_root) >= - (int)sizeof(path)) { - return; - } - FILE *file = fopen(path, "re"); - if (file == NULL) { - return; - } - int value; - if (fscanf(file, "%d", &value) == 1) { - ipv6_dual_stack = value == 0; - } - fclose(file); -} - -// Runs the snapshots in `directory` as successive scans; see the top of the -// file. -static int run_test(const char *directory) -{ - static char root[4096]; - test_mode = 1; - for (unsigned int index = 0;; ++index) { - char path[4200]; - if (snprintf(root, sizeof(root), "%s/%u", directory, index) >= (int)sizeof(root) || - snprintf(path, sizeof(path), "%s/time", root) >= (int)sizeof(path)) { - return 1; - } - FILE *file = fopen(path, "re"); - if (file == NULL) { - return index == 0; - } - unsigned long long now; - int parsed = fscanf(file, "%llu", &now); - fclose(file); - if (parsed != 1) { - return 1; - } - proc_root = root; - ipv6_dual_stack = 1; - read_bindv6only(); - size_t length = collect(FIRST_SOURCE_PORT, now); - printf("%llu ", now); - for (size_t offset = 0; offset < length; ++offset) { - printf("%02x", payload[offset]); - } - printf("\n"); - } -} - -int main(int argc, char **argv) -{ - if (argc == 3 && strcmp(argv[1], "test") == 0) { - return run_test(argv[2]); - } - struct sockaddr_in gateway = { - .sin_family = AF_INET, - .sin_port = htons(ANNOUNCEMENT_PORT), - }; - if (argc != 2 || inet_pton(AF_INET, argv[1], &gateway.sin_addr) != 1) { - fprintf(stderr, "usage: nvx-ingress GATEWAY_IPV4\n"); - return 2; - } - signal(SIGINT, SIG_IGN); - signal(SIGHUP, SIG_IGN); - read_bindv6only(); - - int sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); - if (sock < 0) { - fprintf(stderr, "nvx-ingress: socket: %s\n", strerror(errno)); - return 1; - } - uint16_t own_port = 0; - for (int port = FIRST_SOURCE_PORT; port >= LAST_SOURCE_PORT && own_port == 0; --port) { - struct sockaddr_in source = { - .sin_family = AF_INET, - .sin_port = htons((uint16_t)port), - .sin_addr.s_addr = htonl(INADDR_ANY), - }; - if (bind(sock, (struct sockaddr *)&source, sizeof(source)) == 0) { - own_port = (uint16_t)port; - } else if (errno != EADDRINUSE) { - fprintf(stderr, "nvx-ingress: bind privileged port: %s\n", strerror(errno)); - return 1; - } - } - if (own_port == 0) { - fprintf(stderr, "nvx-ingress: no privileged source port is free\n"); - return 1; - } - - uint64_t last_sent = 0; - for (;;) { - uint64_t now = now_ms(); - size_t length = collect(own_port, now); - if (length != last_length || memcmp(payload, last_payload, length) != 0 || - now - last_sent >= REFRESH_INTERVAL_MS) { - if (sendto(sock, payload, length, 0, (struct sockaddr *)&gateway, - sizeof(gateway)) == (ssize_t)length) { - memcpy(last_payload, payload, length); - last_length = length; - last_sent = now; - } - } - sleep_ms(SCAN_INTERVAL_MS); - } -} diff --git a/guest/common/nvx-init-agent b/guest/common/nvx-init-agent index 56f4a8527..b2bf2a04f 100644 --- a/guest/common/nvx-init-agent +++ b/guest/common/nvx-init-agent @@ -161,8 +161,9 @@ validate_share_target() { esac # The container runtime mounts or binds these paths after the share, so a # share there would be shadowed or would expose host data to runtime writes. + # It also creates /run and /run/nvx, and binds its time state over /run/nvx/time. case "$1" in - /proc | /proc/* | /sys | /sys/* | /dev | /dev/* | /.nvx-agent | /.nvx-agent/* | /etc | /etc/machine-id | /etc/machine-id/*) + /proc | /proc/* | /sys | /sys/* | /dev | /dev/* | /.nvx-agent | /.nvx-agent/* | /etc | /etc/machine-id | /etc/machine-id/* | /run | /run/nvx | /run/nvx/time | /run/nvx/time/*) fatal "live share target is reserved: $1" ;; esac @@ -396,13 +397,11 @@ teardown() { echo "NVX-SANDBOX-ERROR: failed to unmount the container overlay" >&2 status=1 fi - for role in custom runtime distro; do - directory=$layers/$role - if mountpoint -q "$directory" && ! umount "$directory"; then - echo "NVX-SANDBOX-ERROR: failed to unmount the $role layer" >&2 - status=1 - fi - done + directory=$layers/distro + if mountpoint -q "$directory" && ! umount "$directory"; then + echo "NVX-SANDBOX-ERROR: failed to unmount the distro layer" >&2 + status=1 + fi if ! umount "$scratch"; then echo "NVX-SANDBOX-ERROR: failed to unmount scratch" >&2 status=1 @@ -423,8 +422,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 +443,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 "only the distro layer is supported" set +f mount_layer "$role" "$address" "$expected_uuid" "$layer_capacity" set -f @@ -467,16 +456,7 @@ done set +f [ "$layer_count" -gt 0 ] || fatal "at least one EROFS layer is required" -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 +lowerdirs=$layers/distro scratch_spec=$(cmdline_value nvx_scratch || true) [ -n "$scratch_spec" ] || fatal "a scratch device is required" diff --git a/guest/common/nvx-managed-agent.c b/guest/common/nvx-managed-agent.c deleted file mode 100644 index ac3b1e2b7..000000000 --- a/guest/common/nvx-managed-agent.c +++ /dev/null @@ -1,2431 +0,0 @@ -#define _GNU_SOURCE - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define OUTER_HEADER_LEN 44U -#define OUTER_MAX_PAYLOAD 65536U -#define OUTER_VERSION 1U -#define OUTER_GUEST_ATTACH 1U -#define OUTER_RESET 3U -#define OUTER_ACK 4U -#define OUTER_DATA 5U -#define OUTER_CREDIT 9U -#define OUTER_RECEIVE_WINDOW (1024U * 1024U) - -#define APP_HEADER_LEN 24U -#define APP_VERSION 1U -#define APP_PING 1U -#define APP_EXEC 2U -#define APP_STOP 3U -#define APP_CANCEL 4U -#define APP_FEATURES 5U -#define APP_MAPS 6U -#define APP_READY 0x81U -#define APP_STDOUT 0x82U -#define APP_STDERR 0x83U -#define APP_EXIT 0x84U -#define APP_STOPPED 0x85U -#define APP_ERROR 0xffU - -/* - * Control features that a host can rely on, advertised in the answer to - * FEATURES. The host refuses an image that lacks a feature it needs, so a - * behavior that a host depends on needs a bit here and in the host's copy - * (aci_edge_sandboxes, src/openvmm/protocol.rs). The init script of the image provides - * FEATURE_WORKLOAD_ACCOUNT; the agent speaks for it because both ship in one - * initramfs. An agent that predates FEATURES refuses the request as an - * unsupported operation, which the host reads as no features. Bit 1 announced - * bind mounts that nvx_map= kernel command-line tokens listed; it stays unused, - * so hosts that still send those tokens refuse this image. - */ -#define FEATURE_CANCEL (1U << 0) -#define FEATURE_WORKLOAD_ACCOUNT (1U << 2) -#define FEATURE_EXEC_CGROUP (1U << 3) -#define FEATURE_EXEC_ENVIRONMENT (1U << 4) -#define FEATURE_EXEC_CWD (1U << 5) -#define FEATURE_HOST_MAPPING_TABLE (1U << 6) - -#define MAX_ARGUMENTS 64U -#define MAX_ARGUMENT_LEN 4096U -#define MAX_ENVIRONMENT 256U -#define EXEC_EXTENDED 1U -#define EXEC_CWD_PRESENT 1U -#define EXEC_ENVIRONMENT_PRESENT 2U -#define EXEC_INHERIT_DEFAULT_ENV 4U -/* - * Set only in the configuration that a direct-mode child hands its launch - * helper, never on the wire: the child already entered the working directory, - * so the helper keeps it instead of looking the path up again. - */ -#define EXEC_CWD_ENTERED 0x8000U -#define MAX_OUTPUT_BYTES (1024U * 1024U) -#define OUTPUT_CHUNK_BYTES 32768U -#define CONTAINER_BARRIER_ATTEMPTS 500U -#define PORTB_CONSOLE 0xe9 -#define AGENT_STOPPED 1 -#ifndef CGROUP_ROOT -#define CGROUP_ROOT "/sys/fs/cgroup" -#endif -#define EXEC_CGROUP CGROUP_ROOT "/nvx-exec" -#ifndef HOSTFS_DIR -#define HOSTFS_DIR "/run/nvx/hostfs" -#endif -#define HOSTFS_ROOT HOSTFS_DIR "/root" -#define MAPS_TOKEN "nvx_maps=" -#define MAX_HOST_MAPPINGS 4096U -#define MAPS_HEADER_LEN 8U -#define MAP_ENTRY_HEADER_LEN 6U -#define MAP_READ_ONLY 1U -#define MAX_COMMAND_LINE 4096U -#define MAX_GUEST_PATH 4096U - -struct outer_record { - uint8_t type; - uint8_t instance_id[16]; - uint64_t epoch; - uint64_t sequence; - uint32_t payload_len; - uint8_t *payload; -}; - -struct control_session { - int fd; - uint8_t instance_id[16]; - uint64_t epoch; - uint64_t guest_sequence; - uint64_t host_sequence; -}; - -struct app_request { - uint8_t kind; - uint64_t request_id; - int32_t status; - uint32_t payload_len; - const uint8_t *payload; -}; - -struct agent_config { - const char *rootfs; - const char *hostname; - const char *uid; - const char *gid; - const char *user; - const char *home; - int direct; -}; - -struct exec_config { - char *cwd; - char **environment; - uint16_t environment_count; - int environment_present; - int inherit_default_env; -}; - -/* - * Host mappings that nvx_maps= announces and APP_MAPS requests deliver. Until - * every announced mapping is mounted, workloads are refused. - */ -struct host_mappings { - uint32_t expected; - uint32_t applied; - int failed; -}; - -#define LAUNCH_STAGE_CWD 1 -#define LAUNCH_STAGE_SETUP 2 - -/* What a direct-mode child reports when it fails before it executes setpriv. */ -struct launch_failure { - int32_t stage; - int32_t error; -}; - -static uint16_t read_u16(const uint8_t *bytes) -{ - return (uint16_t)bytes[0] | ((uint16_t)bytes[1] << 8); -} - -static uint32_t read_u32(const uint8_t *bytes) -{ - return (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) | - ((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24); -} - -static uint64_t read_u64(const uint8_t *bytes) -{ - uint64_t value = 0; - unsigned int index; - - for (index = 0; index < 8; ++index) { - value |= (uint64_t)bytes[index] << (index * 8); - } - return value; -} - -static void write_u16(uint8_t *bytes, uint16_t value) -{ - bytes[0] = (uint8_t)value; - bytes[1] = (uint8_t)(value >> 8); -} - -static void write_u32(uint8_t *bytes, uint32_t value) -{ - bytes[0] = (uint8_t)value; - bytes[1] = (uint8_t)(value >> 8); - bytes[2] = (uint8_t)(value >> 16); - bytes[3] = (uint8_t)(value >> 24); -} - -static void write_u64(uint8_t *bytes, uint64_t value) -{ - unsigned int index; - - for (index = 0; index < 8; ++index) { - bytes[index] = (uint8_t)(value >> (index * 8)); - } -} - -static int read_exact(int fd, void *buffer, size_t length) -{ - uint8_t *bytes = buffer; - size_t offset = 0; - - while (offset < length) { - ssize_t count = read(fd, bytes + offset, length - offset); - if (count > 0) { - offset += (size_t)count; - } else if (count == 0) { - return -1; - } else if (errno != EINTR) { - return -1; - } - } - return 0; -} - -static int write_all(int fd, const void *buffer, size_t length) -{ - const uint8_t *bytes = buffer; - size_t offset = 0; - - while (offset < length) { - ssize_t count = write(fd, bytes + offset, length - offset); - if (count > 0) { - offset += (size_t)count; - } else if (count < 0 && errno == EINTR) { - continue; - } else { - return -1; - } - } - return 0; -} - -static void portb_log(const char *message) -{ - int fd = open("/dev/port", O_WRONLY | O_CLOEXEC); - - if (fd < 0) { - return; - } - while (*message != '\0') { - if (pwrite(fd, message, 1, PORTB_CONSOLE) != 1) { - break; - } - ++message; - } - close(fd); -} - -static void portb_error(const char *stage, int status) -{ - char message[128]; - int length = snprintf( - message, - sizeof(message), - "NVX-MANAGED-ERROR: stage=%s status=%d\n", - stage, - status); - - if (length > 0 && (size_t)length < sizeof(message)) { - portb_log(message); - } -} - -static void free_outer_record(struct outer_record *record) -{ - free(record->payload); - record->payload = NULL; - record->payload_len = 0; -} - -static int read_outer_record(int fd, struct outer_record *record) -{ - uint8_t header[OUTER_HEADER_LEN]; - - memset(record, 0, sizeof(*record)); - if (read_exact(fd, header, sizeof(header)) != 0) { - return -1; - } - if (memcmp(header, "NVXS", 4) != 0 || read_u16(header + 4) != OUTER_VERSION || - header[7] != 0) { - return -1; - } - record->type = header[6]; - memcpy(record->instance_id, header + 8, sizeof(record->instance_id)); - record->epoch = read_u64(header + 24); - record->sequence = read_u64(header + 32); - record->payload_len = read_u32(header + 40); - if (record->payload_len > OUTER_MAX_PAYLOAD) { - return -1; - } - if (record->payload_len != 0) { - record->payload = malloc(record->payload_len); - if (record->payload == NULL || - read_exact(fd, record->payload, record->payload_len) != 0) { - free_outer_record(record); - return -1; - } - } - return 0; -} - -static int write_outer_record( - int fd, - uint8_t type, - const uint8_t instance_id[16], - uint64_t epoch, - uint64_t sequence, - const void *payload, - uint32_t payload_len) -{ - uint8_t header[OUTER_HEADER_LEN] = {0}; - - if (payload_len > OUTER_MAX_PAYLOAD) { - return -1; - } - memcpy(header, "NVXS", 4); - write_u16(header + 4, OUTER_VERSION); - header[6] = type; - memcpy(header + 8, instance_id, 16); - write_u64(header + 24, epoch); - write_u64(header + 32, sequence); - write_u32(header + 40, payload_len); - if (write_all(fd, header, sizeof(header)) != 0) { - return -1; - } - return payload_len == 0 || write_all(fd, payload, payload_len) == 0 ? 0 : -1; -} - -/* Accepts a /-separated path without empty, "." (unless alone), or ".." components. */ -static int safe_path(const char *path, int absolute) -{ - const char *component = path; - - if (absolute) { - if (path[0] != '/') { - return 0; - } - component = path + 1; - if (*component == '\0') { - return 0; - } - } else if (strcmp(path, ".") == 0) { - return 1; - } - while (*component != '\0') { - const char *end = strchr(component, '/'); - size_t length = end == NULL ? strlen(component) : (size_t)(end - component); - - if (length == 0 || (length == 1 && component[0] == '.') || - (length == 2 && component[0] == '.' && component[1] == '.')) { - return 0; - } - component += length; - if (*component == '/') { - ++component; - if (*component == '\0') { - return 0; - } - } - } - return 1; -} - -/* Creates every missing directory of `path` up to, and optionally including, its last component. */ -static int make_directories(const char *path, int include_last) -{ - char buffer[MAX_GUEST_PATH]; - size_t index; - size_t length = strlen(path); - - if (length >= sizeof(buffer)) { - return -1; - } - memcpy(buffer, path, length + 1); - for (index = 1; index <= length; ++index) { - if (buffer[index] == '/' || (buffer[index] == '\0' && include_last)) { - char saved = buffer[index]; - - buffer[index] = '\0'; - if (mkdir(buffer, 0755) != 0 && errno != EEXIST) { - return -1; - } - buffer[index] = saved; - } - } - return 0; -} - -/* - * Reads the number of host mappings that nvx_maps=COUNT announces into - * `count`, or 0 without the token. Tokens are split in place. Older hosts - * listed their mappings in nvx_map= tokens, which are ignored. - */ -static int parse_host_mapping_count(char *command_line, uint32_t *count) -{ - char *token; - char *cursor; - int found = 0; - - *count = 0; - for (token = strtok_r(command_line, " \t\n", &cursor); token != NULL; - token = strtok_r(NULL, " \t\n", &cursor)) { - const char *digit = token + strlen(MAPS_TOKEN); - uint32_t value = 0; - - if (strncmp(token, MAPS_TOKEN, strlen(MAPS_TOKEN)) != 0) { - continue; - } - if (found || *digit == '\0') { - errno = EINVAL; - return -1; - } - for (; *digit != '\0'; ++digit) { - if (*digit < '0' || *digit > '9' || value > MAX_HOST_MAPPINGS) { - errno = EINVAL; - return -1; - } - value = value * 10U + (uint32_t)(*digit - '0'); - } - if (value == 0 || value > MAX_HOST_MAPPINGS) { - errno = EINVAL; - return -1; - } - *count = value; - found = 1; - } - return 0; -} - -/* - * Reads the number of announced host mappings and, while the host's export is - * mounted at HOSTFS_ROOT, makes its parent a directory that only root may - * enter, so workloads reach mapped paths only through the bind mounts. The - * announced mappings need that export. - */ -static int prepare_host_mappings(struct host_mappings *mappings) -{ - char command_line[MAX_COMMAND_LINE]; - struct stat parent; - struct stat export; - ssize_t count; - int mounted; - int fd = open("/proc/cmdline", O_RDONLY | O_CLOEXEC); - - if (fd < 0) { - return -1; - } - count = read(fd, command_line, sizeof(command_line) - 1); - close(fd); - if (count < 0) { - return -1; - } - command_line[count] = '\0'; - if (parse_host_mapping_count(command_line, &mappings->expected) != 0) { - return -1; - } - mounted = stat(HOSTFS_DIR, &parent) == 0 && stat(HOSTFS_ROOT, &export) == 0 && - parent.st_dev != export.st_dev; - if (mounted && (chown(HOSTFS_DIR, 0, 0) != 0 || chmod(HOSTFS_DIR, 0700) != 0)) { - return -1; - } - if (mappings->expected != 0 && !mounted) { - errno = ENOENT; - return -1; - } - return 0; -} - -/* - * Bind-mounts SOURCE, a path relative to the export, at the guest path TARGET, - * read-only or read-write, without set-user-ID programs or device nodes. - */ -static int apply_host_mapping(const char *source, const char *target, int read_only) -{ - char host[MAX_GUEST_PATH + sizeof(HOSTFS_ROOT) + 1]; - struct stat status; - unsigned long flags = MS_BIND | MS_REMOUNT | MS_NOSUID | MS_NODEV; - - if (read_only) { - flags |= MS_RDONLY; - } - snprintf(host, sizeof(host), "%s/%s", HOSTFS_ROOT, source); - if (lstat(host, &status) != 0) { - return -1; - } - if (S_ISDIR(status.st_mode)) { - if (make_directories(target, 1) != 0) { - return -1; - } - } else if (S_ISREG(status.st_mode)) { - struct stat existing; - - if (make_directories(target, 0) != 0) { - return -1; - } - /* - * An existing target may lie inside an earlier read-only bind, where - * even O_CREAT with write access fails, so only a missing one is made. - */ - if (lstat(target, &existing) != 0) { - int file; - - if (errno != ENOENT) { - return -1; - } - file = open(target, O_RDONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0644); - if (file < 0) { - return -1; - } - close(file); - } else if (!S_ISREG(existing.st_mode)) { - errno = EINVAL; - return -1; - } - } else { - errno = EINVAL; - return -1; - } - if (mount(host, target, NULL, MS_BIND, NULL) != 0 || - mount(NULL, target, NULL, flags, NULL) != 0) { - return -1; - } - return 0; -} - -/* - * Decodes the APP_MAPS entry at `*offset` and advances past it: 16-bit flags, - * source length, and target length, then the source, a relative path of the - * export, and the target, an absolute guest path. - */ -static int decode_host_mapping( - const uint8_t *payload, - uint32_t payload_len, - uint32_t *offset, - char *source, - char *target, - int *read_only) -{ - uint16_t flags; - uint16_t source_len; - uint16_t target_len; - - if (payload_len - *offset < MAP_ENTRY_HEADER_LEN) { - return -1; - } - flags = read_u16(payload + *offset); - source_len = read_u16(payload + *offset + 2); - target_len = read_u16(payload + *offset + 4); - *offset += MAP_ENTRY_HEADER_LEN; - if ((flags & ~MAP_READ_ONLY) != 0 || source_len == 0 || target_len == 0 || - source_len >= MAX_GUEST_PATH || target_len >= MAX_GUEST_PATH || - payload_len - *offset < (uint32_t)source_len + target_len) { - return -1; - } - memcpy(source, payload + *offset, source_len); - source[source_len] = '\0'; - *offset += source_len; - memcpy(target, payload + *offset, target_len); - target[target_len] = '\0'; - *offset += target_len; - if (strlen(source) != source_len || strlen(target) != target_len || - strcmp(source, ".") == 0 || !safe_path(source, 0) || !safe_path(target, 1)) { - return -1; - } - *read_only = (flags & MAP_READ_ONLY) != 0; - return 0; -} - -static int send_guest_attach(int fd) -{ - const uint8_t zero_instance[16] = {0}; - - return write_outer_record( - fd, OUTER_GUEST_ATTACH, zero_instance, 0, 0, NULL, 0); -} - -static int acknowledge_reset( - struct control_session *session, - const struct outer_record *record) -{ - uint8_t credit[4]; - - if (record->type != OUTER_RESET || record->payload_len != 0 || - record->epoch == 0) { - return -1; - } - memcpy(session->instance_id, record->instance_id, 16); - session->epoch = record->epoch; - session->guest_sequence = 0; - session->host_sequence = record->sequence + 1; - write_u32(credit, OUTER_RECEIVE_WINDOW); - if (write_outer_record( - session->fd, - OUTER_ACK, - session->instance_id, - session->epoch, - session->guest_sequence, - credit, - sizeof(credit)) != 0) { - return -1; - } - session->guest_sequence = 1; - return 0; -} - -static int send_guest_record( - struct control_session *session, - uint8_t type, - const void *payload, - uint32_t payload_len) -{ - if (write_outer_record( - session->fd, - type, - session->instance_id, - session->epoch, - session->guest_sequence, - payload, - payload_len) != 0) { - return -1; - } - ++session->guest_sequence; - return 0; -} - -static int send_credit(struct control_session *session, uint32_t bytes) -{ - uint8_t payload[4]; - - if (bytes == 0) { - return 0; - } - write_u32(payload, bytes); - return send_guest_record(session, OUTER_CREDIT, payload, sizeof(payload)); -} - -static int send_app_frame( - struct control_session *session, - uint8_t kind, - uint64_t request_id, - int32_t status, - const void *payload, - uint32_t payload_len) -{ - uint8_t *frame; - uint32_t frame_len; - int result; - - if (payload_len > OUTER_MAX_PAYLOAD - APP_HEADER_LEN) { - return -1; - } - frame_len = APP_HEADER_LEN + payload_len; - frame = malloc(frame_len); - if (frame == NULL) { - return -1; - } - memset(frame, 0, APP_HEADER_LEN); - memcpy(frame, "NVXC", 4); - frame[4] = APP_VERSION; - frame[5] = kind; - write_u64(frame + 8, request_id); - write_u32(frame + 16, (uint32_t)status); - write_u32(frame + 20, payload_len); - if (payload_len != 0) { - memcpy(frame + APP_HEADER_LEN, payload, payload_len); - } - result = send_guest_record(session, OUTER_DATA, frame, frame_len); - free(frame); - return result; -} - -static int parse_app_request( - const uint8_t *payload, - uint32_t payload_len, - struct app_request *request) -{ - uint32_t declared_len; - - if (payload_len < APP_HEADER_LEN || memcmp(payload, "NVXC", 4) != 0 || - payload[4] != APP_VERSION || read_u16(payload + 6) != 0) { - return -1; - } - declared_len = read_u32(payload + 20); - if (declared_len != payload_len - APP_HEADER_LEN) { - return -1; - } - request->kind = payload[5]; - request->request_id = read_u64(payload + 8); - request->status = (int32_t)read_u32(payload + 16); - request->payload_len = declared_len; - request->payload = payload + APP_HEADER_LEN; - return request->request_id != 0 && request->status == 0 ? 0 : -1; -} - -static int send_app_error( - struct control_session *session, - uint64_t request_id, - int32_t status, - const char *category) -{ - return send_app_frame( - session, - APP_ERROR, - request_id, - status, - category, - (uint32_t)strlen(category)); -} - -static uint64_t monotonic_milliseconds(void) -{ - struct timespec value; - - if (clock_gettime(CLOCK_MONOTONIC, &value) != 0) { - return 0; - } - return (uint64_t)value.tv_sec * 1000U + (uint64_t)value.tv_nsec / 1000000U; -} - -static int open_control_tty(const char *path) -{ - const struct timespec delay = { - .tv_sec = 0, - .tv_nsec = 50U * 1000U * 1000U, - }; - unsigned int attempt; - - for (attempt = 0; attempt < 600; ++attempt) { - int fd = open(path, O_RDWR | O_CLOEXEC); - if (fd >= 0) { - return fd; - } - if (errno != ENOENT && errno != ENXIO && errno != ENODEV && errno != EIO) { - return -1; - } - nanosleep(&delay, NULL); - } - errno = ETIMEDOUT; - return -1; -} - -static int configure_control_tty(int fd) -{ - struct termios settings; - - if (tcgetattr(fd, &settings) != 0) { - return -1; - } - cfmakeraw(&settings); - settings.c_cflag |= CLOCAL; - return tcsetattr(fd, TCSANOW, &settings); -} - -static int make_nonblocking(int fd) -{ - int flags = fcntl(fd, F_GETFL); - - return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0 ? 0 : -1; -} - -static int write_pid_to_cgroup(pid_t pid) -{ - char buffer[32]; - int fd; - int length; - - fd = open("/sys/fs/cgroup/container/cgroup.procs", O_WRONLY | O_CLOEXEC); - if (fd < 0) { - return -1; - } - length = snprintf(buffer, sizeof(buffer), "%ld\n", (long)pid); - if (length <= 0 || (size_t)length >= sizeof(buffer) || - write_all(fd, buffer, (size_t)length) != 0) { - close(fd); - return -1; - } - return close(fd); -} - -static int write_container_barrier(int fd) -{ - struct sigaction ignored = {0}; - struct sigaction previous; - int result; - int status; - - ignored.sa_handler = SIG_IGN; - if (sigemptyset(&ignored.sa_mask) != 0 || - sigaction(SIGPIPE, &ignored, &previous) != 0) { - return -1; - } - result = write_all(fd, "start\n", 6); - status = errno; - if (sigaction(SIGPIPE, &previous, NULL) != 0) { - return -1; - } - if (result != 0) { - errno = status; - } - return result; -} - -/* Returns 0 after writing, 1 for an unreaped child exit, or -1 on failure. */ -static int release_container_barrier(const char *path, pid_t child) -{ - unsigned int attempt; - - for (attempt = 0; attempt < CONTAINER_BARRIER_ATTEMPTS; ++attempt) { - int fd = open(path, O_WRONLY | O_CLOEXEC | O_NONBLOCK); - - if (fd >= 0) { - int result = write_container_barrier(fd); - - close(fd); - return result; - } - if (errno != ENXIO && errno != EINTR) { - return -1; - } - if (child > 0) { - siginfo_t information = {0}; - - if (waitid( - P_PID, - (id_t)child, - &information, - WEXITED | WNOHANG | WNOWAIT) != 0) { - if (errno == EINTR) { - continue; - } - return -1; - } - if (information.si_pid == child) { - return 1; - } - } - { - const struct timespec delay = {.tv_nsec = 10000000L}; - - nanosleep(&delay, NULL); - } - } - errno = ETIMEDOUT; - return -1; -} - -static int enable_pids_controller(void) -{ - int fd = open(CGROUP_ROOT "/cgroup.subtree_control", O_WRONLY | O_CLOEXEC); - int result; - int status; - - if (fd < 0) { - return -1; - } - result = write_all(fd, "+pids", 5); - status = errno; - if (close(fd) != 0 && result == 0) { - return -1; - } - errno = status; - return result; -} - -/* - * Direct-mode workloads run in the guest's own root file system, contained by - * a cgroup that every process of the workload inherits, whatever session or - * process group it moves to. Killing the cgroup ends all of them, and its pids - * controller counts each of them until PID 1 reaps it. - */ -static int prepare_exec_cgroup(void) -{ - struct stat status; - - if (stat(CGROUP_ROOT "/cgroup.procs", &status) != 0) { - if (mkdir(CGROUP_ROOT, 0755) != 0 && errno != EEXIST) { - return -1; - } - if (mount("cgroup2", CGROUP_ROOT, "cgroup2", MS_NOSUID | MS_NODEV | MS_NOEXEC, NULL) != - 0) { - return -1; - } - } - if (mkdir(EXEC_CGROUP, 0755) != 0 && errno != EEXIST) { - return -1; - } - if (stat(EXEC_CGROUP "/pids.current", &status) == 0) { - return 0; - } - return errno == ENOENT ? enable_pids_controller() : -1; -} - -static int join_exec_cgroup(void) -{ - char buffer[32]; - int fd = open(EXEC_CGROUP "/cgroup.procs", O_WRONLY | O_CLOEXEC); - int length; - int result; - - if (fd < 0) { - return -1; - } - length = snprintf(buffer, sizeof(buffer), "%ld\n", (long)getpid()); - result = length > 0 && (size_t)length < sizeof(buffer) - ? write_all(fd, buffer, (size_t)length) - : -1; - close(fd); - return result; -} - -static int kill_exec_cgroup(void) -{ - int fd = open(EXEC_CGROUP "/cgroup.kill", O_WRONLY | O_CLOEXEC); - int result; - int status; - - if (fd < 0) { - goto error; - } - result = write_all(fd, "1", 1); - status = errno; - if (close(fd) != 0 && result == 0) { - goto error; - } - if (result == 0) { - return 0; - } - errno = status; -error: - status = errno; - portb_error("exec-cgroup-kill", status); - errno = status; - return -1; -} - -/* - * Returns 1 if the pids controller still charges a task to the exec cgroup, - * 0 if it charges none, or -1. A task stays charged until it is reaped, while - * cgroup.events stops counting a killed process as populated before the - * process even becomes a zombie. - */ -static int exec_cgroup_has_tasks(void) -{ - char buffer[32]; - size_t length = 0; - size_t index; - int fd = open(EXEC_CGROUP "/pids.current", O_RDONLY | O_CLOEXEC); - - if (fd < 0) { - return -1; - } - for (;;) { - ssize_t count = read(fd, buffer + length, sizeof(buffer) - length); - - if (count > 0) { - length += (size_t)count; - if (length < sizeof(buffer)) { - continue; - } - errno = EOVERFLOW; - } else if (count == 0) { - break; - } else if (errno == EINTR) { - continue; - } - { - int status = errno; - - close(fd); - errno = status; - return -1; - } - } - if (close(fd) != 0) { - return -1; - } - /* A decimal count without sign or leading zeros, then a newline. */ - if (length < 2 || buffer[length - 1] != '\n' || (buffer[0] == '0' && length != 2)) { - errno = EPROTO; - return -1; - } - for (index = 0; index + 1 < length; ++index) { - if (buffer[index] < '0' || buffer[index] > '9') { - errno = EPROTO; - return -1; - } - } - return buffer[0] != '0'; -} - -/* - * Reaps exited children without blocking. The agent runs as PID 1, so besides - * the workload's direct child it inherits every orphaned workload process. - */ -static void reap_children(pid_t child, int *child_exited, int *wait_status) -{ - for (;;) { - int status; - pid_t pid = waitpid(-1, &status, WNOHANG); - - if (pid < 0 && errno == EINTR) { - continue; - } - if (pid <= 0) { - return; - } - if (pid == child && !*child_exited) { - *child_exited = 1; - *wait_status = status; - } - } -} - -/* - * Reaps what the workload left to PID 1, then returns 0 if no task of the - * exec cgroup remains, 1 if one does, or -1 on failure. - */ -static int reap_exec_cgroup(void) -{ - int exited = 1; - int status = 0; - - reap_children(0, &exited, &status); - return exec_cgroup_has_tasks(); -} - -/* - * Kills what is left of a direct-mode workload and waits until it is gone: - * every process of the workload has exited and been reaped, so the next - * workload cannot find any of them in /proc. - */ -static int settle_exec_cgroup(void) -{ - const struct timespec delay = { - .tv_sec = 0, - .tv_nsec = 10U * 1000U * 1000U, - }; - unsigned int attempt; - - if (kill_exec_cgroup() != 0) { - return -1; - } - for (attempt = 0; attempt <= 200; ++attempt) { - int remaining = reap_exec_cgroup(); - - if (remaining < 0) { - return -1; - } - if (remaining == 0) { - return 0; - } - if (attempt == 200) { - break; - } - nanosleep(&delay, NULL); - } - errno = ETIMEDOUT; - return -1; -} - -/* Kills the workload: its process group, and in direct mode its whole cgroup. */ -static void terminate_workload(const struct agent_config *config, pid_t child) -{ - kill(-child, SIGKILL); - if (config->direct) { - (void)kill_exec_cgroup(); - } -} - -/* Parses a workload user or group ID: decimal, neither root nor the "unchanged" value -1. */ -static int parse_workload_id(const char *text, uint32_t *id) -{ - char *end = NULL; - unsigned long value; - - if (text[0] < '0' || text[0] > '9') { - errno = EINVAL; - return -1; - } - errno = 0; - value = strtoul(text, &end, 10); - if (errno != 0 || *end != '\0' || value == 0 || value >= UINT32_MAX) { - errno = EINVAL; - return -1; - } - *id = (uint32_t)value; - return 0; -} - -/* Tells the agent why a direct-mode child cannot launch its workload, then exits. */ -static void report_launch_failure(int launch_fd, int32_t stage, int error) -{ - struct launch_failure failure = {.stage = stage, .error = error}; - - (void)write_all(launch_fd, &failure, sizeof(failure)); - _exit(125); -} - -/* - * Enters the working directory with the workload's IDs, no supplementary - * groups, and no effective capabilities, as setpriv will run the workload, so - * a directory that the workload cannot search is refused before anything - * runs, instead of failing in the launch helper with an exit status that the - * workload could also return. The real and saved IDs stay root, so the child - * regains root for setpriv afterwards. setpriv and the launch helper inherit - * the directory instead of looking its path up again, so the workload starts - * in the directory that was checked even if the path is renamed, replaced, or - * retargeted in the meantime. - */ -static void enter_workload_directory( - const struct agent_config *config, - const char *directory, - int launch_fd) -{ - uint32_t uid; - uint32_t gid; - - if (parse_workload_id(config->uid, &uid) != 0 || - parse_workload_id(config->gid, &gid) != 0 || setgroups(0, NULL) != 0 || - setegid((gid_t)gid) != 0 || seteuid((uid_t)uid) != 0) { - report_launch_failure(launch_fd, LAUNCH_STAGE_SETUP, errno); - } - if (chdir(directory) != 0) { - report_launch_failure(launch_fd, LAUNCH_STAGE_CWD, errno); - } - if (seteuid(0) != 0 || setegid(0) != 0) { - report_launch_failure(launch_fd, LAUNCH_STAGE_SETUP, errno); - } -} - -/* - * Points PWD at the working directory, so a shell reports it as the caller - * named it, symbolic links included. A path with empty, ".", or ".." - * components is replaced by the directory's physical path. - */ -static int export_working_directory(const char *directory) -{ - char physical[MAX_GUEST_PATH]; - - if (strcmp(directory, "/") == 0 || safe_path(directory, 1)) { - return setenv("PWD", directory, 1); - } - if (getcwd(physical, sizeof(physical)) == NULL) { - return -1; - } - return setenv("PWD", physical, 1); -} - -static void exec_direct( - const struct agent_config *config, - const char *config_fd, - char *const workload_argv[]) -{ - char *arguments[MAX_ARGUMENTS + 20]; - size_t index = 0; - size_t workload_index = 0; - - if (join_exec_cgroup() != 0) { - _exit(125); - } - if (setenv("HOME", config->home, 1) != 0 || - setenv("USER", config->user, 1) != 0 || - setenv("LOGNAME", config->user, 1) != 0) { - dprintf( - STDERR_FILENO, - "nvx-managed-agent: cannot configure workload environment: %s\n", - strerror(errno)); - _exit(125); - } - arguments[index++] = "setpriv"; - arguments[index++] = "--reuid"; - arguments[index++] = (char *)config->uid; - arguments[index++] = "--regid"; - arguments[index++] = (char *)config->gid; - arguments[index++] = "--clear-groups"; - arguments[index++] = "--no-new-privs"; - arguments[index++] = "--bounding-set=-all"; - arguments[index++] = "--inh-caps=-all"; - arguments[index++] = "--ambient-caps=-all"; - arguments[index++] = "/sbin/nvx-managed-agent"; - arguments[index++] = "--exec-config-fd"; - arguments[index++] = (char *)config_fd; - arguments[index++] = "--"; - while (workload_argv[workload_index] != NULL && index + 1 < MAX_ARGUMENTS + 20) { - arguments[index++] = workload_argv[workload_index++]; - } - arguments[index] = NULL; - execvp("setpriv", arguments); - _exit(125); -} - -static void exec_sandbox( - const struct agent_config *config, - const char *barrier, - const char *config_fd, - char *const workload_argv[]) -{ - char *arguments[MAX_ARGUMENTS + 10]; - size_t index = 0; - size_t workload_index = 0; - - arguments[index++] = "/sbin/nvx-container-launch"; - arguments[index++] = (char *)barrier; - arguments[index++] = (char *)config->rootfs; - arguments[index++] = (char *)config->hostname; - arguments[index++] = (char *)config->uid; - arguments[index++] = (char *)config->gid; - arguments[index++] = (char *)config->user; - arguments[index++] = (char *)config->home; - if (setenv("NVX_EXEC_CONFIG_FD", config_fd, 1) != 0) { - dprintf( - STDERR_FILENO, - "nvx-managed-agent: cannot export execution configuration: %s\n", - strerror(errno)); - _exit(125); - } - while (workload_argv[workload_index] != NULL && index + 1 < MAX_ARGUMENTS + 10) { - arguments[index++] = workload_argv[workload_index++]; - } - arguments[index] = NULL; - execv(arguments[0], arguments); - _exit(125); -} - -static int decode_exec_payload( - const uint8_t *payload, - uint32_t payload_len, - uint32_t *timeout_ms, - char ***workload_argv, - struct exec_config *config) -{ - uint16_t argc; - uint16_t extension; - uint16_t flags = 0; - uint16_t environment_count = 0; - uint32_t cwd_len = 0; - uint32_t offset = 8; - char **arguments; - uint16_t index; - - memset(config, 0, sizeof(*config)); - if (payload_len < 8) { - return -1; - } - *timeout_ms = read_u32(payload); - argc = read_u16(payload + 4); - extension = read_u16(payload + 6); - if (argc == 0 || argc > MAX_ARGUMENTS) { - return -1; - } - if (extension == EXEC_EXTENDED) { - if (payload_len < 16) { - return -1; - } - flags = read_u16(payload + 8); - environment_count = read_u16(payload + 10); - cwd_len = read_u32(payload + 12); - offset = 16; - if ((flags & - ~(EXEC_CWD_PRESENT | EXEC_ENVIRONMENT_PRESENT | - EXEC_INHERIT_DEFAULT_ENV)) != 0 || - ((flags & EXEC_CWD_PRESENT) == 0 && cwd_len != 0) || - ((flags & EXEC_CWD_PRESENT) != 0 && - (cwd_len == 0 || cwd_len > MAX_ARGUMENT_LEN)) || - ((flags & EXEC_ENVIRONMENT_PRESENT) == 0 && - (environment_count != 0 || - (flags & EXEC_INHERIT_DEFAULT_ENV) != 0)) || - environment_count > MAX_ENVIRONMENT) { - return -1; - } - } else if (extension != 0) { - return -1; - } - arguments = calloc((size_t)argc + 1, sizeof(*arguments)); - if (arguments == NULL) { - return -1; - } - for (index = 0; index < argc; ++index) { - uint32_t length; - - if (offset > payload_len || payload_len - offset < 4) { - goto fail; - } - length = read_u32(payload + offset); - offset += 4; - if (length == 0 || length > MAX_ARGUMENT_LEN || length > payload_len - offset || - memchr(payload + offset, '\0', length) != NULL) { - goto fail; - } - arguments[index] = malloc((size_t)length + 1); - if (arguments[index] == NULL) { - goto fail; - } - memcpy(arguments[index], payload + offset, length); - arguments[index][length] = '\0'; - offset += length; - } - if (arguments[0][0] != '/' || - (extension == 0 && offset != payload_len)) { - goto fail; - } - if (extension == EXEC_EXTENDED) { - if (cwd_len != 0) { - if (cwd_len > payload_len - offset || - memchr(payload + offset, '\0', cwd_len) != NULL || - payload[offset] != '/') { - goto fail; - } - config->cwd = malloc((size_t)cwd_len + 1); - if (config->cwd == NULL) { - goto fail; - } - memcpy(config->cwd, payload + offset, cwd_len); - config->cwd[cwd_len] = '\0'; - offset += cwd_len; - } - if ((flags & EXEC_ENVIRONMENT_PRESENT) != 0) { - config->environment = calloc( - (size_t)environment_count + 1, sizeof(*config->environment)); - if (config->environment == NULL) { - goto fail; - } - config->environment_present = 1; - config->inherit_default_env = - (flags & EXEC_INHERIT_DEFAULT_ENV) != 0; - config->environment_count = environment_count; - for (index = 0; index < environment_count; ++index) { - uint32_t length; - const uint8_t *equals; - - if (offset > payload_len || payload_len - offset < 4) { - goto fail; - } - length = read_u32(payload + offset); - offset += 4; - if (length == 0 || length > MAX_ARGUMENT_LEN || - length > payload_len - offset || - memchr(payload + offset, '\0', length) != NULL) { - goto fail; - } - equals = memchr(payload + offset, '=', length); - if (equals == NULL || equals == payload + offset) { - goto fail; - } - for (uint16_t previous = 0; previous < index; ++previous) { - size_t name_length = (size_t)(equals - (payload + offset)); - const char *prior = config->environment[previous]; - if (strcspn(prior, "=") == name_length && - memcmp(prior, payload + offset, name_length) == 0) { - goto fail; - } - } - config->environment[index] = malloc((size_t)length + 1); - if (config->environment[index] == NULL) { - goto fail; - } - memcpy(config->environment[index], payload + offset, length); - config->environment[index][length] = '\0'; - offset += length; - } - } - if (offset != payload_len) { - goto fail; - } - } - *workload_argv = arguments; - return 0; - -fail: - for (index = 0; index < argc; ++index) { - free(arguments[index]); - } - free(arguments); - free(config->cwd); - config->cwd = NULL; - if (config->environment != NULL) { - for (index = 0; index < config->environment_count; ++index) { - free(config->environment[index]); - } - free(config->environment); - config->environment = NULL; - } - return -1; -} - -static void free_arguments(char **arguments) -{ - size_t index; - - if (arguments == NULL) { - return; - } - for (index = 0; arguments[index] != NULL; ++index) { - free(arguments[index]); - } - free(arguments); -} - -static void free_exec_config(struct exec_config *config) -{ - uint16_t index; - - free(config->cwd); - for (index = 0; index < config->environment_count; ++index) { - free(config->environment[index]); - } - free(config->environment); - memset(config, 0, sizeof(*config)); -} - -/* - * Writes the launch helper's configuration. When the caller has entered the - * working directory already, the configuration says so instead of naming it. - */ -static int write_exec_config( - int fd, - const struct exec_config *config, - int cwd_entered) -{ - uint8_t header[8]; - uint16_t flags = 0; - const char *cwd = cwd_entered ? NULL : config->cwd; - uint16_t index; - - if (cwd_entered) { - flags |= EXEC_CWD_ENTERED; - } else if (cwd != NULL) { - flags |= EXEC_CWD_PRESENT; - } - if (config->environment_present) { - flags |= EXEC_ENVIRONMENT_PRESENT; - } - if (config->inherit_default_env) { - flags |= EXEC_INHERIT_DEFAULT_ENV; - } - write_u16(header, flags); - write_u16(header + 2, config->environment_count); - write_u32(header + 4, cwd == NULL ? 0 : (uint32_t)strlen(cwd)); - if (write_all(fd, header, sizeof(header)) != 0 || - (cwd != NULL && write_all(fd, cwd, strlen(cwd)) != 0)) { - return -1; - } - for (index = 0; index < config->environment_count; ++index) { - uint32_t length = (uint32_t)strlen(config->environment[index]); - uint8_t encoded_length[4]; - - write_u32(encoded_length, length); - if (write_all(fd, encoded_length, sizeof(encoded_length)) != 0 || - write_all(fd, config->environment[index], length) != 0) { - return -1; - } - } - return 0; -} - -static int create_exec_config_fd(const struct exec_config *config, int cwd_entered) -{ - int fd = memfd_create("nvx-exec-config", MFD_CLOEXEC | MFD_ALLOW_SEALING); - - if (fd < 0) { - return -1; - } - /* A sealed, bounded anonymous file avoids depending on pipe capacity. */ - if (write_exec_config(fd, config, cwd_entered) != 0 || - lseek(fd, 0, SEEK_SET) < 0 || - fcntl(fd, F_ADD_SEALS, - F_SEAL_WRITE | F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL) != 0) { - close(fd); - return -1; - } - return fd; -} - -static int launch_workload(int argc, char **argv) -{ - char *end = NULL; - long descriptor; - uint8_t header[8]; - uint16_t flags; - uint16_t environment_count; - uint32_t cwd_len; - char *cwd = NULL; - char **environment = NULL; - uint16_t index; - - if (argc < 5 || strcmp(argv[1], "--exec-config-fd") != 0 || - strcmp(argv[3], "--") != 0 || argv[4][0] != '/') { - return 125; - } - errno = 0; - descriptor = strtol(argv[2], &end, 10); - if (errno != 0 || end == argv[2] || *end != '\0' || - descriptor < 0 || descriptor > INT32_MAX || - read_exact((int)descriptor, header, sizeof(header)) != 0) { - return 125; - } - flags = read_u16(header); - environment_count = read_u16(header + 2); - cwd_len = read_u32(header + 4); - if ((flags & - ~(EXEC_CWD_PRESENT | EXEC_ENVIRONMENT_PRESENT | - EXEC_INHERIT_DEFAULT_ENV | EXEC_CWD_ENTERED)) != 0 || - ((flags & EXEC_CWD_ENTERED) != 0 && (flags & EXEC_CWD_PRESENT) != 0) || - environment_count > MAX_ENVIRONMENT || - ((flags & EXEC_CWD_PRESENT) == 0 && cwd_len != 0) || - ((flags & EXEC_CWD_PRESENT) != 0 && - (cwd_len == 0 || cwd_len > MAX_ARGUMENT_LEN)) || - ((flags & EXEC_ENVIRONMENT_PRESENT) == 0 && - (environment_count != 0 || - (flags & EXEC_INHERIT_DEFAULT_ENV) != 0))) { - return 125; - } - if (cwd_len != 0) { - cwd = malloc((size_t)cwd_len + 1); - if (cwd == NULL || read_exact((int)descriptor, cwd, cwd_len) != 0) { - free(cwd); - return 125; - } - cwd[cwd_len] = '\0'; - } - environment = calloc( - (size_t)environment_count + 1, sizeof(*environment)); - if (environment == NULL) { - free(cwd); - return 125; - } - for (index = 0; index < environment_count; ++index) { - uint8_t encoded_length[4]; - uint32_t length; - - if (read_exact((int)descriptor, encoded_length, 4) != 0) { - goto fail; - } - length = read_u32(encoded_length); - if (length == 0 || length > MAX_ARGUMENT_LEN) { - goto fail; - } - environment[index] = malloc((size_t)length + 1); - if (environment[index] == NULL || - read_exact((int)descriptor, environment[index], length) != 0) { - goto fail; - } - if (memchr(environment[index], '\0', length) != NULL || - environment[index][0] == '=' || - memchr(environment[index], '=', length) == NULL) { - goto fail; - } - environment[index][length] = '\0'; - } - close((int)descriptor); - if (cwd != NULL && - (cwd[0] != '/' || memchr(cwd, '\0', cwd_len) != NULL)) { - dprintf(STDERR_FILENO, "nvx-managed-agent: invalid working directory\n"); - goto fail; - } - if (unsetenv("NVX_EXEC_CONFIG_FD") != 0) { - goto fail; - } - if ((flags & EXEC_CWD_ENTERED) == 0) { - const char *directory = cwd == NULL ? "/" : cwd; - - if (chdir(directory) != 0) { - dprintf( - STDERR_FILENO, - "nvx-managed-agent: cannot use working directory %s: %s\n", - directory, - strerror(errno)); - goto fail; - } - if (((flags & EXEC_ENVIRONMENT_PRESENT) == 0 || - (flags & EXEC_INHERIT_DEFAULT_ENV) != 0) && - export_working_directory(directory) != 0) { - dprintf( - STDERR_FILENO, - "nvx-managed-agent: cannot configure working directory " - "environment: %s\n", - strerror(errno)); - goto fail; - } - } - if ((flags & EXEC_ENVIRONMENT_PRESENT) != 0) { - if ((flags & EXEC_INHERIT_DEFAULT_ENV) == 0 && clearenv() != 0) { - goto fail; - } - for (index = 0; index < environment_count; ++index) { - if (putenv(environment[index]) != 0) { - goto fail; - } - environment[index] = NULL; - } - } - execv(argv[4], &argv[4]); - dprintf( - STDERR_FILENO, - "nvx-managed-agent: cannot execute workload: %s\n", - strerror(errno)); - -fail: - free(cwd); - for (index = 0; index < environment_count; ++index) { - free(environment[index]); - } - free(environment); - return 125; -} - -static int stream_output( - struct control_session *session, - uint64_t request_id, - int fd, - uint8_t kind, - size_t *output_bytes) -{ - uint8_t buffer[OUTPUT_CHUNK_BYTES]; - ssize_t count; - - for (;;) { - count = read(fd, buffer, sizeof(buffer)); - if (count > 0) { - if (*output_bytes > MAX_OUTPUT_BYTES - (size_t)count) { - return -2; - } - *output_bytes += (size_t)count; - if (send_app_frame( - session, - kind, - request_id, - 0, - buffer, - (uint32_t)count) != 0) { - return -1; - } - } else if (count == 0) { - return 1; - } else if (errno == EINTR) { - continue; - } else if (errno == EAGAIN || errno == EWOULDBLOCK) { - return 0; - } else { - return -1; - } - } -} - -/* Outcome of control traffic observed while a workload runs. */ -enum exec_control { - EXEC_CONTROL_NONE, - EXEC_CONTROL_CANCEL, - EXEC_CONTROL_SESSION_LOST, - EXEC_CONTROL_FAILED, -}; - -/* - * Handles one control record that arrives while the workload with request ID - * `exec_id` runs. CANCEL for that workload asks for its termination. A RESET - * means the host that could observe the workload is gone: the broker has - * started a new epoch and drops anything sent for the old one. Other requests - * are refused, because executions are sequential. - */ -static enum exec_control handle_exec_control( - struct control_session *session, - uint64_t exec_id) -{ - struct outer_record record; - struct app_request request; - enum exec_control result = EXEC_CONTROL_NONE; - - if (read_outer_record(session->fd, &record) != 0) { - portb_error("exec-outer-read", errno); - return EXEC_CONTROL_FAILED; - } - if (record.type == OUTER_RESET) { - result = acknowledge_reset(session, &record) == 0 - ? EXEC_CONTROL_SESSION_LOST - : EXEC_CONTROL_FAILED; - free_outer_record(&record); - return result; - } - if (record.type != OUTER_DATA || record.sequence != session->host_sequence || - record.epoch != session->epoch || - memcmp(record.instance_id, session->instance_id, 16) != 0 || - parse_app_request(record.payload, record.payload_len, &request) != 0) { - portb_error("exec-record", record.type); - free_outer_record(&record); - return EXEC_CONTROL_FAILED; - } - ++session->host_sequence; - if (send_credit(session, record.payload_len) != 0) { - free_outer_record(&record); - return EXEC_CONTROL_FAILED; - } - if (request.kind == APP_CANCEL) { - if (request.payload_len == 0 && request.request_id == exec_id) { - result = EXEC_CONTROL_CANCEL; - } - } else if (send_app_error(session, request.request_id, 16, "busy") != 0) { - result = EXEC_CONTROL_FAILED; - } - free_outer_record(&record); - return result; -} - -/* Discards output of a workload whose host is gone; returns 1 at end-of-file. */ -static int drain_output(int fd) -{ - uint8_t buffer[OUTPUT_CHUNK_BYTES]; - ssize_t count; - - for (;;) { - count = read(fd, buffer, sizeof(buffer)); - if (count > 0) { - continue; - } - if (count == 0) { - return 1; - } - if (errno == EINTR) { - continue; - } - return errno == EAGAIN || errno == EWOULDBLOCK ? 0 : -1; - } -} - -/* - * Waits until a direct-mode child either executes setpriv, which closes the - * close-on-exec launch pipe, or reports why it cannot launch the workload. - * Returns 0 once setpriv runs, 1 with `failure` filled in, or -1 on error. - */ -static int read_launch_failure(int fd, struct launch_failure *failure) -{ - uint8_t buffer[sizeof(*failure)]; - size_t length = 0; - - while (length < sizeof(buffer)) { - ssize_t count = read(fd, buffer + length, sizeof(buffer) - length); - - if (count > 0) { - length += (size_t)count; - } else if (count == 0) { - break; - } else if (errno != EINTR) { - return -1; - } - } - if (length == 0) { - return 0; - } - if (length != sizeof(buffer)) { - errno = EPROTO; - return -1; - } - memcpy(failure, buffer, sizeof(*failure)); - return 1; -} - -/* - * Refuses a workload whose working directory it cannot enter. Nothing of the - * workload ran; a diagnostic on its standard error names the directory, and - * the refusal carries the error number. - */ -static int refuse_working_directory( - struct control_session *session, - uint64_t request_id, - const char *directory, - int error) -{ - char message[MAX_GUEST_PATH + 128]; - int length = snprintf( - message, - sizeof(message), - "nvx-managed-agent: cannot enter working directory %s: %s\n", - directory, - strerror(error)); - - if (length > 0 && (size_t)length < sizeof(message) && - send_app_frame( - session, APP_STDERR, request_id, 0, message, (uint32_t)length) != 0) { - return -1; - } - return send_app_error(session, request_id, error, "cwd-failed"); -} - -static void close_pipe(int pipe_fds[2]) -{ - unsigned int index; - - for (index = 0; index < 2; ++index) { - if (pipe_fds[index] >= 0) { - close(pipe_fds[index]); - pipe_fds[index] = -1; - } - } -} - -static int run_exec( - struct control_session *session, - const struct agent_config *config, - uint64_t request_id, - uint32_t timeout_ms, - char **workload_argv, - const struct exec_config *exec_config) -{ - const char *barrier = "/run/nvx/managed-container-start"; - int stdout_pipe[2] = {-1, -1}; - int stderr_pipe[2] = {-1, -1}; - int exec_config_fd = -1; - int launch_pipe[2] = {-1, -1}; - struct launch_failure launch_failure = {0}; - int launch_report = 0; - pid_t child; - uint64_t started; - size_t output_bytes = 0; - int stdout_open = 1; - int stderr_open = 1; - int timed_out = 0; - int output_limited = 0; - int cancelled = 0; - int session_lost = 0; - int stragglers_killed = 0; - int wait_status = 0; - int child_exited = 0; - int launch_failed = 0; - - if (config->direct && prepare_exec_cgroup() != 0) { - portb_error("exec-cgroup", errno); - return send_app_error(session, request_id, 125, "launch-failed"); - } - if (config->direct) { - /* A workload whose settlement timed out may have exited since. */ - int remaining = reap_exec_cgroup(); - - if (remaining != 0) { - portb_error("exec-cgroup-verify", remaining < 0 ? errno : EBUSY); - return send_app_error(session, request_id, 125, "containment-failed"); - } - } - if (!config->direct) { - unlink(barrier); - if (mkfifo(barrier, 0600) != 0) { - return send_app_error(session, request_id, 125, "launch-failed"); - } - } - if (pipe2(stdout_pipe, O_CLOEXEC) != 0 || - pipe2(stderr_pipe, O_CLOEXEC) != 0 || - (exec_config_fd = create_exec_config_fd(exec_config, config->direct)) < 0 || - (config->direct && pipe2(launch_pipe, O_CLOEXEC) != 0)) { - unlink(barrier); - if (stdout_pipe[0] >= 0) { - close(stdout_pipe[0]); - close(stdout_pipe[1]); - } - if (stderr_pipe[0] >= 0) { - close(stderr_pipe[0]); - close(stderr_pipe[1]); - } - if (exec_config_fd >= 0) { - close(exec_config_fd); - } - return send_app_error(session, request_id, 125, "launch-failed"); - } - - child = fork(); - if (child < 0) { - close(stdout_pipe[0]); - close(stdout_pipe[1]); - close(stderr_pipe[0]); - close(stderr_pipe[1]); - close(exec_config_fd); - close_pipe(launch_pipe); - unlink(barrier); - return send_app_error(session, request_id, 125, "launch-failed"); - } - if (child == 0) { - int null_fd; - char config_fd[32]; - - setpgid(0, 0); - if (fcntl(exec_config_fd, F_SETFD, 0) != 0 || - snprintf( - config_fd, sizeof(config_fd), "%d", exec_config_fd) <= 0) { - _exit(125); - } - close(stdout_pipe[0]); - close(stderr_pipe[0]); - null_fd = open("/dev/null", O_RDONLY); - if (null_fd >= 0) { - dup2(null_fd, STDIN_FILENO); - close(null_fd); - } - dup2(stdout_pipe[1], STDOUT_FILENO); - dup2(stderr_pipe[1], STDERR_FILENO); - close(stdout_pipe[1]); - close(stderr_pipe[1]); - if (config->direct) { - const char *directory = - exec_config->cwd == NULL ? "/" : exec_config->cwd; - - close(launch_pipe[0]); - enter_workload_directory(config, directory, launch_pipe[1]); - /* - * A request that replaces the environment controls PWD itself; - * one that layers entries over the default keeps this PWD unless - * an entry names it. - */ - if ((!exec_config->environment_present || - exec_config->inherit_default_env) && - export_working_directory(directory) != 0) { - report_launch_failure(launch_pipe[1], LAUNCH_STAGE_SETUP, errno); - } - exec_direct(config, config_fd, workload_argv); - } - exec_sandbox(config, barrier, config_fd, workload_argv); - } - - setpgid(child, child); - close(exec_config_fd); - close(stdout_pipe[1]); - close(stderr_pipe[1]); - /* - * Only the child writes to the launch pipe, and it closes its end when it - * executes setpriv. Its report is read once it has exited, so the agent - * keeps serving control traffic while the child enters the directory. - */ - if (config->direct) { - close(launch_pipe[1]); - launch_pipe[1] = -1; - } - launch_failed = make_nonblocking(stdout_pipe[0]) != 0 || - make_nonblocking(stderr_pipe[0]) != 0; - if (!launch_failed && !config->direct) { - int barrier_result; - - if (write_pid_to_cgroup(child) != 0) { - launch_failed = 1; - } else { - barrier_result = release_container_barrier(barrier, child); - launch_failed = barrier_result < 0; - } - } - if (launch_failed) { - terminate_workload(config, child); - waitpid(child, NULL, 0); - close(stdout_pipe[0]); - close(stderr_pipe[0]); - close_pipe(launch_pipe); - unlink(barrier); - return send_app_error(session, request_id, 125, "launch-failed"); - } - unlink(barrier); - started = monotonic_milliseconds(); - - while (!child_exited || stdout_open || stderr_open) { - struct pollfd descriptors[3]; - int poll_result; - int stdout_result = 0; - int stderr_result = 0; - - descriptors[0].fd = stdout_open ? stdout_pipe[0] : -1; - descriptors[0].events = POLLIN | POLLHUP; - descriptors[0].revents = 0; - descriptors[1].fd = stderr_open ? stderr_pipe[0] : -1; - descriptors[1].events = POLLIN | POLLHUP; - descriptors[1].revents = 0; - /* After a reset the old session is gone; later records belong to the main loop. */ - descriptors[2].fd = session_lost ? -1 : session->fd; - descriptors[2].events = POLLIN; - descriptors[2].revents = 0; - poll_result = poll(descriptors, 3, 25); - if (poll_result < 0 && errno != EINTR) { - terminate_workload(config, child); - } - if (!session_lost && descriptors[2].revents != 0) { - switch (handle_exec_control(session, request_id)) { - case EXEC_CONTROL_NONE: - break; - case EXEC_CONTROL_CANCEL: - if (!child_exited && !cancelled) { - cancelled = 1; - terminate_workload(config, child); - } - break; - case EXEC_CONTROL_SESSION_LOST: - session_lost = 1; - terminate_workload(config, child); - break; - case EXEC_CONTROL_FAILED: - terminate_workload(config, child); - waitpid(child, NULL, 0); - if (stdout_open) { - close(stdout_pipe[0]); - } - if (stderr_open) { - close(stderr_pipe[0]); - } - close_pipe(launch_pipe); - return -1; - } - } - if (stdout_open && descriptors[0].revents != 0) { - stdout_result = session_lost - ? drain_output(stdout_pipe[0]) - : stream_output( - session, - request_id, - stdout_pipe[0], - APP_STDOUT, - &output_bytes); - } - if (stderr_open && descriptors[1].revents != 0) { - stderr_result = session_lost - ? drain_output(stderr_pipe[0]) - : stream_output( - session, - request_id, - stderr_pipe[0], - APP_STDERR, - &output_bytes); - } - if (stdout_result == 1) { - close(stdout_pipe[0]); - stdout_open = 0; - } - if (stderr_result == 1) { - close(stderr_pipe[0]); - stderr_open = 0; - } - if (stdout_result < 0 || stderr_result < 0) { - output_limited = stdout_result == -2 || stderr_result == -2; - terminate_workload(config, child); - } - reap_children(child, &child_exited, &wait_status); - /* Processes the workload left behind must not hold its output open. */ - if (child_exited && config->direct && !stragglers_killed && - (stdout_open || stderr_open)) { - stragglers_killed = 1; - (void)kill_exec_cgroup(); - } - if (!child_exited && timeout_ms != 0 && - monotonic_milliseconds() - started >= timeout_ms) { - timed_out = 1; - terminate_workload(config, child); - } - if ((timed_out || output_limited || cancelled || session_lost) && !child_exited) { - if (waitpid(child, &wait_status, 0) == child) { - child_exited = 1; - } - } - } - - /* The child has exited, so its launch report, if any, is complete. */ - if (config->direct) { - launch_report = read_launch_failure(launch_pipe[0], &launch_failure); - if (launch_report < 0) { - portb_error("exec-launch-report", errno); - } - close_pipe(launch_pipe); - } - if (config->direct && settle_exec_cgroup() != 0) { - portb_error("exec-cgroup-settle", errno); - return session_lost - ? 0 - : send_app_error(session, request_id, 125, "containment-failed"); - } - if (session_lost) { - /* Nothing may be sent for the abandoned exec once the new epoch is acknowledged. */ - return 0; - } - /* A child that reported a launch failure exited without running anything. */ - if (launch_report > 0 && launch_failure.stage == LAUNCH_STAGE_CWD) { - return refuse_working_directory( - session, - request_id, - exec_config->cwd == NULL ? "/" : exec_config->cwd, - launch_failure.error); - } - if (launch_report > 0) { - portb_error("exec-launch", launch_failure.error); - return send_app_error(session, request_id, 125, "launch-failed"); - } - if (cancelled) { - return send_app_frame( - session, APP_EXIT, request_id, 128 + SIGKILL, "cancelled", 9); - } - if (timed_out) { - return send_app_frame( - session, APP_EXIT, request_id, 124, "timeout", 7); - } - if (output_limited) { - return send_app_frame( - session, APP_EXIT, request_id, 125, "output-limit", 12); - } - if (WIFEXITED(wait_status)) { - return send_app_frame( - session, - APP_EXIT, - request_id, - WEXITSTATUS(wait_status), - "exit", - 4); - } - if (WIFSIGNALED(wait_status)) { - int status = 128 + WTERMSIG(wait_status); - return send_app_frame( - session, APP_EXIT, request_id, status, "signal", 6); - } - return send_app_frame( - session, APP_EXIT, request_id, 125, "failed", 6); -} - -/* - * Direct mode alone sets up host mappings, gives each workload a cgroup, and - * refuses a working directory that the workload cannot enter as cwd-failed. - */ -static uint32_t agent_features(const struct agent_config *config) -{ - uint32_t features = - FEATURE_CANCEL | FEATURE_WORKLOAD_ACCOUNT | FEATURE_EXEC_ENVIRONMENT; - - if (config->direct) { - features |= FEATURE_HOST_MAPPING_TABLE | FEATURE_EXEC_CGROUP | FEATURE_EXEC_CWD; - } - return features; -} - -/* - * Mounts the entries FIRST to FIRST + COUNT - 1 of the host mapping table that - * an APP_MAPS request carries after its 32-bit FIRST and COUNT. Entries arrive - * in order and only up to the number that nvx_maps= announced, before any - * workload runs. The whole request is checked before anything is mounted, and - * a mapping that fails to mount fails every later workload. - */ -static int handle_host_mappings( - struct control_session *session, - struct host_mappings *mappings, - const struct app_request *request) -{ - char source[MAX_GUEST_PATH]; - char target[MAX_GUEST_PATH]; - uint32_t first; - uint32_t count; - uint32_t index; - uint32_t offset = MAPS_HEADER_LEN; - int read_only; - - if (mappings->failed || request->payload_len < MAPS_HEADER_LEN) { - return send_app_error(session, request->request_id, 22, "invalid-request"); - } - first = read_u32(request->payload); - count = read_u32(request->payload + 4); - if (first != mappings->applied || count == 0 || - count > mappings->expected - mappings->applied) { - return send_app_error(session, request->request_id, 22, "invalid-request"); - } - for (index = 0; index < count; ++index) { - if (decode_host_mapping( - request->payload, request->payload_len, &offset, source, target, - &read_only) != 0) { - return send_app_error(session, request->request_id, 22, "invalid-request"); - } - } - if (offset != request->payload_len) { - return send_app_error(session, request->request_id, 22, "invalid-request"); - } - offset = MAPS_HEADER_LEN; - for (index = 0; index < count; ++index) { - decode_host_mapping( - request->payload, request->payload_len, &offset, source, target, &read_only); - if (apply_host_mapping(source, target, read_only) != 0) { - int status = errno; - - mappings->failed = 1; - portb_error("host-mapping", status); - dprintf( - STDERR_FILENO, - "NVX-MANAGED-ERROR: stage=host-mapping status=%d entry=%u\n", - status, - (unsigned)(first + index)); - return send_app_error(session, request->request_id, status, "mapping-failed"); - } - ++mappings->applied; - } - return send_app_frame(session, APP_READY, request->request_id, 0, NULL, 0); -} - -/* Serves one application request of the authenticated host. */ -static int handle_app_request( - struct control_session *session, - const struct agent_config *config, - struct host_mappings *mappings, - const struct app_request *request) -{ - char **workload_argv = NULL; - uint8_t features[4]; - struct exec_config exec_config = {0}; - uint32_t timeout_ms = 0; - int result; - - switch (request->kind) { - case APP_PING: - if (request->payload_len != 0) { - portb_error("ping-payload", (int)request->payload_len); - return send_app_error( - session, request->request_id, 22, "invalid-request"); - } - result = send_app_frame( - session, APP_READY, request->request_id, 0, NULL, 0); - if (result != 0) { - portb_error("ping-ready", errno); - } - return result; - case APP_EXEC: - /* Workloads must not run without the host paths that they expect. */ - if (mappings->applied != mappings->expected) { - return send_app_error( - session, request->request_id, EAGAIN, "mappings-incomplete"); - } - if (decode_exec_payload( - request->payload, - request->payload_len, - &timeout_ms, - &workload_argv, - &exec_config) != 0) { - return send_app_error( - session, request->request_id, 22, "invalid-request"); - } - result = run_exec( - session, - config, - request->request_id, - timeout_ms, - workload_argv, - &exec_config); - free_arguments(workload_argv); - free_exec_config(&exec_config); - return result; - case APP_CANCEL: - /* The targeted workload already finished and reported its outcome. */ - return 0; - case APP_FEATURES: - if (request->payload_len != 0) { - return send_app_error( - session, request->request_id, 22, "invalid-request"); - } - write_u32(features, agent_features(config)); - return send_app_frame( - session, - APP_READY, - request->request_id, - 0, - features, - sizeof(features)); - case APP_MAPS: - return handle_host_mappings(session, mappings, request); - case APP_STOP: - if (request->payload_len != 0 || - send_app_frame( - session, APP_STOPPED, request->request_id, 0, NULL, 0) != 0) { - return -1; - } - tcdrain(session->fd); - return AGENT_STOPPED; - default: - return send_app_error( - session, request->request_id, 95, "unsupported-operation"); - } -} - -static int handle_data_record( - struct control_session *session, - const struct agent_config *config, - struct host_mappings *mappings, - const struct outer_record *record) -{ - struct app_request request; - - if (record->sequence != session->host_sequence) { - portb_error("data-sequence", (int)record->sequence); - return -1; - } - if (record->epoch != session->epoch || - memcmp(record->instance_id, session->instance_id, 16) != 0) { - portb_error("data-identity", (int)record->epoch); - return -1; - } - if (parse_app_request(record->payload, record->payload_len, &request) != 0) { - portb_error("data-application", (int)record->payload_len); - return -1; - } - ++session->host_sequence; - if (send_credit(session, record->payload_len) != 0) { - portb_error("data-credit", (int)record->payload_len); - return -1; - } - return handle_app_request(session, config, mappings, &request); -} - -static int run_agent( - struct control_session *session, - const struct agent_config *config, - struct host_mappings *mappings) -{ - struct outer_record record; - - if (send_guest_attach(session->fd) != 0) { - return -1; - } - for (;;) { - if (read_outer_record(session->fd, &record) != 0) { - portb_error("outer-read", errno); - return -1; - } - if (record.type == OUTER_RESET) { - if (acknowledge_reset(session, &record) != 0) { - portb_error("reset-ack", errno); - free_outer_record(&record); - return -1; - } - } else if (record.type == OUTER_DATA) { - int result = handle_data_record(session, config, mappings, &record); - - if (result == AGENT_STOPPED) { - free_outer_record(&record); - return AGENT_STOPPED; - } - if (result != 0) { - portb_error("data", errno); - free_outer_record(&record); - return -1; - } - } else { - portb_error("outer-type", record.type); - free_outer_record(&record); - return -1; - } - free_outer_record(&record); - } -} - -/* - * Direct mode runs as PID 1 and powers the VM off itself. In sandbox mode the - * init agent supervises this process and unmounts the live share, overlay, - * layers, and scratch before it powers the VM off. - */ -static int finish_agent(const struct agent_config *config, int status) -{ - char code[12]; - - if (config->direct) { - snprintf(code, sizeof(code), "%d", status); - execl("/sbin/nvx-exit", "nvx-exit", code, (char *)NULL); - } - return status; -} - -int main(int argc, char **argv) -{ - struct control_session session = {0}; - struct host_mappings mappings = {0}; - struct agent_config config; - int result; - - if (argc >= 2 && strcmp(argv[1], "--exec-config-fd") == 0) { - return launch_workload(argc, argv); - } - if (argc != 8) { - return 125; - } - config.rootfs = argv[2]; - config.hostname = argv[3]; - config.uid = argv[4]; - config.gid = argv[5]; - config.user = argv[6]; - config.home = argv[7]; - config.direct = strcmp(config.rootfs, "-") == 0; - session.fd = open_control_tty(argv[1]); - if (session.fd < 0) { - int status = errno; - portb_error("control-open", status); - dprintf( - STDERR_FILENO, - "NVX-MANAGED-ERROR: stage=control-open status=%d\n", - status); - return finish_agent(&config, 125); - } - if (config.direct && prepare_host_mappings(&mappings) != 0) { - int status = errno; - portb_error("host-mappings", status); - dprintf( - STDERR_FILENO, - "NVX-MANAGED-ERROR: stage=host-mappings status=%d\n", - status); - close(session.fd); - return finish_agent(&config, 125); - } - if (configure_control_tty(session.fd) != 0) { - int status = errno; - portb_error("control-tty", status); - dprintf( - STDERR_FILENO, - "NVX-MANAGED-ERROR: stage=control-tty status=%d\n", - status); - close(session.fd); - return finish_agent(&config, 125); - } - result = run_agent(&session, &config, &mappings); - if (result == AGENT_STOPPED) { - close(session.fd); - return finish_agent(&config, 0); - } - if (result != 0) { - int status = errno; - portb_error("control-session", status); - dprintf( - STDERR_FILENO, - "NVX-MANAGED-ERROR: stage=control-session status=%d\n", - status); - if (session.fd >= 0) { - close(session.fd); - } - return finish_agent(&config, 125); - } - return 0; -} diff --git a/guest/common/nvx-mmio-write.c b/guest/common/nvx-mmio-write.c deleted file mode 100644 index e1749729c..000000000 --- a/guest/common/nvx-mmio-write.c +++ /dev/null @@ -1,72 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -static int parse_u64(const char *text, uint64_t *value) -{ - char *end; - - errno = 0; - *value = strtoull(text, &end, 0); - return errno == 0 && end != text && *end == '\0'; -} - -int main(int argc, char **argv) -{ - uint64_t address; - uint64_t value; - long page_size; - uint64_t page_base; - size_t page_offset; - void *mapping; - int memory; - - if (argc != 3 || !parse_u64(argv[1], &address) || - !parse_u64(argv[2], &value) || value > UINT32_MAX) { - fprintf(stderr, "usage: nvx-mmio-write ADDRESS VALUE\n"); - return 2; - } - - page_size = sysconf(_SC_PAGESIZE); - if (page_size <= 0 || (page_size & (page_size - 1)) != 0) { - fprintf(stderr, "invalid page size\n"); - return 1; - } - page_base = address & ~((uint64_t)page_size - 1); - page_offset = (size_t)(address - page_base); - if (page_offset > (size_t)page_size - sizeof(uint32_t)) { - fprintf(stderr, "MMIO write crosses a page boundary\n"); - return 1; - } - - memory = open("/dev/mem", O_RDWR | O_SYNC); - if (memory < 0) { - perror("open /dev/mem"); - return 1; - } - mapping = mmap(NULL, (size_t)page_size, PROT_READ | PROT_WRITE, MAP_SHARED, - memory, (off_t)page_base); - if (mapping == MAP_FAILED) { - perror("mmap /dev/mem"); - close(memory); - return 1; - } - - *(volatile uint32_t *)((unsigned char *)mapping + page_offset) = - (uint32_t)value; - - if (munmap(mapping, (size_t)page_size) != 0) { - perror("munmap /dev/mem"); - close(memory); - return 1; - } - if (close(memory) != 0) { - perror("close /dev/mem"); - return 1; - } - return 0; -} diff --git a/guest/common/nvx-reseed.c b/guest/common/nvx-reseed.c deleted file mode 100644 index 3d1142aab..000000000 --- a/guest/common/nvx-reseed.c +++ /dev/null @@ -1,178 +0,0 @@ -#define _GNU_SOURCE - -#include -#include -#include -#include -#include -#include -#include -#include - -#define SEED_BYTES 64 -#define GENERATION_ID_BYTES 16 -#define RANDOM_SAMPLE_BYTES 32 - -struct seed_payload { - int entropy_count; - int buf_size; - unsigned char bytes[SEED_BYTES]; -}; - -static int read_seed(const char *path, unsigned char *bytes) -{ - int fd = open(path, O_RDONLY | O_CLOEXEC); - if (fd < 0) { - fprintf(stderr, "nvx-reseed: open %s: %s\n", path, strerror(errno)); - return -1; - } - - size_t offset = 0; - while (offset < SEED_BYTES) { - ssize_t count = read(fd, bytes + offset, SEED_BYTES - offset); - if (count <= 0) { - fprintf(stderr, "nvx-reseed: read %s: %s\n", path, - count == 0 ? "unexpected end of file" : strerror(errno)); - close(fd); - return -1; - } - offset += (size_t)count; - } - close(fd); - return 0; -} - -static int print_hex(const unsigned char *bytes, size_t size) -{ - for (size_t index = 0; index < size; ++index) { - if (printf("%02x", bytes[index]) < 0) { - fprintf(stderr, "nvx-reseed: write output failed\n"); - return -1; - } - } - if (putchar('\n') == EOF) { - fprintf(stderr, "nvx-reseed: write output failed\n"); - return -1; - } - return 0; -} - -static int hex_value(char digit) -{ - if (digit >= '0' && digit <= '9') { - return digit - '0'; - } - if (digit >= 'a' && digit <= 'f') { - return digit - 'a' + 10; - } - if (digit >= 'A' && digit <= 'F') { - return digit - 'A' + 10; - } - return -1; -} - -static int validate_generation_change(const char *previous, - const unsigned char *generation_id) -{ - if (strlen(previous) != GENERATION_ID_BYTES * 2) { - fprintf(stderr, - "nvx-reseed: previous VM generation ID is not 32 hex digits\n"); - return -1; - } - unsigned char previous_bytes[GENERATION_ID_BYTES]; - for (size_t index = 0; index < sizeof(previous_bytes); ++index) { - int high = hex_value(previous[index * 2]); - int low = hex_value(previous[index * 2 + 1]); - if (high < 0 || low < 0) { - fprintf(stderr, - "nvx-reseed: previous VM generation ID is not hexadecimal\n"); - return -1; - } - previous_bytes[index] = (unsigned char)((high << 4) | low); - } - if (memcmp(previous_bytes, generation_id, sizeof(previous_bytes)) == 0) { - fprintf(stderr, "nvx-reseed: VM generation ID did not change\n"); - return -1; - } - return 0; -} - -static int print_random_sample(void) -{ - unsigned char sample[RANDOM_SAMPLE_BYTES]; - size_t offset = 0; - while (offset < sizeof(sample)) { - ssize_t count = - getrandom(sample + offset, sizeof(sample) - offset, 0); - if (count > 0) { - offset += (size_t)count; - continue; - } - if (count < 0 && errno == EINTR) { - continue; - } - fprintf(stderr, "nvx-reseed: getrandom: %s\n", - count == 0 ? "no bytes returned" : strerror(errno)); - return -1; - } - return print_hex(sample, sizeof(sample)); -} - -int main(int argc, char **argv) -{ - if (argc == 2 && strcmp(argv[1], "--sample") == 0) { - return print_random_sample() == 0 ? 0 : 1; - } - - int generation_only = 0; - const char *seed_path; - const char *previous_generation_id = NULL; - if (argc == 2 || argc == 3) { - seed_path = argv[1]; - if (argc == 3) { - previous_generation_id = argv[2]; - } - } else if (argc == 4 && strcmp(argv[1], "--generation-only") == 0) { - generation_only = 1; - seed_path = argv[2]; - previous_generation_id = argv[3]; - } else { - fprintf(stderr, - "usage: nvx-reseed --sample | [--generation-only] SEED_FILE " - "[PREVIOUS_GENERATION_ID]\n"); - return 2; - } - - struct seed_payload seed = { - .entropy_count = SEED_BYTES * 8, - .buf_size = SEED_BYTES, - }; - if (read_seed(seed_path, seed.bytes) != 0) { - return 1; - } - if (previous_generation_id != NULL && - validate_generation_change(previous_generation_id, seed.bytes) != 0) { - return 1; - } - if (generation_only) { - return print_hex(seed.bytes, GENERATION_ID_BYTES) == 0 ? 0 : 1; - } - - int random = open("/dev/random", O_WRONLY | O_CLOEXEC); - if (random < 0) { - fprintf(stderr, "nvx-reseed: open /dev/random: %s\n", strerror(errno)); - return 1; - } - if (ioctl(random, RNDADDENTROPY, &seed) != 0 || - ioctl(random, RNDRESEEDCRNG, 0) != 0) { - fprintf(stderr, "nvx-reseed: random ioctl: %s\n", strerror(errno)); - close(random); - return 1; - } - close(random); - if (previous_generation_id != NULL && - print_hex(seed.bytes, GENERATION_ID_BYTES) != 0) { - return 1; - } - return 0; -} diff --git a/guest/common/nvx-time-probe.c b/guest/common/nvx-time-probe.c deleted file mode 100644 index f77f9b254..000000000 --- a/guest/common/nvx-time-probe.c +++ /dev/null @@ -1,1763 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// nvx-time-probe: guest-side time diagnostics for the NVX time ABI. -// -// latency ns/op of TSC reads and clock_gettime(), p50/p99. -// warp pairwise cross-CPU TSC warp and offset checks. -// clock clocks, clocksources, TSC flags, hypervisor identity, and -// per-CPU clock event devices. -// -// Build (static, x86-64): cc -O2 -static -pthread -o nvx-time-probe -// nvx-time-probe.c - -#define _GNU_SOURCE - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define PROBE_VERSION "1.1" -#define EXIT_FAIL 1 -#define EXIT_USAGE 2 -#define EXIT_ERROR 3 -#define MAX_CPUS 1024 -#define KMSG_RECORD_MAX 8192 -#define SYSLOG_ACTION_READ_ALL 3 -#define SYSLOG_ACTION_SIZE_BUFFER 10 -#define CLOCKSOURCE_DIR "/sys/devices/system/clocksource/clocksource0" - -struct tsc_rate { - double hz; - const char *source; -}; - -static struct tsc_rate g_tsc; -static volatile uint64_t g_sink; - -// --------------------------------------------------------------------------- -// CPU primitives -// --------------------------------------------------------------------------- - -static inline uint64_t rdtsc_plain(void) -{ - uint32_t lo, hi; - - __asm__ __volatile__("rdtsc" : "=a"(lo), "=d"(hi)); - return ((uint64_t)hi << 32) | lo; -} - -static inline uint64_t rdtscp_plain(void) -{ - uint32_t lo, hi, aux; - - __asm__ __volatile__("rdtscp" : "=a"(lo), "=d"(hi), "=c"(aux)); - return ((uint64_t)hi << 32) | lo; -} - -// Linux rdtsc_ordered(): the read cannot pass earlier loads. -static inline uint64_t rdtsc_lfence(void) -{ - uint32_t lo, hi; - - __asm__ __volatile__("lfence\n\trdtsc" : "=a"(lo), "=d"(hi)::"memory"); - return ((uint64_t)hi << 32) | lo; -} - -// Fully fenced read for timing brackets: later work cannot start before it. -static inline uint64_t rdtsc_fenced(void) -{ - uint32_t lo, hi; - - __asm__ __volatile__("lfence\n\trdtsc\n\tlfence" - : "=a"(lo), "=d"(hi)::"memory"); - return ((uint64_t)hi << 32) | lo; -} - -static inline void cpu_pause(void) -{ - __asm__ __volatile__("pause" ::: "memory"); -} - -static void cpuid(uint32_t leaf, uint32_t subleaf, uint32_t regs[4]) -{ - __asm__ __volatile__("cpuid" - : "=a"(regs[0]), "=b"(regs[1]), "=c"(regs[2]), - "=d"(regs[3]) - : "a"(leaf), "c"(subleaf)); -} - -static bool cpu_has_rdtscp(void) -{ - uint32_t regs[4]; - - cpuid(0x80000000u, 0, regs); - if (regs[0] < 0x80000001u) - return false; - cpuid(0x80000001u, 0, regs); - return (regs[3] >> 27) & 1; -} - -static int pin_self(int cpu) -{ - cpu_set_t set; - - CPU_ZERO(&set); - CPU_SET(cpu, &set); - if (sched_setaffinity(0, sizeof(set), &set) != 0) - return -1; - if (sched_getcpu() != cpu) { - errno = EINVAL; - return -1; - } - return 0; -} - -// --------------------------------------------------------------------------- -// Small helpers -// --------------------------------------------------------------------------- - -static double cycles_to_ns(double cycles) -{ - return g_tsc.hz > 0 ? cycles * 1e9 / g_tsc.hz : 0; -} - -static long long round_ll(double value) -{ - return (long long)(value >= 0 ? value + 0.5 : value - 0.5); -} - -static int read_first_line(const char *path, char *buf, size_t size) -{ - FILE *file = fopen(path, "r"); - - if (!file) - return -1; - if (!fgets(buf, (int)size, file)) { - fclose(file); - return -1; - } - fclose(file); - buf[strcspn(buf, "\n")] = '\0'; - return 0; -} - -static int cmp_double(const void *a, const void *b) -{ - double x = *(const double *)a; - double y = *(const double *)b; - - return (x > y) - (x < y); -} - -static int cmp_i64(const void *a, const void *b) -{ - int64_t x = *(const int64_t *)a; - int64_t y = *(const int64_t *)b; - - return (x > y) - (x < y); -} - -// Nearest-rank percentile over a sorted array, as the NVX harness reports. -static double nearest_rank(const double *sorted, size_t count, double pct) -{ - double exact = pct * (double)count; - size_t rank = (size_t)exact; - - if ((double)rank < exact) - rank++; - if (rank < 1) - rank = 1; - if (rank > count) - rank = count; - return sorted[rank - 1]; -} - -static int parse_u64_arg(const char *text, uint64_t *value) -{ - char *end; - - errno = 0; - *value = strtoull(text, &end, 0); - return errno == 0 && end != text && *end == '\0' ? 0 : -1; -} - -static int parse_cpu_list(const char *text, int *cpus, int max) -{ - int count = 0; - const char *p = text; - - while (*p) { - char *end; - long first = strtol(p, &end, 10); - long last = first; - - if (end == p || first < 0 || first >= MAX_CPUS) - return -1; - p = end; - if (*p == '-') { - last = strtol(p + 1, &end, 10); - if (end == p + 1 || last < first || last >= MAX_CPUS) - return -1; - p = end; - } - for (long cpu = first; cpu <= last; cpu++) { - for (int i = 0; i < count; i++) - if (cpus[i] == cpu) - return -1; - if (count >= max) - return -1; - cpus[count++] = (int)cpu; - } - if (*p == ',') - p++; - else if (*p) - return -1; - } - return count; -} - -static int affinity_cpus(int *cpus, int max) -{ - cpu_set_t set; - int count = 0; - - if (sched_getaffinity(0, sizeof(set), &set) != 0) - return -1; - for (int cpu = 0; cpu < CPU_SETSIZE && count < max; cpu++) - if (CPU_ISSET(cpu, &set)) - cpus[count++] = cpu; - return count; -} - -// Rejects CPUs outside the affinity mask (offline or excluded) before any -// measurement starts. -static int check_cpus_allowed(const char *command, const int *cpus, int count) -{ - cpu_set_t allowed; - - if (sched_getaffinity(0, sizeof(allowed), &allowed) != 0) { - fprintf(stderr, "nvx-time-probe %s: sched_getaffinity: %s\n", command, - strerror(errno)); - return -1; - } - for (int i = 0; i < count; i++) { - if (cpus[i] >= CPU_SETSIZE || !CPU_ISSET(cpus[i], &allowed)) { - fprintf(stderr, - "nvx-time-probe %s: cpu %d is not in the affinity mask" - " (offline or excluded)\n", - command, cpus[i]); - return -1; - } - } - return 0; -} - -static void format_cpu_list(const int *cpus, int count, char *buf, size_t size) -{ - size_t used = 0; - - buf[0] = '\0'; - for (int i = 0; i < count && used < size; i++) { - int first = cpus[i]; - int last = first; - - while (i + 1 < count && cpus[i + 1] == last + 1) - last = cpus[++i]; - used += (size_t)snprintf(buf + used, size - used, "%s%d", used ? "," : "", - first); - if (last != first && used < size) - used += (size_t)snprintf(buf + used, size - used, "-%d", last); - } -} - -// --------------------------------------------------------------------------- -// Kernel log access (/dev/kmsg, falling back to syslog(2)) -// --------------------------------------------------------------------------- - -typedef void (*kmsg_fn)(uint64_t usec, const char *text, void *ctx); - -static int kmsg_from_devkmsg(kmsg_fn fn, void *ctx) -{ - char record[KMSG_RECORD_MAX]; - int count = 0; - int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK | O_CLOEXEC); - - if (fd < 0) - return -1; - for (;;) { - ssize_t n = read(fd, record, sizeof(record) - 1); - char *semi; - char *p; - int field = 0; - - if (n < 0 && (errno == EINTR || errno == EPIPE)) - continue; - if (n <= 0) - break; - record[n] = '\0'; - // Record format: "prio,seq,usec,flags[,...];message\n[ KEY=VALUE\n]". - semi = strchr(record, ';'); - if (!semi) - continue; - for (p = record; p < semi && field < 2; p++) - if (*p == ',') - field++; - record[strcspn(record, "\n")] = '\0'; - fn(strtoull(p, NULL, 10), semi + 1, ctx); - count++; - } - close(fd); - return count > 0 ? 0 : -1; -} - -static int kmsg_from_syslog(kmsg_fn fn, void *ctx) -{ - int size = klogctl(SYSLOG_ACTION_SIZE_BUFFER, NULL, 0); - char *buf; - char *line; - int n; - - if (size <= 0) - size = 1 << 20; - buf = malloc((size_t)size + 1); - if (!buf) - return -1; - n = klogctl(SYSLOG_ACTION_READ_ALL, buf, size); - if (n < 0) { - free(buf); - return -1; - } - buf[n] = '\0'; - for (line = buf; line && *line;) { - char *next = strchr(line, '\n'); - char *p = line; - uint64_t usec = 0; - - if (next) - *next++ = '\0'; - if (*p == '<') { - char *close_angle = strchr(p, '>'); - - if (close_angle) - p = close_angle + 1; - } - if (*p == '[') { - char *close_bracket = strchr(p, ']'); - - if (close_bracket) { - usec = (uint64_t)(strtod(p + 1, NULL) * 1e6 + 0.5); - p = close_bracket + 1; - if (*p == ' ') - p++; - } - } - fn(usec, p, ctx); - line = next; - } - free(buf); - return 0; -} - -static int kmsg_foreach(kmsg_fn fn, void *ctx) -{ - if (kmsg_from_devkmsg(fn, ctx) == 0) - return 0; - return kmsg_from_syslog(fn, ctx); -} - -// --------------------------------------------------------------------------- -// /proc/cpuinfo and TSC rate discovery -// --------------------------------------------------------------------------- - -static bool cpuinfo_has_flag(const char *flag) -{ - static char flags[8192]; - static bool loaded; - size_t len = strlen(flag); - const char *p; - - if (!loaded) { - char line[8192]; - FILE *file = fopen("/proc/cpuinfo", "r"); - - loaded = true; - while (file && fgets(line, sizeof(line), file)) { - if (strncmp(line, "flags", 5) == 0 && strchr(line, ':')) { - snprintf(flags, sizeof(flags), " %s", strchr(line, ':') + 1); - flags[strcspn(flags, "\n")] = '\0'; - strncat(flags, " ", sizeof(flags) - strlen(flags) - 1); - break; - } - } - if (file) - fclose(file); - } - for (p = flags; (p = strstr(p, flag)) != NULL; p += len) - if (p[-1] == ' ' && p[len] == ' ') - return true; - return false; -} - -static double cpuinfo_mhz(void) -{ - char line[512]; - double mhz = 0; - FILE *file = fopen("/proc/cpuinfo", "r"); - - if (!file) - return 0; - while (fgets(line, sizeof(line), file)) { - if (strncmp(line, "cpu MHz", 7) == 0 && strchr(line, ':')) { - mhz = strtod(strchr(line, ':') + 1, NULL); - break; - } - } - fclose(file); - return mhz; -} - -struct kmsg_tsc { - double refined_mhz; - double detected_tsc_mhz; - double detected_cpu_mhz; -}; - -static double mhz_after(const char *text, const char *prefix, const char *suffix) -{ - const char *p = strstr(text, prefix); - char *end; - double mhz; - - if (!p) - return 0; - p += strlen(prefix); - mhz = strtod(p, &end); - if (end == p || strncmp(end, suffix, strlen(suffix)) != 0) - return 0; - return mhz; -} - -static void kmsg_tsc_cb(uint64_t usec, const char *text, void *ctx) -{ - struct kmsg_tsc *k = ctx; - double mhz; - - (void)usec; - if ((mhz = mhz_after(text, "Refined TSC clocksource calibration: ", " MHz")) > 0) - k->refined_mhz = mhz; - else if ((mhz = mhz_after(text, "tsc: Detected ", " MHz TSC")) > 0) - k->detected_tsc_mhz = mhz; - else if ((mhz = mhz_after(text, "tsc: Detected ", " MHz processor")) > 0) - k->detected_cpu_mhz = mhz; -} - -// The kernel prints kHz precision as "NNNN.NNN MHz". -static double mhz_to_hz(double mhz) -{ - return (double)(uint64_t)(mhz * 1000.0 + 0.5) * 1000.0; -} - -static double measure_tsc_hz(unsigned ms) -{ - struct timespec a, b; - struct timespec pause = {ms / 1000, (long)(ms % 1000) * 1000000L}; - uint64_t t0, t1; - double ns; - - clock_gettime(CLOCK_MONOTONIC_RAW, &a); - t0 = rdtsc_fenced(); - nanosleep(&pause, NULL); - clock_gettime(CLOCK_MONOTONIC_RAW, &b); - t1 = rdtsc_fenced(); - ns = (double)(b.tv_sec - a.tv_sec) * 1e9 + (double)(b.tv_nsec - a.tv_nsec); - return ns > 0 ? (double)(t1 - t0) * 1e9 / ns : 0; -} - -// Precedence: option, sysfs tsc_freq_khz, kernel log (refined, then detected -// TSC, then detected processor), "cpu MHz" without APERF/MPERF, measurement. -static void detect_tsc_rate(double override_hz) -{ - struct kmsg_tsc k = {0}; - char line[64]; - - if (override_hz > 0) { - g_tsc = (struct tsc_rate){override_hz, "option"}; - return; - } - if (read_first_line("/sys/devices/system/cpu/cpu0/tsc_freq_khz", line, - sizeof(line)) == 0 && - strtoull(line, NULL, 10) > 0) { - g_tsc = (struct tsc_rate){(double)strtoull(line, NULL, 10) * 1000.0, - "sysfs-tsc_freq_khz"}; - return; - } - kmsg_foreach(kmsg_tsc_cb, &k); - if (k.refined_mhz > 0) { - g_tsc = (struct tsc_rate){mhz_to_hz(k.refined_mhz), "kmsg-refined"}; - return; - } - if (k.detected_tsc_mhz > 0) { - g_tsc = (struct tsc_rate){mhz_to_hz(k.detected_tsc_mhz), - "kmsg-detected-tsc"}; - return; - } - if (k.detected_cpu_mhz > 0) { - g_tsc = (struct tsc_rate){mhz_to_hz(k.detected_cpu_mhz), - "kmsg-detected-processor"}; - return; - } - // Without APERF/MPERF, "cpu MHz" reports cpu_khz, which equals tsc_khz. - if (!cpuinfo_has_flag("aperfmperf") && cpuinfo_mhz() > 0) { - g_tsc = (struct tsc_rate){mhz_to_hz(cpuinfo_mhz()), "cpuinfo-cpu-mhz"}; - return; - } - g_tsc = (struct tsc_rate){measure_tsc_hz(200), "measured-monotonic-raw"}; -} - -static void current_clocksource(char *buf, size_t size) -{ - if (read_first_line(CLOCKSOURCE_DIR "/current_clocksource", buf, size) != 0) - snprintf(buf, size, "unknown"); -} - -// --------------------------------------------------------------------------- -// Option parsing -// --------------------------------------------------------------------------- - -// Matches "--name value" or "--name=value" at argv[*index]. -static const char *option_value(int argc, char **argv, int *index, const char *name) -{ - const char *arg = argv[*index]; - size_t len = strlen(name); - - if (strncmp(arg, name, len) != 0) - return NULL; - if (arg[len] == '=') - return arg + len + 1; - if (arg[len] == '\0' && *index + 1 < argc) - return argv[++*index]; - return NULL; -} - -static int parse_hz_arg(const char *text, double *hz) -{ - char *end; - - errno = 0; - *hz = strtod(text, &end); - return errno == 0 && end != text && *end == '\0' && *hz > 0 ? 0 : -1; -} - -static int bad_option(const char *command, const char *arg) -{ - fprintf(stderr, "nvx-time-probe %s: invalid option or value: %s\n", command, arg); - return EXIT_USAGE; -} - -// --------------------------------------------------------------------------- -// latency -// --------------------------------------------------------------------------- - -enum method { - M_RDTSC, - M_RDTSCP, - M_LFENCE_RDTSC, - M_CLOCK_MONOTONIC, - M_CLOCK_REALTIME, - M_SYSCALL_MONOTONIC, - M_SYSCALL_GETPID, - M_SCHED_YIELD, - M_COUNT, -}; - -static const char *const method_names[M_COUNT] = { - "rdtsc", - "rdtscp", - "lfence_rdtsc", - "clock_gettime_monotonic", - "clock_gettime_realtime", - "syscall_clock_gettime_monotonic", - "syscall_getpid", - "sched_yield", -}; - -struct latency_result { - double p50; - double p99; - double min; - double mean; - size_t samples; -}; - -static uint64_t run_batch(enum method method, unsigned batch) -{ - struct timespec ts; - uint64_t sink = 0; - unsigned i; - - switch (method) { - case M_RDTSC: - for (i = 0; i < batch; i++) - sink += rdtsc_plain(); - break; - case M_RDTSCP: - for (i = 0; i < batch; i++) - sink += rdtscp_plain(); - break; - case M_LFENCE_RDTSC: - for (i = 0; i < batch; i++) - sink += rdtsc_lfence(); - break; - case M_CLOCK_MONOTONIC: - for (i = 0; i < batch; i++) { - clock_gettime(CLOCK_MONOTONIC, &ts); - sink += (uint64_t)ts.tv_nsec; - } - break; - case M_CLOCK_REALTIME: - for (i = 0; i < batch; i++) { - clock_gettime(CLOCK_REALTIME, &ts); - sink += (uint64_t)ts.tv_nsec; - } - break; - case M_SYSCALL_MONOTONIC: - for (i = 0; i < batch; i++) { - syscall(SYS_clock_gettime, CLOCK_MONOTONIC, &ts); - sink += (uint64_t)ts.tv_nsec; - } - break; - case M_SYSCALL_GETPID: - for (i = 0; i < batch; i++) - sink += (uint64_t)syscall(SYS_getpid); - break; - case M_SCHED_YIELD: - for (i = 0; i < batch; i++) - sink += (uint64_t)sched_yield(); - break; - default: - break; - } - return sink; -} - -// Median cost of an empty fenced timing bracket, subtracted from samples. -static double bracket_overhead_cycles(size_t iterations) -{ - double *samples = malloc(iterations * sizeof(*samples)); - double median; - - if (!samples) - return 0; - for (size_t i = 0; i < iterations; i++) { - uint64_t t0 = rdtsc_fenced(); - uint64_t t1 = rdtsc_fenced(); - - samples[i] = (double)(t1 - t0); - } - qsort(samples, iterations, sizeof(*samples), cmp_double); - median = nearest_rank(samples, iterations, 0.50); - free(samples); - return median; -} - -static int measure_method(enum method method, size_t iterations, unsigned batch, - double overhead_cycles, struct latency_result *out) -{ - double *per_op = malloc(iterations * sizeof(*per_op)); - double sum = 0; - - if (!per_op) - return -1; - g_sink += run_batch(method, batch); - for (size_t i = 0; i < iterations; i++) { - uint64_t t0 = rdtsc_fenced(); - uint64_t sink = run_batch(method, batch); - uint64_t t1 = rdtsc_fenced(); - double cycles = (double)(t1 - t0) - overhead_cycles; - - g_sink += sink; - if (cycles < 0) - cycles = 0; - per_op[i] = cycles_to_ns(cycles) / batch; - sum += per_op[i]; - } - qsort(per_op, iterations, sizeof(*per_op), cmp_double); - out->p50 = nearest_rank(per_op, iterations, 0.50); - out->p99 = nearest_rank(per_op, iterations, 0.99); - out->min = per_op[0]; - out->mean = sum / (double)iterations; - out->samples = iterations; - free(per_op); - return 0; -} - -static int cmd_latency(int argc, char **argv) -{ - uint64_t iterations = 2000; - uint64_t batch = 50; - uint64_t cpu_arg = UINT64_MAX; - double tsc_hz = 0; - struct latency_result results[M_COUNT] = {0}; - char clocksource[64]; - bool has_rdtscp = cpu_has_rdtscp(); - bool has_vdso = getauxval(AT_SYSINFO_EHDR) != 0; - double overhead; - int cpu; - - for (int i = 2; i < argc; i++) { - const char *value; - - if ((value = option_value(argc, argv, &i, "--iterations"))) { - if (parse_u64_arg(value, &iterations) || iterations == 0 || - iterations > 10000000) - return bad_option("latency", argv[i]); - } else if ((value = option_value(argc, argv, &i, "--batch"))) { - if (parse_u64_arg(value, &batch) || batch == 0 || batch > 1000000) - return bad_option("latency", argv[i]); - } else if ((value = option_value(argc, argv, &i, "--cpu"))) { - if (parse_u64_arg(value, &cpu_arg) || cpu_arg >= MAX_CPUS) - return bad_option("latency", argv[i]); - } else if ((value = option_value(argc, argv, &i, "--tsc-hz"))) { - if (parse_hz_arg(value, &tsc_hz)) - return bad_option("latency", argv[i]); - } else { - return bad_option("latency", argv[i]); - } - } - - cpu = cpu_arg == UINT64_MAX ? sched_getcpu() : (int)cpu_arg; - if (cpu >= 0 && check_cpus_allowed("latency", &cpu, 1) != 0) - return EXIT_USAGE; - if (cpu < 0 || pin_self(cpu) != 0) { - fprintf(stderr, "nvx-time-probe latency: cannot pin to cpu %d: %s\n", cpu, - strerror(errno)); - return EXIT_ERROR; - } - detect_tsc_rate(tsc_hz); - if (g_tsc.hz <= 0) { - fprintf(stderr, "nvx-time-probe latency: unknown TSC rate; pass --tsc-hz\n"); - return EXIT_ERROR; - } - current_clocksource(clocksource, sizeof(clocksource)); - overhead = bracket_overhead_cycles((size_t)iterations); - - printf("nvx-time-probe %s latency: cpu=%d iterations=%" PRIu64 " batch=%" PRIu64 - " tsc_hz=%.0f tsc_hz_source=%s clocksource=%s vdso=%d" - " bracket_overhead_ns=%.1f\n", - PROBE_VERSION, cpu, iterations, batch, g_tsc.hz, g_tsc.source, - clocksource, has_vdso, cycles_to_ns(overhead)); - printf("%-32s %10s %10s %10s %10s\n", "method", "p50_ns", "p99_ns", "min_ns", - "mean_ns"); - for (int m = 0; m < M_COUNT; m++) { - if (m == M_RDTSCP && !has_rdtscp) { - printf("%-32s unsupported (CPUID.80000001H:EDX[27] clear)\n", - method_names[m]); - continue; - } - if (measure_method((enum method)m, (size_t)iterations, (unsigned)batch, - overhead, &results[m]) != 0) { - fprintf(stderr, "nvx-time-probe latency: out of memory\n"); - return EXIT_ERROR; - } - printf("%-32s %10.1f %10.1f %10.1f %10.1f\n", method_names[m], - results[m].p50, results[m].p99, results[m].min, results[m].mean); - fflush(stdout); - } - for (int m = 0; m < M_COUNT; m++) { - if (!results[m].samples) { - printf("NVX-TIME-PROBE latency method=%s unsupported=1\n", method_names[m]); - continue; - } - printf("NVX-TIME-PROBE latency method=%s p50_ns=%.1f p99_ns=%.1f min_ns=%.1f" - " mean_ns=%.1f samples=%zu batch=%" PRIu64 " cpu=%d clocksource=%s\n", - method_names[m], results[m].p50, results[m].p99, results[m].min, - results[m].mean, results[m].samples, batch, cpu, clocksource); - } - return 0; -} - -// --------------------------------------------------------------------------- -// warp -// --------------------------------------------------------------------------- - -#define PP_STOP UINT64_MAX -#define PP_MAX_SAMPLES (1u << 18) -#define STALL_SECONDS 2.0 -#define SPIN_CHECK_MASK 0xfffffu - -// (a) Linux check_tsc_warp(): both CPUs take one spinlock, read the TSC, and -// compare it with the last value either CPU stored. A backward step proves a -// skew larger than the lock handoff latency, so it is a lower bound. -static struct { - _Alignas(64) atomic_int lock; - uint64_t last_tsc; - uint64_t max_warp; - uint64_t warps; - _Alignas(64) atomic_int ready; - atomic_int abort; - atomic_int stop; -} g_warp; - -// (b) Ping-pong: cpu_a reads t1 and signals, cpu_b reads t2 and answers, and -// cpu_a reads t3. The offset of cpu_b relative to cpu_a lies in [t2-t3, t2-t1]; -// the point estimate is t2-(t1+t3)/2 with uncertainty (t3-t1)/2. -static struct { - _Alignas(64) _Atomic uint64_t seq; - _Atomic uint64_t t2; - _Alignas(64) atomic_int ready; - atomic_int abort; -} g_pp; - -struct pair_result { - int cpu_a; - int cpu_b; - uint64_t warp_iterations; - uint64_t warps; - uint64_t max_warp_cycles; - uint64_t rounds; - uint64_t min_rtt_cycles; - int64_t min_rtt_offset_x2; - int64_t lo_cycles; - int64_t hi_cycles; - double median_offset_cycles; - double offset_cycles; - double uncertainty_cycles; - bool consistent; - bool stalled; -}; - -struct thread_arg { - int cpu; - bool leader; - uint64_t budget_cycles; - uint64_t stall_cycles; - uint64_t iterations; - int pin_errno; - struct pair_result *result; - int64_t *offsets_x2; -}; - -static bool pair_barrier(atomic_int *ready, atomic_int *abort_flag, bool failed) -{ - if (failed) - atomic_store(abort_flag, 1); - atomic_fetch_add(ready, 1); - while (atomic_load(ready) < 2) - cpu_pause(); - return !atomic_load(abort_flag); -} - -static uint64_t coarse_ns(void) -{ - struct timespec ts; - - clock_gettime(CLOCK_MONOTONIC_COARSE, &ts); - return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; -} - -static void *warp_thread(void *p) -{ - struct thread_arg *arg = p; - bool pinned = pin_self(arg->cpu) == 0; - uint64_t end; - uint64_t deadline_ns; - uint64_t i; - - if (!pinned) - arg->pin_errno = errno; - if (!pair_barrier(&g_warp.ready, &g_warp.abort, !pinned)) - return NULL; - end = rdtsc_lfence() + arg->budget_cycles; - // Guard against a TSC that never reaches the deadline. - deadline_ns = coarse_ns() + - (uint64_t)(cycles_to_ns((double)arg->budget_cycles) + STALL_SECONDS * 1e9); - for (i = 0;; i++) { - uint64_t prev, now; - - while (atomic_exchange_explicit(&g_warp.lock, 1, memory_order_acquire)) - while (atomic_load_explicit(&g_warp.lock, memory_order_relaxed)) - cpu_pause(); - prev = g_warp.last_tsc; - now = rdtsc_lfence(); - g_warp.last_tsc = now; - if (prev > now) { - if (prev - now > g_warp.max_warp) - g_warp.max_warp = prev - now; - g_warp.warps++; - } - atomic_store_explicit(&g_warp.lock, 0, memory_order_release); - if (!(i & 7)) { - if (arg->leader && - (rdtsc_plain() > end || (!(i & 4095) && coarse_ns() > deadline_ns))) - atomic_store_explicit(&g_warp.stop, 1, memory_order_relaxed); - if (atomic_load_explicit(&g_warp.stop, memory_order_relaxed)) - break; - } - } - arg->iterations = i + 1; - return NULL; -} - -static void *pp_leader(void *p) -{ - struct thread_arg *arg = p; - struct pair_result *r = arg->result; - bool pinned = pin_self(arg->cpu) == 0; - uint64_t stored = 0; - uint64_t end; - uint64_t k; - - if (!pinned) - arg->pin_errno = errno; - if (!pair_barrier(&g_pp.ready, &g_pp.abort, !pinned)) - return NULL; - end = rdtsc_lfence() + arg->budget_cycles; - for (k = 1;; k++) { - uint64_t spins = 0; - uint64_t t1, t2, t3, rtt; - int64_t lo, hi; - - t1 = rdtsc_fenced(); - atomic_store_explicit(&g_pp.seq, 2 * k - 1, memory_order_release); - while (atomic_load_explicit(&g_pp.seq, memory_order_acquire) != 2 * k) { - if ((++spins & SPIN_CHECK_MASK) == 0 && - rdtsc_plain() - t1 > arg->stall_cycles) { - r->stalled = true; - goto done; - } - } - t3 = rdtsc_lfence(); - t2 = atomic_load_explicit(&g_pp.t2, memory_order_relaxed); - rtt = t3 - t1; - lo = (int64_t)(t2 - t3); - hi = (int64_t)(t2 - t1); - if (rtt < r->min_rtt_cycles) { - r->min_rtt_cycles = rtt; - r->min_rtt_offset_x2 = lo + hi; - } - if (lo > r->lo_cycles) - r->lo_cycles = lo; - if (hi < r->hi_cycles) - r->hi_cycles = hi; - if (stored < PP_MAX_SAMPLES) - arg->offsets_x2[stored++] = lo + hi; - r->rounds = k; - if ((k & 63) == 0 && rdtsc_plain() > end) - break; - } -done: - atomic_store_explicit(&g_pp.seq, PP_STOP, memory_order_release); - arg->iterations = stored; - return NULL; -} - -static void *pp_follower(void *p) -{ - struct thread_arg *arg = p; - bool pinned = pin_self(arg->cpu) == 0; - - if (!pinned) - arg->pin_errno = errno; - if (!pair_barrier(&g_pp.ready, &g_pp.abort, !pinned)) - return NULL; - for (uint64_t k = 1;; k++) { - uint64_t spins = 0; - uint64_t wait_start = 0; - uint64_t value; - - while ((value = atomic_load_explicit(&g_pp.seq, memory_order_acquire)) != - 2 * k - 1) { - if (value == PP_STOP) - return NULL; - if ((++spins & SPIN_CHECK_MASK) == 0) { - uint64_t now = rdtsc_plain(); - - if (!wait_start) - wait_start = now; - else if (now - wait_start > arg->stall_cycles) - return NULL; - } - } - atomic_store_explicit(&g_pp.t2, rdtsc_fenced(), memory_order_relaxed); - atomic_store_explicit(&g_pp.seq, 2 * k, memory_order_release); - } -} - -static int run_threads(void *(*fn_a)(void *), void *(*fn_b)(void *), - struct thread_arg *a, struct thread_arg *b) -{ - pthread_t ta, tb; - int err; - - if ((err = pthread_create(&ta, NULL, fn_a, a)) != 0) { - errno = err; - return -1; - } - if ((err = pthread_create(&tb, NULL, fn_b, b)) != 0) { - // Release the first thread from its barrier before joining it. - atomic_store(&g_warp.abort, 1); - atomic_store(&g_pp.abort, 1); - atomic_fetch_add(&g_warp.ready, 1); - atomic_fetch_add(&g_pp.ready, 1); - pthread_join(ta, NULL); - errno = err; - return -1; - } - pthread_join(ta, NULL); - pthread_join(tb, NULL); - if (a->pin_errno || b->pin_errno) { - errno = a->pin_errno ? a->pin_errno : b->pin_errno; - return -1; - } - return 0; -} - -static int run_pair(int cpu_a, int cpu_b, uint64_t budget_cycles, - uint64_t stall_cycles, int64_t *offsets_x2, - struct pair_result *r) -{ - struct thread_arg a = {cpu_a, true, budget_cycles, stall_cycles, 0, 0, r, offsets_x2}; - struct thread_arg b = {cpu_b, false, budget_cycles, stall_cycles, 0, 0, r, NULL}; - - memset(r, 0, sizeof(*r)); - r->cpu_a = cpu_a; - r->cpu_b = cpu_b; - - atomic_store(&g_warp.lock, 0); - atomic_store(&g_warp.ready, 0); - atomic_store(&g_warp.abort, 0); - atomic_store(&g_warp.stop, 0); - g_warp.last_tsc = 0; - g_warp.max_warp = 0; - g_warp.warps = 0; - if (run_threads(warp_thread, warp_thread, &a, &b) != 0) - return -1; - r->warp_iterations = a.iterations + b.iterations; - r->warps = g_warp.warps; - r->max_warp_cycles = g_warp.max_warp; - - atomic_store(&g_pp.seq, 0); - atomic_store(&g_pp.t2, 0); - atomic_store(&g_pp.ready, 0); - atomic_store(&g_pp.abort, 0); - r->min_rtt_cycles = UINT64_MAX; - r->lo_cycles = INT64_MIN; - r->hi_cycles = INT64_MAX; - a.iterations = 0; - if (run_threads(pp_leader, pp_follower, &a, &b) != 0) - return -1; - if (a.iterations == 0) { - r->stalled = true; - return 0; - } - qsort(offsets_x2, (size_t)a.iterations, sizeof(*offsets_x2), cmp_i64); - r->median_offset_cycles = (double)offsets_x2[(a.iterations - 1) / 2] / 2.0; - r->consistent = r->lo_cycles <= r->hi_cycles; - if (r->consistent) { - r->offset_cycles = ((double)r->lo_cycles + (double)r->hi_cycles) / 2.0; - r->uncertainty_cycles = ((double)r->hi_cycles - (double)r->lo_cycles) / 2.0; - } else { - r->offset_cycles = (double)r->min_rtt_offset_x2 / 2.0; - r->uncertainty_cycles = (double)r->min_rtt_cycles / 2.0; - } - return 0; -} - -static double abs_double(double value) -{ - return value < 0 ? -value : value; -} - -static int cmd_warp(int argc, char **argv) -{ - static int cpus[MAX_CPUS]; - uint64_t duration_ms = 100; - uint64_t bound_ns = 1000; - double tsc_hz = 0; - int ncpus = -1; - int pairs = 0; - int inconsistent = 0; - int stalled = 0; - uint64_t total_warps = 0; - uint64_t max_backward = 0; - double max_abs_offset = 0; - double max_uncertainty = 0; - double max_skew_bound = 0; - uint64_t budget_cycles; - uint64_t stall_cycles; - int64_t *offsets_x2; - char cpu_text[256]; - bool pass; - bool conclusive; - - for (int i = 2; i < argc; i++) { - const char *value; - - if ((value = option_value(argc, argv, &i, "--duration-ms"))) { - if (parse_u64_arg(value, &duration_ms) || duration_ms == 0 || - duration_ms > 600000) - return bad_option("warp", argv[i]); - } else if ((value = option_value(argc, argv, &i, "--cpus"))) { - if ((ncpus = parse_cpu_list(value, cpus, MAX_CPUS)) <= 0) - return bad_option("warp", argv[i]); - } else if ((value = option_value(argc, argv, &i, "--bound-ns"))) { - if (parse_u64_arg(value, &bound_ns)) - return bad_option("warp", argv[i]); - } else if ((value = option_value(argc, argv, &i, "--tsc-hz"))) { - if (parse_hz_arg(value, &tsc_hz)) - return bad_option("warp", argv[i]); - } else { - return bad_option("warp", argv[i]); - } - } - if (ncpus < 0 && (ncpus = affinity_cpus(cpus, MAX_CPUS)) <= 0) { - fprintf(stderr, "nvx-time-probe warp: sched_getaffinity: %s\n", strerror(errno)); - return EXIT_ERROR; - } - if (check_cpus_allowed("warp", cpus, ncpus) != 0) - return EXIT_USAGE; - detect_tsc_rate(tsc_hz); - if (g_tsc.hz <= 0) { - fprintf(stderr, "nvx-time-probe warp: unknown TSC rate; pass --tsc-hz\n"); - return EXIT_ERROR; - } - budget_cycles = (uint64_t)((double)duration_ms * g_tsc.hz / 1000.0); - stall_cycles = (uint64_t)(STALL_SECONDS * g_tsc.hz) + budget_cycles; - offsets_x2 = malloc(PP_MAX_SAMPLES * sizeof(*offsets_x2)); - if (!offsets_x2) { - fprintf(stderr, "nvx-time-probe warp: out of memory\n"); - return EXIT_ERROR; - } - format_cpu_list(cpus, ncpus, cpu_text, sizeof(cpu_text)); - printf("nvx-time-probe %s warp: cpus=%s pairs=%d duration_ms=%" PRIu64 - " (per test per pair) tsc_hz=%.0f tsc_hz_source=%s bound_ns=%" PRIu64 "\n", - PROBE_VERSION, cpu_text, ncpus * (ncpus - 1) / 2, duration_ms, g_tsc.hz, - g_tsc.source, bound_ns); - fflush(stdout); - - for (int i = 0; i < ncpus; i++) { - for (int j = i + 1; j < ncpus; j++) { - struct pair_result r; - double skew_bound; - - if (run_pair(cpus[i], cpus[j], budget_cycles, stall_cycles, offsets_x2, - &r) != 0) { - fprintf(stderr, "nvx-time-probe warp: pair %d-%d: %s\n", cpus[i], - cpus[j], strerror(errno)); - free(offsets_x2); - return EXIT_ERROR; - } - pairs++; - total_warps += r.warps; - if (r.max_warp_cycles > max_backward) - max_backward = r.max_warp_cycles; - if (r.stalled) { - stalled++; - printf("pair %d-%d: warp_iterations=%" PRIu64 " warps=%" PRIu64 - " max_backward_cycles=%" PRIu64 " ping_pong=stalled\n", - r.cpu_a, r.cpu_b, r.warp_iterations, r.warps, r.max_warp_cycles); - continue; - } - if (!r.consistent) - inconsistent++; - skew_bound = r.consistent - ? (abs_double((double)r.lo_cycles) > abs_double((double)r.hi_cycles) - ? abs_double((double)r.lo_cycles) - : abs_double((double)r.hi_cycles)) - : abs_double(r.offset_cycles) + r.uncertainty_cycles; - if (abs_double(r.offset_cycles) > max_abs_offset) - max_abs_offset = abs_double(r.offset_cycles); - if (r.uncertainty_cycles > max_uncertainty) - max_uncertainty = r.uncertainty_cycles; - if (skew_bound > max_skew_bound) - max_skew_bound = skew_bound; - printf("pair %d-%d: warp_iterations=%" PRIu64 " warps=%" PRIu64 - " max_backward_cycles=%" PRIu64 " max_backward_ns=%lld rounds=%" PRIu64 - " min_rtt_ns=%lld offset_cycles=%.1f offset_ns=%lld uncertainty_ns=%lld" - " bounds_ns=[%lld,%lld] median_offset_ns=%lld consistent=%d\n", - r.cpu_a, r.cpu_b, r.warp_iterations, r.warps, r.max_warp_cycles, - round_ll(cycles_to_ns((double)r.max_warp_cycles)), r.rounds, - round_ll(cycles_to_ns((double)r.min_rtt_cycles)), r.offset_cycles, - round_ll(cycles_to_ns(r.offset_cycles)), - round_ll(cycles_to_ns(r.uncertainty_cycles)), - round_ll(cycles_to_ns((double)r.lo_cycles)), - round_ll(cycles_to_ns((double)r.hi_cycles)), - round_ll(cycles_to_ns(r.median_offset_cycles)), r.consistent); - fflush(stdout); - } - } - free(offsets_x2); - if (pairs == 0) - printf("single CPU: no pairs to compare\n"); - - pass = stalled == 0 && cycles_to_ns((double)max_backward) <= (double)bound_ns && - cycles_to_ns(max_abs_offset) <= (double)bound_ns; - // The offset estimate decides nothing when its uncertainty reaches the bound - // (for example, under trapped RDTSC, where every read exits to the VMM). - conclusive = pairs == 0 || (stalled == 0 && cycles_to_ns(max_uncertainty) < (double)bound_ns); - if (!conclusive) - printf("warning: offset uncertainty (+/-%lld ns) is not below the bound (%" PRIu64 - " ns); the verdict is not conclusive (trapped RDTSC or a descheduled vCPU?)\n", - round_ll(cycles_to_ns(max_uncertainty)), bound_ns); - printf("NVX-TIME-PROBE warp max_backward_cycles=%" PRIu64 " max_backward_ns=%lld" - " max_abs_offset_ns=%lld pairs=%d verdict=%s bound_ns=%" PRIu64 "\n", - max_backward, round_ll(cycles_to_ns((double)max_backward)), - round_ll(cycles_to_ns(max_abs_offset)), pairs, pass ? "PASS" : "FAIL", - bound_ns); - printf("NVX-TIME-PROBE warp-detail max_abs_offset_cycles=%.1f max_uncertainty_ns=%lld" - " max_skew_bound_ns=%lld total_warps=%" PRIu64 " inconsistent_pairs=%d" - " stalled_pairs=%d cpus=%s duration_ms=%" PRIu64 " tsc_hz=%.0f" - " tsc_hz_source=%s conclusive=%d\n", - max_abs_offset, round_ll(cycles_to_ns(max_uncertainty)), - round_ll(cycles_to_ns(max_skew_bound)), total_warps, inconsistent, stalled, - cpu_text, duration_ms, g_tsc.hz, g_tsc.source, conclusive); - return pass ? 0 : EXIT_FAIL; -} - -// --------------------------------------------------------------------------- -// clock -// --------------------------------------------------------------------------- - -static const char *const tsc_flag_names[] = { - "tsc", "rdtscp", "constant_tsc", "nonstop_tsc", "nonstop_tsc_s3", - "tsc_known_freq", "tsc_reliable", "tsc_deadline_timer", "tsc_adjust", "art", - "hypervisor", "arat", "aperfmperf", "hwp", "x2apic", - NULL, -}; - -static const char *const hypervisor_patterns[] = { - "Hypervisor detected", "Hyper-V", "Booting paravirtualized kernel", "kvm-guest", - NULL, -}; - -static const char *const time_patterns[] = { - "tsc", "TSC", "clocksource", "clockevent", "kvm-clock", - "APIC timer", "LAPIC", "lapic", "calibrat", "Calibrat", - "unchecked MSR", "hpet", "HPET", "timekeeping", "sched_clock", - "detected stall", "watchdog", "lockup", "hung_task", "jiffies", - "rtc", "RTC", NULL, -}; - -static const char *const param_files[] = { - "/sys/module/rcupdate/parameters/rcu_cpu_stall_suppress", - "/sys/module/rcupdate/parameters/rcu_cpu_stall_timeout", - "/proc/sys/kernel/watchdog", - "/proc/sys/kernel/soft_watchdog", - "/proc/sys/kernel/nmi_watchdog", - "/proc/sys/kernel/watchdog_thresh", - "/proc/sys/kernel/hung_task_timeout_secs", - NULL, -}; - -struct kmsg_filter { - const char *const *patterns; - int matches; - int tsc_unstable; - int unchecked_msr; -}; - -struct tick_info { - bool seen; - int tick_mode; - int state; - int hres_active; - int nohz; - char device[64]; - char handler[64]; -}; - -static void add_unique(char *list, size_t size, const char *item) -{ - size_t len = strlen(item); - - for (const char *p = list; (p = strstr(p, item)) != NULL; p += len) - if ((p == list || p[-1] == ',') && (p[len] == ',' || p[len] == '\0')) - return; - if (list[0]) - strncat(list, ",", size - strlen(list) - 1); - strncat(list, item, size - strlen(list) - 1); -} - -static void sanitize_token(char *text) -{ - for (char *p = text; *p; p++) - if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || - (*p >= '0' && *p <= '9') || *p == '#' || *p == '-' || *p == '.')) - *p = '_'; -} - -static void print_clock(const char *name, clockid_t id, struct timespec *out) -{ - struct timespec ts; - - if (clock_gettime(id, &ts) != 0) { - printf("%-20s unavailable (%s)\n", name, strerror(errno)); - return; - } - if (out) - *out = ts; - printf("%-20s %lld.%09ld", name, (long long)ts.tv_sec, ts.tv_nsec); - if (id == CLOCK_REALTIME) { - struct tm tm; - char iso[32]; - time_t seconds = ts.tv_sec; - - gmtime_r(&seconds, &tm); - strftime(iso, sizeof(iso), "%Y-%m-%dT%H:%M:%S", &tm); - printf(" (%s.%09ldZ)", iso, ts.tv_nsec); - } - printf("\n"); -} - -static void kmsg_filter_cb(uint64_t usec, const char *text, void *ctx) -{ - struct kmsg_filter *filter = ctx; - - if (strstr(text, "Marking TSC unstable") || strstr(text, "TSC unstable")) - filter->tsc_unstable++; - if (strstr(text, "unchecked MSR access")) - filter->unchecked_msr++; - for (const char *const *pattern = filter->patterns; *pattern; pattern++) { - if (strstr(text, *pattern)) { - printf("[%5llu.%06llu] %s\n", (unsigned long long)(usec / 1000000), - (unsigned long long)(usec % 1000000), text); - filter->matches++; - return; - } - } -} - -static const char *tick_mode_name(int mode) -{ - return mode == 0 ? "periodic" : mode == 1 ? "oneshot" : "unknown"; -} - -static const char *clockevent_state_name(int state) -{ - static const char *const names[] = {"detached", "shutdown", "periodic", "oneshot", - "oneshot_stopped"}; - - return state >= 0 && state <= 4 ? names[state] : "unknown"; -} - -static void print_timer_list(char *summary, size_t size) -{ - static struct tick_info cpus[MAX_CPUS]; - struct tick_info broadcast = {false, -1, -1, -1, -1, "", ""}; - struct tick_info *cur = NULL; - char devices[256] = ""; - char modes[64] = ""; - char states[128] = ""; - char line[512]; - int hr_cpu = -1; - int tick_mode = -1; - int count = 0; - FILE *file = fopen("/proc/timer_list", "r"); - - if (!file) { - printf("timer_list: unavailable (%s)\n", strerror(errno)); - snprintf(summary, size, "clockevents=unavailable"); - return; - } - for (int i = 0; i < MAX_CPUS; i++) - cpus[i] = (struct tick_info){false, -1, -1, -1, -1, "", ""}; - while (fgets(line, sizeof(line), file)) { - char name[64]; - int value; - - line[strcspn(line, "\n")] = '\0'; - if (strncmp(line, "cpu: ", 5) == 0) { - hr_cpu = atoi(line + 5); - cur = NULL; - } else if (strncmp(line, "Tick Device: mode:", 18) == 0) { - tick_mode = atoi(line + 18); - hr_cpu = -1; - cur = NULL; - } else if (strncmp(line, "Broadcast device", 16) == 0) { - cur = &broadcast; - cur->seen = true; - cur->tick_mode = tick_mode; - } else if (sscanf(line, "Per CPU device: %d", &value) == 1) { - cur = value >= 0 && value < MAX_CPUS ? &cpus[value] : NULL; - if (cur) { - cur->seen = true; - cur->tick_mode = tick_mode; - } - } else if (cur && sscanf(line, "Clock Event Device: %63s", name) == 1) { - snprintf(cur->device, sizeof(cur->device), "%s", name); - } else if (cur && sscanf(line, " mode: %d", &value) == 1) { - cur->state = value; - } else if (cur && sscanf(line, " event_handler: %63s", name) == 1) { - snprintf(cur->handler, sizeof(cur->handler), "%s", name); - } else if (hr_cpu >= 0 && hr_cpu < MAX_CPUS) { - if (sscanf(line, " .hres_active : %d", &value) == 1) - cpus[hr_cpu].hres_active = value; - else if (sscanf(line, " .nohz_mode : %d", &value) == 1 || - sscanf(line, " .nohz : %d", &value) == 1) - cpus[hr_cpu].nohz = value; - } - } - fclose(file); - - if (broadcast.seen) - printf("broadcast: device=%s state=%s tick_mode=%s handler=%s\n", - broadcast.device[0] ? broadcast.device : "none", - clockevent_state_name(broadcast.state), tick_mode_name(broadcast.tick_mode), - broadcast.handler[0] ? broadcast.handler : "none"); - for (int i = 0; i < MAX_CPUS; i++) { - if (!cpus[i].seen) - continue; - count++; - printf("cpu%d: device=%s state=%s tick_mode=%s handler=%s hres_active=%d nohz=%d\n", - i, cpus[i].device[0] ? cpus[i].device : "none", - clockevent_state_name(cpus[i].state), tick_mode_name(cpus[i].tick_mode), - cpus[i].handler[0] ? cpus[i].handler : "none", cpus[i].hres_active, - cpus[i].nohz); - add_unique(devices, sizeof(devices), cpus[i].device[0] ? cpus[i].device : "none"); - add_unique(modes, sizeof(modes), tick_mode_name(cpus[i].tick_mode)); - add_unique(states, sizeof(states), clockevent_state_name(cpus[i].state)); - } - if (!broadcast.seen && count == 0) - printf("timer_list: no tick devices found (file empty or masked)\n"); - snprintf(summary, size, - "clockevent_cpus=%d clockevent_devices=%s tick_modes=%s clockevent_states=%s" - " broadcast=%s:%s", - count, devices[0] ? devices : "none", modes[0] ? modes : "none", - states[0] ? states : "none", - broadcast.seen && broadcast.device[0] ? broadcast.device : "none", - broadcast.seen ? clockevent_state_name(broadcast.state) : "none"); -} - -struct cpuid_summary { - char vendor[16]; - uint32_t max_leaf; - uint32_t features_eax; - uint32_t features_edx; - bool hypervisor; -}; - -static void print_cpuid(struct cpuid_summary *s) -{ - uint32_t r[4]; - uint32_t max_basic, max_ext, family, model; - - memset(s, 0, sizeof(*s)); - cpuid(0, 0, r); - max_basic = r[0]; - cpuid(0x80000000u, 0, r); - max_ext = r[0]; - cpuid(1, 0, r); - family = (r[0] >> 8) & 0xf; - model = (r[0] >> 4) & 0xf; - if (family == 0x6 || family == 0xf) - model |= (r[0] >> 12) & 0xf0; - if (family == 0xf) - family += (r[0] >> 20) & 0xff; - s->hypervisor = (r[2] >> 31) & 1; - printf("cpuid.1: family=%u model=%u stepping=%u edx.tsc=%u ecx.tsc_deadline=%u" - " ecx.hypervisor=%u\n", - family, model, r[0] & 0xf, (r[3] >> 4) & 1, (r[2] >> 24) & 1, s->hypervisor); - if (max_basic >= 6) { - cpuid(6, 0, r); - printf("cpuid.6: eax.arat=%u eax.hwp=%u ecx.aperfmperf=%u\n", (r[0] >> 2) & 1, - (r[0] >> 7) & 1, r[2] & 1); - } - if (max_basic >= 7) { - cpuid(7, 0, r); - printf("cpuid.7.0: ebx.tsc_adjust=%u\n", (r[1] >> 1) & 1); - } - if (max_basic >= 0xa) { - cpuid(0xa, 0, r); - printf("cpuid.0xa: eax=%#010x (pmu_version=%u)\n", r[0], r[0] & 0xff); - } - if (max_basic >= 0x15) { - cpuid(0x15, 0, r); - printf("cpuid.0x15: eax.denominator=%u ebx.numerator=%u ecx.crystal_hz=%u", r[0], - r[1], r[2]); - if (r[0] && r[1] && r[2]) - printf(" (tsc_hz=%.0f)", (double)r[2] * r[1] / r[0]); - printf("\n"); - } - if (max_basic >= 0x16) { - cpuid(0x16, 0, r); - printf("cpuid.0x16: eax.base_mhz=%u ebx.max_mhz=%u ecx.bus_mhz=%u\n", - r[0] & 0xffff, r[1] & 0xffff, r[2] & 0xffff); - } - if (max_ext >= 0x80000001u) { - cpuid(0x80000001u, 0, r); - printf("cpuid.0x80000001: edx.rdtscp=%u\n", (r[3] >> 27) & 1); - } - if (max_ext >= 0x80000007u) { - cpuid(0x80000007u, 0, r); - printf("cpuid.0x80000007: edx.invariant_tsc=%u\n", (r[3] >> 8) & 1); - } - if (!s->hypervisor) { - printf("cpuid.0x40000000: not queried (hypervisor bit clear)\n"); - snprintf(s->vendor, sizeof(s->vendor), "none"); - return; - } - cpuid(0x40000000u, 0, r); - s->max_leaf = r[0]; - memcpy(s->vendor, &r[1], 4); - memcpy(s->vendor + 4, &r[2], 4); - memcpy(s->vendor + 8, &r[3], 4); - s->vendor[12] = '\0'; - for (int i = 0; i < 12 && s->vendor[i]; i++) - if (s->vendor[i] < 0x20 || s->vendor[i] > 0x7e) - s->vendor[i] = '.'; - printf("cpuid.0x40000000: eax.max_leaf=%#x vendor=\"%s\"\n", s->max_leaf, s->vendor); - for (uint32_t leaf = 0x40000001u; leaf <= s->max_leaf && leaf <= 0x40000010u; leaf++) { - cpuid(leaf, 0, r); - printf("cpuid.%#x: eax=%#010x ebx=%#010x ecx=%#010x edx=%#010x\n", leaf, r[0], - r[1], r[2], r[3]); - if (leaf == 0x40000003u) { - s->features_eax = r[0]; - s->features_edx = r[3]; - } - } - if (strcmp(s->vendor, "Microsoft Hv") == 0 && s->max_leaf >= 0x40000003u) - printf("hyperv.features: hypercall=%u vp_index=%u access_frequency_msrs=%u" - " access_tsc_invariant=%u frequency_msrs_available=%u\n", - (s->features_eax >> 5) & 1, (s->features_eax >> 6) & 1, - (s->features_eax >> 11) & 1, (s->features_eax >> 15) & 1, - (s->features_edx >> 8) & 1); -} - -// Opt-in: reads go through the msr driver's rdmsr_safe(), so a #GP returns -// EIO instead of faulting in the guest. -static void print_msrs(void) -{ - static const struct { - uint32_t index; - const char *name; - } msrs[] = { - {0x10, "IA32_TSC"}, - {0x3b, "IA32_TSC_ADJUST"}, - {0x40000002u, "HV_X64_MSR_VP_INDEX"}, - {0x40000022u, "HV_X64_MSR_TSC_FREQUENCY"}, - {0x40000023u, "HV_X64_MSR_APIC_FREQUENCY"}, - {0x40000118u, "HV_X64_MSR_TSC_INVARIANT_CONTROL"}, - {0x4b564d01u, "MSR_KVM_SYSTEM_TIME_NEW"}, - }; - int fd = open("/dev/cpu/0/msr", O_RDONLY | O_CLOEXEC); - - if (fd < 0) { - printf("msr: /dev/cpu/0/msr unavailable (%s)\n", strerror(errno)); - return; - } - for (size_t i = 0; i < sizeof(msrs) / sizeof(msrs[0]); i++) { - uint64_t value; - - if (pread(fd, &value, sizeof(value), (off_t)msrs[i].index) == sizeof(value)) - printf("msr %#010x %-34s = %#" PRIx64 " (%" PRIu64 ")\n", msrs[i].index, - msrs[i].name, value, value); - else - printf("msr %#010x %-34s : %s\n", msrs[i].index, msrs[i].name, - errno == EIO ? "read faulted (#GP)" : strerror(errno)); - } - close(fd); -} - -static int cmd_clock(int argc, char **argv) -{ - uint64_t measure_ms = 200; - double tsc_hz = 0; - bool read_msrs = false; - struct timespec mono = {0}; - struct kmsg_filter hv_filter = {hypervisor_patterns, 0, 0, 0}; - struct kmsg_filter time_filter = {time_patterns, 0, 0, 0}; - struct cpuid_summary ids; - char clocksource[64]; - char available[256] = ""; - char present[256] = ""; - char absent[256] = ""; - char timer_summary[512]; - char line[4096]; - uint64_t tsc; - int cpu; - - for (int i = 2; i < argc; i++) { - const char *value; - - if ((value = option_value(argc, argv, &i, "--measure-ms"))) { - if (parse_u64_arg(value, &measure_ms) || measure_ms > 60000) - return bad_option("clock", argv[i]); - } else if ((value = option_value(argc, argv, &i, "--tsc-hz"))) { - if (parse_hz_arg(value, &tsc_hz)) - return bad_option("clock", argv[i]); - } else if (strcmp(argv[i], "--msr") == 0) { - read_msrs = true; - } else { - return bad_option("clock", argv[i]); - } - } - detect_tsc_rate(tsc_hz); - - printf("nvx-time-probe %s clock\n== clocks\n", PROBE_VERSION); - cpu = sched_getcpu(); - tsc = rdtsc_fenced(); - print_clock("CLOCK_REALTIME", CLOCK_REALTIME, NULL); - print_clock("CLOCK_MONOTONIC", CLOCK_MONOTONIC, &mono); - print_clock("CLOCK_MONOTONIC_RAW", CLOCK_MONOTONIC_RAW, NULL); - print_clock("CLOCK_BOOTTIME", CLOCK_BOOTTIME, NULL); - printf("%-20s %" PRIu64 " on cpu %d", "TSC", tsc, cpu); - if (g_tsc.hz > 0) - printf(" (%.9f s; tsc_minus_monotonic_ns=%lld)", (double)tsc / g_tsc.hz, - round_ll((double)tsc * 1e9 / g_tsc.hz - - ((double)mono.tv_sec * 1e9 + (double)mono.tv_nsec))); - printf("\ntsc_hz=%.0f tsc_hz_source=%s\n", g_tsc.hz, g_tsc.source); - if (measure_ms > 0) - printf("tsc_hz_measured=%.0f (over %" PRIu64 " ms against CLOCK_MONOTONIC_RAW)\n", - measure_tsc_hz((unsigned)measure_ms), measure_ms); - - printf("== clocksource\n"); - current_clocksource(clocksource, sizeof(clocksource)); - if (read_first_line(CLOCKSOURCE_DIR "/available_clocksource", line, sizeof(line)) == 0) - for (char *token = strtok(line, " "); token; token = strtok(NULL, " ")) - add_unique(available, sizeof(available), token); - printf("current: %s\navailable: %s\n", clocksource, available[0] ? available : "unknown"); - - printf("== cpuinfo\n"); - { - FILE *file = fopen("/proc/cpuinfo", "r"); - int processors = 0; - bool model_printed = false; - char mhz[512] = ""; - - while (file && fgets(line, sizeof(line), file)) { - char *colon = strchr(line, ':'); - - line[strcspn(line, "\n")] = '\0'; - if (strncmp(line, "processor", 9) == 0) - processors++; - else if (!model_printed && strncmp(line, "model name", 10) == 0 && colon) { - printf("model name:%s\n", colon + 1); - model_printed = true; - } else if (strncmp(line, "cpu MHz", 7) == 0 && colon) - add_unique(mhz, sizeof(mhz), colon + 2); - } - if (file) - fclose(file); - printf("processors: %d\ncpu MHz: %s\n", processors, mhz[0] ? mhz : "unknown"); - } - for (const char *const *flag = tsc_flag_names; *flag; flag++) - add_unique(cpuinfo_has_flag(*flag) ? present : absent, - cpuinfo_has_flag(*flag) ? sizeof(present) : sizeof(absent), *flag); - printf("time flags present: %s\ntime flags absent: %s\n", present[0] ? present : "none", - absent[0] ? absent : "none"); - - printf("== cpuid\n"); - print_cpuid(&ids); - if (read_msrs) { - printf("== msr (cpu 0)\n"); - print_msrs(); - } - - printf("== kernel\n"); - if (read_first_line("/proc/cmdline", line, sizeof(line)) == 0) - printf("cmdline: %s\n", line); - if (read_first_line("/sys/devices/system/cpu/online", line, sizeof(line)) == 0) - printf("online cpus: %s\n", line); - for (const char *const *path = param_files; *path; path++) { - if (read_first_line(*path, line, sizeof(line)) == 0) - printf("%s = %s\n", *path, line); - else - printf("%s: absent\n", *path); - } - - printf("== kmsg: hypervisor\n"); - if (kmsg_foreach(kmsg_filter_cb, &hv_filter) != 0) - printf("(kernel log unavailable: %s)\n", strerror(errno)); - else if (!hv_filter.matches) - printf("(none)\n"); - printf("== kmsg: time\n"); - if (kmsg_foreach(kmsg_filter_cb, &time_filter) != 0) - printf("(kernel log unavailable: %s)\n", strerror(errno)); - else if (!time_filter.matches) - printf("(none)\n"); - - printf("== timer_list\n"); - print_timer_list(timer_summary, sizeof(timer_summary)); - - sanitize_token(ids.vendor); - printf("NVX-TIME-PROBE clock clocksource=%s available=%s tsc_hz=%.0f tsc_hz_source=%s" - " hv_vendor=%s hv_max_leaf=%#x hv_features_eax=%#x hv_features_edx=%#x" - " flags=%s missing_flags=%s tsc_unstable_msgs=%d unchecked_msr_msgs=%d %s\n", - clocksource, available[0] ? available : "unknown", g_tsc.hz, g_tsc.source, - ids.vendor, ids.max_leaf, ids.features_eax, ids.features_edx, - present[0] ? present : "none", absent[0] ? absent : "none", - time_filter.tsc_unstable, time_filter.unchecked_msr, timer_summary); - return 0; -} - -// --------------------------------------------------------------------------- -// main -// --------------------------------------------------------------------------- - -static void usage(FILE *out) -{ - fprintf(out, - "usage: nvx-time-probe COMMAND [OPTIONS]\n" - "\n" - "Commands:\n" - " latency [--iterations N] [--batch B] [--cpu C] [--tsc-hz HZ]\n" - " Per-operation cost (p50/p99/min/mean ns) of rdtsc, rdtscp,\n" - " lfence;rdtsc, clock_gettime(MONOTONIC/REALTIME) through the vDSO,\n" - " and syscall controls. Default: 2000 timed batches of 50 operations\n" - " on the current CPU.\n" - " warp [--duration-ms N] [--cpus LIST] [--bound-ns N] [--tsc-hz HZ]\n" - " Pairwise cross-CPU TSC checks: a check_tsc_warp-style spinlock test\n" - " and a ping-pong offset estimate, each running N ms per pair\n" - " (default 100). LIST looks like 0-3,6 (default: every CPU in the\n" - " affinity mask). The bound defaults to 1000 ns.\n" - " clock [--measure-ms N] [--tsc-hz HZ] [--msr]\n" - " Clocks, clocksources, TSC flags, CPUID time leaves, hypervisor and\n" - " time kernel messages, and the /proc/timer_list clock event summary.\n" - " --msr also reads the TSC, Hyper-V time, and kvmclock MSRs on CPU 0\n" - " through /dev/cpu/0/msr (a #GP is reported, not raised).\n" - " version\n" - "\n" - "The TSC rate comes from --tsc-hz, sysfs tsc_freq_khz, the kernel log, or\n" - "/proc/cpuinfo, in that order, and is measured as a last resort.\n" - "Exit status: 0 success, 1 warp FAIL, 2 usage error, 3 runtime error.\n"); -} - -int main(int argc, char **argv) -{ - setvbuf(stdout, NULL, _IOLBF, 0); - if (argc < 2) { - usage(stderr); - return EXIT_USAGE; - } - if (strcmp(argv[1], "latency") == 0) - return cmd_latency(argc, argv); - if (strcmp(argv[1], "warp") == 0) - return cmd_warp(argc, argv); - if (strcmp(argv[1], "clock") == 0) - return cmd_clock(argc, argv); - if (strcmp(argv[1], "version") == 0 || strcmp(argv[1], "--version") == 0) { - printf("nvx-time-probe %s\n", PROBE_VERSION); - return 0; - } - if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0 || - strcmp(argv[1], "-h") == 0) { - usage(stdout); - return 0; - } - usage(stderr); - return EXIT_USAGE; -} diff --git a/guest/common/nvx-time.c b/guest/common/nvx-time.c deleted file mode 100644 index ce75ee163..000000000 --- a/guest/common/nvx-time.c +++ /dev/null @@ -1,5076 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// nvx-time: the guest time component of NVX time ABI v1. -// -// doc/design/3.2-clock-and-time.md ("Guest Obligations") is the contract this -// helper enforces. It runs the conformance checks, the violation watcher and -// the wall-clock discipline (one daemon), and the snapshot agent's time steps: -// the guest preparation steps of doc/design/5.3-snapshot-protocol.md and the -// guest repair steps of doc/design/5.5-restore-protocol.md, which the comments -// below cite by number. It prints nothing on success: checks record their -// result in the state file, and only violation events reach the console. This -// file defines the check IDs, the guest codes, and the formats of its output -// lines and the state file. -// -// boot initial clock step (C12), then the other boot -// checks asynchronously, which start the daemon -// check --phase PHASE [--new-cpus LIST] -// capture or restore checks, for diagnosis -// pre-capture capture checks and watchdog suppression -// capture REQUEST GENERATION_ID ENTROPY_FILE [--finish] -// capture request, then restore repair -// cancel-capture undo pre-capture after a rejected capture -// restore-finish [--ack] [--new-cpus LIST] -// acknowledgement, then the daemon's restore -// checks and release -// status print every recorded check and the runtime -// state, after waiting for pending checks -// sample print one host time sample -// generation-id print the VM generation ID -// exhaustive the CI-only exhaustive check -// test NAME ARGUMENTS... pure decoders and checks, for unit tests -// -// --report-only, or nvx_time_abi=report-only on the kernel command line, -// reports failures without powering the VM off and prints NVX-TIME-REPORT -// instead of NVX-TIME-ABI, so that its output never passes for a conformant -// run. Every portb transfer uses four-byte reads: a restore packet without -// memory ranges costs 24 data-port exits. - -#define _GNU_SOURCE - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef SYS_pidfd_open -#define SYS_pidfd_open 434 -#endif - -#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) - -#define NSEC_PER_SEC INT64_C(1000000000) -#define NSEC_PER_MSEC INT64_C(1000000) -#define NSEC_PER_USEC INT64_C(1000) -#define MAX_CPUS 64 - -#define STATUS_CONFORMANCE 193 -#define STATUS_VIOLATION 194 -#define STATUS_REPAIR 195 - -#define PORTB_DATA 0xe9 -#define PORTB_STATUS 0xea -#define PORTB_WINDOW 0xeb -#define PORT_SHUTDOWN 0x604 -#define PORT_SNAPSHOT 0x605 -#define SELECT_PACKET 0xa5 -#define SELECT_GENERATION_ID 0xa6 -#define SELECT_TIME_SAMPLE 0xa7 -#define PORTB_PACKET_AVAILABLE 0x02 -#define PORTB_GENERATION_ID_AVAILABLE 0x20 -#define PORTB_TIME_WINDOW 0x40 -#define SNAPSHOT_ACKNOWLEDGE 2 - -#define PACKET_HEADER_SIZE 32 -#define PACKET_RANGE_SIZE 16 -#define PACKET_ENTROPY_SIZE 64 -#define PACKET_MAX_RANGES 255 -#define PACKET_MAX_SIZE \ - (PACKET_HEADER_SIZE + PACKET_MAX_RANGES * PACKET_RANGE_SIZE + \ - PACKET_ENTROPY_SIZE) -#define PACKET_DOWNTIME_UTC 0x01 -#define PACKET_MEMORY_TARGET 0x02 -#define PACKET_ACK_REQUIRED 0x04 -#define PACKET_TEST_HOOKS 0x08 -#define PACKET_KNOWN_FLAGS 0x0f -#define SAMPLE_SIZE 16 -#define SAMPLE_TEST_HOOKS 0x08 -#define GENERATION_ID_SIZE 16 - -#define RATE_DEVIATION_LIMIT INT64_C(16384000) -#define FREQUENCY_LIMIT (INT64_C(500) << 16) -#define DOWNTIME_LIMIT_NS (INT64_C(2592000) * NSEC_PER_SEC) -#define REPAIR_BOUND_NS NSEC_PER_MSEC -#define BOOT_BOUND_NS NSEC_PER_MSEC -#define POLL_BOUND_NS (50 * NSEC_PER_USEC) -#define SAMPLE_ATTEMPTS 3 -#define STEP_THRESHOLD_NS (128 * NSEC_PER_MSEC) -#define SYNCHRONIZED_AGE_NS (128 * NSEC_PER_SEC) -#define FAST_PERIOD_S 16 -#define SLOW_PERIOD_S 64 -#define FAST_POLLS 4 -#define FAST_TIME_CONSTANT 4 -#define SLOW_TIME_CONSTANT 6 -#define TIMER_LIST_WAIT_NS (200 * NSEC_PER_MSEC) -#define DEFAULT_STALL_TIMEOUT_S 21 -// The asynchronous boot checks start 150 ms after the time ABI boot step, -// which init runs shortly before shell-ready, and repair step 7 150 ms -// after the acknowledgement: earlier, they contend with the readiness path -// while the VMM still demand-faults guest memory, and with the workload's own -// first activity after readiness. Both start at SCHED_IDLE and continue at -// normal priority 100 ms after they start, which bounds the fail-fast window -// under a CPU-bound workload. -#define DEFERRED_START_NS (150 * NSEC_PER_MSEC) -#define IDLE_BOUND_NS (100 * NSEC_PER_MSEC) -// nvx-time status and pre-capture wait at most 30 s for pending checks. -#define STATUS_WAIT_NS (30 * NSEC_PER_SEC) -#define STATUS_POLL_NS (10 * NSEC_PER_MSEC) -#define EVENT_MAX 512 -#define DETAIL_MAX 384 -#define TEXT_MAX 65536 - -#define RUN_DIR "/run/nvx" -#define TIME_DIR RUN_DIR "/time" -#define STATE_PATH TIME_DIR "/state" -#define STATE_TEMP_PATH TIME_DIR "/.state" -#define STATE_LOCK_PATH TIME_DIR "/state.lock" -#define DAEMON_PID_PATH TIME_DIR "/daemon.pid" -#define SUPPRESSION_PATH TIME_DIR "/suppression" -#define SUPPRESSION_LOCK_PATH TIME_DIR "/suppression.lock" -#define RESTORE_PATH TIME_DIR "/restore" -#define PORTB_LOCK_PATH RUN_DIR "/portb.lock" -#define RCU_SYNC_PATH RUN_DIR "/rcu-sync" - -#define RCU_STALL_SUPPRESS "/sys/module/rcupdate/parameters/rcu_cpu_stall_suppress" -#define RCU_STALL_TIMEOUT "/sys/module/rcupdate/parameters/rcu_cpu_stall_timeout" -#define RCU_NORMAL "/sys/kernel/rcu_normal" -#define RCU_STALL_COUNT "/sys/kernel/rcu_stall_count" -#define SOFT_WATCHDOG "/proc/sys/kernel/soft_watchdog" -#define HUNG_TASK_TIMEOUT "/proc/sys/kernel/hung_task_timeout_secs" -#define CLOCKSOURCE_DIR "/sys/devices/system/clocksource/clocksource0" - -enum phase { PHASE_BOOT, PHASE_CAPTURE, PHASE_RESTORE, PHASE_RUNTIME }; - -static const char *const k_phase_names[] = {"boot", "capture", "restore", - "runtime"}; - -static bool g_report_only; -static bool g_test; -static uint32_t g_generation; -static bool g_ports; -// Set in the daemon. Its lines can arrive while a shell or workload is in the -// middle of a console line, so each one starts with a newline. -static bool g_async_output; - -// --------------------------------------------------------------------------- -// Small utilities -// --------------------------------------------------------------------------- - -static int64_t clock_ns(clockid_t clock) -{ - struct timespec now; - - clock_gettime(clock, &now); - return (int64_t)now.tv_sec * NSEC_PER_SEC + now.tv_nsec; -} - -// The CPU time of this process, every thread's, exited threads' included: -// what a check's cpu_us reports. -static int64_t cpu_time_ns(void) -{ - return clock_ns(CLOCK_PROCESS_CPUTIME_ID); -} - -static int write_all(int fd, const void *data, size_t size) -{ - const char *bytes = data; - - while (size > 0) { - ssize_t count = write(fd, bytes, size); - - if (count < 0 && errno == EINTR) - continue; - if (count <= 0) - return -1; - bytes += count; - size -= (size_t)count; - } - return 0; -} - -static ssize_t read_bytes(const char *path, void *buffer, size_t size) -{ - int fd = open(path, O_RDONLY | O_CLOEXEC); - size_t total = 0; - - if (fd < 0) - return -1; - while (total < size) { - ssize_t count = read(fd, (char *)buffer + total, size - total); - - if (count < 0 && errno == EINTR) - continue; - if (count < 0) { - int error = errno; - - close(fd); - errno = error; - return -1; - } - if (count == 0) - break; - total += (size_t)count; - } - close(fd); - return (ssize_t)total; -} - -static ssize_t read_text(const char *path, char *buffer, size_t size) -{ - ssize_t count = read_bytes(path, buffer, size - 1); - - if (count < 0) - return -1; - buffer[count] = '\0'; - return count; -} - -typedef void (*line_consumer)(void *context, char *line); - -// Passes each line of TEXT to CONSUME, splitting TEXT in place. -static void for_each_text_line(char *text, line_consumer consume, - void *context) -{ - for (char *line = text; line != NULL && *line != '\0';) { - char *next = strchr(line, '\n'); - - if (next != NULL) - *next++ = '\0'; - consume(context, line); - line = next; - } -} - -// Streams the file at PATH into CONSUME one line at a time, so files whose -// size depends on the workload (/proc/timer_list) or on the CPU count -// (/proc/cpuinfo) are never truncated. Longer lines than the line buffer are -// cut. Returns -1 with errno set on failure. -static int for_each_file_line(const char *path, line_consumer consume, - void *context) -{ - char chunk[16384]; - char line[8192]; - size_t length = 0; - int fd = open(path, O_RDONLY | O_CLOEXEC); - - if (fd < 0) - return -1; - for (;;) { - ssize_t count = read(fd, chunk, sizeof(chunk)); - - if (count < 0 && errno == EINTR) - continue; - if (count < 0) { - int error = errno; - - close(fd); - errno = error; - return -1; - } - if (count == 0) - break; - for (ssize_t index = 0; index < count; index++) { - if (chunk[index] == '\n') { - line[length] = '\0'; - consume(context, line); - length = 0; - } else if (length < sizeof(line) - 1) { - line[length++] = chunk[index]; - } - } - } - close(fd); - if (length > 0) { - line[length] = '\0'; - consume(context, line); - } - return 0; -} - -static int parse_int64(const char *text, int64_t *value) -{ - char *end; - - errno = 0; - *value = strtoll(text, &end, 10); - if (errno != 0 || end == text) - return -1; - while (*end == '\n' || *end == ' ') - end++; - return *end == '\0' ? 0 : -1; -} - -static int read_int64(const char *path, int64_t *value) -{ - char text[64]; - - if (read_text(path, text, sizeof(text)) < 0) - return -1; - return parse_int64(text, value); -} - -static int write_text(const char *path, const char *text) -{ - int fd = open(path, O_WRONLY | O_CLOEXEC); - int result; - - if (fd < 0) - return -1; - result = write_all(fd, text, strlen(text)); - if (close(fd) != 0) - result = -1; - return result; -} - -static int write_int64(const char *path, int64_t value) -{ - char text[32]; - - snprintf(text, sizeof(text), "%" PRId64 "\n", value); - return write_text(path, text); -} - -// Atomically replaces PATH with TEXT through a temporary file in its directory. -static int replace_file(const char *path, const char *temporary, - const char *text) -{ - int fd = open(temporary, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); - int result; - - if (fd < 0) - return -1; - result = write_all(fd, text, strlen(text)); - if (close(fd) != 0) - result = -1; - if (result == 0 && rename(temporary, path) != 0) - result = -1; - if (result != 0) - unlink(temporary); - return result; -} - -static void make_runtime_directories(void) -{ - (void)mkdir(RUN_DIR, 0755); - (void)mkdir(TIME_DIR, 0755); -} - -static bool ends_with(const char *text, const char *suffix) -{ - size_t length = strlen(text); - size_t suffix_length = strlen(suffix); - - return length >= suffix_length && - strcmp(text + length - suffix_length, suffix) == 0; -} - -static uint32_t read_le32(const uint8_t *bytes) -{ - return (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 | - (uint32_t)bytes[2] << 16 | (uint32_t)bytes[3] << 24; -} - -static uint64_t read_le64(const uint8_t *bytes) -{ - return (uint64_t)read_le32(bytes) | (uint64_t)read_le32(bytes + 4) << 32; -} - -static void format_hex(const uint8_t *bytes, size_t size, char *text) -{ - static const char digits[] = "0123456789abcdef"; - - for (size_t index = 0; index < size; index++) { - text[index * 2] = digits[bytes[index] >> 4]; - text[index * 2 + 1] = digits[bytes[index] & 0x0f]; - } - text[size * 2] = '\0'; -} - -static int parse_hex(const char *text, uint8_t *bytes, size_t size) -{ - if (strlen(text) != size * 2) - return -1; - for (size_t index = 0; index < size * 2; index++) { - char c = text[index]; - int value; - - if (c >= '0' && c <= '9') - value = c - '0'; - else if (c >= 'a' && c <= 'f') - value = c - 'a' + 10; - else if (c >= 'A' && c <= 'F') - value = c - 'A' + 10; - else - return -1; - if (index % 2 == 0) - bytes[index / 2] = (uint8_t)(value << 4); - else - bytes[index / 2] |= (uint8_t)value; - } - return 0; -} - -// Parses a CPU list such as "0-3,5". Returns the number of CPUs in it. -static int parse_cpu_list(const char *text, bool *cpus) -{ - const char *cursor = text; - int count = 0; - - memset(cpus, 0, sizeof(bool) * MAX_CPUS); - while (*cursor != '\0' && *cursor != '\n') { - char *end; - long first = strtol(cursor, &end, 10); - long last = first; - - if (end == cursor || first < 0) - return -1; - cursor = end; - if (*cursor == '-') { - last = strtol(cursor + 1, &end, 10); - if (end == cursor + 1 || last < first) - return -1; - cursor = end; - } - if (last >= MAX_CPUS) - return -1; - for (long cpu = first; cpu <= last; cpu++) { - if (!cpus[cpu]) { - cpus[cpu] = true; - count++; - } - } - if (*cursor == ',') - cursor++; - else if (*cursor != '\0' && *cursor != '\n') - return -1; - } - return count; -} - -// --------------------------------------------------------------------------- -// Console output, events, and power-off -// --------------------------------------------------------------------------- - -static const char *abi_prefix(void) -{ - return g_report_only ? "NVX-TIME-REPORT" : "NVX-TIME-ABI"; -} - -// Escapes TEXT into OUT, which holds LIMIT characters plus a terminator. An -// escape sequence that would not fit is dropped whole, never split. -static void escape_text(const char *text, char *out, size_t limit) -{ - size_t length = 0; - - for (const unsigned char *c = (const unsigned char *)text; *c != '\0'; - c++) { - char piece[5]; - size_t size; - - if (*c == '"' || *c == '\\') { - piece[0] = '\\'; - piece[1] = (char)*c; - size = 2; - } else if (*c < 0x20 || *c > 0x7e) { - snprintf(piece, sizeof(piece), "\\x%02x", *c); - size = 4; - } else { - piece[0] = (char)*c; - size = 1; - } - if (length + size > limit) - break; - memcpy(out + length, piece, size); - length += size; - } - out[length] = '\0'; -} - -static void console_write(const char *line) -{ - char buffer[EVENT_MAX + 128]; - int fd = g_test ? -1 : open("/dev/console", O_WRONLY | O_NOCTTY | O_CLOEXEC); - - if (g_async_output) { - snprintf(buffer, sizeof(buffer), "\n%s", line); - line = buffer; - } - if (fd < 0) { - (void)write_all(g_test ? STDOUT_FILENO : STDERR_FILENO, line, - strlen(line)); - return; - } - (void)write_all(fd, line, strlen(line)); - close(fd); -} - -static int enable_ports(void) -{ - if (g_ports) - return 0; - if (g_test) { - errno = EPERM; - return -1; - } - // A four-byte read at port P needs permission for P to P + 3, so the - // reads at 0xe9 and 0xeb cover 0xe9 to 0xee. - if (ioperm(PORTB_DATA, PORTB_WINDOW + 4 - PORTB_DATA, 1) != 0 || - ioperm(PORT_SHUTDOWN, 2, 1) != 0) - return -1; - g_ports = true; - return 0; -} - -// Reads SIZE bytes from PORT four bytes per exit; the device zero-fills -// past the end of the selected record. A string read (rep insl) would take one -// exit, but OpenVMM's MSHV and WHP emulation raises #GP for it in user mode. -static void portb_read(uint16_t port, uint8_t *buffer, size_t size) -{ - for (size_t offset = 0; offset < size; offset += 4) { - uint32_t value = inl(port); - size_t count = size - offset < 4 ? size - offset : 4; - - memcpy(buffer + offset, &value, count); - } -} - -static int lock_file(const char *path) -{ - int fd; - - make_runtime_directories(); - fd = open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0600); - if (fd < 0) - return -1; - while (flock(fd, LOCK_EX) != 0) { - if (errno != EINTR) { - close(fd); - return -1; - } - } - return fd; -} - -static void power_off(int status) -{ - fflush(NULL); - if (enable_ports() == 0) { - outb((unsigned char)status, PORT_SHUTDOWN); - } else { - int fd = open("/dev/port", O_WRONLY | O_CLOEXEC); - unsigned char byte = (unsigned char)status; - - if (fd >= 0) { - ssize_t written = pwrite(fd, &byte, 1, PORT_SHUTDOWN); - - (void)written; - close(fd); - } - } - for (;;) - pause(); -} - -static void format_event(char *line, const char *code, const char *source, - enum phase phase, uint32_t generation, - int64_t boottime_ns, const char *detail) -{ - int prefix = snprintf(line, EVENT_MAX + 1, - "%s-VIOLATION: v=1 code=%s source=%s phase=%s " - "generation=%" PRIu32 " boottime_ns=%" PRId64 - " detail=\"", - abi_prefix(), code, source, k_phase_names[phase], - generation, boottime_ns); - - escape_text(detail, line + prefix, EVENT_MAX - (size_t)prefix - 2); - strcat(line, "\"\n"); -} - -static void count_violation(void); - -// Writes a violation event to the kernel log, the portb data port, and the -// console. Consumers treat identical lines as one event. -static void emit_event(const char *code, const char *source, enum phase phase, - const char *detail) -{ - char line[EVENT_MAX + 1]; - char record[EVENT_MAX + 8]; - int fd; - - format_event(line, code, source, phase, g_generation, - clock_ns(CLOCK_BOOTTIME), detail); - if (g_test) { - console_write(line); - return; - } - fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC); - if (fd >= 0) { - snprintf(record, sizeof(record), "<2>%s", line); - (void)write_all(fd, record, strlen(record)); - close(fd); - } - if (enable_ports() == 0) { - if (g_async_output) - outb('\n', PORTB_DATA); - for (const char *c = line; *c != '\0'; c++) - outb((unsigned char)*c, PORTB_DATA); - } - console_write(line); - count_violation(); -} - -// Reports a fatal time-ABI failure. In report-only mode it returns, and the -// caller abandons the failed step. -static void fail_fatal(int status, const char *code, const char *source, - enum phase phase, const char *format, ...) - __attribute__((format(printf, 5, 6))); - -static void fail_fatal(int status, const char *code, const char *source, - enum phase phase, const char *format, ...) -{ - char detail[DETAIL_MAX]; - va_list arguments; - - va_start(arguments, format); - vsnprintf(detail, sizeof(detail), format, arguments); - va_end(arguments); - emit_event(code, source, phase, detail); - if (!g_report_only) - power_off(status); -} - -// --------------------------------------------------------------------------- -// Published state (/run/nvx/time/state) and private records -// --------------------------------------------------------------------------- - -// A conformance check's record: STATUS is empty until the phase first runs, -// then "pending" while its check runs, then "ok" (a failed check powers the -// guest off; report-only mode records "fail" instead). ELAPSED_US is the -// check's wall time and CPU_US its CPU time over every process that ran it. -// GENERATION is g when the check ran; the boot check's is 0. FAILURES is the -// check's failure count, kept in report-only mode only, and -1 otherwise. -struct check_state { - char status[16]; - uint64_t cpus; - int64_t elapsed_us; - int64_t cpu_us; - uint64_t generation; - int64_t failures; -}; - -// The phases with a check record, indexed by enum phase. -#define CHECK_PHASES 3 - -struct time_state { - uint64_t generation; - struct check_state checks[CHECK_PHASES]; - uint64_t tsc_hz; - uint64_t lapic_hz; - uint64_t discontinuities; - char last_discontinuity[16]; - int64_t last_step_ns; - int64_t last_step_realtime_ns; - uint64_t last_downtime_ns; - char last_downtime_source[16]; - uint64_t synchronized; - int64_t offset_ns; - int64_t uncertainty_ns; - int64_t frequency_ppb; - uint64_t samples; - uint64_t rejected_samples; - char last_sample_error[24]; - uint64_t violations; -}; - -// A fresh state is published at the start of the boot checks, which are then -// pending; a state without a boot record also counts as pending. -static void state_init(struct time_state *state) -{ - memset(state, 0, sizeof(*state)); - for (int phase = 0; phase < CHECK_PHASES; phase++) - state->checks[phase].failures = -1; - snprintf(state->checks[PHASE_BOOT].status, - sizeof(state->checks[PHASE_BOOT].status), "pending"); - snprintf(state->last_discontinuity, sizeof(state->last_discontinuity), - "none"); - snprintf(state->last_downtime_source, sizeof(state->last_downtime_source), - "none"); - snprintf(state->last_sample_error, sizeof(state->last_sample_error), - "none"); -} - -static void copy_word(char *destination, size_t size, const char *value) -{ - snprintf(destination, size, "%.*s", (int)strcspn(value, "\n"), value); -} - -// Parses key=value lines. Unknown keys are ignored; a malformed number in a -// known numeric key fails the parse. -static int state_parse(char *text, struct time_state *state) -{ - struct check_state *boot = &state->checks[PHASE_BOOT]; - struct check_state *capture = &state->checks[PHASE_CAPTURE]; - struct check_state *restore = &state->checks[PHASE_RESTORE]; - struct { - const char *key; - uint64_t *unsigned_value; - int64_t *signed_value; - } const numbers[] = { - {"generation", &state->generation, NULL}, - {"boot_cpus", &boot->cpus, NULL}, - {"capture_cpus", &capture->cpus, NULL}, - {"restore_cpus", &restore->cpus, NULL}, - {"boot_elapsed_us", NULL, &boot->elapsed_us}, - {"capture_elapsed_us", NULL, &capture->elapsed_us}, - {"restore_elapsed_us", NULL, &restore->elapsed_us}, - {"boot_cpu_us", NULL, &boot->cpu_us}, - {"capture_cpu_us", NULL, &capture->cpu_us}, - {"restore_cpu_us", NULL, &restore->cpu_us}, - {"capture_generation", &capture->generation, NULL}, - {"restore_generation", &restore->generation, NULL}, - {"boot_failures", NULL, &boot->failures}, - {"capture_failures", NULL, &capture->failures}, - {"restore_failures", NULL, &restore->failures}, - {"tsc_hz", &state->tsc_hz, NULL}, - {"lapic_hz", &state->lapic_hz, NULL}, - {"discontinuities", &state->discontinuities, NULL}, - {"last_step_ns", NULL, &state->last_step_ns}, - {"last_step_realtime_ns", NULL, &state->last_step_realtime_ns}, - {"last_downtime_ns", &state->last_downtime_ns, NULL}, - {"synchronized", &state->synchronized, NULL}, - {"offset_ns", NULL, &state->offset_ns}, - {"uncertainty_ns", NULL, &state->uncertainty_ns}, - {"frequency_ppb", NULL, &state->frequency_ppb}, - {"samples", &state->samples, NULL}, - {"rejected_samples", &state->rejected_samples, NULL}, - {"violations", &state->violations, NULL}, - }; - struct { - const char *key; - char *value; - size_t size; - } const words[] = { - {"boot_status", boot->status, sizeof(boot->status)}, - {"capture_status", capture->status, sizeof(capture->status)}, - {"restore_status", restore->status, sizeof(restore->status)}, - {"last_discontinuity", state->last_discontinuity, - sizeof(state->last_discontinuity)}, - {"last_downtime_source", state->last_downtime_source, - sizeof(state->last_downtime_source)}, - {"last_sample_error", state->last_sample_error, - sizeof(state->last_sample_error)}, - }; - - state_init(state); - for (char *line = text; line != NULL && *line != '\0';) { - char *next = strchr(line, '\n'); - char *value = strchr(line, '='); - - if (next != NULL) - *next++ = '\0'; - if (value != NULL) { - *value++ = '\0'; - for (size_t index = 0; index < ARRAY_SIZE(words); index++) { - if (strcmp(line, words[index].key) == 0) - copy_word(words[index].value, words[index].size, value); - } - for (size_t index = 0; index < ARRAY_SIZE(numbers); index++) { - int64_t number; - - if (strcmp(line, numbers[index].key) != 0) - continue; - if (parse_int64(value, &number) != 0) - return -1; - if (numbers[index].unsigned_value != NULL) - *numbers[index].unsigned_value = (uint64_t)number; - else - *numbers[index].signed_value = number; - } - } - line = next; - } - return 0; -} - -// Appends a formatted line to TEXT at *LENGTH, if it fits. -static void append_text(char *text, size_t size, size_t *length, - const char *format, ...) - __attribute__((format(printf, 4, 5))); - -static void append_text(char *text, size_t size, size_t *length, - const char *format, ...) -{ - va_list arguments; - int written; - - if (*length >= size) - return; - va_start(arguments, format); - written = vsnprintf(text + *length, size - *length, format, arguments); - va_end(arguments); - if (written > 0) - *length += (size_t)written; -} - -// Formats the state file's key=value lines in their fixed order; the keys of a -// phase whose check never ran are absent. -static void state_format(const struct time_state *state, char *text, - size_t size) -{ - size_t length = 0; - - append_text(text, size, &length, "version=1\ngeneration=%" PRIu64 "\n", - state->generation); - for (int phase = 0; phase < CHECK_PHASES; phase++) { - if (state->checks[phase].status[0] != '\0') - append_text(text, size, &length, "%s_status=%s\n", - k_phase_names[phase], state->checks[phase].status); - } - for (int phase = 0; phase < CHECK_PHASES; phase++) { - if (state->checks[phase].status[0] != '\0') - append_text(text, size, &length, "%s_cpus=%" PRIu64 "\n", - k_phase_names[phase], state->checks[phase].cpus); - } - for (int phase = 0; phase < CHECK_PHASES; phase++) { - if (state->checks[phase].status[0] != '\0') - append_text(text, size, &length, "%s_elapsed_us=%" PRId64 "\n", - k_phase_names[phase], state->checks[phase].elapsed_us); - } - for (int phase = 0; phase < CHECK_PHASES; phase++) { - if (state->checks[phase].status[0] != '\0') - append_text(text, size, &length, "%s_cpu_us=%" PRId64 "\n", - k_phase_names[phase], state->checks[phase].cpu_us); - } - for (int phase = PHASE_CAPTURE; phase < CHECK_PHASES; phase++) { - if (state->checks[phase].status[0] != '\0') - append_text(text, size, &length, "%s_generation=%" PRIu64 "\n", - k_phase_names[phase], state->checks[phase].generation); - } - for (int phase = 0; phase < CHECK_PHASES; phase++) { - if (state->checks[phase].status[0] != '\0' && - state->checks[phase].failures >= 0) - append_text(text, size, &length, "%s_failures=%" PRId64 "\n", - k_phase_names[phase], state->checks[phase].failures); - } - append_text(text, size, &length, - "tsc_hz=%" PRIu64 "\n" - "lapic_hz=%" PRIu64 "\n" - "discontinuities=%" PRIu64 "\n" - "last_discontinuity=%s\n" - "last_step_ns=%" PRId64 "\n" - "last_step_realtime_ns=%" PRId64 "\n" - "last_downtime_ns=%" PRIu64 "\n" - "last_downtime_source=%s\n" - "synchronized=%" PRIu64 "\n" - "offset_ns=%" PRId64 "\n" - "uncertainty_ns=%" PRId64 "\n" - "frequency_ppb=%" PRId64 "\n" - "samples=%" PRIu64 "\n" - "rejected_samples=%" PRIu64 "\n" - "last_sample_error=%s\n" - "violations=%" PRIu64 "\n", - state->tsc_hz, state->lapic_hz, state->discontinuities, - state->last_discontinuity, state->last_step_ns, - state->last_step_realtime_ns, state->last_downtime_ns, - state->last_downtime_source, state->synchronized, - state->offset_ns, state->uncertainty_ns, state->frequency_ppb, - state->samples, state->rejected_samples, - state->last_sample_error, state->violations); -} - -static int state_load(struct time_state *state) -{ - char text[2048]; - - if (read_text(STATE_PATH, text, sizeof(text)) < 0) - return -1; - return state_parse(text, state); -} - -static int state_store(const struct time_state *state) -{ - char text[2048]; - - state_format(state, text, sizeof(text)); - return replace_file(STATE_PATH, STATE_TEMP_PATH, text); -} - -typedef void (*state_update)(struct time_state *state, const void *context); - -// Applies UPDATE to the published state under its lock. The helpers and the -// daemon all write the state this way, so updates never interleave. -static int state_apply(state_update update, const void *context) -{ - struct time_state state; - int lock = lock_file(STATE_LOCK_PATH); - int result; - - if (lock < 0) - return -1; - if (state_load(&state) != 0) - state_init(&state); - update(&state, context); - result = state_store(&state); - close(lock); - return result; -} - -static void add_violation(struct time_state *state, const void *context) -{ - (void)context; - state->violations++; -} - -static void count_violation(void) -{ - if (!g_test && access(STATE_PATH, F_OK) == 0) - (void)state_apply(add_violation, NULL); -} - -static int64_t frequency_to_ppb(int64_t frequency) -{ - int64_t scaled = frequency * 1000; - - return (scaled + (scaled >= 0 ? 32768 : -32768)) / 65536; -} - -// F and L are published in the state file by the boot check. -static int rates_load(uint64_t *tsc_hz, uint64_t *lapic_hz) -{ - struct time_state state; - - if (state_load(&state) != 0 || state.tsc_hz == 0 || state.lapic_hz == 0) - return -1; - *tsc_hz = state.tsc_hz; - *lapic_hz = state.lapic_hz; - return 0; -} - -struct check_record { - enum phase phase; - const char *status; - int cpus; - int64_t elapsed_us; - int64_t cpu_us; - uint64_t tsc_hz; - uint64_t lapic_hz; - int failures; -}; - -static void apply_check(struct time_state *state, const void *context) -{ - const struct check_record *record = context; - struct check_state *check = &state->checks[record->phase]; - - snprintf(check->status, sizeof(check->status), "%s", record->status); - check->cpus = (uint64_t)record->cpus; - check->elapsed_us = record->elapsed_us; - check->cpu_us = record->cpu_us; - check->generation = record->phase == PHASE_BOOT ? 0 : state->generation; - check->failures = -1; - // Report-only mode keeps the failures in the state file, in keys that only - // it writes. - if (g_report_only) { - check->failures = record->failures; - if (record->failures > 0 && strcmp(record->status, "ok") == 0) - snprintf(check->status, sizeof(check->status), "fail"); - } - if (record->tsc_hz != 0) - state->tsc_hz = record->tsc_hz; - if (record->lapic_hz != 0) - state->lapic_hz = record->lapic_hz; -} - -// Records a phase's conformance check in the state file: "pending" while it -// runs, then "ok" (a failed check powers the guest off instead), with the -// generation it ran in and its wall and CPU times. Rates of 0 keep the -// published ones. Report-only mode records "fail" and the failure count of a -// check that failed. -static int record_check(enum phase phase, const char *status, int cpus, - int64_t elapsed_us, int64_t cpu_us, uint64_t tsc_hz, - uint64_t lapic_hz, int failures) -{ - struct check_record record = {phase, status, cpus, elapsed_us, - cpu_us, tsc_hz, lapic_hz, failures}; - - return state_apply(apply_check, &record); -} - -// The watchdog settings saved before a capture, in the order they are -// written. Missing debug-kernel sysctls are skipped. -static const char *const k_suppressed[] = {RCU_STALL_SUPPRESS, SOFT_WATCHDOG, - HUNG_TASK_TIMEOUT}; -static const int64_t k_suppressed_values[] = {1, 0, 0}; - -static int suppression_save(void) -{ - char text[512] = ""; - size_t length = 0; - int fd; - - for (size_t index = 0; index < ARRAY_SIZE(k_suppressed); index++) { - int64_t value; - - if (access(k_suppressed[index], F_OK) != 0) { - if (index == 0) - return -1; - continue; - } - if (read_int64(k_suppressed[index], &value) != 0) - return -1; - length += (size_t)snprintf(text + length, sizeof(text) - length, - "%s=%" PRId64 "\n", k_suppressed[index], - value); - } - fd = open(SUPPRESSION_PATH, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600); - if (fd < 0) - return -1; - if (write_all(fd, text, length) != 0) { - close(fd); - unlink(SUPPRESSION_PATH); - return -1; - } - close(fd); - for (size_t index = 0; index < ARRAY_SIZE(k_suppressed); index++) { - if (access(k_suppressed[index], F_OK) == 0 && - write_int64(k_suppressed[index], k_suppressed_values[index]) != 0) - return -1; - } - return 0; -} - -// Writes back every value saved by suppression_save, then forgets them. A -// lock serializes the daemon's release at the stall timeout with the restore -// worker's; with MISSING_OK, values already written back count as restored. -static int suppression_restore(char *detail, size_t size, bool missing_ok) -{ - char text[512]; - char *line = text; - int lock = lock_file(SUPPRESSION_LOCK_PATH); - int result = -1; - - if (lock < 0) { - snprintf(detail, size, "lock %s: %s", SUPPRESSION_LOCK_PATH, - strerror(errno)); - return -1; - } - if (read_text(SUPPRESSION_PATH, text, sizeof(text)) < 0) { - if (missing_ok && errno == ENOENT) - result = 0; - else - snprintf(detail, size, "read %s: %s", SUPPRESSION_PATH, - strerror(errno)); - close(lock); - return result; - } - while (*line != '\0') { - char *next = strchr(line, '\n'); - char *value = strchr(line, '='); - - if (next != NULL) - *next++ = '\0'; - if (value == NULL) - break; - *value++ = '\0'; - if (write_text(line, value) != 0) { - snprintf(detail, size, "restore %.200s: %s", line, - strerror(errno)); - close(lock); - return -1; - } - line = next != NULL ? next : line + strlen(line); - } - if (unlink(SUPPRESSION_PATH) != 0) - snprintf(detail, size, "remove %s: %s", SUPPRESSION_PATH, - strerror(errno)); - else - result = 0; - close(lock); - return result; -} - -// --------------------------------------------------------------------------- -// Decoders and decisions (pure; exercised by `nvx-time test`) -// --------------------------------------------------------------------------- - -struct restore_packet { - uint8_t flags; - uint8_t online_vp_count; - uint8_t range_count; - uint32_t generation; - int32_t rate_deviation; - uint64_t downtime_ns; - uint64_t utc_ns; - uint64_t ranges[PACKET_MAX_RANGES][2]; - uint8_t entropy[PACKET_ENTROPY_SIZE]; -}; - -static int parse_packet_header(const uint8_t *bytes, - struct restore_packet *packet, char *detail, - size_t size) -{ - uint8_t online; - - memset(packet, 0, sizeof(*packet)); - if (memcmp(bytes, "OVR", 3) != 0) { - snprintf(detail, size, "restore packet magic is %02x%02x%02x", - bytes[0], bytes[1], bytes[2]); - return -1; - } - if (bytes[3] != 4) { - snprintf(detail, size, "restore packet version is %u", bytes[3]); - return -1; - } - packet->flags = bytes[4]; - online = bytes[5]; - packet->online_vp_count = online; - packet->range_count = bytes[6]; - packet->generation = read_le32(bytes + 8); - packet->rate_deviation = (int32_t)read_le32(bytes + 12); - packet->downtime_ns = read_le64(bytes + 16); - packet->utc_ns = read_le64(bytes + 24); - if ((packet->flags & ~PACKET_KNOWN_FLAGS) != 0) { - snprintf(detail, size, "restore packet flags 0x%02x set reserved bits", - packet->flags); - return -1; - } - if (online != 0 && online != 1 && online != 2 && online != 4 && - online != 8) { - snprintf(detail, size, "restore packet online VP count is %u", online); - return -1; - } - if ((packet->flags & PACKET_MEMORY_TARGET) == 0 && packet->range_count != 0) { - snprintf(detail, size, - "restore packet has %u memory ranges without a memory target", - packet->range_count); - return -1; - } - if (bytes[7] != 0) { - snprintf(detail, size, "restore packet reserved byte is %u", bytes[7]); - return -1; - } - if (packet->generation == 0) { - snprintf(detail, size, "restore packet generation is 0"); - return -1; - } - if (packet->rate_deviation > RATE_DEVIATION_LIMIT || - packet->rate_deviation < -RATE_DEVIATION_LIMIT) { - snprintf(detail, size, - "restore packet rate deviation %" PRId32 - " exceeds the 250 ppm tolerance", - packet->rate_deviation); - return -1; - } - if (packet->downtime_ns > (uint64_t)DOWNTIME_LIMIT_NS) { - snprintf(detail, size, - "restore packet downtime %" PRIu64 " ns exceeds 30 days", - packet->downtime_ns); - return -1; - } - if (packet->utc_ns == 0 || packet->utc_ns > (uint64_t)INT64_MAX) { - snprintf(detail, size, "restore packet UTC %" PRIu64 " is invalid", - packet->utc_ns); - return -1; - } - return 0; -} - -static size_t packet_body_size(const struct restore_packet *packet) -{ - return (size_t)packet->range_count * PACKET_RANGE_SIZE + PACKET_ENTROPY_SIZE; -} - -static int parse_packet_body(const uint8_t *bytes, - struct restore_packet *packet, char *detail, - size_t size) -{ - for (unsigned index = 0; index < packet->range_count; index++) { - uint64_t start = read_le64(bytes + index * PACKET_RANGE_SIZE); - uint64_t length = read_le64(bytes + index * PACKET_RANGE_SIZE + 8); - - if (length == 0 || start + length < start) { - snprintf(detail, size, "restore memory range %u is invalid", index); - return -1; - } - packet->ranges[index][0] = start; - packet->ranges[index][1] = length; - } - memcpy(packet->entropy, bytes + packet->range_count * PACKET_RANGE_SIZE, - PACKET_ENTROPY_SIZE); - return 0; -} - -struct time_sample { - uint8_t flags; - uint32_t generation; - uint64_t utc_ns; - int64_t t0; - int64_t t1; -}; - -static int parse_sample(const uint8_t *bytes, struct time_sample *sample, - char *detail, size_t size) -{ - uint16_t reserved = (uint16_t)(bytes[2] | bytes[3] << 8); - - sample->flags = bytes[1]; - sample->generation = read_le32(bytes + 4); - sample->utc_ns = read_le64(bytes + 8); - if (bytes[0] != 1) { - snprintf(detail, size, "time sample version is %u", bytes[0]); - return -1; - } - if ((sample->flags & ~SAMPLE_TEST_HOOKS) != 0 || reserved != 0) { - snprintf(detail, size, "time sample flags 0x%02x or reserved 0x%04x", - sample->flags, reserved); - return -1; - } - if (sample->utc_ns == 0 || sample->utc_ns > (uint64_t)INT64_MAX) { - snprintf(detail, size, "time sample UTC %" PRIu64 " is invalid", - sample->utc_ns); - return -1; - } - return 0; -} - -// The host latched UTC between the guest's CLOCK_REALTIME reads T0 and T1. -static void pair_clocks(int64_t t0, int64_t t1, uint64_t utc_ns, - int64_t *theta, int64_t *epsilon) -{ - *epsilon = (t1 - t0) / 2; - *theta = (int64_t)utc_ns - (t0 + (t1 - t0) / 2); -} - -static void offset_to_timeval(int64_t offset_ns, struct timeval *time) -{ - time->tv_sec = (time_t)(offset_ns / NSEC_PER_SEC); - time->tv_usec = (suseconds_t)(offset_ns % NSEC_PER_SEC); - if (time->tv_usec < 0) { - time->tv_sec -= 1; - time->tv_usec += NSEC_PER_SEC; - } -} - -static int64_t restore_frequency(int32_t rate_deviation) -{ - int64_t frequency = -(int64_t)rate_deviation; - - if (frequency > FREQUENCY_LIMIT) - return FREQUENCY_LIMIT; - if (frequency < -FREQUENCY_LIMIT) - return -FREQUENCY_LIMIT; - return frequency; -} - -static int64_t ceil_us(int64_t nanoseconds) -{ - return (nanoseconds + NSEC_PER_USEC - 1) / NSEC_PER_USEC; -} - -// Builds the discipline's correction for an accepted sample: a step at or -// beyond 128 ms, otherwise a PLL slew. Returns true for a step. -static bool plan_correction(int64_t theta, int64_t epsilon, bool fast, - struct timex *tx) -{ - int64_t magnitude = theta < 0 ? -theta : theta; - - memset(tx, 0, sizeof(*tx)); - if (magnitude >= STEP_THRESHOLD_NS) { - tx->modes = ADJ_SETOFFSET | ADJ_NANO; - offset_to_timeval(theta, &tx->time); - return true; - } - tx->modes = ADJ_OFFSET | ADJ_STATUS | ADJ_NANO | ADJ_TIMECONST | - ADJ_MAXERROR | ADJ_ESTERROR; - tx->offset = (long)theta; - tx->status = STA_PLL | STA_NANO; - tx->constant = fast ? FAST_TIME_CONSTANT : SLOW_TIME_CONSTANT; - tx->maxerror = (long)ceil_us(magnitude + epsilon); - tx->esterror = (long)ceil_us(epsilon); - return false; -} - -struct watch_rule { - const char *code; - const char *all[2]; - const char *any[4]; -}; - -static const struct watch_rule k_watch_rules[] = { - {"G_TSC_UNSTABLE", {"Marking TSC unstable due to ", NULL}, {NULL}}, - {"G_CLOCKSOURCE_UNSTABLE", - {"timekeeping watchdog on CPU", "as unstable because the skew is too large"}, - {NULL}}, - {"G_CLOCKSOURCE_SWITCH", {"clocksource: Switched to clocksource ", NULL}, - {NULL}}, - {"G_CLOCKSOURCE_SKEW", - {"clocksource: ", NULL}, - {" ahead of CPU ", " behind CPU ", NULL}}, - {"G_TSC_WARP", - {NULL, NULL}, - {"TSC synchronization [CPU#", "cycles TSC warp between CPUs", - "TSC warped randomly between CPUs", NULL}}, - {"G_TSC_ADJUST", {"TSC ADJUST", NULL}, {NULL}}, - {"G_RCU_STALL", - {"rcu: INFO: ", NULL}, - {" detected stalls on CPUs/tasks", " self-detected stall on CPU", - " detected expedited stalls", NULL}}, - {"G_RCU_STARVED", - {"rcu: ", NULL}, - {" kthread starved for ", " kthread timer wakeup didn't happen for ", - NULL}}, - {"G_SOFT_LOCKUP", {"watchdog: BUG: soft lockup - CPU#", NULL}, {NULL}}, - {"G_HARD_LOCKUP", {"Watchdog detected hard LOCKUP", NULL}, {NULL}}, - {"G_HUNG_TASK", {"INFO: task ", " blocked for more than "}, {NULL}}, - {"G_UNCHECKED_MSR", {"unchecked MSR access error", NULL}, {NULL}}, - {"G_APIC_ID_MISMATCH", {"APIC ID mismatch", NULL}, {NULL}}, -}; - -// Returns the code of the first rule whose substrings all appear in MESSAGE. -// The guest's own NVX-TIME lines quote kernel text and are never matched. -static const char *match_watch_rule(const char *message) -{ - if (strncmp(message, "NVX-TIME-", 9) == 0) - return NULL; - for (size_t index = 0; index < ARRAY_SIZE(k_watch_rules); index++) { - const struct watch_rule *rule = &k_watch_rules[index]; - bool all = true; - bool any = rule->any[0] == NULL; - - for (size_t part = 0; part < ARRAY_SIZE(rule->all) && rule->all[part]; - part++) - all = all && strstr(message, rule->all[part]) != NULL; - for (size_t part = 0; part < ARRAY_SIZE(rule->any) && rule->any[part]; - part++) - any = any || strstr(message, rule->any[part]) != NULL; - if (all && any) - return rule->code; - } - return NULL; -} - -struct boot_log { - bool hypervisor; - bool privileges; - bool lapic; - char last_switch[48]; - char problem[DETAIL_MAX]; - char wx[DETAIL_MAX]; -}; - -// CONFIG_DEBUG_WX audits the kernel page tables before init runs and logs -// this for a mapping that is both writable and executable. -static const char k_wx_record[] = "Found insecure W+X mapping"; - -static void boot_log_add(struct boot_log *log, const char *message) -{ - static const char *const forbidden[] = { - "Fast TSC calibration", "Refined TSC clocksource calibration", - "kvm-clock", "APIC timer: using supplied frequency"}; - static const char switched[] = "clocksource: Switched to clocksource "; - const char *name = strstr(message, switched); - const char *code; - - if (log->wx[0] == '\0' && strncmp(message, "NVX-TIME-", 9) != 0 && - strstr(message, k_wx_record) != NULL) - snprintf(log->wx, sizeof(log->wx), "%.300s", message); - if (strstr(message, "Hypervisor detected: Microsoft Hyper-V") != NULL) - log->hypervisor = true; - if (strstr(message, "Hyper-V: privilege flags low 0x8860,") != NULL) - log->privileges = true; - if (ends_with(message, "Hyper-V: LAPIC Timer Frequency: 0x989680") || - ends_with(message, "Hyper-V: LAPIC Timer Frequency: 0x1e8480")) - log->lapic = true; - if (name != NULL) { - name += sizeof(switched) - 1; - snprintf(log->last_switch, sizeof(log->last_switch), "%.*s", - (int)strcspn(name, " \n"), name); - } - if (log->problem[0] != '\0') - return; - code = match_watch_rule(message); - if (code != NULL && strcmp(code, "G_CLOCKSOURCE_SWITCH") != 0) { - snprintf(log->problem, sizeof(log->problem), "%s: %.300s", code, - message); - return; - } - for (size_t index = 0; index < ARRAY_SIZE(forbidden); index++) { - if (strncmp(message, "NVX-TIME-", 9) != 0 && - strstr(message, forbidden[index]) != NULL) { - snprintf(log->problem, sizeof(log->problem), - "forbidden record: %.300s", message); - return; - } - } -} - -static bool boot_log_verdict(const struct boot_log *log, char *detail, - size_t size) -{ - if (log->problem[0] != '\0') - snprintf(detail, size, "%s", log->problem); - else if (!log->hypervisor) - snprintf(detail, size, "no 'Hypervisor detected: Microsoft Hyper-V'"); - else if (!log->privileges) - snprintf(detail, size, "no 'Hyper-V: privilege flags low 0x8860,'"); - else if (!log->lapic) - snprintf(detail, size, "no 'Hyper-V: LAPIC Timer Frequency' of 1 GHz " - "or 200 MHz"); - else if (strcmp(log->last_switch, "tsc") != 0) - snprintf(detail, size, "last clocksource switch is to '%s', not tsc", - log->last_switch); - else - return true; - return false; -} - -// --------------------------------------------------------------------------- -// Kernel log -// --------------------------------------------------------------------------- - -typedef void (*kmsg_consumer)(void *context, const char *message); - -// Reads every available /dev/kmsg record from the non-blocking FD and passes -// each message text to CONSUME. Returns 0 when drained, -EPIPE when records -// were overwritten before they were read, or another negative errno. -static int kmsg_drain(int fd, kmsg_consumer consume, void *context) -{ - char record[8192]; - - for (;;) { - ssize_t count = read(fd, record, sizeof(record) - 1); - char *message; - - if (count < 0 && errno == EINTR) - continue; - if (count < 0) - return errno == EAGAIN ? 0 : -errno; - if (count == 0) - return 0; - record[count] = '\0'; - message = strchr(record, ';'); - if (message == NULL) - continue; - message++; - message[strcspn(message, "\n")] = '\0'; - consume(context, message); - } -} - -#define SYSLOG_ACTION_READ_ALL 3 -#define SYSLOG_ACTION_SIZE_BUFFER 10 - -// Returns the message text of one SYSLOG_ACTION_READ_ALL line, without its -// "" prefix and its printk timestamp ("[ 1.234567] "). -static const char *syslog_message(const char *line) -{ - const char *cursor = line; - const char *end = strchr(line, '>'); - const char *scan; - - if (*cursor == '<' && end != NULL) - cursor = end + 1; - if (*cursor != '[') - return cursor; - scan = cursor + 1; - while (*scan == ' ') - scan++; - while (*scan >= '0' && *scan <= '9') - scan++; - if (*scan++ != '.') - return cursor; - while (*scan >= '0' && *scan <= '9') - scan++; - if (*scan != ']') - return cursor; - return scan[1] == ' ' ? scan + 2 : scan + 1; -} - -static void boot_log_line(void *context, char *line) -{ - boot_log_add(context, syslog_message(line)); -} - -struct line_starts { - const char **starts; - size_t count; - size_t capacity; -}; - -// Records the start of every line of BUFFER that contains NEEDLE. -static bool mark_lines(const char *buffer, const char *needle, - struct line_starts *lines) -{ - for (const char *hit = strstr(buffer, needle); hit != NULL; - hit = strstr(hit + 1, needle)) { - const char *start = hit; - - while (start > buffer && start[-1] != '\n') - start--; - if (lines->count == lines->capacity) { - size_t capacity = lines->capacity * 2; - const char **grown = - realloc(lines->starts, capacity * sizeof(*lines->starts)); - - if (grown == NULL) - return false; - lines->starts = grown; - lines->capacity = capacity; - } - lines->starts[lines->count++] = start; - } - return true; -} - -static int compare_starts(const void *left, const void *right) -{ - const char *a = *(const char *const *)left; - const char *b = *(const char *const *)right; - - return a < b ? -1 : a > b; -} - -// C4 and K1 over the whole SYSLOG_ACTION_READ_ALL text. Only lines that -// contain a required record, a forbidden record, the W+X record, or the first -// substring of a watcher row can change the verdict, so a few whole-buffer -// searches find them, and boot_log_add() sees just those lines, in log order. -// Matching every rule against every line cost about a millisecond on a -// guest's boot log. -static void boot_log_scan(char *buffer, struct boot_log *log) -{ - static const char *const anchors[] = { - "Hypervisor detected: Microsoft Hyper-V", - "Hyper-V: privilege flags low 0x8860,", - "Hyper-V: LAPIC Timer Frequency: ", - "Fast TSC calibration", - "Refined TSC clocksource calibration", - "kvm-clock", - "APIC timer: using supplied frequency", - k_wx_record}; - struct line_starts lines = {malloc(256 * sizeof(const char *)), 0, 256}; - bool ok = lines.starts != NULL; - - for (size_t index = 0; ok && index < ARRAY_SIZE(anchors); index++) - ok = mark_lines(buffer, anchors[index], &lines); - for (size_t index = 0; ok && index < ARRAY_SIZE(k_watch_rules); index++) { - const struct watch_rule *rule = &k_watch_rules[index]; - - if (rule->all[0] != NULL) { - ok = mark_lines(buffer, rule->all[0], &lines); - continue; - } - for (size_t part = 0; - ok && part < ARRAY_SIZE(rule->any) && rule->any[part]; part++) - ok = mark_lines(buffer, rule->any[part], &lines); - } - if (!ok) { - free(lines.starts); - for_each_text_line(buffer, boot_log_line, log); - return; - } - qsort(lines.starts, lines.count, sizeof(*lines.starts), compare_starts); - for (size_t index = 0; index < lines.count; index++) { - char *start = (char *)lines.starts[index]; - char *end = strchr(start, '\n'); - - if (index > 0 && lines.starts[index - 1] == start) - continue; - if (end != NULL) - *end = '\0'; - boot_log_add(log, syslog_message(start)); - if (end != NULL) - *end = '\n'; - } - free(lines.starts); -} - -// Reads the whole kernel log for C4 with one SYSLOG_ACTION_READ_ALL. The -// caller has already positioned the daemon's /dev/kmsg descriptor at the end -// of the log, so records logged in between are seen twice, never missed. -// BUFFER holds SIZE + 1 bytes, the size SYSLOG_ACTION_SIZE_BUFFER reported. -static int read_boot_log_into(struct boot_log *log, char *buffer, int size) -{ - int count = klogctl(SYSLOG_ACTION_READ_ALL, buffer, size); - int error = errno; - - if (count >= 0) { - buffer[count] = '\0'; - boot_log_scan(buffer, log); - } - errno = error; - return count < 0 ? -1 : 0; -} - -// --------------------------------------------------------------------------- -// Conformance checks -// --------------------------------------------------------------------------- - -struct checks { - enum phase phase; - int failures; - int possible; - int online_count; - bool online[MAX_CPUS]; - uint64_t cpu_khz[MAX_CPUS]; - bool cpu_khz_known[MAX_CPUS]; - uint64_t tsc_hz; - uint64_t lapic_hz; -}; - -static bool check_failed_code(struct checks *checks, const char *code, - const char *format, ...) - __attribute__((format(printf, 3, 4))); - -// Serializes failure reports from the boot check's per-CPU threads, which -// share the console and the violation count. -static pthread_mutex_t g_report_lock = PTHREAD_MUTEX_INITIALIZER; - -// Emits CODE and powers off with status 193; a failed check prints no -// marker. In report-only mode it returns false and the run continues. -static bool check_failed_code(struct checks *checks, const char *code, - const char *format, ...) -{ - char detail[DETAIL_MAX]; - va_list arguments; - - va_start(arguments, format); - vsnprintf(detail, sizeof(detail), format, arguments); - va_end(arguments); - pthread_mutex_lock(&g_report_lock); - checks->failures++; - emit_event(code, "conformance", checks->phase, detail); - if (!g_report_only) - power_off(STATUS_CONFORMANCE); - pthread_mutex_unlock(&g_report_lock); - return false; -} - -static bool check_failed(struct checks *checks, const char *id, - const char *format, ...) - __attribute__((format(printf, 3, 4))); - -// check_failed_code() with the conformance code G_CONFORMANCE_. -static bool check_failed(struct checks *checks, const char *id, - const char *format, ...) -{ - char detail[DETAIL_MAX]; - char code[32]; - va_list arguments; - - va_start(arguments, format); - vsnprintf(detail, sizeof(detail), format, arguments); - va_end(arguments); - snprintf(code, sizeof(code), "G_CONFORMANCE_%s", id); - return check_failed_code(checks, code, "%s", detail); -} - -static int checks_init(struct checks *checks, enum phase phase) -{ - char text[256]; - bool possible[MAX_CPUS]; - - memset(checks, 0, sizeof(*checks)); - checks->phase = phase; - if (read_text("/sys/devices/system/cpu/possible", text, sizeof(text)) < 0) - return -1; - checks->possible = parse_cpu_list(text, possible); - if (read_text("/sys/devices/system/cpu/online", text, sizeof(text)) < 0) - return -1; - checks->online_count = parse_cpu_list(text, checks->online); - return checks->possible > 0 && checks->online_count > 0 ? 0 : -1; -} - -static void cpuid(uint32_t leaf, uint32_t subleaf, uint32_t registers[4]) -{ - __asm__ __volatile__("cpuid" - : "=a"(registers[0]), "=b"(registers[1]), - "=c"(registers[2]), "=d"(registers[3]) - : "a"(leaf), "c"(subleaf)); -} - -// Runs the calling thread on CPU only, so inline CPUID and the msr driver -// read that CPU without inter-processor calls. -static int pin_to_cpu(int cpu) -{ - cpu_set_t set; - - CPU_ZERO(&set); - CPU_SET(cpu, &set); - if (sched_setaffinity(0, sizeof(set), &set) != 0) - return -1; - return sched_getcpu() == cpu ? 0 : -1; -} - -static bool all_zero(const uint32_t value[4]) -{ - return (value[0] | value[1] | value[2] | value[3]) == 0; -} - -// C1: the identity leaves and the explicit zero leaves. The absence of a -// hypervisor signature at other bases is a static backend property that the -// CI exhaustive check covers, so the boot check skips that 255-leaf scan. -static bool check_identity(struct checks *checks, int cpu) -{ - const uint32_t capacity = (uint32_t)checks->possible; - const uint32_t expected[6][4] = { - {0x40000005, 0x7263694d, 0x666f736f, 0x76482074}, - {0x31237648, 0, 0, 0}, - {0x0058564e, 0x00010000, 0, 0}, - {0x00008860, 0, 0, 0x00000100}, - {0, 0xffffffff, 0, 0}, - {capacity, capacity, 0, 0}, - }; - static const uint32_t zero_leaves[][2] = {{0x40000006, 0x4000000f}, - {0x40000080, 0x40000082}}; - uint32_t value[4]; - - for (uint32_t index = 0; index < 6; index++) { - cpuid(0x40000000 + index, 0, value); - if (memcmp(value, expected[index], sizeof(value)) != 0) - return check_failed( - checks, "C1", - "cpu %d leaf 0x%08" PRIx32 " is %08" PRIx32 ":%08" PRIx32 - ":%08" PRIx32 ":%08" PRIx32 ", expected %08" PRIx32 - ":%08" PRIx32 ":%08" PRIx32 ":%08" PRIx32, - cpu, 0x40000000 + index, value[0], value[1], value[2], - value[3], expected[index][0], expected[index][1], - expected[index][2], expected[index][3]); - } - for (size_t range = 0; range < ARRAY_SIZE(zero_leaves); range++) { - for (uint32_t leaf = zero_leaves[range][0]; leaf <= zero_leaves[range][1]; - leaf++) { - cpuid(leaf, 0, value); - if (!all_zero(value)) - return check_failed( - checks, "C1", - "cpu %d leaf 0x%08" PRIx32 " is %08" PRIx32 ":%08" PRIx32 - ":%08" PRIx32 ":%08" PRIx32 ", expected zero", - cpu, leaf, value[0], value[1], value[2], value[3]); - } - } - return true; -} - -static bool check_bit(struct checks *checks, int cpu, const char *name, - uint32_t reg, unsigned bit, bool expected) -{ - if (((reg >> bit) & 1) == (expected ? 1u : 0u)) - return true; - return check_failed(checks, "C2", "cpu %d %s is %s", cpu, name, - expected ? "clear" : "set"); -} - -static bool check_leaf(struct checks *checks, int cpu, uint32_t leaf, - const uint32_t expected[4]) -{ - uint32_t value[4]; - - cpuid(leaf, 0, value); - if (memcmp(value, expected, sizeof(value)) == 0) - return true; - return check_failed(checks, "C2", - "cpu %d leaf 0x%08" PRIx32 " is %08" PRIx32 - ":%08" PRIx32 ":%08" PRIx32 ":%08" PRIx32 - ", expected %08" PRIx32 ":%08" PRIx32 ":%08" PRIx32 - ":%08" PRIx32, - cpu, leaf, value[0], value[1], value[2], value[3], - expected[0], expected[1], expected[2], expected[3]); -} - -// C2: the CPU time bits every profile fixes. -static bool check_time_bits(struct checks *checks, int cpu) -{ - static const uint32_t arat_only[4] = {4, 0, 0, 0}; - static const uint32_t zero[4] = {0, 0, 0, 0}; - static const uint32_t invariant_tsc[4] = {0, 0, 0, 0x100}; - uint32_t value[4]; - uint32_t max_basic; - bool ok; - - cpuid(0, 0, value); - max_basic = value[0]; - cpuid(1, 0, value); - ok = check_bit(checks, cpu, "CPUID.1:ECX[31] (hypervisor)", value[2], 31, - true) && - check_bit(checks, cpu, "CPUID.1:ECX[24] (TSC-deadline)", value[2], - 24, false) && - check_bit(checks, cpu, "CPUID.1:ECX[15] (PDCM)", value[2], 15, - false) && - check_bit(checks, cpu, "CPUID.1:EDX[4] (TSC)", value[3], 4, true); - if (!ok) - return false; - if (max_basic < 6) - return check_failed(checks, "C2", "cpu %d lacks leaf 0x6 (ARAT)", cpu); - if (!check_leaf(checks, cpu, 6, arat_only)) - return false; - if (max_basic >= 7) { - cpuid(7, 0, value); - if (!check_bit(checks, cpu, "CPUID.7.0:EBX[1] (TSC_ADJUST)", value[1], - 1, false)) - return false; - } - if (max_basic >= 0xa && !check_leaf(checks, cpu, 0xa, zero)) - return false; - if (max_basic >= 0x15 && !check_leaf(checks, cpu, 0x15, zero)) - return false; - if (max_basic >= 0x16 && !check_leaf(checks, cpu, 0x16, zero)) - return false; - cpuid(0x80000000, 0, value); - if (value[0] < 0x80000007) - return check_failed(checks, "C2", "cpu %d lacks leaf 0x80000007", cpu); - cpuid(0x80000001, 0, value); - if (!check_bit(checks, cpu, "CPUID.80000001:EDX[27] (RDTSCP)", value[3], - 27, true)) - return false; - return check_leaf(checks, cpu, 0x80000007, invariant_tsc); -} - -static int msr_read(int fd, uint32_t msr, uint64_t *value) -{ - ssize_t count = pread(fd, value, sizeof(*value), (off_t)msr); - - if (count == (ssize_t)sizeof(*value)) - return 0; - return count < 0 ? -errno : -EIO; -} - -// C3: the synthetic MSRs. At boot (FULL), the VP index on every CPU and the -// partition-wide MSRs on CPU 0 only; at restore, CPU 0's declared TSC rate. -static bool check_msrs(struct checks *checks, int cpu, bool full) -{ - static const uint32_t faulting[] = {0x40000000, 0x40000001, 0x40000020}; - char path[64]; - uint64_t value = 0; - bool ok = true; - int error = 0; - int fd; - - snprintf(path, sizeof(path), "/dev/cpu/%d/msr", cpu); - fd = open(path, O_RDONLY | O_CLOEXEC); - if (fd < 0) - return check_failed(checks, "C3", "cpu %d: open %s: %s", cpu, path, - strerror(errno)); - if (full && ((error = msr_read(fd, 0x40000002, &value)) != 0 || - value != (uint64_t)cpu)) - ok = check_failed(checks, "C3", - "cpu %d: MSR 0x40000002 (VP index) is %" PRIu64 - " (error %d)", - cpu, value, error); - if (cpu != 0) { - close(fd); - return ok; - } - if (ok && (error = msr_read(fd, 0x40000022, &value)) != 0) - ok = check_failed(checks, "C3", "cpu %d: MSR 0x40000022 read: %s", - cpu, strerror(-error)); - if (ok && checks->tsc_hz != 0 && value != checks->tsc_hz) - ok = check_failed(checks, "C3", - "cpu %d: MSR 0x40000022 is %" PRIu64 - " Hz, expected %" PRIu64 " Hz", - cpu, value, checks->tsc_hz); - if (ok && checks->tsc_hz == 0) - checks->tsc_hz = value; - if (ok && checks->cpu_khz_known[cpu] && - value / 1000 != checks->cpu_khz[cpu]) - ok = check_failed(checks, "C3", - "cpu %d: floor(F / 1000) is %" PRIu64 - " kHz but cpu MHz is %" PRIu64 " kHz", - cpu, value / 1000, checks->cpu_khz[cpu]); - if (ok && full) { - error = msr_read(fd, 0x40000023, &value); - if (error != 0 || (value != 1000000000 && value != 200000000) || - (checks->lapic_hz != 0 && value != checks->lapic_hz)) - ok = check_failed(checks, "C3", - "cpu %d: MSR 0x40000023 (LAPIC rate) is %" PRIu64 - " (error %d)", - cpu, value, error); - else - checks->lapic_hz = value; - } - if (ok && full && - ((error = msr_read(fd, 0x40000118, &value)) != 0 || value != 1)) - ok = check_failed(checks, "C3", - "cpu %d: MSR 0x40000118 is %" PRIu64 " (error %d)", - cpu, value, error); - for (size_t index = 0; ok && full && index < ARRAY_SIZE(faulting); - index++) { - error = msr_read(fd, faulting[index], &value); - if (error != -EIO) - ok = check_failed(checks, "C3", - "cpu %d: MSR 0x%08" PRIx32 - " read did not fail with EIO (error %d)", - cpu, faulting[index], error); - } - close(fd); - return ok; -} - -static bool has_word(const char *list, const char *word) -{ - size_t length = strlen(word); - - for (const char *cursor = list; (cursor = strstr(cursor, word)) != NULL; - cursor += length) { - bool starts = cursor == list || cursor[-1] == ' ' || cursor[-1] == '\t'; - char after = cursor[length]; - - if (starts && (after == '\0' || after == ' ' || after == '\n')) - return true; - } - return false; -} - -// Parses /proc/cpuinfo line by line: records each CPU's `cpu MHz` in kHz and, -// when FLAGS is set, checks C5 on the CPUs in SELECTED. The first C5 failure -// ends the check. -struct cpuinfo_parse { - struct checks *checks; - const bool *selected; - bool flags; - bool failed; - int cpu; - bool seen[MAX_CPUS]; -}; - -static void cpuinfo_init(struct cpuinfo_parse *parse, struct checks *checks, - const bool *selected, bool flags) -{ - memset(parse, 0, sizeof(*parse)); - parse->checks = checks; - parse->selected = selected; - parse->flags = flags; - parse->cpu = -1; -} - -static void cpuinfo_line(void *context, char *line) -{ - static const char *const required[] = { - "tsc", "constant_tsc", "nonstop_tsc", "tsc_known_freq", - "tsc_reliable", "rdtscp", "hypervisor", "arat"}; - static const char *const forbidden[] = {"tsc_deadline_timer", "tsc_adjust"}; - struct cpuinfo_parse *parse = context; - char *colon = strchr(line, ':'); - int cpu = parse->cpu; - - if (parse->failed || colon == NULL) - return; - if (strncmp(line, "processor", 9) == 0) { - cpu = atoi(colon + 1); - parse->cpu = cpu >= 0 && cpu < MAX_CPUS ? cpu : -1; - if (parse->cpu >= 0) - parse->seen[cpu] = true; - } else if (cpu >= 0 && strncmp(line, "cpu MHz", 7) == 0) { - char fraction[4] = "000"; - const char *dot = strchr(colon, '.'); - unsigned long long whole = strtoull(colon + 1, NULL, 10); - - for (int digit = 0; dot != NULL && digit < 3 && - dot[1 + digit] >= '0' && dot[1 + digit] <= '9'; - digit++) - fraction[digit] = dot[1 + digit]; - parse->checks->cpu_khz[cpu] = whole * 1000 + strtoull(fraction, NULL, 10); - parse->checks->cpu_khz_known[cpu] = true; - } else if (cpu >= 0 && parse->flags && parse->selected[cpu] && - strncmp(line, "flags", 5) == 0) { - for (size_t index = 0; index < ARRAY_SIZE(required); index++) { - if (!has_word(colon + 1, required[index])) { - parse->failed = true; - check_failed(parse->checks, "C5", "cpu %d lacks the %s flag", - cpu, required[index]); - return; - } - } - for (size_t index = 0; index < ARRAY_SIZE(forbidden); index++) { - if (has_word(colon + 1, forbidden[index])) { - parse->failed = true; - check_failed(parse->checks, "C5", "cpu %d has the %s flag", cpu, - forbidden[index]); - return; - } - } - } -} - -static bool cpuinfo_result(const struct cpuinfo_parse *parse) -{ - if (parse->failed) - return false; - for (int index = 0; parse->flags && index < MAX_CPUS; index++) { - if (parse->selected[index] && !parse->seen[index]) - return check_failed(parse->checks, "C5", - "cpu %d is not in /proc/cpuinfo", index); - } - return true; -} - -static bool check_cpuinfo_text(struct checks *checks, char *text, - const bool *selected, bool flags) -{ - struct cpuinfo_parse parse; - - cpuinfo_init(&parse, checks, selected, flags); - for_each_text_line(text, cpuinfo_line, &parse); - return cpuinfo_result(&parse); -} - -static bool check_cpuinfo(struct checks *checks, const bool *selected, - bool flags) -{ - struct cpuinfo_parse parse; - - cpuinfo_init(&parse, checks, selected, flags); - if (for_each_file_line("/proc/cpuinfo", cpuinfo_line, &parse) != 0) - return check_failed(checks, "C5", "read /proc/cpuinfo: %s", - strerror(errno)); - return cpuinfo_result(&parse); -} - -// --------------------------------------------------------------------------- -// Exhaustive check (CI only) -// --------------------------------------------------------------------------- - -static int msr_write(int fd, uint32_t msr, uint64_t value) -{ - ssize_t count = pwrite(fd, &value, sizeof(value), (off_t)msr); - - if (count == (ssize_t)sizeof(value)) - return 0; - return count < 0 ? -errno : -EIO; -} - -// The leaves that OpenVMM programs as explicit zeros. -static bool explicit_zero_leaf(uint32_t leaf) -{ - return (leaf >= 0x40000006 && leaf <= 0x4000000f) || - (leaf >= 0x40000080 && leaf <= 0x40000082); -} - -// X1 and X2 for one CPUID result: all zeros or, except for an explicit zero -// leaf, the Intel out-of-range result (the highest basic leaf's result for -// the same subleaf). -static bool exhaustive_leaf_ok(uint32_t leaf, const uint32_t value[4], - const uint32_t out_of_range[4], char *detail, - size_t size) -{ - char signature[13]; - - if (all_zero(value) || - (!explicit_zero_leaf(leaf) && - memcmp(value, out_of_range, 4 * sizeof(uint32_t)) == 0)) - return true; - memcpy(signature, &value[1], 4); - memcpy(signature + 4, &value[2], 4); - memcpy(signature + 8, &value[3], 4); - signature[12] = '\0'; - for (int index = 0; index < 12; index++) { - if (signature[index] < 0x20 || signature[index] > 0x7e) - signature[index] = '.'; - } - snprintf(detail, size, - "leaf 0x%08" PRIx32 " is %08" PRIx32 " %08" PRIx32 " %08" PRIx32 - " %08" PRIx32 " (signature \"%s\"), expected zero%s", - leaf, value[0], value[1], value[2], value[3], signature, - explicit_zero_leaf(leaf) ? "" : " or the highest basic leaf"); - return false; -} - -struct exhaustive { - int failures; - uint64_t tsc_hz; - uint64_t lapic_hz; - struct checks cpuinfo; -}; - -static void exhaustive_report(struct exhaustive *state, const char *id, int cpu, - bool passed, const char *detail) -{ - char escaped[DETAIL_MAX]; - - escape_text(detail, escaped, sizeof(escaped) - 1); - printf("NVX-TIME-ABI-EXHAUSTIVE: v=1 check=%s cpu=%d status=%s " - "detail=\"%s\"\n", - id, cpu, passed ? "pass" : "fail", escaped); - if (!passed) - state->failures++; -} - -static int exhaustive_summary(const struct exhaustive *state, int online) -{ - printf("NVX-TIME-ABI-EXHAUSTIVE: v=1 status=%s cpus=%d failures=%d\n", - state->failures == 0 ? "ok" : "fail", online, state->failures); - return state->failures == 0 ? 0 : 1; -} - -// X1 (every leaf 0x40000006..=0x400000ff) when STEP is 1, or X2 (every base -// 0x40000100..=0x4000ff00) when STEP is 0x100, on the current CPU. -static void exhaustive_leaves(struct exhaustive *state, const char *id, int cpu, - uint32_t first, uint32_t last, uint32_t step, - const uint32_t out_of_range[4]) -{ - char detail[DETAIL_MAX] = ""; - int failing = 0; - int count = 0; - - for (uint32_t leaf = first; leaf <= last; leaf += step, count++) { - char reason[DETAIL_MAX]; - uint32_t value[4]; - - cpuid(leaf, 0, value); - if (!exhaustive_leaf_ok(leaf, value, out_of_range, reason, - sizeof(reason)) && - failing++ == 0) - snprintf(detail, sizeof(detail), "%s", reason); - } - if (failing == 0) { - snprintf(detail, sizeof(detail), "%d %s read zero or the highest basic leaf", - count, step == 1 ? "leaves" : "bases"); - } else if (failing > 1) { - size_t length = strlen(detail); - - snprintf(detail + length, sizeof(detail) - length, "; %d of %d fail", - failing, count); - } - exhaustive_report(state, id, cpu, failing == 0, detail); -} - -// X3: every C3 property on this CPU. -static void exhaustive_x3(struct exhaustive *state, int fd, int cpu) -{ - static const uint32_t faulting[] = {0x40000000, 0x40000001, 0x40000020}; - char detail[DETAIL_MAX]; - uint64_t index = 0; - uint64_t tsc = 0; - uint64_t lapic = 0; - uint64_t control = 0; - uint64_t value; - bool ok = false; - int error; - - if ((error = msr_read(fd, 0x40000002, &index)) != 0 || - index != (uint64_t)cpu) - snprintf(detail, sizeof(detail), - "MSR 0x40000002 (VP index) is %" PRIu64 " (error %d)", index, - error); - else if ((error = msr_read(fd, 0x40000022, &tsc)) != 0 || tsc == 0 || - (state->tsc_hz != 0 && tsc != state->tsc_hz)) - snprintf(detail, sizeof(detail), - "MSR 0x40000022 (TSC rate) is %" PRIu64 - " (error %d; first CPU %" PRIu64 ")", - tsc, error, state->tsc_hz); - else if (state->cpuinfo.cpu_khz_known[cpu] && - tsc / 1000 != state->cpuinfo.cpu_khz[cpu]) - snprintf(detail, sizeof(detail), - "floor(F / 1000) is %" PRIu64 " kHz but cpu MHz is %" PRIu64 - " kHz", - tsc / 1000, state->cpuinfo.cpu_khz[cpu]); - else if ((error = msr_read(fd, 0x40000023, &lapic)) != 0 || - (lapic != 1000000000 && lapic != 200000000) || - (state->lapic_hz != 0 && lapic != state->lapic_hz)) - snprintf(detail, sizeof(detail), - "MSR 0x40000023 (LAPIC rate) is %" PRIu64 - " (error %d; first CPU %" PRIu64 ")", - lapic, error, state->lapic_hz); - else if ((error = msr_read(fd, 0x40000118, &control)) != 0 || control != 1) - snprintf(detail, sizeof(detail), - "MSR 0x40000118 is %" PRIu64 " (error %d)", control, error); - else - ok = true; - for (size_t item = 0; ok && item < ARRAY_SIZE(faulting); item++) { - error = msr_read(fd, faulting[item], &value); - if (error != -EIO) { - ok = false; - snprintf(detail, sizeof(detail), - "MSR 0x%08" PRIx32 " read did not fail with EIO (error %d)", - faulting[item], error); - } - } - if (ok) { - snprintf(detail, sizeof(detail), - "vp_index=%d tsc_hz=%" PRIu64 " lapic_hz=%" PRIu64 - " invariant_control=1", - cpu, tsc, lapic); - if (state->tsc_hz == 0) { - state->tsc_hz = tsc; - state->lapic_hz = lapic; - } - } - exhaustive_report(state, "X3", cpu, ok, detail); -} - -// X4: MSR 0x40000118 accepts 0 and 1, each read back, and rejects 2. It is -// left at 1, the value Linux wrote at boot, whatever the outcome. -static void exhaustive_x4(struct exhaustive *state, int fd, int cpu) -{ - char detail[DETAIL_MAX] = "0 and 1 accepted, 2 rejected, left at 1"; - uint64_t value = 0; - bool ok = true; - int error = 0; - - for (uint64_t written = 0; ok && written <= 1; written++) { - if ((error = msr_write(fd, 0x40000118, written)) != 0 || - (error = msr_read(fd, 0x40000118, &value)) != 0 || - value != written) { - ok = false; - snprintf(detail, sizeof(detail), - "after writing %" PRIu64 ", MSR 0x40000118 is %" PRIu64 - " (error %d)", - written, value, error); - } - } - if (ok && (error = msr_write(fd, 0x40000118, 2)) != -EIO) { - ok = false; - snprintf(detail, sizeof(detail), - "write of 2 to MSR 0x40000118 did not fail with EIO (error %d)", - error); - } - (void)msr_write(fd, 0x40000118, 1); - if (ok && (msr_read(fd, 0x40000118, &value) != 0 || value != 1)) { - ok = false; - snprintf(detail, sizeof(detail), - "MSR 0x40000118 is %" PRIu64 " after the check", value); - } - exhaustive_report(state, "X4", cpu, ok, detail); -} - -// X5: writes to the identity MSRs that are read-only or absent fail. X6: -// reads of IA32_TSC_ADJUST and IA32_TSC_DEADLINE fail. -static void exhaustive_faults(struct exhaustive *state, int fd, int cpu, - bool writes) -{ - static const uint32_t read_only[] = {0x40000000, 0x40000001, 0x40000002, - 0x40000020, 0x40000022, 0x40000023}; - static const uint32_t absent[] = {0x3b, 0x6e0}; - const uint32_t *list = writes ? read_only : absent; - size_t length = writes ? ARRAY_SIZE(read_only) : ARRAY_SIZE(absent); - char detail[DETAIL_MAX]; - bool ok = true; - - snprintf(detail, sizeof(detail), "%zu %s fail with EIO", length, - writes ? "writes" : "reads"); - for (size_t item = 0; ok && item < length; item++) { - uint64_t value = 0; - int error = writes ? msr_write(fd, list[item], 0) - : msr_read(fd, list[item], &value); - - if (error != -EIO) { - ok = false; - snprintf(detail, sizeof(detail), - "%s MSR 0x%" PRIx32 " did not fail with EIO (error %d)", - writes ? "write of 0 to" : "read of", list[item], error); - } - } - exhaustive_report(state, writes ? "X5" : "X6", cpu, ok, detail); -} - -// The CI-only exhaustive check: X1 to X6 on every online CPU. It reports and -// exits, never powers off, and production boots never run it. -static int exhaustive_run(void) -{ - static const char *const ids[] = {"X1", "X2", "X3", "X4", "X5", "X6"}; - struct cpuinfo_parse parse; - struct exhaustive state; - bool cpus[MAX_CPUS]; - cpu_set_t original; - char text[256]; - int online; - - memset(&state, 0, sizeof(state)); - if (read_text("/sys/devices/system/cpu/online", text, sizeof(text)) < 0 || - (online = parse_cpu_list(text, cpus)) <= 0) { - state.failures = 1; - return exhaustive_summary(&state, 0); - } - cpuinfo_init(&parse, &state.cpuinfo, cpus, false); - (void)for_each_file_line("/proc/cpuinfo", cpuinfo_line, &parse); - if (sched_getaffinity(0, sizeof(original), &original) != 0) - CPU_ZERO(&original); - for (int cpu = 0; cpu < MAX_CPUS; cpu++) { - char detail[DETAIL_MAX]; - uint32_t basic[4]; - uint32_t out_of_range[4]; - char path[64]; - int fd; - - if (!cpus[cpu]) - continue; - if (pin_to_cpu(cpu) != 0) { - for (size_t item = 0; item < ARRAY_SIZE(ids); item++) - exhaustive_report(&state, ids[item], cpu, false, - "cannot run on this CPU"); - continue; - } - cpuid(0, 0, basic); - cpuid(basic[0], 0, out_of_range); - exhaustive_leaves(&state, "X1", cpu, 0x40000006, 0x400000ff, 1, - out_of_range); - exhaustive_leaves(&state, "X2", cpu, 0x40000100, 0x4000ff00, 0x100, - out_of_range); - snprintf(path, sizeof(path), "/dev/cpu/%d/msr", cpu); - fd = open(path, O_RDWR | O_CLOEXEC); - if (fd < 0) { - snprintf(detail, sizeof(detail), "open %s: %s", path, - strerror(errno)); - for (size_t item = 2; item < ARRAY_SIZE(ids); item++) - exhaustive_report(&state, ids[item], cpu, false, detail); - continue; - } - exhaustive_x3(&state, fd, cpu); - exhaustive_x4(&state, fd, cpu); - exhaustive_faults(&state, fd, cpu, true); - exhaustive_faults(&state, fd, cpu, false); - close(fd); - } - if (CPU_COUNT(&original) > 0) - (void)sched_setaffinity(0, sizeof(original), &original); - return exhaustive_summary(&state, online); -} - -// C6: `tsc` is current, and only tsc, refined-jiffies, or jiffies is -// available. -static bool check_clocksource_values(struct checks *checks, const char *current, - char *available) -{ - bool tsc = false; - - if (strncmp(current, "tsc", 3) != 0 || - (current[3] != '\0' && current[3] != '\n')) - return check_failed(checks, "C6", "current clocksource is %.*s", - (int)strcspn(current, "\n"), current); - for (char *token = strtok(available, " \n"); token != NULL; - token = strtok(NULL, " \n")) { - if (strcmp(token, "tsc") == 0) - tsc = true; - else if (strcmp(token, "refined-jiffies") != 0 && - strcmp(token, "jiffies") != 0) - return check_failed(checks, "C6", "clocksource %.64s is available", - token); - } - if (!tsc) - return check_failed(checks, "C6", "tsc is not an available clocksource"); - return true; -} - -static bool check_clocksource(struct checks *checks) -{ - char current[64]; - char available[256]; - - if (read_text(CLOCKSOURCE_DIR "/current_clocksource", current, - sizeof(current)) < 0 || - read_text(CLOCKSOURCE_DIR "/available_clocksource", available, - sizeof(available)) < 0) - return check_failed(checks, "C6", "clocksource sysfs: %s", - strerror(errno)); - return check_clocksource_values(checks, current, available); -} - -struct tick_section { - bool seen; - int mode; - int state; - char device[48]; - char handler[48]; -}; - -// Line-by-line /proc/timer_list parser. The file lists every active hrtimer -// before the tick devices, so its size depends on the workload. -struct timer_list_parse { - struct tick_section cpus[MAX_CPUS]; - struct tick_section broadcast; - struct tick_section *current; - int tick_mode; -}; - -static void timer_list_init(struct timer_list_parse *parse) -{ - static const struct tick_section empty = {false, -1, -1, "", ""}; - - for (int index = 0; index < MAX_CPUS; index++) - parse->cpus[index] = empty; - parse->broadcast = empty; - parse->current = NULL; - parse->tick_mode = -1; -} - -static void timer_list_line(void *context, char *line) -{ - struct timer_list_parse *parse = context; - struct tick_section *current = parse->current; - char name[48]; - int value; - - if (sscanf(line, "Tick Device: mode: %d", &value) == 1) { - parse->tick_mode = value; - parse->current = NULL; - } else if (strncmp(line, "Broadcast device", 16) == 0) { - parse->current = &parse->broadcast; - parse->current->seen = true; - parse->current->mode = parse->tick_mode; - } else if (sscanf(line, "Per CPU device: %d", &value) == 1) { - parse->current = - value >= 0 && value < MAX_CPUS ? &parse->cpus[value] : NULL; - if (parse->current != NULL) { - parse->current->seen = true; - parse->current->mode = parse->tick_mode; - } - } else if (current != NULL && - sscanf(line, "Clock Event Device: %47s", name) == 1) { - snprintf(current->device, sizeof(current->device), "%s", name); - } else if (current != NULL && sscanf(line, " mode: %d", &value) == 1) { - current->state = value; - } else if (current != NULL && - sscanf(line, " event_handler: %47s", name) == 1) { - snprintf(current->handler, sizeof(current->handler), "%s", name); - } -} - -// C7 verdict: every CPU in SELECTED ticks with the LAPIC in one-shot mode -// through hrtimer_interrupt, and no broadcast device exists. -static bool timer_list_result(const struct timer_list_parse *parse, - const bool *selected, char *detail, size_t size) -{ - if (parse->broadcast.seen && - strcmp(parse->broadcast.device, "") != 0) { - snprintf(detail, size, "broadcast clock event device is %s", - parse->broadcast.device); - return false; - } - for (int cpu = 0; cpu < MAX_CPUS; cpu++) { - const struct tick_section *section = &parse->cpus[cpu]; - - if (!selected[cpu]) - continue; - if (!section->seen || strcmp(section->device, "lapic") != 0 || - strcmp(section->handler, "hrtimer_interrupt") != 0 || - section->mode != 1 || (section->state != 3 && section->state != 4)) { - snprintf(detail, size, - "cpu %d tick device %s handler %s tick mode %d state %d", - cpu, section->seen ? section->device : "missing", - section->handler[0] != '\0' ? section->handler : "none", - section->mode, section->state); - return false; - } - } - return true; -} - -static bool timer_list_verdict(char *text, const bool *selected, char *detail, - size_t size) -{ - static struct timer_list_parse parse; - - timer_list_init(&parse); - for_each_text_line(text, timer_list_line, &parse); - return timer_list_result(&parse, selected, detail, size); -} - -// Polls C7 for at most WAIT_NS, because a CPU switches to one-shot mode at -// its first tick. A zero wait checks once. -static bool check_timer_list(struct checks *checks, int64_t wait_ns) -{ - static struct timer_list_parse parse; - char detail[DETAIL_MAX] = ""; - int64_t deadline = clock_ns(CLOCK_MONOTONIC) + wait_ns; - - for (;;) { - timer_list_init(&parse); - if (for_each_file_line("/proc/timer_list", timer_list_line, &parse) != - 0) - return check_failed(checks, "C7", "read /proc/timer_list: %s", - strerror(errno)); - if (timer_list_result(&parse, checks->online, detail, sizeof(detail))) - return true; - if (clock_ns(CLOCK_MONOTONIC) >= deadline) - return check_failed(checks, "C7", "%s", detail); - nanosleep(&(struct timespec){0, 10 * NSEC_PER_MSEC}, NULL); - } -} - -// C8: no VMBus, no cpufreq, and the production RCU stall settings. -static bool check_platform(struct checks *checks) -{ - struct stat info; - int64_t value = -1; - - if (stat("/sys/bus/vmbus", &info) == 0) - return check_failed(checks, "C8", "/sys/bus/vmbus exists"); - if (stat("/sys/devices/system/cpu/cpufreq/policy0", &info) == 0) - return check_failed(checks, "C8", "cpufreq policy0 exists"); - if (read_int64(RCU_STALL_SUPPRESS, &value) != 0 || value != 0) - return check_failed(checks, "C8", "rcu_cpu_stall_suppress is %" PRId64, - value); - if (read_int64(RCU_STALL_TIMEOUT, &value) != 0 || value != 21) - return check_failed(checks, "C8", "rcu_cpu_stall_timeout is %" PRId64, - value); - return true; -} - -// C9: no clock workaround on the kernel command line. -static bool check_cmdline_text(struct checks *checks, char *cmdline) -{ - static const char *const prefixes[] = {"tsc_early_khz=", "lapic_timer_hz="}; - static const char *const tokens[] = {"notsc", "nolapic", "nolapic_timer", - "tsc=unstable", "hpet=force"}; - - for (char *token = strtok(cmdline, " \t\n"); token != NULL; - token = strtok(NULL, " \t\n")) { - bool bad = strncmp(token, "clocksource=", 12) == 0 && - strcmp(token + 12, "tsc") != 0; - - for (size_t index = 0; index < ARRAY_SIZE(prefixes); index++) - bad = bad || strncmp(token, prefixes[index], - strlen(prefixes[index])) == 0; - for (size_t index = 0; index < ARRAY_SIZE(tokens); index++) - bad = bad || strcmp(token, tokens[index]) == 0; - if (bad) - return check_failed(checks, "C9", "command line contains %.120s", - token); - } - return true; -} - -static bool check_cmdline(struct checks *checks) -{ - char text[4096]; - - if (read_text("/proc/cmdline", text, sizeof(text)) < 0) - return check_failed(checks, "C9", "read /proc/cmdline: %s", - strerror(errno)); - return check_cmdline_text(checks, text); -} - -static pid_t daemon_pid(void) -{ - char path[64]; - char comm[32]; - int64_t pid; - - if (read_int64(DAEMON_PID_PATH, &pid) != 0 || pid <= 1 || pid > INT_MAX || - kill((pid_t)pid, 0) != 0) - return -1; - snprintf(path, sizeof(path), "/proc/%" PRId64 "/comm", pid); - if (read_text(path, comm, sizeof(comm)) < 0 || - strcmp(comm, "nvx-time\n") != 0) - return -1; - return (pid_t)pid; -} - -// The asynchronous boot checks and repair step 7 start at idle priority, so -// that on few vCPUs they never take a CPU from the workload, and continue at -// normal priority after IDLE_BOUND_NS; the watcher and the discipline keep -// normal priority. musl does not implement sched_setscheduler(), so this uses -// the per-thread system call: PID 0 is the calling thread. -static void set_idle_priority(pid_t pid, bool idle) -{ - struct sched_param param = {0}; - - (void)syscall(SYS_sched_setscheduler, pid, idle ? SCHED_IDLE : SCHED_OTHER, - ¶m); -} - -// Raises every thread of this process to normal priority, twice, so that a -// thread an idle thread created during the first pass is raised too. -static void promote_threads(void) -{ - for (int pass = 0; pass < 2; pass++) { - DIR *tasks = opendir("/proc/self/task"); - struct dirent *entry; - - if (tasks == NULL) - return; - while ((entry = readdir(tasks)) != NULL) { - long tid = strtol(entry->d_name, NULL, 10); - - if (tid > 0) - set_idle_priority((pid_t)tid, false); - } - closedir(tasks); - } -} - -// A normal-priority thread that raises the boot checker's idle threads when -// the checks still run IDLE_BOUND_NS after they started. -struct promoter { - pthread_t thread; - pthread_mutex_t lock; - pthread_cond_t stop; - struct timespec deadline; - bool stopped; -}; - -static void *promoter_run(void *argument) -{ - struct promoter *promoter = argument; - bool stopped; - int result = 0; - - pthread_mutex_lock(&promoter->lock); - while (!promoter->stopped && result == 0) - result = pthread_cond_timedwait(&promoter->stop, &promoter->lock, - &promoter->deadline); - stopped = promoter->stopped; - pthread_mutex_unlock(&promoter->lock); - if (!stopped) - promote_threads(); - return NULL; -} - -// Starts PROMOTER, at the caller's priority, for checks that started at -// START_NS. Returns true when it runs. -static bool promoter_start(struct promoter *promoter, int64_t start_ns) -{ - int64_t deadline = start_ns + IDLE_BOUND_NS; - pthread_condattr_t attributes; - bool ready; - - promoter->stopped = false; - promoter->deadline.tv_sec = deadline / NSEC_PER_SEC; - promoter->deadline.tv_nsec = deadline % NSEC_PER_SEC; - if (pthread_condattr_init(&attributes) != 0) - return false; - ready = pthread_condattr_setclock(&attributes, CLOCK_MONOTONIC) == 0 && - pthread_cond_init(&promoter->stop, &attributes) == 0; - pthread_condattr_destroy(&attributes); - return ready && pthread_mutex_init(&promoter->lock, NULL) == 0 && - pthread_create(&promoter->thread, NULL, promoter_run, promoter) == - 0; -} - -static void promoter_stop(struct promoter *promoter) -{ - pthread_mutex_lock(&promoter->lock); - promoter->stopped = true; - pthread_cond_signal(&promoter->stop); - pthread_mutex_unlock(&promoter->lock); - pthread_join(promoter->thread, NULL); -} - -// Repair steps 6 and 7 on the readiness path: the acknowledgement when the -// host gated its input, then hand step 7 to DAEMON. The sigqueue() value is -// the readiness path's wall time since START_NS (CLOCK_MONOTONIC), in -// microseconds clamped to 32 bits, read after the acknowledgement so that the -// read does not delay it. The readiness path's CPU time is not reported: the -// restore latency gate measures that path. Returns -1, without acknowledging, -// when no daemon runs; 1 when the daemon vanished after the acknowledgement. -// The caller has enabled the ports when ACK is set. -static int signal_restore(pid_t daemon, bool ack, int64_t start_ns) -{ - union sigval value; - int64_t elapsed_us; - - if (daemon <= 0 || kill(daemon, 0) != 0) - return -1; - if (ack) - outb(SNAPSHOT_ACKNOWLEDGE, PORT_SNAPSHOT); - elapsed_us = (clock_ns(CLOCK_MONOTONIC) - start_ns) / NSEC_PER_USEC; - if (elapsed_us > UINT32_MAX) - elapsed_us = UINT32_MAX; - value.sival_ptr = (void *)(uintptr_t)(elapsed_us < 0 ? 0 : elapsed_us); - return sigqueue(daemon, SIGUSR1, value) == 0 ? 0 : 1; -} - -// C10: the daemon runs, has recorded no violation, and no stall was counted. -static bool check_daemon(struct checks *checks) -{ - struct time_state state; - int64_t stalls = -1; - - if (daemon_pid() < 0) - return check_failed(checks, "C10", "the time daemon is not running"); - if (state_load(&state) != 0) - return check_failed(checks, "C10", "time state is unreadable"); - if (state.violations != 0) - return check_failed(checks, "C10", - "the time daemon recorded %" PRIu64 " violations", - state.violations); - if (read_int64(RCU_STALL_COUNT, &stalls) != 0 || stalls != 0) - return check_failed(checks, "C10", "rcu_stall_count is %" PRId64, - stalls); - return true; -} - -// C11: on the CI debug kernel, the soft-lockup and hung-task detectors run. -static bool check_debug_watchdogs(struct checks *checks) -{ - bool soft = access(SOFT_WATCHDOG, F_OK) == 0; - bool hung = access(HUNG_TASK_TIMEOUT, F_OK) == 0; - int64_t value = -1; - - if (!soft && !hung) - return true; - if (!soft || read_int64(SOFT_WATCHDOG, &value) != 0 || value != 1) - return check_failed(checks, "C11", "soft_watchdog is %" PRId64, value); - if (!hung || read_int64(HUNG_TASK_TIMEOUT, &value) != 0 || value == 0) - return check_failed(checks, "C11", - "hung_task_timeout_secs is %" PRId64, value); - return true; -} - -// C1, C2, and C3 on every CPU in SELECTED, pinned to each in turn. The -// three checks are independent, so a report-only run shows each failure; -// the loop stops at the first CPU that fails. -static bool check_cpus(struct checks *checks, const bool *selected, - bool identity, bool msrs) -{ - cpu_set_t original; - bool ok = true; - - if (sched_getaffinity(0, sizeof(original), &original) != 0) - return check_failed(checks, "C1", "sched_getaffinity: %s", - strerror(errno)); - for (int cpu = 0; ok && cpu < MAX_CPUS; cpu++) { - if (!selected[cpu]) - continue; - if (pin_to_cpu(cpu) != 0) { - ok = check_failed(checks, "C1", "cannot run on cpu %d: %s", cpu, - strerror(errno)); - break; - } - if (identity) { - if (!check_identity(checks, cpu)) - ok = false; - if (!check_time_bits(checks, cpu)) - ok = false; - } - if (msrs && !check_msrs(checks, cpu, true)) - ok = false; - } - (void)sched_setaffinity(0, sizeof(original), &original); - return ok; -} - -struct cpu_job { - pthread_t thread; - struct checks checks; - bool identity; - bool msrs; - bool running; - int cpu; -}; - -static void *cpu_job_run(void *argument) -{ - struct cpu_job *job = argument; - - if (pin_to_cpu(job->cpu) != 0) { - check_failed(&job->checks, "C1", "cannot run on cpu %d: %s", job->cpu, - strerror(errno)); - return NULL; - } - if (job->identity) { - check_identity(&job->checks, job->cpu); - check_time_bits(&job->checks, job->cpu); - } - if (job->msrs) - check_msrs(&job->checks, job->cpu, true); - return NULL; -} - -// A boot check that runs in its own thread beside the per-CPU checks, on a -// private copy of the checks. -struct side_job { - pthread_t thread; - struct checks checks; - void (*run)(struct checks *checks, void *context); - void *context; - bool running; -}; - -static void *side_job_run(void *argument) -{ - struct side_job *job = argument; - - job->run(&job->checks, job->context); - return NULL; -} - -// check_cpus() with every CPU in its own thread pinned to it, beside the -// SIDES checks, each in a thread of its own, while the caller runs FOREGROUND -// (when set). Running them in parallel keeps the cost from growing with the -// CPU count and leaves the caller on its warm CPU. The threads share one -// stack mapping, so creating one maps nothing. Each job works on a copy of -// CHECKS whose failures are added back; a job whose thread cannot start runs -// in the caller afterwards. -static void check_cpus_parallel(struct checks *checks, const bool *selected, - bool identity, bool msrs, - struct side_job *sides, int side_count, - void (*foreground)(struct checks *, void *), - void *foreground_context) -{ - enum { STACK_SIZE = 128 * 1024 }; - static struct cpu_job jobs[MAX_CPUS]; - bool serial[MAX_CPUS] = {false}; - bool any_serial = false; - pthread_attr_t attributes; - pthread_attr_t *attr = NULL; - size_t threads = (size_t)side_count; - size_t next = 0; - uint8_t *stacks; - - for (int cpu = 0; cpu < MAX_CPUS; cpu++) - threads += selected[cpu] ? 1 : 0; - // With one CPU, threads only add their own cost. - if (threads <= (size_t)side_count + 1) { - if (foreground != NULL) - foreground(checks, foreground_context); - for (int index = 0; index < side_count; index++) { - struct side_job *side = &sides[index]; - - side->checks = *checks; - side->checks.failures = 0; - side->run(&side->checks, side->context); - checks->failures += side->checks.failures; - } - check_cpus(checks, selected, identity, msrs); - return; - } - stacks = mmap(NULL, threads * STACK_SIZE, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - if (stacks != MAP_FAILED && pthread_attr_init(&attributes) == 0) - attr = &attributes; - for (int index = 0; index < side_count; index++) { - struct side_job *side = &sides[index]; - - side->checks = *checks; - side->checks.failures = 0; - if (attr != NULL) - pthread_attr_setstack(attr, stacks + STACK_SIZE * next++, - STACK_SIZE); - side->running = - pthread_create(&side->thread, attr, side_job_run, side) == 0; - } - for (int cpu = 0; cpu < MAX_CPUS; cpu++) { - struct cpu_job *job = &jobs[cpu]; - - job->running = false; - if (!selected[cpu]) - continue; - job->checks = *checks; - job->checks.failures = 0; - job->identity = identity; - job->msrs = msrs; - job->cpu = cpu; - if (attr != NULL) - pthread_attr_setstack(attr, stacks + STACK_SIZE * next++, - STACK_SIZE); - if (pthread_create(&job->thread, attr, cpu_job_run, job) == 0) - job->running = true; - else - serial[cpu] = any_serial = true; - } - if (foreground != NULL) - foreground(checks, foreground_context); - for (int index = 0; index < side_count; index++) { - struct side_job *side = &sides[index]; - - if (side->running) - pthread_join(side->thread, NULL); - else - side->run(&side->checks, side->context); - checks->failures += side->checks.failures; - } - for (int cpu = 0; cpu < MAX_CPUS; cpu++) { - struct cpu_job *job = &jobs[cpu]; - - if (!job->running) - continue; - pthread_join(job->thread, NULL); - checks->failures += job->checks.failures; - // C3 reads the partition-wide rates on CPU 0 only. - if (cpu == 0 && msrs) { - checks->tsc_hz = job->checks.tsc_hz; - checks->lapic_hz = job->checks.lapic_hz; - } - } - if (attr != NULL) - pthread_attr_destroy(attr); - if (stacks != MAP_FAILED) - munmap(stacks, threads * STACK_SIZE); - if (any_serial) - check_cpus(checks, serial, identity, msrs); -} -// --------------------------------------------------------------------------- -// Host time samples and clock adjustment -// --------------------------------------------------------------------------- - -// Latches a sample with selector 0xa7 and reads it from 0xeb. CLOCK_REALTIME -// brackets the selector write, so the host instant lies in [t0, t1]. The -// caller holds the portb lock. -static int take_sample(struct time_sample *sample, char *detail, size_t size) -{ - uint8_t bytes[SAMPLE_SIZE]; - - sample->t0 = clock_ns(CLOCK_REALTIME); - outb(SELECT_TIME_SAMPLE, PORTB_STATUS); - sample->t1 = clock_ns(CLOCK_REALTIME); - portb_read(PORTB_WINDOW, bytes, sizeof(bytes)); - return parse_sample(bytes, sample, detail, size); -} - -static int lock_portb(char *detail, size_t size) -{ - int lock = lock_file(PORTB_LOCK_PATH); - - if (lock < 0) - snprintf(detail, size, "portb lock: %s", strerror(errno)); - return lock; -} - -// Takes up to ATTEMPTS samples of generation G and keeps the first whose -// uncertainty is within BOUND_NS. Returns 0 when one is, 1 when only wider -// samples were seen (the narrowest is returned), and -1 when none was valid -// or the VMM has no time-sample window. A sample from another generation -// stops the search with -2. The caller holds the portb lock. -static int best_sample(int attempts, int64_t bound_ns, uint32_t generation, - struct time_sample *best, int64_t *theta, - int64_t *epsilon, char *detail, size_t size) -{ - unsigned status; - int result = -1; - - *epsilon = INT64_MAX; - if (enable_ports() != 0) { - snprintf(detail, size, "ioperm: %s", strerror(errno)); - return -1; - } - status = inb(PORTB_STATUS); - if ((status & PORTB_TIME_WINDOW) == 0) { - snprintf(detail, size, "portb status 0x%02x has no time-sample window", - status); - return -1; - } - for (int attempt = 0; attempt < attempts; attempt++) { - struct time_sample sample; - int64_t sample_theta; - int64_t sample_epsilon; - - if (take_sample(&sample, detail, size) != 0) - continue; - if (sample.generation != generation) { - snprintf(detail, size, - "time sample generation %" PRIu32 ", expected %" PRIu32, - sample.generation, generation); - return -2; - } - pair_clocks(sample.t0, sample.t1, sample.utc_ns, &sample_theta, - &sample_epsilon); - if (sample_epsilon < *epsilon) { - *best = sample; - *theta = sample_theta; - *epsilon = sample_epsilon; - result = 1; - } - if (sample_epsilon <= bound_ns) - return 0; - snprintf(detail, size, - "time sample uncertainty %" PRId64 " ns exceeds %" PRId64 - " ns", - sample_epsilon, bound_ns); - } - return result; -} - -static int step_clock(int64_t offset_ns) -{ - struct timex tx; - - memset(&tx, 0, sizeof(tx)); - tx.modes = ADJ_SETOFFSET | ADJ_NANO; - offset_to_timeval(offset_ns, &tx.time); - return adjtimex(&tx) < 0 ? -1 : 0; -} - -static int set_frequency(int64_t frequency) -{ - struct timex tx; - - memset(&tx, 0, sizeof(tx)); - tx.modes = ADJ_FREQUENCY | ADJ_STATUS; - tx.freq = (long)frequency; - tx.status = STA_PLL | STA_NANO; - return adjtimex(&tx) < 0 ? -1 : 0; -} - -static int64_t current_frequency(void) -{ - struct timex tx; - - memset(&tx, 0, sizeof(tx)); - return adjtimex(&tx) < 0 ? 0 : (int64_t)tx.freq; -} - -// C12: one sample within 1 ms (three attempts), then a clock step. The boot -// step is not a discontinuity. -static bool check_initial_sample(struct checks *checks, int64_t *theta, - int64_t *epsilon) -{ - struct time_sample sample; - char detail[DETAIL_MAX] = "no valid time sample"; - int result; - int lock; - - lock = lock_portb(detail, sizeof(detail)); - if (lock < 0) - return check_failed(checks, "C12", "%s", detail); - result = best_sample(SAMPLE_ATTEMPTS, BOOT_BOUND_NS, g_generation, &sample, - theta, epsilon, detail, sizeof(detail)); - close(lock); - if (result != 0) - return check_failed(checks, "C12", "%s", detail); - if (step_clock(*theta) != 0) - return check_failed(checks, "C12", "adjtimex step: %s", - strerror(errno)); - return true; -} - -// --------------------------------------------------------------------------- -// Daemon: violation watcher, wall-clock discipline, restore completion -// --------------------------------------------------------------------------- - -struct daemon { - int kmsg; - int signals; - int timer; - int polls; - int64_t last_accepted_ns; - pid_t worker_pid; - int64_t worker_promote_ns; - int64_t worker_deadline_ns; - bool suppression_released; - bool restore_pending; - int64_t restore_due_ns; - int64_t readiness_us; -}; - -static void watcher_consume(void *context, const char *message) -{ - const char *code = match_watch_rule(message); - - (void)context; - if (code != NULL) - fail_fatal(STATUS_VIOLATION, code, "watcher", PHASE_RUNTIME, "%.400s", - message); -} - -static void watch_kernel_log(struct daemon *daemon) -{ - if (kmsg_drain(daemon->kmsg, watcher_consume, daemon) == -EPIPE) - fail_fatal(STATUS_VIOLATION, "G_KMSG_OVERRUN", "watcher", - PHASE_RUNTIME, "kernel log records were lost (EPIPE)"); -} - -static int arm_timer(struct daemon *daemon) -{ - struct itimerspec period; - - memset(&period, 0, sizeof(period)); - period.it_value.tv_sec = - daemon->polls < FAST_POLLS ? FAST_PERIOD_S : SLOW_PERIOD_S; - return timerfd_settime(daemon->timer, 0, &period, NULL); -} - -struct poll_outcome { - bool accepted; - bool stepped; - bool synchronized; - int64_t theta; - int64_t epsilon; - int64_t step_realtime_ns; - int64_t frequency; -}; - -static void apply_poll(struct time_state *state, const void *context) -{ - const struct poll_outcome *outcome = context; - - if (outcome->accepted) { - state->samples++; - state->offset_ns = outcome->theta; - state->uncertainty_ns = outcome->epsilon; - state->frequency_ppb = frequency_to_ppb(outcome->frequency); - snprintf(state->last_sample_error, sizeof(state->last_sample_error), - "none"); - if (outcome->stepped) { - state->discontinuities++; - snprintf(state->last_discontinuity, - sizeof(state->last_discontinuity), "step"); - state->last_step_ns = outcome->theta; - state->last_step_realtime_ns = outcome->step_realtime_ns; - } - } else { - state->rejected_samples++; - snprintf(state->last_sample_error, sizeof(state->last_sample_error), - "G_SAMPLE_UNCERTAIN"); - } - state->synchronized = outcome->synchronized; -} - -// One discipline poll: the RCU stall backstop, then up to three samples. A -// step reapplies the frequency and status that the step's NTP reset clears. -// The poll holds the portb lock from its first sample to the published state, -// as a capture does from its request to the restored clock, so a snapshot -// never contains a half-applied poll: a poll blocked behind a capture samples -// after the restore, sees the new generation, and is discarded. -static void discipline_poll(struct daemon *daemon) -{ - struct poll_outcome outcome; - struct time_sample sample; - char detail[DETAIL_MAX] = ""; - bool fast = daemon->polls < FAST_POLLS; - int64_t stalls = 0; - int result; - int lock; - - memset(&outcome, 0, sizeof(outcome)); - if (read_int64(RCU_STALL_COUNT, &stalls) == 0 && stalls != 0) - fail_fatal(STATUS_VIOLATION, "G_RCU_STALL", "watcher", PHASE_RUNTIME, - "rcu_stall_count is %" PRId64, stalls); - daemon->polls++; - lock = lock_portb(detail, sizeof(detail)); - result = lock < 0 ? -1 - : best_sample(SAMPLE_ATTEMPTS, POLL_BOUND_NS, - g_generation, &sample, &outcome.theta, - &outcome.epsilon, detail, sizeof(detail)); - if (result == -2) { - close(lock); - return; - } - if (result == 0) { - struct timex tx; - int64_t frequency = current_frequency(); - - outcome.stepped = plan_correction(outcome.theta, outcome.epsilon, fast, - &tx); - outcome.accepted = adjtimex(&tx) >= 0 && - (!outcome.stepped || set_frequency(frequency) == 0); - outcome.step_realtime_ns = clock_ns(CLOCK_REALTIME); - outcome.frequency = current_frequency(); - if (outcome.accepted) - daemon->last_accepted_ns = clock_ns(CLOCK_MONOTONIC); - } - outcome.synchronized = - daemon->last_accepted_ns != 0 && - clock_ns(CLOCK_MONOTONIC) - daemon->last_accepted_ns <= - SYNCHRONIZED_AGE_NS; - (void)state_apply(apply_poll, &outcome); - if (lock >= 0) - close(lock); -} - -// Waits for a full normal RCU grace period: with rcu_normal set, the -// synchronize_rcu_expedited() in every unmount waits for one that starts -// after the call, on any number of CPUs. The private mount namespace makes the -// caller a process of its own: the restore worker, or a child. Returns 0 when -// the release completed. -static int grace_period_release(void) -{ - int64_t normal = 0; - bool saved = read_int64(RCU_NORMAL, &normal) == 0 && - write_int64(RCU_NORMAL, 1) == 0; - int status = 1; - - if (saved && unshare(CLONE_NEWNS) == 0 && - mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) == 0) { - (void)mkdir(RCU_SYNC_PATH, 0700); - if (mount("nvx-rcu-sync", RCU_SYNC_PATH, "tmpfs", - MS_NOSUID | MS_NODEV | MS_NOEXEC, "size=4k") == 0 && - umount2(RCU_SYNC_PATH, 0) == 0) - status = 0; - (void)rmdir(RCU_SYNC_PATH); - } - if (saved && write_int64(RCU_NORMAL, normal) != 0) - status = 1; - return status; -} - -// Runs the grace-period release in a child and waits at most TIMEOUT_S -// seconds for it, for callers without the daemon's event loop. A release -// still pending at the deadline counts as done: the grace period is stuck, -// and the kernel then reports the real stall. -static bool release_with_deadline(int64_t timeout_s) -{ - int64_t deadline = clock_ns(CLOCK_MONOTONIC) + timeout_s * NSEC_PER_SEC; - sigset_t mask; - sigset_t previous; - bool released = false; - pid_t pid; - - sigemptyset(&mask); - sigaddset(&mask, SIGCHLD); - sigprocmask(SIG_BLOCK, &mask, &previous); - pid = fork(); - if (pid == 0) { - sigprocmask(SIG_SETMASK, &previous, NULL); - _exit(grace_period_release()); - } - while (pid > 0) { - int status; - pid_t done = waitpid(pid, &status, WNOHANG); - int64_t remaining = deadline - clock_ns(CLOCK_MONOTONIC); - - if (done == pid) { - released = WIFEXITED(status) && WEXITSTATUS(status) == 0; - break; - } - if (done < 0 && errno != EINTR) - break; - if (remaining <= 0) { - released = true; - break; - } - (void)sigtimedwait(&mask, NULL, - &(struct timespec){remaining / NSEC_PER_SEC, - remaining % NSEC_PER_SEC}); - } - sigprocmask(SIG_SETMASK, &previous, NULL); - return released; -} - -static void run_restore_checks(struct checks *checks, const bool *new_cpus, - int new_count); - -static int parse_new_cpus(const char *text, bool *cpus) -{ - if (strcmp(text, "") == 0 || strcmp(text, "none") == 0) { - memset(cpus, 0, sizeof(bool) * MAX_CPUS); - return 0; - } - return parse_cpu_list(text, cpus); -} - -// Parses the restore record that restore-finish writes for the daemon: the -// generation it finished and the CPUs that activation brought online. -// Returns the number of those CPUs, or -1 when the record is malformed. -static int parse_restore_record(const char *text, unsigned long long *generation, - bool *new_cpus) -{ - char cpus[128] = "none"; - - if (sscanf(text, "generation=%llu new_cpus=%127s", generation, cpus) < 1) - return -1; - return parse_new_cpus(cpus, new_cpus); -} - -// The times that restore_work records. The wall time is READINESS_US, the -// readiness path's, plus the checks' since START_NS, when they were due. The -// CPU time is repair step 7's alone: this process's since CPU_BASE_NS, plus -// the daemon's preparation, PRIOR_CPU_NS up to the fork and the fork's own -// share, which the daemon writes to FORK_FD (-1 when there is none) once the -// fork returns. -struct restore_timing { - int64_t start_ns; - int64_t readiness_us; - int64_t prior_cpu_ns; - int64_t cpu_base_ns; - int fork_fd; -}; - -// Repair step 7's CPU time so far, in a process that TIMING describes. The -// read waits for the daemon's fork share if the daemon has not yet written it. -static int64_t deferred_restore_cpu_ns(const struct restore_timing *timing) -{ - int64_t cpu = timing->prior_cpu_ns + cpu_time_ns() - timing->cpu_base_ns; - int64_t fork_share = 0; - - if (timing->fork_fd >= 0 && - read(timing->fork_fd, &fork_share, sizeof(fork_share)) == - sizeof(fork_share) && - fork_share > 0) - cpu += fork_share; - return cpu; -} - -// Forks the repair step 7 worker at SCHED_IDLE for a daemon whose preparation -// started at CPU_START_NS, its CPU clock, and completes TIMING's CPU fields -// for the child. The parent returns to normal priority as soon as the fork -// does. Its fork costs CPU time after the child's copy of TIMING was taken, so -// it writes that share to a pipe that the child reads. Returns fork()'s -// result. -static pid_t fork_deferred_restore(int64_t cpu_start_ns, - struct restore_timing *timing) -{ - int fds[2] = {-1, -1}; - int64_t fork_share; - pid_t pid; - - if (pipe2(fds, O_CLOEXEC) != 0) - fds[0] = fds[1] = -1; - timing->fork_fd = fds[0]; - timing->cpu_base_ns = 0; - set_idle_priority(0, true); - timing->prior_cpu_ns = cpu_time_ns() - cpu_start_ns; - pid = fork(); - if (pid == 0) { - if (fds[1] >= 0) - close(fds[1]); - return 0; - } - set_idle_priority(0, false); - if (fds[0] >= 0) - close(fds[0]); - timing->fork_fd = -1; - if (fds[1] >= 0) { - fork_share = cpu_time_ns() - cpu_start_ns - timing->prior_cpu_ns; - if (pid > 0) - (void)write_all(fds[1], &fork_share, sizeof(fork_share)); - close(fds[1]); - } - return pid; -} - -// Repair step 7, fail-fast: the restore checks, the grace-period release, the -// saved watchdog settings, the deferred C7, and the restore check's record. A -// child of the daemon runs it at SCHED_IDLE from DEFERRED_START_NS after the -// acknowledgement; the daemon raises it to normal priority IDLE_BOUND_NS later -// and releases the suppression itself at the stall timeout. restore-finish -// runs it inline, with its own deadline (WITH_DEADLINE), in report-only mode -// when no daemon runs. Neither recorded time counts the grace period wait or -// C7. Returns 0, or 1 when a report-only release failed. -static int restore_work(const bool *new_cpus, int new_count, bool with_deadline, - const struct restore_timing *timing) -{ - int64_t timeout_s = DEFAULT_STALL_TIMEOUT_S; - char detail[DETAIL_MAX]; - struct checks checks; - int64_t elapsed; - int64_t cpu; - bool released; - - if (checks_init(&checks, PHASE_RESTORE) != 0) - check_failed(&checks, "C6", "cannot read the CPU sets"); - else - run_restore_checks(&checks, new_cpus, new_count); - elapsed = clock_ns(CLOCK_MONOTONIC) - timing->start_ns; - cpu = deferred_restore_cpu_ns(timing); - if (with_deadline) { - (void)read_int64(RCU_STALL_TIMEOUT, &timeout_s); - released = release_with_deadline(timeout_s); - } else { - released = grace_period_release() == 0; - } - if (!released) - fail_fatal(STATUS_REPAIR, "G_REPAIR_SUPPRESSION", "repair", - PHASE_RESTORE, "the RCU grace-period release failed"); - if (suppression_restore(detail, sizeof(detail), true) != 0) - fail_fatal(STATUS_REPAIR, "G_REPAIR_SUPPRESSION", "repair", - PHASE_RESTORE, "%s", detail); - check_timer_list(&checks, TIMER_LIST_WAIT_NS); - if (record_check(PHASE_RESTORE, "ok", checks.online_count, - timing->readiness_us + elapsed / NSEC_PER_USEC, - cpu / NSEC_PER_USEC, 0, 0, checks.failures) != 0) - check_failed(&checks, "C10", "cannot record the restore check: %s", - strerror(errno)); - return released ? 0 : 1; -} - -// Starts repair step 7 when it is due, DEFERRED_START_NS after the -// acknowledgement, in a child at SCHED_IDLE, so the watcher and the -// discipline keep normal priority; the fork itself also runs at SCHED_IDLE. -// The readiness path's wall time came with the signal. -static void start_restore(struct daemon *daemon) -{ - int64_t cpu_start = cpu_time_ns(); - bool new_cpus[MAX_CPUS] = {false}; - struct restore_timing timing; - struct time_state state; - unsigned long long generation; - int64_t timeout_s = DEFAULT_STALL_TIMEOUT_S; - char text[256]; - int new_count = 0; - pid_t pid; - - if (daemon->worker_pid > 0) - return; - if (state_load(&state) != 0) { - fail_fatal(STATUS_REPAIR, "G_REPAIR_SUPPRESSION", "repair", - PHASE_RESTORE, "the time state is unreadable"); - return; - } - g_generation = (uint32_t)state.generation; - if (read_text(RESTORE_PATH, text, sizeof(text)) >= 0) { - new_count = parse_restore_record(text, &generation, new_cpus); - if (new_count < 0) { - fail_fatal(STATUS_REPAIR, "G_REPAIR_SUPPRESSION", "repair", - PHASE_RESTORE, "the restore record is malformed"); - return; - } - if (generation != state.generation) { - memset(new_cpus, 0, sizeof(new_cpus)); - new_count = 0; - } - } - (void)read_int64(RCU_STALL_TIMEOUT, &timeout_s); - // The worker's wall time runs from when step 7 was due. Its CPU time is - // step 7's: this preparation, the fork, and the worker's own. - timing.start_ns = daemon->restore_due_ns; - timing.readiness_us = daemon->readiness_us; - pid = fork_deferred_restore(cpu_start, &timing); - if (pid == 0) - _exit(restore_work(new_cpus, new_count, false, &timing)); - if (pid < 0) { - fail_fatal(STATUS_REPAIR, "G_REPAIR_SUPPRESSION", "repair", - PHASE_RESTORE, "cannot start the restore work: %s", - strerror(errno)); - return; - } - daemon->worker_pid = pid; - daemon->worker_promote_ns = daemon->restore_due_ns + IDLE_BOUND_NS; - daemon->worker_deadline_ns = - clock_ns(CLOCK_MONOTONIC) + timeout_s * NSEC_PER_SEC; - daemon->suppression_released = false; -} - -// Releases the stall suppression for a worker whose grace period is still -// pending at the stall timeout: the grace period is stuck, and the kernel -// then reports the real stall. -static void release_suppression(struct daemon *daemon) -{ - char detail[DETAIL_MAX]; - - daemon->suppression_released = true; - if (suppression_restore(detail, sizeof(detail), true) != 0) - fail_fatal(STATUS_REPAIR, "G_REPAIR_SUPPRESSION", "repair", - PHASE_RESTORE, "%s", detail); -} - -static void reap_children(struct daemon *daemon) -{ - int status; - pid_t pid; - - while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { - if (pid != daemon->worker_pid) - continue; - daemon->worker_pid = 0; - daemon->worker_promote_ns = 0; - // A failing worker powered the guest off itself, or reported in - // report-only mode; a crash leaves the suppression to the daemon. - if (!WIFEXITED(status)) { - release_suppression(daemon); - fail_fatal(STATUS_REPAIR, "G_REPAIR_SUPPRESSION", "repair", - PHASE_RESTORE, "the restore work ended with signal %d", - WTERMSIG(status)); - } - // Repair step 7 ends by restarting the discipline at the fast cadence. - daemon->polls = 0; - (void)arm_timer(daemon); - } -} - -static void handle_signals(struct daemon *daemon) -{ - struct signalfd_siginfo info; - - while (read(daemon->signals, &info, sizeof(info)) == sizeof(info)) { - if (info.ssi_signo == SIGTERM) - _exit(0); - // sigqueue() carries the readiness path's wall time. Step 7 is due - // DEFERRED_START_NS later; the repair paired the clock with the - // packet's UTC now. - if (info.ssi_signo == SIGUSR1) { - daemon->restore_pending = true; - daemon->last_accepted_ns = clock_ns(CLOCK_MONOTONIC); - daemon->restore_due_ns = - daemon->last_accepted_ns + DEFERRED_START_NS; - daemon->readiness_us = info.ssi_code == SI_QUEUE - ? (int64_t)(info.ssi_ptr & UINT32_MAX) - : 0; - } - } - // A worker that ended, if any: the daemon loop starts a due restore only - // once the previous worker is reaped. - reap_children(daemon); -} - -// Sets up the daemon's signal and timer descriptors and its OOM exemption. -// Returns 0 or an errno value. -static int daemon_setup(struct daemon *daemon) -{ - sigset_t mask; - - sigemptyset(&mask); - sigaddset(&mask, SIGUSR1); - sigaddset(&mask, SIGTERM); - sigaddset(&mask, SIGCHLD); - if (sigprocmask(SIG_BLOCK, &mask, NULL) != 0) - return errno; - signal(SIGHUP, SIG_IGN); - signal(SIGPIPE, SIG_IGN); - daemon->signals = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK); - if (daemon->signals < 0) - return errno; - daemon->timer = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (daemon->timer < 0) - return errno; - if (write_text("/proc/self/oom_score_adj", "-1000") != 0 || - arm_timer(daemon) != 0) - return errno; - return 0; -} - -// Returns the poll timeout in milliseconds until DEADLINE_NS, or -1 when -// DEADLINE_NS is 0, for none. -static int poll_timeout_until(int64_t deadline_ns) -{ - int64_t remaining; - - if (deadline_ns == 0) - return -1; - remaining = deadline_ns - clock_ns(CLOCK_MONOTONIC); - return remaining <= 0 - ? 0 - : (int)((remaining + NSEC_PER_MSEC - 1) / NSEC_PER_MSEC); -} - -// Returns the earlier of two poll timeouts, where -1 is none. -static int earlier_timeout(int first, int second) -{ - if (first < 0) - return second; - if (second < 0) - return first; - return first < second ? first : second; -} - -static void daemon_loop(struct daemon *daemon) -{ - for (;;) { - struct pollfd fds[3] = {{daemon->kmsg, POLLIN, 0}, - {daemon->signals, POLLIN, 0}, - {daemon->timer, POLLIN, 0}}; - bool waiting = daemon->worker_pid > 0 && !daemon->suppression_released; - bool idle = daemon->worker_pid > 0 && daemon->worker_promote_ns != 0; - bool due = daemon->restore_pending && daemon->worker_pid == 0; - int timeout = earlier_timeout( - earlier_timeout( - poll_timeout_until(idle ? daemon->worker_promote_ns : 0), - poll_timeout_until(waiting ? daemon->worker_deadline_ns : 0)), - poll_timeout_until(due ? daemon->restore_due_ns : 0)); - int64_t now; - - if (poll(fds, ARRAY_SIZE(fds), timeout) < 0 && errno != EINTR) { - sleep(1); - continue; - } - if ((fds[0].revents & (POLLIN | POLLERR)) != 0) - watch_kernel_log(daemon); - if ((fds[1].revents & POLLIN) != 0) - handle_signals(daemon); - if ((fds[2].revents & POLLIN) != 0) { - uint64_t expirations; - ssize_t count = read(daemon->timer, &expirations, - sizeof(expirations)); - - (void)count; - discipline_poll(daemon); - arm_timer(daemon); - } - now = clock_ns(CLOCK_MONOTONIC); - // Step 7 starts DEFERRED_START_NS after the ack, and continues at - // normal priority IDLE_BOUND_NS after it starts. - if (daemon->restore_pending && daemon->worker_pid == 0 && - now >= daemon->restore_due_ns) { - daemon->restore_pending = false; - start_restore(daemon); - now = clock_ns(CLOCK_MONOTONIC); - } - if (daemon->worker_pid > 0 && daemon->worker_promote_ns != 0 && - now >= daemon->worker_promote_ns) { - daemon->worker_promote_ns = 0; - set_idle_priority(daemon->worker_pid, false); - } - if (daemon->worker_pid > 0 && !daemon->suppression_released && - now >= daemon->worker_deadline_ns) - release_suppression(daemon); - } -} - -// Turns the boot checker into the time daemon, at normal priority. It keeps -// reading the kernel log from where the boot check positioned KMSG and -// publishes its PID once its descriptors are ready. Returns 0 or -1. -static int become_daemon(struct daemon *daemon, int kmsg, - int64_t last_accepted_ns) -{ - char text[32]; - int error; - - memset(daemon, 0, sizeof(*daemon)); - daemon->kmsg = kmsg; - daemon->last_accepted_ns = last_accepted_ns; - set_idle_priority(0, false); - error = daemon_setup(daemon); - if (error != 0) { - errno = error; - return -1; - } - snprintf(text, sizeof(text), "%d\n", (int)getpid()); - return replace_file(DAEMON_PID_PATH, DAEMON_PID_PATH ".tmp", text); -} - -// --------------------------------------------------------------------------- -// Commands -// --------------------------------------------------------------------------- - -static void load_generation(void) -{ - struct time_state state; - - if (state_load(&state) == 0) - g_generation = (uint32_t)state.generation; -} - -struct boot_log_job { - struct boot_log *log; - char *buffer; - int size; - bool read_log; -}; - -// C4 and K1 over the kernel log, beside the per-CPU checks. The caller -// allocates the buffer before the threads start: freeing a large allocation -// unmaps it, and an unmap in a process with running threads waits for every -// CPU running one of them. -static void boot_kernel_log(struct checks *checks, void *context) -{ - struct boot_log_job *job = context; - char detail[DETAIL_MAX]; - - if (!job->read_log) - return; - if (job->buffer == NULL) { - check_failed(checks, "C4", "no buffer for the %d-byte kernel log", - job->size); - return; - } - if (read_boot_log_into(job->log, job->buffer, job->size) != 0) { - check_failed(checks, "C4", "read the kernel log: %s", strerror(errno)); - return; - } - if (!boot_log_verdict(job->log, detail, sizeof(detail))) - check_failed(checks, "C4", "%s", detail); - // K1 is kernel hardening, not time: a writable and executable kernel - // mapping, such as unprotected ITS thunk pages. - if (job->log->wx[0] != '\0') - check_failed_code(checks, "G_KERNEL_WX", "%s", job->log->wx); -} - -// C7 beside the per-CPU checks. -static void boot_timer_list(struct checks *checks, void *context) -{ - (void)context; - check_timer_list(checks, TIMER_LIST_WAIT_NS); -} - -// C5 beside the per-CPU checks. -static void boot_cpuinfo(struct checks *checks, void *context) -{ - (void)context; - check_cpuinfo(checks, checks->online, true); -} - -struct boot_sample { - int64_t theta; - int64_t epsilon; - int64_t accepted_ns; -}; - -// The boot checks that need no CPU of their own run in the caller while the -// parallel jobs run. -static void boot_foreground(struct checks *checks, void *context) -{ - (void)context; - check_cmdline(checks); - check_platform(checks); - check_clocksource(checks); - check_debug_watchdogs(checks); -} - -// Sleeps until DEADLINE_NS on CLOCK_MONOTONIC. -static void sleep_until(int64_t deadline_ns) -{ - struct timespec when = {(time_t)(deadline_ns / NSEC_PER_SEC), - (long)(deadline_ns % NSEC_PER_SEC)}; - - while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &when, NULL) == - EINTR) - ; -} - -// The asynchronous boot checks: every boot check but C12, fail-fast, from -// DEFERRED_START_NS after the boot step at SCHED_IDLE, and at normal priority -// from IDLE_BOUND_NS after they start. When they pass, the checker becomes the -// daemon and records the boot check, whose wall and CPU times add C12's -// (C12_NS and C12_CPU_NS) and the daemon's start, but not the delay. It never -// returns. -static void boot_checker(const struct boot_sample *sample, int64_t c12_ns, - int64_t c12_cpu_ns, int c12_failures) -{ - // The delay counts from this fork in the time ABI boot step, the one - // point that every guest mode shares: init runs it before the rest of its - // setup, so the checks start somewhat less than DEFERRED_START_NS after - // shell-ready. - int64_t start = clock_ns(CLOCK_MONOTONIC) + DEFERRED_START_NS; - int64_t cpu_start = cpu_time_ns(); - int null = open("/dev/null", O_RDWR | O_CLOEXEC); - struct boot_log_job log_job; - struct promoter promoter; - struct side_job sides[3]; - struct daemon daemon; - struct checks checks; - struct boot_log log; - bool started = false; - bool promoting; - int kmsg; - - g_async_output = true; - (void)setsid(); - if (null >= 0) { - (void)dup2(null, STDIN_FILENO); - (void)dup2(null, STDOUT_FILENO); - (void)dup2(null, STDERR_FILENO); - close(null); - } - if (chdir("/") != 0) - _exit(1); - promoting = promoter_start(&promoter, start); - set_idle_priority(0, true); - sleep_until(start); - if (checks_init(&checks, PHASE_BOOT) != 0) { - check_failed(&checks, "C1", "cannot read the CPU sets"); - _exit(1); - } - checks.failures = c12_failures; - memset(&log, 0, sizeof(log)); - log_job.log = &log; - log_job.read_log = false; - log_job.size = klogctl(SYSLOG_ACTION_SIZE_BUFFER, NULL, 0); - log_job.buffer = log_job.size > 0 ? malloc((size_t)log_job.size + 1) : NULL; - kmsg = open("/dev/kmsg", O_RDONLY | O_NONBLOCK | O_CLOEXEC); - if (kmsg < 0 || lseek(kmsg, 0, SEEK_END) < 0) { - check_failed(&checks, "C4", "open /dev/kmsg: %s", strerror(errno)); - if (kmsg >= 0) - close(kmsg); - kmsg = -1; - } else { - log_job.read_log = true; - } - // The kernel log, the tick mode, and cpuinfo run beside the per-CPU - // checks, longest first; the other checks run in the caller meanwhile. - sides[0] = (struct side_job){.run = boot_kernel_log, .context = &log_job}; - sides[1] = (struct side_job){.run = boot_timer_list}; - sides[2] = (struct side_job){.run = boot_cpuinfo}; - check_cpus_parallel(&checks, checks.online, true, true, sides, - ARRAY_SIZE(sides), boot_foreground, NULL); - free(log_job.buffer); - memcpy(checks.cpu_khz, sides[2].checks.cpu_khz, sizeof(checks.cpu_khz)); - memcpy(checks.cpu_khz_known, sides[2].checks.cpu_khz_known, - sizeof(checks.cpu_khz_known)); - // C3 compares CPU 0's declared rate with cpuinfo once both have run. - if (checks.tsc_hz != 0 && checks.cpu_khz_known[0] && - checks.tsc_hz / 1000 != checks.cpu_khz[0]) - check_failed(&checks, "C3", - "cpu 0: floor(F / 1000) is %" PRIu64 - " kHz but cpu MHz is %" PRIu64 " kHz", - checks.tsc_hz / 1000, checks.cpu_khz[0]); - // The daemon blocks its signals in its only thread. - if (promoting) - promoter_stop(&promoter); - if (kmsg >= 0) { - started = become_daemon(&daemon, kmsg, sample->accepted_ns) == 0; - if (!started) - check_failed(&checks, "C10", "cannot start the time daemon: %s", - strerror(errno)); - } - if (record_check(PHASE_BOOT, "ok", checks.online_count, - (c12_ns + clock_ns(CLOCK_MONOTONIC) - start) / - NSEC_PER_USEC, - (c12_cpu_ns + cpu_time_ns() - cpu_start) / NSEC_PER_USEC, - checks.tsc_hz, checks.lapic_hz, checks.failures) != 0) - check_failed(&checks, "C10", "cannot publish the time state: %s", - strerror(errno)); - if (!started) - _exit(1); - daemon_loop(&daemon); - _exit(0); -} - -// Before shell-ready, only C12: the initial time sample and clock step, then -// the published state. Every other boot check runs asynchronously in a child, -// which becomes the daemon. -static int cmd_boot(void) -{ - int64_t start = clock_ns(CLOCK_MONOTONIC); - int64_t cpu_start = cpu_time_ns(); - struct boot_sample sample = {0, 0, 0}; - struct time_state state; - struct checks checks; - int64_t c12_cpu; - pid_t pid; - - g_generation = 0; - make_runtime_directories(); - memset(&checks, 0, sizeof(checks)); - checks.phase = PHASE_BOOT; - if (check_initial_sample(&checks, &sample.theta, &sample.epsilon)) - sample.accepted_ns = clock_ns(CLOCK_MONOTONIC); - state_init(&state); - state.synchronized = sample.accepted_ns != 0; - state.samples = sample.accepted_ns != 0; - state.offset_ns = sample.accepted_ns != 0 ? sample.theta : 0; - state.uncertainty_ns = sample.accepted_ns != 0 ? sample.epsilon : 0; - state.frequency_ppb = frequency_to_ppb(current_frequency()); - if (state_store(&state) != 0) { - check_failed(&checks, "C10", "cannot publish the time state: %s", - strerror(errno)); - return 1; - } - fflush(NULL); - // The child cannot read this process's CPU clock. - c12_cpu = cpu_time_ns() - cpu_start; - pid = fork(); - if (pid == 0) - boot_checker(&sample, clock_ns(CLOCK_MONOTONIC) - start, c12_cpu, - checks.failures); - if (pid < 0) { - check_failed(&checks, "C10", "cannot start the boot checks: %s", - strerror(errno)); - return 1; - } - return 0; -} - -// Capture checks (C6, C7, and C10); they never wait. -static void run_capture_checks(struct checks *checks) -{ - (void)rates_load(&checks->tsc_hz, &checks->lapic_hz); - check_clocksource(checks); - check_timer_list(checks, 0); - check_daemon(checks); -} - -// Restore checks: C1, C2, and C5 on newly onlined CPUs, C3 for CPU 0, C6, -// and C10. They never wait. -static void run_restore_checks(struct checks *checks, const bool *new_cpus, - int new_count) -{ - uint64_t tsc_hz = 0; - uint64_t lapic_hz = 0; - - if (rates_load(&tsc_hz, &lapic_hz) != 0) { - check_failed(checks, "C3", "the boot rates are unavailable"); - } else { - checks->tsc_hz = tsc_hz; - checks->lapic_hz = lapic_hz; - } - if (new_count > 0) { - check_cpus(checks, new_cpus, true, false); - check_cpuinfo(checks, new_cpus, true); - } - check_msrs(checks, 0, false); - check_clocksource(checks); - check_daemon(checks); -} - -static int cmd_check(int argc, char **argv) -{ - int64_t start = clock_ns(CLOCK_MONOTONIC); - int64_t cpu_start = cpu_time_ns(); - bool new_cpus[MAX_CPUS] = {false}; - int new_count = 0; - enum phase phase = PHASE_RUNTIME; - struct checks checks; - - for (int index = 0; index < argc; index++) { - if (strcmp(argv[index], "--phase") == 0 && index + 1 < argc) { - const char *name = argv[++index]; - - phase = strcmp(name, "capture") == 0 ? PHASE_CAPTURE - : strcmp(name, "restore") == 0 ? PHASE_RESTORE - : PHASE_RUNTIME; - } else if (strcmp(argv[index], "--new-cpus") == 0 && index + 1 < argc) { - new_count = parse_new_cpus(argv[++index], new_cpus); - } else { - new_count = -1; - } - } - if (phase == PHASE_RUNTIME || new_count < 0) { - fprintf(stderr, "usage: nvx-time check --phase capture|restore " - "[--new-cpus LIST]\n"); - return 2; - } - load_generation(); - if (checks_init(&checks, phase) != 0) { - check_failed(&checks, "C6", "cannot read the CPU sets"); - return 1; - } - if (phase == PHASE_CAPTURE) - run_capture_checks(&checks); - else - run_restore_checks(&checks, new_cpus, new_count); - (void)record_check(phase, "ok", checks.online_count, - (clock_ns(CLOCK_MONOTONIC) - start) / NSEC_PER_USEC, - (cpu_time_ns() - cpu_start) / NSEC_PER_USEC, 0, 0, - checks.failures); - return checks.failures == 0 ? 0 : 1; -} - -// Whether a recorded check of STATE is still pending: the boot check only -// with BOOT_ONLY, or any. A state without a boot record parses as a pending -// boot check. -static bool checks_pending(const struct time_state *state, bool boot_only) -{ - for (int phase = 0; phase < (boot_only ? 1 : CHECK_PHASES); phase++) { - if (strcmp(state->checks[phase].status, "pending") == 0) - return true; - } - return false; -} - -// Waits until no recorded check (with BOOT_ONLY, no boot check) is pending, -// for at most STATUS_WAIT_NS, and loads the state into STATE; a missing -// state file counts as a pending boot check. Returns 0, or 1 when a check is -// still pending. -static int wait_for_checks(struct time_state *state, bool boot_only) -{ - int64_t deadline = clock_ns(CLOCK_MONOTONIC) + STATUS_WAIT_NS; - - for (;;) { - if (state_load(state) != 0) - state_init(state); - else if (!checks_pending(state, boot_only)) - return 0; - if (clock_ns(CLOCK_MONOTONIC) >= deadline) - return 1; - (void)nanosleep(&(struct timespec){0, STATUS_POLL_NS}, NULL); - } -} - -static int cmd_pre_capture(void) -{ - struct time_state state; - struct checks checks; - int64_t cpu_start; - int64_t elapsed; - int64_t start; - int64_t cpu; - - // Preparation step 1: the asynchronous boot checks start the daemon that - // C10 requires. - if (wait_for_checks(&state, true) != 0) { - memset(&checks, 0, sizeof(checks)); - checks.phase = PHASE_CAPTURE; - check_failed(&checks, "C10", "the boot checks are still pending"); - return 1; - } - start = clock_ns(CLOCK_MONOTONIC); - cpu_start = cpu_time_ns(); - g_generation = (uint32_t)state.generation; - if (checks_init(&checks, PHASE_CAPTURE) != 0) { - check_failed(&checks, "C6", "cannot read the CPU sets"); - return 1; - } - run_capture_checks(&checks); - elapsed = clock_ns(CLOCK_MONOTONIC) - start; - cpu = cpu_time_ns() - cpu_start; - if (suppression_save() != 0) { - char detail[DETAIL_MAX]; - int error = errno; - - if (error != EEXIST) - (void)suppression_restore(detail, sizeof(detail), false); - fprintf(stderr, "nvx-time: cannot suppress the stall detectors: %s\n", - error == EEXIST ? "a capture is already pending" - : strerror(error)); - return 1; - } - // Step 1's record travels in the snapshot. - if (record_check(PHASE_CAPTURE, "ok", checks.online_count, - elapsed / NSEC_PER_USEC, cpu / NSEC_PER_USEC, 0, 0, - checks.failures) != 0) - check_failed(&checks, "C10", "cannot record the capture check: %s", - strerror(errno)); - return 0; -} - -static int cmd_cancel_capture(void) -{ - char detail[DETAIL_MAX]; - - if (suppression_restore(detail, sizeof(detail), false) != 0) { - fprintf(stderr, "nvx-time: %s\n", detail); - return 1; - } - return 0; -} - -struct repair_record { - const struct restore_packet *packet; - int64_t theta; - int64_t epsilon; - int64_t step_realtime_ns; - int64_t frequency; - int64_t elapsed_us; -}; - -static void apply_repair(struct time_state *state, const void *context) -{ - const struct repair_record *repair = context; - - state->generation = repair->packet->generation; - state->discontinuities++; - snprintf(state->last_discontinuity, sizeof(state->last_discontinuity), - "restore"); - state->last_step_ns = repair->theta; - state->last_step_realtime_ns = repair->step_realtime_ns; - state->last_downtime_ns = repair->packet->downtime_ns; - snprintf(state->last_downtime_source, sizeof(state->last_downtime_source), - "%s", - (repair->packet->flags & PACKET_DOWNTIME_UTC) != 0 ? "utc" - : "monotonic"); - state->synchronized = 1; - state->offset_ns = repair->theta; - state->uncertainty_ns = repair->epsilon; - state->frequency_ppb = frequency_to_ppb(repair->frequency); - state->samples++; - snprintf(state->last_sample_error, sizeof(state->last_sample_error), - "none"); - // Repair step 3 marks this restore's check pending, so nvx-time status - // waits for the daemon's step 7. Until then, restore_elapsed_us is the - // readiness path up to the clock step, at last_step_realtime_ns; - // restore_cpu_us counts only step 7, which records it. - snprintf(state->checks[PHASE_RESTORE].status, - sizeof(state->checks[PHASE_RESTORE].status), "pending"); - state->checks[PHASE_RESTORE].cpus = 0; - state->checks[PHASE_RESTORE].elapsed_us = repair->elapsed_us; - state->checks[PHASE_RESTORE].cpu_us = 0; - state->checks[PHASE_RESTORE].generation = state->generation; - state->checks[PHASE_RESTORE].failures = -1; -} - -static int repair_failed(const char *code, const char *detail) -{ - fail_fatal(STATUS_REPAIR, code, "repair", PHASE_RESTORE, "%s", detail); - return 1; -} - -// Repair steps 2 and 3: read the packet with four-byte reads, validate it, -// and set the wall clock from its bracketed UTC, then write the entropy to -// ENTROPY_FD, the caller's file at ENTROPY_PATH. START_NS is when the capture -// request returned. The caller holds the portb lock. With FINISH (untiered) -// and a DAEMON, a restore with no processors or memory to activate needs no -// shell work before the acknowledgement, so steps 6 and 7 start here too -// and no second helper process starts; the metadata then begins with 2 -// instead of 1. -static int repair_restore(const uint8_t *previous_id, const char *entropy_path, - int entropy_fd, int64_t start_ns, bool finish, - pid_t daemon) -{ - static uint8_t body[PACKET_MAX_SIZE]; - static struct restore_packet packet; - uint8_t header[PACKET_HEADER_SIZE]; - struct repair_record repair; - struct time_sample sample; - char detail[DETAIL_MAX]; - char id[GENERATION_ID_SIZE * 2 + 1]; - int64_t t0; - int64_t t1; - int64_t sample_theta; - int64_t sample_epsilon; - bool finishing; - int finished = -1; - - t0 = clock_ns(CLOCK_REALTIME); - outb(SELECT_PACKET, PORTB_STATUS); - t1 = clock_ns(CLOCK_REALTIME); - portb_read(PORTB_DATA, header, sizeof(header)); - if (parse_packet_header(header, &packet, detail, sizeof(detail)) != 0) - return repair_failed("G_REPAIR_PACKET", detail); - if (packet.generation == g_generation) { - close(entropy_fd); - unlink(entropy_path); - puts("0"); - return 0; - } - if ((uint64_t)packet.generation != (uint64_t)g_generation + 1) { - snprintf(detail, sizeof(detail), - "restore packet generation %" PRIu32 - " does not follow recorded generation %" PRIu32, - packet.generation, g_generation); - return repair_failed("G_REPAIR_GENERATION", detail); - } - portb_read(PORTB_DATA, body, packet_body_size(&packet)); - if (parse_packet_body(body, &packet, detail, sizeof(detail)) != 0) - return repair_failed("G_REPAIR_PACKET", detail); - if (memcmp(packet.entropy, previous_id, GENERATION_ID_SIZE) == 0) - return repair_failed("G_REPAIR_GENERATION", - "the VM generation ID did not change"); - g_generation = packet.generation; - pair_clocks(t0, t1, packet.utc_ns, &repair.theta, &repair.epsilon); - if (repair.epsilon > REPAIR_BOUND_NS) { - int result = best_sample(SAMPLE_ATTEMPTS - 1, REPAIR_BOUND_NS, - packet.generation, &sample, &sample_theta, - &sample_epsilon, detail, sizeof(detail)); - - if (result == -2) - return repair_failed("G_REPAIR_GENERATION", detail); - if (result >= 0 && sample_epsilon < repair.epsilon) { - repair.theta = sample_theta; - repair.epsilon = sample_epsilon; - } - if (repair.epsilon > REPAIR_BOUND_NS) { - snprintf(detail, sizeof(detail), - "UTC pairing uncertainty %" PRId64 " ns exceeds 1 ms", - repair.epsilon); - return repair_failed("G_REPAIR_SAMPLE", detail); - } - } - repair.frequency = restore_frequency(packet.rate_deviation); - if (step_clock(repair.theta) != 0 || set_frequency(repair.frequency) != 0) { - snprintf(detail, sizeof(detail), "adjtimex: %s", strerror(errno)); - return repair_failed("G_REPAIR_CLOCK", detail); - } - repair.step_realtime_ns = clock_ns(CLOCK_REALTIME); - repair.elapsed_us = (clock_ns(CLOCK_MONOTONIC) - start_ns) / NSEC_PER_USEC; - repair.packet = &packet; - // A plain untiered restore needs no shell work: this helper also - // acknowledges and hands step 7 to the daemon. - finishing = finish && daemon > 0 && packet.online_vp_count == 0 && - packet.range_count == 0 && - (packet.flags & PACKET_MEMORY_TARGET) == 0; - // The entropy goes to the caller in every case, an untiered restore's - // too; the file already exists, empty, so only its bytes are written. - if (pwrite(entropy_fd, packet.entropy, PACKET_ENTROPY_SIZE, 0) != - PACKET_ENTROPY_SIZE || - close(entropy_fd) != 0) { - snprintf(detail, sizeof(detail), "write %.200s: %s", entropy_path, - strerror(errno)); - return repair_failed("G_REPAIR_PACKET", detail); - } - if (state_apply(apply_repair, &repair) != 0) { - snprintf(detail, sizeof(detail), "record the restore: %s", - strerror(errno)); - return repair_failed("G_REPAIR_CLOCK", detail); - } - format_hex(packet.entropy, GENERATION_ID_SIZE, id); - if (finishing) { - finished = signal_restore(daemon, - (packet.flags & PACKET_ACK_REQUIRED) != 0, - start_ns); - if (finished > 0) - fail_fatal(STATUS_CONFORMANCE, "G_CONFORMANCE_C10", "conformance", - PHASE_RESTORE, "the time daemon is not running"); - } - printf("%d %u %u %u %s", finished >= 0 ? 2 : 1, packet.flags, - packet.online_vp_count, packet.range_count, id); - for (unsigned index = 0; index < packet.range_count; index++) - printf(" %" PRIu64 " %" PRIu64, packet.ranges[index][0], - packet.ranges[index][1]); - putchar('\n'); - return 0; -} - -// Writes the capture request to 0x605. In a restored process the write -// returns with a restore packet available, and repair steps 1 to 3 run here -// before any other guest work. Prints 0 when no restore happened. The portb -// lock is held from the request to the restored clock, so the snapshot never -// contains a discipline poll that would apply a pre-capture sample after the -// restore. -static int cmd_capture(int argc, char **argv) -{ - uint8_t previous_id[GENERATION_ID_SIZE]; - char detail[DETAIL_MAX]; - char *end; - long request; - int64_t start; - unsigned status; - pid_t daemon = -1; - int entropy_fd; - int result; - int lock; - - if ((argc != 3 && (argc != 4 || strcmp(argv[3], "--finish") != 0)) || - (request = strtol(argv[0], &end, 10), *end != '\0') || - (request != 0 && request != 1) || - parse_hex(argv[1], previous_id, sizeof(previous_id)) != 0) { - fprintf(stderr, "usage: nvx-time capture 0|1 GENERATION_ID " - "ENTROPY_FILE [--finish]\n"); - return 2; - } - if (state_load(&(struct time_state){0}) != 0) { - fprintf(stderr, "nvx-time: the time state is unavailable\n"); - return 1; - } - load_generation(); - if (enable_ports() != 0) { - fprintf(stderr, "nvx-time: ioperm: %s\n", strerror(errno)); - return 1; - } - // The daemon keeps its PID in the restored guest; looking it up now keeps - // the file reads off the restore path. - if (argc == 4) - daemon = daemon_pid(); - // The entropy file is created before the capture, empty, so that a - // restored helper only writes its bytes: creating a file right after a - // restore faults in the filesystem's metadata, 4 to 9 ms on WHP. - entropy_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | - O_CLOEXEC, - 0600); - if (entropy_fd < 0) { - fprintf(stderr, "nvx-time: open %s: %s\n", argv[2], strerror(errno)); - return 1; - } - lock = lock_portb(detail, sizeof(detail)); - if (lock < 0) { - fprintf(stderr, "nvx-time: %s\n", detail); - close(entropy_fd); - unlink(argv[2]); - return 1; - } - outb((unsigned char)request, PORT_SNAPSHOT); - start = clock_ns(CLOCK_MONOTONIC); - status = inb(PORTB_STATUS); - if ((status & PORTB_PACKET_AVAILABLE) == 0) { - close(lock); - close(entropy_fd); - unlink(argv[2]); - puts("0"); - return 0; - } - result = repair_restore(previous_id, argv[2], entropy_fd, start, argc == 4, - daemon); - close(lock); - return result; -} - -// Repair steps 6 and 7 after the shell's activation and identity work: the -// acknowledgement when the packet asked for one, then the daemon runs step 7 -// asynchronously. The record tells it which CPUs activation onlined. The -// readiness path started restore_elapsed_us before the helper's clock step, -// at last_step_realtime_ns. -static int cmd_restore_finish(int argc, char **argv) -{ - bool new_cpus[MAX_CPUS] = {false}; - const char *new_list = "none"; - struct restore_timing timing; - struct time_state state; - struct checks checks; - char text[256]; - int64_t start; - bool ack = false; - int new_count = 0; - int signaled; - - for (int index = 0; index < argc; index++) { - if (strcmp(argv[index], "--ack") == 0) { - ack = true; - } else if (strcmp(argv[index], "--new-cpus") == 0 && index + 1 < argc) { - new_list = argv[++index]; - new_count = parse_new_cpus(new_list, new_cpus); - } else { - new_count = -1; - } - } - if (new_count < 0 || strlen(new_list) > 127) { - fprintf(stderr, "usage: nvx-time restore-finish [--ack] " - "[--new-cpus LIST]\n"); - return 2; - } - if (state_load(&state) != 0) - return repair_failed("G_REPAIR_CLOCK", "the time state is unreadable"); - g_generation = (uint32_t)state.generation; - snprintf(text, sizeof(text), "generation=%" PRIu64 "\nnew_cpus=%s\n", - state.generation, new_count > 0 ? new_list : "none"); - if (replace_file(RESTORE_PATH, RESTORE_PATH ".tmp", text) != 0) { - snprintf(text, sizeof(text), "record the restore: %s", - strerror(errno)); - return repair_failed("G_REPAIR_CLOCK", text); - } - if (ack && enable_ports() != 0) { - fprintf(stderr, "nvx-time: ioperm: %s\n", strerror(errno)); - return 1; - } - start = clock_ns(CLOCK_MONOTONIC) - - (clock_ns(CLOCK_REALTIME) - state.last_step_realtime_ns) - - state.checks[PHASE_RESTORE].elapsed_us * NSEC_PER_USEC; - signaled = signal_restore(daemon_pid(), ack, start); - if (signaled == 0) - return 0; - if (signaled > 0 || !g_report_only) { - memset(&checks, 0, sizeof(checks)); - checks.phase = PHASE_RESTORE; - check_failed(&checks, "C10", "the time daemon is not running"); - return 1; - } - // Report-only runs may lack the daemon; finish the restore here, where - // step 7's CPU time is restore_work's own. - if (ack) - outb(SNAPSHOT_ACKNOWLEDGE, PORT_SNAPSHOT); - timing.start_ns = clock_ns(CLOCK_MONOTONIC); - timing.readiness_us = (timing.start_ns - start) / NSEC_PER_USEC; - timing.prior_cpu_ns = 0; - timing.cpu_base_ns = cpu_time_ns(); - timing.fork_fd = -1; - (void)restore_work(new_cpus, new_count, true, &timing); - return 0; -} - -// Formats nvx-time status's lines from STATE: one per recorded phase, in the -// order boot, capture, and restore, with status=pending and neither -// elapsed_us nor cpu_us while its check runs, then the runtime line. -// Report-only mode appends each check's failure count, and a failed check's -// status is fail. Returns whether every phase line reports ok. -static bool format_status(const struct time_state *state, char *text, - size_t size) -{ - size_t length = 0; - bool ok = true; - - text[0] = '\0'; - for (int phase = 0; phase < CHECK_PHASES; phase++) { - const struct check_state *check = &state->checks[phase]; - const char *status = check->status; - char times[64] = ""; - char suffix[40] = ""; - - if (status[0] == '\0') - continue; - if (strcmp(status, "pending") != 0) - snprintf(times, sizeof(times), - " elapsed_us=%" PRId64 " cpu_us=%" PRId64, - check->elapsed_us, check->cpu_us); - if (g_report_only) - snprintf(suffix, sizeof(suffix), " failures=%" PRId64, - check->failures > 0 ? check->failures : 0); - ok = ok && strcmp(status, "ok") == 0; - append_text(text, size, &length, - "%s: v=1 phase=%s status=%s cpus=%" PRIu64 - " tsc_hz=%" PRIu64 " lapic_hz=%" PRIu64 - " generation=%" PRIu64 "%s%s\n", - abi_prefix(), k_phase_names[phase], status, check->cpus, - state->tsc_hz, state->lapic_hz, check->generation, times, - suffix); - } - append_text(text, size, &length, - "%s: v=1 phase=runtime status=%s generation=%" PRIu64 - " discontinuities=%" PRIu64 " offset_ns=%" PRId64 - " uncertainty_ns=%" PRId64 " rejected_samples=%" PRIu64 - " last_sample_error=%s\n", - abi_prefix(), - state->synchronized != 0 ? "synchronized" : "unsynchronized", - state->generation, state->discontinuities, state->offset_ns, - state->uncertainty_ns, state->rejected_samples, - state->last_sample_error); - return ok; -} - -// Prints the time state on demand, after waiting for pending checks: CI and -// the matrix driver run it over the console; production paths never do. -// Exits with 0 only when every phase line reports ok. -static int cmd_status(void) -{ - struct time_state state; - char text[1024]; - int waited = wait_for_checks(&state, false); - bool ok = format_status(&state, text, sizeof(text)); - - fputs(text, stdout); - fflush(stdout); - return waited == 0 && ok ? 0 : 1; -} - -static int cmd_sample(void) -{ - struct time_sample sample; - char detail[DETAIL_MAX] = ""; - int64_t theta; - int64_t epsilon; - unsigned status; - int result; - int lock; - - if (enable_ports() != 0) { - fprintf(stderr, "nvx-time: ioperm: %s\n", strerror(errno)); - return 1; - } - status = inb(PORTB_STATUS); - if ((status & PORTB_TIME_WINDOW) == 0) { - fprintf(stderr, "nvx-time: portb status 0x%02x has no time window\n", - status); - return 1; - } - lock = lock_portb(detail, sizeof(detail)); - result = lock < 0 ? -1 : take_sample(&sample, detail, sizeof(detail)); - if (lock >= 0) - close(lock); - if (result != 0) { - fprintf(stderr, "nvx-time: %s\n", detail); - return 1; - } - pair_clocks(sample.t0, sample.t1, sample.utc_ns, &theta, &epsilon); - printf("flags=0x%02x generation=%" PRIu32 " utc_ns=%" PRIu64 - " t0=%" PRId64 " t1=%" PRId64 " theta_ns=%" PRId64 - " epsilon_ns=%" PRId64 "\n", - sample.flags, sample.generation, sample.utc_ns, sample.t0, - sample.t1, theta, epsilon); - return 0; -} - -static int cmd_generation_id(void) -{ - uint8_t id[GENERATION_ID_SIZE]; - char text[GENERATION_ID_SIZE * 2 + 1]; - unsigned status; - int lock; - - if (enable_ports() != 0) { - fprintf(stderr, "nvx-time: ioperm: %s\n", strerror(errno)); - return 1; - } - status = inb(PORTB_STATUS); - if ((status & PORTB_GENERATION_ID_AVAILABLE) == 0) { - fprintf(stderr, "nvx-time: the VM generation ID is unavailable\n"); - return 1; - } - lock = lock_file(PORTB_LOCK_PATH); - if (lock < 0) { - fprintf(stderr, "nvx-time: portb lock: %s\n", strerror(errno)); - return 1; - } - outb(SELECT_GENERATION_ID, PORTB_STATUS); - portb_read(PORTB_DATA, id, sizeof(id)); - close(lock); - format_hex(id, sizeof(id), text); - puts(text); - return 0; -} - -// --------------------------------------------------------------------------- -// Unit-test entry points: pure decoders and decisions over fixture files -// --------------------------------------------------------------------------- - -static int test_bytes(const char *path, uint8_t *buffer, size_t size) -{ - memset(buffer, 0, size); - return read_bytes(path, buffer, size) < 0 ? -1 : 0; -} - -static int test_text(const char *path, char *buffer, size_t size) -{ - return read_text(path, buffer, size) < 0 ? -1 : 0; -} - -static int test_packet(int argc, char **argv) -{ - static uint8_t bytes[PACKET_MAX_SIZE]; - static struct restore_packet packet; - uint8_t previous[GENERATION_ID_SIZE]; - char detail[DETAIL_MAX]; - char id[GENERATION_ID_SIZE * 2 + 1]; - uint64_t recorded; - - if (argc != 3 || test_bytes(argv[0], bytes, sizeof(bytes)) != 0 || - parse_hex(argv[2], previous, sizeof(previous)) != 0) - return 2; - recorded = strtoull(argv[1], NULL, 10); - if (parse_packet_header(bytes, &packet, detail, sizeof(detail)) != 0) { - printf("error G_REPAIR_PACKET %s\n", detail); - return 0; - } - if (packet.generation == recorded) { - printf("stale\n"); - return 0; - } - if ((uint64_t)packet.generation != recorded + 1) { - printf("error G_REPAIR_GENERATION generation %" PRIu32 "\n", - packet.generation); - return 0; - } - if (parse_packet_body(bytes + PACKET_HEADER_SIZE, &packet, detail, - sizeof(detail)) != 0) { - printf("error G_REPAIR_PACKET %s\n", detail); - return 0; - } - if (memcmp(packet.entropy, previous, GENERATION_ID_SIZE) == 0) { - printf("error G_REPAIR_GENERATION generation ID unchanged\n"); - return 0; - } - format_hex(packet.entropy, GENERATION_ID_SIZE, id); - printf("ok flags=%u online=%u ranges=%u generation=%" PRIu32 - " rate_deviation=%" PRId32 " frequency=%" PRId64 - " downtime_ns=%" PRIu64 " utc_ns=%" PRIu64 " generation_id=%s", - packet.flags, packet.online_vp_count, packet.range_count, - packet.generation, packet.rate_deviation, - restore_frequency(packet.rate_deviation), packet.downtime_ns, - packet.utc_ns, id); - for (unsigned index = 0; index < packet.range_count; index++) - printf(" range=%" PRIu64 ":%" PRIu64, packet.ranges[index][0], - packet.ranges[index][1]); - putchar('\n'); - return 0; -} - -static int test_sample(int argc, char **argv) -{ - uint8_t bytes[SAMPLE_SIZE]; - struct time_sample sample; - char detail[DETAIL_MAX]; - - if (argc != 2 || test_bytes(argv[0], bytes, sizeof(bytes)) != 0) - return 2; - if (parse_sample(bytes, &sample, detail, sizeof(detail)) != 0) - printf("error %s\n", detail); - else if (sample.generation != strtoul(argv[1], NULL, 10)) - printf("error generation %" PRIu32 "\n", sample.generation); - else - printf("ok flags=%u generation=%" PRIu32 " utc_ns=%" PRIu64 "\n", - sample.flags, sample.generation, sample.utc_ns); - return 0; -} - -// Watcher rules per line (KMSG), or the C4 verdict over message lines -// (BOOT_LOG) or SYSLOG_ACTION_READ_ALL text (SYSLOG). -enum line_test { LINES_KMSG, LINES_BOOT_LOG, LINES_SYSLOG }; - -static int test_lines(int argc, char **argv, enum line_test mode) -{ - static char text[TEXT_MAX]; - struct boot_log log; - char detail[DETAIL_MAX]; - bool boot = mode != LINES_KMSG; - - if (argc != 1 || test_text(argv[0], text, sizeof(text)) != 0) - return 2; - memset(&log, 0, sizeof(log)); - if (mode == LINES_SYSLOG) { - boot_log_scan(text, &log); - } else { - for (char *line = strtok(text, "\n"); line != NULL; - line = strtok(NULL, "\n")) { - const char *code = match_watch_rule(line); - - if (boot) - boot_log_add(&log, line); - else - printf("%s\n", code != NULL ? code : "none"); - } - } - if (boot) { - if (boot_log_verdict(&log, detail, sizeof(detail))) - printf("ok\n"); - else - printf("fail %s\n", detail); - if (log.wx[0] != '\0') - printf("wx %s\n", log.wx); - } - return 0; -} - -static int test_timer_list(int argc, char **argv) -{ - static char text[TEXT_MAX]; - bool cpus[MAX_CPUS]; - char detail[DETAIL_MAX]; - - if (argc != 2 || test_text(argv[0], text, sizeof(text)) != 0 || - parse_cpu_list(argv[1], cpus) <= 0) - return 2; - if (timer_list_verdict(text, cpus, detail, sizeof(detail))) - printf("ok\n"); - else - printf("fail %s\n", detail); - return 0; -} - -static int test_checks(const char *name, int argc, char **argv) -{ - static char text[TEXT_MAX]; - struct checks checks; - bool cpus[MAX_CPUS]; - - memset(&checks, 0, sizeof(checks)); - checks.phase = PHASE_BOOT; - if (strcmp(name, "cpuinfo") == 0) { - if (argc != 2 || test_text(argv[0], text, sizeof(text)) != 0 || - parse_cpu_list(argv[1], cpus) <= 0) - return 2; - if (check_cpuinfo_text(&checks, text, cpus, true)) { - printf("ok"); - for (int cpu = 0; cpu < MAX_CPUS; cpu++) { - if (checks.cpu_khz_known[cpu]) - printf(" cpu%d_khz=%" PRIu64, cpu, checks.cpu_khz[cpu]); - } - putchar('\n'); - } - } else if (strcmp(name, "cmdline") == 0) { - if (argc != 1) - return 2; - snprintf(text, sizeof(text), "%s", argv[0]); - if (check_cmdline_text(&checks, text)) - printf("ok\n"); - } else if (strcmp(name, "clocksource") == 0) { - if (argc != 2) - return 2; - snprintf(text, sizeof(text), "%s", argv[1]); - if (check_clocksource_values(&checks, argv[0], text)) - printf("ok\n"); - } else { - return 2; - } - return 0; -} - -static int test_correction(int argc, char **argv) -{ - struct timex tx; - bool step; - - if (argc != 3) - return 2; - step = plan_correction(strtoll(argv[0], NULL, 10), strtoll(argv[1], NULL, 10), - strcmp(argv[2], "fast") == 0, &tx); - if (step) - printf("step modes=0x%x sec=%ld nsec=%ld\n", tx.modes, - (long)tx.time.tv_sec, (long)tx.time.tv_usec); - else - printf("slew modes=0x%x offset=%ld status=0x%x constant=%ld " - "maxerror=%ld esterror=%ld\n", - tx.modes, tx.offset, (unsigned)tx.status, tx.constant, - tx.maxerror, tx.esterror); - return 0; -} - -static int test_state(int argc, char **argv) -{ - static char text[4096]; - struct time_state state; - - if (argc != 1 || test_text(argv[0], text, sizeof(text)) != 0) - return 2; - if (state_parse(text, &state) != 0) { - printf("error\n"); - return 0; - } - state_format(&state, text, sizeof(text)); - fputs(text, stdout); - return 0; -} - -static int test_restore_record(int argc, char **argv) -{ - static char text[4096]; - unsigned long long generation; - bool cpus[MAX_CPUS]; - const char *separator = ""; - int count; - - if (argc != 1 || test_text(argv[0], text, sizeof(text)) != 0) - return 2; - count = parse_restore_record(text, &generation, cpus); - if (count < 0) { - printf("error\n"); - return 0; - } - printf("generation=%llu new_cpus=", generation); - for (int cpu = 0; cpu < MAX_CPUS; cpu++) { - if (cpus[cpu]) { - printf("%s%d", separator, cpu); - separator = ","; - } - } - printf("%s count=%d\n", count == 0 ? "none" : "", count); - return 0; -} - -// A side job and a foreground that fail a known number of times, so the test -// sees whether check_cpus_parallel() merges every job's failures. -static void test_failing_job(struct checks *checks, void *context) -{ - const int *failures = context; - - for (int index = 0; index < *failures; index++) - check_failed(checks, "C4", "side job failure %d", index + 1); -} - -static void test_failing_foreground(struct checks *checks, void *context) -{ - (void)context; - check_failed(checks, "C9", "foreground failure"); -} - -static int test_parallel(int argc, char **argv) -{ - static int failures[3] = {1, 2, 3}; - struct side_job sides[ARRAY_SIZE(failures)]; - struct checks checks; - bool cpus[MAX_CPUS]; - - if (argc != 1 || parse_cpu_list(argv[0], cpus) <= 0) - return 2; - memset(&checks, 0, sizeof(checks)); - checks.phase = PHASE_BOOT; - for (size_t index = 0; index < ARRAY_SIZE(sides); index++) - sides[index] = (struct side_job){.run = test_failing_job, - .context = &failures[index]}; - check_cpus_parallel(&checks, cpus, false, false, sides, ARRAY_SIZE(sides), - test_failing_foreground, NULL); - printf("failures=%d\n", checks.failures); - return 0; -} - -// A thread that an idle thread starts, so it is idle too; it waits until the -// test ends. -struct test_thread { - pthread_mutex_t lock; - pthread_cond_t changed; - pid_t tid; - bool done; -}; - -static void *test_thread_run(void *argument) -{ - struct test_thread *thread = argument; - - pthread_mutex_lock(&thread->lock); - thread->tid = (pid_t)syscall(SYS_gettid); - pthread_cond_broadcast(&thread->changed); - while (!thread->done) - pthread_cond_wait(&thread->changed, &thread->lock); - pthread_mutex_unlock(&thread->lock); - return NULL; -} - -// Prints the policies of an idle caller and of the idle thread it started -// once a promoter fires 50 ms later, then the caller's policy after a -// promoter stopped before its 10 s bound, and how long stopping took. -static int test_promote(void) -{ - struct test_thread thread = {.lock = PTHREAD_MUTEX_INITIALIZER, - .changed = PTHREAD_COND_INITIALIZER}; - struct promoter promoter; - pthread_t handle; - long caller; - long started; - int64_t start; - - if (!promoter_start(&promoter, clock_ns(CLOCK_MONOTONIC) - IDLE_BOUND_NS + - 50 * NSEC_PER_MSEC)) - return 1; - set_idle_priority(0, true); - if (pthread_create(&handle, NULL, test_thread_run, &thread) != 0) - return 1; - pthread_mutex_lock(&thread.lock); - while (thread.tid == 0) - pthread_cond_wait(&thread.changed, &thread.lock); - pthread_mutex_unlock(&thread.lock); - pthread_join(promoter.thread, NULL); - caller = syscall(SYS_sched_getscheduler, 0); - started = syscall(SYS_sched_getscheduler, thread.tid); - pthread_mutex_lock(&thread.lock); - thread.done = true; - pthread_cond_broadcast(&thread.changed); - pthread_mutex_unlock(&thread.lock); - pthread_join(handle, NULL); - printf("promoted caller=%ld thread=%ld\n", caller, started); - - start = clock_ns(CLOCK_MONOTONIC); - if (!promoter_start(&promoter, - start - IDLE_BOUND_NS + 10 * NSEC_PER_SEC)) - return 1; - set_idle_priority(0, true); - promoter_stop(&promoter); - printf("stopped caller=%ld stop_ms=%" PRId64 "\n", - syscall(SYS_sched_getscheduler, 0), - (clock_ns(CLOCK_MONOTONIC) - start) / NSEC_PER_MSEC); - return 0; -} - -static int64_t g_test_fork_burn_ms; - -// Spends MS milliseconds of this process's CPU time. -static void burn_cpu(int64_t ms) -{ - int64_t until = cpu_time_ns() + ms * NSEC_PER_MSEC; - - while (cpu_time_ns() < until) - ; -} - -static void test_fork_burn(void) -{ - burn_cpu(g_test_fork_burn_ms); -} - -// Forks a repair step 7 worker as start_restore does, after PRE_MS of -// preparation, with a fork that costs the parent FORK_MS; the worker spends -// CHILD_MS and prints its preparation share and step 7's CPU time. -static int test_deferred_restore_cpu(int argc, char **argv) -{ - struct restore_timing timing = {0}; - int64_t start = cpu_time_ns(); - int status; - pid_t pid; - - if (argc != 3) - return 2; - g_test_fork_burn_ms = strtoll(argv[1], NULL, 10); - if (pthread_atfork(NULL, test_fork_burn, NULL) != 0) - return 1; - burn_cpu(strtoll(argv[0], NULL, 10)); - pid = fork_deferred_restore(start, &timing); - if (pid == 0) { - burn_cpu(strtoll(argv[2], NULL, 10)); - printf("prior_ms=%" PRId64 " total_ms=%" PRId64 "\n", - timing.prior_cpu_ns / NSEC_PER_MSEC, - deferred_restore_cpu_ns(&timing) / NSEC_PER_MSEC); - fflush(stdout); - _exit(0); - } - if (pid < 0 || waitpid(pid, &status, 0) != pid) - return 1; - return WIFEXITED(status) ? WEXITSTATUS(status) : 1; -} - -static int cmd_test(int argc, char **argv) -{ - const char *name = argc > 0 ? argv[0] : ""; - - g_test = true; - g_report_only = true; - argc--; - argv++; - if (strcmp(name, "packet") == 0) - return test_packet(argc, argv); - if (strcmp(name, "sample") == 0) - return test_sample(argc, argv); - if (strcmp(name, "pair") == 0 && argc == 3) { - int64_t theta; - int64_t epsilon; - - pair_clocks(strtoll(argv[0], NULL, 10), strtoll(argv[1], NULL, 10), - strtoull(argv[2], NULL, 10), &theta, &epsilon); - printf("theta_ns=%" PRId64 " epsilon_ns=%" PRId64 "\n", theta, epsilon); - return 0; - } - if (strcmp(name, "kmsg") == 0) - return test_lines(argc, argv, LINES_KMSG); - if (strcmp(name, "console") == 0 && argc == 2) { - g_async_output = strcmp(argv[1], "async") == 0; - console_write(argv[0]); - return 0; - } - if (strcmp(name, "exhaustive-leaf") == 0 && argc == 9) { - uint32_t value[4]; - uint32_t out_of_range[4]; - char detail[DETAIL_MAX]; - - for (int index = 0; index < 4; index++) { - value[index] = (uint32_t)strtoul(argv[1 + index], NULL, 0); - out_of_range[index] = (uint32_t)strtoul(argv[5 + index], NULL, 0); - } - if (exhaustive_leaf_ok((uint32_t)strtoul(argv[0], NULL, 0), value, - out_of_range, detail, sizeof(detail))) - printf("pass\n"); - else - printf("fail %s\n", detail); - return 0; - } - if (strcmp(name, "exhaustive-report") == 0 && argc == 4) { - struct exhaustive state; - - memset(&state, 0, sizeof(state)); - exhaustive_report(&state, argv[0], atoi(argv[1]), - strcmp(argv[2], "pass") == 0, argv[3]); - printf("failures=%d\n", state.failures); - return 0; - } - if (strcmp(name, "exhaustive-summary") == 0 && argc == 2) { - struct exhaustive state; - int status; - - memset(&state, 0, sizeof(state)); - state.failures = atoi(argv[1]); - status = exhaustive_summary(&state, atoi(argv[0])); - printf("exit=%d\n", status); - return 0; - } - if (strcmp(name, "boot-log") == 0) - return test_lines(argc, argv, LINES_BOOT_LOG); - if (strcmp(name, "syslog") == 0) - return test_lines(argc, argv, LINES_SYSLOG); - if (strcmp(name, "timer-list") == 0) - return test_timer_list(argc, argv); - if (strcmp(name, "correction") == 0) - return test_correction(argc, argv); - if (strcmp(name, "frequency") == 0 && argc == 1) { - int64_t frequency = restore_frequency((int32_t)strtol(argv[0], NULL, 10)); - - printf("frequency=%" PRId64 " ppb=%" PRId64 "\n", frequency, - frequency_to_ppb(frequency)); - return 0; - } - if (strcmp(name, "event") == 0 && argc == 6) { - char line[EVENT_MAX + 1]; - enum phase phase = PHASE_RUNTIME; - - for (size_t index = 0; index < ARRAY_SIZE(k_phase_names); index++) { - if (strcmp(argv[2], k_phase_names[index]) == 0) - phase = (enum phase)index; - } - g_report_only = false; - format_event(line, argv[0], argv[1], phase, - (uint32_t)strtoul(argv[3], NULL, 10), - strtoll(argv[4], NULL, 10), argv[5]); - fputs(line, stdout); - return 0; - } - if (strcmp(name, "state") == 0) - return test_state(argc, argv); - if (strcmp(name, "status") == 0 && argc == 2) { - static char text[4096]; - struct time_state state; - char out[1024]; - bool ok; - - if (test_text(argv[0], text, sizeof(text)) != 0 || - state_parse(text, &state) != 0) - return 2; - g_report_only = strcmp(argv[1], "report-only") == 0; - ok = format_status(&state, out, sizeof(out)); - fputs(out, stdout); - printf("ok=%d\n", ok ? 1 : 0); - return 0; - } - if (strcmp(name, "restore-record") == 0) - return test_restore_record(argc, argv); - if (strcmp(name, "deferred-restore-cpu") == 0) - return test_deferred_restore_cpu(argc, argv); - if (strcmp(name, "deferral") == 0 && argc == 0) { - printf("deferred_start_ms=%" PRId64 " idle_bound_ms=%" PRId64 "\n", - (int64_t)(DEFERRED_START_NS / NSEC_PER_MSEC), - (int64_t)(IDLE_BOUND_NS / NSEC_PER_MSEC)); - return 0; - } - if (strcmp(name, "parallel") == 0) - return test_parallel(argc, argv); - if (strcmp(name, "promote") == 0 && argc == 0) - return test_promote(); - if (strcmp(name, "idle-priority") == 0 && argc == 0) { - long idle; - - set_idle_priority(0, true); - idle = syscall(SYS_sched_getscheduler, 0); - set_idle_priority(0, false); - printf("idle=%ld normal=%ld\n", idle, - syscall(SYS_sched_getscheduler, 0)); - return 0; - } - return test_checks(name, argc, argv); -} - -// --------------------------------------------------------------------------- -// Entry point -// --------------------------------------------------------------------------- - -static bool cmdline_requests_report_only(void) -{ - char text[4096]; - - if (read_text("/proc/cmdline", text, sizeof(text)) < 0) - return false; - for (char *token = strtok(text, " \t\n"); token != NULL; - token = strtok(NULL, " \t\n")) { - if (strcmp(token, "nvx_time_abi=report-only") == 0) - return true; - } - return false; -} - -static int usage(void) -{ - fprintf(stderr, - "usage: nvx-time [--report-only] COMMAND\n" - " boot\n" - " check --phase capture|restore [--new-cpus LIST]\n" - " pre-capture\n" - " capture 0|1 GENERATION_ID ENTROPY_FILE [--finish]\n" - " cancel-capture\n" - " restore-finish [--ack] [--new-cpus LIST]\n" - " status\n" - " sample\n" - " generation-id\n" - " exhaustive\n" - " test NAME ARGUMENTS...\n"); - return 2; -} - -int main(int argc, char **argv) -{ - char *arguments[32]; - int count = 0; - const char *command; - - for (int index = 1; index < argc; index++) { - if (strcmp(argv[index], "--report-only") == 0) - g_report_only = true; - else if (count < (int)ARRAY_SIZE(arguments)) - arguments[count++] = argv[index]; - else - return usage(); - } - if (count == 0) - return usage(); - command = arguments[0]; - if (strcmp(command, "test") == 0) - return cmd_test(count - 1, arguments + 1); - // An inherited SIG_IGN would make the kernel reap the release child and - // the daemon before waitpid() sees them. - signal(SIGCHLD, SIG_DFL); - if (cmdline_requests_report_only()) - g_report_only = true; - if (strcmp(command, "boot") == 0 && count == 1) - return cmd_boot(); - if (strcmp(command, "check") == 0) - return cmd_check(count - 1, arguments + 1); - if (strcmp(command, "pre-capture") == 0 && count == 1) - return cmd_pre_capture(); - if (strcmp(command, "capture") == 0) - return cmd_capture(count - 1, arguments + 1); - if (strcmp(command, "cancel-capture") == 0 && count == 1) - return cmd_cancel_capture(); - if (strcmp(command, "restore-finish") == 0) - return cmd_restore_finish(count - 1, arguments + 1); - if (strcmp(command, "status") == 0 && count == 1) - return cmd_status(); - if (strcmp(command, "sample") == 0 && count == 1) - return cmd_sample(); - if (strcmp(command, "generation-id") == 0 && count == 1) - return cmd_generation_id(); - if (strcmp(command, "exhaustive") == 0 && count == 1) - return exhaustive_run(); - return usage(); -} diff --git a/nvx-rs/Cargo.toml b/nvx-rs/Cargo.toml index 577d0936d..ec40b88d3 100644 --- a/nvx-rs/Cargo.toml +++ b/nvx-rs/Cargo.toml @@ -19,14 +19,25 @@ name = "nvx" [features] default = ["host"] # The host side: artifact discovery, launch, network and filesystem lowering, control sessions, -# process and platform primitives, and the supervision of VM instances. -host = ["serde", "dep:getrandom", "dep:serde_json", "dep:nix", "dep:windows-sys"] +# process and platform primitives, the supervision of VM instances, and the sandbox launch +# profile, whose aggregate child names are SHA-256 digests. +host = [ + "serde", + "dep:getrandom", + "dep:serde_json", + "dep:sha2", + "dep:nix", + "dep:windows-sys", +] # Stages the OpenVMM executable, guest kernel, and control initramfs at build time; see # `Artifacts::bundled`. The build script downloads the release pinned in `artifacts.json`, or # copies the release directory named by `NVX_BUNDLE_DIR`. bundled = ["host", "dep:sha2"] # Test doubles: the fake OpenVMM of `testing::fake_openvmm` and its `nvx-fake-openvmm` binary. testing = ["dep:getrandom", "dep:serde", "dep:serde_json", "dep:nix", "dep:windows-sys"] +# The `nvx-rs` command-line interface of `host::cli` and its `nvx-rs` binary, which `nvx.py` +# runs for the steps that it delegates. +cli = ["host"] # The serde form of `Hypervisor`. The `host` feature enables it. serde = ["dep:serde"] # The programs that run in Linux guests: the `guest` module and the `nvx-*` guest binaries. Build @@ -37,12 +48,15 @@ guest = ["dep:nix", "dep:getrandom"] getrandom = { version = "0.4", optional = true } serde = { version = "1.0.200", features = ["derive"], optional = true } serde_json = { version = "1.0.120", optional = true } +# The same release as the build script's: `host` enables both. +sha2 = { version = "0.10", optional = true } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", optional = true, features = [ "Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", + "Win32_System_Console", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Pipes", @@ -69,7 +83,8 @@ nix = { version = "0.31.3", default-features = false, optional = true, features ] } [build-dependencies] -# Only the `bundled` build script reads JSON; `host`, which `bundled` enables, enables it here too. +# Only the `bundled` build script reads JSON and computes digests; `host`, which `bundled` +# enables, enables both here too. serde_json = { version = "1.0.120", optional = true } sha2 = { version = "0.10", optional = true } @@ -83,6 +98,12 @@ path = "src/bin/nvx-fake-openvmm.rs" required-features = ["testing"] doc = false +[[bin]] +name = "nvx-rs" +path = "src/bin/nvx-rs.rs" +required-features = ["cli"] +doc = false + # The guest binaries. Their unit tests live in the library, so the binaries have no test harness. [[bin]] name = "nvx-console-pending" diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 242a03c91..434f06665 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -4,9 +4,12 @@ control protocol, and the launch, policy lowering, and supervision of NVX microVMs. Its library is called `nvx`. The [`aci_edge_sandboxes` crate](../aci_edge_sandboxes/README.md) builds on it and -re-exports it as `aci_edge_sandboxes::nvx`, and later phases of -[microsoft/nvx#356](https://github.com/microsoft/nvx/issues/356) add the guest -programs and a host CLI to it. +re-exports it as `aci_edge_sandboxes::nvx`. The crate also holds the programs +that run in NVX's Linux guests, and the `nvx-rs` command-line interface, the +backend to which `nvx.py` delegates its sandbox, egress-policy, and artifact +steps, as phases of +[microsoft/nvx#356](https://github.com/microsoft/nvx/issues/356) move them to +Rust. An NVX sandbox is a microVM that OpenVMM runs with the managed lifecycle: the guest's init starts a managed agent, which runs the workloads that the host @@ -25,11 +28,15 @@ nvx-rs/ │ ├── net/ CIDR parsing and subtraction, and hostname and guest-network forms │ ├── hypervisor.rs │ ├── error.rs nvx::Error and its ErrorKind -│ ├── host/ feature `host` (default): artifacts, launch, network, filesystem, session, -│ │ process, platform, VM instances, and outcome reports +│ ├── host/ feature `host` (default): artifacts, launch, network and egress policy files, +│ │ filesystem, session, process, platform, VM instances, outcome reports, and +│ │ the sandbox launch profile of `nvx.py`, with the Python semantics they keep; +│ │ with feature `cli`, the `nvx-rs` CLI │ ├── testing/ feature `testing`: the fake OpenVMM │ ├── guest/ feature `guest`: the programs that run in Linux guests -│ └── bin/ nvx-fake-openvmm and the guest binaries +│ └── bin/ nvx-rs, nvx-fake-openvmm, and the guest binaries +├── tests/ fake-OpenVMM, CLI, and guest-binary tests, and the golden fixtures that Python +│ recorded for the ports of its code, in fixtures/ ├── build.rs bundled-release staging └── artifacts.json the NVX release that the `bundled` feature stages ``` @@ -40,26 +47,338 @@ nvx-rs/ | `nvx::contract` | The OpenVMM microVM ABI, broker protocol, and control-contract revision that the crate speaks, and `manifest_is_compatible` for a release's `SOURCE-MANIFEST.json` | | `nvx::net` | `Cidr`: IPv4 and IPv6 networks and their subtraction; `is_valid_hostname` and `is_valid_guest_network`, the forms of a VM's hostname and guest address | | `nvx::Hypervisor` | KVM, MSHV, or WHP, with its serde form behind the `serde` feature | -| `nvx::host::artifacts` | `Artifacts` (also `nvx::Artifacts`): the OpenVMM executable, guest kernel, and Alpine initramfs, from a release, a repository checkout, the environment, a directory next to the executable, or the bundle | +| `nvx::host::artifacts` | `Artifacts` (also `nvx::Artifacts`): the OpenVMM executable, guest kernel, and Alpine initramfs, from a release, a repository checkout, the environment, a directory next to the executable, or the bundle. `ArtifactDir` and `Guest`: every artifact of the guests that `nvx.py` runs, the OpenVMM executable, the kernel, the Alpine, Ubuntu, and Azure Linux initramfs images and their package manifests, and the Ubuntu distro layer and its manifest, in a checkout's `build/` and `openvmm/target/release/` or a release's `guest/` and `bin/`, checked in the order and with the messages of `nvx.py`. `ArtifactPaths` lists them all and serializes as JSON | | `nvx::host::launch` | `LaunchSpec` and OpenVMM's command line: the managed lifecycle, the `WorkloadIdentity`, the 1024-byte kernel command line, its reserved parameters, the 32,767-character limit of a Windows command line, and `LaunchSpec::check`, which every command line applies to the memory size, the non-root identity, the hostname, the guest network, and the kernel parameters | -| `nvx::host::network` | Egress rules and their exact expansion into OpenVMM's IPv4 and IPv6 rules: exceptions, protocol-only rules, and port ranges, with at most 256 rules for each action | +| `nvx::host::network` | `NetworkPolicy` and `openvmm_arguments`: the egress default and rules, ingress, host-loopback access and forwards as OpenVMM options. Egress rules are lowered exactly into OpenVMM's IPv4 and IPv6 rules by the lowering of egress policy files, with at most 256 rules for each action | +| `nvx::host::network::policy` | Egress policy files: `compile_policy_file`, `compile_policy_bytes`, and `compile_policy` compile one to a `CompiledEgressPolicy` of native rules; see [Egress policy files](#egress-policy-files) | | `nvx::host::filesystem` | The `ExportPlan` of host paths through OpenVMM's aggregate virtio-fs device, the host-mapping table, the reserved guest targets, and `guest_path` and `resolve_guest_path` (also at `nvx::`) | | `nvx::host::instance` | `Instance`: a VM in a directory of its own. It starts OpenVMM with a fresh capability and waits until the guest is ready and has mounted the mapped paths, reconciles recorded state, recovers interrupted launches, runs workloads and pumps their output, and stops the VM with a forced fallback | | `nvx::host::session`, `process`, `platform`, `report` | The authenticated control session, OpenVMM processes, Linux and Windows primitives, and OpenVMM's outcome report | -| `nvx::testing::fake_openvmm` | A fake OpenVMM that serves the control console and runs scripted workloads without a VM | +| `nvx::host::sandbox` | The launch profile of `nvx.py sandbox`, ported from the one that `scripts/nvx_tools/sandbox.py` held: `SandboxLaunch`, with the EROFS distro `SandboxLayer`, the one layer that a sandbox takes, the ext4 scratch disk, the entrypoint, arguments, hostname, workload identity, and resource limits, and its `--microvm-sandbox-block` and `--microvm-workload-identity` arguments and kernel command line, within the 1024-byte budget that OpenVMM leaves it. `mount`: `SandboxMount` live shares of host directories and files, their guest targets and the reserved ones, their access policies and owner, the aggregate of several shares, and the checks of their host paths. `options`: the checks that `nvx.py sandbox` and `nvx.py run` apply to their options: the attribution of `--mount-deny`, `--mount-allow`, and `--mount-write` to shares, the systemd policy, the targets of `nvx.py run` shares, and the managed exec environment; see [Python parity](#python-parity) | +| `nvx::testing::fake_openvmm` | A fake OpenVMM that serves the control console and runs scripted workloads without a VM, for direct launches and for the managed and one-shot sandboxes of `nvx-rs sandbox` and `nvx.py sandbox` | +| `nvx::host::cli` | Feature `cli`: the `nvx-rs` executable; see [The nvx-rs CLI](#the-nvx-rs-cli) | | `nvx::guest` | The programs that run in Linux guests; see [Guest binaries](#guest-binaries) | Lowering functions take the names by which messages refer to their inputs, such as `EgressRules::name` and `PathList::name`, so callers' messages name their own fields. +## Egress policy files + +`nvx::host::network::policy` compiles the egress policy files that +`nvx.py run` and `nvx.py sandbox` take with `--network-egress-policy-file` +([format](../doc/run.md#run-openvmm-directly)). It is a faithful port of the +former `scripts/nvx_tools/egress_policy.py`, which it replaced when `nvx.py` +delegated to `nvx-rs`: it accepts the same files, compiles them to the same +rules, and fails with the same messages. + +- A policy is at most 1 MiB of UTF-8 JSON whose root has only `allow` and + `deny` arrays. JSON is read as Python's `json.loads` reads it, so `NaN` and + `Infinity` are numbers, while a repeated field and an integer of more than 64 + digits are errors. +- Rules take the MXC form, with optional `to` destinations (`cidr` and + `except`) and `ports` selectors (`protocol`, `port`, and `endPort`), or the + legacy flat form (`cidr`, `except`, `protocol`, `port`, and `endPort`). + Protocols are `tcp`, `udp`, `icmp`, and `any`, and TCP and UDP take one port + or a range. A CIDR is read as Python's `ipaddress.ip_interface` reads it: + host bits are cleared, IPv4 netmasks such as `/255.255.0.0` are accepted, and + IPv6 scopes are refused. +- Each action, `allow` or `deny`, lowers to at most 256 native rules: + exceptions are subtracted, destinations merge into the fewest networks, rules + for every port of a protocol subsume the narrower rules that they cover, and + a sweep over the port boundaries merges overlapping and adjacent ranges. The + rules sort by family, address, prefix length, protocol, and ports, and read + `CIDR`, `CIDR:PROTOCOL`, `CIDR:PROTOCOL:PORT`, or `CIDR:PROTOCOL:FIRST-LAST`. + +`openvmm_arguments` lowers `NetworkPolicy` rules, and so `aci_edge_sandboxes` +policies, the same way: an `EgressRule` is a policy file's MXC rule, its +messages name `EgressRules::name` where a file's name `allow` or `deny`, such +as `network.egress.allow[0].to[1].cidr is not a valid IPv4 or IPv6 CIDR: x`, +and the checks that only `NetworkPolicy` has, such as the host-loopback ones, +stay in `openvmm_arguments`. + +Messages match Python's byte for byte, but two cases have no Rust equivalent. A +string that Python would hold with an unpaired surrogate, such as one decoded +from a `\ud800` escape or a path that is not Unicode, shows the surrogate as +`\udXXX`, which is how Python prints it, and paths show as +`str(pathlib.Path(path))` does. Python's decoder raises `RecursionError` for a +document nested more deeply than its stack allows; this reader has no such +limit, so those documents fail as invalid policies instead. + +[`tests/fixtures/egress-policy.json`](tests/fixtures/egress-policy.json) holds +the Python compiler's outcomes for every policy that its tests compiled, +hand-written edge cases, and a seeded random corpus, which the tests replay +without Python; `NVX_EGRESS_POLICY_FIXTURE` points the tests at another corpus +in the same format. See [Python parity](#python-parity). + +## Python parity + +`nvx::host::network::policy`, `nvx::host::sandbox`, and the guest lookups of +`nvx::host::artifacts` replaced Python code that `nvx.py` ran, so they keep its +contract byte for byte: its limits, option semantics, outputs, and messages. +They check values as Python does: whitespace is what `str.isspace` accepts, +integers are what `int` reads, UUIDs what `uuid.UUID` reads, and JSON what +`json.loads` reads, failing with its messages; host paths are normalized as +`pathlib` normalizes them, with backslashes on Windows, and resolved as +`Path.resolve` resolves them; file identities are those of `os.stat`, which on +Windows are the volume serial number and the 128-bit file ID; and messages +quote values as `repr` does. The private `python` module implements these +semantics after CPython 3.14, with the Unicode 16.0.0 tables of Python 3.14. +Python decodes operating-system strings that are not valid Unicode into +surrogates: paths print as Python prints them, and the filesystem queries +refuse them. + +`tests/fixtures` holds what the Python code did with fixed inputs and seeded +random corpora: `python.json` for the Python semantics, `sandbox.json` and +`sandbox_options.json` for the launch profile and the sandbox options, for +POSIX and Windows paths alike, `egress-policy.json` for the policy compiler, +and `cli.json` for `nvx.py sandbox` as a command; see +[Golden scenarios](#golden-scenarios). `scripts/nvx_rs_fixtures.py` recorded +them with CPython 3.14, and generated the Unicode tables of +`src/host/python/unicode.rs`, until `nvx.py` delegated to `nvx-rs` and +[microsoft/nvx#359](https://github.com/microsoft/nvx/issues/359) retired the +script with the Python code that it recorded. The golden tests compare the +ports with these records and need no Python. The records are now the +reference of that behavior: a deliberate change of behavior updates the +records that it changes by hand, in the same change. + +## The nvx-rs CLI + +The `cli` feature builds the `nvx-rs` executable, a faithful port of the +runtime steps of `nvx.py` that `nvx.py` delegates to it +([microsoft/nvx#359](https://github.com/microsoft/nvx/issues/359)): + +```text +nvx-rs [-h] [--root PATH] [--prog PROG] artifacts +nvx-rs [-h] [--root PATH] [--prog PROG] egress compile PATH +nvx-rs [-h] [--root PATH] [--prog PROG] sandbox [run|provision|start|exec|stop|deprovision] [OPTIONS] +``` + +```bash +cd nvx-rs +cargo build --release --features cli # target/release/nvx-rs +``` + +Every NVX release package ships the release build as `bin/nvx-rs`, a static +`x86_64-unknown-linux-musl` executable, in the Linux packages, and as +`bin/nvx-rs.exe`, with a static C runtime, in the Windows package, with its +provenance in `provenance/nvx-rs.provenance.json` +([Package and source delivery](../doc/distribution.md)). The `nvx-rs` of an +extracted release runs as is: without `--root`, it uses the release that holds +it; see [Artifact root](#artifact-root). +[`nvx.py download`](../doc/usage.md#download) installs it in a checkout of the +sources that built it, so that `nvx.py` runs it without Cargo. + +`nvx.py` runs the executable that `NVX_RS` names, else +`nvx-rs/target/release/nvx-rs[.exe]` of its checkout, and else builds that one +with Cargo first, with its output on standard error; see +[Usage](../doc/usage.md#nvx-rs). `nvx.py` records the provenance of the builds +that it makes and installs, and `nvx.py record-nvx-rs-provenance` that of a +build made otherwise. It runs `nvx-rs --root REPOSITORY --prog +"nvx.py sandbox" sandbox OPERATION OPTIONS` with the options that the user +gave, in full, and the standard streams and exit status of the command; +`nvx-rs egress compile PATH` and `nvx-rs --root REPOSITORY artifacts` for +`nvx.py run`; and `nvx-rs egress compile PATH` for the egress scenarios of +`nvx.py test-microvm`. + +Errors print `error: MESSAGE` to standard error and exit with status 1, as +`nvx.py` reports a `ScriptError`. Usage errors print the parser's usage and +`PROG: error: MESSAGE` and exit with status 2, as `argparse` does, and help +exits with status 0. Ctrl-C prints `Interrupted` and exits with status 130. +`sandbox run` and `sandbox exec` exit with the status of OpenVMM or of the +workload. Text ends its lines as Python's `print` ends them, with `\r\n` on +Windows and `\n` elsewhere, while workload output passes through unchanged. + +`--prog PROG` names the program in usage, help, and error messages, `nvx-rs` by +default. `nvx.py` passes its own name, so that the usage errors of +`nvx.py sandbox` that `nvx-rs` reports, such as an invalid `--layer`, read +`usage: nvx.py sandbox ...` and `nvx.py sandbox: error: ...` as they did before +`nvx.py` delegated the command. The usage wraps for the name as `argparse` wraps +it for 80 columns. + +### Artifact root + +Every command uses the artifacts of one root: a repository checkout, with the +guest artifacts in `build/` and OpenVMM in `openvmm/target/release/`, or an +extracted release, with them in `guest/` and `bin/`. + +- `--root PATH` names the root, as `nvx.py`, which knows its repository root, + passes it. A directory that holds `bin/openvmm[.exe]`, `guest/vmlinux`, or + `guest/initramfs.cpio.gz` is a release, and any other is a checkout. +- Without `--root`, the root is the release whose `bin/` holds the executable, + as for a release's `bin/nvx-rs[.exe]`, or the checkout whose `nvx-rs/target/` + holds it at any depth, as after `cargo build`. Either must have its + `SOURCE-MANIFEST.json`; otherwise the command fails and asks for `--root`. +- The `NVX_OPENVMM` environment variable names an OpenVMM executable to run + instead of the root's, such as `nvx-fake-openvmm` in tests. A relative path is + relative to the working directory. + +### `artifacts` + +Prints the paths of the artifacts as JSON, as `json.dumps(value, indent=2, +sort_keys=True)` writes it. Paths are absolute and not checked: + +```json +{ + "guests": [ + { + "distribution": "Alpine Linux", + "distro_layer": null, + "distro_manifest": null, + "initramfs": "/nvx/build/initramfs.cpio.gz", + "name": "alpine", + "package_manifest": "/nvx/build/initramfs.cpio.gz.packages.json" + }, + { + "distribution": "Ubuntu Base", + "distro_layer": "/nvx/build/ubuntu-distro.erofs", + "distro_manifest": "/nvx/build/ubuntu-distro.erofs.manifest.json", + "initramfs": "/nvx/build/initramfs-ubuntu.cpio.gz", + "name": "ubuntu", + "package_manifest": "/nvx/build/initramfs-ubuntu.cpio.gz.packages.json" + }, + { + "distribution": "Azure Linux", + "distro_layer": null, + "distro_manifest": null, + "initramfs": "/nvx/build/initramfs-azurelinux.cpio.gz", + "name": "azurelinux", + "package_manifest": "/nvx/build/initramfs-azurelinux.cpio.gz.packages.json" + } + ], + "kernel": "/nvx/build/vmlinux", + "layout": "repository", + "openvmm": "/nvx/openvmm/target/release/openvmm", + "root": "/nvx" +} +``` + +`layout` is `repository` or `release`, the guests are listed in the order of +`nvx.py`, a guest without a distro layer has `null` for it and its manifest, +and `openvmm` is `NVX_OPENVMM` when that is set. + +### `egress compile PATH` + +Compiles an egress policy file, as `--network-egress-policy-file` does, and +prints its native OpenVMM rules in the same JSON form: +`{"allow": [RULE, ...], "deny": [RULE, ...]}`, each list in the order in which +OpenVMM receives the rules, which are `--network-egress-allow` and +`--network-egress-deny` values. A file that `nvx.py` refuses fails with its +message. + +### `sandbox` + +`nvx-rs sandbox` keeps the contract of `nvx.py sandbox`: every option of its +parser, with the same names, defaults, and value checks, and the same +operations, checks, messages, outputs, exit statuses, outcome reports, and +state directories. The operation defaults to `run`. Options take their values +as `--option VALUE` or `--option=VALUE`, repeated options accumulate, and +`--mount-deny`, `--mount-allow`, and `--mount-write` apply to the `--mount` +before them. + +- `run` prints `>> COMMAND`, OpenVMM's command line, quoted as + `subprocess.list2cmdline` quotes it on Windows and as `shlex.join` does + elsewhere. Unless `--dry-run` is given, it then runs OpenVMM with the + standard streams and exits with its status. `--outcome-report` asks OpenVMM + for its outcome report. +- `provision` records the sandbox in its state directory. `start` launches + OpenVMM detached with the managed lifecycle, passes it a fresh capability + through its standard input, and waits until the guest answers. `exec` runs a + workload, prints its standard output and then its standard error, and exits + with its status; `--outcome-report` writes + `{"operation_id": HEX, "outcome": {"category": CATEGORY, "operation": + "exec", "status_code": STATUS}, "schema_version": 1}`. `stop` shuts the VM + down and prints nothing. `deprovision` removes the state directory. + +The state directory and its files are those that `nvx.py` wrote before it +delegated to `nvx-rs`, so `nvx-rs` and an older `nvx.py` can each operate a +sandbox that the other provisioned or started: + +| File | Contents | +| --- | --- | +| `config.json` | The provisioned sandbox, in the lowest of formats 1 to 6 that holds it, written as `json.dumps(value, indent=2, sort_keys=True)` writes it, with mode 0600, through a temporary file | +| `runtime.json` | Format 1: the `pid` and `start_time` of the running OpenVMM, as Python identifies a process, and its `control_endpoint`: `STATE/control.sock`, or `//./pipe/openvmm-microvm-HEX` on Windows | +| `control.capability` | The 32-byte capability of the running OpenVMM, with mode 0600 | +| `control.sock` | The control endpoint on POSIX hosts | +| `openvmm.log` | OpenVMM's output, appended by each start | +| `outcome.json` | OpenVMM's outcome report of the last start | +| `lifecycle.lock` | The lock that serializes lifecycle transitions, an exclusive `flock` on POSIX and a lock of its first byte on Windows, as Python takes it; deprovision leaves `deprovisioned` in it while it removes the directory | + +#### Guest features + +After the guest answers, `start` asks it for its control features and +requires `GuestFeatures::LAYERED_SANDBOX`, the features that its launches use: +per-execution environments (bit 4), since `exec` sends a working directory and +an environment. It needs neither cancellation (bit 0), since the CLI never +cancels a workload, and Ctrl-C ends the session, nor workload accounts +(bit 2, [#566](https://github.com/microsoft/nvx/issues/566)). A guest image +that lacks a required feature, or that predates the request, is stopped as a +failed start is, and the start fails with +`the guest image lacks control features that layered sandboxes need (...)`, +which names the missing features. `nvx.py` does not ask; this is the one +intended change of behavior. The guest agents of NVX advertise bits 0, 2, and 4 +in sandbox mode. + +#### Other differences from nvx.py + +- Long options cannot be abbreviated; `nvx.py` passes them in full. +- Help and usage are formatted for 80 columns, as `argparse` formats them for a + pipe, and an invalid choice lists the choices quoted, as Python 3.14.5 and + later do. +- Where `nvx.py` would end with a Python traceback, as for a configuration + without a field that `start` reads, `nvx-rs` prints `error: MESSAGE`, with the + message of the exception, and exits with status 1. +- Text goes to pipes and files in UTF-8, where Python on Windows uses the ANSI + code page unless `PYTHONIOENCODING` or UTF-8 mode says otherwise. +- `start` refuses a control endpoint that another process serves, which it + treats as one that has not appeared yet, and, on Windows, starts OpenVMM + without a console window, as `nvx::host::process` starts it. +- `exec` refuses more than 1 MiB of workload output, which the guest agent + never forwards. + +### Test hooks + +`--root` and `NVX_OPENVMM` let tests run the CLI without a VM. `nvx-fake-openvmm` +serves the sandbox profile: its managed lifecycle, with the capability on its +standard input, the control console, and an outcome report at stop, and its +one-shot runs, which run `/bin/echo`, `/bin/sleep`, `/bin/true`, or +`/bin/false`, write an outcome report if asked, and exit with the workload's +status. Kernel command-line tokens make it fail: `fake_exit_on_start=STATUS`, +`fake_exit_status=STATUS`, `fake_guest_features=MASK`, and +`fake_legacy_guest=1`, among others; see `nvx::testing::fake_openvmm`. + +```bash +cd nvx-rs +cargo build --features cli,testing --bins +NVX_OPENVMM=target/debug/nvx-fake-openvmm \ + target/debug/nvx-rs --root .. sandbox run --layer distro,LAYER,UUID \ + --scratch SCRATCH --entrypoint /bin/echo --arg hello +``` + +### Golden scenarios + +`tests/fixtures/cli.json` holds scenarios that the fixture script recorded from +the Python implementation of `nvx.py sandbox`, run as a command with the root's +OpenVMM a copy of `nvx-fake-openvmm`, and from the Python functions behind +`egress compile` and `artifacts`: the option and usage errors, dry runs, +one-shot runs, the lifecycles of sandboxes in every configuration format, and +the refusals of the lifecycle operations. Each step records the exit status, +standard output and error, and the state files, with the root, process IDs, and +random identifiers as placeholders. Its `posix` and `windows` sections were +recorded on each kind of host, where the script also played every scenario with +`nvx-rs`, and each lifecycle with the Python implementation and `nvx-rs` taking +turns both ways, and found no difference. `tests/cli.rs` replays the section of +its host with `nvx-rs`, without Python; since `nvx.py sandbox` runs `nvx-rs`, +these scenarios are its reference. + ## Features | Feature | Contents | | --- | --- | -| `host` (default) | The `host` module. It enables `serde` | +| `host` (default) | The `host` module. It enables `serde`, and `sha2`, which names the children of a sandbox's aggregate share | | `bundled` | Stages a release at build time; see [Bundled artifacts](#bundled-artifacts) | | `testing` | The fake OpenVMM and the `nvx-fake-openvmm` binary | +| `cli` | The `nvx-rs` CLI of `host::cli` and its `nvx-rs` binary; it enables `host`. See [The nvx-rs CLI](#the-nvx-rs-cli) | | `serde` | The serde form of `Hypervisor` | | `guest` | The `guest` module and the guest binaries; see [Guest binaries](#guest-binaries) | @@ -148,15 +467,18 @@ Without `NVX_BUNDLE_DIR`, the build script downloads the release package that Windows packages are ZIP archives, which need bsdtar (Windows' own `tar`) or `unzip`. Every staged file must match the source's `SHA256SUMS`, a download must match its pinned digest, and the release must declare the control contract -of `nvx::contract`. The build scripts of dependents receive the staged +of `nvx::contract`. The build script extracts and verifies only the files that +the bundle holds, so a release that ships `bin/nvx-rs[.exe]` stages as the +older release that `artifacts.json` pins, which doesn't. The build scripts of +dependents receive the staged directory as `DEP_NVX_ARTIFACTS_DIR` and can ship it next to their executables as `nvx/`. ## Guest binaries The `guest` feature adds the programs that run in NVX's Linux guests: the -`nvx::guest` module and one binary per program. Guest images install these -executables as `/sbin/nvx-*`. +`nvx::guest` module and one binary per program. The Alpine, Ubuntu, and Azure +Linux images ship all nine at their established `/sbin/nvx-*` paths. | Binary | Program | | --- | --- | @@ -181,20 +503,25 @@ python3 scripts/nvx.py build-guest-binaries \ --output build/nvx-rs-guest-binaries ``` +Native guest builds, the digest-pinned Docker Rust stage, and CI all call this +routine. Docker builds the set once and installs identical bytes in all three +guest images; Ubuntu determinism checks rebuild from fresh targets. + ## Testing ```bash cd nvx-rs -NVX_BUNDLE=skip cargo test --all-features # unit, golden-fixture, and fake-OpenVMM tests +NVX_BUNDLE=skip cargo test --all-features # unit, golden-fixture, parity-fixture, and fake-OpenVMM tests ``` The tests need no hypervisor. The fake-OpenVMM test drives `Instance` end to end -against `nvx-fake-openvmm`, and the protocol tests compare the wire constants -with the guest agent's source in `guest/common/nvx-managed-agent.c`. On Linux, -a loopback test drives the host `ControlSession` through a broker shim into the -real managed agent's session, dispatch, and execution engine, and `nvx.py` also -lints and tests the guest binaries, and runs their integration tests against a -build of them: +against `nvx-fake-openvmm`, the CLI test replays the +[golden scenarios](#golden-scenarios) and checks the guest-feature +requirement and, on Linux, Ctrl-C, and shared protocol definitions plus golden +byte fixtures cover both sides of every record. On Linux, a loopback test drives +the host `ControlSession` through a broker shim into the real managed agent's +session, dispatch, and execution engine, and `nvx.py` also lints and tests the +guest binaries, and runs their integration tests against a build of them: ```bash python3 ../scripts/nvx.py check-guest-binaries @@ -210,3 +537,16 @@ rustdoc with warnings denied, the minimum supported Rust version (1.89), and a macOS check. On Linux, it also lints, builds, and tests the guest binaries through `nvx.py`. `python3 scripts/nvx.py test-aci-edge-sandboxes --backend kvm` exercises the crate on a real hypervisor through `aci_edge_sandboxes`. + +`tests/managed_lifecycle.rs` is an ignored test of the managed lifecycle of a +guest whose init hands the boot to the managed agent, on a real hypervisor: +authenticated sessions and reconnects, request-scoped working directories and +environments, the workload identity, timeouts, refused working directories, +and stop, which `nvx::host::session` drives. `nvx.py test-microvm --scenario +managed-lifecycle` builds OpenVMM's command line for its guest and backend, +runs the test with Cargo, and checks the outcome report and the guest console +that it leaves: + +```bash +python3 ../scripts/nvx.py test-microvm --backend kvm --scenario managed-lifecycle +``` diff --git a/nvx-rs/src/bin/nvx-rs.rs b/nvx-rs/src/bin/nvx-rs.rs new file mode 100644 index 000000000..fa8bf161a --- /dev/null +++ b/nvx-rs/src/bin/nvx-rs.rs @@ -0,0 +1,5 @@ +//! The `nvx-rs` command-line interface; see `nvx::host::cli`. + +fn main() { + std::process::exit(nvx::host::cli::main()); +} diff --git a/nvx-rs/src/guest/reseed.rs b/nvx-rs/src/guest/reseed.rs index 877825329..1328f15d8 100644 --- a/nvx-rs/src/guest/reseed.rs +++ b/nvx-rs/src/guest/reseed.rs @@ -22,7 +22,7 @@ const SEED_BYTES: usize = 64; const GENERATION_ID_BYTES: usize = 16; /// Number of random bytes printed by `--sample`. const RANDOM_SAMPLE_BYTES: usize = 32; -/// Exact usage diagnostic inherited from the C program. +/// Exact public usage diagnostic. const USAGE: &[u8] = b"usage: nvx-reseed --sample | [--generation-only] SEED_FILE [PREVIOUS_GENERATION_ID]\n"; @@ -52,8 +52,8 @@ fn run( { return print_random_sample(out, err); } - // As in the C program, a first argument of `--generation-only` names the seed file unless - // three arguments follow the program name. + // A first argument of `--generation-only` names the seed file unless three arguments follow + // the program name. let (generation_only, seed_path, previous) = match args { [_, seed] => (false, seed, None), [_, seed, previous] => (false, seed, Some(previous)), @@ -107,8 +107,7 @@ fn reseed(random: &CStr, seed: &[u8; SEED_BYTES], err: &mut dyn Write) -> Result }) } -/// Reads exactly `bytes.len()` bytes from the file at `path`. Like the C program, it does not -/// retry after `EINTR`. +/// Reads exactly `bytes.len()` bytes from the file at `path`, without retrying after `EINTR`. fn read_seed(path: &CStr, bytes: &mut [u8], err: &mut dyn Write) -> Result<(), CommandError> { let fail = |err: &mut dyn Write, operation: &[u8], reason: &[u8]| { let path = path.to_bytes(); diff --git a/nvx-rs/src/guest/time/mod.rs b/nvx-rs/src/guest/time/mod.rs index 8ab087643..2ec1b791c 100644 --- a/nvx-rs/src/guest/time/mod.rs +++ b/nvx-rs/src/guest/time/mod.rs @@ -1,12 +1,12 @@ -//! `nvx-time`, the guest side of NVX time ABI v1, which replaces `guest/common/nvx-time.c`. +//! `nvx-time`, the guest side of NVX time ABI v1. //! //! It runs the conformance checks, the violation watcher and the wall-clock discipline in one //! daemon, and the snapshot agent's time steps: the guest preparation steps of the architecture //! guide's section 5.3.2 and the guest repair steps of its section 5.5.7, which the comments cite //! by number. Section 3.2.10 is the contract that it enforces. It prints nothing on success: //! checks record their results in the state file, and only violation events reach the console. -//! The C program defines the check IDs, the guest codes, and the formats of the output lines and -//! the state file, which this program keeps. +//! It keeps the check IDs, the guest codes, and the formats of the output lines and the state +//! file of the C program that it replaced. //! //! `nvx-time test NAME ARGUMENTS...` runs one of the C program's test hooks, which exercise its //! decoders, formats, and process structure without the hypervisor. diff --git a/nvx-rs/src/guest/time_probe/mod.rs b/nvx-rs/src/guest/time_probe/mod.rs index bf5b27750..4ca4ddc4a 100644 --- a/nvx-rs/src/guest/time_probe/mod.rs +++ b/nvx-rs/src/guest/time_probe/mod.rs @@ -1,12 +1,11 @@ -//! `nvx-time-probe`, the guest-side diagnostics for the NVX time ABI, which replaces -//! `guest/common/nvx-time-probe.c`. +//! `nvx-time-probe`, the guest-side diagnostics for the NVX time ABI. //! -//! It keeps the C program's commands, output, and exit statuses: 0 on success, 1 for a warp -//! check that fails, 2 for an invalid command line, and 3 for a measurement that could not -//! run. Its numeric options are plain decimal, or `0x`-prefixed hexadecimal for the integers, -//! and its rate options decimal floating point, as the other guest programs read their -//! arguments. It reads the kernel's numbers, such as `/proc/cpuinfo`'s rates, only in the form -//! in which the kernel writes them. +//! It keeps the commands, output, and exit statuses of the C program that it replaced: 0 on +//! success, 1 for a warp check that fails, 2 for an invalid command line, and 3 for a +//! measurement that could not run. Its numeric options are plain decimal, or `0x`-prefixed +//! hexadecimal for the integers, and its rate options decimal floating point, as the other guest +//! programs read their arguments. It reads the kernel's numbers, such as `/proc/cpuinfo`'s +//! rates, only in the form in which the kernel writes them. mod clock; mod format; diff --git a/nvx-rs/src/host/artifacts.rs b/nvx-rs/src/host/artifacts.rs index 20a9fe1a0..a23d69489 100644 --- a/nvx-rs/src/host/artifacts.rs +++ b/nvx-rs/src/host/artifacts.rs @@ -1,20 +1,43 @@ -//! Locating the OpenVMM executable and the NVX guest kernel and initramfs. +//! Locating the OpenVMM executable and the NVX guest artifacts. //! //! [`Artifacts`] names the three files that a VM runs. They come from an extracted NVX release //! archive, an NVX repository checkout, explicit paths, or a bundle that the build script staged //! with the `bundled` feature. [`Artifacts::discover`] tries these sources in a fixed order so //! consumers such as MXC need no NVX-specific configuration. +//! +//! [`ArtifactDir`] names every artifact of the guests that `nvx.py` runs, which [`Guest`] +//! describes, in a repository checkout or an extracted release: +//! +//! | Artifact | Repository layout | Release layout | +//! | --- | --- | --- | +//! | OpenVMM | `openvmm/target/release/openvmm[.exe]` | `bin/openvmm[.exe]` | +//! | Kernel | `build/vmlinux` | `guest/vmlinux` | +//! | Alpine initramfs | `build/initramfs.cpio.gz` | `guest/initramfs.cpio.gz` | +//! | Ubuntu initramfs | `build/initramfs-ubuntu.cpio.gz` | `guest/initramfs-ubuntu.cpio.gz` | +//! | Azure Linux initramfs | `build/initramfs-azurelinux.cpio.gz` | `guest/initramfs-azurelinux.cpio.gz` | +//! | Ubuntu distro layer | `build/ubuntu-distro.erofs` | `guest/ubuntu-distro.erofs` | +//! +//! Each initramfs has a package manifest beside it, `NAME.packages.json`, and the distro layer a +//! manifest, `NAME.manifest.json`. A release package holds the guest artifacts that its +//! `SOURCE-MANIFEST.json` declares, and `nvx.py download` installs them into the repository +//! layout of a checkout, with the release's OpenVMM executable. The paths and messages are those +//! of `artifact_path`, `openvmm_binary_path`, and `require_file` in +//! `scripts/nvx_tools/common.py`, so the `nvx.py` commands can look their artifacts up here. use std::env; use std::ffi::OsString; +use std::fmt; use std::fs; use std::path::{Path, PathBuf}; +use serde::Serialize; use serde_json::Value; use crate::contract; use crate::error::{Error, Result}; use crate::host::platform; +use crate::host::python::builtins::repr; +use crate::host::sandbox::require_file; /// Name of the release manifest at the root of a release archive or repository checkout. pub const MANIFEST_NAME: &str = "SOURCE-MANIFEST.json"; @@ -123,12 +146,7 @@ impl Artifacts { /// other directory is a repository checkout. pub fn from_dir(directory: impl AsRef) -> Result { let directory = directory.as_ref(); - let release_artifacts = [ - directory.join("bin").join(openvmm_executable()), - directory.join(RELEASE_KERNEL[0]).join(RELEASE_KERNEL[1]), - directory.join(RELEASE_INITRD[0]).join(RELEASE_INITRD[1]), - ]; - if release_artifacts.iter().any(|path| path.is_file()) { + if is_release_dir(directory) { Self::from_release_dir(directory) } else { Self::from_repo_layout(directory) @@ -235,6 +253,18 @@ fn discover_with( ))) } +/// Returns whether `directory` is in the release layout: whether it contains `bin/openvmm[.exe]`, +/// `guest/vmlinux`, or `guest/initramfs.cpio.gz`. +fn is_release_dir(directory: &Path) -> bool { + [ + directory.join("bin").join(openvmm_executable()), + directory.join(RELEASE_KERNEL[0]).join(RELEASE_KERNEL[1]), + directory.join(RELEASE_INITRD[0]).join(RELEASE_INITRD[1]), + ] + .iter() + .any(|path| path.is_file()) +} + /// Returns the file name of the OpenVMM executable on this host. pub fn openvmm_executable() -> &'static str { if cfg!(windows) { @@ -282,6 +312,343 @@ pub fn check_manifest(path: &Path) -> Result<()> { } } +/// File name of the guest kernel in both layouts. +pub const KERNEL_NAME: &str = "vmlinux"; +/// Suffix of the package manifest that lists the packages of an initramfs. +pub const PACKAGE_MANIFEST_SUFFIX: &str = ".packages.json"; +/// Suffix of the manifest of a distro layer, which binds the layer's digest and packages. +pub const DISTRO_MANIFEST_SUFFIX: &str = ".manifest.json"; +/// Repository-layout path of the OpenVMM build output directory. +const REPO_OPENVMM_DIR: [&str; 3] = ["openvmm", "target", "release"]; +/// Release-layout directory of the OpenVMM executable. +const RELEASE_BIN_DIR: &str = "bin"; +/// Release-layout directory of the guest artifacts. +const RELEASE_GUEST_DIR: &str = "guest"; + +/// A Linux guest that `nvx.py` builds and runs, as `scripts/nvx_tools/guests.py` describes it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Guest { + /// Alpine Linux: the default guest of `nvx.py run`, and the one that runs sandboxes. + Alpine, + /// Ubuntu Base, which also provides the distro layer of sandbox workloads. + Ubuntu, + /// Azure Linux. + AzureLinux, +} + +impl Guest { + /// Every guest, in the order in which `nvx.py` lists them. + pub const ALL: [Self; 3] = [Self::Alpine, Self::Ubuntu, Self::AzureLinux]; + + /// Returns the guest's name in `nvx.py` options: `alpine`, `ubuntu`, or `azurelinux`. + pub const fn name(self) -> &'static str { + match self { + Self::Alpine => "alpine", + Self::Ubuntu => "ubuntu", + Self::AzureLinux => "azurelinux", + } + } + + /// Returns the guest named `name`. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) and the message of + /// `guest_descriptor` in `nvx.py`: `unsupported guest 'NAME'; choose alpine, ubuntu, + /// azurelinux`. + pub fn from_name(name: &str) -> Result { + Self::ALL + .into_iter() + .find(|guest| guest.name() == name) + .ok_or_else(|| { + let names: Vec<&str> = Self::ALL.iter().map(|guest| guest.name()).collect(); + Error::unsupported(format!( + "unsupported guest {}; choose {}", + repr(name), + names.join(", ") + )) + }) + } + + /// Returns the name of the guest's distribution, as messages show it: `Alpine Linux`, + /// `Ubuntu Base`, or `Azure Linux`. + pub const fn distribution(self) -> &'static str { + match self { + Self::Alpine => "Alpine Linux", + Self::Ubuntu => "Ubuntu Base", + Self::AzureLinux => "Azure Linux", + } + } + + /// Returns the file name of the guest's initramfs. + pub const fn initramfs_name(self) -> &'static str { + match self { + Self::Alpine => "initramfs.cpio.gz", + Self::Ubuntu => "initramfs-ubuntu.cpio.gz", + Self::AzureLinux => "initramfs-azurelinux.cpio.gz", + } + } + + /// Returns the file name of the package manifest of the guest's initramfs. + pub fn package_manifest_name(self) -> String { + format!("{}{PACKAGE_MANIFEST_SUFFIX}", self.initramfs_name()) + } + + /// Returns the file name of the guest's EROFS distro layer, `GUEST-distro.erofs`, if the + /// guest provides one: only Ubuntu does. + pub const fn distro_layer_name(self) -> Option<&'static str> { + match self { + Self::Ubuntu => Some("ubuntu-distro.erofs"), + Self::Alpine | Self::AzureLinux => None, + } + } + + /// Returns the file name of the manifest of the guest's distro layer, if it has one. + pub fn distro_manifest_name(self) -> Option { + self.distro_layer_name() + .map(|name| format!("{name}{DISTRO_MANIFEST_SUFFIX}")) + } +} + +impl fmt::Display for Guest { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.name()) + } +} + +/// The layout of a directory of NVX artifacts. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Layout { + /// A repository checkout: the guest artifacts in `build/`, and OpenVMM in + /// `openvmm/target/release/`, where a local build writes them and where `nvx.py download` + /// installs a release. + Repository, + /// An extracted release package: the guest artifacts in `guest/`, and OpenVMM in `bin/`. + Release, +} + +/// A directory of the artifacts that `nvx.py` runs: a repository checkout or an extracted +/// release, with the paths that `artifact_path` and `openvmm_binary_path` of +/// `scripts/nvx_tools/common.py` give for a checkout. +/// +/// Paths are joined to the directory, made absolute, without checking that they exist; the +/// `require_*` methods check them with the messages of `nvx.py`. Unlike [`Artifacts`], nothing +/// here reads the release's `SOURCE-MANIFEST.json`, which `nvx.py` does not check either. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArtifactDir { + /// The absolute directory. + root: PathBuf, + /// Its layout. + layout: Layout, +} + +impl ArtifactDir { + /// Uses a repository checkout. + /// + /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) if a relative + /// `root` meets an unreadable current directory. + pub fn repository(root: impl AsRef) -> Result { + Self::new(root.as_ref(), Layout::Repository) + } + + /// Uses an extracted release package. + /// + /// Fails as [`repository`](Self::repository) does. + pub fn release(root: impl AsRef) -> Result { + Self::new(root.as_ref(), Layout::Release) + } + + /// Uses a directory in the layout that its built artifacts reveal, as + /// [`Artifacts::from_dir`] tells them apart: a directory that contains `bin/openvmm[.exe]`, + /// `guest/vmlinux`, or `guest/initramfs.cpio.gz` is a release, and any other directory is a + /// repository checkout. + /// + /// Fails as [`repository`](Self::repository) does. + pub fn from_dir(directory: impl AsRef) -> Result { + let directory = directory.as_ref(); + let layout = if is_release_dir(directory) { + Layout::Release + } else { + Layout::Repository + }; + Self::new(directory, layout) + } + + /// Returns the directory of `layout` at `root`, made absolute. + fn new(root: &Path, layout: Layout) -> Result { + Ok(Self { + root: absolute(root)?, + layout, + }) + } + + /// Returns the absolute directory. + pub fn root(&self) -> &Path { + &self.root + } + + /// Returns the directory's layout. + pub fn layout(&self) -> Layout { + self.layout + } + + /// Returns the path of the guest artifact `name`, `artifact_path(name)` of `nvx.py`: + /// `build/NAME` in a checkout, and `guest/NAME` in a release. + pub fn artifact(&self, name: &str) -> PathBuf { + let directory = match self.layout { + Layout::Repository => REPO_BUILD_DIR, + Layout::Release => RELEASE_GUEST_DIR, + }; + self.root.join(directory).join(name) + } + + /// Returns the path of the OpenVMM executable, `openvmm_binary_path()` of `nvx.py`: + /// `openvmm/target/release/openvmm[.exe]` in a checkout, and `bin/openvmm[.exe]` in a + /// release. + pub fn openvmm(&self) -> PathBuf { + let directory = match self.layout { + Layout::Repository => REPO_OPENVMM_DIR.iter().collect(), + Layout::Release => PathBuf::from(RELEASE_BIN_DIR), + }; + self.root.join(directory).join(openvmm_executable()) + } + + /// Returns the path of the guest kernel, which every guest boots. + pub fn kernel(&self) -> PathBuf { + self.artifact(KERNEL_NAME) + } + + /// Returns the path of a guest's initramfs. + pub fn initramfs(&self, guest: Guest) -> PathBuf { + self.artifact(guest.initramfs_name()) + } + + /// Returns the path of the package manifest of a guest's initramfs. + pub fn package_manifest(&self, guest: Guest) -> PathBuf { + self.artifact(&guest.package_manifest_name()) + } + + /// Returns the path of a guest's distro layer, if the guest provides one. + pub fn distro_layer(&self, guest: Guest) -> Option { + guest.distro_layer_name().map(|name| self.artifact(name)) + } + + /// Returns the path of the manifest of a guest's distro layer, if the guest provides one. + pub fn distro_manifest(&self, guest: Guest) -> Option { + guest + .distro_manifest_name() + .map(|name| self.artifact(&name)) + } + + /// Checks that the OpenVMM executable is a regular file and returns its path. + /// + /// Fails as [`require_file`] does, with the message `OpenVMM release binary not found: + /// PATH`. + pub fn require_openvmm(&self) -> Result { + required(self.openvmm(), "OpenVMM release binary") + } + + /// Checks that the guest kernel is a regular file and returns its path. + /// + /// Fails as [`require_file`] does, with the message `Linux direct kernel not found: PATH`. + pub fn require_kernel(&self) -> Result { + required(self.kernel(), "Linux direct kernel") + } + + /// Checks that a guest's initramfs is a regular file and returns its path, as `nvx.py run` + /// does before it boots the guest. + /// + /// Fails as [`require_file`] does, with the message `DISTRIBUTION initramfs not found: + /// PATH`, such as `Ubuntu Base initramfs not found: PATH`. + pub fn require_initramfs(&self, guest: Guest) -> Result { + required( + self.initramfs(guest), + &format!("{} initramfs", guest.distribution()), + ) + } + + /// Returns the artifacts of a sandbox, as `nvx.py sandbox run` and `nvx.py sandbox start` + /// require them: the OpenVMM executable, the kernel, and the Alpine initramfs, in that order. + /// + /// Fails as [`require_file`] does for the first one that is not a regular file, with the + /// messages of [`require_openvmm`](Self::require_openvmm) and + /// [`require_kernel`](Self::require_kernel), or `initramfs not found: PATH`. + pub fn sandbox_artifacts(&self) -> Result { + let openvmm = self.require_openvmm()?; + let kernel = self.require_kernel()?; + let initrd = required(self.initramfs(Guest::Alpine), "initramfs")?; + Ok(Artifacts::new(openvmm, kernel, initrd)) + } + + /// Returns the paths of every artifact, for a caller to show or record. + pub fn paths(&self) -> ArtifactPaths { + ArtifactPaths { + layout: self.layout, + root: self.root.clone(), + openvmm: self.openvmm(), + kernel: self.kernel(), + guests: Guest::ALL + .iter() + .map(|&guest| GuestPaths { + name: guest.name(), + distribution: guest.distribution(), + initramfs: self.initramfs(guest), + package_manifest: self.package_manifest(guest), + distro_layer: self.distro_layer(guest), + distro_manifest: self.distro_manifest(guest), + }) + .collect(), + } + } +} + +/// Checks that `path` is a regular file, as `require_file` of `nvx.py` does, and returns it. +fn required(path: PathBuf, description: &str) -> Result { + require_file(&path, description)?; + Ok(path) +} + +/// The paths of the artifacts of an [`ArtifactDir`], which serialize as a JSON object such as: +/// +/// ```json +/// {"layout": "repository", "root": "/nvx", "openvmm": "/nvx/openvmm/target/release/openvmm", +/// "kernel": "/nvx/build/vmlinux", "guests": [{"name": "alpine", +/// "distribution": "Alpine Linux", "initramfs": "/nvx/build/initramfs.cpio.gz", +/// "package_manifest": "/nvx/build/initramfs.cpio.gz.packages.json", "distro_layer": null, +/// "distro_manifest": null}, ...]} +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[non_exhaustive] +pub struct ArtifactPaths { + /// The directory's layout. + pub layout: Layout, + /// The absolute directory. + pub root: PathBuf, + /// The OpenVMM executable. + pub openvmm: PathBuf, + /// The guest kernel. + pub kernel: PathBuf, + /// The artifacts of each guest, in the order of [`Guest::ALL`]. + pub guests: Vec, +} + +/// The paths of the artifacts of one guest. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[non_exhaustive] +pub struct GuestPaths { + /// The guest's name, [`Guest::name`]. + pub name: &'static str, + /// The guest's distribution, [`Guest::distribution`]. + pub distribution: &'static str, + /// The initramfs. + pub initramfs: PathBuf, + /// The package manifest of the initramfs. + pub package_manifest: PathBuf, + /// The distro layer, if the guest provides one. + pub distro_layer: Option, + /// The manifest of the distro layer, if the guest provides one. + pub distro_manifest: Option, +} + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -393,6 +760,9 @@ mod tests { let artifacts = Artifacts::from_dir(root).unwrap(); let kernel = std::path::absolute(root).unwrap().join("build"); assert_eq!(artifacts.kernel, kernel.join("vmlinux")); + let directory = ArtifactDir::from_dir(root).unwrap(); + assert_eq!(directory.layout(), Layout::Repository); + assert_eq!(directory.kernel(), artifacts.kernel); } } @@ -494,6 +864,237 @@ mod tests { } } + #[test] + fn guests_name_their_artifacts_as_nvx_py_does() { + assert_eq!( + Guest::ALL.map(Guest::name), + ["alpine", "ubuntu", "azurelinux"] + ); + for guest in Guest::ALL { + assert_eq!(Guest::from_name(guest.name()).unwrap(), guest); + assert_eq!(guest.to_string(), guest.name()); + } + let error = Guest::from_name("Ubuntu").unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert_eq!( + error.message(), + "unsupported guest 'Ubuntu'; choose alpine, ubuntu, azurelinux" + ); + assert_eq!( + Guest::ALL.map(Guest::distribution), + ["Alpine Linux", "Ubuntu Base", "Azure Linux"] + ); + assert_eq!( + Guest::ALL.map(Guest::package_manifest_name), + [ + "initramfs.cpio.gz.packages.json", + "initramfs-ubuntu.cpio.gz.packages.json", + "initramfs-azurelinux.cpio.gz.packages.json", + ] + ); + assert_eq!( + Guest::ALL.map(Guest::distro_layer_name), + [None, Some("ubuntu-distro.erofs"), None] + ); + assert_eq!( + Guest::Ubuntu.distro_manifest_name().as_deref(), + Some("ubuntu-distro.erofs.manifest.json") + ); + assert_eq!(Guest::Alpine.distro_manifest_name(), None); + } + + #[test] + fn a_checkout_holds_the_guest_artifacts_in_build() { + let directory = tempfile::tempdir().unwrap(); + checkout(directory.path()); + let artifacts = ArtifactDir::from_dir(directory.path()).unwrap(); + let root = std::path::absolute(directory.path()).unwrap(); + let build = root.join("build"); + assert_eq!(artifacts.layout(), Layout::Repository); + assert_eq!(artifacts.root(), root); + assert_eq!( + artifacts.openvmm(), + root.join("openvmm") + .join("target") + .join("release") + .join(openvmm_executable()) + ); + assert_eq!(artifacts.kernel(), build.join("vmlinux")); + assert_eq!( + Guest::ALL.map(|guest| artifacts.initramfs(guest)), + [ + build.join("initramfs.cpio.gz"), + build.join("initramfs-ubuntu.cpio.gz"), + build.join("initramfs-azurelinux.cpio.gz"), + ] + ); + assert_eq!( + artifacts.package_manifest(Guest::Ubuntu), + build.join("initramfs-ubuntu.cpio.gz.packages.json") + ); + assert_eq!( + Guest::ALL.map(|guest| artifacts.distro_layer(guest)), + [None, Some(build.join("ubuntu-distro.erofs")), None] + ); + assert_eq!( + artifacts.distro_manifest(Guest::Ubuntu), + Some(build.join("ubuntu-distro.erofs.manifest.json")) + ); + assert_eq!( + artifacts.artifact("vmlinux.provenance.json"), + build.join("vmlinux.provenance.json") + ); + + // A relative directory is made absolute, and an explicit layout overrides detection. + let relative = ArtifactDir::repository(".").unwrap(); + assert_eq!(relative.root(), std::path::absolute(".").unwrap()); + release(directory.path()); + assert_eq!( + ArtifactDir::repository(directory.path()).unwrap().layout(), + Layout::Repository + ); + } + + #[test] + fn a_release_holds_the_guest_artifacts_in_guest() { + let directory = tempfile::tempdir().unwrap(); + release(directory.path()); + let artifacts = ArtifactDir::from_dir(directory.path()).unwrap(); + let root = std::path::absolute(directory.path()).unwrap(); + let guest = root.join("guest"); + assert_eq!(artifacts.layout(), Layout::Release); + assert_eq!(artifacts, ArtifactDir::release(directory.path()).unwrap()); + assert_eq!( + artifacts.openvmm(), + root.join("bin").join(openvmm_executable()) + ); + assert_eq!(artifacts.kernel(), guest.join("vmlinux")); + assert_eq!( + Guest::ALL.map(|guest| artifacts.initramfs(guest)), + [ + guest.join("initramfs.cpio.gz"), + guest.join("initramfs-ubuntu.cpio.gz"), + guest.join("initramfs-azurelinux.cpio.gz"), + ] + ); + assert_eq!( + artifacts.distro_layer(Guest::Ubuntu), + Some(guest.join("ubuntu-distro.erofs")) + ); + + // The release's artifacts are the ones that discovery finds for its directory. + let discovered = Artifacts::from_dir(directory.path()).unwrap(); + assert_eq!(artifacts.sandbox_artifacts().unwrap(), discovered); + } + + #[test] + fn guest_artifacts_are_required_in_the_order_and_words_of_nvx_py() { + let directory = tempfile::tempdir().unwrap(); + checkout(directory.path()); + let artifacts = ArtifactDir::repository(directory.path()).unwrap(); + fn missing(result: Result, description: &str, path: PathBuf) { + let error = result.unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unavailable); + assert_eq!( + error.message(), + format!("{description} not found: {}", path.display()) + ); + } + let create = |path: PathBuf| { + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, b"artifact").unwrap(); + }; + missing( + artifacts.sandbox_artifacts(), + "OpenVMM release binary", + artifacts.openvmm(), + ); + create(artifacts.openvmm()); + missing( + artifacts.sandbox_artifacts(), + "Linux direct kernel", + artifacts.kernel(), + ); + create(artifacts.kernel()); + missing( + artifacts.sandbox_artifacts(), + "initramfs", + artifacts.initramfs(Guest::Alpine), + ); + create(artifacts.initramfs(Guest::Alpine)); + assert_eq!( + artifacts.sandbox_artifacts().unwrap(), + Artifacts::new( + artifacts.openvmm(), + artifacts.kernel(), + artifacts.initramfs(Guest::Alpine) + ) + ); + + assert_eq!(artifacts.require_openvmm().unwrap(), artifacts.openvmm()); + assert_eq!(artifacts.require_kernel().unwrap(), artifacts.kernel()); + missing( + artifacts.require_initramfs(Guest::Ubuntu), + "Ubuntu Base initramfs", + artifacts.initramfs(Guest::Ubuntu), + ); + missing( + artifacts.require_initramfs(Guest::AzureLinux), + "Azure Linux initramfs", + artifacts.initramfs(Guest::AzureLinux), + ); + create(artifacts.initramfs(Guest::Ubuntu)); + assert_eq!( + artifacts.require_initramfs(Guest::Ubuntu).unwrap(), + artifacts.initramfs(Guest::Ubuntu) + ); + + // A directory is not a regular file. + fs::create_dir_all(artifacts.initramfs(Guest::AzureLinux)).unwrap(); + missing( + artifacts.require_initramfs(Guest::AzureLinux), + "Azure Linux initramfs", + artifacts.initramfs(Guest::AzureLinux), + ); + } + + #[test] + fn artifact_paths_serialize_every_guest_in_order() { + let directory = tempfile::tempdir().unwrap(); + release(directory.path()); + let artifacts = ArtifactDir::from_dir(directory.path()).unwrap(); + let text = |path: PathBuf| Value::from(path.to_str().unwrap()); + let value = serde_json::to_value(artifacts.paths()).unwrap(); + assert_eq!(value["layout"], "release"); + assert_eq!(value["root"], text(artifacts.root().to_path_buf())); + assert_eq!(value["openvmm"], text(artifacts.openvmm())); + assert_eq!(value["kernel"], text(artifacts.kernel())); + let guests = value["guests"].as_array().unwrap(); + assert_eq!(guests.len(), Guest::ALL.len()); + for (paths, guest) in guests.iter().zip(Guest::ALL) { + assert_eq!(paths["name"], guest.name()); + assert_eq!(paths["distribution"], guest.distribution()); + assert_eq!(paths["initramfs"], text(artifacts.initramfs(guest))); + assert_eq!( + paths["package_manifest"], + text(artifacts.package_manifest(guest)) + ); + assert_eq!( + paths["distro_layer"], + artifacts.distro_layer(guest).map_or(Value::Null, text) + ); + assert_eq!( + paths["distro_manifest"], + artifacts.distro_manifest(guest).map_or(Value::Null, text) + ); + } + let repository = ArtifactDir::repository(directory.path()).unwrap(); + assert_eq!( + serde_json::to_value(repository.paths()).unwrap()["layout"], + "repository" + ); + } + #[cfg(feature = "bundled")] #[test] fn the_bundle_is_a_compatible_release() { diff --git a/nvx-rs/src/host/cli/args.rs b/nvx-rs/src/host/cli/args.rs new file mode 100644 index 000000000..d074d9d4c --- /dev/null +++ b/nvx-rs/src/host/cli/args.rs @@ -0,0 +1,828 @@ +//! A command-line parser with the semantics of Python's `argparse` for the parsers of `nvx.py` +//! that `nvx-rs` replaces: long options that take one value or none, and positionals that take +//! exactly one value or at most one. +//! +//! It follows `ArgumentParser.parse_args` step by step: +//! +//! - Each argument is classified as `argparse` classifies it: the first `--` ends the options, +//! and every argument after it is a positional; a known option, or one with `=VALUE` attached, +//! is an option; an argument that does not start with `-`, a lone `-`, an argument that starts +//! like a negative number, such as `-1` or `-.5`, and one that contains a space are +//! positionals; and any other argument that starts with `-` is an unknown option. +//! - Options and positionals are consumed in `argparse`'s alternation: the positionals before +//! each option, then the option with its value, which must be the next argument and a +//! positional by that classification, or the value after `=`. A value that starts with `-` +//! therefore needs the `--option=VALUE` form. A positional takes the `--` next to its value, +//! and an optional positional that would take nothing before an option waits for the +//! arguments after it. +//! - Each value is handed to the caller as it is consumed, so conversion and choice errors +//! surface in command-line order, as `argument NAME: MESSAGE`, and end the parse. +//! - Unknown options and positionals beyond those that the parser takes are returned for the +//! caller to report last, as `unrecognized arguments: ...`, since `argparse` reports them from +//! the top-level parser. +//! - `-h` and `--help` stop the parse for the help as soon as they are consumed. +//! +//! Unlike `argparse`, it does not expand unambiguous prefixes of option names: `nvx.py` passes +//! canonical options, so `--dry` is an unknown option rather than `--dry-run`. +//! +//! [`Syntax`] holds what a parser's usage and help show, and formats its usage for any program +//! name as `argparse` wraps it for 80 columns. + +use crate::host::python::builtins::repr; + +/// The width of usage and help: 80 columns, less the margin of 2 that `argparse` keeps, as for a +/// pipe, whose `shutil.get_terminal_size()` is 80 columns. +const WIDTH: usize = 78; +/// What usage messages start with. +const USAGE_PREFIX: &str = "usage: "; + +/// What the usage and help of a parser show. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Syntax { + /// The command of the parser after the program's name, such as `sandbox`, or nothing for the + /// top-level parser. + pub(crate) command: &'static str, + /// The usage parts of the options, such as `[-h]` and `[--root PATH]`, in order. + pub(crate) optionals: &'static [&'static str], + /// The usage parts of the positionals, such as `PATH`, in order. + pub(crate) positionals: &'static [&'static str], + /// The help after the usage and its blank line: the description and the sections of the + /// arguments, without a final line end. + pub(crate) help: &'static str, +} + +impl Syntax { + /// Returns the parser's name for the program `program`, such as `nvx-rs sandbox`: what + /// `argparse` calls the `prog` of a subparser. + pub(crate) fn prog(&self, program: &str) -> String { + if self.command.is_empty() { + program.to_owned() + } else { + format!("{program} {}", self.command) + } + } + + /// Returns the usage for the program `program`, `usage: PROG ...`, without a line end. + pub(crate) fn usage(&self, program: &str) -> String { + format_usage(&self.prog(program), self.optionals, self.positionals) + } + + /// Returns the help for the program `program`: the usage, a blank line, and the rest. + pub(crate) fn help(&self, program: &str) -> String { + format!("{}\n\n{}", self.usage(program), self.help) + } +} + +/// Returns the usage of the parser `prog` with the usage parts `optionals` and `positionals`, as +/// `HelpFormatter._format_usage` of CPython 3.14 formats it for [`WIDTH`]: on one line if it +/// fits, and otherwise wrapped at the parts, after the name if the name is short and on the lines +/// after it if not. +fn format_usage(prog: &str, optionals: &[&str], positionals: &[&str]) -> String { + let length = |text: &str| text.chars().count(); + let parts: Vec<&str> = optionals.iter().chain(positionals).copied().collect(); + let full = std::iter::once(prog) + .chain(parts.iter().copied()) + .collect::>() + .join(" "); + if USAGE_PREFIX.len() + length(&full) <= WIDTH { + return format!("{USAGE_PREFIX}{full}"); + } + // Wraps `parts` into lines after `indent`, the first of which continues the prefix if given. + let lines_of = |parts: &[&str], indent: &str, prefix: Option<&str>| -> Vec { + let mut lines = Vec::new(); + let mut line: Vec<&str> = Vec::new(); + let indent_length = length(indent); + let mut line_length = prefix.map_or(indent_length, length) - 1; + for part in parts { + if line_length + 1 + length(part) > WIDTH && !line.is_empty() { + lines.push(format!("{indent}{}", line.join(" "))); + line.clear(); + line_length = indent_length - 1; + } + line.push(part); + line_length += length(part) + 1; + } + if !line.is_empty() { + lines.push(format!("{indent}{}", line.join(" "))); + } + if prefix.is_some() + && let Some(first) = lines.first_mut() + { + *first = first[indent.len()..].to_owned(); + } + lines + }; + let prog_length = length(prog); + // A name of at most three quarters of the width is followed by the parts. + let lines = if 4 * (USAGE_PREFIX.len() + prog_length) <= 3 * WIDTH { + let indent = " ".repeat(USAGE_PREFIX.len() + prog_length + 1); + if optionals.is_empty() { + let leading: Vec<&str> = std::iter::once(prog) + .chain(positionals.iter().copied()) + .collect(); + lines_of(&leading, &indent, Some(USAGE_PREFIX)) + } else { + let leading: Vec<&str> = std::iter::once(prog) + .chain(optionals.iter().copied()) + .collect(); + let mut lines = lines_of(&leading, &indent, Some(USAGE_PREFIX)); + lines.extend(lines_of(positionals, &indent, None)); + lines + } + } else { + let indent = " ".repeat(USAGE_PREFIX.len()); + let mut lines = lines_of(&parts, &indent, None); + if lines.len() > 1 { + lines = lines_of(optionals, &indent, None); + lines.extend(lines_of(positionals, &indent, None)); + } + lines.insert(0, prog.to_owned()); + lines + }; + format!("{USAGE_PREFIX}{}", lines.join("\n")) +} + +/// How a parse ends early: with a usage error, or with the help that `-h` asked for. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum Stop { + /// `PROG: error: MESSAGE` after the parser's usage, with status 2. + Usage(String), + /// The help text, on standard output, with status 0. + Help, +} + +/// How many values an option or positional takes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Arity { + /// No value: a flag, such as `store_true`. + Flag, + /// Exactly one value. + One, + /// At most one value: an optional positional, `nargs='?'`. + Optional, +} + +/// An argument that the parser recognizes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Spec { + /// The option string, such as `--layer`, or the name with which messages refer to a + /// positional, such as `sandbox_operation`. + pub(crate) name: &'static str, + /// How many values it takes. + pub(crate) arity: Arity, +} + +impl Spec { + /// An option that takes no value. + pub(crate) const fn flag(name: &'static str) -> Self { + Self { + name, + arity: Arity::Flag, + } + } + + /// An option or positional that takes exactly one value. + pub(crate) const fn one(name: &'static str) -> Self { + Self { + name, + arity: Arity::One, + } + } + + /// A positional that takes at most one value. + pub(crate) const fn optional(name: &'static str) -> Self { + Self { + name, + arity: Arity::Optional, + } + } +} + +/// Receives each argument as the parser consumes it, in `argparse`'s order. +pub(crate) trait Receiver { + /// Takes `value` for the option or positional `name`: `None` for a flag, and for an optional + /// positional that received no value, which then takes its default. An error message ends + /// the parse as `argument NAME: MESSAGE`. + fn take(&mut self, name: &'static str, value: Option<&str>) -> Result<(), String>; +} + +impl) -> Result<(), String>> Receiver for F { + fn take(&mut self, name: &'static str, value: Option<&str>) -> Result<(), String> { + self(name, value) + } +} + +/// The classification of one argument. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Kind { + /// An option that the parser knows, with the byte offset of the value attached after `=`, + /// if any. + Option(usize, Option), + /// An option-like argument that the parser does not know. + Unknown, + /// A positional value. + Positional, + /// The `--` that ends the options. + End, +} + +/// A parser of options and positionals. +pub(crate) struct Parser { + /// The options, such as `--layer`, and `-h` and `--help`, which every parser has. + options: Vec, + /// The positionals, in order. + positionals: Vec, +} + +/// What an argument is to a parser, as [`Parser::token`] classifies it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Token<'a> { + /// A known option, with the value attached after `=`, if any. + Option(&'static str, Option<&'a str>), + /// An option-like argument that the parser does not know. + Unknown, + /// A positional value. + Positional, + /// The `--` that ends the options. + End, +} + +impl Parser { + /// Returns the parser of `options` and `positionals`, which also knows `-h` and `--help`. + pub(crate) fn new(options: &[Spec], positionals: &[Spec]) -> Self { + let mut all = vec![Spec::flag("-h"), Spec::flag("--help")]; + all.extend_from_slice(options); + Self { + options: all, + positionals: positionals.to_vec(), + } + } + + /// Classifies one argument before the options end, for a caller that walks the arguments of + /// a parser whose last positional takes all the arguments after it, such as a command. + pub(crate) fn token<'a>(&self, argument: &'a str) -> Token<'a> { + if argument == "--" { + return Token::End; + } + match self.classify(argument) { + Kind::Option(option, attached) => Token::Option( + self.options[option].name, + attached.map(|offset| &argument[offset..]), + ), + Kind::Unknown => Token::Unknown, + Kind::Positional | Kind::End => Token::Positional, + } + } + + /// Returns the option whose string is `name`. + fn option(&self, name: &str) -> Option { + self.options.iter().position(|spec| spec.name == name) + } + + /// Classifies `argument` as `ArgumentParser._parse_optional` does, without abbreviations. + fn classify(&self, argument: &str) -> Kind { + if !argument.starts_with('-') { + return Kind::Positional; + } + if let Some(option) = self.option(argument) { + return Kind::Option(option, None); + } + if argument.len() == 1 { + return Kind::Positional; + } + if let Some((name, _)) = argument.split_once('=') + && let Some(option) = self.option(name) + { + return Kind::Option(option, Some(name.len() + 1)); + } + if looks_like_negative_number(argument) || argument.contains(' ') { + return Kind::Positional; + } + Kind::Unknown + } + + /// Walks `arguments` up to the command that the parser's last positional names, as + /// `argparse` walks the arguments of a parser with subparsers: the options before the + /// command go to `receiver`, unknown ones are collected, and the arguments from the command + /// on are the command's. Returns the index of the command, which `--` may precede, and the + /// unknown arguments. + pub(crate) fn split_command( + &self, + arguments: &[String], + receiver: &mut dyn Receiver, + ) -> Result<(usize, Vec), Stop> { + let command = self.positionals.last().map_or("command", |spec| spec.name); + let missing = || Stop::Usage(format!("the following arguments are required: {command}")); + let mut extras = Vec::new(); + let mut index = 0; + loop { + let argument = arguments.get(index).ok_or_else(missing)?; + match self.token(argument) { + Token::End => { + return if index + 1 < arguments.len() { + Ok((index + 1, extras)) + } else { + Err(missing()) + }; + } + Token::Positional => return Ok((index, extras)), + Token::Unknown => { + extras.push(argument.clone()); + index += 1; + } + Token::Option(name, attached) => { + let spec = self.options[self.option(name).unwrap_or_default()]; + let help = name == "-h" || name == "--help"; + let label = if help { "-h/--help" } else { name }; + let take = |receiver: &mut dyn Receiver, value: Option<&str>| { + receiver + .take(name, value) + .map_err(|message| Stop::Usage(format!("argument {label}: {message}"))) + }; + match (spec.arity, attached) { + (Arity::Flag, Some(value)) => { + return Err(Stop::Usage(format!( + "argument {label}: ignored explicit argument {}", + repr(value) + ))); + } + (Arity::Flag, None) if help => return Err(Stop::Help), + (Arity::Flag, None) => { + take(receiver, None)?; + index += 1; + } + (_, Some(value)) => { + take(receiver, Some(value))?; + index += 1; + } + (_, None) => match arguments.get(index + 1) { + Some(value) if self.token(value) == Token::Positional => { + take(receiver, Some(value))?; + index += 2; + } + _ => { + return Err(Stop::Usage(format!( + "argument {label}: expected one argument" + ))); + } + }, + } + } + } + } + } + + /// Parses `arguments`, handing each value to `receiver`, and returns the arguments that the + /// parser does not recognize, for the caller to report. + pub(crate) fn parse( + &self, + arguments: &[String], + receiver: &mut dyn Receiver, + ) -> Result, Stop> { + let mut kinds = Vec::with_capacity(arguments.len()); + let mut ended = false; + for argument in arguments { + kinds.push(if ended { + Kind::Positional + } else if argument == "--" { + ended = true; + Kind::End + } else { + self.classify(argument) + }); + } + let mut state = State { + parser: self, + arguments, + kinds: &kinds, + positionals: 0, + extras: Vec::new(), + receiver, + }; + let options: Vec = (0..kinds.len()) + .filter(|&index| matches!(kinds[index], Kind::Option(..) | Kind::Unknown)) + .collect(); + let mut start = 0; + while let Some(&next) = options.iter().find(|&&index| index >= start) { + if start != next { + let end = state.consume_positionals(start)?; + if end > start { + start = end; + continue; + } + state.extras.extend_from_slice(&arguments[start..next]); + start = next; + } + start = state.consume_option(start)?; + } + let stop = state.consume_positionals(start)?; + state.extras.extend_from_slice(&arguments[stop..]); + let missing: Vec<&str> = self.positionals[state.positionals..] + .iter() + .filter(|spec| spec.arity == Arity::One) + .map(|spec| spec.name) + .collect(); + if !missing.is_empty() { + return Err(Stop::Usage(format!( + "the following arguments are required: {}", + missing.join(", ") + ))); + } + Ok(state.extras) + } +} + +/// Returns whether `argument` starts like a negative number, as `argparse`'s +/// `_negative_number_matcher`, `-\.?\d`, finds it. +fn looks_like_negative_number(argument: &str) -> bool { + let rest = &argument.as_bytes()[1..]; + let rest = rest.strip_prefix(b".").unwrap_or(rest); + rest.first().is_some_and(u8::is_ascii_digit) +} + +/// The progress of one parse. +struct State<'a> { + /// The parser. + parser: &'a Parser, + /// The arguments. + arguments: &'a [String], + /// Their classifications. + kinds: &'a [Kind], + /// How many positionals have been consumed. + positionals: usize, + /// The arguments that the parser does not recognize. + extras: Vec, + /// The receiver of the values. + receiver: &'a mut dyn Receiver, +} + +impl State<'_> { + /// Hands `value` to the receiver, turning its error into a usage error for `name`. + fn take(&mut self, name: &'static str, value: Option<&str>) -> Result<(), Stop> { + self.receiver + .take(name, value) + .map_err(|message| Stop::Usage(format!("argument {name}: {message}"))) + } + + /// Consumes the option at `start` and its value, as `consume_optional` does, and returns the + /// index after them. + fn consume_option(&mut self, start: usize) -> Result { + let Kind::Option(option, attached) = self.kinds[start] else { + self.extras.push(self.arguments[start].clone()); + return Ok(start + 1); + }; + let spec = self.parser.options[option]; + let attached = attached.map(|offset| &self.arguments[start][offset..]); + let help = spec.name == "-h" || spec.name == "--help"; + let name = if help { "-h/--help" } else { spec.name }; + match (spec.arity, attached) { + (Arity::Flag, Some(value)) => Err(Stop::Usage(format!( + "argument {name}: ignored explicit argument {}", + repr(value) + ))), + (Arity::Flag, None) if help => Err(Stop::Help), + (Arity::Flag, None) => { + self.take(spec.name, None)?; + Ok(start + 1) + } + (_, Some(value)) => { + self.take(spec.name, Some(value))?; + Ok(start + 1) + } + (_, None) => match self.kinds.get(start + 1) { + Some(Kind::Positional) => { + self.take(spec.name, Some(&self.arguments[start + 1]))?; + Ok(start + 2) + } + _ => Err(Stop::Usage(format!( + "argument {name}: expected one argument" + ))), + }, + } + } + + /// Consumes as many positionals as the arguments from `start` provide, as + /// `consume_positionals` does, and returns the index after them. + fn consume_positionals(&mut self, mut start: usize) -> Result { + while let Some(&spec) = self.parser.positionals.get(self.positionals) { + // A positional takes the `--` before and after its value, which `argparse` drops. + let mut end = start; + if self.kinds.get(end) == Some(&Kind::End) { + end += 1; + } + let value = match self.kinds.get(end) { + Some(Kind::Positional) => { + end += 1; + Some(self.arguments[end - 1].as_str()) + } + _ if spec.arity == Arity::One => return Ok(start), + _ => None, + }; + if self.kinds.get(end) == Some(&Kind::End) { + end += 1; + } + // An optional positional that would take nothing before an option waits for the + // arguments after the option. + if end == start && matches!(self.kinds.get(end), Some(Kind::Option(..) | Kind::Unknown)) + { + return Ok(start); + } + self.positionals += 1; + self.take(spec.name, value)?; + start = end; + } + Ok(start) + } +} + +/// Returns `argparse`'s message for `value` that is not one of `choices`. +pub(crate) fn invalid_choice(value: &str, choices: &[&str]) -> String { + let listed: Vec = choices.iter().map(|choice| repr(choice)).collect(); + format!( + "invalid choice: {} (choose from {})", + repr(value), + listed.join(", ") + ) +} + +/// Returns `argparse`'s message for a value that a `type` function refused with `ValueError`, +/// such as `invalid int value: 'x'`. +pub(crate) fn invalid_value(type_name: &str, value: &str) -> String { + format!("invalid {type_name} value: {}", repr(value)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Records what the parser hands over, refusing the value `bad`. + #[derive(Default)] + struct Recorder(Vec<(&'static str, Option)>); + + impl Receiver for Recorder { + fn take(&mut self, name: &'static str, value: Option<&str>) -> Result<(), String> { + if value == Some("bad") { + return Err(invalid_value("int", "bad")); + } + self.0.push((name, value.map(str::to_owned))); + Ok(()) + } + } + + type Events = Vec<(&'static str, Option)>; + + fn parse_with(parser: &Parser, arguments: &[&str]) -> (Result, Stop>, Events) { + let arguments: Vec = arguments.iter().map(|&a| a.to_owned()).collect(); + let mut recorder = Recorder::default(); + let result = parser.parse(&arguments, &mut recorder); + (result, recorder.0) + } + + fn parse(arguments: &[&str]) -> (Result, Stop>, Events) { + let parser = Parser::new( + &[Spec::one("--layer"), Spec::flag("--dry-run")], + &[Spec::optional("operation")], + ); + parse_with(&parser, arguments) + } + + fn taken(pairs: &[(&'static str, Option<&str>)]) -> Events { + pairs + .iter() + .map(|&(name, value)| (name, value.map(str::to_owned))) + .collect() + } + + fn strings(values: &[&str]) -> Vec { + values.iter().map(|&value| value.to_owned()).collect() + } + + fn usage(message: &str) -> Result, Stop> { + Err(Stop::Usage(message.to_owned())) + } + + #[test] + fn options_and_positionals_are_consumed_in_order() { + let (result, events) = parse(&["--layer", "a", "start", "--dry-run", "--layer=-b"]); + assert_eq!(result, Ok(Vec::new())); + assert_eq!( + events, + taken(&[ + ("--layer", Some("a")), + ("operation", Some("start")), + ("--dry-run", None), + ("--layer", Some("-b")), + ]) + ); + let (result, events) = parse(&["--dry-run"]); + assert_eq!(result, Ok(Vec::new())); + assert_eq!(events, taken(&[("--dry-run", None), ("operation", None)])); + let (_, events) = parse(&["--layer=a=b", "--layer="]); + assert_eq!( + events[..2], + taken(&[("--layer", Some("a=b")), ("--layer", Some(""))]) + ); + } + + #[test] + fn values_that_look_like_options_need_the_attached_form() { + let expected = usage("argument --layer: expected one argument"); + assert_eq!(parse(&["--layer", "-c"]).0, expected); + assert_eq!(parse(&["--layer", "--", "x"]).0, expected); + assert_eq!(parse(&["--layer"]).0, expected); + assert_eq!(parse(&["--layer", "--dry-run"]).0, expected); + for value in ["-1abc", "-.5x", "-", "-a b", ""] { + let (result, events) = parse(&["--layer", value]); + assert_eq!(result, Ok(Vec::new()), "{value:?}"); + assert_eq!(events[0], ("--layer", Some(value.to_owned()))); + } + } + + #[test] + fn unknown_arguments_are_returned_for_the_caller() { + let (result, events) = parse(&["--bogus", "run", "extra", "-x"]); + assert_eq!(result, Ok(strings(&["--bogus", "extra", "-x"]))); + assert_eq!(events, taken(&[("operation", Some("run"))])); + // A prefix of an option is unknown: options are not abbreviated. + assert_eq!(parse(&["--dry"]).0, Ok(strings(&["--dry"]))); + let (result, events) = parse(&["run", "start", "--dry-run"]); + assert_eq!(result, Ok(strings(&["start"]))); + assert_eq!( + events, + taken(&[("operation", Some("run")), ("--dry-run", None)]) + ); + } + + #[test] + fn errors_end_the_parse_where_they_occur() { + let (result, events) = parse(&["--layer", "a", "--layer", "bad", "--layer", "c"]); + assert_eq!(result, usage("argument --layer: invalid int value: 'bad'")); + assert_eq!(events, taken(&[("--layer", Some("a"))])); + assert_eq!( + parse(&["--dry-run=1"]).0, + usage("argument --dry-run: ignored explicit argument '1'") + ); + assert_eq!( + parse(&["--help=x"]).0, + usage("argument -h/--help: ignored explicit argument 'x'") + ); + assert_eq!( + parse(&["--layer", "a", "-h", "--layer", "bad"]).0, + Err(Stop::Help) + ); + assert_eq!( + parse(&["--layer", "bad", "--help"]).0, + usage("argument --layer: invalid int value: 'bad'") + ); + // An unknown option before a positional leaves the positional to its parser. + let (result, events) = parse(&["--bogus", "bad"]); + assert_eq!( + result, + usage("argument operation: invalid int value: 'bad'") + ); + assert!(events.is_empty()); + } + + #[test] + fn double_dashes_end_the_options() { + let (result, events) = parse(&["--", "--layer"]); + assert_eq!(result, Ok(Vec::new())); + assert_eq!(events, taken(&[("operation", Some("--layer"))])); + let (result, events) = parse(&["run", "--", "start"]); + assert_eq!(result, Ok(strings(&["start"]))); + assert_eq!(events, taken(&[("operation", Some("run"))])); + let (result, events) = parse(&["--layer=1", "--", "a", "--", "b"]); + assert_eq!(result, Ok(strings(&["--", "b"]))); + assert_eq!( + events, + taken(&[("--layer", Some("1")), ("operation", Some("a"))]) + ); + let (result, events) = parse(&["--dry-run", "--"]); + assert_eq!(result, Ok(Vec::new())); + assert_eq!(events, taken(&[("--dry-run", None), ("operation", None)])); + let (result, _) = parse(&["run", "--dry-run", "--", "start"]); + assert_eq!(result, Ok(strings(&["--", "start"]))); + } + + #[test] + fn required_positionals_must_be_given() { + let parser = Parser::new(&[], &[Spec::one("PATH")]); + assert_eq!( + parse_with(&parser, &[]).0, + usage("the following arguments are required: PATH") + ); + let (result, events) = parse_with(&parser, &["policy.json", "more"]); + assert_eq!(result, Ok(strings(&["more"]))); + assert_eq!(events, taken(&[("PATH", Some("policy.json"))])); + assert_eq!( + parse_with(&parser, &["-x"]).0, + usage("the following arguments are required: PATH") + ); + } + + #[test] + fn commands_split_after_the_options_before_them() { + let parser = Parser::new(&[Spec::one("--root")], &[Spec::one("command")]); + let split = |arguments: &[&str]| { + let mut recorder = Recorder::default(); + let result = parser.split_command(&strings(arguments), &mut recorder); + (result, recorder.0) + }; + let (result, events) = split(&["--root", "/r", "--x", "sandbox", "--root", "y"]); + assert_eq!(result, Ok((3, strings(&["--x"])))); + assert_eq!(events, taken(&[("--root", Some("/r"))])); + assert_eq!(split(&["--root=-r", "--", "-x"]).0, Ok((2, Vec::new()))); + assert_eq!( + split(&["--root", "-x", "sandbox"]).0, + Err(Stop::Usage( + "argument --root: expected one argument".to_owned() + )) + ); + let required = Err(Stop::Usage( + "the following arguments are required: command".to_owned(), + )); + assert_eq!(split(&["--root", "/r"]).0, required); + assert_eq!(split(&["--"]).0, required); + assert_eq!(split(&["-h", "sandbox"]).0, Err(Stop::Help)); + assert_eq!( + split(&["--root", "bad"]).0, + Err(Stop::Usage( + "argument --root: invalid int value: 'bad'".to_owned() + )) + ); + } + + #[test] + fn choices_and_values_are_described_as_argparse_describes_them() { + assert_eq!( + invalid_choice("x", &["auto", "whp"]), + "invalid choice: 'x' (choose from 'auto', 'whp')" + ); + assert_eq!( + invalid_value("positive_float", "x'y"), + "invalid positive_float value: \"x'y\"" + ); + } + + // The expected usages are what CPython 3.14's argparse prints for 80 columns. + #[test] + fn usages_wrap_as_argparse_wraps_them() { + const OPTIONALS: &[&str] = &[ + "[-h]", + "[--layer ROLE,PATH,EROFS_UUID]", + "[--scratch SCRATCH]", + "[--state-dir STATE_DIR]", + "[--entrypoint ENTRYPOINT]", + "[--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]", + ]; + const POSITIONALS: &[&str] = &["[{run,provision}]"]; + let long = "a-program-whose-name-is-long-enough-to-take-its-own-line.py"; + let syntax = |command, optionals, positionals| Syntax { + command, + optionals, + positionals, + help: "", + }; + // A usage that fits stays on one line. + assert_eq!( + syntax("egress compile", &["[-h]"], &["PATH"]).usage("nvx-rs"), + "usage: nvx-rs egress compile [-h] PATH" + ); + // A short name is followed by the options, and the positionals start a line of their own. + assert_eq!( + syntax("", OPTIONALS, POSITIONALS).usage("tool"), + "usage: tool [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n \ + [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n \ + [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n \ + [{run,provision}]" + ); + // A long name takes a line of its own, and so do the options and the positionals if all + // of them do not fit on one line. + assert_eq!( + syntax("", OPTIONALS, POSITIONALS).usage(long), + format!( + "usage: {long}\n [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n \ + [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n \ + [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n \ + [{{run,provision}}]" + ) + ); + assert_eq!( + syntax("egress compile", &["[-h]"], &["PATH"]).usage(long), + format!("usage: {long} egress compile\n [-h] PATH") + ); + assert_eq!( + syntax("", &["[-h]", "[--root PATH]"], &["path"]).usage(long), + format!("usage: {long}\n [-h] [--root PATH] path") + ); + let help = Syntax { + help: "options:\n -h, --help show this help message and exit", + ..syntax("artifacts", &["[-h]"], &[]) + }; + assert_eq!(help.prog("nvx.py"), "nvx.py artifacts"); + assert_eq!( + help.help("nvx.py"), + "usage: nvx.py artifacts [-h]\n\noptions:\n -h, --help show this help message and exit" + ); + } +} diff --git a/nvx-rs/src/host/cli/artifacts.rs b/nvx-rs/src/host/cli/artifacts.rs new file mode 100644 index 000000000..e0f19c86c --- /dev/null +++ b/nvx-rs/src/host/cli/artifacts.rs @@ -0,0 +1,179 @@ +//! `nvx-rs artifacts`: the paths of the artifacts that `nvx.py` runs, as JSON, and the artifact +//! root that every command uses. +//! +//! The root is the directory that `--root` names, in the layout that its built artifacts reveal, +//! as [`ArtifactDir::from_dir`] tells a release from a repository checkout. Without `--root`, it +//! is the release whose `bin/` holds the executable, or the repository checkout whose +//! `nvx-rs/target/` holds it, each with its `SOURCE-MANIFEST.json`. The `NVX_OPENVMM` +//! environment variable names an OpenVMM executable to run instead of the root's, such as a test +//! double. + +use std::path::{Path, PathBuf}; + +use super::args::{Parser, Syntax}; +use super::{Exit, Result, output}; +use crate::host::artifacts::{ArtifactDir, ArtifactPaths, MANIFEST_NAME, absolute}; +use crate::host::python::builtins::Int; +use crate::host::python::dumps::{self, Value}; + +/// The environment variable that names the OpenVMM executable to run instead of the root's. +pub(crate) const OPENVMM_VARIABLE: &str = "NVX_OPENVMM"; + +/// The usage and help of the parser. +const SYNTAX: Syntax = Syntax { + command: "artifacts", + optionals: &["[-h]"], + positionals: &[], + help: "\ +Print the paths of the NVX artifacts as a JSON object: the layout and root of +the artifact directory, the OpenVMM executable, the guest kernel, and for each +guest, its name and distribution, its initramfs and package manifest, and its +distro layer and manifest, or null. + +options: + -h, --help show this help message and exit", +}; + +/// Parses the arguments of `artifacts` for the program `program`, and returns those that it does +/// not recognize. +pub(super) fn parse(arguments: &[String], program: &str) -> Result> { + Parser::new(&[], &[]) + .parse(arguments, &mut |_: &'static str, _: Option<&str>| Ok(())) + .map_err(|stop| Exit::from_stop(stop, &SYNTAX, program)) +} + +/// Prints the paths of the artifacts of the root. +pub(super) fn run(root: Option<&str>) -> Result { + let located = locate(root)?; + let mut paths: ArtifactPaths = located.dir.paths(); + paths.openvmm = located.openvmm; + let value = serde_json::to_value(&paths) + .map_err(|error| Exit::error(format!("cannot encode the artifact paths: {error}")))?; + output::print_stdout(&dumps::dumps(&json_value(&value))) + .map_err(|error| Exit::error(output::stream_error(&error))) +} + +/// The artifacts that a command runs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Located { + /// The artifact directory. + pub(crate) dir: ArtifactDir, + /// The OpenVMM executable: [`OPENVMM_VARIABLE`] if set, or the directory's. + pub(crate) openvmm: PathBuf, +} + +/// Returns the artifacts of `--root`, or of the directory that holds the executable. +pub(crate) fn locate(root: Option<&str>) -> Result { + let dir = match root { + Some(root) => ArtifactDir::from_dir(root)?, + None => { + let executable = std::env::current_exe().map_err(|error| { + Exit::error(format!("cannot locate the nvx-rs executable: {error}")) + })?; + default_dir(&executable).ok_or_else(|| { + Exit::error( + "cannot locate the NVX artifacts: pass --root with an NVX repository \ + checkout or extracted release, or run the nvx-rs of a release's bin \ + directory or of a checkout's nvx-rs/target directory", + ) + })?? + } + }; + let openvmm = match std::env::var_os(OPENVMM_VARIABLE).filter(|value| !value.is_empty()) { + Some(value) => { + if value.to_str().is_none() { + return Err(Exit::error(format!( + "{OPENVMM_VARIABLE} is not valid Unicode" + ))); + } + absolute(Path::new(&value))? + } + None => dir.openvmm(), + }; + Ok(Located { dir, openvmm }) +} + +/// Returns the artifact directory that holds `executable`, if any: the release whose `bin/` +/// holds it, or the repository checkout whose `nvx-rs/target/` holds it, at any depth below, as +/// for a build for another target. Either must have its `SOURCE-MANIFEST.json`. +fn default_dir(executable: &Path) -> Option> { + let directory = executable.parent()?; + let has_manifest = |root: &Path| root.join(MANIFEST_NAME).is_file(); + if directory.file_name().is_some_and(|name| name == "bin") + && let Some(root) = directory.parent() + && has_manifest(root) + { + return Some(ArtifactDir::release(root)); + } + let target = directory.ancestors().find(|ancestor| { + ancestor.file_name().is_some_and(|name| name == "target") + && ancestor + .parent() + .and_then(Path::file_name) + .is_some_and(|name| name == "nvx-rs") + })?; + let root = target.parent()?.parent()?; + has_manifest(root).then(|| ArtifactDir::repository(root)) +} + +/// Returns the [`Value`] of a JSON value without floats, as `serde_json` serializes the paths. +fn json_value(value: &serde_json::Value) -> Value { + use serde_json::Value as Json; + + match value { + Json::Null => Value::Null, + Json::Bool(value) => Value::Bool(*value), + Json::Number(number) => { + let text = number.to_string(); + Int::parse(&text).map_or(Value::Str(text), Value::Int) + } + Json::String(text) => Value::Str(text.clone()), + Json::Array(items) => Value::List(items.iter().map(json_value).collect()), + Json::Object(members) => Value::Dict( + members + .iter() + .map(|(name, member)| (name.clone(), json_value(member))) + .collect(), + ), + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::*; + use crate::host::artifacts::Layout; + + #[test] + fn executables_find_the_release_or_checkout_that_holds_them() { + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let executable = |path: &str| root.join(path).join("nvx-rs"); + // Without a manifest, nothing is recognized. + assert!(default_dir(&executable("bin")).is_none()); + fs::write(root.join(MANIFEST_NAME), "{}").unwrap(); + let release = default_dir(&executable("bin")).unwrap().unwrap(); + assert_eq!(release.layout(), Layout::Release); + assert_eq!(release.root(), absolute(root).unwrap()); + for build in [ + "nvx-rs/target/release", + "nvx-rs/target/x86_64-unknown-linux-musl/debug", + ] { + let checkout = default_dir(&executable(build)).unwrap().unwrap(); + assert_eq!(checkout.layout(), Layout::Repository, "{build}"); + assert_eq!(checkout.root(), absolute(root).unwrap(), "{build}"); + } + assert!(default_dir(&executable("target/release")).is_none()); + assert!(default_dir(&executable("elsewhere")).is_none()); + } + + #[test] + fn json_values_keep_their_shape() { + let value = serde_json::json!({"b": [null, true, "x"], "a": {}}); + assert_eq!( + dumps::dumps(&json_value(&value)), + "{\n \"a\": {},\n \"b\": [\n null,\n true,\n \"x\"\n ]\n}" + ); + } +} diff --git a/nvx-rs/src/host/cli/deadline.rs b/nvx-rs/src/host/cli/deadline.rs new file mode 100644 index 000000000..e752fad0b --- /dev/null +++ b/nvx-rs/src/host/cli/deadline.rs @@ -0,0 +1,94 @@ +//! Deadlines with Python's float seconds, as `nvx.py` computes them with `time.monotonic()`. + +use std::thread; +use std::time::{Duration, Instant}; + +use super::{Result, check_interrupt}; + +/// The longest that a wait sleeps between checks for Ctrl-C and for the condition it awaits. +pub(crate) const RETRY_INTERVAL: Duration = Duration::from_millis(25); + +/// A point in time `timeout` seconds after it was taken, or none when that is beyond what an +/// [`Instant`] holds, which then never passes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Deadline(Option); + +impl Deadline { + /// Returns the deadline `seconds` from now: `time.monotonic() + seconds`. + pub(crate) fn after(seconds: f64) -> Self { + Self( + Duration::try_from_secs_f64(seconds) + .ok() + .and_then(|duration| Instant::now().checked_add(duration)), + ) + } + + /// Returns the time left, which is zero once the deadline has passed, or `None` if the + /// deadline never passes. + pub(crate) fn remaining(self) -> Option { + self.0 + .map(|instant| instant.saturating_duration_since(Instant::now())) + } + + /// Returns whether the deadline has passed: `time.monotonic() >= deadline`. + pub(crate) fn passed(self) -> bool { + self.0.is_some_and(|instant| Instant::now() >= instant) + } + + /// Returns the deadline as an [`Instant`], which for a deadline that never passes is a + /// century away. + pub(crate) fn instant(self) -> Instant { + self.0.unwrap_or_else(|| { + let now = Instant::now(); + now.checked_add(Duration::from_secs(100 * 365 * 24 * 3600)) + .unwrap_or(now) + }) + } + + /// Waits for the next retry of a step that the deadline bounds, as `_pause` does: returns + /// `false` at once if the deadline has passed, and otherwise sleeps for `interval` or the + /// time left, whichever is shorter, and returns whether time is left. Fails if Ctrl-C + /// arrives. + pub(crate) fn pause(self, interval: Duration) -> Result { + let remaining = self.remaining(); + if remaining.is_some_and(|remaining| remaining.is_zero()) { + return Ok(false); + } + sleep(remaining.map_or(interval, |remaining| remaining.min(interval)))?; + Ok(!self.passed()) + } +} + +/// Sleeps for `duration`, as `time.sleep` does, failing if Ctrl-C arrives meanwhile. +pub(crate) fn sleep(duration: Duration) -> Result { + let end = Instant::now().checked_add(duration); + loop { + check_interrupt()?; + let left = end.map_or(RETRY_INTERVAL, |end| { + end.saturating_duration_since(Instant::now()) + }); + if left.is_zero() { + return Ok(()); + } + thread::sleep(left.min(RETRY_INTERVAL)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deadlines_pass_after_their_seconds() { + assert!(!Deadline::after(60.0).passed()); + assert!(Deadline::after(1e-300).passed()); + let unbounded = Deadline::after(1e300); + assert_eq!(unbounded.remaining(), None); + assert!(!unbounded.passed()); + assert!(unbounded.instant() > Instant::now()); + let short = Deadline::after(0.01); + thread::sleep(Duration::from_millis(20)); + assert!(!short.pause(RETRY_INTERVAL).unwrap()); + assert_eq!(short.remaining(), Some(Duration::ZERO)); + } +} diff --git a/nvx-rs/src/host/cli/egress.rs b/nvx-rs/src/host/cli/egress.rs new file mode 100644 index 000000000..a73c7a723 --- /dev/null +++ b/nvx-rs/src/host/cli/egress.rs @@ -0,0 +1,85 @@ +//! `nvx-rs egress compile PATH`: compiles an egress policy file, as `nvx.py run` and `nvx.py +//! sandbox` compile their `--network-egress-policy-file`, and prints the native OpenVMM rules as +//! a JSON object, `{"allow": [...], "deny": [...]}`, with the rules of each action in the order +//! in which OpenVMM receives them. + +use std::path::Path; + +use super::args::{Parser, Spec, Syntax}; +use super::{Exit, Result, args, output}; +use crate::host::network::policy::compile_policy_file; +use crate::host::python::dumps::{self, Value}; + +/// The usage and help of the parser. +const SYNTAX: Syntax = Syntax { + command: "egress", + optionals: &["[-h]"], + positionals: &["{compile} ..."], + help: "\ +Compile egress policy files. + +positional arguments: + {compile} + compile compile an egress policy file into native OpenVMM rules + +options: + -h, --help show this help message and exit", +}; +/// The actions, in the order in which usage messages list them. +const ACTIONS: [&str; 1] = ["compile"]; +/// The usage and help of the `compile` parser. +const COMPILE_SYNTAX: Syntax = Syntax { + command: "egress compile", + optionals: &["[-h]"], + positionals: &["PATH"], + help: "\ +Compile the egress policy file at PATH, as --network-egress-policy-file does, +and print its native rules as a JSON object: {\"allow\": [...], \"deny\": [...]}. + +positional arguments: + PATH the egress policy file: at most 1 MiB of JSON with allow and deny + lists of rules + +options: + -h, --help show this help message and exit", +}; + +/// Parses the arguments of `egress` for the program `program`, and returns the policy file and +/// the arguments that no parser recognizes. +pub(super) fn parse(arguments: &[String], program: &str) -> Result<(String, Vec)> { + let parser = Parser::new(&[], &[Spec::one("action")]); + let (index, mut extras) = parser + .split_command(arguments, &mut |_: &'static str, _: Option<&str>| Ok(())) + .map_err(|stop| Exit::from_stop(stop, &SYNTAX, program))?; + if arguments[index] != "compile" { + return Err(Exit::usage( + &SYNTAX, + program, + format!( + "argument action: {}", + args::invalid_choice(&arguments[index], &ACTIONS) + ), + )); + } + let mut path = String::new(); + let unrecognized = Parser::new(&[], &[Spec::one("PATH")]) + .parse( + &arguments[index + 1..], + &mut |_: &'static str, value: Option<&str>| { + path = value.unwrap_or_default().to_owned(); + Ok(()) + }, + ) + .map_err(|stop| Exit::from_stop(stop, &COMPILE_SYNTAX, program))?; + extras.extend(unrecognized); + Ok((path, extras)) +} + +/// Compiles the policy file at `path` and prints its rules. +pub(super) fn run(path: &str) -> Result { + let policy = compile_policy_file(Path::new(path))?; + let rules = |rules: Vec| Value::List(rules.into_iter().map(Value::Str).collect()); + let value = dumps::dict([("allow", rules(policy.allow)), ("deny", rules(policy.deny))]); + output::print_stdout(&dumps::dumps(&value)) + .map_err(|error| Exit::error(output::stream_error(&error))) +} diff --git a/nvx-rs/src/host/cli/mod.rs b/nvx-rs/src/host/cli/mod.rs new file mode 100644 index 000000000..2c5f7762d --- /dev/null +++ b/nvx-rs/src/host/cli/mod.rs @@ -0,0 +1,291 @@ +//! The `nvx-rs` command-line interface, the backend of the runtime steps of `nvx.py`. +//! +//! ```text +//! nvx-rs [-h] [--root PATH] [--prog PROG] artifacts +//! nvx-rs [-h] [--root PATH] [--prog PROG] egress compile PATH +//! nvx-rs [-h] [--root PATH] [--prog PROG] sandbox [run|provision|start|exec|stop|deprovision] [OPTIONS] +//! ``` +//! +//! - `artifacts` prints the paths of the artifacts that `nvx.py` runs as JSON. +//! - `egress compile PATH` compiles an egress policy file of `--network-egress-policy-file` and +//! prints its native rules as JSON. +//! - `sandbox` is `nvx.py sandbox`: the same options, outputs, exit statuses, outcome reports, +//! and state directories. +//! +//! The artifact root is `--root`, or else the release or repository checkout that holds the +//! executable, and the `NVX_OPENVMM` environment variable names another OpenVMM executable. +//! `--prog` names the program in usage and error messages, `nvx-rs` by default: `nvx.py`, which +//! runs `nvx-rs` for its `sandbox` command, passes its own name, so that its users see the +//! messages that it printed before it delegated the command. +//! +//! Errors print `error: MESSAGE` to standard error and exit with status 1, as `nvx.py` reports a +//! `ScriptError`. Usage errors print the usage and `PROG: error: MESSAGE`, and exit with status +//! 2, as `argparse` does. Ctrl-C prints `Interrupted` and exits with status 130. Text ends its +//! lines as Python's `print` ends them: `\r\n` on Windows, and `\n` elsewhere. + +mod args; +mod artifacts; +mod deadline; +mod egress; +mod output; +mod sandbox; +mod session; + +use std::ffi::OsString; + +use self::args::{Parser, Receiver, Spec, Stop, Syntax}; +use crate::host::platform; +use crate::host::python::builtins::repr; + +/// The name of the program in usage messages unless `--prog` names another. +const PROGRAM: &str = "nvx-rs"; +/// The commands, in the order in which usage messages list them. +const COMMANDS: [&str; 3] = ["artifacts", "egress", "sandbox"]; +/// The usage and help of the top-level parser. +const SYNTAX: Syntax = Syntax { + command: "", + optionals: &["[-h]", "[--root PATH]", "[--prog PROG]"], + positionals: &["{artifacts,egress,sandbox} ..."], + help: "\ +Run the NVX steps that nvx.py delegates: artifact lookup, egress policy +compilation, and sandboxes. + +positional arguments: + {artifacts,egress,sandbox} + artifacts print the paths of the NVX artifacts as JSON + egress compile egress policy files + sandbox run or manage workloads over EROFS layers and private + ext4 scratch + +options: + -h, --help show this help message and exit + --root PATH the NVX repository checkout or extracted release whose + artifacts to use (default: the release or checkout + that holds this executable) + --prog PROG the program name that usage and error messages show, + such as nvx.py, which runs nvx-rs for its commands + (default: nvx-rs) + +environment: + NVX_OPENVMM the OpenVMM executable to run instead of the one of the + artifact root", +}; + +/// How a command ends other than by succeeding. +#[derive(Debug)] +pub(crate) enum Exit { + /// `error: MESSAGE` on standard error, and status 1: what `nvx.py` does for a `ScriptError`, + /// an `OSError`, a `RuntimeError`, or a `ValueError`. + Error(String), + /// OpenVMM closed the control endpoint, as it does when it shuts down: Python's + /// `ControlEndpointClosed`, which `nvx.py` reports as any other error. + EndpointClosed, + /// A usage error of the parser `prog`: its usage, then `PROG: error: MESSAGE`, and status 2. + Usage { + /// The parser's usage. + usage: String, + /// The parser's name, such as `nvx-rs sandbox`. + prog: String, + /// The message. + message: String, + }, + /// Help on standard output, and status 0. + Help(String), + /// Ctrl-C: `Interrupted` on standard error, and status 130. + Interrupted, + /// `SystemExit(status)`: the status of OpenVMM or of a managed workload. + Status(i32), +} + +/// The message of [`Exit::EndpointClosed`], `str(ControlEndpointClosed())`. +pub(crate) const ENDPOINT_CLOSED: &str = "managed control endpoint closed"; + +impl Exit { + /// Returns the error that prints `message`. + pub(crate) fn error(message: impl Into) -> Self { + Self::Error(message.into()) + } + + /// Returns the usage error `message` of the parser of `syntax` for the program `program`. + pub(crate) fn usage(syntax: &Syntax, program: &str, message: String) -> Self { + Self::Usage { + usage: syntax.usage(program), + prog: syntax.prog(program), + message, + } + } + + /// Returns the end of a parse that `stop` ended, for the parser of `syntax` and the program + /// `program`. + pub(crate) fn from_stop(stop: Stop, syntax: &Syntax, program: &str) -> Self { + match stop { + Stop::Usage(message) => Self::usage(syntax, program, message), + Stop::Help => Self::Help(syntax.help(program)), + } + } + + /// Returns the message that the end prints after `error: `, if it is an error. + pub(crate) fn message(&self) -> Option<&str> { + match self { + Self::Error(message) => Some(message), + Self::EndpointClosed => Some(ENDPOINT_CLOSED), + _ => None, + } + } + + /// Reports the end on the standard streams and returns the exit status. + fn report(self) -> i32 { + match self { + Self::Error(_) | Self::EndpointClosed => { + output::print_stderr(&format!("error: {}", self.message().unwrap_or_default())); + 1 + } + Self::Usage { + usage, + prog, + message, + } => { + output::print_stderr(&format!("{usage}\n{prog}: error: {message}")); + 2 + } + Self::Help(text) => match output::print_stdout(&text) { + Ok(()) => 0, + Err(error) => Self::Error(output::stream_error(&error)).report(), + }, + Self::Interrupted => { + output::print_stderr("Interrupted"); + 130 + } + Self::Status(status) => status, + } + } +} + +impl From for Exit { + fn from(error: crate::Error) -> Self { + Self::Error(error.message().to_owned()) + } +} + +/// The result of a command. +pub(crate) type Result = std::result::Result; + +/// Fails with [`Exit::Interrupted`] if Ctrl-C arrived. +pub(crate) fn check_interrupt() -> Result { + if platform::interrupted() { + Err(Exit::Interrupted) + } else { + Ok(()) + } +} + +/// Runs `nvx-rs` with the process's arguments and returns its exit status. +/// +/// The CLI runs on the calling thread, which should be the main thread of the process, and from +/// here on, Ctrl-C ends the command that runs, with status 130, instead of the process. +pub fn main() -> i32 { + // Like Python, which installs its handler as it starts, run on without one if that fails. + let _ = platform::watch_interrupts(); + let arguments: Vec = std::env::args_os().skip(1).collect(); + let result = run(&arguments); + // Python raises KeyboardInterrupt wherever Ctrl-C finds its command, so a command that Ctrl-C + // reached ends as interrupted even if it finished meanwhile, as a run does whose OpenVMM the + // same Ctrl-C ended. + if platform::interrupted() { + return Exit::Interrupted.report(); + } + match result { + Ok(()) => 0, + Err(exit) => exit.report(), + } +} + +/// A parsed command. +enum Command { + /// `artifacts`. + Artifacts, + /// `egress compile PATH`. + EgressCompile(String), + /// `sandbox ...`. + Sandbox(Box), +} + +/// The top-level options. +#[derive(Default)] +struct TopLevel { + /// `--root`, if given. + root: Option, + /// `--prog`, if given. + prog: Option, +} + +impl Receiver for TopLevel { + fn take(&mut self, name: &'static str, value: Option<&str>) -> std::result::Result<(), String> { + let value = value.map(str::to_owned); + match name { + "--root" => self.root = value, + _ => self.prog = value, + } + Ok(()) + } +} + +/// Parses the arguments, reports the unrecognized ones, and runs the command. +fn run(arguments: &[OsString]) -> Result { + let mut strings = Vec::with_capacity(arguments.len()); + for (index, argument) in arguments.iter().enumerate() { + let text = argument.to_str().ok_or_else(|| { + Exit::usage( + &SYNTAX, + PROGRAM, + format!( + "argument {} is not valid Unicode: {}", + index + 1, + repr(&argument.to_string_lossy()) + ), + ) + })?; + strings.push(text.to_owned()); + } + let parser = Parser::new( + &[Spec::one("--root"), Spec::one("--prog")], + &[Spec::one("command")], + ); + let mut top = TopLevel::default(); + let split = parser.split_command(&strings, &mut top); + // A usage error names the program that the options before it named. + let program = top.prog.clone().unwrap_or_else(|| PROGRAM.to_owned()); + let usage = |message: String| Exit::usage(&SYNTAX, &program, message); + let (index, mut extras) = split.map_err(|stop| Exit::from_stop(stop, &SYNTAX, &program))?; + let rest = &strings[index + 1..]; + let (command, unrecognized) = match strings[index].as_str() { + "artifacts" => (Command::Artifacts, artifacts::parse(rest, &program)?), + "egress" => { + let (path, unrecognized) = egress::parse(rest, &program)?; + (Command::EgressCompile(path), unrecognized) + } + "sandbox" => { + let (options, unrecognized) = sandbox::parse(rest, &program)?; + (Command::Sandbox(Box::new(options)), unrecognized) + } + other => { + return Err(usage(format!( + "argument command: {}", + args::invalid_choice(other, &COMMANDS) + ))); + } + }; + extras.extend(unrecognized); + if !extras.is_empty() { + return Err(usage(format!( + "unrecognized arguments: {}", + extras.join(" ") + ))); + } + let root = top.root.as_deref(); + match command { + Command::Artifacts => artifacts::run(root), + Command::EgressCompile(path) => egress::run(&path), + Command::Sandbox(options) => sandbox::run(root, *options), + } +} diff --git a/nvx-rs/src/host/cli/output.rs b/nvx-rs/src/host/cli/output.rs new file mode 100644 index 000000000..d698687b0 --- /dev/null +++ b/nvx-rs/src/host/cli/output.rs @@ -0,0 +1,65 @@ +//! The standard streams, written as `nvx.py` writes them. +//! +//! Python's `print` ends lines with `os.linesep`, `\r\n` on Windows and `\n` elsewhere, and +//! translates every `\n` of the text it writes, so [`print_stdout`] and [`print_stderr`] do too. +//! Workload output passes through [`write_stdout`] and [`write_stderr`] unchanged, as +//! `sys.stdout.buffer.write` passes it. + +use std::io::{self, Write}; + +use crate::host::python::fs::os_error_message; + +/// The line separator of this host, `os.linesep`. +pub(crate) const LINE_SEPARATOR: &str = if cfg!(windows) { "\r\n" } else { "\n" }; + +/// Returns `text` and a line end, with each `\n` translated to [`LINE_SEPARATOR`]. +pub(crate) fn line(text: &str) -> String { + let mut output = text.replace('\n', LINE_SEPARATOR); + output.push_str(LINE_SEPARATOR); + output +} + +/// Prints `text` and a line end to standard output, and flushes it. +pub(crate) fn print_stdout(text: &str) -> io::Result<()> { + write_stdout(line(text).as_bytes()) +} + +/// Prints `text` and a line end to standard error. Like Python, which prints errors on a stream +/// that it cannot write anyway, it ignores a failure. +pub(crate) fn print_stderr(text: &str) { + let _ = write_stderr(line(text).as_bytes()); +} + +/// Writes `data` to standard output as it is, and flushes it. +pub(crate) fn write_stdout(data: &[u8]) -> io::Result<()> { + let mut stdout = io::stdout().lock(); + stdout.write_all(data)?; + stdout.flush() +} + +/// Writes `data` to standard error as it is, and flushes it. +pub(crate) fn write_stderr(data: &[u8]) -> io::Result<()> { + let mut stderr = io::stderr().lock(); + stderr.write_all(data)?; + stderr.flush() +} + +/// Returns the message of the `OSError` that writing to a standard stream raises, such as +/// `[Errno 32] Broken pipe`. +pub(crate) fn stream_error(error: &io::Error) -> String { + os_error_message(error, None) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lines_end_as_python_prints_them() { + if cfg!(windows) { + assert_eq!(line("a\nb"), "a\r\nb\r\n"); + } else { + assert_eq!(line("a\nb"), "a\nb\n"); + } + } +} diff --git a/nvx-rs/src/host/cli/sandbox/identity.rs b/nvx-rs/src/host/cli/sandbox/identity.rs new file mode 100644 index 000000000..7175d45c8 --- /dev/null +++ b/nvx-rs/src/host/cli/sandbox/identity.rs @@ -0,0 +1,223 @@ +//! The identity of the OpenVMM process of a running sandbox, as the former `sandbox_lifecycle.py` +//! recorded and checked it: `_process_start_time` and `_process_running`. +//! +//! A runtime record names OpenVMM by its process ID and start time, which together never repeat: +//! on Linux the `starttime` field of `/proc/PID/stat`, in clock ticks since boot, and on Windows +//! the creation time of the process as a 64-bit `FILETIME`. Both implementations compute the same +//! value, so a record that either wrote identifies the process for the other. + +use crate::host::cli::{Exit, Result}; +use crate::host::python::builtins::Int; +#[cfg(any(target_os = "linux", windows))] +use crate::host::python::fs::os_error_message; + +/// Returns the start time of a live process, or `None` if no live process has the ID, as +/// `_process_start_time` does, or the message of the error with which the state of the process +/// cannot be determined. +/// +/// A process runs until its last thread exits, as [`crate::host::platform::process_start_time`] +/// holds, so a zombie counts as exited only once its other threads have exited too, where Python +/// counted every zombie as exited. On Windows, a process that the caller cannot open counts as +/// exited: OpenVMM runs as the caller, so such a process cannot be its VM. Other hosts have no +/// `/proc`, so every process counts as exited there, as in Python. +pub(crate) fn process_start_time(pid: &Int) -> std::result::Result, String> { + #[cfg(target_os = "linux")] + { + let path = format!("/proc/{pid}/stat"); + let stat = match std::fs::read(&path) { + Ok(stat) => stat, + Err(error) + if error.kind() == std::io::ErrorKind::NotFound + || error.raw_os_error() == Some(nix::errno::Errno::ESRCH as i32) => + { + return Ok(None); + } + Err(error) => return Err(os_error_message(&error, Some(&path))), + }; + linux_start_time(&stat, || { + pid.to_u32() + .map_or(Ok(false), crate::host::platform::other_threads_remain) + .map_err(|error| os_error_message(&error, Some(&format!("/proc/{pid}/task")))) + }) + } + #[cfg(windows)] + { + let Some(pid) = pid.to_u32() else { + return Ok(None); + }; + crate::host::platform::process_start_time(pid) + .map_err(|error| os_error_message(&error, None)) + } + #[cfg(not(any(target_os = "linux", windows)))] + { + let _ = pid; + Ok(None) + } +} + +/// Reads the start time from the `/proc/PID/stat` line of a process, as `_linux_start_time` +/// does: the command name may contain spaces and parentheses, so the fields after it start at +/// its last `)`, with the state as field 3 and `starttime` as field 22. Only for a zombie does it +/// ask `other_threads_remain` whether the process still runs. +#[cfg(any(target_os = "linux", test))] +fn linux_start_time( + stat: &[u8], + other_threads_remain: impl FnOnce() -> std::result::Result, +) -> std::result::Result, String> { + let unexpected = || "process status has an unexpected format".to_owned(); + let fields: Vec<&[u8]> = match stat.iter().rposition(|&byte| byte == b')') { + Some(end) => stat[end + 1..] + .split(|byte| matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)) + .filter(|field| !field.is_empty()) + .collect(), + None => Vec::new(), + }; + let start = fields.get(19).copied().unwrap_or_default(); + if fields.len() < 20 || start.is_empty() || !start.iter().all(u8::is_ascii_digit) { + return Err(unexpected()); + } + let state = fields[0]; + if state == b"X" || (state == b"Z" && !other_threads_remain()?) { + return Ok(None); + } + std::str::from_utf8(start) + .ok() + .and_then(|start| start.parse().ok()) + .map(Some) + .ok_or_else(unexpected) +} + +/// Returns whether the OpenVMM process that a runtime record names still runs, as +/// `_process_running` does. A record without a start time, written by an earlier NVX version, +/// identifies OpenVMM by its process ID alone. +pub(crate) fn process_running(pid: &Int, start_time: Option<&Int>) -> Result { + if !pid.is_positive() { + return Ok(false); + } + let current = process_start_time(pid).map_err(|message| { + Exit::error(format!( + "cannot determine whether OpenVMM process {pid} is running: {message}" + )) + })?; + Ok(match start_time { + None => current.is_some(), + Some(start_time) => current.map(Int::from).as_ref() == Some(start_time), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stat_lines_give_the_start_time_of_live_processes() { + let line = |state: &str| { + format!( + "123 (my (odd) prog) {state} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 4242 19 20\n" + ) + }; + let unasked = + || -> std::result::Result { panic!("only a zombie's threads matter") }; + let alone = || Ok(false); + let threads = || Ok(true); + assert_eq!( + linux_start_time(line("S").as_bytes(), unasked), + Ok(Some(4242)) + ); + assert_eq!( + linux_start_time(line("R").as_bytes(), unasked), + Ok(Some(4242)) + ); + assert_eq!(linux_start_time(line("Z").as_bytes(), alone), Ok(None)); + // A zombie main thread whose other threads still run is a live process. + assert_eq!( + linux_start_time(line("Z").as_bytes(), threads), + Ok(Some(4242)) + ); + assert_eq!( + linux_start_time(line("Z").as_bytes(), || Err("denied".to_owned())), + Err("denied".to_owned()) + ); + assert_eq!(linux_start_time(line("X").as_bytes(), unasked), Ok(None)); + for malformed in [ + &b"123 (prog) S 1 2"[..], + b"no parenthesis", + b"1 (p) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 x", + ] { + assert_eq!( + linux_start_time(malformed, unasked), + Err("process status has an unexpected format".to_owned()) + ); + } + } + + /// Kills and reaps a child when dropped, so that a failed assertion leaves no process. + #[cfg(target_os = "linux")] + struct Reaped(std::process::Child); + + #[cfg(target_os = "linux")] + impl Drop for Reaped { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } + } + + #[cfg(target_os = "linux")] + #[test] + fn a_vm_runs_until_its_last_thread_exits() { + // As in `process::tests::a_process_runs_until_its_last_thread_exits`, the main thread + // leaves through pthread_exit, which turns it into a zombie, while another thread sleeps. + use std::time::{Duration, Instant}; + let spawned = std::process::Command::new("python3") + .args([ + "-c", + "import ctypes, threading, time\n\ + threading.Thread(target=time.sleep, args=(60,)).start()\n\ + ctypes.CDLL(None).pthread_exit(None)\n", + ]) + .stdin(std::process::Stdio::null()) + .spawn(); + let mut child = match spawned { + Ok(child) => Reaped(child), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + eprintln!("skipping: python3 is not installed"); + return; + } + Err(error) => panic!("cannot start python3: {error}"), + }; + let pid = Int::from(child.0.id()); + let start_time = Int::from(process_start_time(&pid).unwrap().expect("python3 runs")); + let deadline = Instant::now() + Duration::from_secs(10); + loop { + let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).unwrap(); + if stat[stat.rfind(')').unwrap() + 1..] + .trim_start() + .starts_with('Z') + { + break; + } + assert!(Instant::now() < deadline, "the main thread did not exit"); + std::thread::sleep(Duration::from_millis(10)); + } + assert!(process_running(&pid, Some(&start_time)).unwrap()); + child.0.kill().unwrap(); + child.0.wait().unwrap(); + assert!(!process_running(&pid, Some(&start_time)).unwrap()); + } + + #[test] + fn this_process_runs_and_others_are_identified_by_start_time() { + let pid = Int::from(std::process::id()); + let start_time = process_start_time(&pid).unwrap(); + if cfg!(any(target_os = "linux", windows)) { + let start_time = Int::from(start_time.expect("this process runs")); + assert!(process_running(&pid, Some(&start_time)).unwrap()); + assert!(!process_running(&pid, Some(&start_time.plus_one())).unwrap()); + assert!(process_running(&pid, None).unwrap()); + } + assert!(!process_running(&Int::zero(), None).unwrap()); + assert!(!process_running(&Int::parse("-5").unwrap(), None).unwrap()); + assert!(!process_running(&Int::parse("99999999999999999999").unwrap(), None).unwrap()); + } +} diff --git a/nvx-rs/src/host/cli/sandbox/lifecycle.rs b/nvx-rs/src/host/cli/sandbox/lifecycle.rs new file mode 100644 index 000000000..fe4ef1caa --- /dev/null +++ b/nvx-rs/src/host/cli/sandbox/lifecycle.rs @@ -0,0 +1,1006 @@ +//! The lifecycle of a managed sandbox, a port of the former +//! `scripts/nvx_tools/sandbox_lifecycle.py`, which it replaced (microsoft/nvx#359). +//! +//! A sandbox lives in its state directory: +//! +//! ```text +//! / +//! config.json the launch that provision records, in formats 1 to 6 +//! lifecycle.lock the lock that serializes provision, start, stop, and deprovision +//! runtime.json format 1: the pid, start_time, and control_endpoint of OpenVMM +//! control.capability the 32-byte capability of the running OpenVMM +//! control.sock the control endpoint of the running OpenVMM, on POSIX +//! openvmm.log OpenVMM's output, appended by each start +//! outcome.json OpenVMM's outcome report of the latest start +//! ``` +//! +//! [`provision`], [`start`], [`stop`], and [`deprovision`] hold the [`LifecycleLock`] for their +//! whole transition; [`exec_workload`] does not, so a long workload cannot delay a stop. Every +//! state file is written as `sandbox_lifecycle.py` wrote it, and a state directory that either +//! implementation wrote serves the other, so an older `nvx.py` and `nvx-rs` share sandboxes. + +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::time::Duration; + +use super::Artifacts; +use super::identity::{process_running, process_start_time}; +use super::lock::{ + LifecycleLock, OsErrorClass, directory_not_empty, directory_status, entries, lingers, + os_error_class, +}; +use super::state::{ + CONFIG_FORMATS, STATE_FORMAT, config_int, config_text, deserialize_launch, hex, member, + random_bytes, read_json, serialize_launch, temporary_path, uuid4_hex, write_json, +}; +use crate::host::cli::deadline::{Deadline, RETRY_INTERVAL, sleep}; +use crate::host::cli::session::{EXIT_CATEGORIES, ExecRequest, ExecResult, Session}; +use crate::host::cli::{Exit, Result}; +use crate::host::process::{self, Launched}; +use crate::host::python::builtins::{Int, repr}; +use crate::host::python::dumps::{self, Value, dict}; +use crate::host::python::fs::{self, os_error_message}; +use crate::host::python::json::{Document, Node}; +use crate::host::python::path::PurePath; +use crate::host::python::value::py_str; +use crate::host::sandbox::{SandboxLaunch, require_file}; +use crate::protocol::{CAPABILITY_LEN, GuestFeatures}; + +/// The launch configuration. +pub(crate) const CONFIG_NAME: &str = "config.json"; +/// The record of the running OpenVMM process. +pub(crate) const RUNTIME_NAME: &str = "runtime.json"; +/// The control capability of the running OpenVMM process. +pub(crate) const CAPABILITY_NAME: &str = "control.capability"; +/// OpenVMM's output. +pub(crate) const LOG_NAME: &str = "openvmm.log"; +/// The control endpoint on POSIX. +pub(crate) const CONTROL_SOCKET_NAME: &str = "control.sock"; +/// OpenVMM's outcome report. +pub(crate) const OUTCOME_NAME: &str = "outcome.json"; +/// The lifecycle lock. +pub(crate) const LOCK_NAME: &str = "lifecycle.lock"; +/// The files that exist only from the start that launches an OpenVMM process until the stop or +/// failed start that ends it, whether or not the runtime record names it. +pub(crate) const RUNTIME_FILE_NAMES: [&str; 3] = + [RUNTIME_NAME, CAPABILITY_NAME, CONTROL_SOCKET_NAME]; +/// How often a caller retries a lifecycle lock that another caller holds, and the removal of a +/// state directory that a removed lock file still keeps. +pub(crate) const LOCK_RETRY_INTERVAL: Duration = RETRY_INTERVAL; +/// What deprovision writes into the lock file before it removes the file, so that a waiter that +/// then takes the lock of the removed file knows that the directory goes away. +pub(crate) const LOCK_TOMBSTONE: &[u8] = b"deprovisioned\n"; +/// The version of outcome reports. +pub(crate) const OUTCOME_SCHEMA_VERSION: u64 = 1; + +/// The options of a launch, besides the launch itself, that `provision` records. +pub(crate) struct ProvisionConfig<'a> { + /// `--hypervisor`, resolved. + pub(crate) hypervisor: &'a str, + /// `--memory-mib`. + pub(crate) memory_mib: &'a Int, + /// `--net`. + pub(crate) net: Option<&'a str>, + /// `--network-profile`. + pub(crate) network_profile: Option<&'a str>, + /// `--network-egress`. + pub(crate) network_egress: Option<&'a str>, + /// `--network-ingress`. + pub(crate) network_ingress: Option<&'a str>, + /// The egress allow rules. + pub(crate) network_egress_allow: &'a [String], + /// The egress deny rules. + pub(crate) network_egress_deny: &'a [String], + /// `--host-loopback`. + pub(crate) host_loopback: Option<&'a str>, + /// `--network-proxy`. + pub(crate) network_proxy: Option<&'a str>, + /// The `--host-loopback-forward` options. + pub(crate) host_loopback_forward: &'a [String], + /// `--cmdline`. + pub(crate) cmdline: &'a str, +} + +/// Returns the error for an `OSError` that an operation on `path` raised. +fn os_error(error: &std::io::Error, path: &PurePath) -> Exit { + Exit::error(os_error_message(error, Some(path.as_str()))) +} + +/// Removes `path` unless it is missing, as `Path.unlink(missing_ok=True)` does. +fn unlink_missing_ok(path: &PurePath) -> Result { + match std::fs::remove_file(path.as_path()) { + Err(error) if os_error_class(&error) != OsErrorClass::NotFound => { + Err(os_error(&error, path)) + } + _ => Ok(()), + } +} + +/// Returns a builder of directories of `mode`, which Windows ignores, as `os.mkdir` does. +fn dir_builder(mode: u32) -> std::fs::DirBuilder { + #[cfg_attr(not(unix), allow(unused_mut))] + let mut builder = std::fs::DirBuilder::new(); + #[cfg(unix)] + std::os::unix::fs::DirBuilderExt::mode(&mut builder, mode); + #[cfg(not(unix))] + let _ = mode; + builder +} + +/// Creates `path` and its missing parents, as `Path.mkdir(mode, parents=True, exist_ok=True)` +/// does: parents with the default mode, `path` with `mode`, and no error if `path` is a +/// directory already. +fn make_directories(path: &PurePath, mode: u32) -> Result { + match dir_builder(mode).create(path.as_path()) { + Ok(()) => Ok(()), + Err(error) if os_error_class(&error) == OsErrorClass::NotFound => { + match path.parents().next() { + Some(parent) if parent != *path => { + make_directories(&parent, 0o777)?; + make_directories_once(path, mode) + } + _ => Err(os_error(&error, path)), + } + } + Err(_) if fs::is_dir(path.as_path()) => Ok(()), + Err(error) => Err(os_error(&error, path)), + } +} + +/// Creates `path` once its parent exists, as `self.mkdir(mode, parents=False, exist_ok=True)` +/// does. +fn make_directories_once(path: &PurePath, mode: u32) -> Result { + match dir_builder(mode).create(path.as_path()) { + Ok(()) => Ok(()), + Err(_) if fs::is_dir(path.as_path()) => Ok(()), + Err(error) => Err(os_error(&error, path)), + } +} + +/// Gives a directory mode 0700, as `os.chmod(path, 0o700)` does: on Windows, that only clears the +/// read-only attribute. +fn make_private(path: &PurePath) -> Result { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + std::fs::set_permissions(path.as_path(), std::fs::Permissions::from_mode(0o700)) + .map_err(|error| os_error(&error, path)) + } + #[cfg(not(unix))] + { + let metadata = std::fs::metadata(path.as_path()).map_err(|error| os_error(&error, path))?; + let mut permissions = metadata.permissions(); + if permissions.readonly() { + #[allow(clippy::permissions_set_readonly_false)] + permissions.set_readonly(false); + std::fs::set_permissions(path.as_path(), permissions) + .map_err(|error| os_error(&error, path))?; + } + Ok(()) + } +} + +/// Resolves the state directory at `path`, and creates it with `create`, as +/// `_prepare_state_directory` does. It must be a directory and not a symbolic link. +pub(crate) fn prepare_state_directory(path: &str, create: bool) -> Result { + let resolved = fs::resolve(&PurePath::native(path)) + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + if create { + make_directories(&resolved, 0o700)?; + make_private(&resolved)?; + } + if fs::is_symlink(resolved.as_path()) || !fs::is_dir(resolved.as_path()) { + return Err(Exit::error(format!( + "sandbox state path is not a plain directory: {resolved}" + ))); + } + Ok(resolved) +} + +/// Returns the destination of an outcome report, as `_outcome_destination` does: in a plain +/// directory, with a file name, and new. +fn outcome_destination(path: &str) -> Result { + let path = PurePath::native(path); + let candidate = if path.is_absolute() { + path + } else { + fs::cwd() + .map_err(|error| Exit::error(os_error_message(&error, None)))? + .join(path.as_str()) + }; + let parent = candidate + .parents() + .next() + .unwrap_or_else(|| candidate.clone()); + if fs::is_symlink(parent.as_path()) || !fs::is_dir(parent.as_path()) { + return Err(Exit::error(format!( + "outcome report parent is not a plain directory: {parent}" + ))); + } + if candidate.name().is_empty() { + return Err(Exit::error("outcome report path has no filename")); + } + let resolved = fs::resolve(&parent) + .map_err(|error| Exit::error(os_error_message(&error, None)))? + .join(candidate.name()); + if fs::lexists(resolved.as_path()) { + return Err(Exit::error(format!( + "outcome report already exists: {resolved}" + ))); + } + Ok(resolved) +} + +/// Checks the destination of an outcome report before a workload runs, as +/// `validate_outcome_destination` does. +pub(crate) fn validate_outcome_destination(path: &str) -> Result { + outcome_destination(path).map(drop) +} + +/// Writes `value` to a new file at `path`, as `_write_new_json` does: through a temporary file of +/// mode 0600, synced, then linked to its name, which must not exist, with `\n` line ends on every +/// host. +fn write_new_json(path: &str, value: &Value) -> Result { + let resolved = outcome_destination(path)?; + let temporary = temporary_path(&resolved)?; + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600); + let mut file = match options.open(temporary.as_path()) { + Ok(file) => file, + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + return Err(Exit::error( + "failed to reserve an outcome report staging file", + )); + } + Err(error) => return Err(os_error(&error, &temporary)), + }; + let written = (|| -> Result { + let text = format!("{}\n", dumps::dumps(value)); + file.write_all(text.as_bytes()) + .and_then(|()| file.sync_all()) + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + match std::fs::hard_link(temporary.as_path(), resolved.as_path()) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Err(Exit::error( + format!("outcome report already exists: {resolved}"), + )), + Err(_) => Err(Exit::error(format!( + "failed to publish outcome report: {resolved}" + ))), + } + })(); + drop(file); + let _ = std::fs::remove_file(temporary.as_path()); + written +} + +/// The sections of an OpenVMM outcome report that a reader requires. +const OUTCOME_SECTIONS: [&str; 3] = ["outcome", "network_policy", "teardown"]; + +/// Reads an OpenVMM outcome report, as `_read_openvmm_outcome` does: of schema version 1, with +/// `outcome`, `network_policy`, and `teardown` objects. +fn read_openvmm_outcome(path: &PurePath) -> Result { + let document = read_json( + path, + "OpenVMM outcome report", + "schema_version", + &[OUTCOME_SCHEMA_VERSION], + )?; + for name in OUTCOME_SECTIONS { + let section = member(&document, document.root(), name); + if !section.is_some_and(|section| matches!(document.node(section), Node::Object(_))) { + return Err(Exit::error(format!( + "OpenVMM outcome report has an invalid {name} section: {path}" + ))); + } + } + Ok(document) +} + +/// Writes the outcome report of a managed workload, as `write_exec_outcome` does: its category +/// and status, with a random operation ID, and nothing of its output. +pub(crate) fn write_exec_outcome(path: &str, result: &ExecResult) -> Result { + if !EXIT_CATEGORIES.contains(&result.category) { + return Err(Exit::error( + "managed workload returned an unsupported outcome category", + )); + } + let operation_id = hex(&random_bytes::<16>()?); + write_new_json( + path, + &dict([ + ("schema_version", Value::from(OUTCOME_SCHEMA_VERSION)), + ("operation_id", Value::from(operation_id)), + ( + "outcome", + dict([ + ("operation", Value::from("exec")), + ("category", Value::from(result.category)), + ("status_code", Value::from(i64::from(result.returncode))), + ]), + ), + ]), + ) +} + +/// Records a launch in a new state directory, as `provision` does. +pub(crate) fn provision( + state_path: &str, + launch: &SandboxLaunch, + config: &ProvisionConfig<'_>, + timeout: f64, +) -> Result { + let state_dir = prepare_state_directory(state_path, true)?; + let config_path = state_dir.join(CONFIG_NAME); + let runtime_path = state_dir.join(RUNTIME_NAME); + let _lock = LifecycleLock::acquire(&state_dir, timeout, true)?; + if fs::exists(config_path.as_path()) || fs::exists(runtime_path.as_path()) { + return Err(Exit::error("sandbox is already provisioned")); + } + launch.validated()?; + write_json(&config_path, &serialize_launch(launch, config)?) +} + +/// The pid and start time that a runtime record names, as `_runtime_process` reads them. +struct RuntimeProcess { + /// The process ID, of any size. + pid: Int, + /// The start time, if the record has one. + start_time: Option, +} + +/// Reads the pid and start time of a runtime record, as `_runtime_process` does. +fn runtime_process(document: &Document) -> Result { + let invalid_pid = || Exit::error("sandbox runtime state has an invalid process ID"); + let root = document.root(); + let pid = member(document, root, "pid").ok_or_else(invalid_pid)?; + let pid = crate::host::python::value::py_int(document, pid).map_err(|error| match error { + crate::host::python::value::IntError::Invalid => invalid_pid(), + crate::host::python::value::IntError::Overflow => { + Exit::error("cannot convert float infinity to integer") + } + })?; + let start_time = match member(document, root, "start_time").map(|id| document.node(id)) { + None | Some(Node::Null) => None, + Some(Node::Integer(literal)) => { + let value = Int::parse(literal).map_err(|_| invalid_start_time())?; + if value.is_negative() { + return Err(invalid_start_time()); + } + Some(value) + } + Some(_) => return Err(invalid_start_time()), + }; + Ok(RuntimeProcess { pid, start_time }) +} + +/// Returns the error of a runtime record with an invalid start time. +fn invalid_start_time() -> Exit { + Exit::error("sandbox runtime state has an invalid process start time") +} + +/// A running sandbox, as `_load_running` finds it. +struct Running { + /// The runtime record. + runtime: Document, + /// The control capability. + capability: [u8; CAPABILITY_LEN], +} + +/// Reads the runtime record and capability of a running sandbox, as `_load_running` does. +fn load_running(state_dir: &PurePath) -> Result { + let runtime_path = state_dir.join(RUNTIME_NAME); + if !fs::is_file(runtime_path.as_path()) { + return Err(Exit::error("sandbox is not running")); + } + let runtime = read_json( + &runtime_path, + "sandbox runtime state", + "format", + &[STATE_FORMAT], + )?; + let process = runtime_process(&runtime)?; + if !process_running(&process.pid, process.start_time.as_ref())? { + return Err(Exit::error( + "sandbox runtime state is stale because the OpenVMM process is not running", + )); + } + let capability_path = state_dir.join(CAPABILITY_NAME); + require_file(capability_path.as_path(), "sandbox control capability")?; + let bytes = std::fs::read(capability_path.as_path()) + .map_err(|error| os_error(&error, &capability_path))?; + let capability: [u8; CAPABILITY_LEN] = bytes + .try_into() + .ok() + .filter(|capability| *capability != [0; CAPABILITY_LEN]) + .ok_or_else(|| Exit::error("sandbox control capability is invalid"))?; + Ok(Running { + runtime, + capability, + }) +} + +/// Returns the control endpoint of a runtime record, as `_endpoint` does. +fn endpoint(runtime: &Document) -> Result { + let value = member(runtime, runtime.root(), "control_endpoint") + .ok_or_else(|| Exit::error("sandbox runtime state has no control endpoint"))?; + py_str(runtime, value) + .ok_or_else(|| Exit::error("sandbox runtime state has no control endpoint")) +} + +/// Connects to the running sandbox of `running`. +fn connect(running: &Running, timeout: f64) -> Result { + let endpoint = endpoint(&running.runtime)?; + let process = runtime_process(&running.runtime)?; + // A running process has an ID of 32 bits. + let pid = process.pid.to_u32().unwrap_or_default(); + Session::connect(&endpoint, pid, &running.capability, timeout) +} + +/// Starts a provisioned sandbox, as `start` does. +pub(crate) fn start(state_path: &str, artifacts: &Artifacts<'_>, timeout: f64) -> Result { + let state_dir = prepare_state_directory(state_path, false)?; + let _lock = LifecycleLock::acquire(&state_dir, timeout, false)?; + start_locked(&state_dir, artifacts, timeout) +} + +/// The control features that the layered sandbox root must provide for the requests of +/// `nvx-rs sandbox`. +pub(crate) const REQUIRED_FEATURES: GuestFeatures = GuestFeatures::LAYERED_SANDBOX; + +/// Starts the sandbox of a locked state directory, as `_start` does. +fn start_locked(state_dir: &PurePath, artifacts: &Artifacts<'_>, timeout: f64) -> Result { + let config_path = state_dir.join(CONFIG_NAME); + require_file(config_path.as_path(), "sandbox configuration")?; + let config = read_json( + &config_path, + "sandbox configuration", + "format", + &CONFIG_FORMATS, + )?; + // Any runtime file belongs to an OpenVMM process that may still run, even without a runtime + // record, as after a start that was killed. Without them, every runtime file that appears + // while this start holds the lifecycle lock is its own. + if RUNTIME_FILE_NAMES + .iter() + .any(|name| fs::lexists(state_dir.join(name).as_path())) + { + return Err(Exit::error( + "sandbox is already running or has stale runtime state", + )); + } + let outcome_path = state_dir.join(OUTCOME_NAME); + unlink_missing_ok(&outcome_path)?; + let launch = deserialize_launch(&config)?; + let [executable, kernel, initrd] = artifacts.require()?; + let capability = random_bytes::()?; + if capability == [0; CAPABILITY_LEN] { + return Err(Exit::error( + "the system returned an all-zero control capability", + )); + } + let endpoint = if cfg!(windows) { + format!("//./pipe/openvmm-microvm-{}", uuid4_hex()?) + } else { + state_dir + .join(super::lifecycle::CONTROL_SOCKET_NAME) + .as_str() + .to_owned() + }; + let mut command = vec![executable]; + command.extend(launch.openvmm_arguments()?); + command.extend( + [ + "--microvm-lifecycle", + "managed", + "--single-process", + "--hypervisor", + ] + .map(str::to_owned), + ); + command.push(config_text(&config, "hypervisor")?); + command.push("--memory".to_owned()); + command.push(format!("{}M", config_int(&config, "memory_mib")?)); + command.extend(["--kernel".to_owned(), kernel, "--initrd".to_owned(), initrd]); + command.push("--cmdline".to_owned()); + command.push(launch.kernel_command_line(&config_text(&config, "cmdline")?)?); + command.extend(["--virtio-console", "none", "--microvm-control-console"].map(str::to_owned)); + command.push(format!("listen={endpoint}")); + command.push("--microvm-control-auth-stdin".to_owned()); + command.push("--microvm-report".to_owned()); + command.push(outcome_path.as_str().to_owned()); + extend_recorded_network_arguments(&mut command, &config)?; + + let capability_path = state_dir.join(CAPABILITY_NAME); + create_capability(&capability_path, &capability)?; + let log_path = state_dir.join(LOG_NAME); + let log = match OpenOptions::new() + .append(true) + .create(true) + .open(log_path.as_path()) + { + Ok(log) => log, + Err(error) => { + let _ = unlink_missing_ok(&capability_path); + return Err(os_error(&error, &log_path)); + } + }; + let mut launched: Option = None; + let started = launch_and_connect( + &command, + &capability, + log, + &endpoint, + state_dir, + timeout, + &mut launched, + ); + let Err(error) = started else { + if let Some(launched) = launched { + process::detach_child(launched); + } + return Ok(()); + }; + let mut status = None; + let mut ended = launched.is_none(); + if let Some(launched) = launched.as_mut() { + // OpenVMM closes the control endpoint as it tears the VM down, before it publishes its + // outcome report and exits, so only a closed endpoint warrants waiting for it. + let grace = if matches!(error, Exit::EndpointClosed) { + timeout + } else { + 0.0 + }; + match end_failed_start(launched, grace, &command) { + Ok(exited) => { + status = exited; + ended = true; + } + Err(exit) => return Err(exit), + } + } + if ended { + // This start found none of these files and has held the lifecycle lock since, so they are + // its own. The runtime record goes last, so that an interrupted cleanup leaves a record + // that shows OpenVMM gone rather than runtime files that deprovision cannot vouch for. + unlink_missing_ok(&state_dir.join(CONTROL_SOCKET_NAME))?; + unlink_missing_ok(&capability_path)?; + unlink_missing_ok(&state_dir.join(RUNTIME_NAME))?; + } + match status { + Some(status) if !matches!(error, Exit::Interrupted) => { + Err(Exit::error(startup_exit_message(&outcome_path, status)?)) + } + _ => Err(error), + } +} + +/// Launches OpenVMM, records it, and waits until its guest is ready and provides the features of +/// [`REQUIRED_FEATURES`], leaving the launched process in `launched` for the caller to end if +/// that fails. +fn launch_and_connect( + command: &[String], + capability: &[u8; CAPABILITY_LEN], + log: File, + endpoint: &str, + state_dir: &PurePath, + timeout: f64, + launched: &mut Option, +) -> Result { + let arguments: Vec = command[1..].iter().map(Into::into).collect(); + let directory = + std::env::current_dir().map_err(|error| Exit::error(os_error_message(&error, None)))?; + let child = process::spawn( + std::path::Path::new(&command[0]), + false, + &arguments, + capability, + log, + &directory, + ) + .map_err(|error| Exit::error(super::spawn_error(&error, &command[0])))?; + let child = launched.insert(child); + // Until this process reaps the child or closes its handle, no other process can reuse its ID, + // so this identifies OpenVMM itself. + let pid = child.id(); + let start_time = process_start_time(&Int::from(pid)) + .map_err(|error| Exit::error(format!("cannot identify the OpenVMM process: {error}")))?; + let Some(start_time) = start_time else { + let exited = child + .wait_timeout(Duration::ZERO) + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + return Err(Exit::error(if exited.is_some() { + "OpenVMM exited during startup" + } else { + "cannot identify the OpenVMM process" + })); + }; + write_json( + &state_dir.join(RUNTIME_NAME), + &dict([ + ("format", Value::from(STATE_FORMAT)), + ("pid", Value::from(pid)), + ("start_time", Value::from(start_time)), + ("control_endpoint", Value::from(endpoint)), + ]), + )?; + let mut session = Session::connect(endpoint, pid, capability, timeout)?; + session.ping(timeout)?; + let features = session.features(timeout)?; + let missing = features.missing(REQUIRED_FEATURES); + if !missing.is_empty() { + return Err(Exit::error(format!( + "the guest image lacks control features that layered sandboxes need ({}); use an \ + NVX release or build that includes them", + missing.join(", ") + ))); + } + Ok(()) +} + +/// Appends the network options that a configuration records, in the order of `_start`. +fn extend_recorded_network_arguments(command: &mut Vec, config: &Document) -> Result { + let root = config.root(); + let text = + |id| py_str(config, id).ok_or_else(|| Exit::error("sandbox configuration is malformed")); + let present = + |name| member(config, root, name).filter(|&id| !matches!(config.node(id), Node::Null)); + if let Some(net) = present("net") { + command.push("--net".to_owned()); + command.push(text(net)?); + command.push("--network-profile".to_owned()); + command.push(match member(config, root, "network_profile") { + Some(profile) => text(profile)?, + None => "None".to_owned(), + }); + } + for name in ["network_egress", "network_ingress", "host_loopback"] { + if let Some(value) = present(name) { + command.push(format!("--{}", name.replace('_', "-"))); + command.push(text(value)?); + } + } + for name in [ + "network_egress_allow", + "network_egress_deny", + "host_loopback_forward", + ] { + let Some(values) = member(config, root, name) else { + continue; + }; + let Node::Array(items) = config.node(values) else { + return Err(Exit::error("sandbox configuration is malformed")); + }; + for &item in items { + command.push(format!("--{}", name.replace('_', "-"))); + command.push(text(item)?); + } + } + if let Some(proxy) = present("network_proxy") { + command.push("--network-proxy".to_owned()); + command.push(text(proxy)?); + } + Ok(()) +} + +/// Creates the control capability file of a start, which only it may hold, as +/// `_create_capability` does. +fn create_capability(path: &PurePath, capability: &[u8; CAPABILITY_LEN]) -> Result { + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600); + let mut file = match options.open(path.as_path()) { + Ok(file) => file, + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + return Err(Exit::error( + "sandbox is already running or has stale runtime state", + )); + } + Err(error) => return Err(os_error(&error, path)), + }; + if let Err(error) = file.write_all(capability) { + drop(file); + let _ = unlink_missing_ok(path); + return Err(os_error(&error, path)); + } + Ok(()) +} + +/// Ends the OpenVMM process of a failed start, as `_end_failed_start` does: returns OpenVMM's +/// status if it exits on its own within `grace` seconds, or terminates it and returns `None`. +/// Terminating OpenVMM while it publishes its outcome report would leave the report's staging +/// file in the state directory, which deprovision refuses to remove. +fn end_failed_start( + launched: &mut Launched, + grace: f64, + command: &[String], +) -> Result> { + let waited = if grace > 0.0 { + wait_interruptibly(launched, Deadline::after(grace)) + } else { + Ok(()) + }; + let status = launched + .wait_timeout(Duration::ZERO) + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + if status.is_none() { + launched + .terminate() + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + let exited = launched + .wait_timeout(Duration::from_secs(5)) + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + if exited.is_none() { + launched + .kill() + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + let killed = launched + .wait_timeout(Duration::from_secs(5)) + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + if killed.is_none() { + let arguments: Vec = + command.iter().map(|argument| repr(argument)).collect(); + return Err(Exit::error(format!( + "Command '[{}]' timed out after 5 seconds", + arguments.join(", ") + ))); + } + } + } + // Ctrl-C during the grace period ends the start as interrupted, after OpenVMM. + waited?; + Ok(status) +} + +/// Waits until `launched` exits or `deadline` passes, failing if Ctrl-C arrives. +fn wait_interruptibly(launched: &mut Launched, deadline: Deadline) -> Result { + loop { + let slice = deadline + .remaining() + .map_or(RETRY_INTERVAL, |left| left.min(RETRY_INTERVAL)); + crate::host::cli::check_interrupt()?; + if launched + .wait_timeout(slice) + .map_err(|error| Exit::error(os_error_message(&error, None)))? + .is_some() + || deadline.passed() + { + return Ok(()); + } + } +} + +/// Describes how OpenVMM ended a start, preferring its outcome report, which keeps the guest's +/// own status, as `_startup_exit_message` does. +fn startup_exit_message(outcome_path: &PurePath, status: i64) -> Result { + let report = match read_openvmm_outcome(outcome_path) { + Ok(report) => Some(report), + // A `ValueError` of `json.loads` escapes the `except ScriptError`. + Err(exit) + if exit + .message() + .is_some_and(|message| message.starts_with("Exceeds the limit")) => + { + return Err(exit); + } + Err(_) => None, + }; + if let Some(report) = &report + && let Some(outcome) = member(report, report.root(), "outcome") + && let Some(category) = member(report, outcome, "category") + && let Some(status_code) = member(report, outcome, "status_code") + && let Node::String(category) = report.node(category) + && let Node::Integer(status_code) = report.node(status_code) + { + let status_code = Int::parse(status_code).map_err(|_| Exit::error("invalid status"))?; + return Ok(format!( + "OpenVMM exited during startup: {} status {status_code}", + category.render() + )); + } + Ok(format!( + "OpenVMM exited during startup with status {status}" + )) +} + +/// Runs a workload in a running sandbox, as `exec_workload` does. +pub(crate) fn exec_workload( + state_path: &str, + request: &ExecRequest, + response_timeout: f64, +) -> Result { + let state_dir = prepare_state_directory(state_path, false)?; + let running = load_running(&state_dir)?; + let mut session = connect(&running, response_timeout)?; + session.exec(request, response_timeout) +} + +/// Stops a running sandbox and returns OpenVMM's outcome report, as `stop` does. +pub(crate) fn stop(state_path: &str, timeout: f64) -> Result { + let state_dir = prepare_state_directory(state_path, false)?; + let _lock = LifecycleLock::acquire(&state_dir, timeout, false)?; + let running = load_running(&state_dir)?; + let process = runtime_process(&running.runtime)?; + { + let mut session = connect(&running, timeout)?; + session.stop(timeout)?; + } + let deadline = Deadline::after(timeout); + while process_running(&process.pid, process.start_time.as_ref())? { + if deadline.passed() { + return Err(Exit::error("OpenVMM did not terminate after managed stop")); + } + sleep(RETRY_INTERVAL)?; + } + let outcome = read_openvmm_outcome(&state_dir.join(OUTCOME_NAME)); + // As in a failed start, the runtime record goes last. + unlink_missing_ok(&state_dir.join(CONTROL_SOCKET_NAME))?; + unlink_missing_ok(&state_dir.join(CAPABILITY_NAME))?; + unlink_missing_ok(&state_dir.join(RUNTIME_NAME))?; + outcome +} + +/// Removes a stopped sandbox and its state directory, as `deprovision` does. +pub(crate) fn deprovision(state_path: &str, timeout: f64) -> Result { + let state_dir = prepare_state_directory(state_path, false)?; + let mut lock = LifecycleLock::acquire(&state_dir, timeout, false)?; + let runtime_path = state_dir.join(RUNTIME_NAME); + if fs::is_file(runtime_path.as_path()) { + let runtime = read_json( + &runtime_path, + "sandbox runtime state", + "format", + &[STATE_FORMAT], + )?; + let process = runtime_process(&runtime)?; + if process_running(&process.pid, process.start_time.as_ref())? { + return Err(Exit::error("sandbox must be stopped before deprovision")); + } + } else if RUNTIME_FILE_NAMES + .iter() + .any(|name| fs::lexists(state_dir.join(name).as_path())) + { + // As start does, refuse runtime files that no record vouches for. + return Err(Exit::error(format!( + "sandbox has runtime files but no runtime record, so its OpenVMM process may still \ + run: end any OpenVMM process whose arguments name the state directory, then remove \ + {CAPABILITY_NAME} and {CONTROL_SOCKET_NAME}" + ))); + } + for name in [ + RUNTIME_NAME, + CAPABILITY_NAME, + CONTROL_SOCKET_NAME, + OUTCOME_NAME, + LOG_NAME, + CONFIG_NAME, + ] { + unlink_missing_ok(&state_dir.join(name))?; + } + let unknown: Vec = entries(&state_dir) + .map_err(|error| os_error(&error, &state_dir))? + .into_iter() + .filter(|name| name != LOCK_NAME) + .collect(); + if !unknown.is_empty() { + // `iterdir` lists the names in the order of the directory, which the message keeps. + return Err(Exit::error(format!( + "sandbox state directory contains files not owned by NVX: {}", + unknown.join(", ") + ))); + } + // Holding the lock until the directory is gone keeps a waiting transition from creating a new + // lock file in it. + lock.remove()?; + let lingering = match std::fs::remove_dir(state_dir.as_path()) { + Ok(()) => false, + Err(error) if directory_not_empty(&error) => true, + Err(error) => { + lock.retract(); + return Err(os_error(&error, &state_dir)); + } + }; + if !lingering { + lock.retract(); + return Ok(()); + } + // A removed lock file that a handle still keeps stays in the directory, as do the lock files of + // transitions that start meanwhile. Each waiter closes its handle once it finds the sandbox + // gone, so the directory empties after this call releases the lock. + let (removing, held) = hold_directory(&state_dir)?; + lock.release(); + let removed = remove_emptied_state_directory(&state_dir, removing, timeout); + drop(held); + removed +} + +/// Returns the identity of a directory, and a handle that keeps it, as `_hold_directory` does: on +/// POSIX, the open directory keeps a directory that replaces it from reusing its inode number; +/// NTFS and ReFS do not reuse file IDs. +fn hold_directory(path: &PurePath) -> Result<((u64, u128), Option)> { + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + + let file = OpenOptions::new() + .read(true) + .custom_flags((nix::fcntl::OFlag::O_DIRECTORY | nix::fcntl::OFlag::O_NOFOLLOW).bits()) + .open(path.as_path()) + .map_err(|error| os_error(&error, path))?; + let status = + fs::fstat(&file).map_err(|error| Exit::error(os_error_message(&error, None)))?; + Ok((status.identity, Some(file))) + } + #[cfg(not(unix))] + { + let status = fs::lstat(path.as_path()).map_err(|error| os_error(&error, path))?; + Ok((status.identity(), None)) + } +} + +/// Removes a state directory once no lingering lock file keeps it, as +/// `_remove_emptied_state_directory` does. A provision that waits for the removal may complete it +/// and provision the path again, which ends this attempt too. +fn remove_emptied_state_directory( + state_dir: &PurePath, + removing: (u64, u128), + timeout: f64, +) -> Result { + let deadline = Deadline::after(timeout); + let mut retried_at_once = false; + loop { + let error = match std::fs::remove_dir(state_dir.as_path()) { + Ok(()) => return Ok(()), + Err(error) if os_error_class(&error) == OsErrorClass::NotFound => return Ok(()), + Err(error) => error, + }; + if directory_status(state_dir) != Some(removing) { + return Ok(()); + } + let remaining = match entries(state_dir) { + Ok(remaining) => remaining, + Err(error) if os_error_class(&error) == OsErrorClass::NotFound => return Ok(()), + Err(error) => return Err(os_error(&error, state_dir)), + }; + let mut unexpected: Vec = remaining + .iter() + .filter(|name| !lingers(name)) + .cloned() + .collect(); + unexpected.sort(); + // A provision that starts after this deprovision removed the lock file, and before it + // removed the directory, provisions it again. + if unexpected.iter().any(|name| name == CONFIG_NAME) { + return Err(Exit::error( + "sandbox was provisioned again while deprovision removed it", + )); + } + if !unexpected.is_empty() { + return Err(Exit::error(format!( + "sandbox state directory contains files not owned by NVX: {}", + unexpected.join(", ") + ))); + } + if !directory_not_empty(&error) { + return Err(os_error(&error, state_dir)); + } + // Without remaining entries, the last lingering lock file went away after rmdir failed, + // so the next attempt follows at once. Another such failure right after it pauses first, + // so that a file system that does not report emptiness right away cannot spin. + if remaining.is_empty() && !retried_at_once { + retried_at_once = true; + if deadline.passed() { + return Err(os_error(&error, state_dir)); + } + continue; + } + retried_at_once = false; + if !deadline.pause(LOCK_RETRY_INTERVAL)? { + return Err(os_error(&error, state_dir)); + } + } +} diff --git a/nvx-rs/src/host/cli/sandbox/lock.rs b/nvx-rs/src/host/cli/sandbox/lock.rs new file mode 100644 index 000000000..de5becec2 --- /dev/null +++ b/nvx-rs/src/host/cli/sandbox/lock.rs @@ -0,0 +1,507 @@ +//! The lifecycle lock of a state directory, `_LifecycleLock` of the former `sandbox_lifecycle.py`, +//! with the file primitives that it shares with that Python implementation, so that an older +//! `nvx.py` and `nvx-rs` exclude each other: +//! +//! - `lifecycle.lock` is opened or created without following a link that it names: with +//! `O_NOFOLLOW` on POSIX, and on Windows with `FILE_FLAG_OPEN_REPARSE_POINT` and every sharing +//! mode, so that deprovision can delete it while others wait for it. It must be a regular file +//! with one link, and not a reparse point. +//! - The lock is `flock(LOCK_EX | LOCK_NB)` on POSIX, and on Windows the lock of the file's first +//! byte, as `msvcrt.locking(fd, LK_NBLCK, 1)` takes it at offset 0. A caller that cannot take +//! it retries every 25 ms within its timeout. +//! - Deprovision writes `deprovisioned\n` into the lock file before it removes the file, so that +//! a waiter that then takes the lock of the removed file knows that the directory goes away. +//! +//! [`LifecycleLock::acquire`] and [`LifecycleLock::release`] follow `__enter__` and `release` +//! step by step; see the comments of `_LifecycleLock`, in the history of +//! `scripts/nvx_tools/sandbox_lifecycle.py`, for the protocol. + +use std::fs::{File, OpenOptions}; +use std::io::{self, Read, Seek, SeekFrom, Write}; + +use super::lifecycle::{ + CONFIG_NAME, LOCK_NAME, LOCK_RETRY_INTERVAL, LOCK_TOMBSTONE, prepare_state_directory, +}; +use crate::host::cli::deadline::Deadline; +use crate::host::cli::{Exit, Result}; +use crate::host::python::fs::{self, os_error_message}; +use crate::host::python::path::PurePath; + +/// How Python classifies an `OSError`, as far as the lifecycle tells the classes apart. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum OsErrorClass { + /// `FileNotFoundError`: `ENOENT`. + NotFound, + /// `PermissionError`: `EACCES` or `EPERM`. + PermissionDenied, + /// Any other `OSError`. + Other, +} + +/// Returns the class of the `OSError` that Python raises for `error`. On Windows, Python maps the +/// Win32 error to an `errno` as `winerror_to_errno` does. +pub(crate) fn os_error_class(error: &io::Error) -> OsErrorClass { + #[cfg(windows)] + if let Some(code) = error.raw_os_error() { + return match code { + 2 | 3 | 15 | 18 | 53 | 67 | 161 | 206 => OsErrorClass::NotFound, + 5 | 16 | 19..=36 | 65 | 82 | 83 | 108 | 132 | 158 | 167 => { + OsErrorClass::PermissionDenied + } + _ => OsErrorClass::Other, + }; + } + match error.kind() { + io::ErrorKind::NotFound => OsErrorClass::NotFound, + io::ErrorKind::PermissionDenied => OsErrorClass::PermissionDenied, + _ => OsErrorClass::Other, + } +} + +/// Returns whether `error` is `ELOOP`, with which `O_NOFOLLOW` refuses a symbolic link. +fn is_loop(error: &io::Error) -> bool { + #[cfg(unix)] + return error.raw_os_error() == Some(nix::errno::Errno::ELOOP as i32); + #[cfg(not(unix))] + { + let _ = error; + false + } +} + +/// Returns the error of a lock file that is not a regular file with one name. +fn not_standalone(path: &PurePath) -> Exit { + Exit::error(format!( + "sandbox lifecycle lock is not a standalone regular file: {path}" + )) +} + +/// Returns the message of an `OSError` that opening the lock file at `path` raised: with the +/// file's name from `os.open` on POSIX, and without it from `ctypes.WinError` on Windows. +fn open_error(error: &io::Error, path: &PurePath) -> Exit { + let path = if cfg!(windows) { + None + } else { + Some(path.as_str()) + }; + Exit::error(os_error_message(error, path)) +} + +/// Opens or creates `path` without following a link that it names, as `_open_lock_path` does. +fn open_lock_path(path: &PurePath) -> io::Result { + let mut options = OpenOptions::new(); + options.read(true).write(true).create(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + + options + .mode(0o600) + .custom_flags(nix::fcntl::OFlag::O_NOFOLLOW.bits()); + } + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt; + + /// `FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE`. + const SHARE_ALL: u32 = 0x7; + /// `FILE_ATTRIBUTE_NORMAL`. + const NORMAL: u32 = 0x80; + /// `FILE_FLAG_OPEN_REPARSE_POINT`. + const OPEN_REPARSE_POINT: u32 = 0x0020_0000; + options + .share_mode(SHARE_ALL) + .attributes(NORMAL) + .custom_flags(OPEN_REPARSE_POINT); + } + options.open(path.as_path()) +} + +/// Why opening the lock file failed. +enum OpenFailure { + /// An `OSError` that the caller may handle. + Os(io::Error), + /// Any other failure. + Exit(Exit), +} + +/// Opens the lock file of a state directory, creating it if needed, as `_open_lock_file` does: +/// it must be a regular file that no other name links, and not a reparse point. +fn open_lock_file(path: &PurePath) -> std::result::Result { + let file = match open_lock_path(path) { + Ok(file) => file, + Err(error) if is_loop(&error) => return Err(OpenFailure::Exit(not_standalone(path))), + Err(error) => return Err(OpenFailure::Os(error)), + }; + let status = fs::fstat(&file) + .map_err(|error| OpenFailure::Exit(Exit::error(os_error_message(&error, None))))?; + if !status.regular || status.links != 1 || status.reparse_point { + return Err(OpenFailure::Exit(not_standalone(path))); + } + Ok(file) +} + +/// Takes the exclusive lock of an open lock file unless another caller holds it, and returns +/// whether it took it, as `_try_lock_file` does. +fn try_lock_file(file: &File) -> Result { + #[cfg(windows)] + return Ok(crate::host::platform::try_lock_first_byte(file)); + #[cfg(not(windows))] + match file.try_lock() { + Ok(()) => Ok(true), + Err(std::fs::TryLockError::WouldBlock) => Ok(false), + Err(std::fs::TryLockError::Error(error)) => { + Err(Exit::error(os_error_message(&error, None))) + } + } +} + +/// Releases the lock of an open lock file, as `_unlock_file` does. +fn unlock_file(file: &File) -> io::Result<()> { + #[cfg(windows)] + return crate::host::platform::unlock_first_byte(file); + #[cfg(not(windows))] + file.unlock() +} + +/// Returns whether `path` still names the open `file`, as `_names_open_file` does. Windows keeps +/// a removed file in its directory until its last handle closes, and denies access to it +/// meanwhile, which counts as another file. +fn names_open_file(path: &PurePath, file: &File) -> io::Result { + let current = match fs::stat(path.as_path()) { + Ok(current) => current, + Err(error) => { + return match os_error_class(&error) { + OsErrorClass::NotFound => Ok(false), + OsErrorClass::PermissionDenied if cfg!(windows) => Ok(false), + _ => Err(error), + }; + } + }; + Ok(current.identity() == fs::fstat(file)?.identity) +} + +/// Reads up to `size` bytes from the start of the lock file, as `_read_lock_file` does. +fn read_lock_file(mut file: &File, size: usize) -> io::Result> { + file.seek(SeekFrom::Start(0))?; + let mut data = Vec::with_capacity(size); + let read = (&mut file).take(size as u64).read_to_end(&mut data); + file.seek(SeekFrom::Start(0))?; + read.map(|_| data) +} + +/// Writes `data` over the start of the lock file, as `_write_lock_file` does. +fn write_lock_file(mut file: &File, data: &[u8]) -> io::Result<()> { + file.seek(SeekFrom::Start(0))?; + let mut remaining = data; + let written = (|| { + while !remaining.is_empty() { + let count = file.write(remaining)?; + if count == 0 { + return Err(io::Error::other( + "sandbox lifecycle lock write made no progress", + )); + } + remaining = &remaining[count..]; + } + Ok(()) + })(); + // `msvcrt.locking()` unlocks from the file position, as it locked. + file.seek(SeekFrom::Start(0))?; + written +} + +/// Returns the error of a transition that still waits for another: Python's `TimeoutError`. +pub(crate) fn transition_in_progress() -> Exit { + Exit::error("another lifecycle transition of the sandbox is still in progress") +} + +/// Returns `os.lstat(path)`'s identity, or `None` if the path cannot be examined, as +/// `_directory_status` does. +pub(crate) fn directory_status(path: &PurePath) -> Option<(u64, u128)> { + fs::lstat(path.as_path()) + .ok() + .map(|status| status.identity()) +} + +/// Returns whether `name` is a removed lock file that a handle still keeps, as `_lingers` does: +/// NFS renames a removed file that is still open to `.nfs*`, and Windows file systems without +/// POSIX deletion keep it under its own name. +pub(crate) fn lingers(name: &str) -> bool { + name == LOCK_NAME || name.starts_with(".nfs") +} + +/// Returns whether `error` is how `rmdir` reports a directory that is not empty. +pub(crate) fn directory_not_empty(error: &io::Error) -> bool { + #[cfg(windows)] + let not_empty = matches!(error.raw_os_error(), Some(145 | 80 | 183)); + #[cfg(unix)] + let not_empty = matches!( + error.raw_os_error(), + Some(code) if code == nix::errno::Errno::ENOTEMPTY as i32 + || code == nix::errno::Errno::EEXIST as i32 + ); + #[cfg(not(any(windows, unix)))] + let not_empty = error.kind() == io::ErrorKind::DirectoryNotEmpty; + not_empty +} + +/// Returns the names of the entries of `directory`. +pub(crate) fn entries(directory: &PurePath) -> io::Result> { + std::fs::read_dir(directory.as_path())? + .map(|entry| { + let name = entry?.file_name(); + name.into_string() + .map_err(|_| crate::host::python::not_unicode("a file name")) + }) + .collect() +} + +/// Waits while lingering lock files keep the directory that deprovision removes, as +/// `_wait_for_handoff` does. The wait ends once the path names no directory or another one, or +/// the directory holds anything else; a directory that nothing keeps any longer is removed here, +/// in case deprovision has given up. +fn wait_for_handoff(state_dir: &PurePath, removing: (u64, u128), deadline: Deadline) -> Result { + loop { + if directory_status(state_dir) != Some(removing) { + return Ok(()); + } + let Ok(names) = entries(state_dir) else { + return Ok(()); + }; + if !names.iter().all(|name| lingers(name)) { + return Ok(()); + } + if names.is_empty() { + match std::fs::remove_dir(state_dir.as_path()) { + Ok(()) => return Ok(()), + Err(error) if !directory_not_empty(&error) => return Ok(()), + Err(_) => {} + } + } + if !deadline.pause(LOCK_RETRY_INTERVAL)? { + return Err(transition_in_progress()); + } + } +} + +/// The exclusive lock that serializes the lifecycle transitions of a state directory. +pub(crate) struct LifecycleLock { + /// The state directory. + state_dir: PurePath, + /// Its lock file. + path: PurePath, + /// The locked file, until the lock is released. + file: Option, +} + +impl LifecycleLock { + /// Takes the lock of `state_dir` within `timeout` seconds, as `_LifecycleLock(state_dir, + /// timeout, provision=provision).__enter__()` does. + /// + /// A caller that takes the lock of a lock file that its holder removed meanwhile holds no + /// lock. Unless it provisions, it retries only while the sandbox is still provisioned, and a + /// provision that finds the lock file or the directory removed waits until the old directory + /// is gone, then provisions a new one, unless another provision has done so first. + pub(crate) fn acquire(state_dir: &PurePath, timeout: f64, provision: bool) -> Result { + if !(timeout > 0.0 && timeout < f64::INFINITY) { + return Err(Exit::error( + "lifecycle timeout must be finite and greater than 0", + )); + } + let path = state_dir.join(LOCK_NAME); + let deadline = Deadline::after(timeout); + loop { + let file = match open_lock_file(&path) { + Ok(file) => file, + Err(OpenFailure::Exit(exit)) => return Err(exit), + Err(OpenFailure::Os(error)) => match os_error_class(&error) { + // Deprovision removed the state directory meanwhile, which only a provision + // creates again. + OsErrorClass::NotFound => { + if !provision { + return Err(Exit::error(format!( + "sandbox state path is not a plain directory: {state_dir}" + ))); + } + if deadline.passed() { + return Err(transition_in_progress()); + } + prepare_state_directory(state_dir.as_str(), true)?; + continue; + } + // Without POSIX deletion, Windows denies opening a removed lock file until + // its last handle closes. + OsErrorClass::PermissionDenied + if provision && cfg!(windows) && deadline.pause(LOCK_RETRY_INTERVAL)? => + { + continue; + } + _ => return Err(open_error(&error, &path)), + }, + }; + while !try_lock_file(&file)? { + if !deadline.pause(LOCK_RETRY_INTERVAL)? { + return Err(transition_in_progress()); + } + } + if names_open_file(&path, &file) + .map_err(|error| Exit::error(os_error_message(&error, Some(path.as_str()))))? + { + return Ok(Self { + state_dir: state_dir.clone(), + path, + file: Some(file), + }); + } + let tombstone = read_lock_file(&file, LOCK_TOMBSTONE.len()) + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + // Deprovision leaves the mark only if lingering lock files, such as the one that this + // handle may keep, kept it from removing the directory, which the path then still + // names. + let removing = if provision && tombstone == LOCK_TOMBSTONE { + directory_status(state_dir) + } else { + None + }; + drop(file); + if !provision { + if !fs::lexists(state_dir.join(CONFIG_NAME).as_path()) { + return Err(Exit::error("sandbox is not provisioned")); + } + } else if let Some(removing) = removing { + wait_for_handoff(state_dir, removing, deadline)?; + } + if deadline.passed() { + return Err(transition_in_progress()); + } + } + } + + /// Removes the held lock file, marking it so that waiters know why, as `remove` does. If the + /// removal fails, the mark goes again, as deprovision then keeps the directory. + pub(crate) fn remove(&mut self) -> Result { + let file = self.file.as_ref().expect("the lock is held"); + let removed = write_lock_file(file, LOCK_TOMBSTONE) + .map_err(|error| Exit::error(os_error_message(&error, None))) + .and_then(|()| { + std::fs::remove_file(self.path.as_path()).map_err(|error| { + Exit::error(os_error_message(&error, Some(self.path.as_str()))) + }) + }); + if removed.is_err() { + self.retract(); + } + removed + } + + /// Clears the mark of the removed lock file, so that waiters retry at once, as `retract` + /// does. + pub(crate) fn retract(&mut self) { + if let Some(file) = &self.file { + let _ = write_lock_file(file, &[0; LOCK_TOMBSTONE.len()]); + } + } + + /// Releases the lock, as `release` does: the lock file of a directory that holds no + /// provisioned sandbox goes with it. + pub(crate) fn release(&mut self) { + let Some(file) = self.file.take() else { + return; + }; + if !fs::lexists(self.state_dir.join(CONFIG_NAME).as_path()) + && names_open_file(&self.path, &file).unwrap_or(false) + { + let _ = std::fs::remove_file(self.path.as_path()); + } + // Closing the file releases the lock even if the unlock fails. + let _ = unlock_file(&file); + } +} + +impl Drop for LifecycleLock { + fn drop(&mut self) { + self.release(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn state_dir(directory: &tempfile::TempDir) -> PurePath { + PurePath::native(directory.path().to_str().unwrap()) + } + + #[test] + fn the_lock_excludes_other_holders_until_released() { + let directory = tempfile::tempdir().unwrap(); + let state = state_dir(&directory); + std::fs::write(state.join(CONFIG_NAME).as_path(), "{}").unwrap(); + let mut held = LifecycleLock::acquire(&state, 10.0, false).unwrap(); + let started = std::time::Instant::now(); + let error = LifecycleLock::acquire(&state, 0.1, false).err().unwrap(); + assert!(started.elapsed() >= std::time::Duration::from_millis(100)); + assert_eq!( + error.message(), + Some("another lifecycle transition of the sandbox is still in progress") + ); + held.release(); + drop(LifecycleLock::acquire(&state, 10.0, false).unwrap()); + // A provisioned directory keeps its lock file. + assert!(state.join(LOCK_NAME).as_path().is_file()); + } + + #[test] + fn released_locks_leave_no_file_in_unprovisioned_directories() { + let directory = tempfile::tempdir().unwrap(); + let state = state_dir(&directory); + drop(LifecycleLock::acquire(&state, 10.0, true).unwrap()); + assert!(!state.join(LOCK_NAME).as_path().exists()); + let error = LifecycleLock::acquire(&state.join("missing"), 1.0, false) + .err() + .unwrap(); + assert_eq!( + error.message().unwrap(), + format!( + "sandbox state path is not a plain directory: {}", + state.join("missing") + ) + ); + } + + #[test] + fn linked_lock_files_are_refused() { + let directory = tempfile::tempdir().unwrap(); + let state = state_dir(&directory); + let other = state.join("other"); + std::fs::write(other.as_path(), "").unwrap(); + if std::fs::hard_link(other.as_path(), state.join(LOCK_NAME).as_path()).is_err() { + return; + } + let error = LifecycleLock::acquire(&state, 1.0, true).err().unwrap(); + assert_eq!( + error.message().unwrap(), + format!( + "sandbox lifecycle lock is not a standalone regular file: {}", + state.join(LOCK_NAME) + ) + ); + } + + #[test] + fn deprovision_marks_the_removed_lock_file() { + let directory = tempfile::tempdir().unwrap(); + let state = state_dir(&directory); + let mut lock = LifecycleLock::acquire(&state, 10.0, true).unwrap(); + let file = lock.file.as_ref().unwrap().try_clone().unwrap(); + lock.remove().unwrap(); + assert_eq!(read_lock_file(&file, 64).unwrap(), LOCK_TOMBSTONE); + lock.retract(); + assert_eq!( + read_lock_file(&file, 64).unwrap(), + vec![0; LOCK_TOMBSTONE.len()] + ); + assert!(!state.join(LOCK_NAME).as_path().exists()); + } +} diff --git a/nvx-rs/src/host/cli/sandbox/mod.rs b/nvx-rs/src/host/cli/sandbox/mod.rs new file mode 100644 index 000000000..f2ed06754 --- /dev/null +++ b/nvx-rs/src/host/cli/sandbox/mod.rs @@ -0,0 +1,795 @@ +//! `nvx-rs sandbox`: `nvx.py sandbox`, ported from `command_sandbox` and the `sandbox` parser of +//! `scripts/nvx.py`, with its options, defaults, checks, outputs, and exit statuses. +//! +//! - `run`, the default, boots a one-shot sandbox: it prints the OpenVMM command as `>> COMMAND`, +//! then runs it on the terminal, unless `--dry-run`, and exits with OpenVMM's status. +//! - `provision`, `start`, `exec`, `stop`, and `deprovision` manage a persistent sandbox in its +//! `--state-dir`; see [`lifecycle`]. +//! +//! The parser reads the options of `nvx.py sandbox` with their names, defaults, types, and +//! choices, and fails as `argparse` fails, with status 2. The `--mount-deny`, `--mount-allow`, +//! and `--mount-write` options name paths of the `--mount` before them. + +mod identity; +mod lifecycle; +mod lock; +mod state; + +use std::process::{Child, Command}; +use std::thread; +use std::time::Duration; + +use super::args::{Parser, Receiver, Spec, Syntax, invalid_choice, invalid_value}; +use super::artifacts::{Located, locate}; +use super::{Exit, Result, output}; +use crate::host::artifacts::{Guest, KERNEL_NAME}; +use crate::host::network::policy::compile_policy_file; +use crate::host::platform; +use crate::host::python::builtins::{Int, parse_float}; +use crate::host::python::path::PurePath; +use crate::host::python::shell::format_command; +use crate::host::sandbox::mount::{MountOwner, PolicyKind}; +use crate::host::sandbox::options::{ + PolicyPathOption, SandboxOperation, SandboxOptions, sandbox_mounts, + validate_sandbox_systemd_policy, +}; +use crate::host::sandbox::{ + DEFAULT_ENTRYPOINT, DEFAULT_HOSTNAME, DEFAULT_WORKLOAD_IDENTITY, SandboxLaunch, SandboxLayer, + SandboxSettings, parse_workload_identity, require_file, +}; + +pub(crate) use self::lifecycle::ProvisionConfig; + +/// The usage and help of the parser. +const SYNTAX: Syntax = Syntax { + command: "sandbox", + optionals: &[ + "[-h]", + "[--layer ROLE,PATH,EROFS_UUID]", + "[--scratch SCRATCH]", + "[--state-dir STATE_DIR]", + "[--entrypoint ENTRYPOINT]", + "[--arg SANDBOX_ARG]", + "[--hostname HOSTNAME]", + "[--workload-user UID:GID]", + "[--memory-max MEMORY_MAX]", + "[--pids-max PIDS_MAX]", + "[--memory-mib MEMORY_MIB]", + "[--timeout TIMEOUT]", + "[--exec-timeout-ms EXEC_TIMEOUT_MS]", + "[--cwd CWD]", + "[--environment KEY=VALUE]", + "[--environment-file PATH]", + "[--inherit-default-environment]", + "[--hypervisor {auto,whp,kvm,mshv}]", + "[--mount GUEST_TARGET,HOST_PATH[,ro|rw]]", + "[--mount-deny HOST_PATH]", + "[--mount-allow HOST_PATH]", + "[--mount-write HOST_PATH]", + "[--mount-owner {vmm,caller}]", + "[--net IPV4/PREFIX]", + "[--network-profile {portable}]", + "[--network-egress {allow,deny}]", + "[--network-ingress {allow,deny}]", + "[--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]", + "[--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]", + "[--network-egress-policy-file PATH]", + "[--host-loopback {allow,deny}]", + "[--network-proxy IPV4:TCP-PORT]", + "[--host-loopback-forward HOST_LOOPBACK_FORWARD]", + "[--outcome-report OUTCOME_REPORT]", + "[--cmdline CMDLINE]", + "[--dry-run]", + ], + positionals: &["[{run,provision,start,exec,stop,deprovision}]"], + help: "\ +positional arguments: + {run,provision,start,exec,stop,deprovision} + +options: + -h, --help show this help message and exit + --layer ROLE,PATH,EROFS_UUID + --scratch SCRATCH + --state-dir STATE_DIR + --entrypoint ENTRYPOINT + --arg SANDBOX_ARG + --hostname HOSTNAME + --workload-user UID:GID + fixed non-root workload identity (default: + 65534:65534) + --memory-max MEMORY_MAX + --pids-max PIDS_MAX + --memory-mib MEMORY_MIB + --timeout TIMEOUT control operation timeout, and the longest wait for + another lifecycle transition of the sandbox, in + seconds (default: 60) + --exec-timeout-ms EXEC_TIMEOUT_MS + guest workload timeout in milliseconds; zero disables + it + --cwd CWD absolute guest working directory for managed exec + (default: /) + --environment KEY=VALUE + set the exact managed exec environment; repeat for + multiple entries + --environment-file PATH + read the exact managed exec environment from a JSON + string array + --inherit-default-environment + layer the managed exec environment over the guest + default environment instead of replacing it + --hypervisor {auto,whp,kvm,mshv} + --mount GUEST_TARGET,HOST_PATH[,ro|rw] + live-share a host directory, or a single regular host + file without its directory, inside the container + rootfs; the mode follows the last comma, so HOST_PATH + may contain commas when the mode is given; repeat to + attach more shares, each with its own target and mode, + as children of one virtio-fs device + --mount-deny HOST_PATH + hide one existing path inside a --mount host + directory; with several --mount options, it applies to + the --mount before it + --mount-allow HOST_PATH + expose one existing path inside a --mount-deny path of + the same share again; the hidden directories on the + way list only it + --mount-write HOST_PATH + make one existing path one of the only writable parts + of a read-write --mount; the rest of the share is + read-only + --mount-owner {vmm,caller} + host identity of the share's file operations: vmm + (default) or caller, the workload identity with guest + root squashed to the owner of the shared directory or + file (Linux only) + --net IPV4/PREFIX + --network-profile {portable} + --network-egress {allow,deny} + --network-ingress {allow,deny} + --network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]] + allow guest egress to an IPv4 or IPv6 CIDR, optionally + only for PROTOCOL tcp, udp, or icmp, and for tcp or + udp only on PORT or on the inclusive range PORT-PORT; + repeat to add rules + --network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]] + deny guest egress to an IPv4 or IPv6 CIDR, optionally + only for PROTOCOL tcp, udp, or icmp, and for tcp or + udp only on PORT or on the inclusive range PORT-PORT; + repeat to add rules + --network-egress-policy-file PATH + load bounded IPv4 and IPv6 ranges and rule-local + exclusions for run/provision + --host-loopback {allow,deny} + --network-proxy IPV4:TCP-PORT + --host-loopback-forward HOST_LOOPBACK_FORWARD + --outcome-report OUTCOME_REPORT + write a bounded local JSON outcome report for run or + exec + --cmdline CMDLINE + --dry-run print the one-shot run command without launching it", +}; + +/// The operations, in the order in which usage messages list them. +const OPERATIONS: [&str; 6] = ["run", "provision", "start", "exec", "stop", "deprovision"]; +/// The choices of `--hypervisor`. +const HYPERVISORS: [&str; 4] = ["auto", "whp", "kvm", "mshv"]; +/// The choices of `--mount-owner`. +const MOUNT_OWNERS: [&str; 2] = ["vmm", "caller"]; +/// The choices of `--network-profile`. +const NETWORK_PROFILES: [&str; 1] = ["portable"]; +/// The choices of `--network-egress`, `--network-ingress`, and `--host-loopback`. +const ACCESS: [&str; 2] = ["allow", "deny"]; + +/// The options of `nvx-rs sandbox`, which parse with the defaults of `nvx.py sandbox`. +const OPTIONS: [Spec; 35] = [ + Spec::one("--layer"), + Spec::one("--scratch"), + Spec::one("--state-dir"), + Spec::one("--entrypoint"), + Spec::one("--arg"), + Spec::one("--hostname"), + Spec::one("--workload-user"), + Spec::one("--memory-max"), + Spec::one("--pids-max"), + Spec::one("--memory-mib"), + Spec::one("--timeout"), + Spec::one("--exec-timeout-ms"), + Spec::one("--cwd"), + Spec::one("--environment"), + Spec::one("--environment-file"), + Spec::flag("--inherit-default-environment"), + Spec::one("--hypervisor"), + Spec::one("--mount"), + Spec::one("--mount-deny"), + Spec::one("--mount-allow"), + Spec::one("--mount-write"), + Spec::one("--mount-owner"), + Spec::one("--net"), + Spec::one("--network-profile"), + Spec::one("--network-egress"), + Spec::one("--network-ingress"), + Spec::one("--network-egress-allow"), + Spec::one("--network-egress-deny"), + Spec::one("--network-egress-policy-file"), + Spec::one("--host-loopback"), + Spec::one("--network-proxy"), + Spec::one("--host-loopback-forward"), + Spec::one("--outcome-report"), + Spec::one("--cmdline"), + Spec::flag("--dry-run"), +]; + +/// The parsed options of `nvx-rs sandbox`: the namespace of `nvx.py sandbox`. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Options { + /// The operation, `run` by default. + pub(crate) operation: SandboxOperation, + /// `--layer`, parsed as `SandboxLayer.parse` parses it. + pub(crate) layers: Vec, + /// `--scratch`, as given. + pub(crate) scratch: Option, + /// `--state-dir`, as given. + pub(crate) state_dir: Option, + /// `--entrypoint`. + pub(crate) entrypoint: String, + /// The `--arg` options. + pub(crate) args: Vec, + /// `--hostname`. + pub(crate) hostname: String, + /// `--workload-user`. + pub(crate) workload_user: (u32, u32), + /// `--memory-max`. + pub(crate) memory_max: Option, + /// `--pids-max`. + pub(crate) pids_max: Option, + /// `--memory-mib`, 256 by default. + pub(crate) memory_mib: Int, + /// `--timeout` in seconds, 60 by default. + pub(crate) timeout: f64, + /// `--exec-timeout-ms`, 0 by default. + pub(crate) exec_timeout_ms: Int, + /// `--cwd`. + pub(crate) cwd: Option, + /// The `--environment` options. + pub(crate) environment: Vec, + /// `--environment-file`, as given. + pub(crate) environment_file: Option, + /// `--inherit-default-environment`. + pub(crate) inherit_default_environment: bool, + /// `--hypervisor`, `auto` by default. + pub(crate) hypervisor: &'static str, + /// The `--mount` options. + pub(crate) mounts: Vec, + /// The `--mount-deny`, `--mount-allow`, and `--mount-write` options, with the `--mount` + /// before each. + pub(crate) policy_paths: Vec, + /// `--mount-owner`. + pub(crate) mount_owner: Option, + /// `--net`. + pub(crate) net: Option, + /// `--network-profile`. + pub(crate) network_profile: Option<&'static str>, + /// `--network-egress`. + pub(crate) network_egress: Option<&'static str>, + /// `--network-ingress`. + pub(crate) network_ingress: Option<&'static str>, + /// The `--network-egress-allow` options. + pub(crate) network_egress_allow: Vec, + /// The `--network-egress-deny` options. + pub(crate) network_egress_deny: Vec, + /// `--network-egress-policy-file`, as given. + pub(crate) network_egress_policy_file: Option, + /// `--host-loopback`. + pub(crate) host_loopback: Option<&'static str>, + /// `--network-proxy`. + pub(crate) network_proxy: Option, + /// The `--host-loopback-forward` options. + pub(crate) host_loopback_forward: Vec, + /// `--outcome-report`, as given. + pub(crate) outcome_report: Option, + /// `--cmdline`, empty by default. + pub(crate) cmdline: String, + /// `--dry-run`. + pub(crate) dry_run: bool, +} + +impl Default for Options { + fn default() -> Self { + Self { + operation: SandboxOperation::Run, + layers: Vec::new(), + scratch: None, + state_dir: None, + entrypoint: DEFAULT_ENTRYPOINT.to_owned(), + args: Vec::new(), + hostname: DEFAULT_HOSTNAME.to_owned(), + workload_user: DEFAULT_WORKLOAD_IDENTITY, + memory_max: None, + pids_max: None, + memory_mib: Int::from(256u32), + timeout: 60.0, + exec_timeout_ms: Int::zero(), + cwd: None, + environment: Vec::new(), + environment_file: None, + inherit_default_environment: false, + hypervisor: "auto", + mounts: Vec::new(), + policy_paths: Vec::new(), + mount_owner: None, + net: None, + network_profile: None, + network_egress: None, + network_ingress: None, + network_egress_allow: Vec::new(), + network_egress_deny: Vec::new(), + network_egress_policy_file: None, + host_loopback: None, + network_proxy: None, + host_loopback_forward: Vec::new(), + outcome_report: None, + cmdline: String::new(), + dry_run: false, + } + } +} + +/// Returns the choice that `value` names among `choices`, as `argparse` checks `choices`. +fn choice(value: &str, choices: &[&'static str]) -> std::result::Result<&'static str, String> { + choices + .iter() + .find(|choice| **choice == value) + .copied() + .ok_or_else(|| invalid_choice(value, choices)) +} + +/// Returns `int(value)` for an option of `type=int`. +fn integer(value: &str) -> std::result::Result { + Int::parse(value).map_err(|_| invalid_value("int", value)) +} + +impl Receiver for Options { + fn take(&mut self, name: &'static str, value: Option<&str>) -> std::result::Result<(), String> { + let text = || value.unwrap_or_default().to_owned(); + match name { + "sandbox_operation" => { + let operation = choice(value.unwrap_or("run"), &OPERATIONS)?; + self.operation = match operation { + "provision" => SandboxOperation::Provision, + "start" => SandboxOperation::Start, + "exec" => SandboxOperation::Exec, + "stop" => SandboxOperation::Stop, + "deprovision" => SandboxOperation::Deprovision, + _ => SandboxOperation::Run, + }; + } + "--layer" => self + .layers + .push(SandboxLayer::parse(&text()).map_err(|error| error.message().to_owned())?), + "--scratch" => self.scratch = Some(text()), + "--state-dir" => self.state_dir = Some(text()), + "--entrypoint" => self.entrypoint = text(), + "--arg" => self.args.push(text()), + "--hostname" => self.hostname = text(), + "--workload-user" => { + self.workload_user = + parse_workload_identity(&text()).map_err(|error| error.message().to_owned())?; + } + "--memory-max" => self.memory_max = Some(integer(&text())?), + "--pids-max" => self.pids_max = Some(integer(&text())?), + "--memory-mib" => self.memory_mib = integer(&text())?, + "--timeout" => { + let timeout = + parse_float(&text()).ok_or_else(|| invalid_value("positive_float", &text()))?; + if !timeout.is_finite() || timeout <= 0.0 { + return Err("must be finite and greater than 0".to_owned()); + } + self.timeout = timeout; + } + "--exec-timeout-ms" => self.exec_timeout_ms = integer(&text())?, + "--cwd" => self.cwd = Some(text()), + "--environment" => self.environment.push(text()), + "--environment-file" => self.environment_file = Some(text()), + "--inherit-default-environment" => self.inherit_default_environment = true, + "--hypervisor" => self.hypervisor = choice(&text(), &HYPERVISORS)?, + "--mount" => self.mounts.push(text()), + "--mount-deny" | "--mount-allow" | "--mount-write" => { + let kind = match name { + "--mount-deny" => PolicyKind::Denied, + "--mount-allow" => PolicyKind::Allowed, + _ => PolicyKind::Writable, + }; + self.policy_paths.push(PolicyPathOption { + kind, + preceding_mount: self.mounts.len().checked_sub(1), + path: text(), + }); + } + "--mount-owner" => { + self.mount_owner = Some(match choice(&text(), &MOUNT_OWNERS)? { + "caller" => MountOwner::Caller, + _ => MountOwner::Vmm, + }); + } + "--net" => self.net = Some(text()), + "--network-profile" => self.network_profile = Some(choice(&text(), &NETWORK_PROFILES)?), + "--network-egress" => self.network_egress = Some(choice(&text(), &ACCESS)?), + "--network-ingress" => self.network_ingress = Some(choice(&text(), &ACCESS)?), + "--network-egress-allow" => self.network_egress_allow.push(text()), + "--network-egress-deny" => self.network_egress_deny.push(text()), + "--network-egress-policy-file" => self.network_egress_policy_file = Some(text()), + "--host-loopback" => self.host_loopback = Some(choice(&text(), &ACCESS)?), + "--network-proxy" => self.network_proxy = Some(text()), + "--host-loopback-forward" => self.host_loopback_forward.push(text()), + "--outcome-report" => self.outcome_report = Some(text()), + "--cmdline" => self.cmdline = text(), + "--dry-run" => self.dry_run = true, + _ => unreachable!("the parser knows only these options"), + } + Ok(()) + } +} + +/// Parses the arguments of `sandbox` for the program `program`, and returns the options and the +/// arguments that the parser does not recognize. +pub(super) fn parse(arguments: &[String], program: &str) -> Result<(Options, Vec)> { + let mut options = Options::default(); + let unrecognized = Parser::new(&OPTIONS, &[Spec::optional("sandbox_operation")]) + .parse(arguments, &mut options) + .map_err(|stop| Exit::from_stop(stop, &SYNTAX, program))?; + Ok((options, unrecognized)) +} + +/// Returns `_hypervisor(selected)`: `auto` selects `whp` on Windows and `kvm` elsewhere. +fn hypervisor(selected: &'static str) -> &'static str { + match selected { + "auto" if cfg!(windows) => "whp", + "auto" => "kvm", + other => other, + } +} + +/// Returns `str(Path(value))`. +fn path_text(value: &str) -> String { + PurePath::native(value).as_str().to_owned() +} + +/// Returns `path` as text, which the commands of `nvx.py` hold as `str`. +pub(crate) fn path_string(path: &std::path::Path) -> Result { + path.to_str() + .map(str::to_owned) + .ok_or_else(|| Exit::error(format!("path is not valid Unicode: {}", path.display()))) +} + +/// The rules of the egress policy of a launch. +struct EgressRules { + /// The `--network-egress-allow` rules. + allow: Vec, + /// The `--network-egress-deny` rules. + deny: Vec, +} + +/// Returns `_resolve_network_egress_rules(args)`: the explicit rules, or those that the +/// `--network-egress-policy-file` compiles to, which neither rule option nor a missing +/// `--network-egress` may accompany. +fn resolve_network_egress_rules(options: &Options) -> Result { + let Some(policy) = &options.network_egress_policy_file else { + return Ok(EgressRules { + allow: options.network_egress_allow.clone(), + deny: options.network_egress_deny.clone(), + }); + }; + if !options.network_egress_allow.is_empty() || !options.network_egress_deny.is_empty() { + return Err(Exit::error( + "--network-egress-policy-file cannot be combined with --network-egress-allow or \ + --network-egress-deny", + )); + } + if options.network_egress.is_none() { + return Err(Exit::error( + "--network-egress is required with --network-egress-policy-file", + )); + } + let compiled = compile_policy_file(std::path::Path::new(policy))?; + Ok(EgressRules { + allow: compiled.allow, + deny: compiled.deny, + }) +} + +/// Appends the network options of `options` to `command`, as `_extend_network_arguments` does. +fn extend_network_arguments(command: &mut Vec, options: &Options, rules: &EgressRules) { + let mut push = |name: &str, value: &str| { + command.push(name.to_owned()); + command.push(value.to_owned()); + }; + if let Some(net) = &options.net { + push("--net", net); + push( + "--network-profile", + options.network_profile.unwrap_or_default(), + ); + } + if let Some(egress) = options.network_egress { + push("--network-egress", egress); + } + if let Some(ingress) = options.network_ingress { + push("--network-ingress", ingress); + } + for rule in &rules.allow { + push("--network-egress-allow", rule); + } + for rule in &rules.deny { + push("--network-egress-deny", rule); + } + if let Some(host_loopback) = options.host_loopback { + push("--host-loopback", host_loopback); + } + if let Some(proxy) = &options.network_proxy { + push("--network-proxy", proxy); + } + for forward in &options.host_loopback_forward { + push("--host-loopback-forward", forward); + } +} + +/// The artifacts of a sandbox, found when an operation first needs them. +pub(crate) struct Artifacts<'a> { + /// `--root`, if given. + root: Option<&'a str>, +} + +impl Artifacts<'_> { + /// Returns the OpenVMM executable, kernel, and Alpine initramfs, checked in that order as + /// `nvx.py sandbox run` and `start` check them. + pub(crate) fn require(&self) -> Result<[String; 3]> { + let Located { dir, openvmm } = locate(self.root)?; + require_file(&openvmm, "OpenVMM release binary")?; + let kernel = dir.artifact(KERNEL_NAME); + require_file(&kernel, "Linux direct kernel")?; + let initrd = dir.initramfs(Guest::Alpine); + require_file(&initrd, "initramfs")?; + Ok([ + path_string(&openvmm)?, + path_string(&kernel)?, + path_string(&initrd)?, + ]) + } +} + +/// Runs the operation of `options`, as `command_sandbox` does. +pub(super) fn run(root: Option<&str>, options: Options) -> Result { + let operation = options.operation; + let checks = SandboxOptions { + operation, + dry_run: options.dry_run, + cwd: options.cwd.clone(), + environment: options.environment.clone(), + environment_file: options.environment_file.clone(), + inherit_default_environment: options.inherit_default_environment, + network_options: options.net.is_some() + || options.network_profile.is_some() + || options.network_egress.is_some() + || options.network_ingress.is_some() + || options.network_egress_policy_file.is_some() + || options.host_loopback.is_some() + || options.network_proxy.is_some() + || !options.network_egress_allow.is_empty() + || !options.network_egress_deny.is_empty() + || !options.host_loopback_forward.is_empty(), + net: options.net.is_some(), + network_profile: options.network_profile.is_some(), + entrypoint: options.entrypoint.clone(), + outcome_report: options.outcome_report.is_some(), + mounts: options.mounts.clone(), + policy_paths: options.policy_paths.clone(), + mount_owner: options.mount_owner, + layers: !options.layers.is_empty(), + scratch: options.scratch.is_some(), + }; + let environment = checks.check()?; + let launch = if matches!( + operation, + SandboxOperation::Run | SandboxOperation::Provision + ) { + let rules = resolve_network_egress_rules(&options)?; + let mounts = sandbox_mounts(&options.mounts, &options.policy_paths, options.mount_owner)?; + let settings = SandboxSettings { + entrypoint: options.entrypoint.clone(), + args: options.args.clone(), + hostname: options.hostname.clone(), + workload_identity: options.workload_user, + memory_max: options.memory_max.clone(), + pids_max: options.pids_max.clone(), + mounts, + }; + let launch = SandboxLaunch::new( + options.layers.clone(), + options.scratch.as_deref().unwrap_or_default(), + settings, + )?; + launch.validated()?; + validate_sandbox_systemd_policy(&launch)?; + Some((launch, rules)) + } else { + None + }; + let artifacts = Artifacts { root }; + let state_dir = |operation: &str| { + options + .state_dir + .as_deref() + .ok_or_else(|| Exit::error(format!("sandbox {operation} requires --state-dir"))) + }; + match (operation, launch) { + (SandboxOperation::Provision, Some((launch, rules))) => { + let state_dir = state_dir("provision")?; + lifecycle::provision( + state_dir, + &launch, + &ProvisionConfig { + hypervisor: hypervisor(options.hypervisor), + memory_mib: &options.memory_mib, + net: options.net.as_deref(), + network_profile: options.network_profile, + network_egress: options.network_egress, + network_ingress: options.network_ingress, + network_egress_allow: &rules.allow, + network_egress_deny: &rules.deny, + host_loopback: options.host_loopback, + network_proxy: options.network_proxy.as_deref(), + host_loopback_forward: &options.host_loopback_forward, + cmdline: &options.cmdline, + }, + options.timeout, + ) + } + (SandboxOperation::Start, _) => { + lifecycle::start(state_dir("start")?, &artifacts, options.timeout) + } + (SandboxOperation::Exec, _) => { + let state_dir = state_dir("exec")?; + if let Some(report) = &options.outcome_report { + lifecycle::validate_outcome_destination(report)?; + } + let mut arguments = vec![options.entrypoint.clone()]; + arguments.extend(options.args.iter().cloned()); + let result = lifecycle::exec_workload( + state_dir, + &super::session::ExecRequest { + arguments, + timeout_ms: options.exec_timeout_ms.clone(), + cwd: options.cwd.clone(), + environment, + inherit_default_environment: options.inherit_default_environment, + }, + options.timeout, + )?; + output::write_stdout(&result.stdout) + .and_then(|()| output::write_stderr(&result.stderr)) + .map_err(|error| Exit::error(output::stream_error(&error)))?; + if let Some(report) = &options.outcome_report { + lifecycle::write_exec_outcome(report, &result)?; + } + Err(Exit::Status(result.returncode)) + } + (SandboxOperation::Stop, _) => { + lifecycle::stop(state_dir("stop")?, options.timeout).map(drop) + } + (SandboxOperation::Deprovision, _) => { + lifecycle::deprovision(state_dir("deprovision")?, options.timeout) + } + (SandboxOperation::Run, Some((launch, rules))) => { + if options.state_dir.is_some() { + return Err(Exit::error( + "one-shot sandbox execution rejects persistent --state-dir settings", + )); + } + let [openvmm, kernel, initrd] = artifacts.require()?; + let mut command = vec![openvmm]; + command.extend(launch.openvmm_arguments()?); + command.extend( + [ + "--microvm-lifecycle", + "one-shot", + "--single-process", + "--hypervisor", + hypervisor(options.hypervisor), + ] + .map(str::to_owned), + ); + command.extend([ + "--memory".to_owned(), + format!("{}M", options.memory_mib), + "--kernel".to_owned(), + kernel, + "--initrd".to_owned(), + initrd, + "--cmdline".to_owned(), + launch.kernel_command_line(&options.cmdline)?, + ]); + extend_network_arguments(&mut command, &options, &rules); + if let Some(report) = &options.outcome_report { + command.extend(["--microvm-report".to_owned(), path_text(report)]); + } + output::print_stdout(&format!(">> {}", format_command(&command))) + .map_err(|error| Exit::error(output::stream_error(&error)))?; + if options.dry_run { + return Ok(()); + } + Err(Exit::Status(run_openvmm(&command)?)) + } + (SandboxOperation::Run | SandboxOperation::Provision, None) => { + unreachable!("run and provision build their launch") + } + } +} + +/// Runs OpenVMM on the terminal, as `subprocess.run(command)` does, and returns its status. +/// +/// OpenVMM shares the terminal and its process group, so Ctrl-C reaches it too. As +/// `subprocess.run` does, the CLI then gives it 0.25 s to exit on Unix before it kills it, and on +/// Windows, where Python cannot interrupt its wait, waits for it to exit; either way the command +/// ends as interrupted, also when OpenVMM exited first, since Python's wait raises +/// `KeyboardInterrupt` once Ctrl-C arrives. +fn run_openvmm(command: &[String]) -> Result { + let mut child = Command::new(&command[0]) + .args(&command[1..]) + .spawn() + .map_err(|error| Exit::error(spawn_error(&error, &command[0])))?; + loop { + let status = poll(&mut child)?; + if platform::interrupted() { + if status.is_none() { + end_interrupted(&mut child)?; + } + return Err(Exit::Interrupted); + } + if let Some(status) = status { + return Ok(status); + } + thread::sleep(Duration::from_millis(10)); + } +} + +/// Ends the OpenVMM of an interrupted run as `subprocess.run` does: on Unix, it waits 0.25 s for +/// OpenVMM to exit and then kills it, and on Windows, it waits for OpenVMM to exit. +fn end_interrupted(child: &mut Child) -> Result { + let deadline = std::time::Instant::now() + Duration::from_millis(250); + while poll(child)?.is_none() { + if cfg!(unix) && std::time::Instant::now() >= deadline { + let _ = child.kill(); + let _ = child.wait(); + break; + } + thread::sleep(Duration::from_millis(10)); + } + Ok(()) +} + +/// Returns the status of `child` if it has exited: `Popen.returncode`, which on Unix is the +/// negated signal of a process that a signal ended. +fn poll(child: &mut Child) -> Result> { + let status = child + .try_wait() + .map_err(|error| Exit::error(crate::host::python::fs::os_error_message(&error, None)))?; + Ok(status.map(|status| { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + + status + .code() + .unwrap_or_else(|| -status.signal().unwrap_or_default()) + } + #[cfg(not(unix))] + status.code().unwrap_or_default() + })) +} + +/// Returns the message of the `OSError` with which `subprocess` fails to start `program`: with +/// the program's name on POSIX, and without on Windows. +pub(crate) fn spawn_error(error: &std::io::Error, program: &str) -> String { + let program = if cfg!(windows) { None } else { Some(program) }; + crate::host::python::fs::os_error_message(error, program) +} diff --git a/nvx-rs/src/host/cli/sandbox/state.rs b/nvx-rs/src/host/cli/sandbox/state.rs new file mode 100644 index 000000000..f9ec310b7 --- /dev/null +++ b/nvx-rs/src/host/cli/sandbox/state.rs @@ -0,0 +1,627 @@ +//! The files of a state directory: `config.json` in formats 1 to 6, `runtime.json`, and the JSON +//! reading and writing of the former `sandbox_lifecycle.py`. +//! +//! - [`write_json`] writes `json.dumps(value, indent=2, sort_keys=True) + "\n"` as +//! `Path.write_text` does, with the `\n`s turned into `os.linesep`, into a temporary file of +//! mode 0600 that then replaces the file. +//! - [`read_json`] reads a file as `json.loads(path.read_text(encoding="utf-8"))` does, and +//! accepts only an object whose version field equals, as `==` compares, one of the given +//! versions. +//! - [`serialize_launch`] writes a provisioned launch in the lowest format that holds it, and +//! [`deserialize_launch`] reads it back with the conversions, defaults, and checks of +//! `_deserialize_launch`. + +use std::fs::OpenOptions; +use std::io::Write; + +use super::lifecycle::ProvisionConfig; +use crate::host::cli::output::LINE_SEPARATOR; +use crate::host::cli::{Exit, Result}; +use crate::host::python::builtins::{Int, repr}; +use crate::host::python::dumps::{self, Value, dict}; +use crate::host::python::fs::{self, os_error_message}; +use crate::host::python::json::{self, Document, Loads, Node, NodeId, ReadError}; +use crate::host::python::path::PurePath; +use crate::host::python::text::decode_utf8; +use crate::host::python::value::{self, IntError, equals_int, py_int, py_str}; +use crate::host::sandbox::layer::{LayerRole, SandboxLayer}; +use crate::host::sandbox::mount::{ + MountAccess, MountKind, MountOwner, MountPolicy, SandboxMount, validate_mount_target, +}; +use crate::host::sandbox::{SandboxLaunch, SandboxSettings}; + +/// The format of `runtime.json`, and the version of the files that `_read_json` reads by default. +pub(crate) const STATE_FORMAT: u64 = 1; +/// The configuration of a sandbox without shares. +pub(crate) const CONFIG_FORMAT: u64 = 1; +/// The configuration of a sandbox with one share, which format-1 readers would start without it. +pub(crate) const MOUNT_CONFIG_FORMAT: u64 = 2; +/// The configuration of a sandbox with one share that the caller owns, which format-2 readers +/// would start as the VMM. +pub(crate) const OWNER_CONFIG_FORMAT: u64 = 3; +/// The configuration of a sandbox with several shares, listed under `mounts`. +pub(crate) const MULTI_MOUNT_CONFIG_FORMAT: u64 = 4; +/// The configuration of a sandbox whose shares have allowed or writable paths. +pub(crate) const POLICY_CONFIG_FORMAT: u64 = 5; +/// The configuration of a sandbox that shares a file, with the kind of each share. +pub(crate) const FILE_MOUNT_CONFIG_FORMAT: u64 = 6; +/// Every configuration format that `start` reads. +pub(crate) const CONFIG_FORMATS: [u64; 6] = [ + CONFIG_FORMAT, + MOUNT_CONFIG_FORMAT, + OWNER_CONFIG_FORMAT, + MULTI_MOUNT_CONFIG_FORMAT, + POLICY_CONFIG_FORMAT, + FILE_MOUNT_CONFIG_FORMAT, +]; + +/// Returns the name of a temporary file next to `path`: `.NAME.UUID.tmp`, with a fresh UUID4 in +/// hexadecimal. +pub(crate) fn temporary_path(path: &PurePath) -> Result { + let name = format!(".{}.{}.tmp", path.name(), uuid4_hex()?); + path.with_name(&name) + .ok_or_else(|| Exit::error(format!("{path} has an empty name"))) +} + +/// Returns `uuid.uuid4().hex`: 32 lowercase hexadecimal digits of 122 random bits, with the +/// version and variant bits of a version-4 UUID. +pub(crate) fn uuid4_hex() -> Result { + let mut bytes = random_bytes::<16>()?; + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + Ok(hex(&bytes)) +} + +/// Returns `N` random bytes from the operating system, as `secrets` and `uuid` take them. +pub(crate) fn random_bytes() -> Result<[u8; N]> { + let mut bytes = [0u8; N]; + getrandom::fill(&mut bytes) + .map_err(|error| Exit::error(format!("cannot read random bytes: {error}")))?; + Ok(bytes) +} + +/// Returns `bytes` in lowercase hexadecimal. +pub(crate) fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +/// Returns `str(error)` for an `OSError` that names two files, as `os.replace` raises it. +fn os_error_with_two_paths(error: &std::io::Error, source: &str, destination: &str) -> String { + let message = os_error_message(error, Some(source)); + match error.raw_os_error() { + Some(_) => format!("{message} -> {}", repr(destination)), + None => message, + } +} + +/// Atomically replaces `path` with `value` as `_write_json` writes it: the JSON text of +/// `json.dumps(value, indent=2, sort_keys=True)` and a newline, with `os.linesep` for each +/// newline, in a temporary file of mode 0600 that then replaces `path`. +pub(crate) fn write_json(path: &PurePath, value: &Value) -> Result { + let temporary = temporary_path(path)?; + let text = format!("{}\n", dumps::dumps(value)).replace('\n', LINE_SEPARATOR); + let written = (|| -> Result { + let mut options = OpenOptions::new(); + options.write(true).create(true).truncate(true); + let mut file = options + .open(temporary.as_path()) + .and_then(|mut file| file.write_all(text.as_bytes()).map(|()| file)) + .map_err(|error| Exit::error(os_error_message(&error, Some(temporary.as_str()))))?; + file.flush() + .map_err(|error| Exit::error(os_error_message(&error, Some(temporary.as_str()))))?; + drop(file); + set_private(&temporary)?; + std::fs::rename(temporary.as_path(), path.as_path()).map_err(|error| { + Exit::error(os_error_with_two_paths( + &error, + temporary.as_str(), + path.as_str(), + )) + }) + })(); + let _ = std::fs::remove_file(temporary.as_path()); + written +} + +/// Gives `path` mode 0600, as `os.chmod(path, 0o600)` does: on Windows, that only clears the +/// read-only attribute. +fn set_private(path: &PurePath) -> Result { + let error = |error: std::io::Error| Exit::error(os_error_message(&error, Some(path.as_str()))); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + std::fs::set_permissions(path.as_path(), std::fs::Permissions::from_mode(0o600)) + .map_err(error) + } + #[cfg(not(unix))] + { + let mut permissions = std::fs::metadata(path.as_path()) + .map_err(error)? + .permissions(); + if permissions.readonly() { + #[allow(clippy::permissions_set_readonly_false)] + permissions.set_readonly(false); + std::fs::set_permissions(path.as_path(), permissions).map_err(error)?; + } + Ok(()) + } +} + +/// Returns the member `name` of the object at `id`, as `value.get(name)` finds it in the dict +/// that Python builds, in which the last of repeated names wins. +pub(crate) fn member(document: &Document, id: NodeId, name: &str) -> Option { + let Node::Object(members) = document.node(id) else { + return None; + }; + members + .iter() + .rev() + .find(|(member, _)| member.as_bytes() == name.as_bytes()) + .map(|&(_, value)| value) +} + +/// Reads the JSON object at `path`, as `_read_json` does: it must hold `version_field` equal, as +/// `==` compares, to one of `versions`. +/// +/// A file that cannot be read, is not UTF-8, or is not JSON fails as `failed to read +/// DESCRIPTION: PATH`, and another value or version as `DESCRIPTION has an unsupported format: +/// PATH`. An integer of more than 4300 digits fails with the `ValueError` of `int`. +pub(crate) fn read_json( + path: &PurePath, + description: &str, + version_field: &str, + versions: &[u64], +) -> Result { + let unreadable = || Exit::error(format!("failed to read {description}: {path}")); + let bytes = std::fs::read(path.as_path()).map_err(|_| unreadable())?; + // `read_text` decodes strictly and translates universal newlines. + let text = decode_utf8(&bytes) + .map_err(|_| unreadable())? + .replace("\r\n", "\n") + .replace('\r', "\n"); + let document = match json::read(&text, &mut Loads) { + Ok(document) => document, + Err(ReadError::Syntax { .. }) => return Err(unreadable()), + Err(ReadError::Hook(error)) => return Err(Exit::error(error.message(&text))), + }; + let unsupported = || Exit::error(format!("{description} has an unsupported format: {path}")); + let root = document.root(); + if !matches!(document.node(root), Node::Object(_)) { + return Err(unsupported()); + } + let version = member(&document, root, version_field); + if !version.is_some_and(|version| { + versions + .iter() + .any(|&expected| equals_int(&document, version, expected)) + }) { + return Err(unsupported()); + } + Ok(document) +} + +/// Returns the format of the configuration of a sandbox with `mounts`: the lowest that holds +/// them, as `_config_format` chooses it. +pub(crate) fn config_format(mounts: &[SandboxMount]) -> u64 { + if mounts.iter().any(|mount| mount.kind() == MountKind::File) { + FILE_MOUNT_CONFIG_FORMAT + } else if mounts + .iter() + .any(|mount| !mount.policy().allowed.is_empty() || !mount.policy().writable.is_empty()) + { + POLICY_CONFIG_FORMAT + } else if mounts.len() > 1 { + MULTI_MOUNT_CONFIG_FORMAT + } else if mounts.len() == 1 && mounts[0].owner() == MountOwner::Caller { + OWNER_CONFIG_FORMAT + } else if mounts.len() == 1 { + MOUNT_CONFIG_FORMAT + } else { + CONFIG_FORMAT + } +} + +/// Returns whether a format lists every share under `mounts`. +fn lists_mounts(format: u64) -> bool { + matches!( + format, + MULTI_MOUNT_CONFIG_FORMAT | POLICY_CONFIG_FORMAT | FILE_MOUNT_CONFIG_FORMAT + ) +} + +/// Returns `path.resolve()` as text. +fn resolved(path: &std::path::Path) -> Result { + let text = crate::host::cli::sandbox::path_string(path)?; + let resolved = fs::resolve(&PurePath::native(&text)) + .map_err(|error| Exit::error(os_error_message(&error, None)))?; + Ok(resolved.as_str().to_owned()) +} + +/// Returns a list of strings. +fn strings(values: &[String]) -> Value { + Value::List( + values + .iter() + .map(|value| Value::from(value.as_str())) + .collect(), + ) +} + +/// Returns the configuration that `provision` records for `launch` and the other options, as +/// `_serialize_launch` builds it. +pub(crate) fn serialize_launch( + launch: &SandboxLaunch, + config: &ProvisionConfig<'_>, +) -> Result { + let format = config_format(launch.mounts()); + let mut layers = Vec::new(); + for layer in launch.ordered_layers() { + layers.push(dict([ + ("role", Value::from(layer.role().as_str())), + ("path", Value::from(resolved(layer.path())?)), + ("uuid", Value::from(layer.uuid())), + ])); + } + let settings = launch.settings(); + let (uid, gid) = settings.workload_identity; + let mut value = dict([ + ("format", Value::from(format)), + ("layers", Value::List(layers)), + ("scratch", Value::from(resolved(launch.scratch())?)), + ("hostname", Value::from(settings.hostname.as_str())), + ("workload_uid", Value::from(uid)), + ("workload_gid", Value::from(gid)), + ("memory_max", Value::from(settings.memory_max.clone())), + ("pids_max", Value::from(settings.pids_max.clone())), + ("hypervisor", Value::from(config.hypervisor)), + ("memory_mib", Value::from(config.memory_mib.clone())), + ("net", Value::from(config.net)), + ("network_profile", Value::from(config.network_profile)), + ("network_egress", Value::from(config.network_egress)), + ("network_ingress", Value::from(config.network_ingress)), + ("network_egress_allow", strings(config.network_egress_allow)), + ("network_egress_deny", strings(config.network_egress_deny)), + ("host_loopback", Value::from(config.host_loopback)), + ("network_proxy", Value::from(config.network_proxy)), + ( + "host_loopback_forward", + strings(config.host_loopback_forward), + ), + ("cmdline", Value::from(config.cmdline)), + ]); + let Value::Dict(members) = &mut value else { + unreachable!("dict builds a dict") + }; + if lists_mounts(format) { + let mut mounts = Vec::new(); + for mount in launch.mounts() { + mounts.push(serialize_mount( + mount, + format != MULTI_MOUNT_CONFIG_FORMAT, + format == FILE_MOUNT_CONFIG_FORMAT, + )?); + } + members.insert("mounts".to_owned(), Value::List(mounts)); + } else { + let mount = match launch.mounts().first() { + Some(mount) => serialize_mount(mount, false, false)?, + None => Value::Null, + }; + members.insert("mount".to_owned(), mount); + } + Ok(value) +} + +/// Returns a share as `_serialize_mount` records it, with its host path made absolute, and with +/// its allowed and writable paths and its kind if the format holds them. +fn serialize_mount(mount: &SandboxMount, policy: bool, kind: bool) -> Result { + let absolute = mount.absolute()?; + let mut value = dict([ + ("guest_target", Value::from(absolute.guest_target())), + ( + "host_path", + Value::from(crate::host::cli::sandbox::path_string( + absolute.host_path(), + )?), + ), + ("access", Value::from(absolute.access().as_str())), + ("denied_paths", strings(&absolute.policy().denied)), + ("owner", Value::from(absolute.owner().as_str())), + ]); + let Value::Dict(members) = &mut value else { + unreachable!("dict builds a dict") + }; + if policy { + members.insert( + "allowed_paths".to_owned(), + strings(&absolute.policy().allowed), + ); + members.insert( + "writable_paths".to_owned(), + strings(&absolute.policy().writable), + ); + } + if kind { + members.insert("kind".to_owned(), Value::from(absolute.kind().as_str())); + } + Ok(value) +} + +/// Why reading a recorded launch failed. +enum Malformed { + /// The `KeyError`, `TypeError`, or `ValueError` that `_deserialize_launch` reports as a + /// malformed configuration. + Malformed, + /// Any other failure, which it reports as it is. + Exit(Exit), +} + +impl From for Malformed { + fn from(exit: Exit) -> Self { + Self::Exit(exit) + } +} + +impl From for Malformed { + fn from(error: crate::Error) -> Self { + Self::Exit(error.into()) + } +} + +/// Returns `str(value[name])` of the object at `id`. +fn text_member( + document: &Document, + id: NodeId, + name: &str, +) -> std::result::Result { + let value = member(document, id, name).ok_or(Malformed::Malformed)?; + py_str(document, value).ok_or(Malformed::Malformed) +} + +/// Returns `int(value)`, which `_deserialize_launch` reports as malformed unless it overflows. +fn integer(document: &Document, id: NodeId) -> std::result::Result { + py_int(document, id).map_err(|error| match error { + IntError::Invalid => Malformed::Malformed, + IntError::Overflow => { + Malformed::Exit(Exit::error("cannot convert float infinity to integer")) + } + }) +} + +/// Returns the items of the list or other iterable that Python iterates at `id`, as `for item in +/// value` sees them: a list's items, a dict's names, and a string's characters, which are no +/// mappings, so that only a list of objects reads as layers. +fn iterate(document: &Document, id: NodeId) -> std::result::Result>, Malformed> { + match document.node(id) { + Node::Array(items) => Ok(items.iter().map(|&item| Some(item)).collect()), + Node::Object(members) => Ok(members.iter().map(|_| None).collect()), + Node::String(text) => Ok(text.code_points().map(|_| None).collect()), + _ => Err(Malformed::Malformed), + } +} + +/// Returns the paths of a share's `key`, which must be a list, as `_deserialize_paths` reads +/// them. +fn paths_member( + document: &Document, + mount: NodeId, + key: &str, +) -> std::result::Result, Malformed> { + let value = member(document, mount, key).ok_or(Malformed::Malformed)?; + let Node::Array(items) = document.node(value) else { + return Err(Malformed::Malformed); + }; + items + .iter() + .map(|&item| py_str(document, item).ok_or(Malformed::Malformed)) + .collect() +} + +/// Reads one recorded share, as `_deserialize_mount` does, and checks it as +/// `SandboxMount.__post_init__` does. +fn deserialize_mount( + document: &Document, + id: NodeId, + policy: bool, + kind: bool, +) -> std::result::Result { + if !matches!(document.node(id), Node::Object(_)) { + return Err(Malformed::Malformed); + } + let guest_target = text_member(document, id, "guest_target")?; + let host_path = text_member(document, id, "host_path")?; + let access = text_member(document, id, "access")?; + let denied = paths_member(document, id, "denied_paths")?; + // Configurations written before ownership modes ran shares as the VMM. + let owner = match member(document, id, "owner") { + Some(owner) => py_str(document, owner).ok_or(Malformed::Malformed)?, + None => "vmm".to_owned(), + }; + let allowed = if policy { + paths_member(document, id, "allowed_paths")? + } else { + Vec::new() + }; + let writable = if policy { + paths_member(document, id, "writable_paths")? + } else { + Vec::new() + }; + let kind = if kind { + text_member(document, id, "kind")? + } else { + "directory".to_owned() + }; + validate_mount_target(&guest_target)?; + let access = MountAccess::from_name(&access)?; + let owner = MountOwner::from_name(&owner)?; + let kind = MountKind::from_name(&kind)?; + let policy = MountPolicy { + denied, + allowed, + writable, + }; + Ok(SandboxMount::new( + &guest_target, + &host_path, + access, + policy, + owner, + kind, + )?) +} + +/// Reads the shares of a recorded configuration, as `_deserialize_mounts` does. +fn deserialize_mounts( + document: &Document, + root: NodeId, +) -> std::result::Result, Malformed> { + let format = member(document, root, "format"); + let is_format = |expected| format.is_some_and(|format| equals_int(document, format, expected)); + let mounts_format = [ + MULTI_MOUNT_CONFIG_FORMAT, + POLICY_CONFIG_FORMAT, + FILE_MOUNT_CONFIG_FORMAT, + ] + .into_iter() + .find(|&format| is_format(format)); + if let Some(format) = mounts_format { + let mounts = member(document, root, "mounts").ok_or(Malformed::Malformed)?; + let Node::Array(items) = document.node(mounts) else { + return Err(Malformed::Malformed); + }; + return items + .iter() + .map(|&item| { + deserialize_mount( + document, + item, + format != MULTI_MOUNT_CONFIG_FORMAT, + format == FILE_MOUNT_CONFIG_FORMAT, + ) + }) + .collect(); + } + match member(document, root, "mount") { + None => Ok(Vec::new()), + Some(mount) if matches!(document.node(mount), Node::Null) => Ok(Vec::new()), + Some(mount) => Ok(vec![deserialize_mount(document, mount, false, false)?]), + } +} + +/// Reads the launch that a configuration records, as `_deserialize_launch` does, and checks it +/// against the host. +/// +/// A missing field, or a value of the wrong type, fails as `sandbox configuration is malformed`, +/// as does a layer whose role is none of `distro`, `runtime`, and `custom`, which Python would +/// accept and then leave out of the launch. A configuration whose format is not the one that its +/// shares need fails as `sandbox configuration format does not match its mounts`. +pub(crate) fn deserialize_launch(document: &Document) -> Result { + let malformed = || Exit::error("sandbox configuration is malformed"); + let launch = (|| -> std::result::Result { + let root = document.root(); + let layers_value = member(document, root, "layers").ok_or(Malformed::Malformed)?; + let mut layers = Vec::new(); + for layer in iterate(document, layers_value)? { + let layer = layer.ok_or(Malformed::Malformed)?; + if !matches!(document.node(layer), Node::Object(_)) { + return Err(Malformed::Malformed); + } + let role = text_member(document, layer, "role")?; + let path = text_member(document, layer, "path")?; + let uuid = text_member(document, layer, "uuid")?; + let role = LayerRole::from_name(&role).ok_or(Malformed::Malformed)?; + layers.push(SandboxLayer::new(role, &path, uuid)); + } + let identity = |name| -> std::result::Result { + let value = member(document, root, name).ok_or(Malformed::Malformed)?; + // An identity outside the range of IDs fails as root's does. + Ok(integer(document, value)? + .to_u32() + .filter(|&id| id != 0) + .unwrap_or(0)) + }; + let workload_identity = (identity("workload_uid")?, identity("workload_gid")?); + let scratch = text_member(document, root, "scratch")?; + let hostname = text_member(document, root, "hostname")?; + let limit = |name| -> std::result::Result, Malformed> { + let value = member(document, root, name).ok_or(Malformed::Malformed)?; + if matches!(document.node(value), Node::Null) { + return Ok(None); + } + integer(document, value).map(Some) + }; + let memory_max = limit("memory_max")?; + let pids_max = limit("pids_max")?; + let mounts = deserialize_mounts(document, root)?; + let launch = SandboxLaunch::new( + layers, + &scratch, + SandboxSettings { + hostname, + workload_identity, + memory_max, + pids_max, + mounts, + ..SandboxSettings::default() + }, + )?; + Ok(launch) + })() + .map_err(|error| match error { + Malformed::Malformed => malformed(), + Malformed::Exit(exit) => exit, + })?; + let format = member(document, document.root(), "format"); + if !format.is_some_and(|format| equals_int(document, format, config_format(launch.mounts()))) { + return Err(Exit::error( + "sandbox configuration format does not match its mounts", + )); + } + launch.validated()?; + Ok(launch) +} + +/// Returns `str(value)` of the member `name` of the configuration, failing as Python's +/// `KeyError` reads, `'NAME'`, if the member is missing. +pub(crate) fn config_text(document: &Document, name: &str) -> Result { + let value = member(document, document.root(), name).ok_or_else(|| Exit::error(repr(name)))?; + py_str(document, value).ok_or_else(|| Exit::error("sandbox configuration is malformed")) +} + +/// Returns `int(config[name])`, failing with the `KeyError`, `TypeError`, `ValueError`, or +/// `OverflowError` that Python raises. +pub(crate) fn config_int(document: &Document, name: &str) -> Result { + let value = member(document, document.root(), name).ok_or_else(|| Exit::error(repr(name)))?; + py_int(document, value).map_err(|error| match error { + IntError::Overflow => Exit::error("cannot convert float infinity to integer"), + IntError::Invalid => Exit::error(int_error(document, value)), + }) +} + +/// Returns the message of the error with which `int(value)` refuses the value at `id`. +fn int_error(document: &Document, id: NodeId) -> String { + let type_error = |name: &str| { + format!( + "int() argument must be a string, a bytes-like object or a real number, not '{name}'" + ) + }; + match document.node(id) { + Node::Null => type_error("NoneType"), + Node::Array(_) => type_error("list"), + Node::Object(_) => type_error("dict"), + Node::Float(_) => "cannot convert float NaN to integer".to_owned(), + Node::String(text) => match text.as_str() { + Some(text) => match Int::parse(text) { + Err(error) => error.message(text), + Ok(_) => String::new(), + }, + None => format!( + "invalid literal for int() with base 10: {}", + value::py_repr(document, id).unwrap_or_default() + ), + }, + Node::Integer(_) | Node::Bool(_) => String::new(), + } +} diff --git a/nvx-rs/src/host/cli/session.rs b/nvx-rs/src/host/cli/session.rs new file mode 100644 index 000000000..dce9658fa --- /dev/null +++ b/nvx-rs/src/host/cli/session.rs @@ -0,0 +1,530 @@ +//! The control session of `nvx.py sandbox`, a port of the former +//! `scripts/nvx_tools/control_session.py` on the framing, sequencing, attachment, and transports +//! of [`crate::host::session`]. +//! +//! It keeps the observable behavior of the Python session: +//! +//! - [`Session::connect`] retries the endpoint every 25 ms until its timeout, then attaches with +//! the capability within a fresh timeout. On Windows, it gives up at once on the errors that +//! Python does not retry. +//! - Each operation bounds its request and its response by one timeout. A transfer that times +//! out fails with `managed control request timed out` while it sends and `managed control +//! response timed out` while it receives. +//! - An endpoint that closes, as OpenVMM closes it when it shuts down, ends the operation with +//! [`Exit::EndpointClosed`], while a session that OpenVMM resets fails with `managed control +//! session was reset`. +//! - `exec` refuses invalid requests before it sends anything, with Python's messages, buffers +//! the workload's output, and accepts the exit categories of `MANAGED_EXIT_CATEGORIES`; a +//! refusal keeps its status and its category, which the guest sends in ASCII. +//! - A session whose transfer failed is closed, and refuses further requests. +//! +//! The transports also wait in slices of at most 250 ms, as Python's do, so that Ctrl-C ends a +//! wait within that time, except while a request is being sent. + +use std::io; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::time::Duration; + +use super::deadline::{Deadline, RETRY_INTERVAL, sleep}; +use super::{Exit, Result, check_interrupt}; +use crate::host::platform::{self, Transport}; +use crate::host::python::fs::os_error_message; +use crate::host::python::path::PurePath; +use crate::host::sandbox::options::validate_exec_environment; +use crate::host::session::{ControlSession, SessionError}; +use crate::protocol::{ + APP_ERROR, APP_EXEC, APP_EXIT, APP_FEATURES, APP_PING, APP_READY, APP_STDERR, APP_STDOUT, + APP_STOP, APP_STOPPED, CAPABILITY_LEN, GuestFeatures, MAX_OUTPUT_BYTES, UNSUPPORTED_OPERATION, + Workload, WorkloadEnvironment, encode_exec_payload, +}; + +/// The exit categories that `exec` accepts, `MANAGED_EXIT_CATEGORIES`. A cancellation, which this +/// session never requests, is not among them. +pub(crate) const EXIT_CATEGORIES: [&str; 5] = + ["exit", "timeout", "output-limit", "signal", "failed"]; +/// The most arguments of a workload, `APP_MAX_ARGUMENTS`. +const MAX_ARGUMENTS: usize = 64; +/// The longest argument and working directory, in UTF-8 bytes. +const MAX_ARGUMENT_BYTES: usize = 4096; +/// The longest that one wait of a transport blocks before it checks for Ctrl-C. +const POLL_SLICE: Duration = Duration::from_millis(250); + +/// The direction of the last transfer, for the message of a timeout. +const SENDING: u8 = 1; +/// See [`SENDING`]. +const RECEIVING: u8 = 2; + +/// What the transport observed of the failure of a transfer. +#[derive(Default)] +struct Probe { + /// [`SENDING`] or [`RECEIVING`]: the direction of the transfer in progress. + direction: AtomicU8, + /// Whether the endpoint closed the stream. + closed: AtomicBool, + /// Whether Ctrl-C ended a wait. + interrupted: AtomicBool, +} + +/// A transport that records what [`Probe`] reports, and waits in slices that Ctrl-C can end. +struct ProbedTransport { + /// The platform's transport. + inner: Box, + /// What this transport observed. + probe: Arc, +} + +/// Returns whether `error` tells that the endpoint closed the stream, as Python's +/// `_PEER_CLOSED` and `_PIPE_CLOSED_ERRORS` do. +fn closes(error: &io::Error) -> bool { + matches!( + error.kind(), + io::ErrorKind::BrokenPipe + | io::ErrorKind::ConnectionReset + | io::ErrorKind::ConnectionAborted + | io::ErrorKind::WriteZero + | io::ErrorKind::UnexpectedEof + ) || (cfg!(windows) && matches!(error.raw_os_error(), Some(109 | 232 | 233))) +} + +impl ProbedTransport { + /// Returns the error with which a wait that Ctrl-C ended fails. + fn interrupted(&self) -> io::Error { + self.probe.interrupted.store(true, Ordering::SeqCst); + io::Error::other("interrupted") + } +} + +impl Transport for ProbedTransport { + fn read(&mut self, buffer: &mut [u8], timeout: Option) -> io::Result { + self.probe.direction.store(RECEIVING, Ordering::SeqCst); + let deadline = timeout.map(|timeout| std::time::Instant::now() + timeout); + loop { + if platform::interrupted() { + return Err(self.interrupted()); + } + let left = deadline + .map(|deadline| deadline.saturating_duration_since(std::time::Instant::now())); + let slice = left.map_or(POLL_SLICE, |left| left.min(POLL_SLICE)); + match self.inner.read(buffer, Some(slice)) { + Ok(count) => { + if count == 0 { + self.probe.closed.store(true, Ordering::SeqCst); + } + return Ok(count); + } + Err(error) if error.kind() == io::ErrorKind::TimedOut => { + if left.is_some_and(|left| left <= slice) { + return Err(error); + } + } + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => { + if closes(&error) { + self.probe.closed.store(true, Ordering::SeqCst); + } + return Err(error); + } + } + } + } + + fn write_all(&mut self, data: &[u8], timeout: Option) -> io::Result<()> { + self.probe.direction.store(SENDING, Ordering::SeqCst); + match self.inner.write_all(data, timeout) { + Ok(()) => { + self.probe.direction.store(RECEIVING, Ordering::SeqCst); + Ok(()) + } + Err(error) => { + if closes(&error) { + self.probe.closed.store(true, Ordering::SeqCst); + } + Err(error) + } + } + } +} + +/// A workload that [`Session::exec`] runs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ExecRequest { + /// The program and its arguments. + pub(crate) arguments: Vec, + /// `--exec-timeout-ms`, as given. + pub(crate) timeout_ms: crate::host::python::builtins::Int, + /// `--cwd`. + pub(crate) cwd: Option, + /// The environment, if one was given. + pub(crate) environment: Option>, + /// `--inherit-default-environment`. + pub(crate) inherit_default_environment: bool, +} + +/// How a workload ended: `ManagedExecResult`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ExecResult { + /// The workload's status. + pub(crate) returncode: i32, + /// One of [`EXIT_CATEGORIES`]. + pub(crate) category: &'static str, + /// The workload's standard output. + pub(crate) stdout: Vec, + /// The workload's standard error. + pub(crate) stderr: Vec, +} + +/// An authenticated control session: `ControlSession` of the former `control_session.py`. +pub(crate) struct Session { + /// The session, until a transfer fails. + inner: Option, + /// What its transport observed. + probe: Arc, +} + +impl Session { + /// Connects to `endpoint`, which OpenVMM process `pid` serves, and attaches with + /// `capability`, as `ControlSession.connect(endpoint, capability, timeout)` does: the endpoint + /// gets `timeout` seconds to appear, and the attachment `timeout` seconds more. + /// + /// Like `nvx::host::session`, it refuses an endpoint that another process serves, which it + /// treats as one that has not appeared yet, rather than handing that process the capability. + pub(crate) fn connect( + endpoint: &str, + pid: u32, + capability: &[u8; CAPABILITY_LEN], + timeout: f64, + ) -> Result { + let path = PurePath::native(endpoint); + let deadline = Deadline::after(timeout); + let transport = loop { + check_interrupt()?; + let slice = deadline + .remaining() + .map_or(POLL_SLICE, |left| left.min(POLL_SLICE)); + match platform::connect_endpoint(path.as_str(), pid, slice) { + Ok(transport) => break transport, + Err(error) => { + if cfg!(windows) + && error + .raw_os_error() + .is_some_and(|code| raised_at_once(code as u32)) + { + return Err(Exit::error(os_error_message(&error, None))); + } + if deadline.passed() { + return Err(Exit::error(format!( + "managed control endpoint did not become available: {path}" + ))); + } + sleep(RETRY_INTERVAL)?; + } + } + }; + Self::attach(transport, capability, timeout) + } + + /// Attaches over a connected `transport` with `capability` within `timeout` seconds, as the + /// second half of [`connect`](Self::connect). + fn attach( + transport: Box, + capability: &[u8; CAPABILITY_LEN], + timeout: f64, + ) -> Result { + let probe = Arc::new(Probe::default()); + let transport = Box::new(ProbedTransport { + inner: transport, + probe: Arc::clone(&probe), + }); + let deadline = Deadline::after(timeout); + probe.direction.store(SENDING, Ordering::SeqCst); + let mut session = Self { inner: None, probe }; + let attached = ControlSession::attach(transport, capability, deadline.instant()) + .map_err(|error| session.failure(error))?; + session.inner = Some(attached); + Ok(session) + } + + /// Returns the open session, or fails as a closed `ControlSession` does. + fn session(&mut self) -> Result<&mut ControlSession> { + self.inner + .as_mut() + .ok_or_else(|| Exit::error("managed control session is closed")) + } + + /// Sends a request and closes the session if the transfer fails. + fn send(&mut self, kind: u8, request_id: u64, payload: &[u8], deadline: Deadline) -> Result { + self.probe.direction.store(SENDING, Ordering::SeqCst); + let sent = self + .session()? + .send_request(kind, request_id, payload, deadline.instant()); + sent.map_err(|error| self.fail(error)) + } + + /// Reads the next response and closes the session if the transfer fails. + fn receive(&mut self, deadline: Deadline) -> Result<(u8, u64, i32, Vec)> { + self.probe.direction.store(RECEIVING, Ordering::SeqCst); + let received = self.session()?.read_response(Some(deadline.instant())); + received.map_err(|error| self.fail(error)) + } + + /// Closes the session after a failed transfer and returns the failure. + fn fail(&mut self, error: SessionError) -> Exit { + self.inner = None; + self.failure(error) + } + + /// Returns what Python raises for `error`. + fn failure(&self, error: SessionError) -> Exit { + if self.probe.interrupted.load(Ordering::SeqCst) { + return Exit::Interrupted; + } + let closed = self.probe.closed.load(Ordering::SeqCst); + match error { + SessionError::TimedOut => { + Exit::error(if self.probe.direction.load(Ordering::SeqCst) == SENDING { + "managed control request timed out" + } else { + "managed control response timed out" + }) + } + SessionError::Closed => Exit::EndpointClosed, + SessionError::Io(error) | SessionError::Refused(error) => { + if closed || closes(&error) { + Exit::EndpointClosed + } else { + Exit::error(os_error_message(&error, None)) + } + } + SessionError::Reset => Exit::error("managed control session was reset"), + SessionError::AuthenticationFailed => { + Exit::error("control capability authentication failed") + } + SessionError::ProcessExited => Exit::EndpointClosed, + SessionError::Protocol(message) => Exit::error(protocol_message(message)), + } + } + + /// Waits until the guest agent acknowledges readiness, as `ControlSession.ping` does. + pub(crate) fn ping(&mut self, timeout: f64) -> Result { + let request_id = request_id()?; + let deadline = Deadline::after(timeout); + self.send(APP_PING, request_id, &[], deadline)?; + let (kind, response_id, status, payload) = self.receive(deadline)?; + if kind != APP_READY || response_id != request_id || status != 0 || !payload.is_empty() { + return Err(Exit::error("managed guest did not acknowledge readiness")); + } + Ok(()) + } + + /// Asks the guest agent which control features its image provides. An agent that predates + /// the request refuses it as an unsupported operation, so its image provides none. + pub(crate) fn features(&mut self, timeout: f64) -> Result { + let request_id = request_id()?; + let deadline = Deadline::after(timeout); + self.send(APP_FEATURES, request_id, &[], deadline)?; + let (kind, response_id, status, payload) = self.receive(deadline)?; + if response_id != request_id { + return Err(Exit::error( + "managed guest returned a mismatched request ID", + )); + } + match kind { + APP_READY if status == 0 => GuestFeatures::decode(&payload) + .map_err(|_| Exit::error("managed guest returned an invalid features response")), + APP_ERROR if payload == UNSUPPORTED_OPERATION => Ok(GuestFeatures::NONE), + _ => Err(Exit::error( + "managed guest returned an invalid features response", + )), + } + } + + /// Runs a workload and returns how it ended, as `ControlSession.exec` does: the request and + /// every response must arrive within `response_timeout` seconds of each other's start. + pub(crate) fn exec( + &mut self, + request: &ExecRequest, + response_timeout: f64, + ) -> Result { + if !(response_timeout > 0.0 && response_timeout < f64::INFINITY) { + return Err(Exit::error( + "managed exec response timeout must be positive and finite", + )); + } + let arguments = &request.arguments; + if !(1..=MAX_ARGUMENTS).contains(&arguments.len()) { + return Err(Exit::error("managed exec requires 1 through 64 arguments")); + } + for argument in arguments { + if argument.is_empty() || argument.len() > MAX_ARGUMENT_BYTES || argument.contains('\0') + { + return Err(Exit::error( + "managed exec argument is empty or exceeds 4096 bytes", + )); + } + } + if !arguments[0].starts_with('/') { + return Err(Exit::error("managed exec entrypoint must be absolute")); + } + let timeout_ms = request + .timeout_ms + .to_u32() + .ok_or_else(|| Exit::error("managed exec timeout must be 0 through 4294967295 ms"))?; + if let Some(cwd) = &request.cwd + && (cwd.is_empty() + || cwd.len() > MAX_ARGUMENT_BYTES + || cwd.contains('\0') + || !cwd.starts_with('/')) + { + return Err(Exit::error( + "managed exec working directory must be an absolute path of at most 4096 bytes", + )); + } + if let Some(entries) = &request.environment { + validate_exec_environment(entries)?; + } + let environment = match &request.environment { + None => WorkloadEnvironment::Default, + Some(entries) if request.inherit_default_environment => { + WorkloadEnvironment::Layered(entries) + } + Some(entries) => WorkloadEnvironment::Replaced(entries), + }; + let workload = Workload { + argv: arguments.clone(), + timeout_ms, + cwd: request.cwd.as_deref(), + environment, + }; + // The checks above leave only the size of the request for the encoder to refuse. + let payload = encode_exec_payload(&workload) + .map_err(|_| Exit::error("managed exec request exceeds the protocol limit"))?; + let request_id = request_id()?; + let deadline = Deadline::after(response_timeout); + self.send(APP_EXEC, request_id, &payload, deadline)?; + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + loop { + let (kind, response_id, status, response) = self.receive(deadline)?; + if response_id != request_id { + return Err(Exit::error( + "managed guest returned a mismatched request ID", + )); + } + match kind { + APP_STDOUT | APP_STDERR => { + // The guest agent forwards at most this much output, so more breaks the + // protocol, as `nvx::host::instance` also finds. + if stdout.len() + stderr.len() + response.len() > MAX_OUTPUT_BYTES { + return Err(Exit::error(format!( + "managed guest exceeded the {MAX_OUTPUT_BYTES}-byte output limit" + ))); + } + let output = if kind == APP_STDOUT { + &mut stdout + } else { + &mut stderr + }; + output.extend_from_slice(&response); + } + APP_EXIT => { + if !response.is_ascii() { + return Err(Exit::error( + "managed guest returned an invalid exit category", + )); + } + let category = EXIT_CATEGORIES + .iter() + .find(|category| category.as_bytes() == response) + .ok_or_else(|| { + Exit::error("managed guest returned an unsupported exit category") + })?; + return Ok(ExecResult { + returncode: status, + category, + stdout, + stderr, + }); + } + APP_ERROR => { + // `bytes.decode("ascii", "replace")`. + let category: String = response + .iter() + .map(|&byte| { + if byte.is_ascii() { + char::from(byte) + } else { + '\u{fffd}' + } + }) + .collect(); + return Err(Exit::error(format!( + "managed guest rejected exec (status={status}, category={category})" + ))); + } + _ => { + return Err(Exit::error( + "managed guest returned an invalid exec response", + )); + } + } + } + } + + /// Asks the guest agent to shut the VM down, as `ControlSession.stop` does. + pub(crate) fn stop(&mut self, timeout: f64) -> Result { + let request_id = request_id()?; + let deadline = Deadline::after(timeout); + self.send(APP_STOP, request_id, &[], deadline)?; + let (kind, response_id, status, payload) = self.receive(deadline)?; + if kind != APP_STOPPED || response_id != request_id || status != 0 || !payload.is_empty() { + return Err(Exit::error("managed guest did not acknowledge stop")); + } + Ok(()) + } +} + +/// Returns Python's message for a violation of the protocol that `nvx::host::session` reports. +/// +/// The two sessions check the same records in the same order, and their messages differ only +/// where Python folds a case into a broader one. +fn protocol_message(message: String) -> String { + match message.as_str() { + "control attach response carried a payload" => { + "control attach response carried an invalid payload".to_owned() + } + "control endpoint returned an oversized outer record" => { + "control endpoint returned an invalid outer record".to_owned() + } + // Python checks the length of the frame with the record that carries it. + "control endpoint returned a truncated application frame" => { + "control endpoint returned an invalid data record".to_owned() + } + _ => message, + } +} + +/// Returns whether Python's `_NamedPipeStream.connect` raises the Windows error `code` at once +/// instead of retrying: those that CPython maps to an `errno` other than `ENOENT`, `EACCES`, +/// `EBUSY`, `EAGAIN`, and `EINVAL`, its default. +fn raised_at_once(code: u32) -> bool { + matches!( + code, + // E2BIG, ENOEXEC, EBADF, ECHILD, and ENOMEM. + 10 | 11 | 188 + ..=202 | 6 | 114 | 130 | 128 | 129 | 7 | 8 | 9 | 1816 + // EEXIST, EXDEV, ENOTDIR, EMFILE, ENOSPC, EPIPE, ENOTEMPTY, and EILSEQ. + | 80 | 183 | 17 | 267 | 4 | 112 | 109 | 232 | 145 | 1113 + ) +} + +/// Returns a request ID: `secrets.randbits(64) or 1`. +fn request_id() -> Result { + let mut bytes = [0u8; 8]; + getrandom::fill(&mut bytes) + .map_err(|error| Exit::error(format!("cannot generate a request ID: {error}")))?; + Ok(u64::from_le_bytes(bytes).max(1)) +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/cli/session/tests.rs b/nvx-rs/src/host/cli/session/tests.rs new file mode 100644 index 000000000..66747dfe1 --- /dev/null +++ b/nvx-rs/src/host/cli/session/tests.rs @@ -0,0 +1,1024 @@ +//! The tests of the former `scripts/test_control_session.py`, on in-memory streams and on the +//! endpoints of each host, except those that the tests of [`crate::host::session`] cover. + +use std::collections::VecDeque; +use std::sync::Mutex; +use std::sync::mpsc::{self, RecvTimeoutError, TrySendError}; +use std::thread; +use std::time::Instant; + +use super::*; +use crate::host::python::builtins::Int; +use crate::protocol::{ + EXEC_CWD_PRESENT, EXEC_ENVIRONMENT_PRESENT, EXEC_EXTENDED, EXEC_INHERIT_DEFAULT_ENV, + OUTER_DATA, OUTER_HEADER_LEN, OUTER_HOST_ATTACH, OUTER_READY, OUTER_RESET, decode_app, + decode_outer_header, encode_app, encode_outer, +}; + +const CAPABILITY: [u8; CAPABILITY_LEN] = [7; CAPABILITY_LEN]; +const INSTANCE: [u8; 16] = [0x11; 16]; + +/// What a [`Recording`] transport records: the direction and timeout of each transfer. +type Transfers = Arc)>>>; + +/// One end of an in-memory stream, like one socket of `socket.socketpair()`: it reads what the +/// other end writes, reads the end of the stream once the other end is gone, and holds at most +/// `capacity` unread writes, after which a write waits for the reader until its timeout. +struct End { + incoming: mpsc::Receiver>, + outgoing: mpsc::SyncSender>, + buffer: VecDeque, +} + +fn stream_pair(capacity: usize) -> (End, End) { + let (to_guest, from_host) = mpsc::sync_channel(capacity); + let (to_host, from_guest) = mpsc::sync_channel(64); + ( + End { + incoming: from_guest, + outgoing: to_guest, + buffer: VecDeque::new(), + }, + End { + incoming: from_host, + outgoing: to_host, + buffer: VecDeque::new(), + }, + ) +} + +impl Transport for End { + fn read(&mut self, buffer: &mut [u8], timeout: Option) -> io::Result { + if self.buffer.is_empty() { + let chunk = match timeout { + Some(timeout) => self.incoming.recv_timeout(timeout), + None => self + .incoming + .recv() + .map_err(|_| RecvTimeoutError::Disconnected), + }; + match chunk { + Ok(chunk) => self.buffer.extend(chunk), + Err(RecvTimeoutError::Timeout) => return Err(io::ErrorKind::TimedOut.into()), + Err(RecvTimeoutError::Disconnected) => return Ok(0), + } + } + let count = buffer.len().min(self.buffer.len()); + for (byte, value) in buffer.iter_mut().zip(self.buffer.drain(..count)) { + *byte = value; + } + Ok(count) + } + + fn write_all(&mut self, data: &[u8], timeout: Option) -> io::Result<()> { + let deadline = timeout.map(|timeout| Instant::now() + timeout); + let mut chunk = data.to_vec(); + loop { + match self.outgoing.try_send(chunk) { + Ok(()) => return Ok(()), + Err(TrySendError::Disconnected(_)) => { + return Err(io::ErrorKind::BrokenPipe.into()); + } + Err(TrySendError::Full(returned)) => { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { + return Err(io::ErrorKind::TimedOut.into()); + } + chunk = returned; + thread::sleep(Duration::from_millis(5)); + } + } + } + } +} + +/// The guest side of a session: OpenVMM's endpoint and the guest agent behind it. +struct Guest { + stream: T, + sequence: u64, +} + +impl Guest { + fn new(stream: T) -> Self { + Self { + stream, + sequence: 0, + } + } + + /// Reads `length` bytes, or returns `None` at the end of the stream. + fn read_exact(&mut self, length: usize) -> Option> { + let mut data = vec![0; length]; + let mut filled = 0; + while filled < length { + match self + .stream + .read(&mut data[filled..], Some(Duration::from_secs(10))) + { + Ok(0) | Err(_) => return None, + Ok(count) => filled += count, + } + } + Some(data) + } + + /// Reads one outer record and returns its type and payload. + fn record(&mut self) -> Option<(u8, Vec)> { + let header = self.read_exact(OUTER_HEADER_LEN)?; + let header = decode_outer_header(header.as_slice().try_into().unwrap()).unwrap(); + Some((header.record_type, self.read_exact(header.length)?)) + } + + /// Authenticates the host as OpenVMM's endpoint does. + fn accept(&mut self) { + let (record_type, payload) = self.record().unwrap(); + assert_eq!( + (record_type, payload.as_slice()), + (OUTER_HOST_ATTACH, &CAPABILITY[..]) + ); + let ready = encode_outer(OUTER_READY, &INSTANCE, 1, 0, &[]).unwrap(); + self.stream.write_all(&ready, None).unwrap(); + } + + /// Reads one application request and returns its kind, request ID, and payload. + fn request(&mut self) -> (u8, u64, Vec) { + let (record_type, frame) = self.record().unwrap(); + assert_eq!(record_type, OUTER_DATA); + let frame = decode_app(&frame).unwrap(); + (frame.kind, frame.request_id, frame.payload.to_vec()) + } + + fn send(&mut self, kind: u8, request_id: u64, status: i32, payload: &[u8]) { + self.sequence += 1; + let frame = encode_app(kind, request_id, status, payload); + let record = encode_outer(OUTER_DATA, &INSTANCE, 1, self.sequence, &frame).unwrap(); + self.stream.write_all(&record, None).unwrap(); + } +} + +/// Returns a session attached over an in-memory stream to a guest that `serve` plays. +fn session_with( + serve: impl FnOnce(&mut Guest) + Send + 'static, +) -> (Session, thread::JoinHandle<()>) { + let (host, guest) = stream_pair(64); + let worker = thread::spawn(move || { + let mut guest = Guest::new(guest); + guest.accept(); + serve(&mut guest); + }); + let session = Session::attach(Box::new(host), &CAPABILITY, 5.0).unwrap(); + (session, worker) +} + +fn request(arguments: &[&str]) -> ExecRequest { + ExecRequest { + arguments: arguments + .iter() + .map(|argument| (*argument).to_owned()) + .collect(), + timeout_ms: Int::from(0_u32), + cwd: None, + environment: None, + inherit_default_environment: false, + } +} + +/// Returns the message of a failure. +fn message(result: Result) -> String { + match result { + Ok(value) => panic!("unexpected success: {value:?}"), + Err(exit) => exit + .message() + .map_or_else(|| format!("{exit:?}"), str::to_owned), + } +} + +/// Returns the payload of the exec request of `workload`, which the guest answers. +fn exec_payload(workload: ExecRequest) -> Vec { + let (sender, receiver) = mpsc::channel(); + let (mut session, worker) = session_with(move |guest| { + let (kind, request_id, payload) = guest.request(); + assert_eq!(kind, APP_EXEC); + sender.send(payload).unwrap(); + guest.send(APP_EXIT, request_id, 0, b"exit"); + }); + session.exec(&workload, 5.0).unwrap(); + worker.join().unwrap(); + receiver.recv().unwrap() +} + +fn u16_at(payload: &[u8], offset: usize) -> u16 { + u16::from_le_bytes(payload[offset..offset + 2].try_into().unwrap()) +} + +fn u32_at(payload: &[u8], offset: usize) -> u32 { + u32::from_le_bytes(payload[offset..offset + 4].try_into().unwrap()) +} + +/// Returns the length-prefixed values that start at `offset`, and the offset after them. +fn values(payload: &[u8], mut offset: usize, count: usize) -> (Vec>, usize) { + let mut values = Vec::new(); + for _ in 0..count { + let length = u32_at(payload, offset) as usize; + values.push(payload[offset + 4..offset + 4 + length].to_vec()); + offset += 4 + length; + } + (values, offset) +} + +#[test] +fn exec_streams_output_and_returns_bounded_status() { + let (mut session, worker) = session_with(|guest| { + let (kind, request_id, payload) = guest.request(); + assert_eq!(kind, APP_EXEC); + // The legacy request: the timeout, the argument count, and no extension. + assert_eq!( + ( + u32_at(&payload, 0), + u16_at(&payload, 4), + u16_at(&payload, 6) + ), + (5000, 3, 0) + ); + guest.send(APP_STDOUT, request_id, 0, b"hello"); + guest.send(APP_STDERR, request_id, 0, b"warning"); + guest.send(APP_EXIT, request_id, 7, b"exit"); + }); + let mut workload = request(&["/bin/sh", "-c", "echo hello"]); + workload.timeout_ms = Int::from(5000_u32); + let result = session.exec(&workload, 5.0).unwrap(); + worker.join().unwrap(); + assert_eq!( + result, + ExecResult { + returncode: 7, + category: "exit", + stdout: b"hello".to_vec(), + stderr: b"warning".to_vec(), + } + ); +} + +#[test] +fn exec_rejects_unbounded_or_relative_arguments() { + let (mut session, worker) = session_with(|_| {}); + assert_eq!( + message(session.exec(&request(&["relative"]), 1.0)), + "managed exec entrypoint must be absolute" + ); + assert_eq!( + message(session.exec(&request(&["/bin/true"; 65]), 1.0)), + "managed exec requires 1 through 64 arguments" + ); + for argument in ["", "a\0b", &"a".repeat(4097)] { + assert_eq!( + message(session.exec(&request(&["/bin/true", argument]), 1.0)), + "managed exec argument is empty or exceeds 4096 bytes" + ); + } + drop(session); + worker.join().unwrap(); +} + +#[test] +fn exec_encodes_extended_cwd_and_environment_exactly() { + let environment = ["EMPTY=", "SPACED=a b", "EQUALS=a=b", "UTF8=\u{2603}"]; + let cwd = "/tmp/space \u{2603}"; + let payload = exec_payload(ExecRequest { + arguments: ["/bin/sh", "-c", "printf exact"] + .map(str::to_owned) + .to_vec(), + timeout_ms: Int::from(0xFFFF_FFFF_u32), + cwd: Some(cwd.to_owned()), + environment: Some(environment.map(str::to_owned).to_vec()), + inherit_default_environment: false, + }); + assert_eq!( + ( + u32_at(&payload, 0), + u16_at(&payload, 4), + u16_at(&payload, 6), + u16_at(&payload, 8), + u16_at(&payload, 10), + u32_at(&payload, 12) as usize + ), + ( + 0xFFFF_FFFF, + 3, + EXEC_EXTENDED, + EXEC_CWD_PRESENT | EXEC_ENVIRONMENT_PRESENT, + 4, + cwd.len() + ) + ); + let (arguments, offset) = values(&payload, 16, 3); + assert_eq!(arguments, [&b"/bin/sh"[..], b"-c", b"printf exact"]); + assert_eq!(&payload[offset..offset + cwd.len()], cwd.as_bytes()); + let (entries, end) = values(&payload, offset + cwd.len(), 4); + assert_eq!(entries, environment.map(|entry| entry.as_bytes().to_vec())); + assert_eq!(end, payload.len()); +} + +fn environment_payload(environment: Option<&[&str]>, inherit: bool) -> Vec { + let mut workload = request(&["/bin/true"]); + workload.environment = + environment.map(|entries| entries.iter().map(|entry| (*entry).to_owned()).collect()); + workload.inherit_default_environment = inherit; + exec_payload(workload) +} + +#[test] +fn exec_distinguishes_omitted_and_empty_environment() { + let omitted = environment_payload(None, false); + let empty = environment_payload(Some(&[]), false); + assert_eq!(u16_at(&omitted, 6), 0); + assert_eq!(u16_at(&empty, 6), EXEC_EXTENDED); + assert_eq!(u16_at(&empty, 8), EXEC_ENVIRONMENT_PRESENT); + assert_eq!(u16_at(&empty, 10), 0); +} + +#[test] +fn exec_layers_only_a_supplied_environment_over_the_defaults() { + let layered_flags = EXEC_ENVIRONMENT_PRESENT | EXEC_INHERIT_DEFAULT_ENV; + let fields = |payload: &[u8]| { + ( + u16_at(payload, 6), + u16_at(payload, 8), + u16_at(payload, 10), + u32_at(payload, 12), + ) + }; + let layered = environment_payload(Some(&["A=1"]), true); + assert_eq!(fields(&layered), (EXEC_EXTENDED, layered_flags, 1, 0)); + assert!(layered.ends_with(b"\x03\x00\x00\x00A=1")); + let empty = environment_payload(Some(&[]), true); + assert_eq!(fields(&empty), (EXEC_EXTENDED, layered_flags, 0, 0)); + let replaced = environment_payload(Some(&["A=1"]), false); + assert_eq!(u16_at(&replaced, 8), EXEC_ENVIRONMENT_PRESENT); + // An omitted environment already is the default one, so the request stays the legacy one, + // which the guest agent accepts without an environment. + assert_eq!( + environment_payload(None, true), + environment_payload(None, false) + ); +} + +#[test] +fn exec_accepts_full_uint32_timeout_range_without_waiting() { + for timeout in [0_u32, 3_600_001, 86_400_000, 0xFFFF_FFFF] { + let mut workload = request(&["/bin/true"]); + workload.timeout_ms = Int::from(timeout); + let started = Instant::now(); + assert_eq!(u32_at(&exec_payload(workload), 0), timeout); + assert!(started.elapsed() < Duration::from_secs(5)); + } +} + +/// Returns a session whose guest reports how many bytes reach it before the end of the stream. +fn silent_session() -> (Session, thread::JoinHandle<()>, mpsc::Receiver) { + let (sender, receiver) = mpsc::channel(); + let (session, worker) = session_with(move |guest| { + let mut buffer = [0u8; 64]; + let count = guest.stream.read(&mut buffer, None).unwrap_or(0); + sender.send(count).unwrap(); + }); + (session, worker, receiver) +} + +#[test] +fn exec_rejects_invalid_timeout_and_execution_fields_before_sending() { + let (mut session, worker, received) = silent_session(); + for timeout in [Int::from(-1_i64), Int::parse("4294967296").unwrap()] { + let mut workload = request(&["/bin/true"]); + workload.timeout_ms = timeout; + assert_eq!( + message(session.exec(&workload, 1.0)), + "managed exec timeout must be 0 through 4294967295 ms" + ); + } + for cwd in [ + "", + "relative", + "/bad\0path", + &format!("/{}", "a".repeat(4096)), + ] { + let mut workload = request(&["/bin/true"]); + workload.cwd = Some(cwd.to_owned()); + assert_eq!( + message(session.exec(&workload, 1.0)), + "managed exec working directory must be an absolute path of at most 4096 bytes" + ); + } + let invalid = "managed exec environment entries must be non-empty KEY=VALUE strings of at \ + most 4096 bytes"; + for (environment, expected) in [ + (&["MISSING_EQUALS"][..], invalid), + (&["=empty-name"][..], invalid), + ( + &["DUP=1", "DUP=2"][..], + "managed exec environment contains duplicate key: DUP", + ), + ] { + let mut workload = request(&["/bin/true"]); + workload.environment = Some( + environment + .iter() + .map(|entry| (*entry).to_owned()) + .collect(), + ); + assert_eq!(message(session.exec(&workload, 1.0)), expected); + } + // Nothing reached the guest, which reads the end of the stream. + drop(session); + worker.join().unwrap(); + assert_eq!(received.recv().unwrap(), 0); +} + +#[test] +fn exec_rejects_invalid_response_timeout_before_sending() { + let (mut session, worker, received) = silent_session(); + for timeout in [0.0, -1.0, f64::NAN, f64::INFINITY] { + assert_eq!( + message(session.exec(&request(&["/bin/true"]), timeout)), + "managed exec response timeout must be positive and finite" + ); + } + drop(session); + worker.join().unwrap(); + assert_eq!(received.recv().unwrap(), 0); +} + +#[test] +fn exec_rejects_unknown_exit_category() { + // A cancellation, which this session never requests, is not among the categories. + for category in [&b"guest-provided-detail"[..], b"cancelled"] { + let (mut session, worker) = session_with(move |guest| { + let (_, request_id, _) = guest.request(); + guest.send(APP_EXIT, request_id, 125, category); + }); + assert_eq!( + message(session.exec(&request(&["/bin/true"]), 5.0)), + "managed guest returned an unsupported exit category" + ); + worker.join().unwrap(); + } + let (mut session, worker) = session_with(|guest| { + let (_, request_id, _) = guest.request(); + guest.send(APP_EXIT, request_id, 0, b"\xffexit"); + }); + assert_eq!( + message(session.exec(&request(&["/bin/true"]), 5.0)), + "managed guest returned an invalid exit category" + ); + worker.join().unwrap(); +} + +#[test] +fn exec_refusal_keeps_status_category_and_diagnostic() { + let (mut session, worker) = session_with(|guest| { + let (_, request_id, _) = guest.request(); + guest.send( + APP_STDERR, + request_id, + 0, + b"nvx-managed-agent: cannot enter working directory /missing: No such file or \ + directory\n", + ); + guest.send(APP_ERROR, request_id, 2, b"cwd-failed"); + }); + let mut workload = request(&["/bin/true"]); + workload.cwd = Some("/missing".to_owned()); + assert_eq!( + message(session.exec(&workload, 5.0)), + "managed guest rejected exec (status=2, category=cwd-failed)" + ); + worker.join().unwrap(); + // A category that is not ASCII is decoded with replacement characters. + let (mut session, worker) = session_with(|guest| { + let (_, request_id, _) = guest.request(); + guest.send(APP_ERROR, request_id, 5, b"bad\xff"); + }); + assert_eq!( + message(session.exec(&request(&["/bin/true"]), 5.0)), + "managed guest rejected exec (status=5, category=bad\u{fffd})" + ); + worker.join().unwrap(); +} + +#[test] +fn session_reset_is_not_a_closed_endpoint() { + // A reset comes from an OpenVMM that still serves the endpoint, so it must not look like the + // closure with which OpenVMM shuts down. + let (mut session, worker) = session_with(|guest| { + let _ = guest.request(); + let reset = encode_outer(OUTER_RESET, &INSTANCE, 1, 0, &[]).unwrap(); + guest.stream.write_all(&reset, None).unwrap(); + let _ = guest.record(); + }); + let failure = session.ping(5.0).unwrap_err(); + assert!( + matches!(&failure, Exit::Error(text) if text == "managed control session was reset"), + "{failure:?}" + ); + drop(session); + worker.join().unwrap(); +} + +/// A guest transport that answers each request at once, PING with READY, EXEC with an exit, and +/// STOP with STOPPED, records the direction and timeout of each transfer, and from the transfer +/// `from` on fails each transfer in the direction `failure` by timing out: the +/// `_RecordingStream` of the Python tests. +struct Recording { + responses: VecDeque, + sequence: u64, + transfers: Transfers, + failure: Option<(&'static str, usize)>, +} + +impl Recording { + fn new(failure: Option<(&'static str, usize)>) -> (Self, Transfers) { + let transfers = Transfers::default(); + let transport = Self { + responses: VecDeque::new(), + sequence: 0, + transfers: Arc::clone(&transfers), + failure, + }; + (transport, transfers) + } + + fn transfer(&mut self, kind: &'static str, timeout: Option) -> io::Result<()> { + let mut transfers = self.transfers.lock().unwrap(); + let index = transfers.len(); + transfers.push((kind, timeout)); + match self.failure { + Some((failing, from)) if failing == kind && index >= from => { + Err(io::ErrorKind::TimedOut.into()) + } + _ => Ok(()), + } + } +} + +impl Transport for Recording { + fn read(&mut self, buffer: &mut [u8], timeout: Option) -> io::Result { + self.transfer("read", timeout)?; + let count = buffer.len().min(self.responses.len()); + for (byte, value) in buffer.iter_mut().zip(self.responses.drain(..count)) { + *byte = value; + } + Ok(count) + } + + fn write_all(&mut self, data: &[u8], timeout: Option) -> io::Result<()> { + self.transfer("write", timeout)?; + let header = decode_outer_header(data[..OUTER_HEADER_LEN].try_into().unwrap()).unwrap(); + let response = if header.record_type == OUTER_HOST_ATTACH { + encode_outer(OUTER_READY, &INSTANCE, 1, 0, &[]).unwrap() + } else { + let request = decode_app(&data[OUTER_HEADER_LEN..]).unwrap(); + let (kind, payload) = match request.kind { + APP_PING => (APP_READY, &b""[..]), + APP_EXEC => (APP_EXIT, &b"exit"[..]), + _ => (APP_STOPPED, &b""[..]), + }; + self.sequence += 1; + let frame = encode_app(kind, request.request_id, 0, payload); + encode_outer(OUTER_DATA, &INSTANCE, 1, self.sequence, &frame).unwrap() + }; + self.responses.extend(response); + Ok(()) + } +} + +#[test] +fn operations_bound_their_requests_by_their_deadlines() { + // Issue #464: an operation's timeout bounds sending its request as well as receiving its + // response, so attach, ping, exec, and stop each write within the time in which they then + // read: no transfer gets more than what is left of its operation's timeout. + let (transport, transfers) = Recording::new(None); + let mut session = Session::attach(Box::new(transport), &CAPABILITY, 7.0).unwrap(); + let mut operations = vec![(7.0, transfers.lock().unwrap().split_off(0))]; + session.ping(5.0).unwrap(); + operations.push((5.0, transfers.lock().unwrap().split_off(0))); + session.exec(&request(&["/bin/true"]), 3.0).unwrap(); + operations.push((3.0, transfers.lock().unwrap().split_off(0))); + session.stop(2.0).unwrap(); + operations.push((2.0, transfers.lock().unwrap().split_off(0))); + for (timeout, recorded) in operations { + let kinds: Vec<_> = recorded.iter().map(|(kind, _)| *kind).collect(); + assert_eq!(kinds.first(), Some(&"write"), "{kinds:?}"); + assert!(kinds[1..].iter().all(|kind| *kind == "read"), "{kinds:?}"); + for (kind, left) in recorded { + let left = left.unwrap().as_secs_f64(); + // Like Python's, reads wait in slices of at most 250 ms, so that Ctrl-C ends them. + let (least, most) = if kind == "write" { + (timeout - 1.0, timeout) + } else { + (0.0, 0.25) + }; + assert!(left > least && left <= most, "{kind}: {left} of {timeout}"); + } + } +} + +#[test] +fn failed_transfer_closes_the_session() { + // A transfer that fails, as by timing out, may cut its record short, after which the + // endpoint would frame every later record wrongly. + for (failure, expected) in [ + ("write", "managed control request timed out"), + ("read", "managed control response timed out"), + ] { + // The attachment's write and read succeed. + let (transport, transfers) = Recording::new(Some((failure, 2))); + let mut session = Session::attach(Box::new(transport), &CAPABILITY, 5.0).unwrap(); + assert_eq!(message(session.ping(5.0)), expected); + let count = transfers.lock().unwrap().len(); + assert_eq!( + message(session.exec(&request(&["/bin/true"]), 5.0)), + "managed control session is closed" + ); + assert_eq!( + message(session.stop(5.0)), + "managed control session is closed" + ); + assert_eq!(transfers.lock().unwrap().len(), count); + } +} + +#[test] +fn request_to_a_stalled_endpoint_times_out_and_closes_the_session() { + // Issue #464: a request that the endpoint does not read must not hold the operation past its + // timeout. + let (host, guest) = stream_pair(0); + let (release, released) = mpsc::channel::<()>(); + let worker = thread::spawn(move || { + let mut guest = Guest::new(guest); + guest.accept(); + // The endpoint reads nothing more until the client gives up. + let _ = released.recv_timeout(Duration::from_secs(10)); + }); + let mut session = Session::attach(Box::new(host), &CAPABILITY, 5.0).unwrap(); + let started = Instant::now(); + assert_eq!( + message(session.exec(&request(&["/bin/true"]), 0.25)), + "managed control request timed out" + ); + let elapsed = started.elapsed(); + assert!(elapsed >= Duration::from_millis(250), "{elapsed:?}"); + assert!(elapsed < Duration::from_millis(2250), "{elapsed:?}"); + assert_eq!( + message(session.ping(5.0)), + "managed control session is closed" + ); + release.send(()).unwrap(); + worker.join().unwrap(); +} + +#[test] +fn response_that_does_not_arrive_times_out() { + let (mut session, worker) = session_with(|guest| { + let _ = guest.request(); + // The guest answers nothing until the host gives up. + let _ = guest.record(); + }); + let started = Instant::now(); + assert_eq!( + message(session.ping(0.2)), + "managed control response timed out" + ); + assert!(started.elapsed() < Duration::from_secs(2)); + drop(session); + worker.join().unwrap(); +} + +#[test] +fn a_closed_endpoint_ends_the_operation_with_its_own_error() { + // OpenVMM closes the endpoint as it tears the VM down, which start and stop tell apart from + // other failures. + let (mut session, worker) = session_with(|guest| { + let _ = guest.record(); + }); + let failure = session.ping(5.0).unwrap_err(); + worker.join().unwrap(); + assert!(matches!(failure, Exit::EndpointClosed), "{failure:?}"); + assert_eq!(failure.message(), Some("managed control endpoint closed")); + // So does a write to an endpoint that is gone. + let (mut session, worker) = session_with(|_| {}); + worker.join().unwrap(); + let failure = session.stop(5.0).unwrap_err(); + assert!(matches!(failure, Exit::EndpointClosed), "{failure:?}"); +} + +#[test] +fn output_beyond_the_guest_limit_breaks_the_protocol() { + // The guest agent forwards at most 1 MiB of output, so a guest that sends more misbehaves. + let (mut session, worker) = session_with(|guest| { + let (_, request_id, _) = guest.request(); + let chunk = vec![b'x'; 60_000]; + for _ in 0..18 { + guest.send(APP_STDOUT, request_id, 0, &chunk); + } + guest.send(APP_EXIT, request_id, 0, b"exit"); + }); + assert_eq!( + message(session.exec(&request(&["/bin/true"]), 5.0)), + format!("managed guest exceeded the {MAX_OUTPUT_BYTES}-byte output limit") + ); + drop(session); + worker.join().unwrap(); +} + +#[test] +fn features_of_a_guest_that_predates_them_are_none() { + let (mut session, worker) = session_with(|guest| { + let (kind, request_id, _) = guest.request(); + assert_eq!(kind, APP_FEATURES); + guest.send(APP_ERROR, request_id, 95, UNSUPPORTED_OPERATION); + let (_, request_id, _) = guest.request(); + guest.send( + APP_READY, + request_id, + 0, + &GuestFeatures::SANDBOX_AGENT.encode(), + ); + let (_, request_id, _) = guest.request(); + guest.send(APP_ERROR, request_id, 22, b"invalid-request"); + }); + assert_eq!(session.features(5.0).unwrap(), GuestFeatures::NONE); + assert_eq!(session.features(5.0).unwrap(), GuestFeatures::SANDBOX_AGENT); + assert_eq!( + message(session.features(5.0)), + "managed guest returned an invalid features response" + ); + worker.join().unwrap(); +} + +#[test] +fn protocol_violations_keep_the_messages_of_python() { + let (mut session, worker) = session_with(|guest| { + let (_, request_id, _) = guest.request(); + guest.send(APP_READY, request_id ^ 1, 0, &[]); + }); + assert_eq!( + message(session.ping(5.0)), + "managed guest did not acknowledge readiness" + ); + worker.join().unwrap(); + let (mut session, worker) = session_with(|guest| { + let (_, request_id, _) = guest.request(); + guest.send(APP_STDOUT, request_id ^ 1, 0, b"x"); + }); + assert_eq!( + message(session.exec(&request(&["/bin/true"]), 5.0)), + "managed guest returned a mismatched request ID" + ); + worker.join().unwrap(); + let (mut session, worker) = session_with(|guest| { + let (_, request_id, _) = guest.request(); + guest.send(APP_READY, request_id, 0, b""); + }); + assert_eq!( + message(session.stop(5.0)), + "managed guest did not acknowledge stop" + ); + worker.join().unwrap(); +} + +/// The endpoints of POSIX hosts: Unix-domain sockets that this process serves. +#[cfg(unix)] +mod endpoints { + use std::io::{Read, Write}; + use std::os::unix::net::{UnixListener, UnixStream}; + + use super::*; + + /// The server side of a Unix-domain socket. + struct Server(UnixStream); + + impl Transport for Server { + fn read(&mut self, buffer: &mut [u8], timeout: Option) -> io::Result { + self.0.set_read_timeout(timeout)?; + self.0.read(buffer) + } + + fn write_all(&mut self, data: &[u8], _timeout: Option) -> io::Result<()> { + self.0.write_all(data) + } + } + + /// Serves `serve` on a fresh endpoint and returns a session connected to it. + fn connected( + serve: impl FnOnce(&mut Guest) + Send + 'static, + ) -> (Session, thread::JoinHandle<()>, tempfile::TempDir) { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("control.sock"); + let listener = UnixListener::bind(&path).unwrap(); + let worker = thread::spawn(move || { + let (stream, _) = listener.accept().unwrap(); + let mut guest = Guest::new(Server(stream)); + guest.accept(); + serve(&mut guest); + }); + let session = + Session::connect(path.to_str().unwrap(), std::process::id(), &CAPABILITY, 5.0).unwrap(); + (session, worker, directory) + } + + #[test] + fn read_reports_a_closed_endpoint() { + let (mut session, worker, _directory) = connected(|guest| { + let _ = guest.record(); + }); + let failure = session.ping(5.0).unwrap_err(); + worker.join().unwrap(); + assert!(matches!(failure, Exit::EndpointClosed), "{failure:?}"); + } + + #[test] + fn reset_and_broken_pipe_report_a_closed_endpoint() { + // OpenVMM may close the socket before it reads everything that the client sent, which + // resets the client's reads, and a later write fails with a broken pipe. + let (mut session, worker, _directory) = connected(|guest| { + let _ = guest.read_exact(OUTER_HEADER_LEN); + }); + let mut workload = request(&["/bin/true"]); + workload + .arguments + .extend(std::iter::repeat_n("a".repeat(4000), 10)); + let failure = session.exec(&workload, 5.0).unwrap_err(); + worker.join().unwrap(); + assert!(matches!(failure, Exit::EndpointClosed), "{failure:?}"); + let (mut session, worker, _directory) = connected(|_| {}); + worker.join().unwrap(); + let failure = session.stop(5.0).unwrap_err(); + assert!(matches!(failure, Exit::EndpointClosed), "{failure:?}"); + } + + #[test] + fn endpoints_that_never_appear_time_out_with_their_path() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("control.sock"); + let started = Instant::now(); + let failure = + Session::connect(path.to_str().unwrap(), std::process::id(), &CAPABILITY, 0.2) + .err() + .unwrap(); + assert!(started.elapsed() >= Duration::from_millis(200)); + assert_eq!( + failure.message().unwrap(), + format!( + "managed control endpoint did not become available: {}", + path.display() + ) + ); + } +} + +/// The endpoints of Windows hosts: named pipes that this process serves, through the Win32 calls +/// that the standard library lacks. +#[cfg(windows)] +#[allow(unsafe_code)] +mod endpoints { + use std::fs::File; + use std::io::{Read, Write}; + use std::os::windows::io::{AsRawHandle, FromRawHandle}; + + use windows_sys::Win32::Foundation::{ + ERROR_PIPE_CONNECTED, GetLastError, INVALID_HANDLE_VALUE, + }; + use windows_sys::Win32::Storage::FileSystem::PIPE_ACCESS_DUPLEX; + use windows_sys::Win32::System::Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, PIPE_READMODE_BYTE, PIPE_TYPE_BYTE, PIPE_WAIT, + }; + + use super::*; + + /// The server side of a named pipe. + struct Server(File); + + impl Transport for Server { + fn read(&mut self, buffer: &mut [u8], _timeout: Option) -> io::Result { + match self.0.read(buffer) { + // ERROR_BROKEN_PIPE: the client closed its end. + Err(error) if error.raw_os_error() == Some(109) => Ok(0), + result => result, + } + } + + fn write_all(&mut self, data: &[u8], _timeout: Option) -> io::Result<()> { + self.0.write_all(data) + } + } + + /// Creates a named pipe that this process serves, and returns its name and server end. + fn create_pipe() -> (String, File) { + let mut id = [0u8; 16]; + getrandom::fill(&mut id).unwrap(); + let id: String = id.iter().map(|byte| format!("{byte:02x}")).collect(); + let name = format!(r"\\.\pipe\nvx-cli-session-test-{id}"); + let wide: Vec = name.encode_utf16().chain(Some(0)).collect(); + // SAFETY: `wide` is NUL-terminated and outlives the call. + let handle = unsafe { + CreateNamedPipeW( + wide.as_ptr(), + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 1, + 65_536, + 65_536, + 0, + std::ptr::null(), + ) + }; + assert_ne!(handle, INVALID_HANDLE_VALUE); + // SAFETY: CreateNamedPipeW returned a fresh handle that nothing else owns. + (name, unsafe { File::from_raw_handle(handle) }) + } + + /// Waits for the client of a pipe. + fn accept(server: &File) { + // SAFETY: the handle is the open server end of a pipe. + let connected = unsafe { ConnectNamedPipe(server.as_raw_handle(), std::ptr::null_mut()) }; + // SAFETY: reads the calling thread's last error. + assert!(connected != 0 || unsafe { GetLastError() } == ERROR_PIPE_CONNECTED); + } + + /// Serves `serve` on a fresh pipe and returns a session connected to it. + fn connected( + serve: impl FnOnce(&mut Guest) + Send + 'static, + ) -> (Session, thread::JoinHandle<()>) { + let (name, server) = create_pipe(); + let worker = thread::spawn(move || { + accept(&server); + let mut guest = Guest::new(Server(server)); + guest.accept(); + serve(&mut guest); + }); + let endpoint = name.replace('\\', "/"); + let session = Session::connect(&endpoint, std::process::id(), &CAPABILITY, 5.0).unwrap(); + (session, worker) + } + + #[test] + fn write_reports_a_closed_endpoint() { + // OpenVMM closes the pipe as it tears the VM down. + let (mut session, worker) = connected(|_| {}); + worker.join().unwrap(); + let failure = session.ping(5.0).unwrap_err(); + assert!(matches!(failure, Exit::EndpointClosed), "{failure:?}"); + } + + #[test] + fn read_reports_a_closed_endpoint_after_its_data() { + let (mut session, worker) = connected(|guest| { + let (_, request_id, _) = guest.request(); + guest.send(APP_READY, request_id, 0, &[]); + let _ = guest.request(); + }); + session.ping(5.0).unwrap(); + let failure = session.ping(5.0).unwrap_err(); + worker.join().unwrap(); + assert!(matches!(failure, Exit::EndpointClosed), "{failure:?}"); + } + + #[test] + fn read_times_out_and_closes_the_session() { + let (release, released) = mpsc::channel::<()>(); + let (mut session, worker) = connected(move |guest| { + let _ = guest.request(); + let _ = released.recv_timeout(Duration::from_secs(10)); + }); + let started = Instant::now(); + assert_eq!( + message(session.ping(0.1)), + "managed control response timed out" + ); + assert!(started.elapsed() < Duration::from_millis(2100)); + release.send(()).unwrap(); + worker.join().unwrap(); + // Like Python's session, a session whose transfer failed is closed. + assert_eq!( + message(session.ping(5.0)), + "managed control session is closed" + ); + } + + #[test] + fn endpoints_that_never_appear_time_out_with_their_path() { + let started = Instant::now(); + let failure = Session::connect( + "//./pipe/nvx-cli-session-test-missing", + std::process::id(), + &CAPABILITY, + 0.2, + ) + .err() + .unwrap(); + assert!(started.elapsed() >= Duration::from_millis(200)); + assert_eq!( + failure.message().unwrap(), + r"managed control endpoint did not become available: \\.\pipe\nvx-cli-session-test-missing" + ); + } +} diff --git a/nvx-rs/src/host/mod.rs b/nvx-rs/src/host/mod.rs index 129d27afd..26a07c23a 100644 --- a/nvx-rs/src/host/mod.rs +++ b/nvx-rs/src/host/mod.rs @@ -1,31 +1,45 @@ //! The host side of NVX. //! -//! - [`artifacts`] locates the OpenVMM executable and the guest kernel and initramfs. +//! - [`artifacts`] locates the OpenVMM executable and the guest kernel and initramfs, and every +//! artifact of the guests that `nvx.py` runs, in a repository checkout or a release. //! - [`launch`] builds OpenVMM's command line from a [`LaunchSpec`](launch::LaunchSpec): the //! managed lifecycle, the workload identity, the kernel command line, and the network and //! filesystem options. -//! - [`network`] lowers egress rules into OpenVMM's rules, and [`filesystem`] plans the export of -//! host paths through OpenVMM's aggregate virtio-fs device. +//! - [`network`] lowers egress rules, and the egress policy files of `nvx.py`, into OpenVMM's +//! rules, and [`filesystem`] plans the export of host paths through OpenVMM's aggregate +//! virtio-fs device. //! - [`instance`] supervises one VM instance in a directory of its own: it starts OpenVMM and //! waits until the guest is ready, reconciles recorded state with the host, runs workloads, //! and stops the VM. [`report`] reads OpenVMM's outcome report. //! - [`session`] speaks the authenticated control protocol, [`process`] launches and terminates //! OpenVMM processes, and [`platform`] holds the primitives of Linux and Windows hosts. Other //! hosts compile but report NVX as unavailable. +//! - [`sandbox`] is the launch profile of `nvx.py sandbox`: a container over read-only EROFS +//! layers and a private ext4 scratch disk, with live shares, and the checks of its options. +//! - `cli` (feature `cli`) is the `nvx-rs` command-line interface, which `nvx.py` runs for the +//! steps that it delegates: artifact lookup, egress policy compilation, and sandboxes. +//! +//! The ports of `nvx.py` code keep its Python semantics, which the private `python` module +//! provides. //! //! The host modules deny unsafe code. On Unix, the private `sys` module confines the system calls //! that the standard library lacks behind documented safe functions, which call `nix`. The -//! Windows platform module still calls `windows-sys` directly. +//! Windows platform module, and the module of the Win32 calls that Python's filesystem queries +//! make, still call `windows-sys` directly. #![deny(unsafe_code)] pub mod artifacts; +#[cfg(feature = "cli")] +pub mod cli; pub mod filesystem; pub mod instance; pub mod launch; pub mod network; pub mod platform; pub mod process; +pub(crate) mod python; pub mod report; +pub mod sandbox; pub mod session; #[cfg(unix)] #[allow(unsafe_code)] diff --git a/nvx-rs/src/host/network/address.rs b/nvx-rs/src/host/network/address.rs new file mode 100644 index 000000000..d6754bba0 --- /dev/null +++ b/nvx-rs/src/host/network/address.rs @@ -0,0 +1,729 @@ +//! IP networks as Python's `ipaddress` module parses, orders, and prints them, and the intervals +//! of addresses that egress rules select. +//! +//! The egress policy compiler ports the former `scripts/nvx_tools/egress_policy.py`, which read +//! CIDRs with `ipaddress.ip_interface`, so [`parse_interface`] accepts exactly what it accepted: +//! host bits, which it clears, IPv4 netmasks and hostmasks such as `/255.0.0.0`, prefix lengths +//! with leading zeros, embedded IPv4 in IPv6 addresses, and IPv6 scopes, which callers reject. A +//! [`Network`] prints as `str(network)` does in Python 3.13 and later, which writes an +//! IPv4-mapped IPv6 address such as `::ffff:192.0.2.0` with its IPv4 address in dotted form. + +use std::fmt; +use std::net::Ipv4Addr; + +use crate::host::python::text::PyText; + +/// Address family of a [`Network`]. +/// +/// IPv4 orders before IPv6, as Python's IP versions 4 and 6 do. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum Family { + /// IPv4: 32-bit addresses. + V4, + /// IPv6: 128-bit addresses. + V6, +} + +impl Family { + /// Both families, in the order in which the compiler lowers them. + pub(crate) const ALL: [Self; 2] = [Self::V4, Self::V6]; + + /// Returns the number of bits of an address of this family. + const fn bits(self) -> u32 { + match self { + Self::V4 => 32, + Self::V6 => 128, + } + } + + /// Returns the last address of this family, whose bits are all set. + pub(crate) const fn last_address(self) -> u128 { + match self { + Self::V4 => u32::MAX as u128, + Self::V6 => u128::MAX, + } + } + + /// Returns every address of this family as one interval. + pub(crate) const fn everything(self) -> Interval { + (0, self.last_address()) + } +} + +/// An inclusive interval of addresses of one family: its first and its last address. +pub(crate) type Interval = (u128, u128); + +/// A network of one family, without host bits. +/// +/// Networks order as the compiler sorts its output: by family, then by address, then by prefix +/// length. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Network { + /// Address family. + family: Family, + /// First address of the network, with every host bit clear. + address: u128, + /// Number of leading network bits, at most the family's address width. + prefix: u8, +} + +impl Network { + /// Returns the network of `family` with `prefix` that holds `address`, clearing its host + /// bits. + fn new(family: Family, address: u128, prefix: u32) -> Self { + let network = Self { + family, + address, + prefix: prefix as u8, + }; + Self { + address: address & !network.host_mask(), + ..network + } + } + + /// Returns the address family of the network. + pub(crate) fn family(self) -> Family { + self.family + } + + /// Returns the interval of the network's addresses: its network and broadcast addresses. + pub(crate) fn interval(self) -> Interval { + (self.address, self.address | self.host_mask()) + } + + /// Returns the host bits of an address of the network's family. + fn host_mask(self) -> u128 { + let prefix = u32::from(self.prefix); + if prefix >= self.family.bits() { + 0 + } else { + self.family.last_address() >> prefix + } + } + + /// Returns whether `other` lies within this network, as Python's `subnet_of` decides. + pub(crate) fn contains(self, other: Self) -> bool { + let (first, last) = self.interval(); + let (other_first, other_last) = other.interval(); + self.family == other.family && first <= other_first && last >= other_last + } +} + +impl fmt::Display for Network { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.family { + Family::V4 => write!(formatter, "{}", Ipv4Addr::from(self.address as u32))?, + Family::V6 => write_ipv6(formatter, self.address)?, + } + write!(formatter, "/{}", self.prefix) + } +} + +/// Writes an IPv6 address as Python's `str(IPv6Address)` does: an IPv4-mapped address as +/// `::ffff:` and its IPv4 address in dotted form, and any other address as lowercase hexadecimal +/// groups whose first longest run of two or more zero groups becomes `::`. +fn write_ipv6(formatter: &mut fmt::Formatter<'_>, address: u128) -> fmt::Result { + if address >> 32 == 0xFFFF { + return write!(formatter, "::ffff:{}", Ipv4Addr::from(address as u32)); + } + let groups: [u16; 8] = std::array::from_fn(|index| (address >> (112 - 16 * index)) as u16); + // The first of the longest runs of zero groups, if a run has at least two groups. + let mut best: Option<(usize, usize)> = None; + let mut index = 0; + while index < groups.len() { + if groups[index] != 0 { + index += 1; + continue; + } + let start = index; + while index < groups.len() && groups[index] == 0 { + index += 1; + } + let length = index - start; + if length > best.map_or(1, |(_, best_length)| best_length) { + best = Some((start, length)); + } + } + let write_groups = |formatter: &mut fmt::Formatter<'_>, groups: &[u16]| { + for (index, group) in groups.iter().enumerate() { + if index > 0 { + formatter.write_str(":")?; + } + write!(formatter, "{group:x}")?; + } + Ok(()) + }; + match best { + None => write_groups(formatter, &groups), + Some((start, length)) => { + write_groups(formatter, &groups[..start])?; + formatter.write_str("::")?; + write_groups(formatter, &groups[start + length..]) + } + } +} + +/// Why [`parse_interface`] refused a string. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum InterfaceError { + /// The string is neither an IPv4 nor an IPv6 interface. + Invalid, + /// The string is an IPv6 interface that names a scope, such as `fe80::1%eth0/64`. + Scoped, +} + +/// Parses `text` as Python's `ipaddress.ip_interface` does and returns its network, whose host +/// bits are clear. +pub(crate) fn parse_interface(text: &PyText) -> Result { + let text = text.as_bytes(); + // `ip_interface` tries IPv4 first, and no string is both. + if let Some(network) = parse_ipv4_interface(text) { + return Ok(network); + } + match parse_ipv6_interface(text) { + Some((network, false)) => Ok(network), + Some((_, true)) => Err(InterfaceError::Scoped), + None => Err(InterfaceError::Invalid), + } +} + +/// Splits `text` at every `/` as `_split_optional_netmask` does, and returns the address and the +/// netmask, if any, or `None` for more than one `/`. +fn split_netmask(text: &[u8]) -> Option<(&[u8], Option<&[u8]>)> { + let mut parts = text.split(|&byte| byte == b'/'); + let address = parts.next()?; + let netmask = parts.next(); + parts.next().is_none().then_some((address, netmask)) +} + +/// Parses an `IPv4Interface`. +fn parse_ipv4_interface(text: &[u8]) -> Option { + let (address, netmask) = split_netmask(text)?; + let address = parse_ipv4(address)?; + let prefix = match netmask { + None => 32, + Some(netmask) => { + prefix_from_prefix_string(netmask, 32).or_else(|| prefix_from_ipv4_mask(netmask))? + } + }; + Some(Network::new(Family::V4, u128::from(address), prefix)) +} + +/// Parses an `IPv6Interface` and returns its network and whether it names a scope. +fn parse_ipv6_interface(text: &[u8]) -> Option<(Network, bool)> { + let (address, netmask) = split_netmask(text)?; + // `_split_scope_id`: a scope follows the first `%`, and is not empty and has no other `%`. + let (address, scoped) = match address.iter().position(|&byte| byte == b'%') { + None => (address, false), + Some(position) => { + let scope = &address[position + 1..]; + if scope.is_empty() || scope.contains(&b'%') { + return None; + } + (&address[..position], true) + } + }; + let address = parse_ipv6(address)?; + let prefix = match netmask { + None => 128, + Some(netmask) => prefix_from_prefix_string(netmask, 128)?, + }; + Some((Network::new(Family::V6, address, prefix), scoped)) +} + +/// Parses a prefix length as `_prefix_from_prefix_string` does: ASCII digits, leading zeros +/// included, whose value is at most `maximum`. +fn prefix_from_prefix_string(text: &[u8], maximum: u32) -> Option { + // Python's `int` refuses strings of more than 4300 digits, its default + // `sys.get_int_max_str_digits()`, even when they are mostly leading zeros. + const MAX_INT_DIGITS: usize = 4300; + if text.is_empty() || text.len() > MAX_INT_DIGITS || !text.iter().all(u8::is_ascii_digit) { + return None; + } + let significant = &text[text.iter().take_while(|&&byte| byte == b'0').count()..]; + if significant.len() > 3 { + return None; + } + let value = significant + .iter() + .fold(0, |value, digit| value * 10 + u32::from(digit - b'0')); + (value <= maximum).then_some(value) +} + +/// Parses an IPv4 netmask, such as `255.255.0.0`, or a hostmask, such as `0.0.255.255`, as +/// `_prefix_from_ip_string` does, and returns its prefix length. The ambiguous all-ones and +/// all-zeros masks are netmasks. +fn prefix_from_ipv4_mask(text: &[u8]) -> Option { + let mask = parse_ipv4(text)?; + prefix_from_ipv4_netmask(mask).or_else(|| prefix_from_ipv4_netmask(!mask)) +} + +/// Returns the prefix length of an IPv4 netmask: leading ones and trailing zeros. +fn prefix_from_ipv4_netmask(mask: u32) -> Option { + let prefix = mask.leading_ones(); + (mask.checked_shl(prefix).unwrap_or(0) == 0).then_some(prefix) +} + +/// Parses an IPv4 address as `IPv4Address._ip_int_from_string` does: four decimal octets of at +/// most three ASCII digits each, without leading zeros, and at most 255. +fn parse_ipv4(text: &[u8]) -> Option { + let mut octets = text.split(|&byte| byte == b'.'); + let mut address = 0u32; + for _ in 0..4 { + let octet = octets.next()?; + let valid = !octet.is_empty() + && octet.len() <= 3 + && octet.iter().all(u8::is_ascii_digit) + && (octet == b"0" || octet[0] != b'0'); + if !valid { + return None; + } + let value = octet + .iter() + .fold(0u32, |value, digit| value * 10 + u32::from(digit - b'0')); + if value > 255 { + return None; + } + address = (address << 8) | value; + } + octets.next().is_none().then_some(address) +} + +/// One colon-separated part of an IPv6 address: its text, or a group that an embedded IPv4 +/// address contributes. +#[derive(Clone, Copy)] +enum Part<'a> { + /// A hexadecimal group as written, possibly empty. + Text(&'a [u8]), + /// A group of an embedded IPv4 address. + Group(u16), +} + +impl Part<'_> { + /// Returns whether the part is empty, as at a `::`. + fn is_empty(self) -> bool { + matches!(self, Self::Text(text) if text.is_empty()) + } + + /// Returns the value of the group, as `_parse_hextet` parses it: one to four hexadecimal + /// digits. + fn value(self) -> Option { + match self { + Self::Group(group) => Some(group), + Self::Text(text) => { + if text.is_empty() || text.len() > 4 { + return None; + } + text.iter().try_fold(0u16, |value, &digit| { + Some((value << 4) | (char::from(digit).to_digit(16)? as u16)) + }) + } + } + } +} + +/// Parses an IPv6 address without a scope as `IPv6Address._ip_int_from_string` does. +fn parse_ipv6(text: &[u8]) -> Option { + // Python counts characters, not bytes, but no address with a non-ASCII character is valid. + if text.is_empty() || text.len() > 45 || !text.is_ascii() { + return None; + } + // At most eight groups and one more part for a leading or trailing `::`; `split` keeps the + // rest of a longer address in a tenth part. + const MAX_PARTS: usize = 9; + let mut parts: Vec> = text + .splitn(MAX_PARTS + 1, |&byte| byte == b':') + .map(Part::Text) + .collect(); + if parts.len() < 3 { + return None; + } + if let Some(&Part::Text(last)) = parts.last() + && last.contains(&b'.') + { + let ipv4 = parse_ipv4(last)?; + parts.pop(); + parts.push(Part::Group((ipv4 >> 16) as u16)); + parts.push(Part::Group(ipv4 as u16)); + } + if parts.len() > MAX_PARTS { + return None; + } + let count = parts.len(); + // A `::` is an empty part between the endpoints, and at most one is allowed. + let mut skip = None; + for (index, part) in parts.iter().enumerate().take(count - 1).skip(1) { + if part.is_empty() { + if skip.is_some() { + return None; + } + skip = Some(index); + } + } + let (high, low, skipped) = match skip { + Some(skip) => { + let mut high = skip; + let mut low = count - skip - 1; + if parts[0].is_empty() { + high -= 1; + if high != 0 { + return None; + } + } + if parts[count - 1].is_empty() { + low -= 1; + if low != 0 { + return None; + } + } + let skipped = 8usize + .checked_sub(high + low) + .filter(|&skipped| skipped >= 1)?; + (high, low, skipped) + } + None => { + if count != 8 || parts[0].is_empty() || parts[count - 1].is_empty() { + return None; + } + (count, 0, 0) + } + }; + let mut address = 0u128; + for part in &parts[..high] { + address = (address << 16) | u128::from(part.value()?); + } + address = address.checked_shl(16 * skipped as u32).unwrap_or(0); + for part in &parts[count - low..] { + address = (address << 16) | u128::from(part.value()?); + } + Some(address) +} + +/// The networks that Python's `ipaddress.summarize_address_range` yields for an interval: the +/// fewest networks that cover it exactly, from its first address. +pub(crate) struct Summary { + /// Address family of the interval. + family: Family, + /// First address that the networks yielded so far do not cover, or `None` once they cover the + /// interval. + next: Option, + /// Last address of the interval. + last: u128, +} + +/// Returns the networks that cover the addresses of `family` from `first` through `last`, as +/// `ipaddress.summarize_address_range` yields them. +pub(crate) fn summarize(family: Family, (first, last): Interval) -> Summary { + #[cfg(test)] + probe::SUMMARIES.with(|count| count.set(count.get() + 1)); + Summary { + family, + next: Some(first), + last, + } +} + +impl Iterator for Summary { + type Item = Network; + + fn next(&mut self) -> Option { + let first = self.next.filter(|&first| first <= self.last)?; + let bits = self.family.bits(); + let alignment = if first == 0 { + bits + } else { + first.trailing_zeros().min(bits) + }; + // The largest power of two that fits in the rest of the interval; the whole IPv6 address + // space holds 2^128 addresses, one more than a `u128` holds. + let span = self.last - first; + let fitting = span + .checked_add(1) + .map_or(128, |count| 127 - count.leading_zeros()); + let size_bits = alignment.min(fitting); + let network = Network::new(self.family, first, bits - size_bits); + // Python stops once the next address passes the family's last address. + self.next = 1u128 + .checked_shl(size_bits) + .and_then(|size| first.checked_add(size)) + .filter(|&next| next - 1 != self.family.last_address()); + Some(network) + } +} + +/// Returns the union of `intervals`, sorted, with overlapping and adjacent intervals merged, as +/// `_merge_intervals` does. +pub(crate) fn merge(intervals: impl IntoIterator) -> Vec { + let mut sorted: Vec = intervals.into_iter().collect(); + #[cfg(test)] + probe::MERGED_INTERVALS.with(|count| count.set(count.get() + sorted.len())); + sorted.sort_unstable(); + let mut merged: Vec = Vec::with_capacity(sorted.len()); + for (start, end) in sorted { + match merged.last_mut() { + Some(previous) if start <= previous.1.saturating_add(1) => { + previous.1 = previous.1.max(end); + } + _ => merged.push((start, end)), + } + } + merged +} + +/// Returns the addresses of `sources` that `exclusions` do not hold, as `_subtract_intervals` +/// does. Both lists are sorted and their intervals disjoint. +pub(crate) fn subtract(sources: &[Interval], exclusions: &[Interval]) -> Vec { + let mut remaining = Vec::new(); + let mut first_exclusion = 0; + for &(source_start, source_end) in sources { + while first_exclusion < exclusions.len() && exclusions[first_exclusion].1 < source_start { + first_exclusion += 1; + } + let mut cursor = source_start; + // Whether the cursor passed the last address of the family, which a `u128` cannot hold. + let mut exhausted = false; + for &(excluded_start, excluded_end) in exclusions[first_exclusion..] + .iter() + .take_while(|(excluded_start, _)| *excluded_start <= source_end) + { + if cursor < excluded_start { + remaining.push((cursor, excluded_start - 1)); + } + match excluded_end.checked_add(1) { + Some(next) => cursor = cursor.max(next), + None => { + exhausted = true; + break; + } + } + if cursor > source_end { + break; + } + } + if !exhausted && cursor <= source_end { + remaining.push((cursor, source_end)); + } + } + remaining +} + +/// Counters that tests read to check how much work the compiler does, as the Python tests check +/// by mocking `ipaddress.summarize_address_range` and `_merge_intervals`. +#[cfg(test)] +pub(crate) mod probe { + use std::cell::Cell; + + thread_local! { + /// Number of [`summarize`](super::summarize) calls on this thread. + pub(crate) static SUMMARIES: Cell = const { Cell::new(0) }; + /// Number of intervals that [`merge`](super::merge) received on this thread. + pub(crate) static MERGED_INTERVALS: Cell = const { Cell::new(0) }; + } + + /// Resets the counters and returns the result of `operation` with the + /// [`summarize`](super::summarize) calls and merged intervals that it caused. + pub(crate) fn count(operation: impl FnOnce() -> T) -> (T, usize, usize) { + SUMMARIES.with(|count| count.set(0)); + MERGED_INTERVALS.with(|count| count.set(0)); + let result = operation(); + ( + result, + SUMMARIES.with(Cell::get), + MERGED_INTERVALS.with(Cell::get), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn network(text: &str) -> Result { + parse_interface(&PyText::from(text)).map(|network| network.to_string()) + } + + #[test] + fn interfaces_parse_as_python_does() { + for (text, expected) in [ + ("10.0.0.5/24", "10.0.0.0/24"), + ("10.0.0.0/255.0.0.0", "10.0.0.0/8"), + ("10.0.0.0/0.255.255.255", "10.0.0.0/8"), + ("10.0.0.0/255.255.255.255", "10.0.0.0/32"), + ("10.0.0.0/0.0.0.0", "0.0.0.0/0"), + ("10.0.0.0/08", "10.0.0.0/8"), + ("10.0.0.0/0008", "10.0.0.0/8"), + ("192.0.2.7", "192.0.2.7/32"), + ("::/0", "::/0"), + ("::/000128", "::/128"), + ("::1.2.3.4", "::102:304/128"), + ("1:2:3:4:5:6:7:8", "1:2:3:4:5:6:7:8/128"), + ("1:2:3:4:5:6:7::", "1:2:3:4:5:6:7:0/128"), + ("::2:3:4:5:6:7:8", "0:2:3:4:5:6:7:8/128"), + ("1:2:3:4:5:6:1.2.3.4", "1:2:3:4:5:6:102:304/128"), + ("1:2:3:4:5::1.2.3.4", "1:2:3:4:5:0:102:304/128"), + ("0000::", "::/128"), + ("::FFFF:1.2.3.4", "::ffff:1.2.3.4/128"), + ("::ffff:c000:201/120", "::ffff:192.0.2.0/120"), + ("::c000:200/120", "::c000:200/120"), + ("::1.2.3.4/96", "::/96"), + ("2001:DB8::/32", "2001:db8::/32"), + ("::ffff:0:0/95", "::fffe:0:0/95"), + ("1:0:0:2:0:0:0:3", "1:0:0:2::3/128"), + ("0:0:1::", "0:0:1::/128"), + ("::1:2:3:4:5:6:7", "0:1:2:3:4:5:6:7/128"), + ("fe80::1/64", "fe80::/64"), + ] { + assert_eq!(network(text).as_deref(), Ok(expected), "{text}"); + } + let long_prefix = format!("10.0.0.0/{}8", "0".repeat(4299)); + assert_eq!(network(&long_prefix).as_deref(), Ok("10.0.0.0/8")); + } + + #[test] + fn invalid_and_scoped_interfaces_are_refused() { + for text in [ + "10.0.0.0/+8", + "10.0.0.0/-8", + " 10.0.0.0/8", + "10.0.0.0/8 ", + "10.0.0.0/", + "10.0.0.0//8", + "10.0.0.0/8/8", + "010.0.0.0/8", + "1.2.3", + "1.2.3.4.5", + "256.0.0.0", + "1.2.3.4/33", + "1.2.3.4/255.0.255.0", + "1.2.3.4/255.255.255.000", + "::/129", + "fe80::1%/64", + "fe80::1%a%b", + "fe80::1%%", + "1::2::3", + ":1::2", + "1::2:", + "1:2:3:4:5:6:7:8:9", + "1:2:3:4:5:6:7:1.2.3.4", + "1:2:3:4:5:6::1.2.3.4", + "00000::", + "::ffff:1.2.3.04", + "::1.2.3", + "1.2.3.4/255.255.255.0x", + "\u{FF11}.2.3.4", + "::/12\u{FF18}", + "0:0:0:0:0:0:0:0:0", + ":::", + "1:::2", + "", + "/8", + "1.2.3.4/0x8", + "1.2.3.4/8.0", + "::/0x8", + "1.2.3.4/\u{0668}", + "fe80::1%eth0/129", + "::1:2:3:4:5:6:7:8", + "1:2:3:4:5:6:7:8:", + ":1:2:3:4:5:6:7", + ] { + assert_eq!(network(text), Err(InterfaceError::Invalid), "{text:?}"); + } + let long_prefix = format!("10.0.0.0/{}8", "0".repeat(4300)); + assert_eq!(network(&long_prefix), Err(InterfaceError::Invalid)); + for text in [ + "fe80::1%eth0/64", + "fe80::%eth0/64", + "fe80::1%1", + "::ffff:192.0.2.1%eth0/120", + "fe80::1%\u{FF11}", + "1:2:3:4:5:6:7:8%x/128", + ] { + assert_eq!(network(text), Err(InterfaceError::Scoped), "{text:?}"); + } + } + + #[test] + fn ipv6_addresses_print_as_python_does() { + for (address, expected) in [ + (0u128, "::/128"), + (1, "::1/128"), + (0xFFFF_0000_0000, "::ffff:0.0.0.0/128"), + (0xFFFF_C000_0201, "::ffff:192.0.2.1/128"), + (0xFFFE_C000_0201, "::fffe:c000:201/128"), + (0x2001_0db8 << 96, "2001:db8::/128"), + ((0x2001_0db8 << 96) | 1, "2001:db8::1/128"), + // The longest run of zero groups becomes `::`, the first of two equally long runs, + // and a single zero group stays. + ((1 << 112) | (1 << 64) | 1, "1:0:0:1::1/128"), + ((1 << 112) | (1 << 64) | (1 << 16) | 1, "1::1:0:0:1:1/128"), + ( + (1 << 112) | (2 << 96) | (3 << 80) | (4 << 64) | (5 << 48) | (6 << 32) | (7 << 16), + "1:2:3:4:5:6:7:0/128", + ), + (u128::MAX, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"), + ] { + let network = Network::new(Family::V6, address, 128); + assert_eq!(network.to_string(), expected, "{address:#x}"); + } + } + + #[test] + fn summaries_match_python() { + let summary = |family, first, last| -> Vec { + summarize(family, (first, last)) + .map(|network| network.to_string()) + .collect() + }; + let ipv4 = |text: &str| u128::from(u32::from(text.parse::().unwrap())); + assert_eq!( + summary(Family::V4, ipv4("192.0.2.0"), ipv4("192.0.2.130")), + ["192.0.2.0/25", "192.0.2.128/31", "192.0.2.130/32"] + ); + assert_eq!(summary(Family::V4, 0, u128::from(u32::MAX)), ["0.0.0.0/0"]); + assert_eq!( + summary(Family::V4, 1, u128::from(u32::MAX)).len(), + 32, + "every power of two below 2^32" + ); + assert_eq!(summary(Family::V6, 0, u128::MAX), ["::/0"]); + assert_eq!( + summary(Family::V6, u128::MAX, u128::MAX), + ["ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"] + ); + assert_eq!(summary(Family::V6, 1, u128::MAX).len(), 128); + assert_eq!(summary(Family::V6, 1 << 127, u128::MAX), ["8000::/1"]); + } + + #[test] + fn intervals_merge_and_subtract_at_the_family_bounds() { + assert_eq!( + merge([(5, 9), (0, 2), (3, 4), (20, 30), (25, 26)]), + [(0, 9), (20, 30)] + ); + assert_eq!(merge([(0, u128::MAX), (7, 9)]), [(0, u128::MAX)]); + assert_eq!(merge([(10, u128::MAX), (0, 9)]), [(0, u128::MAX)]); + assert!(merge([]).is_empty()); + assert_eq!( + subtract(&[(0, 100)], &[(10, 20), (30, 40)]), + [(0, 9), (21, 29), (41, 100)] + ); + assert_eq!(subtract(&[(0, u128::MAX)], &[(1, u128::MAX)]), [(0, 0)]); + assert!(subtract(&[(5, u128::MAX)], &[(0, u128::MAX)]).is_empty()); + assert_eq!( + subtract(&[(0, 9), (20, 29)], &[(5, 24)]), + [(0, 4), (25, 29)] + ); + assert_eq!(subtract(&[(0, 9)], &[]), [(0, 9)]); + } + + #[test] + fn containment_is_family_aware() { + let parse = |text: &str| parse_interface(&PyText::from(text)).unwrap(); + assert!(parse("10.0.0.0/8").contains(parse("10.1.0.0/16"))); + assert!(!parse("10.0.0.0/8").contains(parse("11.0.0.0/16"))); + assert!(!parse("::/96").contains(parse("0.0.0.0/0"))); + assert!(!parse("0.0.0.0/0").contains(parse("::/96"))); + assert_eq!(parse("10.0.0.0/8").interval(), (0x0A00_0000, 0x0AFF_FFFF)); + } +} diff --git a/nvx-rs/src/host/network/lowering.rs b/nvx-rs/src/host/network/lowering.rs new file mode 100644 index 000000000..78b5dffc9 --- /dev/null +++ b/nvx-rs/src/host/network/lowering.rs @@ -0,0 +1,717 @@ +//! The lowering of egress rules into OpenVMM's native rules, which the egress policy compiler of +//! [`policy`](super::policy) and [`openvmm_arguments`](super::openvmm_arguments) share. +//! +//! It ports the former `scripts/nvx_tools/egress_policy.py` exactly. Rules of one action, allow +//! or deny, become [`Rule`]s: one address family, the address intervals that the rule's +//! destinations select once their exceptions are subtracted, and a protocol and port range. +//! [`lower`] then emits the native rules of each family, within one budget of +//! [`MAX_RULES_PER_ACTION`]: +//! +//! 1. the union of the rules for every protocol, as the fewest networks that cover it; +//! 2. for ICMP, TCP, and UDP, the union of the rules for every port of the protocol, as the +//! networks that cover it together with the addresses of step 1, except those that step 1 +//! covers entirely; +//! 3. for TCP and UDP, a sweep over the ports at which rules start and end: each segment of +//! ports with the same active rules gets the networks of their union, without the addresses +//! that steps 1 and 2 cover, and a network whose addresses stay active extends its range. +//! +//! The native rules are then sorted by family, address, prefix length, protocol, and ports, and +//! written as `CIDR`, `CIDR:PROTOCOL`, `CIDR:PROTOCOL:PORT`, or `CIDR:PROTOCOL:FIRST-LAST`. + +use std::cmp::Ordering; +use std::collections::{BTreeMap, BTreeSet}; +use std::rc::Rc; + +use super::address::{self, Family, InterfaceError, Interval, Network}; +use super::policy::MAX_RULES_PER_ACTION; +use crate::error::{Error, Result}; +use crate::host::python::text::PyText; + +/// A protocol that a native rule can select. +/// +/// Protocols order by name, as the compiler sorts them. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum NativeProtocol { + /// ICMP, or ICMPv6 for IPv6 networks. + Icmp, + /// TCP. + Tcp, + /// UDP. + Udp, +} + +impl NativeProtocol { + /// Returns OpenVMM's name of the protocol. + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::Icmp => "icmp", + Self::Tcp => "tcp", + Self::Udp => "udp", + } + } +} + +/// The protocol of a rule as written: a native protocol, or `any`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RuleProtocol { + /// TCP. + Tcp, + /// UDP. + Udp, + /// ICMP. + Icmp, + /// Every protocol, or TCP and UDP with ports. + Any, +} + +impl RuleProtocol { + /// Parses one of `tcp`, `udp`, `icmp`, and `any`, which must be lowercase. + pub(crate) fn parse(text: &PyText) -> Option { + match text.as_bytes() { + b"tcp" => Some(Self::Tcp), + b"udp" => Some(Self::Udp), + b"icmp" => Some(Self::Icmp), + b"any" => Some(Self::Any), + _ => None, + } + } + + /// Returns the native protocol, or `None` for `any`. + pub(crate) fn native(self) -> Option { + match self { + Self::Tcp => Some(NativeProtocol::Tcp), + Self::Udp => Some(NativeProtocol::Udp), + Self::Icmp => Some(NativeProtocol::Icmp), + Self::Any => None, + } + } + + /// Returns the native protocols that a port selector of this protocol selects: TCP and UDP + /// for `any`. + fn port_protocols(self) -> &'static [NativeProtocol] { + match self { + Self::Tcp => &[NativeProtocol::Tcp], + Self::Udp => &[NativeProtocol::Udp], + Self::Icmp => &[NativeProtocol::Icmp], + Self::Any => &[NativeProtocol::Tcp, NativeProtocol::Udp], + } + } +} + +/// An inclusive range of ports. +pub(crate) type PortRange = (u16, u16); + +/// Traffic that a rule selects at its destinations: a protocol, or every protocol, and a range of +/// its ports, or every port. +pub(crate) type Selector = (Option, Option); + +/// A value that should be a port number. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PortValue { + /// An integer, saturated to the range of `i128`. + Integer(i128), + /// Anything else, including a Boolean, which Python's `_port` refuses although `bool` is a + /// subclass of `int`. + Other, +} + +/// A rule of one action, as the compiler lowers it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Rule { + /// Address family of the rule's addresses. + pub(crate) family: Family, + /// Addresses that the rule selects: sorted, disjoint intervals of `family`, possibly none. + /// + /// The rules of one rule's selectors share them, as Python's share one tuple, so that many + /// destinations times many selectors cost no more memory than they take to write. + pub(crate) addresses: Rc<[Interval]>, + /// Protocol that the rule selects, or `None` for every protocol. + pub(crate) protocol: Option, + /// Ports that the rule selects, or `None` for every port. Only TCP and UDP rules have ports. + pub(crate) ports: Option, +} + +/// Returns an [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) error with `message`. +pub(crate) fn invalid(message: impl Into) -> Error { + Error::unsupported(message) +} + +/// Parses the CIDR `value` of the field `description`, as `_network` does: the network of an +/// IPv4 or IPv6 interface without an IPv6 scope. +pub(crate) fn network(value: &PyText, description: &str) -> Result { + address::parse_interface(value).map_err(|error| { + let value = value.render(); + invalid(match error { + InterfaceError::Invalid => { + format!("{description} is not a valid IPv4 or IPv6 CIDR: {value}") + } + InterfaceError::Scoped => { + format!("{description} must not name an IPv6 scope: {value}") + } + }) + }) +} + +/// Returns the error for a CIDR field whose value is not a string. +pub(crate) fn not_a_cidr_string(description: &str) -> Error { + invalid(format!("{description} must be an IPv4 or IPv6 CIDR string")) +} + +/// Returns the family of the destination `description`, whose network is `parent`, and the +/// addresses that it holds outside `exceptions`, as `_subtract_exclusions` does. Each exception, +/// which is parsed when it is read, must lie within `parent`. +pub(crate) fn destination( + parent: Network, + exceptions: impl IntoIterator>, + description: &str, +) -> Result<(Family, Vec)> { + let mut excluded = Vec::new(); + for (index, exception) in exceptions.into_iter().enumerate() { + let exception = exception?; + if !parent.contains(exception) { + return Err(invalid(format!( + "{description}.except[{index}] must be contained in {parent}" + ))); + } + excluded.push(exception.interval()); + } + Ok(( + parent.family(), + address::subtract(&[parent.interval()], &address::merge(excluded)), + )) +} + +/// Returns the addresses of each family that `destinations`, each a family and its addresses, +/// select, for the families that have destinations, as an MXC rule's `to` selects them. +pub(crate) fn group_destinations( + destinations: &[(Family, Vec)], +) -> Vec<(Family, Vec)> { + Family::ALL + .into_iter() + .filter(|&family| destinations.iter().any(|(each, _)| *each == family)) + .map(|family| { + let addresses = address::merge( + destinations + .iter() + .filter(|(each, _)| *each == family) + .flat_map(|(_, addresses)| addresses.iter().copied()), + ); + (family, addresses) + }) + .collect() +} + +/// Checks the port `value` of the field `description`, as `_port` does. +fn port(value: PortValue, description: &str) -> Result { + match value { + PortValue::Other => Err(invalid(format!("{description} must be an integer"))), + PortValue::Integer(port) => u16::try_from(port) + .ok() + .filter(|&port| port != 0) + .ok_or_else(|| invalid(format!("{description} must be between 1 and 65535"))), + } +} + +/// Returns the range of the `port` and optional `endPort` fields of the selector +/// `description`, which ends at `port` without `endPort`. +pub(crate) fn port_range( + start: PortValue, + end: Option, + description: &str, +) -> Result { + let start = port(start, &format!("{description}.port"))?; + let end = match end { + Some(end) => port(end, &format!("{description}.endPort"))?, + None => start, + }; + if end < start { + return Err(invalid(format!( + "{description}.endPort {end} cannot be below port {start}" + ))); + } + Ok((start, end)) +} + +/// Returns the selectors of an MXC port selector `description` whose protocol is valid, as the +/// rest of `_parse_mxc_port` does. +pub(crate) fn port_selectors( + protocol: RuleProtocol, + port: Option, + end_port: Option, + description: &str, +) -> Result> { + let Some(port) = port else { + if end_port.is_some() { + return Err(invalid(format!("{description}.endPort requires port"))); + } + return Ok(vec![(protocol.native(), None)]); + }; + port_protocol_selectors(protocol, port, end_port, description) +} + +/// Returns the selectors of a protocol with ports, which ICMP does not have. +pub(crate) fn port_protocol_selectors( + protocol: RuleProtocol, + port: PortValue, + end_port: Option, + description: &str, +) -> Result> { + if protocol == RuleProtocol::Icmp { + return Err(invalid(format!( + "{description}.port is not supported with icmp" + ))); + } + let range = port_range(port, end_port, description)?; + Ok(protocol + .port_protocols() + .iter() + .map(|&protocol| (Some(protocol), Some(range))) + .collect()) +} + +/// Returns the rules that select each of `selectors` at the addresses of each family of +/// `destinations`, which the rules share, as `_parse_mxc_rule` and `_parse_legacy_rule` build +/// them. Without destinations, the rules select every address of both families, and without +/// selectors every protocol and port. +pub(crate) fn combine( + destinations: Option)>>, + selectors: Option>, +) -> Vec { + let destinations: Vec<(Family, Rc<[Interval]>)> = match destinations { + Some(destinations) => destinations + .into_iter() + .map(|(family, addresses)| (family, Rc::from(addresses))) + .collect(), + None => Family::ALL + .into_iter() + .map(|family| (family, Rc::from([family.everything()]))) + .collect(), + }; + let selectors = selectors.unwrap_or_else(|| vec![(None, None)]); + destinations + .iter() + .flat_map(|(family, addresses)| { + selectors.iter().map(|&(protocol, ports)| Rule { + family: *family, + addresses: Rc::clone(addresses), + protocol, + ports, + }) + }) + .collect() +} + +/// A native rule: a network, and optionally a protocol and a range of its ports. Native rules +/// order as the compiler sorts them. +type NativeRule = (Network, Option, Option); + +/// Returns the error of an action `category` whose rules need more native rules than OpenVMM +/// accepts. +fn too_many_rules(category: &str) -> Error { + invalid(format!( + "{category} emits at most {MAX_RULES_PER_ACTION} native rules" + )) +} + +/// Lowers the rules of the action `category` into native rules, in the order in which the +/// compiler sorts them, as `_compile_category` does once it has parsed them. +pub(crate) fn lower(rules: &[Rule], category: &str) -> Result> { + // A native rule matches only its own family, so each family is lowered on its own, within + // the action's shared limit. + let mut lowered: Vec = Vec::new(); + for family in Family::ALL { + let family_rules: Vec<&Rule> = rules.iter().filter(|rule| rule.family == family).collect(); + if !family_rules.is_empty() { + let budget = MAX_RULES_PER_ACTION - lowered.len(); + let family_lowered = lower_family(&family_rules, family, category, budget)?; + lowered.extend(family_lowered); + } + } + lowered.sort_unstable(); + Ok(lowered.into_iter().map(render).collect()) +} + +/// Writes a native rule in OpenVMM's form. +fn render((network, protocol, ports): NativeRule) -> String { + match (protocol, ports) { + (None, _) => network.to_string(), + (Some(protocol), None) => format!("{network}:{}", protocol.as_str()), + (Some(protocol), Some((start, end))) if start == end => { + format!("{network}:{}:{start}", protocol.as_str()) + } + (Some(protocol), Some((start, end))) => { + format!("{network}:{}:{start}-{end}", protocol.as_str()) + } + } +} + +/// Lowers the rules of one family, emitting at most `budget` native rules, as `_compile_family` +/// does. +fn lower_family( + rules: &[&Rule], + family: Family, + category: &str, + budget: usize, +) -> Result> { + let address_only = union(rules.iter().copied().filter(|rule| rule.protocol.is_none())); + let mut lowered: Vec = + intervals_to_networks(&address_only, family, budget, category)? + .into_iter() + .map(|network| (network, None, None)) + .collect(); + // Addresses that already match every port of each protocol. + let mut covered: BTreeMap> = BTreeMap::new(); + for protocol in [ + NativeProtocol::Icmp, + NativeProtocol::Tcp, + NativeProtocol::Udp, + ] { + let protocol_wide = union( + rules + .iter() + .copied() + .filter(|rule| rule.protocol == Some(protocol) && rule.ports.is_none()), + ); + if protocol_wide.is_empty() { + continue; + } + let networks = protocol_intervals_to_networks( + &protocol_wide, + &address_only, + family, + budget - lowered.len(), + category, + )?; + lowered.extend( + networks + .into_iter() + .map(|network| (network, Some(protocol), None)), + ); + covered.insert( + protocol, + address::merge(address_only.iter().chain(&protocol_wide).copied()), + ); + } + for protocol in [NativeProtocol::Tcp, NativeProtocol::Udp] { + let covered = covered.get(&protocol).unwrap_or(&address_only); + let port_rules = lower_port_rules( + rules, + protocol, + family, + category, + covered, + budget - lowered.len(), + )?; + lowered.extend(port_rules); + } + Ok(lowered) +} + +/// Returns the union of the addresses of `rules`, as `_compile_family` merges them, reading the +/// addresses that rules share once: the rules of one rule's selectors would otherwise repeat +/// them once for each selector. +fn union<'a>(rules: impl IntoIterator) -> Vec { + let sets: BTreeSet> = rules + .into_iter() + .map(|rule| AddressSet(&rule.addresses)) + .collect(); + address::merge(sets.into_iter().flat_map(|set| set.0.iter().copied())) +} + +/// Returns the networks that cover `intervals`, failing once they exceed `maximum`, as +/// `_intervals_to_networks` does. +fn intervals_to_networks( + intervals: &[Interval], + family: Family, + maximum: usize, + category: &str, +) -> Result> { + let mut networks = Vec::new(); + for &interval in intervals { + for network in address::summarize(family, interval) { + if networks.len() >= maximum { + return Err(too_many_rules(category)); + } + networks.push(network); + } + } + Ok(networks) +} + +/// Returns the networks that cover `protocol_intervals` without those that `covered` holds +/// entirely, failing once they exceed `maximum`, as `_protocol_intervals_to_networks` does. +/// +/// Rules over `covered` already match the traffic, so widening networks into it keeps the result +/// compact without changing what matches. +fn protocol_intervals_to_networks( + protocol_intervals: &[Interval], + covered: &[Interval], + family: Family, + maximum: usize, + category: &str, +) -> Result> { + let combined = address::merge(protocol_intervals.iter().chain(covered).copied()); + let mut networks = Vec::new(); + for interval in combined { + for network in address::summarize(family, interval) { + if address::subtract(&[network.interval()], covered).is_empty() { + continue; + } + if networks.len() >= maximum { + return Err(too_many_rules(category)); + } + networks.push(network); + } + } + Ok(networks) +} + +/// Lowers the rules of one family for the ports of `protocol`, TCP or UDP, emitting at most +/// `budget` native rules, as `_lower_protocol_rules` does. +/// +/// Rules start and end at port boundaries. Between two consecutive boundaries, the active rules +/// select the union of their addresses; the networks of the previous segment whose addresses stay +/// selected extend their ranges, and new networks cover the rest of the union, without the +/// addresses that `covered` already matches on every port. +fn lower_port_rules( + rules: &[&Rule], + protocol: NativeProtocol, + family: Family, + category: &str, + covered: &[Interval], + budget: usize, +) -> Result> { + // Each boundary port, and the address sets that start (+1) or end (-1) there. Ports run to + // 65536, where rules that end at port 65535 end. + let mut events: BTreeMap)>> = BTreeMap::new(); + // Whether each address set holds addresses that `covered` does not, which rules that share + // their addresses ask once. + let mut uncovered: BTreeMap, bool> = BTreeMap::new(); + for rule in rules { + let Some((start, end)) = rule.ports else { + continue; + }; + if rule.protocol != Some(protocol) || rule.addresses.is_empty() { + continue; + } + let addresses = AddressSet(&rule.addresses); + if !*uncovered + .entry(addresses) + .or_insert_with(|| !address::subtract(addresses.0, covered).is_empty()) + { + continue; + } + events + .entry(u32::from(start)) + .or_default() + .push((1, addresses)); + events + .entry(u32::from(end) + 1) + .or_default() + .push((-1, addresses)); + } + + // The number of active rules with each address set. + let mut active: BTreeMap, i64> = BTreeMap::new(); + let mut port_ranges: BTreeMap> = BTreeMap::new(); + // Networks whose last range ends just before the current segment. + let mut open_networks: Vec = Vec::new(); + let mut rule_count = 0; + let mut previous_port: Option = None; + for (&port, port_events) in &events { + if previous_port.is_some() && active.is_empty() { + open_networks.clear(); + } + if let Some(previous) = previous_port + && previous < port + && !active.is_empty() + { + let addresses = address::merge( + active + .keys() + .flat_map(|addresses| addresses.0.iter().copied()), + ); + let available = address::merge(addresses.iter().chain(covered).copied()); + let mut kept: Vec = Vec::new(); + for &network in &open_networks { + if address::subtract(&[network.interval()], &available).is_empty() + && let Some(range) = port_ranges + .get_mut(&network) + .and_then(|ranges| ranges.last_mut()) + { + range.1 = port - 1; + kept.push(network); + } + } + let kept_cover = address::merge( + covered + .iter() + .copied() + .chain(kept.iter().map(|network| network.interval())), + ); + let networks = protocol_intervals_to_networks( + &addresses, + &kept_cover, + family, + budget - rule_count, + category, + )?; + for network in networks { + if rule_count >= budget { + return Err(too_many_rules(category)); + } + port_ranges + .entry(network) + .or_default() + .push((previous, port - 1)); + rule_count += 1; + kept.push(network); + } + open_networks = kept; + } + for &(direction, addresses) in port_events { + let count = active.entry(addresses).or_insert(0); + *count += direction; + if *count == 0 { + active.remove(&addresses); + } + } + previous_port = Some(port); + } + Ok(port_ranges + .into_iter() + .flat_map(|(network, ranges)| { + ranges.into_iter().map(move |(start, end)| { + // Segments end before a boundary of at most 65536, so their ports fit. + let range = (start as u16, end as u16); + (network, Some(protocol), Some(range)) + }) + }) + .collect()) +} + +/// The addresses of a rule, which compare by value, as Python's tuples do: the port sweep counts +/// the active rules of each address set, as `_lower_protocol_rules` counts them in a `Counter`. +/// Like CPython's dictionaries, it compares identical addresses without reading them, so that the +/// rules of one rule's selectors, which share their addresses, compare at once. +#[derive(Debug, Clone, Copy)] +struct AddressSet<'a>(&'a [Interval]); + +impl PartialEq for AddressSet<'_> { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl Eq for AddressSet<'_> {} + +impl PartialOrd for AddressSet<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for AddressSet<'_> { + fn cmp(&self, other: &Self) -> Ordering { + if std::ptr::eq(self.0, other.0) { + Ordering::Equal + } else { + self.0.cmp(other.0) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_rules_of_one_rule_share_its_addresses() { + let ipv4: Vec = vec![(1, 1), (3, 3)]; + let ipv6: Vec = vec![(5, 6)]; + let selectors: Vec = vec![ + (Some(NativeProtocol::Tcp), Some((80, 80))), + (Some(NativeProtocol::Udp), Some((53, 60))), + (None, None), + ]; + let rules = combine( + Some(vec![(Family::V4, ipv4.clone()), (Family::V6, ipv6.clone())]), + Some(selectors.clone()), + ); + assert_eq!(rules.len(), 6); + for (family_rules, (family, addresses)) in rules + .chunks(selectors.len()) + .zip([(Family::V4, &ipv4), (Family::V6, &ipv6)]) + { + for (rule, &(protocol, ports)) in family_rules.iter().zip(&selectors) { + assert_eq!(rule.family, family); + assert_eq!(*rule.addresses, **addresses); + assert_eq!((rule.protocol, rule.ports), (protocol, ports)); + assert!(Rc::ptr_eq(&rule.addresses, &family_rules[0].addresses)); + } + assert_eq!( + Rc::strong_count(&family_rules[0].addresses), + selectors.len() + ); + } + + let rules = combine(None, Some(selectors.clone())); + assert_eq!(rules.len(), 6); + for (family_rules, family) in rules.chunks(selectors.len()).zip(Family::ALL) { + for rule in family_rules { + assert_eq!(rule.family, family); + assert_eq!(*rule.addresses, [family.everything()]); + assert!(Rc::ptr_eq(&rule.addresses, &family_rules[0].addresses)); + } + } + + let rules = combine(Some(vec![(Family::V6, ipv6.clone())]), None); + assert_eq!( + rules, + [Rule { + family: Family::V6, + addresses: Rc::from(ipv6), + protocol: None, + ports: None, + }] + ); + } + + #[test] + fn address_sets_compare_by_value() { + let addresses: Rc<[Interval]> = Rc::from([(1, 2), (4, 4)]); + let copy: Rc<[Interval]> = Rc::from(&*addresses); + let longer: Rc<[Interval]> = Rc::from([(1, 2), (4, 4), (6, 6)]); + let later: Rc<[Interval]> = Rc::from([(1, 3)]); + let empty: Rc<[Interval]> = Rc::from([]); + assert_eq!(AddressSet(&addresses), AddressSet(&addresses)); + assert_eq!(AddressSet(&addresses), AddressSet(©)); + assert!(AddressSet(&empty) < AddressSet(&addresses)); + assert!(AddressSet(&addresses) < AddressSet(&longer)); + assert!(AddressSet(&longer) < AddressSet(&later)); + let sets: BTreeSet> = [&later, &addresses, ©, &empty, &longer] + .into_iter() + .map(|addresses| AddressSet(addresses)) + .collect(); + let sorted: Vec<&[Interval]> = sets.iter().map(|set| set.0).collect(); + assert_eq!(sorted, [&*empty, &*addresses, &*longer, &*later]); + } + + #[test] + fn shared_addresses_are_merged_once() { + let addresses: Vec = (0..64).map(|index| (2 * index, 2 * index)).collect(); + // The union of the 64 rules for every protocol reads their addresses once; for ICMP, so + // do the cover of its networks and the addresses that it covers. Python merges the + // addresses of each rule into the union: 64 times as many. + for (protocol, merged) in [(None, 64), (Some(NativeProtocol::Icmp), 3 * 64)] { + let rules = combine( + Some(vec![(Family::V4, addresses.clone())]), + Some(vec![(protocol, None); 64]), + ); + assert_eq!(rules.len(), 64); + let (lowered, _, merged_intervals) = address::probe::count(|| lower(&rules, "allow")); + assert_eq!(lowered.unwrap().len(), 64, "{protocol:?}"); + assert_eq!(merged_intervals, merged, "{protocol:?}"); + } + } +} diff --git a/nvx-rs/src/host/network.rs b/nvx-rs/src/host/network/mod.rs similarity index 73% rename from nvx-rs/src/host/network.rs rename to nvx-rs/src/host/network/mod.rs index 22ce03cba..72dfc5115 100644 --- a/nvx-rs/src/host/network.rs +++ b/nvx-rs/src/host/network/mod.rs @@ -1,21 +1,33 @@ -//! Translation of a network posture into OpenVMM options. +//! Translation of a network posture into OpenVMM options, and the egress policy files of +//! `nvx.py`. //! //! OpenVMM's portable profile enforces an egress default plus IPv4 and IPv6 allow and deny rules //! that match a network of one family and, optionally, one protocol (TCP, UDP, or ICMP) and, for -//! TCP or UDP, one port or an inclusive range of ports; deny rules take precedence. [`EgressRule`]s -//! are expanded into that form exactly: exceptions are subtracted from their networks, a rule -//! without destinations matches `0.0.0.0/0` and `::/0`, and `any` protocol with a port or a port -//! range becomes a TCP and a UDP rule. Rules that cannot be expressed exactly are rejected. +//! TCP or UDP, one port or an inclusive range of ports; deny rules take precedence. +//! [`EgressRule`]s are lowered into that form exactly, by the lowering that also compiles egress +//! policy files ([`policy`]): exceptions are subtracted from their networks, a rule without +//! destinations matches `0.0.0.0/0` and `::/0`, `any` protocol with a port or a port range becomes +//! a TCP and a UDP rule, destinations merge into the fewest networks that cover them, rules for +//! every port of a protocol subsume narrower rules, and overlapping and adjacent port ranges +//! merge. Each action emits at most [`MAX_RULES`] OpenVMM rules, sorted by family, address, prefix +//! length, protocol, and ports. //! //! Host-loopback access is bidirectional: with it allowed, the guest reaches host loopback //! services through its gateway, and OpenVMM publishes the guest's listeners on host loopback. //! [`LoopbackForward`]s publish further host loopback ports at guest ports of their choice. +mod address; +mod lowering; +pub mod policy; + use std::collections::HashSet; use std::net::Ipv4Addr; +use self::address::{Family, Interval}; +use self::lowering::{PortValue, Rule, RuleProtocol}; use crate::error::{Error, Result}; -use crate::net::cidr::{self, Cidr}; +use crate::host::python::text::PyText; +use crate::net::cidr; /// Largest number of allow rules, and of deny rules, that OpenVMM accepts. pub const MAX_RULES: usize = 256; @@ -62,11 +74,14 @@ pub enum Protocol { } /// A destination network of an [`EgressRule`], without its exceptions. +/// +/// Networks are read as egress policy files read their CIDRs ([`policy`]): host bits are cleared, +/// IPv4 netmasks such as `/255.0.0.0` are accepted, and IPv6 scopes are refused. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Destination { /// Destination address or CIDR, such as `"10.0.0.0/8"` or `"192.0.2.1"`. pub cidr: String, - /// Sub-networks of `cidr` that the rule does not match. + /// Sub-networks of `cidr` that the rule does not match, each within `cidr`. pub except: Vec, } @@ -149,7 +164,9 @@ impl EgressRule { } /// The egress rules of one action, and the name by which messages refer to them, such as -/// `egress.allow`. Messages name a rule by its index, as in `egress.allow[0]`. +/// `egress.allow`. Messages name a rule by its index and its fields by their MXC names, as in +/// `egress.allow[0].to[1].cidr`, and the action by the name, as in `egress.allow emits at most 256 +/// native rules`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct EgressRules { /// The name of the rules in messages. @@ -269,10 +286,12 @@ impl NetworkPolicy { /// Without a policy, or when egress is denied without allow rules and ingress and host-loopback /// access are denied, no network device is attached. Otherwise the portable profile applies the /// egress posture, the ingress default, the host-loopback access, and the host-loopback forwards. +/// Each action's rules are lowered as an egress policy file's are ([`policy`]), and fail with its +/// messages, which name the rules after [`EgressRules::name`]. /// -/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) when a rule cannot be -/// expressed exactly, when the rules of one action expand to more than [`MAX_RULES`] OpenVMM -/// rules, when host-loopback access is allowed for a guest network whose prefix is longer than +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) when a rule is invalid, +/// when the rules of one action need more than [`MAX_RULES`] OpenVMM rules, when host-loopback +/// access is allowed for a guest network whose prefix is longer than /// [`MAX_HOST_LOOPBACK_PREFIX`], or when the host-loopback forwards break the rules of /// [`NetworkPolicy::host_loopback_forwards`]. pub fn openvmm_arguments( @@ -321,30 +340,21 @@ pub fn openvmm_arguments( ] .map(str::to_owned) .to_vec(); + // As in an egress policy file, the allow rules are checked and lowered before the deny rules. + let mut deny = Vec::new(); for (rules, option) in [ (&policy.allow, "--network-egress-allow"), (&policy.deny, "--network-egress-deny"), ] { - let field = &rules.name; - let mut expanded = Vec::new(); - for (index, rule) in rules.rules.iter().enumerate() { - expand_rule(rule, &format!("{field}[{index}]"), &mut expanded)?; - } - expanded.sort_unstable(); - expanded.dedup(); - if expanded.len() > MAX_RULES { - return Err(Error::unsupported(format!( - "{field} expands to {} OpenVMM rules; the openvmm backend accepts at most \ - {MAX_RULES}", - expanded.len() - ))); - } - for rule in expanded { + let parsed = parse_rules(rules)?; + let lowered = lowering::lower(&parsed.concat(), &rules.name)?; + for rule in lowered { arguments.push(option.to_owned()); arguments.push(rule); } + deny = parsed; } - check_loopback_forwards(policy, guest_network)?; + check_loopback_forwards(policy, &deny, guest_network)?; for forward in &policy.host_loopback_forwards { arguments.push("--host-loopback-forward".to_owned()); arguments.push(format!( @@ -357,10 +367,67 @@ pub fn openvmm_arguments( Ok(arguments) } -/// Checks the host-loopback forwards of a policy whose rules expand, and whose host-loopback +/// Parses the rules of one action, each into the rules that the lowering takes, as an egress +/// policy file's MXC rules are parsed: an empty `to` or `ports` is a rule without that field. +fn parse_rules(rules: &EgressRules) -> Result>> { + rules + .rules + .iter() + .enumerate() + .map(|(index, rule)| parse_rule(rule, &format!("{}[{index}]", rules.name))) + .collect() +} + +/// Parses one rule, which messages call `description`. +fn parse_rule(rule: &EgressRule, description: &str) -> Result> { + let mut destinations = None; + if !rule.to.is_empty() { + let mut parsed = Vec::with_capacity(rule.to.len()); + for (index, peer) in rule.to.iter().enumerate() { + let description = format!("{description}.to[{index}]"); + let network = |value: &str, field: &str| lowering::network(&PyText::from(value), field); + let parent = network(&peer.cidr, &format!("{description}.cidr"))?; + parsed.push(lowering::destination( + parent, + peer.except.iter().enumerate().map(|(exception, value)| { + network(value, &format!("{description}.except[{exception}]")) + }), + &description, + )?); + } + destinations = Some(lowering::group_destinations(&parsed)); + } + let mut selectors = None; + if !rule.ports.is_empty() { + let mut parsed = Vec::new(); + for (index, port) in rule.ports.iter().enumerate() { + let protocol = match port.protocol { + Protocol::Tcp => RuleProtocol::Tcp, + Protocol::Udp => RuleProtocol::Udp, + Protocol::Icmp => RuleProtocol::Icmp, + Protocol::Any => RuleProtocol::Any, + }; + let value = |port: u16| PortValue::Integer(i128::from(port)); + parsed.extend(lowering::port_selectors( + protocol, + port.port.map(value), + port.end_port.map(value), + &format!("{description}.ports[{index}]"), + )?); + } + selectors = Some(parsed); + } + Ok(lowering::combine(destinations, selectors)) +} + +/// Checks the host-loopback forwards of a policy whose rules lower, and whose host-loopback /// access is allowed when it has forwards, against the rules of -/// [`NetworkPolicy::host_loopback_forwards`]. -fn check_loopback_forwards(policy: &NetworkPolicy, guest_network: &str) -> Result<()> { +/// [`NetworkPolicy::host_loopback_forwards`]. `deny` holds the parsed deny rules. +fn check_loopback_forwards( + policy: &NetworkPolicy, + deny: &[Vec], + guest_network: &str, +) -> Result<()> { let forwards = &policy.host_loopback_forwards; if forwards.is_empty() { return Ok(()); @@ -401,11 +468,19 @@ fn check_loopback_forwards(policy: &NetworkPolicy, guest_network: &str) -> Resul "host-loopback forwards need egress allowed by default for their replies".to_owned(), ); } - let Some(network) = guest_subnet(guest_network) else { + let Some((first, last)) = guest_subnet(guest_network) else { return unsupported(format!("guest network {guest_network:?} is not valid")); }; - for (index, rule) in policy.deny.rules.iter().enumerate() { - if rule_covers(rule, network) { + // A deny rule covers the guest network if it selects one of its addresses, on any protocol. + for (index, rules) in deny.iter().enumerate() { + let covers = rules.iter().any(|rule| { + rule.family == Family::V4 + && rule + .addresses + .iter() + .any(|&(start, end)| start <= last && first <= end) + }); + if covers { return unsupported(format!( "{}[{index}] cannot cover the guest network, which carries host-loopback forward \ replies", @@ -416,132 +491,15 @@ fn check_loopback_forwards(policy: &NetworkPolicy, guest_network: &str) -> Resul Ok(()) } -/// Returns the IPv4 network of `guest_network`, a static guest address in `ADDRESS/PREFIX` form. -fn guest_subnet(guest_network: &str) -> Option { +/// Returns the addresses of the IPv4 network of `guest_network`, a static guest address in +/// `ADDRESS/PREFIX` form. +fn guest_subnet(guest_network: &str) -> Option { let (address, prefix) = guest_network.split_once('/')?; let address: Ipv4Addr = address.parse().ok()?; let prefix = cidr::parse_prefix_length(prefix).filter(|prefix| (1..=32).contains(prefix))?; let mask = u32::MAX << (32 - u32::from(prefix)); - Cidr::parse(&format!( - "{}/{prefix}", - Ipv4Addr::from(u32::from(address) & mask) - )) - .ok() -} - -/// Returns whether `rule`, whose destinations parse, matches an address of `network`. -fn rule_covers(rule: &EgressRule, network: Cidr) -> bool { - rule.to.is_empty() - || rule.to.iter().any(|peer| { - let Ok(destination) = Cidr::parse(&peer.cidr) else { - return false; - }; - let excluded = peer - .except - .iter() - .filter_map(|value| Cidr::parse(value).ok()) - .collect::>(); - destination - .subtract(&excluded, MAX_RULES) - .is_none_or(|remainder| remainder.iter().any(|part| part.overlaps(network))) - }) -} - -/// Appends the OpenVMM rules equivalent to `rule`. -fn expand_rule(rule: &EgressRule, field: &str, output: &mut Vec) -> Result<()> { - let unsupported = |message: String| Err(Error::unsupported(format!("{field}{message}"))); - let mut networks = Vec::new(); - if rule.to.is_empty() { - networks.extend([Cidr::everything(false), Cidr::everything(true)]); - } - for (index, peer) in rule.to.iter().enumerate() { - let Ok(network) = Cidr::parse(&peer.cidr) else { - return unsupported(format!(".to[{index}]: the destination is not a network")); - }; - let mut excluded = Vec::new(); - for value in &peer.except { - match Cidr::parse(value) { - Ok(exclusion) if network.contains(exclusion) => excluded.push(exclusion), - _ => { - return unsupported(format!( - ".to[{index}].except: an exception is not a network within the destination" - )); - } - } - } - let Some(remainder) = network.subtract(&excluded, MAX_RULES - networks.len()) else { - return unsupported(format!( - " covers more than {MAX_RULES} networks after removing its exceptions" - )); - }; - networks.extend(remainder); - } - - let mut selectors = Vec::new(); - if rule.ports.is_empty() { - selectors.push(Selector::Everything); - } - for (index, port) in rule.ports.iter().enumerate() { - let protocols: &[&'static str] = match port.protocol { - Protocol::Tcp => &["tcp"], - Protocol::Udp => &["udp"], - Protocol::Icmp => &["icmp"], - Protocol::Any => &["tcp", "udp"], - }; - let Some(start) = port.port else { - if port.end_port.is_some() { - return unsupported(format!(".ports[{index}].endPort requires port")); - } - if port.protocol == Protocol::Any { - selectors.push(Selector::Everything); - } else { - selectors.extend(protocols.iter().copied().map(Selector::Protocol)); - } - continue; - }; - if port.protocol == Protocol::Icmp { - return unsupported(format!(".ports[{index}]: ICMP has no ports")); - } - let end = port.end_port.unwrap_or(start); - if end < start { - return unsupported(format!(".ports[{index}].endPort must not be below port")); - } - selectors.extend( - protocols - .iter() - .map(|&protocol| Selector::Ports(protocol, start, end)), - ); - } - - for network in &networks { - for selector in &selectors { - output.push(match *selector { - Selector::Everything => network.to_string(), - Selector::Protocol(protocol) => format!("{network}:{protocol}"), - Selector::Ports(protocol, start, end) if start == end => { - format!("{network}:{protocol}:{start}") - } - Selector::Ports(protocol, start, end) => { - format!("{network}:{protocol}:{start}-{end}") - } - }); - if output.len() > MAX_RULES * 4 { - return unsupported(format!(" expands to more than {MAX_RULES} OpenVMM rules")); - } - } - } - Ok(()) -} - -/// The traffic that one OpenVMM rule selects at its destination network. -#[derive(Clone, Copy)] -enum Selector { - /// Every protocol and port. - Everything, - /// Every port of TCP or UDP, or every ICMP message. - Protocol(&'static str), - /// The TCP or UDP ports from the first through the last, both included. - Ports(&'static str, u16, u16), + let network = u32::from(address) & mask; + Some((u128::from(network), u128::from(network | !mask))) } #[cfg(test)] @@ -721,6 +679,9 @@ mod tests { EgressRule::to("10.0.0.0/8"), EgressRule::to("10.0.0.0/24"), EgressRule::to("10.0.0.77").on_port(Protocol::Tcp, 443), + // Networks are read as egress policy files read them, host bits and netmasks included. + EgressRule::to("10.0.0.77/24"), + EgressRule::to("10.0.0.0/255.255.255.0"), ] { rejected(&valid.clone().with_deny(rule), "forward replies"); } @@ -785,7 +746,7 @@ mod tests { } #[test] - fn ipv6_rules_expand_exactly() { + fn ipv6_rules_lower_exactly() { let egress = NetworkPolicy::new(Access::Deny) .with_allow(EgressRule::to("2001:db8:1::/64").on_port(Protocol::Tcp, 443)) .with_allow(EgressRule { @@ -803,16 +764,17 @@ mod tests { .with_allow(EgressRule::to("::/0").on_protocol(Protocol::Icmp)) .with_deny(EgressRule::to("2001:db8:1::123")); let arguments = openvmm_arguments(Some(&egress), "10.0.0.2/24").unwrap(); + // Rules sort by family and then by address, so `::/0` leads the IPv6 rules. assert_eq!( rules(&arguments, "--network-egress-allow"), [ "192.0.2.0/24:icmp", + "::/0:icmp", "2001:db8:1::/64:tcp:443", "2001:db8:2::/128:tcp:53", "2001:db8:2::/128:udp:53", "2001:db8:2::2/127:tcp:53", "2001:db8:2::2/127:udp:53", - "::/0:icmp", ] ); assert_eq!( @@ -821,6 +783,167 @@ mod tests { ); } + #[test] + fn rules_lower_like_egress_policy_files() { + let lowered = |rules: Vec| { + let mut egress = NetworkPolicy::new(Access::Deny); + egress.allow.rules = rules; + let arguments = openvmm_arguments(Some(&egress), "10.0.0.2/24").unwrap(); + self::rules(&arguments, "--network-egress-allow") + }; + let tcp = |cidr: &str, port, end_port| { + EgressRule::to(cidr).on_port_range(Protocol::Tcp, port, end_port) + }; + // Destinations merge into the fewest networks, and port ranges into one range. + assert_eq!( + lowered(vec![EgressRule { + to: (0..300) + .map(|index| Destination::new(format!("10.0.{}.0/24", index % 256))) + .collect(), + ports: vec![PortSelector { + protocol: Protocol::Tcp, + port: Some(1), + end_port: Some(2), + }], + }]), + ["10.0.0.0/16:tcp:1-2"] + ); + assert_eq!( + lowered(vec![ + tcp("192.0.2.7", 8000, 8004), + tcp("192.0.2.7", 8003, 8010), + tcp("192.0.2.6", 8000, 8010), + ]), + ["192.0.2.6/31:tcp:8000-8010"] + ); + // Rules for every port of a protocol, or every protocol, subsume narrower ones. + assert_eq!( + lowered(vec![ + EgressRule::to("192.0.2.7").on_port(Protocol::Tcp, 443), + EgressRule::to("192.0.2.0/24").on_protocol(Protocol::Tcp), + EgressRule::to("198.51.100.0/24").on_port(Protocol::Udp, 53), + EgressRule::to("198.51.100.0/23"), + ]), + ["192.0.2.0/24:tcp", "198.51.100.0/23"] + ); + // Host bits are cleared, as an egress policy file's are. + assert_eq!( + lowered(vec![EgressRule::to("10.0.0.5/255.255.255.0")]), + ["10.0.0.0/24"] + ); + } + + #[test] + fn invalid_rules_are_rejected_with_the_policy_compilers_messages() { + let port = |protocol, port, end_port| EgressRule { + to: vec![Destination::new("10.0.0.0/8")], + ports: vec![PortSelector { + protocol, + port, + end_port, + }], + }; + let excluding = |cidr: &str, except: &str| EgressRule { + to: vec![ + Destination::new("192.0.2.0/24"), + Destination { + cidr: cidr.to_owned(), + except: vec![except.to_owned()], + }, + ], + ports: Vec::new(), + }; + for (rule, message) in [ + ( + port(Protocol::Icmp, Some(8), None), + "egress.allow[0].ports[0].port is not supported with icmp", + ), + // Structural validation rejects these first, unless the backend is called directly. + ( + port(Protocol::Tcp, Some(90), Some(80)), + "egress.allow[0].ports[0].endPort 80 cannot be below port 90", + ), + ( + port(Protocol::Udp, None, Some(80)), + "egress.allow[0].ports[0].endPort requires port", + ), + ( + port(Protocol::Tcp, Some(0), None), + "egress.allow[0].ports[0].port must be between 1 and 65535", + ), + ( + port(Protocol::Any, Some(80), Some(0)), + "egress.allow[0].ports[0].endPort must be between 1 and 65535", + ), + ( + EgressRule::to("example.com"), + "egress.allow[0].to[0].cidr is not a valid IPv4 or IPv6 CIDR: example.com", + ), + ( + EgressRule::to("fe80::1%eth0/64"), + "egress.allow[0].to[0].cidr must not name an IPv6 scope: fe80::1%eth0/64", + ), + ( + excluding("10.0.0.0/8", "11.0.0.0/16"), + "egress.allow[0].to[1].except[0] must be contained in 10.0.0.0/8", + ), + ( + excluding("10.0.0.7/8", "10.1.0.0/+16"), + "egress.allow[0].to[1].except[0] is not a valid IPv4 or IPv6 CIDR: 10.1.0.0/+16", + ), + // 129 separate ranges of both TCP and UDP need 258 OpenVMM rules. + ( + EgressRule { + to: vec![Destination::new("10.0.0.0/8")], + ports: (0..129) + .map(|index| PortSelector { + protocol: Protocol::Any, + port: Some(3 * index + 1), + end_port: Some(3 * index + 2), + }) + .collect(), + }, + "egress.allow emits at most 256 native rules", + ), + ] { + let egress = NetworkPolicy::new(Access::Deny).with_allow(rule.clone()); + let error = openvmm_arguments(Some(&egress), "10.0.0.2/24").unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{rule:?}"); + assert_eq!(error.message(), message, "{rule:?}"); + } + // The allow rules are checked and lowered before the deny rules. + let egress = NetworkPolicy::new(Access::Allow) + .with_allow(EgressRule::to("10.0.0.0/33")) + .with_deny(port(Protocol::Icmp, Some(8), None)); + let error = openvmm_arguments(Some(&egress), "10.0.0.2/24").unwrap_err(); + assert_eq!( + error.message(), + "egress.allow[0].to[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.0/33" + ); + } + + #[test] + fn exceptions_that_split_a_network_too_finely_are_rejected() { + // Without a limit, these exceptions would split the address space into 512 * 23 networks. + let peer = Destination { + cidr: "0.0.0.0/0".to_owned(), + except: (0..512u32) + .map(|index| std::net::Ipv4Addr::from(index << 23).to_string()) + .collect(), + }; + let rule = EgressRule { + to: vec![peer], + ports: Vec::new(), + }; + let egress = NetworkPolicy::new(Access::Deny).with_allow(rule); + let error = openvmm_arguments(Some(&egress), "10.0.0.2/24").unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert_eq!( + error.message(), + "egress.allow emits at most 256 native rules" + ); + } + #[test] fn exclusions_remain_local_to_their_rules() { let excluding = |cidr: &str, excluded: &str| EgressRule { @@ -908,74 +1031,4 @@ mod tests { ["192.0.2.7/32:tcp:8005"] ); } - - #[test] - fn inexpressible_rules_are_rejected() { - let port = |protocol, port, end_port| EgressRule { - to: vec![Destination::new("10.0.0.0/8")], - ports: vec![PortSelector { - protocol, - port, - end_port, - }], - }; - for rule in [ - port(Protocol::Icmp, Some(8), None), - // Structural validation rejects these first, unless the backend is called directly. - port(Protocol::Tcp, Some(90), Some(80)), - port(Protocol::Udp, None, Some(80)), - EgressRule { - to: (0..300) - .map(|index| Destination::new(format!("10.0.{}.0/24", index % 256))) - .collect(), - ports: vec![PortSelector { - protocol: Protocol::Tcp, - port: Some(1), - end_port: Some(2), - }], - }, - // 129 separate ranges of both TCP and UDP need 258 OpenVMM rules. - EgressRule { - to: vec![Destination::new("10.0.0.0/8")], - ports: (0..129) - .map(|index| PortSelector { - protocol: Protocol::Any, - port: Some(3 * index + 1), - end_port: Some(3 * index + 2), - }) - .collect(), - }, - ] { - let egress = NetworkPolicy::new(Access::Deny).with_allow(rule.clone()); - assert_eq!( - openvmm_arguments(Some(&egress), "10.0.0.2/24") - .unwrap_err() - .kind(), - ErrorKind::Unsupported, - "{rule:?}" - ); - } - } - - #[test] - fn exceptions_that_split_a_network_too_finely_are_rejected() { - // Without a limit, these exceptions would split the address space into 512 * 23 networks. - let peer = Destination { - cidr: "0.0.0.0/0".to_owned(), - except: (0..512u32) - .map(|index| std::net::Ipv4Addr::from(index << 23).to_string()) - .collect(), - }; - let rule = EgressRule { - to: vec![peer], - ports: Vec::new(), - }; - let egress = NetworkPolicy::new(Access::Deny).with_allow(rule); - let error = openvmm_arguments(Some(&egress), "10.0.0.2/24").unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported); - assert!( - error.message().contains("covers more than 256 networks"), - "{error}" - ); - } } diff --git a/nvx-rs/src/host/network/policy/mod.rs b/nvx-rs/src/host/network/policy/mod.rs new file mode 100644 index 000000000..1f510c361 --- /dev/null +++ b/nvx-rs/src/host/network/policy/mod.rs @@ -0,0 +1,491 @@ +//! Egress policy files, which `nvx.py run` and `nvx.py sandbox` accept with +//! `--network-egress-policy-file`, and their compilation into OpenVMM's native rules. +//! +//! This is a faithful port of the former `scripts/nvx_tools/egress_policy.py`, which it replaced +//! (microsoft/nvx#359): it accepts the same documents, compiles them to the same rules, and fails +//! with the same messages. +//! +//! # Format +//! +//! A policy is a JSON object of at most [`MAX_POLICY_FILE_SIZE`] bytes of UTF-8, without a +//! byte-order mark, whose only fields are `allow` and `deny`, each an array of rules. A field may +//! appear only once in an object, and an integer may have at most 64 digits. Each rule has one of +//! two forms: +//! +//! - The MXC form, `{"to": [PEER, ...], "ports": [PORT, ...]}`, whose fields are both optional: +//! - each `PEER` is `{"cidr": CIDR, "except": [CIDR, ...]}`, a destination network without +//! the networks of its optional `except`, which must lie within it; a rule without `to` +//! selects every address of both families; +//! - each `PORT` is `{"protocol": PROTOCOL, "port": FIRST, "endPort": LAST}`, all optional, +//! where `PROTOCOL` is `tcp`, `udp`, `icmp`, or `any`, the default; `port` selects one TCP or +//! UDP port, and with `endPort` the ports from `port` through `endPort`, from 1 to 65535; a +//! rule without `ports` selects every protocol and port. +//! - The legacy flat form, `{"cidr": CIDR, "except": [CIDR, ...], "protocol": PROTOCOL, "port": +//! FIRST, "endPort": LAST}`, which needs `cidr`, and `protocol` with `port`. +//! +//! `any` selects every protocol, or with a port both TCP and UDP. A `CIDR` is an IPv4 or IPv6 +//! network as Python's `ipaddress.ip_interface` reads it: host bits are cleared, IPv4 netmasks +//! such as `/255.255.0.0` are accepted, and IPv6 scopes are refused. +//! +//! # Compilation +//! +//! Each action compiles to at most [`MAX_RULES_PER_ACTION`] native rules: `CIDR`, `CIDR:PROTOCOL`, +//! `CIDR:PROTOCOL:PORT`, or `CIDR:PROTOCOL:FIRST-LAST`, sorted by family, address, prefix length, +//! protocol, and ports. Destinations are merged into the fewest networks that cover them, rules +//! for every port of a protocol subsume the narrower rules that they cover, and overlapping and +//! adjacent port ranges merge. OpenVMM applies deny rules before allow rules, so each action is +//! compiled on its own. +//! +//! # Fidelity +//! +//! Messages are Python's, byte for byte, with two representation choices: a string that Python +//! would hold with an unpaired surrogate, which JSON escapes such as `\ud800` and paths that are +//! not Unicode produce, shows each surrogate as `\udXXX`, which is how Python prints it, and +//! paths show as `str(pathlib.Path(path))`. Python's decoder raises `RecursionError` for documents +//! nested more deeply than its stack allows; this reader has no such limit, so those documents +//! fail validation like any other invalid policy. + +#[cfg(test)] +mod tests; + +use std::fs::File; +use std::io::Read; +use std::path::Path; + +use serde::Serialize; + +use super::address::Family; +use super::lowering::{self, PortValue, Rule, RuleProtocol, Selector, invalid}; +use crate::error::{Error, Result}; +use crate::host::python::json::{self, Document, Hooks, Node, NodeId, ReadError}; +use crate::host::python::path::python_path; +use crate::host::python::text::PyText; + +/// Largest number of native rules that one action, allow or deny, compiles to: the number of +/// rules of each action that OpenVMM accepts, [`MAX_RULES`](super::MAX_RULES). +pub const MAX_RULES_PER_ACTION: usize = super::MAX_RULES; + +/// Largest egress policy file, in bytes: 1 MiB. +pub const MAX_POLICY_FILE_SIZE: usize = 1024 * 1024; + +/// Longest integer, in digits without its sign, that a policy file may hold. +const MAX_JSON_INTEGER_DIGITS: usize = 64; + +/// The native OpenVMM rules of an egress policy. +/// +/// Each rule is an `--network-egress-allow` or `--network-egress-deny` value: `CIDR`, +/// `CIDR:PROTOCOL`, `CIDR:PROTOCOL:PORT`, or `CIDR:PROTOCOL:FIRST-LAST`. Its serde form is +/// `{"allow": [...], "deny": [...]}`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +pub struct CompiledEgressPolicy { + /// Rules of traffic to permit, at most [`MAX_RULES_PER_ACTION`]. + pub allow: Vec, + /// Rules of traffic to block, which OpenVMM applies first, at most [`MAX_RULES_PER_ACTION`]. + pub deny: Vec, +} + +/// Compiles the egress policy file at `path`, as `compile_policy_file` of the former +/// `egress_policy.py` did for `nvx.py`. +/// +/// Messages name the file as `str(pathlib.Path(path))`, which drops empty and `.` components and, +/// on Windows, writes `\` for `/`, and the file opened is that normalized path, as in Python. +/// +/// # Errors +/// +/// [`ErrorKind::Io`](crate::ErrorKind::Io) when the file cannot be read; otherwise +/// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) when the file exceeds +/// [`MAX_POLICY_FILE_SIZE`], is not UTF-8 JSON that Python reads, or holds an invalid policy; +/// see [`compile_policy_bytes`]. +pub fn compile_policy_file(path: &Path) -> Result { + let path = python_path(path); + let unreadable = |error: std::io::Error| { + Error::io(format!( + "failed to read egress policy file: {}", + path.display + )) + .with_source(error) + }; + // Reading one byte more than the limit detects oversized files whatever their metadata say. + let mut data = Vec::new(); + File::open(&path.native) + .and_then(|file| { + file.take(MAX_POLICY_FILE_SIZE as u64 + 1) + .read_to_end(&mut data) + }) + .map_err(unreadable)?; + compile_contents(&data, &path.display) +} + +/// Compiles `data`, the contents of the egress policy file at `path`, which messages name as +/// [`compile_policy_file`] does, after it has read them. +/// +/// # Errors +/// +/// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) with Python's message: +/// +/// - `egress policy file exceeds 1048576-byte limit: PATH`; +/// - `failed to read egress policy file: PATH` for data that is not UTF-8, or not JSON that +/// Python's `json.loads` reads; +/// - `duplicate JSON property: NAME` and `JSON integer exceeds 64-digit limit`; or +/// - an error of [`compile_policy`]. +pub fn compile_policy_bytes(data: &[u8], path: &Path) -> Result { + compile_contents(data, &python_path(path).display) +} + +/// Compiles the contents of the policy file that messages name `path`. +fn compile_contents(data: &[u8], path: &str) -> Result { + if data.len() > MAX_POLICY_FILE_SIZE { + return Err(invalid(format!( + "egress policy file exceeds {MAX_POLICY_FILE_SIZE}-byte limit: {path}" + ))); + } + let unreadable = || invalid(format!("failed to read egress policy file: {path}")); + let text = std::str::from_utf8(data).map_err(|_| unreadable())?; + let document = json::read(text, &mut StrictJson).map_err(|error| match error { + ReadError::Syntax { .. } => unreadable(), + ReadError::Hook(error) => error, + })?; + compile_document(&document) +} + +/// Compiles an egress policy that has already been parsed, as `compile_policy` in `nvx.py` does. +/// +/// A parsed value cannot repeat a field, and its integers have no length limit: those checks +/// belong to the file format, which [`compile_policy_bytes`] reads. +/// +/// # Errors +/// +/// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) with Python's message, such as +/// `allow[0].to[1].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.256/8` or `deny emits at most +/// 256 native rules`. +pub fn compile_policy(value: &serde_json::Value) -> Result { + compile_document(&Document::from_value(value)) +} + +/// The checks of `nvx.py`'s policy reader: `strict_json_object` refuses repeated fields, and +/// `_bounded_json_integer` integers of more than 64 digits. +struct StrictJson; + +impl Hooks for StrictJson { + type Error = Error; + + fn object(&mut self, members: &[(PyText, NodeId)]) -> Result<()> { + let mut seen = std::collections::HashSet::with_capacity(members.len()); + for (name, _) in members { + if !seen.insert(name) { + return Err(invalid(format!( + "duplicate JSON property: {}", + name.render() + ))); + } + } + Ok(()) + } + + fn integer(&mut self, literal: &str) -> Result<()> { + if literal.trim_start_matches('-').len() > MAX_JSON_INTEGER_DIGITS { + return Err(invalid(format!( + "JSON integer exceeds {MAX_JSON_INTEGER_DIGITS}-digit limit" + ))); + } + Ok(()) + } +} + +/// Field names of a policy. +const ROOT_FIELDS: &[&str] = &["allow", "deny"]; +/// Field names of an MXC rule. +const MXC_RULE_FIELDS: &[&str] = &["to", "ports"]; +/// Field names of a destination of an MXC rule. +const PEER_FIELDS: &[&str] = &["cidr", "except"]; +/// Field names of a port selector of an MXC rule. +const PORT_FIELDS: &[&str] = &["protocol", "port", "endPort"]; +/// Field names of a legacy flat rule. +const LEGACY_RULE_FIELDS: &[&str] = &["cidr", "except", "protocol", "port", "endPort"]; + +/// A JSON object of a policy document. +struct Object<'a> { + /// The document that holds the object. + document: &'a Document, + /// The object's members, whose names are distinct. + members: &'a [(PyText, NodeId)], +} + +impl<'a> Object<'a> { + /// Returns the value of the field `name`, if the object has it. + fn get(&self, name: &str) -> Option { + self.members + .iter() + .find(|(field, _)| field.as_bytes() == name.as_bytes()) + .map(|&(_, value)| value) + } + + /// Returns whether the object has the field `name`. + fn has(&self, name: &str) -> bool { + self.get(name).is_some() + } + + /// Fails if the object has a field other than `fields`, naming the first unknown field in + /// code-point order, as `sorted(set(rule) - FIELDS)[0]` does. + fn check_fields(&self, fields: &[&str], description: &str) -> Result<()> { + let unknown = self + .members + .iter() + .map(|(name, _)| name) + .filter(|name| { + !fields + .iter() + .any(|field| name.as_bytes() == field.as_bytes()) + }) + .min(); + match unknown { + Some(name) => Err(invalid(format!( + "{description} has unknown field '{}'", + name.render() + ))), + None => Ok(()), + } + } + + /// Returns whether the object has any of `fields`. + fn has_any(&self, fields: &[&str]) -> bool { + fields.iter().any(|field| self.has(field)) + } + + /// Returns the node of `id` in the object's document. + fn node(&self, id: NodeId) -> &'a Node { + self.document.node(id) + } +} + +/// Returns the object at `id`, as `_object` does. +fn object<'a>(document: &'a Document, id: NodeId, description: &str) -> Result> { + match document.node(id) { + Node::Object(members) => Ok(Object { document, members }), + _ => Err(invalid(format!("{description} must be an object"))), + } +} + +/// Returns the elements of the array at `id`, as `_array` does. +fn array<'a>(document: &'a Document, id: NodeId, description: &str) -> Result<&'a [NodeId]> { + match document.node(id) { + Node::Array(elements) => Ok(elements), + _ => Err(invalid(format!("{description} must be an array"))), + } +} + +/// Returns the value of a field that should be a port number. +fn port_value(node: &Node) -> PortValue { + match node { + // Integers longer than an `i128` are out of range either way. + Node::Integer(literal) => PortValue::Integer(literal.parse().unwrap_or_else(|_| { + if literal.starts_with('-') { + i128::MIN + } else { + i128::MAX + } + })), + _ => PortValue::Other, + } +} + +/// Returns the protocol of the field at `id`, as the policy reader checks it. +fn protocol(document: &Document, id: NodeId, description: &str) -> Result { + match document.node(id) { + Node::String(text) => RuleProtocol::parse(text), + _ => None, + } + .ok_or_else(|| { + invalid(format!( + "{description}.protocol must be tcp, udp, icmp, or any" + )) + }) +} + +/// Compiles a policy document, as `compile_policy` does. +fn compile_document(document: &Document) -> Result { + let root = object(document, document.root(), "egress policy")?; + root.check_fields(ROOT_FIELDS, "egress policy")?; + Ok(CompiledEgressPolicy { + allow: compile_category(document, root.get("allow"), "allow")?, + deny: compile_category(document, root.get("deny"), "deny")?, + }) +} + +/// Compiles the rules of one action, which a policy without the field does not have, as +/// `_compile_category` does: every rule is checked before any is lowered. +fn compile_category( + document: &Document, + id: Option, + category: &str, +) -> Result> { + let elements = match id { + Some(id) => array(document, id, category)?, + None => &[], + }; + let mut rules = Vec::new(); + for (index, &element) in elements.iter().enumerate() { + rules.extend(parse_rule( + document, + element, + &format!("{category}[{index}]"), + )?); + } + lowering::lower(&rules, category) +} + +/// Parses a rule of either form, as `_parse_rule` does. +fn parse_rule(document: &Document, id: NodeId, description: &str) -> Result> { + let rule = object(document, id, description)?; + if rule.has_any(LEGACY_RULE_FIELDS) { + if rule.has_any(MXC_RULE_FIELDS) { + return Err(invalid(format!( + "{description} cannot mix MXC to/ports fields with legacy flat fields" + ))); + } + return parse_legacy_rule(&rule, description); + } + parse_mxc_rule(&rule, description) +} + +/// Parses a destination network and its exceptions, the `cidr` and optional `except` fields of +/// `description`, as the rest of `_parse_peer` does. +fn parse_peer_fields( + object: &Object<'_>, + cidr: NodeId, + except: Option, + description: &str, +) -> Result<(Family, Vec<(u128, u128)>)> { + let parent = parse_network(object.node(cidr), &format!("{description}.cidr"))?; + let exceptions = match except { + Some(except) => array(object.document, except, &format!("{description}.except"))?, + None => &[], + }; + lowering::destination( + parent, + exceptions.iter().enumerate().map(|(index, &exception)| { + parse_network( + object.node(exception), + &format!("{description}.except[{index}]"), + ) + }), + description, + ) +} + +/// Parses the CIDR value of a field, as `_network` does. +fn parse_network(node: &Node, description: &str) -> Result { + match node { + Node::String(text) => lowering::network(text, description), + _ => Err(lowering::not_a_cidr_string(description)), + } +} + +/// Parses a destination of an MXC rule, as `_parse_peer` does. +fn parse_peer( + document: &Document, + id: NodeId, + description: &str, +) -> Result<(Family, Vec<(u128, u128)>)> { + let peer = object(document, id, description)?; + peer.check_fields(PEER_FIELDS, description)?; + let Some(cidr) = peer.get("cidr") else { + return Err(invalid(format!("{description}.cidr is required"))); + }; + parse_peer_fields(&peer, cidr, peer.get("except"), description) +} + +/// Parses a legacy flat rule, as `_parse_legacy_rule` does. +fn parse_legacy_rule(rule: &Object<'_>, description: &str) -> Result> { + rule.check_fields(LEGACY_RULE_FIELDS, description)?; + let Some(cidr) = rule.get("cidr") else { + return Err(invalid(format!("{description}.cidr is required"))); + }; + let (family, addresses) = parse_peer_fields(rule, cidr, rule.get("except"), description)?; + let destinations = Some(vec![(family, addresses)]); + let port = rule.get("port"); + let end_port = rule.get("endPort"); + if end_port.is_some() && port.is_none() { + return Err(invalid(format!("{description}.endPort requires port"))); + } + let Some(protocol) = rule.get("protocol") else { + if port.is_some() { + return Err(invalid(format!("{description}.port requires protocol"))); + } + return Ok(lowering::combine(destinations, None)); + }; + let protocol = self::protocol(rule.document, protocol, description)?; + let selectors: Vec = match port { + None => vec![(protocol.native(), None)], + Some(port) => lowering::port_protocol_selectors( + protocol, + port_value(rule.node(port)), + end_port.map(|end_port| port_value(rule.node(end_port))), + description, + )?, + }; + Ok(lowering::combine(destinations, Some(selectors))) +} + +/// Parses a port selector of an MXC rule, as `_parse_mxc_port` does. +fn parse_mxc_port(document: &Document, id: NodeId, description: &str) -> Result> { + let port = object(document, id, description)?; + port.check_fields(PORT_FIELDS, description)?; + let protocol = match port.get("protocol") { + Some(protocol) => self::protocol(document, protocol, description)?, + None => RuleProtocol::Any, + }; + lowering::port_selectors( + protocol, + port.get("port").map(|value| port_value(port.node(value))), + port.get("endPort") + .map(|value| port_value(port.node(value))), + description, + ) +} + +/// Parses an MXC rule, as `_parse_mxc_rule` does. +fn parse_mxc_rule(rule: &Object<'_>, description: &str) -> Result> { + rule.check_fields(MXC_RULE_FIELDS, description)?; + let mut destinations = None; + if let Some(to) = rule.get("to") { + let description = format!("{description}.to"); + let peers = array(rule.document, to, &description)?; + if peers.is_empty() { + return Err(invalid(format!( + "{description} must contain at least one destination" + ))); + } + let mut parsed = Vec::with_capacity(peers.len()); + for (index, &peer) in peers.iter().enumerate() { + parsed.push(parse_peer( + rule.document, + peer, + &format!("{description}[{index}]"), + )?); + } + // A native rule names one family, so the peers of each family form their own + // destination set. + destinations = Some(lowering::group_destinations(&parsed)); + } + let mut selectors = None; + if let Some(ports) = rule.get("ports") { + let description = format!("{description}.ports"); + let ports = array(rule.document, ports, &description)?; + if ports.is_empty() { + return Err(invalid(format!( + "{description} must contain at least one selector" + ))); + } + let mut parsed = Vec::new(); + for (index, &port) in ports.iter().enumerate() { + parsed.extend(parse_mxc_port( + rule.document, + port, + &format!("{description}[{index}]"), + )?); + } + selectors = Some(parsed); + } + Ok(lowering::combine(destinations, selectors)) +} diff --git a/nvx-rs/src/host/network/policy/tests.rs b/nvx-rs/src/host/network/policy/tests.rs new file mode 100644 index 000000000..71ff5535f --- /dev/null +++ b/nvx-rs/src/host/network/policy/tests.rs @@ -0,0 +1,1166 @@ +//! Tests of the egress policy compiler: the parity fixture that the Python compiler recorded, and +//! ports of the cases of the former `scripts/test_egress_policy.py`. + +use std::path::{Path, PathBuf}; + +use serde_json::{Value, json}; + +use super::*; +use crate::ErrorKind; +use crate::host::network::address::probe; +use crate::host::python::path::{posix_str, windows_str}; + +/// The fixture that the retired `scripts/nvx_rs_fixtures.py` recorded from the Python compiler, +/// or the file in its format that `NVX_EGRESS_POLICY_FIXTURE` names, such as a larger corpus. +fn fixture() -> Value { + let path = std::env::var_os("NVX_EGRESS_POLICY_FIXTURE") + .map(PathBuf::from) + .unwrap_or_else(|| { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/egress-policy.json") + }); + let text = std::fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display())); + serde_json::from_str(&text).unwrap() +} + +/// Returns the bytes that a fixture case describes: text, or hexadecimal, each repeated. +fn input(segments: &Value) -> Vec { + let mut data = Vec::new(); + for segment in segments.as_array().unwrap() { + let bytes = match (segment.get("text"), segment.get("hex")) { + (Some(text), None) => text.as_str().unwrap().as_bytes().to_vec(), + (None, Some(hex)) => { + let hex = hex.as_str().unwrap(); + (0..hex.len()) + .step_by(2) + .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).unwrap()) + .collect() + } + _ => panic!("invalid segment {segment}"), + }; + let repeat = segment + .get("repeat") + .map_or(1, |count| count.as_u64().unwrap()); + for _ in 0..repeat { + data.extend_from_slice(&bytes); + } + } + data +} + +/// Describes a compilation as the fixture does. +fn outcome(result: Result) -> Value { + match result { + Ok(compiled) => json!({"allow": compiled.allow, "deny": compiled.deny}), + Err(error) => json!({"error": error.message()}), + } +} + +/// Returns the expected outcome of a fixture case for a file that messages call `path`. +fn expected(expected: &Value, path: &str) -> Value { + match expected.get("error") { + Some(message) => json!({"error": message.as_str().unwrap().replace("{path}", path)}), + None => expected.clone(), + } +} + +#[test] +fn every_fixture_case_compiles_as_python_compiled_it() { + let fixture = fixture(); + let cases = fixture["cases"].as_array().unwrap(); + assert!(cases.len() > 500, "{} cases", cases.len()); + let directory = tempfile::tempdir().unwrap(); + let file = directory.path().join("policy.json"); + let file_display = python_path(&file).display; + let mut failures = Vec::new(); + for case in cases { + let name = case["name"].as_str().unwrap(); + let data = input(&case["input"]); + let actual = outcome(compile_policy_bytes(&data, Path::new("policy.json"))); + let wanted = expected(&case["expected"], "policy.json"); + if actual != wanted { + failures.push(format!("{name}:\n expected {wanted}\n actual {actual}")); + continue; + } + std::fs::write(&file, &data).unwrap(); + let actual = outcome(compile_policy_file(&file)); + let wanted = expected(&case["expected"], &file_display); + if actual != wanted { + failures.push(format!( + "{name} (file):\n expected {wanted}\n actual {actual}" + )); + } + } + assert!( + failures.is_empty(), + "{} of {} cases differ:\n{}", + failures.len(), + cases.len(), + failures.join("\n") + ); +} + +#[test] +fn unreadable_files_fail_as_in_python() { + let fixture = fixture(); + let directory = tempfile::tempdir().unwrap(); + for case in fixture["files"].as_array().unwrap() { + let path = match case["name"].as_str().unwrap() { + "missing" => directory.path().join("missing.json"), + "directory" => directory.path().to_path_buf(), + name => panic!("unknown file case {name}"), + }; + let error = compile_policy_file(&path).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Io); + assert_eq!( + outcome(Err(error)), + expected(&case["expected"], &python_path(&path).display), + "{case}" + ); + } +} + +#[test] +fn paths_print_as_pathlib_prints_them() { + let fixture = fixture(); + for case in fixture["paths"].as_array().unwrap() { + let path = case["path"].as_str().unwrap(); + let posix = String::from_utf8(posix_str(path.as_bytes())).unwrap(); + assert_eq!(posix, case["posix"].as_str().unwrap(), "{path:?}"); + let units: Vec = path.encode_utf16().collect(); + let windows = String::from_utf16(&windows_str(&units)).unwrap(); + assert_eq!(windows, case["windows"].as_str().unwrap(), "{path:?}"); + } +} + +// Ports of the former `scripts/test_egress_policy.py`, one test for each of its tests, under the +// same names. + +/// Compiles `value` as `EgressPolicyTests.compile` does, from its JSON text in a policy file, and +/// checks that compiling the value itself agrees. +fn compile(value: Value) -> Result { + let data = serde_json::to_vec(&value).unwrap(); + let compiled = compile_policy_bytes(&data, Path::new("policy.json")); + let parsed = compile_policy(&value); + match (&compiled, &parsed) { + (Ok(compiled), Ok(parsed)) => assert_eq!(compiled, parsed, "{value}"), + (Err(compiled), Err(parsed)) => { + assert_eq!(compiled.message(), parsed.message(), "{value}"); + } + _ => panic!("{value}: the file gives {compiled:?} and the value {parsed:?}"), + } + compiled +} + +/// Compiles the text of a policy file. +fn compile_text(text: &str) -> Result { + compile_policy_bytes(text.as_bytes(), Path::new("policy.json")) +} + +/// Returns the message of a compilation that must fail. +fn message(result: Result) -> String { + let error = result.unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + error.message().to_owned() +} + +/// Returns the rules of `category`, `allow` or `deny`. +fn action<'a>(compiled: &'a CompiledEgressPolicy, category: &str) -> &'a [String] { + match category { + "allow" => &compiled.allow, + "deny" => &compiled.deny, + _ => panic!("unknown category {category}"), + } +} + +/// Returns `object` with the fields of `extra` added. +fn merged(object: &Value, extra: &Value) -> Value { + let mut object = object.as_object().unwrap().clone(); + object.extend(extra.as_object().unwrap().clone()); + Value::Object(object) +} + +#[test] +fn test_compiles_inclusive_tcp_and_udp_ranges() { + let compiled = compile(json!({ + "allow": [ + { + "to": [{"cidr": "192.0.2.7"}], + "ports": [{"protocol": "tcp", "port": 8000, "endPort": 8002}], + }, + { + "to": [{"cidr": "198.51.100.0/24"}], + "ports": [{"protocol": "udp", "port": 5000, "endPort": 5001}], + }, + ] + })) + .unwrap(); + assert_eq!( + compiled.allow, + [ + "192.0.2.7/32:tcp:8000-8002", + "198.51.100.0/24:udp:5000-5001" + ] + ); + assert!(compiled.deny.is_empty()); +} + +#[test] +fn test_lowers_each_range_to_one_native_range_rule() { + let ranges = [ + ( + json!({"protocol": "tcp", "port": 8000, "endPort": 8010}), + vec!["192.0.2.7/32:tcp:8000-8010"], + ), + ( + json!({"protocol": "udp", "port": 5000, "endPort": 5010}), + vec!["192.0.2.7/32:udp:5000-5010"], + ), + ( + json!({"protocol": "tcp", "port": 1, "endPort": 65535}), + vec!["192.0.2.7/32:tcp:1-65535"], + ), + ( + json!({"protocol": "any", "port": 1, "endPort": 65535}), + vec!["192.0.2.7/32:tcp:1-65535", "192.0.2.7/32:udp:1-65535"], + ), + // A range of one port lowers to that port's rule. + ( + json!({"protocol": "tcp", "port": 8000, "endPort": 8000}), + vec!["192.0.2.7/32:tcp:8000"], + ), + ]; + for category in ["allow", "deny"] { + for (selector, expected) in &ranges { + for rule in [ + json!({"to": [{"cidr": "192.0.2.7"}], "ports": [selector]}), + merged(&json!({"cidr": "192.0.2.7"}), selector), + ] { + let compiled = compile(json!({category: [rule]})).unwrap(); + assert_eq!(action(&compiled, category), expected, "{category} {rule}"); + } + } + } +} + +#[test] +fn test_keeps_a_denied_port_inside_an_allowed_range() { + let compiled = compile(json!({ + "allow": [{ + "to": [{"cidr": "192.0.2.0/24"}], + "ports": [{"protocol": "tcp", "port": 8000, "endPort": 8010}], + }], + "deny": [{ + "to": [{"cidr": "192.0.2.0/24"}], + "ports": [{"protocol": "tcp", "port": 8005}], + }], + })) + .unwrap(); + // OpenVMM applies deny precedence, so each list keeps its own rule. + assert_eq!(compiled.allow, ["192.0.2.0/24:tcp:8000-8010"]); + assert_eq!(compiled.deny, ["192.0.2.0/24:tcp:8005"]); +} + +#[test] +fn test_merges_adjacent_and_overlapping_ranges_of_one_network() { + let ranges = |bounds: &[(u16, u16)]| -> Vec { + bounds + .iter() + .map(|&(start, end)| { + json!({"cidr": "192.0.2.7", "protocol": "tcp", "port": start, "endPort": end}) + }) + .collect() + }; + let singles: Vec<(u16, u16)> = (8000..=8010).map(|port| (port, port)).collect(); + for (rules, expected) in [ + ( + ranges(&[(8000, 8004), (8005, 8010)]), + vec!["192.0.2.7/32:tcp:8000-8010"], + ), + ( + ranges(&[(8000, 8008), (8003, 8010)]), + vec!["192.0.2.7/32:tcp:8000-8010"], + ), + ( + [ranges(&[(8000, 8010)]), ranges(&singles)].concat(), + vec!["192.0.2.7/32:tcp:8000-8010"], + ), + ( + ranges(&[(8000, 8004), (8006, 8010)]), + vec!["192.0.2.7/32:tcp:8000-8004", "192.0.2.7/32:tcp:8006-8010"], + ), + ] { + let compiled = compile(json!({"allow": rules})).unwrap(); + assert_eq!(compiled.allow, expected); + } + // Networks keep their own ranges where their segments differ. + let compiled = compile(json!({ + "allow": [ + {"cidr": "192.0.2.7", "protocol": "udp", "port": 1, "endPort": 10}, + {"cidr": "198.51.100.9", "protocol": "udp", "port": 5, "endPort": 20}, + ] + })) + .unwrap(); + assert_eq!( + compiled.allow, + ["192.0.2.7/32:udp:1-10", "198.51.100.9/32:udp:5-20"] + ); +} + +#[test] +fn test_keeps_network_range_whole_when_adjacent_rule_changes_union() { + let compiled = compile(json!({ + "allow": [ + {"cidr": "10.0.0.0/25", "protocol": "tcp", "port": 1, "endPort": 10}, + {"cidr": "10.0.0.128/25", "protocol": "tcp", "port": 5, "endPort": 6}, + ] + })) + .unwrap(); + assert_eq!( + compiled.allow, + ["10.0.0.0/24:tcp:5-6", "10.0.0.0/25:tcp:1-10"] + ); +} + +#[test] +fn test_rejects_invalid_port_shapes_and_values() { + for rule in [ + json!({"cidr": "192.0.2.0/24", "protocol": "tcp", "endPort": 80}), + json!({"cidr": "192.0.2.0/24", "protocol": "any", "endPort": 80}), + json!({"cidr": "192.0.2.0/24", "protocol": "tcp", "port": 81, "endPort": 80}), + json!({"cidr": "192.0.2.0/24", "protocol": "tcp", "port": 0}), + json!({"cidr": "192.0.2.0/24", "protocol": "udp", "port": 65536}), + json!({"cidr": "192.0.2.0/24", "protocol": "tcp", "port": true}), + json!({"cidr": "192.0.2.0/24", "protocol": "any", "port": 0}), + json!({"cidr": "192.0.2.0/24", "port": 80}), + json!({"cidr": "192.0.2.0/24", "protocol": "icmp", "port": 8}), + json!({"cidr": "192.0.2.0/24", "protocol": "icmp", "endPort": 8}), + json!({"cidr": "192.0.2.0/24", "protocol": "ICMP"}), + json!({"cidr": "192.0.2.0/24", "protocol": "sctp"}), + ] { + message(compile(json!({"allow": [rule]}))); + } +} + +#[test] +fn test_reports_invalid_port_ranges_clearly() { + let cidr = "192.0.2.0/24"; + for (rule, expected) in [ + ( + json!({ + "to": [{"cidr": cidr}], + "ports": [{"protocol": "tcp", "port": 8010, "endPort": 8000}], + }), + "deny[0].ports[0].endPort 8000 cannot be below port 8010", + ), + ( + json!({"ports": [{"protocol": "udp", "endPort": 8010}]}), + "deny[0].ports[0].endPort requires port", + ), + ( + json!({"ports": [{"endPort": 8010}]}), + "deny[0].ports[0].endPort requires port", + ), + ( + json!({"ports": [{"protocol": "tcp", "port": 8000, "endPort": 65536}]}), + "deny[0].ports[0].endPort must be between 1 and 65535", + ), + ( + json!({"cidr": cidr, "protocol": "tcp", "port": 8010, "endPort": 8000}), + "deny[0].endPort 8000 cannot be below port 8010", + ), + ( + json!({"cidr": cidr, "protocol": "udp", "endPort": 8010}), + "deny[0].endPort requires port", + ), + ( + json!({"cidr": cidr, "endPort": 8010}), + "deny[0].endPort requires port", + ), + ( + json!({"cidr": cidr, "protocol": "tcp", "port": 8000, "endPort": 65536}), + "deny[0].endPort must be between 1 and 65535", + ), + ( + json!({"cidr": cidr, "protocol": "tcp", "port": 0, "endPort": 8010}), + "deny[0].port must be between 1 and 65535", + ), + ] { + assert_eq!(message(compile(json!({"deny": [rule]}))), expected); + } +} + +#[test] +fn test_compiles_protocol_selectors_with_and_without_ports() { + let selectors = [ + (json!({"protocol": "tcp"}), vec!["192.0.2.7/32:tcp"]), + (json!({"protocol": "udp"}), vec!["192.0.2.7/32:udp"]), + (json!({"protocol": "icmp"}), vec!["192.0.2.7/32:icmp"]), + (json!({"protocol": "any"}), vec!["192.0.2.7/32"]), + (json!({}), vec!["192.0.2.7/32"]), + ( + json!({"protocol": "any", "port": 443}), + vec!["192.0.2.7/32:tcp:443", "192.0.2.7/32:udp:443"], + ), + ( + json!({"protocol": "any", "port": 443, "endPort": 444}), + vec!["192.0.2.7/32:tcp:443-444", "192.0.2.7/32:udp:443-444"], + ), + ]; + for category in ["allow", "deny"] { + for (selector, expected) in &selectors { + let rule = merged(&json!({"cidr": "192.0.2.7"}), selector); + let compiled = compile(json!({category: [rule]})).unwrap(); + assert_eq!( + action(&compiled, category), + expected, + "{category} {selector}" + ); + } + } +} + +#[test] +fn test_protocol_wide_rules_cover_their_port_rules_only() { + let compiled = compile(json!({ + "allow": [ + {"cidr": "192.0.2.7", "protocol": "tcp", "port": 443}, + {"cidr": "192.0.2.0/24", "protocol": "tcp"}, + {"cidr": "192.0.2.7", "protocol": "udp", "port": 443}, + {"cidr": "192.0.2.0/24", "protocol": "icmp"}, + ], + "deny": [ + {"cidr": "198.51.100.0/24", "protocol": "any", "port": 53}, + {"cidr": "198.51.100.0/24", "protocol": "udp"}, + ], + })) + .unwrap(); + assert_eq!( + compiled.allow, + [ + "192.0.2.0/24:icmp", + "192.0.2.0/24:tcp", + "192.0.2.7/32:udp:443" + ] + ); + assert_eq!( + compiled.deny, + ["198.51.100.0/24:tcp:53", "198.51.100.0/24:udp"] + ); +} + +#[test] +fn test_address_only_rules_cover_protocol_wide_rules() { + let compiled = compile(json!({ + "allow": [ + {"cidr": "192.0.2.0/24", "protocol": "udp"}, + {"cidr": "192.0.2.0/25"}, + {"cidr": "192.0.2.0/25", "protocol": "icmp"}, + {"cidr": "192.0.2.128/25", "protocol": "udp", "port": 53}, + ] + })) + .unwrap(); + // Widening the UDP rule into the address-only half keeps it to one prefix. + assert_eq!(compiled.allow, ["192.0.2.0/24:udp", "192.0.2.0/25"]); +} + +/// One address-only rule for each of [`MAX_RULES_PER_ACTION`] hosts in distinct networks. +fn address_only() -> Vec { + (0..MAX_RULES_PER_ACTION) + .map(|index| json!({"cidr": format!("10.0.{index}.1/32")})) + .collect() +} + +#[test] +fn test_protocol_wide_rules_share_the_native_rule_budget() { + let address_only = address_only(); + let mut rules = address_only[..MAX_RULES_PER_ACTION - 1].to_vec(); + rules.push(json!({"cidr": "192.0.2.0/24", "protocol": "tcp"})); + let accepted = compile(json!({"allow": rules})).unwrap(); + assert_eq!(accepted.allow.len(), MAX_RULES_PER_ACTION); + assert!(accepted.allow.contains(&"192.0.2.0/24:tcp".to_owned())); + + let mut rules = address_only.clone(); + rules.push(json!({"cidr": "192.0.2.0/24", "protocol": "icmp"})); + assert!(message(compile(json!({"allow": rules}))).contains("at most 256")); + + // Protocol `any` with a port range lowers to a TCP and a UDP range. + let any_range = json!({"cidr": "192.0.2.1", "protocol": "any", "port": 1, "endPort": 129}); + let mut rules = address_only[..MAX_RULES_PER_ACTION - 2].to_vec(); + rules.push(any_range.clone()); + let accepted = compile(json!({"deny": rules})).unwrap(); + assert_eq!(accepted.deny.len(), MAX_RULES_PER_ACTION); + assert!(accepted.deny.contains(&"192.0.2.1/32:udp:1-129".to_owned())); + let mut rules = address_only[..MAX_RULES_PER_ACTION - 1].to_vec(); + rules.push(any_range); + assert!(message(compile(json!({"deny": rules}))).contains("at most 256")); +} + +#[test] +fn test_normalizes_host_bits_like_the_native_cidr_parser() { + let compiled = + compile(json!({"allow": [{"cidr": "10.0.0.5/24"}, {"cidr": "fe80::1/64"}]})).unwrap(); + assert_eq!(compiled.allow, ["10.0.0.0/24", "fe80::/64"]); +} + +#[test] +fn test_subtracts_rule_local_cidr_exclusions() { + let compiled = compile(json!({ + "allow": [ + {"to": [{"cidr": "192.0.2.0/24", "except": ["192.0.2.128/25"]}]}, + {"to": [{"cidr": "192.0.2.200/32"}]}, + ], + "deny": [ + {"to": [{"cidr": "198.51.100.0/24", "except": ["198.51.100.128/25"]}]}, + ], + })) + .unwrap(); + assert_eq!(compiled.allow, ["192.0.2.0/25", "192.0.2.200/32"]); + assert_eq!(compiled.deny, ["198.51.100.0/25"]); +} + +#[test] +fn test_compiles_mxc_destination_and_port_unions() { + let compiled = compile(json!({ + "allow": [{"ports": [{"port": 53}, {"protocol": "icmp"}]}], + "deny": [{ + "to": [{"cidr": "198.51.100.0/25"}, {"cidr": "198.51.100.128/25"}], + "ports": [{"protocol": "tcp", "port": 80}, {"protocol": "udp", "port": 53}], + }], + })) + .unwrap(); + // A rule without destinations matches every destination of both families. + assert_eq!( + compiled.allow, + [ + "0.0.0.0/0:icmp", + "0.0.0.0/0:tcp:53", + "0.0.0.0/0:udp:53", + "::/0:icmp", + "::/0:tcp:53", + "::/0:udp:53", + ] + ); + assert_eq!( + compiled.deny, + ["198.51.100.0/24:tcp:80", "198.51.100.0/24:udp:53"] + ); +} + +#[test] +fn test_deduplicates_overlapping_exclusions_and_collapses_safe_prefixes() { + let compiled = compile(json!({ + "allow": [ + { + "cidr": "192.0.2.0/24", + "except": ["192.0.2.128/25", "192.0.2.128/25", "192.0.2.192/26"], + "protocol": "tcp", + "port": 443, + }, + {"cidr": "198.51.100.0/25", "protocol": "udp", "port": 53}, + {"cidr": "198.51.100.128/25", "protocol": "udp", "port": 53}, + ] + })) + .unwrap(); + assert_eq!( + compiled.allow, + ["192.0.2.0/25:tcp:443", "198.51.100.0/24:udp:53"] + ); +} + +#[test] +fn test_rejects_exclusions_outside_parent_or_wrong_family() { + for (cidr, excluded) in [ + ("192.0.2.0/24", "198.51.100.0/24"), + ("192.0.2.0/24", "2001:db8::/32"), + ("2001:db8::/32", "2001:db9::/48"), + ("2001:db8::/32", "192.0.2.0/24"), + ("::/96", "0.0.0.0/8"), + ] { + let policy = json!({"allow": [{"to": [{"cidr": cidr, "except": [excluded]}]}]}); + assert!( + message(compile(policy)).contains("except"), + "{cidr} {excluded}" + ); + } +} + +#[test] +fn test_compiles_ipv6_networks_with_every_selector() { + let selectors = [ + ( + json!({"ports": [{"protocol": "tcp"}]}), + vec!["2001:db8::7/128:tcp"], + ), + ( + json!({"ports": [{"protocol": "udp"}]}), + vec!["2001:db8::7/128:udp"], + ), + ( + json!({"ports": [{"protocol": "icmp"}]}), + vec!["2001:db8::7/128:icmp"], + ), + (json!({}), vec!["2001:db8::7/128"]), + ( + json!({"ports": [{"protocol": "any", "port": 443}]}), + vec!["2001:db8::7/128:tcp:443", "2001:db8::7/128:udp:443"], + ), + ( + json!({"ports": [{"protocol": "tcp", "port": 8000, "endPort": 8010}]}), + vec!["2001:db8::7/128:tcp:8000-8010"], + ), + ]; + for category in ["allow", "deny"] { + for (selector, expected) in &selectors { + let rule = merged(&json!({"to": [{"cidr": "2001:db8::7"}]}), selector); + let compiled = compile(json!({category: [rule]})).unwrap(); + assert_eq!( + action(&compiled, category), + expected, + "{category} {selector}" + ); + } + } +} + +#[test] +fn test_compiles_one_allowed_ipv6_network_with_a_denied_address() { + let compiled = compile(json!({ + "allow": [ + {"to": [{"cidr": "2001:db8:1::/64"}], "ports": [{"protocol": "tcp", "port": 443}]}, + {"to": [{"cidr": "::/0"}], "ports": [{"protocol": "udp", "port": 53}]}, + ], + "deny": [{"to": [{"cidr": "2001:db8:1::123"}]}], + })) + .unwrap(); + assert_eq!(compiled.allow, ["::/0:udp:53", "2001:db8:1::/64:tcp:443"]); + assert_eq!(compiled.deny, ["2001:db8:1::123/128"]); +} + +#[test] +fn test_flat_rules_accept_ipv6_networks() { + let compiled = compile(json!({ + "allow": [{ + "cidr": "2001:db8::/32", + "except": ["2001:db8::/33"], + "protocol": "tcp", + "port": 443, + }] + })) + .unwrap(); + assert_eq!(compiled.allow, ["2001:db8:8000::/33:tcp:443"]); +} + +#[test] +fn test_keeps_each_address_family_separate() { + // IPv4 0.0.0.0/0 and IPv6 ::/96 span the same integers, so lowering both families together + // would merge or cover one with the other. + let compiled = compile(json!({ + "allow": [ + { + "to": [{"cidr": "::/96"}, {"cidr": "0.0.0.0/0"}], + "ports": [{"protocol": "tcp", "port": 80}], + }, + {"to": [{"cidr": "192.0.2.0/24"}]}, + {"to": [{"cidr": "::c000:200/120"}], "ports": [{"protocol": "udp", "port": 53}]}, + ], + "deny": [ + {"to": [{"cidr": "0.0.0.0/0"}]}, + {"to": [{"cidr": "::/96"}], "ports": [{"protocol": "icmp"}]}, + ], + })) + .unwrap(); + assert_eq!( + compiled.allow, + [ + "0.0.0.0/0:tcp:80", + "192.0.2.0/24", + "::/96:tcp:80", + "::c000:200/120:udp:53", + ] + ); + assert_eq!(compiled.deny, ["0.0.0.0/0", "::/96:icmp"]); +} + +#[test] +fn test_lowers_the_destinations_of_each_family_separately() { + let compiled = compile(json!({ + "allow": [{ + "to": [ + {"cidr": "192.0.2.0/25"}, + {"cidr": "2001:db8::/32", "except": ["2001:db8::/33"]}, + {"cidr": "192.0.2.128/25"}, + ], + "ports": [{"protocol": "tcp", "port": 443}], + }] + })) + .unwrap(); + assert_eq!( + compiled.allow, + ["192.0.2.0/24:tcp:443", "2001:db8:8000::/33:tcp:443"] + ); +} + +#[test] +fn test_subtracts_ipv6_exclusions() { + let compiled = compile(json!({ + "deny": [{ + "to": [{"cidr": "2001:db8::/32", "except": ["2001:db8:1::/48", "2001:db8:1::/64"]}], + "ports": [{"protocol": "tcp", "port": 22}], + }] + })) + .unwrap(); + // `IPv6Network("2001:db8::/32").address_exclude(IPv6Network("2001:db8:1::/48"))`, sorted. + assert_eq!( + compiled.deny, + [ + "2001:db8::/48:tcp:22", + "2001:db8:2::/47:tcp:22", + "2001:db8:4::/46:tcp:22", + "2001:db8:8::/45:tcp:22", + "2001:db8:10::/44:tcp:22", + "2001:db8:20::/43:tcp:22", + "2001:db8:40::/42:tcp:22", + "2001:db8:80::/41:tcp:22", + "2001:db8:100::/40:tcp:22", + "2001:db8:200::/39:tcp:22", + "2001:db8:400::/38:tcp:22", + "2001:db8:800::/37:tcp:22", + "2001:db8:1000::/36:tcp:22", + "2001:db8:2000::/35:tcp:22", + "2001:db8:4000::/34:tcp:22", + "2001:db8:8000::/33:tcp:22", + ] + ); + assert_eq!(compiled.deny.len(), 16); +} + +#[test] +fn test_rejects_ipv6_scopes() { + // Clearing host bits also clears the scope of the network address, so the scoped addresses + // with host bits must not compile as unscoped. + let cidrs = [ + "fe80::%eth0/64", + "fe80::1%1", + "fe80::1%eth0/64", + "fe80::1%1/64", + "::ffff:192.0.2.1%eth0/120", + ]; + for category in ["allow", "deny"] { + for cidr in cidrs { + for (field, rule) in [ + ("to[0].cidr", json!({"to": [{"cidr": cidr}]})), + ( + "to[0].except[0]", + json!({"to": [{"cidr": "::/0", "except": [cidr]}]}), + ), + ("cidr", json!({"cidr": cidr})), + ("except[0]", json!({"cidr": "::/0", "except": [cidr]})), + ] { + assert_eq!( + message(compile(json!({category: [rule]}))), + format!("{category}[0].{field} must not name an IPv6 scope: {cidr}") + ); + } + } + } +} + +#[test] +fn test_both_families_share_the_native_rule_budget() { + // Ports that are not adjacent cannot form one range, so each lowers to its own native rule. + let rules = |ipv4_ports: u16, ipv6_ports: u16| { + let rule = |cidr: &str, ports: u16| { + let selectors: Vec = (0..ports) + .map(|index| json!({"protocol": "tcp", "port": 2 * index + 1})) + .collect(); + json!({"to": [{"cidr": cidr}], "ports": selectors}) + }; + json!({"allow": [rule("192.0.2.1", ipv4_ports), rule("2001:db8::1", ipv6_ports)]}) + }; + assert_eq!(compile(rules(128, 128)).unwrap().allow.len(), 256); + for (ipv4_ports, ipv6_ports) in [(129, 128), (128, 129)] { + assert!(message(compile(rules(ipv4_ports, ipv6_ports))).contains("at most 256")); + } +} + +#[test] +fn test_rejects_invalid_mxc_rule_shapes() { + for rule in [ + json!({"to": []}), + json!({"to": [{}]}), + json!({"to": [{"cidr": "192.0.2.0/24", "unknown": true}]}), + json!({"ports": []}), + json!({"ports": [{"protocol": "icmp", "port": 8}]}), + json!({"ports": [{"protocol": "ICMP"}]}), + json!({"ports": [{"protocol": "sctp"}]}), + json!({"ports": [{"endPort": 80}]}), + json!({"to": [{"cidr": "192.0.2.0/24"}], "cidr": "192.0.2.0/24"}), + ] { + message(compile(json!({"allow": [rule]}))); + } +} + +#[test] +fn test_rejects_unknown_and_malformed_fields() { + for value in [ + json!([]), + json!({"unknown": []}), + json!({"allow": {}}), + json!({"allow": ["192.0.2.0/24"]}), + json!({"allow": [{"cidr": "192.0.2.0/24", "unknown": 1}]}), + json!({"allow": [{"cidr": 7}]}), + json!({"allow": [{"cidr": "192.0.2.0/24", "except": "192.0.2.1"}]}), + json!({"allow": [{"cidr": "192.0.2.0/24", "protocol": null}]}), + ] { + message(compile(value)); + } +} + +#[test] +fn test_rejects_duplicate_json_fields() { + for text in [ + r#"{"deny":[{"cidr":"0.0.0.0/0"}],"deny":[]}"#, + r#"{"allow":[{"cidr":"192.0.2.0/24","port":80,"port":443,"protocol":"tcp"}]}"#, + ] { + assert!( + message(compile_text(text)).contains("duplicate JSON property"), + "{text}" + ); + } +} + +#[test] +fn test_empty_destination_does_not_expand_port_range() { + let compiled = compile(json!({ + "allow": [{ + "cidr": "192.0.2.0/24", + "except": ["192.0.2.0/24"], + "protocol": "tcp", + "port": 1, + "endPort": 65535, + }] + })) + .unwrap(); + assert!(compiled.allow.is_empty()); + assert!(compiled.deny.is_empty()); +} + +#[test] +fn test_limits_the_actual_policy_read() { + // The compiler reads one byte past the limit instead of trusting the file's metadata, which + // the Python test fakes as empty. + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("policy.json"); + std::fs::write(&path, vec![b' '; MAX_POLICY_FILE_SIZE + 1]).unwrap(); + assert!(message(compile_policy_file(&path)).contains("byte limit")); +} + +#[test] +fn test_reports_oversized_json_integer_rejection() { + let text = format!(r#"{{"allow":[{{"cidr":{}}}]}}"#, "9".repeat(10000)); + assert_eq!( + message(compile_text(&text)), + "JSON integer exceeds 64-digit limit" + ); +} + +#[test] +fn test_many_source_networks_can_collapse_within_native_budget() { + let start = u32::from(std::net::Ipv4Addr::new(192, 0, 2, 0)); + let rules: Vec = (0..512) + .map(|offset| { + let address = std::net::Ipv4Addr::from(start + offset); + json!({"cidr": address.to_string(), "protocol": "tcp", "port": 80}) + }) + .collect(); + let compiled = compile(json!({"allow": rules})).unwrap(); + assert_eq!(compiled.allow, ["192.0.2.0/23:tcp:80"]); +} + +#[test] +fn test_accepts_exact_256_rule_boundaries() { + // Ranges with a port between them cannot merge. + let separate_ranges = |cidr: &str, protocol: &str| -> Vec { + (0..MAX_RULES_PER_ACTION) + .map(|index| { + json!({ + "cidr": cidr, + "protocol": protocol, + "port": 3 * index + 1, + "endPort": 3 * index + 2, + }) + }) + .collect() + }; + let compiled = compile(json!({ + "allow": separate_ranges("192.0.2.1", "tcp"), + "deny": separate_ranges("198.51.100.1", "udp"), + })) + .unwrap(); + assert_eq!(compiled.allow.len(), 256); + assert_eq!(compiled.deny.len(), 256); + assert_eq!( + compiled.allow[..2], + ["192.0.2.1/32:tcp:1-2", "192.0.2.1/32:tcp:4-5"] + ); + assert_eq!(compiled.deny.last().unwrap(), "198.51.100.1/32:udp:766-767"); +} + +#[test] +fn test_rejects_257_rules_before_materializing_them() { + for category in ["allow", "deny"] { + let rules: Vec = (0..=MAX_RULES_PER_ACTION) + .map(|index| json!({"cidr": "192.0.2.1", "protocol": "tcp", "port": 2 * index + 1})) + .collect(); + assert!(message(compile(json!({category: rules}))).contains("at most 256")); + } +} + +#[test] +fn test_overlapping_segments_do_not_inflate_native_rule_budget() { + let internal_ports = (1000..1056).step_by(2); + let mut rules = vec![json!({ + "cidr": "0.0.0.0/0", + "except": ["10.0.0.0/8"], + "protocol": "tcp", + "port": 1, + "endPort": 65535, + })]; + rules.extend( + internal_ports + .clone() + .map(|port| json!({"cidr": "10.0.0.0/8", "protocol": "tcp", "port": port})), + ); + let compiled = compile(json!({"allow": rules})).unwrap(); + let mut expected: Vec = [ + "0.0.0.0/5", + "8.0.0.0/7", + "11.0.0.0/8", + "12.0.0.0/6", + "16.0.0.0/4", + "32.0.0.0/3", + "64.0.0.0/2", + "128.0.0.0/1", + ] + .iter() + .map(|cidr| format!("{cidr}:tcp:1-65535")) + .chain(internal_ports.map(|port| format!("0.0.0.0/0:tcp:{port}"))) + .collect(); + let mut actual = compiled.allow.clone(); + expected.sort(); + actual.sort(); + assert_eq!(actual, expected); + assert_eq!(compiled.allow.len(), 36); +} + +#[test] +fn test_bounds_exclusions_times_port_segments() { + // The exclusion leaves eight networks, and the gap between ports keeps each port a segment + // of its own. + let rules = |count: usize, port: &dyn Fn(usize) -> usize| -> Vec { + (0..count) + .map(|index| { + json!({ + "cidr": "192.0.2.0/24", + "except": ["192.0.2.1/32"], + "protocol": "tcp", + "port": port(index), + }) + }) + .collect() + }; + let separate = |index: usize| 2 * index + 1; + let accepted = compile(json!({"allow": rules(32, &separate)})).unwrap(); + assert_eq!(accepted.allow.len(), 256); + assert!(message(compile(json!({"allow": rules(33, &separate)}))).contains("at most 256")); + + // Adjacent ports merge into one range for each network. + let merged = compile(json!({"allow": rules(33, &|index| index + 1)})).unwrap(); + assert_eq!(merged.allow.len(), 8); + assert!(merged.allow.iter().all(|rule| rule.ends_with(":tcp:1-33"))); +} + +#[test] +fn test_delays_fragment_conversion_until_after_covering_union() { + let first = u32::from(std::net::Ipv4Addr::new(192, 0, 0, 1)); + let exclusions: Vec = (0..4096) + .map(|index| std::net::Ipv4Addr::from(first + 2 * index).to_string()) + .collect(); + let fragmented = json!({ + "cidr": "192.0.0.0/16", + "except": exclusions, + "protocol": "tcp", + "port": 80, + "endPort": 81, + }); + let covering = json!({"cidr": "192.0.0.0/16", "protocol": "tcp", "port": 80, "endPort": 81}); + // The Python test counts calls of `ipaddress.summarize_address_range` over both orders: one + // for each compilation, of the union, and none for the fragments. + let ((), summaries, _) = probe::count(|| { + for rules in [ + [fragmented.clone(), covering.clone()], + [covering.clone(), fragmented.clone()], + ] { + let policy = serde_json::to_vec(&json!({"allow": rules})).unwrap(); + let compiled = compile_policy_bytes(&policy, Path::new("policy.json")).unwrap(); + assert_eq!(compiled.allow, ["192.0.0.0/16:tcp:80-81"]); + } + }); + assert_eq!(summaries, 2); +} + +#[test] +fn test_validates_redundant_rules_before_canonicalization() { + let policy = json!({ + "allow": [ + {"cidr": "192.0.2.0/24"}, + {"cidr": "192.0.2.1", "unknown": true}, + ] + }); + assert!(message(compile(policy)).contains("unknown field")); +} + +#[test] +fn test_address_only_rule_covers_large_protocol_range() { + let ranged = json!({"cidr": "192.0.2.0/24", "protocol": "tcp", "port": 1, "endPort": 65535}); + let address_only = json!({"cidr": "192.0.2.0/24"}); + for rules in [ + [ranged.clone(), address_only.clone()], + [address_only.clone(), ranged.clone()], + ] { + let compiled = compile(json!({"allow": rules})).unwrap(); + assert_eq!(compiled.allow, ["192.0.2.0/24"]); + } +} + +#[test] +fn test_address_only_rule_prunes_redundant_port_events_before_sweep() { + let covering = json!({"cidr": "0.0.0.0/0"}); + let first = u32::from(std::net::Ipv4Addr::new(192, 0, 0, 0)); + let protocol_rules: Vec = (1..=1000u32) + .map(|start| { + json!({ + "cidr": format!("{}/32", std::net::Ipv4Addr::from(first + start)), + "protocol": "tcp", + "port": start, + "endPort": 2001 - start, + }) + }) + .collect(); + for category in ["allow", "deny"] { + for covering_first in [true, false] { + let mut rules = protocol_rules.clone(); + if covering_first { + rules.insert(0, covering.clone()); + } else { + rules.push(covering.clone()); + } + let policy = serde_json::to_vec(&json!({category: rules})).unwrap(); + // The Python test counts the intervals that `_merge_intervals` receives: only the + // address-only rule's, because its coverage drops every port rule before the sweep. + let (compiled, _, merged_intervals) = + probe::count(|| compile_policy_bytes(&policy, Path::new("policy.json"))); + assert_eq!(action(&compiled.unwrap(), category), ["0.0.0.0/0"]); + assert_eq!(merged_intervals, 1, "{category} {covering_first}"); + } + } +} + +#[test] +fn test_partial_address_only_coverage_keeps_protocol_union_compact() { + let address_only_rules: Vec = (0..128) + .map(|index| json!({"cidr": format!("192.0.0.{}/32", 2 * index + 1)})) + .collect(); + let protocol_rule = json!({"cidr": "192.0.0.0/16", "protocol": "tcp", "port": 443}); + for category in ["allow", "deny"] { + let mut compiled_orders = Vec::new(); + for protocol_first in [false, true] { + let mut rules = address_only_rules.clone(); + if protocol_first { + rules.insert(0, protocol_rule.clone()); + } else { + rules.push(protocol_rule.clone()); + } + let compiled = compile(json!({category: rules})).unwrap(); + let compiled_rules = action(&compiled, category).to_vec(); + assert_eq!(compiled_rules.len(), 129); + assert!(compiled_rules.contains(&"192.0.0.0/16:tcp:443".to_owned())); + let others: std::collections::BTreeSet<&str> = compiled_rules + .iter() + .filter(|rule| !rule.ends_with(":tcp:443")) + .map(String::as_str) + .collect(); + let hosts: std::collections::BTreeSet<&str> = address_only_rules + .iter() + .map(|rule| rule["cidr"].as_str().unwrap()) + .collect(); + assert_eq!(others, hosts); + compiled_orders.push(compiled_rules); + } + assert_eq!(compiled_orders[0], compiled_orders[1]); + } +} + +#[test] +fn test_partial_address_only_canonicalization_does_not_bridge_gaps() { + let compiled = compile(json!({ + "allow": [ + {"cidr": "192.0.2.1/32"}, + {"cidr": "192.0.2.0/32", "protocol": "tcp", "port": 443}, + {"cidr": "192.0.2.3/32", "protocol": "tcp", "port": 443}, + ] + })) + .unwrap(); + assert_eq!( + compiled.allow, + [ + "192.0.2.0/31:tcp:443", + "192.0.2.1/32", + "192.0.2.3/32:tcp:443", + ] + ); +} + +// Tests of the port that the Python tests do not have. + +#[test] +fn rules_share_their_destinations_among_their_selectors() { + // 8192 destinations, none adjacent to another, for 8192 selectors: copying the destinations + // for each selector would take 2 GiB. Python 3.14 compiles each policy as asserted here. + let first = u32::from(std::net::Ipv4Addr::new(10, 0, 0, 0)); + let destinations: Vec = (0..8192u32) + .map(|index| json!({"cidr": std::net::Ipv4Addr::from(first + 2 * index).to_string()})) + .collect(); + let tcp: Vec = (0..8192u32) + .map(|index| json!({"protocol": "tcp", "port": 2 * index + 1})) + .collect(); + let mixed: Vec = [ + vec![json!({"protocol": "any"}); 2048], + vec![json!({"protocol": "icmp"}); 2048], + tcp[..4096].to_vec(), + ] + .concat(); + let compiled = compile(json!({ + "allow": [ + {"to": destinations, "ports": mixed}, + {"to": [{"cidr": "0.0.0.0/0"}]}, + ] + })) + .unwrap(); + assert_eq!(compiled.allow, ["0.0.0.0/0"]); + assert!(compiled.deny.is_empty()); + assert_eq!( + message(compile( + json!({"deny": [{"to": destinations, "ports": tcp}]}) + )), + "deny emits at most 256 native rules" + ); + let any = vec![json!({"protocol": "any"}); 8192]; + assert_eq!( + message(compile( + json!({"allow": [{"to": destinations, "ports": any}]}) + )), + "allow emits at most 256 native rules" + ); +} diff --git a/nvx-rs/src/host/platform/linux.rs b/nvx-rs/src/host/platform/linux.rs index 83bd68ca0..90139165b 100644 --- a/nvx-rs/src/host/platform/linux.rs +++ b/nvx-rs/src/host/platform/linux.rs @@ -11,6 +11,9 @@ use super::Transport; use crate::host::sys::{self, Errno}; use crate::hypervisor::Hypervisor; +#[cfg(feature = "cli")] +pub(crate) use crate::host::sys::{interrupted, watch_interrupts}; + /// Whether this host can run NVX microVMs. pub const SUPPORTED: bool = true; @@ -61,7 +64,7 @@ pub(crate) fn start_time_from_stat( } /// Returns whether a process has threads besides its main thread. -fn other_threads_remain(pid: u32) -> io::Result { +pub(crate) fn other_threads_remain(pid: u32) -> io::Result { match fs::read_dir(format!("/proc/{pid}/task")) { Ok(threads) => Ok(threads.count() > 1), Err(error) diff --git a/nvx-rs/src/host/platform/other.rs b/nvx-rs/src/host/platform/other.rs index 9af804e52..a29e79d3a 100644 --- a/nvx-rs/src/host/platform/other.rs +++ b/nvx-rs/src/host/platform/other.rs @@ -7,9 +7,24 @@ use std::time::Duration; use super::Transport; use crate::hypervisor::Hypervisor; +#[cfg(all(feature = "cli", unix))] +pub(crate) use crate::host::sys::{interrupted, watch_interrupts}; + /// Whether this host can run NVX microVMs. pub const SUPPORTED: bool = false; +/// Does nothing: this host has no console interrupts to watch. +#[cfg(all(feature = "cli", not(unix)))] +pub(crate) fn watch_interrupts() -> io::Result<()> { + Ok(()) +} + +/// Reports no interrupt: this host has no console interrupts to watch. +#[cfg(all(feature = "cli", not(unix)))] +pub(crate) fn interrupted() -> bool { + false +} + fn unsupported() -> io::Error { io::Error::new( io::ErrorKind::Unsupported, diff --git a/nvx-rs/src/host/platform/windows.rs b/nvx-rs/src/host/platform/windows.rs index 0c9596c42..67f71e202 100644 --- a/nvx-rs/src/host/platform/windows.rs +++ b/nvx-rs/src/host/platform/windows.rs @@ -20,11 +20,17 @@ use windows_sys::Win32::Storage::FileSystem::{ GetFileInformationByHandle, OPEN_EXISTING, ReadFile, SECURITY_IDENTIFICATION, SECURITY_SQOS_PRESENT, WriteFile, }; +#[cfg(feature = "cli")] +use windows_sys::Win32::Storage::FileSystem::{LockFile, UnlockFile}; +#[cfg(feature = "cli")] +use windows_sys::Win32::System::Console::{CTRL_C_EVENT, SetConsoleCtrlHandler}; use windows_sys::Win32::System::IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED}; use windows_sys::Win32::System::LibraryLoader::{ GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExW, }; use windows_sys::Win32::System::Pipes::{GetNamedPipeServerProcessId, WaitNamedPipeW}; +#[cfg(feature = "cli")] +use windows_sys::Win32::System::Threading::GetExitCodeProcess; use windows_sys::Win32::System::Threading::{ CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, CreateEventW, CreateProcessW, DeleteProcThreadAttributeList, @@ -187,6 +193,40 @@ impl LaunchedProcess { // SAFETY: as above. unsafe { WaitForSingleObject(raw(&self.process), milliseconds) == WAIT_OBJECT_0 } } + + /// Waits up to `timeout` for the process to exit, and returns its exit status if it has, + /// as `Popen.wait` reads it with `GetExitCodeProcess`. + #[cfg(feature = "cli")] + pub(crate) fn wait_timeout(&self, timeout: Duration) -> io::Result> { + let milliseconds = u32::try_from(timeout.as_millis()).unwrap_or(INFINITE - 1); + // SAFETY: CreateProcessW returned the handle with full access. + match unsafe { WaitForSingleObject(raw(&self.process), milliseconds) } { + WAIT_OBJECT_0 => {} + WAIT_TIMEOUT => return Ok(None), + _ => return Err(io::Error::last_os_error()), + } + let mut status = 0u32; + // SAFETY: as above, and the output pointer is valid. + if unsafe { GetExitCodeProcess(raw(&self.process), &mut status) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(Some(status)) + } + + /// Asks the process to end with status 1 without waiting for it, as `Popen.terminate` and + /// `Popen.kill` do on Windows. + #[cfg(feature = "cli")] + pub(crate) fn request_termination(&self) -> io::Result<()> { + // SAFETY: CreateProcessW returned the handle with full access. + if unsafe { TerminateProcess(raw(&self.process), 1) } == 0 { + let error = io::Error::last_os_error(); + // A process that already exited cannot be terminated; Python ignores that too. + if self.wait_timeout(Duration::ZERO)?.is_none() { + return Err(error); + } + } + Ok(()) + } } /// Owns an initialized process-thread attribute list. @@ -651,6 +691,61 @@ impl Transport for PipeTransport { } } +/// Whether Ctrl-C reached the console since [`watch_interrupts`] installed its handler. +#[cfg(feature = "cli")] +static INTERRUPTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +/// The console control handler, which records Ctrl-C and leaves every other event, such as +/// Ctrl-Break, to the next handler, which terminates the process. +#[cfg(feature = "cli")] +unsafe extern "system" fn record_interrupt(control: u32) -> windows_sys::core::BOOL { + if control == CTRL_C_EVENT { + INTERRUPTED.store(true, std::sync::atomic::Ordering::SeqCst); + 1 + } else { + 0 + } +} + +/// Records each Ctrl-C for [`interrupted`] instead of letting it terminate the process, as +/// Python's `SIGINT` handler, which raises `KeyboardInterrupt`, does. Ctrl-Break still +/// terminates it, as it terminates Python. +#[cfg(feature = "cli")] +pub(crate) fn watch_interrupts() -> io::Result<()> { + // SAFETY: the handler only stores to an atomic, and stays valid for the process's lifetime. + if unsafe { SetConsoleCtrlHandler(Some(record_interrupt), 1) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Returns whether Ctrl-C reached the console since [`watch_interrupts`]. +#[cfg(feature = "cli")] +pub(crate) fn interrupted() -> bool { + INTERRUPTED.load(std::sync::atomic::Ordering::SeqCst) +} + +/// Locks the first byte of `file` unless another handle holds a lock on it, as +/// `msvcrt.locking(fd, LK_NBLCK, 1)` does at offset 0, and returns whether it took the lock. +/// +/// Like the C runtime, which reports every failure of `LockFile` as `EACCES` in this mode, any +/// failure counts as a lock that another handle holds. +#[cfg(feature = "cli")] +pub(crate) fn try_lock_first_byte(file: &fs::File) -> bool { + // SAFETY: the handle is open for as long as `file` is borrowed. + unsafe { LockFile(file.as_raw_handle() as HANDLE, 0, 0, 1, 0) != 0 } +} + +/// Unlocks the first byte of `file`, as `msvcrt.locking(fd, LK_UNLCK, 1)` does at offset 0. +#[cfg(feature = "cli")] +pub(crate) fn unlock_first_byte(file: &fs::File) -> io::Result<()> { + // SAFETY: the handle is open for as long as `file` is borrowed. + if unsafe { UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, 1, 0) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + #[cfg(test)] mod tests { use std::io::Read; diff --git a/nvx-rs/src/host/process.rs b/nvx-rs/src/host/process.rs index 23d2752ce..333917135 100644 --- a/nvx-rs/src/host/process.rs +++ b/nvx-rs/src/host/process.rs @@ -37,6 +37,76 @@ impl Launched { #[cfg(windows)] return self.process.id(); } + + /// Waits up to `timeout` for the process to exit, and returns its status if it has, as + /// `Popen.wait` and `Popen.poll` report it: the exit code, or on Unix the negated number of + /// the signal that ended the process. + #[cfg(feature = "cli")] + pub(crate) fn wait_timeout(&mut self, timeout: Duration) -> io::Result> { + #[cfg(windows)] + return Ok(self.process.wait_timeout(timeout)?.map(i64::from)); + #[cfg(not(windows))] + { + let deadline = Instant::now().checked_add(timeout); + loop { + if let Some(status) = self.child.try_wait()? { + return Ok(Some(status_value(status))); + } + let now = Instant::now(); + match deadline { + Some(deadline) if now >= deadline => return Ok(None), + Some(deadline) => thread::sleep((deadline - now).min(POLL_INTERVAL)), + None => thread::sleep(POLL_INTERVAL), + } + } + } + } + + /// Asks the process to end, as `Popen.terminate` does: with `SIGTERM` on Unix, and by + /// terminating it with status 1 on Windows. A process that has exited is left alone. + #[cfg(feature = "cli")] + pub(crate) fn terminate(&mut self) -> io::Result<()> { + #[cfg(windows)] + return self.process.request_termination(); + #[cfg(not(windows))] + self.signal(nix::sys::signal::Signal::SIGTERM) + } + + /// Ends the process, as `Popen.kill` does: with `SIGKILL` on Unix, and by terminating it with + /// status 1 on Windows. A process that has exited is left alone. + #[cfg(feature = "cli")] + pub(crate) fn kill(&mut self) -> io::Result<()> { + #[cfg(windows)] + return self.process.request_termination(); + #[cfg(not(windows))] + self.signal(nix::sys::signal::Signal::SIGKILL) + } + + /// Sends `signal` to the process unless it has exited. The child is not reaped until it is + /// waited for, so its process ID cannot name another process meanwhile. + #[cfg(all(feature = "cli", not(windows)))] + fn signal(&mut self, signal: nix::sys::signal::Signal) -> io::Result<()> { + if self.child.try_wait()?.is_some() { + return Ok(()); + } + let pid = i32::try_from(self.child.id()).map_err(io::Error::other)?; + match nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), signal) { + Ok(()) | Err(nix::errno::Errno::ESRCH) => Ok(()), + Err(error) => Err(error.into()), + } + } +} + +/// Returns `Popen.returncode` for `status`: the exit code, or the negated signal number. +#[cfg(all(feature = "cli", not(windows)))] +fn status_value(status: std::process::ExitStatus) -> i64 { + use std::os::unix::process::ExitStatusExt; + + match (status.code(), status.signal()) { + (Some(code), _) => i64::from(code), + (None, Some(signal)) => -i64::from(signal), + (None, None) => 0, + } } /// Starts OpenVMM detached from the caller. diff --git a/nvx-rs/src/host/python/builtins.rs b/nvx-rs/src/host/python/builtins.rs new file mode 100644 index 000000000..5e0789dd7 --- /dev/null +++ b/nvx-rs/src/host/python/builtins.rs @@ -0,0 +1,614 @@ +//! Python's built-in string and integer operations. +//! +//! - [`is_space`], [`split`], and [`strip`]: `str.isspace`, `str.split()`, and `str.strip()`. +//! Python's whitespace includes the ASCII separators U+001C through U+001F, which Rust's +//! [`char::is_whitespace`] does not. +//! - [`repr`]: `repr` of a `str`, with which messages quote values. +//! - [`Int`]: a Python `int` of any size, from `int(text)`, with its acceptance of signs, +//! underscores, Unicode decimal digits, and surrounding whitespace, and its limit of +//! [`MAX_INT_DIGITS`] decimal digits. + +use std::cmp::Ordering; +use std::fmt::{self, Write as _}; + +use super::unicode; + +/// The most decimal digits that `int(text)` converts: `sys.get_int_max_str_digits()`. +pub(crate) const MAX_INT_DIGITS: usize = 4300; + +/// Returns whether Python's `str.isspace` holds for `character`. +pub(crate) fn is_space(character: char) -> bool { + matches!( + u32::from(character), + 0x09..=0x0D + | 0x1C..=0x20 + | 0x85 + | 0xA0 + | 0x1680 + | 0x2000..=0x200A + | 0x2028 + | 0x2029 + | 0x202F + | 0x205F + | 0x3000 + ) +} + +/// Returns whether `str.isprintable` holds for `character`, so that `repr` shows it as is. +pub(crate) fn is_printable(character: char) -> bool { + let code = u32::from(character); + if code < 0x80 { + return (0x20..0x7F).contains(&code); + } + let ranges = unicode::NON_PRINTABLE; + // The ranges are sorted and disjoint: find the last one that starts at or before `code`. + let index = ranges.partition_point(|&(start, _)| start <= code); + index == 0 || ranges[index - 1].1 < code +} + +/// Returns the value of a Unicode decimal digit, of category Nd, as `int` reads it. +pub(crate) fn decimal_value(character: char) -> Option { + let code = u32::from(character); + let starts = unicode::DECIMAL_STARTS; + // Every run of decimal digits is ten consecutive code points, from zero to nine. + let index = starts.partition_point(|&start| start <= code); + let start = *starts.get(index.checked_sub(1)?)?; + (code - start < 10).then_some(code - start) +} + +/// Splits `text` at runs of Python whitespace and drops empty fields, as `str.split()` does. +pub(crate) fn split(text: &str) -> impl Iterator { + text.split(is_space).filter(|field| !field.is_empty()) +} + +/// Strips Python whitespace from both ends of `text`, as `str.strip()` does. +pub(crate) fn strip(text: &str) -> &str { + text.trim_matches(is_space) +} + +/// Returns `repr(text)`: `text` in quotes, with backslashes, the quote, and the characters that +/// are not printable escaped. +pub(crate) fn repr(text: &str) -> String { + // Single quotes, unless the text holds single quotes and no double quotes. + let quote = if text.contains('\'') && !text.contains('"') { + '"' + } else { + '\'' + }; + let mut quoted = String::with_capacity(text.len() + 2); + quoted.push(quote); + for character in text.chars() { + let code = u32::from(character); + match character { + _ if character == quote || character == '\\' => { + quoted.push('\\'); + quoted.push(character); + } + '\t' => quoted.push_str("\\t"), + '\n' => quoted.push_str("\\n"), + '\r' => quoted.push_str("\\r"), + _ if code < 0x20 || code == 0x7F => { + let _ = write!(quoted, "\\x{code:02x}"); + } + _ if code < 0x7F || is_printable(character) => quoted.push(character), + _ if code <= 0xFF => { + let _ = write!(quoted, "\\x{code:02x}"); + } + _ if code <= 0xFFFF => { + let _ = write!(quoted, "\\u{code:04x}"); + } + _ => { + let _ = write!(quoted, "\\U{code:08x}"); + } + } + } + quoted.push(quote); + quoted +} + +/// Why `int(text)` refuses `text`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IntError { + /// The text is not an integer literal. + Invalid, + /// The literal has this many decimal digits, more than `int` converts (4300). + TooManyDigits(usize), +} + +impl IntError { + /// Returns the message of the `ValueError` with which `int(text)` refuses `text`. + pub fn message(&self, text: &str) -> String { + match self { + Self::Invalid => { + // `%.200R` keeps the first 200 characters of the representation. + let quoted: String = repr(text).chars().take(200).collect(); + format!("invalid literal for int() with base 10: {quoted}") + } + Self::TooManyDigits(digits) => too_many_digits_message(*digits), + } + } +} + +/// Returns the message of the `ValueError` that refuses a decimal literal of `digits` digits, +/// more than [`MAX_INT_DIGITS`]. +pub(crate) fn too_many_digits_message(digits: usize) -> String { + format!( + "Exceeds the limit ({MAX_INT_DIGITS} digits) for integer string conversion: value has \ + {digits} digits; use sys.set_int_max_str_digits() to increase the limit" + ) +} + +/// A Python `int` of any size. +/// +/// [`Display`](fmt::Display) gives its decimal form, as `str()` does. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Int { + /// Whether the integer is below zero; zero is never negative. + negative: bool, + /// The decimal digits of the magnitude, without leading zeros: `"0"` for zero. + digits: String, +} + +impl Int { + /// Zero. + pub fn zero() -> Self { + Self { + negative: false, + digits: "0".to_owned(), + } + } + + /// Parses `text` as `int(text)` does. + /// + /// Python first maps Unicode whitespace to spaces and Unicode decimal digits to ASCII + /// digits, and refuses any other character outside ASCII. It then accepts surrounding ASCII + /// whitespace, a sign, leading zeros, and single underscores between digits. + pub fn parse(text: &str) -> Result { + let (negative, digits) = parse_digits(text, 10)?; + Ok(Self::from_digits(negative, &digits)) + } + + /// Builds the integer of decimal `digits`, most significant first. + fn from_digits(negative: bool, digits: &[u8]) -> Self { + let significant = digits + .iter() + .position(|&digit| digit != 0) + .map_or(&[][..], |first| &digits[first..]); + if significant.is_empty() { + return Self::zero(); + } + Self { + negative, + digits: significant + .iter() + .map(|&digit| char::from(b'0' + digit)) + .collect(), + } + } + + /// Returns whether the integer is below zero. + pub fn is_negative(&self) -> bool { + self.negative + } + + /// Returns whether the integer is above zero. + pub fn is_positive(&self) -> bool { + !self.negative && self.digits != "0" + } + + /// Returns the integer as a `u32`, if it is one. + pub fn to_u32(&self) -> Option { + self.to_u64().and_then(|value| u32::try_from(value).ok()) + } + + /// Returns the integer as a `u64`, if it is one. + pub fn to_u64(&self) -> Option { + if self.negative { + return None; + } + self.digits.parse().ok() + } + + /// Returns the integer plus one. + #[must_use] + pub fn plus_one(&self) -> Self { + if self.negative { + // -n + 1 = -(n - 1), and -1 + 1 = 0. + let magnitude = decrement(&self.digits); + return Self { + negative: magnitude != "0", + digits: magnitude, + }; + } + Self { + negative: false, + digits: increment(&self.digits), + } + } +} + +impl From for Int { + fn from(value: u64) -> Self { + Self { + negative: false, + digits: value.to_string(), + } + } +} + +impl From for Int { + fn from(value: u32) -> Self { + Self::from(u64::from(value)) + } +} + +impl From for Int { + fn from(value: i64) -> Self { + Self { + negative: value < 0, + digits: value.unsigned_abs().to_string(), + } + } +} + +impl PartialOrd for Int { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Int { + fn cmp(&self, other: &Self) -> Ordering { + let magnitude = || { + self.digits + .len() + .cmp(&other.digits.len()) + .then_with(|| self.digits.cmp(&other.digits)) + }; + match (self.negative, other.negative) { + (false, false) => magnitude(), + (true, true) => magnitude().reverse(), + (false, true) => Ordering::Greater, + (true, false) => Ordering::Less, + } + } +} + +impl fmt::Display for Int { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.negative { + formatter.write_str("-")?; + } + formatter.write_str(&self.digits) + } +} + +impl fmt::Debug for Int { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, formatter) + } +} + +/// Adds one to a decimal magnitude. +fn increment(digits: &str) -> String { + let mut bytes = digits.as_bytes().to_vec(); + for byte in bytes.iter_mut().rev() { + if *byte == b'9' { + *byte = b'0'; + } else { + *byte += 1; + return bytes.into_iter().map(char::from).collect(); + } + } + std::iter::once('1') + .chain(bytes.into_iter().map(char::from)) + .collect() +} + +/// Subtracts one from a positive decimal magnitude. +fn decrement(digits: &str) -> String { + let mut bytes = digits.as_bytes().to_vec(); + for byte in bytes.iter_mut().rev() { + if *byte == b'0' { + *byte = b'9'; + } else { + *byte -= 1; + break; + } + } + let text: String = bytes.into_iter().map(char::from).collect(); + match text.trim_start_matches('0') { + "" => "0".to_owned(), + trimmed => trimmed.to_owned(), + } +} + +/// Parses `text` as `int(text, base)` does for base 10 or 16, returning its sign and its digit +/// values, most significant first and with any leading zeros. See [`Int::parse`]; base 16 also +/// accepts a `0x` prefix, which a single underscore may follow. +pub(crate) fn parse_digits(text: &str, base: u32) -> Result<(bool, Vec), IntError> { + let mut ascii = Vec::with_capacity(text.len()); + for character in text.chars() { + let code = u32::from(character); + if code < 0x7F { + ascii.push(code as u8); + } else if is_space(character) { + ascii.push(b' '); + } else if let Some(value) = decimal_value(character) { + ascii.push(b'0' + value as u8); + } else { + return Err(IntError::Invalid); + } + } + let is_ascii_space = |byte: u8| matches!(byte, b' ' | b'\t' | b'\n' | 0x0B | 0x0C | b'\r'); + let mut index = 0; + while ascii.get(index).copied().is_some_and(is_ascii_space) { + index += 1; + } + let mut negative = false; + match ascii.get(index) { + Some(b'+') => index += 1, + Some(b'-') => { + negative = true; + index += 1; + } + _ => {} + } + if base == 16 + && ascii.get(index) == Some(&b'0') + && matches!(ascii.get(index + 1), Some(b'x' | b'X')) + { + index += 2; + if ascii.get(index) == Some(&b'_') { + index += 1; + } + } + if ascii.get(index) == Some(&b'_') { + return Err(IntError::Invalid); + } + let mut digits = Vec::new(); + let mut previous = 0; + while let Some(&byte) = ascii.get(index) { + if byte == b'_' { + if previous == b'_' { + return Err(IntError::Invalid); + } + } else { + match char::from(byte).to_digit(base) { + Some(value) => digits.push(value as u8), + None => break, + } + } + previous = byte; + index += 1; + } + if previous == b'_' || digits.is_empty() { + return Err(IntError::Invalid); + } + while ascii.get(index).copied().is_some_and(is_ascii_space) { + index += 1; + } + // Anything else after the digits, including a NUL, makes the literal invalid. + if index != ascii.len() { + return Err(IntError::Invalid); + } + if base == 10 && digits.len() > MAX_INT_DIGITS { + return Err(IntError::TooManyDigits(digits.len())); + } + Ok((negative, digits)) +} + +/// Parses `text` as `float(text)` does, or returns `None` where it raises `ValueError`. +/// +/// Python maps Unicode whitespace to spaces and Unicode decimal digits to ASCII digits, and +/// refuses any other character outside ASCII. Single underscores may separate digits. It then +/// strips surrounding ASCII whitespace and reads a decimal number with an optional exponent, or +/// `inf`, `infinity`, or `nan` in any case, each with an optional sign; numbers beyond the range +/// of a float become infinities. +#[cfg(feature = "cli")] +pub(crate) fn parse_float(text: &str) -> Option { + let mut ascii = String::with_capacity(text.len()); + for character in text.chars() { + if character.is_ascii() { + ascii.push(character); + } else if is_space(character) { + ascii.push(' '); + } else { + ascii.push(char::from(b'0' + decimal_value(character)? as u8)); + } + } + let mut digits = String::with_capacity(ascii.len()); + let mut previous = '\0'; + for character in ascii.chars() { + if character == '_' { + if !previous.is_ascii_digit() { + return None; + } + } else { + if previous == '_' && !character.is_ascii_digit() { + return None; + } + digits.push(character); + } + previous = character; + } + if previous == '_' { + return None; + } + let number = digits.trim_matches(|c| matches!(c, ' ' | '\t' | '\n' | '\x0b' | '\x0c' | '\r')); + // Rust reads the grammar of `PyOS_string_to_double`: digits with an optional fraction and + // exponent, or `inf`, `infinity`, and `nan` in any case, after an optional sign. + number.parse().ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn python_whitespace_includes_the_ascii_separators() { + for space in [ + '\t', '\n', '\x0b', '\x0c', '\r', '\x1c', '\x1f', ' ', '\u{85}', '\u{3000}', + ] { + assert!(is_space(space), "{space:?}"); + } + for other in ['a', '\0', '\u{200b}', '\u{feff}', '\u{180e}'] { + assert!(!is_space(other), "{other:?}"); + } + assert_eq!( + split(" a\x1cb c\u{3000}").collect::>(), + ["a", "b", "c"] + ); + assert_eq!(strip("\x1f a b \u{85}"), "a b"); + } + + #[test] + fn repr_quotes_and_escapes_as_python() { + for (text, expected) in [ + ("abc", "'abc'"), + ("it's", "\"it's\""), + ("say \"hi\"", "'say \"hi\"'"), + ("both ' and \"", "'both \\' and \"'"), + ("a\\b", "'a\\\\b'"), + ("\t\n\r\0\x7f\x1b", "'\\t\\n\\r\\x00\\x7f\\x1b'"), + ("é\u{a0}\u{ad}", "'é\\xa0\\xad'"), + ("\u{2028}\u{200b}\u{378}", "'\\u2028\\u200b\\u0378'"), + ("\u{1f600}\u{e0001}", "'\u{1f600}\\U000e0001'"), + ] { + assert_eq!(repr(text), expected, "{text:?}"); + } + } + + #[test] + fn integers_parse_as_python_int() { + for (text, expected) in [ + ("007", "7"), + ("+5", "5"), + ("-5", "-5"), + (" 5 ", "5"), + ("5_000", "5000"), + ("\u{2003}5\u{3000}", "5"), + ("\u{ff15}", "5"), + ("\u{664}\u{662}", "42"), + ("4\u{660}2", "402"), + ("-0", "0"), + ("\x0b42\x0c", "42"), + ] { + assert_eq!(Int::parse(text).unwrap().to_string(), expected, "{text:?}"); + } + for text in [ + "", + " ", + "+", + "-", + "+-5", + "_5", + "5_", + "5__0", + "0x5", + "0d5", + "1e3", + "5.0", + "4\u{200b}2", + "\x1c42\x1f", + "1\x002", + "\u{7f}1", + "5 5", + ] { + assert_eq!(Int::parse(text), Err(IntError::Invalid), "{text:?}"); + } + assert!(Int::parse(&"1".repeat(MAX_INT_DIGITS)).is_ok()); + // Leading zeros count as digits. + assert_eq!( + Int::parse(&format!("-{}1", "0".repeat(MAX_INT_DIGITS))), + Err(IntError::TooManyDigits(MAX_INT_DIGITS + 1)) + ); + assert_eq!( + IntError::TooManyDigits(4301).message(""), + "Exceeds the limit (4300 digits) for integer string conversion: value has 4301 \ + digits; use sys.set_int_max_str_digits() to increase the limit" + ); + assert_eq!( + IntError::Invalid.message("x'y"), + "invalid literal for int() with base 10: \"x'y\"" + ); + } + + #[test] + fn integers_compare_and_increment_at_any_size() { + let parse = |text: &str| Int::parse(text).unwrap(); + assert_eq!( + parse("99999999999999999999").plus_one().to_string(), + "100000000000000000000" + ); + assert_eq!(parse("-1").plus_one(), Int::zero()); + assert_eq!(parse("-10").plus_one().to_string(), "-9"); + assert_eq!(parse("4294967295").to_u32(), Some(u32::MAX)); + assert_eq!(parse("4294967296").to_u32(), None); + assert_eq!(parse("-1").to_u64(), None); + assert!(parse("-3") < parse("-2")); + assert!(parse("-2") < Int::zero()); + assert!(parse("10") > parse("9")); + assert!(!Int::zero().is_positive()); + assert!(!Int::zero().is_negative()); + assert_eq!(Int::from(-7_i64).to_string(), "-7"); + } + + #[test] + #[cfg(feature = "cli")] + fn floats_parse_as_python_float() { + for (text, expected) in [ + ("60", 60.0), + (" 0.25 ", 0.25), + ("1_000.5", 1000.5), + ("1e1_0", 1e10), + (".5", 0.5), + ("5.", 5.0), + ("1.e2", 100.0), + ("+1E-2", 0.01), + ("\u{664}\u{662}", 42.0), + ("\u{3000}7\u{85}", 7.0), + ("1e400", f64::INFINITY), + ("-Infinity", f64::NEG_INFINITY), + ("iNf", f64::INFINITY), + ] { + assert_eq!(parse_float(text), Some(expected), "{text:?}"); + } + assert!(parse_float("-nan").is_some_and(f64::is_nan)); + for text in [ + "", + " ", + ".", + "e5", + "1e", + "_1", + "1_", + "1__0", + "1_.5", + "1._5", + "inf_", + "0x10", + "1 2", + "nan(1)", + "in", + "\u{200b}1", + "1\0", + "+", + "--1", + ] { + assert_eq!(parse_float(text), None, "{text:?}"); + } + } + + #[test] + fn unicode_tables_classify_digits_and_printable_characters() { + assert_eq!(decimal_value('7'), Some(7)); + assert_eq!(decimal_value('\u{663}'), Some(3)); + assert_eq!(decimal_value('\u{ff19}'), Some(9)); + assert_eq!(decimal_value('\u{2155}'), None); + assert_eq!(decimal_value('a'), None); + assert!(is_printable('é')); + assert!(!is_printable('\u{a0}')); + assert!(!is_printable('\u{10ffff}')); + } +} diff --git a/nvx-rs/src/host/python/dumps.rs b/nvx-rs/src/host/python/dumps.rs new file mode 100644 index 000000000..190fb6bc8 --- /dev/null +++ b/nvx-rs/src/host/python/dumps.rs @@ -0,0 +1,212 @@ +//! `json.dumps(value, indent=2, sort_keys=True)`, with which `nvx.py` writes its JSON files. +//! +//! [`Value`] holds what the ports write: Python's `None`, `bool`, `int` of any size, `str`, and +//! lists and dicts of them, and [`dumps`] writes it as Python does: ASCII text with one item or +//! member per line, each line indented by two spaces per level and ended by `,` where another +//! follows, members sorted by name and written as `"name": value`, and empty containers as `[]` +//! and `{}`. Like `ensure_ascii`, it escapes every character outside the printable ASCII range +//! `' '..='~'`, so `DEL` and anything beyond ASCII become `\uXXXX`, with surrogate pairs for +//! characters beyond the Basic Multilingual Plane. + +use std::collections::BTreeMap; +use std::fmt::Write as _; + +use super::builtins::Int; + +/// A value that [`dumps`] writes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum Value { + /// `None`, which JSON writes as `null`. + Null, + /// `True` or `False`. + Bool(bool), + /// An `int` of any size. + Int(Int), + /// A `str`. + Str(String), + /// A `list`. + List(Vec), + /// A `dict` with string keys, which `sort_keys` writes in code point order, as a + /// `BTreeMap` iterates UTF-8 strings. + Dict(BTreeMap), +} + +impl From<&str> for Value { + fn from(text: &str) -> Self { + Self::Str(text.to_owned()) + } +} + +impl From for Value { + fn from(text: String) -> Self { + Self::Str(text) + } +} + +impl From for Value { + fn from(value: u64) -> Self { + Self::Int(Int::from(value)) + } +} + +impl From for Value { + fn from(value: u32) -> Self { + Self::Int(Int::from(value)) + } +} + +impl From for Value { + fn from(value: i64) -> Self { + Self::Int(Int::from(value)) + } +} + +impl From for Value { + fn from(value: Int) -> Self { + Self::Int(value) + } +} + +impl> From> for Value { + fn from(value: Option) -> Self { + value.map_or(Self::Null, Into::into) + } +} + +/// Builds a [`Value::Dict`] from its members. +pub(crate) fn dict(members: [(&str, Value); N]) -> Value { + Value::Dict( + members + .into_iter() + .map(|(name, value)| (name.to_owned(), value)) + .collect(), + ) +} + +/// Returns `json.dumps(value, indent=2, sort_keys=True)`, without a trailing newline. +pub(crate) fn dumps(value: &Value) -> String { + let mut output = String::new(); + write_value(&mut output, value, 0); + output +} + +/// Appends `value` at indentation `level`, whose first line the caller has already indented. +fn write_value(output: &mut String, value: &Value, level: usize) { + match value { + Value::Null => output.push_str("null"), + Value::Bool(true) => output.push_str("true"), + Value::Bool(false) => output.push_str("false"), + Value::Int(value) => { + let _ = write!(output, "{value}"); + } + Value::Str(text) => write_string(output, text), + Value::List(items) if items.is_empty() => output.push_str("[]"), + Value::List(items) => { + output.push('['); + for (index, item) in items.iter().enumerate() { + if index > 0 { + output.push(','); + } + newline(output, level + 1); + write_value(output, item, level + 1); + } + newline(output, level); + output.push(']'); + } + Value::Dict(members) if members.is_empty() => output.push_str("{}"), + Value::Dict(members) => { + output.push('{'); + for (index, (name, member)) in members.iter().enumerate() { + if index > 0 { + output.push(','); + } + newline(output, level + 1); + write_string(output, name); + output.push_str(": "); + write_value(output, member, level + 1); + } + newline(output, level); + output.push('}'); + } + } +} + +/// Starts a new line indented to `level`. +fn newline(output: &mut String, level: usize) { + output.push('\n'); + for _ in 0..level { + output.push_str(" "); + } +} + +/// Appends `text` as `json.dumps` quotes it with `ensure_ascii`. +fn write_string(output: &mut String, text: &str) { + output.push('"'); + for character in text.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + '\u{8}' => output.push_str("\\b"), + '\u{c}' => output.push_str("\\f"), + ' '..='~' => output.push(character), + _ => { + let mut units = [0u16; 2]; + for unit in character.encode_utf16(&mut units) { + let _ = write!(output, "\\u{unit:04x}"); + } + } + } + } + output.push('"'); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn values_are_written_as_json_dumps_writes_them() { + let value = dict([ + ("zeta", Value::List(vec![Value::from(1u64), Value::Null])), + ("alpha", Value::Bool(true)), + ("empty", Value::List(Vec::new())), + ( + "nested", + dict([("b", Value::from("x")), ("a", Value::Bool(false))]), + ), + ("none", Value::Dict(BTreeMap::new())), + ( + "big", + Value::Int(Int::parse("-123456789012345678901234567890").unwrap()), + ), + ]); + assert_eq!( + dumps(&value), + "{\n \"alpha\": true,\n \"big\": -123456789012345678901234567890,\n \ + \"empty\": [],\n \"nested\": {\n \"a\": false,\n \"b\": \"x\"\n },\n \ + \"none\": {},\n \"zeta\": [\n 1,\n null\n ]\n}" + ); + } + + #[test] + fn strings_are_escaped_to_ascii() { + assert_eq!( + dumps(&Value::from( + "a\"b\\c\n\r\t\u{8}\u{c}\u{0}\u{1f} ~\u{7f}é\u{20ac}\u{1f600}" + )), + "\"a\\\"b\\\\c\\n\\r\\t\\b\\f\\u0000\\u001f ~\\u007f\\u00e9\\u20ac\\ud83d\\ude00\"" + ); + } + + #[test] + fn keys_sort_by_code_point() { + let value = dict([("é", Value::Null), ("z", Value::Null), ("Z", Value::Null)]); + assert_eq!( + dumps(&value), + "{\n \"Z\": null,\n \"z\": null,\n \"\\u00e9\": null\n}" + ); + } +} diff --git a/nvx-rs/src/host/python/fs.rs b/nvx-rs/src/host/python/fs.rs new file mode 100644 index 000000000..e6be18b36 --- /dev/null +++ b/nvx-rs/src/host/python/fs.rs @@ -0,0 +1,656 @@ +//! The filesystem queries of Python's `os`, `os.path`, and `pathlib.Path`, as CPython 3.14 +//! performs them on Linux and Windows. +//! +//! - [`cwd`]: the current directory, as `Path.cwd()` gives it. +//! - [`is_file`], [`is_dir`], [`is_symlink`], and [`exists`]: `os.path.isfile`, `isdir`, `islink`, +//! and `exists`, which follow symbolic links, except `islink`, and are false on any error. On +//! Windows, only `IO_REPARSE_TAG_SYMLINK` reparse points are links, not junctions. +//! - [`stat`], [`lstat`], and [`fstat`]: the identity and owner that `os.stat` and `os.lstat` +//! report for a path, and the identity, type, and link count that `os.fstat` reports for an +//! open file. On Windows, the identity is the 64-bit volume serial number and the 128-bit file +//! ID of `FILE_ID_INFO`, as Python 3.12 and later report them, which also tell apart the files +//! of ReFS volumes. [`lexists`] is `os.path.lexists`. +//! - [`resolve`]: `Path.resolve()`, which follows `os.path.realpath`: it resolves symbolic links +//! component by component on POSIX, keeping the components that do not exist, and asks Windows +//! for the final path of the longest existing prefix, dropping the `\\?\` prefix again. +//! - [`os_error_message`]: the text of an `OSError`. +//! +//! Paths must be valid Unicode: a link target or working directory that is not is an error, +//! where Python would decode it into surrogates. + +use std::io; +use std::path::Path; + +use super::builtins::repr; +use super::not_unicode; +use super::path::PurePath; + +/// The fields of `os.stat(path)` that NVX reads. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Stat { + /// `st_dev`: the device, or on Windows the volume serial number. + pub(crate) device: u64, + /// `st_ino`: the inode, or on Windows the file ID. + pub(crate) inode: u128, + /// `st_uid`: the owner, or zero on Windows. + pub(crate) uid: u32, + /// `st_gid`: the group, or zero on Windows. + pub(crate) gid: u32, +} + +impl Stat { + /// Returns `(st_dev, st_ino)`, the identity of the file. + pub(crate) fn identity(&self) -> (u64, u128) { + (self.device, self.inode) + } +} + +/// Returns `Path.cwd()`. +pub(crate) fn cwd() -> io::Result { + let directory = std::env::current_dir()?; + let text = directory + .to_str() + .ok_or_else(|| not_unicode("the current directory"))?; + Ok(PurePath::native(text)) +} + +/// Returns `os.path.isfile(path)`: whether `path` is a regular file, following links. +pub(crate) fn is_file(path: &Path) -> bool { + imp::is_file(path) +} + +/// Returns `os.path.isdir(path)`: whether `path` is a directory, following links. +pub(crate) fn is_dir(path: &Path) -> bool { + imp::is_dir(path) +} + +/// Returns `os.path.islink(path)`: whether `path` is a symbolic link. +pub(crate) fn is_symlink(path: &Path) -> bool { + imp::is_symlink(path) +} + +/// Returns `os.path.exists(path)`: whether `path` exists, following links. +pub(crate) fn exists(path: &Path) -> bool { + imp::exists(path) +} + +/// Returns `os.stat(path)`, following links. +pub(crate) fn stat(path: &Path) -> io::Result { + imp::stat(path) +} + +/// Returns `os.lstat(path)`, which does not follow a link that `path` names. +#[cfg(feature = "cli")] +pub(crate) fn lstat(path: &Path) -> io::Result { + imp::lstat(path) +} + +/// Returns `os.path.lexists(path)`: whether `os.lstat(path)` succeeds, so a link counts even if +/// its target is missing. +#[cfg(feature = "cli")] +pub(crate) fn lexists(path: &Path) -> bool { + lstat(path).is_ok() +} + +/// The fields of `os.fstat` on an open file that NVX reads. +#[cfg(all(feature = "cli", any(unix, windows)))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct FileStat { + /// `st_dev` and `st_ino`, the identity that [`Stat::identity`] gives for the same file. + pub(crate) identity: (u64, u128), + /// Whether `st_mode` is a regular file. + pub(crate) regular: bool, + /// `st_nlink`: the number of names that link the file. + pub(crate) links: u64, + /// Whether the file is a reparse point, from `st_file_attributes` on Windows; never on POSIX. + pub(crate) reparse_point: bool, +} + +/// Returns `os.fstat` of an open file, as CPython's `_Py_fstat_noraise` computes it. +#[cfg(all(feature = "cli", any(unix, windows)))] +pub(crate) fn fstat(file: &std::fs::File) -> io::Result { + imp::fstat(file) +} + +/// Returns `path.resolve()`: the absolute path of `path`, with symbolic links resolved and `..` +/// components removed, as `os.path.realpath(path)` gives it. +pub(crate) fn resolve(path: &PurePath) -> io::Result { + imp::realpath(path.as_str()).map(|resolved| PurePath::native(&resolved)) +} + +/// Returns `str(error)` for an `OSError` that an operation on `filename` raised: `[Errno N] +/// text: 'filename'` on POSIX, and `[WinError N] text: 'filename'` on Windows, with the text +/// that the system gives for the error. +pub(crate) fn os_error_message(error: &io::Error, filename: Option<&str>) -> String { + let Some(code) = error.raw_os_error() else { + return error.to_string(); + }; + // Rust appends ` (os error N)` to the system's text, and Python drops the trailing period + // and whitespace of Windows messages. + let text = error.to_string(); + let text = text + .strip_suffix(&format!(" (os error {code})")) + .unwrap_or(&text); + let (label, text) = if cfg!(windows) { + ( + "WinError", + text.trim_end_matches(|c: char| c <= ' ' || c == '.'), + ) + } else { + ("Errno", text) + }; + match filename { + Some(filename) => format!("[{label} {code}] {text}: {}", repr(filename)), + None => format!("[{label} {code}] {text}"), + } +} + +#[cfg(unix)] +/// The POSIX implementation, on the standard library. +mod imp { + use std::fs; + use std::io; + use std::os::unix::fs::MetadataExt; + use std::path::Path; + + use super::{Stat, not_unicode}; + + /// See [`super::is_file`]. + pub(super) fn is_file(path: &Path) -> bool { + fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) + } + + /// See [`super::is_dir`]. + pub(super) fn is_dir(path: &Path) -> bool { + fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) + } + + /// See [`super::is_symlink`]. + pub(super) fn is_symlink(path: &Path) -> bool { + fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink()) + } + + /// See [`super::exists`]. + pub(super) fn exists(path: &Path) -> bool { + fs::metadata(path).is_ok() + } + + /// See [`super::stat`]. + pub(super) fn stat(path: &Path) -> io::Result { + status(fs::metadata(path)?) + } + + /// See [`super::lstat`]. + #[cfg(feature = "cli")] + pub(super) fn lstat(path: &Path) -> io::Result { + status(fs::symlink_metadata(path)?) + } + + /// Returns the fields of [`Stat`] that `metadata` holds. + fn status(metadata: fs::Metadata) -> io::Result { + Ok(Stat { + device: metadata.dev(), + inode: u128::from(metadata.ino()), + uid: metadata.uid(), + gid: metadata.gid(), + }) + } + + /// See [`super::fstat`]. + #[cfg(feature = "cli")] + pub(super) fn fstat(file: &fs::File) -> io::Result { + let metadata = file.metadata()?; + Ok(super::FileStat { + identity: (metadata.dev(), u128::from(metadata.ino())), + regular: metadata.file_type().is_file(), + links: metadata.nlink(), + reparse_point: false, + }) + } + + /// Returns `posixpath.realpath(path)` in its non-strict mode: each component is resolved in + /// turn, a symbolic link is replaced by its target, and a component that cannot be examined + /// is kept as it is. A symbolic-link loop stops resolution at the link. + pub(super) fn realpath(path: &str) -> io::Result { + // The unresolved components, last first. `None` marks the end of a link's target, and + // is followed by the link's own path. + let mut rest: Vec> = path + .split('/') + .rev() + .map(|part| Some(part.to_owned())) + .collect(); + let mut part_count = rest.len(); + let mut resolved = if path.starts_with('/') { + "/".to_owned() + } else { + let directory = std::env::current_dir()?; + directory + .to_str() + .ok_or_else(|| not_unicode("the current directory"))? + .to_owned() + }; + // Each link that has been seen, with its resolved target once known. + let mut seen: std::collections::HashMap> = + std::collections::HashMap::new(); + while part_count > 0 { + let Some(name) = rest.pop() else { + break; + }; + let Some(name) = name else { + // The target of a link is resolved. + if let Some(Some(link)) = rest.pop() { + seen.insert(link, Some(resolved.clone())); + } + continue; + }; + part_count -= 1; + if name.is_empty() || name == "." { + continue; + } + if name == ".." { + let end = resolved.rfind('/').unwrap_or(0); + resolved.truncate(end); + if resolved.is_empty() { + resolved.push('/'); + } + continue; + } + let candidate = if resolved == "/" { + format!("/{name}") + } else { + format!("{resolved}/{name}") + }; + if candidate.contains('\0') { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "embedded null byte", + )); + } + let target = match fs::symlink_metadata(&candidate) { + Ok(metadata) if !metadata.file_type().is_symlink() => { + resolved = candidate; + continue; + } + Ok(_) => match seen.get(&candidate) { + Some(Some(target)) => { + resolved = target.clone(); + continue; + } + // A link whose resolution is in progress: a loop. + Some(None) => { + resolved = candidate; + continue; + } + None => fs::read_link(&candidate), + }, + Err(error) => Err(error), + }; + let Ok(target) = target else { + // Errors are ignored: the component is kept as it is. + resolved = candidate; + continue; + }; + let target = target + .into_os_string() + .into_string() + .map_err(|_| not_unicode("a symbolic link's target"))?; + if target.starts_with('/') { + "/".clone_into(&mut resolved); + } + seen.insert(candidate.clone(), None); + rest.push(Some(candidate)); + rest.push(None); + let parts: Vec<&str> = target.split('/').collect(); + part_count += parts.len(); + rest.extend(parts.into_iter().rev().map(|part| Some(part.to_owned()))); + } + Ok(resolved) + } +} + +#[cfg(windows)] +/// The Windows implementation, on the Win32 calls that CPython makes. +mod imp { + use std::io; + use std::path::Path; + + use super::super::path::{Flavor, windows_normpath}; + use super::super::windows::{self as win32, StatMode}; + use super::Stat; + + /// See [`super::is_file`]. + pub(super) fn is_file(path: &Path) -> bool { + win32::stat(path, true).is_ok_and(|status| status.mode == StatMode::Regular) + } + + /// See [`super::is_dir`]. + pub(super) fn is_dir(path: &Path) -> bool { + win32::stat(path, true).is_ok_and(|status| status.mode == StatMode::Directory) + } + + /// See [`super::is_symlink`]. + pub(super) fn is_symlink(path: &Path) -> bool { + win32::stat(path, false).is_ok_and(|status| status.mode == StatMode::Link) + } + + /// See [`super::exists`]. + pub(super) fn exists(path: &Path) -> bool { + win32::stat(path, true).is_ok() + } + + /// See [`super::stat`]. + pub(super) fn stat(path: &Path) -> io::Result { + let status = win32::stat(path, true)?; + Ok(Stat { + device: status.device, + inode: status.inode, + uid: 0, + gid: 0, + }) + } + + /// See [`super::lstat`]. + #[cfg(feature = "cli")] + pub(super) fn lstat(path: &Path) -> io::Result { + let status = win32::stat(path, false)?; + Ok(Stat { + device: status.device, + inode: status.inode, + uid: 0, + gid: 0, + }) + } + + /// See [`super::fstat`]. + #[cfg(feature = "cli")] + pub(super) fn fstat(file: &std::fs::File) -> io::Result { + let status = win32::fstat(file)?; + Ok(super::FileStat { + identity: (status.device, status.inode), + regular: status.mode == StatMode::Regular, + links: u64::from(status.links), + reparse_point: status.reparse_point, + }) + } + + /// Returns `ntpath.normcase(path)`: lowercase, with `\` separators. + fn normcase(path: &str) -> String { + path.replace('/', "\\").to_lowercase() + } + + /// Returns `ntpath.dirname(path)`. + fn dirname(path: &str) -> String { + Flavor::Windows.split(path).0 + } + + /// Returns whether `error` is one of the Win32 errors in `codes`. + fn is_error(error: &io::Error, codes: &[i32]) -> bool { + error + .raw_os_error() + .is_some_and(|code| codes.contains(&code)) + } + + /// Follows the links at `path` as `ntpath._readlink_deep` does, stopping at a path that is + /// not a link, at a loop, or at an error that ends resolution. + fn readlink_deep(path: &str) -> io::Result { + const STOP: [i32; 12] = [1, 2, 3, 5, 21, 32, 50, 67, 87, 4390, 4392, 4393]; + let mut path = path.to_owned(); + let mut seen = std::collections::HashSet::new(); + while seen.insert(normcase(&path)) { + let old = path.clone(); + match win32::read_link(Path::new(&path)) { + Ok(Some(target)) => { + path = target; + if !Flavor::Windows.is_absolute(&path) { + // A relative target resolves against the link's own directory, but + // only a symbolic link has one. + if !super::is_symlink(Path::new(&old)) { + path = old; + break; + } + path = windows_normpath(&Flavor::Windows.join(&dirname(&old), &path)); + } + } + // A reparse point that is not a link. + Ok(None) => break, + Err(error) if is_error(&error, &STOP) => break, + Err(error) => return Err(error), + } + } + Ok(path) + } + + /// Resolves as much of `path` as exists and appends the rest, as + /// `ntpath._getfinalpathname_nonstrict` does. + fn final_path_name_nonstrict(path: &str) -> io::Result { + const STOP: [i32; 16] = [ + 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1005, 1920, 1921, + ]; + const BY_NAME: [i32; 7] = [1, 5, 32, 50, 87, 1920, 1921]; + let mut path = path.to_owned(); + let mut tail = String::new(); + while !path.is_empty() { + let error = match win32::final_path_name(Path::new(&path)) { + Ok(resolved) => { + return Ok(if tail.is_empty() { + resolved + } else { + Flavor::Windows.join(&resolved, &tail) + }); + } + Err(error) if is_error(&error, &STOP) => error, + Err(error) => return Err(error), + }; + // Windows could not resolve the path: follow its links ourselves. + match readlink_deep(&path) { + Ok(target) if target != path => { + return Ok(if tail.is_empty() { + target + } else { + Flavor::Windows.join(&target, &tail) + }); + } + _ => {} + } + let (head, name) = if is_error(&error, &BY_NAME) { + // Read the real name of the file without opening it. + match win32::find_first_file_name(Path::new(&path)) { + Ok(name) => (Flavor::Windows.split(&path).0, name), + Err(_) => Flavor::Windows.split(&path), + } + } else { + Flavor::Windows.split(&path) + }; + path = head; + if !path.is_empty() && name.is_empty() { + return Ok(format!("{path}{tail}")); + } + tail = if tail.is_empty() { + name + } else { + Flavor::Windows.join(&name, &tail) + }; + } + Ok(tail) + } + + /// Returns `ntpath.realpath(path)` in its non-strict mode. + pub(super) fn realpath(path: &str) -> io::Result { + const PREFIX: &str = "\\\\?\\"; + const UNC_PREFIX: &str = "\\\\?\\UNC\\"; + let mut path = windows_normpath(path); + if normcase(&path) == "nul" { + return Ok("\\\\.\\NUL".to_owned()); + } + let had_prefix = path.starts_with(PREFIX); + if !had_prefix && !Flavor::Windows.is_absolute(&path) { + let directory = std::env::current_dir()?; + let directory = directory + .to_str() + .ok_or_else(|| super::not_unicode("the current directory"))?; + path = Flavor::Windows.join(directory, &path); + } + if path.contains('\0') { + // Python cannot pass the path to Windows and returns it as it is. + return Ok(windows_normpath(&path)); + } + let initial_error = match win32::final_path_name(Path::new(&path)) { + Ok(resolved) => { + path = resolved; + None + } + Err(error) => { + let code = error.raw_os_error(); + path = final_path_name_nonstrict(&path)?; + code + } + }; + if !had_prefix && path.starts_with(PREFIX) { + let stripped = match path.strip_prefix(UNC_PREFIX) { + Some(rest) => format!("\\\\{rest}"), + None => path[PREFIX.len()..].to_owned(), + }; + // Drop the prefix if the path without it resolves to the same path, or if neither + // exists for the same reason. + match win32::final_path_name(Path::new(&stripped)) { + Ok(resolved) if resolved == path => path = stripped, + Ok(_) => {} + Err(error) if error.raw_os_error() == initial_error.or(Some(0)) => { + path = stripped; + } + Err(_) => {} + } + } + Ok(path) + } +} + +#[cfg(not(any(unix, windows)))] +/// Hosts other than POSIX and Windows have no filesystem queries. +mod imp { + use std::io; + use std::path::Path; + + use super::Stat; + + fn unsupported() -> io::Error { + io::Error::new(io::ErrorKind::Unsupported, "unsupported host") + } + + pub(super) fn is_file(_path: &Path) -> bool { + false + } + + pub(super) fn is_dir(_path: &Path) -> bool { + false + } + + pub(super) fn is_symlink(_path: &Path) -> bool { + false + } + + pub(super) fn exists(_path: &Path) -> bool { + false + } + + pub(super) fn stat(_path: &Path) -> io::Result { + Err(unsupported()) + } + + #[cfg(feature = "cli")] + pub(super) fn lstat(_path: &Path) -> io::Result { + Err(unsupported()) + } + + pub(super) fn realpath(_path: &str) -> io::Result { + Err(unsupported()) + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::*; + + #[test] + fn queries_follow_links_except_islink() { + let directory = tempfile::tempdir().unwrap(); + let root = directory.path(); + let file = root.join("file"); + let folder = root.join("folder"); + fs::write(&file, b"x").unwrap(); + fs::create_dir(&folder).unwrap(); + assert!(is_file(&file) && !is_dir(&file) && !is_symlink(&file)); + assert!(is_dir(&folder) && !is_file(&folder) && !is_symlink(&folder)); + assert!(!exists(&root.join("missing")) && !is_file(&root.join("missing"))); + assert!(!is_file(Path::new("nul\0byte"))); + let link = root.join("link"); + #[cfg(unix)] + let linked = std::os::unix::fs::symlink(&file, &link); + #[cfg(windows)] + let linked = std::os::windows::fs::symlink_file(&file, &link); + if linked.is_ok() { + assert!(is_symlink(&link) && is_file(&link)); + assert_eq!( + stat(&link).unwrap().identity(), + stat(&file).unwrap().identity() + ); + } + assert_ne!( + stat(&file).unwrap().identity(), + stat(&folder).unwrap().identity() + ); + } + + #[test] + fn hard_links_share_an_identity() { + let directory = tempfile::tempdir().unwrap(); + let file = directory.path().join("file"); + let link = directory.path().join("hard"); + fs::write(&file, b"x").unwrap(); + fs::hard_link(&file, &link).unwrap(); + assert_eq!( + stat(&file).unwrap().identity(), + stat(&link).unwrap().identity() + ); + } + + #[test] + fn resolve_keeps_missing_components_and_follows_links() { + let directory = tempfile::tempdir().unwrap(); + let root = resolve(&PurePath::native(directory.path().to_str().unwrap())).unwrap(); + assert!(root.is_absolute()); + let target = root.join("target"); + fs::create_dir(target.as_str()).unwrap(); + assert_eq!(resolve(&root.join("target")).unwrap(), target); + assert_eq!( + resolve(&root.join("missing/deeper")).unwrap(), + root.join("missing").join("deeper") + ); + assert_eq!(resolve(&root.join("target/../target")).unwrap(), target); + let link = root.join("link"); + #[cfg(unix)] + let linked = std::os::unix::fs::symlink(target.as_str(), link.as_str()); + #[cfg(windows)] + let linked = std::os::windows::fs::symlink_dir(target.as_str(), link.as_str()); + if linked.is_ok() { + assert_eq!(resolve(&link).unwrap(), target); + assert_eq!(resolve(&link.join("child")).unwrap(), target.join("child")); + } + } + + #[test] + fn os_errors_read_as_python_formats_them() { + let error = fs::metadata("definitely/missing/path").unwrap_err(); + let message = os_error_message(&error, Some("missing")); + let label = if cfg!(windows) { + "[WinError 3]" + } else { + "[Errno 2]" + }; + assert!(message.starts_with(label), "{message}"); + assert!(message.ends_with(": 'missing'"), "{message}"); + assert!(!message.contains("os error"), "{message}"); + } +} diff --git a/nvx-rs/src/host/python/golden.rs b/nvx-rs/src/host/python/golden.rs new file mode 100644 index 000000000..fafee68de --- /dev/null +++ b/nvx-rs/src/host/python/golden.rs @@ -0,0 +1,314 @@ +//! Checks the Python semantics against `tests/fixtures/python.json`, which the retired +//! `scripts/nvx_rs_fixtures.py` recorded from Python, and the helpers of the other golden tests. + +use std::fmt::Debug; + +use serde_json::{Value, json}; + +use super::builtins::{Int, is_space, repr, split, strip}; +use super::json::{Document, Loads, Node, NodeId, ReadError, read, syntax_error}; +use super::path::{Flavor, PurePath, windows_normpath}; +use super::text::{PyText, decode_utf8}; +use super::unicode::{DECIMAL_STARTS, NON_PRINTABLE}; +use super::uuid::parse_uuid; + +/// Returns the fixture `name` of `tests/fixtures`. +pub(crate) fn fixture(name: &str) -> Value { + let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR")); + let text = std::fs::read_to_string(&path).unwrap_or_else(|error| panic!("{path}: {error}")); + serde_json::from_str(&text).unwrap_or_else(|error| panic!("{path}: {error}")) +} + +/// Returns the cases of a fixture's section. +pub(crate) fn cases<'a>(fixture: &'a Value, section: &str) -> &'a [Value] { + let cases = fixture[section] + .as_array() + .unwrap_or_else(|| panic!("the fixture has no section {section}")); + assert!(!cases.is_empty(), "section {section} has no cases"); + cases +} + +/// Returns a string of a case. +pub(crate) fn text<'a>(case: &'a Value, key: &str) -> &'a str { + case[key] + .as_str() + .unwrap_or_else(|| panic!("{key} is not a string in {case}")) +} + +/// Returns a list of strings of a case. +pub(crate) fn texts(case: &Value, key: &str) -> Vec { + case[key] + .as_array() + .unwrap_or_else(|| panic!("{key} is not a list in {case}")) + .iter() + .map(|value| value.as_str().expect("a string").to_owned()) + .collect() +} + +/// Returns the flavor of a case. +pub(crate) fn flavor(case: &Value) -> Flavor { + match text(case, "flavor") { + "posix" => Flavor::Posix, + "windows" => Flavor::Windows, + other => panic!("unknown flavor {other}"), + } +} + +/// Returns the bytes of a hex string. +pub(crate) fn hex(text: &str) -> Vec { + (0..text.len()) + .step_by(2) + .map(|index| u8::from_str_radix(&text[index..index + 2], 16).expect("hex")) + .collect() +} + +/// Collects the cases on which the port and Python differ, to report them together. +pub(crate) struct Mismatches { + /// The section being checked. + section: String, + /// The number of cases checked. + checked: usize, + /// A description of each mismatch. + found: Vec, +} + +impl Mismatches { + /// Starts checking a section. + pub(crate) fn new(section: &str) -> Self { + Self { + section: section.to_owned(), + checked: 0, + found: Vec::new(), + } + } + + /// Records whether the port's `actual` value for `case` equals Python's `expected` value. + pub(crate) fn check(&mut self, case: &Value, expected: T, actual: T) { + self.checked += 1; + if expected != actual { + self.found.push(format!( + "{case}\n python: {expected:?}\n rust: {actual:?}" + )); + } + } + + /// Fails with the mismatches, if any. + pub(crate) fn finish(self) { + assert!(self.checked > 0, "{} checked no cases", self.section); + assert!( + self.found.is_empty(), + "{} of {} {} cases differ from Python:\n{}", + self.found.len(), + self.checked, + self.section, + self.found + .iter() + .take(20) + .cloned() + .collect::>() + .join("\n") + ); + } +} + +/// Returns the ranges of code points from `start` that satisfy `predicate`, as the generator +/// lists them. +fn ranges(start: u32, predicate: impl Fn(char) -> bool) -> Vec<(u32, u32)> { + let mut ranges: Vec<(u32, u32)> = Vec::new(); + for code in start..=0x10_FFFF { + // Python's predicates are false for surrogates, which no `char` holds. + if char::from_u32(code).is_some_and(&predicate) { + match ranges.last_mut() { + Some((_, last)) if *last + 1 == code => *last = code, + _ => ranges.push((code, code)), + } + } + } + ranges +} + +/// Returns a fixture's list of inclusive ranges. +fn fixture_ranges(value: &Value) -> Vec<(u32, u32)> { + serde_json::from_value(value.clone()).expect("ranges") +} + +#[test] +fn unicode_tables_match_python() { + let fixture = fixture("python.json"); + assert_eq!(ranges(0, is_space), fixture_ranges(&fixture["space"])); + let starts: Vec = serde_json::from_value(fixture["decimal_starts"].clone()).unwrap(); + assert_eq!(DECIMAL_STARTS, starts.as_slice()); + assert_eq!( + NON_PRINTABLE, + fixture_ranges(&fixture["non_printable"]).as_slice() + ); +} + +#[test] +fn strings_integers_and_uuids_match_python() { + let fixture = fixture("python.json"); + let mut mismatches = Mismatches::new("strings"); + for case in cases(&fixture, "strings") { + let value = text(case, "text"); + mismatches.check(case, text(case, "repr"), repr(value).as_str()); + mismatches.check( + case, + texts(case, "split"), + split(value).map(str::to_owned).collect(), + ); + mismatches.check(case, text(case, "strip"), strip(value)); + mismatches.check( + case, + case["uuid"].as_str().map(str::to_owned), + parse_uuid(value), + ); + let expected = match case.get("int") { + Some(int) => Ok(int.as_str().unwrap().to_owned()), + None => Err(text(case, "int_error").to_owned()), + }; + let actual = Int::parse(value) + .map(|int| int.to_string()) + .map_err(|error| error.message(value)); + mismatches.check(case, expected, actual); + } + mismatches.finish(); +} + +#[test] +fn pure_paths_match_pathlib() { + let fixture = fixture("python.json"); + let mut mismatches = Mismatches::new("paths"); + for case in cases(&fixture, "paths") { + let path = PurePath::new(flavor(case), text(case, "text")); + let parents: Vec = path + .parents() + .map(|parent| parent.as_str().to_owned()) + .collect(); + mismatches.check( + case, + ( + text(case, "str"), + texts(case, "parts"), + case["absolute"].as_bool().unwrap(), + text(case, "name"), + texts(case, "parents"), + ), + ( + path.as_str(), + path.parts(), + path.is_absolute(), + path.name(), + parents, + ), + ); + } + mismatches.finish(); + + let mut mismatches = Mismatches::new("joins"); + for case in cases(&fixture, "joins") { + let path = PurePath::new(flavor(case), text(case, "left")).join(text(case, "right")); + mismatches.check(case, text(case, "str"), path.as_str()); + } + mismatches.finish(); + + let mut mismatches = Mismatches::new("equality"); + for case in cases(&fixture, "equality") { + let left = PurePath::new(Flavor::Windows, text(case, "left")); + let right = PurePath::new(Flavor::Windows, text(case, "right")); + mismatches.check(case, case["equal"].as_bool().unwrap(), left == right); + } + mismatches.finish(); + + let mut mismatches = Mismatches::new("normpath"); + for case in cases(&fixture, "normpath") { + mismatches.check( + case, + text(case, "normpath"), + windows_normpath(text(case, "text")).as_str(), + ); + } + mismatches.finish(); +} + +/// Returns a string in the generator's tagged form. +fn tagged_string(string: &PyText) -> Value { + match string.as_str() { + Some(text) => json!(["str", text]), + None => json!(["code_points", string.code_points().collect::>()]), + } +} + +/// Returns a decoded value in the generator's tagged form, with the members of an object as a +/// Python `dict` holds them: in the order of their first keys, with the last values. +fn tagged(document: &Document, id: NodeId) -> Value { + match document.node(id) { + Node::Null => json!(["null"]), + Node::Bool(value) => json!(["bool", value]), + // `str(int(literal))`, which drops the sign of `-0`. + Node::Integer(literal) => json!(["int", Int::parse(literal).unwrap().to_string()]), + // Python's `NaN` is a positive quiet NaN. + Node::Float(value) if value.is_nan() && value.is_sign_positive() => { + json!(["float", "7ff8000000000000"]) + } + Node::Float(value) => json!(["float", format!("{:016x}", value.to_bits())]), + Node::String(string) => tagged_string(string), + Node::Array(items) => json!([ + "list", + items + .iter() + .map(|&item| tagged(document, item)) + .collect::>() + ]), + Node::Object(members) => { + let mut entries: Vec<(&PyText, NodeId)> = Vec::new(); + for (key, value) in members { + match entries.iter().position(|&(name, _)| name == key) { + Some(position) => entries[position].1 = *value, + None => entries.push((key, *value)), + } + } + json!([ + "dict", + entries + .iter() + .map(|&(key, value)| json!([tagged_string(key), tagged(document, value)])) + .collect::>() + ]) + } + } +} + +#[test] +fn json_documents_decode_as_python_decodes_them() { + let fixture = fixture("python.json"); + let mut mismatches = Mismatches::new("json"); + for case in cases(&fixture, "json") { + let expected = if let Some(value) = case.get("value") { + Ok(value.clone()) + } else if let Some(message) = case.get("decode_error") { + Err(("decode", message.as_str().unwrap().to_owned())) + } else { + Err(("value", text(case, "value_error").to_owned())) + }; + let source = text(case, "text"); + let actual = match read(source, &mut Loads) { + Ok(document) => Ok(tagged(&document, document.root())), + Err(ReadError::Syntax { message, offset }) => { + Err(("decode", syntax_error(source, message, offset))) + } + Err(ReadError::Hook(error)) => Err(("value", error.message(source))), + }; + mismatches.check(case, expected, actual); + } + mismatches.finish(); + + let mut mismatches = Mismatches::new("utf8"); + for case in cases(&fixture, "utf8") { + mismatches.check( + case, + case["error"].as_str().map(str::to_owned), + decode_utf8(&hex(text(case, "bytes"))).err(), + ); + } + mismatches.finish(); +} diff --git a/nvx-rs/src/host/python/json.rs b/nvx-rs/src/host/python/json.rs new file mode 100644 index 000000000..1bf82f765 --- /dev/null +++ b/nvx-rs/src/host/python/json.rs @@ -0,0 +1,845 @@ +//! A JSON reader that accepts exactly what Python's `json.loads` accepts, and refuses the rest as +//! Python refuses it. +//! +//! Python's decoder differs from strict JSON parsers such as `serde_json`: +//! +//! - it reads the constants `NaN`, `Infinity`, and `-Infinity` as floats, and numbers beyond the +//! range of a float as infinities; +//! - it reads integers of any length, which `parse_int` receives as written, although `int`, its +//! default, refuses more than 4300 digits; +//! - it keeps a `\ud800` escape without its pair as an unpaired surrogate; and +//! - it hands each object's members, duplicates included, to `object_pairs_hook` when the object +//! closes, and each integer to `parse_int` when it reads the integer, so the errors that these +//! hooks raise surface in document order. [`Hooks`] are their equivalents, and [`Loads`] are +//! those of `json.loads` without arguments. +//! +//! [`read`] ports the C scanner of CPython 3.14, `scan_once_unicode` and the functions that it +//! calls in `Modules/_json.c`, with the checks of `JSONDecoder.decode` around it, so a document +//! that it refuses fails with the message and at the position of the `JSONDecodeError` that +//! Python raises, which [`syntax_error`] formats as Python does. +//! +//! The reader keeps values in an arena and never recurses, so it reads documents nested to any +//! depth. Python's decoder recurses and raises `RecursionError` at a depth that depends on the +//! Python version and the stack, so documents nested that deep are the one input on which the two +//! differ. + +use super::builtins::{Int, IntError}; +use super::text::PyText; + +/// Index of a value in a [`Document`]. +pub(crate) type NodeId = usize; + +/// A JSON value, as Python's decoder represents it. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum Node { + /// `null`, Python's `None`. + Null, + /// `true` or `false`. + Bool(bool), + /// An integer, as written, which is what `parse_int` receives. + Integer(Box), + /// A number with a fraction or an exponent, or one of `NaN`, `Infinity`, and `-Infinity`. + Float(f64), + /// A string. + String(PyText), + /// An array of values. + Array(Vec), + /// An object's members, in document order, including any duplicate names. + Object(Vec<(PyText, NodeId)>), +} + +/// A JSON document: its values and its top-level value. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Document { + /// Every value of the document; containers refer to their elements by index. + nodes: Vec, + /// Index of the top-level value. + root: NodeId, +} + +impl Document { + /// Returns the top-level value. + pub(crate) fn root(&self) -> NodeId { + self.root + } + + /// Returns the value at `id`, which a container of this document holds or [`root`](Self::root) + /// returned. + pub(crate) fn node(&self, id: NodeId) -> &Node { + &self.nodes[id] + } + + /// Returns `value.get(name)` for the `dict` that Python builds from the object at `id`, in + /// which the last of repeated names wins, or `None` if the value at `id` is not an object. + pub(crate) fn get(&self, id: NodeId, name: &str) -> Option<&Node> { + let Node::Object(members) = self.node(id) else { + return None; + }; + members + .iter() + .rev() + .find(|(member, _)| member.as_bytes() == name.as_bytes()) + .map(|&(_, value)| self.node(value)) + } + + /// Returns the document that Python's decoder reads from the JSON text of `value`. + /// + /// A number with a fraction or an exponent is a float, and any other an integer, as in that + /// text. The value has no repeated field to check, but its integers keep any length. + pub(crate) fn from_value(value: &serde_json::Value) -> Self { + use serde_json::Value; + + let mut nodes = vec![Node::Null]; + // Each value and the index reserved for it, which its container already holds. + let mut pending = vec![(value, 0)]; + while let Some((value, id)) = pending.pop() { + let mut reserve = |value| { + nodes.push(Node::Null); + let id = nodes.len() - 1; + pending.push((value, id)); + id + }; + let node = match value { + Value::Null => Node::Null, + Value::Bool(value) => Node::Bool(*value), + Value::Number(number) => { + // `serde_json` writes integers as digits, and other numbers with a fraction + // or an exponent, also with its `arbitrary_precision` feature. + let literal = number.to_string(); + if literal + .bytes() + .all(|byte| byte.is_ascii_digit() || byte == b'-') + { + Node::Integer(literal.into()) + } else { + Node::Float(number.as_f64().unwrap_or(f64::NAN)) + } + } + Value::String(text) => Node::String(PyText::from(text.as_str())), + Value::Array(elements) => Node::Array(elements.iter().map(&mut reserve).collect()), + Value::Object(members) => Node::Object( + members + .iter() + .map(|(name, value)| (PyText::from(name.as_str()), reserve(value))) + .collect(), + ), + }; + nodes[id] = node; + } + Self { nodes, root: 0 } + } +} + +/// Checks that run while a document is read, as Python's `object_pairs_hook` and `parse_int` +/// do. Their errors end the read. +pub(crate) trait Hooks { + /// Error of a failed check. + type Error; + + /// Runs when an object closes, with its members in document order. + fn object(&mut self, members: &[(PyText, NodeId)]) -> Result<(), Self::Error> { + let _ = members; + Ok(()) + } + + /// Runs when an integer is read, with the integer as written, such as `-12`. + fn integer(&mut self, literal: &str) -> Result<(), Self::Error> { + let _ = literal; + Ok(()) + } +} + +/// The hooks of `json.loads` without arguments: `int`, the default `parse_int`, refuses an +/// integer of more than 4300 digits with a `ValueError` that [`IntError::message`] gives. +pub(crate) struct Loads; + +impl Hooks for Loads { + type Error = IntError; + + fn integer(&mut self, literal: &str) -> Result<(), IntError> { + Int::parse(literal).map(drop) + } +} + +/// Why [`read`] failed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ReadError { + /// The text is not a document that Python's decoder accepts: Python raises a + /// `JSONDecodeError` with this message, at the character at this byte offset. + Syntax { + /// The message of the `JSONDecodeError`, such as `Expecting value`. + message: &'static str, + /// Byte offset in the text of the character at which Python reports the error. + offset: usize, + }, + /// A hook failed. + Hook(E), +} + +/// Returns `str()` of the `JSONDecodeError` that Python raises with `message` at byte `offset` +/// of `text`: the message, the line and column of the position, and the position itself, in +/// characters, such as `Expecting value: line 1 column 2 (char 1)`. +pub(crate) fn syntax_error(text: &str, message: &str, offset: usize) -> String { + let before = &text[..offset]; + let position = before.chars().count(); + let line = before.matches('\n').count() + 1; + let column = match before.rfind('\n') { + Some(newline) => before[newline + 1..].chars().count() + 1, + None => position + 1, + }; + format!("{message}: line {line} column {column} (char {position})") +} + +/// Reads `text` as `json.loads(text)` does, running `hooks` as the document is read. +pub(crate) fn read(text: &str, hooks: &mut H) -> Result> { + // Python refuses a string that starts with a byte-order mark before decoding it. + if text.starts_with('\u{FEFF}') { + return Err(ReadError::Syntax { + message: "Unexpected UTF-8 BOM (decode using utf-8-sig)", + offset: 0, + }); + } + let mut scanner = Scanner { + text, + bytes: text.as_bytes(), + nodes: Vec::new(), + hooks, + }; + let start = scanner.skip_whitespace(0); + let (root, end) = scanner.scan(start).map_err(|failure| match failure { + Failure::Stop(offset) => ReadError::Syntax { + message: "Expecting value", + offset, + }, + Failure::Decode(message, offset) => ReadError::Syntax { message, offset }, + Failure::Hook(error) => ReadError::Hook(error), + })?; + let end = scanner.skip_whitespace(end); + if end != text.len() { + return Err(ReadError::Syntax { + message: "Extra data", + offset: end, + }); + } + Ok(Document { + nodes: scanner.nodes, + root, + }) +} + +/// Why scanning stopped. +enum Failure { + /// No value starts at this byte offset: the scanner's `StopIteration`. + Stop(usize), + /// A `JSONDecodeError` with this message at this byte offset. + Decode(&'static str, usize), + /// A hook failed. + Hook(E), +} + +/// An array or object whose values are being scanned. +enum Frame { + /// An array, by its node. + Array(NodeId), + /// An object, by its node, with the name of the member whose value is being scanned. + Object(NodeId, PyText), +} + +/// The state of [`read`]. +struct Scanner<'a, H> { + /// The text being read. + text: &'a str, + /// The bytes of `text`. + bytes: &'a [u8], + /// The values read so far. + nodes: Vec, + /// The checks to run. + hooks: &'a mut H, +} + +impl Scanner<'_, H> { + /// Returns the byte at `index`, if any. + fn at(&self, index: usize) -> Option { + self.bytes.get(index).copied() + } + + /// Returns the offset of the first byte from `index` that is not whitespace that Python's + /// decoder skips: a space, a tab, a line feed, or a carriage return. + fn skip_whitespace(&self, mut index: usize) -> usize { + while matches!(self.at(index), Some(b' ' | b'\t' | b'\n' | b'\r')) { + index += 1; + } + index + } + + /// Adds a value to the document and returns its index. + fn push(&mut self, node: Node) -> NodeId { + self.nodes.push(node); + self.nodes.len() - 1 + } + + /// Runs the object hook on the members of the object at `node`, which has just closed. + fn close_object(&mut self, node: NodeId) -> Result<(), Failure> { + if let Node::Object(members) = &self.nodes[node] { + self.hooks.object(members).map_err(Failure::Hook)?; + } + Ok(()) + } + + /// Scans the value at `index`, returning its node and the offset after it, as `scan_once` + /// does, without recursion. + fn scan(&mut self, mut index: usize) -> Result<(NodeId, usize), Failure> { + let mut frames: Vec = Vec::new(); + loop { + // Scan one value, or open an array or object. + let mut value = None; + let Some(first) = self.at(index) else { + return Err(Failure::Stop(index)); + }; + match first { + b'"' => { + let (string, next) = self.scan_string(index + 1)?; + value = Some(self.push(Node::String(string))); + index = next; + } + b'[' => { + let node = self.push(Node::Array(Vec::new())); + index = self.skip_whitespace(index + 1); + if self.at(index) == Some(b']') { + value = Some(node); + index += 1; + } else { + frames.push(Frame::Array(node)); + } + } + b'{' => { + let node = self.push(Node::Object(Vec::new())); + index = self.skip_whitespace(index + 1); + if self.at(index) == Some(b'}') { + self.close_object(node)?; + value = Some(node); + index += 1; + } else { + let (name, next) = self.scan_name(index)?; + frames.push(Frame::Object(node, name)); + index = next; + } + } + _ => { + let (node, next) = self.scan_scalar(index)?; + value = Some(self.push(node)); + index = next; + } + } + // Attach finished values to their arrays and objects. + while let Some(finished) = value.take() { + let Some(frame) = frames.last_mut() else { + return Ok((finished, index)); + }; + index = self.skip_whitespace(index); + let (node, closing) = match frame { + Frame::Array(node) => (*node, b']'), + Frame::Object(node, _) => (*node, b'}'), + }; + match (&mut self.nodes[node], frame) { + (Node::Array(values), Frame::Array(_)) => values.push(finished), + (Node::Object(members), Frame::Object(_, name)) => { + members.push((std::mem::take(name), finished)); + } + _ => unreachable!("frames refer to their own arrays and objects"), + } + if self.at(index) == Some(closing) { + frames.pop(); + if closing == b'}' { + self.close_object(node)?; + } + value = Some(node); + index += 1; + continue; + } + if self.at(index) != Some(b',') { + return Err(Failure::Decode("Expecting ',' delimiter", index)); + } + let comma = index; + index = self.skip_whitespace(index + 1); + if self.at(index) == Some(closing) { + return Err(Failure::Decode( + if closing == b']' { + "Illegal trailing comma before end of array" + } else { + "Illegal trailing comma before end of object" + }, + comma, + )); + } + if let Some(Frame::Object(_, name)) = frames.last_mut() { + let (next_name, next) = self.scan_name(index)?; + *name = next_name; + index = next; + } + } + } + } + + /// Scans a member's name at `index`, its colon, and the whitespace after them, returning the + /// name and the offset of its value. + fn scan_name(&self, index: usize) -> Result<(PyText, usize), Failure> { + if self.at(index) != Some(b'"') { + return Err(Failure::Decode( + "Expecting property name enclosed in double quotes", + index, + )); + } + let (name, next) = self.scan_string(index + 1)?; + let index = self.skip_whitespace(next); + if self.at(index) != Some(b':') { + return Err(Failure::Decode("Expecting ':' delimiter", index)); + } + Ok((name, self.skip_whitespace(index + 1))) + } + + /// Scans a constant or a number at `index`. Python matches the constants as prefixes, so + /// whatever follows one is scanned separately. + fn scan_scalar(&mut self, index: usize) -> Result<(Node, usize), Failure> { + for (literal, node) in [ + ("null", Node::Null), + ("true", Node::Bool(true)), + ("false", Node::Bool(false)), + ("NaN", Node::Float(f64::NAN)), + ("Infinity", Node::Float(f64::INFINITY)), + ("-Infinity", Node::Float(f64::NEG_INFINITY)), + ] { + if self.bytes[index..].starts_with(literal.as_bytes()) { + return Ok((node, index + literal.len())); + } + } + self.scan_number(index) + } + + /// Scans a number at `start` as `_match_number_unicode` does: an optional `-`, `0` or digits + /// without a leading zero, a fraction if a digit follows the point, and an exponent if a + /// digit follows the `e` and its optional sign. A number with a fraction or an exponent is a + /// float, and any other is an integer, which the integer hook receives. + fn scan_number(&mut self, start: usize) -> Result<(Node, usize), Failure> { + let digit = |index: usize| self.at(index).is_some_and(|byte| byte.is_ascii_digit()); + let last = self.bytes.len().saturating_sub(1); + let mut index = start; + if self.at(index) == Some(b'-') { + index += 1; + if index > last { + return Err(Failure::Stop(start)); + } + } + match self.at(index) { + Some(b'1'..=b'9') => { + index += 1; + while digit(index) { + index += 1; + } + } + Some(b'0') => index += 1, + _ => return Err(Failure::Stop(start)), + } + let mut float = false; + if index < last && self.at(index) == Some(b'.') && digit(index + 1) { + float = true; + index += 2; + while digit(index) { + index += 1; + } + } + if index < last && matches!(self.at(index), Some(b'e' | b'E')) { + let exponent = index; + index += 1; + if index < last && matches!(self.at(index), Some(b'-' | b'+')) { + index += 1; + } + while digit(index) { + index += 1; + } + if digit(index - 1) { + float = true; + } else { + index = exponent; + } + } + let literal = &self.text[start..index]; + if float { + // Like Python's `float`, Rust rounds correctly and overflows to an infinity, so any + // literal of this form parses. + return Ok((Node::Float(literal.parse().unwrap_or(f64::NAN)), index)); + } + self.hooks.integer(literal).map_err(Failure::Hook)?; + Ok((Node::Integer(literal.into()), index)) + } + + /// Scans a string whose opening quote precedes `end`, as `scanstring_unicode` does in strict + /// mode, returning it and the offset after its closing quote: control characters are refused, + /// and a `\u` escape of a high surrogate followed by a `\u` escape of a low surrogate is one + /// character, while any other surrogate escape stays an unpaired surrogate. + fn scan_string(&self, mut end: usize) -> Result<(PyText, usize), Failure> { + let length = self.bytes.len(); + let begin = end - 1; + let mut decoded = PyText::new(); + loop { + // Find the end of the string or the next escape. + let mut next = end; + let mut found = None; + while next < length { + let byte = self.bytes[next]; + if byte == b'"' || byte == b'\\' { + found = Some(byte); + break; + } + if byte <= 0x1F { + return Err(Failure::Decode("Invalid control character at", next)); + } + next += 1; + } + let Some(terminator) = found else { + return Err(Failure::Decode("Unterminated string starting at", begin)); + }; + decoded.push_str(&self.text[end..next]); + next += 1; + if terminator == b'"' { + return Ok((decoded, next)); + } + let Some(escape) = self.at(next) else { + return Err(Failure::Decode("Unterminated string starting at", begin)); + }; + if escape != b'u' { + end = next + 1; + let character = match escape { + b'"' | b'\\' | b'/' => char::from(escape), + b'b' => '\u{8}', + b'f' => '\u{c}', + b'n' => '\n', + b'r' => '\r', + b't' => '\t', + _ => return Err(Failure::Decode("Invalid \\escape", end - 2)), + }; + decoded.push_code_point(u32::from(character)); + continue; + } + next += 1; + end = next + 4; + if end > length { + return Err(Failure::Decode("Invalid \\uXXXX escape", next - 1)); + } + let mut code_point = self + .hex4(next) + .ok_or(Failure::Decode("Invalid \\uXXXX escape", end - 5))?; + // A high surrogate and a low-surrogate escape after it form one character. + if (0xD800..=0xDBFF).contains(&code_point) + && end + 6 < length + && self.at(end) == Some(b'\\') + && self.at(end + 1) == Some(b'u') + { + let low = self + .hex4(end + 2) + .ok_or(Failure::Decode("Invalid \\uXXXX escape", end + 1))?; + if (0xDC00..=0xDFFF).contains(&low) { + code_point = 0x10000 + (((code_point - 0xD800) << 10) | (low - 0xDC00)); + end += 6; + } + } + decoded.push_code_point(code_point); + } + } + + /// Returns the value of the four hexadecimal digits at `index`, of either case. + fn hex4(&self, index: usize) -> Option { + self.bytes + .get(index..index + 4)? + .iter() + .try_fold(0, |value, &digit| { + Some((value << 4) | char::from(digit).to_digit(16)?) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Hooks that refuse duplicate names and integers of more than 64 digits, as the egress + /// policy reader does. + struct Strict; + + impl Hooks for Strict { + type Error = String; + + fn object(&mut self, members: &[(PyText, NodeId)]) -> Result<(), String> { + let mut seen = std::collections::HashSet::new(); + for (name, _) in members { + if !seen.insert(name) { + return Err(format!("duplicate {}", name.render())); + } + } + Ok(()) + } + + fn integer(&mut self, literal: &str) -> Result<(), String> { + if literal.trim_start_matches('-').len() > 64 { + return Err("long integer".to_owned()); + } + Ok(()) + } + } + + /// Reads `text` with [`Strict`] hooks and describes the result. + fn describe(text: &str) -> String { + match read(text, &mut Strict) { + Ok(document) => render(&document, document.root()), + Err(ReadError::Syntax { .. }) => "syntax".to_owned(), + Err(ReadError::Hook(message)) => message, + } + } + + /// Renders a value compactly, with the type of each scalar. + fn render(document: &Document, id: NodeId) -> String { + match document.node(id) { + Node::Null => "null".to_owned(), + Node::Bool(value) => value.to_string(), + Node::Integer(literal) => format!("int:{literal}"), + Node::Float(value) => format!("float:{value:?}"), + Node::String(text) => format!("str:{}", text.render()), + Node::Array(elements) => format!( + "[{}]", + elements + .iter() + .map(|&element| render(document, element)) + .collect::>() + .join(",") + ), + Node::Object(members) => format!( + "{{{}}}", + members + .iter() + .map(|(name, value)| format!("{}={}", name.render(), render(document, *value))) + .collect::>() + .join(",") + ), + } + } + + #[test] + fn values_read_as_python_reads_them() { + for (text, expected) in [ + ( + " \t\n\r{\"a\": [1, -0, 2.5, 1e5, 1E+5, 1e-400, 1e400]}\n", + "{a=[int:1,int:-0,float:2.5,float:100000.0,float:100000.0,float:0.0,float:inf]}", + ), + ( + "[NaN,Infinity,-Infinity]", + "[float:NaN,float:inf,float:-inf]", + ), + ("[true,false,null]", "[true,false,null]"), + ( + "\"\\u0031\\/\\b\\f\\n\\r\\t\\\"\\\\\"", + "str:1/\u{8}\u{c}\n\r\t\"\\", + ), + ("\"\\ud83d\\ude00\"", "str:\u{1F600}"), + ("\"\\ud800\"", "str:\\ud800"), + ("\"\\udc00\\ud800\"", "str:\\udc00\\ud800"), + ("\"\\ud800\\u0041\"", "str:\\ud800A"), + ("\"\\ud800x\\udc00\"", "str:\\ud800x\\udc00"), + ("\"\u{7F}é\u{2028}\"", "str:\u{7F}é\u{2028}"), + ("{}", "{}"), + ("[]", "[]"), + ("[[[]]]", "[[[]]]"), + ("{\"b\":1,\"a\":{\"c\":[]}}", "{b=int:1,a={c=[]}}"), + ("-0.0", "float:-0.0"), + ("0", "int:0"), + ] { + assert_eq!(describe(text), expected, "{text:?}"); + } + } + + #[test] + fn malformed_documents_are_refused() { + for text in [ + "", + " ", + "\u{FEFF}{}", + "{} x", + "{}\u{C}", + "[1,]", + "{\"a\":1,}", + "[01]", + "[1.]", + "[.5]", + "[1e]", + "[1e+]", + "[-]", + "[-a]", + "[+1]", + "[nul]", + "[Nan]", + "[-Inf]", + "[inf]", + "\"a", + "\"a\\", + "\"\\x\"", + "\"\\u12\"", + "\"\\u12g4\"", + "\"\\ud800\\u12g4\"", + "\"tab\there\"", + "\"\u{0}\"", + "{\"a\" 1}", + "{a:1}", + "{'a':1}", + "[1 2]", + "nullx", + "truefalse", + "[\"a\"\"b\"]", + "{\"a\":1 \"b\":2}", + "[", + "{", + "{\"a\":", + "[1,", + ] { + assert_eq!(describe(text), "syntax", "{text:?}"); + } + } + + #[test] + fn hooks_run_in_document_order() { + let long = "9".repeat(65); + // The duplicate name is found when its object closes, after the long integer. + assert_eq!( + describe(&format!("{{\"a\":1,\"a\":2,\"b\":{long}}}")), + "long integer" + ); + // An inner object closes before the integer that follows it. + assert_eq!( + describe(&format!("{{\"a\":{{\"x\":1,\"x\":2}},\"b\":{long}}}")), + "duplicate x" + ); + // The object hook runs before trailing text is refused, and a syntax error before the + // object closes prevails. + assert_eq!(describe("{\"a\":1,\"a\":2} x"), "duplicate a"); + assert_eq!(describe("{\"a\":1,\"a\":2,}"), "syntax"); + // The first duplicate in document order is reported. + assert_eq!(describe("{\"b\":1,\"a\":2,\"a\":3,\"b\":4}"), "duplicate a"); + // The integer hook runs before what follows the integer is checked. + assert_eq!(describe(&format!("[{long}x]")), "long integer"); + assert_eq!( + describe(&format!("[-{}]", "9".repeat(64))), + format!("[int:-{}]", "9".repeat(64)) + ); + // Escaped names are compared after decoding. + assert_eq!(describe("{\"a\":1,\"\\u0061\":2}"), "duplicate a"); + assert_eq!( + describe("{\"\\ud800\":1,\"\\ud800\":2}"), + "duplicate \\ud800" + ); + } + + #[test] + fn deep_nesting_needs_no_recursion() { + let depth = 500_000; + let text = format!("{}{}", "[".repeat(depth), "]".repeat(depth)); + let document = read(&text, &mut Strict).unwrap(); + assert!( + matches!(document.node(document.root()), Node::Array(elements) if elements.len() == 1) + ); + let text = format!("{}1{}", "{\"a\":".repeat(depth), "}".repeat(depth)); + read(&text, &mut Strict).unwrap(); + } + + /// Reads `text` as `json.loads(text)` does and describes its error as Python prints it. + fn error(text: &str) -> String { + match read(text, &mut Loads) { + Ok(_) => panic!("{text:?} was read"), + Err(ReadError::Syntax { message, offset }) => syntax_error(text, message, offset), + Err(ReadError::Hook(error)) => error.message(text), + } + } + + #[test] + fn documents_read_as_json_loads_reads_them() { + let text = " [\"a\", 1, -2.5e1, NaN, -Infinity, null, true, {\"k\": [], \"k\": {}}] "; + let document = read(text, &mut Loads).unwrap(); + let Node::Array(values) = document.node(document.root()) else { + panic!("not an array"); + }; + let nodes: Vec<&Node> = values.iter().map(|&id| document.node(id)).collect(); + assert_eq!(nodes[0], &Node::String(PyText::from("a"))); + assert_eq!(nodes[1], &Node::Integer("1".into())); + assert_eq!(nodes[2], &Node::Float(-25.0)); + assert!(matches!(nodes[3], Node::Float(value) if value.is_nan())); + assert_eq!(nodes[4], &Node::Float(f64::NEG_INFINITY)); + assert_eq!(nodes[5], &Node::Null); + assert_eq!(nodes[6], &Node::Bool(true)); + // The last of repeated names wins, as in the `dict` that Python builds. + assert_eq!( + document.get(values[7], "k"), + Some(&Node::Object(Vec::new())) + ); + assert_eq!(document.get(values[7], "missing"), None); + assert_eq!(document.get(values[0], "k"), None); + } + + #[test] + fn errors_read_as_python_reports_them() { + for (text, expected) in [ + ("", "Expecting value: line 1 column 1 (char 0)"), + ("[", "Expecting value: line 1 column 2 (char 1)"), + ("[1", "Expecting ',' delimiter: line 1 column 3 (char 2)"), + ( + "[1,]", + "Illegal trailing comma before end of array: line 1 column 3 (char 2)", + ), + ( + "{\"a\":1,}", + "Illegal trailing comma before end of object: line 1 column 7 (char 6)", + ), + ( + "{1:2}", + "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + ), + ( + "{\"a\" 1}", + "Expecting ':' delimiter: line 1 column 6 (char 5)", + ), + ( + "\"abc", + "Unterminated string starting at: line 1 column 1 (char 0)", + ), + ( + "\"a\tb\"", + "Invalid control character at: line 1 column 3 (char 2)", + ), + ("\"\\x\"", "Invalid \\escape: line 1 column 2 (char 1)"), + ( + "\"\\u12\"", + "Invalid \\uXXXX escape: line 1 column 3 (char 2)", + ), + ("[]\n x", "Extra data: line 2 column 2 (char 4)"), + ( + "\u{feff}[]", + "Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)", + ), + ("-", "Expecting value: line 1 column 1 (char 0)"), + ("01", "Extra data: line 1 column 2 (char 1)"), + // Positions count characters, not bytes. + ( + "[\"é\" x]", + "Expecting ',' delimiter: line 1 column 6 (char 5)", + ), + ( + "{\"\u{1F600}\":\n 1 2}", + "Expecting ',' delimiter: line 2 column 4 (char 9)", + ), + ( + &format!("[{}]", "1".repeat(4301)), + "Exceeds the limit (4300 digits) for integer string conversion: value has 4301 \ + digits; use sys.set_int_max_str_digits() to increase the limit", + ), + ] { + assert_eq!(error(text), expected, "{text:?}"); + } + assert!(read(&format!("[-{}]", "1".repeat(4300)), &mut Loads).is_ok()); + } +} diff --git a/nvx-rs/src/host/python/mod.rs b/nvx-rs/src/host/python/mod.rs new file mode 100644 index 000000000..9d5814a4c --- /dev/null +++ b/nvx-rs/src/host/python/mod.rs @@ -0,0 +1,58 @@ +//! Python semantics that the faithful ports of `scripts/nvx.py` tools need. +//! +//! Commands that `nvx.py` delegates to `nvx-rs` keep the behavior of the Python code they +//! replace, which depends on how Python reads JSON and integers, holds and prints strings and +//! paths, and queries the filesystem. These modules implement what the ports rely on, after +//! CPython 3.14: +//! +//! - [`text`]: Python strings, which may hold unpaired surrogates, how Python prints them, and +//! the errors of its UTF-8 codec. +//! - [`builtins`]: `str.isspace`, `str.split()`, `str.strip()`, `repr` of a `str`, `int`, and +//! `float`. +//! - [`json`]: a JSON reader that accepts what `json.loads` accepts, with the equivalents of its +//! `object_pairs_hook` and `parse_int` hooks, and that fails with its messages. +//! - [`dumps`]: `json.dumps(value, indent=2, sort_keys=True)`, with which `nvx.py` writes its +//! state files. +//! - [`path`]: `str(pathlib.Path(...))`, which Python messages show, the path that Python opens +//! for it, and the pure paths of `pathlib` with the `os.path` operations that they use. +//! - [`uuid`]: the UUIDs that `uuid.UUID` reads. +//! - [`fs`]: the filesystem queries of `os`, `os.path`, and `pathlib.Path`, among them +//! `Path.resolve()` and the file identities of `os.stat`, `os.lstat`, and `os.fstat`. +//! - [`shell`]: `subprocess.list2cmdline` and `shlex.join`, with which `nvx.py` prints the +//! commands that it runs. +//! - [`value`]: `str()`, `int()`, and `==` of the values that `json.loads` returns, as `nvx.py` +//! applies them to its state files. +//! +//! Character properties come from the Unicode 16.0.0 database of Python 3.14, in [`unicode`], +//! which `scripts/nvx_rs_fixtures.py` generated. The same script recorded what Python does with +//! fixed and seeded random inputs in `tests/fixtures`, and the golden tests compare these +//! implementations, and the ports that use them, with those records. microsoft/nvx#359 retired +//! the script with the Python code that it recorded, so the records are frozen. Python decodes +//! operating-system strings that are not valid Unicode into surrogates: [`path`] prints such +//! paths as Python does, and the filesystem queries refuse them. + +pub(crate) mod builtins; +#[cfg(feature = "cli")] +pub(crate) mod dumps; +pub(crate) mod fs; +#[cfg(test)] +pub(crate) mod golden; +pub(crate) mod json; +pub(crate) mod path; +#[cfg(feature = "cli")] +pub(crate) mod shell; +pub(crate) mod text; +pub(crate) mod unicode; +pub(crate) mod uuid; +#[cfg(feature = "cli")] +pub(crate) mod value; +#[cfg(windows)] +mod windows; + +/// Returns the error for an operating-system string that is not valid Unicode. +pub(crate) fn not_unicode(what: &str) -> std::io::Error { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("{what} is not valid Unicode"), + ) +} diff --git a/nvx-rs/src/host/python/path.rs b/nvx-rs/src/host/python/path.rs new file mode 100644 index 000000000..19ed8ceea --- /dev/null +++ b/nvx-rs/src/host/python/path.rs @@ -0,0 +1,925 @@ +//! Paths as Python's `pathlib` and `os.path` hold, print, and open them. +//! +//! Python messages name files by `str(path)` for a `pathlib.Path`, which normalizes the path it +//! was given: it drops empty and `.` components, and on Windows turns `/` into `\` and gives UNC +//! shares a root. Python also opens that string, not the one it was given, so `config.json/` +//! opens `config.json`. [`python_path`] reproduces both for this host's flavor of paths, for any +//! host path, Unicode or not. +//! +//! A [`PurePath`] holds a Unicode path of either [`Flavor`], as `PurePosixPath` or +//! `PureWindowsPath` does, with its parts, parents, joins, and case-insensitive comparison on +//! Windows. [`Flavor`] has the `os.path` operations of `posixpath` and `ntpath` that the ports +//! use, `splitroot`, `join`, `isabs`, and `split`, and [`windows_normpath`] is `ntpath.normpath`. +//! One parser serves both: [`python_path`] and [`PurePath`] read paths alike. +//! +//! Windows paths follow CPython 3.14 on Windows, whose `ntpath.splitroot` and `ntpath.normpath` +//! are C functions that index UTF-16 code units: a character outside the Basic Multilingual Plane +//! followed by `:` is no drive there. Python's own `ntpath`, which `PureWindowsPath` uses on other +//! hosts, indexes code points instead, which differs only for such a character. + +use std::fmt; +use std::path::{Path, PathBuf}; + +/// A path as a `pathlib.Path` of this host holds it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PythonPath { + /// `str(path)` as Python prints it to a UTF-8 stream, which escapes surrogates as `\udXXX`. + pub(crate) display: String, + /// The path that Python opens: `os.fspath(path)`. + pub(crate) native: PathBuf, +} + +/// Returns `path` as Python's `pathlib.Path(path)` prints and opens it. +/// +/// On Unix, Python decodes bytes that are not UTF-8 into surrogates and prints each as `\udcXX`; +/// on Windows, unpaired surrogates print as `\udXXX`. +pub(crate) fn python_path(path: &Path) -> PythonPath { + #[cfg(windows)] + { + use std::ffi::OsString; + use std::os::windows::ffi::{OsStrExt, OsStringExt}; + + let units: Vec = path.as_os_str().encode_wide().collect(); + let normalized = windows_str(&units); + PythonPath { + display: render_utf16(&normalized), + native: PathBuf::from(OsString::from_wide(&normalized)), + } + } + #[cfg(unix)] + { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let normalized = posix_str(path.as_os_str().as_bytes()); + PythonPath { + display: render_surrogateescape(&normalized), + native: PathBuf::from(OsStr::from_bytes(&normalized)), + } + } + #[cfg(not(any(unix, windows)))] + { + let normalized = posix_str(path.to_string_lossy().as_bytes()); + let display = String::from_utf8_lossy(&normalized).into_owned(); + PythonPath { + native: PathBuf::from(&display), + display, + } + } +} + +/// Returns `str(PurePosixPath(path))` for a path given as bytes, which Python sees as the +/// characters that they decode to. +/// +/// One leading `/`, or three or more, are a root of `/`, and exactly two are a root of `//`; +/// empty and `.` components are dropped, and an empty result is `.`. +#[cfg(any(not(windows), test))] +pub(crate) fn posix_str(path: &[u8]) -> Vec { + let (root, tail) = parse_posix(path); + format_posix(root, &tail) +} + +/// Returns `str(PureWindowsPath(path))` on Windows for a path given as UTF-16 code units, as +/// Python 3.14 computes it with `nt._path_splitroot_ex`, which indexes UTF-16 code units. +/// +/// `/` becomes `\`; a drive is a letter or other code unit and a colon, a UNC share +/// (`\\server\share`, `\\?\UNC\server\share`), or a device (`\\.\device`, `\\?\device`); UNC +/// shares gain a root; empty and `.` components are dropped; a relative path whose first +/// component looks like a drive gains a `.\` prefix; and an empty result is `.`. +#[cfg(any(windows, test))] +pub(crate) fn windows_str(path: &[u16]) -> Vec { + let (drive, root, tail) = parse_windows(path); + format_windows(&drive, &root, &tail) +} + +/// The separator of Windows paths, `\`. +const SEPARATOR: u16 = b'\\' as u16; + +/// Returns whether a code unit separates the components of a Windows path: `\` or `/`. +fn is_windows_separator(unit: u16) -> bool { + unit == SEPARATOR || unit == u16::from(b'/') +} + +/// Returns the length of the root of a POSIX path, as `posixpath.splitroot` finds it: exactly two +/// leading slashes are a root of their own, and one, or three or more, a root of `/`. +fn posix_root_length(path: &[u8]) -> usize { + match path { + [b'/', b'/', b'/', ..] => 1, + [b'/', b'/', ..] => 2, + [b'/', ..] => 1, + _ => 0, + } +} + +/// Returns the root and the components of a POSIX path, as `pathlib` parses it: empty and `.` +/// components are dropped. +fn parse_posix(path: &[u8]) -> (&[u8], Vec<&[u8]>) { + let root = posix_root_length(path); + let tail = path[root..] + .split(|&byte| byte == b'/') + .filter(|part| !part.is_empty() && *part != b".") + .collect(); + (&path[..root], tail) +} + +/// Returns `str()` of the POSIX path of a root and components, as `pathlib` formats it. +fn format_posix(root: &[u8], tail: &[&[u8]]) -> Vec { + let mut result = root.to_vec(); + for (index, part) in tail.iter().enumerate() { + if index > 0 { + result.push(b'/'); + } + result.extend_from_slice(part); + } + if result.is_empty() { + result.push(b'.'); + } + result +} + +/// Returns the drive, the root, and the components of a Windows path given as UTF-16 code units, +/// as `pathlib` parses it: `/` becomes `\`, a UNC share gains a root, and empty and `.` +/// components are dropped. +fn parse_windows(path: &[u16]) -> (Vec, Vec, Vec>) { + let path: Vec = path + .iter() + .map(|&unit| { + if is_windows_separator(unit) { + SEPARATOR + } else { + unit + } + }) + .collect(); + let (drive_length, root_length) = windows_skip_root(&path); + let drive = &path[..drive_length]; + let mut root = path[drive_length..drive_length + root_length].to_vec(); + if root.is_empty() && drive.first() == Some(&SEPARATOR) && drive.last() != Some(&SEPARATOR) { + // `pathlib` gives a share, `\\server\share` or `\\?\UNC\server\share`, a root, but not a + // device such as `\\.\device` or `\\?\device`. Its `not in '?.'` test is a substring + // test, which the empty string also passes. + let parts: Vec<&[u16]> = drive.split(|&unit| unit == SEPARATOR).collect(); + let device = |part: &[u16]| { + part.is_empty() + || part == [u16::from(b'?')] + || part == [u16::from(b'.')] + || part == [u16::from(b'?'), u16::from(b'.')] + }; + if (parts.len() == 4 && !device(parts[2])) || parts.len() == 6 { + root = vec![SEPARATOR]; + } + } + let tail = path[drive_length + root_length..] + .split(|&unit| unit == SEPARATOR) + .filter(|part| !part.is_empty() && *part != [u16::from(b'.')]) + .map(<[u16]>::to_vec) + .collect(); + (drive.to_vec(), root, tail) +} + +/// Returns `str()` of the Windows path of a drive, a root, and components, as `pathlib` formats +/// it: a relative path whose first component would read as a drive gains a `.\` prefix. +fn format_windows(drive: &[u16], root: &[u16], tail: &[Vec]) -> Vec { + let mut result = Vec::new(); + if !drive.is_empty() || !root.is_empty() { + result.extend_from_slice(drive); + result.extend_from_slice(root); + } else if tail + .first() + .is_some_and(|first| windows_skip_root(first).0 > 0) + { + // A first component such as `c:foo` would read as a drive. + result.extend_from_slice(&[u16::from(b'.'), SEPARATOR]); + } + for (index, part) in tail.iter().enumerate() { + if index > 0 { + result.push(SEPARATOR); + } + result.extend_from_slice(part); + } + if result.is_empty() { + result.push(u16::from(b'.')); + } + result +} + +/// Returns the lengths in code units of the drive and the root of a Windows path, as CPython's +/// `_Py_skiproot` finds them on Windows, with `\` and `/` as separators. +fn windows_skip_root(path: &[u16]) -> (usize, usize) { + let unit = |index: usize| path.get(index).copied(); + let separator = |index: usize| unit(index).is_some_and(is_windows_separator); + let either = |index: usize, upper: u8, lower: u8| { + unit(index) == Some(u16::from(upper)) || unit(index) == Some(u16::from(lower)) + }; + if separator(0) { + if !separator(1) { + // A root without a drive, such as `\Windows`. + return (0, 1); + } + // A UNC share, such as `\\server\share` or `\\?\UNC\server\share`, or a device, such as + // `\\.\device` or `\\?\device`. + let unc = unit(2) == Some(u16::from(b'?')) + && separator(3) + && either(4, b'U', b'u') + && either(5, b'N', b'n') + && either(6, b'C', b'c') + && separator(7); + let mut index = if unc { 8 } else { 2 }; + while index < path.len() && !separator(index) { + index += 1; + } + if index >= path.len() { + return (path.len(), 0); + } + index += 1; + while index < path.len() && !separator(index) { + index += 1; + } + if index >= path.len() { + (path.len(), 0) + } else { + (index, 1) + } + } else if !path.is_empty() && unit(1) == Some(u16::from(b':')) { + // A drive, such as `X:`, and a root if a separator follows it. + (2, usize::from(separator(2))) + } else { + (0, 0) + } +} + +/// Returns UTF-16 code units as Python prints them to a UTF-8 stream: unpaired surrogates as +/// `\udXXX`. +#[cfg(any(windows, test))] +fn render_utf16(units: &[u16]) -> String { + char::decode_utf16(units.iter().copied()) + .map(|unit| match unit { + Ok(character) => character.to_string(), + Err(error) => format!("\\u{:04x}", error.unpaired_surrogate()), + }) + .collect() +} + +/// Returns bytes as Python prints the string that it decodes them to with the `surrogateescape` +/// error handler: valid UTF-8 as is, and each other byte as `\udcXX`. +#[cfg(any(unix, test))] +fn render_surrogateescape(bytes: &[u8]) -> String { + let mut rendered = String::with_capacity(bytes.len()); + for chunk in bytes.utf8_chunks() { + rendered.push_str(chunk.valid()); + for byte in chunk.invalid() { + rendered.push_str(&format!("\\udc{byte:02x}")); + } + } + rendered +} + +/// Returns the string of bytes that come from a Unicode path cut at ASCII separators. +fn from_utf8(bytes: &[u8]) -> String { + String::from_utf8_lossy(bytes).into_owned() +} + +/// Returns the string of code units that come from a Unicode path cut at ASCII separators. +fn from_utf16(units: &[u16]) -> String { + String::from_utf16_lossy(units) +} + +/// Returns the byte offset in `text` of the character that starts at UTF-16 code unit `units`. +fn utf16_offset(text: &str, units: usize) -> usize { + let mut counted = 0; + for (offset, character) in text.char_indices() { + if counted >= units { + return offset; + } + counted += character.len_utf16(); + } + text.len() +} + +/// The two flavors of `pathlib` paths: `PurePosixPath` and `PureWindowsPath`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum Flavor { + /// `posixpath`, with `/` as its only separator. + Posix, + /// `ntpath`, with `\` as its separator and `/` as an alternative. + Windows, +} + +impl Flavor { + /// The flavor of `pathlib.Path` on this host. + pub(crate) const NATIVE: Self = if cfg!(windows) { + Self::Windows + } else { + Self::Posix + }; + + /// Returns whether `character` separates components: `os.sep` or `os.altsep`. + pub(crate) fn is_separator(self, character: char) -> bool { + character == '/' || (self == Self::Windows && character == '\\') + } + + /// Returns `os.path.splitroot(path)`: its drive, root, and the rest, as slices of `path`. + pub(crate) fn split_root(self, path: &str) -> (&str, &str, &str) { + let (drive, root) = match self { + Self::Posix => (0, posix_root_length(path.as_bytes())), + Self::Windows => { + let units: Vec = path.encode_utf16().collect(); + let (drive, root) = windows_skip_root(&units); + let drive_end = utf16_offset(path, drive); + (drive_end, utf16_offset(path, drive + root) - drive_end) + } + }; + ( + &path[..drive], + &path[drive..drive + root], + &path[drive + root..], + ) + } + + /// Returns `os.path.join(first, second)`. + pub(crate) fn join(self, first: &str, second: &str) -> String { + match self { + Self::Posix => { + if second.starts_with('/') || first.is_empty() { + second.to_owned() + } else if first.ends_with('/') { + format!("{first}{second}") + } else { + format!("{first}/{second}") + } + } + Self::Windows => windows_join(first, second), + } + } + + /// Returns `os.path.isabs(path)`. A Windows path is absolute with a drive and a root, or as + /// a UNC or device path; `ntpath.isabs` tests the code points of the path, even on Windows. + pub(crate) fn is_absolute(self, path: &str) -> bool { + match self { + Self::Posix => path.starts_with('/'), + Self::Windows => { + let start: String = path + .chars() + .take(3) + .map(|character| if character == '/' { '\\' } else { character }) + .collect(); + start.starts_with("\\\\") || start.chars().skip(1).eq([':', '\\']) + } + } + } + + /// Returns `os.path.split(path)`: the path without its last component, and that component. + #[cfg(any(windows, test))] + pub(crate) fn split(self, path: &str) -> (String, String) { + match self { + Self::Posix => { + let index = path.rfind('/').map_or(0, |index| index + 1); + let (head, tail) = path.split_at(index); + // A head of slashes only is kept whole. + let head = if head.chars().all(|character| character == '/') { + head + } else { + head.trim_end_matches('/') + }; + (head.to_owned(), tail.to_owned()) + } + Self::Windows => { + let (drive, root, rest) = self.split_root(path); + let index = rest.rfind(['\\', '/']).map_or(0, |index| index + 1); + let (head, tail) = rest.split_at(index); + ( + format!("{drive}{root}{}", head.trim_end_matches(['\\', '/'])), + tail.to_owned(), + ) + } + } + } +} + +/// Returns `ntpath.join(first, second)`. +fn windows_join(first: &str, second: &str) -> String { + let is_separator = |character: char| character == '\\' || character == '/'; + let (drive, root, path) = Flavor::Windows.split_root(first); + let (mut drive, mut root, mut path) = (drive.to_owned(), root.to_owned(), path.to_owned()); + let (second_drive, second_root, second_path) = Flavor::Windows.split_root(second); + if !second_root.is_empty() { + // The second path is absolute. + if !second_drive.is_empty() || drive.is_empty() { + second_drive.clone_into(&mut drive); + } + second_root.clone_into(&mut root); + second_path.clone_into(&mut path); + } else if !second_drive.is_empty() + && second_drive != drive + && second_drive.to_lowercase() != drive.to_lowercase() + { + // Another drive: the first path is dropped. + second_drive.clone_into(&mut drive); + second_root.clone_into(&mut root); + second_path.clone_into(&mut path); + } else { + if !second_drive.is_empty() { + // The same drive, in other case. + second_drive.clone_into(&mut drive); + } + if path.chars().last().is_some_and(|last| !is_separator(last)) { + path.push('\\'); + } + path.push_str(second_path); + } + // A UNC drive and a relative path need a separator between them. + if !path.is_empty() + && root.is_empty() + && drive + .chars() + .last() + .is_some_and(|last| !matches!(last, ':' | '\\' | '/')) + { + return format!("{drive}\\{path}"); + } + format!("{drive}{root}{path}") +} + +/// Returns `ntpath.normpath(path)`, as CPython's `_Py_normpath_and_size` computes it on Windows: +/// separators become `\`, empty and `.` components disappear, and `..` removes the component +/// before it, except at the root or after another `..`. +#[cfg(any(windows, test))] +pub(crate) fn windows_normpath(path: &str) -> String { + let input: Vec = path.chars().collect(); + if input.is_empty() { + return ".".to_owned(); + } + let separator = |character: char| character == '\\' || character == '/'; + let is_separator_or_end = |index: usize| input.get(index).is_none_or(|&c| separator(c)); + let (drive, root, _) = Flavor::Windows.split_root(path); + let prefix_length = path[..drive.len() + root.len()].chars().count(); + let mut output: Vec = Vec::with_capacity(input.len()); + let mut read = 0; + // The output index that `..` never removes, and the last character that was copied. + let mut minimum = 0; + let mut last = '\0'; + if prefix_length > 0 { + output.extend( + input[..prefix_length] + .iter() + .map(|&character| if character == '/' { '\\' } else { character }), + ); + read = prefix_length; + minimum = output.len() - 1; + last = output[minimum]; + if last != '\\' { + minimum += 1; + } + } + if input.get(read) == Some(&'.') && is_separator_or_end(read + 1) { + // Skip a leading `.\`. + read += 1; + last = match input.get(read) { + Some('/') => '\\', + Some(&character) => character, + None => '\0', + }; + while input + .get(read) + .is_some_and(|&character| separator(character)) + { + read += 1; + } + } + while read < input.len() { + let character = match input[read] { + '/' => '\\', + character => character, + }; + if last == '\\' { + if character == '.' { + let separator_at_1 = is_separator_or_end(read + 1); + let separator_at_2 = !separator_at_1 && is_separator_or_end(read + 2); + if separator_at_2 && input[read + 1] == '.' { + let mut start = output.len(); + while start != minimum { + start -= 1; + if output[start] != '\\' { + break; + } + } + while start != minimum && output[start - 1] != '\\' { + start -= 1; + } + let previous_is_parent = output.get(start) == Some(&'.') + && output.get(start + 1) == Some(&'.') + && output.get(start + 2).is_some_and(|&c| separator(c)); + if output.len() == minimum || previous_is_parent { + // Keep a `..` that has nothing to remove, or that follows another. + output.extend(['.', '.']); + last = '.'; + } else if output.get(start) == Some(&'\\') { + // At the root of an absolute path, `..` removes the component. + output.truncate(start + 1); + } else { + output.truncate(start); + } + read += 1; + } else if !separator_at_1 { + output.push(character); + last = character; + } + } else if character != '\\' { + output.push(character); + last = character; + } + } else { + output.push(character); + last = character; + } + read += 1; + } + // Drop trailing separators, but not the root. + let mut length = output.len(); + if length != minimum { + loop { + length -= 1; + if length == minimum || output[length] != '\\' { + break; + } + } + length += 1; + } + if length == 0 { + return ".".to_owned(); + } + output[..length].iter().collect() +} + +/// A pure path of one [`Flavor`], as `PurePosixPath` or `PureWindowsPath` holds it. +#[derive(Clone)] +pub(crate) struct PurePath { + /// The flavor of the path. + flavor: Flavor, + /// The drive: a drive letter and colon, or a UNC or device drive, on Windows; empty on POSIX. + drive: String, + /// The root: one separator, two slashes on POSIX, or empty for a relative path. + root: String, + /// The components after the anchor. + tail: Vec, + /// `str(path)`. + text: String, +} + +impl PurePath { + /// Returns the path of `text` in `flavor`, as `PurePosixPath(text)` or + /// `PureWindowsPath(text)` does. + pub(crate) fn new(flavor: Flavor, text: &str) -> Self { + let (drive, root, tail) = match flavor { + Flavor::Posix => { + let (root, tail) = parse_posix(text.as_bytes()); + ( + String::new(), + from_utf8(root), + tail.into_iter().map(from_utf8).collect(), + ) + } + Flavor::Windows => { + let units: Vec = text.encode_utf16().collect(); + let (drive, root, tail) = parse_windows(&units); + ( + from_utf16(&drive), + from_utf16(&root), + tail.iter().map(|part| from_utf16(part)).collect(), + ) + } + }; + Self::from_parts(flavor, drive, root, tail) + } + + /// Returns `pathlib.Path(text)` on this host. + pub(crate) fn native(text: &str) -> Self { + Self::new(Flavor::NATIVE, text) + } + + /// Returns the path of these parts, formatted as `pathlib` formats them. + fn from_parts(flavor: Flavor, drive: String, root: String, tail: Vec) -> Self { + let text = match flavor { + Flavor::Posix => { + let parts: Vec<&[u8]> = tail.iter().map(String::as_bytes).collect(); + from_utf8(&format_posix(root.as_bytes(), &parts)) + } + Flavor::Windows => { + let units = |text: &str| text.encode_utf16().collect::>(); + let parts: Vec> = tail.iter().map(|part| units(part)).collect(); + from_utf16(&format_windows(&units(&drive), &units(&root), &parts)) + } + }; + Self { + flavor, + drive, + root, + tail, + text, + } + } + + /// Returns the flavor of the path. + pub(crate) fn flavor(&self) -> Flavor { + self.flavor + } + + /// Returns `str(path)`. + pub(crate) fn as_str(&self) -> &str { + &self.text + } + + /// Returns `str(path)` as a host path. + pub(crate) fn as_path(&self) -> &Path { + Path::new(&self.text) + } + + /// Returns `path.parts`: the drive and root together, then each component. + pub(crate) fn parts(&self) -> Vec { + let anchor = format!("{}{}", self.drive, self.root); + (!anchor.is_empty()) + .then_some(anchor) + .into_iter() + .chain(self.tail.iter().cloned()) + .collect() + } + + /// Returns `path.name`: the last component, or an empty string. + pub(crate) fn name(&self) -> &str { + self.tail.last().map_or("", String::as_str) + } + + /// Returns `path.parents`, from the parent to the anchor. + pub(crate) fn parents(&self) -> impl Iterator + '_ { + (0..self.tail.len()).rev().map(|length| { + Self::from_parts( + self.flavor, + self.drive.clone(), + self.root.clone(), + self.tail[..length].to_vec(), + ) + }) + } + + /// Returns `path.is_absolute()`. + pub(crate) fn is_absolute(&self) -> bool { + self.flavor.is_absolute(&self.text) + } + + /// Returns `path / segment`. + #[must_use] + pub(crate) fn join(&self, segment: &str) -> Self { + Self::new(self.flavor, &self.flavor.join(&self.text, segment)) + } + + /// Returns `path.with_name(name)`, or `None` where it raises `ValueError`: for a name that is + /// empty, `.`, or holds a separator, and for a path without components. + pub(crate) fn with_name(&self, name: &str) -> Option { + if name.is_empty() || name == "." || name.chars().any(|c| self.flavor.is_separator(c)) { + return None; + } + let (_, rest) = self.tail.split_last()?; + let mut tail = rest.to_vec(); + tail.push(name.to_owned()); + Some(Self::from_parts( + self.flavor, + self.drive.clone(), + self.root.clone(), + tail, + )) + } + + /// Returns the string by which `pathlib` compares the path: `str(path)`, in lowercase on + /// Windows. + fn case_normalized(&self) -> std::borrow::Cow<'_, str> { + match self.flavor { + Flavor::Posix => self.text.as_str().into(), + Flavor::Windows => self.text.to_lowercase().into(), + } + } +} + +impl PartialEq for PurePath { + fn eq(&self, other: &Self) -> bool { + self.flavor == other.flavor && self.case_normalized() == other.case_normalized() + } +} + +impl Eq for PurePath {} + +impl fmt::Display for PurePath { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.text) + } +} + +impl fmt::Debug for PurePath { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let class = match self.flavor { + Flavor::Posix => "PurePosixPath", + Flavor::Windows => "PureWindowsPath", + }; + write!(formatter, "{class}({:?})", self.text) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn posix(path: &str) -> String { + String::from_utf8(posix_str(path.as_bytes())).unwrap() + } + + fn windows(path: &str) -> String { + String::from_utf16(&windows_str(&path.encode_utf16().collect::>())).unwrap() + } + + fn pure_posix(text: &str) -> PurePath { + PurePath::new(Flavor::Posix, text) + } + + fn pure_windows(text: &str) -> PurePath { + PurePath::new(Flavor::Windows, text) + } + + #[test] + fn paths_normalize_as_pathlib_does() { + // Expected values from Python 3.14's `PurePosixPath` and `PureWindowsPath` on Windows. + for (path, expected_posix, expected_windows) in [ + ("", ".", "."), + ("./", ".", "."), + ("a//b", "a/b", "a\\b"), + ("a/./b/.", "a/b", "a\\b"), + ("../a", "../a", "..\\a"), + ("/", "/", "\\"), + ("//", "//", "\\\\"), + ("///", "/", "\\\\\\"), + ("////a", "/a", "\\\\\\\\a"), + ("//a/b", "//a/b", "\\\\a\\b\\"), + ("c:/a", "c:/a", "c:\\a"), + ("c:\\a\\", "c:\\a\\", "c:\\a"), + ("C:/", "C:", "C:\\"), + ( + "\\\\server\\share", + "\\\\server\\share", + "\\\\server\\share\\", + ), + ("//?/UNC/a/b", "//?/UNC/a/b", "\\\\?\\UNC\\a\\b\\"), + ( + "\\\\?\\unc\\server\\share", + "\\\\?\\unc\\server\\share", + "\\\\?\\unc\\server\\share\\", + ), + ("\\\\.\\device\\x", "\\\\.\\device\\x", "\\\\.\\device\\x"), + ("\\\\\\share", "\\\\\\share", "\\\\\\share"), + ("./c:foo", "c:foo", ".\\c:foo"), + ("é:x", "é:x", "é:x"), + (".\\.\\", ".\\.\\", "."), + ("a /b", "a /b", "a \\b"), + // On Windows, a character beyond the Basic Multilingual Plane is two code units, so + // the colon after it makes no drive, and a NUL before a colon makes one. + ("\u{1F600}:x", "\u{1F600}:x", "\u{1F600}:x"), + ("./\u{1F600}:x", "\u{1F600}:x", "\u{1F600}:x"), + ("./\0:x", "\0:x", ".\\\0:x"), + ] { + assert_eq!(posix(path), expected_posix, "{path:?}"); + assert_eq!(windows(path), expected_windows, "{path:?}"); + assert_eq!(pure_posix(path).as_str(), expected_posix, "{path:?}"); + assert_eq!(pure_windows(path).as_str(), expected_windows, "{path:?}"); + } + } + + #[test] + fn unprintable_paths_render_as_python_prints_them() { + assert_eq!( + render_surrogateescape(b"a\xffb\xe2\x82"), + "a\\udcffb\\udce2\\udc82" + ); + assert_eq!(render_surrogateescape("é".as_bytes()), "é"); + assert_eq!(render_utf16(&[0x61, 0xD800, 0x62]), "a\\ud800b"); + assert_eq!(render_utf16(&[0xD83D, 0xDE00]), "\u{1F600}"); + } + + #[test] + fn hosts_open_the_normalized_path() { + let path = python_path(Path::new("a//b/./policy.json/")); + if cfg!(windows) { + assert_eq!(path.display, "a\\b\\policy.json"); + } else { + assert_eq!(path.display, "a/b/policy.json"); + } + assert_eq!(path.native, PathBuf::from(&path.display)); + assert_eq!( + path.display, + PurePath::native("a//b/./policy.json/").as_str() + ); + } + + #[test] + fn posix_paths_hold_their_parts() { + assert!(pure_posix("//a").is_absolute()); + assert!(!pure_posix("a/b").is_absolute()); + assert_eq!(pure_posix("/a/b").parts(), ["/", "a", "b"]); + assert_eq!(pure_posix("a/b").name(), "b"); + assert_eq!( + pure_posix("/a/b/c") + .parents() + .map(|p| p.text) + .collect::>(), + ["/a/b", "/a", "/"] + ); + assert_eq!(pure_posix("/a/b").join("c").as_str(), "/a/b/c"); + assert_eq!(pure_posix("/a/b").join("/c").as_str(), "/c"); + assert_ne!(pure_posix("/A"), pure_posix("/a")); + assert_eq!( + pure_posix("/a/b.erofs") + .with_name("b.erofs.manifest.json") + .unwrap() + .as_str(), + "/a/b.erofs.manifest.json" + ); + assert!(pure_posix("/").with_name("x").is_none()); + } + + #[test] + fn windows_paths_hold_their_parts() { + assert!(pure_windows("C:\\a").is_absolute()); + assert!(pure_windows("//server/share").is_absolute()); + assert!(!pure_windows("\\a").is_absolute()); + assert!(!pure_windows("C:a").is_absolute()); + // `ntpath.isabs` tests code points, even where `splitroot` finds no drive. + assert!(pure_windows("\u{1F600}:\\x").is_absolute()); + assert_eq!(pure_windows("\u{1F600}:\\x").parts(), ["\u{1F600}:", "x"]); + assert_eq!(pure_windows("C:\\A"), pure_windows("c:/a")); + assert_eq!(pure_windows("C:\\a\\b").parts(), ["C:\\", "a", "b"]); + assert_eq!( + pure_windows("C:\\cwd").join("a/b").as_str(), + "C:\\cwd\\a\\b" + ); + assert_eq!(pure_windows("C:\\cwd").join("C:x").as_str(), "C:\\cwd\\x"); + assert_eq!(pure_windows("C:\\cwd").join("D:x").as_str(), "D:x"); + assert_eq!(pure_windows("C:\\cwd").join("\\x").as_str(), "C:\\x"); + assert_eq!( + Flavor::Windows.split_root("//server/share/x"), + ("//server/share", "/", "x") + ); + assert_eq!( + Flavor::Windows.split_root("\u{1F600}:x"), + ("", "", "\u{1F600}:x") + ); + assert_eq!(Flavor::Windows.split_root("é:\\x"), ("é:", "\\", "x")); + } + + #[test] + fn windows_normpath_follows_cpython() { + for (text, expected) in [ + ("", "."), + (".", "."), + ("a", "a"), + ("a\\", "a"), + ("A//B", "A\\B"), + ("A/./B", "A\\B"), + ("A/foo/../B", "A\\B"), + ("C:\\", "C:\\"), + ("C:", "C:"), + ("C:a\\..\\..", "C:.."), + ("C:\\..\\a", "C:\\a"), + ("..\\..\\a", "..\\..\\a"), + ("a\\..\\..\\b", "..\\b"), + ("\\\\server\\share\\", "\\\\server\\share\\"), + ("\\\\server\\share\\a\\..", "\\\\server\\share\\"), + (".\\a\\.\\b\\", "a\\b"), + ("\\", "\\"), + ("\u{1F600}:x\\..\\y", "y"), + ("\0:x\\..\\y", "\0:y"), + ] { + assert_eq!(windows_normpath(text), expected, "{text:?}"); + } + } + + #[test] + fn split_returns_the_head_and_the_last_component() { + assert_eq!( + Flavor::Windows.split("C:\\a\\b"), + ("C:\\a".to_owned(), "b".to_owned()) + ); + assert_eq!( + Flavor::Windows.split("C:\\a"), + ("C:\\".to_owned(), "a".to_owned()) + ); + assert_eq!( + Flavor::Windows.split("C:\\"), + ("C:\\".to_owned(), String::new()) + ); + assert_eq!( + Flavor::Posix.split("/a/b"), + ("/a".to_owned(), "b".to_owned()) + ); + assert_eq!(Flavor::Posix.split("/a"), ("/".to_owned(), "a".to_owned())); + assert_eq!( + Flavor::Posix.split("//a"), + ("//".to_owned(), "a".to_owned()) + ); + } +} diff --git a/nvx-rs/src/host/python/shell.rs b/nvx-rs/src/host/python/shell.rs new file mode 100644 index 000000000..66716524f --- /dev/null +++ b/nvx-rs/src/host/python/shell.rs @@ -0,0 +1,121 @@ +//! How Python quotes command lines for display: `subprocess.list2cmdline` on Windows, and +//! `shlex.join` elsewhere. +//! +//! `nvx.py` prints the OpenVMM command that it runs as `>> COMMAND`, quoted with +//! [`list2cmdline`] on Windows and [`shlex_join`] on other hosts; [`format_command`] picks the +//! one of this host. + +/// Returns `subprocess.list2cmdline(arguments)`: the arguments joined by spaces, as the +/// Microsoft C runtime splits them again. +/// +/// An argument that is empty or holds a space or a tab is quoted. Backslashes are literal except +/// before a quote, where they are doubled and the quote escaped, and before the closing quote of +/// a quoted argument, where they are doubled. +pub(crate) fn list2cmdline>(arguments: &[S]) -> String { + let mut result = String::new(); + for argument in arguments { + let argument = argument.as_ref(); + if !result.is_empty() { + result.push(' '); + } + let quoted = argument.is_empty() || argument.contains([' ', '\t']); + if quoted { + result.push('"'); + } + let mut backslashes = 0; + for character in argument.chars() { + match character { + '\\' => backslashes += 1, + '"' => { + result.extend(std::iter::repeat_n('\\', backslashes * 2)); + backslashes = 0; + result.push_str("\\\""); + } + _ => { + result.extend(std::iter::repeat_n('\\', backslashes)); + backslashes = 0; + result.push(character); + } + } + } + result.extend(std::iter::repeat_n('\\', backslashes)); + if quoted { + result.extend(std::iter::repeat_n('\\', backslashes)); + result.push('"'); + } + } + result +} + +/// Returns `shlex.quote(text)`: `text` itself if it is a nonempty run of the characters that a +/// POSIX shell keeps as they are, `[A-Za-z0-9_@%+=:,./-]`, and otherwise `text` in single +/// quotes, with each single quote written as `'"'"'`. +pub(crate) fn shlex_quote(text: &str) -> String { + if text.is_empty() { + return "''".to_owned(); + } + let safe = |byte: u8| byte.is_ascii_alphanumeric() || b"_@%+=:,./-".contains(&byte); + if text.bytes().all(safe) { + return text.to_owned(); + } + format!("'{}'", text.replace('\'', "'\"'\"'")) +} + +/// Returns `shlex.join(arguments)`: each argument quoted with [`shlex_quote`], joined by spaces. +pub(crate) fn shlex_join>(arguments: &[S]) -> String { + arguments + .iter() + .map(|argument| shlex_quote(argument.as_ref())) + .collect::>() + .join(" ") +} + +/// Returns `_format_command(arguments)` of `nvx.py`: [`list2cmdline`] on Windows, and +/// [`shlex_join`] on other hosts. +pub(crate) fn format_command>(arguments: &[S]) -> String { + if cfg!(windows) { + list2cmdline(arguments) + } else { + shlex_join(arguments) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn list2cmdline_quotes_as_the_c_runtime_reads() { + let cases: [(&[&str], &str); 9] = [ + (&["a", "b c", ""], "a \"b c\" \"\""), + (&["a\tb"], "\"a\tb\""), + (&[r#"say "hi""#], r#""say \"hi\"""#), + (&[r#"a"b"#], r#"a\"b"#), + (&[r"a\\b"], r"a\\b"), + (&[r#"a\"b"#], r#"a\\\"b"#), + (&[r"dir\ with\"], r#""dir\ with\\""#), + (&[r"C:\path\"], r"C:\path\"), + (&[r#"x\\" y"#], r#""x\\\\\" y""#), + ]; + for (arguments, expected) in cases { + assert_eq!(list2cmdline(arguments), expected, "{arguments:?}"); + } + } + + #[test] + fn shlex_quotes_what_a_shell_would_expand() { + let cases: [(&str, &str); 7] = [ + ("", "''"), + ("plain-_@%+=:,./0", "plain-_@%+=:,./0"), + ("a b", "'a b'"), + ("it's", "'it'\"'\"'s'"), + ("$HOME", "'$HOME'"), + ("é", "'é'"), + ("a\nb", "'a\nb'"), + ]; + for (text, expected) in cases { + assert_eq!(shlex_quote(text), expected, "{text:?}"); + } + assert_eq!(shlex_join(&["/bin/echo", "a b", ""]), "/bin/echo 'a b' ''"); + } +} diff --git a/nvx-rs/src/host/python/text.rs b/nvx-rs/src/host/python/text.rs new file mode 100644 index 000000000..9d4dbf851 --- /dev/null +++ b/nvx-rs/src/host/python/text.rs @@ -0,0 +1,287 @@ +//! Strings as Python holds them. +//! +//! A Python `str` is a sequence of code points, and unlike a Rust `str` it may hold unpaired +//! surrogates: Python's JSON decoder keeps a lone `\ud800` escape as is, and Python decodes +//! command-line paths that are not valid Unicode into surrogates. [`PyText`] keeps such strings +//! exactly, so the egress policy compiler compares and orders them as Python does, and +//! [`PyText::render`] shows them the way Python prints them. +//! +//! Python's UTF-8 codec refuses what Rust's would, with messages of its own: +//! [`PyText::encode_utf8`] and [`decode_utf8`] give them. + +use std::fmt; + +/// Returns `bytes` as a string, or the message of the `UnicodeDecodeError` with which Python's +/// UTF-8 codec refuses them, such as `'utf-8' codec can't decode byte 0xff in position 0: invalid +/// start byte`. +pub(crate) fn decode_utf8(bytes: &[u8]) -> Result<&str, String> { + std::str::from_utf8(bytes).map_err(|error| { + let start = error.valid_up_to(); + // Rust and Python agree on the longest invalid sequence, so only the reason differs. + let (end, reason) = match error.error_len() { + None => (bytes.len(), "unexpected end of data"), + Some(length) => match bytes[start] { + 0x80..=0xC1 | 0xF5..=0xFF => (start + 1, "invalid start byte"), + _ => (start + length, "invalid continuation byte"), + }, + }; + if end == start + 1 { + format!( + "'utf-8' codec can't decode byte 0x{:02x} in position {start}: {reason}", + bytes[start] + ) + } else { + format!( + "'utf-8' codec can't decode bytes in position {start}-{}: {reason}", + end - 1 + ) + } + }) +} + +/// A Python `str`: Unicode scalar values and unpaired surrogates. +/// +/// Each code point is held in its UTF-8 form, a surrogate in the three-byte form that UTF-8 would +/// give it. Strings without surrogates are therefore plain UTF-8, and byte order is code-point +/// order, which is how Python orders strings. +#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct PyText(Vec); + +impl PyText { + /// Returns an empty string. + pub(crate) fn new() -> Self { + Self(Vec::new()) + } + + /// Returns the bytes of the string: UTF-8, with surrogates in their three-byte form. + pub(crate) fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Returns the string as a Rust string, or `None` if it holds a surrogate. + pub(crate) fn as_str(&self) -> Option<&str> { + std::str::from_utf8(&self.0).ok() + } + + /// Returns the string as `str.encode('utf-8')` encodes it, or the message of the + /// `UnicodeEncodeError` with which Python refuses its first run of surrogates, such as + /// `'utf-8' codec can't encode character '\ud800' in position 2: surrogates not allowed`. + pub(crate) fn encode_utf8(&self) -> Result<&str, String> { + if let Some(text) = self.as_str() { + return Ok(text); + } + let code_points: Vec = self.code_points().collect(); + let surrogate = |code_point: &u32| (0xD800..=0xDFFF).contains(code_point); + let start = code_points.iter().position(surrogate).unwrap_or_default(); + let end = code_points[start..] + .iter() + .position(|code_point| !surrogate(code_point)) + .map_or(code_points.len(), |length| start + length); + Err(if end == start + 1 { + format!( + "'utf-8' codec can't encode character '\\u{:04x}' in position {start}: \ + surrogates not allowed", + code_points[start] + ) + } else { + format!( + "'utf-8' codec can't encode characters in position {start}-{}: surrogates not \ + allowed", + end - 1 + ) + }) + } + + /// Appends a Rust string. + pub(crate) fn push_str(&mut self, text: &str) { + self.0.extend_from_slice(text.as_bytes()); + } + + /// Appends a code point, which may be a surrogate. `code_point` must not exceed `0x10FFFF`. + pub(crate) fn push_code_point(&mut self, code_point: u32) { + match char::from_u32(code_point) { + Some(character) => { + let mut buffer = [0; 4]; + self.push_str(character.encode_utf8(&mut buffer)); + } + // Only surrogates, 0xD800 through 0xDFFF, are code points but not characters. + None => self.0.extend_from_slice(&[ + 0xE0 | (code_point >> 12) as u8, + 0x80 | ((code_point >> 6) & 0x3F) as u8, + 0x80 | (code_point & 0x3F) as u8, + ]), + } + } + + /// Returns the code points of the string. + pub(crate) fn code_points(&self) -> CodePoints<'_> { + CodePoints(&self.0) + } + + /// Returns the string as Python prints it to a UTF-8 stream, which escapes each surrogate as + /// `\udXXX` with its `backslashreplace` error handler. Strings without surrogates are + /// returned unchanged. + pub(crate) fn render(&self) -> String { + if let Some(text) = self.as_str() { + return text.to_owned(); + } + let mut rendered = String::with_capacity(self.0.len() + 8); + for code_point in self.code_points() { + match char::from_u32(code_point) { + Some(character) => rendered.push(character), + None => rendered.push_str(&format!("\\u{code_point:04x}")), + } + } + rendered + } +} + +impl From<&str> for PyText { + fn from(text: &str) -> Self { + Self(text.as_bytes().to_vec()) + } +} + +impl fmt::Debug for PyText { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.render(), formatter) + } +} + +/// The code points of a [`PyText`], from first to last. +pub(crate) struct CodePoints<'a>(&'a [u8]); + +impl Iterator for CodePoints<'_> { + type Item = u32; + + fn next(&mut self) -> Option { + let (&lead, rest) = self.0.split_first()?; + // A `PyText` holds well-formed sequences only, so the lead byte gives the length. + let (length, initial) = match lead { + 0x00..=0x7F => (1, u32::from(lead)), + 0xC0..=0xDF => (2, u32::from(lead & 0x1F)), + 0xE0..=0xEF => (3, u32::from(lead & 0x0F)), + _ => (4, u32::from(lead & 0x07)), + }; + let continuation = &rest[..length - 1]; + self.0 = &rest[length - 1..]; + Some(continuation.iter().fold(initial, |code_point, byte| { + (code_point << 6) | u32::from(byte & 0x3F) + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn surrogates_render_as_python_escapes() { + let mut text = PyText::from("a"); + text.push_code_point(0xD800); + text.push_code_point(0x1F600); + text.push_code_point(0xDFFF); + text.push_code_point(u32::from('é')); + assert_eq!(text.as_str(), None); + assert_eq!(text.render(), "a\\ud800\u{1F600}\\udfffé"); + assert_eq!( + text.code_points().collect::>(), + [0x61, 0xD800, 0x1F600, 0xDFFF, 0xE9] + ); + assert_eq!(PyText::from("plain").render(), "plain"); + assert_eq!(PyText::from("plain").as_str(), Some("plain")); + } + + #[test] + fn strings_order_by_code_point() { + let surrogate = { + let mut text = PyText::new(); + text.push_code_point(0xD800); + text + }; + // Python orders `'\ud7ff' < '\ud800' < '\ue000' < '\U00010000'`. + let mut texts = vec![ + PyText::from("\u{10000}"), + PyText::from("\u{E000}"), + surrogate.clone(), + PyText::from("\u{D7FF}"), + PyText::from("b"), + PyText::from("ab"), + PyText::from("a"), + ]; + texts.sort(); + assert_eq!( + texts, + [ + PyText::from("a"), + PyText::from("ab"), + PyText::from("b"), + PyText::from("\u{D7FF}"), + surrogate, + PyText::from("\u{E000}"), + PyText::from("\u{10000}"), + ] + ); + } + + #[test] + fn encoding_fails_at_the_first_run_of_surrogates() { + assert_eq!(PyText::from("plain é").encode_utf8(), Ok("plain é")); + let mut text = PyText::from("ab"); + text.push_code_point(0xD800); + text.push_str("c"); + text.push_code_point(0xDC00); + text.push_code_point(0xD800); + assert_eq!( + text.encode_utf8(), + Err( + "'utf-8' codec can't encode character '\\ud800' in position 2: surrogates not \ + allowed" + .to_owned() + ) + ); + let mut text = PyText::from("\u{1F600}"); + text.push_code_point(0xDC00); + text.push_code_point(0xD800); + assert_eq!( + text.encode_utf8(), + Err( + "'utf-8' codec can't encode characters in position 1-2: surrogates not allowed" + .to_owned() + ) + ); + } + + #[test] + fn decoding_fails_as_python_reports_it() { + assert_eq!(decode_utf8(b"ok \xc3\xa9"), Ok("ok é")); + for (bytes, expected) in [ + ( + &b"\xff"[..], + "'utf-8' codec can't decode byte 0xff in position 0: invalid start byte", + ), + ( + b"a\xe2\x82", + "'utf-8' codec can't decode bytes in position 1-2: unexpected end of data", + ), + ( + b"\xe2(", + "'utf-8' codec can't decode byte 0xe2 in position 0: invalid continuation byte", + ), + ( + b"\xf0\x90(", + "'utf-8' codec can't decode bytes in position 0-1: invalid continuation byte", + ), + ( + b"\xed\xa0\x80", + "'utf-8' codec can't decode byte 0xed in position 0: invalid continuation byte", + ), + ( + b"\xc0\x80", + "'utf-8' codec can't decode byte 0xc0 in position 0: invalid start byte", + ), + ] { + assert_eq!(decode_utf8(bytes), Err(expected.to_owned()), "{bytes:?}"); + } + } +} diff --git a/nvx-rs/src/host/python/unicode.rs b/nvx-rs/src/host/python/unicode.rs new file mode 100644 index 000000000..d03df740d --- /dev/null +++ b/nvx-rs/src/host/python/unicode.rs @@ -0,0 +1,211 @@ +//! Unicode properties of Python's `str`, from the Unicode 16.0.0 database +//! of Python 3.14. +//! +//! Generated from Python 3.14's `unicodedata` by `scripts/nvx_rs_fixtures.py`, which +//! microsoft/nvx#359 retired. + +/// The first code point of each run of ten decimal digits, of category Nd, which `int` +/// reads as the digits zero to nine. +#[rustfmt::skip] +pub(crate) const DECIMAL_STARTS: &[u32] = &[ + 0x00030, 0x00660, 0x006F0, 0x007C0, 0x00966, 0x009E6, 0x00A66, 0x00AE6, + 0x00B66, 0x00BE6, 0x00C66, 0x00CE6, 0x00D66, 0x00DE6, 0x00E50, 0x00ED0, + 0x00F20, 0x01040, 0x01090, 0x017E0, 0x01810, 0x01946, 0x019D0, 0x01A80, + 0x01A90, 0x01B50, 0x01BB0, 0x01C40, 0x01C50, 0x0A620, 0x0A8D0, 0x0A900, + 0x0A9D0, 0x0A9F0, 0x0AA50, 0x0ABF0, 0x0FF10, 0x104A0, 0x10D30, 0x10D40, + 0x11066, 0x110F0, 0x11136, 0x111D0, 0x112F0, 0x11450, 0x114D0, 0x11650, + 0x116C0, 0x116D0, 0x116DA, 0x11730, 0x118E0, 0x11950, 0x11BF0, 0x11C50, + 0x11D50, 0x11DA0, 0x11F50, 0x16130, 0x16A60, 0x16AC0, 0x16B50, 0x16D70, + 0x1CCF0, 0x1D7CE, 0x1D7D8, 0x1D7E2, 0x1D7EC, 0x1D7F6, 0x1E140, 0x1E2F0, + 0x1E4F0, 0x1E5F1, 0x1E950, 0x1FBF0, +]; + +/// The inclusive ranges of the code points from U+0080 for which `str.isprintable` is false, +/// in order: `repr` escapes them. +#[rustfmt::skip] +pub(crate) const NON_PRINTABLE: &[(u32, u32)] = &[ + (0x00080, 0x000A0), (0x000AD, 0x000AD), (0x00378, 0x00379), (0x00380, 0x00383), + (0x0038B, 0x0038B), (0x0038D, 0x0038D), (0x003A2, 0x003A2), (0x00530, 0x00530), + (0x00557, 0x00558), (0x0058B, 0x0058C), (0x00590, 0x00590), (0x005C8, 0x005CF), + (0x005EB, 0x005EE), (0x005F5, 0x00605), (0x0061C, 0x0061C), (0x006DD, 0x006DD), + (0x0070E, 0x0070F), (0x0074B, 0x0074C), (0x007B2, 0x007BF), (0x007FB, 0x007FC), + (0x0082E, 0x0082F), (0x0083F, 0x0083F), (0x0085C, 0x0085D), (0x0085F, 0x0085F), + (0x0086B, 0x0086F), (0x0088F, 0x00896), (0x008E2, 0x008E2), (0x00984, 0x00984), + (0x0098D, 0x0098E), (0x00991, 0x00992), (0x009A9, 0x009A9), (0x009B1, 0x009B1), + (0x009B3, 0x009B5), (0x009BA, 0x009BB), (0x009C5, 0x009C6), (0x009C9, 0x009CA), + (0x009CF, 0x009D6), (0x009D8, 0x009DB), (0x009DE, 0x009DE), (0x009E4, 0x009E5), + (0x009FF, 0x00A00), (0x00A04, 0x00A04), (0x00A0B, 0x00A0E), (0x00A11, 0x00A12), + (0x00A29, 0x00A29), (0x00A31, 0x00A31), (0x00A34, 0x00A34), (0x00A37, 0x00A37), + (0x00A3A, 0x00A3B), (0x00A3D, 0x00A3D), (0x00A43, 0x00A46), (0x00A49, 0x00A4A), + (0x00A4E, 0x00A50), (0x00A52, 0x00A58), (0x00A5D, 0x00A5D), (0x00A5F, 0x00A65), + (0x00A77, 0x00A80), (0x00A84, 0x00A84), (0x00A8E, 0x00A8E), (0x00A92, 0x00A92), + (0x00AA9, 0x00AA9), (0x00AB1, 0x00AB1), (0x00AB4, 0x00AB4), (0x00ABA, 0x00ABB), + (0x00AC6, 0x00AC6), (0x00ACA, 0x00ACA), (0x00ACE, 0x00ACF), (0x00AD1, 0x00ADF), + (0x00AE4, 0x00AE5), (0x00AF2, 0x00AF8), (0x00B00, 0x00B00), (0x00B04, 0x00B04), + (0x00B0D, 0x00B0E), (0x00B11, 0x00B12), (0x00B29, 0x00B29), (0x00B31, 0x00B31), + (0x00B34, 0x00B34), (0x00B3A, 0x00B3B), (0x00B45, 0x00B46), (0x00B49, 0x00B4A), + (0x00B4E, 0x00B54), (0x00B58, 0x00B5B), (0x00B5E, 0x00B5E), (0x00B64, 0x00B65), + (0x00B78, 0x00B81), (0x00B84, 0x00B84), (0x00B8B, 0x00B8D), (0x00B91, 0x00B91), + (0x00B96, 0x00B98), (0x00B9B, 0x00B9B), (0x00B9D, 0x00B9D), (0x00BA0, 0x00BA2), + (0x00BA5, 0x00BA7), (0x00BAB, 0x00BAD), (0x00BBA, 0x00BBD), (0x00BC3, 0x00BC5), + (0x00BC9, 0x00BC9), (0x00BCE, 0x00BCF), (0x00BD1, 0x00BD6), (0x00BD8, 0x00BE5), + (0x00BFB, 0x00BFF), (0x00C0D, 0x00C0D), (0x00C11, 0x00C11), (0x00C29, 0x00C29), + (0x00C3A, 0x00C3B), (0x00C45, 0x00C45), (0x00C49, 0x00C49), (0x00C4E, 0x00C54), + (0x00C57, 0x00C57), (0x00C5B, 0x00C5C), (0x00C5E, 0x00C5F), (0x00C64, 0x00C65), + (0x00C70, 0x00C76), (0x00C8D, 0x00C8D), (0x00C91, 0x00C91), (0x00CA9, 0x00CA9), + (0x00CB4, 0x00CB4), (0x00CBA, 0x00CBB), (0x00CC5, 0x00CC5), (0x00CC9, 0x00CC9), + (0x00CCE, 0x00CD4), (0x00CD7, 0x00CDC), (0x00CDF, 0x00CDF), (0x00CE4, 0x00CE5), + (0x00CF0, 0x00CF0), (0x00CF4, 0x00CFF), (0x00D0D, 0x00D0D), (0x00D11, 0x00D11), + (0x00D45, 0x00D45), (0x00D49, 0x00D49), (0x00D50, 0x00D53), (0x00D64, 0x00D65), + (0x00D80, 0x00D80), (0x00D84, 0x00D84), (0x00D97, 0x00D99), (0x00DB2, 0x00DB2), + (0x00DBC, 0x00DBC), (0x00DBE, 0x00DBF), (0x00DC7, 0x00DC9), (0x00DCB, 0x00DCE), + (0x00DD5, 0x00DD5), (0x00DD7, 0x00DD7), (0x00DE0, 0x00DE5), (0x00DF0, 0x00DF1), + (0x00DF5, 0x00E00), (0x00E3B, 0x00E3E), (0x00E5C, 0x00E80), (0x00E83, 0x00E83), + (0x00E85, 0x00E85), (0x00E8B, 0x00E8B), (0x00EA4, 0x00EA4), (0x00EA6, 0x00EA6), + (0x00EBE, 0x00EBF), (0x00EC5, 0x00EC5), (0x00EC7, 0x00EC7), (0x00ECF, 0x00ECF), + (0x00EDA, 0x00EDB), (0x00EE0, 0x00EFF), (0x00F48, 0x00F48), (0x00F6D, 0x00F70), + (0x00F98, 0x00F98), (0x00FBD, 0x00FBD), (0x00FCD, 0x00FCD), (0x00FDB, 0x00FFF), + (0x010C6, 0x010C6), (0x010C8, 0x010CC), (0x010CE, 0x010CF), (0x01249, 0x01249), + (0x0124E, 0x0124F), (0x01257, 0x01257), (0x01259, 0x01259), (0x0125E, 0x0125F), + (0x01289, 0x01289), (0x0128E, 0x0128F), (0x012B1, 0x012B1), (0x012B6, 0x012B7), + (0x012BF, 0x012BF), (0x012C1, 0x012C1), (0x012C6, 0x012C7), (0x012D7, 0x012D7), + (0x01311, 0x01311), (0x01316, 0x01317), (0x0135B, 0x0135C), (0x0137D, 0x0137F), + (0x0139A, 0x0139F), (0x013F6, 0x013F7), (0x013FE, 0x013FF), (0x01680, 0x01680), + (0x0169D, 0x0169F), (0x016F9, 0x016FF), (0x01716, 0x0171E), (0x01737, 0x0173F), + (0x01754, 0x0175F), (0x0176D, 0x0176D), (0x01771, 0x01771), (0x01774, 0x0177F), + (0x017DE, 0x017DF), (0x017EA, 0x017EF), (0x017FA, 0x017FF), (0x0180E, 0x0180E), + (0x0181A, 0x0181F), (0x01879, 0x0187F), (0x018AB, 0x018AF), (0x018F6, 0x018FF), + (0x0191F, 0x0191F), (0x0192C, 0x0192F), (0x0193C, 0x0193F), (0x01941, 0x01943), + (0x0196E, 0x0196F), (0x01975, 0x0197F), (0x019AC, 0x019AF), (0x019CA, 0x019CF), + (0x019DB, 0x019DD), (0x01A1C, 0x01A1D), (0x01A5F, 0x01A5F), (0x01A7D, 0x01A7E), + (0x01A8A, 0x01A8F), (0x01A9A, 0x01A9F), (0x01AAE, 0x01AAF), (0x01ACF, 0x01AFF), + (0x01B4D, 0x01B4D), (0x01BF4, 0x01BFB), (0x01C38, 0x01C3A), (0x01C4A, 0x01C4C), + (0x01C8B, 0x01C8F), (0x01CBB, 0x01CBC), (0x01CC8, 0x01CCF), (0x01CFB, 0x01CFF), + (0x01F16, 0x01F17), (0x01F1E, 0x01F1F), (0x01F46, 0x01F47), (0x01F4E, 0x01F4F), + (0x01F58, 0x01F58), (0x01F5A, 0x01F5A), (0x01F5C, 0x01F5C), (0x01F5E, 0x01F5E), + (0x01F7E, 0x01F7F), (0x01FB5, 0x01FB5), (0x01FC5, 0x01FC5), (0x01FD4, 0x01FD5), + (0x01FDC, 0x01FDC), (0x01FF0, 0x01FF1), (0x01FF5, 0x01FF5), (0x01FFF, 0x0200F), + (0x02028, 0x0202F), (0x0205F, 0x0206F), (0x02072, 0x02073), (0x0208F, 0x0208F), + (0x0209D, 0x0209F), (0x020C1, 0x020CF), (0x020F1, 0x020FF), (0x0218C, 0x0218F), + (0x0242A, 0x0243F), (0x0244B, 0x0245F), (0x02B74, 0x02B75), (0x02B96, 0x02B96), + (0x02CF4, 0x02CF8), (0x02D26, 0x02D26), (0x02D28, 0x02D2C), (0x02D2E, 0x02D2F), + (0x02D68, 0x02D6E), (0x02D71, 0x02D7E), (0x02D97, 0x02D9F), (0x02DA7, 0x02DA7), + (0x02DAF, 0x02DAF), (0x02DB7, 0x02DB7), (0x02DBF, 0x02DBF), (0x02DC7, 0x02DC7), + (0x02DCF, 0x02DCF), (0x02DD7, 0x02DD7), (0x02DDF, 0x02DDF), (0x02E5E, 0x02E7F), + (0x02E9A, 0x02E9A), (0x02EF4, 0x02EFF), (0x02FD6, 0x02FEF), (0x03000, 0x03000), + (0x03040, 0x03040), (0x03097, 0x03098), (0x03100, 0x03104), (0x03130, 0x03130), + (0x0318F, 0x0318F), (0x031E6, 0x031EE), (0x0321F, 0x0321F), (0x0A48D, 0x0A48F), + (0x0A4C7, 0x0A4CF), (0x0A62C, 0x0A63F), (0x0A6F8, 0x0A6FF), (0x0A7CE, 0x0A7CF), + (0x0A7D2, 0x0A7D2), (0x0A7D4, 0x0A7D4), (0x0A7DD, 0x0A7F1), (0x0A82D, 0x0A82F), + (0x0A83A, 0x0A83F), (0x0A878, 0x0A87F), (0x0A8C6, 0x0A8CD), (0x0A8DA, 0x0A8DF), + (0x0A954, 0x0A95E), (0x0A97D, 0x0A97F), (0x0A9CE, 0x0A9CE), (0x0A9DA, 0x0A9DD), + (0x0A9FF, 0x0A9FF), (0x0AA37, 0x0AA3F), (0x0AA4E, 0x0AA4F), (0x0AA5A, 0x0AA5B), + (0x0AAC3, 0x0AADA), (0x0AAF7, 0x0AB00), (0x0AB07, 0x0AB08), (0x0AB0F, 0x0AB10), + (0x0AB17, 0x0AB1F), (0x0AB27, 0x0AB27), (0x0AB2F, 0x0AB2F), (0x0AB6C, 0x0AB6F), + (0x0ABEE, 0x0ABEF), (0x0ABFA, 0x0ABFF), (0x0D7A4, 0x0D7AF), (0x0D7C7, 0x0D7CA), + (0x0D7FC, 0x0F8FF), (0x0FA6E, 0x0FA6F), (0x0FADA, 0x0FAFF), (0x0FB07, 0x0FB12), + (0x0FB18, 0x0FB1C), (0x0FB37, 0x0FB37), (0x0FB3D, 0x0FB3D), (0x0FB3F, 0x0FB3F), + (0x0FB42, 0x0FB42), (0x0FB45, 0x0FB45), (0x0FBC3, 0x0FBD2), (0x0FD90, 0x0FD91), + (0x0FDC8, 0x0FDCE), (0x0FDD0, 0x0FDEF), (0x0FE1A, 0x0FE1F), (0x0FE53, 0x0FE53), + (0x0FE67, 0x0FE67), (0x0FE6C, 0x0FE6F), (0x0FE75, 0x0FE75), (0x0FEFD, 0x0FF00), + (0x0FFBF, 0x0FFC1), (0x0FFC8, 0x0FFC9), (0x0FFD0, 0x0FFD1), (0x0FFD8, 0x0FFD9), + (0x0FFDD, 0x0FFDF), (0x0FFE7, 0x0FFE7), (0x0FFEF, 0x0FFFB), (0x0FFFE, 0x0FFFF), + (0x1000C, 0x1000C), (0x10027, 0x10027), (0x1003B, 0x1003B), (0x1003E, 0x1003E), + (0x1004E, 0x1004F), (0x1005E, 0x1007F), (0x100FB, 0x100FF), (0x10103, 0x10106), + (0x10134, 0x10136), (0x1018F, 0x1018F), (0x1019D, 0x1019F), (0x101A1, 0x101CF), + (0x101FE, 0x1027F), (0x1029D, 0x1029F), (0x102D1, 0x102DF), (0x102FC, 0x102FF), + (0x10324, 0x1032C), (0x1034B, 0x1034F), (0x1037B, 0x1037F), (0x1039E, 0x1039E), + (0x103C4, 0x103C7), (0x103D6, 0x103FF), (0x1049E, 0x1049F), (0x104AA, 0x104AF), + (0x104D4, 0x104D7), (0x104FC, 0x104FF), (0x10528, 0x1052F), (0x10564, 0x1056E), + (0x1057B, 0x1057B), (0x1058B, 0x1058B), (0x10593, 0x10593), (0x10596, 0x10596), + (0x105A2, 0x105A2), (0x105B2, 0x105B2), (0x105BA, 0x105BA), (0x105BD, 0x105BF), + (0x105F4, 0x105FF), (0x10737, 0x1073F), (0x10756, 0x1075F), (0x10768, 0x1077F), + (0x10786, 0x10786), (0x107B1, 0x107B1), (0x107BB, 0x107FF), (0x10806, 0x10807), + (0x10809, 0x10809), (0x10836, 0x10836), (0x10839, 0x1083B), (0x1083D, 0x1083E), + (0x10856, 0x10856), (0x1089F, 0x108A6), (0x108B0, 0x108DF), (0x108F3, 0x108F3), + (0x108F6, 0x108FA), (0x1091C, 0x1091E), (0x1093A, 0x1093E), (0x10940, 0x1097F), + (0x109B8, 0x109BB), (0x109D0, 0x109D1), (0x10A04, 0x10A04), (0x10A07, 0x10A0B), + (0x10A14, 0x10A14), (0x10A18, 0x10A18), (0x10A36, 0x10A37), (0x10A3B, 0x10A3E), + (0x10A49, 0x10A4F), (0x10A59, 0x10A5F), (0x10AA0, 0x10ABF), (0x10AE7, 0x10AEA), + (0x10AF7, 0x10AFF), (0x10B36, 0x10B38), (0x10B56, 0x10B57), (0x10B73, 0x10B77), + (0x10B92, 0x10B98), (0x10B9D, 0x10BA8), (0x10BB0, 0x10BFF), (0x10C49, 0x10C7F), + (0x10CB3, 0x10CBF), (0x10CF3, 0x10CF9), (0x10D28, 0x10D2F), (0x10D3A, 0x10D3F), + (0x10D66, 0x10D68), (0x10D86, 0x10D8D), (0x10D90, 0x10E5F), (0x10E7F, 0x10E7F), + (0x10EAA, 0x10EAA), (0x10EAE, 0x10EAF), (0x10EB2, 0x10EC1), (0x10EC5, 0x10EFB), + (0x10F28, 0x10F2F), (0x10F5A, 0x10F6F), (0x10F8A, 0x10FAF), (0x10FCC, 0x10FDF), + (0x10FF7, 0x10FFF), (0x1104E, 0x11051), (0x11076, 0x1107E), (0x110BD, 0x110BD), + (0x110C3, 0x110CF), (0x110E9, 0x110EF), (0x110FA, 0x110FF), (0x11135, 0x11135), + (0x11148, 0x1114F), (0x11177, 0x1117F), (0x111E0, 0x111E0), (0x111F5, 0x111FF), + (0x11212, 0x11212), (0x11242, 0x1127F), (0x11287, 0x11287), (0x11289, 0x11289), + (0x1128E, 0x1128E), (0x1129E, 0x1129E), (0x112AA, 0x112AF), (0x112EB, 0x112EF), + (0x112FA, 0x112FF), (0x11304, 0x11304), (0x1130D, 0x1130E), (0x11311, 0x11312), + (0x11329, 0x11329), (0x11331, 0x11331), (0x11334, 0x11334), (0x1133A, 0x1133A), + (0x11345, 0x11346), (0x11349, 0x1134A), (0x1134E, 0x1134F), (0x11351, 0x11356), + (0x11358, 0x1135C), (0x11364, 0x11365), (0x1136D, 0x1136F), (0x11375, 0x1137F), + (0x1138A, 0x1138A), (0x1138C, 0x1138D), (0x1138F, 0x1138F), (0x113B6, 0x113B6), + (0x113C1, 0x113C1), (0x113C3, 0x113C4), (0x113C6, 0x113C6), (0x113CB, 0x113CB), + (0x113D6, 0x113D6), (0x113D9, 0x113E0), (0x113E3, 0x113FF), (0x1145C, 0x1145C), + (0x11462, 0x1147F), (0x114C8, 0x114CF), (0x114DA, 0x1157F), (0x115B6, 0x115B7), + (0x115DE, 0x115FF), (0x11645, 0x1164F), (0x1165A, 0x1165F), (0x1166D, 0x1167F), + (0x116BA, 0x116BF), (0x116CA, 0x116CF), (0x116E4, 0x116FF), (0x1171B, 0x1171C), + (0x1172C, 0x1172F), (0x11747, 0x117FF), (0x1183C, 0x1189F), (0x118F3, 0x118FE), + (0x11907, 0x11908), (0x1190A, 0x1190B), (0x11914, 0x11914), (0x11917, 0x11917), + (0x11936, 0x11936), (0x11939, 0x1193A), (0x11947, 0x1194F), (0x1195A, 0x1199F), + (0x119A8, 0x119A9), (0x119D8, 0x119D9), (0x119E5, 0x119FF), (0x11A48, 0x11A4F), + (0x11AA3, 0x11AAF), (0x11AF9, 0x11AFF), (0x11B0A, 0x11BBF), (0x11BE2, 0x11BEF), + (0x11BFA, 0x11BFF), (0x11C09, 0x11C09), (0x11C37, 0x11C37), (0x11C46, 0x11C4F), + (0x11C6D, 0x11C6F), (0x11C90, 0x11C91), (0x11CA8, 0x11CA8), (0x11CB7, 0x11CFF), + (0x11D07, 0x11D07), (0x11D0A, 0x11D0A), (0x11D37, 0x11D39), (0x11D3B, 0x11D3B), + (0x11D3E, 0x11D3E), (0x11D48, 0x11D4F), (0x11D5A, 0x11D5F), (0x11D66, 0x11D66), + (0x11D69, 0x11D69), (0x11D8F, 0x11D8F), (0x11D92, 0x11D92), (0x11D99, 0x11D9F), + (0x11DAA, 0x11EDF), (0x11EF9, 0x11EFF), (0x11F11, 0x11F11), (0x11F3B, 0x11F3D), + (0x11F5B, 0x11FAF), (0x11FB1, 0x11FBF), (0x11FF2, 0x11FFE), (0x1239A, 0x123FF), + (0x1246F, 0x1246F), (0x12475, 0x1247F), (0x12544, 0x12F8F), (0x12FF3, 0x12FFF), + (0x13430, 0x1343F), (0x13456, 0x1345F), (0x143FB, 0x143FF), (0x14647, 0x160FF), + (0x1613A, 0x167FF), (0x16A39, 0x16A3F), (0x16A5F, 0x16A5F), (0x16A6A, 0x16A6D), + (0x16ABF, 0x16ABF), (0x16ACA, 0x16ACF), (0x16AEE, 0x16AEF), (0x16AF6, 0x16AFF), + (0x16B46, 0x16B4F), (0x16B5A, 0x16B5A), (0x16B62, 0x16B62), (0x16B78, 0x16B7C), + (0x16B90, 0x16D3F), (0x16D7A, 0x16E3F), (0x16E9B, 0x16EFF), (0x16F4B, 0x16F4E), + (0x16F88, 0x16F8E), (0x16FA0, 0x16FDF), (0x16FE5, 0x16FEF), (0x16FF2, 0x16FFF), + (0x187F8, 0x187FF), (0x18CD6, 0x18CFE), (0x18D09, 0x1AFEF), (0x1AFF4, 0x1AFF4), + (0x1AFFC, 0x1AFFC), (0x1AFFF, 0x1AFFF), (0x1B123, 0x1B131), (0x1B133, 0x1B14F), + (0x1B153, 0x1B154), (0x1B156, 0x1B163), (0x1B168, 0x1B16F), (0x1B2FC, 0x1BBFF), + (0x1BC6B, 0x1BC6F), (0x1BC7D, 0x1BC7F), (0x1BC89, 0x1BC8F), (0x1BC9A, 0x1BC9B), + (0x1BCA0, 0x1CBFF), (0x1CCFA, 0x1CCFF), (0x1CEB4, 0x1CEFF), (0x1CF2E, 0x1CF2F), + (0x1CF47, 0x1CF4F), (0x1CFC4, 0x1CFFF), (0x1D0F6, 0x1D0FF), (0x1D127, 0x1D128), + (0x1D173, 0x1D17A), (0x1D1EB, 0x1D1FF), (0x1D246, 0x1D2BF), (0x1D2D4, 0x1D2DF), + (0x1D2F4, 0x1D2FF), (0x1D357, 0x1D35F), (0x1D379, 0x1D3FF), (0x1D455, 0x1D455), + (0x1D49D, 0x1D49D), (0x1D4A0, 0x1D4A1), (0x1D4A3, 0x1D4A4), (0x1D4A7, 0x1D4A8), + (0x1D4AD, 0x1D4AD), (0x1D4BA, 0x1D4BA), (0x1D4BC, 0x1D4BC), (0x1D4C4, 0x1D4C4), + (0x1D506, 0x1D506), (0x1D50B, 0x1D50C), (0x1D515, 0x1D515), (0x1D51D, 0x1D51D), + (0x1D53A, 0x1D53A), (0x1D53F, 0x1D53F), (0x1D545, 0x1D545), (0x1D547, 0x1D549), + (0x1D551, 0x1D551), (0x1D6A6, 0x1D6A7), (0x1D7CC, 0x1D7CD), (0x1DA8C, 0x1DA9A), + (0x1DAA0, 0x1DAA0), (0x1DAB0, 0x1DEFF), (0x1DF1F, 0x1DF24), (0x1DF2B, 0x1DFFF), + (0x1E007, 0x1E007), (0x1E019, 0x1E01A), (0x1E022, 0x1E022), (0x1E025, 0x1E025), + (0x1E02B, 0x1E02F), (0x1E06E, 0x1E08E), (0x1E090, 0x1E0FF), (0x1E12D, 0x1E12F), + (0x1E13E, 0x1E13F), (0x1E14A, 0x1E14D), (0x1E150, 0x1E28F), (0x1E2AF, 0x1E2BF), + (0x1E2FA, 0x1E2FE), (0x1E300, 0x1E4CF), (0x1E4FA, 0x1E5CF), (0x1E5FB, 0x1E5FE), + (0x1E600, 0x1E7DF), (0x1E7E7, 0x1E7E7), (0x1E7EC, 0x1E7EC), (0x1E7EF, 0x1E7EF), + (0x1E7FF, 0x1E7FF), (0x1E8C5, 0x1E8C6), (0x1E8D7, 0x1E8FF), (0x1E94C, 0x1E94F), + (0x1E95A, 0x1E95D), (0x1E960, 0x1EC70), (0x1ECB5, 0x1ED00), (0x1ED3E, 0x1EDFF), + (0x1EE04, 0x1EE04), (0x1EE20, 0x1EE20), (0x1EE23, 0x1EE23), (0x1EE25, 0x1EE26), + (0x1EE28, 0x1EE28), (0x1EE33, 0x1EE33), (0x1EE38, 0x1EE38), (0x1EE3A, 0x1EE3A), + (0x1EE3C, 0x1EE41), (0x1EE43, 0x1EE46), (0x1EE48, 0x1EE48), (0x1EE4A, 0x1EE4A), + (0x1EE4C, 0x1EE4C), (0x1EE50, 0x1EE50), (0x1EE53, 0x1EE53), (0x1EE55, 0x1EE56), + (0x1EE58, 0x1EE58), (0x1EE5A, 0x1EE5A), (0x1EE5C, 0x1EE5C), (0x1EE5E, 0x1EE5E), + (0x1EE60, 0x1EE60), (0x1EE63, 0x1EE63), (0x1EE65, 0x1EE66), (0x1EE6B, 0x1EE6B), + (0x1EE73, 0x1EE73), (0x1EE78, 0x1EE78), (0x1EE7D, 0x1EE7D), (0x1EE7F, 0x1EE7F), + (0x1EE8A, 0x1EE8A), (0x1EE9C, 0x1EEA0), (0x1EEA4, 0x1EEA4), (0x1EEAA, 0x1EEAA), + (0x1EEBC, 0x1EEEF), (0x1EEF2, 0x1EFFF), (0x1F02C, 0x1F02F), (0x1F094, 0x1F09F), + (0x1F0AF, 0x1F0B0), (0x1F0C0, 0x1F0C0), (0x1F0D0, 0x1F0D0), (0x1F0F6, 0x1F0FF), + (0x1F1AE, 0x1F1E5), (0x1F203, 0x1F20F), (0x1F23C, 0x1F23F), (0x1F249, 0x1F24F), + (0x1F252, 0x1F25F), (0x1F266, 0x1F2FF), (0x1F6D8, 0x1F6DB), (0x1F6ED, 0x1F6EF), + (0x1F6FD, 0x1F6FF), (0x1F777, 0x1F77A), (0x1F7DA, 0x1F7DF), (0x1F7EC, 0x1F7EF), + (0x1F7F1, 0x1F7FF), (0x1F80C, 0x1F80F), (0x1F848, 0x1F84F), (0x1F85A, 0x1F85F), + (0x1F888, 0x1F88F), (0x1F8AE, 0x1F8AF), (0x1F8BC, 0x1F8BF), (0x1F8C2, 0x1F8FF), + (0x1FA54, 0x1FA5F), (0x1FA6E, 0x1FA6F), (0x1FA7D, 0x1FA7F), (0x1FA8A, 0x1FA8E), + (0x1FAC7, 0x1FACD), (0x1FADD, 0x1FADE), (0x1FAEA, 0x1FAEF), (0x1FAF9, 0x1FAFF), + (0x1FB93, 0x1FB93), (0x1FBFA, 0x1FFFF), (0x2A6E0, 0x2A6FF), (0x2B73A, 0x2B73F), + (0x2B81E, 0x2B81F), (0x2CEA2, 0x2CEAF), (0x2EBE1, 0x2EBEF), (0x2EE5E, 0x2F7FF), + (0x2FA1E, 0x2FFFF), (0x3134B, 0x3134F), (0x323B0, 0xE00FF), (0xE01F0, 0x10FFFF), +]; diff --git a/nvx-rs/src/host/python/uuid.rs b/nvx-rs/src/host/python/uuid.rs new file mode 100644 index 000000000..46bff1d6e --- /dev/null +++ b/nvx-rs/src/host/python/uuid.rs @@ -0,0 +1,98 @@ +//! Python's `uuid.UUID` parser. + +use super::builtins::parse_digits; + +/// Returns `str(uuid.UUID(text))`, the lowercase hyphenated form of the UUID in `text`, or `None` +/// if `uuid.UUID` refuses `text`. +/// +/// `uuid.UUID` removes every `urn:` and then every `uuid:`, strips braces from both ends, removes +/// every hyphen, and requires 32 characters to remain, which it reads with `int(text, 16)`. It +/// therefore accepts braces, a `urn:uuid:` prefix, the form without hyphens, and uppercase +/// digits, and also Unicode decimal digits, a `0x` prefix, a plus sign, underscores, and +/// surrounding whitespace, as long as 32 characters remain. A minus sign is a hyphen, which is +/// removed. +pub(crate) fn parse_uuid(text: &str) -> Option { + let hex = text.replace("urn:", "").replace("uuid:", ""); + let hex = hex.trim_matches(['{', '}']).replace('-', ""); + if hex.chars().count() != 32 { + return None; + } + let (_, digits) = parse_digits(&hex, 16).ok()?; + // At most 32 hexadecimal digits remain, so the value fits in 128 bits. + let value = digits + .iter() + .fold(0u128, |value, &digit| (value << 4) | u128::from(digit)); + let hex = format!("{value:032x}"); + Some(format!( + "{}-{}-{}-{}-{}", + &hex[..8], + &hex[8..12], + &hex[12..16], + &hex[16..20], + &hex[20..] + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uuids_take_every_form_that_python_accepts() { + let canonical = "12345678-1234-5678-1234-567812345678"; + for text in [ + "12345678-1234-5678-1234-567812345678", + "{12345678-1234-5678-1234-567812345678}", + "urn:uuid:12345678-1234-5678-1234-567812345678", + "12345678123456781234567812345678", + "12345678-1234-5678-1234-567812345678" + .to_uppercase() + .as_str(), + "{{urn:12345678-1234-5678-1234-567812345678}}", + "{urn:uuid:12345678-1234-5678-1234-567812345678}", + ] { + assert_eq!(parse_uuid(text).as_deref(), Some(canonical), "{text:?}"); + } + // Each prefix is removed in one pass, as `str.replace` removes it. + assert_eq!( + parse_uuid("uuuuid:id:12345678123456781234567812345678"), + None + ); + // `int(text, 16)` reads a prefix, whitespace, a sign, and Unicode digits too. + for (text, expected) in [ + ( + format!("0x{}", "1".repeat(30)), + "00111111-1111-1111-1111-111111111111", + ), + ( + format!(" {}", "a".repeat(31)), + "0aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + ), + ( + format!("+{}", "1".repeat(31)), + "01111111-1111-1111-1111-111111111111", + ), + ( + format!("1_{}", "1".repeat(30)), + "01111111-1111-1111-1111-111111111111", + ), + ( + format!("\u{661}{}", "0".repeat(31)), + "10000000-0000-0000-0000-000000000000", + ), + ] { + assert_eq!(parse_uuid(&text).as_deref(), Some(expected), "{text:?}"); + } + for text in [ + "not-a-uuid".to_owned(), + String::new(), + // Hyphens are removed, a minus sign with them. + format!("-{}", "0".repeat(31)), + format!("{}g", "1".repeat(31)), + "1".repeat(33), + format!("{}_", "1".repeat(31)), + ] { + assert_eq!(parse_uuid(&text), None, "{text:?}"); + } + } +} diff --git a/nvx-rs/src/host/python/value.rs b/nvx-rs/src/host/python/value.rs new file mode 100644 index 000000000..454ae24a9 --- /dev/null +++ b/nvx-rs/src/host/python/value.rs @@ -0,0 +1,243 @@ +//! The built-in conversions that `nvx.py` applies to the values that `json.loads` returns: +//! `str()`, `int()`, and the `==` with which `in` compares a value with integers. +//! +//! A [`Node`] of a [`Document`] is the value that Python's decoder builds: `None`, `bool`, `int`, +//! `float`, `str`, `list`, or `dict`. [`py_str`] gives `str(value)`, which is the string itself +//! for a string and `repr(value)` for anything else, with floats as Python's `repr` writes them; +//! [`py_int`] gives `int(value)`; and [`equals_int`] gives `value == n`, under which `True` +//! equals 1 and `1.0` equals 1. + +use super::builtins::{Int, repr}; +use super::json::{Document, Node, NodeId}; + +/// Why `int(value)` fails. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum IntError { + /// `TypeError` or `ValueError`, which callers of `int` usually catch. + Invalid, + /// `OverflowError` for an infinite float, which `nvx.py` does not catch. + Overflow, +} + +/// Returns `int(value)` for the value at `id`: an integer as it is, a float truncated toward +/// zero, `True` and `False` as 1 and 0, and a string as `int(text)` reads it. +pub(crate) fn py_int(document: &Document, id: NodeId) -> Result { + match document.node(id) { + Node::Integer(literal) => Int::parse(literal).map_err(|_| IntError::Invalid), + Node::Bool(value) => Ok(Int::from(u64::from(*value))), + Node::Float(value) if value.is_nan() => Err(IntError::Invalid), + Node::Float(value) if value.is_infinite() => Err(IntError::Overflow), + Node::Float(value) => { + Int::parse(&format!("{:.0}", value.trunc())).map_err(|_| IntError::Invalid) + } + Node::String(text) => text + .as_str() + .and_then(|text| Int::parse(text).ok()) + .ok_or(IntError::Invalid), + Node::Null | Node::Array(_) | Node::Object(_) => Err(IntError::Invalid), + } +} + +/// Returns `str(value)` for the value at `id`, or `None` for a string that is not valid +/// Unicode, which holds unpaired surrogates. +pub(crate) fn py_str(document: &Document, id: NodeId) -> Option { + match document.node(id) { + Node::String(text) => text.as_str().map(str::to_owned), + _ => py_repr(document, id), + } +} + +/// Returns `repr(value)` for the value at `id`, or `None` if a string in it is not valid +/// Unicode. +pub(crate) fn py_repr(document: &Document, id: NodeId) -> Option { + Some(match document.node(id) { + Node::Null => "None".to_owned(), + Node::Bool(true) => "True".to_owned(), + Node::Bool(false) => "False".to_owned(), + Node::Integer(literal) => Int::parse(literal).ok()?.to_string(), + Node::Float(value) => float_repr(*value), + Node::String(text) => repr(text.as_str()?), + Node::Array(items) => { + let items: Option> = + items.iter().map(|&item| py_repr(document, item)).collect(); + format!("[{}]", items?.join(", ")) + } + Node::Object(members) => { + // A dict keeps the first position of a repeated name, with its last value. + let mut entries: Vec<(&[u8], String, NodeId)> = Vec::new(); + for (name, value) in members { + match entries.iter_mut().find(|entry| entry.0 == name.as_bytes()) { + Some(entry) => entry.2 = *value, + None => entries.push((name.as_bytes(), repr(name.as_str()?), *value)), + } + } + let entries: Option> = entries + .iter() + .map(|(_, name, value)| Some(format!("{name}: {}", py_repr(document, *value)?))) + .collect(); + format!("{{{}}}", entries?.join(", ")) + } + }) +} + +/// Returns whether the value at `id` equals the integer `expected` under Python's `==`. +pub(crate) fn equals_int(document: &Document, id: NodeId, expected: u64) -> bool { + match document.node(id) { + Node::Integer(literal) => Int::parse(literal).ok() == Some(Int::from(expected)), + Node::Bool(value) => u64::from(*value) == expected, + // Every integer that NVX compares with is exact as a float. + Node::Float(value) => *value == expected as f64, + _ => false, + } +} + +/// Returns `repr(value)` for a float: the shortest digits that read back as `value`, in fixed +/// notation from 1e-4 up to 1e16 with at least one digit after the point, and in exponent +/// notation otherwise, with a sign and at least two exponent digits. +pub(crate) fn float_repr(value: f64) -> String { + if value.is_nan() { + return "nan".to_owned(); + } + if value.is_infinite() { + return if value > 0.0 { "inf" } else { "-inf" }.to_owned(); + } + let sign = if value.is_sign_negative() { "-" } else { "" }; + // Rust's `{:e}` writes the shortest digits that read back as the value. + let scientific = format!("{:e}", value.abs()); + let (mantissa, exponent) = scientific + .split_once('e') + .expect("`{:e}` writes an exponent"); + let digits: String = mantissa.chars().filter(|&c| c != '.').collect(); + let exponent: i32 = exponent.parse().expect("`{:e}` writes a decimal exponent"); + // The position of the decimal point relative to the start of the digits. + let point = exponent + 1; + let length = digits.len() as i32; + let body = if (-3..=16).contains(&point) { + if point <= 0 { + format!("0.{}{digits}", "0".repeat((-point) as usize)) + } else if point >= length { + format!("{digits}{}.0", "0".repeat((point - length) as usize)) + } else { + let (whole, fraction) = digits.split_at(point as usize); + format!("{whole}.{fraction}") + } + } else { + let (first, rest) = digits.split_at(1); + let fraction = if rest.is_empty() { + String::new() + } else { + format!(".{rest}") + }; + let exponent = point - 1; + let exponent_sign = if exponent < 0 { '-' } else { '+' }; + format!("{first}{fraction}e{exponent_sign}{:02}", exponent.abs()) + }; + format!("{sign}{body}") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::host::python::json::{Loads, read}; + + fn document(text: &str) -> Document { + read(text, &mut Loads).unwrap() + } + + #[test] + fn floats_print_as_python_repr() { + for (value, expected) in [ + (1.0, "1.0"), + (0.1, "0.1"), + (-0.0, "-0.0"), + (1e16, "1e+16"), + (1e15, "1000000000000000.0"), + (123456789012345678.0, "1.2345678901234568e+17"), + (0.0001, "0.0001"), + (0.00001, "1e-05"), + (1.5e-7, "1.5e-07"), + (2.5, "2.5"), + (1e300, "1e+300"), + (f64::INFINITY, "inf"), + (f64::NEG_INFINITY, "-inf"), + (f64::NAN, "nan"), + (12345.678, "12345.678"), + ] { + assert_eq!(float_repr(value), expected, "{value:?}"); + } + } + + #[test] + fn values_convert_as_python_converts_them() { + let value = document( + r#"[1, -2.9, true, false, "7", " 8 ", "x", null, [1], {"a": 1}, 1e400, NaN, 1.0]"#, + ); + let Node::Array(items) = document_root(&value) else { + panic!("not an array") + }; + let ints: Vec> = items + .iter() + .map(|&item| py_int(&value, item).map(|value| value.to_string())) + .collect(); + let ok = |text: &str| Ok(text.to_owned()); + assert_eq!( + ints, + [ + ok("1"), + ok("-2"), + ok("1"), + ok("0"), + ok("7"), + ok("8"), + Err(IntError::Invalid), + Err(IntError::Invalid), + Err(IntError::Invalid), + Err(IntError::Invalid), + Err(IntError::Overflow), + Err(IntError::Invalid), + ok("1"), + ] + ); + let strings: Vec = items + .iter() + .map(|&item| py_str(&value, item).unwrap()) + .collect(); + assert_eq!( + strings, + [ + "1", "-2.9", "True", "False", "7", " 8 ", "x", "None", "[1]", "{'a': 1}", "inf", + "nan", "1.0" + ] + ); + let equal: Vec = items + .iter() + .map(|&item| equals_int(&value, item, 1)) + .collect(); + assert_eq!( + equal, + [ + true, false, true, false, false, false, false, false, false, false, false, false, + true + ] + ); + } + + #[test] + fn containers_print_as_python_repr() { + let value = document(r#"{"b": ["it's", 2.5, {"x": null}], "a": 1, "b": "last"}"#); + assert_eq!( + py_str(&value, value.root()).unwrap(), + "{'b': 'last', 'a': 1}" + ); + let value = document(r#"["it's", [2.5, {"x": null}]]"#); + assert_eq!( + py_str(&value, value.root()).unwrap(), + "[\"it's\", [2.5, {'x': None}]]" + ); + assert_eq!(py_str(&document(r#""\ud800""#), 0), None); + } + + fn document_root(document: &Document) -> &Node { + document.node(document.root()) + } +} diff --git a/nvx-rs/src/host/python/windows.rs b/nvx-rs/src/host/python/windows.rs new file mode 100644 index 000000000..d8804eb64 --- /dev/null +++ b/nvx-rs/src/host/python/windows.rs @@ -0,0 +1,539 @@ +//! The Win32 calls with which CPython's `nt` module stats files, reads links, and asks for final +//! path names. +//! +//! [`stat`] ports `win32_xstat_slow_impl`: it opens the path for its attributes only, and falls +//! back to `FindFirstFileW` when the path cannot be opened. Without links followed, only +//! `IO_REPARSE_TAG_SYMLINK` reparse points are links, as `os.lstat` reports them. +#![allow(unsafe_code)] + +use std::ffi::c_void; +use std::io; +use std::os::windows::ffi::OsStrExt; +use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle, RawHandle}; +use std::path::Path; +use std::{mem, ptr}; + +use windows_sys::Win32::Foundation::{ + ERROR_ACCESS_DENIED, ERROR_BAD_NET_NAME, ERROR_CANT_ACCESS_FILE, ERROR_FILE_NOT_FOUND, + ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER, ERROR_NOT_READY, ERROR_NOT_SUPPORTED, + ERROR_PATH_NOT_FOUND, ERROR_SHARING_VIOLATION, GENERIC_READ, GetLastError, HANDLE, + INVALID_HANDLE_VALUE, +}; +use windows_sys::Win32::Storage::FileSystem::{ + BY_HANDLE_FILE_INFORMATION, CreateFileW, FILE_ATTRIBUTE_DIRECTORY, + FILE_ATTRIBUTE_REPARSE_POINT, FILE_ATTRIBUTE_TAG_INFO, FILE_BASIC_INFO, + FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_ID_INFO, FILE_READ_ATTRIBUTES, + FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TYPE_CHAR, FILE_TYPE_DISK, FILE_TYPE_PIPE, + FILE_TYPE_UNKNOWN, FileAttributeTagInfo, FileBasicInfo, FileIdInfo, FindClose, FindFirstFileW, + GetFileAttributesW, GetFileInformationByHandle, GetFileInformationByHandleEx, GetFileType, + GetFinalPathNameByHandleW, INVALID_FILE_ATTRIBUTES, OPEN_EXISTING, VOLUME_NAME_DOS, + WIN32_FIND_DATAW, +}; +use windows_sys::Win32::System::IO::DeviceIoControl; + +/// The reparse tag of a symbolic link. +const IO_REPARSE_TAG_SYMLINK: u32 = 0xA000_000C; +/// The reparse tag of a junction, or mount point. +const IO_REPARSE_TAG_MOUNT_POINT: u32 = 0xA000_0003; +/// The control code that reads a reparse point's data (`winioctl.h`). +const FSCTL_GET_REPARSE_POINT: u32 = 0x0009_00A8; +/// The largest reparse point that `FSCTL_GET_REPARSE_POINT` returns. +const MAXIMUM_REPARSE_DATA_BUFFER_SIZE: usize = 16 * 1024; + +/// The type of file that `st_mode` gives. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum StatMode { + /// `S_IFREG`. + Regular, + /// `S_IFDIR`. + Directory, + /// `S_IFLNK`: a symbolic link, without links followed. + Link, + /// `S_IFCHR`, as `\\.\nul`. + Character, + /// `S_IFIFO`, as a named pipe. + Fifo, + /// `S_IFBLK`, as a volume. + Block, + /// No type, as an unknown device. + Unknown, +} + +/// The fields of `os.stat` and `os.lstat` that NVX reads. +#[derive(Clone, Copy, Debug)] +pub(crate) struct Status { + /// The type of file. + pub(crate) mode: StatMode, + /// `st_dev`: the volume serial number. + pub(crate) device: u64, + /// `st_ino`: the file ID. + pub(crate) inode: u128, +} + +/// Returns the NUL-terminated UTF-16 form of `path`, refusing an embedded NUL as Python does. +fn wide(path: &Path) -> io::Result> { + let mut units: Vec = path.as_os_str().encode_wide().collect(); + if units.contains(&0) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "embedded null character in path", + )); + } + units.push(0); + Ok(units) +} + +/// Returns the calling thread's last Win32 error. +fn last_error() -> u32 { + // SAFETY: GetLastError has no preconditions. + unsafe { GetLastError() } +} + +/// Returns the error with Win32 error code `code`. +fn error(code: u32) -> io::Error { + io::Error::from_raw_os_error(code as i32) +} + +/// Opens `path` with `access`, `share`, and `flags`, or returns the Win32 error code. +fn open(path: &[u16], access: u32, share: u32, flags: u32) -> Result { + // SAFETY: `path` is NUL-terminated, and the other arguments are plain values or null. + let handle = unsafe { + CreateFileW( + path.as_ptr(), + access, + share, + ptr::null(), + OPEN_EXISTING, + flags, + ptr::null_mut(), + ) + }; + if handle.is_null() || handle == INVALID_HANDLE_VALUE { + Err(last_error()) + } else { + // SAFETY: the handle was just opened, and nothing else owns it. + Ok(unsafe { OwnedHandle::from_raw_handle(handle as RawHandle) }) + } +} + +/// Borrows `handle` in the form that `windows-sys` takes. +fn raw(handle: &OwnedHandle) -> HANDLE { + handle.as_raw_handle() as HANDLE +} + +/// Reads the information of `class` into `T`, or returns the Win32 error code. `T` must be the +/// plain-data structure of `class`. +fn information(handle: &OwnedHandle, class: i32) -> Result { + // SAFETY: the structures that this module reads are plain data, for which all-zero bytes are + // valid. + let mut value: T = unsafe { mem::zeroed() }; + // SAFETY: the handle is open, and the buffer is a `T` of the size that is passed. + let ok = unsafe { + GetFileInformationByHandleEx( + raw(handle), + class, + (&raw mut value).cast::(), + mem::size_of::() as u32, + ) + }; + if ok == 0 { + Err(last_error()) + } else { + Ok(value) + } +} + +/// The attributes and reparse tag that `FindFirstFileW` gives for `path`, as CPython's +/// `attributes_from_dir` reads them, with the file's real name. +struct FindData { + /// The file attributes. + attributes: u32, + /// The reparse tag, or zero if the file is not a reparse point. + reparse_tag: u32, + /// The name of the file as the directory holds it. + name: Vec, +} + +/// Calls `FindFirstFileW` for `path`, which must be NUL-terminated, or returns the Win32 error. +fn find_first_file(path: &[u16]) -> Result { + // SAFETY: WIN32_FIND_DATAW is plain data, for which all-zero bytes are valid. + let mut data: WIN32_FIND_DATAW = unsafe { mem::zeroed() }; + // SAFETY: the path is NUL-terminated and the output pointer is valid. + let handle = unsafe { FindFirstFileW(path.as_ptr(), &mut data) }; + if handle == INVALID_HANDLE_VALUE { + return Err(last_error()); + } + // SAFETY: the handle is a find handle that was just opened. + unsafe { FindClose(handle) }; + let length = data + .cFileName + .iter() + .position(|&unit| unit == 0) + .unwrap_or(data.cFileName.len()); + Ok(FindData { + attributes: data.dwFileAttributes, + reparse_tag: if data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 { + data.dwReserved0 + } else { + 0 + }, + name: data.cFileName[..length].to_vec(), + }) +} + +/// Returns the find data of `path` as `attributes_from_dir` reads it: a path that ends in +/// separators is queried without them, and a root has nothing to query. +fn attributes_from_dir(path: &[u16]) -> Result { + let separator = |unit: u16| unit == u16::from(b'\\') || unit == u16::from(b'/'); + let text = &path[..path.len() - 1]; + if text.last().is_some_and(|&unit| separator(unit)) { + let mut end = text.len() - 1; + while end > 0 && separator(text[end]) { + end -= 1; + } + if end == 0 || (end == 1 && text[1] == u16::from(b':')) { + // Nothing is left to query; CPython leaves the last error as it was. + return Err(last_error()); + } + let mut trimmed = text[..=end].to_vec(); + trimmed.push(0); + return find_first_file(&trimmed); + } + find_first_file(path) +} + +/// Returns whether a reparse tag names another file, as links and junctions do. +fn is_name_surrogate(tag: u32) -> bool { + tag & 0x2000_0000 != 0 +} + +/// Returns the type of file that a file's attributes and reparse tag give. +fn mode(attributes: u32, reparse_tag: u32) -> StatMode { + if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 && reparse_tag == IO_REPARSE_TAG_SYMLINK { + StatMode::Link + } else if attributes & FILE_ATTRIBUTE_DIRECTORY != 0 { + StatMode::Directory + } else { + StatMode::Regular + } +} + +/// Returns `os.stat(path)`, or `os.lstat(path)` without `traverse`, as CPython's +/// `win32_xstat_slow_impl` computes it. +pub(crate) fn stat(path: &Path, traverse: bool) -> io::Result { + stat_wide(&wide(path)?, traverse) +} + +/// Implements [`stat`] for a NUL-terminated path. +fn stat_wide(path: &[u16], mut traverse: bool) -> io::Result { + let mut unhandled_tag = false; + let mut flags = FILE_FLAG_BACKUP_SEMANTICS; + if !traverse { + flags |= FILE_FLAG_OPEN_REPARSE_POINT; + } + let handle = match open(path, FILE_READ_ATTRIBUTES, 0, flags) { + Ok(handle) => handle, + Err(code @ (ERROR_ACCESS_DENIED | ERROR_SHARING_VIOLATION)) => { + // Read the attributes from the parent directory instead. + let data = attributes_from_dir(path).map_err(|find_error| match find_error { + ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND | ERROR_NOT_READY + | ERROR_BAD_NET_NAME => error(find_error), + _ => error(code), + })?; + if data.attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + && (traverse || !is_name_surrogate(data.reparse_tag)) + { + // The link would have to be followed, but cannot be. + return Err(error(code)); + } + return Ok(Status { + mode: mode(data.attributes, data.reparse_tag), + device: 0, + inode: 0, + }); + } + Err(code @ ERROR_INVALID_PARAMETER) => { + // Devices such as `\\.\con` need read or write access. + open( + path, + FILE_READ_ATTRIBUTES | GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + flags, + ) + .map_err(|_| error(code))? + } + Err(code @ ERROR_CANT_ACCESS_FILE) if traverse => { + // Open a reparse point that Windows cannot follow as itself. + traverse = false; + unhandled_tag = true; + open( + path, + FILE_READ_ATTRIBUTES, + 0, + flags | FILE_FLAG_OPEN_REPARSE_POINT, + ) + .map_err(|_| error(code))? + } + Err(code) => return Err(error(code)), + }; + + // SAFETY: the handle is open. + let file_type = unsafe { GetFileType(raw(&handle)) }; + if file_type != FILE_TYPE_DISK { + if file_type == FILE_TYPE_UNKNOWN && last_error() != 0 { + return Err(io::Error::last_os_error()); + } + // SAFETY: the path is NUL-terminated. + let attributes = unsafe { GetFileAttributesW(path.as_ptr()) }; + let mode = if attributes != INVALID_FILE_ATTRIBUTES + && attributes & FILE_ATTRIBUTE_DIRECTORY != 0 + { + StatMode::Directory + } else if file_type == FILE_TYPE_CHAR { + StatMode::Character + } else if file_type == FILE_TYPE_PIPE { + StatMode::Fifo + } else { + StatMode::Unknown + }; + return Ok(Status { + mode, + device: 0, + inode: 0, + }); + } + + let mut reparse_tag = 0; + if !traverse { + match information::(&handle, FileAttributeTagInfo) { + Ok(tag) if tag.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 => { + if is_name_surrogate(tag.ReparseTag) { + if unhandled_tag { + return Err(error(ERROR_CANT_ACCESS_FILE)); + } + } else if !unhandled_tag { + // Follow a reparse point that is not a link. + drop(handle); + return stat_wide(path, true); + } + reparse_tag = tag.ReparseTag; + } + Ok(_) => {} + // Devices without reparse tags are ordinary files. + Err(ERROR_INVALID_PARAMETER | ERROR_INVALID_FUNCTION | ERROR_NOT_SUPPORTED) => {} + Err(code) => return Err(error(code)), + } + } + + // SAFETY: BY_HANDLE_FILE_INFORMATION is plain data, for which all-zero bytes are valid. + let mut file: BY_HANDLE_FILE_INFORMATION = unsafe { mem::zeroed() }; + // SAFETY: the handle is open and the output pointer is valid. + let queried = if unsafe { GetFileInformationByHandle(raw(&handle), &mut file) } == 0 { + Err(last_error()) + } else { + information::(&handle, FileBasicInfo).map(|_| ()) + }; + match queried { + Ok(()) => {} + // Volumes and physical disks are block devices. + Err(ERROR_INVALID_PARAMETER | ERROR_INVALID_FUNCTION | ERROR_NOT_SUPPORTED) => { + return Ok(Status { + mode: StatMode::Block, + device: 0, + inode: 0, + }); + } + Err(code) => return Err(error(code)), + } + let id = information::(&handle, FileIdInfo).ok(); + let device = id.map_or(u64::from(file.dwVolumeSerialNumber), |id| { + id.VolumeSerialNumber + }); + let inode = match id.map_or(0, |id| u128::from_le_bytes(id.FileId.Identifier)) { + 0 => (u128::from(file.nFileIndexHigh) << 32) | u128::from(file.nFileIndexLow), + inode => inode, + }; + Ok(Status { + mode: mode(file.dwFileAttributes, reparse_tag), + device, + inode, + }) +} + +/// Returns `os.fstat` of an open file, as CPython's `_Py_fstat_noraise` computes it: a disk file's +/// type, identity, and link count come from `GetFileInformationByHandle` and `FILE_ID_INFO`, as +/// [`stat`] reads them, and its attributes tell whether the handle is a reparse point that was +/// opened as itself. +#[cfg(feature = "cli")] +pub(crate) fn fstat(file: &impl AsRawHandle) -> io::Result { + let handle = file.as_raw_handle() as HANDLE; + // SAFETY: the handle is open for as long as `file` is borrowed. + let file_type = unsafe { GetFileType(handle) }; + if file_type != FILE_TYPE_DISK { + if file_type == FILE_TYPE_UNKNOWN && last_error() != 0 { + return Err(io::Error::last_os_error()); + } + let mode = match file_type { + FILE_TYPE_CHAR => StatMode::Character, + FILE_TYPE_PIPE => StatMode::Fifo, + _ => StatMode::Unknown, + }; + return Ok(HandleStatus { + mode, + device: 0, + inode: 0, + links: 0, + reparse_point: false, + }); + } + // SAFETY: BY_HANDLE_FILE_INFORMATION is plain data, for which all-zero bytes are valid. + let mut information: BY_HANDLE_FILE_INFORMATION = unsafe { mem::zeroed() }; + // SAFETY: the handle is open and the output pointer is valid. + if unsafe { GetFileInformationByHandle(handle, &mut information) } == 0 { + return Err(io::Error::last_os_error()); + } + // SAFETY: FILE_ID_INFO is plain data, for which all-zero bytes are valid. + let mut id: FILE_ID_INFO = unsafe { mem::zeroed() }; + // SAFETY: the handle is open, and the buffer is a FILE_ID_INFO of the size that is passed. + let has_id = unsafe { + GetFileInformationByHandleEx( + handle, + FileIdInfo, + (&raw mut id).cast::(), + mem::size_of::() as u32, + ) + } != 0; + let device = if has_id { + id.VolumeSerialNumber + } else { + u64::from(information.dwVolumeSerialNumber) + }; + let inode = match has_id.then(|| u128::from_le_bytes(id.FileId.Identifier)) { + Some(inode) if inode != 0 => inode, + _ => (u128::from(information.nFileIndexHigh) << 32) | u128::from(information.nFileIndexLow), + }; + Ok(HandleStatus { + mode: mode(information.dwFileAttributes, 0), + device, + inode, + links: information.nNumberOfLinks, + reparse_point: information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0, + }) +} + +/// The fields of `os.fstat` that NVX reads. +#[cfg(feature = "cli")] +#[derive(Clone, Copy, Debug)] +pub(crate) struct HandleStatus { + /// The type of file. `os.fstat` reads no reparse tag, so a link opened as itself is no + /// [`StatMode::Link`]. + pub(crate) mode: StatMode, + /// `st_dev`: the volume serial number. + pub(crate) device: u64, + /// `st_ino`: the file ID. + pub(crate) inode: u128, + /// `st_nlink`: the number of names that link the file. + pub(crate) links: u32, + /// Whether `st_file_attributes` has `FILE_ATTRIBUTE_REPARSE_POINT`. + pub(crate) reparse_point: bool, +} + +/// Returns `nt._getfinalpathname(path)`: the path that Windows gives for the file, with its +/// `\\?\` prefix. +pub(crate) fn final_path_name(path: &Path) -> io::Result { + let path = wide(path)?; + let handle = open(&path, 0, 0, FILE_FLAG_BACKUP_SEMANTICS).map_err(error)?; + let mut buffer = vec![0u16; 260]; + loop { + // SAFETY: the handle is open and the buffer holds `buffer.len()` units. + let length = unsafe { + GetFinalPathNameByHandleW( + raw(&handle), + buffer.as_mut_ptr(), + buffer.len() as u32, + VOLUME_NAME_DOS, + ) + } as usize; + if length == 0 { + return Err(io::Error::last_os_error()); + } + if length < buffer.len() { + buffer.truncate(length); + break; + } + buffer.resize(length, 0); + } + String::from_utf16(&buffer).map_err(|_| super::not_unicode("a final path name")) +} + +/// Returns `nt._findfirstfile(path)`: the name of the file as its directory holds it. +pub(crate) fn find_first_file_name(path: &Path) -> io::Result { + let data = find_first_file(&wide(path)?).map_err(error)?; + String::from_utf16(&data.name).map_err(|_| super::not_unicode("a file name")) +} + +/// Returns `nt.readlink(path)`: the substitute name of a symbolic link or junction, with `\??\` +/// turned into `\\?\`, or `None` for another kind of reparse point. +pub(crate) fn read_link(path: &Path) -> io::Result> { + let path = wide(path)?; + let handle = open( + &path, + 0, + 0, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, + ) + .map_err(error)?; + let mut buffer = vec![0u8; MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + let mut returned = 0u32; + // SAFETY: the handle is open, there is no input buffer, and the output buffer holds + // `buffer.len()` bytes. + let ok = unsafe { + DeviceIoControl( + raw(&handle), + FSCTL_GET_REPARSE_POINT, + ptr::null(), + 0, + buffer.as_mut_ptr().cast::(), + buffer.len() as u32, + &mut returned, + ptr::null_mut(), + ) + }; + if ok == 0 { + return Err(io::Error::last_os_error()); + } + let data = &buffer[..(returned as usize).min(buffer.len())]; + let u16_at = |offset: usize| -> Option { + Some(usize::from(u16::from_le_bytes( + data.get(offset..offset + 2)?.try_into().ok()?, + ))) + }; + let tag = data.get(..4).map_or(0, |bytes| { + u32::from_le_bytes(bytes.try_into().unwrap_or_default()) + }); + // The substitute name follows the fixed fields of REPARSE_DATA_BUFFER: a symbolic link has + // flags before its path buffer, and a junction does not. + let path_buffer = match tag { + IO_REPARSE_TAG_SYMLINK => 20, + IO_REPARSE_TAG_MOUNT_POINT => 16, + _ => return Ok(None), + }; + let malformed = || io::Error::new(io::ErrorKind::InvalidData, "malformed reparse point"); + let (offset, length) = ( + u16_at(8).ok_or_else(malformed)?, + u16_at(10).ok_or_else(malformed)?, + ); + let start = path_buffer + offset; + let bytes = data.get(start..start + length).ok_or_else(malformed)?; + let mut name: Vec = bytes + .chunks_exact(2) + .map(|pair| u16::from_le_bytes([pair[0], pair[1]])) + .collect(); + let nt_prefix = ['\\', '?', '?', '\\'].map(|c| c as u16); + if name.len() > 4 && name[..4] == nt_prefix { + name[1] = u16::from(b'\\'); + } + String::from_utf16(&name) + .map(Some) + .map_err(|_| super::not_unicode("a symbolic link's target")) +} diff --git a/nvx-rs/src/host/sandbox/golden.rs b/nvx-rs/src/host/sandbox/golden.rs new file mode 100644 index 000000000..12c476244 --- /dev/null +++ b/nvx-rs/src/host/sandbox/golden.rs @@ -0,0 +1,383 @@ +//! Checks the launch profile and the option checks against `tests/fixtures/sandbox.json` and +//! `tests/fixtures/sandbox_options.json`, which the retired `scripts/nvx_rs_fixtures.py` +//! recorded from the Python launch profile and option checks of `nvx.py sandbox`, which nvx-rs +//! replaced. +//! +//! The fixtures hold the paths of both `pathlib` flavors, with a fixed current directory, so +//! every case runs on every host. The cases that read files run on files of a temporary +//! directory. + +use std::io; + +use serde_json::{Value, json}; + +use super::mount::{ + aggregate_child_name, mounts_openvmm_arguments_with, parse_mount_value, validate_mount_target, +}; +use super::options::{ + PolicyPathOption, SandboxOperation, SandboxOptions, sandbox_mounts_with, + validate_exec_environment, validate_run_share_targets, validate_sandbox_systemd_policy, +}; +use super::*; +use crate::host::python::golden::{Mismatches, cases, fixture, flavor, hex, text, texts}; +use crate::host::python::uuid::parse_uuid; + +/// Returns the message of a result's error, or `None` if it succeeded. +fn error(result: &Result) -> Option { + result.as_ref().err().map(ToString::to_string) +} + +/// Returns `Path.cwd()` of the fixture for paths of `flavor`. +fn cwd(fixture: &Value, flavor: Flavor) -> impl Fn() -> io::Result { + let name = match flavor { + Flavor::Posix => "posix", + Flavor::Windows => "windows", + }; + let directory = PurePath::new(flavor, text(&fixture["cwds"], name)); + move || Ok(directory.clone()) +} + +/// Returns the fields of a share as the generator records them. +fn mount_fields(mount: &SandboxMount) -> Value { + let policy = mount.policy(); + json!({ + "guest_target": mount.guest_target(), + "host_path": mount.host_path().to_str().expect("a Unicode path"), + "access": mount.access().as_str(), + "denied": policy.denied, + "allowed": policy.allowed, + "writable": policy.writable, + "owner": mount.owner().as_str(), + "kind": mount.kind().as_str(), + }) +} + +/// Parses the share of a generator's specification. +fn parse_mount(flavor: Flavor, spec: &Value) -> Result { + let policy = MountPolicy { + denied: texts(spec, "denied"), + allowed: texts(spec, "allowed"), + writable: texts(spec, "writable"), + }; + SandboxMount::parse_in( + flavor, + text(spec, "value"), + policy, + MountOwner::from_name(text(spec, "owner"))?, + MountKind::from_name(text(spec, "kind"))?, + ) +} + +/// Returns the expected outcome of a case: its `key` field, or its `error` field. +fn outcome(case: &Value, key: &str) -> std::result::Result { + match case.get("error") { + Some(Value::String(message)) => Err(message.clone()), + _ => Ok(case[key].clone()), + } +} + +/// Returns the layer of a launch's recorded `--layer` value. +/// +/// The generator recorded launches of every role, and a sandbox now takes only the distro layer +/// (microsoft/nvx#556), which `--layer` alone accepts. A layer of a retired role is therefore the +/// one that the configuration of a sandbox provisioned before then names, which the launch +/// refuses, as the records of those launches say. +fn recorded_layer(flavor: Flavor, value: &str) -> SandboxLayer { + SandboxLayer::parse_in(flavor, value).unwrap_or_else(|_| { + let [role, path, uuid] = value.split(',').collect::>()[..] else { + panic!("the generator records launches whose layers parse: {value}"); + }; + let role = LayerRole::from_name(role).expect("a retired role"); + let uuid = parse_uuid(uuid).expect("the UUID of a recorded layer"); + SandboxLayer::new_in(flavor, role, path, uuid) + }) +} + +#[test] +fn identities_and_layers_parse_as_python_parses_them() { + let fixture = fixture("sandbox.json"); + let mut mismatches = Mismatches::new("identities"); + for case in cases(&fixture, "identities") { + let actual = parse_workload_identity(text(case, "value")) + .map(|(uid, gid)| json!([uid, gid])) + .map_err(|error| error.to_string()); + mismatches.check(case, outcome(case, "identity"), actual); + } + mismatches.finish(); + + let mut mismatches = Mismatches::new("layers"); + for case in cases(&fixture, "layers") { + let expected = + outcome(case, "role").map(|_| json!([case["role"], case["path"], case["uuid"]])); + let actual = SandboxLayer::parse_in(flavor(case), text(case, "value")) + .map(|layer| { + json!([ + layer.role().as_str(), + layer.pure_path().as_str(), + layer.uuid() + ]) + }) + .map_err(|error| error.to_string()); + mismatches.check(case, expected, actual); + } + mismatches.finish(); +} + +#[test] +fn mount_values_targets_and_child_names_match_python() { + let fixture = fixture("sandbox.json"); + let mut mismatches = Mismatches::new("mount_values"); + for case in cases(&fixture, "mount_values") { + let expected = outcome(case, "target") + .map(|_| json!([case["target"], case["host_path"], case["access"]])); + let actual = parse_mount_value(text(case, "value")) + .map(|(target, host_path, access)| json!([target, host_path, access.as_str()])) + .map_err(|error| error.to_string()); + mismatches.check(case, expected, actual); + } + mismatches.finish(); + + let mut mismatches = Mismatches::new("mount_targets"); + for case in cases(&fixture, "mount_targets") { + mismatches.check( + case, + case["error"].as_str().map(str::to_owned), + error(&validate_mount_target(text(case, "target"))), + ); + } + mismatches.finish(); + + let mut mismatches = Mismatches::new("child_names"); + for case in cases(&fixture, "child_names") { + let index = usize::try_from(case["index"].as_u64().unwrap()).unwrap(); + mismatches.check( + case, + text(case, "name"), + aggregate_child_name(index, text(case, "target")).as_str(), + ); + } + mismatches.finish(); +} + +#[test] +fn mounts_lower_to_the_arguments_that_python_gives() { + let fixture = fixture("sandbox.json"); + let mut mismatches = Mismatches::new("mounts"); + for case in cases(&fixture, "mounts") { + let flavor = flavor(case); + let cwd = cwd(&fixture, flavor); + let expected = outcome(case, "mount").map(|_| { + json!([ + case["mount"], + case["arguments"], + case["fragment"], + case["absolute"], + case["absolute_policy"], + ]) + }); + let actual = parse_mount(flavor, case).and_then(|mount| { + let policy: Vec = mount + .absolute_policy_paths_with(&cwd)? + .into_iter() + .map(|(kind, paths)| json!([kind.as_str(), paths])) + .collect(); + Ok(json!([ + mount_fields(&mount), + mounts_openvmm_arguments_with(std::slice::from_ref(&mount), &cwd)?, + mount.command_line_fragment(), + mount_fields(&mount.absolute_with(&cwd)?), + policy, + ])) + }); + mismatches.check(case, expected, actual.map_err(|error| error.to_string())); + } + mismatches.finish(); +} + +#[test] +fn launches_give_the_arguments_and_command_lines_that_python_gives() { + let fixture = fixture("sandbox.json"); + let mut mismatches = Mismatches::new("launches"); + for case in cases(&fixture, "launches") { + let flavor = flavor(case); + let layers = texts(case, "layers") + .iter() + .map(|value| recorded_layer(flavor, value)) + .collect(); + let mounts = case["mounts"] + .as_array() + .unwrap() + .iter() + .map(|spec| parse_mount(flavor, spec)) + .collect::>>() + .expect("the generator records launches whose shares parse"); + let limit = |key: &str| case[key].as_str().map(|value| Int::parse(value).unwrap()); + let identity = &case["identity"]; + let id = |index: usize| u32::try_from(identity[index].as_u64().unwrap()).unwrap(); + let settings = SandboxSettings { + entrypoint: text(case, "entrypoint").to_owned(), + args: texts(case, "args"), + hostname: text(case, "hostname").to_owned(), + workload_identity: (id(0), id(1)), + memory_max: limit("memory_max"), + pids_max: limit("pids_max"), + mounts, + }; + let expected = match (case.get("error"), case.get("kernel_error")) { + (Some(message), _) => Err(message.as_str().unwrap().to_owned()), + (None, kernel_error) => Ok(json!([ + case["arguments"], + case.get("kernel_command_line").unwrap_or(&Value::Null), + kernel_error.unwrap_or(&Value::Null), + ])), + }; + let actual = SandboxLaunch::new_in(flavor, layers, text(case, "scratch"), settings) + .and_then(|launch| { + let arguments = launch.openvmm_arguments_with(&cwd(&fixture, flavor))?; + let command_line = launch.kernel_command_line(text(case, "command_line")); + Ok(json!([ + arguments, + command_line.as_ref().ok(), + error(&command_line), + ])) + }) + .map_err(|error| error.to_string()); + mismatches.check(case, expected, actual); + } + mismatches.finish(); +} + +#[test] +fn sandbox_option_helpers_match_nvx_py() { + let fixture = fixture("sandbox_options.json"); + let mut mismatches = Mismatches::new("run_share_targets"); + for case in cases(&fixture, "run_share_targets") { + let targets = texts(case, "targets"); + let targets: Vec<&str> = targets.iter().map(String::as_str).collect(); + mismatches.check( + case, + case["error"].as_str().map(str::to_owned), + error(&validate_run_share_targets(&targets)), + ); + } + mismatches.finish(); + + let mut mismatches = Mismatches::new("sandbox_mounts"); + for case in cases(&fixture, "sandbox_mounts") { + let files = texts(case, "files"); + let policy_paths: Vec = case["policy"] + .as_array() + .unwrap() + .iter() + .map(|option| PolicyPathOption { + kind: match option[0].as_str().unwrap() { + "denied" => PolicyKind::Denied, + "allowed" => PolicyKind::Allowed, + _ => PolicyKind::Writable, + }, + preceding_mount: option[1] + .as_u64() + .map(|index| usize::try_from(index).unwrap()), + path: option[2].as_str().unwrap().to_owned(), + }) + .collect(); + let owner = case["owner"] + .as_str() + .map(|owner| MountOwner::from_name(owner).unwrap()); + let kind_of = |mount: &SandboxMount| { + let host_path = mount.host_path().to_str().unwrap(); + if files.iter().any(|file| file == host_path) { + MountKind::File + } else { + MountKind::Directory + } + }; + let actual = sandbox_mounts_with( + flavor(case), + &texts(case, "mounts"), + &policy_paths, + owner, + &kind_of, + ) + .map(|mounts| Value::Array(mounts.iter().map(mount_fields).collect())) + .map_err(|error| error.to_string()); + mismatches.check(case, outcome(case, "result"), actual); + } + mismatches.finish(); + + let mut mismatches = Mismatches::new("environments"); + for case in cases(&fixture, "environments") { + mismatches.check( + case, + case["error"].as_str().map(str::to_owned), + error(&validate_exec_environment(&texts(case, "entries"))), + ); + } + mismatches.finish(); +} + +/// Returns the bytes of a document of the generator: its text, its bytes in hex, or a number +/// of spaces. +fn document(case: &Value) -> Vec { + if let Some(text) = case["text"].as_str() { + text.as_bytes().to_vec() + } else if let Some(bytes) = case["bytes"].as_str() { + hex(bytes) + } else { + let spaces = case["spaces"].as_u64().expect("a document"); + vec![b' '; usize::try_from(spaces).unwrap()] + } +} + +#[test] +fn environment_files_and_distro_manifests_read_as_python_reads_them() { + let fixture = fixture("sandbox_options.json"); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("environment.json"); + let path = path.to_str().unwrap(); + let shown = PurePath::native(path).as_str().to_owned(); + let mut mismatches = Mismatches::new("environment_files"); + for case in cases(&fixture, "environment_files") { + std::fs::write(path, document(case)).unwrap(); + let mut options = SandboxOptions::new(SandboxOperation::Exec); + options.environment_file = Some(path.to_owned()); + let expected = match case.get("environment") { + Some(environment) => Ok(environment.clone()), + None => Err(text(case, "error").to_owned()), + }; + let actual = options + .check() + .map(|environment| json!(environment)) + .map_err(|error| error.to_string().replace(&shown, "{path}")); + mismatches.check(case, expected, actual); + } + mismatches.finish(); + + let layer = directory.path().join("distro.erofs"); + std::fs::write(&layer, text(&fixture, "distro_layer")).unwrap(); + let manifest = directory.path().join("distro.erofs.manifest.json"); + let scratch = directory.path().join("scratch.ext4"); + let launch = SandboxLaunch::new( + vec![SandboxLayer::new( + LayerRole::Distro, + layer.to_str().unwrap(), + "11111111-1111-1111-1111-111111111111", + )], + scratch.to_str().unwrap(), + SandboxSettings { + entrypoint: "/sbin/init".to_owned(), + ..SandboxSettings::default() + }, + ) + .unwrap(); + let mut mismatches = Mismatches::new("distro_manifests"); + for case in cases(&fixture, "distro_manifests") { + std::fs::write(&manifest, document(&case["manifest"])).unwrap(); + mismatches.check( + case, + case["error"].as_str().map(str::to_owned), + error(&validate_sandbox_systemd_policy(&launch)), + ); + } + mismatches.finish(); +} diff --git a/nvx-rs/src/host/sandbox/layer.rs b/nvx-rs/src/host/sandbox/layer.rs new file mode 100644 index 000000000..2ce04663c --- /dev/null +++ b/nvx-rs/src/host/sandbox/layer.rs @@ -0,0 +1,212 @@ +//! The read-only EROFS layers of a sandbox. + +use std::fmt; +use std::path::Path; + +use super::refused; +use crate::error::Result; +use crate::host::python::builtins::repr; +use crate::host::python::path::{Flavor, PurePath}; +use crate::host::python::uuid::parse_uuid; + +/// The role of a read-only layer, which fixes the MMIO base of its block device. +/// +/// A sandbox takes exactly one layer, the distro layer (microsoft/nvx#556). The retired roles +/// remain so that the configuration of a sandbox provisioned before then still reads, and fails +/// as no launch accepts them. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum LayerRole { + /// The distribution, the only layer of a sandbox. + Distro, + /// A retired role: a language runtime over the distribution. + Runtime, + /// A retired role: the workload's own files, on top. + Custom, +} + +/// The layer roles that `--layer` accepts and that a launch holds: `LAYER_ROLES`. +pub const LAYER_ROLES: [LayerRole; 1] = [LayerRole::Distro]; + +/// Every role that a configuration may name, the retired ones included. +pub(crate) const KNOWN_LAYER_ROLES: [LayerRole; 3] = + [LayerRole::Distro, LayerRole::Runtime, LayerRole::Custom]; + +impl LayerRole { + /// Returns the role's name: `distro`, or a retired `runtime` or `custom`. + pub const fn as_str(self) -> &'static str { + match self { + Self::Distro => "distro", + Self::Runtime => "runtime", + Self::Custom => "custom", + } + } + + /// Returns the role that `name` names, if any, a retired one included, as a configuration + /// may name it; [`SandboxLayer::parse`] accepts only the [`LAYER_ROLES`]. + pub fn from_name(name: &str) -> Option { + KNOWN_LAYER_ROLES + .into_iter() + .find(|role| role.as_str() == name) + } + + /// Returns the MMIO base of the role's sandbox block device: `BLOCK_MMIO_BASES`. The + /// retired roles keep the bases that they had, which no launch assigns. + pub const fn mmio_base(self) -> u64 { + match self { + Self::Distro => 0xD000_3000, + Self::Runtime => 0xD000_4000, + Self::Custom => 0xD000_5000, + } + } +} + +impl fmt::Display for LayerRole { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// A read-only EROFS layer: its role, its host image, and the UUID of its filesystem. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SandboxLayer { + /// The layer's role. + role: LayerRole, + /// The image, as `pathlib` normalizes it. + path: PurePath, + /// The filesystem UUID, which the guest checks before it mounts the layer. + uuid: String, +} + +impl SandboxLayer { + /// Returns the layer of `role` at `path` with filesystem `uuid`, which are used as given, + /// except that `path` is normalized as `pathlib.Path` normalizes it. + pub fn new(role: LayerRole, path: &str, uuid: impl Into) -> Self { + Self::new_in(Flavor::NATIVE, role, path, uuid) + } + + /// Implements [`new`](Self::new) for paths of `flavor`. + pub(crate) fn new_in( + flavor: Flavor, + role: LayerRole, + path: &str, + uuid: impl Into, + ) -> Self { + Self { + role, + path: PurePath::new(flavor, path), + uuid: uuid.into(), + } + } + + /// Parses a `--layer` value, `ROLE,PATH,EROFS_UUID`, whose role is one of the + /// [`LAYER_ROLES`]. + /// + /// The UUID may take any form that Python's `uuid.UUID` accepts, such as braces, a + /// `urn:uuid:` prefix, no hyphens, or uppercase digits, and is kept in its lowercase + /// hyphenated form. Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). + pub fn parse(value: &str) -> Result { + Self::parse_in(Flavor::NATIVE, value) + } + + /// Implements [`parse`](Self::parse) for paths of `flavor`. + pub(crate) fn parse_in(flavor: Flavor, value: &str) -> Result { + let fields: Vec<&str> = value.split(',').collect(); + let [role, path, uuid] = fields[..] else { + return Err(refused("--layer must be ROLE,PATH,EROFS_UUID")); + }; + let Some(role) = LayerRole::from_name(role).filter(|role| LAYER_ROLES.contains(role)) + else { + let choices: Vec<&str> = LAYER_ROLES.iter().map(|role| role.as_str()).collect(); + return Err(refused(format!( + "unsupported layer role {}; choose {}", + repr(role), + choices.join(", ") + ))); + }; + if path.is_empty() { + return Err(refused(format!("{role} layer path is empty"))); + } + let Some(uuid) = parse_uuid(uuid) else { + return Err(refused(format!( + "{role} layer UUID is invalid: {}", + repr(uuid) + ))); + }; + Ok(Self::new_in(flavor, role, path, uuid)) + } + + /// Returns the layer's role. + pub fn role(&self) -> LayerRole { + self.role + } + + /// Returns the layer's image. + pub fn path(&self) -> &Path { + self.path.as_path() + } + + /// Returns the layer's image as a pure path. + pub(crate) fn pure_path(&self) -> &PurePath { + &self.path + } + + /// Returns the UUID of the layer's filesystem. + pub fn uuid(&self) -> &str { + &self.uuid + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn message(value: &str) -> String { + SandboxLayer::parse(value).unwrap_err().to_string() + } + + #[test] + fn layers_parse_with_python_uuids() { + let layer = SandboxLayer::parse_in( + Flavor::Posix, + "distro,layers//distro.erofs,{22222222-2222-2222-2222-22222222222A}", + ) + .unwrap(); + assert_eq!(layer.role(), LayerRole::Distro); + assert_eq!(layer.pure_path().as_str(), "layers/distro.erofs"); + assert_eq!(layer.uuid(), "22222222-2222-2222-2222-22222222222a"); + let layer = SandboxLayer::parse_in( + Flavor::Windows, + "distro,C:/l/d.erofs,urn:uuid:11111111111111111111111111111111", + ) + .unwrap(); + assert_eq!(layer.pure_path().as_str(), "C:\\l\\d.erofs"); + assert_eq!(layer.uuid(), "11111111-1111-1111-1111-111111111111"); + } + + #[test] + fn layer_parser_rejects_invalid_role_and_uuid() { + assert_eq!( + message("unknown,layer.erofs,11111111-1111-1111-1111-111111111111"), + "unsupported layer role 'unknown'; choose distro" + ); + // The retired roles name no layer that a sandbox takes (microsoft/nvx#556), though a + // configuration may still name them. + for role in ["runtime", "custom"] { + assert_eq!( + message(&format!( + "{role},layer.erofs,22222222-2222-2222-2222-222222222222" + )), + format!("unsupported layer role '{role}'; choose distro") + ); + assert_eq!(LayerRole::from_name(role).unwrap().as_str(), role); + } + assert_eq!( + message("distro,layer.erofs,not-a-uuid"), + "distro layer UUID is invalid: 'not-a-uuid'" + ); + assert_eq!(message("distro,,1"), "distro layer path is empty"); + for value in ["distro", "distro,a", "distro,a,b,c"] { + assert_eq!(message(value), "--layer must be ROLE,PATH,EROFS_UUID"); + } + } +} diff --git a/nvx-rs/src/host/sandbox/mod.rs b/nvx-rs/src/host/sandbox/mod.rs new file mode 100644 index 000000000..c459cb3a7 --- /dev/null +++ b/nvx-rs/src/host/sandbox/mod.rs @@ -0,0 +1,437 @@ +//! The sandbox launch profile: a microVM that runs one workload in a container over a read-only +//! EROFS distro layer and a private ext4 scratch disk, with live virtio-fs shares. +//! +//! This is the profile of `nvx.py sandbox`, ported from the launch profile that +//! `scripts/nvx_tools/sandbox.py` held until microsoft/nvx#359, with its limits, option +//! semantics, OpenVMM arguments, kernel command line, and messages: +//! +//! - [`SandboxLayer`]: the EROFS distro layer, `ROLE,PATH,EROFS_UUID`, the one +//! [`LayerRole`] that a sandbox takes (microsoft/nvx#556), on its own block device. +//! - [`SandboxMount`]: a live share of a host directory or regular file at a guest target, with +//! its access mode, owner, and access-policy paths; [`mount`] holds the rules of guest targets, +//! the aggregate of several shares, and the checks of host paths. +//! - [`SandboxLaunch`]: the layers, the scratch disk, the entrypoint and its arguments, the +//! hostname, the workload identity, the resource limits, and the shares, with +//! [`openvmm_arguments`](SandboxLaunch::openvmm_arguments) and +//! [`kernel_command_line`](SandboxLaunch::kernel_command_line). +//! - [`options`]: the checks that `nvx.py sandbox` and `nvx.py run` apply to their options: the +//! attribution of access-policy paths to shares, the systemd policy, the targets of +//! `nvx.py run` shares, and the managed exec environment. +//! +//! Values are checked as Python checked them: whitespace is what `str.isspace` accepts, +//! integers are what `int` reads ([`Int`]), UUIDs are what `uuid.UUID` reads, host paths are +//! normalized as `pathlib` normalizes them, with backslashes on Windows, and messages quote +//! values with Python's `repr`. +//! +//! # Errors +//! +//! A request that the profile refuses fails with +//! [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), and a missing layer or scratch +//! image with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable). A host query that fails +//! is [`ErrorKind::Io`](crate::ErrorKind::Io), with Python's form of the system's error, such as +//! `[Errno 13] Permission denied: '/path'`. + +pub mod layer; +pub mod mount; +pub mod options; + +use std::io; +use std::path::Path; + +pub use self::layer::{LAYER_ROLES, LayerRole, SandboxLayer}; +pub use self::mount::{MountAccess, MountKind, MountOwner, MountPolicy, PolicyKind, SandboxMount}; +use crate::error::{Error, Result}; +pub use crate::host::python::builtins::{Int, IntError}; +use crate::host::python::builtins::{is_space, split, strip}; +use crate::host::python::fs; +use crate::host::python::path::{Flavor, PurePath}; +use crate::net::is_valid_hostname; + +/// The MMIO base of the scratch disk's sandbox block device. +pub const SCRATCH_MMIO_BASE: u64 = 0xD000_6000; +/// The size of the x86 kernel command line, including its NUL. +pub const KERNEL_COMMAND_LINE_MAX_SIZE: usize = 2048; +/// The part of the kernel command line that OpenVMM keeps for its own tokens. +pub const OPENVMM_COMMAND_LINE_RESERVE: usize = 1024; +/// The part of the kernel command line that the sandbox profile may use, including the NUL and +/// the bootstrap tokens that OpenVMM appends for live shares. +pub const SANDBOX_COMMAND_LINE_MAX_SIZE: usize = + KERNEL_COMMAND_LINE_MAX_SIZE - OPENVMM_COMMAND_LINE_RESERVE; +/// The workload identity by default, `65534:65534`. +pub const DEFAULT_WORKLOAD_IDENTITY: (u32, u32) = (65534, 65534); +/// The entrypoint by default. +pub const DEFAULT_ENTRYPOINT: &str = "/bin/sh"; +/// The hostname by default. +pub const DEFAULT_HOSTNAME: &str = "nvx-sandbox"; + +/// Returns the error of a request that the profile refuses. +pub(crate) fn refused(message: impl Into) -> Error { + Error::unsupported(message) +} + +/// Parses a `--workload-user` value, `UID:GID`, with both IDs between 1 and 4294967295. +/// +/// Each ID is read as Python's `int` reads it, so a sign, underscores between digits, Unicode +/// decimal digits, and surrounding whitespace are accepted. Fails with +/// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). +pub fn parse_workload_identity(value: &str) -> Result<(u32, u32)> { + let fields: Vec<&str> = value.split(':').collect(); + let [uid, gid] = fields[..] else { + return Err(refused("--workload-user must be UID:GID")); + }; + let (Ok(uid), Ok(gid)) = (Int::parse(uid), Int::parse(gid)) else { + return Err(refused("--workload-user must contain numeric UID and GID")); + }; + let id = |value: &Int, name: &str| { + value.to_u32().filter(|&id| id != 0).ok_or_else(|| { + refused(format!( + "--workload-user {name} must be between 1 and 4294967295" + )) + }) + }; + Ok((id(&uid, "UID")?, id(&gid, "GID")?)) +} + +/// Checks that `path` is a regular file, following links, as `require_file` of `nvx.py` does. +/// +/// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) and the message +/// `{description} not found: {path}`, with `path` normalized as `pathlib.Path` normalizes it. +pub fn require_file(path: &Path, description: &str) -> Result<()> { + match path.to_str() { + Some(text) => require_pure_file(&PurePath::native(text), description), + None if fs::is_file(path) => Ok(()), + None => Err(Error::unavailable(format!( + "{description} not found: {}", + path.display() + ))), + } +} + +/// Refuses a disk path that OpenVMM's `--microvm-sandbox-block` value could not hold: one with +/// a comma or semicolon. +fn reject_disk_path(path: &PurePath) -> Result<()> { + let value = path.as_str(); + if value.contains([',', ';']) { + return Err(refused(format!( + "disk paths containing commas or semicolons are unsupported: {value}" + ))); + } + Ok(()) +} + +/// The settings of a [`SandboxLaunch`] besides its layers and scratch disk, with the defaults of +/// `nvx.py sandbox`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SandboxSettings { + /// The absolute path of the workload's program in the container, without whitespace. + pub entrypoint: String, + /// The workload's arguments, each nonempty and without whitespace. + pub args: Vec, + /// The workload's hostname: a lowercase RFC 1123 label of up to 63 characters. + pub hostname: String, + /// The workload's UID and GID, neither of them root's. + pub workload_identity: (u32, u32), + /// The workload's memory limit in bytes, `memory.max` of its cgroup, if positive. + pub memory_max: Option, + /// The most processes that the workload may run, if positive; the guest's own init counts + /// too, so its `pids.max` is one more. + pub pids_max: Option, + /// The live shares, in child order. + pub mounts: Vec, +} + +impl Default for SandboxSettings { + fn default() -> Self { + Self { + entrypoint: DEFAULT_ENTRYPOINT.to_owned(), + args: Vec::new(), + hostname: DEFAULT_HOSTNAME.to_owned(), + workload_identity: DEFAULT_WORKLOAD_IDENTITY, + memory_max: None, + pids_max: None, + mounts: Vec::new(), + } + } +} + +/// The launch of a sandbox: everything that its OpenVMM arguments and kernel command line +/// depend on. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SandboxLaunch { + /// The read-only layers, in the order given. + layers: Vec, + /// The ext4 scratch image. + scratch: PurePath, + /// The other settings. + settings: SandboxSettings, +} + +impl SandboxLaunch { + /// Returns the launch of `layers` over the ext4 image at `scratch`, which is normalized as + /// `pathlib.Path` normalizes it. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), checking in this + /// order: no duplicate roles, exactly one layer, the distro layer, the entrypoint, the + /// arguments, the hostname, a non-root identity, positive limits, and shares whose targets do + /// not overlap and that have one owner. + pub fn new( + layers: Vec, + scratch: &str, + settings: SandboxSettings, + ) -> Result { + Self::new_in(Flavor::NATIVE, layers, scratch, settings) + } + + /// Implements [`new`](Self::new) for a scratch path of `flavor`. + pub(crate) fn new_in( + flavor: Flavor, + layers: Vec, + scratch: &str, + settings: SandboxSettings, + ) -> Result { + // A configuration may name a retired role, so the duplicates are those of every role. + let mut duplicates: Vec<&str> = layer::KNOWN_LAYER_ROLES + .iter() + .filter(|&&role| layers.iter().filter(|layer| layer.role() == role).count() > 1) + .map(|role| role.as_str()) + .collect(); + if !duplicates.is_empty() { + duplicates.sort_unstable(); + return Err(refused(format!( + "duplicate sandbox layer role(s): {}", + duplicates.join(", ") + ))); + } + if !matches!(&layers[..], [layer] if layer.role() == LayerRole::Distro) { + return Err(refused( + "a sandbox requires exactly one read-only distro layer", + )); + } + let SandboxSettings { + entrypoint, + args, + hostname, + workload_identity: (uid, gid), + memory_max, + pids_max, + mounts, + } = &settings; + if !entrypoint.starts_with('/') || entrypoint.chars().any(is_space) { + return Err(refused( + "sandbox entrypoint must be an absolute path without spaces", + )); + } + if args + .iter() + .any(|argument| argument.is_empty() || argument.chars().any(is_space)) + { + return Err(refused( + "sandbox arguments must be nonempty and contain no spaces", + )); + } + if !is_valid_hostname(hostname) { + return Err(refused( + "sandbox hostname must be a lowercase RFC 1123 label up to 63 characters", + )); + } + if *uid == 0 || *gid == 0 { + return Err(refused( + "sandbox workload identity must be a non-root UID:GID", + )); + } + for (name, value) in [("memory-max", memory_max), ("pids-max", pids_max)] { + if value.as_ref().is_some_and(|value| !value.is_positive()) { + return Err(refused(format!("--{name} must be positive"))); + } + } + mount::validate_mounts(mounts)?; + Ok(Self { + layers, + scratch: PurePath::new(flavor, scratch), + settings, + }) + } + + /// Returns the layers, in the order given. + pub fn layers(&self) -> &[SandboxLayer] { + &self.layers + } + + /// Returns the scratch image. + pub fn scratch(&self) -> &Path { + self.scratch.as_path() + } + + /// Returns the settings besides the layers and the scratch image. + pub fn settings(&self) -> &SandboxSettings { + &self.settings + } + + /// Returns the shares. + pub fn mounts(&self) -> &[SandboxMount] { + &self.settings.mounts + } + + /// Checks the launch against the host: each layer and the scratch image must be a regular + /// file whose path has no comma or semicolon, each share must pass + /// [`SandboxMount::validated`], and their host paths [`mount::validate_mount_host_paths`]. + /// + /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) for a missing image, + /// and otherwise as those checks do. + pub fn validated(&self) -> Result<()> { + self.validated_with(&mount::HostQueries::native()) + } + + /// Implements [`validated`](Self::validated) with the given host queries. + pub(crate) fn validated_with(&self, host: &mount::HostQueries<'_>) -> Result<()> { + for layer in &self.layers { + require_pure_file(layer.pure_path(), &format!("{} EROFS layer", layer.role()))?; + reject_disk_path(layer.pure_path())?; + } + require_pure_file(&self.scratch, "ext4 scratch image")?; + reject_disk_path(&self.scratch)?; + for mount in &self.settings.mounts { + mount.validated()?; + } + mount::validate_mount_host_paths_with(&self.settings.mounts, host) + } + + /// Returns the layers in role order, which is the distro layer alone, since a launch holds + /// only it. + pub fn ordered_layers(&self) -> Vec<&SandboxLayer> { + LAYER_ROLES + .iter() + .filter_map(|&role| self.layers.iter().rev().find(|layer| layer.role() == role)) + .collect() + } + + /// Returns the OpenVMM arguments of the sandbox profile: the microVM machine, a read-only + /// sandbox block device for each layer in role order, the scratch device, the workload + /// identity, and the arguments that attach the shares. + /// + /// Fails as [`mount::mounts_openvmm_arguments`] does. + pub fn openvmm_arguments(&self) -> Result> { + self.openvmm_arguments_with(&fs::cwd) + } + + /// Implements [`openvmm_arguments`](Self::openvmm_arguments) with `cwd` as `Path.cwd()`. + pub(crate) fn openvmm_arguments_with( + &self, + cwd: &dyn Fn() -> io::Result, + ) -> Result> { + let mut arguments = vec!["--machine".to_owned(), "microvm".to_owned()]; + for layer in self.ordered_layers() { + arguments.push("--microvm-sandbox-block".to_owned()); + arguments.push(format!( + "{}:file:{},ro", + layer.role(), + layer.pure_path().as_str() + )); + } + let (uid, gid) = self.settings.workload_identity; + arguments.extend([ + "--microvm-sandbox-block".to_owned(), + format!("scratch:file:{}", self.scratch.as_str()), + "--microvm-workload-identity".to_owned(), + format!("{uid}:{gid}"), + ]); + arguments.extend(mount::mounts_openvmm_arguments_with( + &self.settings.mounts, + cwd, + )?); + Ok(arguments) + } + + /// Returns the kernel command line of the sandbox: `user_command_line`, stripped of Python + /// whitespace, then `nvx_sandbox=1`, a `nvx_layer=` token for each layer in role order, + /// `nvx_scratch=`, `nvx_entrypoint=`, `nvx_hostname=`, a `nvx_arg=` token for each + /// argument, `nvx_memory_max=` and `nvx_pids_max=` when set, and the `nvx_share=` tokens of + /// an aggregate. + /// + /// The user's command line may hold no NUL and no `nvx_*` or `virtfs_*` token, which the + /// profile and OpenVMM own. The command line, the bootstrap tokens that OpenVMM appends for + /// the shares, and a NUL must fit in [`SANDBOX_COMMAND_LINE_MAX_SIZE`] bytes. Fails with + /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). + pub fn kernel_command_line(&self, user_command_line: &str) -> Result { + if user_command_line.contains('\0') { + return Err(refused("kernel command line contains an embedded NUL")); + } + for token in split(user_command_line) { + let name = token.split('=').next().unwrap_or(token); + if token.starts_with("nvx_") { + return Err(refused(format!("{name} is owned by the sandbox profile"))); + } + if token.starts_with("virtfs_") { + return Err(refused(format!( + "{name} is owned by the sandbox --mount option" + ))); + } + } + let settings = &self.settings; + let mut tokens = vec![ + strip(user_command_line).to_owned(), + "nvx_sandbox=1".to_owned(), + ]; + for layer in self.ordered_layers() { + tokens.push(format!( + "nvx_layer={},0x{:x},{}", + layer.role(), + layer.role().mmio_base(), + layer.uuid() + )); + } + tokens.extend([ + format!("nvx_scratch=0x{SCRATCH_MMIO_BASE:x},ext4"), + format!("nvx_entrypoint={}", settings.entrypoint), + format!("nvx_hostname={}", settings.hostname), + ]); + tokens.extend( + settings + .args + .iter() + .map(|argument| format!("nvx_arg={argument}")), + ); + if let Some(memory_max) = &settings.memory_max { + tokens.push(format!("nvx_memory_max={memory_max}")); + } + if let Some(pids_max) = &settings.pids_max { + tokens.push(format!("nvx_pids_max={}", pids_max.plus_one())); + } + if mount::mounts_use_aggregate(&settings.mounts) { + let shares: Vec<_> = settings + .mounts + .iter() + .map(|mount| (mount.guest_target(), mount.access(), mount.kind())) + .collect(); + tokens.extend(mount::aggregate_share_tokens(&shares)); + } + tokens.retain(|token| !token.is_empty()); + let command_line = tokens.join(" "); + // OpenVMM appends the shares' bootstrap tokens after this command line, so they consume + // the same x86 budget. + let fragment = mount::mounts_command_line_fragment(&settings.mounts); + if command_line.len() + fragment.len() + 1 > SANDBOX_COMMAND_LINE_MAX_SIZE { + return Err(refused( + "sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer \ + --mount shares or use shorter targets and arguments", + )); + } + Ok(command_line) + } +} + +/// Checks that a pure path is a regular file, as [`require_file`] does. +fn require_pure_file(path: &PurePath, description: &str) -> Result<()> { + if fs::is_file(path.as_path()) { + Ok(()) + } else { + Err(Error::unavailable(format!( + "{description} not found: {path}" + ))) + } +} + +#[cfg(test)] +mod golden; +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/sandbox/mount.rs b/nvx-rs/src/host/sandbox/mount.rs new file mode 100644 index 000000000..ce6528df5 --- /dev/null +++ b/nvx-rs/src/host/sandbox/mount.rs @@ -0,0 +1,969 @@ +//! Live virtio-fs shares of host directories and files inside the sandbox's container rootfs. +//! +//! OpenVMM attaches one virtio-fs device, with the tag [`MOUNT_TAG`]. One directory share uses +//! it directly, and the guest mounts it at its target. Several shares, or a file, share it as the +//! children of an aggregate, each named by [`aggregate_child_name`], which the guest mounts at +//! [`AGGREGATE_MOUNT_TARGET`], a directory that only the guest's root can enter, before it binds +//! each child at its target, as the `nvx_share=` kernel parameters of [`aggregate_share_tokens`] +//! tell it. +//! +//! A share's access-policy paths are host paths: `--mount-deny` hides a path, `--mount-allow` +//! exposes a path inside a denied path again, and `--mount-write` makes a path one of the only +//! writable parts of a read-write share. OpenVMM resolves relative paths in the only share, and +//! attributes absolute paths to the child that contains them. + +use std::fmt; +use std::io; +use std::path::Path; + +use sha2::{Digest, Sha256}; + +use super::refused; +use crate::error::{Error, Result}; +use crate::host::python::builtins::{is_space, repr}; +use crate::host::python::fs; +use crate::host::python::path::{Flavor, PurePath}; + +/// The tag of OpenVMM's virtio-fs device. +pub const MOUNT_TAG: &str = "microvm"; +/// The guest directory at which the guest mounts the aggregate of several shares. +pub const AGGREGATE_MOUNT_TARGET: &str = "/run/nvx/shares"; +/// The guest trees that the container runtime owns, which a share may neither be in nor contain: +/// it mounts procfs, sysfs, and a private `/dev` over them, stages its tools under +/// `/.nvx-agent`, binds the workload machine ID at `/etc/machine-id`, which it first replaces +/// with a regular file, and binds its time ABI state over `/run/nvx/time`. +pub const RESERVED_MOUNT_TARGETS: [&str; 6] = [ + "/proc", + "/sys", + "/dev", + "/.nvx-agent", + "/etc/machine-id", + "/run/nvx/time", +]; +/// The guest paths at which a share may not be, though it may be below them: `/etc`, and `/run` +/// and `/run/nvx`, which the container runtime creates below the container root. +pub const RESERVED_EXACT_MOUNT_TARGETS: [&str; 3] = ["/etc", "/run", "/run/nvx"]; +/// The most denied, allowed, and writable paths each that OpenVMM accepts for a share. +pub const MAX_MOUNT_POLICY_PATHS: usize = 128; +/// The longest guest target, in UTF-8 bytes. +pub const MAX_MOUNT_TARGET_BYTES: usize = 4096; + +/// The bootstrap tokens that OpenVMM appends to the kernel command line for its share, with the +/// guest directory, the tag, and the mode. +const MOUNT_COMMAND_LINE_FRAGMENT: [&str; 3] = [" virtfs_dir=", " virtfs_tag=", " virtfs_mode="]; +/// The token that OpenVMM appends after those of an aggregate. +const AGGREGATE_COMMAND_LINE_SUFFIX: &str = " virtfs_aggregate=1"; + +/// The access mode of a share. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MountAccess { + /// `ro`, the default. + ReadOnly, + /// `rw`. + ReadWrite, +} + +impl MountAccess { + /// Returns the mode's name: `ro` or `rw`. + pub const fn as_str(self) -> &'static str { + match self { + Self::ReadOnly => "ro", + Self::ReadWrite => "rw", + } + } + + /// Returns the mode that `name` names, as a recorded share gives it. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for another name. + pub fn from_name(name: &str) -> Result { + match name { + "ro" => Ok(Self::ReadOnly), + "rw" => Ok(Self::ReadWrite), + _ => Err(refused(format!( + "unsupported sandbox mount mode {}; choose ro or rw", + repr(name) + ))), + } + } +} + +impl fmt::Display for MountAccess { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// The host identity of a share's file operations. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MountOwner { + /// `vmm`, the default: every operation runs as OpenVMM. + Vmm, + /// `caller`: each operation runs as the guest caller's identity, with guest root squashed to + /// the owner of the shared directory or file. Linux hosts only. + Caller, +} + +impl MountOwner { + /// Returns the owner's name: `vmm` or `caller`. + pub const fn as_str(self) -> &'static str { + match self { + Self::Vmm => "vmm", + Self::Caller => "caller", + } + } + + /// Returns the owner that `name` names, as a recorded share gives it. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for another name. + pub fn from_name(name: &str) -> Result { + match name { + "vmm" => Ok(Self::Vmm), + "caller" => Ok(Self::Caller), + _ => Err(refused(format!( + "unsupported sandbox mount owner {}; choose vmm or caller", + repr(name) + ))), + } + } +} + +impl fmt::Display for MountOwner { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// What a share binds: a host directory, or one regular host file without its directory, which +/// only the child of an aggregate can share. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MountKind { + /// `directory`, the default. + Directory, + /// `file`. + File, +} + +impl MountKind { + /// Returns the kind's name: `directory` or `file`. + pub const fn as_str(self) -> &'static str { + match self { + Self::Directory => "directory", + Self::File => "file", + } + } + + /// Returns the kind that `name` names, as a recorded share gives it. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for another name. + pub fn from_name(name: &str) -> Result { + match name { + "directory" => Ok(Self::Directory), + "file" => Ok(Self::File), + _ => Err(refused(format!( + "unsupported sandbox mount kind {}; choose directory or file", + repr(name) + ))), + } + } +} + +impl fmt::Display for MountKind { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// A kind of access-policy path. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum PolicyKind { + /// A path that the share hides: `--mount-deny`. + Denied, + /// A path inside a denied path that the share exposes again: `--mount-allow`. + Allowed, + /// One of the only writable paths of a read-write share: `--mount-write`. + Writable, +} + +/// The kinds of access-policy paths, in the order in which OpenVMM's options carry them. +pub const POLICY_KINDS: [PolicyKind; 3] = [ + PolicyKind::Denied, + PolicyKind::Allowed, + PolicyKind::Writable, +]; + +impl PolicyKind { + /// Returns the kind's name in messages: `denied`, `allowed`, or `writable`. + pub const fn as_str(self) -> &'static str { + match self { + Self::Denied => "denied", + Self::Allowed => "allowed", + Self::Writable => "writable", + } + } + + /// Returns the option that requests a path of this kind: `--mount-deny`, `--mount-allow`, or + /// `--mount-write`. + pub const fn option(self) -> &'static str { + match self { + Self::Denied => "--mount-deny", + Self::Allowed => "--mount-allow", + Self::Writable => "--mount-write", + } + } +} + +impl fmt::Display for PolicyKind { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// The access-policy paths of a share, as given: host paths, relative to the share or absolute. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct MountPolicy { + /// Paths that the share hides. + pub denied: Vec, + /// Paths inside denied paths that the share exposes again. + pub allowed: Vec, + /// The only paths of a read-write share that the workload can modify, if any. + pub writable: Vec, +} + +impl MountPolicy { + /// Returns the paths of `kind`. + pub fn paths(&self, kind: PolicyKind) -> &[String] { + match kind { + PolicyKind::Denied => &self.denied, + PolicyKind::Allowed => &self.allowed, + PolicyKind::Writable => &self.writable, + } + } + + /// Returns whether the policy has no paths. + pub fn is_empty(&self) -> bool { + POLICY_KINDS.iter().all(|&kind| self.paths(kind).is_empty()) + } +} + +/// Splits a `--mount` value, `GUEST_TARGET,HOST_PATH[,ro|rw]`, into its guest target, host path, +/// and access mode, which defaults to `ro`. +/// +/// The guest target ends at the first comma, and with another comma the mode follows the last +/// one, so a host path may contain commas when the mode is given. OpenVMM reads its `--mount` +/// and `--mount-child` values the same way. Fails with +/// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). +pub fn parse_mount_value(value: &str) -> Result<(String, String, MountAccess)> { + let Some((target, rest)) = value.split_once(',') else { + return Err(refused("--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]")); + }; + let (host_path, access) = rest.rsplit_once(',').unwrap_or((rest, "ro")); + if host_path.is_empty() { + return Err(refused("--mount host path is empty")); + } + let access = match access { + "ro" => MountAccess::ReadOnly, + "rw" => MountAccess::ReadWrite, + _ => { + return Err(refused(format!( + "unsupported --mount mode {}; choose ro or rw (if HOST_PATH contains commas, end \ + the value with ,ro or ,rw)", + repr(access) + ))); + } + }; + Ok((target.to_owned(), host_path.to_owned(), access)) +} + +/// Returns whether `target` holds a character that no guest target may: Python whitespace, NUL, +/// `\`, `=`, or `,`. +pub(crate) fn has_forbidden_character(target: &str) -> bool { + target + .chars() + .any(|character| is_space(character) || matches!(character, '\0' | '\\' | '=' | ',')) +} + +/// Returns whether a guest target has an empty, `.`, or `..` component after its leading slash. +pub(crate) fn has_irregular_component(target: &str) -> bool { + target + .split('/') + .skip(1) + .any(|component| matches!(component, "" | "." | "..")) +} + +/// Checks a live share's guest target inside the sandbox container rootfs: an absolute non-root +/// path of at most [`MAX_MOUNT_TARGET_BYTES`] bytes, without empty, dot, or parent components, +/// whitespace, `\`, `=`, or `,`, and outside the [`RESERVED_MOUNT_TARGETS`] trees and the +/// [`RESERVED_EXACT_MOUNT_TARGETS`]. +/// +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). +pub fn validate_mount_target(target: &str) -> Result<()> { + if !target.starts_with('/') + || target == "/" + || target.len() > MAX_MOUNT_TARGET_BYTES + || has_forbidden_character(target) + || has_irregular_component(target) + { + return Err(refused(format!( + "invalid sandbox mount target {}: expected an absolute non-root path without empty, \ + dot, or parent components, whitespace, '\\', '=', or ','", + repr(target) + ))); + } + for reserved in RESERVED_MOUNT_TARGETS { + if target == reserved + || target + .strip_prefix(reserved) + .is_some_and(|rest| rest.starts_with('/')) + { + return Err(refused(format!( + "sandbox mount target {target} overlaps the reserved {reserved} tree" + ))); + } + } + if RESERVED_EXACT_MOUNT_TARGETS.contains(&target) { + return Err(refused(format!( + "sandbox mount target {target} is reserved" + ))); + } + Ok(()) +} + +/// Refuses an owner that this host cannot enforce: `caller` on Windows, which has no +/// per-request POSIX identity for OpenVMM to switch to. +/// +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). +pub fn require_mount_owner_supported(owner: MountOwner) -> Result<()> { + require_mount_owner_supported_on(owner, cfg!(windows)) +} + +/// Implements [`require_mount_owner_supported`] for a Windows host or another. +pub(crate) fn require_mount_owner_supported_on(owner: MountOwner, windows: bool) -> Result<()> { + if owner == MountOwner::Caller && windows { + return Err(refused( + "--mount-owner caller requires a Linux host; Windows has no per-request POSIX \ + identity for OpenVMM to switch to", + )); + } + Ok(()) +} + +/// Returns whether two guest targets overlap: whether they are equal, or one contains the other. +pub(crate) fn targets_overlap(left: &str, right: &str) -> bool { + let inside = |path: &str, directory: &str| { + path.strip_prefix(directory) + .is_some_and(|rest| rest.starts_with('/')) + }; + left == right || inside(left, right) || inside(right, left) +} + +/// Returns the kind of share that `host_path` requests: a file for a regular file, and +/// otherwise a directory, which [`SandboxMount::validated`] then requires. +pub fn mount_kind(host_path: &Path) -> MountKind { + if fs::is_file(host_path) { + MountKind::File + } else { + MountKind::Directory + } +} + +/// Returns the error for an `OSError` that an operation on `path` raised. +pub(crate) fn os_error(error: &io::Error, path: Option<&str>) -> Error { + Error::io(fs::os_error_message(error, path)) +} + +/// A live virtio-fs share mounted inside the sandbox container rootfs. +/// +/// Its denied paths hide paths in the share, its allowed paths expose paths inside denied paths +/// again, and its writable paths, when present, are the only paths of a read-write share that +/// the workload can modify. A share of a file binds one regular host file, whose mode applies to +/// the whole file, so it has none of them. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SandboxMount { + /// The guest directory or file at which the share appears. + guest_target: String, + /// The host directory or file, as `pathlib` normalizes it. + host_path: PurePath, + /// The access mode. + access: MountAccess, + /// The access-policy paths. + policy: MountPolicy, + /// The identity of the share's file operations. + owner: MountOwner, + /// Whether the share binds a directory or a file. + kind: MountKind, +} + +impl SandboxMount { + /// Returns the share of `host_path` at `guest_target`, with `host_path` normalized as + /// `pathlib.Path` normalizes it. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a guest target + /// that [`validate_mount_target`] refuses, a host path with a NUL character or a `..` + /// component, policy paths for a file, more than [`MAX_MOUNT_POLICY_PATHS`] policy paths of a + /// kind, empty or repeated ones, allowed paths without denied paths, and writable paths of a + /// read-only share. + pub fn new( + guest_target: &str, + host_path: &str, + access: MountAccess, + policy: MountPolicy, + owner: MountOwner, + kind: MountKind, + ) -> Result { + Self::new_in( + Flavor::NATIVE, + guest_target, + host_path, + access, + policy, + owner, + kind, + ) + } + + /// Implements [`new`](Self::new) for host paths of `flavor`. + pub(crate) fn new_in( + flavor: Flavor, + guest_target: &str, + host_path: &str, + access: MountAccess, + policy: MountPolicy, + owner: MountOwner, + kind: MountKind, + ) -> Result { + Self::checked(Self { + guest_target: guest_target.to_owned(), + host_path: PurePath::new(flavor, host_path), + access, + policy, + owner, + kind, + }) + } + + /// Parses a `--mount` value with [`parse_mount_value`] into a share of the given policy, + /// owner, and kind. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), as + /// [`parse_mount_value`] and [`new`](Self::new) do. + pub fn parse( + value: &str, + policy: MountPolicy, + owner: MountOwner, + kind: MountKind, + ) -> Result { + Self::parse_in(Flavor::NATIVE, value, policy, owner, kind) + } + + /// Implements [`parse`](Self::parse) for host paths of `flavor`. + pub(crate) fn parse_in( + flavor: Flavor, + value: &str, + policy: MountPolicy, + owner: MountOwner, + kind: MountKind, + ) -> Result { + let (guest_target, host_path, access) = parse_mount_value(value)?; + Self::new_in( + flavor, + &guest_target, + &host_path, + access, + policy, + owner, + kind, + ) + } + + /// Checks a share's fields in the order in which Python's `__post_init__` checks them. + fn checked(self) -> Result { + validate_mount_target(&self.guest_target)?; + let raw_path = self.host_path.as_str(); + if raw_path.contains('\0') { + return Err(refused(format!( + "sandbox mount host path contains a NUL character: {}", + repr(raw_path) + ))); + } + if self.host_path.parts().iter().any(|part| part == "..") { + return Err(refused(format!( + "sandbox mount host path contains a parent component: {raw_path}" + ))); + } + if self.kind == MountKind::File && !self.policy.is_empty() { + return Err(refused(format!( + "--mount-deny, --mount-allow, and --mount-write name paths inside a shared \ + directory, but --mount {} shares a file, whose mode applies to the whole file", + self.guest_target + ))); + } + for kind in POLICY_KINDS { + let paths = self.policy.paths(kind); + if paths.len() > MAX_MOUNT_POLICY_PATHS { + return Err(refused(format!( + "a sandbox mount permits at most {MAX_MOUNT_POLICY_PATHS} {kind} paths" + ))); + } + if paths + .iter() + .any(|path| path.is_empty() || path.contains('\0')) + { + return Err(refused(format!( + "sandbox mount {kind} paths must be nonempty" + ))); + } + let unique: std::collections::HashSet<&String> = paths.iter().collect(); + if unique.len() != paths.len() { + return Err(refused(format!( + "sandbox mount {kind} paths must be unique" + ))); + } + } + if !self.policy.allowed.is_empty() && self.policy.denied.is_empty() { + return Err(refused( + "--mount-allow exposes a path inside a --mount-deny path of the same share, \ + which has none", + )); + } + if !self.policy.writable.is_empty() && self.access != MountAccess::ReadWrite { + return Err(refused(format!( + "--mount-write narrows the writes of a read-write share; --mount {} is read-only", + self.guest_target + ))); + } + Ok(self) + } + + /// Returns the share with another kind, checked again, as `dataclasses.replace` does. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a file share + /// with policy paths. + pub fn with_kind(&self, kind: MountKind) -> Result { + Self::checked(Self { + kind, + ..self.clone() + }) + } + + /// Returns the guest target. + pub fn guest_target(&self) -> &str { + &self.guest_target + } + + /// Returns the host directory or file. + pub fn host_path(&self) -> &Path { + self.host_path.as_path() + } + + /// Returns the access mode. + pub fn access(&self) -> MountAccess { + self.access + } + + /// Returns the access-policy paths. + pub fn policy(&self) -> &MountPolicy { + &self.policy + } + + /// Returns the identity of the share's file operations. + pub fn owner(&self) -> MountOwner { + self.owner + } + + /// Returns whether the share binds a directory or a file. + pub fn kind(&self) -> MountKind { + self.kind + } + + /// Checks the share against the host: its owner must be one that the host supports, its + /// host path must be a regular file or a plain directory, as its kind requires, without + /// being a symbolic link, and with the `caller` owner on Linux, it must not belong to UID 0 + /// or GID 0, to which guest root would be squashed. + /// + /// The kind was fixed when the share was requested, so a directory that replaced a shared + /// file is not shared in its place. Fails with + /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), or + /// [`ErrorKind::Io`](crate::ErrorKind::Io) if the host path cannot be examined. + pub fn validated(&self) -> Result<()> { + require_mount_owner_supported(self.owner)?; + let host_path = self.host_path.as_path(); + match self.kind { + MountKind::File if fs::is_symlink(host_path) || !fs::is_file(host_path) => { + return Err(refused(format!( + "sandbox mount host path is not a regular file: {}", + self.host_path + ))); + } + MountKind::Directory if fs::is_symlink(host_path) || !fs::is_dir(host_path) => { + return Err(refused(format!( + "sandbox mount host path is not a plain directory: {}", + self.host_path + ))); + } + _ => {} + } + if self.owner == MountOwner::Caller && !cfg!(windows) { + let status = fs::stat(host_path) + .map_err(|error| os_error(&error, Some(self.host_path.as_str())))?; + if status.uid == 0 || status.gid == 0 { + return Err(refused(format!( + "--mount-owner caller squashes guest root to the owner of the shared {}, so \ + {} must not be owned by UID 0 or GID 0", + self.kind, self.host_path + ))); + } + } + Ok(()) + } + + /// Returns the share with its host path made absolute by joining it to the current + /// directory, without normalizing it, so OpenVMM still sees, and refuses, every + /// symbolic-link component of the path that was requested. + /// + /// Fails with [`ErrorKind::Io`](crate::ErrorKind::Io) if a relative host path meets an + /// unreadable current directory. + pub fn absolute(&self) -> Result { + self.absolute_with(&fs::cwd) + } + + /// Implements [`absolute`](Self::absolute) with `cwd` as `Path.cwd()`. + pub(crate) fn absolute_with(&self, cwd: &dyn Fn() -> io::Result) -> Result { + if self.host_path.is_absolute() { + return Ok(self.clone()); + } + let directory = cwd().map_err(|error| os_error(&error, None))?; + Self::checked(Self { + host_path: directory.join(self.host_path.as_str()), + ..self.clone() + }) + } + + /// Returns each kind of access-policy path with its requested paths, relative paths joined + /// to the absolute host path of the share, as OpenVMM attributes them to the children of an + /// aggregate. + /// + /// Fails as [`absolute`](Self::absolute) does. + pub fn absolute_policy_paths(&self) -> Result)>> { + self.absolute_policy_paths_with(&fs::cwd) + } + + /// Implements [`absolute_policy_paths`](Self::absolute_policy_paths) with `cwd` as + /// `Path.cwd()`. + pub(crate) fn absolute_policy_paths_with( + &self, + cwd: &dyn Fn() -> io::Result, + ) -> Result)>> { + let host_path = self.absolute_with(cwd)?.host_path; + let flavor = host_path.flavor(); + Ok(POLICY_KINDS + .iter() + .map(|&kind| { + let paths = self + .policy + .paths(kind) + .iter() + .map(|path| { + if PurePath::new(flavor, path).is_absolute() { + path.clone() + } else { + host_path.join(path).as_str().to_owned() + } + }) + .collect(); + (kind, paths) + }) + .collect()) + } + + /// Returns the bootstrap tokens that OpenVMM appends to the kernel command line for this + /// share alone: its target, the device tag, and its mode. + pub fn command_line_fragment(&self) -> String { + command_line_fragment(&self.guest_target, self.access) + } +} + +/// Returns the bootstrap tokens of a share at `target` with `access`. +fn command_line_fragment(target: &str, access: MountAccess) -> String { + let [directory, tag, mode] = MOUNT_COMMAND_LINE_FRAGMENT; + format!("{directory}{target}{tag}{MOUNT_TAG}{mode}{access}") +} + +/// Returns whether `mounts` share the virtio-fs device as the children of an aggregate: several +/// shares do, and so does a file, because the guest mounts the device's root, which is a +/// directory. +pub fn mounts_use_aggregate(mounts: &[SandboxMount]) -> bool { + mounts.len() > 1 || mounts.iter().any(|mount| mount.kind == MountKind::File) +} + +/// Checks the live shares that one sandbox attaches, in child order: no target may overlap +/// another, so that no share hides another, and every share has the same owner. +/// +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). +pub fn validate_mounts(mounts: &[SandboxMount]) -> Result<()> { + for (index, mount) in mounts.iter().enumerate() { + for other in &mounts[..index] { + if targets_overlap(&other.guest_target, &mount.guest_target) { + return Err(refused(format!( + "sandbox mount targets {} and {} overlap; a share cannot hide another", + other.guest_target, mount.guest_target + ))); + } + } + } + if mounts.iter().any(|mount| mount.owner != mounts[0].owner) { + return Err(refused( + "every sandbox --mount must use the same --mount-owner", + )); + } + Ok(()) +} + +/// The queries of the host that [`validate_mount_host_paths`] makes, which tests replace. +pub(crate) struct HostQueries<'a> { + /// `Path.cwd()`. + pub(crate) cwd: &'a dyn Fn() -> io::Result, + /// `Path.resolve()`. + pub(crate) resolve: &'a dyn Fn(&PurePath) -> io::Result, + /// `(st_dev, st_ino)` of `Path.stat()`. + pub(crate) identity: &'a dyn Fn(&PurePath) -> io::Result<(u64, u128)>, +} + +impl HostQueries<'_> { + /// Returns the queries of this host. + pub(crate) fn native() -> HostQueries<'static> { + HostQueries { + cwd: &fs::cwd, + resolve: &fs::resolve, + identity: &|path| fs::stat(path.as_path()).map(|status| status.identity()), + } + } +} + +/// Refuses shares whose host paths overlap, and policy paths outside their share. +/// +/// Two host paths overlap if one contains the other once both are resolved, or, since resolving +/// follows symbolic links but not bind mounts, if one has the file identity of the other or of +/// one of its ancestors, as a hard link or a path inside a bind mount of it does. A shared file +/// therefore overlaps a shared directory that contains it, or another share of the same file, +/// but not a file beside it. With several shares, each policy path must name a path inside the +/// host directory of its share, and only a denied path may name the share itself. One share has +/// nothing to compare. +/// +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), or +/// [`ErrorKind::Io`](crate::ErrorKind::Io) if a path cannot be resolved or examined. +pub fn validate_mount_host_paths(mounts: &[SandboxMount]) -> Result<()> { + validate_mount_host_paths_with(mounts, &HostQueries::native()) +} + +/// Implements [`validate_mount_host_paths`] with the given host queries. +pub(crate) fn validate_mount_host_paths_with( + mounts: &[SandboxMount], + host: &HostQueries<'_>, +) -> Result<()> { + if mounts.len() < 2 { + return Ok(()); + } + let roots = mounts + .iter() + .map(|mount| { + (host.resolve)(&mount.host_path) + .map_err(|error| os_error(&error, Some(mount.host_path.as_str()))) + }) + .collect::>>()?; + // The identity of each root, then of each of its ancestors. + let lineages = roots + .iter() + .map(|root| { + std::iter::once(root.clone()) + .chain(root.parents()) + .map(|path| { + (host.identity)(&path).map_err(|error| os_error(&error, Some(path.as_str()))) + }) + .collect::>>() + }) + .collect::>>()?; + for (index, root) in roots.iter().enumerate() { + for (other_index, other) in roots[..index].iter().enumerate() { + if root == other + || root.parents().any(|parent| parent == *other) + || other.parents().any(|parent| parent == *root) + { + return Err(refused(format!( + "sandbox mount host paths {other} and {root} overlap; a share could reach \ + files of another under its access mode" + ))); + } + if lineages[other_index].contains(&lineages[index][0]) + || lineages[index].contains(&lineages[other_index][0]) + { + return Err(refused(format!( + "sandbox mount host paths {other} and {root} name the same directory or \ + file, or one is inside the other through a bind mount" + ))); + } + } + } + for (mount, root) in mounts.iter().zip(&roots) { + for (kind, paths) in mount.absolute_policy_paths_with(host.cwd)? { + let option = kind.option(); + for path in paths { + let pure = PurePath::new(root.flavor(), &path); + let resolved = + (host.resolve)(&pure).map_err(|error| os_error(&error, Some(pure.as_str())))?; + // A denied root hides the share behind its allowed paths, which OpenVMM requires; + // it accepts no other policy path at a root. + if resolved.parents().any(|parent| parent == *root) + || (kind == PolicyKind::Denied && resolved == *root) + { + continue; + } + return Err(refused(format!( + "{option} {path} is not inside the host directory of --mount {}; with several \ + shares, each {option} follows the --mount whose directory it names", + mount.guest_target + ))); + } + } + } + Ok(()) +} + +/// Returns the name of the aggregate child at `index` that the guest binds at `target`: the +/// index, a hyphen, and the first 32 hexadecimal digits of the SHA-256 digest of the target. +/// +/// A snapshot records every child's name, so it pins each target, and OpenVMM refuses a restore +/// that requests other targets before the guest runs, rather than keep the snapshot's binds. +pub fn aggregate_child_name(index: usize, target: &str) -> String { + let digest = Sha256::digest(target.as_bytes()); + let hex: String = digest[..16] + .iter() + .map(|byte| format!("{byte:02x}")) + .collect(); + format!("{index}-{hex}") +} + +/// Returns the `--mount-child` value that attaches `host_path` as the child `name`. It always +/// gives the mode, which OpenVMM reads after the last comma or before the `file` flag, so the +/// host path may contain commas. A file child carries OpenVMM's `file` flag, so OpenVMM refuses +/// a directory that replaced the file instead of exporting all of it. +pub fn aggregate_child_argument( + name: &str, + host_path: &str, + access: MountAccess, + kind: MountKind, +) -> String { + let file = if kind == MountKind::File { ",file" } else { "" }; + format!("{name},{host_path},{access}{file}") +} + +/// Returns the `--mount` value that attaches `host_path` at `target`. It always gives the mode, +/// which OpenVMM reads after the last comma, so the host path may contain commas. +pub fn mount_argument(target: &str, host_path: &str, access: MountAccess) -> String { + format!("{target},{host_path},{access}") +} + +/// Returns the OpenVMM arguments that attach `mounts`: one directory share directly, and +/// several shares, or a file, as the children of an aggregate, in order. Then come the +/// access-policy paths of each share, absolute for an aggregate, and the owner unless it is +/// `vmm`. +/// +/// Fails as [`SandboxMount::absolute`] does. +pub fn mounts_openvmm_arguments(mounts: &[SandboxMount]) -> Result> { + mounts_openvmm_arguments_with(mounts, &fs::cwd) +} + +/// Implements [`mounts_openvmm_arguments`] with `cwd` as `Path.cwd()`. +pub(crate) fn mounts_openvmm_arguments_with( + mounts: &[SandboxMount], + cwd: &dyn Fn() -> io::Result, +) -> Result> { + let mut arguments = Vec::new(); + let aggregate = mounts_use_aggregate(mounts); + match mounts { + [] => {} + [mount] if !aggregate => arguments.extend([ + "--mount".to_owned(), + mount_argument(&mount.guest_target, mount.host_path.as_str(), mount.access), + ]), + _ => { + arguments.extend([ + "--mount-aggregate".to_owned(), + AGGREGATE_MOUNT_TARGET.to_owned(), + ]); + for (index, mount) in mounts.iter().enumerate() { + let name = aggregate_child_name(index, &mount.guest_target); + arguments.push("--mount-child".to_owned()); + arguments.push(aggregate_child_argument( + &name, + mount.host_path.as_str(), + mount.access, + mount.kind, + )); + } + } + } + for mount in mounts { + let policy = if aggregate { + mount.absolute_policy_paths_with(cwd)? + } else { + POLICY_KINDS + .iter() + .map(|&kind| (kind, mount.policy.paths(kind).to_vec())) + .collect() + }; + for (kind, paths) in policy { + for path in paths { + arguments.push(kind.option().to_owned()); + arguments.push(path); + } + } + } + if let Some(first) = mounts.first() + && first.owner != MountOwner::Vmm + { + arguments.extend(["--mount-owner".to_owned(), first.owner.as_str().to_owned()]); + } + Ok(arguments) +} + +/// Returns the kernel command-line tokens that tell the guest where to bind each child of an +/// aggregate, given each share's guest target, access, and kind. A file child's token ends in +/// `,file`, so the guest binds it at a file. +pub fn aggregate_share_tokens(shares: &[(&str, MountAccess, MountKind)]) -> Vec { + shares + .iter() + .enumerate() + .map(|(index, &(target, access, kind))| { + let file = if kind == MountKind::File { ",file" } else { "" }; + format!( + "nvx_share={},{target},{access}{file}", + aggregate_child_name(index, target) + ) + }) + .collect() +} + +/// Returns the bootstrap tokens that OpenVMM appends for an aggregate of shares with +/// `accesses`, which it mounts read-write if any child is. +pub fn aggregate_command_line_fragment(accesses: &[MountAccess]) -> String { + let access = if accesses.contains(&MountAccess::ReadWrite) { + MountAccess::ReadWrite + } else { + MountAccess::ReadOnly + }; + command_line_fragment(AGGREGATE_MOUNT_TARGET, access) + AGGREGATE_COMMAND_LINE_SUFFIX +} + +/// Returns the bootstrap tokens that OpenVMM appends for `mounts`. +pub fn mounts_command_line_fragment(mounts: &[SandboxMount]) -> String { + match mounts { + [] => String::new(), + [mount] if !mounts_use_aggregate(mounts) => mount.command_line_fragment(), + _ => aggregate_command_line_fragment( + &mounts.iter().map(|mount| mount.access).collect::>(), + ), + } +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/sandbox/mount/tests.rs b/nvx-rs/src/host/sandbox/mount/tests.rs new file mode 100644 index 000000000..2338bf93e --- /dev/null +++ b/nvx-rs/src/host/sandbox/mount/tests.rs @@ -0,0 +1,520 @@ +//! The share tests of `scripts/test_nvx_tools.py`, ported. + +use std::fs; + +use super::*; +use crate::ErrorKind; +use crate::host::python::fs::cwd; + +fn policy(denied: &[&str], allowed: &[&str], writable: &[&str]) -> MountPolicy { + let owned = |paths: &[&str]| paths.iter().map(|&path| path.to_owned()).collect(); + MountPolicy { + denied: owned(denied), + allowed: owned(allowed), + writable: owned(writable), + } +} + +fn parse(value: &str, policy: MountPolicy) -> Result { + SandboxMount::parse(value, policy, MountOwner::Vmm, MountKind::Directory) +} + +fn message(result: Result) -> String { + let error = result.unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + error.to_string() +} + +fn native(path: &str) -> String { + PurePath::native(path).as_str().to_owned() +} + +#[test] +fn mount_parser_defaults_to_read_only_and_accepts_rw() { + let default = parse("/workspace,host-dir", MountPolicy::default()).unwrap(); + assert_eq!(default.guest_target(), "/workspace"); + assert_eq!(default.host_path(), Path::new("host-dir")); + assert_eq!(default.access(), MountAccess::ReadOnly); + assert!(default.policy().denied.is_empty()); + + let writable = parse( + "/opt/hostedtoolcache,host-dir,rw", + policy(&["logs", "secrets"], &[], &[]), + ) + .unwrap(); + assert_eq!(writable.access(), MountAccess::ReadWrite); + assert_eq!(writable.policy().denied, ["logs", "secrets"]); + assert_eq!( + mounts_openvmm_arguments(std::slice::from_ref(&writable)).unwrap(), + [ + "--mount", + "/opt/hostedtoolcache,host-dir,rw", + "--mount-deny", + "logs", + "--mount-deny", + "secrets", + ] + ); + assert_eq!( + writable.command_line_fragment(), + " virtfs_dir=/opt/hostedtoolcache virtfs_tag=microvm virtfs_mode=rw" + ); +} + +#[test] +fn mount_parser_rejects_malformed_specifications() { + let modes = "choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"; + for (value, expected) in [ + ( + "/workspace", + "--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]".to_owned(), + ), + ("/workspace,", "--mount host path is empty".to_owned()), + ("/workspace,,rw", "--mount host path is empty".to_owned()), + ( + "/workspace,host,rx", + format!("unsupported --mount mode 'rx'; {modes}"), + ), + // With more than one comma, the value must end in the mode, which the error suggests for + // a host path that contains commas. + ( + "/workspace,host,rw,extra", + format!("unsupported --mount mode 'extra'; {modes}"), + ), + ( + "/workspace,alpha,beta", + format!("unsupported --mount mode 'beta'; {modes}"), + ), + ] { + assert_eq!(message(parse(value, MountPolicy::default())), expected); + } + assert_eq!( + message(SandboxMount::new( + "/workspace", + "a\0b", + MountAccess::ReadOnly, + MountPolicy::default(), + MountOwner::Vmm, + MountKind::Directory, + )), + "sandbox mount host path contains a NUL character: 'a\\x00b'" + ); + assert_eq!( + message(parse("/workspace,link/../share", MountPolicy::default())), + format!( + "sandbox mount host path contains a parent component: {}", + native("link/../share") + ) + ); +} + +#[test] +fn mount_parser_accepts_host_paths_with_commas() { + let alpha = r"C:\projects\alpha,beta"; + let reports = r"C:\reports\2026, final"; + for (value, target, host_path, access) in [ + ( + format!("/work,{alpha},rw"), + "/work", + alpha, + MountAccess::ReadWrite, + ), + ( + format!("/reports,{reports},ro"), + "/reports", + reports, + MountAccess::ReadOnly, + ), + // A host path that ends in a comma, or in what looks like a mode, keeps it before the + // mode. + ( + "/data,/srv/data,rw,ro".to_owned(), + "/data", + "/srv/data,rw", + MountAccess::ReadOnly, + ), + ( + "/data,/srv/data,,rw".to_owned(), + "/data", + "/srv/data,", + MountAccess::ReadWrite, + ), + // Values without commas in the host path keep their meaning. + ( + "/data,/srv/data,rw".to_owned(), + "/data", + "/srv/data", + MountAccess::ReadWrite, + ), + ( + "/data,/srv/data".to_owned(), + "/data", + "/srv/data", + MountAccess::ReadOnly, + ), + ] { + assert_eq!( + parse_mount_value(&value).unwrap(), + (target.to_owned(), host_path.to_owned(), access) + ); + let mount = parse(&value, policy(&["secret"], &[], &[])).unwrap(); + assert_eq!(mount.host_path(), Path::new(&native(host_path))); + assert_eq!(mount.access(), access); + // OpenVMM finds the end of the host path before the mode, which NVX therefore always + // gives. + assert_eq!( + mounts_openvmm_arguments(&[mount]).unwrap(), + [ + "--mount".to_owned(), + format!("{target},{},{access}", native(host_path)), + "--mount-deny".to_owned(), + "secret".to_owned(), + ] + ); + } + let alpha_mount = parse(&format!("/work,{alpha},rw"), MountPolicy::default()).unwrap(); + let reports_mount = parse(&format!("/reports,{reports},ro"), MountPolicy::default()).unwrap(); + let arguments = mounts_openvmm_arguments(&[alpha_mount, reports_mount]).unwrap(); + let children: Vec<&String> = arguments + .iter() + .zip(arguments.iter().skip(1)) + .filter(|(option, _)| *option == "--mount-child") + .map(|(_, value)| value) + .collect(); + assert_eq!( + children, + [ + &format!("{},{},rw", aggregate_child_name(0, "/work"), native(alpha)), + &format!( + "{},{},ro", + aggregate_child_name(1, "/reports"), + native(reports) + ), + ] + ); +} + +#[test] +fn mount_owner_defaults_to_vmm_and_forwards_caller() { + let default = parse("/workspace,host-dir,rw", MountPolicy::default()).unwrap(); + assert_eq!(default.owner(), MountOwner::Vmm); + assert!( + !mounts_openvmm_arguments(std::slice::from_ref(&default)) + .unwrap() + .contains(&"--mount-owner".to_owned()) + ); + let caller = SandboxMount::parse( + "/workspace,host-dir,rw", + policy(&["secrets"], &[], &[]), + MountOwner::Caller, + MountKind::Directory, + ) + .unwrap(); + assert_eq!( + mounts_openvmm_arguments(std::slice::from_ref(&caller)).unwrap(), + [ + "--mount", + "/workspace,host-dir,rw", + "--mount-deny", + "secrets", + "--mount-owner", + "caller", + ] + ); + assert_eq!(caller.absolute().unwrap().owner(), MountOwner::Caller); + assert_eq!( + caller.command_line_fragment(), + default.command_line_fragment() + ); + assert_eq!( + message(MountOwner::from_name("root")), + "unsupported sandbox mount owner 'root'; choose vmm or caller" + ); + assert_eq!( + message(MountAccess::from_name("rx")), + "unsupported sandbox mount mode 'rx'; choose ro or rw" + ); +} + +#[test] +fn mount_access_policy_is_validated_and_forwarded() { + let mount = parse( + "/tmp/mcp-runtime,host-dir,rw", + policy(&["mcp-logs"], &["mcp-logs/payloads"], &["agent", "cache"]), + ) + .unwrap(); + assert_eq!( + mounts_openvmm_arguments(std::slice::from_ref(&mount)).unwrap(), + [ + "--mount", + "/tmp/mcp-runtime,host-dir,rw", + "--mount-deny", + "mcp-logs", + "--mount-allow", + "mcp-logs/payloads", + "--mount-write", + "agent", + "--mount-write", + "cache", + ] + ); + let absolute = mount.absolute().unwrap(); + assert_eq!(absolute.policy().allowed, ["mcp-logs/payloads"]); + assert_eq!(absolute.policy().writable, ["agent", "cache"]); + // OpenVMM enforces the policy on the host, so the guest's bootstrap tokens are those of the + // plain share. + assert_eq!( + mount.command_line_fragment(), + " virtfs_dir=/tmp/mcp-runtime virtfs_tag=microvm virtfs_mode=rw" + ); + let paths: Vec = (0..129).map(|index| format!("path-{index}")).collect(); + let many: Vec<&str> = paths.iter().map(String::as_str).collect(); + for (value, policy, expected) in [ + ( + "/w,host,ro", + policy(&[], &[], &["out"]), + "--mount-write narrows the writes of a read-write share; --mount /w is read-only", + ), + ( + "/w,host,rw", + policy(&[], &["logs/payloads"], &[]), + "--mount-allow exposes a path inside a --mount-deny path of the same share, which \ + has none", + ), + ( + "/w,host,rw", + policy(&["logs"], &["logs/a", "logs/a"], &[]), + "sandbox mount allowed paths must be unique", + ), + ( + "/w,host,rw", + policy(&[], &[], &[""]), + "sandbox mount writable paths must be nonempty", + ), + ( + "/w,host,rw", + policy(&[], &[], &["a\0"]), + "sandbox mount writable paths must be nonempty", + ), + ( + "/w,host,rw", + policy(&[], &[], &many), + "a sandbox mount permits at most 128 writable paths", + ), + ] { + assert_eq!(message(parse(value, policy)), expected); + } +} + +#[test] +fn caller_owner_requires_linux_and_a_directory_that_root_does_not_own() { + assert_eq!( + message(require_mount_owner_supported_on(MountOwner::Caller, true)), + "--mount-owner caller requires a Linux host; Windows has no per-request POSIX identity \ + for OpenVMM to switch to" + ); + require_mount_owner_supported_on(MountOwner::Vmm, true).unwrap(); + require_mount_owner_supported_on(MountOwner::Caller, false).unwrap(); + let directory = tempfile::tempdir().unwrap(); + let temporary = directory.path().to_str().unwrap(); + let caller = |path: &str| { + SandboxMount::new( + "/workspace", + path, + MountAccess::ReadOnly, + MountPolicy::default(), + MountOwner::Caller, + MountKind::Directory, + ) + .unwrap() + }; + if cfg!(windows) { + assert!(message(caller(temporary).validated()).contains("requires a Linux host")); + return; + } + let owner = crate::host::python::fs::stat(directory.path()).unwrap(); + if owner.uid != 0 && owner.gid != 0 { + caller(temporary).validated().unwrap(); + } + if crate::host::python::fs::stat(Path::new("/")).unwrap().uid == 0 { + assert_eq!( + message(caller("/").validated()), + "--mount-owner caller squashes guest root to the owner of the shared directory, so / \ + must not be owned by UID 0 or GID 0" + ); + SandboxMount::new( + "/workspace", + "/", + MountAccess::ReadOnly, + MountPolicy::default(), + MountOwner::Vmm, + MountKind::Directory, + ) + .unwrap() + .validated() + .unwrap(); + } +} + +#[test] +fn mount_absolute_path_keeps_symbolic_link_components() { + let mount = parse("/workspace,link/share,rw", MountPolicy::default()).unwrap(); + let absolute = mount.absolute().unwrap(); + let expected = cwd().unwrap().join("link").join("share"); + assert_eq!(absolute.host_path(), expected.as_path()); + assert!(absolute.host_path.parts().iter().any(|part| part == "link")); + assert_eq!(absolute.absolute().unwrap(), absolute); + let fixed = PurePath::new(Flavor::Posix, "/fixture/cwd"); + let relative = SandboxMount::parse_in( + Flavor::Posix, + "/workspace,link/share,rw", + MountPolicy::default(), + MountOwner::Vmm, + MountKind::Directory, + ) + .unwrap(); + let joined = relative.absolute_with(&|| Ok(fixed.clone())).unwrap(); + assert_eq!(joined.host_path.as_str(), "/fixture/cwd/link/share"); + for (policy, expected) in [ + ( + policy(&["logs", "logs"], &[], &[]), + "sandbox mount denied paths must be unique", + ), + ( + policy(&[""], &[], &[]), + "sandbox mount denied paths must be nonempty", + ), + ] { + assert_eq!(message(parse("/workspace,host", policy)), expected); + } + let paths: Vec = (0..129).map(|index| format!("path-{index}")).collect(); + let many: Vec<&str> = paths.iter().map(String::as_str).collect(); + assert_eq!( + message(parse("/workspace,host", policy(&many, &[], &[]))), + "a sandbox mount permits at most 128 denied paths" + ); +} + +#[test] +fn mount_absolute_path_keeps_a_link_on_the_host() { + let directory = tempfile::tempdir().unwrap(); + let root = PurePath::native(directory.path().to_str().unwrap()); + fs::create_dir_all(root.join("link-target/share").as_path()).unwrap(); + #[cfg(unix)] + let linked = std::os::unix::fs::symlink( + root.join("link-target").as_path(), + root.join("link").as_path(), + ); + #[cfg(windows)] + let linked = std::os::windows::fs::symlink_dir( + root.join("link-target").as_path(), + root.join("link").as_path(), + ); + if linked.is_err() { + // Symbolic links need a privilege that the host may not grant. + return; + } + let mount = parse( + &format!("/workspace,{},rw", root.join("link/share")), + MountPolicy::default(), + ) + .unwrap(); + let absolute = mount.absolute().unwrap(); + assert!( + !absolute + .host_path + .parts() + .iter() + .any(|part| part == "link-target") + ); +} + +#[test] +fn mount_target_validation_rejects_unsafe_and_reserved_targets() { + let long = format!("/{}", "x".repeat(4096)); + for target in [ + "", + "/", + "workspace", + "/workspace/", + "/work//space", + "/work/./space", + "/work/../space", + "/work space", + "/work=space", + "/work\\space", + "/work,space", + "/work\0space", + "/work\u{85}space", + "/work\x1cspace", + &long, + ] { + assert_eq!( + message(validate_mount_target(target)), + format!( + "invalid sandbox mount target {}: expected an absolute non-root path without \ + empty, dot, or parent components, whitespace, '\\', '=', or ','", + repr(target) + ) + ); + } + for (target, reserved) in [ + ("/proc", "/proc"), + ("/proc/self", "/proc"), + ("/sys", "/sys"), + ("/sys/fs", "/sys"), + ("/dev", "/dev"), + ("/dev/shm", "/dev"), + ("/.nvx-agent", "/.nvx-agent"), + ("/.nvx-agent/tools", "/.nvx-agent"), + ] { + assert_eq!( + message(validate_mount_target(target)), + format!("sandbox mount target {target} overlaps the reserved {reserved} tree") + ); + } + assert_eq!( + message(validate_mount_target("/etc")), + "sandbox mount target /etc is reserved" + ); + for target in ["/workspace", "/procfs", "/etc/app", "/opt/hostedtoolcache"] { + validate_mount_target(target).unwrap(); + } + // The longest target fits. + validate_mount_target(&format!("/{}", "x".repeat(4095))).unwrap(); +} + +#[test] +fn aggregate_child_names_pin_their_targets() { + let name = aggregate_child_name(0, "/workspace"); + assert_eq!(name, "0-c52ddf65534b7b46035084358ab7902b"); + // Equal targets give equal names, so a restore names its children as the boot did, and + // another target or position gives another name. + assert_eq!(aggregate_child_name(0, "/workspace"), name); + assert_ne!(aggregate_child_name(0, "/workspace2"), name); + assert_ne!(aggregate_child_name(1, "/workspace"), name); + // Every name follows OpenVMM's rule for the name of a child. + for (index, target) in [ + (0, "/w".to_owned()), + (999, format!("/{}", "x".repeat(1000))), + ] { + let name = aggregate_child_name(index, &target); + let (number, digest) = name.split_once('-').unwrap(); + assert!(number.chars().all(|c| c.is_ascii_digit())); + assert_eq!(digest.len(), 32); + assert!(digest.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f'))); + } + assert!(aggregate_child_name(999, "/w").len() <= 64); + assert_eq!( + aggregate_child_argument("n", "/h,x", MountAccess::ReadWrite, MountKind::File), + "n,/h,x,rw,file" + ); + assert_eq!( + mount_argument("/t", "/h", MountAccess::ReadOnly), + "/t,/h,ro" + ); + assert_eq!( + aggregate_command_line_fragment(&[MountAccess::ReadOnly, MountAccess::ReadOnly]), + " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=ro virtfs_aggregate=1" + ); +} diff --git a/nvx-rs/src/host/sandbox/options.rs b/nvx-rs/src/host/sandbox/options.rs new file mode 100644 index 000000000..dfdb1afb3 --- /dev/null +++ b/nvx-rs/src/host/sandbox/options.rs @@ -0,0 +1,585 @@ +//! The checks that `nvx.py sandbox` and `nvx.py run` apply to their options, ported from +//! `scripts/nvx.py` with their order and messages: +//! +//! - [`SandboxOptions::check`]: the checks of `command_sandbox` before it builds the launch: the +//! operations that each option is valid for, the options that need others, and the managed exec +//! environment, from `--environment` or `--environment-file`. +//! - [`sandbox_mounts`]: the shares of the `--mount` options, with each `--mount-deny`, +//! `--mount-allow`, and `--mount-write` attributed to its share, and each share's kind. +//! - [`validate_sandbox_systemd_policy`]: the refusal of systemd entrypoints and images. +//! - [`validate_run_share_targets`]: the guest targets of several `nvx.py run` shares, or of a +//! shared file. +//! - [`validate_exec_environment`] and [`exec_environment_from_file`]: the environment of a +//! managed exec. + +use std::fmt; +use std::fs::File; +use std::io::Read; + +use sha2::{Digest, Sha256}; + +use super::layer::LayerRole; +use super::mount::{ + AGGREGATE_MOUNT_TARGET, MountKind, MountOwner, MountPolicy, POLICY_KINDS, PolicyKind, + SandboxMount, has_forbidden_character, has_irregular_component, mount_kind, os_error, +}; +use super::{SandboxLaunch, refused}; +use crate::error::Result; +use crate::host::artifacts::DISTRO_MANIFEST_SUFFIX; +use crate::host::python::builtins::repr; +use crate::host::python::fs; +use crate::host::python::json::{self, Loads, Node, ReadError}; +use crate::host::python::path::{Flavor, PurePath}; +use crate::host::python::text::{PyText, decode_utf8}; + +/// The entrypoints of systemd, which the sandbox security profile does not run. +pub const SYSTEMD_ENTRYPOINTS: [&str; 2] = ["/usr/lib/systemd/systemd", "/lib/systemd/systemd"]; +/// The largest environment file of a managed exec, in bytes. +pub const MAX_ENVIRONMENT_FILE_BYTES: usize = 1024 * 1024; +/// The most entries of a managed exec environment. +pub const MAX_EXEC_ENVIRONMENT: usize = 256; +/// The longest entry of a managed exec environment, in UTF-8 bytes. +pub const MAX_EXEC_ENVIRONMENT_BYTES: usize = 4096; + +/// An operation of `nvx.py sandbox`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum SandboxOperation { + /// `run`: a one-shot sandbox, the default. + Run, + /// `provision`: record a managed sandbox's launch in its state directory. + Provision, + /// `start`: start a provisioned sandbox. + Start, + /// `exec`: run a workload in a started sandbox. + Exec, + /// `stop`: stop a started sandbox. + Stop, + /// `deprovision`: remove a sandbox's state directory. + Deprovision, +} + +impl SandboxOperation { + /// Returns the operation's name. + pub const fn as_str(self) -> &'static str { + match self { + Self::Run => "run", + Self::Provision => "provision", + Self::Start => "start", + Self::Exec => "exec", + Self::Stop => "stop", + Self::Deprovision => "deprovision", + } + } + + /// Returns whether the operation launches a sandbox: `run` or `provision`. + fn launches(self) -> bool { + matches!(self, Self::Run | Self::Provision) + } +} + +impl fmt::Display for SandboxOperation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// A `--mount-deny`, `--mount-allow`, or `--mount-write` option, with the position among the +/// `--mount` options of the one before it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyPathOption { + /// The kind of path, which the option gives. + pub kind: PolicyKind, + /// The index of the last `--mount` option before this one, if any. + pub preceding_mount: Option, + /// The host path, as given. + pub path: String, +} + +/// The options of `nvx.py sandbox` that [`SandboxOptions::check`] examines, as given. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SandboxOptions { + /// The operation. + pub operation: SandboxOperation, + /// `--dry-run`. + pub dry_run: bool, + /// `--cwd`. + pub cwd: Option, + /// The `--environment` options. + pub environment: Vec, + /// `--environment-file`. + pub environment_file: Option, + /// `--inherit-default-environment`. + pub inherit_default_environment: bool, + /// Whether any network policy option was given: `--net`, `--network-profile`, + /// `--network-egress`, `--network-ingress`, `--network-egress-policy-file`, + /// `--host-loopback`, `--network-proxy`, `--network-egress-allow`, + /// `--network-egress-deny`, or `--host-loopback-forward`. + pub network_options: bool, + /// Whether `--net` was given. + pub net: bool, + /// Whether `--network-profile` was given. + pub network_profile: bool, + /// `--entrypoint`. + pub entrypoint: String, + /// Whether `--outcome-report` was given. + pub outcome_report: bool, + /// The `--mount` options. + pub mounts: Vec, + /// The `--mount-deny`, `--mount-allow`, and `--mount-write` options, in order. + pub policy_paths: Vec, + /// `--mount-owner`. + pub mount_owner: Option, + /// Whether any `--layer` was given. + pub layers: bool, + /// Whether `--scratch` was given. + pub scratch: bool, +} + +impl SandboxOptions { + /// Returns the options of an operation with the defaults of `nvx.py sandbox`. + pub fn new(operation: SandboxOperation) -> Self { + Self { + operation, + dry_run: false, + cwd: None, + environment: Vec::new(), + environment_file: None, + inherit_default_environment: false, + network_options: false, + net: false, + network_profile: false, + entrypoint: super::DEFAULT_ENTRYPOINT.to_owned(), + outcome_report: false, + mounts: Vec::new(), + policy_paths: Vec::new(), + mount_owner: None, + layers: false, + scratch: false, + } + } + + /// Applies the checks of `command_sandbox` that precede the launch, in its order, and + /// returns the managed exec environment of an `exec`, if one was given. + /// + /// It refuses `--dry-run` except for `run`, both `--environment` and `--environment-file`, + /// the managed exec options except for `exec`, an `exec` environment that + /// [`exec_environment_from_file`] or [`validate_exec_environment`] refuses, network policy + /// options except for `run` and `provision`, a systemd entrypoint for `run`, `provision`, + /// and `exec`, `--outcome-report` except for `run` and `exec`, share options without a + /// `--mount`, `--mount` except for `run` and `provision`, and for `run` and `provision`, + /// `--net` without `--network-profile` or the reverse, and a missing `--layer` or + /// `--scratch`. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). + pub fn check(&self) -> Result>> { + let operation = self.operation; + if self.dry_run && operation != SandboxOperation::Run { + return Err(refused("--dry-run is only valid for sandbox run")); + } + if !self.environment.is_empty() && self.environment_file.is_some() { + return Err(refused( + "--environment and --environment-file are mutually exclusive", + )); + } + if operation != SandboxOperation::Exec + && (self.cwd.is_some() + || !self.environment.is_empty() + || self.environment_file.is_some() + || self.inherit_default_environment) + { + return Err(refused( + "managed execution options require the sandbox exec operation", + )); + } + let mut environment = None; + if operation == SandboxOperation::Exec { + if let Some(file) = &self.environment_file { + environment = Some(exec_environment_from_file(file)?); + } else if !self.environment.is_empty() { + validate_exec_environment(&self.environment)?; + environment = Some(self.environment.clone()); + } + } + if !operation.launches() && self.network_options { + return Err(refused( + "network policy options are only valid for sandbox run or provision", + )); + } + if matches!( + operation, + SandboxOperation::Run | SandboxOperation::Provision | SandboxOperation::Exec + ) && SYSTEMD_ENTRYPOINTS.contains(&self.entrypoint.as_str()) + { + return Err(refused( + "systemd entrypoints are unsupported by the sandbox security profile", + )); + } + if self.outcome_report + && !matches!(operation, SandboxOperation::Run | SandboxOperation::Exec) + { + return Err(refused( + "--outcome-report is only valid for one-shot run or managed exec", + )); + } + for kind in POLICY_KINDS { + if self.mounts.is_empty() && self.policy_paths.iter().any(|path| path.kind == kind) { + return Err(refused(format!("{} requires --mount", kind.option()))); + } + } + if self.mount_owner.is_some() && self.mounts.is_empty() { + return Err(refused("--mount-owner requires --mount")); + } + if !self.mounts.is_empty() && !operation.launches() { + return Err(refused( + "--mount is only valid for sandbox run or provision", + )); + } + if operation.launches() { + if self.net != self.network_profile { + return Err(refused( + "--net and --network-profile must be specified together", + )); + } + if !self.layers || !self.scratch { + return Err(refused(format!( + "sandbox {operation} requires --layer and --scratch" + ))); + } + } + Ok(environment) + } +} + +/// Returns the shares of the `--mount` values in `mounts`, each with its access-policy paths, the +/// `owner` (`vmm` by default), and the kind that [`mount_kind`] finds for its host path. +/// +/// With one `--mount`, every policy path names a path in it. With several, each names a path in +/// the `--mount` before it. Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) +/// for a policy path without a `--mount` before it among several, and as +/// [`SandboxMount::parse`] and [`SandboxMount::with_kind`] do. +pub fn sandbox_mounts( + mounts: &[String], + policy_paths: &[PolicyPathOption], + owner: Option, +) -> Result> { + sandbox_mounts_with(Flavor::NATIVE, mounts, policy_paths, owner, &|mount| { + mount_kind(mount.host_path()) + }) +} + +/// Implements [`sandbox_mounts`] for host paths of `flavor`, with `kind_of` as [`mount_kind`]. +pub(crate) fn sandbox_mounts_with( + flavor: Flavor, + mounts: &[String], + policy_paths: &[PolicyPathOption], + owner: Option, + kind_of: &dyn Fn(&SandboxMount) -> MountKind, +) -> Result> { + let mut policies = vec![MountPolicy::default(); mounts.len()]; + for kind in POLICY_KINDS { + for option in policy_paths.iter().filter(|option| option.kind == kind) { + let index = match (mounts.len(), option.preceding_mount) { + (1, _) => 0, + (_, Some(index)) => index, + (_, None) => { + return Err(refused(format!( + "with several --mount options, each {} must follow the --mount whose \ + host directory it names", + kind.option() + ))); + } + }; + let policy = policies.get_mut(index).ok_or_else(|| { + refused(format!( + "{} follows --mount number {}, which is missing", + kind.option(), + index + 1 + )) + })?; + match kind { + PolicyKind::Denied => &mut policy.denied, + PolicyKind::Allowed => &mut policy.allowed, + PolicyKind::Writable => &mut policy.writable, + } + .push(option.path.clone()); + } + } + let owner = owner.unwrap_or(MountOwner::Vmm); + mounts + .iter() + .zip(policies) + .map(|(value, policy)| { + let mount = SandboxMount::parse_in(flavor, value, policy, owner, MountKind::Directory)?; + mount.with_kind(kind_of(&mount)) + }) + .collect() +} + +/// Refuses the guest targets of `nvx.py run` shares that the guest cannot bind from an +/// aggregate: targets that would not survive the kernel command line, or that hide each other or +/// the aggregate itself. `nvx.py run` checks them when it attaches several shares, or a file. +/// +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). +pub fn validate_run_share_targets(targets: &[&str]) -> Result<()> { + for (index, target) in targets.iter().enumerate() { + if !target.starts_with('/') + || *target == "/" + || has_forbidden_character(target) + || has_irregular_component(target) + { + return Err(refused(format!( + "invalid --mount target {}: with several shares, or a shared file, each target \ + must be an absolute non-root path without empty, dot, or parent components, \ + whitespace, '\\', '=', or ','", + repr(target) + ))); + } + for other in std::iter::once(&AGGREGATE_MOUNT_TARGET).chain(&targets[..index]) { + let inside = |path: &str, directory: &str| { + path.strip_prefix(directory) + .is_some_and(|rest| rest.starts_with('/')) + }; + if target == other || inside(target, other) || inside(other, target) { + return Err(refused(format!( + "--mount targets {other} and {target} overlap; a share cannot hide another" + ))); + } + } + } + Ok(()) +} + +/// Refuses a launch that would run systemd: a systemd entrypoint, or a distro layer whose +/// manifest, `LAYER.manifest.json` next to it, lists the `systemd` package. +/// +/// A layer without a manifest passes. A manifest must be a JSON object of format 1 that names +/// the layer and its SHA-256 digest, with a list of packages that each have a name. Fails with +/// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), or +/// [`ErrorKind::Io`](crate::ErrorKind::Io) if the layer cannot be read to compute its digest. +pub fn validate_sandbox_systemd_policy(launch: &SandboxLaunch) -> Result<()> { + if SYSTEMD_ENTRYPOINTS.contains(&launch.settings().entrypoint.as_str()) { + return Err(refused( + "systemd entrypoints are unsupported by the sandbox security profile", + )); + } + let Some(distro) = launch + .layers() + .iter() + .find(|layer| layer.role() == LayerRole::Distro) + else { + return Ok(()); + }; + let path = distro.pure_path(); + let Some(manifest) = path.with_name(&format!("{}{DISTRO_MANIFEST_SUFFIX}", path.name())) else { + // `with_name` raises `ValueError` for a path without a name. + let class = match path.flavor() { + Flavor::Posix => "PosixPath", + Flavor::Windows => "WindowsPath", + }; + return Err(refused(format!( + "{class}({}) has an empty name", + repr(&path.as_str().replace('\\', "/")) + ))); + }; + if !fs::exists(manifest.as_path()) { + return Ok(()); + } + let invalid = |detail: String| refused(format!("invalid sandbox distro manifest: {detail}")); + let bytes = std::fs::read(manifest.as_path()) + .map_err(|error| invalid(fs::os_error_message(&error, Some(manifest.as_str()))))?; + // `read_text` decodes strictly, outside the errors that become this message, and reads with + // universal newlines. + let text = decode_utf8(&bytes) + .map_err(refused)? + .replace("\r\n", "\n") + .replace('\r', "\n"); + let document = match json::read(&text, &mut Loads) { + Ok(document) => document, + Err(ReadError::Syntax { message, offset }) => { + return Err(invalid(json::syntax_error(&text, message, offset))); + } + Err(ReadError::Hook(error)) => return Err(refused(error.message(&text))), + }; + let root = document.root(); + if !matches!(document.node(root), Node::Object(_)) { + return Err(invalid("expected a JSON object".to_owned())); + } + let mismatch = || refused("sandbox distro manifest does not match its artifact"); + if !document.get(root, "format").is_some_and(equals_one) { + return Err(mismatch()); + } + if !document + .get(root, "artifact") + .is_some_and(|artifact| string_equals(artifact, path.name())) + { + return Err(mismatch()); + } + let digest = sha256_file(path)?; + if !document + .get(root, "artifact_sha256") + .is_some_and(|value| string_equals(value, &digest)) + { + return Err(mismatch()); + } + let invalid_packages = || refused("sandbox distro manifest has invalid package metadata"); + let Some(Node::Array(packages)) = document.get(root, "packages") else { + return Err(invalid_packages()); + }; + let mut systemd = false; + for &package in packages { + if !matches!(document.node(package), Node::Object(_)) { + return Err(invalid_packages()); + } + let Some(Node::String(name)) = document.get(package, "name") else { + return Err(invalid_packages()); + }; + systemd |= name.as_bytes() == b"systemd"; + } + if systemd { + return Err(refused( + "systemd images are unsupported by the sandbox security profile", + )); + } + Ok(()) +} + +/// Returns whether a JSON value equals Python's `1`: the integer, a float of that value, or +/// `true`. +fn equals_one(value: &Node) -> bool { + match value { + Node::Integer(literal) => literal.as_ref() == "1", + Node::Float(float) => *float == 1.0, + Node::Bool(boolean) => *boolean, + _ => false, + } +} + +/// Returns whether a JSON value is the string `text`. +fn string_equals(value: &Node, text: &str) -> bool { + matches!(value, Node::String(string) if string.as_bytes() == text.as_bytes()) +} + +/// Returns the lowercase hexadecimal SHA-256 digest of the file at `path`. +fn sha256_file(path: &PurePath) -> Result { + let io_error = |error: std::io::Error| os_error(&error, Some(path.as_str())); + let mut file = File::open(path.as_path()).map_err(io_error)?; + let mut hasher = Sha256::new(); + let mut buffer = vec![0; 1024 * 1024]; + loop { + let read = file.read(&mut buffer).map_err(io_error)?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(hasher + .finalize() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect()) +} + +/// Checks a managed exec environment, as `encode_exec_environment` does: at most +/// [`MAX_EXEC_ENVIRONMENT`] entries, each a `KEY=VALUE` string with a nonempty key, without NUL, +/// of at most [`MAX_EXEC_ENVIRONMENT_BYTES`] bytes, and no key twice. +/// +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). +pub fn validate_exec_environment(entries: &[String]) -> Result<()> { + let entries: Vec = entries + .iter() + .map(|entry| PyText::from(entry.as_str())) + .collect(); + validate_entries(&entries) +} + +/// Implements [`validate_exec_environment`] for strings that may hold surrogates. +fn validate_entries(entries: &[PyText]) -> Result<()> { + if entries.len() > MAX_EXEC_ENVIRONMENT { + return Err(refused(format!( + "managed exec environment exceeds {MAX_EXEC_ENVIRONMENT} entries" + ))); + } + let mut names = std::collections::HashSet::new(); + for entry in entries { + // Python encodes the entry, which fails at its first run of surrogates, before it + // checks it. + let entry = entry.encode_utf8().map_err(refused)?; + let (name, separator) = match entry.split_once('=') { + Some((name, _)) => (name, true), + None => (entry, false), + }; + if !separator + || name.is_empty() + || entry.contains('\0') + || entry.len() > MAX_EXEC_ENVIRONMENT_BYTES + { + return Err(refused(format!( + "managed exec environment entries must be non-empty KEY=VALUE strings of at most \ + {MAX_EXEC_ENVIRONMENT_BYTES} bytes" + ))); + } + if !names.insert(name.to_owned()) { + return Err(refused(format!( + "managed exec environment contains duplicate key: {name}" + ))); + } + } + Ok(()) +} + +/// Reads the managed exec environment of `--environment-file`: a JSON array of `KEY=VALUE` +/// strings of at most [`MAX_ENVIRONMENT_FILE_BYTES`] bytes, which must then pass +/// [`validate_exec_environment`]. +/// +/// The file is read as `json.loads` reads it, so Python's extensions to JSON are accepted, and +/// `\u` escapes of unpaired surrogates fail as Python fails to encode them. Fails with +/// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), naming the file as `pathlib` +/// normalizes it when it cannot be read or decoded. +pub fn exec_environment_from_file(path: &str) -> Result> { + let path = PurePath::native(path); + let unreadable = || { + refused(format!( + "failed to read managed execution environment: {path}" + )) + }; + let mut data = Vec::new(); + File::open(path.as_path()) + .and_then(|file| { + file.take(MAX_ENVIRONMENT_FILE_BYTES as u64 + 1) + .read_to_end(&mut data) + }) + .map_err(|_| unreadable())?; + if data.len() > MAX_ENVIRONMENT_FILE_BYTES { + return Err(refused(format!( + "managed execution environment file exceeds {MAX_ENVIRONMENT_FILE_BYTES}-byte limit" + ))); + } + let text = std::str::from_utf8(&data).map_err(|_| unreadable())?; + let document = match json::read(text, &mut Loads) { + Ok(document) => document, + Err(ReadError::Syntax { .. }) => return Err(unreadable()), + // `int` raises a `ValueError` that is not a decoding error. + Err(ReadError::Hook(error)) => return Err(refused(error.message(text))), + }; + let not_strings = || { + refused("managed execution environment file must contain a JSON array of KEY=VALUE strings") + }; + let Node::Array(values) = document.node(document.root()) else { + return Err(not_strings()); + }; + let entries = values + .iter() + .map(|&index| match document.node(index) { + Node::String(string) => Ok(string.clone()), + _ => Err(not_strings()), + }) + .collect::>>()?; + validate_entries(&entries)?; + Ok(entries + .iter() + .map(|entry| entry.as_str().unwrap_or_default().to_owned()) + .collect()) +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/sandbox/options/tests.rs b/nvx-rs/src/host/sandbox/options/tests.rs new file mode 100644 index 000000000..47fa37b90 --- /dev/null +++ b/nvx-rs/src/host/sandbox/options/tests.rs @@ -0,0 +1,974 @@ +//! The `nvx.py sandbox` and `nvx.py run` option tests of `scripts/test_nvx_tools.py`, ported. + +use std::fs; +use std::path::Path; + +use super::*; +use crate::ErrorKind; +use crate::host::python::fs::resolve; +use crate::host::sandbox::mount::{aggregate_child_name, mounts_openvmm_arguments}; +use crate::host::sandbox::{SandboxLayer, SandboxSettings}; + +fn message(result: Result) -> String { + let error = result.unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + error.to_string() +} + +fn launching(operation: SandboxOperation) -> SandboxOptions { + SandboxOptions { + layers: true, + scratch: true, + ..SandboxOptions::new(operation) + } +} + +#[test] +fn sandbox_rejects_systemd_entrypoint() { + for entrypoint in SYSTEMD_ENTRYPOINTS { + for operation in [ + SandboxOperation::Run, + SandboxOperation::Provision, + SandboxOperation::Exec, + ] { + let options = SandboxOptions { + entrypoint: entrypoint.to_owned(), + ..launching(operation) + }; + assert_eq!( + message(options.check()), + "systemd entrypoints are unsupported by the sandbox security profile" + ); + } + let stop = SandboxOptions { + entrypoint: entrypoint.to_owned(), + ..SandboxOptions::new(SandboxOperation::Stop) + }; + assert_eq!(stop.check().unwrap(), None); + } + let init = SandboxOptions { + entrypoint: "/sbin/init".to_owned(), + ..launching(SandboxOperation::Run) + }; + assert_eq!(init.check().unwrap(), None); +} + +/// A distro layer in a temporary directory. +struct Layer { + /// The temporary directory. + directory: tempfile::TempDir, +} + +impl Layer { + fn new() -> Self { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("distro.erofs"), b"distro").unwrap(); + Self { directory } + } + + fn path(&self, name: &str) -> String { + PurePath::native(self.directory.path().to_str().unwrap()) + .join(name) + .as_str() + .to_owned() + } + + fn launch(&self, entrypoint: &str) -> SandboxLaunch { + SandboxLaunch::new( + vec![SandboxLayer::new( + LayerRole::Distro, + &self.path("distro.erofs"), + "11111111-1111-1111-1111-111111111111", + )], + &self.path("scratch.ext4"), + SandboxSettings { + entrypoint: entrypoint.to_owned(), + ..SandboxSettings::default() + }, + ) + .unwrap() + } + + fn manifest(&self, text: &str) { + fs::write(self.path("distro.erofs.manifest.json"), text).unwrap(); + } +} + +fn distro_sha256() -> String { + Sha256::digest(b"distro") + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +#[test] +fn sandbox_allows_non_systemd_sbin_init() { + let layer = Layer::new(); + validate_sandbox_systemd_policy(&layer.launch("/sbin/init")).unwrap(); + assert_eq!( + message(validate_sandbox_systemd_policy( + &layer.launch("/lib/systemd/systemd") + )), + "systemd entrypoints are unsupported by the sandbox security profile" + ); +} + +#[test] +fn sandbox_rejects_systemd_from_bound_distro_metadata() { + let layer = Layer::new(); + let digest = distro_sha256(); + let manifest = |format: &str, artifact: &str, sha256: &str, packages: &str| { + layer.manifest(&format!( + "{{\"format\": {format}, \"artifact\": {artifact}, \"artifact_sha256\": {sha256}, \ + \"packages\": {packages}}}" + )); + validate_sandbox_systemd_policy(&layer.launch("/sbin/init")) + }; + let sha256 = format!("\"{digest}\""); + assert_eq!( + message(manifest( + "1", + "\"distro.erofs\"", + &sha256, + "[{\"name\": \"systemd\"}]" + )), + "systemd images are unsupported by the sandbox security profile" + ); + // Python compares the format numerically, so 1.0 and true are 1. + for format in ["1", "1.0", "true"] { + manifest( + format, + "\"distro.erofs\"", + &sha256, + "[{\"name\": \"bash\"}]", + ) + .unwrap(); + } + for (format, artifact, sha256) in [ + ("\"1\"", "\"distro.erofs\"", sha256.as_str()), + ("2", "\"distro.erofs\"", &sha256), + ("1", "\"other.erofs\"", &sha256), + ("1", "\"distro.erofs\"", "\"0\""), + ] { + assert_eq!( + message(manifest(format, artifact, sha256, "[]")), + "sandbox distro manifest does not match its artifact" + ); + } + for packages in [ + "{}", + "[1]", + "[{}]", + "[{\"name\": 1}]", + "[{\"name\": \"systemd\"}, 1]", + ] { + assert_eq!( + message(manifest("1", "\"distro.erofs\"", &sha256, packages)), + "sandbox distro manifest has invalid package metadata" + ); + } + layer.manifest("[]"); + assert_eq!( + message(validate_sandbox_systemd_policy(&layer.launch("/sbin/init"))), + "invalid sandbox distro manifest: expected a JSON object" + ); + layer.manifest("{\r\n\"format\": }"); + assert_eq!( + message(validate_sandbox_systemd_policy(&layer.launch("/sbin/init"))), + "invalid sandbox distro manifest: Expecting value: line 2 column 11 (char 12)" + ); + fs::write(layer.path("distro.erofs.manifest.json"), b"{\xff}").unwrap(); + assert_eq!( + message(validate_sandbox_systemd_policy(&layer.launch("/sbin/init"))), + "'utf-8' codec can't decode byte 0xff in position 1: invalid start byte" + ); + // A layer without a manifest passes. + fs::remove_file(layer.path("distro.erofs.manifest.json")).unwrap(); + validate_sandbox_systemd_policy(&layer.launch("/sbin/init")).unwrap(); +} + +#[test] +fn sandbox_managed_operations_reject_dry_run_before_state_access() { + for operation in [ + SandboxOperation::Provision, + SandboxOperation::Start, + SandboxOperation::Exec, + SandboxOperation::Stop, + SandboxOperation::Deprovision, + ] { + let options = SandboxOptions { + dry_run: true, + ..SandboxOptions::new(operation) + }; + assert_eq!( + message(options.check()), + "--dry-run is only valid for sandbox run" + ); + } + let run = SandboxOptions { + dry_run: true, + ..launching(SandboxOperation::Run) + }; + run.check().unwrap(); +} + +#[test] +fn sandbox_exec_forwards_explicit_empty_environment_and_cwd() { + let directory = tempfile::tempdir().unwrap(); + let file = directory.path().join("environment.json"); + fs::write(&file, "[]").unwrap(); + let options = SandboxOptions { + cwd: Some("/work".to_owned()), + environment_file: Some(file.to_str().unwrap().to_owned()), + ..SandboxOptions::new(SandboxOperation::Exec) + }; + assert_eq!(options.check().unwrap(), Some(Vec::new())); +} + +#[test] +fn sandbox_exec_forwards_environment_inheritance() { + for (environment, expected) in [ + (vec!["A=1"], Some(vec!["A=1"])), + (vec!["A=1", "TERM=dumb"], Some(vec!["A=1", "TERM=dumb"])), + // Without entries the workload gets the defaults whatever the flag says. + (Vec::new(), None), + ] { + let options = SandboxOptions { + environment: environment.iter().map(|&entry| entry.to_owned()).collect(), + inherit_default_environment: true, + ..SandboxOptions::new(SandboxOperation::Exec) + }; + let expected = + expected.map(|entries| entries.iter().map(|&entry| entry.to_owned()).collect()); + assert_eq!(options.check().unwrap(), expected); + } +} + +#[test] +fn sandbox_exec_rejects_ambiguous_or_one_shot_environment() { + let both = SandboxOptions { + environment: vec!["A=1".to_owned()], + environment_file: Some("environment.json".to_owned()), + ..SandboxOptions::new(SandboxOperation::Exec) + }; + assert_eq!( + message(both.check()), + "--environment and --environment-file are mutually exclusive" + ); + let one_shot = SandboxOptions { + environment: vec!["A=1".to_owned()], + ..SandboxOptions::new(SandboxOperation::Run) + }; + let exec_only = "managed execution options require the sandbox exec operation"; + assert_eq!(message(one_shot.check()), exec_only); + for operation in [SandboxOperation::Run, SandboxOperation::Start] { + let inheriting = SandboxOptions { + inherit_default_environment: true, + ..SandboxOptions::new(operation) + }; + assert_eq!(message(inheriting.check()), exec_only); + let cwd = SandboxOptions { + cwd: Some("/".to_owned()), + ..SandboxOptions::new(operation) + }; + assert_eq!(message(cwd.check()), exec_only); + } +} + +#[test] +fn sandbox_environment_file_is_bounded_before_state_access() { + let directory = tempfile::tempdir().unwrap(); + let file = directory.path().join("environment.json"); + fs::write(&file, vec![b' '; MAX_ENVIRONMENT_FILE_BYTES + 1]).unwrap(); + assert_eq!( + message(exec_environment_from_file(file.to_str().unwrap())), + "managed execution environment file exceeds 1048576-byte limit" + ); + // A file of the limit is read, and its spaces are no JSON value. + fs::write(&file, vec![b' '; MAX_ENVIRONMENT_FILE_BYTES]).unwrap(); + let path = PurePath::native(file.to_str().unwrap()); + assert_eq!( + message(exec_environment_from_file(file.to_str().unwrap())), + format!("failed to read managed execution environment: {path}") + ); +} + +#[test] +fn sandbox_environment_is_validated_before_state_access() { + let too_many: Vec = (0..257).map(|index| format!("KEY{index}=value")).collect(); + let entries = |entries: &[&str]| entries.iter().map(|&entry| entry.to_owned()).collect(); + let non_empty = "managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"; + for (environment, expected) in [ + ( + too_many, + "managed exec environment exceeds 256 entries".to_owned(), + ), + (entries(&["NO_EQUALS"]), non_empty.to_owned()), + (entries(&["=empty-key"]), non_empty.to_owned()), + (entries(&["KEY=embedded\0nul"]), non_empty.to_owned()), + ( + vec![format!("KEY={}", "\u{2603}".repeat(1365))], + non_empty.to_owned(), + ), + ( + entries(&["DUPLICATE=one", "DUPLICATE=two"]), + "managed exec environment contains duplicate key: DUPLICATE".to_owned(), + ), + ] { + let options = SandboxOptions { + environment, + ..SandboxOptions::new(SandboxOperation::Exec) + }; + assert_eq!(message(options.check()), expected); + } + // An entry of exactly 4096 bytes fits. + validate_exec_environment(&[format!("K={}", "v".repeat(4094))]).unwrap(); +} + +#[test] +fn sandbox_environment_file_entries_are_validated_before_state_access() { + let directory = tempfile::tempdir().unwrap(); + let file = directory.path().join("environment.json"); + let read = |text: &str| { + fs::write(&file, text).unwrap(); + exec_environment_from_file(file.to_str().unwrap()) + }; + assert_eq!( + message(read(r#"["DUPLICATE=one", "DUPLICATE=two"]"#)), + "managed exec environment contains duplicate key: DUPLICATE" + ); + assert_eq!( + read(r#"["A=1", "B=\u00e9"]"#).unwrap(), + ["A=1".to_owned(), "B=\u{e9}".to_owned()] + ); + let not_strings = + "managed execution environment file must contain a JSON array of KEY=VALUE strings"; + for text in ["{}", "\"A=1\"", "[1]", "[NaN]", "[[\"A=1\"]]"] { + assert_eq!(message(read(text)), not_strings, "{text}"); + } + let path = PurePath::native(file.to_str().unwrap()); + let unreadable = format!("failed to read managed execution environment: {path}"); + for text in ["", "[", "[\"A=1\",]", "\u{feff}[]", "[\"a\tb\"]"] { + assert_eq!(message(read(text)), unreadable, "{text:?}"); + } + fs::write(&file, b"[\"\xff\"]").unwrap(); + assert_eq!( + message(exec_environment_from_file(file.to_str().unwrap())), + unreadable + ); + // Python refuses to encode unpaired surrogates, after it accepted them in the file. + assert_eq!( + message(read(r#"["A=1", "B=\ud800"]"#)), + "'utf-8' codec can't encode character '\\ud800' in position 2: surrogates not allowed" + ); + assert_eq!( + message(read(r#"["B=\udc00\ud800x"]"#)), + "'utf-8' codec can't encode characters in position 2-3: surrogates not allowed" + ); + // `int` refuses an integer of more than 4300 digits. + assert_eq!( + message(read(&format!("[{}]", "1".repeat(4301)))), + "Exceeds the limit (4300 digits) for integer string conversion: value has 4301 digits; \ + use sys.set_int_max_str_digits() to increase the limit" + ); + assert_eq!( + message(exec_environment_from_file( + directory.path().join("missing.json").to_str().unwrap() + )), + format!( + "failed to read managed execution environment: {}", + PurePath::native(directory.path().join("missing.json").to_str().unwrap()) + ) + ); +} + +#[test] +fn managed_non_launch_operations_reject_network_policy_options() { + for operation in [ + SandboxOperation::Start, + SandboxOperation::Exec, + SandboxOperation::Stop, + SandboxOperation::Deprovision, + ] { + let options = SandboxOptions { + network_options: true, + ..SandboxOptions::new(operation) + }; + assert_eq!( + message(options.check()), + "network policy options are only valid for sandbox run or provision" + ); + } + for operation in [SandboxOperation::Run, SandboxOperation::Provision] { + let options = SandboxOptions { + network_options: true, + net: true, + ..launching(operation) + }; + assert_eq!( + message(options.check()), + "--net and --network-profile must be specified together" + ); + let profile = SandboxOptions { + network_options: true, + network_profile: true, + ..launching(operation) + }; + assert_eq!( + message(profile.check()), + "--net and --network-profile must be specified together" + ); + let missing = SandboxOptions { + scratch: false, + ..launching(operation) + }; + assert_eq!( + message(missing.check()), + format!("sandbox {operation} requires --layer and --scratch") + ); + } + let report = SandboxOptions { + outcome_report: true, + ..SandboxOptions::new(SandboxOperation::Start) + }; + assert_eq!( + message(report.check()), + "--outcome-report is only valid for one-shot run or managed exec" + ); +} + +#[test] +fn sandbox_command_rejects_misplaced_mount_options() { + for kind in POLICY_KINDS { + let options = SandboxOptions { + policy_paths: vec![PolicyPathOption { + kind, + preceding_mount: None, + path: "secrets".to_owned(), + }], + ..launching(SandboxOperation::Run) + }; + assert_eq!( + message(options.check()), + format!("{} requires --mount", kind.option()) + ); + } + let owner = SandboxOptions { + mount_owner: Some(MountOwner::Caller), + ..launching(SandboxOperation::Run) + }; + assert_eq!(message(owner.check()), "--mount-owner requires --mount"); + for operation in [ + SandboxOperation::Start, + SandboxOperation::Exec, + SandboxOperation::Stop, + SandboxOperation::Deprovision, + ] { + let options = SandboxOptions { + mounts: vec!["/workspace,share,rw".to_owned()], + ..SandboxOptions::new(operation) + }; + assert_eq!( + message(options.check()), + "--mount is only valid for sandbox run or provision" + ); + } +} + +/// Host directories for shares in a temporary directory, with a distro layer and a scratch +/// image. +struct Shares { + /// The temporary directory. + directory: tempfile::TempDir, + /// The directory, resolved. + root: PurePath, +} + +impl Shares { + fn new() -> Self { + let directory = tempfile::tempdir().unwrap(); + let root = resolve(&PurePath::native(directory.path().to_str().unwrap())).unwrap(); + fs::write(root.join("distro.erofs").as_path(), b"distro").unwrap(); + fs::write(root.join("scratch.ext4").as_path(), b"scratch").unwrap(); + Self { directory, root } + } + + fn path(&self, name: &str) -> String { + let _ = &self.directory; + self.root.join(name).as_str().to_owned() + } + + fn directory(&self, name: &str) -> String { + let path = self.path(name); + fs::create_dir_all(&path).unwrap(); + path + } + + /// Runs the steps of `nvx.py sandbox --dry-run` that build the OpenVMM arguments and the + /// kernel command line of these shares. + fn run( + &self, + mounts: &[String], + policy_paths: &[(PolicyKind, Option, &str)], + owner: Option, + ) -> Result<(Vec, String)> { + let mounts = mounts.to_vec(); + let policy_paths: Vec = policy_paths + .iter() + .map(|&(kind, preceding_mount, path)| PolicyPathOption { + kind, + preceding_mount, + path: path.to_owned(), + }) + .collect(); + SandboxOptions { + mounts: mounts.clone(), + policy_paths: policy_paths.clone(), + mount_owner: owner, + dry_run: true, + ..launching(SandboxOperation::Run) + } + .check()?; + let launch = SandboxLaunch::new( + vec![SandboxLayer::new( + LayerRole::Distro, + &self.path("distro.erofs"), + "11111111-1111-1111-1111-111111111111", + )], + &self.path("scratch.ext4"), + SandboxSettings { + mounts: sandbox_mounts(&mounts, &policy_paths, owner)?, + ..SandboxSettings::default() + }, + )?; + launch.validated()?; + validate_sandbox_systemd_policy(&launch)?; + Ok((launch.openvmm_arguments()?, launch.kernel_command_line("")?)) + } +} + +/// Returns the values of `option` in `arguments`. +fn values(arguments: &[String], option: &str) -> Vec { + arguments + .windows(2) + .filter(|pair| pair[0] == option) + .map(|pair| pair[1].clone()) + .collect() +} + +#[test] +fn sandbox_command_forwards_mount_to_openvmm() { + let shares = Shares::new(); + let share = shares.directory("share"); + let (arguments, _) = shares + .run( + &[format!("/workspace,{share},rw")], + &[(PolicyKind::Denied, Some(0), "secrets")], + None, + ) + .unwrap(); + assert_eq!( + values(&arguments, "--mount"), + [format!("/workspace,{share},rw")] + ); + assert_eq!(values(&arguments, "--mount-deny"), ["secrets"]); + assert!(!arguments.contains(&"--mount-owner".to_owned())); +} + +#[test] +fn sandbox_command_shares_a_file() { + let shares = Shares::new(); + let settings = shares.path("settings.json"); + fs::write(&settings, b"{}").unwrap(); + let (arguments, command_line) = shares + .run( + &[format!("/etc/app/settings.json,{settings},ro")], + &[], + None, + ) + .unwrap(); + let name = aggregate_child_name(0, "/etc/app/settings.json"); + // The regular file is a child of its own, without its directory, which OpenVMM must find to + // be a regular file. + assert!(!arguments.contains(&"--mount".to_owned())); + assert_eq!( + values(&arguments, "--mount-child"), + [format!("{name},{settings},ro,file")] + ); + assert!(command_line.contains(&format!("nvx_share={name},/etc/app/settings.json,ro,file"))); + let error = shares + .run( + &[format!("/etc/app/settings.json,{settings},rw")], + &[(PolicyKind::Writable, Some(0), &settings)], + None, + ) + .unwrap_err(); + assert!(error.to_string().contains("shares a file"), "{error}"); +} + +#[test] +fn sandbox_command_shares_host_paths_with_commas() { + let shares = Shares::new(); + let alpha = shares.directory("alpha,beta"); + let reports = shares.directory("2026, final"); + shares.directory("alpha,beta/secret"); + shares.directory("2026, final/secret"); + let (arguments, _) = shares + .run( + &[format!("/work,{alpha},rw")], + &[(PolicyKind::Denied, Some(0), "secret")], + None, + ) + .unwrap(); + assert_eq!(values(&arguments, "--mount"), [format!("/work,{alpha},rw")]); + assert_eq!(values(&arguments, "--mount-deny"), ["secret"]); + let (arguments, _) = shares + .run( + &[ + format!("/work,{alpha},rw"), + format!("/reports,{reports},ro"), + ], + &[ + (PolicyKind::Denied, Some(0), "secret"), + (PolicyKind::Denied, Some(1), "secret"), + ], + None, + ) + .unwrap(); + let (work, report) = ( + aggregate_child_name(0, "/work"), + aggregate_child_name(1, "/reports"), + ); + let pairs: Vec<(String, String)> = arguments + .windows(2) + .filter(|pair| pair[0] == "--mount-child" || pair[0] == "--mount-deny") + .map(|pair| (pair[0].clone(), pair[1].clone())) + .collect(); + assert_eq!( + pairs, + [ + ("--mount-child".to_owned(), format!("{work},{alpha},rw")), + ("--mount-child".to_owned(), format!("{report},{reports},ro")), + ( + "--mount-deny".to_owned(), + shares.root.join("alpha,beta/secret").as_str().to_owned() + ), + ( + "--mount-deny".to_owned(), + shares.root.join("2026, final/secret").as_str().to_owned() + ), + ] + ); + let error = shares + .run(&[format!("/work,{alpha}")], &[], None) + .unwrap_err(); + assert!( + error.to_string().contains("end the value with ,ro or ,rw"), + "{error}" + ); +} + +#[test] +fn sandbox_command_forwards_mount_owner() { + let shares = Shares::new(); + let share = shares.directory("share"); + let result = shares.run( + &[format!("/workspace,{share},rw")], + &[], + Some(MountOwner::Caller), + ); + if cfg!(windows) { + assert!(message(result).contains("Linux host")); + return; + } + let owner = crate::host::python::fs::stat(Path::new(&share)).unwrap(); + if owner.uid == 0 || owner.gid == 0 { + // Caller ownership refuses a root-owned share. + return; + } + let (arguments, _) = result.unwrap(); + assert_eq!(values(&arguments, "--mount-owner"), ["caller"]); + let position = |option: &str| arguments.iter().position(|argument| argument == option); + assert!(position("--mount") < position("--mount-owner")); +} + +#[test] +fn sandbox_command_attributes_policy_paths_to_the_preceding_mount() { + let shares = Shares::new(); + let work = shares.directory("work"); + let tools = shares.directory("tools"); + let mounts = [ + format!("/workspace,{work},rw"), + format!("/opt/hostedtoolcache,{tools},ro"), + ]; + let policy = [ + (PolicyKind::Denied, Some(0), "logs"), + (PolicyKind::Allowed, Some(0), "logs/payloads"), + (PolicyKind::Writable, Some(0), "out"), + (PolicyKind::Denied, Some(1), "credentials"), + ]; + let options: Vec = policy + .iter() + .map(|&(kind, preceding_mount, path)| PolicyPathOption { + kind, + preceding_mount, + path: path.to_owned(), + }) + .collect(); + let parsed = sandbox_mounts(&mounts, &options, None).unwrap(); + assert_eq!(parsed[0].policy().denied, ["logs"]); + assert_eq!(parsed[0].policy().allowed, ["logs/payloads"]); + assert_eq!(parsed[0].policy().writable, ["out"]); + assert_eq!(parsed[1].policy().denied, ["credentials"]); + assert!(parsed[1].policy().allowed.is_empty() && parsed[1].policy().writable.is_empty()); + let (arguments, _) = shares.run(&mounts, &policy, None).unwrap(); + // With several shares, OpenVMM attributes absolute paths. + assert_eq!( + values(&arguments, "--mount-allow"), + [shares.root.join("work/logs/payloads").as_str()] + ); + assert_eq!( + values(&arguments, "--mount-write"), + [shares.root.join("work/out").as_str()] + ); + + let workspace = format!("/workspace,{work},rw"); + let toolcache = format!("/opt/hostedtoolcache,{tools}"); + let cache = shares.root.join("tools/cache").as_str().to_owned(); + let error = |mounts: &[String], policy: &[(PolicyKind, Option, &str)]| { + shares.run(mounts, policy, None).unwrap_err().to_string() + }; + assert_eq!( + error(&[], &[(PolicyKind::Writable, None, "out")]), + "--mount-write requires --mount" + ); + // A --mount-write after the read-only share would narrow it. + assert!( + error( + &[workspace.clone(), format!("{toolcache},ro")], + &[(PolicyKind::Writable, Some(1), "cache")] + ) + .contains("is read-only") + ); + assert!( + error( + &[workspace.clone(), toolcache.clone()], + &[(PolicyKind::Allowed, None, "logs")] + ) + .contains("each --mount-allow must follow the --mount") + ); + // A path in one share cannot be attributed to the other. + assert_eq!( + error( + &[workspace.clone(), toolcache.clone()], + &[(PolicyKind::Writable, Some(0), &cache)] + ), + format!( + "--mount-write {cache} is not inside the host directory of --mount /workspace; with \ + several shares, each --mount-write follows the --mount whose directory it names" + ) + ); + // Only a denied path may name a share's root. + assert!( + error( + &[workspace.clone(), toolcache.clone()], + &[ + (PolicyKind::Denied, Some(0), "logs"), + (PolicyKind::Allowed, Some(0), &work) + ] + ) + .contains(&format!( + "--mount-allow {work} is not inside the host directory" + )) + ); + assert!( + error( + &[workspace.clone(), toolcache.clone()], + &[(PolicyKind::Writable, Some(0), &work)] + ) + .contains(&format!( + "--mount-write {work} is not inside the host directory" + )) + ); +} + +#[test] +fn sandbox_command_hides_a_share_root_behind_its_allowed_paths() { + let shares = Shares::new(); + let work = shares.directory("work"); + let src = shares.directory("work/src"); + let tools = shares.directory("tools"); + let (arguments, _) = shares + .run( + &[ + format!("/workspace,{work},rw"), + format!("/opt/hostedtoolcache,{tools}"), + ], + &[ + (PolicyKind::Denied, Some(0), &work), + (PolicyKind::Allowed, Some(0), &src), + ], + None, + ) + .unwrap(); + // OpenVMM hides the workspace's root behind its one allowed path. + let (workspace, toolcache) = ( + aggregate_child_name(0, "/workspace"), + aggregate_child_name(1, "/opt/hostedtoolcache"), + ); + assert_eq!( + values(&arguments, "--mount-child"), + [ + format!("{workspace},{work},rw"), + format!("{toolcache},{tools},ro") + ] + ); + assert_eq!(values(&arguments, "--mount-deny"), [work]); + assert_eq!(values(&arguments, "--mount-allow"), [src]); +} + +#[test] +fn sandbox_command_attributes_mount_deny_to_the_preceding_mount() { + let shares = Shares::new(); + let work = shares.directory("work"); + let tools = shares.directory("tools"); + let mounts = [ + format!("/workspace,{work},rw"), + format!("/opt/hostedtoolcache,{tools},ro"), + ]; + let policy = [ + (PolicyKind::Denied, Some(0), "secrets"), + (PolicyKind::Denied, Some(1), "credentials"), + (PolicyKind::Denied, Some(1), "logs"), + ]; + let (arguments, command_line) = shares.run(&mounts, &policy, None).unwrap(); + let (workspace, toolcache) = ( + aggregate_child_name(0, "/workspace"), + aggregate_child_name(1, "/opt/hostedtoolcache"), + ); + assert!(!arguments.contains(&"--mount".to_owned())); + assert_eq!(values(&arguments, "--mount-aggregate"), ["/run/nvx/shares"]); + assert_eq!( + values(&arguments, "--mount-child"), + [ + format!("{workspace},{work},rw"), + format!("{toolcache},{tools},ro") + ] + ); + assert_eq!( + values(&arguments, "--mount-deny"), + [ + shares.root.join("work/secrets").as_str(), + shares.root.join("tools/credentials").as_str(), + shares.root.join("tools/logs").as_str(), + ] + ); + assert!(command_line.contains(&format!( + "nvx_share={workspace},/workspace,rw nvx_share={toolcache},/opt/hostedtoolcache,ro" + ))); + + // With one share, a --mount-deny may come first; with several, it must follow the share + // whose directory it hides. + let single = sandbox_mounts( + &[format!("/w,{work},rw")], + &[PolicyPathOption { + kind: PolicyKind::Denied, + preceding_mount: None, + path: "secrets".to_owned(), + }], + None, + ) + .unwrap(); + assert_eq!(single[0].policy().denied, ["secrets"]); + let error = shares + .run( + &[ + format!("/workspace,{work},rw"), + format!("/opt/hostedtoolcache,{tools}"), + ], + &[(PolicyKind::Denied, None, "secrets")], + None, + ) + .unwrap_err(); + assert_eq!( + error.to_string(), + "with several --mount options, each --mount-deny must follow the --mount whose host \ + directory it names" + ); + // More shares are children of the same aggregate. + let cache = shares.directory("cache"); + let (arguments, _) = shares + .run( + &[ + format!("/a,{work}"), + format!("/b,{tools}"), + format!("/c,{cache}"), + ], + &[], + None, + ) + .unwrap(); + assert_eq!(values(&arguments, "--mount-child").len(), 3); +} + +#[test] +fn sandbox_mounts_take_the_kind_of_each_host_path() { + let mounts = ["/a,a".to_owned(), "/b,b.json,rw".to_owned()]; + let kind_of = |mount: &SandboxMount| { + if mount.guest_target() == "/b" { + MountKind::File + } else { + MountKind::Directory + } + }; + let parsed = sandbox_mounts_with( + Flavor::Posix, + &mounts, + &[], + Some(MountOwner::Caller), + &kind_of, + ) + .unwrap(); + assert_eq!(parsed[1].kind(), MountKind::File); + assert_eq!(parsed[0].owner(), MountOwner::Caller); + let arguments = mounts_openvmm_arguments(&parsed).unwrap(); + assert!(arguments.contains(&format!("{},b.json,rw,file", aggregate_child_name(1, "/b")))); +} + +#[test] +fn run_share_targets_bind_from_an_aggregate() { + validate_run_share_targets(&["/workspace", "/opt/hostedtoolcache", "/srv/cache"]).unwrap(); + validate_run_share_targets(&["/config/settings.json"]).unwrap(); + for (targets, expected) in [ + ( + &["/workspace", "/workspace/cache"][..], + "--mount targets /workspace and /workspace/cache overlap; a share cannot hide another", + ), + ( + &["/run/nvx", "/b"], + "--mount targets /run/nvx/shares and /run/nvx overlap; a share cannot hide another", + ), + ( + &["/run/nvx/shares/x"], + "--mount targets /run/nvx/shares and /run/nvx/shares/x overlap; a share cannot hide \ + another", + ), + ] { + assert_eq!(message(validate_run_share_targets(targets)), expected); + } + for target in [ + "/a b", "relative", "/", "/a//b", "/a/./b", "/a\\b", "/a=b", "/a,b", + ] { + assert_eq!( + message(validate_run_share_targets(&[target, "/b"])), + format!( + "invalid --mount target {}: with several shares, or a shared file, each target \ + must be an absolute non-root path without empty, dot, or parent components, \ + whitespace, '\\', '=', or ','", + repr(target) + ) + ); + } + // Unlike a sandbox target, a run target has no length limit. + validate_run_share_targets(&[&format!("/{}", "x".repeat(5000))]).unwrap(); +} diff --git a/nvx-rs/src/host/sandbox/tests.rs b/nvx-rs/src/host/sandbox/tests.rs new file mode 100644 index 000000000..a22385880 --- /dev/null +++ b/nvx-rs/src/host/sandbox/tests.rs @@ -0,0 +1,934 @@ +//! The launch-profile tests of `scripts/test_nvx_tools.py`, ported. + +use std::fs; + +use super::*; +use crate::ErrorKind; +use crate::host::python::fs::cwd; + +const DISTRO: &str = "distro,distro.erofs,11111111-1111-1111-1111-111111111111"; + +fn distro() -> SandboxLayer { + SandboxLayer::parse(DISTRO).unwrap() +} + +fn policy(denied: &[&str], allowed: &[&str], writable: &[&str]) -> MountPolicy { + let owned = |paths: &[&str]| paths.iter().map(|&path| path.to_owned()).collect(); + MountPolicy { + denied: owned(denied), + allowed: owned(allowed), + writable: owned(writable), + } +} + +fn mount(value: &str, denied: &[&str]) -> SandboxMount { + SandboxMount::parse( + value, + policy(denied, &[], &[]), + MountOwner::Vmm, + MountKind::Directory, + ) + .unwrap() +} + +fn with_mounts(mounts: Vec) -> Result { + SandboxLaunch::new( + vec![distro()], + "scratch.ext4", + SandboxSettings { + mounts, + ..SandboxSettings::default() + }, + ) +} + +fn message(result: Result) -> String { + result.unwrap_err().to_string() +} + +fn native(path: &str) -> String { + PurePath::native(path).as_str().to_owned() +} + +#[test] +fn launch_contract_builds_agent_command_line() { + let launch = SandboxLaunch::new( + vec![distro()], + "scratch.ext4", + SandboxSettings { + entrypoint: "/bin/workload".to_owned(), + args: vec!["--serve".to_owned()], + hostname: "example".to_owned(), + workload_identity: (1000, 1001), + memory_max: Some(Int::from(268_435_456_u32)), + pids_max: Some(Int::from(64_u32)), + mounts: Vec::new(), + }, + ) + .unwrap(); + let roles: Vec = launch + .ordered_layers() + .iter() + .map(|layer| layer.role()) + .collect(); + assert_eq!(roles, [LayerRole::Distro]); + assert_eq!( + launch.kernel_command_line("quiet").unwrap(), + "quiet nvx_sandbox=1 \ + nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 \ + nvx_scratch=0xd0006000,ext4 \ + nvx_entrypoint=/bin/workload nvx_hostname=example \ + nvx_arg=--serve nvx_memory_max=268435456 nvx_pids_max=65" + ); + assert_eq!( + launch.openvmm_arguments().unwrap(), + [ + "--machine", + "microvm", + "--microvm-sandbox-block", + "distro:file:distro.erofs,ro", + "--microvm-sandbox-block", + "scratch:file:scratch.ext4", + "--microvm-workload-identity", + "1000:1001", + ] + ); +} + +#[test] +fn launch_contract_rejects_duplicates_and_reserved_tokens() { + let error = SandboxLaunch::new( + vec![distro(), distro()], + "scratch.ext4", + SandboxSettings::default(), + ) + .unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported); + assert_eq!(error.to_string(), "duplicate sandbox layer role(s): distro"); + let launch = with_mounts(Vec::new()).unwrap(); + assert_eq!( + message(launch.kernel_command_line("nvx_sandbox=0")), + "nvx_sandbox is owned by the sandbox profile" + ); + assert_eq!( + message(launch.kernel_command_line(r"foo=bar\ nvx_memory_max=max")), + "nvx_memory_max is owned by the sandbox profile" + ); + assert_eq!( + message(launch.kernel_command_line(&"x".repeat(SANDBOX_COMMAND_LINE_MAX_SIZE))), + "sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount \ + shares or use shorter targets and arguments" + ); + assert_eq!( + message(launch.kernel_command_line("a\0b")), + "kernel command line contains an embedded NUL" + ); + assert_eq!( + message(parse_workload_identity("0:0")), + "--workload-user UID must be between 1 and 4294967295" + ); +} + +#[test] +fn launch_settings_are_checked_in_python_order() { + let settings = |change: fn(&mut SandboxSettings)| { + let mut settings = SandboxSettings::default(); + change(&mut settings); + message(SandboxLaunch::new(vec![distro()], "s.ext4", settings)) + }; + assert_eq!( + message(SandboxLaunch::new( + Vec::new(), + "s.ext4", + SandboxSettings::default() + )), + "a sandbox requires exactly one read-only distro layer" + ); + assert_eq!( + message(SandboxLaunch::new( + vec![distro(); 4], + "s.ext4", + SandboxSettings::default() + )), + "duplicate sandbox layer role(s): distro" + ); + // The configuration of a sandbox provisioned before microsoft/nvx#556 may name the retired + // roles, which no launch accepts, alone or beside the distro layer. + let retired = |role| SandboxLayer::new(role, "r.erofs", "22222222-2222-2222-2222-222222222222"); + for role in [LayerRole::Runtime, LayerRole::Custom] { + for layers in [vec![retired(role)], vec![distro(), retired(role)]] { + assert_eq!( + message(SandboxLaunch::new( + layers, + "s.ext4", + SandboxSettings::default() + )), + "a sandbox requires exactly one read-only distro layer" + ); + } + } + // Duplicates come first, and name every repeated role. + assert_eq!( + message(SandboxLaunch::new( + vec![ + retired(LayerRole::Custom), + distro(), + retired(LayerRole::Custom), + retired(LayerRole::Runtime), + retired(LayerRole::Runtime), + ], + "s.ext4", + SandboxSettings::default() + )), + "duplicate sandbox layer role(s): custom, runtime" + ); + // The entrypoint is checked before the hostname. + for entrypoint in ["bin/sh", "/bin/s h", "/bin/sh\x1c"] { + let settings = SandboxSettings { + entrypoint: entrypoint.to_owned(), + hostname: "Invalid".to_owned(), + ..SandboxSettings::default() + }; + assert_eq!( + message(SandboxLaunch::new(vec![distro()], "s.ext4", settings)), + "sandbox entrypoint must be an absolute path without spaces" + ); + } + assert_eq!( + settings(|settings| settings.args = vec![String::new()]), + "sandbox arguments must be nonempty and contain no spaces" + ); + assert_eq!( + settings(|settings| settings.args = vec!["a\u{3000}b".to_owned()]), + "sandbox arguments must be nonempty and contain no spaces" + ); + for hostname in ["", "-a", "a-", "A", "a.b", "a\n"] { + let launch = SandboxLaunch::new( + vec![distro()], + "s.ext4", + SandboxSettings { + hostname: hostname.to_owned(), + ..SandboxSettings::default() + }, + ); + assert_eq!( + message(launch), + "sandbox hostname must be a lowercase RFC 1123 label up to 63 characters", + "{hostname:?}" + ); + } + assert_eq!( + settings(|settings| settings.workload_identity = (1000, 0)), + "sandbox workload identity must be a non-root UID:GID" + ); + assert_eq!( + settings(|settings| settings.memory_max = Some(Int::zero())), + "--memory-max must be positive" + ); + assert_eq!( + settings(|settings| settings.pids_max = Some(Int::from(-1_i64))), + "--pids-max must be positive" + ); +} + +#[test] +fn workload_identities_parse_as_python_int() { + assert_eq!(parse_workload_identity("1000:1001").unwrap(), (1000, 1001)); + assert_eq!( + parse_workload_identity(" +1_000 :\u{ff11}\u{ff10}").unwrap(), + (1000, 10) + ); + assert_eq!( + parse_workload_identity("4294967295:1").unwrap(), + (u32::MAX, 1) + ); + for (value, expected) in [ + ("1000", "--workload-user must be UID:GID"), + ("1:2:3", "--workload-user must be UID:GID"), + ("a:1", "--workload-user must contain numeric UID and GID"), + ("1:", "--workload-user must contain numeric UID and GID"), + ("1:0x10", "--workload-user must contain numeric UID and GID"), + ( + "4294967296:1", + "--workload-user UID must be between 1 and 4294967295", + ), + ( + "-1:1", + "--workload-user UID must be between 1 and 4294967295", + ), + ( + "1:0", + "--workload-user GID must be between 1 and 4294967295", + ), + ] { + assert_eq!( + message(parse_workload_identity(value)), + expected, + "{value:?}" + ); + } + let digits = format!("{}:1", "1".repeat(4301)); + assert_eq!( + message(parse_workload_identity(&digits)), + "--workload-user must contain numeric UID and GID" + ); +} + +#[test] +fn launch_contract_attaches_mount_and_reserves_bootstrap_tokens() { + let launch = with_mounts(vec![mount("/workspace,share,rw", &["secrets"])]).unwrap(); + let arguments = launch.openvmm_arguments().unwrap(); + let start = arguments + .iter() + .position(|argument| argument == "--mount") + .unwrap(); + assert_eq!( + arguments[start..], + ["--mount", "/workspace,share,rw", "--mount-deny", "secrets"] + ); + assert!( + !launch + .kernel_command_line("quiet") + .unwrap() + .contains("virtfs_") + ); + for (token, name) in [ + ("virtfs_dir=/other", "virtfs_dir"), + ("virtfs_tag=other", "virtfs_tag"), + ("virtfs_mode=rw", "virtfs_mode"), + ] { + assert_eq!( + message(launch.kernel_command_line(token)), + format!("{name} is owned by the sandbox --mount option") + ); + } + let base = with_mounts(Vec::new()) + .unwrap() + .kernel_command_line("") + .unwrap(); + let fragment = mount::mounts_command_line_fragment(launch.mounts()); + assert_eq!( + fragment, + " virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rw" + ); + let fitting = "x".repeat(SANDBOX_COMMAND_LINE_MAX_SIZE - base.len() - fragment.len() - 2); + launch.kernel_command_line(&fitting).unwrap(); + assert!(message(launch.kernel_command_line(&format!("{fitting}x"))).contains("1024-byte")); +} + +#[test] +fn launch_contract_attaches_several_shares_as_an_aggregate() { + let launch = with_mounts(vec![ + mount("/workspace,work,rw", &["secrets"]), + mount("/opt/hostedtoolcache,tools,ro", &["credentials"]), + mount("/srv/cache,cache,ro", &[]), + ]) + .unwrap(); + let names: Vec = launch + .mounts() + .iter() + .enumerate() + .map(|(index, mount)| mount::aggregate_child_name(index, mount.guest_target())) + .collect(); + let arguments = launch.openvmm_arguments().unwrap(); + // Several shares are the children of one aggregate, and OpenVMM attributes each denied + // path to the child that contains it, so relative paths are joined to their share. + assert!(!arguments.iter().any(|argument| argument == "--mount")); + let start = arguments + .iter() + .position(|argument| argument == "--mount-aggregate") + .unwrap(); + let directory = cwd().unwrap(); + assert_eq!( + arguments[start..], + [ + "--mount-aggregate".to_owned(), + "/run/nvx/shares".to_owned(), + "--mount-child".to_owned(), + format!("{},work,rw", names[0]), + "--mount-child".to_owned(), + format!("{},tools,ro", names[1]), + "--mount-child".to_owned(), + format!("{},cache,ro", names[2]), + "--mount-deny".to_owned(), + directory.join("work").join("secrets").as_str().to_owned(), + "--mount-deny".to_owned(), + directory + .join("tools") + .join("credentials") + .as_str() + .to_owned(), + ] + ); + let fragment = mount::mounts_command_line_fragment(launch.mounts()); + assert_eq!( + fragment, + " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=rw virtfs_aggregate=1" + ); + // The guest binds each child at its target, in order. + let command_line = launch.kernel_command_line("").unwrap(); + assert!(!command_line.contains("virtfs_")); + assert!( + command_line.ends_with(&format!( + " nvx_share={},/workspace,rw nvx_share={},/opt/hostedtoolcache,ro \ + nvx_share={},/srv/cache,ro", + names[0], names[1], names[2] + )), + "{command_line}" + ); + assert_eq!( + mount::mounts_command_line_fragment(&launch.mounts()[1..]), + " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=ro virtfs_aggregate=1" + ); + assert_eq!( + message(launch.kernel_command_line("nvx_share=0,/etc,rw")), + "nvx_share is owned by the sandbox profile" + ); + // Every share's tokens count toward the x86 budget. + let base = with_mounts(Vec::new()) + .unwrap() + .kernel_command_line("") + .unwrap(); + let shares = command_line.len() - base.len(); + let fitting = + "x".repeat(SANDBOX_COMMAND_LINE_MAX_SIZE - base.len() - shares - fragment.len() - 2); + launch.kernel_command_line(&fitting).unwrap(); + assert!(message(launch.kernel_command_line(&format!("{fitting}x"))).contains("1024-byte")); + + let caller: Vec = [("/workspace", "work"), ("/tools", "tools")] + .iter() + .map(|(target, path)| { + SandboxMount::new( + target, + path, + MountAccess::ReadOnly, + MountPolicy::default(), + MountOwner::Caller, + MountKind::Directory, + ) + .unwrap() + }) + .collect(); + let arguments = mount::mounts_openvmm_arguments(&caller).unwrap(); + assert_eq!( + arguments + .iter() + .filter(|argument| *argument == "--mount-owner") + .count(), + 1 + ); + assert_eq!( + arguments[arguments.len() - 2..], + ["--mount-owner", "caller"] + ); +} + +#[test] +fn launch_bounds_shares_by_the_kernel_command_line() { + let launch = |count: usize| { + with_mounts( + (0..count) + .map(|index| mount(&format!("/share/{index:02},host{index}"), &[])) + .collect(), + ) + .unwrap() + }; + // No fixed number of shares is the limit, only the command line. + launch(10).kernel_command_line("").unwrap(); + assert!(message(launch(20).kernel_command_line("")).contains("fewer --mount shares")); +} + +#[test] +fn launch_rejects_invalid_share_sets() { + let owned = |value: &str, owner: MountOwner| { + SandboxMount::parse(value, MountPolicy::default(), owner, MountKind::Directory).unwrap() + }; + let vmm = |value: &str| owned(value, MountOwner::Vmm); + for (mounts, expected) in [ + ( + vec![vmm("/workspace,a,rw"), vmm("/workspace,b")], + "sandbox mount targets /workspace and /workspace overlap; a share cannot hide another", + ), + ( + vec![vmm("/workspace,a,rw"), vmm("/workspace/cache,b")], + "sandbox mount targets /workspace and /workspace/cache overlap; a share cannot hide \ + another", + ), + ( + vec![vmm("/opt/tools/node,a"), vmm("/opt,b,rw")], + "sandbox mount targets /opt/tools/node and /opt overlap; a share cannot hide another", + ), + ( + vec![ + vmm("/workspace,a,rw"), + owned("/tools,b", MountOwner::Caller), + ], + "every sandbox --mount must use the same --mount-owner", + ), + ] { + assert_eq!(message(with_mounts(mounts)), expected); + } + // A shared prefix that is not a path component is not an overlap. + with_mounts(vec![vmm("/work,a,rw"), vmm("/workspace,b")]).unwrap(); +} + +#[test] +fn launch_contract_attaches_a_file_as_an_aggregate_child() { + let file = |value: &str| { + SandboxMount::parse( + value, + MountPolicy::default(), + MountOwner::Vmm, + MountKind::File, + ) + .unwrap() + }; + let settings = file("/etc/app/settings.json,settings.json,ro"); + let launch = with_mounts(vec![settings.clone()]).unwrap(); + let name = mount::aggregate_child_name(0, "/etc/app/settings.json"); + // Even alone, a file is the child of an aggregate, because the guest mounts the device's + // root, which is a directory. + assert!(mount::mounts_use_aggregate(launch.mounts())); + let arguments = launch.openvmm_arguments().unwrap(); + assert!(!arguments.iter().any(|argument| argument == "--mount")); + let start = arguments + .iter() + .position(|argument| argument == "--mount-aggregate") + .unwrap(); + assert_eq!( + arguments[start..], + [ + "--mount-aggregate".to_owned(), + "/run/nvx/shares".to_owned(), + "--mount-child".to_owned(), + format!("{name},settings.json,ro,file"), + ] + ); + assert_eq!( + mount::mounts_command_line_fragment(launch.mounts()), + " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=ro virtfs_aggregate=1" + ); + assert!( + launch + .kernel_command_line("") + .unwrap() + .ends_with(&format!(" nvx_share={name},/etc/app/settings.json,ro,file")) + ); + + // Files and directories share one aggregate, and only a file's token is marked. + let workspace = mount("/workspace,work,rw", &["secrets"]); + let output = file("/results/output.txt,output.txt,rw"); + let mixed = with_mounts(vec![workspace, settings, output]).unwrap(); + let names: Vec = mixed + .mounts() + .iter() + .enumerate() + .map(|(index, mount)| mount::aggregate_child_name(index, mount.guest_target())) + .collect(); + assert!(mixed.kernel_command_line("").unwrap().ends_with(&format!( + " nvx_share={},/workspace,rw nvx_share={},/etc/app/settings.json,ro,file \ + nvx_share={},/results/output.txt,rw,file", + names[0], names[1], names[2] + ))); + let arguments = mixed.openvmm_arguments().unwrap(); + let denied = arguments + .iter() + .position(|argument| argument == "--mount-deny") + .unwrap(); + assert_eq!( + arguments[denied + 1], + cwd().unwrap().join("work").join("secrets").as_str() + ); + + // A file's mode applies to the whole file, so no policy path names one. + for policy in [ + policy(&["x"], &[], &[]), + policy(&[], &["x"], &[]), + policy(&[], &[], &["x"]), + ] { + let error = SandboxMount::new( + "/results/output.txt", + "output.txt", + MountAccess::ReadWrite, + policy, + MountOwner::Vmm, + MountKind::File, + ) + .unwrap_err(); + assert!(error.to_string().contains("shares a file"), "{error}"); + } + assert_eq!( + message(MountKind::from_name("socket")), + "unsupported sandbox mount kind 'socket'; choose directory or file" + ); + // The runtime replaces /etc/machine-id with a file, and binds its time state over + // /run/nvx/time, so nothing at or below them may be shared. + for target in [ + "/etc/machine-id", + "/etc/machine-id/child", + "/run/nvx/time", + "/run/nvx/time/state", + ] { + let error = SandboxMount::parse( + &format!("{target},id"), + MountPolicy::default(), + MountOwner::Vmm, + MountKind::File, + ) + .unwrap_err(); + assert!(error.to_string().contains("reserved"), "{error}"); + } + // Only /etc itself is reserved, not what lies below it, and the runtime creates /run and + // /run/nvx, but nothing else of them (microsoft/nvx#486). + mount("/etc/app,app", &[]); + for target in ["/etc", "/run", "/run/nvx"] { + assert_eq!( + message(mount::validate_mount_target(target)), + format!("sandbox mount target {target} is reserved") + ); + } + assert_eq!( + message(mount::validate_mount_target("/run/nvx/time/state")), + "sandbox mount target /run/nvx/time/state overlaps the reserved /run/nvx/time tree" + ); + for target in [ + "/run/secrets", + "/run/nvx/other", + "/run/nvxtime", + "/run/nvx/timer", + ] { + mount::validate_mount_target(target).unwrap(); + } +} + +/// A sandbox's distro layer and scratch image in a temporary directory. +struct Fixture { + /// The temporary directory. + directory: tempfile::TempDir, +} + +impl Fixture { + fn new() -> Self { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("distro.erofs"), b"layer").unwrap(); + fs::write(directory.path().join("scratch.ext4"), b"scratch").unwrap(); + Self { directory } + } + + /// Returns the path of `name` in the directory, or of the directory. + fn path(&self, name: &str) -> String { + let root = PurePath::native(self.directory.path().to_str().unwrap()); + if name.is_empty() { + return root.as_str().to_owned(); + } + root.join(name).as_str().to_owned() + } + + fn launch(&self, mounts: Vec) -> SandboxLaunch { + SandboxLaunch::new( + vec![SandboxLayer::new( + LayerRole::Distro, + &self.path("distro.erofs"), + "11111111-1111-1111-1111-111111111111", + )], + &self.path("scratch.ext4"), + SandboxSettings { + mounts, + ..SandboxSettings::default() + }, + ) + .unwrap() + } +} + +fn parse(value: &str, policy: MountPolicy) -> SandboxMount { + SandboxMount::parse(value, policy, MountOwner::Vmm, MountKind::Directory).unwrap() +} + +fn resolve(path: &str) -> PurePath { + crate::host::python::fs::resolve(&PurePath::native(path)).unwrap() +} + +fn path_hash(text: &str) -> u64 { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + text.hash(&mut hasher); + hasher.finish() +} + +#[test] +fn launch_validation_rejects_overlapping_or_foreign_host_paths() { + let fixture = Fixture::new(); + let (work, tools, root) = ( + fixture.path("work"), + fixture.path("tools"), + fixture.path(""), + ); + fs::create_dir_all(fixture.path("work/nested")).unwrap(); + fs::create_dir_all(fixture.path("tools/credentials")).unwrap(); + let nested = fixture.path("work/nested"); + let credentials = fixture.path("tools/credentials"); + + let workspace = parse(&format!("/workspace,{work},rw"), MountPolicy::default()); + for host_path in [&nested, &work, &root] { + let toolcache = parse( + &format!("/opt/hostedtoolcache,{host_path}"), + MountPolicy::default(), + ); + let error = fixture + .launch(vec![workspace.clone(), toolcache]) + .validated() + .unwrap_err(); + assert!(error.to_string().contains("overlap"), "{error}"); + } + + // Each denied path must be inside its own share. + let foreign = parse( + &format!("/workspace,{work},rw"), + policy(&[&credentials], &[], &[]), + ); + let toolcache = parse( + &format!("/opt/hostedtoolcache,{tools}"), + MountPolicy::default(), + ); + let error = fixture + .launch(vec![foreign, toolcache.clone()]) + .validated() + .unwrap_err(); + assert_eq!( + error.to_string(), + format!( + "--mount-deny {credentials} is not inside the host directory of --mount /workspace; \ + with several shares, each --mount-deny follows the --mount whose directory it names" + ) + ); + // A denied path may name a share's root, which OpenVMM then hides behind its allowed paths, + // but no allowed or writable path may. + fixture + .launch(vec![ + parse( + &format!("/workspace,{work},rw"), + policy(&["."], &[&nested], &[]), + ), + toolcache.clone(), + ]) + .validated() + .unwrap(); + for (allowed, writable) in [(&["."][..], &[][..]), (&[], &["."])] { + let error = fixture + .launch(vec![ + parse( + &format!("/workspace,{work},rw"), + policy(&["nested"], allowed, writable), + ), + toolcache.clone(), + ]) + .validated() + .unwrap_err(); + assert!(error.to_string().contains("not inside"), "{error}"); + } + let validated = fixture.launch(vec![ + parse( + &format!("/workspace,{work},rw"), + policy(&["nested"], &[], &[]), + ), + parse( + &format!("/opt/hostedtoolcache,{tools}"), + policy(&[&credentials], &[], &[]), + ), + ]); + validated.validated().unwrap(); + assert_eq!(validated.mounts().len(), 2); + + // Resolving a path doesn't see through a bind mount, so one directory reached through two + // paths, or a share inside a bind mount of the other share's directory, is rejected by its + // file identities. + let shares = [workspace.clone(), toolcache.clone()]; + mount::validate_mount_host_paths(&shares).unwrap(); + let same = |_: &PurePath| -> std::io::Result<(u64, u128)> { Ok((1, 1)) }; + let queries = mount::HostQueries { + identity: &same, + ..mount::HostQueries::native() + }; + let error = mount::validate_mount_host_paths_with(&shares, &queries).unwrap_err(); + assert!(error.to_string().contains("same directory"), "{error}"); + fs::create_dir_all(fixture.path("alias/cache")).unwrap(); + let (work_resolved, alias_resolved) = (resolve(&work), resolve(&fixture.path("alias"))); + let bound = |path: &PurePath| -> std::io::Result<(u64, u128)> { + // `alias` is a bind mount of the workspace's directory. + if *path == work_resolved || *path == alias_resolved { + return Ok((1, 1)); + } + Ok((0, u128::from(path_hash(path.as_str())))) + }; + let queries = mount::HostQueries { + identity: &bound, + ..mount::HostQueries::native() + }; + let cache = parse( + &format!("/opt/hostedtoolcache,{}", fixture.path("alias/cache")), + MountPolicy::default(), + ); + mount::validate_mount_host_paths_with(&shares, &queries).unwrap(); + for mounts in [ + [workspace.clone(), cache.clone()], + [cache.clone(), workspace.clone()], + ] { + let error = mount::validate_mount_host_paths_with(&mounts, &queries).unwrap_err(); + assert!(error.to_string().contains("inside the other"), "{error}"); + } +} + +#[test] +fn launch_validation_requires_plain_mount_directory() { + let fixture = Fixture::new(); + let share = fixture.path("share"); + let launch = || { + fixture.launch(vec![ + SandboxMount::new( + "/workspace", + &share, + MountAccess::ReadOnly, + MountPolicy::default(), + MountOwner::Vmm, + MountKind::Directory, + ) + .unwrap(), + ]) + }; + let expected = format!("sandbox mount host path is not a plain directory: {share}"); + assert_eq!(launch().validated().unwrap_err().to_string(), expected); + fs::write(&share, b"file").unwrap(); + assert_eq!(launch().validated().unwrap_err().to_string(), expected); + fs::remove_file(&share).unwrap(); + fs::create_dir(&share).unwrap(); + let validated = launch(); + validated.validated().unwrap(); + assert_eq!(validated.mounts()[0].host_path(), Path::new(&share)); +} + +#[test] +fn launch_validation_requires_the_kind_of_each_share() { + let fixture = Fixture::new(); + let config = fixture.path("config"); + fs::create_dir(&config).unwrap(); + let settings = fixture.path("config/settings.json"); + fs::write(&settings, b"{}").unwrap(); + assert_eq!(mount::mount_kind(Path::new(&settings)), MountKind::File); + assert_eq!(mount::mount_kind(Path::new(&config)), MountKind::Directory); + assert_eq!( + mount::mount_kind(Path::new(&fixture.path("missing"))), + MountKind::Directory + ); + let share = |target: &str, path: &str, kind: MountKind| { + SandboxMount::new( + target, + path, + MountAccess::ReadOnly, + MountPolicy::default(), + MountOwner::Vmm, + kind, + ) + .unwrap() + }; + let file = share("/etc/app/settings.json", &settings, MountKind::File); + file.validated().unwrap(); + // A directory at the file's path is not shared in its place. + fs::remove_file(&settings).unwrap(); + fs::create_dir(&settings).unwrap(); + let expected = format!("sandbox mount host path is not a regular file: {settings}"); + assert_eq!(file.validated().unwrap_err().to_string(), expected); + fs::remove_dir(&settings).unwrap(); + assert_eq!(file.validated().unwrap_err().to_string(), expected); + + // A file overlaps a shared directory that contains it, and another share of the same file, + // such as through a hard link, but not a file beside it. + fs::write(&settings, b"{}").unwrap(); + let other = fixture.path("config/other.json"); + fs::write(&other, b"{}").unwrap(); + let link = fixture.path("link.json"); + fs::hard_link(&settings, &link).unwrap(); + mount::validate_mount_host_paths(&[ + share("/a.json", &settings, MountKind::File), + share("/b.json", &other, MountKind::File), + ]) + .unwrap(); + let error = mount::validate_mount_host_paths(&[ + share("/config", &config, MountKind::Directory), + share("/a.json", &settings, MountKind::File), + ]) + .unwrap_err(); + assert!(error.to_string().contains("overlap"), "{error}"); + let error = mount::validate_mount_host_paths(&[ + share("/a.json", &settings, MountKind::File), + share("/b.json", &link, MountKind::File), + ]) + .unwrap_err(); + assert!( + error.to_string().contains("same directory or file"), + "{error}" + ); +} + +#[test] +fn launch_contract_rejects_disk_option_delimiters() { + let fixture = Fixture::new(); + let layer = fixture.path("layer;create=1G"); + fs::write(&layer, b"").unwrap(); + let launch = SandboxLaunch::new( + vec![SandboxLayer::new( + LayerRole::Distro, + &layer, + "11111111-1111-1111-1111-111111111111", + )], + &fixture.path("scratch.ext4"), + SandboxSettings::default(), + ) + .unwrap(); + assert_eq!( + launch.validated().unwrap_err().to_string(), + format!("disk paths containing commas or semicolons are unsupported: {layer}") + ); +} + +#[test] +fn launch_validation_requires_the_layers_and_the_scratch_image() { + let fixture = Fixture::new(); + let missing = SandboxLaunch::new( + vec![SandboxLayer::new( + LayerRole::Distro, + &fixture.path("missing.erofs"), + "11111111-1111-1111-1111-111111111111", + )], + &fixture.path("scratch.ext4"), + SandboxSettings::default(), + ) + .unwrap(); + let error = missing.validated().unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unavailable); + assert_eq!( + error.to_string(), + format!( + "distro EROFS layer not found: {}", + fixture.path("missing.erofs") + ) + ); + let scratch = fixture.launch(Vec::new()); + fs::remove_file(fixture.path("scratch.ext4")).unwrap(); + assert_eq!( + scratch.validated().unwrap_err().to_string(), + format!( + "ext4 scratch image not found: {}", + fixture.path("scratch.ext4") + ) + ); + assert_eq!( + require_file(Path::new("missing//file"), "OpenVMM release binary") + .unwrap_err() + .to_string(), + format!( + "OpenVMM release binary not found: {}", + native("missing/file") + ) + ); +} diff --git a/nvx-rs/src/host/session.rs b/nvx-rs/src/host/session.rs index 8c087c2f1..52d6ebcc0 100644 --- a/nvx-rs/src/host/session.rs +++ b/nvx-rs/src/host/session.rs @@ -331,6 +331,28 @@ impl ControlSession { Ok(()) } + /// Sends an application request of `kind` with `request_id` and `payload` in the next + /// sequenced outer record, for a caller that checks the responses itself, as the `nvx-rs` + /// CLI does with the messages of `nvx.py`. + pub fn send_request( + &mut self, + kind: u8, + request_id: u64, + payload: &[u8], + deadline: Instant, + ) -> Result<(), SessionError> { + self.send_app(kind, request_id, payload, Some(deadline)) + } + + /// Reads the next application frame, checked against the session's identity and sequence, + /// and returns its kind, request ID, status, and payload. + pub fn read_response( + &mut self, + deadline: Option, + ) -> Result<(u8, u64, i32, Vec), SessionError> { + self.read_app(deadline) + } + /// Encodes and sends one application frame in the next sequenced outer record. fn send_app( &mut self, diff --git a/nvx-rs/src/host/sys/interrupt.rs b/nvx-rs/src/host/sys/interrupt.rs new file mode 100644 index 000000000..75c254bfb --- /dev/null +++ b/nvx-rs/src/host/sys/interrupt.rs @@ -0,0 +1,46 @@ +//! Ctrl-C on Unix hosts: a `SIGINT` handler that records the interrupt, which the CLI polls. + +use std::io; +use std::mem::MaybeUninit; +use std::ptr; +use std::sync::atomic::{AtomicBool, Ordering}; + +use nix::errno::Errno; +use nix::libc; +use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction}; + +/// Whether a `SIGINT` arrived since [`watch_interrupts`] installed its handler. +static INTERRUPTED: AtomicBool = AtomicBool::new(false); + +/// The `SIGINT` handler, which only records the signal. +extern "C" fn record(_signal: libc::c_int) { + INTERRUPTED.store(true, Ordering::SeqCst); +} + +/// Records each `SIGINT` for [`interrupted`] instead of letting it terminate the process, as +/// Python's handler, which raises `KeyboardInterrupt`, does. +/// +/// Like Python, it keeps a `SIGINT` that the process started with ignored: a caller that ignores +/// the signal for its children keeps it from interrupting them. +pub(crate) fn watch_interrupts() -> io::Result<()> { + let mut current = MaybeUninit::::zeroed(); + // SAFETY: a null action only reads the current disposition into `current`. + Errno::result(unsafe { libc::sigaction(libc::SIGINT, ptr::null(), current.as_mut_ptr()) })?; + // SAFETY: sigaction filled `current` in. + if unsafe { current.assume_init() }.sa_sigaction == libc::SIG_IGN { + return Ok(()); + } + let action = SigAction::new( + SigHandler::Handler(record), + SaFlags::SA_RESTART, + SigSet::empty(), + ); + // SAFETY: the handler only stores to an atomic, which is async-signal-safe. + unsafe { sigaction(Signal::SIGINT, &action) }?; + Ok(()) +} + +/// Returns whether a `SIGINT` arrived since [`watch_interrupts`]. +pub(crate) fn interrupted() -> bool { + INTERRUPTED.load(Ordering::SeqCst) +} diff --git a/nvx-rs/src/host/sys/mod.rs b/nvx-rs/src/host/sys/mod.rs index d0757e44e..21b247a58 100644 --- a/nvx-rs/src/host/sys/mod.rs +++ b/nvx-rs/src/host/sys/mod.rs @@ -4,9 +4,11 @@ //! standard library or this module, which makes its system calls through `nix` and confines the //! unsafe ones behind wrappers that establish their ownership and async-signal-safety invariants. //! `nix` has no wrapper for `pidfd_open`, `pidfd_send_signal`, `close_range`, or `getdents64`, so -//! those go through `nix`'s re-export of libc. The Windows platform module still calls -//! `windows-sys` directly. +//! those go through `nix`'s re-export of libc, as does the query of the `SIGINT` disposition that +//! precedes the CLI's handler. The Windows platform module still calls `windows-sys` directly. +#[cfg(feature = "cli")] +mod interrupt; #[cfg(target_os = "linux")] mod process; #[cfg(target_os = "linux")] @@ -16,6 +18,8 @@ mod socket; pub(crate) use nix::errno::Errno; use nix::unistd::{getegid, geteuid}; +#[cfg(feature = "cli")] +pub(crate) use interrupt::{interrupted, watch_interrupts}; #[cfg(target_os = "linux")] pub(crate) use process::{PidFd, detach}; #[cfg(all(test, target_os = "linux"))] diff --git a/nvx-rs/src/protocol/features.rs b/nvx-rs/src/protocol/features.rs index 2252b2239..cd7db3176 100644 --- a/nvx-rs/src/protocol/features.rs +++ b/nvx-rs/src/protocol/features.rs @@ -7,10 +7,10 @@ use super::{ProtocolError, array, violation}; /// Control features of a guest image, advertised in the response to /// [`APP_FEATURES`](super::APP_FEATURES). /// -/// The bits are defined by the guest agent in `guest/common/nvx-managed-agent.c`, which also -/// speaks for the features of its image's init script. A host drives a sandbox only when its -/// guest provides every feature in [`GuestFeatures::REQUIRED`]; an older image would silently -/// ignore the policy or the request that depends on the missing feature. +/// The Rust guest agent uses these shared definitions and also speaks for the features of its +/// image's init script. A host drives a sandbox only when its guest provides every feature in +/// [`GuestFeatures::REQUIRED`]; an older image would silently ignore the policy or the request +/// that depends on the missing feature. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct GuestFeatures(u32); @@ -50,6 +50,17 @@ impl GuestFeatures { Self(Self::CANCEL.0 | Self::WORKLOAD_ACCOUNT.0 | Self::EXEC_ENVIRONMENT.0); /// The features that the host depends on. pub const REQUIRED: Self = Self::DIRECT_AGENT; + /// The features that `nvx-rs sandbox` depends on in the layered sandbox root, whose agent + /// announces [`SANDBOX_AGENT`](Self::SANDBOX_AGENT): only + /// [`EXEC_ENVIRONMENT`](Self::EXEC_ENVIRONMENT), because `exec` sends a working directory and + /// an environment in an extended `EXEC`. + /// + /// It holds no [`CANCEL`](Self::CANCEL), because the CLI sends no `APP_CANCEL`: as `nvx.py` + /// did, Ctrl-C ends an `exec` by closing the session, and the guest agent ends the workload + /// when OpenVMM resets the session that it ran for. It holds no + /// [`WORKLOAD_ACCOUNT`](Self::WORKLOAD_ACCOUNT), because no sandbox launch requests account + /// creation, which the layered root does not perform (microsoft/nvx#566). + pub const LAYERED_SANDBOX: Self = Self::EXEC_ENVIRONMENT; /// Human-readable names of every required feature, used in compatibility errors. pub(crate) const NAMES: [(Self, &'static str); 6] = [ diff --git a/nvx-rs/src/protocol/mod.rs b/nvx-rs/src/protocol/mod.rs index 776c66075..a003e866d 100644 --- a/nvx-rs/src/protocol/mod.rs +++ b/nvx-rs/src/protocol/mod.rs @@ -1,8 +1,8 @@ //! Codec of the OpenVMM control-console protocol. //! //! OpenVMM brokers an authenticated stream of outer records (magic `NVXS`) between one host -//! client and the guest agent. Data records carry application frames (magic `NVXC`) that belong -//! to the guest agent in `guest/common/nvx-managed-agent.c`. All integers are little-endian. +//! client and the Rust guest agent. Data records carry application frames (magic `NVXC`) that +//! the host and guest share from this module. All integers are little-endian. //! //! - Outer records: [`encode_outer`] and [`decode_outer_header`], and the `OUTER_*` record types. //! - Application frames: [`encode_app`] and [`decode_app`], the `APP_*` request and response @@ -17,8 +17,7 @@ //! such as [`MAPS_TOKEN`]. //! //! The module depends on nothing outside the standard library, so the host, the guest, and test -//! doubles share it. Its wire constants are those of the guest agent, which tests compare with -//! the agent's source, and golden byte fixtures pin every record that both sides exchange. +//! doubles share it. Tests pin its wire constants as literals and its records as golden bytes. use std::fmt; diff --git a/nvx-rs/src/protocol/tests.rs b/nvx-rs/src/protocol/tests.rs index 506e3799b..34860531b 100644 --- a/nvx-rs/src/protocol/tests.rs +++ b/nvx-rs/src/protocol/tests.rs @@ -1,7 +1,7 @@ //! Tests of the control protocol, including golden byte fixtures. //! -//! The fixtures are literal bytes, produced with Python's struct module using the formats of -//! `scripts/nvx_tools/control_session.py` and the guest agent: `"<4sHBB16sQQI"` for outer +//! The fixtures are literal bytes, produced with Python's struct module using the formats of the +//! former `scripts/nvx_tools/control_session.py` and the guest agent: `"<4sHBB16sQQI"` for outer //! headers, `"<4sBBHQiI"` for application frames, `" Option { - let source = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .parent()? - .join("guest") - .join("common") - .join("nvx-managed-agent.c"); - std::fs::read_to_string(source).ok() +// Hosts and guests share the wire constants, so changing one passes every round trip between +// them yet breaks compatibility with released peers. These tests pin each value literally. + +#[test] +fn record_definitions_keep_their_wire_values() { + assert_eq!(OUTER_HEADER_LEN, 44); + assert_eq!(OUTER_MAX_PAYLOAD, 65_536); + assert_eq!(OUTER_VERSION, 1); + assert_eq!(OUTER_GUEST_ATTACH, 1); + assert_eq!(OUTER_RESET, 3); + assert_eq!(OUTER_ACK, 4); + assert_eq!(OUTER_DATA, 5); + assert_eq!(OUTER_CREDIT, 9); + assert_eq!(OUTER_RECEIVE_WINDOW, 1024 * 1024); + assert_eq!(APP_HEADER_LEN, 24); + assert_eq!(APP_VERSION, 1); + assert_eq!(APP_PING, 1); + assert_eq!(APP_EXEC, 2); + assert_eq!(APP_STOP, 3); + assert_eq!(APP_CANCEL, 4); + assert_eq!(APP_READY, 0x81); + assert_eq!(APP_STDOUT, 0x82); + assert_eq!(APP_STDERR, 0x83); + assert_eq!(APP_EXIT, 0x84); + assert_eq!(APP_STOPPED, 0x85); + assert_eq!(APP_ERROR, 0xff); } #[test] -fn feature_definitions_match_the_guest_agent_source() { - let Some(source) = guest_agent_source() else { - return; - }; - let mut definitions = vec![ - format!("#define APP_FEATURES {APP_FEATURES}U"), - format!("#define APP_MAPS {APP_MAPS}U"), - ]; - for (name, feature) in [ - ("CANCEL", GuestFeatures::CANCEL), - ("WORKLOAD_ACCOUNT", GuestFeatures::WORKLOAD_ACCOUNT), - ("EXEC_CGROUP", GuestFeatures::EXEC_CGROUP), - ("EXEC_ENVIRONMENT", GuestFeatures::EXEC_ENVIRONMENT), - ("EXEC_CWD", GuestFeatures::EXEC_CWD), - ("HOST_MAPPING_TABLE", GuestFeatures::HOST_MAPPING_TABLE), - ] { - let bit = feature.bits().trailing_zeros(); - definitions.push(format!("#define FEATURE_{name} (1U << {bit})")); - } - for definition in definitions { - assert!( - source.contains(&definition), - "the guest agent does not define `{definition}`" - ); - } - // The retired bit stays unused, so hosts that need it refuse the image. - assert!(!source.contains("(1U << 1)")); +fn feature_definitions_keep_their_wire_values() { + assert_eq!(APP_FEATURES, 5); + assert_eq!(APP_MAPS, 6); + assert_eq!(GuestFeatures::CANCEL.bits(), 0x01); + // Bit 1 is retired and stays unused, so hosts that still need it refuse the image. + assert_eq!(GuestFeatures::WORKLOAD_ACCOUNT.bits(), 0x04); + assert_eq!(GuestFeatures::EXEC_CGROUP.bits(), 0x08); + assert_eq!(GuestFeatures::EXEC_ENVIRONMENT.bits(), 0x10); + assert_eq!(GuestFeatures::EXEC_CWD.bits(), 0x20); + assert_eq!(GuestFeatures::HOST_MAPPING_TABLE.bits(), 0x40); } #[test] -fn host_mapping_table_matches_the_guest_agent_source() { - let Some(source) = guest_agent_source() else { - return; - }; - for expected in [ - format!("#define MAX_HOST_MAPPINGS {MAX_HOST_MAPPINGS}U"), - format!("#define MAX_GUEST_PATH {}U", MAX_MAP_PATH_BYTES + 1), - format!("#define MAPS_HEADER_LEN {MAPS_HEADER_LEN}U"), - format!("#define MAP_ENTRY_HEADER_LEN {MAP_ENTRY_HEADER_LEN}U"), - format!("#define MAP_READ_ONLY {MAP_READ_ONLY}U"), - format!("#define MAPS_TOKEN \"{MAPS_TOKEN}\""), - format!("\"{MAPPINGS_INCOMPLETE}\""), - format!("\"{MAPPING_FAILED}\""), - ] { - assert!( - source.contains(&expected), - "the guest agent does not contain `{expected}`" - ); - } +fn host_mapping_table_keeps_its_wire_values() { + assert_eq!(MAX_HOST_MAPPINGS, 4096); + assert_eq!(MAX_MAP_PATH_BYTES, 4095); + assert_eq!(MAPS_HEADER_LEN, 8); + assert_eq!(MAP_ENTRY_HEADER_LEN, 6); + assert_eq!(MAP_READ_ONLY, 1); + assert_eq!(MAPS_TOKEN, "nvx_maps="); + assert_eq!(MAPPINGS_INCOMPLETE, "mappings-incomplete"); + assert_eq!(MAPPING_FAILED, "mapping-failed"); } #[test] -fn exec_extension_matches_the_guest_agent_source() { - // The agent accepts a working directory as long as one argument, more than any directory - // that a workload can enter. - const { assert!(MAX_CWD_BYTES < MAX_ARGUMENT_BYTES) }; - let Some(source) = guest_agent_source() else { - return; - }; - for expected in [ - format!("#define EXEC_EXTENDED {EXEC_EXTENDED}U"), - format!("#define EXEC_CWD_PRESENT {EXEC_CWD_PRESENT}U"), - format!("#define EXEC_ENVIRONMENT_PRESENT {EXEC_ENVIRONMENT_PRESENT}U"), - format!("#define EXEC_INHERIT_DEFAULT_ENV {EXEC_INHERIT_DEFAULT_ENV}U"), - format!("#define MAX_ARGUMENTS {MAX_ARGUMENTS}U"), - format!("#define MAX_ARGUMENT_LEN {MAX_ARGUMENT_BYTES}U"), - format!("#define MAX_ENVIRONMENT {MAX_ENVIRONMENT}U"), - format!( - "#define MAX_OUTPUT_BYTES ({}U * 1024U)", - MAX_OUTPUT_BYTES / 1024 - ), - format!("\"{CWD_FAILED}\""), - format!("\"{LAUNCH_FAILED}\""), - format!("\"{INVALID_REQUEST}\""), - format!("\"{BUSY}\""), - format!("\"{}\"", String::from_utf8_lossy(UNSUPPORTED_OPERATION)), - ] { - assert!( - source.contains(&expected), - "the guest agent does not contain `{expected}`" - ); - } +fn exec_extension_keeps_its_wire_values() { + assert_eq!(EXEC_EXTENDED, 1); + assert_eq!(EXEC_CWD_PRESENT, 1); + assert_eq!(EXEC_ENVIRONMENT_PRESENT, 2); + assert_eq!(EXEC_INHERIT_DEFAULT_ENV, 4); + assert_eq!(MAX_ARGUMENTS, 64); + assert_eq!(MAX_ARGUMENT_BYTES, 4096); + assert_eq!(MAX_ENVIRONMENT, 256); + assert_eq!(MAX_OUTPUT_BYTES, 1024 * 1024); + assert_eq!(CWD_FAILED, "cwd-failed"); + assert_eq!(LAUNCH_FAILED, "launch-failed"); + assert_eq!(INVALID_REQUEST, "invalid-request"); + assert_eq!(BUSY, "busy"); + assert_eq!(UNSUPPORTED_OPERATION, b"unsupported-operation"); } #[test] -fn record_definitions_match_the_guest_agent_source() { - let Some(source) = guest_agent_source() else { - return; - }; - for expected in [ - format!("#define OUTER_HEADER_LEN {OUTER_HEADER_LEN}U"), - format!("#define OUTER_MAX_PAYLOAD {OUTER_MAX_PAYLOAD}U"), - format!("#define OUTER_VERSION {OUTER_VERSION}U"), - format!("#define OUTER_GUEST_ATTACH {OUTER_GUEST_ATTACH}U"), - format!("#define OUTER_RESET {OUTER_RESET}U"), - format!("#define OUTER_ACK {OUTER_ACK}U"), - format!("#define OUTER_DATA {OUTER_DATA}U"), - format!("#define OUTER_CREDIT {OUTER_CREDIT}U"), - format!( - "#define OUTER_RECEIVE_WINDOW ({}U * 1024U)", - OUTER_RECEIVE_WINDOW / 1024 - ), - format!("#define APP_HEADER_LEN {APP_HEADER_LEN}U"), - format!("#define APP_VERSION {APP_VERSION}U"), - format!("#define APP_PING {APP_PING}U"), - format!("#define APP_EXEC {APP_EXEC}U"), - format!("#define APP_STOP {APP_STOP}U"), - format!("#define APP_CANCEL {APP_CANCEL}U"), - format!("#define APP_READY {APP_READY:#x}U"), - format!("#define APP_STDOUT {APP_STDOUT:#x}U"), - format!("#define APP_STDERR {APP_STDERR:#x}U"), - format!("#define APP_EXIT {APP_EXIT:#x}U"), - format!("#define APP_STOPPED {APP_STOPPED:#x}U"), - format!("#define APP_ERROR {APP_ERROR:#x}U"), - ] { - assert!( - source.contains(&expected), - "the guest agent does not contain `{expected}`" - ); - } +fn exec_limits_support_linux_paths() { + // A working directory may be as long as one argument, and Linux cannot + // enter a directory longer than that argument limit. + const { assert!(MAX_CWD_BYTES < MAX_ARGUMENT_BYTES) }; } #[test] @@ -1121,6 +1066,24 @@ fn agent_feature_sets_match_the_c_modes() { ); } +#[test] +fn layered_sandboxes_need_only_per_execution_environments() { + // The CLI's exec sends working directories and environments; it never cancels a workload or + // asks for a workload account (microsoft/nvx#566). + assert_eq!(GuestFeatures::LAYERED_SANDBOX.bits(), 1 << 4); + assert!(GuestFeatures::SANDBOX_AGENT.contains(GuestFeatures::LAYERED_SANDBOX)); + assert!( + GuestFeatures::SANDBOX_AGENT + .missing(GuestFeatures::LAYERED_SANDBOX) + .is_empty() + ); + assert_eq!( + (GuestFeatures::CANCEL | GuestFeatures::WORKLOAD_ACCOUNT) + .missing(GuestFeatures::LAYERED_SANDBOX), + ["per-execution environments"] + ); +} + #[test] fn host_exec_encoding_round_trips_through_the_guest_decoder() { let environment = strings(&["FOO=a b", "EMPTY="]); diff --git a/nvx-rs/src/testing/fake_openvmm.rs b/nvx-rs/src/testing/fake_openvmm.rs index 58affaf43..0e92db20a 100644 --- a/nvx-rs/src/testing/fake_openvmm.rs +++ b/nvx-rs/src/testing/fake_openvmm.rs @@ -47,6 +47,22 @@ //! predates it, and `fake_refuse_maps=1` fails every `MAPS` request like a guest that cannot //! mount a mapped path. The command line is recorded in `fake-openvmm-.json` next to the //! kernel. +//! +//! The fake also serves the sandbox profile of `nvx.py sandbox` and `nvx-rs sandbox`, which a +//! `--microvm-sandbox-block` option selects: read-only `distro`, `runtime`, and `custom` layers +//! and a `scratch` disk, each an existing file, with `nvx_sandbox=1`, `nvx_entrypoint=`, and +//! `nvx_hostname=` on the kernel command line, and shares through one `--mount` or the children +//! of an aggregate at `/run/nvx/shares`. Its guest advertises the features of the sandbox agent, +//! [`GuestFeatures::SANDBOX_AGENT`], and the guest targets of its directory shares are +//! directories that a workload can enter. With `--microvm-lifecycle managed`, it serves the +//! control console as above. With `--microvm-lifecycle one-shot`, it reads no capability and +//! serves no console: it runs the entrypoint and arguments of the kernel command line with its +//! own standard streams, `/bin/echo` printing its arguments, `/bin/sleep` sleeping for its +//! argument in seconds, `/bin/true` and `/bin/false` exiting with 0 and 1, and any other program +//! not found with 127, then writes an outcome report +//! of operation `run` if `--microvm-report` asks for one, and exits with the workload's status, +//! or with `STATUS` if the kernel command line has `fake_exit_status=STATUS`. Sandbox outcome +//! reports take the form of OpenVMM's. use std::collections::{BTreeMap, BTreeSet}; use std::fs::{self, OpenOptions}; @@ -139,14 +155,29 @@ fn atoi(value: &str) -> i32 { } } +/// The lifecycle that the command line selects. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Lifecycle { + /// `--microvm-lifecycle managed`: the guest serves the control console until a stop. + Managed, + /// `--microvm-lifecycle one-shot`: the guest runs its entrypoint once, and OpenVMM exits with + /// its status. Only the sandbox profile uses it. + OneShot, +} + /// Parsed OpenVMM-compatible command line and fake-specific test controls. struct Options { /// Name of the executable, which prefixes the fake's diagnostics. program: String, - /// Control endpoint at which the fake accepts host sessions. + /// The lifecycle. + lifecycle: Lifecycle, + /// Whether the command line launches the sandbox profile of `nvx.py sandbox`: a container + /// over EROFS layers and an ext4 scratch disk. + sandbox: bool, + /// Control endpoint at which the fake accepts host sessions; empty for a one-shot run. endpoint: String, - /// Path to the synthetic OpenVMM outcome report. - report: PathBuf, + /// Path to the synthetic OpenVMM outcome report, which a one-shot run may omit. + report: Option, /// Backend name recorded in the synthetic report. hypervisor: String, /// Path that receives the parsed command-line dump. @@ -155,6 +186,8 @@ struct Options { maps_dump: PathBuf, /// Status with which to stop immediately after launch, when requested. exit_on_start: Option, + /// Status with which a one-shot workload ends instead of its own, when requested. + exit_status: Option, /// Artificial delay before the fake guest becomes ready. boot_delay: Duration, /// Artificial delay before the fake OpenVMM crashes, when requested. @@ -175,6 +208,28 @@ struct Options { children: Vec, /// Number of host mappings that the kernel command line announces. maps: usize, + /// The guest directories at which a sandbox's directory shares appear. + share_targets: Vec, + /// The entrypoint and arguments of a sandbox, from its kernel command line. + workload: Vec, + /// The network policy of the command line, as OpenVMM summarizes it in its report. + network: NetworkSummary, +} + +/// The network policy of a command line, as the `network_policy` section of OpenVMM's outcome +/// report summarizes it. +#[derive(Debug, Clone, Default)] +struct NetworkSummary { + /// Whether any network option was given. + requested: bool, + /// `rules`, `deny-all`, `allow-all`, or `none`. + mode: &'static str, + /// The number of `--network-egress-allow` rules. + allow: usize, + /// The number of `--network-egress-deny` rules. + deny: usize, + /// `allow`, `deny`, or `none`. + host_loopback: &'static str, } /// A host path mapped into the guest. @@ -210,6 +265,12 @@ pub fn main() -> ExitCode { eprintln!("{program}: failing the launch as requested"); return ExitCode::from(code); } + if options.lifecycle == Lifecycle::OneShot { + let status = run_one_shot(&options); + let _ = io::stdout().flush(); + let _ = io::stderr().flush(); + std::process::exit(status); + } let capability = match read_capability() { Ok(capability) => capability, Err(message) => { @@ -233,6 +294,51 @@ pub fn main() -> ExitCode { } } +/// Runs the entrypoint of a one-shot sandbox with its output on the fake's own, as OpenVMM runs +/// the guest on its console, writes the outcome report, and returns the status with which +/// OpenVMM exits: the workload's. +/// +/// The entrypoint is `/bin/echo`, which prints its arguments, `/bin/sleep`, which sleeps for its +/// argument in seconds, `/bin/true` or `/bin/false`, or any other program, which is not found; +/// the `fake_exit_status=STATUS` token replaces the status. +fn run_one_shot(options: &Options) -> i32 { + let (program, arguments) = options + .workload + .split_first() + .map_or(("", &[][..]), |(program, arguments)| { + (program.as_str(), arguments) + }); + let status = match program { + "/bin/echo" => { + println!("{}", arguments.join(" ")); + 0 + } + "/bin/sleep" => { + let seconds = arguments + .first() + .and_then(|value| value.parse::().ok()) + .filter(|seconds| seconds.is_finite()) + .unwrap_or(0.0); + thread::sleep(Duration::from_secs_f64(seconds.clamp(0.0, 3600.0))); + 0 + } + "/bin/true" => 0, + "/bin/false" => 1, + _ => { + eprintln!("{}: {program}: not found", options.program); + 127 + } + }; + let status = options.exit_status.unwrap_or(status); + let (category, status_code) = if status == 0 { + ("success", 0) + } else { + ("guest-exit", status) + }; + write_report(options, "run", category, status_code); + status +} + /// Returns the name of the running executable, such as `nvx-fake-openvmm`. fn program_name() -> String { std::env::args_os() @@ -247,17 +353,20 @@ fn program_name() -> String { /// Parses the OpenVMM command line plus fake controls, rejecting unsupported combinations. fn parse(program: &str, arguments: &[String]) -> Result { - const VALUED: [&str; 24] = [ + const VALUED: [&str; 28] = [ "--mount-aggregate", "--mount-child", "--mount-deny", "--mount-allow", "--mount-write", + "--mount", + "--mount-owner", "--network-egress-allow", "--network-egress-deny", "--machine", "--microvm-workload-identity", "--microvm-lifecycle", + "--microvm-sandbox-block", "--hypervisor", "--memory", "--kernel", @@ -270,6 +379,7 @@ fn parse(program: &str, arguments: &[String]) -> Result { "--network-profile", "--network-egress", "--network-ingress", + "--network-proxy", "--host-loopback", "--host-loopback-forward", ]; @@ -304,13 +414,46 @@ fn parse(program: &str, arguments: &[String]) -> Result { } }; expect("--machine", "microvm")?; - expect("--microvm-lifecycle", "managed")?; - expect("--virtio-console", "none")?; - for flag in ["--single-process", "--microvm-control-auth-stdin"] { - if !flags.contains(flag) { - return Err(format!("{flag} is required")); - } + // The sandbox profile of `nvx.py sandbox` puts its layers and scratch disk on sandbox block + // devices, and may run one-shot. + let sandbox = values.contains_key("--microvm-sandbox-block"); + let lifecycle = match single("--microvm-lifecycle")? { + "managed" => Lifecycle::Managed, + "one-shot" if sandbox => Lifecycle::OneShot, + other => return Err(format!("unsupported lifecycle {other}")), + }; + if !flags.contains("--single-process") { + return Err("--single-process is required".to_owned()); } + let (endpoint, report) = match lifecycle { + Lifecycle::Managed => { + expect("--virtio-console", "none")?; + if !flags.contains("--microvm-control-auth-stdin") { + return Err("--microvm-control-auth-stdin is required".to_owned()); + } + let endpoint = single("--microvm-control-console")? + .strip_prefix("listen=") + .ok_or("--microvm-control-console must use listen=")? + .to_owned(); + (endpoint, Some(PathBuf::from(single("--microvm-report")?))) + } + Lifecycle::OneShot => { + for option in ["--virtio-console", "--microvm-control-console"] { + if values.contains_key(option) { + return Err(format!("a one-shot run takes no {option}")); + } + } + if flags.contains("--microvm-control-auth-stdin") { + return Err("a one-shot run takes no --microvm-control-auth-stdin".to_owned()); + } + let report = match values.get("--microvm-report").map(Vec::as_slice) { + None => None, + Some([report]) => Some(PathBuf::from(report)), + Some(_) => return Err("--microvm-report must be given at most once".to_owned()), + }; + (String::new(), report) + } + }; let hypervisor = single("--hypervisor")?; if !["kvm", "mshv", "whp"].contains(&hypervisor) { return Err(format!("unsupported hypervisor {hypervisor}")); @@ -331,78 +474,74 @@ fn parse(program: &str, arguments: &[String]) -> Result { return Err(format!("{} does not exist", path.display())); } } - let endpoint = single("--microvm-control-console")? - .strip_prefix("listen=") - .ok_or("--microvm-control-console must use listen=")? - .to_owned(); - let report = PathBuf::from(single("--microvm-report")?); - if report.exists() { + if let Some(report) = &report + && report.exists() + { return Err(format!("report path {} already exists", report.display())); } let command_line = single("--cmdline")?; let tokens: Vec<&str> = command_line.split_whitespace().collect(); - if tokens.iter().any(|token| { - token.starts_with("nvx_") - && !token.starts_with(MAPS_TOKEN) - && *token != WORKLOAD_ACCOUNT_TOKEN - }) { - return Err("the kernel command line must not select a sandbox".to_owned()); - } - let maps = match tokens - .iter() - .filter_map(|token| token.strip_prefix(MAPS_TOKEN)) - .collect::>() - .as_slice() - { - [] => 0, - [count] => count - .parse::() - .ok() - .filter(|count| (1..=MAX_HOST_MAPPINGS).contains(count)) - .ok_or_else(|| format!("invalid {MAPS_TOKEN}{count}"))?, - _ => return Err(format!("{MAPS_TOKEN} must be given at most once")), + let token_values = |name: &str| -> Vec<&str> { + tokens + .iter() + .filter_map(|token| token.strip_prefix(name)) + .collect() }; - let children = match values.get("--mount-aggregate").map(Vec::as_slice) { - None if maps == 0 => Vec::new(), - Some([GUEST_EXPORT]) if maps > 0 => { - let mut children = Vec::new(); - for (index, child) in values - .get("--mount-child") - .into_iter() - .flatten() - .enumerate() - { - let invalid = || format!("invalid --mount-child {child}"); - let (name, rest) = child.split_once(',').ok_or_else(invalid)?; - // Like OpenVMM, a child exposes a directory or, with the `file` flag, a regular - // file, and refuses a host path of the other kind. - let (rest, file) = match rest.strip_suffix(",file") { - Some(rest) => (rest, true), - None => (rest, false), - }; - let (host, mode) = rest.rsplit_once(',').ok_or_else(invalid)?; - let kind_matches = if file { - Path::new(host).is_file() - } else { - Path::new(host).is_dir() - }; - if name != index.to_string() || !kind_matches || !["ro", "rw"].contains(&mode) { - return Err(invalid()); - } - children.push(PathBuf::from(host)); + let (maps, children, share_targets, workload) = if sandbox { + if !tokens.contains(&"nvx_sandbox=1") { + return Err("a sandbox block device requires nvx_sandbox=1".to_owned()); + } + check_sandbox_blocks(values.get("--microvm-sandbox-block").into_iter().flatten())?; + let mut workload: Vec = match token_values("nvx_entrypoint=").as_slice() { + [entrypoint] => vec![(*entrypoint).to_owned()], + _ => return Err("the kernel command line must set one nvx_entrypoint".to_owned()), + }; + workload.extend(token_values("nvx_arg=").into_iter().map(str::to_owned)); + if token_values("nvx_hostname=").len() != 1 { + return Err("the kernel command line must set one nvx_hostname".to_owned()); + } + if !token_values(MAPS_TOKEN).is_empty() { + return Err(format!("a sandbox takes no {MAPS_TOKEN}")); + } + let (children, mut share_targets) = sandbox_shares(&values)?; + // The guest binds each directory child of an aggregate at the target of its + // `nvx_share=NAME,TARGET,ACCESS[,file]` token. + for share in token_values("nvx_share=") { + match share.split(',').collect::>().as_slice() { + [_, target, _] => share_targets.push((*target).to_owned()), + [_, _, _, "file"] => {} + _ => return Err(format!("invalid nvx_share={share}")), } - if children.is_empty() { - return Err("--mount-aggregate requires --mount-child".to_owned()); + } + (0, children, share_targets, workload) + } else { + for option in ["--mount", "--mount-owner"] { + if values.contains_key(option) { + return Err(format!("unsupported argument {option}")); } - children } - _ => { - return Err(format!("--mount-aggregate and {MAPS_TOKEN} must agree")); + if tokens.iter().any(|token| { + token.starts_with("nvx_") + && !token.starts_with(MAPS_TOKEN) + && *token != WORKLOAD_ACCOUNT_TOKEN + }) { + return Err("the kernel command line must not select a sandbox".to_owned()); + } + if !tokens.iter().any(|token| token.starts_with("hostname=")) { + return Err("the kernel command line must set the hostname".to_owned()); } + let maps = match token_values(MAPS_TOKEN).as_slice() { + [] => 0, + [count] => count + .parse::() + .ok() + .filter(|count| (1..=MAX_HOST_MAPPINGS).contains(count)) + .ok_or_else(|| format!("invalid {MAPS_TOKEN}{count}"))?, + _ => return Err(format!("{MAPS_TOKEN} must be given at most once")), + }; + let children = mapped_children(&values, maps)?; + (maps, children, Vec::new(), Vec::new()) }; - if children.is_empty() && values.contains_key("--mount-child") { - return Err("--mount-child requires --mount-aggregate".to_owned()); - } for option in ["--mount-deny", "--mount-allow", "--mount-write"] { for path in values.get(option).into_iter().flatten() { let path = Path::new(path); @@ -427,9 +566,6 @@ fn parse(program: &str, arguments: &[String]) -> Result { { return Err("--host-loopback-forward requires explicit --host-loopback allow".to_owned()); } - if !tokens.iter().any(|token| token.starts_with("hostname=")) { - return Err("the kernel command line must set the hostname".to_owned()); - } if values.contains_key("--net") { expect("--network-profile", "portable")?; } @@ -444,18 +580,27 @@ fn parse(program: &str, arguments: &[String]) -> Result { .map(Duration::from_millis) }; let token = report - .parent() + .as_ref() + .and_then(|report| report.parent()) .and_then(Path::file_name) .map(|name| name.to_string_lossy().into_owned()) .unwrap_or_default(); + let default_features = if sandbox { + GuestFeatures::SANDBOX_AGENT + } else { + GUEST_FEATURES + }; Ok(Options { program: program.to_owned(), + lifecycle, + sandbox, endpoint, args_dump: kernel.with_file_name(format!("fake-openvmm-{token}.json")), maps_dump: kernel.with_file_name(format!("fake-openvmm-{token}-maps.json")), report, hypervisor: hypervisor.to_owned(), exit_on_start: knob("fake_exit_on_start").and_then(|value| value.parse().ok()), + exit_status: knob("fake_exit_status").and_then(|value| value.parse().ok()), boot_delay: millis("fake_boot_delay_ms").unwrap_or_default(), crash_after: millis("fake_crash_after_ms"), ignore_stop: knob("fake_ignore_stop") == Some("1"), @@ -465,12 +610,186 @@ fn parse(program: &str, arguments: &[String]) -> Result { workload_uid, guest_features: knob("fake_guest_features") .and_then(|value| value.parse().ok()) - .map_or(GUEST_FEATURES, GuestFeatures::from_bits), + .map_or(default_features, GuestFeatures::from_bits), children, maps, + share_targets, + workload, + network: network_summary(&values), }) } +/// Checks the sandbox block devices of a sandbox: each layer, `ROLE:file:PATH,ro` with a role of +/// `distro`, `runtime`, or `custom`, at most once each, and one scratch disk, `scratch:file:PATH`, +/// each an existing regular file. +fn check_sandbox_blocks<'a>(blocks: impl Iterator) -> Result<(), String> { + let mut roles = BTreeSet::new(); + for block in blocks { + let invalid = || format!("invalid --microvm-sandbox-block {block}"); + let (role, rest) = block.split_once(':').ok_or_else(invalid)?; + let path = rest.strip_prefix("file:").ok_or_else(invalid)?; + let path = match role { + "distro" | "runtime" | "custom" => path.strip_suffix(",ro").ok_or_else(invalid)?, + "scratch" => path, + _ => return Err(invalid()), + }; + if !roles.insert(role) || !Path::new(path).is_file() { + return Err(invalid()); + } + } + if !roles.contains("scratch") || roles.len() < 2 { + return Err("a sandbox needs a layer and a scratch disk".to_owned()); + } + Ok(()) +} + +/// Returns the host directories of a sandbox's shares, for the checks of its policy paths, and +/// the guest targets of its directory shares: one `--mount TARGET,HOST,MODE` of a directory, or +/// the `--mount-child NAME,HOST,MODE[,file]` children of an aggregate at `/run/nvx/shares`. +fn sandbox_shares( + values: &BTreeMap<&str, Vec<&str>>, +) -> Result<(Vec, Vec), String> { + let mut roots = Vec::new(); + let mut targets = Vec::new(); + for share in values.get("--mount").into_iter().flatten() { + let invalid = || format!("invalid --mount {share}"); + let (target, rest) = share.split_once(',').ok_or_else(invalid)?; + let (host, mode) = rest.rsplit_once(',').ok_or_else(invalid)?; + if !Path::new(host).is_dir() || !["ro", "rw"].contains(&mode) { + return Err(invalid()); + } + roots.push(PathBuf::from(host)); + targets.push(target.to_owned()); + } + match values.get("--mount-aggregate").map(Vec::as_slice) { + None => {} + Some(["/run/nvx/shares"]) if roots.is_empty() => { + for child in values.get("--mount-child").into_iter().flatten() { + let invalid = || format!("invalid --mount-child {child}"); + let (_, rest) = child.split_once(',').ok_or_else(invalid)?; + let (rest, file) = match rest.strip_suffix(",file") { + Some(rest) => (rest, true), + None => (rest, false), + }; + let (host, mode) = rest.rsplit_once(',').ok_or_else(invalid)?; + let kind_matches = if file { + Path::new(host).is_file() + } else { + Path::new(host).is_dir() + }; + if !kind_matches || !["ro", "rw"].contains(&mode) { + return Err(invalid()); + } + roots.push(PathBuf::from(host)); + } + } + Some(_) => return Err("invalid --mount-aggregate".to_owned()), + } + if values.contains_key("--mount-child") && !values.contains_key("--mount-aggregate") { + return Err("--mount-child requires --mount-aggregate".to_owned()); + } + Ok((roots, targets)) +} + +/// Returns the host directories that the aggregate export of the host mapping table shares, in +/// child order, which `--mount-aggregate` and the `nvx_maps=` count of the kernel command line +/// must agree on. +fn mapped_children( + values: &BTreeMap<&str, Vec<&str>>, + maps: usize, +) -> Result, String> { + let children = match values.get("--mount-aggregate").map(Vec::as_slice) { + None if maps == 0 => Vec::new(), + Some([GUEST_EXPORT]) if maps > 0 => { + let mut children = Vec::new(); + for (index, child) in values + .get("--mount-child") + .into_iter() + .flatten() + .enumerate() + { + let invalid = || format!("invalid --mount-child {child}"); + let (name, rest) = child.split_once(',').ok_or_else(invalid)?; + // Like OpenVMM, a child exposes a directory or, with the `file` flag, a regular + // file, and refuses a host path of the other kind. + let (rest, file) = match rest.strip_suffix(",file") { + Some(rest) => (rest, true), + None => (rest, false), + }; + let (host, mode) = rest.rsplit_once(',').ok_or_else(invalid)?; + let kind_matches = if file { + Path::new(host).is_file() + } else { + Path::new(host).is_dir() + }; + if name != index.to_string() || !kind_matches || !["ro", "rw"].contains(&mode) { + return Err(invalid()); + } + children.push(PathBuf::from(host)); + } + if children.is_empty() { + return Err("--mount-aggregate requires --mount-child".to_owned()); + } + children + } + _ => { + return Err(format!("--mount-aggregate and {MAPS_TOKEN} must agree")); + } + }; + if children.is_empty() && values.contains_key("--mount-child") { + return Err("--mount-child requires --mount-aggregate".to_owned()); + } + Ok(children) +} + +/// Summarizes the network policy of a command line as OpenVMM's outcome report does. +fn network_summary(values: &BTreeMap<&str, Vec<&str>>) -> NetworkSummary { + let count = |name: &str| values.get(name).map_or(0, Vec::len); + let requested = [ + "--net", + "--network-profile", + "--network-egress", + "--network-ingress", + "--network-egress-allow", + "--network-egress-deny", + "--host-loopback", + "--network-proxy", + "--host-loopback-forward", + ] + .iter() + .any(|name| values.contains_key(name)); + let egress = values + .get("--network-egress") + .and_then(|values| values.last()); + let mode = if count("--network-egress-allow") + count("--network-egress-deny") > 0 { + "rules" + } else if egress == Some(&"deny") { + "deny-all" + } else if requested { + "allow-all" + } else { + "none" + }; + let host_loopback = if !requested { + "none" + } else if values + .get("--host-loopback") + .and_then(|values| values.last()) + == Some(&"deny") + { + "deny" + } else { + "allow" + }; + NetworkSummary { + requested, + mode, + allow: count("--network-egress-allow"), + deny: count("--network-egress-deny"), + host_loopback, + } +} + /// Reads exactly one nonzero control capability from standard input. fn read_capability() -> Result<[u8; CAPABILITY_LEN], String> { let mut stdin = io::stdin().lock(); @@ -525,7 +844,11 @@ impl Guest { Self { values: BTreeMap::new(), default_environment, - directories: ["/", "/tmp", "/work"].map(str::to_owned).into(), + directories: ["/", "/tmp", "/work"] + .into_iter() + .map(str::to_owned) + .chain(options.share_targets.iter().cloned()) + .collect(), files: BTreeSet::new(), mapped: Vec::new(), } @@ -1034,28 +1357,66 @@ fn serve_client( }; match session.run(options, guest) { Ok(Flow::Exit) => { - write_report(options); + write_report(options, "managed", "success", 0); Flow::Exit } _ => Flow::Continue, } } -/// Writes the successful synthetic OpenVMM outcome report if the destination is new. -fn write_report(options: &Options) { - let report = serde_json::json!({ - "schema_version": 1, - "backend": options.hypervisor, - "outcome": { "operation": "managed", "category": "success", "status_code": 0 }, - "network_policy": { "status": "not-requested" }, - "teardown": { "workers": true, "memory": true }, - }); - if let Ok(mut file) = OpenOptions::new() - .write(true) - .create_new(true) - .open(&options.report) - { - let _ = file.write_all(report.to_string().as_bytes()); +/// Writes the synthetic OpenVMM outcome report of `operation` if a report was requested and its +/// destination is new. +/// +/// A sandbox's report takes the form of OpenVMM's, with the instance, the network policy that the +/// command line requested, and every step of the teardown; a direct launch's keeps a shorter +/// form. +fn write_report(options: &Options, operation: &str, category: &str, status_code: i32) { + let Some(path) = &options.report else { + return; + }; + let report = if options.sandbox { + let mut instance = [0u8; 16]; + let _ = getrandom::fill(&mut instance); + let instance: String = instance.iter().map(|byte| format!("{byte:02x}")).collect(); + let network = &options.network; + let teardown = serde_json::json!({ + "guest_workload_stopped": true, + "vm_stopped": true, + "openvmm_process_terminated": true, + "virtiofs_released": true, + "network_released": true, + "temporary_storage_removed": true, + "control_channels_closed": true, + }); + let text = serde_json::to_string_pretty(&serde_json::json!({ + "schema_version": 1, + "instance_id": instance, + "backend": options.hypervisor, + "outcome": { "operation": operation, "category": category, "status_code": status_code }, + "network_policy": { + "status": if network.requested { "applied" } else { "not-requested" }, + "status_code": 0, + "mode": network.mode, + "allow_rule_count": network.allow, + "deny_rule_count": network.deny, + "host_loopback": network.host_loopback, + }, + "teardown": teardown, + })) + .unwrap_or_default(); + format!("{text}\n") + } else { + serde_json::json!({ + "schema_version": 1, + "backend": options.hypervisor, + "outcome": { "operation": operation, "category": category, "status_code": status_code }, + "network_policy": { "status": "not-requested" }, + "teardown": { "workers": true, "memory": true }, + }) + .to_string() + }; + if let Ok(mut file) = OpenOptions::new().write(true).create_new(true).open(path) { + let _ = file.write_all(report.as_bytes()); } } diff --git a/nvx-rs/tests/cli.rs b/nvx-rs/tests/cli.rs new file mode 100644 index 000000000..a2fbfb32a --- /dev/null +++ b/nvx-rs/tests/cli.rs @@ -0,0 +1,839 @@ +//! Replays the scenarios of `tests/fixtures/cli.json` with the `nvx-rs` binary, and checks what +//! `nvx-rs` adds to `nvx.py`. +//! +//! The retired `scripts/nvx_rs_fixtures.py` recorded the scenarios from the Python +//! implementation of `nvx.py sandbox`, which `nvx-rs` replaced, and from the Python functions +//! behind `nvx-rs egress compile` and `nvx-rs artifacts`, with `nvx-fake-openvmm` as OpenVMM, in a +//! section for POSIX hosts and one for Windows. Each scenario runs here in a fresh root, with +//! `nvx-rs --root ROOT`, and every step must exit with the recorded status, print the recorded +//! output, and leave the recorded files, after the root, process IDs, and random identifiers are +//! replaced with the placeholders that the script used. Now that `nvx.py sandbox` runs `nvx-rs`, +//! the recordings are its reference: a deliberate change of behavior updates them by hand. +#![cfg(all( + feature = "cli", + feature = "testing", + any(target_os = "linux", windows) +))] + +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output, Stdio}; +use std::thread; +use std::time::Duration; + +use serde_json::{Value, json}; + +/// The recorded scenarios. +const FIXTURE: &str = include_str!("fixtures/cli.json"); +/// The CLI under test. +const NVX_RS: &str = env!("CARGO_BIN_EXE_nvx-rs"); +/// The fake OpenVMM, which the root of every scenario holds as its OpenVMM executable. +const FAKE_OPENVMM: &str = env!("CARGO_BIN_EXE_nvx-fake-openvmm"); +/// The section of the fixture for this host. +const PLATFORM: &str = if cfg!(windows) { "windows" } else { "posix" }; +/// The files whose permissions the CLI sets, which the fixture records on POSIX hosts. +#[cfg(unix)] +const MODE_NAMES: [&str; 5] = [ + "state", + "config.json", + "runtime.json", + "control.capability", + "lifecycle.lock", +]; +/// How many scenarios run at once. +const PARALLELISM: usize = 4; + +/// A fresh scenario root, removed on drop after the fake OpenVMM processes that it records end. +struct Root { + /// The temporary directory. + _directory: tempfile::TempDir, + /// Its canonical path, as Python's `Path.resolve()` gives it. + path: PathBuf, +} + +impl Root { + fn new() -> Self { + let directory = tempfile::Builder::new() + .prefix("nvx-cli-") + .tempdir() + .unwrap(); + let path = canonical(directory.path()); + let text = path.to_str().expect("the temporary directory is Unicode"); + // The recorded commands quote no part of the root, as `shlex.join` or + // `subprocess.list2cmdline` would quote a path with spaces or other special characters. + let plain = if cfg!(windows) { + text.is_ascii() && !text.contains([' ', '\t', '"']) + } else { + text.chars() + .all(|c| c.is_ascii_alphanumeric() || "_@%+=:,./-".contains(c)) + }; + assert!( + plain, + "the golden CLI tests need a temporary directory without spaces or special \ + characters, not {text}; set TMPDIR or TEMP" + ); + Self { + _directory: directory, + path, + } + } + + fn text(&self) -> &str { + self.path.to_str().unwrap() + } + + /// Returns the path of `relative`, with `/` separators and placeholders, below the root. + fn below(&self, relative: &str) -> PathBuf { + self.expand(relative) + .split('/') + .fold(self.path.clone(), |path, part| path.join(part)) + } + + /// Replaces the `{root}` and `{exe}` placeholders of an input. + fn expand(&self, text: &str) -> String { + text.replace("{root}", self.text()) + .replace("{exe}", std::env::consts::EXE_SUFFIX) + } + + /// Replaces the root and the volatile values of an output with the fixture's placeholders. + fn normalize(&self, text: &str) -> String { + let raw = self.text(); + let escaped = python_json_escape(raw); + let mut text = text.to_owned(); + if escaped != raw { + text = text.replace(&escaped, "{root_json}"); + } + text = text.replace(raw, "{root}"); + for prefix in ["\"pid\": ", "\"start_time\": "] { + text = replace_after(&text, prefix, |c| c.is_ascii_digit(), None, "{int}"); + } + text = replace_after(&text, "openvmm-microvm-", is_hex, Some(32), "{hex32}"); + for prefix in ["\"operation_id\": \"", "\"instance_id\": \""] { + text = replace_after(&text, prefix, is_hex, Some(32), "{hex32}"); + } + text + } + + /// Creates the tree of the fixture. + fn populate(&self, tree: &[Value]) { + for entry in tree { + let path = self.below(entry["path"].as_str().unwrap()); + if entry.get("openvmm").is_some() { + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::copy(FAKE_OPENVMM, &path).unwrap(); + } else if let Some(text) = entry.get("text") { + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(&path, text.as_str().unwrap()).unwrap(); + } else { + fs::create_dir_all(&path).unwrap(); + } + } + } + + /// Returns what a step shows of a file or directory below the root, as the script records + /// it. + fn snapshot(&self, relative: &str) -> Value { + let path = self.below(relative); + let Ok(metadata) = fs::symlink_metadata(&path) else { + return Value::Null; + }; + let name = path.file_name().unwrap().to_str().unwrap(); + let mut entry = serde_json::Map::new(); + if metadata.is_dir() { + let mut names: Vec = fs::read_dir(&path) + .unwrap() + .map(|entry| entry.unwrap().file_name().into_string().unwrap()) + .collect(); + names.sort(); + entry.insert("entries".into(), json!(names)); + } else if name == "control.capability" { + entry.insert("size".into(), json!(metadata.len())); + } else { + let text = String::from_utf8(fs::read(&path).unwrap()).unwrap(); + entry.insert("text".into(), json!(self.normalize(&text))); + } + #[cfg(unix)] + if MODE_NAMES.contains(&name) || name.starts_with("exec") { + use std::os::unix::fs::PermissionsExt; + entry.insert("mode".into(), json!(metadata.permissions().mode() & 0o7777)); + } + Value::Object(entry) + } + + /// Ends the fake OpenVMM processes that the runtime records below the root name, so that the + /// root can be removed after a failure. + fn end_leftovers(&self) { + let mut pending = vec![self.path.clone()]; + while let Some(directory) = pending.pop() { + let Ok(entries) = fs::read_dir(&directory) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + pending.push(path); + } else if entry.file_name() == "runtime.json" + && let Some(pid) = fs::read_to_string(&path) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|record| record["pid"].as_u64()) + { + kill(pid); + } + } + } + } +} + +impl Drop for Root { + fn drop(&mut self) { + self.end_leftovers(); + } +} + +#[cfg(unix)] +fn kill(pid: u64) { + if let Ok(pid) = i32::try_from(pid) { + let _ = nix::sys::signal::kill( + nix::unistd::Pid::from_raw(pid), + nix::sys::signal::Signal::SIGKILL, + ); + thread::sleep(Duration::from_millis(200)); + } +} + +#[cfg(windows)] +fn kill(pid: u64) { + let _ = Command::new("taskkill") + .args(["/F", "/PID", &pid.to_string()]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + thread::sleep(Duration::from_millis(200)); +} + +/// Returns the canonical form of `path`, without the `\\?\` prefix that Windows adds and Python's +/// `Path.resolve()` drops. +fn canonical(path: &Path) -> PathBuf { + let path = fs::canonicalize(path).unwrap(); + let text = path.to_str().unwrap(); + match text.strip_prefix(r"\\?\") { + Some(rest) if !rest.starts_with("UNC\\") => PathBuf::from(rest), + _ => path, + } +} + +fn is_hex(c: char) -> bool { + c.is_ascii_digit() || ('a'..='f').contains(&c) +} + +/// Replaces each run of characters of `class` after `prefix`, of exactly `length` characters if +/// given, with `placeholder`. +fn replace_after( + text: &str, + prefix: &str, + class: fn(char) -> bool, + length: Option, + placeholder: &str, +) -> String { + let mut output = String::with_capacity(text.len()); + let mut rest = text; + while let Some(position) = rest.find(prefix) { + let start = position + prefix.len(); + output.push_str(&rest[..start]); + let tail = &rest[start..]; + let run = tail.find(|c| !class(c)).unwrap_or(tail.len()); + if run > 0 && length.is_none_or(|length| run == length) { + output.push_str(placeholder); + rest = &tail[run..]; + } else { + rest = tail; + } + } + output.push_str(rest); + output +} + +/// Returns `json.dumps(text)` without its quotes: `ensure_ascii` escapes. +fn python_json_escape(text: &str) -> String { + let mut output = String::new(); + for c in text.chars() { + match c { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + '\u{8}' => output.push_str("\\b"), + '\u{c}' => output.push_str("\\f"), + ' '..='~' => output.push(c), + _ => { + let mut units = [0u16; 2]; + for unit in c.encode_utf16(&mut units) { + output.push_str(&format!("\\u{unit:04x}")); + } + } + } + } + output +} + +/// Runs `nvx-rs --root ROOT ARGUMENTS` in the root, without the variables that change its +/// output. +fn nvx_rs(root: &Root, arguments: &[String], environment: &BTreeMap) -> Output { + Command::new(NVX_RS) + .arg("--root") + .arg(&root.path) + .args(arguments) + .current_dir(&root.path) + .env_remove("NVX_OPENVMM") + .env_remove("COLUMNS") + .env_remove("LINES") + .envs(environment) + .stdin(Stdio::null()) + .output() + .unwrap() +} + +/// Writes a file of a step: text, `hex` bytes, or `text` then `fill` repeated to `size` bytes. +fn write_file(path: &Path, content: &Value) { + let data = match content { + Value::String(text) => text.clone().into_bytes(), + Value::Object(spec) => match spec.get("hex") { + Some(hex) => (0..hex.as_str().unwrap().len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex.as_str().unwrap()[i..i + 2], 16).unwrap()) + .collect(), + None => { + let mut data = spec + .get("text") + .and_then(Value::as_str) + .unwrap_or_default() + .as_bytes() + .to_vec(); + let fill = spec["fill"].as_str().unwrap().as_bytes(); + let size = usize::try_from(spec["size"].as_u64().unwrap()).unwrap(); + while data.len() < size { + data.extend_from_slice(fill); + } + data + } + }, + other => panic!("invalid file content {other}"), + }; + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, data).unwrap(); +} + +/// Plays a scenario and returns how its steps differ from the recording. +fn play(tree: &[Value], scenario: &Value) -> Vec { + let name = scenario["name"].as_str().unwrap(); + let root = Root::new(); + root.populate(tree); + let mut problems = Vec::new(); + for (index, step) in scenario["steps"].as_array().unwrap().iter().enumerate() { + for relative in step + .get("remove") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + fs::remove_file(root.below(relative.as_str().unwrap())).unwrap(); + } + for (relative, content) in step + .get("write") + .and_then(Value::as_object) + .into_iter() + .flatten() + { + write_file(&root.below(relative), content); + } + let arguments: Vec = step["argv"] + .as_array() + .unwrap() + .iter() + .map(|argument| root.expand(argument.as_str().unwrap())) + .collect(); + let environment: BTreeMap = step + .get("env") + .and_then(Value::as_object) + .into_iter() + .flatten() + .map(|(name, value)| (name.clone(), root.expand(value.as_str().unwrap()))) + .collect(); + let output = nvx_rs(&root, &arguments, &environment); + let mut actual = json!({ + "status": output.status.code(), + "stdout": root.normalize(&String::from_utf8_lossy(&output.stdout)), + "stderr": root.normalize(&String::from_utf8_lossy(&output.stderr)), + }); + if let Some(checks) = step.get("check").and_then(Value::as_array) { + let files: serde_json::Map = checks + .iter() + .map(|relative| { + let relative = relative.as_str().unwrap(); + (relative.to_owned(), root.snapshot(relative)) + }) + .collect(); + actual["files"] = Value::Object(files); + } + for field in ["status", "stdout", "stderr", "files"] { + let expected = step.get(field).unwrap_or(&Value::Null); + let got = actual.get(field).unwrap_or(&Value::Null); + if expected != got { + problems.push(format!( + "{name}, step {} ({}), {field}:\n expected: {expected}\n actual: {got}", + index + 1, + arguments.join(" ") + )); + } + } + } + problems +} + +#[test] +fn nvx_rs_reproduces_the_recorded_scenarios() { + let fixture: Value = serde_json::from_str(FIXTURE).unwrap(); + let tree = fixture["tree"].as_array().unwrap().clone(); + let scenarios = fixture[PLATFORM].as_array().unwrap().clone(); + assert!( + !scenarios.is_empty(), + "tests/fixtures/cli.json has no {PLATFORM} scenarios" + ); + let chunks: Vec<&[Value]> = scenarios + .chunks(scenarios.len().div_ceil(PARALLELISM)) + .collect(); + let problems: Vec = thread::scope(|scope| { + let handles: Vec<_> = chunks + .iter() + .map(|chunk| { + let tree = &tree; + scope.spawn(move || { + chunk + .iter() + .flat_map(|scenario| play(tree, scenario)) + .collect::>() + }) + }) + .collect(); + handles + .into_iter() + .flat_map(|handle| handle.join().unwrap()) + .collect() + }); + assert!( + problems.is_empty(), + "{} differences from nvx.py:\n{}", + problems.len(), + problems.join("\n") + ); +} + +/// Returns the arguments that provision a minimal sandbox in `state` below the root. +fn provision(root: &Root, state: &str, command_line: &str) -> Vec { + let path = |name: &str| root.path.join(name).to_str().unwrap().to_owned(); + vec![ + "sandbox".into(), + "provision".into(), + "--state-dir".into(), + path(state), + "--layer".into(), + format!( + "distro,{},11111111-1111-1111-1111-111111111111", + path("distro.erofs") + ), + "--scratch".into(), + path("scratch.ext4"), + "--cmdline".into(), + command_line.into(), + ] +} + +/// Returns a root with the fixture's tree. +fn populated_root() -> Root { + let fixture: Value = serde_json::from_str(FIXTURE).unwrap(); + let root = Root::new(); + root.populate(fixture["tree"].as_array().unwrap()); + root +} + +fn run(root: &Root, arguments: &[String]) -> Output { + nvx_rs(root, arguments, &BTreeMap::new()) +} + +fn lifecycle(operation: &str, root: &Root, state: &str) -> Vec { + vec![ + "sandbox".into(), + operation.into(), + "--state-dir".into(), + root.path.join(state).to_str().unwrap().to_owned(), + ] +} + +fn text(data: &[u8]) -> String { + String::from_utf8_lossy(data).replace("\r\n", "\n") +} + +#[test] +fn start_requires_the_guest_features_of_layered_sandboxes() { + let root = populated_root(); + // A guest that lacks per-execution environments, or that predates feature negotiation, is + // stopped, and its start fails like any other. + for (state, command_line) in [ + ("cancel-only", "fake_guest_features=1"), + ("legacy", "fake_legacy_guest=1"), + ] { + let output = run(&root, &provision(&root, state, command_line)); + assert!(output.status.success(), "{}", text(&output.stderr)); + let output = run(&root, &lifecycle("start", &root, state)); + assert_eq!(output.status.code(), Some(1)); + assert_eq!( + text(&output.stderr), + "error: the guest image lacks control features that layered sandboxes need \ + (per-execution environments); use an NVX release or build that includes them\n" + ); + let mut names: Vec = fs::read_dir(root.path.join(state)) + .unwrap() + .map(|entry| entry.unwrap().file_name().into_string().unwrap()) + .collect(); + names.sort(); + assert_eq!(names, ["config.json", "lifecycle.lock", "openvmm.log"]); + let output = run(&root, &lifecycle("deprovision", &root, state)); + assert!(output.status.success(), "{}", text(&output.stderr)); + } + // Per-execution environments are all that the CLI needs: neither cancellation nor workload + // accounts. + let output = run( + &root, + &provision(&root, "environments", "fake_guest_features=16"), + ); + assert!(output.status.success(), "{}", text(&output.stderr)); + for operation in ["start", "stop", "deprovision"] { + let output = run(&root, &lifecycle(operation, &root, "environments")); + assert!( + output.status.success(), + "{operation}: {}", + text(&output.stderr) + ); + } +} + +#[test] +fn a_sandbox_of_a_retired_layer_role_does_not_start() { + // An older nvx.py provisioned sandboxes with runtime and custom layers, which a sandbox no + // longer takes (microsoft/nvx#556): their configuration still reads, and the launch refuses + // it before OpenVMM starts. + let root = populated_root(); + let output = run(&root, &provision(&root, "retired", "")); + assert!(output.status.success(), "{}", text(&output.stderr)); + let config = root.path.join("retired").join("config.json"); + let mut document: Value = serde_json::from_str(&fs::read_to_string(&config).unwrap()).unwrap(); + document["layers"] + .as_array_mut() + .unwrap() + .push(serde_json::json!({ + "path": root.path.join("runtime.erofs").to_str().unwrap(), + "role": "runtime", + "uuid": "22222222-2222-2222-2222-222222222222", + })); + fs::write(&config, serde_json::to_string_pretty(&document).unwrap()).unwrap(); + let output = run(&root, &lifecycle("start", &root, "retired")); + assert_eq!(output.status.code(), Some(1)); + assert_eq!( + text(&output.stderr), + "error: a sandbox requires exactly one read-only distro layer\n" + ); + assert!(!root.path.join("retired").join("runtime.json").exists()); + let output = run(&root, &lifecycle("deprovision", &root, "retired")); + assert!(output.status.success(), "{}", text(&output.stderr)); +} + +#[test] +fn artifacts_default_to_the_checkout_that_holds_the_executable() { + let output = Command::new(NVX_RS) + .arg("artifacts") + .env_remove("NVX_OPENVMM") + .output() + .unwrap(); + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let in_checkout = fs::canonicalize(manifest_dir.join("target")) + .is_ok_and(|target| canonical(Path::new(NVX_RS)).starts_with(canonical(&target))); + if in_checkout { + assert!(output.status.success(), "{}", text(&output.stderr)); + let paths: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(paths["layout"], "repository"); + assert_eq!( + canonical(Path::new(paths["root"].as_str().unwrap())), + canonical(manifest_dir.parent().unwrap()) + ); + } else { + // A build elsewhere, as with CARGO_TARGET_DIR, needs --root. + assert_eq!(output.status.code(), Some(1)); + assert!(text(&output.stderr).starts_with("error: cannot locate the NVX artifacts")); + } +} + +#[test] +fn a_relative_openvmm_override_is_relative_to_the_working_directory() { + let root = Root::new(); + let output = Command::new(NVX_RS) + .args(["--root", ".", "artifacts"]) + .current_dir(&root.path) + .env("NVX_OPENVMM", Path::new("alt").join("openvmm")) + .output() + .unwrap(); + assert!(output.status.success(), "{}", text(&output.stderr)); + let paths: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(paths["root"], root.text()); + assert_eq!( + paths["openvmm"], + root.path.join("alt").join("openvmm").to_str().unwrap() + ); +} + +#[test] +fn top_level_usage_errors_exit_with_status_2() { + let usage = "usage: nvx-rs [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\n"; + for (arguments, message) in [ + ( + &[][..], + "nvx-rs: error: the following arguments are required: command\n", + ), + ( + &["--root"][..], + "nvx-rs: error: argument --root: expected one argument\n", + ), + ( + &["build"][..], + "nvx-rs: error: argument command: invalid choice: 'build' (choose from \ + 'artifacts', 'egress', 'sandbox')\n", + ), + ] { + let output = Command::new(NVX_RS).args(arguments).output().unwrap(); + assert_eq!(output.status.code(), Some(2), "{arguments:?}"); + assert_eq!( + text(&output.stderr), + format!("{usage}{message}"), + "{arguments:?}" + ); + } + let output = Command::new(NVX_RS) + .args(["egress", "compile"]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(2)); + assert_eq!( + text(&output.stderr), + "usage: nvx-rs egress compile [-h] PATH\n\ + nvx-rs egress compile: error: the following arguments are required: PATH\n" + ); +} + +/// What `nvx.py sandbox` printed for a refused `--workload-user` before it ran `nvx-rs`, as +/// CPython 3.14 prints it for 80 columns: the usage of its parser, wrapped after its name. +const NVX_PY_SANDBOX_USAGE_ERROR: &str = "\ +usage: nvx.py sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH] + [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT] + [--arg SANDBOX_ARG] [--hostname HOSTNAME] + [--workload-user UID:GID] [--memory-max MEMORY_MAX] + [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB] + [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS] + [--cwd CWD] [--environment KEY=VALUE] + [--environment-file PATH] + [--inherit-default-environment] + [--hypervisor {auto,whp,kvm,mshv}] + [--mount GUEST_TARGET,HOST_PATH[,ro|rw]] + [--mount-deny HOST_PATH] [--mount-allow HOST_PATH] + [--mount-write HOST_PATH] [--mount-owner {vmm,caller}] + [--net IPV4/PREFIX] [--network-profile {portable}] + [--network-egress {allow,deny}] + [--network-ingress {allow,deny}] + [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]] + [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]] + [--network-egress-policy-file PATH] + [--host-loopback {allow,deny}] + [--network-proxy IPV4:TCP-PORT] + [--host-loopback-forward HOST_LOOPBACK_FORWARD] + [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE] + [--dry-run] + [{run,provision,start,exec,stop,deprovision}] +nvx.py sandbox: error: argument --workload-user: --workload-user UID must be between 1 and \ +4294967295 +"; + +#[test] +fn messages_name_the_program_that_prog_names() { + // nvx.py passes its own name, so that usage errors read as they did before it delegated. + let output = Command::new(NVX_RS) + .args(["--prog", "nvx.py", "sandbox", "--workload-user", "0:0"]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(2)); + assert_eq!(text(&output.stderr), NVX_PY_SANDBOX_USAGE_ERROR); + assert!(output.stdout.is_empty()); + // The usage wraps for the name as argparse wraps it: a long name takes a line of its own. + let long = "a-program-whose-name-is-long-enough-to-take-its-own-line.py"; + let output = Command::new(NVX_RS) + .args(["--prog", long, "egress", "compile"]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(2)); + assert_eq!( + text(&output.stderr), + format!( + "usage: {long} egress compile\n [-h] PATH\n\ + {long} egress compile: error: the following arguments are required: PATH\n" + ) + ); + // The top-level parser names it as soon as it has read it, and so does the help. + let output = Command::new(NVX_RS) + .args(["--prog=nvx.py", "build"]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(2)); + assert_eq!( + text(&output.stderr), + "usage: nvx.py [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\n\ + nvx.py: error: argument command: invalid choice: 'build' (choose from 'artifacts', \ + 'egress', 'sandbox')\n" + ); + let output = Command::new(NVX_RS) + .args(["--prog", "nvx.py", "artifacts", "--help"]) + .output() + .unwrap(); + assert!(output.status.success()); + assert!(text(&output.stdout).starts_with("usage: nvx.py artifacts [-h]\n\nPrint the paths")); +} + +#[cfg(unix)] +mod interrupts { + use std::time::Instant; + + use super::*; + + /// Sends SIGINT once OpenVMM has started, to the CLI alone or, as a terminal's Ctrl-C does, + /// to its whole process group, which a run's OpenVMM shares, and returns the CLI's output. + fn interrupt( + root: &Root, + arguments: &[String], + started: &Path, + group: bool, + ) -> (Output, Duration) { + use std::os::unix::process::CommandExt; + + let mut command = Command::new(NVX_RS); + command + .arg("--root") + .arg(&root.path) + .args(arguments) + .current_dir(&root.path) + .env_remove("NVX_OPENVMM") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if group { + command.process_group(0); + } + let child = command.spawn().unwrap(); + let deadline = Instant::now() + Duration::from_secs(30); + while !started.exists() && Instant::now() < deadline { + thread::sleep(Duration::from_millis(20)); + } + thread::sleep(Duration::from_millis(200)); + let interrupted = Instant::now(); + let pid = nix::unistd::Pid::from_raw(i32::try_from(child.id()).unwrap()); + if group { + nix::sys::signal::killpg(pid, nix::sys::signal::Signal::SIGINT).unwrap(); + } else { + nix::sys::signal::kill(pid, nix::sys::signal::Signal::SIGINT).unwrap(); + } + let output = child.wait_with_output().unwrap(); + (output, interrupted.elapsed()) + } + + /// Returns the arguments of a one-shot run of `/bin/sleep 60` in the fake OpenVMM, and the + /// file that the fake writes when it starts. + fn sleeping_run(root: &Root) -> (Vec, PathBuf) { + let path = |name: &str| root.path.join(name).to_str().unwrap().to_owned(); + let arguments: Vec = [ + "sandbox", + "run", + "--layer", + &format!( + "distro,{},11111111-1111-1111-1111-111111111111", + path("distro.erofs") + ), + "--scratch", + &path("scratch.ext4"), + "--entrypoint", + "/bin/sleep", + "--arg", + "60", + ] + .map(str::to_owned) + .to_vec(); + // The fake records its command line when it starts. + ( + arguments, + root.path.join("build").join("fake-openvmm-.json"), + ) + } + + #[test] + fn interrupting_a_run_kills_openvmm_and_exits_with_130() { + let root = populated_root(); + let (arguments, started) = sleeping_run(&root); + let (output, elapsed) = interrupt(&root, &arguments, &started, false); + assert_eq!(output.status.code(), Some(130)); + assert_eq!(text(&output.stderr), "Interrupted\n"); + assert!(text(&output.stdout).starts_with(">> ")); + assert!(elapsed < Duration::from_secs(10), "{elapsed:?}"); + } + + #[test] + fn interrupting_a_run_ends_it_as_interrupted_even_if_openvmm_exits_first() { + // A terminal's Ctrl-C reaches OpenVMM too, which the fake lets end it at once; Python's + // wait raises KeyboardInterrupt all the same, rather than return OpenVMM's status. + let root = populated_root(); + let (arguments, started) = sleeping_run(&root); + let (output, elapsed) = interrupt(&root, &arguments, &started, true); + assert_eq!(output.status.code(), Some(130)); + assert_eq!(text(&output.stderr), "Interrupted\n"); + assert!(elapsed < Duration::from_secs(10), "{elapsed:?}"); + } + + #[test] + fn interrupting_an_exec_leaves_the_sandbox_running() { + let root = populated_root(); + let output = run(&root, &provision(&root, "state", "")); + assert!(output.status.success(), "{}", text(&output.stderr)); + let output = run(&root, &lifecycle("start", &root, "state")); + assert!(output.status.success(), "{}", text(&output.stderr)); + let mut arguments = lifecycle("exec", &root, "state"); + arguments.extend( + [ + "--entrypoint", + "/bin/sh", + "--arg=-c", + "--arg", + "sleep 60000", + ] + .map(str::to_owned), + ); + let started = root.path.join("state").join("runtime.json"); + let (output, elapsed) = interrupt(&root, &arguments, &started, false); + assert_eq!(output.status.code(), Some(130)); + assert_eq!(text(&output.stderr), "Interrupted\n"); + assert!(elapsed < Duration::from_secs(10), "{elapsed:?}"); + for operation in ["stop", "deprovision"] { + let output = run(&root, &lifecycle(operation, &root, "state")); + assert!( + output.status.success(), + "{operation}: {}", + text(&output.stderr) + ); + } + } +} diff --git a/nvx-rs/tests/fixtures/cli.json b/nvx-rs/tests/fixtures/cli.json new file mode 100644 index 000000000..10187ab2d --- /dev/null +++ b/nvx-rs/tests/fixtures/cli.json @@ -0,0 +1,305 @@ +{ +"generator": "scripts/nvx_rs_fixtures.py, retired by microsoft/nvx#359", +"python": "3.14", +"tree": [ +{"path":"build/vmlinux","text":"kernel\n"}, +{"path":"build/initramfs.cpio.gz","text":"initramfs\n"}, +{"path":"distro.erofs","text":"distro\n"}, +{"path":"runtime.erofs","text":"runtime\n"}, +{"path":"custom.erofs","text":"custom\n"}, +{"path":"scratch.ext4","text":"scratch\n"}, +{"path":"share/file.txt","text":"file\n"}, +{"path":"share/sub/data.txt","text":"data\n"}, +{"path":"share/hidden/shown/data.txt","text":"data\n"}, +{"path":"share/writable/data.txt","text":"data\n"}, +{"path":"share2/data.txt","text":"data\n"}, +{"path":"single.txt","text":"single\n"}, +{"path":"reports"}, +{"path":"openvmm/target/release/openvmm{exe}","openvmm":true} +], +"posix": [ +{"name":"egress compile: empty","steps":[{"argv":["egress","compile","{root}/policy.json"],"write":{"policy.json":"{}"},"status":0,"stdout":"{\n \"allow\": [],\n \"deny\": []\n}\n","stderr":""}]}, +{"name":"egress compile: allow","steps":[{"argv":["egress","compile","{root}/policy.json"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\"}]}"},"status":0,"stdout":"{\n \"allow\": [\n \"192.0.2.0/24\"\n ],\n \"deny\": []\n}\n","stderr":""}]}, +{"name":"egress compile: both","steps":[{"argv":["egress","compile","{root}/policy.json"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.128/25\"]}, {\"to\": [{\"cidr\": \"2001:db8::/32\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 80, \"endPort\": 81}]}], \"deny\": [{\"cidr\": \"198.51.100.0/24\", \"protocol\": \"icmp\"}]}"},"status":0,"stdout":"{\n \"allow\": [\n \"192.0.2.0/25\",\n \"2001:db8::/32:tcp:80-81\"\n ],\n \"deny\": [\n \"198.51.100.0/24:icmp\"\n ]\n}\n","stderr":""}]}, +{"name":"egress compile: invalid JSON","steps":[{"argv":["egress","compile","{root}/policy.json"],"write":{"policy.json":"{"},"status":1,"stdout":"","stderr":"error: failed to read egress policy file: {root}/policy.json\n"}]}, +{"name":"egress compile: invalid rule","steps":[{"argv":["egress","compile","{root}/policy.json"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"port\": 70000}]}"},"status":1,"stdout":"","stderr":"error: allow[0].port requires protocol\n"}]}, +{"name":"egress compile: unknown field","steps":[{"argv":["egress","compile","{root}/policy.json"],"write":{"policy.json":"{\"allow\": [], \"other\": []}"},"status":1,"stdout":"","stderr":"error: egress policy has unknown field 'other'\n"}]}, +{"name":"egress compile: missing file","steps":[{"argv":["egress","compile","{root}/missing.json"],"status":1,"stdout":"","stderr":"error: failed to read egress policy file: {root}/missing.json\n"}]}, +{"name":"egress compile: directory","steps":[{"argv":["egress","compile","{root}/share"],"status":1,"stdout":"","stderr":"error: failed to read egress policy file: {root}/share\n"}]}, +{"name":"artifacts: checkout","steps":[{"argv":["artifacts"],"status":0,"stdout":"{\n \"guests\": [\n {\n \"distribution\": \"Alpine Linux\",\n \"distro_layer\": null,\n \"distro_manifest\": null,\n \"initramfs\": \"{root}/build/initramfs.cpio.gz\",\n \"name\": \"alpine\",\n \"package_manifest\": \"{root}/build/initramfs.cpio.gz.packages.json\"\n },\n {\n \"distribution\": \"Ubuntu Base\",\n \"distro_layer\": \"{root}/build/ubuntu-distro.erofs\",\n \"distro_manifest\": \"{root}/build/ubuntu-distro.erofs.manifest.json\",\n \"initramfs\": \"{root}/build/initramfs-ubuntu.cpio.gz\",\n \"name\": \"ubuntu\",\n \"package_manifest\": \"{root}/build/initramfs-ubuntu.cpio.gz.packages.json\"\n },\n {\n \"distribution\": \"Azure Linux\",\n \"distro_layer\": null,\n \"distro_manifest\": null,\n \"initramfs\": \"{root}/build/initramfs-azurelinux.cpio.gz\",\n \"name\": \"azurelinux\",\n \"package_manifest\": \"{root}/build/initramfs-azurelinux.cpio.gz.packages.json\"\n }\n ],\n \"kernel\": \"{root}/build/vmlinux\",\n \"layout\": \"repository\",\n \"openvmm\": \"{root}/openvmm/target/release/openvmm\",\n \"root\": \"{root}\"\n}\n","stderr":""}]}, +{"name":"artifacts: OpenVMM override","steps":[{"argv":["artifacts"],"env":{"NVX_OPENVMM":"{root}/alt/openvmm{exe}"},"status":0,"stdout":"{\n \"guests\": [\n {\n \"distribution\": \"Alpine Linux\",\n \"distro_layer\": null,\n \"distro_manifest\": null,\n \"initramfs\": \"{root}/build/initramfs.cpio.gz\",\n \"name\": \"alpine\",\n \"package_manifest\": \"{root}/build/initramfs.cpio.gz.packages.json\"\n },\n {\n \"distribution\": \"Ubuntu Base\",\n \"distro_layer\": \"{root}/build/ubuntu-distro.erofs\",\n \"distro_manifest\": \"{root}/build/ubuntu-distro.erofs.manifest.json\",\n \"initramfs\": \"{root}/build/initramfs-ubuntu.cpio.gz\",\n \"name\": \"ubuntu\",\n \"package_manifest\": \"{root}/build/initramfs-ubuntu.cpio.gz.packages.json\"\n },\n {\n \"distribution\": \"Azure Linux\",\n \"distro_layer\": null,\n \"distro_manifest\": null,\n \"initramfs\": \"{root}/build/initramfs-azurelinux.cpio.gz\",\n \"name\": \"azurelinux\",\n \"package_manifest\": \"{root}/build/initramfs-azurelinux.cpio.gz.packages.json\"\n }\n ],\n \"kernel\": \"{root}/build/vmlinux\",\n \"layout\": \"repository\",\n \"openvmm\": \"{root}/alt/openvmm\",\n \"root\": \"{root}\"\n}\n","stderr":""}]}, +{"name":"usage: help","steps":[{"argv":["sandbox","-h"],"status":0,"stdout":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\n\npositional arguments:\n {run,provision,start,exec,stop,deprovision}\n\noptions:\n -h, --help show this help message and exit\n --layer ROLE,PATH,EROFS_UUID\n --scratch SCRATCH\n --state-dir STATE_DIR\n --entrypoint ENTRYPOINT\n --arg SANDBOX_ARG\n --hostname HOSTNAME\n --workload-user UID:GID\n fixed non-root workload identity (default:\n 65534:65534)\n --memory-max MEMORY_MAX\n --pids-max PIDS_MAX\n --memory-mib MEMORY_MIB\n --timeout TIMEOUT control operation timeout, and the longest wait for\n another lifecycle transition of the sandbox, in\n seconds (default: 60)\n --exec-timeout-ms EXEC_TIMEOUT_MS\n guest workload timeout in milliseconds; zero disables\n it\n --cwd CWD absolute guest working directory for managed exec\n (default: /)\n --environment KEY=VALUE\n set the exact managed exec environment; repeat for\n multiple entries\n --environment-file PATH\n read the exact managed exec environment from a JSON\n string array\n --inherit-default-environment\n layer the managed exec environment over the guest\n default environment instead of replacing it\n --hypervisor {auto,whp,kvm,mshv}\n --mount GUEST_TARGET,HOST_PATH[,ro|rw]\n live-share a host directory, or a single regular host\n file without its directory, inside the container\n rootfs; the mode follows the last comma, so HOST_PATH\n may contain commas when the mode is given; repeat to\n attach more shares, each with its own target and mode,\n as children of one virtio-fs device\n --mount-deny HOST_PATH\n hide one existing path inside a --mount host\n directory; with several --mount options, it applies to\n the --mount before it\n --mount-allow HOST_PATH\n expose one existing path inside a --mount-deny path of\n the same share again; the hidden directories on the\n way list only it\n --mount-write HOST_PATH\n make one existing path one of the only writable parts\n of a read-write --mount; the rest of the share is\n read-only\n --mount-owner {vmm,caller}\n host identity of the share's file operations: vmm\n (default) or caller, the workload identity with guest\n root squashed to the owner of the shared directory or\n file (Linux only)\n --net IPV4/PREFIX\n --network-profile {portable}\n --network-egress {allow,deny}\n --network-ingress {allow,deny}\n --network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]\n allow guest egress to an IPv4 or IPv6 CIDR, optionally\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\n udp only on PORT or on the inclusive range PORT-PORT;\n repeat to add rules\n --network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]\n deny guest egress to an IPv4 or IPv6 CIDR, optionally\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\n udp only on PORT or on the inclusive range PORT-PORT;\n repeat to add rules\n --network-egress-policy-file PATH\n load bounded IPv4 and IPv6 ranges and rule-local\n exclusions for run/provision\n --host-loopback {allow,deny}\n --network-proxy IPV4:TCP-PORT\n --host-loopback-forward HOST_LOOPBACK_FORWARD\n --outcome-report OUTCOME_REPORT\n write a bounded local JSON outcome report for run or\n exec\n --cmdline CMDLINE\n --dry-run print the one-shot run command without launching it\n","stderr":""}]}, +{"name":"usage: help after operation","steps":[{"argv":["sandbox","exec","--help"],"status":0,"stdout":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\n\npositional arguments:\n {run,provision,start,exec,stop,deprovision}\n\noptions:\n -h, --help show this help message and exit\n --layer ROLE,PATH,EROFS_UUID\n --scratch SCRATCH\n --state-dir STATE_DIR\n --entrypoint ENTRYPOINT\n --arg SANDBOX_ARG\n --hostname HOSTNAME\n --workload-user UID:GID\n fixed non-root workload identity (default:\n 65534:65534)\n --memory-max MEMORY_MAX\n --pids-max PIDS_MAX\n --memory-mib MEMORY_MIB\n --timeout TIMEOUT control operation timeout, and the longest wait for\n another lifecycle transition of the sandbox, in\n seconds (default: 60)\n --exec-timeout-ms EXEC_TIMEOUT_MS\n guest workload timeout in milliseconds; zero disables\n it\n --cwd CWD absolute guest working directory for managed exec\n (default: /)\n --environment KEY=VALUE\n set the exact managed exec environment; repeat for\n multiple entries\n --environment-file PATH\n read the exact managed exec environment from a JSON\n string array\n --inherit-default-environment\n layer the managed exec environment over the guest\n default environment instead of replacing it\n --hypervisor {auto,whp,kvm,mshv}\n --mount GUEST_TARGET,HOST_PATH[,ro|rw]\n live-share a host directory, or a single regular host\n file without its directory, inside the container\n rootfs; the mode follows the last comma, so HOST_PATH\n may contain commas when the mode is given; repeat to\n attach more shares, each with its own target and mode,\n as children of one virtio-fs device\n --mount-deny HOST_PATH\n hide one existing path inside a --mount host\n directory; with several --mount options, it applies to\n the --mount before it\n --mount-allow HOST_PATH\n expose one existing path inside a --mount-deny path of\n the same share again; the hidden directories on the\n way list only it\n --mount-write HOST_PATH\n make one existing path one of the only writable parts\n of a read-write --mount; the rest of the share is\n read-only\n --mount-owner {vmm,caller}\n host identity of the share's file operations: vmm\n (default) or caller, the workload identity with guest\n root squashed to the owner of the shared directory or\n file (Linux only)\n --net IPV4/PREFIX\n --network-profile {portable}\n --network-egress {allow,deny}\n --network-ingress {allow,deny}\n --network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]\n allow guest egress to an IPv4 or IPv6 CIDR, optionally\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\n udp only on PORT or on the inclusive range PORT-PORT;\n repeat to add rules\n --network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]\n deny guest egress to an IPv4 or IPv6 CIDR, optionally\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\n udp only on PORT or on the inclusive range PORT-PORT;\n repeat to add rules\n --network-egress-policy-file PATH\n load bounded IPv4 and IPv6 ranges and rule-local\n exclusions for run/provision\n --host-loopback {allow,deny}\n --network-proxy IPV4:TCP-PORT\n --host-loopback-forward HOST_LOOPBACK_FORWARD\n --outcome-report OUTCOME_REPORT\n write a bounded local JSON outcome report for run or\n exec\n --cmdline CMDLINE\n --dry-run print the one-shot run command without launching it\n","stderr":""}]}, +{"name":"usage: timeout not a number","steps":[{"argv":["sandbox","--timeout","x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --timeout: invalid positive_float value: 'x'\n"}]}, +{"name":"usage: zero timeout","steps":[{"argv":["sandbox","--timeout","0"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\n"}]}, +{"name":"usage: negative timeout","steps":[{"argv":["sandbox","--timeout","-1"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\n"}]}, +{"name":"usage: infinite timeout","steps":[{"argv":["sandbox","--timeout","inf"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\n"}]}, +{"name":"usage: timeout not finite","steps":[{"argv":["sandbox","--timeout=1e400"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\n"}]}, +{"name":"usage: timeout not a number either","steps":[{"argv":["sandbox","--timeout","nan"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\n"}]}, +{"name":"usage: exec timeout not an integer","steps":[{"argv":["sandbox","--exec-timeout-ms","1.5"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --exec-timeout-ms: invalid int value: '1.5'\n"}]}, +{"name":"usage: memory not an integer","steps":[{"argv":["sandbox","--memory-mib","x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --memory-mib: invalid int value: 'x'\n"}]}, +{"name":"usage: invalid hypervisor","steps":[{"argv":["sandbox","--hypervisor","hyperv"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --hypervisor: invalid choice: 'hyperv' (choose from 'auto', 'whp', 'kvm', 'mshv')\n"}]}, +{"name":"usage: invalid operation","steps":[{"argv":["sandbox","restart"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument sandbox_operation: invalid choice: 'restart' (choose from 'run', 'provision', 'start', 'exec', 'stop', 'deprovision')\n"}]}, +{"name":"usage: second operation","steps":[{"argv":["sandbox","run","start"],"status":2,"stdout":"","stderr":"usage: nvx-rs [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\nnvx-rs: error: unrecognized arguments: start\n"}]}, +{"name":"usage: invalid mount owner","steps":[{"argv":["sandbox","--mount-owner","root"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --mount-owner: invalid choice: 'root' (choose from 'vmm', 'caller')\n"}]}, +{"name":"usage: invalid network profile","steps":[{"argv":["sandbox","--network-profile","open"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --network-profile: invalid choice: 'open' (choose from 'portable')\n"}]}, +{"name":"usage: invalid egress default","steps":[{"argv":["sandbox","--network-egress","block"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --network-egress: invalid choice: 'block' (choose from 'allow', 'deny')\n"}]}, +{"name":"usage: invalid host loopback","steps":[{"argv":["sandbox","--host-loopback","yes"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --host-loopback: invalid choice: 'yes' (choose from 'allow', 'deny')\n"}]}, +{"name":"usage: missing value","steps":[{"argv":["sandbox","--layer"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --layer: expected one argument\n"}]}, +{"name":"usage: option as value","steps":[{"argv":["sandbox","--layer","--scratch"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --layer: expected one argument\n"}]}, +{"name":"usage: dash value","steps":[{"argv":["sandbox","--arg","-x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --arg: expected one argument\n"}]}, +{"name":"usage: flag with value","steps":[{"argv":["sandbox","--dry-run=1"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --dry-run: ignored explicit argument '1'\n"}]}, +{"name":"usage: unrecognized option","steps":[{"argv":["sandbox","--bogus"],"status":2,"stdout":"","stderr":"usage: nvx-rs [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\nnvx-rs: error: unrecognized arguments: --bogus\n"}]}, +{"name":"usage: unrecognized short option","steps":[{"argv":["sandbox","-x"],"status":2,"stdout":"","stderr":"usage: nvx-rs [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\nnvx-rs: error: unrecognized arguments: -x\n"}]}, +{"name":"usage: unrecognized after separator","steps":[{"argv":["sandbox","run","--","extra"],"status":2,"stdout":"","stderr":"usage: nvx-rs [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\nnvx-rs: error: unrecognized arguments: extra\n"}]}, +{"name":"validation: dry run outside run","steps":[{"argv":["sandbox","provision","--dry-run"],"status":1,"stdout":"","stderr":"error: --dry-run is only valid for sandbox run\n"}]}, +{"name":"validation: environment and environment file","steps":[{"argv":["sandbox","exec","--environment","A=1","--environment-file","{root}/env.json"],"write":{"env.json":"[\"A=1\"]"},"status":1,"stdout":"","stderr":"error: --environment and --environment-file are mutually exclusive\n"}]}, +{"name":"validation: cwd outside exec","steps":[{"argv":["sandbox","run","--cwd","/"],"status":1,"stdout":"","stderr":"error: managed execution options require the sandbox exec operation\n"}]}, +{"name":"validation: environment outside exec","steps":[{"argv":["sandbox","start","--environment","A=1"],"status":1,"stdout":"","stderr":"error: managed execution options require the sandbox exec operation\n"}]}, +{"name":"validation: environment file outside exec","steps":[{"argv":["sandbox","provision","--environment-file","{root}/env.json"],"write":{"env.json":"[\"A=1\"]"},"status":1,"stdout":"","stderr":"error: managed execution options require the sandbox exec operation\n"}]}, +{"name":"validation: inherited environment outside exec","steps":[{"argv":["sandbox","stop","--inherit-default-environment"],"status":1,"stdout":"","stderr":"error: managed execution options require the sandbox exec operation\n"}]}, +{"name":"validation: environment file too large","steps":[{"argv":["sandbox","exec","--environment-file","{root}/env.json"],"write":{"env.json":{"fill":" ","size":1048577}},"status":1,"stdout":"","stderr":"error: managed execution environment file exceeds 1048576-byte limit\n"}]}, +{"name":"validation: environment file at the limit","steps":[{"argv":["sandbox","exec","--environment-file","{root}/env.json"],"write":{"env.json":{"text":"[\"A=1\"]","fill":" ","size":1048576}},"status":1,"stdout":"","stderr":"error: sandbox exec requires --state-dir\n"}]}, +{"name":"validation: environment file missing","steps":[{"argv":["sandbox","exec","--environment-file","{root}/missing.json"],"status":1,"stdout":"","stderr":"error: failed to read managed execution environment: {root}/missing.json\n"}]}, +{"name":"validation: environment file directory","steps":[{"argv":["sandbox","exec","--environment-file","{root}/share"],"status":1,"stdout":"","stderr":"error: failed to read managed execution environment: {root}/share\n"}]}, +{"name":"validation: environment file not UTF-8","steps":[{"argv":["sandbox","exec","--environment-file","{root}/env.json"],"write":{"env.json":{"hex":"5b22ff225d"}},"status":1,"stdout":"","stderr":"error: failed to read managed execution environment: {root}/env.json\n"}]}, +{"name":"validation: environment file not JSON","steps":[{"argv":["sandbox","exec","--environment-file","{root}/env.json"],"write":{"env.json":"[\"A=1\""},"status":1,"stdout":"","stderr":"error: failed to read managed execution environment: {root}/env.json\n"}]}, +{"name":"validation: environment file object","steps":[{"argv":["sandbox","exec","--environment-file","{root}/env.json"],"write":{"env.json":"{\"A\": \"1\"}"},"status":1,"stdout":"","stderr":"error: managed execution environment file must contain a JSON array of KEY=VALUE strings\n"}]}, +{"name":"validation: environment file non-string","steps":[{"argv":["sandbox","exec","--environment-file","{root}/env.json"],"write":{"env.json":"[\"A=1\", 2]"},"status":1,"stdout":"","stderr":"error: managed execution environment file must contain a JSON array of KEY=VALUE strings\n"}]}, +{"name":"validation: environment file entry without equals","steps":[{"argv":["sandbox","exec","--environment-file","{root}/env.json"],"write":{"env.json":"[\"A\"]"},"status":1,"stdout":"","stderr":"error: managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes\n"}]}, +{"name":"validation: environment file entry with NUL","steps":[{"argv":["sandbox","exec","--environment-file","{root}/env.json"],"write":{"env.json":"[\"A=\\u0000\"]"},"status":1,"stdout":"","stderr":"error: managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes\n"}]}, +{"name":"validation: environment file duplicate entry","steps":[{"argv":["sandbox","exec","--environment-file","{root}/env.json"],"write":{"env.json":"[\"A=1\", \"A=2\"]"},"status":1,"stdout":"","stderr":"error: managed exec environment contains duplicate key: A\n"}]}, +{"name":"validation: environment entry without name","steps":[{"argv":["sandbox","exec","--environment","=x"],"status":1,"stdout":"","stderr":"error: managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes\n"}]}, +{"name":"validation: environment entry with space","steps":[{"argv":["sandbox","exec","--environment","A B=x"],"status":1,"stdout":"","stderr":"error: sandbox exec requires --state-dir\n"}]}, +{"name":"validation: network outside run","steps":[{"argv":["sandbox","start","--net","10.0.0.2/24"],"status":1,"stdout":"","stderr":"error: network policy options are only valid for sandbox run or provision\n"}]}, +{"name":"validation: egress rule outside run","steps":[{"argv":["sandbox","exec","--network-egress-allow","10.0.0.0/8"],"status":1,"stdout":"","stderr":"error: network policy options are only valid for sandbox run or provision\n"}]}, +{"name":"validation: forward outside run","steps":[{"argv":["sandbox","stop","--host-loopback-forward","80"],"status":1,"stdout":"","stderr":"error: network policy options are only valid for sandbox run or provision\n"}]}, +{"name":"validation: policy file outside run","steps":[{"argv":["sandbox","deprovision","--network-egress-policy-file","{root}/policy.json"],"status":1,"stdout":"","stderr":"error: network policy options are only valid for sandbox run or provision\n"}]}, +{"name":"validation: systemd entrypoint of run","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/usr/lib/systemd/systemd"],"status":1,"stdout":"","stderr":"error: systemd entrypoints are unsupported by the sandbox security profile\n"}]}, +{"name":"validation: systemd entrypoint of exec","steps":[{"argv":["sandbox","exec","--entrypoint","/lib/systemd/systemd"],"status":1,"stdout":"","stderr":"error: systemd entrypoints are unsupported by the sandbox security profile\n"}]}, +{"name":"validation: outcome report outside run","steps":[{"argv":["sandbox","provision","--outcome-report","{root}/reports/exec.json"],"status":1,"stdout":"","stderr":"error: --outcome-report is only valid for one-shot run or managed exec\n"}]}, +{"name":"validation: deny without mount","steps":[{"argv":["sandbox","run","--mount-deny","{root}/share/hidden"],"status":1,"stdout":"","stderr":"error: --mount-deny requires --mount\n"}]}, +{"name":"validation: allow without mount","steps":[{"argv":["sandbox","run","--mount-allow","{root}/share/hidden"],"status":1,"stdout":"","stderr":"error: --mount-allow requires --mount\n"}]}, +{"name":"validation: write without mount","steps":[{"argv":["sandbox","run","--mount-write","{root}/share/writable"],"status":1,"stdout":"","stderr":"error: --mount-write requires --mount\n"}]}, +{"name":"validation: owner without mount","steps":[{"argv":["sandbox","run","--mount-owner","vmm"],"status":1,"stdout":"","stderr":"error: --mount-owner requires --mount\n"}]}, +{"name":"validation: mount outside run","steps":[{"argv":["sandbox","start","--mount","/data,{root}/share"],"status":1,"stdout":"","stderr":"error: --mount is only valid for sandbox run or provision\n"}]}, +{"name":"validation: net without profile","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--net","10.0.0.2/24"],"status":1,"stdout":"","stderr":"error: --net and --network-profile must be specified together\n"}]}, +{"name":"validation: profile without net","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--network-profile","portable"],"status":1,"stdout":"","stderr":"error: --net and --network-profile must be specified together\n"}]}, +{"name":"validation: run without layers","steps":[{"argv":["sandbox","run"],"status":1,"stdout":"","stderr":"error: sandbox run requires --layer and --scratch\n"}]}, +{"name":"validation: provision without scratch","steps":[{"argv":["sandbox","provision","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111"],"status":1,"stdout":"","stderr":"error: sandbox provision requires --layer and --scratch\n"}]}, +{"name":"validation: run without layer","steps":[{"argv":["sandbox","run","--scratch","{root}/scratch.ext4"],"status":1,"stdout":"","stderr":"error: sandbox run requires --layer and --scratch\n"}]}, +{"name":"validation: policy file with rules","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--network-egress","allow","--network-egress-policy-file","{root}/policy.json","--network-egress-deny","10.0.0.0/8"],"status":1,"stdout":"","stderr":"error: --network-egress-policy-file cannot be combined with --network-egress-allow or --network-egress-deny\n"}]}, +{"name":"validation: policy file without egress","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--network-egress-policy-file","{root}/policy.json"],"status":1,"stdout":"","stderr":"error: --network-egress is required with --network-egress-policy-file\n"}]}, +{"name":"validation: policy file missing","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--network-egress","deny","--network-egress-policy-file","{root}/missing.json"],"status":1,"stdout":"","stderr":"error: failed to read egress policy file: {root}/missing.json\n"}]}, +{"name":"validation: policy file invalid","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--network-egress","deny","--network-egress-policy-file","{root}/policy.json"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"10.0.0.0/8\", \"port\": 0}]}"},"status":1,"stdout":"","stderr":"error: allow[0].port requires protocol\n"}]}, +{"name":"validation: provision without state directory","steps":[{"argv":["sandbox","provision","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":1,"stdout":"","stderr":"error: sandbox provision requires --state-dir\n"}]}, +{"name":"validation: start without state directory","steps":[{"argv":["sandbox","start"],"status":1,"stdout":"","stderr":"error: sandbox start requires --state-dir\n"}]}, +{"name":"validation: exec without state directory","steps":[{"argv":["sandbox","exec"],"status":1,"stdout":"","stderr":"error: sandbox exec requires --state-dir\n"}]}, +{"name":"validation: stop without state directory","steps":[{"argv":["sandbox","stop"],"status":1,"stdout":"","stderr":"error: sandbox stop requires --state-dir\n"}]}, +{"name":"validation: deprovision without state directory","steps":[{"argv":["sandbox","deprovision"],"status":1,"stdout":"","stderr":"error: sandbox deprovision requires --state-dir\n"}]}, +{"name":"validation: run with state directory","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: one-shot sandbox execution rejects persistent --state-dir settings\n"}]}, +{"name":"validation: invalid layer UUID","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,nope","--scratch","{root}/scratch.ext4"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --layer: distro layer UUID is invalid: 'nope'\n"}]}, +{"name":"validation: invalid layer role","steps":[{"argv":["sandbox","run","--layer","base,{root}/distro.erofs,11111111-1111-1111-1111-111111111111"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --layer: unsupported layer role 'base'; choose distro\n"}]}, +{"name":"validation: retired layer role","steps":[{"argv":["sandbox","run","--layer","runtime,{root}/runtime.erofs,22222222-2222-2222-2222-222222222222"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --layer: unsupported layer role 'runtime'; choose distro\n"}]}, +{"name":"validation: missing layer","steps":[{"argv":["sandbox","run","--layer","distro,{root}/missing.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":1,"stdout":"","stderr":"error: distro EROFS layer not found: {root}/missing.erofs\n"}]}, +{"name":"validation: duplicate layer","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":1,"stdout":"","stderr":"error: duplicate sandbox layer role(s): distro\n"}]}, +{"name":"validation: missing scratch","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/missing.ext4"],"status":1,"stdout":"","stderr":"error: ext4 scratch image not found: {root}/missing.ext4\n"}]}, +{"name":"validation: relative entrypoint","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","bin/sh"],"status":1,"stdout":"","stderr":"error: sandbox entrypoint must be an absolute path without spaces\n"}]}, +{"name":"validation: invalid hostname","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--hostname","bad host"],"status":1,"stdout":"","stderr":"error: sandbox hostname must be a lowercase RFC 1123 label up to 63 characters\n"}]}, +{"name":"validation: argument with space","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--arg","a b"],"status":1,"stdout":"","stderr":"error: sandbox arguments must be nonempty and contain no spaces\n"}]}, +{"name":"validation: root workload","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--workload-user","0:0"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --workload-user: --workload-user UID must be between 1 and 4294967295\n"}]}, +{"name":"validation: invalid workload user","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--workload-user","x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --workload-user: --workload-user must be UID:GID\n"}]}, +{"name":"validation: invalid memory limit","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--memory-max","x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\n [--cwd CWD] [--environment KEY=VALUE]\n [--environment-file PATH]\n [--inherit-default-environment]\n [--hypervisor {auto,whp,kvm,mshv}]\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\n [--net IPV4/PREFIX] [--network-profile {portable}]\n [--network-egress {allow,deny}]\n [--network-ingress {allow,deny}]\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\n [--network-egress-policy-file PATH]\n [--host-loopback {allow,deny}]\n [--network-proxy IPV4:TCP-PORT]\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\n [--dry-run]\n [{run,provision,start,exec,stop,deprovision}]\nnvx-rs sandbox: error: argument --memory-max: invalid int value: 'x'\n"}]}, +{"name":"validation: invalid process limit","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--pids-max","0"],"status":1,"stdout":"","stderr":"error: --pids-max must be positive\n"}]}, +{"name":"validation: relative mount target","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","data,{root}/share"],"status":1,"stdout":"","stderr":"error: invalid sandbox mount target 'data': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','\n"}]}, +{"name":"validation: missing mount host","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/missing"],"status":1,"stdout":"","stderr":"error: sandbox mount host path is not a plain directory: {root}/missing\n"}]}, +{"name":"validation: invalid mount mode","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rx"],"status":1,"stdout":"","stderr":"error: unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)\n"}]}, +{"name":"validation: deny outside share","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--mount-deny","{root}/share2"],"status":2,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --mount /data,{root}/share,ro --mount-deny {root}/share2 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox'\n","stderr":"openvmm: invalid --mount-deny {root}/share2\n"}]}, +{"name":"validation: allow outside denied path","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--mount-allow","{root}/share/hidden/shown"],"status":1,"stdout":"","stderr":"error: --mount-allow exposes a path inside a --mount-deny path of the same share, which has none\n"}]}, +{"name":"validation: write in read-only share","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,ro","--mount-write","{root}/share/writable"],"status":1,"stdout":"","stderr":"error: --mount-write narrows the writes of a read-write share; --mount /data is read-only\n"}]}, +{"name":"validation: reserved command-line token","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--cmdline","nvx_sandbox=0"],"status":1,"stdout":"","stderr":"error: nvx_sandbox is owned by the sandbox profile\n"}]}, +{"name":"validation: systemd distro image","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"write":{"distro.erofs.manifest.json":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"8a475efd5fd711d481a9a19de6d12b88dff0cd81cb8135ffcd5b65b062c7bf04\", \"packages\": [{\"name\": \"busybox\", \"version\": \"1\"}, {\"name\": \"systemd\", \"version\": \"1\"}]}"},"status":1,"stdout":"","stderr":"error: systemd images are unsupported by the sandbox security profile\n"}]}, +{"name":"validation: mismatched distro manifest","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"write":{"distro.erofs.manifest.json":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0000000000000000000000000000000000000000000000000000000000000000\", \"packages\": []}"},"status":1,"stdout":"","stderr":"error: sandbox distro manifest does not match its artifact\n"}]}, +{"name":"validation: invalid distro manifest","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"write":{"distro.erofs.manifest.json":"{"},"status":1,"stdout":"","stderr":"error: invalid sandbox distro manifest: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)\n"}]}, +{"name":"validation: distro manifest without packages","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"write":{"distro.erofs.manifest.json":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"8a475efd5fd711d481a9a19de6d12b88dff0cd81cb8135ffcd5b65b062c7bf04\"}"},"status":1,"stdout":"","stderr":"error: sandbox distro manifest has invalid package metadata\n"}]}, +{"name":"dry run: minimal","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox'\n","stderr":""}]}, +{"name":"dry run: every layer and option","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/server","--arg=--port","--arg","8080","--hostname","worker","--workload-user","1000:1000","--memory-max","268435456","--pids-max","64","--memory-mib","512","--hypervisor","mshv","--cmdline","quiet loglevel=3","--outcome-report","{root}/reports/openvmm-run.json","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 1000:1000 --microvm-lifecycle one-shot --single-process --hypervisor mshv --memory 512M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'quiet loglevel=3 nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/server nvx_hostname=worker nvx_arg=--port nvx_arg=8080 nvx_memory_max=268435456 nvx_pids_max=65' --microvm-report {root}/reports/openvmm-run.json\n","stderr":""}]}, +{"name":"dry run: explicit hypervisor","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--hypervisor","kvm","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox'\n","stderr":""}]}, +{"name":"dry run: one share","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --mount /data,{root}/share,ro --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox'\n","stderr":""}]}, +{"name":"dry run: writable share","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rw","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --mount /data,{root}/share,rw --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox'\n","stderr":""}]}, +{"name":"dry run: file share","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/etc/single.txt,{root}/single.txt,ro","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --mount-aggregate /run/nvx/shares --mount-child 0-a80a05e9cb129bc27b7ba103bf2f5ac0,{root}/single.txt,ro,file --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-a80a05e9cb129bc27b7ba103bf2f5ac0,/etc/single.txt,ro,file'\n","stderr":""}]}, +{"name":"dry run: several shares with policies","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rw","--mount-deny","{root}/share/hidden","--mount-allow","{root}/share/hidden/shown","--mount-write","{root}/share/writable","--mount","/other,{root}/share2","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --mount-aggregate /run/nvx/shares --mount-child 0-bd47413b5c03dfc6e4b5b8143fe205ea,{root}/share,rw --mount-child 1-bf2faee2596026620b5cbc997c2d7d45,{root}/share2,ro --mount-deny {root}/share/hidden --mount-allow {root}/share/hidden/shown --mount-write {root}/share/writable --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-bd47413b5c03dfc6e4b5b8143fe205ea,/data,rw nvx_share=1-bf2faee2596026620b5cbc997c2d7d45,/other,ro'\n","stderr":""}]}, +{"name":"dry run: network","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--net","10.0.0.2/24","--network-profile","portable","--network-egress","deny","--network-ingress","allow","--network-egress-allow","10.0.0.0/8:tcp:443","--network-egress-deny","10.1.0.0/16","--host-loopback","allow","--network-proxy","10.0.0.1:3128","--host-loopback-forward","8080","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox' --net 10.0.0.2/24 --network-profile portable --network-egress deny --network-ingress allow --network-egress-allow 10.0.0.0/8:tcp:443 --network-egress-deny 10.1.0.0/16 --host-loopback allow --network-proxy 10.0.0.1:3128 --host-loopback-forward 8080\n","stderr":""}]}, +{"name":"dry run: egress policy file","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--network-egress","deny","--network-egress-policy-file","{root}/policy.json","--dry-run"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.128/25\"]}, {\"cidr\": \"2001:db8::/32\", \"protocol\": \"tcp\", \"port\": 443}], \"deny\": [{\"cidr\": \"198.51.100.0/24\", \"protocol\": \"udp\"}]}"},"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox' --network-egress deny --network-egress-allow 192.0.2.0/25 --network-egress-allow 2001:db8::/32:tcp:443 --network-egress-deny 198.51.100.0/24:udp\n","stderr":""}]}, +{"name":"dry run: quoted arguments","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--cmdline","a=\"b c\" d","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'a=\"b c\" d nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox'\n","stderr":""}]}, +{"name":"dry run: caller-owned share","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--mount-owner","caller","--dry-run"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --mount /data,{root}/share,ro --mount-owner caller --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox'\n","stderr":""}]}, +{"name":"run: echo","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/echo","--arg","hello","--arg","world"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/echo nvx_hostname=nvx-sandbox nvx_arg=hello nvx_arg=world'\nhello world\n","stderr":""}]}, +{"name":"run: default operation","steps":[{"argv":["sandbox","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/echo","--arg","hello","--arg","world"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/echo nvx_hostname=nvx-sandbox nvx_arg=hello nvx_arg=world'\nhello world\n","stderr":""}]}, +{"name":"run: failure","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/false"],"status":1,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/false nvx_hostname=nvx-sandbox'\n","stderr":""}]}, +{"name":"run: missing program","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/missing"],"status":127,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/missing nvx_hostname=nvx-sandbox'\n","stderr":"openvmm: /bin/missing: not found\n"}]}, +{"name":"run: exit status","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/true","--cmdline","fake_exit_status=7"],"status":7,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'fake_exit_status=7 nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/true nvx_hostname=nvx-sandbox'\n","stderr":""}]}, +{"name":"run: outcome report","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/echo","--arg","hello","--arg","world","--outcome-report","{root}/reports/openvmm-run.json"],"check":["reports/openvmm-run.json","build/fake-openvmm-reports.json"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/echo nvx_hostname=nvx-sandbox nvx_arg=hello nvx_arg=world' --microvm-report {root}/reports/openvmm-run.json\nhello world\n","stderr":"","files":{"reports/openvmm-run.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"run\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"build/fake-openvmm-reports.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root}/distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root}/scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--microvm-lifecycle\",\"one-shot\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/echo nvx_hostname=nvx-sandbox nvx_arg=hello nvx_arg=world\",\"--microvm-report\",\"{root}/reports/openvmm-run.json\"]"}}}]}, +{"name":"run: network outcome report","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/true","--net","10.0.0.2/24","--network-profile","portable","--network-egress","deny","--network-egress-allow","10.0.0.0/8:tcp:443","--outcome-report","{root}/reports/openvmm-run.json"],"check":["reports/openvmm-run.json"],"status":0,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/true nvx_hostname=nvx-sandbox' --net 10.0.0.2/24 --network-profile portable --network-egress deny --network-egress-allow 10.0.0.0/8:tcp:443 --microvm-report {root}/reports/openvmm-run.json\n","stderr":"","files":{"reports/openvmm-run.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"run\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"}}}]}, +{"name":"run: OpenVMM refuses its options","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--host-loopback-forward","8080"],"status":2,"stdout":">> {root}/openvmm/target/release/openvmm --machine microvm --microvm-sandbox-block distro:file:{root}/distro.erofs,ro --microvm-sandbox-block scratch:file:{root}/scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}/build/vmlinux --initrd {root}/build/initramfs.cpio.gz --cmdline 'nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox' --host-loopback-forward 8080\n","stderr":"openvmm: --host-loopback-forward requires explicit --host-loopback allow\n"}]}, +{"name":"run: missing OpenVMM","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"remove":["openvmm/target/release/openvmm{exe}"],"status":1,"stdout":"","stderr":"error: OpenVMM release binary not found: {root}/openvmm/target/release/openvmm\n"}]}, +{"name":"run: missing kernel","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"remove":["build/vmlinux"],"status":1,"stdout":"","stderr":"error: Linux direct kernel not found: {root}/build/vmlinux\n"}]}, +{"name":"run: missing initramfs","steps":[{"argv":["sandbox","run","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"remove":["build/initramfs.cpio.gz"],"status":1,"stdout":"","stderr":"error: initramfs not found: {root}/build/initramfs.cpio.gz\n"}]}, +{"name":"lifecycle: minimal","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 1,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 1,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root}/distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root}/scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/echo","--arg","hello","--arg","world"],"status":0,"stdout":"hello world\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/tmp"],"status":0,"stdout":"/tmp\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/sh","--arg=-c","--arg","echo out; echoerr err; exit 3","--outcome-report","{root}/reports/exec.json"],"check":["reports/exec.json"],"status":3,"stdout":"out\n","stderr":"err\n","files":{"reports/exec.json":{"text":"{\n \"operation_id\": \"{hex32}\",\n \"outcome\": {\n \"category\": \"exit\",\n \"operation\": \"exec\",\n \"status_code\": 3\n },\n \"schema_version\": 1\n}\n","mode":384}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/usr/bin/env","--environment","A=1","--environment","B=two"],"status":0,"stdout":"A=1\nB=two\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/usr/bin/printenv","--arg","PATH","--inherit-default-environment","--environment","A=1"],"status":0,"stdout":"/usr/sbin:/usr/bin:/sbin:/bin\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/usr/bin/env","--environment-file","{root}/env.json"],"write":{"env.json":"[\"X=1\", \"Y=two words\"]"},"status":0,"stdout":"X=1\nY=two words\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/usr/bin/env","--environment-file","{root}/empty.json"],"write":{"empty.json":"[]"},"status":0,"stdout":"","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/sh","--arg=-c","--arg","sleep 5000","--exec-timeout-ms","50","--outcome-report","{root}/reports/exec-timeout.json"],"check":["reports/exec-timeout.json"],"status":124,"stdout":"","stderr":"","files":{"reports/exec-timeout.json":{"text":"{\n \"operation_id\": \"{hex32}\",\n \"outcome\": {\n \"category\": \"timeout\",\n \"operation\": \"exec\",\n \"status_code\": 124\n },\n \"schema_version\": 1\n}\n","mode":384}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/sh","--arg=-c","--arg","signal 9"],"status":137,"stdout":"","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/missing"],"status":127,"stdout":"","stderr":"openvmm: /bin/missing: not found\n"},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/missing"],"status":1,"stdout":"","stderr":"error: managed guest rejected exec (status=2, category=cwd-failed)\n"},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","relative"],"status":1,"stdout":"","stderr":"error: managed exec working directory must be an absolute path of at most 4096 bytes\n"},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/echo","--outcome-report","{root}/reports/exec.json"],"status":1,"stdout":"","stderr":"error: outcome report already exists: {root}/reports/exec.json\n"},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/echo","--outcome-report","{root}/missing/exec.json"],"status":1,"stdout":"","stderr":"error: outcome report parent is not a plain directory: {root}/missing\n"},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 1,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 1,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 1,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: one share","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 2,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 2,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root}/distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root}/scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root}/share,ro\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/data"],"status":0,"stdout":"/data\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 2,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 2,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 2,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: caller-owned share","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rw","--mount-owner","caller"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 3,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 3,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root}/distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root}/scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root}/share,rw\",\"--mount-owner\",\"caller\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 3,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 3,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 3,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"caller\"\n },\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: several shares","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share","--mount","/other,{root}/share2,rw"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 4,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 4,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root}/distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root}/scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-bd47413b5c03dfc6e4b5b8143fe205ea,{root}/share,ro\",\"--mount-child\",\"1-bf2faee2596026620b5cbc997c2d7d45,{root}/share2,rw\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-bd47413b5c03dfc6e4b5b8143fe205ea,/data,ro nvx_share=1-bf2faee2596026620b5cbc997c2d7d45,/other,rw\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}/state","--entrypoint","/bin/pwd","--cwd","/other"],"status":0,"stdout":"/other\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 4,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 4,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 4,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"denied_paths\": [],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\"\n },\n {\n \"access\": \"rw\",\n \"denied_paths\": [],\n \"guest_target\": \"/other\",\n \"host_path\": \"{root}/share2\",\n \"owner\": \"vmm\"\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: share policies","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/data,{root}/share,rw","--mount-deny","{root}/share/hidden","--mount-allow","{root}/share/hidden/shown","--mount-write","{root}/share/writable"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 5,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 5,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root}/distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root}/scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root}/share,rw\",\"--mount-deny\",\"{root}/share/hidden\",\"--mount-allow\",\"{root}/share/hidden/shown\",\"--mount-write\",\"{root}/share/writable\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 5,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 5,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 5,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"rw\",\n \"allowed_paths\": [\n \"{root}/share/hidden/shown\"\n ],\n \"denied_paths\": [\n \"{root}/share/hidden\"\n ],\n \"guest_target\": \"/data\",\n \"host_path\": \"{root}/share\",\n \"owner\": \"vmm\",\n \"writable_paths\": [\n \"{root}/share/writable\"\n ]\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: file share","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--mount","/etc/single.txt,{root}/single.txt,ro"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 6,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 6,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root}/distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root}/scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-a80a05e9cb129bc27b7ba103bf2f5ac0,{root}/single.txt,ro,file\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"256M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-a80a05e9cb129bc27b7ba103bf2f5ac0,/etc/single.txt,ro,file\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 6,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 6,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 6,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mounts\": [\n {\n \"access\": \"ro\",\n \"allowed_paths\": [],\n \"denied_paths\": [],\n \"guest_target\": \"/etc/single.txt\",\n \"host_path\": \"{root}/single.txt\",\n \"kind\": \"file\",\n \"owner\": \"vmm\",\n \"writable_paths\": []\n }\n ],\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: network and launch options","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--entrypoint","/bin/server","--arg=--serve","--hostname","worker","--workload-user","1000:1000","--memory-max","268435456","--pids-max","64","--memory-mib","384","--hypervisor","auto","--cmdline","quiet","--net","10.0.0.2/24","--network-profile","portable","--network-egress","deny","--network-ingress","deny","--network-egress-policy-file","{root}/policy.json","--host-loopback","allow","--network-proxy","10.0.0.1:3128","--host-loopback-forward","8080","--host-loopback-forward","9090"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 443}], \"deny\": [{\"cidr\": \"192.0.2.128/25\"}]}"},"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"quiet\",\n \"format\": 1,\n \"host_loopback\": \"allow\",\n \"host_loopback_forward\": [\n \"8080\",\n \"9090\"\n ],\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"net\": \"10.0.0.2/24\",\n \"network_egress\": \"deny\",\n \"network_egress_allow\": [\n \"192.0.2.0/24:tcp:443\"\n ],\n \"network_egress_deny\": [\n \"192.0.2.128/25\"\n ],\n \"network_ingress\": \"deny\",\n \"network_profile\": \"portable\",\n \"network_proxy\": \"10.0.0.1:3128\",\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"quiet\",\n \"format\": 1,\n \"host_loopback\": \"allow\",\n \"host_loopback_forward\": [\n \"8080\",\n \"9090\"\n ],\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"net\": \"10.0.0.2/24\",\n \"network_egress\": \"deny\",\n \"network_egress_allow\": [\n \"192.0.2.0/24:tcp:443\"\n ],\n \"network_egress_deny\": [\n \"192.0.2.128/25\"\n ],\n \"network_ingress\": \"deny\",\n \"network_profile\": \"portable\",\n \"network_proxy\": \"10.0.0.1:3128\",\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root}/distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root}/scratch.ext4\",\"--microvm-workload-identity\",\"1000:1000\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"kvm\",\"--memory\",\"384M\",\"--kernel\",\"{root}/build/vmlinux\",\"--initrd\",\"{root}/build/initramfs.cpio.gz\",\"--cmdline\",\"quiet nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=worker nvx_memory_max=268435456 nvx_pids_max=65\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen={root}/state/control.sock\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root}/state/outcome.json\",\"--net\",\"10.0.0.2/24\",\"--network-profile\",\"portable\",\"--network-egress\",\"deny\",\"--network-ingress\",\"deny\",\"--host-loopback\",\"allow\",\"--network-egress-allow\",\"192.0.2.0/24:tcp:443\",\"--network-egress-deny\",\"192.0.2.128/25\",\"--host-loopback-forward\",\"8080\",\"--host-loopback-forward\",\"9090\",\"--network-proxy\",\"10.0.0.1:3128\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"quiet\",\n \"format\": 1,\n \"host_loopback\": \"allow\",\n \"host_loopback_forward\": [\n \"8080\",\n \"9090\"\n ],\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"net\": \"10.0.0.2/24\",\n \"network_egress\": \"deny\",\n \"network_egress_allow\": [\n \"192.0.2.0/24:tcp:443\"\n ],\n \"network_egress_deny\": [\n \"192.0.2.128/25\"\n ],\n \"network_ingress\": \"deny\",\n \"network_profile\": \"portable\",\n \"network_proxy\": \"10.0.0.1:3128\",\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 1,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","control.sock","lifecycle.lock","openvmm.log","runtime.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"quiet\",\n \"format\": 1,\n \"host_loopback\": \"allow\",\n \"host_loopback_forward\": [\n \"8080\",\n \"9090\"\n ],\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"net\": \"10.0.0.2/24\",\n \"network_egress\": \"deny\",\n \"network_egress_allow\": [\n \"192.0.2.0/24:tcp:443\"\n ],\n \"network_egress_deny\": [\n \"192.0.2.128/25\"\n ],\n \"network_ingress\": \"deny\",\n \"network_profile\": \"portable\",\n \"network_proxy\": \"10.0.0.1:3128\",\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n","mode":384},"state/runtime.json":{"text":"{\n \"control_endpoint\": \"{root}/state/control.sock\",\n \"format\": 1,\n \"pid\": {int},\n \"start_time\": {int}\n}\n","mode":384},"state/control.capability":{"size":32,"mode":384},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}/state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"quiet\",\n \"format\": 1,\n \"host_loopback\": \"allow\",\n \"host_loopback_forward\": [\n \"8080\",\n \"9090\"\n ],\n \"hostname\": \"worker\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": 268435456,\n \"memory_mib\": 384,\n \"mount\": null,\n \"net\": \"10.0.0.2/24\",\n \"network_egress\": \"deny\",\n \"network_egress_allow\": [\n \"192.0.2.0/24:tcp:443\"\n ],\n \"network_egress_deny\": [\n \"192.0.2.128/25\"\n ],\n \"network_ingress\": \"deny\",\n \"network_profile\": \"portable\",\n \"network_proxy\": \"10.0.0.1:3128\",\n \"pids_max\": 64,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 1000,\n \"workload_uid\": 1000\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"kvm\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 1,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"state: operations without a sandbox","steps":[{"argv":["sandbox","start","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/state\n"},{"argv":["sandbox","exec","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/state\n"},{"argv":["sandbox","stop","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/state\n"},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/state\n"}]}, +{"name":"state: refusals of a running sandbox","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":1,"stdout":"","stderr":"error: sandbox is already provisioned\n"},{"argv":["sandbox","start","--state-dir","{root}/state"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox is already running or has stale runtime state\n"},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox must be stopped before deprovision\n"},{"argv":["sandbox","stop","--state-dir","{root}/state"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox is not running\n"},{"argv":["sandbox","exec","--state-dir","{root}/state"],"status":1,"stdout":"","stderr":"error: sandbox is not running\n"},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"state: relative and nested state directory","steps":[{"argv":["sandbox","provision","--state-dir","nested/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"check":["nested/state","nested/state/config.json"],"status":0,"stdout":"","stderr":"","files":{"nested/state":{"entries":["config.json","lifecycle.lock"],"mode":448},"nested/state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 1,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384}}},{"argv":["sandbox","deprovision","--state-dir","nested/state"],"check":["nested/state"],"status":0,"stdout":"","stderr":"","files":{"nested/state":null}}]}, +{"name":"state: failed start","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4","--cmdline","fake_exit_on_start=5"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}/state","--timeout","0.5"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM exited during startup with status 5\n","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"fake_exit_on_start=5\",\n \"format\": 1,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":{"text":"openvmm: failing the launch as requested\n"}}},{"argv":["sandbox","start","--state-dir","{root}/state","--timeout","0.5"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM exited during startup with status 5\n","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"fake_exit_on_start=5\",\n \"format\": 1,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":{"text":"openvmm: failing the launch as requested\nopenvmm: failing the launch as requested\n"}}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"state: start without OpenVMM","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}/state"],"remove":["openvmm/target/release/openvmm{exe}"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM release binary not found: {root}/openvmm/target/release/openvmm\n","files":{"state":{"entries":["config.json","lifecycle.lock"],"mode":448},"state/config.json":{"text":"{\n \"cmdline\": \"\",\n \"format\": 1,\n \"host_loopback\": null,\n \"host_loopback_forward\": [],\n \"hostname\": \"nvx-sandbox\",\n \"hypervisor\": \"kvm\",\n \"layers\": [\n {\n \"path\": \"{root}/distro.erofs\",\n \"role\": \"distro\",\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\n }\n ],\n \"memory_max\": null,\n \"memory_mib\": 256,\n \"mount\": null,\n \"net\": null,\n \"network_egress\": null,\n \"network_egress_allow\": [],\n \"network_egress_deny\": [],\n \"network_ingress\": null,\n \"network_profile\": null,\n \"network_proxy\": null,\n \"pids_max\": null,\n \"scratch\": \"{root}/scratch.ext4\",\n \"workload_gid\": 65534,\n \"workload_uid\": 65534\n}\n","mode":384},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"status":0,"stdout":"","stderr":""}]}, +{"name":"state: malformed configuration","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}/state"],"write":{"state/config.json":"{\"format\": 7}"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}/state/config.json\n"},{"argv":["sandbox","start","--state-dir","{root}/state"],"write":{"state/config.json":"[]"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}/state/config.json\n"},{"argv":["sandbox","start","--state-dir","{root}/state"],"write":{"state/config.json":"{\"format\": 1}"},"status":1,"stdout":"","stderr":"error: sandbox configuration is malformed\n"},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}]}, +{"name":"state: not a directory","steps":[{"argv":["sandbox","start","--state-dir","{root}/single.txt"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/single.txt\n"},{"argv":["sandbox","provision","--state-dir","{root}/single.txt","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":1,"stdout":"","stderr":"error: [Errno 17] File exists: '{root}/single.txt'\n"},{"argv":["sandbox","deprovision","--state-dir","{root}/single.txt"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}/single.txt\n"}]}, +{"name":"state: foreign file","steps":[{"argv":["sandbox","provision","--state-dir","{root}/state","--layer","distro,{root}/distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}/scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","deprovision","--state-dir","{root}/state"],"write":{"state/foreign.txt":"x"},"check":["state"],"status":1,"stdout":"","stderr":"error: sandbox state directory contains files not owned by NVX: foreign.txt\n","files":{"state":{"entries":["foreign.txt"],"mode":448}}}]} +], +"windows": [ +{"name":"egress compile: empty","steps":[{"argv":["egress","compile","{root}\\policy.json"],"write":{"policy.json":"{}"},"status":0,"stdout":"{\r\n \"allow\": [],\r\n \"deny\": []\r\n}\r\n","stderr":""}]}, +{"name":"egress compile: allow","steps":[{"argv":["egress","compile","{root}\\policy.json"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\"}]}"},"status":0,"stdout":"{\r\n \"allow\": [\r\n \"192.0.2.0/24\"\r\n ],\r\n \"deny\": []\r\n}\r\n","stderr":""}]}, +{"name":"egress compile: both","steps":[{"argv":["egress","compile","{root}\\policy.json"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.128/25\"]}, {\"to\": [{\"cidr\": \"2001:db8::/32\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 80, \"endPort\": 81}]}], \"deny\": [{\"cidr\": \"198.51.100.0/24\", \"protocol\": \"icmp\"}]}"},"status":0,"stdout":"{\r\n \"allow\": [\r\n \"192.0.2.0/25\",\r\n \"2001:db8::/32:tcp:80-81\"\r\n ],\r\n \"deny\": [\r\n \"198.51.100.0/24:icmp\"\r\n ]\r\n}\r\n","stderr":""}]}, +{"name":"egress compile: invalid JSON","steps":[{"argv":["egress","compile","{root}\\policy.json"],"write":{"policy.json":"{"},"status":1,"stdout":"","stderr":"error: failed to read egress policy file: {root}\\policy.json\r\n"}]}, +{"name":"egress compile: invalid rule","steps":[{"argv":["egress","compile","{root}\\policy.json"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"port\": 70000}]}"},"status":1,"stdout":"","stderr":"error: allow[0].port requires protocol\r\n"}]}, +{"name":"egress compile: unknown field","steps":[{"argv":["egress","compile","{root}\\policy.json"],"write":{"policy.json":"{\"allow\": [], \"other\": []}"},"status":1,"stdout":"","stderr":"error: egress policy has unknown field 'other'\r\n"}]}, +{"name":"egress compile: missing file","steps":[{"argv":["egress","compile","{root}\\missing.json"],"status":1,"stdout":"","stderr":"error: failed to read egress policy file: {root}\\missing.json\r\n"}]}, +{"name":"egress compile: directory","steps":[{"argv":["egress","compile","{root}\\share"],"status":1,"stdout":"","stderr":"error: failed to read egress policy file: {root}\\share\r\n"}]}, +{"name":"artifacts: checkout","steps":[{"argv":["artifacts"],"status":0,"stdout":"{\r\n \"guests\": [\r\n {\r\n \"distribution\": \"Alpine Linux\",\r\n \"distro_layer\": null,\r\n \"distro_manifest\": null,\r\n \"initramfs\": \"{root_json}\\\\build\\\\initramfs.cpio.gz\",\r\n \"name\": \"alpine\",\r\n \"package_manifest\": \"{root_json}\\\\build\\\\initramfs.cpio.gz.packages.json\"\r\n },\r\n {\r\n \"distribution\": \"Ubuntu Base\",\r\n \"distro_layer\": \"{root_json}\\\\build\\\\ubuntu-distro.erofs\",\r\n \"distro_manifest\": \"{root_json}\\\\build\\\\ubuntu-distro.erofs.manifest.json\",\r\n \"initramfs\": \"{root_json}\\\\build\\\\initramfs-ubuntu.cpio.gz\",\r\n \"name\": \"ubuntu\",\r\n \"package_manifest\": \"{root_json}\\\\build\\\\initramfs-ubuntu.cpio.gz.packages.json\"\r\n },\r\n {\r\n \"distribution\": \"Azure Linux\",\r\n \"distro_layer\": null,\r\n \"distro_manifest\": null,\r\n \"initramfs\": \"{root_json}\\\\build\\\\initramfs-azurelinux.cpio.gz\",\r\n \"name\": \"azurelinux\",\r\n \"package_manifest\": \"{root_json}\\\\build\\\\initramfs-azurelinux.cpio.gz.packages.json\"\r\n }\r\n ],\r\n \"kernel\": \"{root_json}\\\\build\\\\vmlinux\",\r\n \"layout\": \"repository\",\r\n \"openvmm\": \"{root_json}\\\\openvmm\\\\target\\\\release\\\\openvmm.exe\",\r\n \"root\": \"{root_json}\"\r\n}\r\n","stderr":""}]}, +{"name":"artifacts: OpenVMM override","steps":[{"argv":["artifacts"],"env":{"NVX_OPENVMM":"{root}\\alt\\openvmm{exe}"},"status":0,"stdout":"{\r\n \"guests\": [\r\n {\r\n \"distribution\": \"Alpine Linux\",\r\n \"distro_layer\": null,\r\n \"distro_manifest\": null,\r\n \"initramfs\": \"{root_json}\\\\build\\\\initramfs.cpio.gz\",\r\n \"name\": \"alpine\",\r\n \"package_manifest\": \"{root_json}\\\\build\\\\initramfs.cpio.gz.packages.json\"\r\n },\r\n {\r\n \"distribution\": \"Ubuntu Base\",\r\n \"distro_layer\": \"{root_json}\\\\build\\\\ubuntu-distro.erofs\",\r\n \"distro_manifest\": \"{root_json}\\\\build\\\\ubuntu-distro.erofs.manifest.json\",\r\n \"initramfs\": \"{root_json}\\\\build\\\\initramfs-ubuntu.cpio.gz\",\r\n \"name\": \"ubuntu\",\r\n \"package_manifest\": \"{root_json}\\\\build\\\\initramfs-ubuntu.cpio.gz.packages.json\"\r\n },\r\n {\r\n \"distribution\": \"Azure Linux\",\r\n \"distro_layer\": null,\r\n \"distro_manifest\": null,\r\n \"initramfs\": \"{root_json}\\\\build\\\\initramfs-azurelinux.cpio.gz\",\r\n \"name\": \"azurelinux\",\r\n \"package_manifest\": \"{root_json}\\\\build\\\\initramfs-azurelinux.cpio.gz.packages.json\"\r\n }\r\n ],\r\n \"kernel\": \"{root_json}\\\\build\\\\vmlinux\",\r\n \"layout\": \"repository\",\r\n \"openvmm\": \"{root_json}\\\\alt\\\\openvmm.exe\",\r\n \"root\": \"{root_json}\"\r\n}\r\n","stderr":""}]}, +{"name":"usage: help","steps":[{"argv":["sandbox","-h"],"status":0,"stdout":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\n\r\npositional arguments:\r\n {run,provision,start,exec,stop,deprovision}\r\n\r\noptions:\r\n -h, --help show this help message and exit\r\n --layer ROLE,PATH,EROFS_UUID\r\n --scratch SCRATCH\r\n --state-dir STATE_DIR\r\n --entrypoint ENTRYPOINT\r\n --arg SANDBOX_ARG\r\n --hostname HOSTNAME\r\n --workload-user UID:GID\r\n fixed non-root workload identity (default:\r\n 65534:65534)\r\n --memory-max MEMORY_MAX\r\n --pids-max PIDS_MAX\r\n --memory-mib MEMORY_MIB\r\n --timeout TIMEOUT control operation timeout, and the longest wait for\r\n another lifecycle transition of the sandbox, in\r\n seconds (default: 60)\r\n --exec-timeout-ms EXEC_TIMEOUT_MS\r\n guest workload timeout in milliseconds; zero disables\r\n it\r\n --cwd CWD absolute guest working directory for managed exec\r\n (default: /)\r\n --environment KEY=VALUE\r\n set the exact managed exec environment; repeat for\r\n multiple entries\r\n --environment-file PATH\r\n read the exact managed exec environment from a JSON\r\n string array\r\n --inherit-default-environment\r\n layer the managed exec environment over the guest\r\n default environment instead of replacing it\r\n --hypervisor {auto,whp,kvm,mshv}\r\n --mount GUEST_TARGET,HOST_PATH[,ro|rw]\r\n live-share a host directory, or a single regular host\r\n file without its directory, inside the container\r\n rootfs; the mode follows the last comma, so HOST_PATH\r\n may contain commas when the mode is given; repeat to\r\n attach more shares, each with its own target and mode,\r\n as children of one virtio-fs device\r\n --mount-deny HOST_PATH\r\n hide one existing path inside a --mount host\r\n directory; with several --mount options, it applies to\r\n the --mount before it\r\n --mount-allow HOST_PATH\r\n expose one existing path inside a --mount-deny path of\r\n the same share again; the hidden directories on the\r\n way list only it\r\n --mount-write HOST_PATH\r\n make one existing path one of the only writable parts\r\n of a read-write --mount; the rest of the share is\r\n read-only\r\n --mount-owner {vmm,caller}\r\n host identity of the share's file operations: vmm\r\n (default) or caller, the workload identity with guest\r\n root squashed to the owner of the shared directory or\r\n file (Linux only)\r\n --net IPV4/PREFIX\r\n --network-profile {portable}\r\n --network-egress {allow,deny}\r\n --network-ingress {allow,deny}\r\n --network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]\r\n allow guest egress to an IPv4 or IPv6 CIDR, optionally\r\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\r\n udp only on PORT or on the inclusive range PORT-PORT;\r\n repeat to add rules\r\n --network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]\r\n deny guest egress to an IPv4 or IPv6 CIDR, optionally\r\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\r\n udp only on PORT or on the inclusive range PORT-PORT;\r\n repeat to add rules\r\n --network-egress-policy-file PATH\r\n load bounded IPv4 and IPv6 ranges and rule-local\r\n exclusions for run/provision\r\n --host-loopback {allow,deny}\r\n --network-proxy IPV4:TCP-PORT\r\n --host-loopback-forward HOST_LOOPBACK_FORWARD\r\n --outcome-report OUTCOME_REPORT\r\n write a bounded local JSON outcome report for run or\r\n exec\r\n --cmdline CMDLINE\r\n --dry-run print the one-shot run command without launching it\r\n","stderr":""}]}, +{"name":"usage: help after operation","steps":[{"argv":["sandbox","exec","--help"],"status":0,"stdout":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\n\r\npositional arguments:\r\n {run,provision,start,exec,stop,deprovision}\r\n\r\noptions:\r\n -h, --help show this help message and exit\r\n --layer ROLE,PATH,EROFS_UUID\r\n --scratch SCRATCH\r\n --state-dir STATE_DIR\r\n --entrypoint ENTRYPOINT\r\n --arg SANDBOX_ARG\r\n --hostname HOSTNAME\r\n --workload-user UID:GID\r\n fixed non-root workload identity (default:\r\n 65534:65534)\r\n --memory-max MEMORY_MAX\r\n --pids-max PIDS_MAX\r\n --memory-mib MEMORY_MIB\r\n --timeout TIMEOUT control operation timeout, and the longest wait for\r\n another lifecycle transition of the sandbox, in\r\n seconds (default: 60)\r\n --exec-timeout-ms EXEC_TIMEOUT_MS\r\n guest workload timeout in milliseconds; zero disables\r\n it\r\n --cwd CWD absolute guest working directory for managed exec\r\n (default: /)\r\n --environment KEY=VALUE\r\n set the exact managed exec environment; repeat for\r\n multiple entries\r\n --environment-file PATH\r\n read the exact managed exec environment from a JSON\r\n string array\r\n --inherit-default-environment\r\n layer the managed exec environment over the guest\r\n default environment instead of replacing it\r\n --hypervisor {auto,whp,kvm,mshv}\r\n --mount GUEST_TARGET,HOST_PATH[,ro|rw]\r\n live-share a host directory, or a single regular host\r\n file without its directory, inside the container\r\n rootfs; the mode follows the last comma, so HOST_PATH\r\n may contain commas when the mode is given; repeat to\r\n attach more shares, each with its own target and mode,\r\n as children of one virtio-fs device\r\n --mount-deny HOST_PATH\r\n hide one existing path inside a --mount host\r\n directory; with several --mount options, it applies to\r\n the --mount before it\r\n --mount-allow HOST_PATH\r\n expose one existing path inside a --mount-deny path of\r\n the same share again; the hidden directories on the\r\n way list only it\r\n --mount-write HOST_PATH\r\n make one existing path one of the only writable parts\r\n of a read-write --mount; the rest of the share is\r\n read-only\r\n --mount-owner {vmm,caller}\r\n host identity of the share's file operations: vmm\r\n (default) or caller, the workload identity with guest\r\n root squashed to the owner of the shared directory or\r\n file (Linux only)\r\n --net IPV4/PREFIX\r\n --network-profile {portable}\r\n --network-egress {allow,deny}\r\n --network-ingress {allow,deny}\r\n --network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]\r\n allow guest egress to an IPv4 or IPv6 CIDR, optionally\r\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\r\n udp only on PORT or on the inclusive range PORT-PORT;\r\n repeat to add rules\r\n --network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]\r\n deny guest egress to an IPv4 or IPv6 CIDR, optionally\r\n only for PROTOCOL tcp, udp, or icmp, and for tcp or\r\n udp only on PORT or on the inclusive range PORT-PORT;\r\n repeat to add rules\r\n --network-egress-policy-file PATH\r\n load bounded IPv4 and IPv6 ranges and rule-local\r\n exclusions for run/provision\r\n --host-loopback {allow,deny}\r\n --network-proxy IPV4:TCP-PORT\r\n --host-loopback-forward HOST_LOOPBACK_FORWARD\r\n --outcome-report OUTCOME_REPORT\r\n write a bounded local JSON outcome report for run or\r\n exec\r\n --cmdline CMDLINE\r\n --dry-run print the one-shot run command without launching it\r\n","stderr":""}]}, +{"name":"usage: timeout not a number","steps":[{"argv":["sandbox","--timeout","x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --timeout: invalid positive_float value: 'x'\r\n"}]}, +{"name":"usage: zero timeout","steps":[{"argv":["sandbox","--timeout","0"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\r\n"}]}, +{"name":"usage: negative timeout","steps":[{"argv":["sandbox","--timeout","-1"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\r\n"}]}, +{"name":"usage: infinite timeout","steps":[{"argv":["sandbox","--timeout","inf"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\r\n"}]}, +{"name":"usage: timeout not finite","steps":[{"argv":["sandbox","--timeout=1e400"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\r\n"}]}, +{"name":"usage: timeout not a number either","steps":[{"argv":["sandbox","--timeout","nan"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --timeout: must be finite and greater than 0\r\n"}]}, +{"name":"usage: exec timeout not an integer","steps":[{"argv":["sandbox","--exec-timeout-ms","1.5"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --exec-timeout-ms: invalid int value: '1.5'\r\n"}]}, +{"name":"usage: memory not an integer","steps":[{"argv":["sandbox","--memory-mib","x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --memory-mib: invalid int value: 'x'\r\n"}]}, +{"name":"usage: invalid hypervisor","steps":[{"argv":["sandbox","--hypervisor","hyperv"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --hypervisor: invalid choice: 'hyperv' (choose from 'auto', 'whp', 'kvm', 'mshv')\r\n"}]}, +{"name":"usage: invalid operation","steps":[{"argv":["sandbox","restart"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument sandbox_operation: invalid choice: 'restart' (choose from 'run', 'provision', 'start', 'exec', 'stop', 'deprovision')\r\n"}]}, +{"name":"usage: second operation","steps":[{"argv":["sandbox","run","start"],"status":2,"stdout":"","stderr":"usage: nvx-rs [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\r\nnvx-rs: error: unrecognized arguments: start\r\n"}]}, +{"name":"usage: invalid mount owner","steps":[{"argv":["sandbox","--mount-owner","root"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --mount-owner: invalid choice: 'root' (choose from 'vmm', 'caller')\r\n"}]}, +{"name":"usage: invalid network profile","steps":[{"argv":["sandbox","--network-profile","open"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --network-profile: invalid choice: 'open' (choose from 'portable')\r\n"}]}, +{"name":"usage: invalid egress default","steps":[{"argv":["sandbox","--network-egress","block"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --network-egress: invalid choice: 'block' (choose from 'allow', 'deny')\r\n"}]}, +{"name":"usage: invalid host loopback","steps":[{"argv":["sandbox","--host-loopback","yes"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --host-loopback: invalid choice: 'yes' (choose from 'allow', 'deny')\r\n"}]}, +{"name":"usage: missing value","steps":[{"argv":["sandbox","--layer"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --layer: expected one argument\r\n"}]}, +{"name":"usage: option as value","steps":[{"argv":["sandbox","--layer","--scratch"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --layer: expected one argument\r\n"}]}, +{"name":"usage: dash value","steps":[{"argv":["sandbox","--arg","-x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --arg: expected one argument\r\n"}]}, +{"name":"usage: flag with value","steps":[{"argv":["sandbox","--dry-run=1"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --dry-run: ignored explicit argument '1'\r\n"}]}, +{"name":"usage: unrecognized option","steps":[{"argv":["sandbox","--bogus"],"status":2,"stdout":"","stderr":"usage: nvx-rs [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\r\nnvx-rs: error: unrecognized arguments: --bogus\r\n"}]}, +{"name":"usage: unrecognized short option","steps":[{"argv":["sandbox","-x"],"status":2,"stdout":"","stderr":"usage: nvx-rs [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\r\nnvx-rs: error: unrecognized arguments: -x\r\n"}]}, +{"name":"usage: unrecognized after separator","steps":[{"argv":["sandbox","run","--","extra"],"status":2,"stdout":"","stderr":"usage: nvx-rs [-h] [--root PATH] [--prog PROG] {artifacts,egress,sandbox} ...\r\nnvx-rs: error: unrecognized arguments: extra\r\n"}]}, +{"name":"validation: dry run outside run","steps":[{"argv":["sandbox","provision","--dry-run"],"status":1,"stdout":"","stderr":"error: --dry-run is only valid for sandbox run\r\n"}]}, +{"name":"validation: environment and environment file","steps":[{"argv":["sandbox","exec","--environment","A=1","--environment-file","{root}\\env.json"],"write":{"env.json":"[\"A=1\"]"},"status":1,"stdout":"","stderr":"error: --environment and --environment-file are mutually exclusive\r\n"}]}, +{"name":"validation: cwd outside exec","steps":[{"argv":["sandbox","run","--cwd","/"],"status":1,"stdout":"","stderr":"error: managed execution options require the sandbox exec operation\r\n"}]}, +{"name":"validation: environment outside exec","steps":[{"argv":["sandbox","start","--environment","A=1"],"status":1,"stdout":"","stderr":"error: managed execution options require the sandbox exec operation\r\n"}]}, +{"name":"validation: environment file outside exec","steps":[{"argv":["sandbox","provision","--environment-file","{root}\\env.json"],"write":{"env.json":"[\"A=1\"]"},"status":1,"stdout":"","stderr":"error: managed execution options require the sandbox exec operation\r\n"}]}, +{"name":"validation: inherited environment outside exec","steps":[{"argv":["sandbox","stop","--inherit-default-environment"],"status":1,"stdout":"","stderr":"error: managed execution options require the sandbox exec operation\r\n"}]}, +{"name":"validation: environment file too large","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\env.json"],"write":{"env.json":{"fill":" ","size":1048577}},"status":1,"stdout":"","stderr":"error: managed execution environment file exceeds 1048576-byte limit\r\n"}]}, +{"name":"validation: environment file at the limit","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\env.json"],"write":{"env.json":{"text":"[\"A=1\"]","fill":" ","size":1048576}},"status":1,"stdout":"","stderr":"error: sandbox exec requires --state-dir\r\n"}]}, +{"name":"validation: environment file missing","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\missing.json"],"status":1,"stdout":"","stderr":"error: failed to read managed execution environment: {root}\\missing.json\r\n"}]}, +{"name":"validation: environment file directory","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\share"],"status":1,"stdout":"","stderr":"error: failed to read managed execution environment: {root}\\share\r\n"}]}, +{"name":"validation: environment file not UTF-8","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\env.json"],"write":{"env.json":{"hex":"5b22ff225d"}},"status":1,"stdout":"","stderr":"error: failed to read managed execution environment: {root}\\env.json\r\n"}]}, +{"name":"validation: environment file not JSON","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\env.json"],"write":{"env.json":"[\"A=1\""},"status":1,"stdout":"","stderr":"error: failed to read managed execution environment: {root}\\env.json\r\n"}]}, +{"name":"validation: environment file object","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\env.json"],"write":{"env.json":"{\"A\": \"1\"}"},"status":1,"stdout":"","stderr":"error: managed execution environment file must contain a JSON array of KEY=VALUE strings\r\n"}]}, +{"name":"validation: environment file non-string","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\env.json"],"write":{"env.json":"[\"A=1\", 2]"},"status":1,"stdout":"","stderr":"error: managed execution environment file must contain a JSON array of KEY=VALUE strings\r\n"}]}, +{"name":"validation: environment file entry without equals","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\env.json"],"write":{"env.json":"[\"A\"]"},"status":1,"stdout":"","stderr":"error: managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes\r\n"}]}, +{"name":"validation: environment file entry with NUL","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\env.json"],"write":{"env.json":"[\"A=\\u0000\"]"},"status":1,"stdout":"","stderr":"error: managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes\r\n"}]}, +{"name":"validation: environment file duplicate entry","steps":[{"argv":["sandbox","exec","--environment-file","{root}\\env.json"],"write":{"env.json":"[\"A=1\", \"A=2\"]"},"status":1,"stdout":"","stderr":"error: managed exec environment contains duplicate key: A\r\n"}]}, +{"name":"validation: environment entry without name","steps":[{"argv":["sandbox","exec","--environment","=x"],"status":1,"stdout":"","stderr":"error: managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes\r\n"}]}, +{"name":"validation: environment entry with space","steps":[{"argv":["sandbox","exec","--environment","A B=x"],"status":1,"stdout":"","stderr":"error: sandbox exec requires --state-dir\r\n"}]}, +{"name":"validation: network outside run","steps":[{"argv":["sandbox","start","--net","10.0.0.2/24"],"status":1,"stdout":"","stderr":"error: network policy options are only valid for sandbox run or provision\r\n"}]}, +{"name":"validation: egress rule outside run","steps":[{"argv":["sandbox","exec","--network-egress-allow","10.0.0.0/8"],"status":1,"stdout":"","stderr":"error: network policy options are only valid for sandbox run or provision\r\n"}]}, +{"name":"validation: forward outside run","steps":[{"argv":["sandbox","stop","--host-loopback-forward","80"],"status":1,"stdout":"","stderr":"error: network policy options are only valid for sandbox run or provision\r\n"}]}, +{"name":"validation: policy file outside run","steps":[{"argv":["sandbox","deprovision","--network-egress-policy-file","{root}\\policy.json"],"status":1,"stdout":"","stderr":"error: network policy options are only valid for sandbox run or provision\r\n"}]}, +{"name":"validation: systemd entrypoint of run","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/usr/lib/systemd/systemd"],"status":1,"stdout":"","stderr":"error: systemd entrypoints are unsupported by the sandbox security profile\r\n"}]}, +{"name":"validation: systemd entrypoint of exec","steps":[{"argv":["sandbox","exec","--entrypoint","/lib/systemd/systemd"],"status":1,"stdout":"","stderr":"error: systemd entrypoints are unsupported by the sandbox security profile\r\n"}]}, +{"name":"validation: outcome report outside run","steps":[{"argv":["sandbox","provision","--outcome-report","{root}\\reports\\exec.json"],"status":1,"stdout":"","stderr":"error: --outcome-report is only valid for one-shot run or managed exec\r\n"}]}, +{"name":"validation: deny without mount","steps":[{"argv":["sandbox","run","--mount-deny","{root}\\share\\hidden"],"status":1,"stdout":"","stderr":"error: --mount-deny requires --mount\r\n"}]}, +{"name":"validation: allow without mount","steps":[{"argv":["sandbox","run","--mount-allow","{root}\\share\\hidden"],"status":1,"stdout":"","stderr":"error: --mount-allow requires --mount\r\n"}]}, +{"name":"validation: write without mount","steps":[{"argv":["sandbox","run","--mount-write","{root}\\share\\writable"],"status":1,"stdout":"","stderr":"error: --mount-write requires --mount\r\n"}]}, +{"name":"validation: owner without mount","steps":[{"argv":["sandbox","run","--mount-owner","vmm"],"status":1,"stdout":"","stderr":"error: --mount-owner requires --mount\r\n"}]}, +{"name":"validation: mount outside run","steps":[{"argv":["sandbox","start","--mount","/data,{root}\\share"],"status":1,"stdout":"","stderr":"error: --mount is only valid for sandbox run or provision\r\n"}]}, +{"name":"validation: net without profile","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--net","10.0.0.2/24"],"status":1,"stdout":"","stderr":"error: --net and --network-profile must be specified together\r\n"}]}, +{"name":"validation: profile without net","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--network-profile","portable"],"status":1,"stdout":"","stderr":"error: --net and --network-profile must be specified together\r\n"}]}, +{"name":"validation: run without layers","steps":[{"argv":["sandbox","run"],"status":1,"stdout":"","stderr":"error: sandbox run requires --layer and --scratch\r\n"}]}, +{"name":"validation: provision without scratch","steps":[{"argv":["sandbox","provision","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111"],"status":1,"stdout":"","stderr":"error: sandbox provision requires --layer and --scratch\r\n"}]}, +{"name":"validation: run without layer","steps":[{"argv":["sandbox","run","--scratch","{root}\\scratch.ext4"],"status":1,"stdout":"","stderr":"error: sandbox run requires --layer and --scratch\r\n"}]}, +{"name":"validation: policy file with rules","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--network-egress","allow","--network-egress-policy-file","{root}\\policy.json","--network-egress-deny","10.0.0.0/8"],"status":1,"stdout":"","stderr":"error: --network-egress-policy-file cannot be combined with --network-egress-allow or --network-egress-deny\r\n"}]}, +{"name":"validation: policy file without egress","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--network-egress-policy-file","{root}\\policy.json"],"status":1,"stdout":"","stderr":"error: --network-egress is required with --network-egress-policy-file\r\n"}]}, +{"name":"validation: policy file missing","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--network-egress","deny","--network-egress-policy-file","{root}\\missing.json"],"status":1,"stdout":"","stderr":"error: failed to read egress policy file: {root}\\missing.json\r\n"}]}, +{"name":"validation: policy file invalid","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--network-egress","deny","--network-egress-policy-file","{root}\\policy.json"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"10.0.0.0/8\", \"port\": 0}]}"},"status":1,"stdout":"","stderr":"error: allow[0].port requires protocol\r\n"}]}, +{"name":"validation: provision without state directory","steps":[{"argv":["sandbox","provision","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":1,"stdout":"","stderr":"error: sandbox provision requires --state-dir\r\n"}]}, +{"name":"validation: start without state directory","steps":[{"argv":["sandbox","start"],"status":1,"stdout":"","stderr":"error: sandbox start requires --state-dir\r\n"}]}, +{"name":"validation: exec without state directory","steps":[{"argv":["sandbox","exec"],"status":1,"stdout":"","stderr":"error: sandbox exec requires --state-dir\r\n"}]}, +{"name":"validation: stop without state directory","steps":[{"argv":["sandbox","stop"],"status":1,"stdout":"","stderr":"error: sandbox stop requires --state-dir\r\n"}]}, +{"name":"validation: deprovision without state directory","steps":[{"argv":["sandbox","deprovision"],"status":1,"stdout":"","stderr":"error: sandbox deprovision requires --state-dir\r\n"}]}, +{"name":"validation: run with state directory","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: one-shot sandbox execution rejects persistent --state-dir settings\r\n"}]}, +{"name":"validation: invalid layer UUID","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,nope","--scratch","{root}\\scratch.ext4"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --layer: distro layer UUID is invalid: 'nope'\r\n"}]}, +{"name":"validation: invalid layer role","steps":[{"argv":["sandbox","run","--layer","base,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --layer: unsupported layer role 'base'; choose distro\r\n"}]}, +{"name":"validation: retired layer role","steps":[{"argv":["sandbox","run","--layer","runtime,{root}\\runtime.erofs,22222222-2222-2222-2222-222222222222"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --layer: unsupported layer role 'runtime'; choose distro\r\n"}]}, +{"name":"validation: missing layer","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\missing.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":1,"stdout":"","stderr":"error: distro EROFS layer not found: {root}\\missing.erofs\r\n"}]}, +{"name":"validation: duplicate layer","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":1,"stdout":"","stderr":"error: duplicate sandbox layer role(s): distro\r\n"}]}, +{"name":"validation: missing scratch","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\missing.ext4"],"status":1,"stdout":"","stderr":"error: ext4 scratch image not found: {root}\\missing.ext4\r\n"}]}, +{"name":"validation: relative entrypoint","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","bin/sh"],"status":1,"stdout":"","stderr":"error: sandbox entrypoint must be an absolute path without spaces\r\n"}]}, +{"name":"validation: invalid hostname","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--hostname","bad host"],"status":1,"stdout":"","stderr":"error: sandbox hostname must be a lowercase RFC 1123 label up to 63 characters\r\n"}]}, +{"name":"validation: argument with space","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--arg","a b"],"status":1,"stdout":"","stderr":"error: sandbox arguments must be nonempty and contain no spaces\r\n"}]}, +{"name":"validation: root workload","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--workload-user","0:0"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --workload-user: --workload-user UID must be between 1 and 4294967295\r\n"}]}, +{"name":"validation: invalid workload user","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--workload-user","x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --workload-user: --workload-user must be UID:GID\r\n"}]}, +{"name":"validation: invalid memory limit","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--memory-max","x"],"status":2,"stdout":"","stderr":"usage: nvx-rs sandbox [-h] [--layer ROLE,PATH,EROFS_UUID] [--scratch SCRATCH]\r\n [--state-dir STATE_DIR] [--entrypoint ENTRYPOINT]\r\n [--arg SANDBOX_ARG] [--hostname HOSTNAME]\r\n [--workload-user UID:GID] [--memory-max MEMORY_MAX]\r\n [--pids-max PIDS_MAX] [--memory-mib MEMORY_MIB]\r\n [--timeout TIMEOUT] [--exec-timeout-ms EXEC_TIMEOUT_MS]\r\n [--cwd CWD] [--environment KEY=VALUE]\r\n [--environment-file PATH]\r\n [--inherit-default-environment]\r\n [--hypervisor {auto,whp,kvm,mshv}]\r\n [--mount GUEST_TARGET,HOST_PATH[,ro|rw]]\r\n [--mount-deny HOST_PATH] [--mount-allow HOST_PATH]\r\n [--mount-write HOST_PATH] [--mount-owner {vmm,caller}]\r\n [--net IPV4/PREFIX] [--network-profile {portable}]\r\n [--network-egress {allow,deny}]\r\n [--network-ingress {allow,deny}]\r\n [--network-egress-allow CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-deny CIDR[:PROTOCOL[:PORT[-PORT]]]]\r\n [--network-egress-policy-file PATH]\r\n [--host-loopback {allow,deny}]\r\n [--network-proxy IPV4:TCP-PORT]\r\n [--host-loopback-forward HOST_LOOPBACK_FORWARD]\r\n [--outcome-report OUTCOME_REPORT] [--cmdline CMDLINE]\r\n [--dry-run]\r\n [{run,provision,start,exec,stop,deprovision}]\r\nnvx-rs sandbox: error: argument --memory-max: invalid int value: 'x'\r\n"}]}, +{"name":"validation: invalid process limit","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--pids-max","0"],"status":1,"stdout":"","stderr":"error: --pids-max must be positive\r\n"}]}, +{"name":"validation: relative mount target","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","data,{root}\\share"],"status":1,"stdout":"","stderr":"error: invalid sandbox mount target 'data': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','\r\n"}]}, +{"name":"validation: missing mount host","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\missing"],"status":1,"stdout":"","stderr":"error: sandbox mount host path is not a plain directory: {root}\\missing\r\n"}]}, +{"name":"validation: invalid mount mode","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share,rx"],"status":1,"stdout":"","stderr":"error: unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)\r\n"}]}, +{"name":"validation: deny outside share","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--mount-deny","{root}\\share2"],"status":2,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --mount /data,{root}\\share,ro --mount-deny {root}\\share2 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\"\r\n","stderr":"openvmm: invalid --mount-deny {root}\\share2\n"}]}, +{"name":"validation: allow outside denied path","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--mount-allow","{root}\\share\\hidden\\shown"],"status":1,"stdout":"","stderr":"error: --mount-allow exposes a path inside a --mount-deny path of the same share, which has none\r\n"}]}, +{"name":"validation: write in read-only share","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share,ro","--mount-write","{root}\\share\\writable"],"status":1,"stdout":"","stderr":"error: --mount-write narrows the writes of a read-write share; --mount /data is read-only\r\n"}]}, +{"name":"validation: reserved command-line token","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--cmdline","nvx_sandbox=0"],"status":1,"stdout":"","stderr":"error: nvx_sandbox is owned by the sandbox profile\r\n"}]}, +{"name":"validation: systemd distro image","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"write":{"distro.erofs.manifest.json":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"8a475efd5fd711d481a9a19de6d12b88dff0cd81cb8135ffcd5b65b062c7bf04\", \"packages\": [{\"name\": \"busybox\", \"version\": \"1\"}, {\"name\": \"systemd\", \"version\": \"1\"}]}"},"status":1,"stdout":"","stderr":"error: systemd images are unsupported by the sandbox security profile\r\n"}]}, +{"name":"validation: mismatched distro manifest","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"write":{"distro.erofs.manifest.json":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0000000000000000000000000000000000000000000000000000000000000000\", \"packages\": []}"},"status":1,"stdout":"","stderr":"error: sandbox distro manifest does not match its artifact\r\n"}]}, +{"name":"validation: invalid distro manifest","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"write":{"distro.erofs.manifest.json":"{"},"status":1,"stdout":"","stderr":"error: invalid sandbox distro manifest: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)\r\n"}]}, +{"name":"validation: distro manifest without packages","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"write":{"distro.erofs.manifest.json":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"8a475efd5fd711d481a9a19de6d12b88dff0cd81cb8135ffcd5b65b062c7bf04\"}"},"status":1,"stdout":"","stderr":"error: sandbox distro manifest has invalid package metadata\r\n"}]}, +{"name":"validation: caller-owned share on Windows","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--mount-owner","caller"],"status":1,"stdout":"","stderr":"error: --mount-owner caller requires a Linux host; Windows has no per-request POSIX identity for OpenVMM to switch to\r\n"}]}, +{"name":"dry run: minimal","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--dry-run"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\"\r\n","stderr":""}]}, +{"name":"dry run: every layer and option","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/server","--arg=--port","--arg","8080","--hostname","worker","--workload-user","1000:1000","--memory-max","268435456","--pids-max","64","--memory-mib","512","--hypervisor","mshv","--cmdline","quiet loglevel=3","--outcome-report","{root}\\reports\\openvmm-run.json","--dry-run"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 1000:1000 --microvm-lifecycle one-shot --single-process --hypervisor mshv --memory 512M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"quiet loglevel=3 nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/server nvx_hostname=worker nvx_arg=--port nvx_arg=8080 nvx_memory_max=268435456 nvx_pids_max=65\" --microvm-report {root}\\reports\\openvmm-run.json\r\n","stderr":""}]}, +{"name":"dry run: explicit hypervisor","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--hypervisor","kvm","--dry-run"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor kvm --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\"\r\n","stderr":""}]}, +{"name":"dry run: one share","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--dry-run"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --mount /data,{root}\\share,ro --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\"\r\n","stderr":""}]}, +{"name":"dry run: writable share","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share,rw","--dry-run"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --mount /data,{root}\\share,rw --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\"\r\n","stderr":""}]}, +{"name":"dry run: file share","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/etc/single.txt,{root}\\single.txt,ro","--dry-run"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --mount-aggregate /run/nvx/shares --mount-child 0-a80a05e9cb129bc27b7ba103bf2f5ac0,{root}\\single.txt,ro,file --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-a80a05e9cb129bc27b7ba103bf2f5ac0,/etc/single.txt,ro,file\"\r\n","stderr":""}]}, +{"name":"dry run: several shares with policies","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share,rw","--mount-deny","{root}\\share\\hidden","--mount-allow","{root}\\share\\hidden\\shown","--mount-write","{root}\\share\\writable","--mount","/other,{root}\\share2","--dry-run"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --mount-aggregate /run/nvx/shares --mount-child 0-bd47413b5c03dfc6e4b5b8143fe205ea,{root}\\share,rw --mount-child 1-bf2faee2596026620b5cbc997c2d7d45,{root}\\share2,ro --mount-deny {root}\\share\\hidden --mount-allow {root}\\share\\hidden\\shown --mount-write {root}\\share\\writable --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-bd47413b5c03dfc6e4b5b8143fe205ea,/data,rw nvx_share=1-bf2faee2596026620b5cbc997c2d7d45,/other,ro\"\r\n","stderr":""}]}, +{"name":"dry run: network","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--net","10.0.0.2/24","--network-profile","portable","--network-egress","deny","--network-ingress","allow","--network-egress-allow","10.0.0.0/8:tcp:443","--network-egress-deny","10.1.0.0/16","--host-loopback","allow","--network-proxy","10.0.0.1:3128","--host-loopback-forward","8080","--dry-run"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\" --net 10.0.0.2/24 --network-profile portable --network-egress deny --network-ingress allow --network-egress-allow 10.0.0.0/8:tcp:443 --network-egress-deny 10.1.0.0/16 --host-loopback allow --network-proxy 10.0.0.1:3128 --host-loopback-forward 8080\r\n","stderr":""}]}, +{"name":"dry run: egress policy file","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--network-egress","deny","--network-egress-policy-file","{root}\\policy.json","--dry-run"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.128/25\"]}, {\"cidr\": \"2001:db8::/32\", \"protocol\": \"tcp\", \"port\": 443}], \"deny\": [{\"cidr\": \"198.51.100.0/24\", \"protocol\": \"udp\"}]}"},"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\" --network-egress deny --network-egress-allow 192.0.2.0/25 --network-egress-allow 2001:db8::/32:tcp:443 --network-egress-deny 198.51.100.0/24:udp\r\n","stderr":""}]}, +{"name":"dry run: quoted arguments","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--cmdline","a=\"b c\" d","--dry-run"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"a=\\\"b c\\\" d nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\"\r\n","stderr":""}]}, +{"name":"run: echo","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/echo","--arg","hello","--arg","world"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/echo nvx_hostname=nvx-sandbox nvx_arg=hello nvx_arg=world\"\r\nhello world\n","stderr":""}]}, +{"name":"run: default operation","steps":[{"argv":["sandbox","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/echo","--arg","hello","--arg","world"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/echo nvx_hostname=nvx-sandbox nvx_arg=hello nvx_arg=world\"\r\nhello world\n","stderr":""}]}, +{"name":"run: failure","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/false"],"status":1,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/false nvx_hostname=nvx-sandbox\"\r\n","stderr":""}]}, +{"name":"run: missing program","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/missing"],"status":127,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/missing nvx_hostname=nvx-sandbox\"\r\n","stderr":"openvmm: /bin/missing: not found\n"}]}, +{"name":"run: exit status","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/true","--cmdline","fake_exit_status=7"],"status":7,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"fake_exit_status=7 nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/true nvx_hostname=nvx-sandbox\"\r\n","stderr":""}]}, +{"name":"run: outcome report","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/echo","--arg","hello","--arg","world","--outcome-report","{root}\\reports\\openvmm-run.json"],"check":["reports/openvmm-run.json","build/fake-openvmm-reports.json"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/echo nvx_hostname=nvx-sandbox nvx_arg=hello nvx_arg=world\" --microvm-report {root}\\reports\\openvmm-run.json\r\nhello world\n","stderr":"","files":{"reports/openvmm-run.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"run\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"build/fake-openvmm-reports.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--microvm-lifecycle\",\"one-shot\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/echo nvx_hostname=nvx-sandbox nvx_arg=hello nvx_arg=world\",\"--microvm-report\",\"{root_json}\\\\reports\\\\openvmm-run.json\"]"}}}]}, +{"name":"run: network outcome report","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/true","--net","10.0.0.2/24","--network-profile","portable","--network-egress","deny","--network-egress-allow","10.0.0.0/8:tcp:443","--outcome-report","{root}\\reports\\openvmm-run.json"],"check":["reports/openvmm-run.json"],"status":0,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/true nvx_hostname=nvx-sandbox\" --net 10.0.0.2/24 --network-profile portable --network-egress deny --network-egress-allow 10.0.0.0/8:tcp:443 --microvm-report {root}\\reports\\openvmm-run.json\r\n","stderr":"","files":{"reports/openvmm-run.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"run\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"}}}]}, +{"name":"run: OpenVMM refuses its options","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--host-loopback-forward","8080"],"status":2,"stdout":">> {root}\\openvmm\\target\\release\\openvmm.exe --machine microvm --microvm-sandbox-block distro:file:{root}\\distro.erofs,ro --microvm-sandbox-block scratch:file:{root}\\scratch.ext4 --microvm-workload-identity 65534:65534 --microvm-lifecycle one-shot --single-process --hypervisor whp --memory 256M --kernel {root}\\build\\vmlinux --initrd {root}\\build\\initramfs.cpio.gz --cmdline \"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\" --host-loopback-forward 8080\r\n","stderr":"openvmm: --host-loopback-forward requires explicit --host-loopback allow\n"}]}, +{"name":"run: missing OpenVMM","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"remove":["openvmm/target/release/openvmm{exe}"],"status":1,"stdout":"","stderr":"error: OpenVMM release binary not found: {root}\\openvmm\\target\\release\\openvmm.exe\r\n"}]}, +{"name":"run: missing kernel","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"remove":["build/vmlinux"],"status":1,"stdout":"","stderr":"error: Linux direct kernel not found: {root}\\build\\vmlinux\r\n"}]}, +{"name":"run: missing initramfs","steps":[{"argv":["sandbox","run","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"remove":["build/initramfs.cpio.gz"],"status":1,"stdout":"","stderr":"error: initramfs not found: {root}\\build\\initramfs.cpio.gz\r\n"}]}, +{"name":"lifecycle: minimal","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 1,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 1,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/echo","--arg","hello","--arg","world"],"status":0,"stdout":"hello world\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/tmp"],"status":0,"stdout":"/tmp\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/sh","--arg=-c","--arg","echo out; echoerr err; exit 3","--outcome-report","{root}\\reports\\exec.json"],"check":["reports/exec.json"],"status":3,"stdout":"out\n","stderr":"err\n","files":{"reports/exec.json":{"text":"{\n \"operation_id\": \"{hex32}\",\n \"outcome\": {\n \"category\": \"exit\",\n \"operation\": \"exec\",\n \"status_code\": 3\n },\n \"schema_version\": 1\n}\n"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/usr/bin/env","--environment","A=1","--environment","B=two"],"status":0,"stdout":"A=1\nB=two\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/usr/bin/printenv","--arg","PATH","--inherit-default-environment","--environment","A=1"],"status":0,"stdout":"/usr/sbin:/usr/bin:/sbin:/bin\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/usr/bin/env","--environment-file","{root}\\env.json"],"write":{"env.json":"[\"X=1\", \"Y=two words\"]"},"status":0,"stdout":"X=1\nY=two words\n","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/usr/bin/env","--environment-file","{root}\\empty.json"],"write":{"empty.json":"[]"},"status":0,"stdout":"","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/sh","--arg=-c","--arg","sleep 5000","--exec-timeout-ms","50","--outcome-report","{root}\\reports\\exec-timeout.json"],"check":["reports/exec-timeout.json"],"status":124,"stdout":"","stderr":"","files":{"reports/exec-timeout.json":{"text":"{\n \"operation_id\": \"{hex32}\",\n \"outcome\": {\n \"category\": \"timeout\",\n \"operation\": \"exec\",\n \"status_code\": 124\n },\n \"schema_version\": 1\n}\n"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/sh","--arg=-c","--arg","signal 9"],"status":137,"stdout":"","stderr":""},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/missing"],"status":127,"stdout":"","stderr":"openvmm: /bin/missing: not found\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/missing"],"status":1,"stdout":"","stderr":"error: managed guest rejected exec (status=2, category=cwd-failed)\r\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","relative"],"status":1,"stdout":"","stderr":"error: managed exec working directory must be an absolute path of at most 4096 bytes\r\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/echo","--outcome-report","{root}\\reports\\exec.json"],"status":1,"stdout":"","stderr":"error: outcome report already exists: {root}\\reports\\exec.json\r\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/echo","--outcome-report","{root}\\missing\\exec.json"],"status":1,"stdout":"","stderr":"error: outcome report parent is not a plain directory: {root}\\missing\r\n"},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 1,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 1,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 1,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: one share","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 2,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 2,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root_json}\\\\share,ro\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/data"],"status":0,"stdout":"/data\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 2,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 2,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 2,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: several shares","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share","--mount","/other,{root}\\share2,rw"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 4,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 4,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-bd47413b5c03dfc6e4b5b8143fe205ea,{root_json}\\\\share,ro\",\"--mount-child\",\"1-bf2faee2596026620b5cbc997c2d7d45,{root_json}\\\\share2,rw\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-bd47413b5c03dfc6e4b5b8143fe205ea,/data,ro nvx_share=1-bf2faee2596026620b5cbc997c2d7d45,/other,rw\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","exec","--state-dir","{root}\\state","--entrypoint","/bin/pwd","--cwd","/other"],"status":0,"stdout":"/other\n","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 4,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 4,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 4,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\"\r\n },\r\n {\r\n \"access\": \"rw\",\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/other\",\r\n \"host_path\": \"{root_json}\\\\share2\",\r\n \"owner\": \"vmm\"\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: share policies","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/data,{root}\\share,rw","--mount-deny","{root}\\share\\hidden","--mount-allow","{root}\\share\\hidden\\shown","--mount-write","{root}\\share\\writable"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 5,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 5,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount\",\"/data,{root_json}\\\\share,rw\",\"--mount-deny\",\"{root_json}\\\\share\\\\hidden\",\"--mount-allow\",\"{root_json}\\\\share\\\\hidden\\\\shown\",\"--mount-write\",\"{root_json}\\\\share\\\\writable\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 5,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 5,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 5,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"rw\",\r\n \"allowed_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\\\\shown\"\r\n ],\r\n \"denied_paths\": [\r\n \"{root_json}\\\\share\\\\hidden\"\r\n ],\r\n \"guest_target\": \"/data\",\r\n \"host_path\": \"{root_json}\\\\share\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": [\r\n \"{root_json}\\\\share\\\\writable\"\r\n ]\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: file share","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--mount","/etc/single.txt,{root}\\single.txt,ro"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 6,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 6,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"65534:65534\",\"--mount-aggregate\",\"/run/nvx/shares\",\"--mount-child\",\"0-a80a05e9cb129bc27b7ba103bf2f5ac0,{root_json}\\\\single.txt,ro,file\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"256M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-a80a05e9cb129bc27b7ba103bf2f5ac0,/etc/single.txt,ro,file\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 6,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 6,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 6,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mounts\": [\r\n {\r\n \"access\": \"ro\",\r\n \"allowed_paths\": [],\r\n \"denied_paths\": [],\r\n \"guest_target\": \"/etc/single.txt\",\r\n \"host_path\": \"{root_json}\\\\single.txt\",\r\n \"kind\": \"file\",\r\n \"owner\": \"vmm\",\r\n \"writable_paths\": []\r\n }\r\n ],\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 0,\n \"deny_rule_count\": 0,\n \"host_loopback\": \"none\",\n \"mode\": \"none\",\n \"status\": \"not-requested\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"lifecycle: network and launch options","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--entrypoint","/bin/server","--arg=--serve","--hostname","worker","--workload-user","1000:1000","--memory-max","268435456","--pids-max","64","--memory-mib","384","--hypervisor","auto","--cmdline","quiet","--net","10.0.0.2/24","--network-profile","portable","--network-egress","deny","--network-ingress","deny","--network-egress-policy-file","{root}\\policy.json","--host-loopback","allow","--network-proxy","10.0.0.1:3128","--host-loopback-forward","8080","--host-loopback-forward","9090"],"write":{"policy.json":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 443}], \"deny\": [{\"cidr\": \"192.0.2.128/25\"}]}"},"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"format\": 1,\r\n \"host_loopback\": \"allow\",\r\n \"host_loopback_forward\": [\r\n \"8080\",\r\n \"9090\"\r\n ],\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"net\": \"10.0.0.2/24\",\r\n \"network_egress\": \"deny\",\r\n \"network_egress_allow\": [\r\n \"192.0.2.0/24:tcp:443\"\r\n ],\r\n \"network_egress_deny\": [\r\n \"192.0.2.128/25\"\r\n ],\r\n \"network_ingress\": \"deny\",\r\n \"network_profile\": \"portable\",\r\n \"network_proxy\": \"10.0.0.1:3128\",\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log","build/fake-openvmm-state.json"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"format\": 1,\r\n \"host_loopback\": \"allow\",\r\n \"host_loopback_forward\": [\r\n \"8080\",\r\n \"9090\"\r\n ],\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"net\": \"10.0.0.2/24\",\r\n \"network_egress\": \"deny\",\r\n \"network_egress_allow\": [\r\n \"192.0.2.0/24:tcp:443\"\r\n ],\r\n \"network_egress_deny\": [\r\n \"192.0.2.128/25\"\r\n ],\r\n \"network_ingress\": \"deny\",\r\n \"network_profile\": \"portable\",\r\n \"network_proxy\": \"10.0.0.1:3128\",\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""},"build/fake-openvmm-state.json":{"text":"[\"--machine\",\"microvm\",\"--microvm-sandbox-block\",\"distro:file:{root_json}\\\\distro.erofs,ro\",\"--microvm-sandbox-block\",\"scratch:file:{root_json}\\\\scratch.ext4\",\"--microvm-workload-identity\",\"1000:1000\",\"--microvm-lifecycle\",\"managed\",\"--single-process\",\"--hypervisor\",\"whp\",\"--memory\",\"384M\",\"--kernel\",\"{root_json}\\\\build\\\\vmlinux\",\"--initrd\",\"{root_json}\\\\build\\\\initramfs.cpio.gz\",\"--cmdline\",\"quiet nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=worker nvx_memory_max=268435456 nvx_pids_max=65\",\"--virtio-console\",\"none\",\"--microvm-control-console\",\"listen=//./pipe/openvmm-microvm-{hex32}\",\"--microvm-control-auth-stdin\",\"--microvm-report\",\"{root_json}\\\\state\\\\outcome.json\",\"--net\",\"10.0.0.2/24\",\"--network-profile\",\"portable\",\"--network-egress\",\"deny\",\"--network-ingress\",\"deny\",\"--host-loopback\",\"allow\",\"--network-egress-allow\",\"192.0.2.0/24:tcp:443\",\"--network-egress-deny\",\"192.0.2.128/25\",\"--host-loopback-forward\",\"8080\",\"--host-loopback-forward\",\"9090\",\"--network-proxy\",\"10.0.0.1:3128\"]"}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"format\": 1,\r\n \"host_loopback\": \"allow\",\r\n \"host_loopback_forward\": [\r\n \"8080\",\r\n \"9090\"\r\n ],\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"net\": \"10.0.0.2/24\",\r\n \"network_egress\": \"deny\",\r\n \"network_egress_allow\": [\r\n \"192.0.2.0/24:tcp:443\"\r\n ],\r\n \"network_egress_deny\": [\r\n \"192.0.2.128/25\"\r\n ],\r\n \"network_ingress\": \"deny\",\r\n \"network_profile\": \"portable\",\r\n \"network_proxy\": \"10.0.0.1:3128\",\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 1,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","start","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","control.capability","lifecycle.lock","openvmm.log","runtime.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"format\": 1,\r\n \"host_loopback\": \"allow\",\r\n \"host_loopback_forward\": [\r\n \"8080\",\r\n \"9090\"\r\n ],\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"net\": \"10.0.0.2/24\",\r\n \"network_egress\": \"deny\",\r\n \"network_egress_allow\": [\r\n \"192.0.2.0/24:tcp:443\"\r\n ],\r\n \"network_egress_deny\": [\r\n \"192.0.2.128/25\"\r\n ],\r\n \"network_ingress\": \"deny\",\r\n \"network_profile\": \"portable\",\r\n \"network_proxy\": \"10.0.0.1:3128\",\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":{"text":"{\r\n \"control_endpoint\": \"//./pipe/openvmm-microvm-{hex32}\",\r\n \"format\": 1,\r\n \"pid\": {int},\r\n \"start_time\": {int}\r\n}\r\n"},"state/control.capability":{"size":32},"state/outcome.json":null,"state/openvmm.log":{"text":""}}},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":0,"stdout":"","stderr":"","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log","outcome.json"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"quiet\",\r\n \"format\": 1,\r\n \"host_loopback\": \"allow\",\r\n \"host_loopback_forward\": [\r\n \"8080\",\r\n \"9090\"\r\n ],\r\n \"hostname\": \"worker\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": 268435456,\r\n \"memory_mib\": 384,\r\n \"mount\": null,\r\n \"net\": \"10.0.0.2/24\",\r\n \"network_egress\": \"deny\",\r\n \"network_egress_allow\": [\r\n \"192.0.2.0/24:tcp:443\"\r\n ],\r\n \"network_egress_deny\": [\r\n \"192.0.2.128/25\"\r\n ],\r\n \"network_ingress\": \"deny\",\r\n \"network_profile\": \"portable\",\r\n \"network_proxy\": \"10.0.0.1:3128\",\r\n \"pids_max\": 64,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 1000,\r\n \"workload_uid\": 1000\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":{"text":"{\n \"backend\": \"whp\",\n \"instance_id\": \"{hex32}\",\n \"network_policy\": {\n \"allow_rule_count\": 1,\n \"deny_rule_count\": 1,\n \"host_loopback\": \"allow\",\n \"mode\": \"rules\",\n \"status\": \"applied\",\n \"status_code\": 0\n },\n \"outcome\": {\n \"category\": \"success\",\n \"operation\": \"managed\",\n \"status_code\": 0\n },\n \"schema_version\": 1,\n \"teardown\": {\n \"control_channels_closed\": true,\n \"guest_workload_stopped\": true,\n \"network_released\": true,\n \"openvmm_process_terminated\": true,\n \"temporary_storage_removed\": true,\n \"virtiofs_released\": true,\n \"vm_stopped\": true\n }\n}\n"},"state/openvmm.log":{"text":""}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"state: operations without a sandbox","steps":[{"argv":["sandbox","start","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\state\r\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\state\r\n"},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\state\r\n"},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\state\r\n"}]}, +{"name":"state: refusals of a running sandbox","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":1,"stdout":"","stderr":"error: sandbox is already provisioned\r\n"},{"argv":["sandbox","start","--state-dir","{root}\\state"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox is already running or has stale runtime state\r\n"},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox must be stopped before deprovision\r\n"},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","stop","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox is not running\r\n"},{"argv":["sandbox","exec","--state-dir","{root}\\state"],"status":1,"stdout":"","stderr":"error: sandbox is not running\r\n"},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"state: relative and nested state directory","steps":[{"argv":["sandbox","provision","--state-dir","nested/state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"check":["nested/state","nested/state/config.json"],"status":0,"stdout":"","stderr":"","files":{"nested/state":{"entries":["config.json","lifecycle.lock"]},"nested/state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 1,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"}}},{"argv":["sandbox","deprovision","--state-dir","nested/state"],"check":["nested/state"],"status":0,"stdout":"","stderr":"","files":{"nested/state":null}}]}, +{"name":"state: failed start","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4","--cmdline","fake_exit_on_start=5"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}\\state","--timeout","0.5"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM exited during startup with status 5\r\n","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"fake_exit_on_start=5\",\r\n \"format\": 1,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":{"text":"openvmm: failing the launch as requested\n"}}},{"argv":["sandbox","start","--state-dir","{root}\\state","--timeout","0.5"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM exited during startup with status 5\r\n","files":{"state":{"entries":["config.json","lifecycle.lock","openvmm.log"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"fake_exit_on_start=5\",\r\n \"format\": 1,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":{"text":"openvmm: failing the launch as requested\nopenvmm: failing the launch as requested\n"}}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}],"interop":true}, +{"name":"state: start without OpenVMM","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}\\state"],"remove":["openvmm/target/release/openvmm{exe}"],"check":["state","state/config.json","state/runtime.json","state/control.capability","state/outcome.json","state/openvmm.log"],"status":1,"stdout":"","stderr":"error: OpenVMM release binary not found: {root}\\openvmm\\target\\release\\openvmm.exe\r\n","files":{"state":{"entries":["config.json","lifecycle.lock"]},"state/config.json":{"text":"{\r\n \"cmdline\": \"\",\r\n \"format\": 1,\r\n \"host_loopback\": null,\r\n \"host_loopback_forward\": [],\r\n \"hostname\": \"nvx-sandbox\",\r\n \"hypervisor\": \"whp\",\r\n \"layers\": [\r\n {\r\n \"path\": \"{root_json}\\\\distro.erofs\",\r\n \"role\": \"distro\",\r\n \"uuid\": \"11111111-1111-1111-1111-111111111111\"\r\n }\r\n ],\r\n \"memory_max\": null,\r\n \"memory_mib\": 256,\r\n \"mount\": null,\r\n \"net\": null,\r\n \"network_egress\": null,\r\n \"network_egress_allow\": [],\r\n \"network_egress_deny\": [],\r\n \"network_ingress\": null,\r\n \"network_profile\": null,\r\n \"network_proxy\": null,\r\n \"pids_max\": null,\r\n \"scratch\": \"{root_json}\\\\scratch.ext4\",\r\n \"workload_gid\": 65534,\r\n \"workload_uid\": 65534\r\n}\r\n"},"state/runtime.json":null,"state/control.capability":null,"state/outcome.json":null,"state/openvmm.log":null}},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"status":0,"stdout":"","stderr":""}]}, +{"name":"state: malformed configuration","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","start","--state-dir","{root}\\state"],"write":{"state/config.json":"{\"format\": 7}"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}\\state\\config.json\r\n"},{"argv":["sandbox","start","--state-dir","{root}\\state"],"write":{"state/config.json":"[]"},"status":1,"stdout":"","stderr":"error: sandbox configuration has an unsupported format: {root}\\state\\config.json\r\n"},{"argv":["sandbox","start","--state-dir","{root}\\state"],"write":{"state/config.json":"{\"format\": 1}"},"status":1,"stdout":"","stderr":"error: sandbox configuration is malformed\r\n"},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"check":["state"],"status":0,"stdout":"","stderr":"","files":{"state":null}}]}, +{"name":"state: not a directory","steps":[{"argv":["sandbox","start","--state-dir","{root}\\single.txt"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\single.txt\r\n"},{"argv":["sandbox","provision","--state-dir","{root}\\single.txt","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":1,"stdout":"","stderr":"error: [WinError 183] Cannot create a file when that file already exists: '{root_json}\\\\single.txt'\r\n"},{"argv":["sandbox","deprovision","--state-dir","{root}\\single.txt"],"status":1,"stdout":"","stderr":"error: sandbox state path is not a plain directory: {root}\\single.txt\r\n"}]}, +{"name":"state: foreign file","steps":[{"argv":["sandbox","provision","--state-dir","{root}\\state","--layer","distro,{root}\\distro.erofs,11111111-1111-1111-1111-111111111111","--scratch","{root}\\scratch.ext4"],"status":0,"stdout":"","stderr":""},{"argv":["sandbox","deprovision","--state-dir","{root}\\state"],"write":{"state/foreign.txt":"x"},"check":["state"],"status":1,"stdout":"","stderr":"error: sandbox state directory contains files not owned by NVX: foreign.txt\r\n","files":{"state":{"entries":["foreign.txt"]}}}]} +] +} diff --git a/nvx-rs/tests/fixtures/egress-policy.json b/nvx-rs/tests/fixtures/egress-policy.json new file mode 100644 index 000000000..44c2012c5 --- /dev/null +++ b/nvx-rs/tests/fixtures/egress-policy.json @@ -0,0 +1,850 @@ +{ + "comment": "Recorded from the Python compiler by scripts/nvx_rs_fixtures.py, which microsoft/nvx#359 retired with it.", + "cases": [ + {"name":"vector: test_accepts_exact_256_rule_boundaries #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 2}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 4, \"endPort\": 5}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 7, \"endPort\": 8}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 10, \"endPort\": 11}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 13, \"endPort\": 14}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 16, \"endPort\": 17}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 19, \"endPort\": 20}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 22, \"endPort\": 23}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 25, \"endPort\": 26}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 28, \"endPort\": 29}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 31, \"endPort\": 32}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 34, \"endPort\": 35}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 37, \"endPort\": 38}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 40, \"endPort\": 41}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 43, \"endPort\": 44}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 46, \"endPort\": 47}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 49, \"endPort\": 50}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 52, \"endPort\": 53}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 55, \"endPort\": 56}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 58, \"endPort\": 59}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 61, \"endPort\": 62}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 64, \"endPort\": 65}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 67, \"endPort\": 68}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 70, \"endPort\": 71}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 73, \"endPort\": 74}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 76, \"endPort\": 77}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 79, \"endPort\": 80}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 82, \"endPort\": 83}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 85, \"endPort\": 86}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 88, \"endPort\": 89}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 91, \"endPort\": 92}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 94, \"endPort\": 95}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 97, \"endPort\": 98}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 100, \"endPort\": 101}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 103, \"endPort\": 104}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 106, \"endPort\": 107}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 109, \"endPort\": 110}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 112, \"endPort\": 113}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 115, \"endPort\": 116}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 118, \"endPort\": 119}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 121, \"endPort\": 122}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 124, \"endPort\": 125}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 127, \"endPort\": 128}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 130, \"endPort\": 131}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 133, \"endPort\": 134}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 136, \"endPort\": 137}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 139, \"endPort\": 140}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 142, \"endPort\": 143}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 145, \"endPort\": 146}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 148, \"endPort\": 149}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 151, \"endPort\": 152}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 154, \"endPort\": 155}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 157, \"endPort\": 158}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 160, \"endPort\": 161}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 163, \"endPort\": 164}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 166, \"endPort\": 167}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 169, \"endPort\": 170}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 172, \"endPort\": 173}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 175, \"endPort\": 176}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 178, \"endPort\": 179}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 181, \"endPort\": 182}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 184, \"endPort\": 185}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 187, \"endPort\": 188}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 190, \"endPort\": 191}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 193, \"endPort\": 194}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 196, \"endPort\": 197}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 199, \"endPort\": 200}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 202, \"endPort\": 203}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 205, \"endPort\": 206}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 208, \"endPort\": 209}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 211, \"endPort\": 212}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 214, \"endPort\": 215}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 217, \"endPort\": 218}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 220, \"endPort\": 221}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 223, \"endPort\": 224}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 226, \"endPort\": 227}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 229, \"endPort\": 230}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 232, \"endPort\": 233}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 235, \"endPort\": 236}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 238, \"endPort\": 239}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 241, \"endPort\": 242}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 244, \"endPort\": 245}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 247, \"endPort\": 248}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 250, \"endPort\": 251}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 253, \"endPort\": 254}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 256, \"endPort\": 257}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 259, \"endPort\": 260}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 262, \"endPort\": 263}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 265, \"endPort\": 266}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 268, \"endPort\": 269}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 271, \"endPort\": 272}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 274, \"endPort\": 275}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 277, \"endPort\": 278}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 280, \"endPort\": 281}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 283, \"endPort\": 284}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 286, \"endPort\": 287}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 289, \"endPort\": 290}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 292, \"endPort\": 293}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 295, \"endPort\": 296}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 298, \"endPort\": 299}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 301, \"endPort\": 302}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 304, \"endPort\": 305}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 307, \"endPort\": 308}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 310, \"endPort\": 311}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 313, \"endPort\": 314}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 316, \"endPort\": 317}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 319, \"endPort\": 320}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 322, \"endPort\": 323}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 325, \"endPort\": 326}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 328, \"endPort\": 329}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 331, \"endPort\": 332}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 334, \"endPort\": 335}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 337, \"endPort\": 338}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 340, \"endPort\": 341}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 343, \"endPort\": 344}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 346, \"endPort\": 347}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 349, \"endPort\": 350}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 352, \"endPort\": 353}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 355, \"endPort\": 356}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 358, \"endPort\": 359}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 361, \"endPort\": 362}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 364, \"endPort\": 365}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 367, \"endPort\": 368}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 370, \"endPort\": 371}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 373, \"endPort\": 374}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 376, \"endPort\": 377}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 379, \"endPort\": 380}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 382, \"endPort\": 383}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 385, \"endPort\": 386}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 388, \"endPort\": 389}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 391, \"endPort\": 392}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 394, \"endPort\": 395}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 397, \"endPort\": 398}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 400, \"endPort\": 401}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 403, \"endPort\": 404}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 406, \"endPort\": 407}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 409, \"endPort\": 410}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 412, \"endPort\": 413}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 415, \"endPort\": 416}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 418, \"endPort\": 419}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 421, \"endPort\": 422}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 424, \"endPort\": 425}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 427, \"endPort\": 428}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 430, \"endPort\": 431}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 433, \"endPort\": 434}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 436, \"endPort\": 437}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 439, \"endPort\": 440}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 442, \"endPort\": 443}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 445, \"endPort\": 446}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 448, \"endPort\": 449}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 451, \"endPort\": 452}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 454, \"endPort\": 455}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 457, \"endPort\": 458}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 460, \"endPort\": 461}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 463, \"endPort\": 464}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 466, \"endPort\": 467}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 469, \"endPort\": 470}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 472, \"endPort\": 473}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 475, \"endPort\": 476}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 478, \"endPort\": 479}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 481, \"endPort\": 482}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 484, \"endPort\": 485}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 487, \"endPort\": 488}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 490, \"endPort\": 491}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 493, \"endPort\": 494}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 496, \"endPort\": 497}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 499, \"endPort\": 500}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 502, \"endPort\": 503}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 505, \"endPort\": 506}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 508, \"endPort\": 509}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 511, \"endPort\": 512}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 514, \"endPort\": 515}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 517, \"endPort\": 518}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 520, \"endPort\": 521}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 523, \"endPort\": 524}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 526, \"endPort\": 527}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 529, \"endPort\": 530}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 532, \"endPort\": 533}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 535, \"endPort\": 536}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 538, \"endPort\": 539}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 541, \"endPort\": 542}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 544, \"endPort\": 545}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 547, \"endPort\": 548}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 550, \"endPort\": 551}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 553, \"endPort\": 554}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 556, \"endPort\": 557}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 559, \"endPort\": 560}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 562, \"endPort\": 563}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 565, \"endPort\": 566}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 568, \"endPort\": 569}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 571, \"endPort\": 572}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 574, \"endPort\": 575}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 577, \"endPort\": 578}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 580, \"endPort\": 581}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 583, \"endPort\": 584}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 586, \"endPort\": 587}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 589, \"endPort\": 590}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 592, \"endPort\": 593}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 595, \"endPort\": 596}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 598, \"endPort\": 599}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 601, \"endPort\": 602}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 604, \"endPort\": 605}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 607, \"endPort\": 608}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 610, \"endPort\": 611}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 613, \"endPort\": 614}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 616, \"endPort\": 617}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 619, \"endPort\": 620}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 622, \"endPort\": 623}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 625, \"endPort\": 626}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 628, \"endPort\": 629}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 631, \"endPort\": 632}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 634, \"endPort\": 635}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 637, \"endPort\": 638}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 640, \"endPort\": 641}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 643, \"endPort\": 644}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 646, \"endPort\": 647}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 649, \"endPort\": 650}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 652, \"endPort\": 653}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 655, \"endPort\": 656}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 658, \"endPort\": 659}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 661, \"endPort\": 662}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 664, \"endPort\": 665}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 667, \"endPort\": 668}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 670, \"endPort\": 671}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 673, \"endPort\": 674}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 676, \"endPort\": 677}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 679, \"endPort\": 680}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 682, \"endPort\": 683}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 685, \"endPort\": 686}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 688, \"endPort\": 689}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 691, \"endPort\": 692}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 694, \"endPort\": 695}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 697, \"endPort\": 698}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 700, \"endPort\": 701}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 703, \"endPort\": 704}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 706, \"endPort\": 707}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 709, \"endPort\": 710}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 712, \"endPort\": 713}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 715, \"endPort\": 716}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 718, \"endPort\": 719}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 721, \"endPort\": 722}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 724, \"endPort\": 725}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 727, \"endPort\": 728}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 730, \"endPort\": 731}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 733, \"endPort\": 734}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 736, \"endPort\": 737}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 739, \"endPort\": 740}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 742, \"endPort\": 743}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 745, \"endPort\": 746}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 748, \"endPort\": 749}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 751, \"endPort\": 752}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 754, \"endPort\": 755}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 757, \"endPort\": 758}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 760, \"endPort\": 761}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 763, \"endPort\": 764}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 766, \"endPort\": 767}], \"deny\": [{\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 1, \"endPort\": 2}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 4, \"endPort\": 5}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 7, \"endPort\": 8}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 10, \"endPort\": 11}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 13, \"endPort\": 14}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 16, \"endPort\": 17}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 19, \"endPort\": 20}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 22, \"endPort\": 23}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 25, \"endPort\": 26}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 28, \"endPort\": 29}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 31, \"endPort\": 32}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 34, \"endPort\": 35}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 37, \"endPort\": 38}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 40, \"endPort\": 41}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 43, \"endPort\": 44}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 46, \"endPort\": 47}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 49, \"endPort\": 50}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 52, \"endPort\": 53}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 55, \"endPort\": 56}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 58, \"endPort\": 59}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 61, \"endPort\": 62}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 64, \"endPort\": 65}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 67, \"endPort\": 68}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 70, \"endPort\": 71}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 73, \"endPort\": 74}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 76, \"endPort\": 77}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 79, \"endPort\": 80}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 82, \"endPort\": 83}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 85, \"endPort\": 86}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 88, \"endPort\": 89}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 91, \"endPort\": 92}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 94, \"endPort\": 95}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 97, \"endPort\": 98}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 100, \"endPort\": 101}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 103, \"endPort\": 104}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 106, \"endPort\": 107}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 109, \"endPort\": 110}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 112, \"endPort\": 113}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 115, \"endPort\": 116}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 118, \"endPort\": 119}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 121, \"endPort\": 122}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 124, \"endPort\": 125}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 127, \"endPort\": 128}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 130, \"endPort\": 131}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 133, \"endPort\": 134}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 136, \"endPort\": 137}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 139, \"endPort\": 140}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 142, \"endPort\": 143}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 145, \"endPort\": 146}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 148, \"endPort\": 149}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 151, \"endPort\": 152}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 154, \"endPort\": 155}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 157, \"endPort\": 158}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 160, \"endPort\": 161}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 163, \"endPort\": 164}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 166, \"endPort\": 167}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 169, \"endPort\": 170}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 172, \"endPort\": 173}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 175, \"endPort\": 176}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 178, \"endPort\": 179}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 181, \"endPort\": 182}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 184, \"endPort\": 185}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 187, \"endPort\": 188}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 190, \"endPort\": 191}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 193, \"endPort\": 194}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 196, \"endPort\": 197}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 199, \"endPort\": 200}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 202, \"endPort\": 203}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 205, \"endPort\": 206}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 208, \"endPort\": 209}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 211, \"endPort\": 212}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 214, \"endPort\": 215}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 217, \"endPort\": 218}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 220, \"endPort\": 221}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 223, \"endPort\": 224}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 226, \"endPort\": 227}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 229, \"endPort\": 230}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 232, \"endPort\": 233}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 235, \"endPort\": 236}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 238, \"endPort\": 239}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 241, \"endPort\": 242}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 244, \"endPort\": 245}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 247, \"endPort\": 248}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 250, \"endPort\": 251}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 253, \"endPort\": 254}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 256, \"endPort\": 257}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 259, \"endPort\": 260}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 262, \"endPort\": 263}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 265, \"endPort\": 266}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 268, \"endPort\": 269}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 271, \"endPort\": 272}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 274, \"endPort\": 275}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 277, \"endPort\": 278}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 280, \"endPort\": 281}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 283, \"endPort\": 284}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 286, \"endPort\": 287}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 289, \"endPort\": 290}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 292, \"endPort\": 293}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 295, \"endPort\": 296}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 298, \"endPort\": 299}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 301, \"endPort\": 302}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 304, \"endPort\": 305}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 307, \"endPort\": 308}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 310, \"endPort\": 311}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 313, \"endPort\": 314}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 316, \"endPort\": 317}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 319, \"endPort\": 320}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 322, \"endPort\": 323}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 325, \"endPort\": 326}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 328, \"endPort\": 329}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 331, \"endPort\": 332}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 334, \"endPort\": 335}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 337, \"endPort\": 338}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 340, \"endPort\": 341}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 343, \"endPort\": 344}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 346, \"endPort\": 347}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 349, \"endPort\": 350}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 352, \"endPort\": 353}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 355, \"endPort\": 356}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 358, \"endPort\": 359}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 361, \"endPort\": 362}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 364, \"endPort\": 365}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 367, \"endPort\": 368}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 370, \"endPort\": 371}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 373, \"endPort\": 374}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 376, \"endPort\": 377}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 379, \"endPort\": 380}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 382, \"endPort\": 383}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 385, \"endPort\": 386}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 388, \"endPort\": 389}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 391, \"endPort\": 392}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 394, \"endPort\": 395}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 397, \"endPort\": 398}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 400, \"endPort\": 401}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 403, \"endPort\": 404}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 406, \"endPort\": 407}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 409, \"endPort\": 410}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 412, \"endPort\": 413}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 415, \"endPort\": 416}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 418, \"endPort\": 419}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 421, \"endPort\": 422}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 424, \"endPort\": 425}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 427, \"endPort\": 428}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 430, \"endPort\": 431}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 433, \"endPort\": 434}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 436, \"endPort\": 437}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 439, \"endPort\": 440}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 442, \"endPort\": 443}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 445, \"endPort\": 446}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 448, \"endPort\": 449}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 451, \"endPort\": 452}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 454, \"endPort\": 455}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 457, \"endPort\": 458}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 460, \"endPort\": 461}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 463, \"endPort\": 464}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 466, \"endPort\": 467}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 469, \"endPort\": 470}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 472, \"endPort\": 473}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 475, \"endPort\": 476}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 478, \"endPort\": 479}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 481, \"endPort\": 482}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 484, \"endPort\": 485}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 487, \"endPort\": 488}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 490, \"endPort\": 491}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 493, \"endPort\": 494}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 496, \"endPort\": 497}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 499, \"endPort\": 500}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 502, \"endPort\": 503}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 505, \"endPort\": 506}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 508, \"endPort\": 509}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 511, \"endPort\": 512}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 514, \"endPort\": 515}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 517, \"endPort\": 518}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 520, \"endPort\": 521}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 523, \"endPort\": 524}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 526, \"endPort\": 527}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 529, \"endPort\": 530}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 532, \"endPort\": 533}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 535, \"endPort\": 536}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 538, \"endPort\": 539}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 541, \"endPort\": 542}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 544, \"endPort\": 545}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 547, \"endPort\": 548}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 550, \"endPort\": 551}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 553, \"endPort\": 554}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 556, \"endPort\": 557}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 559, \"endPort\": 560}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 562, \"endPort\": 563}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 565, \"endPort\": 566}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 568, \"endPort\": 569}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 571, \"endPort\": 572}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 574, \"endPort\": 575}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 577, \"endPort\": 578}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 580, \"endPort\": 581}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 583, \"endPort\": 584}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 586, \"endPort\": 587}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 589, \"endPort\": 590}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 592, \"endPort\": 593}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 595, \"endPort\": 596}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 598, \"endPort\": 599}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 601, \"endPort\": 602}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 604, \"endPort\": 605}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 607, \"endPort\": 608}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 610, \"endPort\": 611}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 613, \"endPort\": 614}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 616, \"endPort\": 617}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 619, \"endPort\": 620}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 622, \"endPort\": 623}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 625, \"endPort\": 626}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 628, \"endPort\": 629}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 631, \"endPort\": 632}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 634, \"endPort\": 635}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 637, \"endPort\": 638}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 640, \"endPort\": 641}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 643, \"endPort\": 644}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 646, \"endPort\": 647}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 649, \"endPort\": 650}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 652, \"endPort\": 653}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 655, \"endPort\": 656}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 658, \"endPort\": 659}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 661, \"endPort\": 662}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 664, \"endPort\": 665}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 667, \"endPort\": 668}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 670, \"endPort\": 671}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 673, \"endPort\": 674}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 676, \"endPort\": 677}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 679, \"endPort\": 680}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 682, \"endPort\": 683}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 685, \"endPort\": 686}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 688, \"endPort\": 689}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 691, \"endPort\": 692}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 694, \"endPort\": 695}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 697, \"endPort\": 698}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 700, \"endPort\": 701}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 703, \"endPort\": 704}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 706, \"endPort\": 707}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 709, \"endPort\": 710}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 712, \"endPort\": 713}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 715, \"endPort\": 716}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 718, \"endPort\": 719}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 721, \"endPort\": 722}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 724, \"endPort\": 725}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 727, \"endPort\": 728}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 730, \"endPort\": 731}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 733, \"endPort\": 734}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 736, \"endPort\": 737}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 739, \"endPort\": 740}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 742, \"endPort\": 743}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 745, \"endPort\": 746}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 748, \"endPort\": 749}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 751, \"endPort\": 752}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 754, \"endPort\": 755}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 757, \"endPort\": 758}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 760, \"endPort\": 761}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 763, \"endPort\": 764}, {\"cidr\": \"198.51.100.1\", \"protocol\": \"udp\", \"port\": 766, \"endPort\": 767}]}"}],"expected":{"allow":["192.0.2.1/32:tcp:1-2","192.0.2.1/32:tcp:4-5","192.0.2.1/32:tcp:7-8","192.0.2.1/32:tcp:10-11","192.0.2.1/32:tcp:13-14","192.0.2.1/32:tcp:16-17","192.0.2.1/32:tcp:19-20","192.0.2.1/32:tcp:22-23","192.0.2.1/32:tcp:25-26","192.0.2.1/32:tcp:28-29","192.0.2.1/32:tcp:31-32","192.0.2.1/32:tcp:34-35","192.0.2.1/32:tcp:37-38","192.0.2.1/32:tcp:40-41","192.0.2.1/32:tcp:43-44","192.0.2.1/32:tcp:46-47","192.0.2.1/32:tcp:49-50","192.0.2.1/32:tcp:52-53","192.0.2.1/32:tcp:55-56","192.0.2.1/32:tcp:58-59","192.0.2.1/32:tcp:61-62","192.0.2.1/32:tcp:64-65","192.0.2.1/32:tcp:67-68","192.0.2.1/32:tcp:70-71","192.0.2.1/32:tcp:73-74","192.0.2.1/32:tcp:76-77","192.0.2.1/32:tcp:79-80","192.0.2.1/32:tcp:82-83","192.0.2.1/32:tcp:85-86","192.0.2.1/32:tcp:88-89","192.0.2.1/32:tcp:91-92","192.0.2.1/32:tcp:94-95","192.0.2.1/32:tcp:97-98","192.0.2.1/32:tcp:100-101","192.0.2.1/32:tcp:103-104","192.0.2.1/32:tcp:106-107","192.0.2.1/32:tcp:109-110","192.0.2.1/32:tcp:112-113","192.0.2.1/32:tcp:115-116","192.0.2.1/32:tcp:118-119","192.0.2.1/32:tcp:121-122","192.0.2.1/32:tcp:124-125","192.0.2.1/32:tcp:127-128","192.0.2.1/32:tcp:130-131","192.0.2.1/32:tcp:133-134","192.0.2.1/32:tcp:136-137","192.0.2.1/32:tcp:139-140","192.0.2.1/32:tcp:142-143","192.0.2.1/32:tcp:145-146","192.0.2.1/32:tcp:148-149","192.0.2.1/32:tcp:151-152","192.0.2.1/32:tcp:154-155","192.0.2.1/32:tcp:157-158","192.0.2.1/32:tcp:160-161","192.0.2.1/32:tcp:163-164","192.0.2.1/32:tcp:166-167","192.0.2.1/32:tcp:169-170","192.0.2.1/32:tcp:172-173","192.0.2.1/32:tcp:175-176","192.0.2.1/32:tcp:178-179","192.0.2.1/32:tcp:181-182","192.0.2.1/32:tcp:184-185","192.0.2.1/32:tcp:187-188","192.0.2.1/32:tcp:190-191","192.0.2.1/32:tcp:193-194","192.0.2.1/32:tcp:196-197","192.0.2.1/32:tcp:199-200","192.0.2.1/32:tcp:202-203","192.0.2.1/32:tcp:205-206","192.0.2.1/32:tcp:208-209","192.0.2.1/32:tcp:211-212","192.0.2.1/32:tcp:214-215","192.0.2.1/32:tcp:217-218","192.0.2.1/32:tcp:220-221","192.0.2.1/32:tcp:223-224","192.0.2.1/32:tcp:226-227","192.0.2.1/32:tcp:229-230","192.0.2.1/32:tcp:232-233","192.0.2.1/32:tcp:235-236","192.0.2.1/32:tcp:238-239","192.0.2.1/32:tcp:241-242","192.0.2.1/32:tcp:244-245","192.0.2.1/32:tcp:247-248","192.0.2.1/32:tcp:250-251","192.0.2.1/32:tcp:253-254","192.0.2.1/32:tcp:256-257","192.0.2.1/32:tcp:259-260","192.0.2.1/32:tcp:262-263","192.0.2.1/32:tcp:265-266","192.0.2.1/32:tcp:268-269","192.0.2.1/32:tcp:271-272","192.0.2.1/32:tcp:274-275","192.0.2.1/32:tcp:277-278","192.0.2.1/32:tcp:280-281","192.0.2.1/32:tcp:283-284","192.0.2.1/32:tcp:286-287","192.0.2.1/32:tcp:289-290","192.0.2.1/32:tcp:292-293","192.0.2.1/32:tcp:295-296","192.0.2.1/32:tcp:298-299","192.0.2.1/32:tcp:301-302","192.0.2.1/32:tcp:304-305","192.0.2.1/32:tcp:307-308","192.0.2.1/32:tcp:310-311","192.0.2.1/32:tcp:313-314","192.0.2.1/32:tcp:316-317","192.0.2.1/32:tcp:319-320","192.0.2.1/32:tcp:322-323","192.0.2.1/32:tcp:325-326","192.0.2.1/32:tcp:328-329","192.0.2.1/32:tcp:331-332","192.0.2.1/32:tcp:334-335","192.0.2.1/32:tcp:337-338","192.0.2.1/32:tcp:340-341","192.0.2.1/32:tcp:343-344","192.0.2.1/32:tcp:346-347","192.0.2.1/32:tcp:349-350","192.0.2.1/32:tcp:352-353","192.0.2.1/32:tcp:355-356","192.0.2.1/32:tcp:358-359","192.0.2.1/32:tcp:361-362","192.0.2.1/32:tcp:364-365","192.0.2.1/32:tcp:367-368","192.0.2.1/32:tcp:370-371","192.0.2.1/32:tcp:373-374","192.0.2.1/32:tcp:376-377","192.0.2.1/32:tcp:379-380","192.0.2.1/32:tcp:382-383","192.0.2.1/32:tcp:385-386","192.0.2.1/32:tcp:388-389","192.0.2.1/32:tcp:391-392","192.0.2.1/32:tcp:394-395","192.0.2.1/32:tcp:397-398","192.0.2.1/32:tcp:400-401","192.0.2.1/32:tcp:403-404","192.0.2.1/32:tcp:406-407","192.0.2.1/32:tcp:409-410","192.0.2.1/32:tcp:412-413","192.0.2.1/32:tcp:415-416","192.0.2.1/32:tcp:418-419","192.0.2.1/32:tcp:421-422","192.0.2.1/32:tcp:424-425","192.0.2.1/32:tcp:427-428","192.0.2.1/32:tcp:430-431","192.0.2.1/32:tcp:433-434","192.0.2.1/32:tcp:436-437","192.0.2.1/32:tcp:439-440","192.0.2.1/32:tcp:442-443","192.0.2.1/32:tcp:445-446","192.0.2.1/32:tcp:448-449","192.0.2.1/32:tcp:451-452","192.0.2.1/32:tcp:454-455","192.0.2.1/32:tcp:457-458","192.0.2.1/32:tcp:460-461","192.0.2.1/32:tcp:463-464","192.0.2.1/32:tcp:466-467","192.0.2.1/32:tcp:469-470","192.0.2.1/32:tcp:472-473","192.0.2.1/32:tcp:475-476","192.0.2.1/32:tcp:478-479","192.0.2.1/32:tcp:481-482","192.0.2.1/32:tcp:484-485","192.0.2.1/32:tcp:487-488","192.0.2.1/32:tcp:490-491","192.0.2.1/32:tcp:493-494","192.0.2.1/32:tcp:496-497","192.0.2.1/32:tcp:499-500","192.0.2.1/32:tcp:502-503","192.0.2.1/32:tcp:505-506","192.0.2.1/32:tcp:508-509","192.0.2.1/32:tcp:511-512","192.0.2.1/32:tcp:514-515","192.0.2.1/32:tcp:517-518","192.0.2.1/32:tcp:520-521","192.0.2.1/32:tcp:523-524","192.0.2.1/32:tcp:526-527","192.0.2.1/32:tcp:529-530","192.0.2.1/32:tcp:532-533","192.0.2.1/32:tcp:535-536","192.0.2.1/32:tcp:538-539","192.0.2.1/32:tcp:541-542","192.0.2.1/32:tcp:544-545","192.0.2.1/32:tcp:547-548","192.0.2.1/32:tcp:550-551","192.0.2.1/32:tcp:553-554","192.0.2.1/32:tcp:556-557","192.0.2.1/32:tcp:559-560","192.0.2.1/32:tcp:562-563","192.0.2.1/32:tcp:565-566","192.0.2.1/32:tcp:568-569","192.0.2.1/32:tcp:571-572","192.0.2.1/32:tcp:574-575","192.0.2.1/32:tcp:577-578","192.0.2.1/32:tcp:580-581","192.0.2.1/32:tcp:583-584","192.0.2.1/32:tcp:586-587","192.0.2.1/32:tcp:589-590","192.0.2.1/32:tcp:592-593","192.0.2.1/32:tcp:595-596","192.0.2.1/32:tcp:598-599","192.0.2.1/32:tcp:601-602","192.0.2.1/32:tcp:604-605","192.0.2.1/32:tcp:607-608","192.0.2.1/32:tcp:610-611","192.0.2.1/32:tcp:613-614","192.0.2.1/32:tcp:616-617","192.0.2.1/32:tcp:619-620","192.0.2.1/32:tcp:622-623","192.0.2.1/32:tcp:625-626","192.0.2.1/32:tcp:628-629","192.0.2.1/32:tcp:631-632","192.0.2.1/32:tcp:634-635","192.0.2.1/32:tcp:637-638","192.0.2.1/32:tcp:640-641","192.0.2.1/32:tcp:643-644","192.0.2.1/32:tcp:646-647","192.0.2.1/32:tcp:649-650","192.0.2.1/32:tcp:652-653","192.0.2.1/32:tcp:655-656","192.0.2.1/32:tcp:658-659","192.0.2.1/32:tcp:661-662","192.0.2.1/32:tcp:664-665","192.0.2.1/32:tcp:667-668","192.0.2.1/32:tcp:670-671","192.0.2.1/32:tcp:673-674","192.0.2.1/32:tcp:676-677","192.0.2.1/32:tcp:679-680","192.0.2.1/32:tcp:682-683","192.0.2.1/32:tcp:685-686","192.0.2.1/32:tcp:688-689","192.0.2.1/32:tcp:691-692","192.0.2.1/32:tcp:694-695","192.0.2.1/32:tcp:697-698","192.0.2.1/32:tcp:700-701","192.0.2.1/32:tcp:703-704","192.0.2.1/32:tcp:706-707","192.0.2.1/32:tcp:709-710","192.0.2.1/32:tcp:712-713","192.0.2.1/32:tcp:715-716","192.0.2.1/32:tcp:718-719","192.0.2.1/32:tcp:721-722","192.0.2.1/32:tcp:724-725","192.0.2.1/32:tcp:727-728","192.0.2.1/32:tcp:730-731","192.0.2.1/32:tcp:733-734","192.0.2.1/32:tcp:736-737","192.0.2.1/32:tcp:739-740","192.0.2.1/32:tcp:742-743","192.0.2.1/32:tcp:745-746","192.0.2.1/32:tcp:748-749","192.0.2.1/32:tcp:751-752","192.0.2.1/32:tcp:754-755","192.0.2.1/32:tcp:757-758","192.0.2.1/32:tcp:760-761","192.0.2.1/32:tcp:763-764","192.0.2.1/32:tcp:766-767"],"deny":["198.51.100.1/32:udp:1-2","198.51.100.1/32:udp:4-5","198.51.100.1/32:udp:7-8","198.51.100.1/32:udp:10-11","198.51.100.1/32:udp:13-14","198.51.100.1/32:udp:16-17","198.51.100.1/32:udp:19-20","198.51.100.1/32:udp:22-23","198.51.100.1/32:udp:25-26","198.51.100.1/32:udp:28-29","198.51.100.1/32:udp:31-32","198.51.100.1/32:udp:34-35","198.51.100.1/32:udp:37-38","198.51.100.1/32:udp:40-41","198.51.100.1/32:udp:43-44","198.51.100.1/32:udp:46-47","198.51.100.1/32:udp:49-50","198.51.100.1/32:udp:52-53","198.51.100.1/32:udp:55-56","198.51.100.1/32:udp:58-59","198.51.100.1/32:udp:61-62","198.51.100.1/32:udp:64-65","198.51.100.1/32:udp:67-68","198.51.100.1/32:udp:70-71","198.51.100.1/32:udp:73-74","198.51.100.1/32:udp:76-77","198.51.100.1/32:udp:79-80","198.51.100.1/32:udp:82-83","198.51.100.1/32:udp:85-86","198.51.100.1/32:udp:88-89","198.51.100.1/32:udp:91-92","198.51.100.1/32:udp:94-95","198.51.100.1/32:udp:97-98","198.51.100.1/32:udp:100-101","198.51.100.1/32:udp:103-104","198.51.100.1/32:udp:106-107","198.51.100.1/32:udp:109-110","198.51.100.1/32:udp:112-113","198.51.100.1/32:udp:115-116","198.51.100.1/32:udp:118-119","198.51.100.1/32:udp:121-122","198.51.100.1/32:udp:124-125","198.51.100.1/32:udp:127-128","198.51.100.1/32:udp:130-131","198.51.100.1/32:udp:133-134","198.51.100.1/32:udp:136-137","198.51.100.1/32:udp:139-140","198.51.100.1/32:udp:142-143","198.51.100.1/32:udp:145-146","198.51.100.1/32:udp:148-149","198.51.100.1/32:udp:151-152","198.51.100.1/32:udp:154-155","198.51.100.1/32:udp:157-158","198.51.100.1/32:udp:160-161","198.51.100.1/32:udp:163-164","198.51.100.1/32:udp:166-167","198.51.100.1/32:udp:169-170","198.51.100.1/32:udp:172-173","198.51.100.1/32:udp:175-176","198.51.100.1/32:udp:178-179","198.51.100.1/32:udp:181-182","198.51.100.1/32:udp:184-185","198.51.100.1/32:udp:187-188","198.51.100.1/32:udp:190-191","198.51.100.1/32:udp:193-194","198.51.100.1/32:udp:196-197","198.51.100.1/32:udp:199-200","198.51.100.1/32:udp:202-203","198.51.100.1/32:udp:205-206","198.51.100.1/32:udp:208-209","198.51.100.1/32:udp:211-212","198.51.100.1/32:udp:214-215","198.51.100.1/32:udp:217-218","198.51.100.1/32:udp:220-221","198.51.100.1/32:udp:223-224","198.51.100.1/32:udp:226-227","198.51.100.1/32:udp:229-230","198.51.100.1/32:udp:232-233","198.51.100.1/32:udp:235-236","198.51.100.1/32:udp:238-239","198.51.100.1/32:udp:241-242","198.51.100.1/32:udp:244-245","198.51.100.1/32:udp:247-248","198.51.100.1/32:udp:250-251","198.51.100.1/32:udp:253-254","198.51.100.1/32:udp:256-257","198.51.100.1/32:udp:259-260","198.51.100.1/32:udp:262-263","198.51.100.1/32:udp:265-266","198.51.100.1/32:udp:268-269","198.51.100.1/32:udp:271-272","198.51.100.1/32:udp:274-275","198.51.100.1/32:udp:277-278","198.51.100.1/32:udp:280-281","198.51.100.1/32:udp:283-284","198.51.100.1/32:udp:286-287","198.51.100.1/32:udp:289-290","198.51.100.1/32:udp:292-293","198.51.100.1/32:udp:295-296","198.51.100.1/32:udp:298-299","198.51.100.1/32:udp:301-302","198.51.100.1/32:udp:304-305","198.51.100.1/32:udp:307-308","198.51.100.1/32:udp:310-311","198.51.100.1/32:udp:313-314","198.51.100.1/32:udp:316-317","198.51.100.1/32:udp:319-320","198.51.100.1/32:udp:322-323","198.51.100.1/32:udp:325-326","198.51.100.1/32:udp:328-329","198.51.100.1/32:udp:331-332","198.51.100.1/32:udp:334-335","198.51.100.1/32:udp:337-338","198.51.100.1/32:udp:340-341","198.51.100.1/32:udp:343-344","198.51.100.1/32:udp:346-347","198.51.100.1/32:udp:349-350","198.51.100.1/32:udp:352-353","198.51.100.1/32:udp:355-356","198.51.100.1/32:udp:358-359","198.51.100.1/32:udp:361-362","198.51.100.1/32:udp:364-365","198.51.100.1/32:udp:367-368","198.51.100.1/32:udp:370-371","198.51.100.1/32:udp:373-374","198.51.100.1/32:udp:376-377","198.51.100.1/32:udp:379-380","198.51.100.1/32:udp:382-383","198.51.100.1/32:udp:385-386","198.51.100.1/32:udp:388-389","198.51.100.1/32:udp:391-392","198.51.100.1/32:udp:394-395","198.51.100.1/32:udp:397-398","198.51.100.1/32:udp:400-401","198.51.100.1/32:udp:403-404","198.51.100.1/32:udp:406-407","198.51.100.1/32:udp:409-410","198.51.100.1/32:udp:412-413","198.51.100.1/32:udp:415-416","198.51.100.1/32:udp:418-419","198.51.100.1/32:udp:421-422","198.51.100.1/32:udp:424-425","198.51.100.1/32:udp:427-428","198.51.100.1/32:udp:430-431","198.51.100.1/32:udp:433-434","198.51.100.1/32:udp:436-437","198.51.100.1/32:udp:439-440","198.51.100.1/32:udp:442-443","198.51.100.1/32:udp:445-446","198.51.100.1/32:udp:448-449","198.51.100.1/32:udp:451-452","198.51.100.1/32:udp:454-455","198.51.100.1/32:udp:457-458","198.51.100.1/32:udp:460-461","198.51.100.1/32:udp:463-464","198.51.100.1/32:udp:466-467","198.51.100.1/32:udp:469-470","198.51.100.1/32:udp:472-473","198.51.100.1/32:udp:475-476","198.51.100.1/32:udp:478-479","198.51.100.1/32:udp:481-482","198.51.100.1/32:udp:484-485","198.51.100.1/32:udp:487-488","198.51.100.1/32:udp:490-491","198.51.100.1/32:udp:493-494","198.51.100.1/32:udp:496-497","198.51.100.1/32:udp:499-500","198.51.100.1/32:udp:502-503","198.51.100.1/32:udp:505-506","198.51.100.1/32:udp:508-509","198.51.100.1/32:udp:511-512","198.51.100.1/32:udp:514-515","198.51.100.1/32:udp:517-518","198.51.100.1/32:udp:520-521","198.51.100.1/32:udp:523-524","198.51.100.1/32:udp:526-527","198.51.100.1/32:udp:529-530","198.51.100.1/32:udp:532-533","198.51.100.1/32:udp:535-536","198.51.100.1/32:udp:538-539","198.51.100.1/32:udp:541-542","198.51.100.1/32:udp:544-545","198.51.100.1/32:udp:547-548","198.51.100.1/32:udp:550-551","198.51.100.1/32:udp:553-554","198.51.100.1/32:udp:556-557","198.51.100.1/32:udp:559-560","198.51.100.1/32:udp:562-563","198.51.100.1/32:udp:565-566","198.51.100.1/32:udp:568-569","198.51.100.1/32:udp:571-572","198.51.100.1/32:udp:574-575","198.51.100.1/32:udp:577-578","198.51.100.1/32:udp:580-581","198.51.100.1/32:udp:583-584","198.51.100.1/32:udp:586-587","198.51.100.1/32:udp:589-590","198.51.100.1/32:udp:592-593","198.51.100.1/32:udp:595-596","198.51.100.1/32:udp:598-599","198.51.100.1/32:udp:601-602","198.51.100.1/32:udp:604-605","198.51.100.1/32:udp:607-608","198.51.100.1/32:udp:610-611","198.51.100.1/32:udp:613-614","198.51.100.1/32:udp:616-617","198.51.100.1/32:udp:619-620","198.51.100.1/32:udp:622-623","198.51.100.1/32:udp:625-626","198.51.100.1/32:udp:628-629","198.51.100.1/32:udp:631-632","198.51.100.1/32:udp:634-635","198.51.100.1/32:udp:637-638","198.51.100.1/32:udp:640-641","198.51.100.1/32:udp:643-644","198.51.100.1/32:udp:646-647","198.51.100.1/32:udp:649-650","198.51.100.1/32:udp:652-653","198.51.100.1/32:udp:655-656","198.51.100.1/32:udp:658-659","198.51.100.1/32:udp:661-662","198.51.100.1/32:udp:664-665","198.51.100.1/32:udp:667-668","198.51.100.1/32:udp:670-671","198.51.100.1/32:udp:673-674","198.51.100.1/32:udp:676-677","198.51.100.1/32:udp:679-680","198.51.100.1/32:udp:682-683","198.51.100.1/32:udp:685-686","198.51.100.1/32:udp:688-689","198.51.100.1/32:udp:691-692","198.51.100.1/32:udp:694-695","198.51.100.1/32:udp:697-698","198.51.100.1/32:udp:700-701","198.51.100.1/32:udp:703-704","198.51.100.1/32:udp:706-707","198.51.100.1/32:udp:709-710","198.51.100.1/32:udp:712-713","198.51.100.1/32:udp:715-716","198.51.100.1/32:udp:718-719","198.51.100.1/32:udp:721-722","198.51.100.1/32:udp:724-725","198.51.100.1/32:udp:727-728","198.51.100.1/32:udp:730-731","198.51.100.1/32:udp:733-734","198.51.100.1/32:udp:736-737","198.51.100.1/32:udp:739-740","198.51.100.1/32:udp:742-743","198.51.100.1/32:udp:745-746","198.51.100.1/32:udp:748-749","198.51.100.1/32:udp:751-752","198.51.100.1/32:udp:754-755","198.51.100.1/32:udp:757-758","198.51.100.1/32:udp:760-761","198.51.100.1/32:udp:763-764","198.51.100.1/32:udp:766-767"]}}, + {"name":"vector: test_address_only_rule_covers_large_protocol_range #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 65535}, {\"cidr\": \"192.0.2.0/24\"}]}"}],"expected":{"allow":["192.0.2.0/24"],"deny":[]}}, + {"name":"vector: test_address_only_rule_covers_large_protocol_range #2","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\"}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 65535}]}"}],"expected":{"allow":["192.0.2.0/24"],"deny":[]}}, + {"name":"vector: test_address_only_rules_cover_protocol_wide_rules #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"udp\"}, {\"cidr\": \"192.0.2.0/25\"}, {\"cidr\": \"192.0.2.0/25\", \"protocol\": \"icmp\"}, {\"cidr\": \"192.0.2.128/25\", \"protocol\": \"udp\", \"port\": 53}]}"}],"expected":{"allow":["192.0.2.0/24:udp","192.0.2.0/25"],"deny":[]}}, + {"name":"vector: test_both_families_share_the_native_rule_budget #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.1\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 1}, {\"protocol\": \"tcp\", \"port\": 3}, {\"protocol\": \"tcp\", \"port\": 5}, {\"protocol\": \"tcp\", \"port\": 7}, {\"protocol\": \"tcp\", \"port\": 9}, {\"protocol\": \"tcp\", \"port\": 11}, {\"protocol\": \"tcp\", \"port\": 13}, {\"protocol\": \"tcp\", \"port\": 15}, {\"protocol\": \"tcp\", \"port\": 17}, {\"protocol\": \"tcp\", \"port\": 19}, {\"protocol\": \"tcp\", \"port\": 21}, {\"protocol\": \"tcp\", \"port\": 23}, {\"protocol\": \"tcp\", \"port\": 25}, {\"protocol\": \"tcp\", \"port\": 27}, {\"protocol\": \"tcp\", \"port\": 29}, {\"protocol\": \"tcp\", \"port\": 31}, {\"protocol\": \"tcp\", \"port\": 33}, {\"protocol\": \"tcp\", \"port\": 35}, {\"protocol\": \"tcp\", \"port\": 37}, {\"protocol\": \"tcp\", \"port\": 39}, {\"protocol\": \"tcp\", \"port\": 41}, {\"protocol\": \"tcp\", \"port\": 43}, {\"protocol\": \"tcp\", \"port\": 45}, {\"protocol\": \"tcp\", \"port\": 47}, {\"protocol\": \"tcp\", \"port\": 49}, {\"protocol\": \"tcp\", \"port\": 51}, {\"protocol\": \"tcp\", \"port\": 53}, {\"protocol\": \"tcp\", \"port\": 55}, {\"protocol\": \"tcp\", \"port\": 57}, {\"protocol\": \"tcp\", \"port\": 59}, {\"protocol\": \"tcp\", \"port\": 61}, {\"protocol\": \"tcp\", \"port\": 63}, {\"protocol\": \"tcp\", \"port\": 65}, {\"protocol\": \"tcp\", \"port\": 67}, {\"protocol\": \"tcp\", \"port\": 69}, {\"protocol\": \"tcp\", \"port\": 71}, {\"protocol\": \"tcp\", \"port\": 73}, {\"protocol\": \"tcp\", \"port\": 75}, {\"protocol\": \"tcp\", \"port\": 77}, {\"protocol\": \"tcp\", \"port\": 79}, {\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 83}, {\"protocol\": \"tcp\", \"port\": 85}, {\"protocol\": \"tcp\", \"port\": 87}, {\"protocol\": \"tcp\", \"port\": 89}, {\"protocol\": \"tcp\", \"port\": 91}, {\"protocol\": \"tcp\", \"port\": 93}, {\"protocol\": \"tcp\", \"port\": 95}, {\"protocol\": \"tcp\", \"port\": 97}, {\"protocol\": \"tcp\", \"port\": 99}, {\"protocol\": \"tcp\", \"port\": 101}, {\"protocol\": \"tcp\", \"port\": 103}, {\"protocol\": \"tcp\", \"port\": 105}, {\"protocol\": \"tcp\", \"port\": 107}, {\"protocol\": \"tcp\", \"port\": 109}, {\"protocol\": \"tcp\", \"port\": 111}, {\"protocol\": \"tcp\", \"port\": 113}, {\"protocol\": \"tcp\", \"port\": 115}, {\"protocol\": \"tcp\", \"port\": 117}, {\"protocol\": \"tcp\", \"port\": 119}, {\"protocol\": \"tcp\", \"port\": 121}, {\"protocol\": \"tcp\", \"port\": 123}, {\"protocol\": \"tcp\", \"port\": 125}, {\"protocol\": \"tcp\", \"port\": 127}, {\"protocol\": \"tcp\", \"port\": 129}, {\"protocol\": \"tcp\", \"port\": 131}, {\"protocol\": \"tcp\", \"port\": 133}, {\"protocol\": \"tcp\", \"port\": 135}, {\"protocol\": \"tcp\", \"port\": 137}, {\"protocol\": \"tcp\", \"port\": 139}, {\"protocol\": \"tcp\", \"port\": 141}, {\"protocol\": \"tcp\", \"port\": 143}, {\"protocol\": \"tcp\", \"port\": 145}, {\"protocol\": \"tcp\", \"port\": 147}, {\"protocol\": \"tcp\", \"port\": 149}, {\"protocol\": \"tcp\", \"port\": 151}, {\"protocol\": \"tcp\", \"port\": 153}, {\"protocol\": \"tcp\", \"port\": 155}, {\"protocol\": \"tcp\", \"port\": 157}, {\"protocol\": \"tcp\", \"port\": 159}, {\"protocol\": \"tcp\", \"port\": 161}, {\"protocol\": \"tcp\", \"port\": 163}, {\"protocol\": \"tcp\", \"port\": 165}, {\"protocol\": \"tcp\", \"port\": 167}, {\"protocol\": \"tcp\", \"port\": 169}, {\"protocol\": \"tcp\", \"port\": 171}, {\"protocol\": \"tcp\", \"port\": 173}, {\"protocol\": \"tcp\", \"port\": 175}, {\"protocol\": \"tcp\", \"port\": 177}, {\"protocol\": \"tcp\", \"port\": 179}, {\"protocol\": \"tcp\", \"port\": 181}, {\"protocol\": \"tcp\", \"port\": 183}, {\"protocol\": \"tcp\", \"port\": 185}, {\"protocol\": \"tcp\", \"port\": 187}, {\"protocol\": \"tcp\", \"port\": 189}, {\"protocol\": \"tcp\", \"port\": 191}, {\"protocol\": \"tcp\", \"port\": 193}, {\"protocol\": \"tcp\", \"port\": 195}, {\"protocol\": \"tcp\", \"port\": 197}, {\"protocol\": \"tcp\", \"port\": 199}, {\"protocol\": \"tcp\", \"port\": 201}, {\"protocol\": \"tcp\", \"port\": 203}, {\"protocol\": \"tcp\", \"port\": 205}, {\"protocol\": \"tcp\", \"port\": 207}, {\"protocol\": \"tcp\", \"port\": 209}, {\"protocol\": \"tcp\", \"port\": 211}, {\"protocol\": \"tcp\", \"port\": 213}, {\"protocol\": \"tcp\", \"port\": 215}, {\"protocol\": \"tcp\", \"port\": 217}, {\"protocol\": \"tcp\", \"port\": 219}, {\"protocol\": \"tcp\", \"port\": 221}, {\"protocol\": \"tcp\", \"port\": 223}, {\"protocol\": \"tcp\", \"port\": 225}, {\"protocol\": \"tcp\", \"port\": 227}, {\"protocol\": \"tcp\", \"port\": 229}, {\"protocol\": \"tcp\", \"port\": 231}, {\"protocol\": \"tcp\", \"port\": 233}, {\"protocol\": \"tcp\", \"port\": 235}, {\"protocol\": \"tcp\", \"port\": 237}, {\"protocol\": \"tcp\", \"port\": 239}, {\"protocol\": \"tcp\", \"port\": 241}, {\"protocol\": \"tcp\", \"port\": 243}, {\"protocol\": \"tcp\", \"port\": 245}, {\"protocol\": \"tcp\", \"port\": 247}, {\"protocol\": \"tcp\", \"port\": 249}, {\"protocol\": \"tcp\", \"port\": 251}, {\"protocol\": \"tcp\", \"port\": 253}, {\"protocol\": \"tcp\", \"port\": 255}]}, {\"to\": [{\"cidr\": \"2001:db8::1\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 1}, {\"protocol\": \"tcp\", \"port\": 3}, {\"protocol\": \"tcp\", \"port\": 5}, {\"protocol\": \"tcp\", \"port\": 7}, {\"protocol\": \"tcp\", \"port\": 9}, {\"protocol\": \"tcp\", \"port\": 11}, {\"protocol\": \"tcp\", \"port\": 13}, {\"protocol\": \"tcp\", \"port\": 15}, {\"protocol\": \"tcp\", \"port\": 17}, {\"protocol\": \"tcp\", \"port\": 19}, {\"protocol\": \"tcp\", \"port\": 21}, {\"protocol\": \"tcp\", \"port\": 23}, {\"protocol\": \"tcp\", \"port\": 25}, {\"protocol\": \"tcp\", \"port\": 27}, {\"protocol\": \"tcp\", \"port\": 29}, {\"protocol\": \"tcp\", \"port\": 31}, {\"protocol\": \"tcp\", \"port\": 33}, {\"protocol\": \"tcp\", \"port\": 35}, {\"protocol\": \"tcp\", \"port\": 37}, {\"protocol\": \"tcp\", \"port\": 39}, {\"protocol\": \"tcp\", \"port\": 41}, {\"protocol\": \"tcp\", \"port\": 43}, {\"protocol\": \"tcp\", \"port\": 45}, {\"protocol\": \"tcp\", \"port\": 47}, {\"protocol\": \"tcp\", \"port\": 49}, {\"protocol\": \"tcp\", \"port\": 51}, {\"protocol\": \"tcp\", \"port\": 53}, {\"protocol\": \"tcp\", \"port\": 55}, {\"protocol\": \"tcp\", \"port\": 57}, {\"protocol\": \"tcp\", \"port\": 59}, {\"protocol\": \"tcp\", \"port\": 61}, {\"protocol\": \"tcp\", \"port\": 63}, {\"protocol\": \"tcp\", \"port\": 65}, {\"protocol\": \"tcp\", \"port\": 67}, {\"protocol\": \"tcp\", \"port\": 69}, {\"protocol\": \"tcp\", \"port\": 71}, {\"protocol\": \"tcp\", \"port\": 73}, {\"protocol\": \"tcp\", \"port\": 75}, {\"protocol\": \"tcp\", \"port\": 77}, {\"protocol\": \"tcp\", \"port\": 79}, {\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 83}, {\"protocol\": \"tcp\", \"port\": 85}, {\"protocol\": \"tcp\", \"port\": 87}, {\"protocol\": \"tcp\", \"port\": 89}, {\"protocol\": \"tcp\", \"port\": 91}, {\"protocol\": \"tcp\", \"port\": 93}, {\"protocol\": \"tcp\", \"port\": 95}, {\"protocol\": \"tcp\", \"port\": 97}, {\"protocol\": \"tcp\", \"port\": 99}, {\"protocol\": \"tcp\", \"port\": 101}, {\"protocol\": \"tcp\", \"port\": 103}, {\"protocol\": \"tcp\", \"port\": 105}, {\"protocol\": \"tcp\", \"port\": 107}, {\"protocol\": \"tcp\", \"port\": 109}, {\"protocol\": \"tcp\", \"port\": 111}, {\"protocol\": \"tcp\", \"port\": 113}, {\"protocol\": \"tcp\", \"port\": 115}, {\"protocol\": \"tcp\", \"port\": 117}, {\"protocol\": \"tcp\", \"port\": 119}, {\"protocol\": \"tcp\", \"port\": 121}, {\"protocol\": \"tcp\", \"port\": 123}, {\"protocol\": \"tcp\", \"port\": 125}, {\"protocol\": \"tcp\", \"port\": 127}, {\"protocol\": \"tcp\", \"port\": 129}, {\"protocol\": \"tcp\", \"port\": 131}, {\"protocol\": \"tcp\", \"port\": 133}, {\"protocol\": \"tcp\", \"port\": 135}, {\"protocol\": \"tcp\", \"port\": 137}, {\"protocol\": \"tcp\", \"port\": 139}, {\"protocol\": \"tcp\", \"port\": 141}, {\"protocol\": \"tcp\", \"port\": 143}, {\"protocol\": \"tcp\", \"port\": 145}, {\"protocol\": \"tcp\", \"port\": 147}, {\"protocol\": \"tcp\", \"port\": 149}, {\"protocol\": \"tcp\", \"port\": 151}, {\"protocol\": \"tcp\", \"port\": 153}, {\"protocol\": \"tcp\", \"port\": 155}, {\"protocol\": \"tcp\", \"port\": 157}, {\"protocol\": \"tcp\", \"port\": 159}, {\"protocol\": \"tcp\", \"port\": 161}, {\"protocol\": \"tcp\", \"port\": 163}, {\"protocol\": \"tcp\", \"port\": 165}, {\"protocol\": \"tcp\", \"port\": 167}, {\"protocol\": \"tcp\", \"port\": 169}, {\"protocol\": \"tcp\", \"port\": 171}, {\"protocol\": \"tcp\", \"port\": 173}, {\"protocol\": \"tcp\", \"port\": 175}, {\"protocol\": \"tcp\", \"port\": 177}, {\"protocol\": \"tcp\", \"port\": 179}, {\"protocol\": \"tcp\", \"port\": 181}, {\"protocol\": \"tcp\", \"port\": 183}, {\"protocol\": \"tcp\", \"port\": 185}, {\"protocol\": \"tcp\", \"port\": 187}, {\"protocol\": \"tcp\", \"port\": 189}, {\"protocol\": \"tcp\", \"port\": 191}, {\"protocol\": \"tcp\", \"port\": 193}, {\"protocol\": \"tcp\", \"port\": 195}, {\"protocol\": \"tcp\", \"port\": 197}, {\"protocol\": \"tcp\", \"port\": 199}, {\"protocol\": \"tcp\", \"port\": 201}, {\"protocol\": \"tcp\", \"port\": 203}, {\"protocol\": \"tcp\", \"port\": 205}, {\"protocol\": \"tcp\", \"port\": 207}, {\"protocol\": \"tcp\", \"port\": 209}, {\"protocol\": \"tcp\", \"port\": 211}, {\"protocol\": \"tcp\", \"port\": 213}, {\"protocol\": \"tcp\", \"port\": 215}, {\"protocol\": \"tcp\", \"port\": 217}, {\"protocol\": \"tcp\", \"port\": 219}, {\"protocol\": \"tcp\", \"port\": 221}, {\"protocol\": \"tcp\", \"port\": 223}, {\"protocol\": \"tcp\", \"port\": 225}, {\"protocol\": \"tcp\", \"port\": 227}, {\"protocol\": \"tcp\", \"port\": 229}, {\"protocol\": \"tcp\", \"port\": 231}, {\"protocol\": \"tcp\", \"port\": 233}, {\"protocol\": \"tcp\", \"port\": 235}, {\"protocol\": \"tcp\", \"port\": 237}, {\"protocol\": \"tcp\", \"port\": 239}, {\"protocol\": \"tcp\", \"port\": 241}, {\"protocol\": \"tcp\", \"port\": 243}, {\"protocol\": \"tcp\", \"port\": 245}, {\"protocol\": \"tcp\", \"port\": 247}, {\"protocol\": \"tcp\", \"port\": 249}, {\"protocol\": \"tcp\", \"port\": 251}, {\"protocol\": \"tcp\", \"port\": 253}, {\"protocol\": \"tcp\", \"port\": 255}]}]}"}],"expected":{"allow":["192.0.2.1/32:tcp:1","192.0.2.1/32:tcp:3","192.0.2.1/32:tcp:5","192.0.2.1/32:tcp:7","192.0.2.1/32:tcp:9","192.0.2.1/32:tcp:11","192.0.2.1/32:tcp:13","192.0.2.1/32:tcp:15","192.0.2.1/32:tcp:17","192.0.2.1/32:tcp:19","192.0.2.1/32:tcp:21","192.0.2.1/32:tcp:23","192.0.2.1/32:tcp:25","192.0.2.1/32:tcp:27","192.0.2.1/32:tcp:29","192.0.2.1/32:tcp:31","192.0.2.1/32:tcp:33","192.0.2.1/32:tcp:35","192.0.2.1/32:tcp:37","192.0.2.1/32:tcp:39","192.0.2.1/32:tcp:41","192.0.2.1/32:tcp:43","192.0.2.1/32:tcp:45","192.0.2.1/32:tcp:47","192.0.2.1/32:tcp:49","192.0.2.1/32:tcp:51","192.0.2.1/32:tcp:53","192.0.2.1/32:tcp:55","192.0.2.1/32:tcp:57","192.0.2.1/32:tcp:59","192.0.2.1/32:tcp:61","192.0.2.1/32:tcp:63","192.0.2.1/32:tcp:65","192.0.2.1/32:tcp:67","192.0.2.1/32:tcp:69","192.0.2.1/32:tcp:71","192.0.2.1/32:tcp:73","192.0.2.1/32:tcp:75","192.0.2.1/32:tcp:77","192.0.2.1/32:tcp:79","192.0.2.1/32:tcp:81","192.0.2.1/32:tcp:83","192.0.2.1/32:tcp:85","192.0.2.1/32:tcp:87","192.0.2.1/32:tcp:89","192.0.2.1/32:tcp:91","192.0.2.1/32:tcp:93","192.0.2.1/32:tcp:95","192.0.2.1/32:tcp:97","192.0.2.1/32:tcp:99","192.0.2.1/32:tcp:101","192.0.2.1/32:tcp:103","192.0.2.1/32:tcp:105","192.0.2.1/32:tcp:107","192.0.2.1/32:tcp:109","192.0.2.1/32:tcp:111","192.0.2.1/32:tcp:113","192.0.2.1/32:tcp:115","192.0.2.1/32:tcp:117","192.0.2.1/32:tcp:119","192.0.2.1/32:tcp:121","192.0.2.1/32:tcp:123","192.0.2.1/32:tcp:125","192.0.2.1/32:tcp:127","192.0.2.1/32:tcp:129","192.0.2.1/32:tcp:131","192.0.2.1/32:tcp:133","192.0.2.1/32:tcp:135","192.0.2.1/32:tcp:137","192.0.2.1/32:tcp:139","192.0.2.1/32:tcp:141","192.0.2.1/32:tcp:143","192.0.2.1/32:tcp:145","192.0.2.1/32:tcp:147","192.0.2.1/32:tcp:149","192.0.2.1/32:tcp:151","192.0.2.1/32:tcp:153","192.0.2.1/32:tcp:155","192.0.2.1/32:tcp:157","192.0.2.1/32:tcp:159","192.0.2.1/32:tcp:161","192.0.2.1/32:tcp:163","192.0.2.1/32:tcp:165","192.0.2.1/32:tcp:167","192.0.2.1/32:tcp:169","192.0.2.1/32:tcp:171","192.0.2.1/32:tcp:173","192.0.2.1/32:tcp:175","192.0.2.1/32:tcp:177","192.0.2.1/32:tcp:179","192.0.2.1/32:tcp:181","192.0.2.1/32:tcp:183","192.0.2.1/32:tcp:185","192.0.2.1/32:tcp:187","192.0.2.1/32:tcp:189","192.0.2.1/32:tcp:191","192.0.2.1/32:tcp:193","192.0.2.1/32:tcp:195","192.0.2.1/32:tcp:197","192.0.2.1/32:tcp:199","192.0.2.1/32:tcp:201","192.0.2.1/32:tcp:203","192.0.2.1/32:tcp:205","192.0.2.1/32:tcp:207","192.0.2.1/32:tcp:209","192.0.2.1/32:tcp:211","192.0.2.1/32:tcp:213","192.0.2.1/32:tcp:215","192.0.2.1/32:tcp:217","192.0.2.1/32:tcp:219","192.0.2.1/32:tcp:221","192.0.2.1/32:tcp:223","192.0.2.1/32:tcp:225","192.0.2.1/32:tcp:227","192.0.2.1/32:tcp:229","192.0.2.1/32:tcp:231","192.0.2.1/32:tcp:233","192.0.2.1/32:tcp:235","192.0.2.1/32:tcp:237","192.0.2.1/32:tcp:239","192.0.2.1/32:tcp:241","192.0.2.1/32:tcp:243","192.0.2.1/32:tcp:245","192.0.2.1/32:tcp:247","192.0.2.1/32:tcp:249","192.0.2.1/32:tcp:251","192.0.2.1/32:tcp:253","192.0.2.1/32:tcp:255","2001:db8::1/128:tcp:1","2001:db8::1/128:tcp:3","2001:db8::1/128:tcp:5","2001:db8::1/128:tcp:7","2001:db8::1/128:tcp:9","2001:db8::1/128:tcp:11","2001:db8::1/128:tcp:13","2001:db8::1/128:tcp:15","2001:db8::1/128:tcp:17","2001:db8::1/128:tcp:19","2001:db8::1/128:tcp:21","2001:db8::1/128:tcp:23","2001:db8::1/128:tcp:25","2001:db8::1/128:tcp:27","2001:db8::1/128:tcp:29","2001:db8::1/128:tcp:31","2001:db8::1/128:tcp:33","2001:db8::1/128:tcp:35","2001:db8::1/128:tcp:37","2001:db8::1/128:tcp:39","2001:db8::1/128:tcp:41","2001:db8::1/128:tcp:43","2001:db8::1/128:tcp:45","2001:db8::1/128:tcp:47","2001:db8::1/128:tcp:49","2001:db8::1/128:tcp:51","2001:db8::1/128:tcp:53","2001:db8::1/128:tcp:55","2001:db8::1/128:tcp:57","2001:db8::1/128:tcp:59","2001:db8::1/128:tcp:61","2001:db8::1/128:tcp:63","2001:db8::1/128:tcp:65","2001:db8::1/128:tcp:67","2001:db8::1/128:tcp:69","2001:db8::1/128:tcp:71","2001:db8::1/128:tcp:73","2001:db8::1/128:tcp:75","2001:db8::1/128:tcp:77","2001:db8::1/128:tcp:79","2001:db8::1/128:tcp:81","2001:db8::1/128:tcp:83","2001:db8::1/128:tcp:85","2001:db8::1/128:tcp:87","2001:db8::1/128:tcp:89","2001:db8::1/128:tcp:91","2001:db8::1/128:tcp:93","2001:db8::1/128:tcp:95","2001:db8::1/128:tcp:97","2001:db8::1/128:tcp:99","2001:db8::1/128:tcp:101","2001:db8::1/128:tcp:103","2001:db8::1/128:tcp:105","2001:db8::1/128:tcp:107","2001:db8::1/128:tcp:109","2001:db8::1/128:tcp:111","2001:db8::1/128:tcp:113","2001:db8::1/128:tcp:115","2001:db8::1/128:tcp:117","2001:db8::1/128:tcp:119","2001:db8::1/128:tcp:121","2001:db8::1/128:tcp:123","2001:db8::1/128:tcp:125","2001:db8::1/128:tcp:127","2001:db8::1/128:tcp:129","2001:db8::1/128:tcp:131","2001:db8::1/128:tcp:133","2001:db8::1/128:tcp:135","2001:db8::1/128:tcp:137","2001:db8::1/128:tcp:139","2001:db8::1/128:tcp:141","2001:db8::1/128:tcp:143","2001:db8::1/128:tcp:145","2001:db8::1/128:tcp:147","2001:db8::1/128:tcp:149","2001:db8::1/128:tcp:151","2001:db8::1/128:tcp:153","2001:db8::1/128:tcp:155","2001:db8::1/128:tcp:157","2001:db8::1/128:tcp:159","2001:db8::1/128:tcp:161","2001:db8::1/128:tcp:163","2001:db8::1/128:tcp:165","2001:db8::1/128:tcp:167","2001:db8::1/128:tcp:169","2001:db8::1/128:tcp:171","2001:db8::1/128:tcp:173","2001:db8::1/128:tcp:175","2001:db8::1/128:tcp:177","2001:db8::1/128:tcp:179","2001:db8::1/128:tcp:181","2001:db8::1/128:tcp:183","2001:db8::1/128:tcp:185","2001:db8::1/128:tcp:187","2001:db8::1/128:tcp:189","2001:db8::1/128:tcp:191","2001:db8::1/128:tcp:193","2001:db8::1/128:tcp:195","2001:db8::1/128:tcp:197","2001:db8::1/128:tcp:199","2001:db8::1/128:tcp:201","2001:db8::1/128:tcp:203","2001:db8::1/128:tcp:205","2001:db8::1/128:tcp:207","2001:db8::1/128:tcp:209","2001:db8::1/128:tcp:211","2001:db8::1/128:tcp:213","2001:db8::1/128:tcp:215","2001:db8::1/128:tcp:217","2001:db8::1/128:tcp:219","2001:db8::1/128:tcp:221","2001:db8::1/128:tcp:223","2001:db8::1/128:tcp:225","2001:db8::1/128:tcp:227","2001:db8::1/128:tcp:229","2001:db8::1/128:tcp:231","2001:db8::1/128:tcp:233","2001:db8::1/128:tcp:235","2001:db8::1/128:tcp:237","2001:db8::1/128:tcp:239","2001:db8::1/128:tcp:241","2001:db8::1/128:tcp:243","2001:db8::1/128:tcp:245","2001:db8::1/128:tcp:247","2001:db8::1/128:tcp:249","2001:db8::1/128:tcp:251","2001:db8::1/128:tcp:253","2001:db8::1/128:tcp:255"],"deny":[]}}, + {"name":"vector: test_both_families_share_the_native_rule_budget #2","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.1\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 1}, {\"protocol\": \"tcp\", \"port\": 3}, {\"protocol\": \"tcp\", \"port\": 5}, {\"protocol\": \"tcp\", \"port\": 7}, {\"protocol\": \"tcp\", \"port\": 9}, {\"protocol\": \"tcp\", \"port\": 11}, {\"protocol\": \"tcp\", \"port\": 13}, {\"protocol\": \"tcp\", \"port\": 15}, {\"protocol\": \"tcp\", \"port\": 17}, {\"protocol\": \"tcp\", \"port\": 19}, {\"protocol\": \"tcp\", \"port\": 21}, {\"protocol\": \"tcp\", \"port\": 23}, {\"protocol\": \"tcp\", \"port\": 25}, {\"protocol\": \"tcp\", \"port\": 27}, {\"protocol\": \"tcp\", \"port\": 29}, {\"protocol\": \"tcp\", \"port\": 31}, {\"protocol\": \"tcp\", \"port\": 33}, {\"protocol\": \"tcp\", \"port\": 35}, {\"protocol\": \"tcp\", \"port\": 37}, {\"protocol\": \"tcp\", \"port\": 39}, {\"protocol\": \"tcp\", \"port\": 41}, {\"protocol\": \"tcp\", \"port\": 43}, {\"protocol\": \"tcp\", \"port\": 45}, {\"protocol\": \"tcp\", \"port\": 47}, {\"protocol\": \"tcp\", \"port\": 49}, {\"protocol\": \"tcp\", \"port\": 51}, {\"protocol\": \"tcp\", \"port\": 53}, {\"protocol\": \"tcp\", \"port\": 55}, {\"protocol\": \"tcp\", \"port\": 57}, {\"protocol\": \"tcp\", \"port\": 59}, {\"protocol\": \"tcp\", \"port\": 61}, {\"protocol\": \"tcp\", \"port\": 63}, {\"protocol\": \"tcp\", \"port\": 65}, {\"protocol\": \"tcp\", \"port\": 67}, {\"protocol\": \"tcp\", \"port\": 69}, {\"protocol\": \"tcp\", \"port\": 71}, {\"protocol\": \"tcp\", \"port\": 73}, {\"protocol\": \"tcp\", \"port\": 75}, {\"protocol\": \"tcp\", \"port\": 77}, {\"protocol\": \"tcp\", \"port\": 79}, {\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 83}, {\"protocol\": \"tcp\", \"port\": 85}, {\"protocol\": \"tcp\", \"port\": 87}, {\"protocol\": \"tcp\", \"port\": 89}, {\"protocol\": \"tcp\", \"port\": 91}, {\"protocol\": \"tcp\", \"port\": 93}, {\"protocol\": \"tcp\", \"port\": 95}, {\"protocol\": \"tcp\", \"port\": 97}, {\"protocol\": \"tcp\", \"port\": 99}, {\"protocol\": \"tcp\", \"port\": 101}, {\"protocol\": \"tcp\", \"port\": 103}, {\"protocol\": \"tcp\", \"port\": 105}, {\"protocol\": \"tcp\", \"port\": 107}, {\"protocol\": \"tcp\", \"port\": 109}, {\"protocol\": \"tcp\", \"port\": 111}, {\"protocol\": \"tcp\", \"port\": 113}, {\"protocol\": \"tcp\", \"port\": 115}, {\"protocol\": \"tcp\", \"port\": 117}, {\"protocol\": \"tcp\", \"port\": 119}, {\"protocol\": \"tcp\", \"port\": 121}, {\"protocol\": \"tcp\", \"port\": 123}, {\"protocol\": \"tcp\", \"port\": 125}, {\"protocol\": \"tcp\", \"port\": 127}, {\"protocol\": \"tcp\", \"port\": 129}, {\"protocol\": \"tcp\", \"port\": 131}, {\"protocol\": \"tcp\", \"port\": 133}, {\"protocol\": \"tcp\", \"port\": 135}, {\"protocol\": \"tcp\", \"port\": 137}, {\"protocol\": \"tcp\", \"port\": 139}, {\"protocol\": \"tcp\", \"port\": 141}, {\"protocol\": \"tcp\", \"port\": 143}, {\"protocol\": \"tcp\", \"port\": 145}, {\"protocol\": \"tcp\", \"port\": 147}, {\"protocol\": \"tcp\", \"port\": 149}, {\"protocol\": \"tcp\", \"port\": 151}, {\"protocol\": \"tcp\", \"port\": 153}, {\"protocol\": \"tcp\", \"port\": 155}, {\"protocol\": \"tcp\", \"port\": 157}, {\"protocol\": \"tcp\", \"port\": 159}, {\"protocol\": \"tcp\", \"port\": 161}, {\"protocol\": \"tcp\", \"port\": 163}, {\"protocol\": \"tcp\", \"port\": 165}, {\"protocol\": \"tcp\", \"port\": 167}, {\"protocol\": \"tcp\", \"port\": 169}, {\"protocol\": \"tcp\", \"port\": 171}, {\"protocol\": \"tcp\", \"port\": 173}, {\"protocol\": \"tcp\", \"port\": 175}, {\"protocol\": \"tcp\", \"port\": 177}, {\"protocol\": \"tcp\", \"port\": 179}, {\"protocol\": \"tcp\", \"port\": 181}, {\"protocol\": \"tcp\", \"port\": 183}, {\"protocol\": \"tcp\", \"port\": 185}, {\"protocol\": \"tcp\", \"port\": 187}, {\"protocol\": \"tcp\", \"port\": 189}, {\"protocol\": \"tcp\", \"port\": 191}, {\"protocol\": \"tcp\", \"port\": 193}, {\"protocol\": \"tcp\", \"port\": 195}, {\"protocol\": \"tcp\", \"port\": 197}, {\"protocol\": \"tcp\", \"port\": 199}, {\"protocol\": \"tcp\", \"port\": 201}, {\"protocol\": \"tcp\", \"port\": 203}, {\"protocol\": \"tcp\", \"port\": 205}, {\"protocol\": \"tcp\", \"port\": 207}, {\"protocol\": \"tcp\", \"port\": 209}, {\"protocol\": \"tcp\", \"port\": 211}, {\"protocol\": \"tcp\", \"port\": 213}, {\"protocol\": \"tcp\", \"port\": 215}, {\"protocol\": \"tcp\", \"port\": 217}, {\"protocol\": \"tcp\", \"port\": 219}, {\"protocol\": \"tcp\", \"port\": 221}, {\"protocol\": \"tcp\", \"port\": 223}, {\"protocol\": \"tcp\", \"port\": 225}, {\"protocol\": \"tcp\", \"port\": 227}, {\"protocol\": \"tcp\", \"port\": 229}, {\"protocol\": \"tcp\", \"port\": 231}, {\"protocol\": \"tcp\", \"port\": 233}, {\"protocol\": \"tcp\", \"port\": 235}, {\"protocol\": \"tcp\", \"port\": 237}, {\"protocol\": \"tcp\", \"port\": 239}, {\"protocol\": \"tcp\", \"port\": 241}, {\"protocol\": \"tcp\", \"port\": 243}, {\"protocol\": \"tcp\", \"port\": 245}, {\"protocol\": \"tcp\", \"port\": 247}, {\"protocol\": \"tcp\", \"port\": 249}, {\"protocol\": \"tcp\", \"port\": 251}, {\"protocol\": \"tcp\", \"port\": 253}, {\"protocol\": \"tcp\", \"port\": 255}, {\"protocol\": \"tcp\", \"port\": 257}]}, {\"to\": [{\"cidr\": \"2001:db8::1\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 1}, {\"protocol\": \"tcp\", \"port\": 3}, {\"protocol\": \"tcp\", \"port\": 5}, {\"protocol\": \"tcp\", \"port\": 7}, {\"protocol\": \"tcp\", \"port\": 9}, {\"protocol\": \"tcp\", \"port\": 11}, {\"protocol\": \"tcp\", \"port\": 13}, {\"protocol\": \"tcp\", \"port\": 15}, {\"protocol\": \"tcp\", \"port\": 17}, {\"protocol\": \"tcp\", \"port\": 19}, {\"protocol\": \"tcp\", \"port\": 21}, {\"protocol\": \"tcp\", \"port\": 23}, {\"protocol\": \"tcp\", \"port\": 25}, {\"protocol\": \"tcp\", \"port\": 27}, {\"protocol\": \"tcp\", \"port\": 29}, {\"protocol\": \"tcp\", \"port\": 31}, {\"protocol\": \"tcp\", \"port\": 33}, {\"protocol\": \"tcp\", \"port\": 35}, {\"protocol\": \"tcp\", \"port\": 37}, {\"protocol\": \"tcp\", \"port\": 39}, {\"protocol\": \"tcp\", \"port\": 41}, {\"protocol\": \"tcp\", \"port\": 43}, {\"protocol\": \"tcp\", \"port\": 45}, {\"protocol\": \"tcp\", \"port\": 47}, {\"protocol\": \"tcp\", \"port\": 49}, {\"protocol\": \"tcp\", \"port\": 51}, {\"protocol\": \"tcp\", \"port\": 53}, {\"protocol\": \"tcp\", \"port\": 55}, {\"protocol\": \"tcp\", \"port\": 57}, {\"protocol\": \"tcp\", \"port\": 59}, {\"protocol\": \"tcp\", \"port\": 61}, {\"protocol\": \"tcp\", \"port\": 63}, {\"protocol\": \"tcp\", \"port\": 65}, {\"protocol\": \"tcp\", \"port\": 67}, {\"protocol\": \"tcp\", \"port\": 69}, {\"protocol\": \"tcp\", \"port\": 71}, {\"protocol\": \"tcp\", \"port\": 73}, {\"protocol\": \"tcp\", \"port\": 75}, {\"protocol\": \"tcp\", \"port\": 77}, {\"protocol\": \"tcp\", \"port\": 79}, {\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 83}, {\"protocol\": \"tcp\", \"port\": 85}, {\"protocol\": \"tcp\", \"port\": 87}, {\"protocol\": \"tcp\", \"port\": 89}, {\"protocol\": \"tcp\", \"port\": 91}, {\"protocol\": \"tcp\", \"port\": 93}, {\"protocol\": \"tcp\", \"port\": 95}, {\"protocol\": \"tcp\", \"port\": 97}, {\"protocol\": \"tcp\", \"port\": 99}, {\"protocol\": \"tcp\", \"port\": 101}, {\"protocol\": \"tcp\", \"port\": 103}, {\"protocol\": \"tcp\", \"port\": 105}, {\"protocol\": \"tcp\", \"port\": 107}, {\"protocol\": \"tcp\", \"port\": 109}, {\"protocol\": \"tcp\", \"port\": 111}, {\"protocol\": \"tcp\", \"port\": 113}, {\"protocol\": \"tcp\", \"port\": 115}, {\"protocol\": \"tcp\", \"port\": 117}, {\"protocol\": \"tcp\", \"port\": 119}, {\"protocol\": \"tcp\", \"port\": 121}, {\"protocol\": \"tcp\", \"port\": 123}, {\"protocol\": \"tcp\", \"port\": 125}, {\"protocol\": \"tcp\", \"port\": 127}, {\"protocol\": \"tcp\", \"port\": 129}, {\"protocol\": \"tcp\", \"port\": 131}, {\"protocol\": \"tcp\", \"port\": 133}, {\"protocol\": \"tcp\", \"port\": 135}, {\"protocol\": \"tcp\", \"port\": 137}, {\"protocol\": \"tcp\", \"port\": 139}, {\"protocol\": \"tcp\", \"port\": 141}, {\"protocol\": \"tcp\", \"port\": 143}, {\"protocol\": \"tcp\", \"port\": 145}, {\"protocol\": \"tcp\", \"port\": 147}, {\"protocol\": \"tcp\", \"port\": 149}, {\"protocol\": \"tcp\", \"port\": 151}, {\"protocol\": \"tcp\", \"port\": 153}, {\"protocol\": \"tcp\", \"port\": 155}, {\"protocol\": \"tcp\", \"port\": 157}, {\"protocol\": \"tcp\", \"port\": 159}, {\"protocol\": \"tcp\", \"port\": 161}, {\"protocol\": \"tcp\", \"port\": 163}, {\"protocol\": \"tcp\", \"port\": 165}, {\"protocol\": \"tcp\", \"port\": 167}, {\"protocol\": \"tcp\", \"port\": 169}, {\"protocol\": \"tcp\", \"port\": 171}, {\"protocol\": \"tcp\", \"port\": 173}, {\"protocol\": \"tcp\", \"port\": 175}, {\"protocol\": \"tcp\", \"port\": 177}, {\"protocol\": \"tcp\", \"port\": 179}, {\"protocol\": \"tcp\", \"port\": 181}, {\"protocol\": \"tcp\", \"port\": 183}, {\"protocol\": \"tcp\", \"port\": 185}, {\"protocol\": \"tcp\", \"port\": 187}, {\"protocol\": \"tcp\", \"port\": 189}, {\"protocol\": \"tcp\", \"port\": 191}, {\"protocol\": \"tcp\", \"port\": 193}, {\"protocol\": \"tcp\", \"port\": 195}, {\"protocol\": \"tcp\", \"port\": 197}, {\"protocol\": \"tcp\", \"port\": 199}, {\"protocol\": \"tcp\", \"port\": 201}, {\"protocol\": \"tcp\", \"port\": 203}, {\"protocol\": \"tcp\", \"port\": 205}, {\"protocol\": \"tcp\", \"port\": 207}, {\"protocol\": \"tcp\", \"port\": 209}, {\"protocol\": \"tcp\", \"port\": 211}, {\"protocol\": \"tcp\", \"port\": 213}, {\"protocol\": \"tcp\", \"port\": 215}, {\"protocol\": \"tcp\", \"port\": 217}, {\"protocol\": \"tcp\", \"port\": 219}, {\"protocol\": \"tcp\", \"port\": 221}, {\"protocol\": \"tcp\", \"port\": 223}, {\"protocol\": \"tcp\", \"port\": 225}, {\"protocol\": \"tcp\", \"port\": 227}, {\"protocol\": \"tcp\", \"port\": 229}, {\"protocol\": \"tcp\", \"port\": 231}, {\"protocol\": \"tcp\", \"port\": 233}, {\"protocol\": \"tcp\", \"port\": 235}, {\"protocol\": \"tcp\", \"port\": 237}, {\"protocol\": \"tcp\", \"port\": 239}, {\"protocol\": \"tcp\", \"port\": 241}, {\"protocol\": \"tcp\", \"port\": 243}, {\"protocol\": \"tcp\", \"port\": 245}, {\"protocol\": \"tcp\", \"port\": 247}, {\"protocol\": \"tcp\", \"port\": 249}, {\"protocol\": \"tcp\", \"port\": 251}, {\"protocol\": \"tcp\", \"port\": 253}, {\"protocol\": \"tcp\", \"port\": 255}]}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"vector: test_both_families_share_the_native_rule_budget #3","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.1\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 1}, {\"protocol\": \"tcp\", \"port\": 3}, {\"protocol\": \"tcp\", \"port\": 5}, {\"protocol\": \"tcp\", \"port\": 7}, {\"protocol\": \"tcp\", \"port\": 9}, {\"protocol\": \"tcp\", \"port\": 11}, {\"protocol\": \"tcp\", \"port\": 13}, {\"protocol\": \"tcp\", \"port\": 15}, {\"protocol\": \"tcp\", \"port\": 17}, {\"protocol\": \"tcp\", \"port\": 19}, {\"protocol\": \"tcp\", \"port\": 21}, {\"protocol\": \"tcp\", \"port\": 23}, {\"protocol\": \"tcp\", \"port\": 25}, {\"protocol\": \"tcp\", \"port\": 27}, {\"protocol\": \"tcp\", \"port\": 29}, {\"protocol\": \"tcp\", \"port\": 31}, {\"protocol\": \"tcp\", \"port\": 33}, {\"protocol\": \"tcp\", \"port\": 35}, {\"protocol\": \"tcp\", \"port\": 37}, {\"protocol\": \"tcp\", \"port\": 39}, {\"protocol\": \"tcp\", \"port\": 41}, {\"protocol\": \"tcp\", \"port\": 43}, {\"protocol\": \"tcp\", \"port\": 45}, {\"protocol\": \"tcp\", \"port\": 47}, {\"protocol\": \"tcp\", \"port\": 49}, {\"protocol\": \"tcp\", \"port\": 51}, {\"protocol\": \"tcp\", \"port\": 53}, {\"protocol\": \"tcp\", \"port\": 55}, {\"protocol\": \"tcp\", \"port\": 57}, {\"protocol\": \"tcp\", \"port\": 59}, {\"protocol\": \"tcp\", \"port\": 61}, {\"protocol\": \"tcp\", \"port\": 63}, {\"protocol\": \"tcp\", \"port\": 65}, {\"protocol\": \"tcp\", \"port\": 67}, {\"protocol\": \"tcp\", \"port\": 69}, {\"protocol\": \"tcp\", \"port\": 71}, {\"protocol\": \"tcp\", \"port\": 73}, {\"protocol\": \"tcp\", \"port\": 75}, {\"protocol\": \"tcp\", \"port\": 77}, {\"protocol\": \"tcp\", \"port\": 79}, {\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 83}, {\"protocol\": \"tcp\", \"port\": 85}, {\"protocol\": \"tcp\", \"port\": 87}, {\"protocol\": \"tcp\", \"port\": 89}, {\"protocol\": \"tcp\", \"port\": 91}, {\"protocol\": \"tcp\", \"port\": 93}, {\"protocol\": \"tcp\", \"port\": 95}, {\"protocol\": \"tcp\", \"port\": 97}, {\"protocol\": \"tcp\", \"port\": 99}, {\"protocol\": \"tcp\", \"port\": 101}, {\"protocol\": \"tcp\", \"port\": 103}, {\"protocol\": \"tcp\", \"port\": 105}, {\"protocol\": \"tcp\", \"port\": 107}, {\"protocol\": \"tcp\", \"port\": 109}, {\"protocol\": \"tcp\", \"port\": 111}, {\"protocol\": \"tcp\", \"port\": 113}, {\"protocol\": \"tcp\", \"port\": 115}, {\"protocol\": \"tcp\", \"port\": 117}, {\"protocol\": \"tcp\", \"port\": 119}, {\"protocol\": \"tcp\", \"port\": 121}, {\"protocol\": \"tcp\", \"port\": 123}, {\"protocol\": \"tcp\", \"port\": 125}, {\"protocol\": \"tcp\", \"port\": 127}, {\"protocol\": \"tcp\", \"port\": 129}, {\"protocol\": \"tcp\", \"port\": 131}, {\"protocol\": \"tcp\", \"port\": 133}, {\"protocol\": \"tcp\", \"port\": 135}, {\"protocol\": \"tcp\", \"port\": 137}, {\"protocol\": \"tcp\", \"port\": 139}, {\"protocol\": \"tcp\", \"port\": 141}, {\"protocol\": \"tcp\", \"port\": 143}, {\"protocol\": \"tcp\", \"port\": 145}, {\"protocol\": \"tcp\", \"port\": 147}, {\"protocol\": \"tcp\", \"port\": 149}, {\"protocol\": \"tcp\", \"port\": 151}, {\"protocol\": \"tcp\", \"port\": 153}, {\"protocol\": \"tcp\", \"port\": 155}, {\"protocol\": \"tcp\", \"port\": 157}, {\"protocol\": \"tcp\", \"port\": 159}, {\"protocol\": \"tcp\", \"port\": 161}, {\"protocol\": \"tcp\", \"port\": 163}, {\"protocol\": \"tcp\", \"port\": 165}, {\"protocol\": \"tcp\", \"port\": 167}, {\"protocol\": \"tcp\", \"port\": 169}, {\"protocol\": \"tcp\", \"port\": 171}, {\"protocol\": \"tcp\", \"port\": 173}, {\"protocol\": \"tcp\", \"port\": 175}, {\"protocol\": \"tcp\", \"port\": 177}, {\"protocol\": \"tcp\", \"port\": 179}, {\"protocol\": \"tcp\", \"port\": 181}, {\"protocol\": \"tcp\", \"port\": 183}, {\"protocol\": \"tcp\", \"port\": 185}, {\"protocol\": \"tcp\", \"port\": 187}, {\"protocol\": \"tcp\", \"port\": 189}, {\"protocol\": \"tcp\", \"port\": 191}, {\"protocol\": \"tcp\", \"port\": 193}, {\"protocol\": \"tcp\", \"port\": 195}, {\"protocol\": \"tcp\", \"port\": 197}, {\"protocol\": \"tcp\", \"port\": 199}, {\"protocol\": \"tcp\", \"port\": 201}, {\"protocol\": \"tcp\", \"port\": 203}, {\"protocol\": \"tcp\", \"port\": 205}, {\"protocol\": \"tcp\", \"port\": 207}, {\"protocol\": \"tcp\", \"port\": 209}, {\"protocol\": \"tcp\", \"port\": 211}, {\"protocol\": \"tcp\", \"port\": 213}, {\"protocol\": \"tcp\", \"port\": 215}, {\"protocol\": \"tcp\", \"port\": 217}, {\"protocol\": \"tcp\", \"port\": 219}, {\"protocol\": \"tcp\", \"port\": 221}, {\"protocol\": \"tcp\", \"port\": 223}, {\"protocol\": \"tcp\", \"port\": 225}, {\"protocol\": \"tcp\", \"port\": 227}, {\"protocol\": \"tcp\", \"port\": 229}, {\"protocol\": \"tcp\", \"port\": 231}, {\"protocol\": \"tcp\", \"port\": 233}, {\"protocol\": \"tcp\", \"port\": 235}, {\"protocol\": \"tcp\", \"port\": 237}, {\"protocol\": \"tcp\", \"port\": 239}, {\"protocol\": \"tcp\", \"port\": 241}, {\"protocol\": \"tcp\", \"port\": 243}, {\"protocol\": \"tcp\", \"port\": 245}, {\"protocol\": \"tcp\", \"port\": 247}, {\"protocol\": \"tcp\", \"port\": 249}, {\"protocol\": \"tcp\", \"port\": 251}, {\"protocol\": \"tcp\", \"port\": 253}, {\"protocol\": \"tcp\", \"port\": 255}]}, {\"to\": [{\"cidr\": \"2001:db8::1\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 1}, {\"protocol\": \"tcp\", \"port\": 3}, {\"protocol\": \"tcp\", \"port\": 5}, {\"protocol\": \"tcp\", \"port\": 7}, {\"protocol\": \"tcp\", \"port\": 9}, {\"protocol\": \"tcp\", \"port\": 11}, {\"protocol\": \"tcp\", \"port\": 13}, {\"protocol\": \"tcp\", \"port\": 15}, {\"protocol\": \"tcp\", \"port\": 17}, {\"protocol\": \"tcp\", \"port\": 19}, {\"protocol\": \"tcp\", \"port\": 21}, {\"protocol\": \"tcp\", \"port\": 23}, {\"protocol\": \"tcp\", \"port\": 25}, {\"protocol\": \"tcp\", \"port\": 27}, {\"protocol\": \"tcp\", \"port\": 29}, {\"protocol\": \"tcp\", \"port\": 31}, {\"protocol\": \"tcp\", \"port\": 33}, {\"protocol\": \"tcp\", \"port\": 35}, {\"protocol\": \"tcp\", \"port\": 37}, {\"protocol\": \"tcp\", \"port\": 39}, {\"protocol\": \"tcp\", \"port\": 41}, {\"protocol\": \"tcp\", \"port\": 43}, {\"protocol\": \"tcp\", \"port\": 45}, {\"protocol\": \"tcp\", \"port\": 47}, {\"protocol\": \"tcp\", \"port\": 49}, {\"protocol\": \"tcp\", \"port\": 51}, {\"protocol\": \"tcp\", \"port\": 53}, {\"protocol\": \"tcp\", \"port\": 55}, {\"protocol\": \"tcp\", \"port\": 57}, {\"protocol\": \"tcp\", \"port\": 59}, {\"protocol\": \"tcp\", \"port\": 61}, {\"protocol\": \"tcp\", \"port\": 63}, {\"protocol\": \"tcp\", \"port\": 65}, {\"protocol\": \"tcp\", \"port\": 67}, {\"protocol\": \"tcp\", \"port\": 69}, {\"protocol\": \"tcp\", \"port\": 71}, {\"protocol\": \"tcp\", \"port\": 73}, {\"protocol\": \"tcp\", \"port\": 75}, {\"protocol\": \"tcp\", \"port\": 77}, {\"protocol\": \"tcp\", \"port\": 79}, {\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 83}, {\"protocol\": \"tcp\", \"port\": 85}, {\"protocol\": \"tcp\", \"port\": 87}, {\"protocol\": \"tcp\", \"port\": 89}, {\"protocol\": \"tcp\", \"port\": 91}, {\"protocol\": \"tcp\", \"port\": 93}, {\"protocol\": \"tcp\", \"port\": 95}, {\"protocol\": \"tcp\", \"port\": 97}, {\"protocol\": \"tcp\", \"port\": 99}, {\"protocol\": \"tcp\", \"port\": 101}, {\"protocol\": \"tcp\", \"port\": 103}, {\"protocol\": \"tcp\", \"port\": 105}, {\"protocol\": \"tcp\", \"port\": 107}, {\"protocol\": \"tcp\", \"port\": 109}, {\"protocol\": \"tcp\", \"port\": 111}, {\"protocol\": \"tcp\", \"port\": 113}, {\"protocol\": \"tcp\", \"port\": 115}, {\"protocol\": \"tcp\", \"port\": 117}, {\"protocol\": \"tcp\", \"port\": 119}, {\"protocol\": \"tcp\", \"port\": 121}, {\"protocol\": \"tcp\", \"port\": 123}, {\"protocol\": \"tcp\", \"port\": 125}, {\"protocol\": \"tcp\", \"port\": 127}, {\"protocol\": \"tcp\", \"port\": 129}, {\"protocol\": \"tcp\", \"port\": 131}, {\"protocol\": \"tcp\", \"port\": 133}, {\"protocol\": \"tcp\", \"port\": 135}, {\"protocol\": \"tcp\", \"port\": 137}, {\"protocol\": \"tcp\", \"port\": 139}, {\"protocol\": \"tcp\", \"port\": 141}, {\"protocol\": \"tcp\", \"port\": 143}, {\"protocol\": \"tcp\", \"port\": 145}, {\"protocol\": \"tcp\", \"port\": 147}, {\"protocol\": \"tcp\", \"port\": 149}, {\"protocol\": \"tcp\", \"port\": 151}, {\"protocol\": \"tcp\", \"port\": 153}, {\"protocol\": \"tcp\", \"port\": 155}, {\"protocol\": \"tcp\", \"port\": 157}, {\"protocol\": \"tcp\", \"port\": 159}, {\"protocol\": \"tcp\", \"port\": 161}, {\"protocol\": \"tcp\", \"port\": 163}, {\"protocol\": \"tcp\", \"port\": 165}, {\"protocol\": \"tcp\", \"port\": 167}, {\"protocol\": \"tcp\", \"port\": 169}, {\"protocol\": \"tcp\", \"port\": 171}, {\"protocol\": \"tcp\", \"port\": 173}, {\"protocol\": \"tcp\", \"port\": 175}, {\"protocol\": \"tcp\", \"port\": 177}, {\"protocol\": \"tcp\", \"port\": 179}, {\"protocol\": \"tcp\", \"port\": 181}, {\"protocol\": \"tcp\", \"port\": 183}, {\"protocol\": \"tcp\", \"port\": 185}, {\"protocol\": \"tcp\", \"port\": 187}, {\"protocol\": \"tcp\", \"port\": 189}, {\"protocol\": \"tcp\", \"port\": 191}, {\"protocol\": \"tcp\", \"port\": 193}, {\"protocol\": \"tcp\", \"port\": 195}, {\"protocol\": \"tcp\", \"port\": 197}, {\"protocol\": \"tcp\", \"port\": 199}, {\"protocol\": \"tcp\", \"port\": 201}, {\"protocol\": \"tcp\", \"port\": 203}, {\"protocol\": \"tcp\", \"port\": 205}, {\"protocol\": \"tcp\", \"port\": 207}, {\"protocol\": \"tcp\", \"port\": 209}, {\"protocol\": \"tcp\", \"port\": 211}, {\"protocol\": \"tcp\", \"port\": 213}, {\"protocol\": \"tcp\", \"port\": 215}, {\"protocol\": \"tcp\", \"port\": 217}, {\"protocol\": \"tcp\", \"port\": 219}, {\"protocol\": \"tcp\", \"port\": 221}, {\"protocol\": \"tcp\", \"port\": 223}, {\"protocol\": \"tcp\", \"port\": 225}, {\"protocol\": \"tcp\", \"port\": 227}, {\"protocol\": \"tcp\", \"port\": 229}, {\"protocol\": \"tcp\", \"port\": 231}, {\"protocol\": \"tcp\", \"port\": 233}, {\"protocol\": \"tcp\", \"port\": 235}, {\"protocol\": \"tcp\", \"port\": 237}, {\"protocol\": \"tcp\", \"port\": 239}, {\"protocol\": \"tcp\", \"port\": 241}, {\"protocol\": \"tcp\", \"port\": 243}, {\"protocol\": \"tcp\", \"port\": 245}, {\"protocol\": \"tcp\", \"port\": 247}, {\"protocol\": \"tcp\", \"port\": 249}, {\"protocol\": \"tcp\", \"port\": 251}, {\"protocol\": \"tcp\", \"port\": 253}, {\"protocol\": \"tcp\", \"port\": 255}, {\"protocol\": \"tcp\", \"port\": 257}]}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"vector: test_bounds_exclusions_times_port_segments #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 1}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 3}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 5}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 7}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 9}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 11}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 13}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 15}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 17}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 19}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 21}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 23}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 25}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 27}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 29}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 31}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 33}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 35}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 37}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 39}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 41}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 43}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 45}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 47}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 49}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 51}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 53}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 55}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 57}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 59}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 61}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 63}]}"}],"expected":{"allow":["192.0.2.0/32:tcp:1","192.0.2.0/32:tcp:3","192.0.2.0/32:tcp:5","192.0.2.0/32:tcp:7","192.0.2.0/32:tcp:9","192.0.2.0/32:tcp:11","192.0.2.0/32:tcp:13","192.0.2.0/32:tcp:15","192.0.2.0/32:tcp:17","192.0.2.0/32:tcp:19","192.0.2.0/32:tcp:21","192.0.2.0/32:tcp:23","192.0.2.0/32:tcp:25","192.0.2.0/32:tcp:27","192.0.2.0/32:tcp:29","192.0.2.0/32:tcp:31","192.0.2.0/32:tcp:33","192.0.2.0/32:tcp:35","192.0.2.0/32:tcp:37","192.0.2.0/32:tcp:39","192.0.2.0/32:tcp:41","192.0.2.0/32:tcp:43","192.0.2.0/32:tcp:45","192.0.2.0/32:tcp:47","192.0.2.0/32:tcp:49","192.0.2.0/32:tcp:51","192.0.2.0/32:tcp:53","192.0.2.0/32:tcp:55","192.0.2.0/32:tcp:57","192.0.2.0/32:tcp:59","192.0.2.0/32:tcp:61","192.0.2.0/32:tcp:63","192.0.2.2/31:tcp:1","192.0.2.2/31:tcp:3","192.0.2.2/31:tcp:5","192.0.2.2/31:tcp:7","192.0.2.2/31:tcp:9","192.0.2.2/31:tcp:11","192.0.2.2/31:tcp:13","192.0.2.2/31:tcp:15","192.0.2.2/31:tcp:17","192.0.2.2/31:tcp:19","192.0.2.2/31:tcp:21","192.0.2.2/31:tcp:23","192.0.2.2/31:tcp:25","192.0.2.2/31:tcp:27","192.0.2.2/31:tcp:29","192.0.2.2/31:tcp:31","192.0.2.2/31:tcp:33","192.0.2.2/31:tcp:35","192.0.2.2/31:tcp:37","192.0.2.2/31:tcp:39","192.0.2.2/31:tcp:41","192.0.2.2/31:tcp:43","192.0.2.2/31:tcp:45","192.0.2.2/31:tcp:47","192.0.2.2/31:tcp:49","192.0.2.2/31:tcp:51","192.0.2.2/31:tcp:53","192.0.2.2/31:tcp:55","192.0.2.2/31:tcp:57","192.0.2.2/31:tcp:59","192.0.2.2/31:tcp:61","192.0.2.2/31:tcp:63","192.0.2.4/30:tcp:1","192.0.2.4/30:tcp:3","192.0.2.4/30:tcp:5","192.0.2.4/30:tcp:7","192.0.2.4/30:tcp:9","192.0.2.4/30:tcp:11","192.0.2.4/30:tcp:13","192.0.2.4/30:tcp:15","192.0.2.4/30:tcp:17","192.0.2.4/30:tcp:19","192.0.2.4/30:tcp:21","192.0.2.4/30:tcp:23","192.0.2.4/30:tcp:25","192.0.2.4/30:tcp:27","192.0.2.4/30:tcp:29","192.0.2.4/30:tcp:31","192.0.2.4/30:tcp:33","192.0.2.4/30:tcp:35","192.0.2.4/30:tcp:37","192.0.2.4/30:tcp:39","192.0.2.4/30:tcp:41","192.0.2.4/30:tcp:43","192.0.2.4/30:tcp:45","192.0.2.4/30:tcp:47","192.0.2.4/30:tcp:49","192.0.2.4/30:tcp:51","192.0.2.4/30:tcp:53","192.0.2.4/30:tcp:55","192.0.2.4/30:tcp:57","192.0.2.4/30:tcp:59","192.0.2.4/30:tcp:61","192.0.2.4/30:tcp:63","192.0.2.8/29:tcp:1","192.0.2.8/29:tcp:3","192.0.2.8/29:tcp:5","192.0.2.8/29:tcp:7","192.0.2.8/29:tcp:9","192.0.2.8/29:tcp:11","192.0.2.8/29:tcp:13","192.0.2.8/29:tcp:15","192.0.2.8/29:tcp:17","192.0.2.8/29:tcp:19","192.0.2.8/29:tcp:21","192.0.2.8/29:tcp:23","192.0.2.8/29:tcp:25","192.0.2.8/29:tcp:27","192.0.2.8/29:tcp:29","192.0.2.8/29:tcp:31","192.0.2.8/29:tcp:33","192.0.2.8/29:tcp:35","192.0.2.8/29:tcp:37","192.0.2.8/29:tcp:39","192.0.2.8/29:tcp:41","192.0.2.8/29:tcp:43","192.0.2.8/29:tcp:45","192.0.2.8/29:tcp:47","192.0.2.8/29:tcp:49","192.0.2.8/29:tcp:51","192.0.2.8/29:tcp:53","192.0.2.8/29:tcp:55","192.0.2.8/29:tcp:57","192.0.2.8/29:tcp:59","192.0.2.8/29:tcp:61","192.0.2.8/29:tcp:63","192.0.2.16/28:tcp:1","192.0.2.16/28:tcp:3","192.0.2.16/28:tcp:5","192.0.2.16/28:tcp:7","192.0.2.16/28:tcp:9","192.0.2.16/28:tcp:11","192.0.2.16/28:tcp:13","192.0.2.16/28:tcp:15","192.0.2.16/28:tcp:17","192.0.2.16/28:tcp:19","192.0.2.16/28:tcp:21","192.0.2.16/28:tcp:23","192.0.2.16/28:tcp:25","192.0.2.16/28:tcp:27","192.0.2.16/28:tcp:29","192.0.2.16/28:tcp:31","192.0.2.16/28:tcp:33","192.0.2.16/28:tcp:35","192.0.2.16/28:tcp:37","192.0.2.16/28:tcp:39","192.0.2.16/28:tcp:41","192.0.2.16/28:tcp:43","192.0.2.16/28:tcp:45","192.0.2.16/28:tcp:47","192.0.2.16/28:tcp:49","192.0.2.16/28:tcp:51","192.0.2.16/28:tcp:53","192.0.2.16/28:tcp:55","192.0.2.16/28:tcp:57","192.0.2.16/28:tcp:59","192.0.2.16/28:tcp:61","192.0.2.16/28:tcp:63","192.0.2.32/27:tcp:1","192.0.2.32/27:tcp:3","192.0.2.32/27:tcp:5","192.0.2.32/27:tcp:7","192.0.2.32/27:tcp:9","192.0.2.32/27:tcp:11","192.0.2.32/27:tcp:13","192.0.2.32/27:tcp:15","192.0.2.32/27:tcp:17","192.0.2.32/27:tcp:19","192.0.2.32/27:tcp:21","192.0.2.32/27:tcp:23","192.0.2.32/27:tcp:25","192.0.2.32/27:tcp:27","192.0.2.32/27:tcp:29","192.0.2.32/27:tcp:31","192.0.2.32/27:tcp:33","192.0.2.32/27:tcp:35","192.0.2.32/27:tcp:37","192.0.2.32/27:tcp:39","192.0.2.32/27:tcp:41","192.0.2.32/27:tcp:43","192.0.2.32/27:tcp:45","192.0.2.32/27:tcp:47","192.0.2.32/27:tcp:49","192.0.2.32/27:tcp:51","192.0.2.32/27:tcp:53","192.0.2.32/27:tcp:55","192.0.2.32/27:tcp:57","192.0.2.32/27:tcp:59","192.0.2.32/27:tcp:61","192.0.2.32/27:tcp:63","192.0.2.64/26:tcp:1","192.0.2.64/26:tcp:3","192.0.2.64/26:tcp:5","192.0.2.64/26:tcp:7","192.0.2.64/26:tcp:9","192.0.2.64/26:tcp:11","192.0.2.64/26:tcp:13","192.0.2.64/26:tcp:15","192.0.2.64/26:tcp:17","192.0.2.64/26:tcp:19","192.0.2.64/26:tcp:21","192.0.2.64/26:tcp:23","192.0.2.64/26:tcp:25","192.0.2.64/26:tcp:27","192.0.2.64/26:tcp:29","192.0.2.64/26:tcp:31","192.0.2.64/26:tcp:33","192.0.2.64/26:tcp:35","192.0.2.64/26:tcp:37","192.0.2.64/26:tcp:39","192.0.2.64/26:tcp:41","192.0.2.64/26:tcp:43","192.0.2.64/26:tcp:45","192.0.2.64/26:tcp:47","192.0.2.64/26:tcp:49","192.0.2.64/26:tcp:51","192.0.2.64/26:tcp:53","192.0.2.64/26:tcp:55","192.0.2.64/26:tcp:57","192.0.2.64/26:tcp:59","192.0.2.64/26:tcp:61","192.0.2.64/26:tcp:63","192.0.2.128/25:tcp:1","192.0.2.128/25:tcp:3","192.0.2.128/25:tcp:5","192.0.2.128/25:tcp:7","192.0.2.128/25:tcp:9","192.0.2.128/25:tcp:11","192.0.2.128/25:tcp:13","192.0.2.128/25:tcp:15","192.0.2.128/25:tcp:17","192.0.2.128/25:tcp:19","192.0.2.128/25:tcp:21","192.0.2.128/25:tcp:23","192.0.2.128/25:tcp:25","192.0.2.128/25:tcp:27","192.0.2.128/25:tcp:29","192.0.2.128/25:tcp:31","192.0.2.128/25:tcp:33","192.0.2.128/25:tcp:35","192.0.2.128/25:tcp:37","192.0.2.128/25:tcp:39","192.0.2.128/25:tcp:41","192.0.2.128/25:tcp:43","192.0.2.128/25:tcp:45","192.0.2.128/25:tcp:47","192.0.2.128/25:tcp:49","192.0.2.128/25:tcp:51","192.0.2.128/25:tcp:53","192.0.2.128/25:tcp:55","192.0.2.128/25:tcp:57","192.0.2.128/25:tcp:59","192.0.2.128/25:tcp:61","192.0.2.128/25:tcp:63"],"deny":[]}}, + {"name":"vector: test_bounds_exclusions_times_port_segments #2","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 1}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 3}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 5}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 7}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 9}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 11}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 13}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 15}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 17}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 19}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 21}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 23}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 25}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 27}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 29}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 31}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 33}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 35}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 37}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 39}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 41}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 43}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 45}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 47}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 49}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 51}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 53}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 55}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 57}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 59}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 61}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 63}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 65}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"vector: test_bounds_exclusions_times_port_segments #3","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 1}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 2}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 3}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 4}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 5}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 6}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 7}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 8}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 9}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 10}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 11}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 12}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 13}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 14}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 15}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 16}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 17}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 18}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 19}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 20}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 21}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 22}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 23}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 24}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 25}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 26}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 27}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 28}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 29}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 30}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 31}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 32}, {\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.1/32\"], \"protocol\": \"tcp\", \"port\": 33}]}"}],"expected":{"allow":["192.0.2.0/32:tcp:1-33","192.0.2.2/31:tcp:1-33","192.0.2.4/30:tcp:1-33","192.0.2.8/29:tcp:1-33","192.0.2.16/28:tcp:1-33","192.0.2.32/27:tcp:1-33","192.0.2.64/26:tcp:1-33","192.0.2.128/25:tcp:1-33"],"deny":[]}}, + {"name":"vector: test_compiles_inclusive_tcp_and_udp_ranges #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8002}]}, {\"to\": [{\"cidr\": \"198.51.100.0/24\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 5000, \"endPort\": 5001}]}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:8000-8002","198.51.100.0/24:udp:5000-5001"],"deny":[]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"tcp\"}]}]}"}],"expected":{"allow":["2001:db8::7/128:tcp"],"deny":[]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #2","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"udp\"}]}]}"}],"expected":{"allow":["2001:db8::7/128:udp"],"deny":[]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #3","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"icmp\"}]}]}"}],"expected":{"allow":["2001:db8::7/128:icmp"],"deny":[]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #4","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}]}]}"}],"expected":{"allow":["2001:db8::7/128"],"deny":[]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #5","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 443}]}]}"}],"expected":{"allow":["2001:db8::7/128:tcp:443","2001:db8::7/128:udp:443"],"deny":[]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #6","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8010}]}]}"}],"expected":{"allow":["2001:db8::7/128:tcp:8000-8010"],"deny":[]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #7","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"tcp\"}]}]}"}],"expected":{"allow":[],"deny":["2001:db8::7/128:tcp"]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #8","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"udp\"}]}]}"}],"expected":{"allow":[],"deny":["2001:db8::7/128:udp"]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #9","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"icmp\"}]}]}"}],"expected":{"allow":[],"deny":["2001:db8::7/128:icmp"]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #10","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}]}]}"}],"expected":{"allow":[],"deny":["2001:db8::7/128"]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #11","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 443}]}]}"}],"expected":{"allow":[],"deny":["2001:db8::7/128:tcp:443","2001:db8::7/128:udp:443"]}}, + {"name":"vector: test_compiles_ipv6_networks_with_every_selector #12","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::7\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8010}]}]}"}],"expected":{"allow":[],"deny":["2001:db8::7/128:tcp:8000-8010"]}}, + {"name":"vector: test_compiles_mxc_destination_and_port_unions #1","input":[{"text":"{\"allow\": [{\"ports\": [{\"port\": 53}, {\"protocol\": \"icmp\"}]}], \"deny\": [{\"to\": [{\"cidr\": \"198.51.100.0/25\"}, {\"cidr\": \"198.51.100.128/25\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 80}, {\"protocol\": \"udp\", \"port\": 53}]}]}"}],"expected":{"allow":["0.0.0.0/0:icmp","0.0.0.0/0:tcp:53","0.0.0.0/0:udp:53","::/0:icmp","::/0:tcp:53","::/0:udp:53"],"deny":["198.51.100.0/24:tcp:80","198.51.100.0/24:udp:53"]}}, + {"name":"vector: test_compiles_one_allowed_ipv6_network_with_a_denied_address #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8:1::/64\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 443}]}, {\"to\": [{\"cidr\": \"::/0\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 53}]}], \"deny\": [{\"to\": [{\"cidr\": \"2001:db8:1::123\"}]}]}"}],"expected":{"allow":["::/0:udp:53","2001:db8:1::/64:tcp:443"],"deny":["2001:db8:1::123/128"]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\"}]}"}],"expected":{"allow":["192.0.2.7/32:tcp"],"deny":[]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #2","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"udp\"}]}"}],"expected":{"allow":["192.0.2.7/32:udp"],"deny":[]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #3","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"icmp\"}]}"}],"expected":{"allow":["192.0.2.7/32:icmp"],"deny":[]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #4","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"any\"}]}"}],"expected":{"allow":["192.0.2.7/32"],"deny":[]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #5","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\"}]}"}],"expected":{"allow":["192.0.2.7/32"],"deny":[]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #6","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"any\", \"port\": 443}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:443","192.0.2.7/32:udp:443"],"deny":[]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #7","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"any\", \"port\": 443, \"endPort\": 444}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:443-444","192.0.2.7/32:udp:443-444"],"deny":[]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #8","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\"}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp"]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #9","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"udp\"}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:udp"]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #10","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"icmp\"}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:icmp"]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #11","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"any\"}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32"]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #12","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\"}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32"]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #13","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"any\", \"port\": 443}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:443","192.0.2.7/32:udp:443"]}}, + {"name":"vector: test_compiles_protocol_selectors_with_and_without_ports #14","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"any\", \"port\": 443, \"endPort\": 444}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:443-444","192.0.2.7/32:udp:443-444"]}}, + {"name":"vector: test_deduplicates_overlapping_exclusions_and_collapses_safe_prefixes #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.128/25\", \"192.0.2.128/25\", \"192.0.2.192/26\"], \"protocol\": \"tcp\", \"port\": 443}, {\"cidr\": \"198.51.100.0/25\", \"protocol\": \"udp\", \"port\": 53}, {\"cidr\": \"198.51.100.128/25\", \"protocol\": \"udp\", \"port\": 53}]}"}],"expected":{"allow":["192.0.2.0/25:tcp:443","198.51.100.0/24:udp:53"],"deny":[]}}, + {"name":"vector: test_empty_destination_does_not_expand_port_range #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.0/24\"], \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 65535}]}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"vector: test_flat_rules_accept_ipv6_networks #1","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8::/32\", \"except\": [\"2001:db8::/33\"], \"protocol\": \"tcp\", \"port\": 443}]}"}],"expected":{"allow":["2001:db8:8000::/33:tcp:443"],"deny":[]}}, + {"name":"vector: test_keeps_a_denied_port_inside_an_allowed_range #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8010}]}], \"deny\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8005}]}]}"}],"expected":{"allow":["192.0.2.0/24:tcp:8000-8010"],"deny":["192.0.2.0/24:tcp:8005"]}}, + {"name":"vector: test_keeps_each_address_family_separate #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"::/96\"}, {\"cidr\": \"0.0.0.0/0\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 80}]}, {\"to\": [{\"cidr\": \"192.0.2.0/24\"}]}, {\"to\": [{\"cidr\": \"::c000:200/120\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 53}]}], \"deny\": [{\"to\": [{\"cidr\": \"0.0.0.0/0\"}]}, {\"to\": [{\"cidr\": \"::/96\"}], \"ports\": [{\"protocol\": \"icmp\"}]}]}"}],"expected":{"allow":["0.0.0.0/0:tcp:80","192.0.2.0/24","::/96:tcp:80","::c000:200/120:udp:53"],"deny":["0.0.0.0/0","::/96:icmp"]}}, + {"name":"vector: test_keeps_network_range_whole_when_adjacent_rule_changes_union #1","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.0/25\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 10}, {\"cidr\": \"10.0.0.128/25\", \"protocol\": \"tcp\", \"port\": 5, \"endPort\": 6}]}"}],"expected":{"allow":["10.0.0.0/24:tcp:5-6","10.0.0.0/25:tcp:1-10"],"deny":[]}}, + {"name":"vector: test_limits_the_actual_policy_read #1","input":[{"text":" ","repeat":1048577}],"expected":{"error":"egress policy file exceeds 1048576-byte limit: {path}"}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8010}]}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:8000-8010"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #2","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8010}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:8000-8010"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #3","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 5000, \"endPort\": 5010}]}]}"}],"expected":{"allow":["192.0.2.7/32:udp:5000-5010"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #4","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"udp\", \"port\": 5000, \"endPort\": 5010}]}"}],"expected":{"allow":["192.0.2.7/32:udp:5000-5010"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #5","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 1, \"endPort\": 65535}]}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:1-65535"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #6","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 65535}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:1-65535"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #7","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 1, \"endPort\": 65535}]}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:1-65535","192.0.2.7/32:udp:1-65535"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #8","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"any\", \"port\": 1, \"endPort\": 65535}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:1-65535","192.0.2.7/32:udp:1-65535"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #9","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8000}]}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:8000"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #10","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8000}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:8000"],"deny":[]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #11","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8010}]}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:8000-8010"]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #12","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8010}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:8000-8010"]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #13","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 5000, \"endPort\": 5010}]}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:udp:5000-5010"]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #14","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"udp\", \"port\": 5000, \"endPort\": 5010}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:udp:5000-5010"]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #15","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 1, \"endPort\": 65535}]}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:1-65535"]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #16","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 65535}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:1-65535"]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #17","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 1, \"endPort\": 65535}]}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:1-65535","192.0.2.7/32:udp:1-65535"]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #18","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"any\", \"port\": 1, \"endPort\": 65535}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:1-65535","192.0.2.7/32:udp:1-65535"]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #19","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.7\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8000}]}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:8000"]}}, + {"name":"vector: test_lowers_each_range_to_one_native_range_rule #20","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8000}]}"}],"expected":{"allow":[],"deny":["192.0.2.7/32:tcp:8000"]}}, + {"name":"vector: test_lowers_the_destinations_of_each_family_separately #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/25\"}, {\"cidr\": \"2001:db8::/32\", \"except\": [\"2001:db8::/33\"]}, {\"cidr\": \"192.0.2.128/25\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 443}]}]}"}],"expected":{"allow":["192.0.2.0/24:tcp:443","2001:db8:8000::/33:tcp:443"],"deny":[]}}, + {"name":"vector: test_many_source_networks_can_collapse_within_native_budget #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.2\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.3\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.4\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.5\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.6\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.8\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.9\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.10\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.11\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.12\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.13\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.14\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.15\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.16\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.17\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.18\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.19\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.20\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.21\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.22\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.23\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.24\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.25\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.26\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.27\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.28\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.29\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.30\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.31\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.32\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.33\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.34\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.35\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.36\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.37\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.38\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.39\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.40\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.41\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.42\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.43\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.44\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.45\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.46\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.47\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.48\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.49\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.50\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.51\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.52\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.53\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.54\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.55\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.56\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.57\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.58\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.59\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.60\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.61\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.62\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.63\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.64\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.65\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.66\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.67\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.68\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.69\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.70\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.71\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.72\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.73\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.74\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.75\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.76\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.77\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.78\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.79\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.80\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.81\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.82\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.83\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.84\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.85\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.86\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.87\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.88\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.89\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.90\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.91\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.92\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.93\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.94\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.95\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.96\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.97\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.98\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.99\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.100\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.101\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.102\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.103\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.104\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.105\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.106\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.107\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.108\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.109\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.110\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.111\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.112\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.113\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.114\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.115\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.116\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.117\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.118\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.119\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.120\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.121\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.122\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.123\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.124\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.125\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.126\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.127\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.128\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.129\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.130\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.131\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.132\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.133\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.134\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.135\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.136\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.137\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.138\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.139\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.140\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.141\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.142\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.143\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.144\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.145\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.146\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.147\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.148\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.149\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.150\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.151\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.152\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.153\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.154\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.155\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.156\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.157\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.158\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.159\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.160\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.161\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.162\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.163\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.164\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.165\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.166\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.167\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.168\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.169\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.170\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.171\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.172\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.173\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.174\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.175\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.176\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.177\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.178\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.179\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.180\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.181\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.182\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.183\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.184\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.185\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.186\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.187\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.188\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.189\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.190\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.191\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.192\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.193\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.194\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.195\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.196\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.197\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.198\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.199\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.200\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.201\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.202\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.203\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.204\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.205\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.206\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.207\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.208\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.209\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.210\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.211\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.212\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.213\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.214\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.215\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.216\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.217\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.218\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.219\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.220\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.221\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.222\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.223\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.224\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.225\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.226\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.227\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.228\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.229\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.230\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.231\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.232\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.233\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.234\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.235\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.236\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.237\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.238\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.239\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.240\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.241\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.242\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.243\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.244\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.245\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.246\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.247\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.248\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.249\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.250\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.251\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.252\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.253\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.254\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.2.255\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.0\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.1\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.2\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.3\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.4\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.5\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.6\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.7\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.8\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.9\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.10\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.11\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.12\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.13\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.14\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.15\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.16\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.17\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.18\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.19\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.20\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.21\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.22\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.23\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.24\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.25\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.26\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.27\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.28\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.29\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.30\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.31\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.32\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.33\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.34\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.35\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.36\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.37\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.38\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.39\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.40\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.41\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.42\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.43\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.44\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.45\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.46\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.47\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.48\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.49\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.50\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.51\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.52\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.53\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.54\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.55\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.56\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.57\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.58\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.59\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.60\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.61\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.62\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.63\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.64\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.65\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.66\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.67\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.68\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.69\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.70\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.71\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.72\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.73\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.74\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.75\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.76\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.77\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.78\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.79\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.80\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.81\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.82\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.83\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.84\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.85\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.86\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.87\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.88\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.89\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.90\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.91\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.92\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.93\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.94\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.95\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.96\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.97\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.98\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.99\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.100\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.101\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.102\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.103\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.104\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.105\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.106\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.107\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.108\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.109\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.110\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.111\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.112\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.113\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.114\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.115\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.116\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.117\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.118\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.119\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.120\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.121\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.122\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.123\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.124\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.125\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.126\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.127\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.128\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.129\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.130\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.131\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.132\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.133\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.134\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.135\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.136\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.137\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.138\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.139\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.140\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.141\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.142\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.143\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.144\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.145\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.146\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.147\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.148\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.149\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.150\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.151\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.152\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.153\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.154\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.155\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.156\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.157\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.158\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.159\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.160\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.161\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.162\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.163\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.164\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.165\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.166\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.167\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.168\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.169\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.170\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.171\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.172\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.173\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.174\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.175\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.176\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.177\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.178\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.179\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.180\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.181\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.182\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.183\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.184\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.185\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.186\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.187\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.188\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.189\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.190\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.191\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.192\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.193\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.194\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.195\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.196\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.197\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.198\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.199\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.200\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.201\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.202\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.203\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.204\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.205\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.206\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.207\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.208\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.209\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.210\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.211\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.212\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.213\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.214\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.215\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.216\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.217\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.218\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.219\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.220\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.221\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.222\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.223\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.224\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.225\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.226\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.227\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.228\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.229\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.230\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.231\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.232\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.233\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.234\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.235\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.236\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.237\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.238\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.239\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.240\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.241\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.242\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.243\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.244\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.245\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.246\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.247\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.248\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.249\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.250\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.251\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.252\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.253\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.254\", \"protocol\": \"tcp\", \"port\": 80}, {\"cidr\": \"192.0.3.255\", \"protocol\": \"tcp\", \"port\": 80}]}"}],"expected":{"allow":["192.0.2.0/23:tcp:80"],"deny":[]}}, + {"name":"vector: test_merges_adjacent_and_overlapping_ranges_of_one_network #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8004}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8005, \"endPort\": 8010}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:8000-8010"],"deny":[]}}, + {"name":"vector: test_merges_adjacent_and_overlapping_ranges_of_one_network #2","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8008}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8003, \"endPort\": 8010}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:8000-8010"],"deny":[]}}, + {"name":"vector: test_merges_adjacent_and_overlapping_ranges_of_one_network #3","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8010}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8000}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8001, \"endPort\": 8001}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8002, \"endPort\": 8002}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8003, \"endPort\": 8003}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8004, \"endPort\": 8004}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8005, \"endPort\": 8005}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8006, \"endPort\": 8006}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8007, \"endPort\": 8007}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8008, \"endPort\": 8008}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8009, \"endPort\": 8009}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8010, \"endPort\": 8010}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:8000-8010"],"deny":[]}}, + {"name":"vector: test_merges_adjacent_and_overlapping_ranges_of_one_network #4","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 8004}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 8006, \"endPort\": 8010}]}"}],"expected":{"allow":["192.0.2.7/32:tcp:8000-8004","192.0.2.7/32:tcp:8006-8010"],"deny":[]}}, + {"name":"vector: test_merges_adjacent_and_overlapping_ranges_of_one_network #5","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"udp\", \"port\": 1, \"endPort\": 10}, {\"cidr\": \"198.51.100.9\", \"protocol\": \"udp\", \"port\": 5, \"endPort\": 20}]}"}],"expected":{"allow":["192.0.2.7/32:udp:1-10","198.51.100.9/32:udp:5-20"],"deny":[]}}, + {"name":"vector: test_normalizes_host_bits_like_the_native_cidr_parser #1","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/24\"}, {\"cidr\": \"fe80::1/64\"}]}"}],"expected":{"allow":["10.0.0.0/24","fe80::/64"],"deny":[]}}, + {"name":"vector: test_overlapping_segments_do_not_inflate_native_rule_budget #1","input":[{"text":"{\"allow\": [{\"cidr\": \"0.0.0.0/0\", \"except\": [\"10.0.0.0/8\"], \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 65535}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1000}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1002}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1004}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1006}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1008}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1010}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1012}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1014}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1016}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1018}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1020}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1022}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1024}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1026}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1028}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1030}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1032}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1034}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1036}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1038}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1040}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1042}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1044}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1046}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1048}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1050}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1052}, {\"cidr\": \"10.0.0.0/8\", \"protocol\": \"tcp\", \"port\": 1054}]}"}],"expected":{"allow":["0.0.0.0/0:tcp:1000","0.0.0.0/0:tcp:1002","0.0.0.0/0:tcp:1004","0.0.0.0/0:tcp:1006","0.0.0.0/0:tcp:1008","0.0.0.0/0:tcp:1010","0.0.0.0/0:tcp:1012","0.0.0.0/0:tcp:1014","0.0.0.0/0:tcp:1016","0.0.0.0/0:tcp:1018","0.0.0.0/0:tcp:1020","0.0.0.0/0:tcp:1022","0.0.0.0/0:tcp:1024","0.0.0.0/0:tcp:1026","0.0.0.0/0:tcp:1028","0.0.0.0/0:tcp:1030","0.0.0.0/0:tcp:1032","0.0.0.0/0:tcp:1034","0.0.0.0/0:tcp:1036","0.0.0.0/0:tcp:1038","0.0.0.0/0:tcp:1040","0.0.0.0/0:tcp:1042","0.0.0.0/0:tcp:1044","0.0.0.0/0:tcp:1046","0.0.0.0/0:tcp:1048","0.0.0.0/0:tcp:1050","0.0.0.0/0:tcp:1052","0.0.0.0/0:tcp:1054","0.0.0.0/5:tcp:1-65535","8.0.0.0/7:tcp:1-65535","11.0.0.0/8:tcp:1-65535","12.0.0.0/6:tcp:1-65535","16.0.0.0/4:tcp:1-65535","32.0.0.0/3:tcp:1-65535","64.0.0.0/2:tcp:1-65535","128.0.0.0/1:tcp:1-65535"],"deny":[]}}, + {"name":"vector: test_partial_address_only_canonicalization_does_not_bridge_gaps #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.1/32\"}, {\"cidr\": \"192.0.2.0/32\", \"protocol\": \"tcp\", \"port\": 443}, {\"cidr\": \"192.0.2.3/32\", \"protocol\": \"tcp\", \"port\": 443}]}"}],"expected":{"allow":["192.0.2.0/31:tcp:443","192.0.2.1/32","192.0.2.3/32:tcp:443"],"deny":[]}}, + {"name":"vector: test_partial_address_only_coverage_keeps_protocol_union_compact #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.0.1/32\"}, {\"cidr\": \"192.0.0.3/32\"}, {\"cidr\": \"192.0.0.5/32\"}, {\"cidr\": \"192.0.0.7/32\"}, {\"cidr\": \"192.0.0.9/32\"}, {\"cidr\": \"192.0.0.11/32\"}, {\"cidr\": \"192.0.0.13/32\"}, {\"cidr\": \"192.0.0.15/32\"}, {\"cidr\": \"192.0.0.17/32\"}, {\"cidr\": \"192.0.0.19/32\"}, {\"cidr\": \"192.0.0.21/32\"}, {\"cidr\": \"192.0.0.23/32\"}, {\"cidr\": \"192.0.0.25/32\"}, {\"cidr\": \"192.0.0.27/32\"}, {\"cidr\": \"192.0.0.29/32\"}, {\"cidr\": \"192.0.0.31/32\"}, {\"cidr\": \"192.0.0.33/32\"}, {\"cidr\": \"192.0.0.35/32\"}, {\"cidr\": \"192.0.0.37/32\"}, {\"cidr\": \"192.0.0.39/32\"}, {\"cidr\": \"192.0.0.41/32\"}, {\"cidr\": \"192.0.0.43/32\"}, {\"cidr\": \"192.0.0.45/32\"}, {\"cidr\": \"192.0.0.47/32\"}, {\"cidr\": \"192.0.0.49/32\"}, {\"cidr\": \"192.0.0.51/32\"}, {\"cidr\": \"192.0.0.53/32\"}, {\"cidr\": \"192.0.0.55/32\"}, {\"cidr\": \"192.0.0.57/32\"}, {\"cidr\": \"192.0.0.59/32\"}, {\"cidr\": \"192.0.0.61/32\"}, {\"cidr\": \"192.0.0.63/32\"}, {\"cidr\": \"192.0.0.65/32\"}, {\"cidr\": \"192.0.0.67/32\"}, {\"cidr\": \"192.0.0.69/32\"}, {\"cidr\": \"192.0.0.71/32\"}, {\"cidr\": \"192.0.0.73/32\"}, {\"cidr\": \"192.0.0.75/32\"}, {\"cidr\": \"192.0.0.77/32\"}, {\"cidr\": \"192.0.0.79/32\"}, {\"cidr\": \"192.0.0.81/32\"}, {\"cidr\": \"192.0.0.83/32\"}, {\"cidr\": \"192.0.0.85/32\"}, {\"cidr\": \"192.0.0.87/32\"}, {\"cidr\": \"192.0.0.89/32\"}, {\"cidr\": \"192.0.0.91/32\"}, {\"cidr\": \"192.0.0.93/32\"}, {\"cidr\": \"192.0.0.95/32\"}, {\"cidr\": \"192.0.0.97/32\"}, {\"cidr\": \"192.0.0.99/32\"}, {\"cidr\": \"192.0.0.101/32\"}, {\"cidr\": \"192.0.0.103/32\"}, {\"cidr\": \"192.0.0.105/32\"}, {\"cidr\": \"192.0.0.107/32\"}, {\"cidr\": \"192.0.0.109/32\"}, {\"cidr\": \"192.0.0.111/32\"}, {\"cidr\": \"192.0.0.113/32\"}, {\"cidr\": \"192.0.0.115/32\"}, {\"cidr\": \"192.0.0.117/32\"}, {\"cidr\": \"192.0.0.119/32\"}, {\"cidr\": \"192.0.0.121/32\"}, {\"cidr\": \"192.0.0.123/32\"}, {\"cidr\": \"192.0.0.125/32\"}, {\"cidr\": \"192.0.0.127/32\"}, {\"cidr\": \"192.0.0.129/32\"}, {\"cidr\": \"192.0.0.131/32\"}, {\"cidr\": \"192.0.0.133/32\"}, {\"cidr\": \"192.0.0.135/32\"}, {\"cidr\": \"192.0.0.137/32\"}, {\"cidr\": \"192.0.0.139/32\"}, {\"cidr\": \"192.0.0.141/32\"}, {\"cidr\": \"192.0.0.143/32\"}, {\"cidr\": \"192.0.0.145/32\"}, {\"cidr\": \"192.0.0.147/32\"}, {\"cidr\": \"192.0.0.149/32\"}, {\"cidr\": \"192.0.0.151/32\"}, {\"cidr\": \"192.0.0.153/32\"}, {\"cidr\": \"192.0.0.155/32\"}, {\"cidr\": \"192.0.0.157/32\"}, {\"cidr\": \"192.0.0.159/32\"}, {\"cidr\": \"192.0.0.161/32\"}, {\"cidr\": \"192.0.0.163/32\"}, {\"cidr\": \"192.0.0.165/32\"}, {\"cidr\": \"192.0.0.167/32\"}, {\"cidr\": \"192.0.0.169/32\"}, {\"cidr\": \"192.0.0.171/32\"}, {\"cidr\": \"192.0.0.173/32\"}, {\"cidr\": \"192.0.0.175/32\"}, {\"cidr\": \"192.0.0.177/32\"}, {\"cidr\": \"192.0.0.179/32\"}, {\"cidr\": \"192.0.0.181/32\"}, {\"cidr\": \"192.0.0.183/32\"}, {\"cidr\": \"192.0.0.185/32\"}, {\"cidr\": \"192.0.0.187/32\"}, {\"cidr\": \"192.0.0.189/32\"}, {\"cidr\": \"192.0.0.191/32\"}, {\"cidr\": \"192.0.0.193/32\"}, {\"cidr\": \"192.0.0.195/32\"}, {\"cidr\": \"192.0.0.197/32\"}, {\"cidr\": \"192.0.0.199/32\"}, {\"cidr\": \"192.0.0.201/32\"}, {\"cidr\": \"192.0.0.203/32\"}, {\"cidr\": \"192.0.0.205/32\"}, {\"cidr\": \"192.0.0.207/32\"}, {\"cidr\": \"192.0.0.209/32\"}, {\"cidr\": \"192.0.0.211/32\"}, {\"cidr\": \"192.0.0.213/32\"}, {\"cidr\": \"192.0.0.215/32\"}, {\"cidr\": \"192.0.0.217/32\"}, {\"cidr\": \"192.0.0.219/32\"}, {\"cidr\": \"192.0.0.221/32\"}, {\"cidr\": \"192.0.0.223/32\"}, {\"cidr\": \"192.0.0.225/32\"}, {\"cidr\": \"192.0.0.227/32\"}, {\"cidr\": \"192.0.0.229/32\"}, {\"cidr\": \"192.0.0.231/32\"}, {\"cidr\": \"192.0.0.233/32\"}, {\"cidr\": \"192.0.0.235/32\"}, {\"cidr\": \"192.0.0.237/32\"}, {\"cidr\": \"192.0.0.239/32\"}, {\"cidr\": \"192.0.0.241/32\"}, {\"cidr\": \"192.0.0.243/32\"}, {\"cidr\": \"192.0.0.245/32\"}, {\"cidr\": \"192.0.0.247/32\"}, {\"cidr\": \"192.0.0.249/32\"}, {\"cidr\": \"192.0.0.251/32\"}, {\"cidr\": \"192.0.0.253/32\"}, {\"cidr\": \"192.0.0.255/32\"}, {\"cidr\": \"192.0.0.0/16\", \"protocol\": \"tcp\", \"port\": 443}]}"}],"expected":{"allow":["192.0.0.0/16:tcp:443","192.0.0.1/32","192.0.0.3/32","192.0.0.5/32","192.0.0.7/32","192.0.0.9/32","192.0.0.11/32","192.0.0.13/32","192.0.0.15/32","192.0.0.17/32","192.0.0.19/32","192.0.0.21/32","192.0.0.23/32","192.0.0.25/32","192.0.0.27/32","192.0.0.29/32","192.0.0.31/32","192.0.0.33/32","192.0.0.35/32","192.0.0.37/32","192.0.0.39/32","192.0.0.41/32","192.0.0.43/32","192.0.0.45/32","192.0.0.47/32","192.0.0.49/32","192.0.0.51/32","192.0.0.53/32","192.0.0.55/32","192.0.0.57/32","192.0.0.59/32","192.0.0.61/32","192.0.0.63/32","192.0.0.65/32","192.0.0.67/32","192.0.0.69/32","192.0.0.71/32","192.0.0.73/32","192.0.0.75/32","192.0.0.77/32","192.0.0.79/32","192.0.0.81/32","192.0.0.83/32","192.0.0.85/32","192.0.0.87/32","192.0.0.89/32","192.0.0.91/32","192.0.0.93/32","192.0.0.95/32","192.0.0.97/32","192.0.0.99/32","192.0.0.101/32","192.0.0.103/32","192.0.0.105/32","192.0.0.107/32","192.0.0.109/32","192.0.0.111/32","192.0.0.113/32","192.0.0.115/32","192.0.0.117/32","192.0.0.119/32","192.0.0.121/32","192.0.0.123/32","192.0.0.125/32","192.0.0.127/32","192.0.0.129/32","192.0.0.131/32","192.0.0.133/32","192.0.0.135/32","192.0.0.137/32","192.0.0.139/32","192.0.0.141/32","192.0.0.143/32","192.0.0.145/32","192.0.0.147/32","192.0.0.149/32","192.0.0.151/32","192.0.0.153/32","192.0.0.155/32","192.0.0.157/32","192.0.0.159/32","192.0.0.161/32","192.0.0.163/32","192.0.0.165/32","192.0.0.167/32","192.0.0.169/32","192.0.0.171/32","192.0.0.173/32","192.0.0.175/32","192.0.0.177/32","192.0.0.179/32","192.0.0.181/32","192.0.0.183/32","192.0.0.185/32","192.0.0.187/32","192.0.0.189/32","192.0.0.191/32","192.0.0.193/32","192.0.0.195/32","192.0.0.197/32","192.0.0.199/32","192.0.0.201/32","192.0.0.203/32","192.0.0.205/32","192.0.0.207/32","192.0.0.209/32","192.0.0.211/32","192.0.0.213/32","192.0.0.215/32","192.0.0.217/32","192.0.0.219/32","192.0.0.221/32","192.0.0.223/32","192.0.0.225/32","192.0.0.227/32","192.0.0.229/32","192.0.0.231/32","192.0.0.233/32","192.0.0.235/32","192.0.0.237/32","192.0.0.239/32","192.0.0.241/32","192.0.0.243/32","192.0.0.245/32","192.0.0.247/32","192.0.0.249/32","192.0.0.251/32","192.0.0.253/32","192.0.0.255/32"],"deny":[]}}, + {"name":"vector: test_partial_address_only_coverage_keeps_protocol_union_compact #2","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.0.0/16\", \"protocol\": \"tcp\", \"port\": 443}, {\"cidr\": \"192.0.0.1/32\"}, {\"cidr\": \"192.0.0.3/32\"}, {\"cidr\": \"192.0.0.5/32\"}, {\"cidr\": \"192.0.0.7/32\"}, {\"cidr\": \"192.0.0.9/32\"}, {\"cidr\": \"192.0.0.11/32\"}, {\"cidr\": \"192.0.0.13/32\"}, {\"cidr\": \"192.0.0.15/32\"}, {\"cidr\": \"192.0.0.17/32\"}, {\"cidr\": \"192.0.0.19/32\"}, {\"cidr\": \"192.0.0.21/32\"}, {\"cidr\": \"192.0.0.23/32\"}, {\"cidr\": \"192.0.0.25/32\"}, {\"cidr\": \"192.0.0.27/32\"}, {\"cidr\": \"192.0.0.29/32\"}, {\"cidr\": \"192.0.0.31/32\"}, {\"cidr\": \"192.0.0.33/32\"}, {\"cidr\": \"192.0.0.35/32\"}, {\"cidr\": \"192.0.0.37/32\"}, {\"cidr\": \"192.0.0.39/32\"}, {\"cidr\": \"192.0.0.41/32\"}, {\"cidr\": \"192.0.0.43/32\"}, {\"cidr\": \"192.0.0.45/32\"}, {\"cidr\": \"192.0.0.47/32\"}, {\"cidr\": \"192.0.0.49/32\"}, {\"cidr\": \"192.0.0.51/32\"}, {\"cidr\": \"192.0.0.53/32\"}, {\"cidr\": \"192.0.0.55/32\"}, {\"cidr\": \"192.0.0.57/32\"}, {\"cidr\": \"192.0.0.59/32\"}, {\"cidr\": \"192.0.0.61/32\"}, {\"cidr\": \"192.0.0.63/32\"}, {\"cidr\": \"192.0.0.65/32\"}, {\"cidr\": \"192.0.0.67/32\"}, {\"cidr\": \"192.0.0.69/32\"}, {\"cidr\": \"192.0.0.71/32\"}, {\"cidr\": \"192.0.0.73/32\"}, {\"cidr\": \"192.0.0.75/32\"}, {\"cidr\": \"192.0.0.77/32\"}, {\"cidr\": \"192.0.0.79/32\"}, {\"cidr\": \"192.0.0.81/32\"}, {\"cidr\": \"192.0.0.83/32\"}, {\"cidr\": \"192.0.0.85/32\"}, {\"cidr\": \"192.0.0.87/32\"}, {\"cidr\": \"192.0.0.89/32\"}, {\"cidr\": \"192.0.0.91/32\"}, {\"cidr\": \"192.0.0.93/32\"}, {\"cidr\": \"192.0.0.95/32\"}, {\"cidr\": \"192.0.0.97/32\"}, {\"cidr\": \"192.0.0.99/32\"}, {\"cidr\": \"192.0.0.101/32\"}, {\"cidr\": \"192.0.0.103/32\"}, {\"cidr\": \"192.0.0.105/32\"}, {\"cidr\": \"192.0.0.107/32\"}, {\"cidr\": \"192.0.0.109/32\"}, {\"cidr\": \"192.0.0.111/32\"}, {\"cidr\": \"192.0.0.113/32\"}, {\"cidr\": \"192.0.0.115/32\"}, {\"cidr\": \"192.0.0.117/32\"}, {\"cidr\": \"192.0.0.119/32\"}, {\"cidr\": \"192.0.0.121/32\"}, {\"cidr\": \"192.0.0.123/32\"}, {\"cidr\": \"192.0.0.125/32\"}, {\"cidr\": \"192.0.0.127/32\"}, {\"cidr\": \"192.0.0.129/32\"}, {\"cidr\": \"192.0.0.131/32\"}, {\"cidr\": \"192.0.0.133/32\"}, {\"cidr\": \"192.0.0.135/32\"}, {\"cidr\": \"192.0.0.137/32\"}, {\"cidr\": \"192.0.0.139/32\"}, {\"cidr\": \"192.0.0.141/32\"}, {\"cidr\": \"192.0.0.143/32\"}, {\"cidr\": \"192.0.0.145/32\"}, {\"cidr\": \"192.0.0.147/32\"}, {\"cidr\": \"192.0.0.149/32\"}, {\"cidr\": \"192.0.0.151/32\"}, {\"cidr\": \"192.0.0.153/32\"}, {\"cidr\": \"192.0.0.155/32\"}, {\"cidr\": \"192.0.0.157/32\"}, {\"cidr\": \"192.0.0.159/32\"}, {\"cidr\": \"192.0.0.161/32\"}, {\"cidr\": \"192.0.0.163/32\"}, {\"cidr\": \"192.0.0.165/32\"}, {\"cidr\": \"192.0.0.167/32\"}, {\"cidr\": \"192.0.0.169/32\"}, {\"cidr\": \"192.0.0.171/32\"}, {\"cidr\": \"192.0.0.173/32\"}, {\"cidr\": \"192.0.0.175/32\"}, {\"cidr\": \"192.0.0.177/32\"}, {\"cidr\": \"192.0.0.179/32\"}, {\"cidr\": \"192.0.0.181/32\"}, {\"cidr\": \"192.0.0.183/32\"}, {\"cidr\": \"192.0.0.185/32\"}, {\"cidr\": \"192.0.0.187/32\"}, {\"cidr\": \"192.0.0.189/32\"}, {\"cidr\": \"192.0.0.191/32\"}, {\"cidr\": \"192.0.0.193/32\"}, {\"cidr\": \"192.0.0.195/32\"}, {\"cidr\": \"192.0.0.197/32\"}, {\"cidr\": \"192.0.0.199/32\"}, {\"cidr\": \"192.0.0.201/32\"}, {\"cidr\": \"192.0.0.203/32\"}, {\"cidr\": \"192.0.0.205/32\"}, {\"cidr\": \"192.0.0.207/32\"}, {\"cidr\": \"192.0.0.209/32\"}, {\"cidr\": \"192.0.0.211/32\"}, {\"cidr\": \"192.0.0.213/32\"}, {\"cidr\": \"192.0.0.215/32\"}, {\"cidr\": \"192.0.0.217/32\"}, {\"cidr\": \"192.0.0.219/32\"}, {\"cidr\": \"192.0.0.221/32\"}, {\"cidr\": \"192.0.0.223/32\"}, {\"cidr\": \"192.0.0.225/32\"}, {\"cidr\": \"192.0.0.227/32\"}, {\"cidr\": \"192.0.0.229/32\"}, {\"cidr\": \"192.0.0.231/32\"}, {\"cidr\": \"192.0.0.233/32\"}, {\"cidr\": \"192.0.0.235/32\"}, {\"cidr\": \"192.0.0.237/32\"}, {\"cidr\": \"192.0.0.239/32\"}, {\"cidr\": \"192.0.0.241/32\"}, {\"cidr\": \"192.0.0.243/32\"}, {\"cidr\": \"192.0.0.245/32\"}, {\"cidr\": \"192.0.0.247/32\"}, {\"cidr\": \"192.0.0.249/32\"}, {\"cidr\": \"192.0.0.251/32\"}, {\"cidr\": \"192.0.0.253/32\"}, {\"cidr\": \"192.0.0.255/32\"}]}"}],"expected":{"allow":["192.0.0.0/16:tcp:443","192.0.0.1/32","192.0.0.3/32","192.0.0.5/32","192.0.0.7/32","192.0.0.9/32","192.0.0.11/32","192.0.0.13/32","192.0.0.15/32","192.0.0.17/32","192.0.0.19/32","192.0.0.21/32","192.0.0.23/32","192.0.0.25/32","192.0.0.27/32","192.0.0.29/32","192.0.0.31/32","192.0.0.33/32","192.0.0.35/32","192.0.0.37/32","192.0.0.39/32","192.0.0.41/32","192.0.0.43/32","192.0.0.45/32","192.0.0.47/32","192.0.0.49/32","192.0.0.51/32","192.0.0.53/32","192.0.0.55/32","192.0.0.57/32","192.0.0.59/32","192.0.0.61/32","192.0.0.63/32","192.0.0.65/32","192.0.0.67/32","192.0.0.69/32","192.0.0.71/32","192.0.0.73/32","192.0.0.75/32","192.0.0.77/32","192.0.0.79/32","192.0.0.81/32","192.0.0.83/32","192.0.0.85/32","192.0.0.87/32","192.0.0.89/32","192.0.0.91/32","192.0.0.93/32","192.0.0.95/32","192.0.0.97/32","192.0.0.99/32","192.0.0.101/32","192.0.0.103/32","192.0.0.105/32","192.0.0.107/32","192.0.0.109/32","192.0.0.111/32","192.0.0.113/32","192.0.0.115/32","192.0.0.117/32","192.0.0.119/32","192.0.0.121/32","192.0.0.123/32","192.0.0.125/32","192.0.0.127/32","192.0.0.129/32","192.0.0.131/32","192.0.0.133/32","192.0.0.135/32","192.0.0.137/32","192.0.0.139/32","192.0.0.141/32","192.0.0.143/32","192.0.0.145/32","192.0.0.147/32","192.0.0.149/32","192.0.0.151/32","192.0.0.153/32","192.0.0.155/32","192.0.0.157/32","192.0.0.159/32","192.0.0.161/32","192.0.0.163/32","192.0.0.165/32","192.0.0.167/32","192.0.0.169/32","192.0.0.171/32","192.0.0.173/32","192.0.0.175/32","192.0.0.177/32","192.0.0.179/32","192.0.0.181/32","192.0.0.183/32","192.0.0.185/32","192.0.0.187/32","192.0.0.189/32","192.0.0.191/32","192.0.0.193/32","192.0.0.195/32","192.0.0.197/32","192.0.0.199/32","192.0.0.201/32","192.0.0.203/32","192.0.0.205/32","192.0.0.207/32","192.0.0.209/32","192.0.0.211/32","192.0.0.213/32","192.0.0.215/32","192.0.0.217/32","192.0.0.219/32","192.0.0.221/32","192.0.0.223/32","192.0.0.225/32","192.0.0.227/32","192.0.0.229/32","192.0.0.231/32","192.0.0.233/32","192.0.0.235/32","192.0.0.237/32","192.0.0.239/32","192.0.0.241/32","192.0.0.243/32","192.0.0.245/32","192.0.0.247/32","192.0.0.249/32","192.0.0.251/32","192.0.0.253/32","192.0.0.255/32"],"deny":[]}}, + {"name":"vector: test_partial_address_only_coverage_keeps_protocol_union_compact #3","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.0.1/32\"}, {\"cidr\": \"192.0.0.3/32\"}, {\"cidr\": \"192.0.0.5/32\"}, {\"cidr\": \"192.0.0.7/32\"}, {\"cidr\": \"192.0.0.9/32\"}, {\"cidr\": \"192.0.0.11/32\"}, {\"cidr\": \"192.0.0.13/32\"}, {\"cidr\": \"192.0.0.15/32\"}, {\"cidr\": \"192.0.0.17/32\"}, {\"cidr\": \"192.0.0.19/32\"}, {\"cidr\": \"192.0.0.21/32\"}, {\"cidr\": \"192.0.0.23/32\"}, {\"cidr\": \"192.0.0.25/32\"}, {\"cidr\": \"192.0.0.27/32\"}, {\"cidr\": \"192.0.0.29/32\"}, {\"cidr\": \"192.0.0.31/32\"}, {\"cidr\": \"192.0.0.33/32\"}, {\"cidr\": \"192.0.0.35/32\"}, {\"cidr\": \"192.0.0.37/32\"}, {\"cidr\": \"192.0.0.39/32\"}, {\"cidr\": \"192.0.0.41/32\"}, {\"cidr\": \"192.0.0.43/32\"}, {\"cidr\": \"192.0.0.45/32\"}, {\"cidr\": \"192.0.0.47/32\"}, {\"cidr\": \"192.0.0.49/32\"}, {\"cidr\": \"192.0.0.51/32\"}, {\"cidr\": \"192.0.0.53/32\"}, {\"cidr\": \"192.0.0.55/32\"}, {\"cidr\": \"192.0.0.57/32\"}, {\"cidr\": \"192.0.0.59/32\"}, {\"cidr\": \"192.0.0.61/32\"}, {\"cidr\": \"192.0.0.63/32\"}, {\"cidr\": \"192.0.0.65/32\"}, {\"cidr\": \"192.0.0.67/32\"}, {\"cidr\": \"192.0.0.69/32\"}, {\"cidr\": \"192.0.0.71/32\"}, {\"cidr\": \"192.0.0.73/32\"}, {\"cidr\": \"192.0.0.75/32\"}, {\"cidr\": \"192.0.0.77/32\"}, {\"cidr\": \"192.0.0.79/32\"}, {\"cidr\": \"192.0.0.81/32\"}, {\"cidr\": \"192.0.0.83/32\"}, {\"cidr\": \"192.0.0.85/32\"}, {\"cidr\": \"192.0.0.87/32\"}, {\"cidr\": \"192.0.0.89/32\"}, {\"cidr\": \"192.0.0.91/32\"}, {\"cidr\": \"192.0.0.93/32\"}, {\"cidr\": \"192.0.0.95/32\"}, {\"cidr\": \"192.0.0.97/32\"}, {\"cidr\": \"192.0.0.99/32\"}, {\"cidr\": \"192.0.0.101/32\"}, {\"cidr\": \"192.0.0.103/32\"}, {\"cidr\": \"192.0.0.105/32\"}, {\"cidr\": \"192.0.0.107/32\"}, {\"cidr\": \"192.0.0.109/32\"}, {\"cidr\": \"192.0.0.111/32\"}, {\"cidr\": \"192.0.0.113/32\"}, {\"cidr\": \"192.0.0.115/32\"}, {\"cidr\": \"192.0.0.117/32\"}, {\"cidr\": \"192.0.0.119/32\"}, {\"cidr\": \"192.0.0.121/32\"}, {\"cidr\": \"192.0.0.123/32\"}, {\"cidr\": \"192.0.0.125/32\"}, {\"cidr\": \"192.0.0.127/32\"}, {\"cidr\": \"192.0.0.129/32\"}, {\"cidr\": \"192.0.0.131/32\"}, {\"cidr\": \"192.0.0.133/32\"}, {\"cidr\": \"192.0.0.135/32\"}, {\"cidr\": \"192.0.0.137/32\"}, {\"cidr\": \"192.0.0.139/32\"}, {\"cidr\": \"192.0.0.141/32\"}, {\"cidr\": \"192.0.0.143/32\"}, {\"cidr\": \"192.0.0.145/32\"}, {\"cidr\": \"192.0.0.147/32\"}, {\"cidr\": \"192.0.0.149/32\"}, {\"cidr\": \"192.0.0.151/32\"}, {\"cidr\": \"192.0.0.153/32\"}, {\"cidr\": \"192.0.0.155/32\"}, {\"cidr\": \"192.0.0.157/32\"}, {\"cidr\": \"192.0.0.159/32\"}, {\"cidr\": \"192.0.0.161/32\"}, {\"cidr\": \"192.0.0.163/32\"}, {\"cidr\": \"192.0.0.165/32\"}, {\"cidr\": \"192.0.0.167/32\"}, {\"cidr\": \"192.0.0.169/32\"}, {\"cidr\": \"192.0.0.171/32\"}, {\"cidr\": \"192.0.0.173/32\"}, {\"cidr\": \"192.0.0.175/32\"}, {\"cidr\": \"192.0.0.177/32\"}, {\"cidr\": \"192.0.0.179/32\"}, {\"cidr\": \"192.0.0.181/32\"}, {\"cidr\": \"192.0.0.183/32\"}, {\"cidr\": \"192.0.0.185/32\"}, {\"cidr\": \"192.0.0.187/32\"}, {\"cidr\": \"192.0.0.189/32\"}, {\"cidr\": \"192.0.0.191/32\"}, {\"cidr\": \"192.0.0.193/32\"}, {\"cidr\": \"192.0.0.195/32\"}, {\"cidr\": \"192.0.0.197/32\"}, {\"cidr\": \"192.0.0.199/32\"}, {\"cidr\": \"192.0.0.201/32\"}, {\"cidr\": \"192.0.0.203/32\"}, {\"cidr\": \"192.0.0.205/32\"}, {\"cidr\": \"192.0.0.207/32\"}, {\"cidr\": \"192.0.0.209/32\"}, {\"cidr\": \"192.0.0.211/32\"}, {\"cidr\": \"192.0.0.213/32\"}, {\"cidr\": \"192.0.0.215/32\"}, {\"cidr\": \"192.0.0.217/32\"}, {\"cidr\": \"192.0.0.219/32\"}, {\"cidr\": \"192.0.0.221/32\"}, {\"cidr\": \"192.0.0.223/32\"}, {\"cidr\": \"192.0.0.225/32\"}, {\"cidr\": \"192.0.0.227/32\"}, {\"cidr\": \"192.0.0.229/32\"}, {\"cidr\": \"192.0.0.231/32\"}, {\"cidr\": \"192.0.0.233/32\"}, {\"cidr\": \"192.0.0.235/32\"}, {\"cidr\": \"192.0.0.237/32\"}, {\"cidr\": \"192.0.0.239/32\"}, {\"cidr\": \"192.0.0.241/32\"}, {\"cidr\": \"192.0.0.243/32\"}, {\"cidr\": \"192.0.0.245/32\"}, {\"cidr\": \"192.0.0.247/32\"}, {\"cidr\": \"192.0.0.249/32\"}, {\"cidr\": \"192.0.0.251/32\"}, {\"cidr\": \"192.0.0.253/32\"}, {\"cidr\": \"192.0.0.255/32\"}, {\"cidr\": \"192.0.0.0/16\", \"protocol\": \"tcp\", \"port\": 443}]}"}],"expected":{"allow":[],"deny":["192.0.0.0/16:tcp:443","192.0.0.1/32","192.0.0.3/32","192.0.0.5/32","192.0.0.7/32","192.0.0.9/32","192.0.0.11/32","192.0.0.13/32","192.0.0.15/32","192.0.0.17/32","192.0.0.19/32","192.0.0.21/32","192.0.0.23/32","192.0.0.25/32","192.0.0.27/32","192.0.0.29/32","192.0.0.31/32","192.0.0.33/32","192.0.0.35/32","192.0.0.37/32","192.0.0.39/32","192.0.0.41/32","192.0.0.43/32","192.0.0.45/32","192.0.0.47/32","192.0.0.49/32","192.0.0.51/32","192.0.0.53/32","192.0.0.55/32","192.0.0.57/32","192.0.0.59/32","192.0.0.61/32","192.0.0.63/32","192.0.0.65/32","192.0.0.67/32","192.0.0.69/32","192.0.0.71/32","192.0.0.73/32","192.0.0.75/32","192.0.0.77/32","192.0.0.79/32","192.0.0.81/32","192.0.0.83/32","192.0.0.85/32","192.0.0.87/32","192.0.0.89/32","192.0.0.91/32","192.0.0.93/32","192.0.0.95/32","192.0.0.97/32","192.0.0.99/32","192.0.0.101/32","192.0.0.103/32","192.0.0.105/32","192.0.0.107/32","192.0.0.109/32","192.0.0.111/32","192.0.0.113/32","192.0.0.115/32","192.0.0.117/32","192.0.0.119/32","192.0.0.121/32","192.0.0.123/32","192.0.0.125/32","192.0.0.127/32","192.0.0.129/32","192.0.0.131/32","192.0.0.133/32","192.0.0.135/32","192.0.0.137/32","192.0.0.139/32","192.0.0.141/32","192.0.0.143/32","192.0.0.145/32","192.0.0.147/32","192.0.0.149/32","192.0.0.151/32","192.0.0.153/32","192.0.0.155/32","192.0.0.157/32","192.0.0.159/32","192.0.0.161/32","192.0.0.163/32","192.0.0.165/32","192.0.0.167/32","192.0.0.169/32","192.0.0.171/32","192.0.0.173/32","192.0.0.175/32","192.0.0.177/32","192.0.0.179/32","192.0.0.181/32","192.0.0.183/32","192.0.0.185/32","192.0.0.187/32","192.0.0.189/32","192.0.0.191/32","192.0.0.193/32","192.0.0.195/32","192.0.0.197/32","192.0.0.199/32","192.0.0.201/32","192.0.0.203/32","192.0.0.205/32","192.0.0.207/32","192.0.0.209/32","192.0.0.211/32","192.0.0.213/32","192.0.0.215/32","192.0.0.217/32","192.0.0.219/32","192.0.0.221/32","192.0.0.223/32","192.0.0.225/32","192.0.0.227/32","192.0.0.229/32","192.0.0.231/32","192.0.0.233/32","192.0.0.235/32","192.0.0.237/32","192.0.0.239/32","192.0.0.241/32","192.0.0.243/32","192.0.0.245/32","192.0.0.247/32","192.0.0.249/32","192.0.0.251/32","192.0.0.253/32","192.0.0.255/32"]}}, + {"name":"vector: test_partial_address_only_coverage_keeps_protocol_union_compact #4","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.0.0/16\", \"protocol\": \"tcp\", \"port\": 443}, {\"cidr\": \"192.0.0.1/32\"}, {\"cidr\": \"192.0.0.3/32\"}, {\"cidr\": \"192.0.0.5/32\"}, {\"cidr\": \"192.0.0.7/32\"}, {\"cidr\": \"192.0.0.9/32\"}, {\"cidr\": \"192.0.0.11/32\"}, {\"cidr\": \"192.0.0.13/32\"}, {\"cidr\": \"192.0.0.15/32\"}, {\"cidr\": \"192.0.0.17/32\"}, {\"cidr\": \"192.0.0.19/32\"}, {\"cidr\": \"192.0.0.21/32\"}, {\"cidr\": \"192.0.0.23/32\"}, {\"cidr\": \"192.0.0.25/32\"}, {\"cidr\": \"192.0.0.27/32\"}, {\"cidr\": \"192.0.0.29/32\"}, {\"cidr\": \"192.0.0.31/32\"}, {\"cidr\": \"192.0.0.33/32\"}, {\"cidr\": \"192.0.0.35/32\"}, {\"cidr\": \"192.0.0.37/32\"}, {\"cidr\": \"192.0.0.39/32\"}, {\"cidr\": \"192.0.0.41/32\"}, {\"cidr\": \"192.0.0.43/32\"}, {\"cidr\": \"192.0.0.45/32\"}, {\"cidr\": \"192.0.0.47/32\"}, {\"cidr\": \"192.0.0.49/32\"}, {\"cidr\": \"192.0.0.51/32\"}, {\"cidr\": \"192.0.0.53/32\"}, {\"cidr\": \"192.0.0.55/32\"}, {\"cidr\": \"192.0.0.57/32\"}, {\"cidr\": \"192.0.0.59/32\"}, {\"cidr\": \"192.0.0.61/32\"}, {\"cidr\": \"192.0.0.63/32\"}, {\"cidr\": \"192.0.0.65/32\"}, {\"cidr\": \"192.0.0.67/32\"}, {\"cidr\": \"192.0.0.69/32\"}, {\"cidr\": \"192.0.0.71/32\"}, {\"cidr\": \"192.0.0.73/32\"}, {\"cidr\": \"192.0.0.75/32\"}, {\"cidr\": \"192.0.0.77/32\"}, {\"cidr\": \"192.0.0.79/32\"}, {\"cidr\": \"192.0.0.81/32\"}, {\"cidr\": \"192.0.0.83/32\"}, {\"cidr\": \"192.0.0.85/32\"}, {\"cidr\": \"192.0.0.87/32\"}, {\"cidr\": \"192.0.0.89/32\"}, {\"cidr\": \"192.0.0.91/32\"}, {\"cidr\": \"192.0.0.93/32\"}, {\"cidr\": \"192.0.0.95/32\"}, {\"cidr\": \"192.0.0.97/32\"}, {\"cidr\": \"192.0.0.99/32\"}, {\"cidr\": \"192.0.0.101/32\"}, {\"cidr\": \"192.0.0.103/32\"}, {\"cidr\": \"192.0.0.105/32\"}, {\"cidr\": \"192.0.0.107/32\"}, {\"cidr\": \"192.0.0.109/32\"}, {\"cidr\": \"192.0.0.111/32\"}, {\"cidr\": \"192.0.0.113/32\"}, {\"cidr\": \"192.0.0.115/32\"}, {\"cidr\": \"192.0.0.117/32\"}, {\"cidr\": \"192.0.0.119/32\"}, {\"cidr\": \"192.0.0.121/32\"}, {\"cidr\": \"192.0.0.123/32\"}, {\"cidr\": \"192.0.0.125/32\"}, {\"cidr\": \"192.0.0.127/32\"}, {\"cidr\": \"192.0.0.129/32\"}, {\"cidr\": \"192.0.0.131/32\"}, {\"cidr\": \"192.0.0.133/32\"}, {\"cidr\": \"192.0.0.135/32\"}, {\"cidr\": \"192.0.0.137/32\"}, {\"cidr\": \"192.0.0.139/32\"}, {\"cidr\": \"192.0.0.141/32\"}, {\"cidr\": \"192.0.0.143/32\"}, {\"cidr\": \"192.0.0.145/32\"}, {\"cidr\": \"192.0.0.147/32\"}, {\"cidr\": \"192.0.0.149/32\"}, {\"cidr\": \"192.0.0.151/32\"}, {\"cidr\": \"192.0.0.153/32\"}, {\"cidr\": \"192.0.0.155/32\"}, {\"cidr\": \"192.0.0.157/32\"}, {\"cidr\": \"192.0.0.159/32\"}, {\"cidr\": \"192.0.0.161/32\"}, {\"cidr\": \"192.0.0.163/32\"}, {\"cidr\": \"192.0.0.165/32\"}, {\"cidr\": \"192.0.0.167/32\"}, {\"cidr\": \"192.0.0.169/32\"}, {\"cidr\": \"192.0.0.171/32\"}, {\"cidr\": \"192.0.0.173/32\"}, {\"cidr\": \"192.0.0.175/32\"}, {\"cidr\": \"192.0.0.177/32\"}, {\"cidr\": \"192.0.0.179/32\"}, {\"cidr\": \"192.0.0.181/32\"}, {\"cidr\": \"192.0.0.183/32\"}, {\"cidr\": \"192.0.0.185/32\"}, {\"cidr\": \"192.0.0.187/32\"}, {\"cidr\": \"192.0.0.189/32\"}, {\"cidr\": \"192.0.0.191/32\"}, {\"cidr\": \"192.0.0.193/32\"}, {\"cidr\": \"192.0.0.195/32\"}, {\"cidr\": \"192.0.0.197/32\"}, {\"cidr\": \"192.0.0.199/32\"}, {\"cidr\": \"192.0.0.201/32\"}, {\"cidr\": \"192.0.0.203/32\"}, {\"cidr\": \"192.0.0.205/32\"}, {\"cidr\": \"192.0.0.207/32\"}, {\"cidr\": \"192.0.0.209/32\"}, {\"cidr\": \"192.0.0.211/32\"}, {\"cidr\": \"192.0.0.213/32\"}, {\"cidr\": \"192.0.0.215/32\"}, {\"cidr\": \"192.0.0.217/32\"}, {\"cidr\": \"192.0.0.219/32\"}, {\"cidr\": \"192.0.0.221/32\"}, {\"cidr\": \"192.0.0.223/32\"}, {\"cidr\": \"192.0.0.225/32\"}, {\"cidr\": \"192.0.0.227/32\"}, {\"cidr\": \"192.0.0.229/32\"}, {\"cidr\": \"192.0.0.231/32\"}, {\"cidr\": \"192.0.0.233/32\"}, {\"cidr\": \"192.0.0.235/32\"}, {\"cidr\": \"192.0.0.237/32\"}, {\"cidr\": \"192.0.0.239/32\"}, {\"cidr\": \"192.0.0.241/32\"}, {\"cidr\": \"192.0.0.243/32\"}, {\"cidr\": \"192.0.0.245/32\"}, {\"cidr\": \"192.0.0.247/32\"}, {\"cidr\": \"192.0.0.249/32\"}, {\"cidr\": \"192.0.0.251/32\"}, {\"cidr\": \"192.0.0.253/32\"}, {\"cidr\": \"192.0.0.255/32\"}]}"}],"expected":{"allow":[],"deny":["192.0.0.0/16:tcp:443","192.0.0.1/32","192.0.0.3/32","192.0.0.5/32","192.0.0.7/32","192.0.0.9/32","192.0.0.11/32","192.0.0.13/32","192.0.0.15/32","192.0.0.17/32","192.0.0.19/32","192.0.0.21/32","192.0.0.23/32","192.0.0.25/32","192.0.0.27/32","192.0.0.29/32","192.0.0.31/32","192.0.0.33/32","192.0.0.35/32","192.0.0.37/32","192.0.0.39/32","192.0.0.41/32","192.0.0.43/32","192.0.0.45/32","192.0.0.47/32","192.0.0.49/32","192.0.0.51/32","192.0.0.53/32","192.0.0.55/32","192.0.0.57/32","192.0.0.59/32","192.0.0.61/32","192.0.0.63/32","192.0.0.65/32","192.0.0.67/32","192.0.0.69/32","192.0.0.71/32","192.0.0.73/32","192.0.0.75/32","192.0.0.77/32","192.0.0.79/32","192.0.0.81/32","192.0.0.83/32","192.0.0.85/32","192.0.0.87/32","192.0.0.89/32","192.0.0.91/32","192.0.0.93/32","192.0.0.95/32","192.0.0.97/32","192.0.0.99/32","192.0.0.101/32","192.0.0.103/32","192.0.0.105/32","192.0.0.107/32","192.0.0.109/32","192.0.0.111/32","192.0.0.113/32","192.0.0.115/32","192.0.0.117/32","192.0.0.119/32","192.0.0.121/32","192.0.0.123/32","192.0.0.125/32","192.0.0.127/32","192.0.0.129/32","192.0.0.131/32","192.0.0.133/32","192.0.0.135/32","192.0.0.137/32","192.0.0.139/32","192.0.0.141/32","192.0.0.143/32","192.0.0.145/32","192.0.0.147/32","192.0.0.149/32","192.0.0.151/32","192.0.0.153/32","192.0.0.155/32","192.0.0.157/32","192.0.0.159/32","192.0.0.161/32","192.0.0.163/32","192.0.0.165/32","192.0.0.167/32","192.0.0.169/32","192.0.0.171/32","192.0.0.173/32","192.0.0.175/32","192.0.0.177/32","192.0.0.179/32","192.0.0.181/32","192.0.0.183/32","192.0.0.185/32","192.0.0.187/32","192.0.0.189/32","192.0.0.191/32","192.0.0.193/32","192.0.0.195/32","192.0.0.197/32","192.0.0.199/32","192.0.0.201/32","192.0.0.203/32","192.0.0.205/32","192.0.0.207/32","192.0.0.209/32","192.0.0.211/32","192.0.0.213/32","192.0.0.215/32","192.0.0.217/32","192.0.0.219/32","192.0.0.221/32","192.0.0.223/32","192.0.0.225/32","192.0.0.227/32","192.0.0.229/32","192.0.0.231/32","192.0.0.233/32","192.0.0.235/32","192.0.0.237/32","192.0.0.239/32","192.0.0.241/32","192.0.0.243/32","192.0.0.245/32","192.0.0.247/32","192.0.0.249/32","192.0.0.251/32","192.0.0.253/32","192.0.0.255/32"]}}, + {"name":"vector: test_protocol_wide_rules_cover_their_port_rules_only #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\", \"protocol\": \"tcp\", \"port\": 443}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\"}, {\"cidr\": \"192.0.2.7\", \"protocol\": \"udp\", \"port\": 443}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\"}], \"deny\": [{\"cidr\": \"198.51.100.0/24\", \"protocol\": \"any\", \"port\": 53}, {\"cidr\": \"198.51.100.0/24\", \"protocol\": \"udp\"}]}"}],"expected":{"allow":["192.0.2.0/24:icmp","192.0.2.0/24:tcp","192.0.2.7/32:udp:443"],"deny":["198.51.100.0/24:tcp:53","198.51.100.0/24:udp"]}}, + {"name":"vector: test_protocol_wide_rules_share_the_native_rule_budget #1","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.1/32\"}, {\"cidr\": \"10.0.1.1/32\"}, {\"cidr\": \"10.0.2.1/32\"}, {\"cidr\": \"10.0.3.1/32\"}, {\"cidr\": \"10.0.4.1/32\"}, {\"cidr\": \"10.0.5.1/32\"}, {\"cidr\": \"10.0.6.1/32\"}, {\"cidr\": \"10.0.7.1/32\"}, {\"cidr\": \"10.0.8.1/32\"}, {\"cidr\": \"10.0.9.1/32\"}, {\"cidr\": \"10.0.10.1/32\"}, {\"cidr\": \"10.0.11.1/32\"}, {\"cidr\": \"10.0.12.1/32\"}, {\"cidr\": \"10.0.13.1/32\"}, {\"cidr\": \"10.0.14.1/32\"}, {\"cidr\": \"10.0.15.1/32\"}, {\"cidr\": \"10.0.16.1/32\"}, {\"cidr\": \"10.0.17.1/32\"}, {\"cidr\": \"10.0.18.1/32\"}, {\"cidr\": \"10.0.19.1/32\"}, {\"cidr\": \"10.0.20.1/32\"}, {\"cidr\": \"10.0.21.1/32\"}, {\"cidr\": \"10.0.22.1/32\"}, {\"cidr\": \"10.0.23.1/32\"}, {\"cidr\": \"10.0.24.1/32\"}, {\"cidr\": \"10.0.25.1/32\"}, {\"cidr\": \"10.0.26.1/32\"}, {\"cidr\": \"10.0.27.1/32\"}, {\"cidr\": \"10.0.28.1/32\"}, {\"cidr\": \"10.0.29.1/32\"}, {\"cidr\": \"10.0.30.1/32\"}, {\"cidr\": \"10.0.31.1/32\"}, {\"cidr\": \"10.0.32.1/32\"}, {\"cidr\": \"10.0.33.1/32\"}, {\"cidr\": \"10.0.34.1/32\"}, {\"cidr\": \"10.0.35.1/32\"}, {\"cidr\": \"10.0.36.1/32\"}, {\"cidr\": \"10.0.37.1/32\"}, {\"cidr\": \"10.0.38.1/32\"}, {\"cidr\": \"10.0.39.1/32\"}, {\"cidr\": \"10.0.40.1/32\"}, {\"cidr\": \"10.0.41.1/32\"}, {\"cidr\": \"10.0.42.1/32\"}, {\"cidr\": \"10.0.43.1/32\"}, {\"cidr\": \"10.0.44.1/32\"}, {\"cidr\": \"10.0.45.1/32\"}, {\"cidr\": \"10.0.46.1/32\"}, {\"cidr\": \"10.0.47.1/32\"}, {\"cidr\": \"10.0.48.1/32\"}, {\"cidr\": \"10.0.49.1/32\"}, {\"cidr\": \"10.0.50.1/32\"}, {\"cidr\": \"10.0.51.1/32\"}, {\"cidr\": \"10.0.52.1/32\"}, {\"cidr\": \"10.0.53.1/32\"}, {\"cidr\": \"10.0.54.1/32\"}, {\"cidr\": \"10.0.55.1/32\"}, {\"cidr\": \"10.0.56.1/32\"}, {\"cidr\": \"10.0.57.1/32\"}, {\"cidr\": \"10.0.58.1/32\"}, {\"cidr\": \"10.0.59.1/32\"}, {\"cidr\": \"10.0.60.1/32\"}, {\"cidr\": \"10.0.61.1/32\"}, {\"cidr\": \"10.0.62.1/32\"}, {\"cidr\": \"10.0.63.1/32\"}, {\"cidr\": \"10.0.64.1/32\"}, {\"cidr\": \"10.0.65.1/32\"}, {\"cidr\": \"10.0.66.1/32\"}, {\"cidr\": \"10.0.67.1/32\"}, {\"cidr\": \"10.0.68.1/32\"}, {\"cidr\": \"10.0.69.1/32\"}, {\"cidr\": \"10.0.70.1/32\"}, {\"cidr\": \"10.0.71.1/32\"}, {\"cidr\": \"10.0.72.1/32\"}, {\"cidr\": \"10.0.73.1/32\"}, {\"cidr\": \"10.0.74.1/32\"}, {\"cidr\": \"10.0.75.1/32\"}, {\"cidr\": \"10.0.76.1/32\"}, {\"cidr\": \"10.0.77.1/32\"}, {\"cidr\": \"10.0.78.1/32\"}, {\"cidr\": \"10.0.79.1/32\"}, {\"cidr\": \"10.0.80.1/32\"}, {\"cidr\": \"10.0.81.1/32\"}, {\"cidr\": \"10.0.82.1/32\"}, {\"cidr\": \"10.0.83.1/32\"}, {\"cidr\": \"10.0.84.1/32\"}, {\"cidr\": \"10.0.85.1/32\"}, {\"cidr\": \"10.0.86.1/32\"}, {\"cidr\": \"10.0.87.1/32\"}, {\"cidr\": \"10.0.88.1/32\"}, {\"cidr\": \"10.0.89.1/32\"}, {\"cidr\": \"10.0.90.1/32\"}, {\"cidr\": \"10.0.91.1/32\"}, {\"cidr\": \"10.0.92.1/32\"}, {\"cidr\": \"10.0.93.1/32\"}, {\"cidr\": \"10.0.94.1/32\"}, {\"cidr\": \"10.0.95.1/32\"}, {\"cidr\": \"10.0.96.1/32\"}, {\"cidr\": \"10.0.97.1/32\"}, {\"cidr\": \"10.0.98.1/32\"}, {\"cidr\": \"10.0.99.1/32\"}, {\"cidr\": \"10.0.100.1/32\"}, {\"cidr\": \"10.0.101.1/32\"}, {\"cidr\": \"10.0.102.1/32\"}, {\"cidr\": \"10.0.103.1/32\"}, {\"cidr\": \"10.0.104.1/32\"}, {\"cidr\": \"10.0.105.1/32\"}, {\"cidr\": \"10.0.106.1/32\"}, {\"cidr\": \"10.0.107.1/32\"}, {\"cidr\": \"10.0.108.1/32\"}, {\"cidr\": \"10.0.109.1/32\"}, {\"cidr\": \"10.0.110.1/32\"}, {\"cidr\": \"10.0.111.1/32\"}, {\"cidr\": \"10.0.112.1/32\"}, {\"cidr\": \"10.0.113.1/32\"}, {\"cidr\": \"10.0.114.1/32\"}, {\"cidr\": \"10.0.115.1/32\"}, {\"cidr\": \"10.0.116.1/32\"}, {\"cidr\": \"10.0.117.1/32\"}, {\"cidr\": \"10.0.118.1/32\"}, {\"cidr\": \"10.0.119.1/32\"}, {\"cidr\": \"10.0.120.1/32\"}, {\"cidr\": \"10.0.121.1/32\"}, {\"cidr\": \"10.0.122.1/32\"}, {\"cidr\": \"10.0.123.1/32\"}, {\"cidr\": \"10.0.124.1/32\"}, {\"cidr\": \"10.0.125.1/32\"}, {\"cidr\": \"10.0.126.1/32\"}, {\"cidr\": \"10.0.127.1/32\"}, {\"cidr\": \"10.0.128.1/32\"}, {\"cidr\": \"10.0.129.1/32\"}, {\"cidr\": \"10.0.130.1/32\"}, {\"cidr\": \"10.0.131.1/32\"}, {\"cidr\": \"10.0.132.1/32\"}, {\"cidr\": \"10.0.133.1/32\"}, {\"cidr\": \"10.0.134.1/32\"}, {\"cidr\": \"10.0.135.1/32\"}, {\"cidr\": \"10.0.136.1/32\"}, {\"cidr\": \"10.0.137.1/32\"}, {\"cidr\": \"10.0.138.1/32\"}, {\"cidr\": \"10.0.139.1/32\"}, {\"cidr\": \"10.0.140.1/32\"}, {\"cidr\": \"10.0.141.1/32\"}, {\"cidr\": \"10.0.142.1/32\"}, {\"cidr\": \"10.0.143.1/32\"}, {\"cidr\": \"10.0.144.1/32\"}, {\"cidr\": \"10.0.145.1/32\"}, {\"cidr\": \"10.0.146.1/32\"}, {\"cidr\": \"10.0.147.1/32\"}, {\"cidr\": \"10.0.148.1/32\"}, {\"cidr\": \"10.0.149.1/32\"}, {\"cidr\": \"10.0.150.1/32\"}, {\"cidr\": \"10.0.151.1/32\"}, {\"cidr\": \"10.0.152.1/32\"}, {\"cidr\": \"10.0.153.1/32\"}, {\"cidr\": \"10.0.154.1/32\"}, {\"cidr\": \"10.0.155.1/32\"}, {\"cidr\": \"10.0.156.1/32\"}, {\"cidr\": \"10.0.157.1/32\"}, {\"cidr\": \"10.0.158.1/32\"}, {\"cidr\": \"10.0.159.1/32\"}, {\"cidr\": \"10.0.160.1/32\"}, {\"cidr\": \"10.0.161.1/32\"}, {\"cidr\": \"10.0.162.1/32\"}, {\"cidr\": \"10.0.163.1/32\"}, {\"cidr\": \"10.0.164.1/32\"}, {\"cidr\": \"10.0.165.1/32\"}, {\"cidr\": \"10.0.166.1/32\"}, {\"cidr\": \"10.0.167.1/32\"}, {\"cidr\": \"10.0.168.1/32\"}, {\"cidr\": \"10.0.169.1/32\"}, {\"cidr\": \"10.0.170.1/32\"}, {\"cidr\": \"10.0.171.1/32\"}, {\"cidr\": \"10.0.172.1/32\"}, {\"cidr\": \"10.0.173.1/32\"}, {\"cidr\": \"10.0.174.1/32\"}, {\"cidr\": \"10.0.175.1/32\"}, {\"cidr\": \"10.0.176.1/32\"}, {\"cidr\": \"10.0.177.1/32\"}, {\"cidr\": \"10.0.178.1/32\"}, {\"cidr\": \"10.0.179.1/32\"}, {\"cidr\": \"10.0.180.1/32\"}, {\"cidr\": \"10.0.181.1/32\"}, {\"cidr\": \"10.0.182.1/32\"}, {\"cidr\": \"10.0.183.1/32\"}, {\"cidr\": \"10.0.184.1/32\"}, {\"cidr\": \"10.0.185.1/32\"}, {\"cidr\": \"10.0.186.1/32\"}, {\"cidr\": \"10.0.187.1/32\"}, {\"cidr\": \"10.0.188.1/32\"}, {\"cidr\": \"10.0.189.1/32\"}, {\"cidr\": \"10.0.190.1/32\"}, {\"cidr\": \"10.0.191.1/32\"}, {\"cidr\": \"10.0.192.1/32\"}, {\"cidr\": \"10.0.193.1/32\"}, {\"cidr\": \"10.0.194.1/32\"}, {\"cidr\": \"10.0.195.1/32\"}, {\"cidr\": \"10.0.196.1/32\"}, {\"cidr\": \"10.0.197.1/32\"}, {\"cidr\": \"10.0.198.1/32\"}, {\"cidr\": \"10.0.199.1/32\"}, {\"cidr\": \"10.0.200.1/32\"}, {\"cidr\": \"10.0.201.1/32\"}, {\"cidr\": \"10.0.202.1/32\"}, {\"cidr\": \"10.0.203.1/32\"}, {\"cidr\": \"10.0.204.1/32\"}, {\"cidr\": \"10.0.205.1/32\"}, {\"cidr\": \"10.0.206.1/32\"}, {\"cidr\": \"10.0.207.1/32\"}, {\"cidr\": \"10.0.208.1/32\"}, {\"cidr\": \"10.0.209.1/32\"}, {\"cidr\": \"10.0.210.1/32\"}, {\"cidr\": \"10.0.211.1/32\"}, {\"cidr\": \"10.0.212.1/32\"}, {\"cidr\": \"10.0.213.1/32\"}, {\"cidr\": \"10.0.214.1/32\"}, {\"cidr\": \"10.0.215.1/32\"}, {\"cidr\": \"10.0.216.1/32\"}, {\"cidr\": \"10.0.217.1/32\"}, {\"cidr\": \"10.0.218.1/32\"}, {\"cidr\": \"10.0.219.1/32\"}, {\"cidr\": \"10.0.220.1/32\"}, {\"cidr\": \"10.0.221.1/32\"}, {\"cidr\": \"10.0.222.1/32\"}, {\"cidr\": \"10.0.223.1/32\"}, {\"cidr\": \"10.0.224.1/32\"}, {\"cidr\": \"10.0.225.1/32\"}, {\"cidr\": \"10.0.226.1/32\"}, {\"cidr\": \"10.0.227.1/32\"}, {\"cidr\": \"10.0.228.1/32\"}, {\"cidr\": \"10.0.229.1/32\"}, {\"cidr\": \"10.0.230.1/32\"}, {\"cidr\": \"10.0.231.1/32\"}, {\"cidr\": \"10.0.232.1/32\"}, {\"cidr\": \"10.0.233.1/32\"}, {\"cidr\": \"10.0.234.1/32\"}, {\"cidr\": \"10.0.235.1/32\"}, {\"cidr\": \"10.0.236.1/32\"}, {\"cidr\": \"10.0.237.1/32\"}, {\"cidr\": \"10.0.238.1/32\"}, {\"cidr\": \"10.0.239.1/32\"}, {\"cidr\": \"10.0.240.1/32\"}, {\"cidr\": \"10.0.241.1/32\"}, {\"cidr\": \"10.0.242.1/32\"}, {\"cidr\": \"10.0.243.1/32\"}, {\"cidr\": \"10.0.244.1/32\"}, {\"cidr\": \"10.0.245.1/32\"}, {\"cidr\": \"10.0.246.1/32\"}, {\"cidr\": \"10.0.247.1/32\"}, {\"cidr\": \"10.0.248.1/32\"}, {\"cidr\": \"10.0.249.1/32\"}, {\"cidr\": \"10.0.250.1/32\"}, {\"cidr\": \"10.0.251.1/32\"}, {\"cidr\": \"10.0.252.1/32\"}, {\"cidr\": \"10.0.253.1/32\"}, {\"cidr\": \"10.0.254.1/32\"}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\"}]}"}],"expected":{"allow":["10.0.0.1/32","10.0.1.1/32","10.0.2.1/32","10.0.3.1/32","10.0.4.1/32","10.0.5.1/32","10.0.6.1/32","10.0.7.1/32","10.0.8.1/32","10.0.9.1/32","10.0.10.1/32","10.0.11.1/32","10.0.12.1/32","10.0.13.1/32","10.0.14.1/32","10.0.15.1/32","10.0.16.1/32","10.0.17.1/32","10.0.18.1/32","10.0.19.1/32","10.0.20.1/32","10.0.21.1/32","10.0.22.1/32","10.0.23.1/32","10.0.24.1/32","10.0.25.1/32","10.0.26.1/32","10.0.27.1/32","10.0.28.1/32","10.0.29.1/32","10.0.30.1/32","10.0.31.1/32","10.0.32.1/32","10.0.33.1/32","10.0.34.1/32","10.0.35.1/32","10.0.36.1/32","10.0.37.1/32","10.0.38.1/32","10.0.39.1/32","10.0.40.1/32","10.0.41.1/32","10.0.42.1/32","10.0.43.1/32","10.0.44.1/32","10.0.45.1/32","10.0.46.1/32","10.0.47.1/32","10.0.48.1/32","10.0.49.1/32","10.0.50.1/32","10.0.51.1/32","10.0.52.1/32","10.0.53.1/32","10.0.54.1/32","10.0.55.1/32","10.0.56.1/32","10.0.57.1/32","10.0.58.1/32","10.0.59.1/32","10.0.60.1/32","10.0.61.1/32","10.0.62.1/32","10.0.63.1/32","10.0.64.1/32","10.0.65.1/32","10.0.66.1/32","10.0.67.1/32","10.0.68.1/32","10.0.69.1/32","10.0.70.1/32","10.0.71.1/32","10.0.72.1/32","10.0.73.1/32","10.0.74.1/32","10.0.75.1/32","10.0.76.1/32","10.0.77.1/32","10.0.78.1/32","10.0.79.1/32","10.0.80.1/32","10.0.81.1/32","10.0.82.1/32","10.0.83.1/32","10.0.84.1/32","10.0.85.1/32","10.0.86.1/32","10.0.87.1/32","10.0.88.1/32","10.0.89.1/32","10.0.90.1/32","10.0.91.1/32","10.0.92.1/32","10.0.93.1/32","10.0.94.1/32","10.0.95.1/32","10.0.96.1/32","10.0.97.1/32","10.0.98.1/32","10.0.99.1/32","10.0.100.1/32","10.0.101.1/32","10.0.102.1/32","10.0.103.1/32","10.0.104.1/32","10.0.105.1/32","10.0.106.1/32","10.0.107.1/32","10.0.108.1/32","10.0.109.1/32","10.0.110.1/32","10.0.111.1/32","10.0.112.1/32","10.0.113.1/32","10.0.114.1/32","10.0.115.1/32","10.0.116.1/32","10.0.117.1/32","10.0.118.1/32","10.0.119.1/32","10.0.120.1/32","10.0.121.1/32","10.0.122.1/32","10.0.123.1/32","10.0.124.1/32","10.0.125.1/32","10.0.126.1/32","10.0.127.1/32","10.0.128.1/32","10.0.129.1/32","10.0.130.1/32","10.0.131.1/32","10.0.132.1/32","10.0.133.1/32","10.0.134.1/32","10.0.135.1/32","10.0.136.1/32","10.0.137.1/32","10.0.138.1/32","10.0.139.1/32","10.0.140.1/32","10.0.141.1/32","10.0.142.1/32","10.0.143.1/32","10.0.144.1/32","10.0.145.1/32","10.0.146.1/32","10.0.147.1/32","10.0.148.1/32","10.0.149.1/32","10.0.150.1/32","10.0.151.1/32","10.0.152.1/32","10.0.153.1/32","10.0.154.1/32","10.0.155.1/32","10.0.156.1/32","10.0.157.1/32","10.0.158.1/32","10.0.159.1/32","10.0.160.1/32","10.0.161.1/32","10.0.162.1/32","10.0.163.1/32","10.0.164.1/32","10.0.165.1/32","10.0.166.1/32","10.0.167.1/32","10.0.168.1/32","10.0.169.1/32","10.0.170.1/32","10.0.171.1/32","10.0.172.1/32","10.0.173.1/32","10.0.174.1/32","10.0.175.1/32","10.0.176.1/32","10.0.177.1/32","10.0.178.1/32","10.0.179.1/32","10.0.180.1/32","10.0.181.1/32","10.0.182.1/32","10.0.183.1/32","10.0.184.1/32","10.0.185.1/32","10.0.186.1/32","10.0.187.1/32","10.0.188.1/32","10.0.189.1/32","10.0.190.1/32","10.0.191.1/32","10.0.192.1/32","10.0.193.1/32","10.0.194.1/32","10.0.195.1/32","10.0.196.1/32","10.0.197.1/32","10.0.198.1/32","10.0.199.1/32","10.0.200.1/32","10.0.201.1/32","10.0.202.1/32","10.0.203.1/32","10.0.204.1/32","10.0.205.1/32","10.0.206.1/32","10.0.207.1/32","10.0.208.1/32","10.0.209.1/32","10.0.210.1/32","10.0.211.1/32","10.0.212.1/32","10.0.213.1/32","10.0.214.1/32","10.0.215.1/32","10.0.216.1/32","10.0.217.1/32","10.0.218.1/32","10.0.219.1/32","10.0.220.1/32","10.0.221.1/32","10.0.222.1/32","10.0.223.1/32","10.0.224.1/32","10.0.225.1/32","10.0.226.1/32","10.0.227.1/32","10.0.228.1/32","10.0.229.1/32","10.0.230.1/32","10.0.231.1/32","10.0.232.1/32","10.0.233.1/32","10.0.234.1/32","10.0.235.1/32","10.0.236.1/32","10.0.237.1/32","10.0.238.1/32","10.0.239.1/32","10.0.240.1/32","10.0.241.1/32","10.0.242.1/32","10.0.243.1/32","10.0.244.1/32","10.0.245.1/32","10.0.246.1/32","10.0.247.1/32","10.0.248.1/32","10.0.249.1/32","10.0.250.1/32","10.0.251.1/32","10.0.252.1/32","10.0.253.1/32","10.0.254.1/32","192.0.2.0/24:tcp"],"deny":[]}}, + {"name":"vector: test_protocol_wide_rules_share_the_native_rule_budget #2","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.1/32\"}, {\"cidr\": \"10.0.1.1/32\"}, {\"cidr\": \"10.0.2.1/32\"}, {\"cidr\": \"10.0.3.1/32\"}, {\"cidr\": \"10.0.4.1/32\"}, {\"cidr\": \"10.0.5.1/32\"}, {\"cidr\": \"10.0.6.1/32\"}, {\"cidr\": \"10.0.7.1/32\"}, {\"cidr\": \"10.0.8.1/32\"}, {\"cidr\": \"10.0.9.1/32\"}, {\"cidr\": \"10.0.10.1/32\"}, {\"cidr\": \"10.0.11.1/32\"}, {\"cidr\": \"10.0.12.1/32\"}, {\"cidr\": \"10.0.13.1/32\"}, {\"cidr\": \"10.0.14.1/32\"}, {\"cidr\": \"10.0.15.1/32\"}, {\"cidr\": \"10.0.16.1/32\"}, {\"cidr\": \"10.0.17.1/32\"}, {\"cidr\": \"10.0.18.1/32\"}, {\"cidr\": \"10.0.19.1/32\"}, {\"cidr\": \"10.0.20.1/32\"}, {\"cidr\": \"10.0.21.1/32\"}, {\"cidr\": \"10.0.22.1/32\"}, {\"cidr\": \"10.0.23.1/32\"}, {\"cidr\": \"10.0.24.1/32\"}, {\"cidr\": \"10.0.25.1/32\"}, {\"cidr\": \"10.0.26.1/32\"}, {\"cidr\": \"10.0.27.1/32\"}, {\"cidr\": \"10.0.28.1/32\"}, {\"cidr\": \"10.0.29.1/32\"}, {\"cidr\": \"10.0.30.1/32\"}, {\"cidr\": \"10.0.31.1/32\"}, {\"cidr\": \"10.0.32.1/32\"}, {\"cidr\": \"10.0.33.1/32\"}, {\"cidr\": \"10.0.34.1/32\"}, {\"cidr\": \"10.0.35.1/32\"}, {\"cidr\": \"10.0.36.1/32\"}, {\"cidr\": \"10.0.37.1/32\"}, {\"cidr\": \"10.0.38.1/32\"}, {\"cidr\": \"10.0.39.1/32\"}, {\"cidr\": \"10.0.40.1/32\"}, {\"cidr\": \"10.0.41.1/32\"}, {\"cidr\": \"10.0.42.1/32\"}, {\"cidr\": \"10.0.43.1/32\"}, {\"cidr\": \"10.0.44.1/32\"}, {\"cidr\": \"10.0.45.1/32\"}, {\"cidr\": \"10.0.46.1/32\"}, {\"cidr\": \"10.0.47.1/32\"}, {\"cidr\": \"10.0.48.1/32\"}, {\"cidr\": \"10.0.49.1/32\"}, {\"cidr\": \"10.0.50.1/32\"}, {\"cidr\": \"10.0.51.1/32\"}, {\"cidr\": \"10.0.52.1/32\"}, {\"cidr\": \"10.0.53.1/32\"}, {\"cidr\": \"10.0.54.1/32\"}, {\"cidr\": \"10.0.55.1/32\"}, {\"cidr\": \"10.0.56.1/32\"}, {\"cidr\": \"10.0.57.1/32\"}, {\"cidr\": \"10.0.58.1/32\"}, {\"cidr\": \"10.0.59.1/32\"}, {\"cidr\": \"10.0.60.1/32\"}, {\"cidr\": \"10.0.61.1/32\"}, {\"cidr\": \"10.0.62.1/32\"}, {\"cidr\": \"10.0.63.1/32\"}, {\"cidr\": \"10.0.64.1/32\"}, {\"cidr\": \"10.0.65.1/32\"}, {\"cidr\": \"10.0.66.1/32\"}, {\"cidr\": \"10.0.67.1/32\"}, {\"cidr\": \"10.0.68.1/32\"}, {\"cidr\": \"10.0.69.1/32\"}, {\"cidr\": \"10.0.70.1/32\"}, {\"cidr\": \"10.0.71.1/32\"}, {\"cidr\": \"10.0.72.1/32\"}, {\"cidr\": \"10.0.73.1/32\"}, {\"cidr\": \"10.0.74.1/32\"}, {\"cidr\": \"10.0.75.1/32\"}, {\"cidr\": \"10.0.76.1/32\"}, {\"cidr\": \"10.0.77.1/32\"}, {\"cidr\": \"10.0.78.1/32\"}, {\"cidr\": \"10.0.79.1/32\"}, {\"cidr\": \"10.0.80.1/32\"}, {\"cidr\": \"10.0.81.1/32\"}, {\"cidr\": \"10.0.82.1/32\"}, {\"cidr\": \"10.0.83.1/32\"}, {\"cidr\": \"10.0.84.1/32\"}, {\"cidr\": \"10.0.85.1/32\"}, {\"cidr\": \"10.0.86.1/32\"}, {\"cidr\": \"10.0.87.1/32\"}, {\"cidr\": \"10.0.88.1/32\"}, {\"cidr\": \"10.0.89.1/32\"}, {\"cidr\": \"10.0.90.1/32\"}, {\"cidr\": \"10.0.91.1/32\"}, {\"cidr\": \"10.0.92.1/32\"}, {\"cidr\": \"10.0.93.1/32\"}, {\"cidr\": \"10.0.94.1/32\"}, {\"cidr\": \"10.0.95.1/32\"}, {\"cidr\": \"10.0.96.1/32\"}, {\"cidr\": \"10.0.97.1/32\"}, {\"cidr\": \"10.0.98.1/32\"}, {\"cidr\": \"10.0.99.1/32\"}, {\"cidr\": \"10.0.100.1/32\"}, {\"cidr\": \"10.0.101.1/32\"}, {\"cidr\": \"10.0.102.1/32\"}, {\"cidr\": \"10.0.103.1/32\"}, {\"cidr\": \"10.0.104.1/32\"}, {\"cidr\": \"10.0.105.1/32\"}, {\"cidr\": \"10.0.106.1/32\"}, {\"cidr\": \"10.0.107.1/32\"}, {\"cidr\": \"10.0.108.1/32\"}, {\"cidr\": \"10.0.109.1/32\"}, {\"cidr\": \"10.0.110.1/32\"}, {\"cidr\": \"10.0.111.1/32\"}, {\"cidr\": \"10.0.112.1/32\"}, {\"cidr\": \"10.0.113.1/32\"}, {\"cidr\": \"10.0.114.1/32\"}, {\"cidr\": \"10.0.115.1/32\"}, {\"cidr\": \"10.0.116.1/32\"}, {\"cidr\": \"10.0.117.1/32\"}, {\"cidr\": \"10.0.118.1/32\"}, {\"cidr\": \"10.0.119.1/32\"}, {\"cidr\": \"10.0.120.1/32\"}, {\"cidr\": \"10.0.121.1/32\"}, {\"cidr\": \"10.0.122.1/32\"}, {\"cidr\": \"10.0.123.1/32\"}, {\"cidr\": \"10.0.124.1/32\"}, {\"cidr\": \"10.0.125.1/32\"}, {\"cidr\": \"10.0.126.1/32\"}, {\"cidr\": \"10.0.127.1/32\"}, {\"cidr\": \"10.0.128.1/32\"}, {\"cidr\": \"10.0.129.1/32\"}, {\"cidr\": \"10.0.130.1/32\"}, {\"cidr\": \"10.0.131.1/32\"}, {\"cidr\": \"10.0.132.1/32\"}, {\"cidr\": \"10.0.133.1/32\"}, {\"cidr\": \"10.0.134.1/32\"}, {\"cidr\": \"10.0.135.1/32\"}, {\"cidr\": \"10.0.136.1/32\"}, {\"cidr\": \"10.0.137.1/32\"}, {\"cidr\": \"10.0.138.1/32\"}, {\"cidr\": \"10.0.139.1/32\"}, {\"cidr\": \"10.0.140.1/32\"}, {\"cidr\": \"10.0.141.1/32\"}, {\"cidr\": \"10.0.142.1/32\"}, {\"cidr\": \"10.0.143.1/32\"}, {\"cidr\": \"10.0.144.1/32\"}, {\"cidr\": \"10.0.145.1/32\"}, {\"cidr\": \"10.0.146.1/32\"}, {\"cidr\": \"10.0.147.1/32\"}, {\"cidr\": \"10.0.148.1/32\"}, {\"cidr\": \"10.0.149.1/32\"}, {\"cidr\": \"10.0.150.1/32\"}, {\"cidr\": \"10.0.151.1/32\"}, {\"cidr\": \"10.0.152.1/32\"}, {\"cidr\": \"10.0.153.1/32\"}, {\"cidr\": \"10.0.154.1/32\"}, {\"cidr\": \"10.0.155.1/32\"}, {\"cidr\": \"10.0.156.1/32\"}, {\"cidr\": \"10.0.157.1/32\"}, {\"cidr\": \"10.0.158.1/32\"}, {\"cidr\": \"10.0.159.1/32\"}, {\"cidr\": \"10.0.160.1/32\"}, {\"cidr\": \"10.0.161.1/32\"}, {\"cidr\": \"10.0.162.1/32\"}, {\"cidr\": \"10.0.163.1/32\"}, {\"cidr\": \"10.0.164.1/32\"}, {\"cidr\": \"10.0.165.1/32\"}, {\"cidr\": \"10.0.166.1/32\"}, {\"cidr\": \"10.0.167.1/32\"}, {\"cidr\": \"10.0.168.1/32\"}, {\"cidr\": \"10.0.169.1/32\"}, {\"cidr\": \"10.0.170.1/32\"}, {\"cidr\": \"10.0.171.1/32\"}, {\"cidr\": \"10.0.172.1/32\"}, {\"cidr\": \"10.0.173.1/32\"}, {\"cidr\": \"10.0.174.1/32\"}, {\"cidr\": \"10.0.175.1/32\"}, {\"cidr\": \"10.0.176.1/32\"}, {\"cidr\": \"10.0.177.1/32\"}, {\"cidr\": \"10.0.178.1/32\"}, {\"cidr\": \"10.0.179.1/32\"}, {\"cidr\": \"10.0.180.1/32\"}, {\"cidr\": \"10.0.181.1/32\"}, {\"cidr\": \"10.0.182.1/32\"}, {\"cidr\": \"10.0.183.1/32\"}, {\"cidr\": \"10.0.184.1/32\"}, {\"cidr\": \"10.0.185.1/32\"}, {\"cidr\": \"10.0.186.1/32\"}, {\"cidr\": \"10.0.187.1/32\"}, {\"cidr\": \"10.0.188.1/32\"}, {\"cidr\": \"10.0.189.1/32\"}, {\"cidr\": \"10.0.190.1/32\"}, {\"cidr\": \"10.0.191.1/32\"}, {\"cidr\": \"10.0.192.1/32\"}, {\"cidr\": \"10.0.193.1/32\"}, {\"cidr\": \"10.0.194.1/32\"}, {\"cidr\": \"10.0.195.1/32\"}, {\"cidr\": \"10.0.196.1/32\"}, {\"cidr\": \"10.0.197.1/32\"}, {\"cidr\": \"10.0.198.1/32\"}, {\"cidr\": \"10.0.199.1/32\"}, {\"cidr\": \"10.0.200.1/32\"}, {\"cidr\": \"10.0.201.1/32\"}, {\"cidr\": \"10.0.202.1/32\"}, {\"cidr\": \"10.0.203.1/32\"}, {\"cidr\": \"10.0.204.1/32\"}, {\"cidr\": \"10.0.205.1/32\"}, {\"cidr\": \"10.0.206.1/32\"}, {\"cidr\": \"10.0.207.1/32\"}, {\"cidr\": \"10.0.208.1/32\"}, {\"cidr\": \"10.0.209.1/32\"}, {\"cidr\": \"10.0.210.1/32\"}, {\"cidr\": \"10.0.211.1/32\"}, {\"cidr\": \"10.0.212.1/32\"}, {\"cidr\": \"10.0.213.1/32\"}, {\"cidr\": \"10.0.214.1/32\"}, {\"cidr\": \"10.0.215.1/32\"}, {\"cidr\": \"10.0.216.1/32\"}, {\"cidr\": \"10.0.217.1/32\"}, {\"cidr\": \"10.0.218.1/32\"}, {\"cidr\": \"10.0.219.1/32\"}, {\"cidr\": \"10.0.220.1/32\"}, {\"cidr\": \"10.0.221.1/32\"}, {\"cidr\": \"10.0.222.1/32\"}, {\"cidr\": \"10.0.223.1/32\"}, {\"cidr\": \"10.0.224.1/32\"}, {\"cidr\": \"10.0.225.1/32\"}, {\"cidr\": \"10.0.226.1/32\"}, {\"cidr\": \"10.0.227.1/32\"}, {\"cidr\": \"10.0.228.1/32\"}, {\"cidr\": \"10.0.229.1/32\"}, {\"cidr\": \"10.0.230.1/32\"}, {\"cidr\": \"10.0.231.1/32\"}, {\"cidr\": \"10.0.232.1/32\"}, {\"cidr\": \"10.0.233.1/32\"}, {\"cidr\": \"10.0.234.1/32\"}, {\"cidr\": \"10.0.235.1/32\"}, {\"cidr\": \"10.0.236.1/32\"}, {\"cidr\": \"10.0.237.1/32\"}, {\"cidr\": \"10.0.238.1/32\"}, {\"cidr\": \"10.0.239.1/32\"}, {\"cidr\": \"10.0.240.1/32\"}, {\"cidr\": \"10.0.241.1/32\"}, {\"cidr\": \"10.0.242.1/32\"}, {\"cidr\": \"10.0.243.1/32\"}, {\"cidr\": \"10.0.244.1/32\"}, {\"cidr\": \"10.0.245.1/32\"}, {\"cidr\": \"10.0.246.1/32\"}, {\"cidr\": \"10.0.247.1/32\"}, {\"cidr\": \"10.0.248.1/32\"}, {\"cidr\": \"10.0.249.1/32\"}, {\"cidr\": \"10.0.250.1/32\"}, {\"cidr\": \"10.0.251.1/32\"}, {\"cidr\": \"10.0.252.1/32\"}, {\"cidr\": \"10.0.253.1/32\"}, {\"cidr\": \"10.0.254.1/32\"}, {\"cidr\": \"10.0.255.1/32\"}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\"}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"vector: test_protocol_wide_rules_share_the_native_rule_budget #3","input":[{"text":"{\"deny\": [{\"cidr\": \"10.0.0.1/32\"}, {\"cidr\": \"10.0.1.1/32\"}, {\"cidr\": \"10.0.2.1/32\"}, {\"cidr\": \"10.0.3.1/32\"}, {\"cidr\": \"10.0.4.1/32\"}, {\"cidr\": \"10.0.5.1/32\"}, {\"cidr\": \"10.0.6.1/32\"}, {\"cidr\": \"10.0.7.1/32\"}, {\"cidr\": \"10.0.8.1/32\"}, {\"cidr\": \"10.0.9.1/32\"}, {\"cidr\": \"10.0.10.1/32\"}, {\"cidr\": \"10.0.11.1/32\"}, {\"cidr\": \"10.0.12.1/32\"}, {\"cidr\": \"10.0.13.1/32\"}, {\"cidr\": \"10.0.14.1/32\"}, {\"cidr\": \"10.0.15.1/32\"}, {\"cidr\": \"10.0.16.1/32\"}, {\"cidr\": \"10.0.17.1/32\"}, {\"cidr\": \"10.0.18.1/32\"}, {\"cidr\": \"10.0.19.1/32\"}, {\"cidr\": \"10.0.20.1/32\"}, {\"cidr\": \"10.0.21.1/32\"}, {\"cidr\": \"10.0.22.1/32\"}, {\"cidr\": \"10.0.23.1/32\"}, {\"cidr\": \"10.0.24.1/32\"}, {\"cidr\": \"10.0.25.1/32\"}, {\"cidr\": \"10.0.26.1/32\"}, {\"cidr\": \"10.0.27.1/32\"}, {\"cidr\": \"10.0.28.1/32\"}, {\"cidr\": \"10.0.29.1/32\"}, {\"cidr\": \"10.0.30.1/32\"}, {\"cidr\": \"10.0.31.1/32\"}, {\"cidr\": \"10.0.32.1/32\"}, {\"cidr\": \"10.0.33.1/32\"}, {\"cidr\": \"10.0.34.1/32\"}, {\"cidr\": \"10.0.35.1/32\"}, {\"cidr\": \"10.0.36.1/32\"}, {\"cidr\": \"10.0.37.1/32\"}, {\"cidr\": \"10.0.38.1/32\"}, {\"cidr\": \"10.0.39.1/32\"}, {\"cidr\": \"10.0.40.1/32\"}, {\"cidr\": \"10.0.41.1/32\"}, {\"cidr\": \"10.0.42.1/32\"}, {\"cidr\": \"10.0.43.1/32\"}, {\"cidr\": \"10.0.44.1/32\"}, {\"cidr\": \"10.0.45.1/32\"}, {\"cidr\": \"10.0.46.1/32\"}, {\"cidr\": \"10.0.47.1/32\"}, {\"cidr\": \"10.0.48.1/32\"}, {\"cidr\": \"10.0.49.1/32\"}, {\"cidr\": \"10.0.50.1/32\"}, {\"cidr\": \"10.0.51.1/32\"}, {\"cidr\": \"10.0.52.1/32\"}, {\"cidr\": \"10.0.53.1/32\"}, {\"cidr\": \"10.0.54.1/32\"}, {\"cidr\": \"10.0.55.1/32\"}, {\"cidr\": \"10.0.56.1/32\"}, {\"cidr\": \"10.0.57.1/32\"}, {\"cidr\": \"10.0.58.1/32\"}, {\"cidr\": \"10.0.59.1/32\"}, {\"cidr\": \"10.0.60.1/32\"}, {\"cidr\": \"10.0.61.1/32\"}, {\"cidr\": \"10.0.62.1/32\"}, {\"cidr\": \"10.0.63.1/32\"}, {\"cidr\": \"10.0.64.1/32\"}, {\"cidr\": \"10.0.65.1/32\"}, {\"cidr\": \"10.0.66.1/32\"}, {\"cidr\": \"10.0.67.1/32\"}, {\"cidr\": \"10.0.68.1/32\"}, {\"cidr\": \"10.0.69.1/32\"}, {\"cidr\": \"10.0.70.1/32\"}, {\"cidr\": \"10.0.71.1/32\"}, {\"cidr\": \"10.0.72.1/32\"}, {\"cidr\": \"10.0.73.1/32\"}, {\"cidr\": \"10.0.74.1/32\"}, {\"cidr\": \"10.0.75.1/32\"}, {\"cidr\": \"10.0.76.1/32\"}, {\"cidr\": \"10.0.77.1/32\"}, {\"cidr\": \"10.0.78.1/32\"}, {\"cidr\": \"10.0.79.1/32\"}, {\"cidr\": \"10.0.80.1/32\"}, {\"cidr\": \"10.0.81.1/32\"}, {\"cidr\": \"10.0.82.1/32\"}, {\"cidr\": \"10.0.83.1/32\"}, {\"cidr\": \"10.0.84.1/32\"}, {\"cidr\": \"10.0.85.1/32\"}, {\"cidr\": \"10.0.86.1/32\"}, {\"cidr\": \"10.0.87.1/32\"}, {\"cidr\": \"10.0.88.1/32\"}, {\"cidr\": \"10.0.89.1/32\"}, {\"cidr\": \"10.0.90.1/32\"}, {\"cidr\": \"10.0.91.1/32\"}, {\"cidr\": \"10.0.92.1/32\"}, {\"cidr\": \"10.0.93.1/32\"}, {\"cidr\": \"10.0.94.1/32\"}, {\"cidr\": \"10.0.95.1/32\"}, {\"cidr\": \"10.0.96.1/32\"}, {\"cidr\": \"10.0.97.1/32\"}, {\"cidr\": \"10.0.98.1/32\"}, {\"cidr\": \"10.0.99.1/32\"}, {\"cidr\": \"10.0.100.1/32\"}, {\"cidr\": \"10.0.101.1/32\"}, {\"cidr\": \"10.0.102.1/32\"}, {\"cidr\": \"10.0.103.1/32\"}, {\"cidr\": \"10.0.104.1/32\"}, {\"cidr\": \"10.0.105.1/32\"}, {\"cidr\": \"10.0.106.1/32\"}, {\"cidr\": \"10.0.107.1/32\"}, {\"cidr\": \"10.0.108.1/32\"}, {\"cidr\": \"10.0.109.1/32\"}, {\"cidr\": \"10.0.110.1/32\"}, {\"cidr\": \"10.0.111.1/32\"}, {\"cidr\": \"10.0.112.1/32\"}, {\"cidr\": \"10.0.113.1/32\"}, {\"cidr\": \"10.0.114.1/32\"}, {\"cidr\": \"10.0.115.1/32\"}, {\"cidr\": \"10.0.116.1/32\"}, {\"cidr\": \"10.0.117.1/32\"}, {\"cidr\": \"10.0.118.1/32\"}, {\"cidr\": \"10.0.119.1/32\"}, {\"cidr\": \"10.0.120.1/32\"}, {\"cidr\": \"10.0.121.1/32\"}, {\"cidr\": \"10.0.122.1/32\"}, {\"cidr\": \"10.0.123.1/32\"}, {\"cidr\": \"10.0.124.1/32\"}, {\"cidr\": \"10.0.125.1/32\"}, {\"cidr\": \"10.0.126.1/32\"}, {\"cidr\": \"10.0.127.1/32\"}, {\"cidr\": \"10.0.128.1/32\"}, {\"cidr\": \"10.0.129.1/32\"}, {\"cidr\": \"10.0.130.1/32\"}, {\"cidr\": \"10.0.131.1/32\"}, {\"cidr\": \"10.0.132.1/32\"}, {\"cidr\": \"10.0.133.1/32\"}, {\"cidr\": \"10.0.134.1/32\"}, {\"cidr\": \"10.0.135.1/32\"}, {\"cidr\": \"10.0.136.1/32\"}, {\"cidr\": \"10.0.137.1/32\"}, {\"cidr\": \"10.0.138.1/32\"}, {\"cidr\": \"10.0.139.1/32\"}, {\"cidr\": \"10.0.140.1/32\"}, {\"cidr\": \"10.0.141.1/32\"}, {\"cidr\": \"10.0.142.1/32\"}, {\"cidr\": \"10.0.143.1/32\"}, {\"cidr\": \"10.0.144.1/32\"}, {\"cidr\": \"10.0.145.1/32\"}, {\"cidr\": \"10.0.146.1/32\"}, {\"cidr\": \"10.0.147.1/32\"}, {\"cidr\": \"10.0.148.1/32\"}, {\"cidr\": \"10.0.149.1/32\"}, {\"cidr\": \"10.0.150.1/32\"}, {\"cidr\": \"10.0.151.1/32\"}, {\"cidr\": \"10.0.152.1/32\"}, {\"cidr\": \"10.0.153.1/32\"}, {\"cidr\": \"10.0.154.1/32\"}, {\"cidr\": \"10.0.155.1/32\"}, {\"cidr\": \"10.0.156.1/32\"}, {\"cidr\": \"10.0.157.1/32\"}, {\"cidr\": \"10.0.158.1/32\"}, {\"cidr\": \"10.0.159.1/32\"}, {\"cidr\": \"10.0.160.1/32\"}, {\"cidr\": \"10.0.161.1/32\"}, {\"cidr\": \"10.0.162.1/32\"}, {\"cidr\": \"10.0.163.1/32\"}, {\"cidr\": \"10.0.164.1/32\"}, {\"cidr\": \"10.0.165.1/32\"}, {\"cidr\": \"10.0.166.1/32\"}, {\"cidr\": \"10.0.167.1/32\"}, {\"cidr\": \"10.0.168.1/32\"}, {\"cidr\": \"10.0.169.1/32\"}, {\"cidr\": \"10.0.170.1/32\"}, {\"cidr\": \"10.0.171.1/32\"}, {\"cidr\": \"10.0.172.1/32\"}, {\"cidr\": \"10.0.173.1/32\"}, {\"cidr\": \"10.0.174.1/32\"}, {\"cidr\": \"10.0.175.1/32\"}, {\"cidr\": \"10.0.176.1/32\"}, {\"cidr\": \"10.0.177.1/32\"}, {\"cidr\": \"10.0.178.1/32\"}, {\"cidr\": \"10.0.179.1/32\"}, {\"cidr\": \"10.0.180.1/32\"}, {\"cidr\": \"10.0.181.1/32\"}, {\"cidr\": \"10.0.182.1/32\"}, {\"cidr\": \"10.0.183.1/32\"}, {\"cidr\": \"10.0.184.1/32\"}, {\"cidr\": \"10.0.185.1/32\"}, {\"cidr\": \"10.0.186.1/32\"}, {\"cidr\": \"10.0.187.1/32\"}, {\"cidr\": \"10.0.188.1/32\"}, {\"cidr\": \"10.0.189.1/32\"}, {\"cidr\": \"10.0.190.1/32\"}, {\"cidr\": \"10.0.191.1/32\"}, {\"cidr\": \"10.0.192.1/32\"}, {\"cidr\": \"10.0.193.1/32\"}, {\"cidr\": \"10.0.194.1/32\"}, {\"cidr\": \"10.0.195.1/32\"}, {\"cidr\": \"10.0.196.1/32\"}, {\"cidr\": \"10.0.197.1/32\"}, {\"cidr\": \"10.0.198.1/32\"}, {\"cidr\": \"10.0.199.1/32\"}, {\"cidr\": \"10.0.200.1/32\"}, {\"cidr\": \"10.0.201.1/32\"}, {\"cidr\": \"10.0.202.1/32\"}, {\"cidr\": \"10.0.203.1/32\"}, {\"cidr\": \"10.0.204.1/32\"}, {\"cidr\": \"10.0.205.1/32\"}, {\"cidr\": \"10.0.206.1/32\"}, {\"cidr\": \"10.0.207.1/32\"}, {\"cidr\": \"10.0.208.1/32\"}, {\"cidr\": \"10.0.209.1/32\"}, {\"cidr\": \"10.0.210.1/32\"}, {\"cidr\": \"10.0.211.1/32\"}, {\"cidr\": \"10.0.212.1/32\"}, {\"cidr\": \"10.0.213.1/32\"}, {\"cidr\": \"10.0.214.1/32\"}, {\"cidr\": \"10.0.215.1/32\"}, {\"cidr\": \"10.0.216.1/32\"}, {\"cidr\": \"10.0.217.1/32\"}, {\"cidr\": \"10.0.218.1/32\"}, {\"cidr\": \"10.0.219.1/32\"}, {\"cidr\": \"10.0.220.1/32\"}, {\"cidr\": \"10.0.221.1/32\"}, {\"cidr\": \"10.0.222.1/32\"}, {\"cidr\": \"10.0.223.1/32\"}, {\"cidr\": \"10.0.224.1/32\"}, {\"cidr\": \"10.0.225.1/32\"}, {\"cidr\": \"10.0.226.1/32\"}, {\"cidr\": \"10.0.227.1/32\"}, {\"cidr\": \"10.0.228.1/32\"}, {\"cidr\": \"10.0.229.1/32\"}, {\"cidr\": \"10.0.230.1/32\"}, {\"cidr\": \"10.0.231.1/32\"}, {\"cidr\": \"10.0.232.1/32\"}, {\"cidr\": \"10.0.233.1/32\"}, {\"cidr\": \"10.0.234.1/32\"}, {\"cidr\": \"10.0.235.1/32\"}, {\"cidr\": \"10.0.236.1/32\"}, {\"cidr\": \"10.0.237.1/32\"}, {\"cidr\": \"10.0.238.1/32\"}, {\"cidr\": \"10.0.239.1/32\"}, {\"cidr\": \"10.0.240.1/32\"}, {\"cidr\": \"10.0.241.1/32\"}, {\"cidr\": \"10.0.242.1/32\"}, {\"cidr\": \"10.0.243.1/32\"}, {\"cidr\": \"10.0.244.1/32\"}, {\"cidr\": \"10.0.245.1/32\"}, {\"cidr\": \"10.0.246.1/32\"}, {\"cidr\": \"10.0.247.1/32\"}, {\"cidr\": \"10.0.248.1/32\"}, {\"cidr\": \"10.0.249.1/32\"}, {\"cidr\": \"10.0.250.1/32\"}, {\"cidr\": \"10.0.251.1/32\"}, {\"cidr\": \"10.0.252.1/32\"}, {\"cidr\": \"10.0.253.1/32\"}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 1, \"endPort\": 129}]}"}],"expected":{"allow":[],"deny":["10.0.0.1/32","10.0.1.1/32","10.0.2.1/32","10.0.3.1/32","10.0.4.1/32","10.0.5.1/32","10.0.6.1/32","10.0.7.1/32","10.0.8.1/32","10.0.9.1/32","10.0.10.1/32","10.0.11.1/32","10.0.12.1/32","10.0.13.1/32","10.0.14.1/32","10.0.15.1/32","10.0.16.1/32","10.0.17.1/32","10.0.18.1/32","10.0.19.1/32","10.0.20.1/32","10.0.21.1/32","10.0.22.1/32","10.0.23.1/32","10.0.24.1/32","10.0.25.1/32","10.0.26.1/32","10.0.27.1/32","10.0.28.1/32","10.0.29.1/32","10.0.30.1/32","10.0.31.1/32","10.0.32.1/32","10.0.33.1/32","10.0.34.1/32","10.0.35.1/32","10.0.36.1/32","10.0.37.1/32","10.0.38.1/32","10.0.39.1/32","10.0.40.1/32","10.0.41.1/32","10.0.42.1/32","10.0.43.1/32","10.0.44.1/32","10.0.45.1/32","10.0.46.1/32","10.0.47.1/32","10.0.48.1/32","10.0.49.1/32","10.0.50.1/32","10.0.51.1/32","10.0.52.1/32","10.0.53.1/32","10.0.54.1/32","10.0.55.1/32","10.0.56.1/32","10.0.57.1/32","10.0.58.1/32","10.0.59.1/32","10.0.60.1/32","10.0.61.1/32","10.0.62.1/32","10.0.63.1/32","10.0.64.1/32","10.0.65.1/32","10.0.66.1/32","10.0.67.1/32","10.0.68.1/32","10.0.69.1/32","10.0.70.1/32","10.0.71.1/32","10.0.72.1/32","10.0.73.1/32","10.0.74.1/32","10.0.75.1/32","10.0.76.1/32","10.0.77.1/32","10.0.78.1/32","10.0.79.1/32","10.0.80.1/32","10.0.81.1/32","10.0.82.1/32","10.0.83.1/32","10.0.84.1/32","10.0.85.1/32","10.0.86.1/32","10.0.87.1/32","10.0.88.1/32","10.0.89.1/32","10.0.90.1/32","10.0.91.1/32","10.0.92.1/32","10.0.93.1/32","10.0.94.1/32","10.0.95.1/32","10.0.96.1/32","10.0.97.1/32","10.0.98.1/32","10.0.99.1/32","10.0.100.1/32","10.0.101.1/32","10.0.102.1/32","10.0.103.1/32","10.0.104.1/32","10.0.105.1/32","10.0.106.1/32","10.0.107.1/32","10.0.108.1/32","10.0.109.1/32","10.0.110.1/32","10.0.111.1/32","10.0.112.1/32","10.0.113.1/32","10.0.114.1/32","10.0.115.1/32","10.0.116.1/32","10.0.117.1/32","10.0.118.1/32","10.0.119.1/32","10.0.120.1/32","10.0.121.1/32","10.0.122.1/32","10.0.123.1/32","10.0.124.1/32","10.0.125.1/32","10.0.126.1/32","10.0.127.1/32","10.0.128.1/32","10.0.129.1/32","10.0.130.1/32","10.0.131.1/32","10.0.132.1/32","10.0.133.1/32","10.0.134.1/32","10.0.135.1/32","10.0.136.1/32","10.0.137.1/32","10.0.138.1/32","10.0.139.1/32","10.0.140.1/32","10.0.141.1/32","10.0.142.1/32","10.0.143.1/32","10.0.144.1/32","10.0.145.1/32","10.0.146.1/32","10.0.147.1/32","10.0.148.1/32","10.0.149.1/32","10.0.150.1/32","10.0.151.1/32","10.0.152.1/32","10.0.153.1/32","10.0.154.1/32","10.0.155.1/32","10.0.156.1/32","10.0.157.1/32","10.0.158.1/32","10.0.159.1/32","10.0.160.1/32","10.0.161.1/32","10.0.162.1/32","10.0.163.1/32","10.0.164.1/32","10.0.165.1/32","10.0.166.1/32","10.0.167.1/32","10.0.168.1/32","10.0.169.1/32","10.0.170.1/32","10.0.171.1/32","10.0.172.1/32","10.0.173.1/32","10.0.174.1/32","10.0.175.1/32","10.0.176.1/32","10.0.177.1/32","10.0.178.1/32","10.0.179.1/32","10.0.180.1/32","10.0.181.1/32","10.0.182.1/32","10.0.183.1/32","10.0.184.1/32","10.0.185.1/32","10.0.186.1/32","10.0.187.1/32","10.0.188.1/32","10.0.189.1/32","10.0.190.1/32","10.0.191.1/32","10.0.192.1/32","10.0.193.1/32","10.0.194.1/32","10.0.195.1/32","10.0.196.1/32","10.0.197.1/32","10.0.198.1/32","10.0.199.1/32","10.0.200.1/32","10.0.201.1/32","10.0.202.1/32","10.0.203.1/32","10.0.204.1/32","10.0.205.1/32","10.0.206.1/32","10.0.207.1/32","10.0.208.1/32","10.0.209.1/32","10.0.210.1/32","10.0.211.1/32","10.0.212.1/32","10.0.213.1/32","10.0.214.1/32","10.0.215.1/32","10.0.216.1/32","10.0.217.1/32","10.0.218.1/32","10.0.219.1/32","10.0.220.1/32","10.0.221.1/32","10.0.222.1/32","10.0.223.1/32","10.0.224.1/32","10.0.225.1/32","10.0.226.1/32","10.0.227.1/32","10.0.228.1/32","10.0.229.1/32","10.0.230.1/32","10.0.231.1/32","10.0.232.1/32","10.0.233.1/32","10.0.234.1/32","10.0.235.1/32","10.0.236.1/32","10.0.237.1/32","10.0.238.1/32","10.0.239.1/32","10.0.240.1/32","10.0.241.1/32","10.0.242.1/32","10.0.243.1/32","10.0.244.1/32","10.0.245.1/32","10.0.246.1/32","10.0.247.1/32","10.0.248.1/32","10.0.249.1/32","10.0.250.1/32","10.0.251.1/32","10.0.252.1/32","10.0.253.1/32","192.0.2.1/32:tcp:1-129","192.0.2.1/32:udp:1-129"]}}, + {"name":"vector: test_protocol_wide_rules_share_the_native_rule_budget #4","input":[{"text":"{\"deny\": [{\"cidr\": \"10.0.0.1/32\"}, {\"cidr\": \"10.0.1.1/32\"}, {\"cidr\": \"10.0.2.1/32\"}, {\"cidr\": \"10.0.3.1/32\"}, {\"cidr\": \"10.0.4.1/32\"}, {\"cidr\": \"10.0.5.1/32\"}, {\"cidr\": \"10.0.6.1/32\"}, {\"cidr\": \"10.0.7.1/32\"}, {\"cidr\": \"10.0.8.1/32\"}, {\"cidr\": \"10.0.9.1/32\"}, {\"cidr\": \"10.0.10.1/32\"}, {\"cidr\": \"10.0.11.1/32\"}, {\"cidr\": \"10.0.12.1/32\"}, {\"cidr\": \"10.0.13.1/32\"}, {\"cidr\": \"10.0.14.1/32\"}, {\"cidr\": \"10.0.15.1/32\"}, {\"cidr\": \"10.0.16.1/32\"}, {\"cidr\": \"10.0.17.1/32\"}, {\"cidr\": \"10.0.18.1/32\"}, {\"cidr\": \"10.0.19.1/32\"}, {\"cidr\": \"10.0.20.1/32\"}, {\"cidr\": \"10.0.21.1/32\"}, {\"cidr\": \"10.0.22.1/32\"}, {\"cidr\": \"10.0.23.1/32\"}, {\"cidr\": \"10.0.24.1/32\"}, {\"cidr\": \"10.0.25.1/32\"}, {\"cidr\": \"10.0.26.1/32\"}, {\"cidr\": \"10.0.27.1/32\"}, {\"cidr\": \"10.0.28.1/32\"}, {\"cidr\": \"10.0.29.1/32\"}, {\"cidr\": \"10.0.30.1/32\"}, {\"cidr\": \"10.0.31.1/32\"}, {\"cidr\": \"10.0.32.1/32\"}, {\"cidr\": \"10.0.33.1/32\"}, {\"cidr\": \"10.0.34.1/32\"}, {\"cidr\": \"10.0.35.1/32\"}, {\"cidr\": \"10.0.36.1/32\"}, {\"cidr\": \"10.0.37.1/32\"}, {\"cidr\": \"10.0.38.1/32\"}, {\"cidr\": \"10.0.39.1/32\"}, {\"cidr\": \"10.0.40.1/32\"}, {\"cidr\": \"10.0.41.1/32\"}, {\"cidr\": \"10.0.42.1/32\"}, {\"cidr\": \"10.0.43.1/32\"}, {\"cidr\": \"10.0.44.1/32\"}, {\"cidr\": \"10.0.45.1/32\"}, {\"cidr\": \"10.0.46.1/32\"}, {\"cidr\": \"10.0.47.1/32\"}, {\"cidr\": \"10.0.48.1/32\"}, {\"cidr\": \"10.0.49.1/32\"}, {\"cidr\": \"10.0.50.1/32\"}, {\"cidr\": \"10.0.51.1/32\"}, {\"cidr\": \"10.0.52.1/32\"}, {\"cidr\": \"10.0.53.1/32\"}, {\"cidr\": \"10.0.54.1/32\"}, {\"cidr\": \"10.0.55.1/32\"}, {\"cidr\": \"10.0.56.1/32\"}, {\"cidr\": \"10.0.57.1/32\"}, {\"cidr\": \"10.0.58.1/32\"}, {\"cidr\": \"10.0.59.1/32\"}, {\"cidr\": \"10.0.60.1/32\"}, {\"cidr\": \"10.0.61.1/32\"}, {\"cidr\": \"10.0.62.1/32\"}, {\"cidr\": \"10.0.63.1/32\"}, {\"cidr\": \"10.0.64.1/32\"}, {\"cidr\": \"10.0.65.1/32\"}, {\"cidr\": \"10.0.66.1/32\"}, {\"cidr\": \"10.0.67.1/32\"}, {\"cidr\": \"10.0.68.1/32\"}, {\"cidr\": \"10.0.69.1/32\"}, {\"cidr\": \"10.0.70.1/32\"}, {\"cidr\": \"10.0.71.1/32\"}, {\"cidr\": \"10.0.72.1/32\"}, {\"cidr\": \"10.0.73.1/32\"}, {\"cidr\": \"10.0.74.1/32\"}, {\"cidr\": \"10.0.75.1/32\"}, {\"cidr\": \"10.0.76.1/32\"}, {\"cidr\": \"10.0.77.1/32\"}, {\"cidr\": \"10.0.78.1/32\"}, {\"cidr\": \"10.0.79.1/32\"}, {\"cidr\": \"10.0.80.1/32\"}, {\"cidr\": \"10.0.81.1/32\"}, {\"cidr\": \"10.0.82.1/32\"}, {\"cidr\": \"10.0.83.1/32\"}, {\"cidr\": \"10.0.84.1/32\"}, {\"cidr\": \"10.0.85.1/32\"}, {\"cidr\": \"10.0.86.1/32\"}, {\"cidr\": \"10.0.87.1/32\"}, {\"cidr\": \"10.0.88.1/32\"}, {\"cidr\": \"10.0.89.1/32\"}, {\"cidr\": \"10.0.90.1/32\"}, {\"cidr\": \"10.0.91.1/32\"}, {\"cidr\": \"10.0.92.1/32\"}, {\"cidr\": \"10.0.93.1/32\"}, {\"cidr\": \"10.0.94.1/32\"}, {\"cidr\": \"10.0.95.1/32\"}, {\"cidr\": \"10.0.96.1/32\"}, {\"cidr\": \"10.0.97.1/32\"}, {\"cidr\": \"10.0.98.1/32\"}, {\"cidr\": \"10.0.99.1/32\"}, {\"cidr\": \"10.0.100.1/32\"}, {\"cidr\": \"10.0.101.1/32\"}, {\"cidr\": \"10.0.102.1/32\"}, {\"cidr\": \"10.0.103.1/32\"}, {\"cidr\": \"10.0.104.1/32\"}, {\"cidr\": \"10.0.105.1/32\"}, {\"cidr\": \"10.0.106.1/32\"}, {\"cidr\": \"10.0.107.1/32\"}, {\"cidr\": \"10.0.108.1/32\"}, {\"cidr\": \"10.0.109.1/32\"}, {\"cidr\": \"10.0.110.1/32\"}, {\"cidr\": \"10.0.111.1/32\"}, {\"cidr\": \"10.0.112.1/32\"}, {\"cidr\": \"10.0.113.1/32\"}, {\"cidr\": \"10.0.114.1/32\"}, {\"cidr\": \"10.0.115.1/32\"}, {\"cidr\": \"10.0.116.1/32\"}, {\"cidr\": \"10.0.117.1/32\"}, {\"cidr\": \"10.0.118.1/32\"}, {\"cidr\": \"10.0.119.1/32\"}, {\"cidr\": \"10.0.120.1/32\"}, {\"cidr\": \"10.0.121.1/32\"}, {\"cidr\": \"10.0.122.1/32\"}, {\"cidr\": \"10.0.123.1/32\"}, {\"cidr\": \"10.0.124.1/32\"}, {\"cidr\": \"10.0.125.1/32\"}, {\"cidr\": \"10.0.126.1/32\"}, {\"cidr\": \"10.0.127.1/32\"}, {\"cidr\": \"10.0.128.1/32\"}, {\"cidr\": \"10.0.129.1/32\"}, {\"cidr\": \"10.0.130.1/32\"}, {\"cidr\": \"10.0.131.1/32\"}, {\"cidr\": \"10.0.132.1/32\"}, {\"cidr\": \"10.0.133.1/32\"}, {\"cidr\": \"10.0.134.1/32\"}, {\"cidr\": \"10.0.135.1/32\"}, {\"cidr\": \"10.0.136.1/32\"}, {\"cidr\": \"10.0.137.1/32\"}, {\"cidr\": \"10.0.138.1/32\"}, {\"cidr\": \"10.0.139.1/32\"}, {\"cidr\": \"10.0.140.1/32\"}, {\"cidr\": \"10.0.141.1/32\"}, {\"cidr\": \"10.0.142.1/32\"}, {\"cidr\": \"10.0.143.1/32\"}, {\"cidr\": \"10.0.144.1/32\"}, {\"cidr\": \"10.0.145.1/32\"}, {\"cidr\": \"10.0.146.1/32\"}, {\"cidr\": \"10.0.147.1/32\"}, {\"cidr\": \"10.0.148.1/32\"}, {\"cidr\": \"10.0.149.1/32\"}, {\"cidr\": \"10.0.150.1/32\"}, {\"cidr\": \"10.0.151.1/32\"}, {\"cidr\": \"10.0.152.1/32\"}, {\"cidr\": \"10.0.153.1/32\"}, {\"cidr\": \"10.0.154.1/32\"}, {\"cidr\": \"10.0.155.1/32\"}, {\"cidr\": \"10.0.156.1/32\"}, {\"cidr\": \"10.0.157.1/32\"}, {\"cidr\": \"10.0.158.1/32\"}, {\"cidr\": \"10.0.159.1/32\"}, {\"cidr\": \"10.0.160.1/32\"}, {\"cidr\": \"10.0.161.1/32\"}, {\"cidr\": \"10.0.162.1/32\"}, {\"cidr\": \"10.0.163.1/32\"}, {\"cidr\": \"10.0.164.1/32\"}, {\"cidr\": \"10.0.165.1/32\"}, {\"cidr\": \"10.0.166.1/32\"}, {\"cidr\": \"10.0.167.1/32\"}, {\"cidr\": \"10.0.168.1/32\"}, {\"cidr\": \"10.0.169.1/32\"}, {\"cidr\": \"10.0.170.1/32\"}, {\"cidr\": \"10.0.171.1/32\"}, {\"cidr\": \"10.0.172.1/32\"}, {\"cidr\": \"10.0.173.1/32\"}, {\"cidr\": \"10.0.174.1/32\"}, {\"cidr\": \"10.0.175.1/32\"}, {\"cidr\": \"10.0.176.1/32\"}, {\"cidr\": \"10.0.177.1/32\"}, {\"cidr\": \"10.0.178.1/32\"}, {\"cidr\": \"10.0.179.1/32\"}, {\"cidr\": \"10.0.180.1/32\"}, {\"cidr\": \"10.0.181.1/32\"}, {\"cidr\": \"10.0.182.1/32\"}, {\"cidr\": \"10.0.183.1/32\"}, {\"cidr\": \"10.0.184.1/32\"}, {\"cidr\": \"10.0.185.1/32\"}, {\"cidr\": \"10.0.186.1/32\"}, {\"cidr\": \"10.0.187.1/32\"}, {\"cidr\": \"10.0.188.1/32\"}, {\"cidr\": \"10.0.189.1/32\"}, {\"cidr\": \"10.0.190.1/32\"}, {\"cidr\": \"10.0.191.1/32\"}, {\"cidr\": \"10.0.192.1/32\"}, {\"cidr\": \"10.0.193.1/32\"}, {\"cidr\": \"10.0.194.1/32\"}, {\"cidr\": \"10.0.195.1/32\"}, {\"cidr\": \"10.0.196.1/32\"}, {\"cidr\": \"10.0.197.1/32\"}, {\"cidr\": \"10.0.198.1/32\"}, {\"cidr\": \"10.0.199.1/32\"}, {\"cidr\": \"10.0.200.1/32\"}, {\"cidr\": \"10.0.201.1/32\"}, {\"cidr\": \"10.0.202.1/32\"}, {\"cidr\": \"10.0.203.1/32\"}, {\"cidr\": \"10.0.204.1/32\"}, {\"cidr\": \"10.0.205.1/32\"}, {\"cidr\": \"10.0.206.1/32\"}, {\"cidr\": \"10.0.207.1/32\"}, {\"cidr\": \"10.0.208.1/32\"}, {\"cidr\": \"10.0.209.1/32\"}, {\"cidr\": \"10.0.210.1/32\"}, {\"cidr\": \"10.0.211.1/32\"}, {\"cidr\": \"10.0.212.1/32\"}, {\"cidr\": \"10.0.213.1/32\"}, {\"cidr\": \"10.0.214.1/32\"}, {\"cidr\": \"10.0.215.1/32\"}, {\"cidr\": \"10.0.216.1/32\"}, {\"cidr\": \"10.0.217.1/32\"}, {\"cidr\": \"10.0.218.1/32\"}, {\"cidr\": \"10.0.219.1/32\"}, {\"cidr\": \"10.0.220.1/32\"}, {\"cidr\": \"10.0.221.1/32\"}, {\"cidr\": \"10.0.222.1/32\"}, {\"cidr\": \"10.0.223.1/32\"}, {\"cidr\": \"10.0.224.1/32\"}, {\"cidr\": \"10.0.225.1/32\"}, {\"cidr\": \"10.0.226.1/32\"}, {\"cidr\": \"10.0.227.1/32\"}, {\"cidr\": \"10.0.228.1/32\"}, {\"cidr\": \"10.0.229.1/32\"}, {\"cidr\": \"10.0.230.1/32\"}, {\"cidr\": \"10.0.231.1/32\"}, {\"cidr\": \"10.0.232.1/32\"}, {\"cidr\": \"10.0.233.1/32\"}, {\"cidr\": \"10.0.234.1/32\"}, {\"cidr\": \"10.0.235.1/32\"}, {\"cidr\": \"10.0.236.1/32\"}, {\"cidr\": \"10.0.237.1/32\"}, {\"cidr\": \"10.0.238.1/32\"}, {\"cidr\": \"10.0.239.1/32\"}, {\"cidr\": \"10.0.240.1/32\"}, {\"cidr\": \"10.0.241.1/32\"}, {\"cidr\": \"10.0.242.1/32\"}, {\"cidr\": \"10.0.243.1/32\"}, {\"cidr\": \"10.0.244.1/32\"}, {\"cidr\": \"10.0.245.1/32\"}, {\"cidr\": \"10.0.246.1/32\"}, {\"cidr\": \"10.0.247.1/32\"}, {\"cidr\": \"10.0.248.1/32\"}, {\"cidr\": \"10.0.249.1/32\"}, {\"cidr\": \"10.0.250.1/32\"}, {\"cidr\": \"10.0.251.1/32\"}, {\"cidr\": \"10.0.252.1/32\"}, {\"cidr\": \"10.0.253.1/32\"}, {\"cidr\": \"10.0.254.1/32\"}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 1, \"endPort\": 129}]}"}],"expected":{"error":"deny emits at most 256 native rules"}}, + {"name":"vector: test_rejects_257_rules_before_materializing_them #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 1}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 3}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 5}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 7}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 9}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 11}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 13}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 15}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 17}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 19}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 21}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 23}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 25}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 27}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 29}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 31}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 33}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 35}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 37}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 39}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 41}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 43}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 45}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 47}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 49}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 51}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 53}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 55}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 57}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 59}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 61}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 63}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 65}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 67}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 69}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 71}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 73}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 75}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 77}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 79}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 81}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 83}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 85}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 87}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 89}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 91}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 93}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 95}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 97}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 99}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 101}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 103}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 105}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 107}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 109}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 111}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 113}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 115}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 117}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 119}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 121}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 123}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 125}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 127}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 129}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 131}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 133}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 135}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 137}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 139}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 141}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 143}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 145}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 147}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 149}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 151}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 153}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 155}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 157}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 159}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 161}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 163}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 165}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 167}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 169}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 171}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 173}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 175}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 177}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 179}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 181}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 183}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 185}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 187}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 189}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 191}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 193}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 195}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 197}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 199}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 201}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 203}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 205}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 207}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 209}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 211}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 213}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 215}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 217}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 219}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 221}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 223}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 225}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 227}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 229}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 231}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 233}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 235}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 237}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 239}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 241}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 243}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 245}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 247}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 249}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 251}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 253}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 255}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 257}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 259}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 261}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 263}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 265}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 267}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 269}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 271}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 273}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 275}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 277}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 279}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 281}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 283}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 285}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 287}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 289}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 291}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 293}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 295}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 297}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 299}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 301}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 303}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 305}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 307}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 309}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 311}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 313}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 315}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 317}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 319}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 321}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 323}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 325}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 327}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 329}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 331}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 333}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 335}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 337}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 339}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 341}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 343}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 345}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 347}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 349}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 351}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 353}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 355}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 357}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 359}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 361}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 363}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 365}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 367}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 369}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 371}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 373}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 375}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 377}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 379}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 381}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 383}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 385}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 387}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 389}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 391}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 393}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 395}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 397}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 399}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 401}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 403}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 405}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 407}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 409}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 411}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 413}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 415}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 417}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 419}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 421}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 423}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 425}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 427}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 429}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 431}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 433}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 435}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 437}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 439}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 441}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 443}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 445}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 447}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 449}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 451}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 453}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 455}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 457}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 459}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 461}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 463}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 465}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 467}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 469}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 471}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 473}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 475}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 477}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 479}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 481}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 483}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 485}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 487}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 489}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 491}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 493}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 495}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 497}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 499}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 501}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 503}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 505}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 507}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 509}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 511}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 513}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"vector: test_rejects_257_rules_before_materializing_them #2","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 1}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 3}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 5}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 7}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 9}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 11}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 13}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 15}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 17}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 19}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 21}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 23}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 25}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 27}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 29}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 31}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 33}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 35}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 37}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 39}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 41}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 43}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 45}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 47}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 49}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 51}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 53}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 55}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 57}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 59}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 61}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 63}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 65}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 67}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 69}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 71}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 73}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 75}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 77}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 79}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 81}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 83}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 85}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 87}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 89}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 91}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 93}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 95}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 97}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 99}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 101}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 103}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 105}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 107}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 109}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 111}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 113}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 115}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 117}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 119}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 121}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 123}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 125}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 127}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 129}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 131}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 133}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 135}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 137}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 139}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 141}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 143}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 145}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 147}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 149}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 151}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 153}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 155}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 157}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 159}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 161}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 163}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 165}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 167}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 169}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 171}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 173}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 175}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 177}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 179}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 181}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 183}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 185}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 187}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 189}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 191}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 193}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 195}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 197}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 199}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 201}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 203}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 205}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 207}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 209}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 211}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 213}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 215}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 217}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 219}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 221}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 223}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 225}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 227}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 229}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 231}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 233}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 235}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 237}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 239}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 241}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 243}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 245}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 247}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 249}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 251}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 253}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 255}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 257}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 259}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 261}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 263}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 265}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 267}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 269}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 271}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 273}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 275}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 277}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 279}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 281}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 283}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 285}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 287}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 289}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 291}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 293}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 295}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 297}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 299}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 301}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 303}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 305}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 307}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 309}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 311}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 313}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 315}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 317}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 319}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 321}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 323}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 325}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 327}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 329}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 331}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 333}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 335}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 337}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 339}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 341}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 343}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 345}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 347}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 349}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 351}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 353}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 355}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 357}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 359}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 361}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 363}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 365}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 367}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 369}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 371}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 373}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 375}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 377}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 379}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 381}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 383}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 385}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 387}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 389}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 391}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 393}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 395}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 397}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 399}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 401}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 403}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 405}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 407}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 409}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 411}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 413}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 415}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 417}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 419}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 421}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 423}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 425}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 427}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 429}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 431}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 433}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 435}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 437}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 439}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 441}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 443}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 445}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 447}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 449}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 451}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 453}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 455}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 457}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 459}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 461}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 463}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 465}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 467}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 469}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 471}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 473}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 475}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 477}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 479}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 481}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 483}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 485}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 487}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 489}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 491}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 493}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 495}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 497}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 499}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 501}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 503}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 505}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 507}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 509}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 511}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"tcp\", \"port\": 513}]}"}],"expected":{"error":"deny emits at most 256 native rules"}}, + {"name":"vector: test_rejects_duplicate_json_fields #1","input":[{"text":"{\"deny\":[{\"cidr\":\"0.0.0.0/0\"}],\"deny\":[]}"}],"expected":{"error":"duplicate JSON property: deny"}}, + {"name":"vector: test_rejects_duplicate_json_fields #2","input":[{"text":"{\"allow\":[{\"cidr\":\"192.0.2.0/24\",\"port\":80,\"port\":443,\"protocol\":\"tcp\"}]}"}],"expected":{"error":"duplicate JSON property: port"}}, + {"name":"vector: test_rejects_exclusions_outside_parent_or_wrong_family #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"198.51.100.0/24\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"vector: test_rejects_exclusions_outside_parent_or_wrong_family #2","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"2001:db8::/32\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"vector: test_rejects_exclusions_outside_parent_or_wrong_family #3","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::/32\", \"except\": [\"2001:db9::/48\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must be contained in 2001:db8::/32"}}, + {"name":"vector: test_rejects_exclusions_outside_parent_or_wrong_family #4","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::/32\", \"except\": [\"192.0.2.0/24\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must be contained in 2001:db8::/32"}}, + {"name":"vector: test_rejects_exclusions_outside_parent_or_wrong_family #5","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"::/96\", \"except\": [\"0.0.0.0/8\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must be contained in ::/96"}}, + {"name":"vector: test_rejects_invalid_mxc_rule_shapes #1","input":[{"text":"{\"allow\": [{\"to\": []}]}"}],"expected":{"error":"allow[0].to must contain at least one destination"}}, + {"name":"vector: test_rejects_invalid_mxc_rule_shapes #2","input":[{"text":"{\"allow\": [{\"to\": [{}]}]}"}],"expected":{"error":"allow[0].to[0].cidr is required"}}, + {"name":"vector: test_rejects_invalid_mxc_rule_shapes #3","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"unknown\": true}]}]}"}],"expected":{"error":"allow[0].to[0] has unknown field 'unknown'"}}, + {"name":"vector: test_rejects_invalid_mxc_rule_shapes #4","input":[{"text":"{\"allow\": [{\"ports\": []}]}"}],"expected":{"error":"allow[0].ports must contain at least one selector"}}, + {"name":"vector: test_rejects_invalid_mxc_rule_shapes #5","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"icmp\", \"port\": 8}]}]}"}],"expected":{"error":"allow[0].ports[0].port is not supported with icmp"}}, + {"name":"vector: test_rejects_invalid_mxc_rule_shapes #6","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"ICMP\"}]}]}"}],"expected":{"error":"allow[0].ports[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"vector: test_rejects_invalid_mxc_rule_shapes #7","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"sctp\"}]}]}"}],"expected":{"error":"allow[0].ports[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"vector: test_rejects_invalid_mxc_rule_shapes #8","input":[{"text":"{\"allow\": [{\"ports\": [{\"endPort\": 80}]}]}"}],"expected":{"error":"allow[0].ports[0].endPort requires port"}}, + {"name":"vector: test_rejects_invalid_mxc_rule_shapes #9","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}], \"cidr\": \"192.0.2.0/24\"}]}"}],"expected":{"error":"allow[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"endPort\": 80}]}"}],"expected":{"error":"allow[0].endPort requires port"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #2","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"any\", \"endPort\": 80}]}"}],"expected":{"error":"allow[0].endPort requires port"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #3","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 81, \"endPort\": 80}]}"}],"expected":{"error":"allow[0].endPort 80 cannot be below port 81"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #4","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 0}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #5","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"udp\", \"port\": 65536}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #6","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": true}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #7","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"any\", \"port\": 0}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #8","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"port\": 80}]}"}],"expected":{"error":"allow[0].port requires protocol"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #9","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\", \"port\": 8}]}"}],"expected":{"error":"allow[0].port is not supported with icmp"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #10","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\", \"endPort\": 8}]}"}],"expected":{"error":"allow[0].endPort requires port"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #11","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"ICMP\"}]}"}],"expected":{"error":"allow[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"vector: test_rejects_invalid_port_shapes_and_values #12","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"sctp\"}]}"}],"expected":{"error":"allow[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"vector: test_rejects_ipv6_scopes #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"fe80::%eth0/64\"}]}]}"}],"expected":{"error":"allow[0].to[0].cidr must not name an IPv6 scope: fe80::%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #2","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::%eth0/64\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must not name an IPv6 scope: fe80::%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #3","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::%eth0/64\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #4","input":[{"text":"{\"allow\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::%eth0/64\"]}]}"}],"expected":{"error":"allow[0].except[0] must not name an IPv6 scope: fe80::%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #5","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"fe80::1%1\"}]}]}"}],"expected":{"error":"allow[0].to[0].cidr must not name an IPv6 scope: fe80::1%1"}}, + {"name":"vector: test_rejects_ipv6_scopes #6","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%1\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must not name an IPv6 scope: fe80::1%1"}}, + {"name":"vector: test_rejects_ipv6_scopes #7","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%1\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::1%1"}}, + {"name":"vector: test_rejects_ipv6_scopes #8","input":[{"text":"{\"allow\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%1\"]}]}"}],"expected":{"error":"allow[0].except[0] must not name an IPv6 scope: fe80::1%1"}}, + {"name":"vector: test_rejects_ipv6_scopes #9","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"fe80::1%eth0/64\"}]}]}"}],"expected":{"error":"allow[0].to[0].cidr must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #10","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%eth0/64\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #11","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%eth0/64\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #12","input":[{"text":"{\"allow\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%eth0/64\"]}]}"}],"expected":{"error":"allow[0].except[0] must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #13","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"fe80::1%1/64\"}]}]}"}],"expected":{"error":"allow[0].to[0].cidr must not name an IPv6 scope: fe80::1%1/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #14","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%1/64\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must not name an IPv6 scope: fe80::1%1/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #15","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%1/64\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::1%1/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #16","input":[{"text":"{\"allow\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%1/64\"]}]}"}],"expected":{"error":"allow[0].except[0] must not name an IPv6 scope: fe80::1%1/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #17","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"::ffff:192.0.2.1%eth0/120\"}]}]}"}],"expected":{"error":"allow[0].to[0].cidr must not name an IPv6 scope: ::ffff:192.0.2.1%eth0/120"}}, + {"name":"vector: test_rejects_ipv6_scopes #18","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"::ffff:192.0.2.1%eth0/120\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must not name an IPv6 scope: ::ffff:192.0.2.1%eth0/120"}}, + {"name":"vector: test_rejects_ipv6_scopes #19","input":[{"text":"{\"allow\": [{\"cidr\": \"::ffff:192.0.2.1%eth0/120\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: ::ffff:192.0.2.1%eth0/120"}}, + {"name":"vector: test_rejects_ipv6_scopes #20","input":[{"text":"{\"allow\": [{\"cidr\": \"::/0\", \"except\": [\"::ffff:192.0.2.1%eth0/120\"]}]}"}],"expected":{"error":"allow[0].except[0] must not name an IPv6 scope: ::ffff:192.0.2.1%eth0/120"}}, + {"name":"vector: test_rejects_ipv6_scopes #21","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"fe80::%eth0/64\"}]}]}"}],"expected":{"error":"deny[0].to[0].cidr must not name an IPv6 scope: fe80::%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #22","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::%eth0/64\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must not name an IPv6 scope: fe80::%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #23","input":[{"text":"{\"deny\": [{\"cidr\": \"fe80::%eth0/64\"}]}"}],"expected":{"error":"deny[0].cidr must not name an IPv6 scope: fe80::%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #24","input":[{"text":"{\"deny\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::%eth0/64\"]}]}"}],"expected":{"error":"deny[0].except[0] must not name an IPv6 scope: fe80::%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #25","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"fe80::1%1\"}]}]}"}],"expected":{"error":"deny[0].to[0].cidr must not name an IPv6 scope: fe80::1%1"}}, + {"name":"vector: test_rejects_ipv6_scopes #26","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%1\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must not name an IPv6 scope: fe80::1%1"}}, + {"name":"vector: test_rejects_ipv6_scopes #27","input":[{"text":"{\"deny\": [{\"cidr\": \"fe80::1%1\"}]}"}],"expected":{"error":"deny[0].cidr must not name an IPv6 scope: fe80::1%1"}}, + {"name":"vector: test_rejects_ipv6_scopes #28","input":[{"text":"{\"deny\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%1\"]}]}"}],"expected":{"error":"deny[0].except[0] must not name an IPv6 scope: fe80::1%1"}}, + {"name":"vector: test_rejects_ipv6_scopes #29","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"fe80::1%eth0/64\"}]}]}"}],"expected":{"error":"deny[0].to[0].cidr must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #30","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%eth0/64\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #31","input":[{"text":"{\"deny\": [{\"cidr\": \"fe80::1%eth0/64\"}]}"}],"expected":{"error":"deny[0].cidr must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #32","input":[{"text":"{\"deny\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%eth0/64\"]}]}"}],"expected":{"error":"deny[0].except[0] must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #33","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"fe80::1%1/64\"}]}]}"}],"expected":{"error":"deny[0].to[0].cidr must not name an IPv6 scope: fe80::1%1/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #34","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%1/64\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must not name an IPv6 scope: fe80::1%1/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #35","input":[{"text":"{\"deny\": [{\"cidr\": \"fe80::1%1/64\"}]}"}],"expected":{"error":"deny[0].cidr must not name an IPv6 scope: fe80::1%1/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #36","input":[{"text":"{\"deny\": [{\"cidr\": \"::/0\", \"except\": [\"fe80::1%1/64\"]}]}"}],"expected":{"error":"deny[0].except[0] must not name an IPv6 scope: fe80::1%1/64"}}, + {"name":"vector: test_rejects_ipv6_scopes #37","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"::ffff:192.0.2.1%eth0/120\"}]}]}"}],"expected":{"error":"deny[0].to[0].cidr must not name an IPv6 scope: ::ffff:192.0.2.1%eth0/120"}}, + {"name":"vector: test_rejects_ipv6_scopes #38","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"::/0\", \"except\": [\"::ffff:192.0.2.1%eth0/120\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must not name an IPv6 scope: ::ffff:192.0.2.1%eth0/120"}}, + {"name":"vector: test_rejects_ipv6_scopes #39","input":[{"text":"{\"deny\": [{\"cidr\": \"::ffff:192.0.2.1%eth0/120\"}]}"}],"expected":{"error":"deny[0].cidr must not name an IPv6 scope: ::ffff:192.0.2.1%eth0/120"}}, + {"name":"vector: test_rejects_ipv6_scopes #40","input":[{"text":"{\"deny\": [{\"cidr\": \"::/0\", \"except\": [\"::ffff:192.0.2.1%eth0/120\"]}]}"}],"expected":{"error":"deny[0].except[0] must not name an IPv6 scope: ::ffff:192.0.2.1%eth0/120"}}, + {"name":"vector: test_rejects_unknown_and_malformed_fields #1","input":[{"text":"[]"}],"expected":{"error":"egress policy must be an object"}}, + {"name":"vector: test_rejects_unknown_and_malformed_fields #2","input":[{"text":"{\"unknown\": []}"}],"expected":{"error":"egress policy has unknown field 'unknown'"}}, + {"name":"vector: test_rejects_unknown_and_malformed_fields #3","input":[{"text":"{\"allow\": {}}"}],"expected":{"error":"allow must be an array"}}, + {"name":"vector: test_rejects_unknown_and_malformed_fields #4","input":[{"text":"{\"allow\": [\"192.0.2.0/24\"]}"}],"expected":{"error":"allow[0] must be an object"}}, + {"name":"vector: test_rejects_unknown_and_malformed_fields #5","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"unknown\": 1}]}"}],"expected":{"error":"allow[0] has unknown field 'unknown'"}}, + {"name":"vector: test_rejects_unknown_and_malformed_fields #6","input":[{"text":"{\"allow\": [{\"cidr\": 7}]}"}],"expected":{"error":"allow[0].cidr must be an IPv4 or IPv6 CIDR string"}}, + {"name":"vector: test_rejects_unknown_and_malformed_fields #7","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": \"192.0.2.1\"}]}"}],"expected":{"error":"allow[0].except must be an array"}}, + {"name":"vector: test_rejects_unknown_and_malformed_fields #8","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": null}]}"}],"expected":{"error":"allow[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"vector: test_reports_invalid_port_ranges_clearly #1","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 8010, \"endPort\": 8000}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort 8000 cannot be below port 8010"}}, + {"name":"vector: test_reports_invalid_port_ranges_clearly #2","input":[{"text":"{\"deny\": [{\"ports\": [{\"protocol\": \"udp\", \"endPort\": 8010}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort requires port"}}, + {"name":"vector: test_reports_invalid_port_ranges_clearly #3","input":[{"text":"{\"deny\": [{\"ports\": [{\"endPort\": 8010}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort requires port"}}, + {"name":"vector: test_reports_invalid_port_ranges_clearly #4","input":[{"text":"{\"deny\": [{\"ports\": [{\"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 65536}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be between 1 and 65535"}}, + {"name":"vector: test_reports_invalid_port_ranges_clearly #5","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 8010, \"endPort\": 8000}]}"}],"expected":{"error":"deny[0].endPort 8000 cannot be below port 8010"}}, + {"name":"vector: test_reports_invalid_port_ranges_clearly #6","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"udp\", \"endPort\": 8010}]}"}],"expected":{"error":"deny[0].endPort requires port"}}, + {"name":"vector: test_reports_invalid_port_ranges_clearly #7","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"endPort\": 8010}]}"}],"expected":{"error":"deny[0].endPort requires port"}}, + {"name":"vector: test_reports_invalid_port_ranges_clearly #8","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 8000, \"endPort\": 65536}]}"}],"expected":{"error":"deny[0].endPort must be between 1 and 65535"}}, + {"name":"vector: test_reports_invalid_port_ranges_clearly #9","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 0, \"endPort\": 8010}]}"}],"expected":{"error":"deny[0].port must be between 1 and 65535"}}, + {"name":"vector: test_reports_oversized_json_integer_rejection #1","input":[{"text":"{\"allow\":[{\"cidr\":"},{"text":"9","repeat":10000},{"text":"}]}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"vector: test_subtracts_ipv6_exclusions #1","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::/32\", \"except\": [\"2001:db8:1::/48\", \"2001:db8:1::/64\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 22}]}]}"}],"expected":{"allow":[],"deny":["2001:db8::/48:tcp:22","2001:db8:2::/47:tcp:22","2001:db8:4::/46:tcp:22","2001:db8:8::/45:tcp:22","2001:db8:10::/44:tcp:22","2001:db8:20::/43:tcp:22","2001:db8:40::/42:tcp:22","2001:db8:80::/41:tcp:22","2001:db8:100::/40:tcp:22","2001:db8:200::/39:tcp:22","2001:db8:400::/38:tcp:22","2001:db8:800::/37:tcp:22","2001:db8:1000::/36:tcp:22","2001:db8:2000::/35:tcp:22","2001:db8:4000::/34:tcp:22","2001:db8:8000::/33:tcp:22"]}}, + {"name":"vector: test_subtracts_rule_local_cidr_exclusions #1","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.128/25\"]}]}, {\"to\": [{\"cidr\": \"192.0.2.200/32\"}]}], \"deny\": [{\"to\": [{\"cidr\": \"198.51.100.0/24\", \"except\": [\"198.51.100.128/25\"]}]}]}"}],"expected":{"allow":["192.0.2.0/25","192.0.2.200/32"],"deny":["198.51.100.0/25"]}}, + {"name":"vector: test_validates_redundant_rules_before_canonicalization #1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\"}, {\"cidr\": \"192.0.2.1\", \"unknown\": true}]}"}],"expected":{"error":"allow[1] has unknown field 'unknown'"}}, + {"name":"json: empty file","input":[{"text":""}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: whitespace only","input":[{"text":" \t\r\n"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: null","input":[{"text":"null"}],"expected":{"error":"egress policy must be an object"}}, + {"name":"json: array root","input":[{"text":"[]"}],"expected":{"error":"egress policy must be an object"}}, + {"name":"json: number root","input":[{"text":"1"}],"expected":{"error":"egress policy must be an object"}}, + {"name":"json: string root","input":[{"text":"\"allow\""}],"expected":{"error":"egress policy must be an object"}}, + {"name":"json: empty object","input":[{"text":"{}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"json: surrounding whitespace","input":[{"text":" \t\n\r{\"allow\": []}\r\n "}],"expected":{"allow":[],"deny":[]}}, + {"name":"json: form feed after","input":[{"text":"{\"allow\": []}\f"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: vertical tab before","input":[{"text":"\u000b{\"allow\": []}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: no-break space after","input":[{"text":"{\"allow\": []}\u00a0"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: trailing comma in object","input":[{"text":"{\"allow\": [],}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: trailing comma in array","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\"},]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: comment","input":[{"text":"{\"allow\": []} // comment"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: single quotes","input":[{"text":"{'allow': []}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: unquoted name","input":[{"text":"{allow: []}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: missing colon","input":[{"text":"{\"allow\" []}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: missing comma","input":[{"text":"{\"allow\": [] \"deny\": []}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: unclosed object","input":[{"text":"{\"allow\": []"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: unclosed string","input":[{"text":"{\"allow"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: extra value","input":[{"text":"{\"allow\": []} {}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: two roots","input":[{"text":"{}{}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: constant prefix","input":[{"text":"{\"allow\": nullx}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: raw tab in string","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\t\"}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: escaped tab in string","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\\t\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 192.0.2.0/24\t"}}, + {"name":"json: raw newline in name","input":[{"text":"{\"al\nlow\": []}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: invalid escape","input":[{"text":"{\"allow\": [{\"cidr\": \"\\x41\"}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: short unicode escape","input":[{"text":"{\"allow\": [{\"cidr\": \"\\u41\"}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: solidus escape","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0\\/24\"}]}"}],"expected":{"allow":["192.0.2.0/24"],"deny":[]}}, + {"name":"json: escaped names","input":[{"text":"{\"\\u0061llow\": [{\"\\u0063idr\": \"192.0.2.7\"}]}"}],"expected":{"allow":["192.0.2.7/32"],"deny":[]}}, + {"name":"json: escaped duplicate","input":[{"text":"{\"allow\": [], \"\\u0061llow\": []}"}],"expected":{"error":"duplicate JSON property: allow"}}, + {"name":"json: deep arrays in a rule list","input":[{"text":"{\"allow\": [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}"}],"expected":{"error":"allow[0] must be an object"}}, + {"name":"json: deep arrays","input":[{"text":"[","repeat":500},{"text":"]","repeat":500}],"expected":{"error":"egress policy must be an object"}}, + {"name":"json: deep objects","input":[{"text":"{\"allow\": [{\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": {\"a\": 1}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}]}"}],"expected":{"error":"allow[0] has unknown field 'a'"}}, + {"name":"json: port 01","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 01}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort 01","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 01}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port 1.","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1.}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort 1.","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 1.}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port .5","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": .5}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort .5","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": .5}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port +80","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": +80}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort +80","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": +80}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port 0x50","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 0x50}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort 0x50","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 0x50}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port 8e","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 8e}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort 8e","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 8e}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port 8e+","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 8e+}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort 8e+","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 8e+}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port nan","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": nan}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort nan","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": nan}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port -NaN","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": -NaN}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort -NaN","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": -NaN}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port Inf","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": Inf}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: endPort Inf","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": Inf}]}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: port NaN","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": NaN}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort NaN","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": NaN}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port Infinity","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": Infinity}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort Infinity","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": Infinity}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port -Infinity","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": -Infinity}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort -Infinity","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": -Infinity}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port 1e400","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1e400}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort 1e400","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 1e400}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port -1e400","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": -1e400}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort -1e400","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": -1e400}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port 80.0","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 80.0}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort 80.0","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 80.0}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port 8e1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 8e1}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort 8e1","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 8e1}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port 1E2","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1E2}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort 1E2","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 1E2}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port 1e-400","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1e-400}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort 1e-400","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 1e-400}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port -0","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": -0}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"json: endPort -0","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": -0}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be between 1 and 65535"}}, + {"name":"json: port 0","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 0}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"json: endPort 0","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 0}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be between 1 and 65535"}}, + {"name":"json: port true","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": true}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort true","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": true}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port false","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": false}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort false","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": false}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port null","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": null}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort null","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": null}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port \"80\"","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": \"80\"}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort \"80\"","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": \"80\"}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port [80]","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": [80]}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort [80]","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": [80]}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port {}","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": {}}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"json: endPort {}","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": {}}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be an integer"}}, + {"name":"json: port 65535","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 65535}]}"}],"expected":{"allow":["192.0.2.0/24:tcp:65535"],"deny":[]}}, + {"name":"json: endPort 65535","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 65535}]}]}"}],"expected":{"allow":[],"deny":["0.0.0.0/0:tcp:1-65535","0.0.0.0/0:udp:1-65535","::/0:tcp:1-65535","::/0:udp:1-65535"]}}, + {"name":"json: port 65536","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 65536}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"json: endPort 65536","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 65536}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be between 1 and 65535"}}, + {"name":"json: port -1","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": -1}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"json: endPort -1","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": -1}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be between 1 and 65535"}}, + {"name":"json: port 9999999999999999","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 9999999999999999999999999999999999999999999999999999999999999999}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"json: endPort 9999999999999999","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 9999999999999999999999999999999999999999999999999999999999999999}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be between 1 and 65535"}}, + {"name":"json: port -999999999999999","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": -9999999999999999999999999999999999999999999999999999999999999999}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"json: endPort -999999999999999","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": -9999999999999999999999999999999999999999999999999999999999999999}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be between 1 and 65535"}}, + {"name":"json: port 9999999999999999","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 99999999999999999999999999999999999999999999999999999999999999999}]}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: endPort 9999999999999999","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 99999999999999999999999999999999999999999999999999999999999999999}]}]}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: port -999999999999999","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": -99999999999999999999999999999999999999999999999999999999999999999}]}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: endPort -999999999999999","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": -99999999999999999999999999999999999999999999999999999999999999999}]}]}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: port 1000000000000000","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1"},{"text":"0","repeat":4300},{"text":"}]}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: endPort 1000000000000000","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1, \"endPort\": 1"},{"text":"0","repeat":4300},{"text":"}]}]}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: duplicate root field","input":[{"text":"{\"deny\": [{\"cidr\": \"0.0.0.0/0\"}], \"deny\": []}"}],"expected":{"error":"duplicate JSON property: deny"}}, + {"name":"json: duplicate rule field","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.0/8\", \"cidr\": \"::/0\"}]}"}],"expected":{"error":"duplicate JSON property: cidr"}}, + {"name":"json: duplicate port field","input":[{"text":"{\"allow\": [{\"ports\": [{\"port\": 1, \"port\": 2}]}]}"}],"expected":{"error":"duplicate JSON property: port"}}, + {"name":"json: duplicate peer field","input":[{"text":"{\"allow\": [{\"to\": [{\"except\": [], \"except\": []}]}]}"}],"expected":{"error":"duplicate JSON property: except"}}, + {"name":"json: first duplicate in order","input":[{"text":"{\"b\": 1, \"a\": 2, \"a\": 3, \"b\": 4}"}],"expected":{"error":"duplicate JSON property: a"}}, + {"name":"json: duplicate before long integer","input":[{"text":"{\"a\": 1, \"a\": 2, \"b\": 99999999999999999999999999999999999999999999999999999999999999999}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: inner duplicate first","input":[{"text":"{\"a\": {\"x\": 1, \"x\": 2}, \"b\": 99999999999999999999999999999999999999999999999999999999999999999}"}],"expected":{"error":"duplicate JSON property: x"}}, + {"name":"json: long integer first","input":[{"text":"{\"b\": 99999999999999999999999999999999999999999999999999999999999999999, \"a\": {\"x\": 1, \"x\": 2}}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: duplicate before syntax error","input":[{"text":"{\"a\": 1, \"a\": 2,}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: duplicate before extra data","input":[{"text":"{\"a\": 1, \"a\": 2} x"}],"expected":{"error":"duplicate JSON property: a"}}, + {"name":"json: long integer before syntax error","input":[{"text":"[99999999999999999999999999999999999999999999999999999999999999999x]"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: long integer in unknown field","input":[{"text":"{\"unknown\": [99999999999999999999999999999999999999999999999999999999999999999]}"}],"expected":{"error":"JSON integer exceeds 64-digit limit"}}, + {"name":"json: duplicate in unknown field","input":[{"text":"{\"unknown\": {\"x\": 1, \"x\": 2}}"}],"expected":{"error":"duplicate JSON property: x"}}, + {"name":"json: duplicate surrogate names","input":[{"text":"{\"\\ud800\": 1, \"\\ud800\": 2}"}],"expected":{"error":"duplicate JSON property: \\ud800"}}, + {"name":"json: distinct surrogate names","input":[{"text":"{\"\\ud800\": 1, \"\\udc00\": 2}"}],"expected":{"error":"egress policy has unknown field '\\ud800'"}}, + {"name":"json: surrogate root name","input":[{"text":"{\"\\ud800\": []}"}],"expected":{"error":"egress policy has unknown field '\\ud800'"}}, + {"name":"json: surrogate rule name","input":[{"text":"{\"allow\": [{\"\\udfff\": 1}]}"}],"expected":{"error":"allow[0] has unknown field '\\udfff'"}}, + {"name":"json: surrogate cidr","input":[{"text":"{\"allow\": [{\"cidr\": \"\\ud800\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: \\ud800"}}, + {"name":"json: surrogate scope","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%\\ud800/64\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::1%\\ud800/64"}}, + {"name":"json: surrogate protocol","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"\\udc00\"}]}"}],"expected":{"error":"allow[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"json: paired surrogates","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%\\ud83d\\ude00/64\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::1%\ud83d\ude00/64"}}, + {"name":"json: reversed surrogates","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%\\ude00\\ud83d/64\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::1%\\ude00\\ud83d/64"}}, + {"name":"json: high surrogate before letter","input":[{"text":"{\"allow\": [{\"cidr\": \"\\ud800A\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: \\ud800A"}}, + {"name":"json: high surrogate before bad escape","input":[{"text":"{\"allow\": [{\"cidr\": \"\\ud800\\u12g4\"}]}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"json: surrogate exception","input":[{"text":"{\"allow\": [{\"cidr\": \"::/0\", \"except\": [\"\\udbff\"]}]}"}],"expected":{"error":"allow[0].except[0] is not a valid IPv4 or IPv6 CIDR: \\udbff"}}, + {"name":"utf-8: byte-order mark","input":[{"text":"\ufeff{\"allow\": []}"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"utf-8: invalid byte","input":[{"hex":"7b22616c6c6f77223a205bff5d7d"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"utf-8: invalid byte in string","input":[{"hex":"7b22616c6c6f77223a205b7b2263696472223a20223139322e302e322e302f3234fe227d5d7d"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"utf-8: overlong encoding","input":[{"hex":"7b22616c6c6f77223a205b7b2263696472223a2022c0af227d5d7d"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"utf-8: encoded surrogate","input":[{"hex":"7b22616c6c6f77223a205b7b2263696472223a2022eda080227d5d7d"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"utf-8: truncated sequence","input":[{"hex":"7b22e282223a205b5d7d"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"utf-8: utf-16","input":[{"hex":"fffe7b00220061006c006c006f00770022003a0020005b005d007d00"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"utf-8: non-ascii text","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\u00e9\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 192.0.2.0/24\u00e9"}}, + {"name":"unknown fields: ascii","input":[{"text":"{\"b\": 1, \"a\": 1, \"B\": 1}"}],"expected":{"error":"egress policy has unknown field 'B'"}}, + {"name":"unknown rule fields: ascii","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"b\": 1, \"a\": 1, \"B\": 1}]}"}],"expected":{"error":"allow[0] has unknown field 'B'"}}, + {"name":"unknown fields: non-ascii","input":[{"text":"{\"\u00e9\": 1, \"z\": 1, \"\u00ff\": 1}"}],"expected":{"error":"egress policy has unknown field 'z'"}}, + {"name":"unknown rule fields: non-ascii","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"\u00e9\": 1, \"z\": 1, \"\u00ff\": 1}]}"}],"expected":{"error":"allow[0] has unknown field 'z'"}}, + {"name":"unknown fields: astral and surrogate","input":[{"text":"{\"\\ud83d\\ude00\": 1, \"\\ud800\": 1, \"\\uffff\": 1}"}],"expected":{"error":"egress policy has unknown field '\\ud800'"}}, + {"name":"unknown rule fields: astral and surrogate","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"\\ud83d\\ude00\": 1, \"\\ud800\": 1, \"\\uffff\": 1}]}"}],"expected":{"error":"allow[0] has unknown field '\\ud800'"}}, + {"name":"allow: rule string","input":[{"text":"{\"allow\": [\"192.0.2.0/24\"]}"}],"expected":{"error":"allow[0] must be an object"}}, + {"name":"allow: rule number","input":[{"text":"{\"allow\": [7]}"}],"expected":{"error":"allow[0] must be an object"}}, + {"name":"allow: rule null","input":[{"text":"{\"allow\": [null]}"}],"expected":{"error":"allow[0] must be an object"}}, + {"name":"allow: rule array","input":[{"text":"{\"allow\": [[{\"cidr\": \"192.0.2.0/24\"}]]}"}],"expected":{"error":"allow[0] must be an object"}}, + {"name":"allow: empty rule","input":[{"text":"{\"allow\": [{}]}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":[]}}, + {"name":"allow: mixed forms","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}], \"cidr\": \"192.0.2.0/24\"}]}"}],"expected":{"error":"allow[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"allow: mixed ports","input":[{"text":"{\"allow\": [{\"ports\": [], \"port\": 80}]}"}],"expected":{"error":"allow[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"allow: legacy unknown field","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"unknown\": 1}]}"}],"expected":{"error":"allow[0] has unknown field 'unknown'"}}, + {"name":"allow: legacy unknown before missing cidr","input":[{"text":"{\"allow\": [{\"port\": 80, \"unknown\": 1}]}"}],"expected":{"error":"allow[0] has unknown field 'unknown'"}}, + {"name":"allow: legacy missing cidr","input":[{"text":"{\"allow\": [{\"protocol\": \"tcp\", \"port\": 80}]}"}],"expected":{"error":"allow[0].cidr is required"}}, + {"name":"allow: legacy except only","input":[{"text":"{\"allow\": [{\"except\": []}]}"}],"expected":{"error":"allow[0].cidr is required"}}, + {"name":"allow: legacy cidr number","input":[{"text":"{\"allow\": [{\"cidr\": 7}]}"}],"expected":{"error":"allow[0].cidr must be an IPv4 or IPv6 CIDR string"}}, + {"name":"allow: legacy cidr null","input":[{"text":"{\"allow\": [{\"cidr\": null}]}"}],"expected":{"error":"allow[0].cidr must be an IPv4 or IPv6 CIDR string"}}, + {"name":"allow: legacy cidr invalid","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/33\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 192.0.2.0/33"}}, + {"name":"allow: legacy cidr scoped","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%eth0/64\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"allow: legacy except string","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": \"192.0.2.1\"}]}"}],"expected":{"error":"allow[0].except must be an array"}}, + {"name":"allow: legacy except null","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": null}]}"}],"expected":{"error":"allow[0].except must be an array"}}, + {"name":"allow: legacy except number","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [7]}]}"}],"expected":{"error":"allow[0].except[0] must be an IPv4 or IPv6 CIDR string"}}, + {"name":"allow: legacy except outside","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"198.51.100.0/24\"]}]}"}],"expected":{"error":"allow[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"allow: legacy except family","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"2001:db8::/32\"]}]}"}],"expected":{"error":"allow[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"allow: legacy except wider","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.0.0/16\"]}]}"}],"expected":{"error":"allow[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"allow: legacy except whole","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.0/24\"]}]}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"allow: legacy except invalid","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.0/24\", \"x\"]}]}"}],"expected":{"error":"allow[0].except[1] is not a valid IPv4 or IPv6 CIDR: x"}}, + {"name":"allow: legacy endPort without port","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"endPort\": 80}]}"}],"expected":{"error":"allow[0].endPort requires port"}}, + {"name":"allow: legacy endPort with protocol","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"endPort\": 80}]}"}],"expected":{"error":"allow[0].endPort requires port"}}, + {"name":"allow: legacy port without protocol","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"port\": 80}]}"}],"expected":{"error":"allow[0].port requires protocol"}}, + {"name":"allow: legacy protocol upper","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"TCP\"}]}"}],"expected":{"error":"allow[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"allow: legacy protocol unknown","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"sctp\"}]}"}],"expected":{"error":"allow[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"allow: legacy protocol empty","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"\"}]}"}],"expected":{"error":"allow[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"allow: legacy protocol null","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": null}]}"}],"expected":{"error":"allow[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"allow: legacy protocol number","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": 6}]}"}],"expected":{"error":"allow[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"allow: legacy icmp port","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\", \"port\": 8}]}"}],"expected":{"error":"allow[0].port is not supported with icmp"}}, + {"name":"allow: legacy icmp endPort","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\", \"endPort\": 8}]}"}],"expected":{"error":"allow[0].endPort requires port"}}, + {"name":"allow: legacy port zero","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 0}]}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"allow: legacy port bool","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": true}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"allow: legacy port string","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": \"80\"}]}"}],"expected":{"error":"allow[0].port must be an integer"}}, + {"name":"allow: legacy endPort zero","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 0}]}"}],"expected":{"error":"allow[0].endPort must be between 1 and 65535"}}, + {"name":"allow: legacy endPort bool","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": false}]}"}],"expected":{"error":"allow[0].endPort must be an integer"}}, + {"name":"allow: legacy endPort below","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 10, \"endPort\": 5}]}"}],"expected":{"error":"allow[0].endPort 5 cannot be below port 10"}}, + {"name":"allow: legacy any range","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"any\", \"port\": 1, \"endPort\": 9}]}"}],"expected":{"allow":["192.0.2.0/24:tcp:1-9","192.0.2.0/24:udp:1-9"],"deny":[]}}, + {"name":"allow: legacy any","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"any\"}]}"}],"expected":{"allow":["192.0.2.0/24"],"deny":[]}}, + {"name":"allow: legacy icmp","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\"}]}"}],"expected":{"allow":["192.0.2.0/24:icmp"],"deny":[]}}, + {"name":"allow: mxc unknown field","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}], \"unknown\": 1}]}"}],"expected":{"error":"allow[0] has unknown field 'unknown'"}}, + {"name":"allow: mxc unknown only","input":[{"text":"{\"allow\": [{\"unknown\": 1}]}"}],"expected":{"error":"allow[0] has unknown field 'unknown'"}}, + {"name":"allow: mxc to object","input":[{"text":"{\"allow\": [{\"to\": {\"cidr\": \"192.0.2.0/24\"}}]}"}],"expected":{"error":"allow[0].to must be an array"}}, + {"name":"allow: mxc to null","input":[{"text":"{\"allow\": [{\"to\": null}]}"}],"expected":{"error":"allow[0].to must be an array"}}, + {"name":"allow: mxc to empty","input":[{"text":"{\"allow\": [{\"to\": []}]}"}],"expected":{"error":"allow[0].to must contain at least one destination"}}, + {"name":"allow: mxc peer string","input":[{"text":"{\"allow\": [{\"to\": [\"192.0.2.0/24\"]}]}"}],"expected":{"error":"allow[0].to[0] must be an object"}}, + {"name":"allow: mxc peer unknown","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"unknown\": 1}]}]}"}],"expected":{"error":"allow[0].to[0] has unknown field 'unknown'"}}, + {"name":"allow: mxc peer empty","input":[{"text":"{\"allow\": [{\"to\": [{}]}]}"}],"expected":{"error":"allow[0].to[0].cidr is required"}}, + {"name":"allow: mxc peer except only","input":[{"text":"{\"allow\": [{\"to\": [{\"except\": []}]}]}"}],"expected":{"error":"allow[0].to[0].cidr is required"}}, + {"name":"allow: mxc peer cidr number","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": 7}]}]}"}],"expected":{"error":"allow[0].to[0].cidr must be an IPv4 or IPv6 CIDR string"}}, + {"name":"allow: mxc peer second invalid","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}, {\"cidr\": \"x\"}]}]}"}],"expected":{"error":"allow[0].to[1].cidr is not a valid IPv4 or IPv6 CIDR: x"}}, + {"name":"allow: mxc peer except null","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"except\": null}]}]}"}],"expected":{"error":"allow[0].to[0].except must be an array"}}, + {"name":"allow: mxc peer except outside","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"10.0.0.0/8\"]}]}]}"}],"expected":{"error":"allow[0].to[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"allow: mxc ports object","input":[{"text":"{\"allow\": [{\"ports\": {\"port\": 80}}]}"}],"expected":{"error":"allow[0].ports must be an array"}}, + {"name":"allow: mxc ports empty","input":[{"text":"{\"allow\": [{\"ports\": []}]}"}],"expected":{"error":"allow[0].ports must contain at least one selector"}}, + {"name":"allow: mxc port string","input":[{"text":"{\"allow\": [{\"ports\": [\"tcp\"]}]}"}],"expected":{"error":"allow[0].ports[0] must be an object"}}, + {"name":"allow: mxc port unknown","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"tcp\", \"unknown\": 1}]}]}"}],"expected":{"error":"allow[0].ports[0] has unknown field 'unknown'"}}, + {"name":"allow: mxc port empty","input":[{"text":"{\"allow\": [{\"ports\": [{}]}]}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":[]}}, + {"name":"allow: mxc port protocol upper","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"Udp\"}]}]}"}],"expected":{"error":"allow[0].ports[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"allow: mxc port protocol null","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": null}]}]}"}],"expected":{"error":"allow[0].ports[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"allow: mxc port endPort only","input":[{"text":"{\"allow\": [{\"ports\": [{\"endPort\": 80}]}]}"}],"expected":{"error":"allow[0].ports[0].endPort requires port"}}, + {"name":"allow: mxc port icmp endPort","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"icmp\", \"endPort\": 80}]}]}"}],"expected":{"error":"allow[0].ports[0].endPort requires port"}}, + {"name":"allow: mxc port icmp port","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"icmp\", \"port\": 8}]}]}"}],"expected":{"error":"allow[0].ports[0].port is not supported with icmp"}}, + {"name":"allow: mxc port null","input":[{"text":"{\"allow\": [{\"ports\": [{\"port\": null}]}]}"}],"expected":{"error":"allow[0].ports[0].port must be an integer"}}, + {"name":"allow: mxc port float","input":[{"text":"{\"allow\": [{\"ports\": [{\"port\": 80.0}]}]}"}],"expected":{"error":"allow[0].ports[0].port must be an integer"}}, + {"name":"allow: mxc port large","input":[{"text":"{\"allow\": [{\"ports\": [{\"port\": 70000}]}]}"}],"expected":{"error":"allow[0].ports[0].port must be between 1 and 65535"}}, + {"name":"allow: mxc endPort large","input":[{"text":"{\"allow\": [{\"ports\": [{\"port\": 80, \"endPort\": 70000}]}]}"}],"expected":{"error":"allow[0].ports[0].endPort must be between 1 and 65535"}}, + {"name":"allow: mxc endPort below","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"udp\", \"port\": 9, \"endPort\": 8}]}]}"}],"expected":{"error":"allow[0].ports[0].endPort 8 cannot be below port 9"}}, + {"name":"allow: mxc second port invalid","input":[{"text":"{\"allow\": [{\"ports\": [{\"port\": 1}, {\"port\": 0}]}]}"}],"expected":{"error":"allow[0].ports[1].port must be between 1 and 65535"}}, + {"name":"allow: mxc to before ports","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"x\"}], \"ports\": [{\"port\": 0}]}]}"}],"expected":{"error":"allow[0].to[0].cidr is not a valid IPv4 or IPv6 CIDR: x"}}, + {"name":"allow: mxc every destination","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"udp\", \"port\": 53}]}]}"}],"expected":{"allow":["0.0.0.0/0:udp:53","::/0:udp:53"],"deny":[]}}, + {"name":"allow: mxc every port","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}]}]}"}],"expected":{"allow":["192.0.2.0/24"],"deny":[]}}, + {"name":"allow: second rule invalid","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\"}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"TCP\"}]}"}],"expected":{"error":"allow[1].protocol must be tcp, udp, icmp, or any"}}, + {"name":"deny: rule string","input":[{"text":"{\"deny\": [\"192.0.2.0/24\"]}"}],"expected":{"error":"deny[0] must be an object"}}, + {"name":"deny: rule number","input":[{"text":"{\"deny\": [7]}"}],"expected":{"error":"deny[0] must be an object"}}, + {"name":"deny: rule null","input":[{"text":"{\"deny\": [null]}"}],"expected":{"error":"deny[0] must be an object"}}, + {"name":"deny: rule array","input":[{"text":"{\"deny\": [[{\"cidr\": \"192.0.2.0/24\"}]]}"}],"expected":{"error":"deny[0] must be an object"}}, + {"name":"deny: empty rule","input":[{"text":"{\"deny\": [{}]}"}],"expected":{"allow":[],"deny":["0.0.0.0/0","::/0"]}}, + {"name":"deny: mixed forms","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}], \"cidr\": \"192.0.2.0/24\"}]}"}],"expected":{"error":"deny[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"deny: mixed ports","input":[{"text":"{\"deny\": [{\"ports\": [], \"port\": 80}]}"}],"expected":{"error":"deny[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"deny: legacy unknown field","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"unknown\": 1}]}"}],"expected":{"error":"deny[0] has unknown field 'unknown'"}}, + {"name":"deny: legacy unknown before missing cidr","input":[{"text":"{\"deny\": [{\"port\": 80, \"unknown\": 1}]}"}],"expected":{"error":"deny[0] has unknown field 'unknown'"}}, + {"name":"deny: legacy missing cidr","input":[{"text":"{\"deny\": [{\"protocol\": \"tcp\", \"port\": 80}]}"}],"expected":{"error":"deny[0].cidr is required"}}, + {"name":"deny: legacy except only","input":[{"text":"{\"deny\": [{\"except\": []}]}"}],"expected":{"error":"deny[0].cidr is required"}}, + {"name":"deny: legacy cidr number","input":[{"text":"{\"deny\": [{\"cidr\": 7}]}"}],"expected":{"error":"deny[0].cidr must be an IPv4 or IPv6 CIDR string"}}, + {"name":"deny: legacy cidr null","input":[{"text":"{\"deny\": [{\"cidr\": null}]}"}],"expected":{"error":"deny[0].cidr must be an IPv4 or IPv6 CIDR string"}}, + {"name":"deny: legacy cidr invalid","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/33\"}]}"}],"expected":{"error":"deny[0].cidr is not a valid IPv4 or IPv6 CIDR: 192.0.2.0/33"}}, + {"name":"deny: legacy cidr scoped","input":[{"text":"{\"deny\": [{\"cidr\": \"fe80::1%eth0/64\"}]}"}],"expected":{"error":"deny[0].cidr must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"deny: legacy except string","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"except\": \"192.0.2.1\"}]}"}],"expected":{"error":"deny[0].except must be an array"}}, + {"name":"deny: legacy except null","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"except\": null}]}"}],"expected":{"error":"deny[0].except must be an array"}}, + {"name":"deny: legacy except number","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [7]}]}"}],"expected":{"error":"deny[0].except[0] must be an IPv4 or IPv6 CIDR string"}}, + {"name":"deny: legacy except outside","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"198.51.100.0/24\"]}]}"}],"expected":{"error":"deny[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"deny: legacy except family","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"2001:db8::/32\"]}]}"}],"expected":{"error":"deny[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"deny: legacy except wider","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.0.0/16\"]}]}"}],"expected":{"error":"deny[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"deny: legacy except whole","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.0/24\"]}]}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"deny: legacy except invalid","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"192.0.2.0/24\", \"x\"]}]}"}],"expected":{"error":"deny[0].except[1] is not a valid IPv4 or IPv6 CIDR: x"}}, + {"name":"deny: legacy endPort without port","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"endPort\": 80}]}"}],"expected":{"error":"deny[0].endPort requires port"}}, + {"name":"deny: legacy endPort with protocol","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"endPort\": 80}]}"}],"expected":{"error":"deny[0].endPort requires port"}}, + {"name":"deny: legacy port without protocol","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"port\": 80}]}"}],"expected":{"error":"deny[0].port requires protocol"}}, + {"name":"deny: legacy protocol upper","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"TCP\"}]}"}],"expected":{"error":"deny[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"deny: legacy protocol unknown","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"sctp\"}]}"}],"expected":{"error":"deny[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"deny: legacy protocol empty","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"\"}]}"}],"expected":{"error":"deny[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"deny: legacy protocol null","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": null}]}"}],"expected":{"error":"deny[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"deny: legacy protocol number","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": 6}]}"}],"expected":{"error":"deny[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"deny: legacy icmp port","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\", \"port\": 8}]}"}],"expected":{"error":"deny[0].port is not supported with icmp"}}, + {"name":"deny: legacy icmp endPort","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\", \"endPort\": 8}]}"}],"expected":{"error":"deny[0].endPort requires port"}}, + {"name":"deny: legacy port zero","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 0}]}"}],"expected":{"error":"deny[0].port must be between 1 and 65535"}}, + {"name":"deny: legacy port bool","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": true}]}"}],"expected":{"error":"deny[0].port must be an integer"}}, + {"name":"deny: legacy port string","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": \"80\"}]}"}],"expected":{"error":"deny[0].port must be an integer"}}, + {"name":"deny: legacy endPort zero","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 0}]}"}],"expected":{"error":"deny[0].endPort must be between 1 and 65535"}}, + {"name":"deny: legacy endPort bool","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": false}]}"}],"expected":{"error":"deny[0].endPort must be an integer"}}, + {"name":"deny: legacy endPort below","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\", \"port\": 10, \"endPort\": 5}]}"}],"expected":{"error":"deny[0].endPort 5 cannot be below port 10"}}, + {"name":"deny: legacy any range","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"any\", \"port\": 1, \"endPort\": 9}]}"}],"expected":{"allow":[],"deny":["192.0.2.0/24:tcp:1-9","192.0.2.0/24:udp:1-9"]}}, + {"name":"deny: legacy any","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"any\"}]}"}],"expected":{"allow":[],"deny":["192.0.2.0/24"]}}, + {"name":"deny: legacy icmp","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\"}]}"}],"expected":{"allow":[],"deny":["192.0.2.0/24:icmp"]}}, + {"name":"deny: mxc unknown field","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}], \"unknown\": 1}]}"}],"expected":{"error":"deny[0] has unknown field 'unknown'"}}, + {"name":"deny: mxc unknown only","input":[{"text":"{\"deny\": [{\"unknown\": 1}]}"}],"expected":{"error":"deny[0] has unknown field 'unknown'"}}, + {"name":"deny: mxc to object","input":[{"text":"{\"deny\": [{\"to\": {\"cidr\": \"192.0.2.0/24\"}}]}"}],"expected":{"error":"deny[0].to must be an array"}}, + {"name":"deny: mxc to null","input":[{"text":"{\"deny\": [{\"to\": null}]}"}],"expected":{"error":"deny[0].to must be an array"}}, + {"name":"deny: mxc to empty","input":[{"text":"{\"deny\": [{\"to\": []}]}"}],"expected":{"error":"deny[0].to must contain at least one destination"}}, + {"name":"deny: mxc peer string","input":[{"text":"{\"deny\": [{\"to\": [\"192.0.2.0/24\"]}]}"}],"expected":{"error":"deny[0].to[0] must be an object"}}, + {"name":"deny: mxc peer unknown","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"unknown\": 1}]}]}"}],"expected":{"error":"deny[0].to[0] has unknown field 'unknown'"}}, + {"name":"deny: mxc peer empty","input":[{"text":"{\"deny\": [{\"to\": [{}]}]}"}],"expected":{"error":"deny[0].to[0].cidr is required"}}, + {"name":"deny: mxc peer except only","input":[{"text":"{\"deny\": [{\"to\": [{\"except\": []}]}]}"}],"expected":{"error":"deny[0].to[0].cidr is required"}}, + {"name":"deny: mxc peer cidr number","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": 7}]}]}"}],"expected":{"error":"deny[0].to[0].cidr must be an IPv4 or IPv6 CIDR string"}}, + {"name":"deny: mxc peer second invalid","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}, {\"cidr\": \"x\"}]}]}"}],"expected":{"error":"deny[0].to[1].cidr is not a valid IPv4 or IPv6 CIDR: x"}}, + {"name":"deny: mxc peer except null","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"except\": null}]}]}"}],"expected":{"error":"deny[0].to[0].except must be an array"}}, + {"name":"deny: mxc peer except outside","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"10.0.0.0/8\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"deny: mxc ports object","input":[{"text":"{\"deny\": [{\"ports\": {\"port\": 80}}]}"}],"expected":{"error":"deny[0].ports must be an array"}}, + {"name":"deny: mxc ports empty","input":[{"text":"{\"deny\": [{\"ports\": []}]}"}],"expected":{"error":"deny[0].ports must contain at least one selector"}}, + {"name":"deny: mxc port string","input":[{"text":"{\"deny\": [{\"ports\": [\"tcp\"]}]}"}],"expected":{"error":"deny[0].ports[0] must be an object"}}, + {"name":"deny: mxc port unknown","input":[{"text":"{\"deny\": [{\"ports\": [{\"protocol\": \"tcp\", \"unknown\": 1}]}]}"}],"expected":{"error":"deny[0].ports[0] has unknown field 'unknown'"}}, + {"name":"deny: mxc port empty","input":[{"text":"{\"deny\": [{\"ports\": [{}]}]}"}],"expected":{"allow":[],"deny":["0.0.0.0/0","::/0"]}}, + {"name":"deny: mxc port protocol upper","input":[{"text":"{\"deny\": [{\"ports\": [{\"protocol\": \"Udp\"}]}]}"}],"expected":{"error":"deny[0].ports[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"deny: mxc port protocol null","input":[{"text":"{\"deny\": [{\"ports\": [{\"protocol\": null}]}]}"}],"expected":{"error":"deny[0].ports[0].protocol must be tcp, udp, icmp, or any"}}, + {"name":"deny: mxc port endPort only","input":[{"text":"{\"deny\": [{\"ports\": [{\"endPort\": 80}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort requires port"}}, + {"name":"deny: mxc port icmp endPort","input":[{"text":"{\"deny\": [{\"ports\": [{\"protocol\": \"icmp\", \"endPort\": 80}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort requires port"}}, + {"name":"deny: mxc port icmp port","input":[{"text":"{\"deny\": [{\"ports\": [{\"protocol\": \"icmp\", \"port\": 8}]}]}"}],"expected":{"error":"deny[0].ports[0].port is not supported with icmp"}}, + {"name":"deny: mxc port null","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": null}]}]}"}],"expected":{"error":"deny[0].ports[0].port must be an integer"}}, + {"name":"deny: mxc port float","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 80.0}]}]}"}],"expected":{"error":"deny[0].ports[0].port must be an integer"}}, + {"name":"deny: mxc port large","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 70000}]}]}"}],"expected":{"error":"deny[0].ports[0].port must be between 1 and 65535"}}, + {"name":"deny: mxc endPort large","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 80, \"endPort\": 70000}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort must be between 1 and 65535"}}, + {"name":"deny: mxc endPort below","input":[{"text":"{\"deny\": [{\"ports\": [{\"protocol\": \"udp\", \"port\": 9, \"endPort\": 8}]}]}"}],"expected":{"error":"deny[0].ports[0].endPort 8 cannot be below port 9"}}, + {"name":"deny: mxc second port invalid","input":[{"text":"{\"deny\": [{\"ports\": [{\"port\": 1}, {\"port\": 0}]}]}"}],"expected":{"error":"deny[0].ports[1].port must be between 1 and 65535"}}, + {"name":"deny: mxc to before ports","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"x\"}], \"ports\": [{\"port\": 0}]}]}"}],"expected":{"error":"deny[0].to[0].cidr is not a valid IPv4 or IPv6 CIDR: x"}}, + {"name":"deny: mxc every destination","input":[{"text":"{\"deny\": [{\"ports\": [{\"protocol\": \"udp\", \"port\": 53}]}]}"}],"expected":{"allow":[],"deny":["0.0.0.0/0:udp:53","::/0:udp:53"]}}, + {"name":"deny: mxc every port","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.0/24\"}]}]}"}],"expected":{"allow":[],"deny":["192.0.2.0/24"]}}, + {"name":"deny: second rule invalid","input":[{"text":"{\"deny\": [{\"cidr\": \"192.0.2.0/24\"}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"TCP\"}]}"}],"expected":{"error":"deny[1].protocol must be tcp, udp, icmp, or any"}}, + {"name":"policy: allow object","input":[{"text":"{\"allow\": {}}"}],"expected":{"error":"allow must be an array"}}, + {"name":"policy: allow null","input":[{"text":"{\"allow\": null}"}],"expected":{"error":"allow must be an array"}}, + {"name":"policy: deny string","input":[{"text":"{\"deny\": \"192.0.2.0/24\"}"}],"expected":{"error":"deny must be an array"}}, + {"name":"policy: deny number","input":[{"text":"{\"deny\": 1}"}],"expected":{"error":"deny must be an array"}}, + {"name":"policy: unknown root field","input":[{"text":"{\"allow\": [], \"denied\": []}"}],"expected":{"error":"egress policy has unknown field 'denied'"}}, + {"name":"policy: allow before deny errors","input":[{"text":"{\"allow\": [{\"cidr\": 7}], \"deny\": [{\"cidr\": 8}]}"}],"expected":{"error":"allow[0].cidr must be an IPv4 or IPv6 CIDR string"}}, + {"name":"policy: later rule error before lowering","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.1\"}, {\"cidr\": \"10.0.1.1\"}, {\"cidr\": \"10.0.2.1\"}, {\"cidr\": \"10.0.3.1\"}, {\"cidr\": \"10.0.4.1\"}, {\"cidr\": \"10.0.5.1\"}, {\"cidr\": \"10.0.6.1\"}, {\"cidr\": \"10.0.7.1\"}, {\"cidr\": \"10.0.8.1\"}, {\"cidr\": \"10.0.9.1\"}, {\"cidr\": \"10.0.10.1\"}, {\"cidr\": \"10.0.11.1\"}, {\"cidr\": \"10.0.12.1\"}, {\"cidr\": \"10.0.13.1\"}, {\"cidr\": \"10.0.14.1\"}, {\"cidr\": \"10.0.15.1\"}, {\"cidr\": \"10.0.16.1\"}, {\"cidr\": \"10.0.17.1\"}, {\"cidr\": \"10.0.18.1\"}, {\"cidr\": \"10.0.19.1\"}, {\"cidr\": \"10.0.20.1\"}, {\"cidr\": \"10.0.21.1\"}, {\"cidr\": \"10.0.22.1\"}, {\"cidr\": \"10.0.23.1\"}, {\"cidr\": \"10.0.24.1\"}, {\"cidr\": \"10.0.25.1\"}, {\"cidr\": \"10.0.26.1\"}, {\"cidr\": \"10.0.27.1\"}, {\"cidr\": \"10.0.28.1\"}, {\"cidr\": \"10.0.29.1\"}, {\"cidr\": \"10.0.30.1\"}, {\"cidr\": \"10.0.31.1\"}, {\"cidr\": \"10.0.32.1\"}, {\"cidr\": \"10.0.33.1\"}, {\"cidr\": \"10.0.34.1\"}, {\"cidr\": \"10.0.35.1\"}, {\"cidr\": \"10.0.36.1\"}, {\"cidr\": \"10.0.37.1\"}, {\"cidr\": \"10.0.38.1\"}, {\"cidr\": \"10.0.39.1\"}, {\"cidr\": \"10.0.40.1\"}, {\"cidr\": \"10.0.41.1\"}, {\"cidr\": \"10.0.42.1\"}, {\"cidr\": \"10.0.43.1\"}, {\"cidr\": \"10.0.44.1\"}, {\"cidr\": \"10.0.45.1\"}, {\"cidr\": \"10.0.46.1\"}, {\"cidr\": \"10.0.47.1\"}, {\"cidr\": \"10.0.48.1\"}, {\"cidr\": \"10.0.49.1\"}, {\"cidr\": \"10.0.50.1\"}, {\"cidr\": \"10.0.51.1\"}, {\"cidr\": \"10.0.52.1\"}, {\"cidr\": \"10.0.53.1\"}, {\"cidr\": \"10.0.54.1\"}, {\"cidr\": \"10.0.55.1\"}, {\"cidr\": \"10.0.56.1\"}, {\"cidr\": \"10.0.57.1\"}, {\"cidr\": \"10.0.58.1\"}, {\"cidr\": \"10.0.59.1\"}, {\"cidr\": \"10.0.60.1\"}, {\"cidr\": \"10.0.61.1\"}, {\"cidr\": \"10.0.62.1\"}, {\"cidr\": \"10.0.63.1\"}, {\"cidr\": \"10.0.64.1\"}, {\"cidr\": \"10.0.65.1\"}, {\"cidr\": \"10.0.66.1\"}, {\"cidr\": \"10.0.67.1\"}, {\"cidr\": \"10.0.68.1\"}, {\"cidr\": \"10.0.69.1\"}, {\"cidr\": \"10.0.70.1\"}, {\"cidr\": \"10.0.71.1\"}, {\"cidr\": \"10.0.72.1\"}, {\"cidr\": \"10.0.73.1\"}, {\"cidr\": \"10.0.74.1\"}, {\"cidr\": \"10.0.75.1\"}, {\"cidr\": \"10.0.76.1\"}, {\"cidr\": \"10.0.77.1\"}, {\"cidr\": \"10.0.78.1\"}, {\"cidr\": \"10.0.79.1\"}, {\"cidr\": \"10.0.80.1\"}, {\"cidr\": \"10.0.81.1\"}, {\"cidr\": \"10.0.82.1\"}, {\"cidr\": \"10.0.83.1\"}, {\"cidr\": \"10.0.84.1\"}, {\"cidr\": \"10.0.85.1\"}, {\"cidr\": \"10.0.86.1\"}, {\"cidr\": \"10.0.87.1\"}, {\"cidr\": \"10.0.88.1\"}, {\"cidr\": \"10.0.89.1\"}, {\"cidr\": \"10.0.90.1\"}, {\"cidr\": \"10.0.91.1\"}, {\"cidr\": \"10.0.92.1\"}, {\"cidr\": \"10.0.93.1\"}, {\"cidr\": \"10.0.94.1\"}, {\"cidr\": \"10.0.95.1\"}, {\"cidr\": \"10.0.96.1\"}, {\"cidr\": \"10.0.97.1\"}, {\"cidr\": \"10.0.98.1\"}, {\"cidr\": \"10.0.99.1\"}, {\"cidr\": \"10.0.100.1\"}, {\"cidr\": \"10.0.101.1\"}, {\"cidr\": \"10.0.102.1\"}, {\"cidr\": \"10.0.103.1\"}, {\"cidr\": \"10.0.104.1\"}, {\"cidr\": \"10.0.105.1\"}, {\"cidr\": \"10.0.106.1\"}, {\"cidr\": \"10.0.107.1\"}, {\"cidr\": \"10.0.108.1\"}, {\"cidr\": \"10.0.109.1\"}, {\"cidr\": \"10.0.110.1\"}, {\"cidr\": \"10.0.111.1\"}, {\"cidr\": \"10.0.112.1\"}, {\"cidr\": \"10.0.113.1\"}, {\"cidr\": \"10.0.114.1\"}, {\"cidr\": \"10.0.115.1\"}, {\"cidr\": \"10.0.116.1\"}, {\"cidr\": \"10.0.117.1\"}, {\"cidr\": \"10.0.118.1\"}, {\"cidr\": \"10.0.119.1\"}, {\"cidr\": \"10.0.120.1\"}, {\"cidr\": \"10.0.121.1\"}, {\"cidr\": \"10.0.122.1\"}, {\"cidr\": \"10.0.123.1\"}, {\"cidr\": \"10.0.124.1\"}, {\"cidr\": \"10.0.125.1\"}, {\"cidr\": \"10.0.126.1\"}, {\"cidr\": \"10.0.127.1\"}, {\"cidr\": \"10.0.128.1\"}, {\"cidr\": \"10.0.129.1\"}, {\"cidr\": \"10.0.130.1\"}, {\"cidr\": \"10.0.131.1\"}, {\"cidr\": \"10.0.132.1\"}, {\"cidr\": \"10.0.133.1\"}, {\"cidr\": \"10.0.134.1\"}, {\"cidr\": \"10.0.135.1\"}, {\"cidr\": \"10.0.136.1\"}, {\"cidr\": \"10.0.137.1\"}, {\"cidr\": \"10.0.138.1\"}, {\"cidr\": \"10.0.139.1\"}, {\"cidr\": \"10.0.140.1\"}, {\"cidr\": \"10.0.141.1\"}, {\"cidr\": \"10.0.142.1\"}, {\"cidr\": \"10.0.143.1\"}, {\"cidr\": \"10.0.144.1\"}, {\"cidr\": \"10.0.145.1\"}, {\"cidr\": \"10.0.146.1\"}, {\"cidr\": \"10.0.147.1\"}, {\"cidr\": \"10.0.148.1\"}, {\"cidr\": \"10.0.149.1\"}, {\"cidr\": \"10.0.150.1\"}, {\"cidr\": \"10.0.151.1\"}, {\"cidr\": \"10.0.152.1\"}, {\"cidr\": \"10.0.153.1\"}, {\"cidr\": \"10.0.154.1\"}, {\"cidr\": \"10.0.155.1\"}, {\"cidr\": \"10.0.156.1\"}, {\"cidr\": \"10.0.157.1\"}, {\"cidr\": \"10.0.158.1\"}, {\"cidr\": \"10.0.159.1\"}, {\"cidr\": \"10.0.160.1\"}, {\"cidr\": \"10.0.161.1\"}, {\"cidr\": \"10.0.162.1\"}, {\"cidr\": \"10.0.163.1\"}, {\"cidr\": \"10.0.164.1\"}, {\"cidr\": \"10.0.165.1\"}, {\"cidr\": \"10.0.166.1\"}, {\"cidr\": \"10.0.167.1\"}, {\"cidr\": \"10.0.168.1\"}, {\"cidr\": \"10.0.169.1\"}, {\"cidr\": \"10.0.170.1\"}, {\"cidr\": \"10.0.171.1\"}, {\"cidr\": \"10.0.172.1\"}, {\"cidr\": \"10.0.173.1\"}, {\"cidr\": \"10.0.174.1\"}, {\"cidr\": \"10.0.175.1\"}, {\"cidr\": \"10.0.176.1\"}, {\"cidr\": \"10.0.177.1\"}, {\"cidr\": \"10.0.178.1\"}, {\"cidr\": \"10.0.179.1\"}, {\"cidr\": \"10.0.180.1\"}, {\"cidr\": \"10.0.181.1\"}, {\"cidr\": \"10.0.182.1\"}, {\"cidr\": \"10.0.183.1\"}, {\"cidr\": \"10.0.184.1\"}, {\"cidr\": \"10.0.185.1\"}, {\"cidr\": \"10.0.186.1\"}, {\"cidr\": \"10.0.187.1\"}, {\"cidr\": \"10.0.188.1\"}, {\"cidr\": \"10.0.189.1\"}, {\"cidr\": \"10.0.190.1\"}, {\"cidr\": \"10.0.191.1\"}, {\"cidr\": \"10.0.192.1\"}, {\"cidr\": \"10.0.193.1\"}, {\"cidr\": \"10.0.194.1\"}, {\"cidr\": \"10.0.195.1\"}, {\"cidr\": \"10.0.196.1\"}, {\"cidr\": \"10.0.197.1\"}, {\"cidr\": \"10.0.198.1\"}, {\"cidr\": \"10.0.199.1\"}, {\"cidr\": \"10.0.200.1\"}, {\"cidr\": \"10.0.201.1\"}, {\"cidr\": \"10.0.202.1\"}, {\"cidr\": \"10.0.203.1\"}, {\"cidr\": \"10.0.204.1\"}, {\"cidr\": \"10.0.205.1\"}, {\"cidr\": \"10.0.206.1\"}, {\"cidr\": \"10.0.207.1\"}, {\"cidr\": \"10.0.208.1\"}, {\"cidr\": \"10.0.209.1\"}, {\"cidr\": \"10.0.210.1\"}, {\"cidr\": \"10.0.211.1\"}, {\"cidr\": \"10.0.212.1\"}, {\"cidr\": \"10.0.213.1\"}, {\"cidr\": \"10.0.214.1\"}, {\"cidr\": \"10.0.215.1\"}, {\"cidr\": \"10.0.216.1\"}, {\"cidr\": \"10.0.217.1\"}, {\"cidr\": \"10.0.218.1\"}, {\"cidr\": \"10.0.219.1\"}, {\"cidr\": \"10.0.220.1\"}, {\"cidr\": \"10.0.221.1\"}, {\"cidr\": \"10.0.222.1\"}, {\"cidr\": \"10.0.223.1\"}, {\"cidr\": \"10.0.224.1\"}, {\"cidr\": \"10.0.225.1\"}, {\"cidr\": \"10.0.226.1\"}, {\"cidr\": \"10.0.227.1\"}, {\"cidr\": \"10.0.228.1\"}, {\"cidr\": \"10.0.229.1\"}, {\"cidr\": \"10.0.230.1\"}, {\"cidr\": \"10.0.231.1\"}, {\"cidr\": \"10.0.232.1\"}, {\"cidr\": \"10.0.233.1\"}, {\"cidr\": \"10.0.234.1\"}, {\"cidr\": \"10.0.235.1\"}, {\"cidr\": \"10.0.236.1\"}, {\"cidr\": \"10.0.237.1\"}, {\"cidr\": \"10.0.238.1\"}, {\"cidr\": \"10.0.239.1\"}, {\"cidr\": \"10.0.240.1\"}, {\"cidr\": \"10.0.241.1\"}, {\"cidr\": \"10.0.242.1\"}, {\"cidr\": \"10.0.243.1\"}, {\"cidr\": \"10.0.244.1\"}, {\"cidr\": \"10.0.245.1\"}, {\"cidr\": \"10.0.246.1\"}, {\"cidr\": \"10.0.247.1\"}, {\"cidr\": \"10.0.248.1\"}, {\"cidr\": \"10.0.249.1\"}, {\"cidr\": \"10.0.250.1\"}, {\"cidr\": \"10.0.251.1\"}, {\"cidr\": \"10.0.252.1\"}, {\"cidr\": \"10.0.253.1\"}, {\"cidr\": \"10.0.254.1\"}, {\"cidr\": \"10.0.255.1\"}, {\"cidr\": \"10.0.256.1\"}, {\"cidr\": \"10.0.257.1\"}, {\"cidr\": \"10.0.258.1\"}, {\"cidr\": \"10.0.259.1\"}, {\"cidr\": \"10.0.260.1\"}, {\"cidr\": \"10.0.261.1\"}, {\"cidr\": \"10.0.262.1\"}, {\"cidr\": \"10.0.263.1\"}, {\"cidr\": \"10.0.264.1\"}, {\"cidr\": \"10.0.265.1\"}, {\"cidr\": \"10.0.266.1\"}, {\"cidr\": \"10.0.267.1\"}, {\"cidr\": \"10.0.268.1\"}, {\"cidr\": \"10.0.269.1\"}, {\"cidr\": \"10.0.270.1\"}, {\"cidr\": \"10.0.271.1\"}, {\"cidr\": \"10.0.272.1\"}, {\"cidr\": \"10.0.273.1\"}, {\"cidr\": \"10.0.274.1\"}, {\"cidr\": \"10.0.275.1\"}, {\"cidr\": \"10.0.276.1\"}, {\"cidr\": \"10.0.277.1\"}, {\"cidr\": \"10.0.278.1\"}, {\"cidr\": \"10.0.279.1\"}, {\"cidr\": \"10.0.280.1\"}, {\"cidr\": \"10.0.281.1\"}, {\"cidr\": \"10.0.282.1\"}, {\"cidr\": \"10.0.283.1\"}, {\"cidr\": \"10.0.284.1\"}, {\"cidr\": \"10.0.285.1\"}, {\"cidr\": \"10.0.286.1\"}, {\"cidr\": \"10.0.287.1\"}, {\"cidr\": \"10.0.288.1\"}, {\"cidr\": \"10.0.289.1\"}, {\"cidr\": \"10.0.290.1\"}, {\"cidr\": \"10.0.291.1\"}, {\"cidr\": \"10.0.292.1\"}, {\"cidr\": \"10.0.293.1\"}, {\"cidr\": \"10.0.294.1\"}, {\"cidr\": \"10.0.295.1\"}, {\"cidr\": \"10.0.296.1\"}, {\"cidr\": \"10.0.297.1\"}, {\"cidr\": \"10.0.298.1\"}, {\"cidr\": \"10.0.299.1\"}, {\"cidr\": 7}]}"}],"expected":{"error":"allow[256].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.256.1"}}, + {"name":"policy: allow limit before deny errors","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.1\"}, {\"cidr\": \"10.0.1.1\"}, {\"cidr\": \"10.0.2.1\"}, {\"cidr\": \"10.0.3.1\"}, {\"cidr\": \"10.0.4.1\"}, {\"cidr\": \"10.0.5.1\"}, {\"cidr\": \"10.0.6.1\"}, {\"cidr\": \"10.0.7.1\"}, {\"cidr\": \"10.0.8.1\"}, {\"cidr\": \"10.0.9.1\"}, {\"cidr\": \"10.0.10.1\"}, {\"cidr\": \"10.0.11.1\"}, {\"cidr\": \"10.0.12.1\"}, {\"cidr\": \"10.0.13.1\"}, {\"cidr\": \"10.0.14.1\"}, {\"cidr\": \"10.0.15.1\"}, {\"cidr\": \"10.0.16.1\"}, {\"cidr\": \"10.0.17.1\"}, {\"cidr\": \"10.0.18.1\"}, {\"cidr\": \"10.0.19.1\"}, {\"cidr\": \"10.0.20.1\"}, {\"cidr\": \"10.0.21.1\"}, {\"cidr\": \"10.0.22.1\"}, {\"cidr\": \"10.0.23.1\"}, {\"cidr\": \"10.0.24.1\"}, {\"cidr\": \"10.0.25.1\"}, {\"cidr\": \"10.0.26.1\"}, {\"cidr\": \"10.0.27.1\"}, {\"cidr\": \"10.0.28.1\"}, {\"cidr\": \"10.0.29.1\"}, {\"cidr\": \"10.0.30.1\"}, {\"cidr\": \"10.0.31.1\"}, {\"cidr\": \"10.0.32.1\"}, {\"cidr\": \"10.0.33.1\"}, {\"cidr\": \"10.0.34.1\"}, {\"cidr\": \"10.0.35.1\"}, {\"cidr\": \"10.0.36.1\"}, {\"cidr\": \"10.0.37.1\"}, {\"cidr\": \"10.0.38.1\"}, {\"cidr\": \"10.0.39.1\"}, {\"cidr\": \"10.0.40.1\"}, {\"cidr\": \"10.0.41.1\"}, {\"cidr\": \"10.0.42.1\"}, {\"cidr\": \"10.0.43.1\"}, {\"cidr\": \"10.0.44.1\"}, {\"cidr\": \"10.0.45.1\"}, {\"cidr\": \"10.0.46.1\"}, {\"cidr\": \"10.0.47.1\"}, {\"cidr\": \"10.0.48.1\"}, {\"cidr\": \"10.0.49.1\"}, {\"cidr\": \"10.0.50.1\"}, {\"cidr\": \"10.0.51.1\"}, {\"cidr\": \"10.0.52.1\"}, {\"cidr\": \"10.0.53.1\"}, {\"cidr\": \"10.0.54.1\"}, {\"cidr\": \"10.0.55.1\"}, {\"cidr\": \"10.0.56.1\"}, {\"cidr\": \"10.0.57.1\"}, {\"cidr\": \"10.0.58.1\"}, {\"cidr\": \"10.0.59.1\"}, {\"cidr\": \"10.0.60.1\"}, {\"cidr\": \"10.0.61.1\"}, {\"cidr\": \"10.0.62.1\"}, {\"cidr\": \"10.0.63.1\"}, {\"cidr\": \"10.0.64.1\"}, {\"cidr\": \"10.0.65.1\"}, {\"cidr\": \"10.0.66.1\"}, {\"cidr\": \"10.0.67.1\"}, {\"cidr\": \"10.0.68.1\"}, {\"cidr\": \"10.0.69.1\"}, {\"cidr\": \"10.0.70.1\"}, {\"cidr\": \"10.0.71.1\"}, {\"cidr\": \"10.0.72.1\"}, {\"cidr\": \"10.0.73.1\"}, {\"cidr\": \"10.0.74.1\"}, {\"cidr\": \"10.0.75.1\"}, {\"cidr\": \"10.0.76.1\"}, {\"cidr\": \"10.0.77.1\"}, {\"cidr\": \"10.0.78.1\"}, {\"cidr\": \"10.0.79.1\"}, {\"cidr\": \"10.0.80.1\"}, {\"cidr\": \"10.0.81.1\"}, {\"cidr\": \"10.0.82.1\"}, {\"cidr\": \"10.0.83.1\"}, {\"cidr\": \"10.0.84.1\"}, {\"cidr\": \"10.0.85.1\"}, {\"cidr\": \"10.0.86.1\"}, {\"cidr\": \"10.0.87.1\"}, {\"cidr\": \"10.0.88.1\"}, {\"cidr\": \"10.0.89.1\"}, {\"cidr\": \"10.0.90.1\"}, {\"cidr\": \"10.0.91.1\"}, {\"cidr\": \"10.0.92.1\"}, {\"cidr\": \"10.0.93.1\"}, {\"cidr\": \"10.0.94.1\"}, {\"cidr\": \"10.0.95.1\"}, {\"cidr\": \"10.0.96.1\"}, {\"cidr\": \"10.0.97.1\"}, {\"cidr\": \"10.0.98.1\"}, {\"cidr\": \"10.0.99.1\"}, {\"cidr\": \"10.0.100.1\"}, {\"cidr\": \"10.0.101.1\"}, {\"cidr\": \"10.0.102.1\"}, {\"cidr\": \"10.0.103.1\"}, {\"cidr\": \"10.0.104.1\"}, {\"cidr\": \"10.0.105.1\"}, {\"cidr\": \"10.0.106.1\"}, {\"cidr\": \"10.0.107.1\"}, {\"cidr\": \"10.0.108.1\"}, {\"cidr\": \"10.0.109.1\"}, {\"cidr\": \"10.0.110.1\"}, {\"cidr\": \"10.0.111.1\"}, {\"cidr\": \"10.0.112.1\"}, {\"cidr\": \"10.0.113.1\"}, {\"cidr\": \"10.0.114.1\"}, {\"cidr\": \"10.0.115.1\"}, {\"cidr\": \"10.0.116.1\"}, {\"cidr\": \"10.0.117.1\"}, {\"cidr\": \"10.0.118.1\"}, {\"cidr\": \"10.0.119.1\"}, {\"cidr\": \"10.0.120.1\"}, {\"cidr\": \"10.0.121.1\"}, {\"cidr\": \"10.0.122.1\"}, {\"cidr\": \"10.0.123.1\"}, {\"cidr\": \"10.0.124.1\"}, {\"cidr\": \"10.0.125.1\"}, {\"cidr\": \"10.0.126.1\"}, {\"cidr\": \"10.0.127.1\"}, {\"cidr\": \"10.0.128.1\"}, {\"cidr\": \"10.0.129.1\"}, {\"cidr\": \"10.0.130.1\"}, {\"cidr\": \"10.0.131.1\"}, {\"cidr\": \"10.0.132.1\"}, {\"cidr\": \"10.0.133.1\"}, {\"cidr\": \"10.0.134.1\"}, {\"cidr\": \"10.0.135.1\"}, {\"cidr\": \"10.0.136.1\"}, {\"cidr\": \"10.0.137.1\"}, {\"cidr\": \"10.0.138.1\"}, {\"cidr\": \"10.0.139.1\"}, {\"cidr\": \"10.0.140.1\"}, {\"cidr\": \"10.0.141.1\"}, {\"cidr\": \"10.0.142.1\"}, {\"cidr\": \"10.0.143.1\"}, {\"cidr\": \"10.0.144.1\"}, {\"cidr\": \"10.0.145.1\"}, {\"cidr\": \"10.0.146.1\"}, {\"cidr\": \"10.0.147.1\"}, {\"cidr\": \"10.0.148.1\"}, {\"cidr\": \"10.0.149.1\"}, {\"cidr\": \"10.0.150.1\"}, {\"cidr\": \"10.0.151.1\"}, {\"cidr\": \"10.0.152.1\"}, {\"cidr\": \"10.0.153.1\"}, {\"cidr\": \"10.0.154.1\"}, {\"cidr\": \"10.0.155.1\"}, {\"cidr\": \"10.0.156.1\"}, {\"cidr\": \"10.0.157.1\"}, {\"cidr\": \"10.0.158.1\"}, {\"cidr\": \"10.0.159.1\"}, {\"cidr\": \"10.0.160.1\"}, {\"cidr\": \"10.0.161.1\"}, {\"cidr\": \"10.0.162.1\"}, {\"cidr\": \"10.0.163.1\"}, {\"cidr\": \"10.0.164.1\"}, {\"cidr\": \"10.0.165.1\"}, {\"cidr\": \"10.0.166.1\"}, {\"cidr\": \"10.0.167.1\"}, {\"cidr\": \"10.0.168.1\"}, {\"cidr\": \"10.0.169.1\"}, {\"cidr\": \"10.0.170.1\"}, {\"cidr\": \"10.0.171.1\"}, {\"cidr\": \"10.0.172.1\"}, {\"cidr\": \"10.0.173.1\"}, {\"cidr\": \"10.0.174.1\"}, {\"cidr\": \"10.0.175.1\"}, {\"cidr\": \"10.0.176.1\"}, {\"cidr\": \"10.0.177.1\"}, {\"cidr\": \"10.0.178.1\"}, {\"cidr\": \"10.0.179.1\"}, {\"cidr\": \"10.0.180.1\"}, {\"cidr\": \"10.0.181.1\"}, {\"cidr\": \"10.0.182.1\"}, {\"cidr\": \"10.0.183.1\"}, {\"cidr\": \"10.0.184.1\"}, {\"cidr\": \"10.0.185.1\"}, {\"cidr\": \"10.0.186.1\"}, {\"cidr\": \"10.0.187.1\"}, {\"cidr\": \"10.0.188.1\"}, {\"cidr\": \"10.0.189.1\"}, {\"cidr\": \"10.0.190.1\"}, {\"cidr\": \"10.0.191.1\"}, {\"cidr\": \"10.0.192.1\"}, {\"cidr\": \"10.0.193.1\"}, {\"cidr\": \"10.0.194.1\"}, {\"cidr\": \"10.0.195.1\"}, {\"cidr\": \"10.0.196.1\"}, {\"cidr\": \"10.0.197.1\"}, {\"cidr\": \"10.0.198.1\"}, {\"cidr\": \"10.0.199.1\"}, {\"cidr\": \"10.0.200.1\"}, {\"cidr\": \"10.0.201.1\"}, {\"cidr\": \"10.0.202.1\"}, {\"cidr\": \"10.0.203.1\"}, {\"cidr\": \"10.0.204.1\"}, {\"cidr\": \"10.0.205.1\"}, {\"cidr\": \"10.0.206.1\"}, {\"cidr\": \"10.0.207.1\"}, {\"cidr\": \"10.0.208.1\"}, {\"cidr\": \"10.0.209.1\"}, {\"cidr\": \"10.0.210.1\"}, {\"cidr\": \"10.0.211.1\"}, {\"cidr\": \"10.0.212.1\"}, {\"cidr\": \"10.0.213.1\"}, {\"cidr\": \"10.0.214.1\"}, {\"cidr\": \"10.0.215.1\"}, {\"cidr\": \"10.0.216.1\"}, {\"cidr\": \"10.0.217.1\"}, {\"cidr\": \"10.0.218.1\"}, {\"cidr\": \"10.0.219.1\"}, {\"cidr\": \"10.0.220.1\"}, {\"cidr\": \"10.0.221.1\"}, {\"cidr\": \"10.0.222.1\"}, {\"cidr\": \"10.0.223.1\"}, {\"cidr\": \"10.0.224.1\"}, {\"cidr\": \"10.0.225.1\"}, {\"cidr\": \"10.0.226.1\"}, {\"cidr\": \"10.0.227.1\"}, {\"cidr\": \"10.0.228.1\"}, {\"cidr\": \"10.0.229.1\"}, {\"cidr\": \"10.0.230.1\"}, {\"cidr\": \"10.0.231.1\"}, {\"cidr\": \"10.0.232.1\"}, {\"cidr\": \"10.0.233.1\"}, {\"cidr\": \"10.0.234.1\"}, {\"cidr\": \"10.0.235.1\"}, {\"cidr\": \"10.0.236.1\"}, {\"cidr\": \"10.0.237.1\"}, {\"cidr\": \"10.0.238.1\"}, {\"cidr\": \"10.0.239.1\"}, {\"cidr\": \"10.0.240.1\"}, {\"cidr\": \"10.0.241.1\"}, {\"cidr\": \"10.0.242.1\"}, {\"cidr\": \"10.0.243.1\"}, {\"cidr\": \"10.0.244.1\"}, {\"cidr\": \"10.0.245.1\"}, {\"cidr\": \"10.0.246.1\"}, {\"cidr\": \"10.0.247.1\"}, {\"cidr\": \"10.0.248.1\"}, {\"cidr\": \"10.0.249.1\"}, {\"cidr\": \"10.0.250.1\"}, {\"cidr\": \"10.0.251.1\"}, {\"cidr\": \"10.0.252.1\"}, {\"cidr\": \"10.0.253.1\"}, {\"cidr\": \"10.0.254.1\"}, {\"cidr\": \"10.0.255.1\"}, {\"cidr\": \"10.0.256.1\"}], \"deny\": [7]}"}],"expected":{"error":"allow[256].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.256.1"}}, + {"name":"cidr: 192.0.2.7","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7\"}]}"}],"expected":{"allow":["192.0.2.7/32"],"deny":[]}}, + {"name":"cidr: 192.0.2.7/32","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7/32\"}]}"}],"expected":{"allow":["192.0.2.7/32"],"deny":[]}}, + {"name":"cidr: 192.0.2.7/24","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.7/24\"}]}"}],"expected":{"allow":["192.0.2.0/24"],"deny":[]}}, + {"name":"cidr: 10.0.0.5/255.255.255.0","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/255.255.255.0\"}]}"}],"expected":{"allow":["10.0.0.0/24"],"deny":[]}}, + {"name":"cidr: 10.0.0.5/0.0.0.255","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/0.0.0.255\"}]}"}],"expected":{"allow":["10.0.0.0/24"],"deny":[]}}, + {"name":"cidr: 10.0.0.5/255.255.255.255","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/255.255.255.255\"}]}"}],"expected":{"allow":["10.0.0.5/32"],"deny":[]}}, + {"name":"cidr: 10.0.0.5/0.0.0.0","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/0.0.0.0\"}]}"}],"expected":{"allow":["0.0.0.0/0"],"deny":[]}}, + {"name":"cidr: 10.0.0.5/0","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/0\"}]}"}],"expected":{"allow":["0.0.0.0/0"],"deny":[]}}, + {"name":"cidr: 10.0.0.5/08","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/08\"}]}"}],"expected":{"allow":["10.0.0.0/8"],"deny":[]}}, + {"name":"cidr: 10.0.0.5/0008","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/0008\"}]}"}],"expected":{"allow":["10.0.0.0/8"],"deny":[]}}, + {"name":"cidr: 10.0.0.5/0000000000000000000000000000000","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/"},{"text":"0","repeat":4299},{"text":"8\"}]}"}],"expected":{"allow":["10.0.0.0/8"],"deny":[]}}, + {"name":"cidr: 10.0.0.5/0000000000000000000000000000000","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/"},{"text":"0","repeat":4300},{"text":"8\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008"}}, + {"name":"cidr: 10.0.0.5/255.0.255.0","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/255.0.255.0\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/255.0.255.0"}}, + {"name":"cidr: 10.0.0.5/255.255.255.000","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/255.255.255.000\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/255.255.255.000"}}, + {"name":"cidr: 10.0.0.5/+8","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/+8\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/+8"}}, + {"name":"cidr: 10.0.0.5/-8","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/-8\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/-8"}}, + {"name":"cidr: 10.0.0.5/8.0","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/8.0\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/8.0"}}, + {"name":"cidr: 10.0.0.5/0x8","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/0x8\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/0x8"}}, + {"name":"cidr: 10.0.0.5/","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/"}}, + {"name":"cidr: 10.0.0.5//8","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5//8\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5//8"}}, + {"name":"cidr: 10.0.0.5/8/8","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/8/8\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/8/8"}}, + {"name":"cidr: 10.0.0.5/33","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5/33\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5/33"}}, + {"name":"cidr: 10.0.0.5","input":[{"text":"{\"allow\": [{\"cidr\": \" 10.0.0.5\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5"}}, + {"name":"cidr: 10.0.0.5 ","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.5 \"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.5 "}}, + {"name":"cidr: 010.0.0.5","input":[{"text":"{\"allow\": [{\"cidr\": \"010.0.0.5\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 010.0.0.5"}}, + {"name":"cidr: 10.0.0.05","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.05\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.05"}}, + {"name":"cidr: 256.0.0.0","input":[{"text":"{\"allow\": [{\"cidr\": \"256.0.0.0\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 256.0.0.0"}}, + {"name":"cidr: 10.0.0","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0"}}, + {"name":"cidr: 10.0.0.0.5","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.0.5\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.0.5"}}, + {"name":"cidr: 10.0.0.\u0665","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.\\u0665\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 10.0.0.\u0665"}}, + {"name":"cidr: ","input":[{"text":"{\"allow\": [{\"cidr\": \"\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: "}}, + {"name":"cidr: /8","input":[{"text":"{\"allow\": [{\"cidr\": \"/8\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: /8"}}, + {"name":"cidr: ::","input":[{"text":"{\"allow\": [{\"cidr\": \"::\"}]}"}],"expected":{"allow":["::/128"],"deny":[]}}, + {"name":"cidr: ::/0","input":[{"text":"{\"allow\": [{\"cidr\": \"::/0\"}]}"}],"expected":{"allow":["::/0"],"deny":[]}}, + {"name":"cidr: ::/000128","input":[{"text":"{\"allow\": [{\"cidr\": \"::/000128\"}]}"}],"expected":{"allow":["::/128"],"deny":[]}}, + {"name":"cidr: ::/129","input":[{"text":"{\"allow\": [{\"cidr\": \"::/129\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: ::/129"}}, + {"name":"cidr: 2001:DB8::/32","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:DB8::/32\"}]}"}],"expected":{"allow":["2001:db8::/32"],"deny":[]}}, + {"name":"cidr: 2001:0db8:0000:0000::/32","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:0db8:0000:0000::/32\"}]}"}],"expected":{"allow":["2001:db8::/32"],"deny":[]}}, + {"name":"cidr: 2001:db8::7/64","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8::7/64\"}]}"}],"expected":{"allow":["2001:db8::/64"],"deny":[]}}, + {"name":"cidr: 2001:db8:0:0:1:0:0:1","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8:0:0:1:0:0:1\"}]}"}],"expected":{"allow":["2001:db8::1:0:0:1/128"],"deny":[]}}, + {"name":"cidr: 1:0:0:2:0:0:0:3","input":[{"text":"{\"allow\": [{\"cidr\": \"1:0:0:2:0:0:0:3\"}]}"}],"expected":{"allow":["1:0:0:2::3/128"],"deny":[]}}, + {"name":"cidr: fe80::1","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1\"}]}"}],"expected":{"allow":["fe80::1/128"],"deny":[]}}, + {"name":"cidr: fe80::1%eth0","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%eth0\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::1%eth0"}}, + {"name":"cidr: fe80::1%eth0/64","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%eth0/64\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::1%eth0/64"}}, + {"name":"cidr: fe80::%1/10","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::%1/10\"}]}"}],"expected":{"error":"allow[0].cidr must not name an IPv6 scope: fe80::%1/10"}}, + {"name":"cidr: fe80::1%","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: fe80::1%"}}, + {"name":"cidr: fe80::1%a%b","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%a%b\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: fe80::1%a%b"}}, + {"name":"cidr: fe80::1%eth0/129","input":[{"text":"{\"allow\": [{\"cidr\": \"fe80::1%eth0/129\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: fe80::1%eth0/129"}}, + {"name":"cidr: ::ffff:192.0.2.1","input":[{"text":"{\"allow\": [{\"cidr\": \"::ffff:192.0.2.1\"}]}"}],"expected":{"allow":["::ffff:192.0.2.1/128"],"deny":[]}}, + {"name":"cidr: ::ffff:192.0.2.1/120","input":[{"text":"{\"allow\": [{\"cidr\": \"::ffff:192.0.2.1/120\"}]}"}],"expected":{"allow":["::ffff:192.0.2.0/120"],"deny":[]}}, + {"name":"cidr: ::ffff:c000:201/120","input":[{"text":"{\"allow\": [{\"cidr\": \"::ffff:c000:201/120\"}]}"}],"expected":{"allow":["::ffff:192.0.2.0/120"],"deny":[]}}, + {"name":"cidr: ::FFFF:192.0.2.1/96","input":[{"text":"{\"allow\": [{\"cidr\": \"::FFFF:192.0.2.1/96\"}]}"}],"expected":{"allow":["::ffff:0.0.0.0/96"],"deny":[]}}, + {"name":"cidr: ::ffff:0:0/95","input":[{"text":"{\"allow\": [{\"cidr\": \"::ffff:0:0/95\"}]}"}],"expected":{"allow":["::fffe:0:0/95"],"deny":[]}}, + {"name":"cidr: ::192.0.2.1","input":[{"text":"{\"allow\": [{\"cidr\": \"::192.0.2.1\"}]}"}],"expected":{"allow":["::c000:201/128"],"deny":[]}}, + {"name":"cidr: ::c000:200/120","input":[{"text":"{\"allow\": [{\"cidr\": \"::c000:200/120\"}]}"}],"expected":{"allow":["::c000:200/120"],"deny":[]}}, + {"name":"cidr: 64:ff9b::192.0.2.1/120","input":[{"text":"{\"allow\": [{\"cidr\": \"64:ff9b::192.0.2.1/120\"}]}"}],"expected":{"allow":["64:ff9b::c000:200/120"],"deny":[]}}, + {"name":"cidr: 1:2:3:4:5:6:1.2.3.4","input":[{"text":"{\"allow\": [{\"cidr\": \"1:2:3:4:5:6:1.2.3.4\"}]}"}],"expected":{"allow":["1:2:3:4:5:6:102:304/128"],"deny":[]}}, + {"name":"cidr: 1:2:3:4:5::1.2.3.4","input":[{"text":"{\"allow\": [{\"cidr\": \"1:2:3:4:5::1.2.3.4\"}]}"}],"expected":{"allow":["1:2:3:4:5:0:102:304/128"],"deny":[]}}, + {"name":"cidr: 1:2:3:4:5:6:7:1.2.3.4","input":[{"text":"{\"allow\": [{\"cidr\": \"1:2:3:4:5:6:7:1.2.3.4\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 1:2:3:4:5:6:7:1.2.3.4"}}, + {"name":"cidr: 1:2:3:4:5:6::1.2.3.4","input":[{"text":"{\"allow\": [{\"cidr\": \"1:2:3:4:5:6::1.2.3.4\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 1:2:3:4:5:6::1.2.3.4"}}, + {"name":"cidr: ::1.2.3","input":[{"text":"{\"allow\": [{\"cidr\": \"::1.2.3\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: ::1.2.3"}}, + {"name":"cidr: ::ffff:1.2.3.04","input":[{"text":"{\"allow\": [{\"cidr\": \"::ffff:1.2.3.04\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: ::ffff:1.2.3.04"}}, + {"name":"cidr: 1:2:3:4:5:6:7:8","input":[{"text":"{\"allow\": [{\"cidr\": \"1:2:3:4:5:6:7:8\"}]}"}],"expected":{"allow":["1:2:3:4:5:6:7:8/128"],"deny":[]}}, + {"name":"cidr: 1:2:3:4:5:6:7::","input":[{"text":"{\"allow\": [{\"cidr\": \"1:2:3:4:5:6:7::\"}]}"}],"expected":{"allow":["1:2:3:4:5:6:7:0/128"],"deny":[]}}, + {"name":"cidr: ::2:3:4:5:6:7:8","input":[{"text":"{\"allow\": [{\"cidr\": \"::2:3:4:5:6:7:8\"}]}"}],"expected":{"allow":["0:2:3:4:5:6:7:8/128"],"deny":[]}}, + {"name":"cidr: 1:2:3:4:5:6:7:8:9","input":[{"text":"{\"allow\": [{\"cidr\": \"1:2:3:4:5:6:7:8:9\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 1:2:3:4:5:6:7:8:9"}}, + {"name":"cidr: ::1:2:3:4:5:6:7:8","input":[{"text":"{\"allow\": [{\"cidr\": \"::1:2:3:4:5:6:7:8\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: ::1:2:3:4:5:6:7:8"}}, + {"name":"cidr: 1::2::3","input":[{"text":"{\"allow\": [{\"cidr\": \"1::2::3\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 1::2::3"}}, + {"name":"cidr: :1::2","input":[{"text":"{\"allow\": [{\"cidr\": \":1::2\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: :1::2"}}, + {"name":"cidr: 1::2:","input":[{"text":"{\"allow\": [{\"cidr\": \"1::2:\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 1::2:"}}, + {"name":"cidr: :::","input":[{"text":"{\"allow\": [{\"cidr\": \":::\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: :::"}}, + {"name":"cidr: 00000::","input":[{"text":"{\"allow\": [{\"cidr\": \"00000::\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 00000::"}}, + {"name":"cidr: 0000::","input":[{"text":"{\"allow\": [{\"cidr\": \"0000::\"}]}"}],"expected":{"allow":["::/128"],"deny":[]}}, + {"name":"cidr: [::1]","input":[{"text":"{\"allow\": [{\"cidr\": \"[::1]\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: [::1]"}}, + {"name":"cidr: ::1/0x8","input":[{"text":"{\"allow\": [{\"cidr\": \"::1/0x8\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: ::1/0x8"}}, + {"name":"cidr: 2001:db8::g","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8::g\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 2001:db8::g"}}, + {"name":"cidr: 2001:db8::\uff11","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8::\\uff11\"}]}"}],"expected":{"error":"allow[0].cidr is not a valid IPv4 or IPv6 CIDR: 2001:db8::\uff11"}}, + {"name":"exception: 10.0.0.0/8","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"10.0.0.0/8\"]}]}]}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"exception: 10.1.2.3/16","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"10.1.2.3/16\"]}]}]}"}],"expected":{"allow":[],"deny":["10.0.0.0/16","10.2.0.0/15","10.4.0.0/14","10.8.0.0/13","10.16.0.0/12","10.32.0.0/11","10.64.0.0/10","10.128.0.0/9"]}}, + {"name":"exception: 10.255.255.255","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"10.255.255.255\"]}]}]}"}],"expected":{"allow":[],"deny":["10.0.0.0/9","10.128.0.0/10","10.192.0.0/11","10.224.0.0/12","10.240.0.0/13","10.248.0.0/14","10.252.0.0/15","10.254.0.0/16","10.255.0.0/17","10.255.128.0/18","10.255.192.0/19","10.255.224.0/20","10.255.240.0/21","10.255.248.0/22","10.255.252.0/23","10.255.254.0/24","10.255.255.0/25","10.255.255.128/26","10.255.255.192/27","10.255.255.224/28","10.255.255.240/29","10.255.255.248/30","10.255.255.252/31","10.255.255.254/32"]}}, + {"name":"exception: 10.0.0.0/255.255.0.0","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"10.0.0.0/255.255.0.0\"]}]}]}"}],"expected":{"allow":[],"deny":["10.1.0.0/16","10.2.0.0/15","10.4.0.0/14","10.8.0.0/13","10.16.0.0/12","10.32.0.0/11","10.64.0.0/10","10.128.0.0/9"]}}, + {"name":"exception: 10.0.0.0/7","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"10.0.0.0/7\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must be contained in 10.0.0.0/8"}}, + {"name":"exception: 11.0.0.0/8","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"11.0.0.0/8\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must be contained in 10.0.0.0/8"}}, + {"name":"exception: 0.0.0.0/0","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"0.0.0.0/0\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must be contained in 10.0.0.0/8"}}, + {"name":"exception: ::/96","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"::/96\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must be contained in 10.0.0.0/8"}}, + {"name":"exception: ::ffff:10.0.0.0/104","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"::ffff:10.0.0.0/104\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must be contained in 10.0.0.0/8"}}, + {"name":"exception: fe80::1%eth0","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"fe80::1%eth0\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] must not name an IPv6 scope: fe80::1%eth0"}}, + {"name":"exception: 10.0.0.0/+16","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/8\", \"except\": [\"10.0.0.0/+16\"]}]}]}"}],"expected":{"error":"deny[0].to[0].except[0] is not a valid IPv4 or IPv6 CIDR: 10.0.0.0/+16"}}, + {"name":"limit: address-only at the limit","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.1\"}, {\"cidr\": \"10.0.1.1\"}, {\"cidr\": \"10.0.2.1\"}, {\"cidr\": \"10.0.3.1\"}, {\"cidr\": \"10.0.4.1\"}, {\"cidr\": \"10.0.5.1\"}, {\"cidr\": \"10.0.6.1\"}, {\"cidr\": \"10.0.7.1\"}, {\"cidr\": \"10.0.8.1\"}, {\"cidr\": \"10.0.9.1\"}, {\"cidr\": \"10.0.10.1\"}, {\"cidr\": \"10.0.11.1\"}, {\"cidr\": \"10.0.12.1\"}, {\"cidr\": \"10.0.13.1\"}, {\"cidr\": \"10.0.14.1\"}, {\"cidr\": \"10.0.15.1\"}, {\"cidr\": \"10.0.16.1\"}, {\"cidr\": \"10.0.17.1\"}, {\"cidr\": \"10.0.18.1\"}, {\"cidr\": \"10.0.19.1\"}, {\"cidr\": \"10.0.20.1\"}, {\"cidr\": \"10.0.21.1\"}, {\"cidr\": \"10.0.22.1\"}, {\"cidr\": \"10.0.23.1\"}, {\"cidr\": \"10.0.24.1\"}, {\"cidr\": \"10.0.25.1\"}, {\"cidr\": \"10.0.26.1\"}, {\"cidr\": \"10.0.27.1\"}, {\"cidr\": \"10.0.28.1\"}, {\"cidr\": \"10.0.29.1\"}, {\"cidr\": \"10.0.30.1\"}, {\"cidr\": \"10.0.31.1\"}, {\"cidr\": \"10.0.32.1\"}, {\"cidr\": \"10.0.33.1\"}, {\"cidr\": \"10.0.34.1\"}, {\"cidr\": \"10.0.35.1\"}, {\"cidr\": \"10.0.36.1\"}, {\"cidr\": \"10.0.37.1\"}, {\"cidr\": \"10.0.38.1\"}, {\"cidr\": \"10.0.39.1\"}, {\"cidr\": \"10.0.40.1\"}, {\"cidr\": \"10.0.41.1\"}, {\"cidr\": \"10.0.42.1\"}, {\"cidr\": \"10.0.43.1\"}, {\"cidr\": \"10.0.44.1\"}, {\"cidr\": \"10.0.45.1\"}, {\"cidr\": \"10.0.46.1\"}, {\"cidr\": \"10.0.47.1\"}, {\"cidr\": \"10.0.48.1\"}, {\"cidr\": \"10.0.49.1\"}, {\"cidr\": \"10.0.50.1\"}, {\"cidr\": \"10.0.51.1\"}, {\"cidr\": \"10.0.52.1\"}, {\"cidr\": \"10.0.53.1\"}, {\"cidr\": \"10.0.54.1\"}, {\"cidr\": \"10.0.55.1\"}, {\"cidr\": \"10.0.56.1\"}, {\"cidr\": \"10.0.57.1\"}, {\"cidr\": \"10.0.58.1\"}, {\"cidr\": \"10.0.59.1\"}, {\"cidr\": \"10.0.60.1\"}, {\"cidr\": \"10.0.61.1\"}, {\"cidr\": \"10.0.62.1\"}, {\"cidr\": \"10.0.63.1\"}, {\"cidr\": \"10.0.64.1\"}, {\"cidr\": \"10.0.65.1\"}, {\"cidr\": \"10.0.66.1\"}, {\"cidr\": \"10.0.67.1\"}, {\"cidr\": \"10.0.68.1\"}, {\"cidr\": \"10.0.69.1\"}, {\"cidr\": \"10.0.70.1\"}, {\"cidr\": \"10.0.71.1\"}, {\"cidr\": \"10.0.72.1\"}, {\"cidr\": \"10.0.73.1\"}, {\"cidr\": \"10.0.74.1\"}, {\"cidr\": \"10.0.75.1\"}, {\"cidr\": \"10.0.76.1\"}, {\"cidr\": \"10.0.77.1\"}, {\"cidr\": \"10.0.78.1\"}, {\"cidr\": \"10.0.79.1\"}, {\"cidr\": \"10.0.80.1\"}, {\"cidr\": \"10.0.81.1\"}, {\"cidr\": \"10.0.82.1\"}, {\"cidr\": \"10.0.83.1\"}, {\"cidr\": \"10.0.84.1\"}, {\"cidr\": \"10.0.85.1\"}, {\"cidr\": \"10.0.86.1\"}, {\"cidr\": \"10.0.87.1\"}, {\"cidr\": \"10.0.88.1\"}, {\"cidr\": \"10.0.89.1\"}, {\"cidr\": \"10.0.90.1\"}, {\"cidr\": \"10.0.91.1\"}, {\"cidr\": \"10.0.92.1\"}, {\"cidr\": \"10.0.93.1\"}, {\"cidr\": \"10.0.94.1\"}, {\"cidr\": \"10.0.95.1\"}, {\"cidr\": \"10.0.96.1\"}, {\"cidr\": \"10.0.97.1\"}, {\"cidr\": \"10.0.98.1\"}, {\"cidr\": \"10.0.99.1\"}, {\"cidr\": \"10.0.100.1\"}, {\"cidr\": \"10.0.101.1\"}, {\"cidr\": \"10.0.102.1\"}, {\"cidr\": \"10.0.103.1\"}, {\"cidr\": \"10.0.104.1\"}, {\"cidr\": \"10.0.105.1\"}, {\"cidr\": \"10.0.106.1\"}, {\"cidr\": \"10.0.107.1\"}, {\"cidr\": \"10.0.108.1\"}, {\"cidr\": \"10.0.109.1\"}, {\"cidr\": \"10.0.110.1\"}, {\"cidr\": \"10.0.111.1\"}, {\"cidr\": \"10.0.112.1\"}, {\"cidr\": \"10.0.113.1\"}, {\"cidr\": \"10.0.114.1\"}, {\"cidr\": \"10.0.115.1\"}, {\"cidr\": \"10.0.116.1\"}, {\"cidr\": \"10.0.117.1\"}, {\"cidr\": \"10.0.118.1\"}, {\"cidr\": \"10.0.119.1\"}, {\"cidr\": \"10.0.120.1\"}, {\"cidr\": \"10.0.121.1\"}, {\"cidr\": \"10.0.122.1\"}, {\"cidr\": \"10.0.123.1\"}, {\"cidr\": \"10.0.124.1\"}, {\"cidr\": \"10.0.125.1\"}, {\"cidr\": \"10.0.126.1\"}, {\"cidr\": \"10.0.127.1\"}, {\"cidr\": \"10.0.128.1\"}, {\"cidr\": \"10.0.129.1\"}, {\"cidr\": \"10.0.130.1\"}, {\"cidr\": \"10.0.131.1\"}, {\"cidr\": \"10.0.132.1\"}, {\"cidr\": \"10.0.133.1\"}, {\"cidr\": \"10.0.134.1\"}, {\"cidr\": \"10.0.135.1\"}, {\"cidr\": \"10.0.136.1\"}, {\"cidr\": \"10.0.137.1\"}, {\"cidr\": \"10.0.138.1\"}, {\"cidr\": \"10.0.139.1\"}, {\"cidr\": \"10.0.140.1\"}, {\"cidr\": \"10.0.141.1\"}, {\"cidr\": \"10.0.142.1\"}, {\"cidr\": \"10.0.143.1\"}, {\"cidr\": \"10.0.144.1\"}, {\"cidr\": \"10.0.145.1\"}, {\"cidr\": \"10.0.146.1\"}, {\"cidr\": \"10.0.147.1\"}, {\"cidr\": \"10.0.148.1\"}, {\"cidr\": \"10.0.149.1\"}, {\"cidr\": \"10.0.150.1\"}, {\"cidr\": \"10.0.151.1\"}, {\"cidr\": \"10.0.152.1\"}, {\"cidr\": \"10.0.153.1\"}, {\"cidr\": \"10.0.154.1\"}, {\"cidr\": \"10.0.155.1\"}, {\"cidr\": \"10.0.156.1\"}, {\"cidr\": \"10.0.157.1\"}, {\"cidr\": \"10.0.158.1\"}, {\"cidr\": \"10.0.159.1\"}, {\"cidr\": \"10.0.160.1\"}, {\"cidr\": \"10.0.161.1\"}, {\"cidr\": \"10.0.162.1\"}, {\"cidr\": \"10.0.163.1\"}, {\"cidr\": \"10.0.164.1\"}, {\"cidr\": \"10.0.165.1\"}, {\"cidr\": \"10.0.166.1\"}, {\"cidr\": \"10.0.167.1\"}, {\"cidr\": \"10.0.168.1\"}, {\"cidr\": \"10.0.169.1\"}, {\"cidr\": \"10.0.170.1\"}, {\"cidr\": \"10.0.171.1\"}, {\"cidr\": \"10.0.172.1\"}, {\"cidr\": \"10.0.173.1\"}, {\"cidr\": \"10.0.174.1\"}, {\"cidr\": \"10.0.175.1\"}, {\"cidr\": \"10.0.176.1\"}, {\"cidr\": \"10.0.177.1\"}, {\"cidr\": \"10.0.178.1\"}, {\"cidr\": \"10.0.179.1\"}, {\"cidr\": \"10.0.180.1\"}, {\"cidr\": \"10.0.181.1\"}, {\"cidr\": \"10.0.182.1\"}, {\"cidr\": \"10.0.183.1\"}, {\"cidr\": \"10.0.184.1\"}, {\"cidr\": \"10.0.185.1\"}, {\"cidr\": \"10.0.186.1\"}, {\"cidr\": \"10.0.187.1\"}, {\"cidr\": \"10.0.188.1\"}, {\"cidr\": \"10.0.189.1\"}, {\"cidr\": \"10.0.190.1\"}, {\"cidr\": \"10.0.191.1\"}, {\"cidr\": \"10.0.192.1\"}, {\"cidr\": \"10.0.193.1\"}, {\"cidr\": \"10.0.194.1\"}, {\"cidr\": \"10.0.195.1\"}, {\"cidr\": \"10.0.196.1\"}, {\"cidr\": \"10.0.197.1\"}, {\"cidr\": \"10.0.198.1\"}, {\"cidr\": \"10.0.199.1\"}, {\"cidr\": \"10.0.200.1\"}, {\"cidr\": \"10.0.201.1\"}, {\"cidr\": \"10.0.202.1\"}, {\"cidr\": \"10.0.203.1\"}, {\"cidr\": \"10.0.204.1\"}, {\"cidr\": \"10.0.205.1\"}, {\"cidr\": \"10.0.206.1\"}, {\"cidr\": \"10.0.207.1\"}, {\"cidr\": \"10.0.208.1\"}, {\"cidr\": \"10.0.209.1\"}, {\"cidr\": \"10.0.210.1\"}, {\"cidr\": \"10.0.211.1\"}, {\"cidr\": \"10.0.212.1\"}, {\"cidr\": \"10.0.213.1\"}, {\"cidr\": \"10.0.214.1\"}, {\"cidr\": \"10.0.215.1\"}, {\"cidr\": \"10.0.216.1\"}, {\"cidr\": \"10.0.217.1\"}, {\"cidr\": \"10.0.218.1\"}, {\"cidr\": \"10.0.219.1\"}, {\"cidr\": \"10.0.220.1\"}, {\"cidr\": \"10.0.221.1\"}, {\"cidr\": \"10.0.222.1\"}, {\"cidr\": \"10.0.223.1\"}, {\"cidr\": \"10.0.224.1\"}, {\"cidr\": \"10.0.225.1\"}, {\"cidr\": \"10.0.226.1\"}, {\"cidr\": \"10.0.227.1\"}, {\"cidr\": \"10.0.228.1\"}, {\"cidr\": \"10.0.229.1\"}, {\"cidr\": \"10.0.230.1\"}, {\"cidr\": \"10.0.231.1\"}, {\"cidr\": \"10.0.232.1\"}, {\"cidr\": \"10.0.233.1\"}, {\"cidr\": \"10.0.234.1\"}, {\"cidr\": \"10.0.235.1\"}, {\"cidr\": \"10.0.236.1\"}, {\"cidr\": \"10.0.237.1\"}, {\"cidr\": \"10.0.238.1\"}, {\"cidr\": \"10.0.239.1\"}, {\"cidr\": \"10.0.240.1\"}, {\"cidr\": \"10.0.241.1\"}, {\"cidr\": \"10.0.242.1\"}, {\"cidr\": \"10.0.243.1\"}, {\"cidr\": \"10.0.244.1\"}, {\"cidr\": \"10.0.245.1\"}, {\"cidr\": \"10.0.246.1\"}, {\"cidr\": \"10.0.247.1\"}, {\"cidr\": \"10.0.248.1\"}, {\"cidr\": \"10.0.249.1\"}, {\"cidr\": \"10.0.250.1\"}, {\"cidr\": \"10.0.251.1\"}, {\"cidr\": \"10.0.252.1\"}, {\"cidr\": \"10.0.253.1\"}, {\"cidr\": \"10.0.254.1\"}, {\"cidr\": \"10.0.255.1\"}]}"}],"expected":{"allow":["10.0.0.1/32","10.0.1.1/32","10.0.2.1/32","10.0.3.1/32","10.0.4.1/32","10.0.5.1/32","10.0.6.1/32","10.0.7.1/32","10.0.8.1/32","10.0.9.1/32","10.0.10.1/32","10.0.11.1/32","10.0.12.1/32","10.0.13.1/32","10.0.14.1/32","10.0.15.1/32","10.0.16.1/32","10.0.17.1/32","10.0.18.1/32","10.0.19.1/32","10.0.20.1/32","10.0.21.1/32","10.0.22.1/32","10.0.23.1/32","10.0.24.1/32","10.0.25.1/32","10.0.26.1/32","10.0.27.1/32","10.0.28.1/32","10.0.29.1/32","10.0.30.1/32","10.0.31.1/32","10.0.32.1/32","10.0.33.1/32","10.0.34.1/32","10.0.35.1/32","10.0.36.1/32","10.0.37.1/32","10.0.38.1/32","10.0.39.1/32","10.0.40.1/32","10.0.41.1/32","10.0.42.1/32","10.0.43.1/32","10.0.44.1/32","10.0.45.1/32","10.0.46.1/32","10.0.47.1/32","10.0.48.1/32","10.0.49.1/32","10.0.50.1/32","10.0.51.1/32","10.0.52.1/32","10.0.53.1/32","10.0.54.1/32","10.0.55.1/32","10.0.56.1/32","10.0.57.1/32","10.0.58.1/32","10.0.59.1/32","10.0.60.1/32","10.0.61.1/32","10.0.62.1/32","10.0.63.1/32","10.0.64.1/32","10.0.65.1/32","10.0.66.1/32","10.0.67.1/32","10.0.68.1/32","10.0.69.1/32","10.0.70.1/32","10.0.71.1/32","10.0.72.1/32","10.0.73.1/32","10.0.74.1/32","10.0.75.1/32","10.0.76.1/32","10.0.77.1/32","10.0.78.1/32","10.0.79.1/32","10.0.80.1/32","10.0.81.1/32","10.0.82.1/32","10.0.83.1/32","10.0.84.1/32","10.0.85.1/32","10.0.86.1/32","10.0.87.1/32","10.0.88.1/32","10.0.89.1/32","10.0.90.1/32","10.0.91.1/32","10.0.92.1/32","10.0.93.1/32","10.0.94.1/32","10.0.95.1/32","10.0.96.1/32","10.0.97.1/32","10.0.98.1/32","10.0.99.1/32","10.0.100.1/32","10.0.101.1/32","10.0.102.1/32","10.0.103.1/32","10.0.104.1/32","10.0.105.1/32","10.0.106.1/32","10.0.107.1/32","10.0.108.1/32","10.0.109.1/32","10.0.110.1/32","10.0.111.1/32","10.0.112.1/32","10.0.113.1/32","10.0.114.1/32","10.0.115.1/32","10.0.116.1/32","10.0.117.1/32","10.0.118.1/32","10.0.119.1/32","10.0.120.1/32","10.0.121.1/32","10.0.122.1/32","10.0.123.1/32","10.0.124.1/32","10.0.125.1/32","10.0.126.1/32","10.0.127.1/32","10.0.128.1/32","10.0.129.1/32","10.0.130.1/32","10.0.131.1/32","10.0.132.1/32","10.0.133.1/32","10.0.134.1/32","10.0.135.1/32","10.0.136.1/32","10.0.137.1/32","10.0.138.1/32","10.0.139.1/32","10.0.140.1/32","10.0.141.1/32","10.0.142.1/32","10.0.143.1/32","10.0.144.1/32","10.0.145.1/32","10.0.146.1/32","10.0.147.1/32","10.0.148.1/32","10.0.149.1/32","10.0.150.1/32","10.0.151.1/32","10.0.152.1/32","10.0.153.1/32","10.0.154.1/32","10.0.155.1/32","10.0.156.1/32","10.0.157.1/32","10.0.158.1/32","10.0.159.1/32","10.0.160.1/32","10.0.161.1/32","10.0.162.1/32","10.0.163.1/32","10.0.164.1/32","10.0.165.1/32","10.0.166.1/32","10.0.167.1/32","10.0.168.1/32","10.0.169.1/32","10.0.170.1/32","10.0.171.1/32","10.0.172.1/32","10.0.173.1/32","10.0.174.1/32","10.0.175.1/32","10.0.176.1/32","10.0.177.1/32","10.0.178.1/32","10.0.179.1/32","10.0.180.1/32","10.0.181.1/32","10.0.182.1/32","10.0.183.1/32","10.0.184.1/32","10.0.185.1/32","10.0.186.1/32","10.0.187.1/32","10.0.188.1/32","10.0.189.1/32","10.0.190.1/32","10.0.191.1/32","10.0.192.1/32","10.0.193.1/32","10.0.194.1/32","10.0.195.1/32","10.0.196.1/32","10.0.197.1/32","10.0.198.1/32","10.0.199.1/32","10.0.200.1/32","10.0.201.1/32","10.0.202.1/32","10.0.203.1/32","10.0.204.1/32","10.0.205.1/32","10.0.206.1/32","10.0.207.1/32","10.0.208.1/32","10.0.209.1/32","10.0.210.1/32","10.0.211.1/32","10.0.212.1/32","10.0.213.1/32","10.0.214.1/32","10.0.215.1/32","10.0.216.1/32","10.0.217.1/32","10.0.218.1/32","10.0.219.1/32","10.0.220.1/32","10.0.221.1/32","10.0.222.1/32","10.0.223.1/32","10.0.224.1/32","10.0.225.1/32","10.0.226.1/32","10.0.227.1/32","10.0.228.1/32","10.0.229.1/32","10.0.230.1/32","10.0.231.1/32","10.0.232.1/32","10.0.233.1/32","10.0.234.1/32","10.0.235.1/32","10.0.236.1/32","10.0.237.1/32","10.0.238.1/32","10.0.239.1/32","10.0.240.1/32","10.0.241.1/32","10.0.242.1/32","10.0.243.1/32","10.0.244.1/32","10.0.245.1/32","10.0.246.1/32","10.0.247.1/32","10.0.248.1/32","10.0.249.1/32","10.0.250.1/32","10.0.251.1/32","10.0.252.1/32","10.0.253.1/32","10.0.254.1/32","10.0.255.1/32"],"deny":[]}}, + {"name":"limit: address-only over the limit","input":[{"text":"{\"deny\": [{\"cidr\": \"10.0.0.1\"}, {\"cidr\": \"10.0.1.1\"}, {\"cidr\": \"10.0.2.1\"}, {\"cidr\": \"10.0.3.1\"}, {\"cidr\": \"10.0.4.1\"}, {\"cidr\": \"10.0.5.1\"}, {\"cidr\": \"10.0.6.1\"}, {\"cidr\": \"10.0.7.1\"}, {\"cidr\": \"10.0.8.1\"}, {\"cidr\": \"10.0.9.1\"}, {\"cidr\": \"10.0.10.1\"}, {\"cidr\": \"10.0.11.1\"}, {\"cidr\": \"10.0.12.1\"}, {\"cidr\": \"10.0.13.1\"}, {\"cidr\": \"10.0.14.1\"}, {\"cidr\": \"10.0.15.1\"}, {\"cidr\": \"10.0.16.1\"}, {\"cidr\": \"10.0.17.1\"}, {\"cidr\": \"10.0.18.1\"}, {\"cidr\": \"10.0.19.1\"}, {\"cidr\": \"10.0.20.1\"}, {\"cidr\": \"10.0.21.1\"}, {\"cidr\": \"10.0.22.1\"}, {\"cidr\": \"10.0.23.1\"}, {\"cidr\": \"10.0.24.1\"}, {\"cidr\": \"10.0.25.1\"}, {\"cidr\": \"10.0.26.1\"}, {\"cidr\": \"10.0.27.1\"}, {\"cidr\": \"10.0.28.1\"}, {\"cidr\": \"10.0.29.1\"}, {\"cidr\": \"10.0.30.1\"}, {\"cidr\": \"10.0.31.1\"}, {\"cidr\": \"10.0.32.1\"}, {\"cidr\": \"10.0.33.1\"}, {\"cidr\": \"10.0.34.1\"}, {\"cidr\": \"10.0.35.1\"}, {\"cidr\": \"10.0.36.1\"}, {\"cidr\": \"10.0.37.1\"}, {\"cidr\": \"10.0.38.1\"}, {\"cidr\": \"10.0.39.1\"}, {\"cidr\": \"10.0.40.1\"}, {\"cidr\": \"10.0.41.1\"}, {\"cidr\": \"10.0.42.1\"}, {\"cidr\": \"10.0.43.1\"}, {\"cidr\": \"10.0.44.1\"}, {\"cidr\": \"10.0.45.1\"}, {\"cidr\": \"10.0.46.1\"}, {\"cidr\": \"10.0.47.1\"}, {\"cidr\": \"10.0.48.1\"}, {\"cidr\": \"10.0.49.1\"}, {\"cidr\": \"10.0.50.1\"}, {\"cidr\": \"10.0.51.1\"}, {\"cidr\": \"10.0.52.1\"}, {\"cidr\": \"10.0.53.1\"}, {\"cidr\": \"10.0.54.1\"}, {\"cidr\": \"10.0.55.1\"}, {\"cidr\": \"10.0.56.1\"}, {\"cidr\": \"10.0.57.1\"}, {\"cidr\": \"10.0.58.1\"}, {\"cidr\": \"10.0.59.1\"}, {\"cidr\": \"10.0.60.1\"}, {\"cidr\": \"10.0.61.1\"}, {\"cidr\": \"10.0.62.1\"}, {\"cidr\": \"10.0.63.1\"}, {\"cidr\": \"10.0.64.1\"}, {\"cidr\": \"10.0.65.1\"}, {\"cidr\": \"10.0.66.1\"}, {\"cidr\": \"10.0.67.1\"}, {\"cidr\": \"10.0.68.1\"}, {\"cidr\": \"10.0.69.1\"}, {\"cidr\": \"10.0.70.1\"}, {\"cidr\": \"10.0.71.1\"}, {\"cidr\": \"10.0.72.1\"}, {\"cidr\": \"10.0.73.1\"}, {\"cidr\": \"10.0.74.1\"}, {\"cidr\": \"10.0.75.1\"}, {\"cidr\": \"10.0.76.1\"}, {\"cidr\": \"10.0.77.1\"}, {\"cidr\": \"10.0.78.1\"}, {\"cidr\": \"10.0.79.1\"}, {\"cidr\": \"10.0.80.1\"}, {\"cidr\": \"10.0.81.1\"}, {\"cidr\": \"10.0.82.1\"}, {\"cidr\": \"10.0.83.1\"}, {\"cidr\": \"10.0.84.1\"}, {\"cidr\": \"10.0.85.1\"}, {\"cidr\": \"10.0.86.1\"}, {\"cidr\": \"10.0.87.1\"}, {\"cidr\": \"10.0.88.1\"}, {\"cidr\": \"10.0.89.1\"}, {\"cidr\": \"10.0.90.1\"}, {\"cidr\": \"10.0.91.1\"}, {\"cidr\": \"10.0.92.1\"}, {\"cidr\": \"10.0.93.1\"}, {\"cidr\": \"10.0.94.1\"}, {\"cidr\": \"10.0.95.1\"}, {\"cidr\": \"10.0.96.1\"}, {\"cidr\": \"10.0.97.1\"}, {\"cidr\": \"10.0.98.1\"}, {\"cidr\": \"10.0.99.1\"}, {\"cidr\": \"10.0.100.1\"}, {\"cidr\": \"10.0.101.1\"}, {\"cidr\": \"10.0.102.1\"}, {\"cidr\": \"10.0.103.1\"}, {\"cidr\": \"10.0.104.1\"}, {\"cidr\": \"10.0.105.1\"}, {\"cidr\": \"10.0.106.1\"}, {\"cidr\": \"10.0.107.1\"}, {\"cidr\": \"10.0.108.1\"}, {\"cidr\": \"10.0.109.1\"}, {\"cidr\": \"10.0.110.1\"}, {\"cidr\": \"10.0.111.1\"}, {\"cidr\": \"10.0.112.1\"}, {\"cidr\": \"10.0.113.1\"}, {\"cidr\": \"10.0.114.1\"}, {\"cidr\": \"10.0.115.1\"}, {\"cidr\": \"10.0.116.1\"}, {\"cidr\": \"10.0.117.1\"}, {\"cidr\": \"10.0.118.1\"}, {\"cidr\": \"10.0.119.1\"}, {\"cidr\": \"10.0.120.1\"}, {\"cidr\": \"10.0.121.1\"}, {\"cidr\": \"10.0.122.1\"}, {\"cidr\": \"10.0.123.1\"}, {\"cidr\": \"10.0.124.1\"}, {\"cidr\": \"10.0.125.1\"}, {\"cidr\": \"10.0.126.1\"}, {\"cidr\": \"10.0.127.1\"}, {\"cidr\": \"10.0.128.1\"}, {\"cidr\": \"10.0.129.1\"}, {\"cidr\": \"10.0.130.1\"}, {\"cidr\": \"10.0.131.1\"}, {\"cidr\": \"10.0.132.1\"}, {\"cidr\": \"10.0.133.1\"}, {\"cidr\": \"10.0.134.1\"}, {\"cidr\": \"10.0.135.1\"}, {\"cidr\": \"10.0.136.1\"}, {\"cidr\": \"10.0.137.1\"}, {\"cidr\": \"10.0.138.1\"}, {\"cidr\": \"10.0.139.1\"}, {\"cidr\": \"10.0.140.1\"}, {\"cidr\": \"10.0.141.1\"}, {\"cidr\": \"10.0.142.1\"}, {\"cidr\": \"10.0.143.1\"}, {\"cidr\": \"10.0.144.1\"}, {\"cidr\": \"10.0.145.1\"}, {\"cidr\": \"10.0.146.1\"}, {\"cidr\": \"10.0.147.1\"}, {\"cidr\": \"10.0.148.1\"}, {\"cidr\": \"10.0.149.1\"}, {\"cidr\": \"10.0.150.1\"}, {\"cidr\": \"10.0.151.1\"}, {\"cidr\": \"10.0.152.1\"}, {\"cidr\": \"10.0.153.1\"}, {\"cidr\": \"10.0.154.1\"}, {\"cidr\": \"10.0.155.1\"}, {\"cidr\": \"10.0.156.1\"}, {\"cidr\": \"10.0.157.1\"}, {\"cidr\": \"10.0.158.1\"}, {\"cidr\": \"10.0.159.1\"}, {\"cidr\": \"10.0.160.1\"}, {\"cidr\": \"10.0.161.1\"}, {\"cidr\": \"10.0.162.1\"}, {\"cidr\": \"10.0.163.1\"}, {\"cidr\": \"10.0.164.1\"}, {\"cidr\": \"10.0.165.1\"}, {\"cidr\": \"10.0.166.1\"}, {\"cidr\": \"10.0.167.1\"}, {\"cidr\": \"10.0.168.1\"}, {\"cidr\": \"10.0.169.1\"}, {\"cidr\": \"10.0.170.1\"}, {\"cidr\": \"10.0.171.1\"}, {\"cidr\": \"10.0.172.1\"}, {\"cidr\": \"10.0.173.1\"}, {\"cidr\": \"10.0.174.1\"}, {\"cidr\": \"10.0.175.1\"}, {\"cidr\": \"10.0.176.1\"}, {\"cidr\": \"10.0.177.1\"}, {\"cidr\": \"10.0.178.1\"}, {\"cidr\": \"10.0.179.1\"}, {\"cidr\": \"10.0.180.1\"}, {\"cidr\": \"10.0.181.1\"}, {\"cidr\": \"10.0.182.1\"}, {\"cidr\": \"10.0.183.1\"}, {\"cidr\": \"10.0.184.1\"}, {\"cidr\": \"10.0.185.1\"}, {\"cidr\": \"10.0.186.1\"}, {\"cidr\": \"10.0.187.1\"}, {\"cidr\": \"10.0.188.1\"}, {\"cidr\": \"10.0.189.1\"}, {\"cidr\": \"10.0.190.1\"}, {\"cidr\": \"10.0.191.1\"}, {\"cidr\": \"10.0.192.1\"}, {\"cidr\": \"10.0.193.1\"}, {\"cidr\": \"10.0.194.1\"}, {\"cidr\": \"10.0.195.1\"}, {\"cidr\": \"10.0.196.1\"}, {\"cidr\": \"10.0.197.1\"}, {\"cidr\": \"10.0.198.1\"}, {\"cidr\": \"10.0.199.1\"}, {\"cidr\": \"10.0.200.1\"}, {\"cidr\": \"10.0.201.1\"}, {\"cidr\": \"10.0.202.1\"}, {\"cidr\": \"10.0.203.1\"}, {\"cidr\": \"10.0.204.1\"}, {\"cidr\": \"10.0.205.1\"}, {\"cidr\": \"10.0.206.1\"}, {\"cidr\": \"10.0.207.1\"}, {\"cidr\": \"10.0.208.1\"}, {\"cidr\": \"10.0.209.1\"}, {\"cidr\": \"10.0.210.1\"}, {\"cidr\": \"10.0.211.1\"}, {\"cidr\": \"10.0.212.1\"}, {\"cidr\": \"10.0.213.1\"}, {\"cidr\": \"10.0.214.1\"}, {\"cidr\": \"10.0.215.1\"}, {\"cidr\": \"10.0.216.1\"}, {\"cidr\": \"10.0.217.1\"}, {\"cidr\": \"10.0.218.1\"}, {\"cidr\": \"10.0.219.1\"}, {\"cidr\": \"10.0.220.1\"}, {\"cidr\": \"10.0.221.1\"}, {\"cidr\": \"10.0.222.1\"}, {\"cidr\": \"10.0.223.1\"}, {\"cidr\": \"10.0.224.1\"}, {\"cidr\": \"10.0.225.1\"}, {\"cidr\": \"10.0.226.1\"}, {\"cidr\": \"10.0.227.1\"}, {\"cidr\": \"10.0.228.1\"}, {\"cidr\": \"10.0.229.1\"}, {\"cidr\": \"10.0.230.1\"}, {\"cidr\": \"10.0.231.1\"}, {\"cidr\": \"10.0.232.1\"}, {\"cidr\": \"10.0.233.1\"}, {\"cidr\": \"10.0.234.1\"}, {\"cidr\": \"10.0.235.1\"}, {\"cidr\": \"10.0.236.1\"}, {\"cidr\": \"10.0.237.1\"}, {\"cidr\": \"10.0.238.1\"}, {\"cidr\": \"10.0.239.1\"}, {\"cidr\": \"10.0.240.1\"}, {\"cidr\": \"10.0.241.1\"}, {\"cidr\": \"10.0.242.1\"}, {\"cidr\": \"10.0.243.1\"}, {\"cidr\": \"10.0.244.1\"}, {\"cidr\": \"10.0.245.1\"}, {\"cidr\": \"10.0.246.1\"}, {\"cidr\": \"10.0.247.1\"}, {\"cidr\": \"10.0.248.1\"}, {\"cidr\": \"10.0.249.1\"}, {\"cidr\": \"10.0.250.1\"}, {\"cidr\": \"10.0.251.1\"}, {\"cidr\": \"10.0.252.1\"}, {\"cidr\": \"10.0.253.1\"}, {\"cidr\": \"10.0.254.1\"}, {\"cidr\": \"10.0.255.1\"}, {\"cidr\": \"10.1.0.1\"}]}"}],"expected":{"error":"deny emits at most 256 native rules"}}, + {"name":"limit: tcp for every port over the limit","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.1\"}, {\"cidr\": \"10.0.1.1\"}, {\"cidr\": \"10.0.2.1\"}, {\"cidr\": \"10.0.3.1\"}, {\"cidr\": \"10.0.4.1\"}, {\"cidr\": \"10.0.5.1\"}, {\"cidr\": \"10.0.6.1\"}, {\"cidr\": \"10.0.7.1\"}, {\"cidr\": \"10.0.8.1\"}, {\"cidr\": \"10.0.9.1\"}, {\"cidr\": \"10.0.10.1\"}, {\"cidr\": \"10.0.11.1\"}, {\"cidr\": \"10.0.12.1\"}, {\"cidr\": \"10.0.13.1\"}, {\"cidr\": \"10.0.14.1\"}, {\"cidr\": \"10.0.15.1\"}, {\"cidr\": \"10.0.16.1\"}, {\"cidr\": \"10.0.17.1\"}, {\"cidr\": \"10.0.18.1\"}, {\"cidr\": \"10.0.19.1\"}, {\"cidr\": \"10.0.20.1\"}, {\"cidr\": \"10.0.21.1\"}, {\"cidr\": \"10.0.22.1\"}, {\"cidr\": \"10.0.23.1\"}, {\"cidr\": \"10.0.24.1\"}, {\"cidr\": \"10.0.25.1\"}, {\"cidr\": \"10.0.26.1\"}, {\"cidr\": \"10.0.27.1\"}, {\"cidr\": \"10.0.28.1\"}, {\"cidr\": \"10.0.29.1\"}, {\"cidr\": \"10.0.30.1\"}, {\"cidr\": \"10.0.31.1\"}, {\"cidr\": \"10.0.32.1\"}, {\"cidr\": \"10.0.33.1\"}, {\"cidr\": \"10.0.34.1\"}, {\"cidr\": \"10.0.35.1\"}, {\"cidr\": \"10.0.36.1\"}, {\"cidr\": \"10.0.37.1\"}, {\"cidr\": \"10.0.38.1\"}, {\"cidr\": \"10.0.39.1\"}, {\"cidr\": \"10.0.40.1\"}, {\"cidr\": \"10.0.41.1\"}, {\"cidr\": \"10.0.42.1\"}, {\"cidr\": \"10.0.43.1\"}, {\"cidr\": \"10.0.44.1\"}, {\"cidr\": \"10.0.45.1\"}, {\"cidr\": \"10.0.46.1\"}, {\"cidr\": \"10.0.47.1\"}, {\"cidr\": \"10.0.48.1\"}, {\"cidr\": \"10.0.49.1\"}, {\"cidr\": \"10.0.50.1\"}, {\"cidr\": \"10.0.51.1\"}, {\"cidr\": \"10.0.52.1\"}, {\"cidr\": \"10.0.53.1\"}, {\"cidr\": \"10.0.54.1\"}, {\"cidr\": \"10.0.55.1\"}, {\"cidr\": \"10.0.56.1\"}, {\"cidr\": \"10.0.57.1\"}, {\"cidr\": \"10.0.58.1\"}, {\"cidr\": \"10.0.59.1\"}, {\"cidr\": \"10.0.60.1\"}, {\"cidr\": \"10.0.61.1\"}, {\"cidr\": \"10.0.62.1\"}, {\"cidr\": \"10.0.63.1\"}, {\"cidr\": \"10.0.64.1\"}, {\"cidr\": \"10.0.65.1\"}, {\"cidr\": \"10.0.66.1\"}, {\"cidr\": \"10.0.67.1\"}, {\"cidr\": \"10.0.68.1\"}, {\"cidr\": \"10.0.69.1\"}, {\"cidr\": \"10.0.70.1\"}, {\"cidr\": \"10.0.71.1\"}, {\"cidr\": \"10.0.72.1\"}, {\"cidr\": \"10.0.73.1\"}, {\"cidr\": \"10.0.74.1\"}, {\"cidr\": \"10.0.75.1\"}, {\"cidr\": \"10.0.76.1\"}, {\"cidr\": \"10.0.77.1\"}, {\"cidr\": \"10.0.78.1\"}, {\"cidr\": \"10.0.79.1\"}, {\"cidr\": \"10.0.80.1\"}, {\"cidr\": \"10.0.81.1\"}, {\"cidr\": \"10.0.82.1\"}, {\"cidr\": \"10.0.83.1\"}, {\"cidr\": \"10.0.84.1\"}, {\"cidr\": \"10.0.85.1\"}, {\"cidr\": \"10.0.86.1\"}, {\"cidr\": \"10.0.87.1\"}, {\"cidr\": \"10.0.88.1\"}, {\"cidr\": \"10.0.89.1\"}, {\"cidr\": \"10.0.90.1\"}, {\"cidr\": \"10.0.91.1\"}, {\"cidr\": \"10.0.92.1\"}, {\"cidr\": \"10.0.93.1\"}, {\"cidr\": \"10.0.94.1\"}, {\"cidr\": \"10.0.95.1\"}, {\"cidr\": \"10.0.96.1\"}, {\"cidr\": \"10.0.97.1\"}, {\"cidr\": \"10.0.98.1\"}, {\"cidr\": \"10.0.99.1\"}, {\"cidr\": \"10.0.100.1\"}, {\"cidr\": \"10.0.101.1\"}, {\"cidr\": \"10.0.102.1\"}, {\"cidr\": \"10.0.103.1\"}, {\"cidr\": \"10.0.104.1\"}, {\"cidr\": \"10.0.105.1\"}, {\"cidr\": \"10.0.106.1\"}, {\"cidr\": \"10.0.107.1\"}, {\"cidr\": \"10.0.108.1\"}, {\"cidr\": \"10.0.109.1\"}, {\"cidr\": \"10.0.110.1\"}, {\"cidr\": \"10.0.111.1\"}, {\"cidr\": \"10.0.112.1\"}, {\"cidr\": \"10.0.113.1\"}, {\"cidr\": \"10.0.114.1\"}, {\"cidr\": \"10.0.115.1\"}, {\"cidr\": \"10.0.116.1\"}, {\"cidr\": \"10.0.117.1\"}, {\"cidr\": \"10.0.118.1\"}, {\"cidr\": \"10.0.119.1\"}, {\"cidr\": \"10.0.120.1\"}, {\"cidr\": \"10.0.121.1\"}, {\"cidr\": \"10.0.122.1\"}, {\"cidr\": \"10.0.123.1\"}, {\"cidr\": \"10.0.124.1\"}, {\"cidr\": \"10.0.125.1\"}, {\"cidr\": \"10.0.126.1\"}, {\"cidr\": \"10.0.127.1\"}, {\"cidr\": \"10.0.128.1\"}, {\"cidr\": \"10.0.129.1\"}, {\"cidr\": \"10.0.130.1\"}, {\"cidr\": \"10.0.131.1\"}, {\"cidr\": \"10.0.132.1\"}, {\"cidr\": \"10.0.133.1\"}, {\"cidr\": \"10.0.134.1\"}, {\"cidr\": \"10.0.135.1\"}, {\"cidr\": \"10.0.136.1\"}, {\"cidr\": \"10.0.137.1\"}, {\"cidr\": \"10.0.138.1\"}, {\"cidr\": \"10.0.139.1\"}, {\"cidr\": \"10.0.140.1\"}, {\"cidr\": \"10.0.141.1\"}, {\"cidr\": \"10.0.142.1\"}, {\"cidr\": \"10.0.143.1\"}, {\"cidr\": \"10.0.144.1\"}, {\"cidr\": \"10.0.145.1\"}, {\"cidr\": \"10.0.146.1\"}, {\"cidr\": \"10.0.147.1\"}, {\"cidr\": \"10.0.148.1\"}, {\"cidr\": \"10.0.149.1\"}, {\"cidr\": \"10.0.150.1\"}, {\"cidr\": \"10.0.151.1\"}, {\"cidr\": \"10.0.152.1\"}, {\"cidr\": \"10.0.153.1\"}, {\"cidr\": \"10.0.154.1\"}, {\"cidr\": \"10.0.155.1\"}, {\"cidr\": \"10.0.156.1\"}, {\"cidr\": \"10.0.157.1\"}, {\"cidr\": \"10.0.158.1\"}, {\"cidr\": \"10.0.159.1\"}, {\"cidr\": \"10.0.160.1\"}, {\"cidr\": \"10.0.161.1\"}, {\"cidr\": \"10.0.162.1\"}, {\"cidr\": \"10.0.163.1\"}, {\"cidr\": \"10.0.164.1\"}, {\"cidr\": \"10.0.165.1\"}, {\"cidr\": \"10.0.166.1\"}, {\"cidr\": \"10.0.167.1\"}, {\"cidr\": \"10.0.168.1\"}, {\"cidr\": \"10.0.169.1\"}, {\"cidr\": \"10.0.170.1\"}, {\"cidr\": \"10.0.171.1\"}, {\"cidr\": \"10.0.172.1\"}, {\"cidr\": \"10.0.173.1\"}, {\"cidr\": \"10.0.174.1\"}, {\"cidr\": \"10.0.175.1\"}, {\"cidr\": \"10.0.176.1\"}, {\"cidr\": \"10.0.177.1\"}, {\"cidr\": \"10.0.178.1\"}, {\"cidr\": \"10.0.179.1\"}, {\"cidr\": \"10.0.180.1\"}, {\"cidr\": \"10.0.181.1\"}, {\"cidr\": \"10.0.182.1\"}, {\"cidr\": \"10.0.183.1\"}, {\"cidr\": \"10.0.184.1\"}, {\"cidr\": \"10.0.185.1\"}, {\"cidr\": \"10.0.186.1\"}, {\"cidr\": \"10.0.187.1\"}, {\"cidr\": \"10.0.188.1\"}, {\"cidr\": \"10.0.189.1\"}, {\"cidr\": \"10.0.190.1\"}, {\"cidr\": \"10.0.191.1\"}, {\"cidr\": \"10.0.192.1\"}, {\"cidr\": \"10.0.193.1\"}, {\"cidr\": \"10.0.194.1\"}, {\"cidr\": \"10.0.195.1\"}, {\"cidr\": \"10.0.196.1\"}, {\"cidr\": \"10.0.197.1\"}, {\"cidr\": \"10.0.198.1\"}, {\"cidr\": \"10.0.199.1\"}, {\"cidr\": \"10.0.200.1\"}, {\"cidr\": \"10.0.201.1\"}, {\"cidr\": \"10.0.202.1\"}, {\"cidr\": \"10.0.203.1\"}, {\"cidr\": \"10.0.204.1\"}, {\"cidr\": \"10.0.205.1\"}, {\"cidr\": \"10.0.206.1\"}, {\"cidr\": \"10.0.207.1\"}, {\"cidr\": \"10.0.208.1\"}, {\"cidr\": \"10.0.209.1\"}, {\"cidr\": \"10.0.210.1\"}, {\"cidr\": \"10.0.211.1\"}, {\"cidr\": \"10.0.212.1\"}, {\"cidr\": \"10.0.213.1\"}, {\"cidr\": \"10.0.214.1\"}, {\"cidr\": \"10.0.215.1\"}, {\"cidr\": \"10.0.216.1\"}, {\"cidr\": \"10.0.217.1\"}, {\"cidr\": \"10.0.218.1\"}, {\"cidr\": \"10.0.219.1\"}, {\"cidr\": \"10.0.220.1\"}, {\"cidr\": \"10.0.221.1\"}, {\"cidr\": \"10.0.222.1\"}, {\"cidr\": \"10.0.223.1\"}, {\"cidr\": \"10.0.224.1\"}, {\"cidr\": \"10.0.225.1\"}, {\"cidr\": \"10.0.226.1\"}, {\"cidr\": \"10.0.227.1\"}, {\"cidr\": \"10.0.228.1\"}, {\"cidr\": \"10.0.229.1\"}, {\"cidr\": \"10.0.230.1\"}, {\"cidr\": \"10.0.231.1\"}, {\"cidr\": \"10.0.232.1\"}, {\"cidr\": \"10.0.233.1\"}, {\"cidr\": \"10.0.234.1\"}, {\"cidr\": \"10.0.235.1\"}, {\"cidr\": \"10.0.236.1\"}, {\"cidr\": \"10.0.237.1\"}, {\"cidr\": \"10.0.238.1\"}, {\"cidr\": \"10.0.239.1\"}, {\"cidr\": \"10.0.240.1\"}, {\"cidr\": \"10.0.241.1\"}, {\"cidr\": \"10.0.242.1\"}, {\"cidr\": \"10.0.243.1\"}, {\"cidr\": \"10.0.244.1\"}, {\"cidr\": \"10.0.245.1\"}, {\"cidr\": \"10.0.246.1\"}, {\"cidr\": \"10.0.247.1\"}, {\"cidr\": \"10.0.248.1\"}, {\"cidr\": \"10.0.249.1\"}, {\"cidr\": \"10.0.250.1\"}, {\"cidr\": \"10.0.251.1\"}, {\"cidr\": \"10.0.252.1\"}, {\"cidr\": \"10.0.253.1\"}, {\"cidr\": \"10.0.254.1\"}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"tcp\"}, {\"cidr\": \"198.51.100.0/24\", \"protocol\": \"tcp\"}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"limit: udp for every port over the limit","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.1\"}, {\"cidr\": \"10.0.1.1\"}, {\"cidr\": \"10.0.2.1\"}, {\"cidr\": \"10.0.3.1\"}, {\"cidr\": \"10.0.4.1\"}, {\"cidr\": \"10.0.5.1\"}, {\"cidr\": \"10.0.6.1\"}, {\"cidr\": \"10.0.7.1\"}, {\"cidr\": \"10.0.8.1\"}, {\"cidr\": \"10.0.9.1\"}, {\"cidr\": \"10.0.10.1\"}, {\"cidr\": \"10.0.11.1\"}, {\"cidr\": \"10.0.12.1\"}, {\"cidr\": \"10.0.13.1\"}, {\"cidr\": \"10.0.14.1\"}, {\"cidr\": \"10.0.15.1\"}, {\"cidr\": \"10.0.16.1\"}, {\"cidr\": \"10.0.17.1\"}, {\"cidr\": \"10.0.18.1\"}, {\"cidr\": \"10.0.19.1\"}, {\"cidr\": \"10.0.20.1\"}, {\"cidr\": \"10.0.21.1\"}, {\"cidr\": \"10.0.22.1\"}, {\"cidr\": \"10.0.23.1\"}, {\"cidr\": \"10.0.24.1\"}, {\"cidr\": \"10.0.25.1\"}, {\"cidr\": \"10.0.26.1\"}, {\"cidr\": \"10.0.27.1\"}, {\"cidr\": \"10.0.28.1\"}, {\"cidr\": \"10.0.29.1\"}, {\"cidr\": \"10.0.30.1\"}, {\"cidr\": \"10.0.31.1\"}, {\"cidr\": \"10.0.32.1\"}, {\"cidr\": \"10.0.33.1\"}, {\"cidr\": \"10.0.34.1\"}, {\"cidr\": \"10.0.35.1\"}, {\"cidr\": \"10.0.36.1\"}, {\"cidr\": \"10.0.37.1\"}, {\"cidr\": \"10.0.38.1\"}, {\"cidr\": \"10.0.39.1\"}, {\"cidr\": \"10.0.40.1\"}, {\"cidr\": \"10.0.41.1\"}, {\"cidr\": \"10.0.42.1\"}, {\"cidr\": \"10.0.43.1\"}, {\"cidr\": \"10.0.44.1\"}, {\"cidr\": \"10.0.45.1\"}, {\"cidr\": \"10.0.46.1\"}, {\"cidr\": \"10.0.47.1\"}, {\"cidr\": \"10.0.48.1\"}, {\"cidr\": \"10.0.49.1\"}, {\"cidr\": \"10.0.50.1\"}, {\"cidr\": \"10.0.51.1\"}, {\"cidr\": \"10.0.52.1\"}, {\"cidr\": \"10.0.53.1\"}, {\"cidr\": \"10.0.54.1\"}, {\"cidr\": \"10.0.55.1\"}, {\"cidr\": \"10.0.56.1\"}, {\"cidr\": \"10.0.57.1\"}, {\"cidr\": \"10.0.58.1\"}, {\"cidr\": \"10.0.59.1\"}, {\"cidr\": \"10.0.60.1\"}, {\"cidr\": \"10.0.61.1\"}, {\"cidr\": \"10.0.62.1\"}, {\"cidr\": \"10.0.63.1\"}, {\"cidr\": \"10.0.64.1\"}, {\"cidr\": \"10.0.65.1\"}, {\"cidr\": \"10.0.66.1\"}, {\"cidr\": \"10.0.67.1\"}, {\"cidr\": \"10.0.68.1\"}, {\"cidr\": \"10.0.69.1\"}, {\"cidr\": \"10.0.70.1\"}, {\"cidr\": \"10.0.71.1\"}, {\"cidr\": \"10.0.72.1\"}, {\"cidr\": \"10.0.73.1\"}, {\"cidr\": \"10.0.74.1\"}, {\"cidr\": \"10.0.75.1\"}, {\"cidr\": \"10.0.76.1\"}, {\"cidr\": \"10.0.77.1\"}, {\"cidr\": \"10.0.78.1\"}, {\"cidr\": \"10.0.79.1\"}, {\"cidr\": \"10.0.80.1\"}, {\"cidr\": \"10.0.81.1\"}, {\"cidr\": \"10.0.82.1\"}, {\"cidr\": \"10.0.83.1\"}, {\"cidr\": \"10.0.84.1\"}, {\"cidr\": \"10.0.85.1\"}, {\"cidr\": \"10.0.86.1\"}, {\"cidr\": \"10.0.87.1\"}, {\"cidr\": \"10.0.88.1\"}, {\"cidr\": \"10.0.89.1\"}, {\"cidr\": \"10.0.90.1\"}, {\"cidr\": \"10.0.91.1\"}, {\"cidr\": \"10.0.92.1\"}, {\"cidr\": \"10.0.93.1\"}, {\"cidr\": \"10.0.94.1\"}, {\"cidr\": \"10.0.95.1\"}, {\"cidr\": \"10.0.96.1\"}, {\"cidr\": \"10.0.97.1\"}, {\"cidr\": \"10.0.98.1\"}, {\"cidr\": \"10.0.99.1\"}, {\"cidr\": \"10.0.100.1\"}, {\"cidr\": \"10.0.101.1\"}, {\"cidr\": \"10.0.102.1\"}, {\"cidr\": \"10.0.103.1\"}, {\"cidr\": \"10.0.104.1\"}, {\"cidr\": \"10.0.105.1\"}, {\"cidr\": \"10.0.106.1\"}, {\"cidr\": \"10.0.107.1\"}, {\"cidr\": \"10.0.108.1\"}, {\"cidr\": \"10.0.109.1\"}, {\"cidr\": \"10.0.110.1\"}, {\"cidr\": \"10.0.111.1\"}, {\"cidr\": \"10.0.112.1\"}, {\"cidr\": \"10.0.113.1\"}, {\"cidr\": \"10.0.114.1\"}, {\"cidr\": \"10.0.115.1\"}, {\"cidr\": \"10.0.116.1\"}, {\"cidr\": \"10.0.117.1\"}, {\"cidr\": \"10.0.118.1\"}, {\"cidr\": \"10.0.119.1\"}, {\"cidr\": \"10.0.120.1\"}, {\"cidr\": \"10.0.121.1\"}, {\"cidr\": \"10.0.122.1\"}, {\"cidr\": \"10.0.123.1\"}, {\"cidr\": \"10.0.124.1\"}, {\"cidr\": \"10.0.125.1\"}, {\"cidr\": \"10.0.126.1\"}, {\"cidr\": \"10.0.127.1\"}, {\"cidr\": \"10.0.128.1\"}, {\"cidr\": \"10.0.129.1\"}, {\"cidr\": \"10.0.130.1\"}, {\"cidr\": \"10.0.131.1\"}, {\"cidr\": \"10.0.132.1\"}, {\"cidr\": \"10.0.133.1\"}, {\"cidr\": \"10.0.134.1\"}, {\"cidr\": \"10.0.135.1\"}, {\"cidr\": \"10.0.136.1\"}, {\"cidr\": \"10.0.137.1\"}, {\"cidr\": \"10.0.138.1\"}, {\"cidr\": \"10.0.139.1\"}, {\"cidr\": \"10.0.140.1\"}, {\"cidr\": \"10.0.141.1\"}, {\"cidr\": \"10.0.142.1\"}, {\"cidr\": \"10.0.143.1\"}, {\"cidr\": \"10.0.144.1\"}, {\"cidr\": \"10.0.145.1\"}, {\"cidr\": \"10.0.146.1\"}, {\"cidr\": \"10.0.147.1\"}, {\"cidr\": \"10.0.148.1\"}, {\"cidr\": \"10.0.149.1\"}, {\"cidr\": \"10.0.150.1\"}, {\"cidr\": \"10.0.151.1\"}, {\"cidr\": \"10.0.152.1\"}, {\"cidr\": \"10.0.153.1\"}, {\"cidr\": \"10.0.154.1\"}, {\"cidr\": \"10.0.155.1\"}, {\"cidr\": \"10.0.156.1\"}, {\"cidr\": \"10.0.157.1\"}, {\"cidr\": \"10.0.158.1\"}, {\"cidr\": \"10.0.159.1\"}, {\"cidr\": \"10.0.160.1\"}, {\"cidr\": \"10.0.161.1\"}, {\"cidr\": \"10.0.162.1\"}, {\"cidr\": \"10.0.163.1\"}, {\"cidr\": \"10.0.164.1\"}, {\"cidr\": \"10.0.165.1\"}, {\"cidr\": \"10.0.166.1\"}, {\"cidr\": \"10.0.167.1\"}, {\"cidr\": \"10.0.168.1\"}, {\"cidr\": \"10.0.169.1\"}, {\"cidr\": \"10.0.170.1\"}, {\"cidr\": \"10.0.171.1\"}, {\"cidr\": \"10.0.172.1\"}, {\"cidr\": \"10.0.173.1\"}, {\"cidr\": \"10.0.174.1\"}, {\"cidr\": \"10.0.175.1\"}, {\"cidr\": \"10.0.176.1\"}, {\"cidr\": \"10.0.177.1\"}, {\"cidr\": \"10.0.178.1\"}, {\"cidr\": \"10.0.179.1\"}, {\"cidr\": \"10.0.180.1\"}, {\"cidr\": \"10.0.181.1\"}, {\"cidr\": \"10.0.182.1\"}, {\"cidr\": \"10.0.183.1\"}, {\"cidr\": \"10.0.184.1\"}, {\"cidr\": \"10.0.185.1\"}, {\"cidr\": \"10.0.186.1\"}, {\"cidr\": \"10.0.187.1\"}, {\"cidr\": \"10.0.188.1\"}, {\"cidr\": \"10.0.189.1\"}, {\"cidr\": \"10.0.190.1\"}, {\"cidr\": \"10.0.191.1\"}, {\"cidr\": \"10.0.192.1\"}, {\"cidr\": \"10.0.193.1\"}, {\"cidr\": \"10.0.194.1\"}, {\"cidr\": \"10.0.195.1\"}, {\"cidr\": \"10.0.196.1\"}, {\"cidr\": \"10.0.197.1\"}, {\"cidr\": \"10.0.198.1\"}, {\"cidr\": \"10.0.199.1\"}, {\"cidr\": \"10.0.200.1\"}, {\"cidr\": \"10.0.201.1\"}, {\"cidr\": \"10.0.202.1\"}, {\"cidr\": \"10.0.203.1\"}, {\"cidr\": \"10.0.204.1\"}, {\"cidr\": \"10.0.205.1\"}, {\"cidr\": \"10.0.206.1\"}, {\"cidr\": \"10.0.207.1\"}, {\"cidr\": \"10.0.208.1\"}, {\"cidr\": \"10.0.209.1\"}, {\"cidr\": \"10.0.210.1\"}, {\"cidr\": \"10.0.211.1\"}, {\"cidr\": \"10.0.212.1\"}, {\"cidr\": \"10.0.213.1\"}, {\"cidr\": \"10.0.214.1\"}, {\"cidr\": \"10.0.215.1\"}, {\"cidr\": \"10.0.216.1\"}, {\"cidr\": \"10.0.217.1\"}, {\"cidr\": \"10.0.218.1\"}, {\"cidr\": \"10.0.219.1\"}, {\"cidr\": \"10.0.220.1\"}, {\"cidr\": \"10.0.221.1\"}, {\"cidr\": \"10.0.222.1\"}, {\"cidr\": \"10.0.223.1\"}, {\"cidr\": \"10.0.224.1\"}, {\"cidr\": \"10.0.225.1\"}, {\"cidr\": \"10.0.226.1\"}, {\"cidr\": \"10.0.227.1\"}, {\"cidr\": \"10.0.228.1\"}, {\"cidr\": \"10.0.229.1\"}, {\"cidr\": \"10.0.230.1\"}, {\"cidr\": \"10.0.231.1\"}, {\"cidr\": \"10.0.232.1\"}, {\"cidr\": \"10.0.233.1\"}, {\"cidr\": \"10.0.234.1\"}, {\"cidr\": \"10.0.235.1\"}, {\"cidr\": \"10.0.236.1\"}, {\"cidr\": \"10.0.237.1\"}, {\"cidr\": \"10.0.238.1\"}, {\"cidr\": \"10.0.239.1\"}, {\"cidr\": \"10.0.240.1\"}, {\"cidr\": \"10.0.241.1\"}, {\"cidr\": \"10.0.242.1\"}, {\"cidr\": \"10.0.243.1\"}, {\"cidr\": \"10.0.244.1\"}, {\"cidr\": \"10.0.245.1\"}, {\"cidr\": \"10.0.246.1\"}, {\"cidr\": \"10.0.247.1\"}, {\"cidr\": \"10.0.248.1\"}, {\"cidr\": \"10.0.249.1\"}, {\"cidr\": \"10.0.250.1\"}, {\"cidr\": \"10.0.251.1\"}, {\"cidr\": \"10.0.252.1\"}, {\"cidr\": \"10.0.253.1\"}, {\"cidr\": \"10.0.254.1\"}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"udp\"}, {\"cidr\": \"198.51.100.0/24\", \"protocol\": \"udp\"}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"limit: icmp for every port over the limit","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.1\"}, {\"cidr\": \"10.0.1.1\"}, {\"cidr\": \"10.0.2.1\"}, {\"cidr\": \"10.0.3.1\"}, {\"cidr\": \"10.0.4.1\"}, {\"cidr\": \"10.0.5.1\"}, {\"cidr\": \"10.0.6.1\"}, {\"cidr\": \"10.0.7.1\"}, {\"cidr\": \"10.0.8.1\"}, {\"cidr\": \"10.0.9.1\"}, {\"cidr\": \"10.0.10.1\"}, {\"cidr\": \"10.0.11.1\"}, {\"cidr\": \"10.0.12.1\"}, {\"cidr\": \"10.0.13.1\"}, {\"cidr\": \"10.0.14.1\"}, {\"cidr\": \"10.0.15.1\"}, {\"cidr\": \"10.0.16.1\"}, {\"cidr\": \"10.0.17.1\"}, {\"cidr\": \"10.0.18.1\"}, {\"cidr\": \"10.0.19.1\"}, {\"cidr\": \"10.0.20.1\"}, {\"cidr\": \"10.0.21.1\"}, {\"cidr\": \"10.0.22.1\"}, {\"cidr\": \"10.0.23.1\"}, {\"cidr\": \"10.0.24.1\"}, {\"cidr\": \"10.0.25.1\"}, {\"cidr\": \"10.0.26.1\"}, {\"cidr\": \"10.0.27.1\"}, {\"cidr\": \"10.0.28.1\"}, {\"cidr\": \"10.0.29.1\"}, {\"cidr\": \"10.0.30.1\"}, {\"cidr\": \"10.0.31.1\"}, {\"cidr\": \"10.0.32.1\"}, {\"cidr\": \"10.0.33.1\"}, {\"cidr\": \"10.0.34.1\"}, {\"cidr\": \"10.0.35.1\"}, {\"cidr\": \"10.0.36.1\"}, {\"cidr\": \"10.0.37.1\"}, {\"cidr\": \"10.0.38.1\"}, {\"cidr\": \"10.0.39.1\"}, {\"cidr\": \"10.0.40.1\"}, {\"cidr\": \"10.0.41.1\"}, {\"cidr\": \"10.0.42.1\"}, {\"cidr\": \"10.0.43.1\"}, {\"cidr\": \"10.0.44.1\"}, {\"cidr\": \"10.0.45.1\"}, {\"cidr\": \"10.0.46.1\"}, {\"cidr\": \"10.0.47.1\"}, {\"cidr\": \"10.0.48.1\"}, {\"cidr\": \"10.0.49.1\"}, {\"cidr\": \"10.0.50.1\"}, {\"cidr\": \"10.0.51.1\"}, {\"cidr\": \"10.0.52.1\"}, {\"cidr\": \"10.0.53.1\"}, {\"cidr\": \"10.0.54.1\"}, {\"cidr\": \"10.0.55.1\"}, {\"cidr\": \"10.0.56.1\"}, {\"cidr\": \"10.0.57.1\"}, {\"cidr\": \"10.0.58.1\"}, {\"cidr\": \"10.0.59.1\"}, {\"cidr\": \"10.0.60.1\"}, {\"cidr\": \"10.0.61.1\"}, {\"cidr\": \"10.0.62.1\"}, {\"cidr\": \"10.0.63.1\"}, {\"cidr\": \"10.0.64.1\"}, {\"cidr\": \"10.0.65.1\"}, {\"cidr\": \"10.0.66.1\"}, {\"cidr\": \"10.0.67.1\"}, {\"cidr\": \"10.0.68.1\"}, {\"cidr\": \"10.0.69.1\"}, {\"cidr\": \"10.0.70.1\"}, {\"cidr\": \"10.0.71.1\"}, {\"cidr\": \"10.0.72.1\"}, {\"cidr\": \"10.0.73.1\"}, {\"cidr\": \"10.0.74.1\"}, {\"cidr\": \"10.0.75.1\"}, {\"cidr\": \"10.0.76.1\"}, {\"cidr\": \"10.0.77.1\"}, {\"cidr\": \"10.0.78.1\"}, {\"cidr\": \"10.0.79.1\"}, {\"cidr\": \"10.0.80.1\"}, {\"cidr\": \"10.0.81.1\"}, {\"cidr\": \"10.0.82.1\"}, {\"cidr\": \"10.0.83.1\"}, {\"cidr\": \"10.0.84.1\"}, {\"cidr\": \"10.0.85.1\"}, {\"cidr\": \"10.0.86.1\"}, {\"cidr\": \"10.0.87.1\"}, {\"cidr\": \"10.0.88.1\"}, {\"cidr\": \"10.0.89.1\"}, {\"cidr\": \"10.0.90.1\"}, {\"cidr\": \"10.0.91.1\"}, {\"cidr\": \"10.0.92.1\"}, {\"cidr\": \"10.0.93.1\"}, {\"cidr\": \"10.0.94.1\"}, {\"cidr\": \"10.0.95.1\"}, {\"cidr\": \"10.0.96.1\"}, {\"cidr\": \"10.0.97.1\"}, {\"cidr\": \"10.0.98.1\"}, {\"cidr\": \"10.0.99.1\"}, {\"cidr\": \"10.0.100.1\"}, {\"cidr\": \"10.0.101.1\"}, {\"cidr\": \"10.0.102.1\"}, {\"cidr\": \"10.0.103.1\"}, {\"cidr\": \"10.0.104.1\"}, {\"cidr\": \"10.0.105.1\"}, {\"cidr\": \"10.0.106.1\"}, {\"cidr\": \"10.0.107.1\"}, {\"cidr\": \"10.0.108.1\"}, {\"cidr\": \"10.0.109.1\"}, {\"cidr\": \"10.0.110.1\"}, {\"cidr\": \"10.0.111.1\"}, {\"cidr\": \"10.0.112.1\"}, {\"cidr\": \"10.0.113.1\"}, {\"cidr\": \"10.0.114.1\"}, {\"cidr\": \"10.0.115.1\"}, {\"cidr\": \"10.0.116.1\"}, {\"cidr\": \"10.0.117.1\"}, {\"cidr\": \"10.0.118.1\"}, {\"cidr\": \"10.0.119.1\"}, {\"cidr\": \"10.0.120.1\"}, {\"cidr\": \"10.0.121.1\"}, {\"cidr\": \"10.0.122.1\"}, {\"cidr\": \"10.0.123.1\"}, {\"cidr\": \"10.0.124.1\"}, {\"cidr\": \"10.0.125.1\"}, {\"cidr\": \"10.0.126.1\"}, {\"cidr\": \"10.0.127.1\"}, {\"cidr\": \"10.0.128.1\"}, {\"cidr\": \"10.0.129.1\"}, {\"cidr\": \"10.0.130.1\"}, {\"cidr\": \"10.0.131.1\"}, {\"cidr\": \"10.0.132.1\"}, {\"cidr\": \"10.0.133.1\"}, {\"cidr\": \"10.0.134.1\"}, {\"cidr\": \"10.0.135.1\"}, {\"cidr\": \"10.0.136.1\"}, {\"cidr\": \"10.0.137.1\"}, {\"cidr\": \"10.0.138.1\"}, {\"cidr\": \"10.0.139.1\"}, {\"cidr\": \"10.0.140.1\"}, {\"cidr\": \"10.0.141.1\"}, {\"cidr\": \"10.0.142.1\"}, {\"cidr\": \"10.0.143.1\"}, {\"cidr\": \"10.0.144.1\"}, {\"cidr\": \"10.0.145.1\"}, {\"cidr\": \"10.0.146.1\"}, {\"cidr\": \"10.0.147.1\"}, {\"cidr\": \"10.0.148.1\"}, {\"cidr\": \"10.0.149.1\"}, {\"cidr\": \"10.0.150.1\"}, {\"cidr\": \"10.0.151.1\"}, {\"cidr\": \"10.0.152.1\"}, {\"cidr\": \"10.0.153.1\"}, {\"cidr\": \"10.0.154.1\"}, {\"cidr\": \"10.0.155.1\"}, {\"cidr\": \"10.0.156.1\"}, {\"cidr\": \"10.0.157.1\"}, {\"cidr\": \"10.0.158.1\"}, {\"cidr\": \"10.0.159.1\"}, {\"cidr\": \"10.0.160.1\"}, {\"cidr\": \"10.0.161.1\"}, {\"cidr\": \"10.0.162.1\"}, {\"cidr\": \"10.0.163.1\"}, {\"cidr\": \"10.0.164.1\"}, {\"cidr\": \"10.0.165.1\"}, {\"cidr\": \"10.0.166.1\"}, {\"cidr\": \"10.0.167.1\"}, {\"cidr\": \"10.0.168.1\"}, {\"cidr\": \"10.0.169.1\"}, {\"cidr\": \"10.0.170.1\"}, {\"cidr\": \"10.0.171.1\"}, {\"cidr\": \"10.0.172.1\"}, {\"cidr\": \"10.0.173.1\"}, {\"cidr\": \"10.0.174.1\"}, {\"cidr\": \"10.0.175.1\"}, {\"cidr\": \"10.0.176.1\"}, {\"cidr\": \"10.0.177.1\"}, {\"cidr\": \"10.0.178.1\"}, {\"cidr\": \"10.0.179.1\"}, {\"cidr\": \"10.0.180.1\"}, {\"cidr\": \"10.0.181.1\"}, {\"cidr\": \"10.0.182.1\"}, {\"cidr\": \"10.0.183.1\"}, {\"cidr\": \"10.0.184.1\"}, {\"cidr\": \"10.0.185.1\"}, {\"cidr\": \"10.0.186.1\"}, {\"cidr\": \"10.0.187.1\"}, {\"cidr\": \"10.0.188.1\"}, {\"cidr\": \"10.0.189.1\"}, {\"cidr\": \"10.0.190.1\"}, {\"cidr\": \"10.0.191.1\"}, {\"cidr\": \"10.0.192.1\"}, {\"cidr\": \"10.0.193.1\"}, {\"cidr\": \"10.0.194.1\"}, {\"cidr\": \"10.0.195.1\"}, {\"cidr\": \"10.0.196.1\"}, {\"cidr\": \"10.0.197.1\"}, {\"cidr\": \"10.0.198.1\"}, {\"cidr\": \"10.0.199.1\"}, {\"cidr\": \"10.0.200.1\"}, {\"cidr\": \"10.0.201.1\"}, {\"cidr\": \"10.0.202.1\"}, {\"cidr\": \"10.0.203.1\"}, {\"cidr\": \"10.0.204.1\"}, {\"cidr\": \"10.0.205.1\"}, {\"cidr\": \"10.0.206.1\"}, {\"cidr\": \"10.0.207.1\"}, {\"cidr\": \"10.0.208.1\"}, {\"cidr\": \"10.0.209.1\"}, {\"cidr\": \"10.0.210.1\"}, {\"cidr\": \"10.0.211.1\"}, {\"cidr\": \"10.0.212.1\"}, {\"cidr\": \"10.0.213.1\"}, {\"cidr\": \"10.0.214.1\"}, {\"cidr\": \"10.0.215.1\"}, {\"cidr\": \"10.0.216.1\"}, {\"cidr\": \"10.0.217.1\"}, {\"cidr\": \"10.0.218.1\"}, {\"cidr\": \"10.0.219.1\"}, {\"cidr\": \"10.0.220.1\"}, {\"cidr\": \"10.0.221.1\"}, {\"cidr\": \"10.0.222.1\"}, {\"cidr\": \"10.0.223.1\"}, {\"cidr\": \"10.0.224.1\"}, {\"cidr\": \"10.0.225.1\"}, {\"cidr\": \"10.0.226.1\"}, {\"cidr\": \"10.0.227.1\"}, {\"cidr\": \"10.0.228.1\"}, {\"cidr\": \"10.0.229.1\"}, {\"cidr\": \"10.0.230.1\"}, {\"cidr\": \"10.0.231.1\"}, {\"cidr\": \"10.0.232.1\"}, {\"cidr\": \"10.0.233.1\"}, {\"cidr\": \"10.0.234.1\"}, {\"cidr\": \"10.0.235.1\"}, {\"cidr\": \"10.0.236.1\"}, {\"cidr\": \"10.0.237.1\"}, {\"cidr\": \"10.0.238.1\"}, {\"cidr\": \"10.0.239.1\"}, {\"cidr\": \"10.0.240.1\"}, {\"cidr\": \"10.0.241.1\"}, {\"cidr\": \"10.0.242.1\"}, {\"cidr\": \"10.0.243.1\"}, {\"cidr\": \"10.0.244.1\"}, {\"cidr\": \"10.0.245.1\"}, {\"cidr\": \"10.0.246.1\"}, {\"cidr\": \"10.0.247.1\"}, {\"cidr\": \"10.0.248.1\"}, {\"cidr\": \"10.0.249.1\"}, {\"cidr\": \"10.0.250.1\"}, {\"cidr\": \"10.0.251.1\"}, {\"cidr\": \"10.0.252.1\"}, {\"cidr\": \"10.0.253.1\"}, {\"cidr\": \"10.0.254.1\"}, {\"cidr\": \"192.0.2.0/24\", \"protocol\": \"icmp\"}, {\"cidr\": \"198.51.100.0/24\", \"protocol\": \"icmp\"}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"limit: 256 separate ports","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.1\"}], \"ports\": [{\"port\": 1, \"protocol\": \"udp\"}, {\"port\": 3, \"protocol\": \"udp\"}, {\"port\": 5, \"protocol\": \"udp\"}, {\"port\": 7, \"protocol\": \"udp\"}, {\"port\": 9, \"protocol\": \"udp\"}, {\"port\": 11, \"protocol\": \"udp\"}, {\"port\": 13, \"protocol\": \"udp\"}, {\"port\": 15, \"protocol\": \"udp\"}, {\"port\": 17, \"protocol\": \"udp\"}, {\"port\": 19, \"protocol\": \"udp\"}, {\"port\": 21, \"protocol\": \"udp\"}, {\"port\": 23, \"protocol\": \"udp\"}, {\"port\": 25, \"protocol\": \"udp\"}, {\"port\": 27, \"protocol\": \"udp\"}, {\"port\": 29, \"protocol\": \"udp\"}, {\"port\": 31, \"protocol\": \"udp\"}, {\"port\": 33, \"protocol\": \"udp\"}, {\"port\": 35, \"protocol\": \"udp\"}, {\"port\": 37, \"protocol\": \"udp\"}, {\"port\": 39, \"protocol\": \"udp\"}, {\"port\": 41, \"protocol\": \"udp\"}, {\"port\": 43, \"protocol\": \"udp\"}, {\"port\": 45, \"protocol\": \"udp\"}, {\"port\": 47, \"protocol\": \"udp\"}, {\"port\": 49, \"protocol\": \"udp\"}, {\"port\": 51, \"protocol\": \"udp\"}, {\"port\": 53, \"protocol\": \"udp\"}, {\"port\": 55, \"protocol\": \"udp\"}, {\"port\": 57, \"protocol\": \"udp\"}, {\"port\": 59, \"protocol\": \"udp\"}, {\"port\": 61, \"protocol\": \"udp\"}, {\"port\": 63, \"protocol\": \"udp\"}, {\"port\": 65, \"protocol\": \"udp\"}, {\"port\": 67, \"protocol\": \"udp\"}, {\"port\": 69, \"protocol\": \"udp\"}, {\"port\": 71, \"protocol\": \"udp\"}, {\"port\": 73, \"protocol\": \"udp\"}, {\"port\": 75, \"protocol\": \"udp\"}, {\"port\": 77, \"protocol\": \"udp\"}, {\"port\": 79, \"protocol\": \"udp\"}, {\"port\": 81, \"protocol\": \"udp\"}, {\"port\": 83, \"protocol\": \"udp\"}, {\"port\": 85, \"protocol\": \"udp\"}, {\"port\": 87, \"protocol\": \"udp\"}, {\"port\": 89, \"protocol\": \"udp\"}, {\"port\": 91, \"protocol\": \"udp\"}, {\"port\": 93, \"protocol\": \"udp\"}, {\"port\": 95, \"protocol\": \"udp\"}, {\"port\": 97, \"protocol\": \"udp\"}, {\"port\": 99, \"protocol\": \"udp\"}, {\"port\": 101, \"protocol\": \"udp\"}, {\"port\": 103, \"protocol\": \"udp\"}, {\"port\": 105, \"protocol\": \"udp\"}, {\"port\": 107, \"protocol\": \"udp\"}, {\"port\": 109, \"protocol\": \"udp\"}, {\"port\": 111, \"protocol\": \"udp\"}, {\"port\": 113, \"protocol\": \"udp\"}, {\"port\": 115, \"protocol\": \"udp\"}, {\"port\": 117, \"protocol\": \"udp\"}, {\"port\": 119, \"protocol\": \"udp\"}, {\"port\": 121, \"protocol\": \"udp\"}, {\"port\": 123, \"protocol\": \"udp\"}, {\"port\": 125, \"protocol\": \"udp\"}, {\"port\": 127, \"protocol\": \"udp\"}, {\"port\": 129, \"protocol\": \"udp\"}, {\"port\": 131, \"protocol\": \"udp\"}, {\"port\": 133, \"protocol\": \"udp\"}, {\"port\": 135, \"protocol\": \"udp\"}, {\"port\": 137, \"protocol\": \"udp\"}, {\"port\": 139, \"protocol\": \"udp\"}, {\"port\": 141, \"protocol\": \"udp\"}, {\"port\": 143, \"protocol\": \"udp\"}, {\"port\": 145, \"protocol\": \"udp\"}, {\"port\": 147, \"protocol\": \"udp\"}, {\"port\": 149, \"protocol\": \"udp\"}, {\"port\": 151, \"protocol\": \"udp\"}, {\"port\": 153, \"protocol\": \"udp\"}, {\"port\": 155, \"protocol\": \"udp\"}, {\"port\": 157, \"protocol\": \"udp\"}, {\"port\": 159, \"protocol\": \"udp\"}, {\"port\": 161, \"protocol\": \"udp\"}, {\"port\": 163, \"protocol\": \"udp\"}, {\"port\": 165, \"protocol\": \"udp\"}, {\"port\": 167, \"protocol\": \"udp\"}, {\"port\": 169, \"protocol\": \"udp\"}, {\"port\": 171, \"protocol\": \"udp\"}, {\"port\": 173, \"protocol\": \"udp\"}, {\"port\": 175, \"protocol\": \"udp\"}, {\"port\": 177, \"protocol\": \"udp\"}, {\"port\": 179, \"protocol\": \"udp\"}, {\"port\": 181, \"protocol\": \"udp\"}, {\"port\": 183, \"protocol\": \"udp\"}, {\"port\": 185, \"protocol\": \"udp\"}, {\"port\": 187, \"protocol\": \"udp\"}, {\"port\": 189, \"protocol\": \"udp\"}, {\"port\": 191, \"protocol\": \"udp\"}, {\"port\": 193, \"protocol\": \"udp\"}, {\"port\": 195, \"protocol\": \"udp\"}, {\"port\": 197, \"protocol\": \"udp\"}, {\"port\": 199, \"protocol\": \"udp\"}, {\"port\": 201, \"protocol\": \"udp\"}, {\"port\": 203, \"protocol\": \"udp\"}, {\"port\": 205, \"protocol\": \"udp\"}, {\"port\": 207, \"protocol\": \"udp\"}, {\"port\": 209, \"protocol\": \"udp\"}, {\"port\": 211, \"protocol\": \"udp\"}, {\"port\": 213, \"protocol\": \"udp\"}, {\"port\": 215, \"protocol\": \"udp\"}, {\"port\": 217, \"protocol\": \"udp\"}, {\"port\": 219, \"protocol\": \"udp\"}, {\"port\": 221, \"protocol\": \"udp\"}, {\"port\": 223, \"protocol\": \"udp\"}, {\"port\": 225, \"protocol\": \"udp\"}, {\"port\": 227, \"protocol\": \"udp\"}, {\"port\": 229, \"protocol\": \"udp\"}, {\"port\": 231, \"protocol\": \"udp\"}, {\"port\": 233, \"protocol\": \"udp\"}, {\"port\": 235, \"protocol\": \"udp\"}, {\"port\": 237, \"protocol\": \"udp\"}, {\"port\": 239, \"protocol\": \"udp\"}, {\"port\": 241, \"protocol\": \"udp\"}, {\"port\": 243, \"protocol\": \"udp\"}, {\"port\": 245, \"protocol\": \"udp\"}, {\"port\": 247, \"protocol\": \"udp\"}, {\"port\": 249, \"protocol\": \"udp\"}, {\"port\": 251, \"protocol\": \"udp\"}, {\"port\": 253, \"protocol\": \"udp\"}, {\"port\": 255, \"protocol\": \"udp\"}, {\"port\": 257, \"protocol\": \"udp\"}, {\"port\": 259, \"protocol\": \"udp\"}, {\"port\": 261, \"protocol\": \"udp\"}, {\"port\": 263, \"protocol\": \"udp\"}, {\"port\": 265, \"protocol\": \"udp\"}, {\"port\": 267, \"protocol\": \"udp\"}, {\"port\": 269, \"protocol\": \"udp\"}, {\"port\": 271, \"protocol\": \"udp\"}, {\"port\": 273, \"protocol\": \"udp\"}, {\"port\": 275, \"protocol\": \"udp\"}, {\"port\": 277, \"protocol\": \"udp\"}, {\"port\": 279, \"protocol\": \"udp\"}, {\"port\": 281, \"protocol\": \"udp\"}, {\"port\": 283, \"protocol\": \"udp\"}, {\"port\": 285, \"protocol\": \"udp\"}, {\"port\": 287, \"protocol\": \"udp\"}, {\"port\": 289, \"protocol\": \"udp\"}, {\"port\": 291, \"protocol\": \"udp\"}, {\"port\": 293, \"protocol\": \"udp\"}, {\"port\": 295, \"protocol\": \"udp\"}, {\"port\": 297, \"protocol\": \"udp\"}, {\"port\": 299, \"protocol\": \"udp\"}, {\"port\": 301, \"protocol\": \"udp\"}, {\"port\": 303, \"protocol\": \"udp\"}, {\"port\": 305, \"protocol\": \"udp\"}, {\"port\": 307, \"protocol\": \"udp\"}, {\"port\": 309, \"protocol\": \"udp\"}, {\"port\": 311, \"protocol\": \"udp\"}, {\"port\": 313, \"protocol\": \"udp\"}, {\"port\": 315, \"protocol\": \"udp\"}, {\"port\": 317, \"protocol\": \"udp\"}, {\"port\": 319, \"protocol\": \"udp\"}, {\"port\": 321, \"protocol\": \"udp\"}, {\"port\": 323, \"protocol\": \"udp\"}, {\"port\": 325, \"protocol\": \"udp\"}, {\"port\": 327, \"protocol\": \"udp\"}, {\"port\": 329, \"protocol\": \"udp\"}, {\"port\": 331, \"protocol\": \"udp\"}, {\"port\": 333, \"protocol\": \"udp\"}, {\"port\": 335, \"protocol\": \"udp\"}, {\"port\": 337, \"protocol\": \"udp\"}, {\"port\": 339, \"protocol\": \"udp\"}, {\"port\": 341, \"protocol\": \"udp\"}, {\"port\": 343, \"protocol\": \"udp\"}, {\"port\": 345, \"protocol\": \"udp\"}, {\"port\": 347, \"protocol\": \"udp\"}, {\"port\": 349, \"protocol\": \"udp\"}, {\"port\": 351, \"protocol\": \"udp\"}, {\"port\": 353, \"protocol\": \"udp\"}, {\"port\": 355, \"protocol\": \"udp\"}, {\"port\": 357, \"protocol\": \"udp\"}, {\"port\": 359, \"protocol\": \"udp\"}, {\"port\": 361, \"protocol\": \"udp\"}, {\"port\": 363, \"protocol\": \"udp\"}, {\"port\": 365, \"protocol\": \"udp\"}, {\"port\": 367, \"protocol\": \"udp\"}, {\"port\": 369, \"protocol\": \"udp\"}, {\"port\": 371, \"protocol\": \"udp\"}, {\"port\": 373, \"protocol\": \"udp\"}, {\"port\": 375, \"protocol\": \"udp\"}, {\"port\": 377, \"protocol\": \"udp\"}, {\"port\": 379, \"protocol\": \"udp\"}, {\"port\": 381, \"protocol\": \"udp\"}, {\"port\": 383, \"protocol\": \"udp\"}, {\"port\": 385, \"protocol\": \"udp\"}, {\"port\": 387, \"protocol\": \"udp\"}, {\"port\": 389, \"protocol\": \"udp\"}, {\"port\": 391, \"protocol\": \"udp\"}, {\"port\": 393, \"protocol\": \"udp\"}, {\"port\": 395, \"protocol\": \"udp\"}, {\"port\": 397, \"protocol\": \"udp\"}, {\"port\": 399, \"protocol\": \"udp\"}, {\"port\": 401, \"protocol\": \"udp\"}, {\"port\": 403, \"protocol\": \"udp\"}, {\"port\": 405, \"protocol\": \"udp\"}, {\"port\": 407, \"protocol\": \"udp\"}, {\"port\": 409, \"protocol\": \"udp\"}, {\"port\": 411, \"protocol\": \"udp\"}, {\"port\": 413, \"protocol\": \"udp\"}, {\"port\": 415, \"protocol\": \"udp\"}, {\"port\": 417, \"protocol\": \"udp\"}, {\"port\": 419, \"protocol\": \"udp\"}, {\"port\": 421, \"protocol\": \"udp\"}, {\"port\": 423, \"protocol\": \"udp\"}, {\"port\": 425, \"protocol\": \"udp\"}, {\"port\": 427, \"protocol\": \"udp\"}, {\"port\": 429, \"protocol\": \"udp\"}, {\"port\": 431, \"protocol\": \"udp\"}, {\"port\": 433, \"protocol\": \"udp\"}, {\"port\": 435, \"protocol\": \"udp\"}, {\"port\": 437, \"protocol\": \"udp\"}, {\"port\": 439, \"protocol\": \"udp\"}, {\"port\": 441, \"protocol\": \"udp\"}, {\"port\": 443, \"protocol\": \"udp\"}, {\"port\": 445, \"protocol\": \"udp\"}, {\"port\": 447, \"protocol\": \"udp\"}, {\"port\": 449, \"protocol\": \"udp\"}, {\"port\": 451, \"protocol\": \"udp\"}, {\"port\": 453, \"protocol\": \"udp\"}, {\"port\": 455, \"protocol\": \"udp\"}, {\"port\": 457, \"protocol\": \"udp\"}, {\"port\": 459, \"protocol\": \"udp\"}, {\"port\": 461, \"protocol\": \"udp\"}, {\"port\": 463, \"protocol\": \"udp\"}, {\"port\": 465, \"protocol\": \"udp\"}, {\"port\": 467, \"protocol\": \"udp\"}, {\"port\": 469, \"protocol\": \"udp\"}, {\"port\": 471, \"protocol\": \"udp\"}, {\"port\": 473, \"protocol\": \"udp\"}, {\"port\": 475, \"protocol\": \"udp\"}, {\"port\": 477, \"protocol\": \"udp\"}, {\"port\": 479, \"protocol\": \"udp\"}, {\"port\": 481, \"protocol\": \"udp\"}, {\"port\": 483, \"protocol\": \"udp\"}, {\"port\": 485, \"protocol\": \"udp\"}, {\"port\": 487, \"protocol\": \"udp\"}, {\"port\": 489, \"protocol\": \"udp\"}, {\"port\": 491, \"protocol\": \"udp\"}, {\"port\": 493, \"protocol\": \"udp\"}, {\"port\": 495, \"protocol\": \"udp\"}, {\"port\": 497, \"protocol\": \"udp\"}, {\"port\": 499, \"protocol\": \"udp\"}, {\"port\": 501, \"protocol\": \"udp\"}, {\"port\": 503, \"protocol\": \"udp\"}, {\"port\": 505, \"protocol\": \"udp\"}, {\"port\": 507, \"protocol\": \"udp\"}, {\"port\": 509, \"protocol\": \"udp\"}, {\"port\": 511, \"protocol\": \"udp\"}]}]}"}],"expected":{"allow":["192.0.2.1/32:udp:1","192.0.2.1/32:udp:3","192.0.2.1/32:udp:5","192.0.2.1/32:udp:7","192.0.2.1/32:udp:9","192.0.2.1/32:udp:11","192.0.2.1/32:udp:13","192.0.2.1/32:udp:15","192.0.2.1/32:udp:17","192.0.2.1/32:udp:19","192.0.2.1/32:udp:21","192.0.2.1/32:udp:23","192.0.2.1/32:udp:25","192.0.2.1/32:udp:27","192.0.2.1/32:udp:29","192.0.2.1/32:udp:31","192.0.2.1/32:udp:33","192.0.2.1/32:udp:35","192.0.2.1/32:udp:37","192.0.2.1/32:udp:39","192.0.2.1/32:udp:41","192.0.2.1/32:udp:43","192.0.2.1/32:udp:45","192.0.2.1/32:udp:47","192.0.2.1/32:udp:49","192.0.2.1/32:udp:51","192.0.2.1/32:udp:53","192.0.2.1/32:udp:55","192.0.2.1/32:udp:57","192.0.2.1/32:udp:59","192.0.2.1/32:udp:61","192.0.2.1/32:udp:63","192.0.2.1/32:udp:65","192.0.2.1/32:udp:67","192.0.2.1/32:udp:69","192.0.2.1/32:udp:71","192.0.2.1/32:udp:73","192.0.2.1/32:udp:75","192.0.2.1/32:udp:77","192.0.2.1/32:udp:79","192.0.2.1/32:udp:81","192.0.2.1/32:udp:83","192.0.2.1/32:udp:85","192.0.2.1/32:udp:87","192.0.2.1/32:udp:89","192.0.2.1/32:udp:91","192.0.2.1/32:udp:93","192.0.2.1/32:udp:95","192.0.2.1/32:udp:97","192.0.2.1/32:udp:99","192.0.2.1/32:udp:101","192.0.2.1/32:udp:103","192.0.2.1/32:udp:105","192.0.2.1/32:udp:107","192.0.2.1/32:udp:109","192.0.2.1/32:udp:111","192.0.2.1/32:udp:113","192.0.2.1/32:udp:115","192.0.2.1/32:udp:117","192.0.2.1/32:udp:119","192.0.2.1/32:udp:121","192.0.2.1/32:udp:123","192.0.2.1/32:udp:125","192.0.2.1/32:udp:127","192.0.2.1/32:udp:129","192.0.2.1/32:udp:131","192.0.2.1/32:udp:133","192.0.2.1/32:udp:135","192.0.2.1/32:udp:137","192.0.2.1/32:udp:139","192.0.2.1/32:udp:141","192.0.2.1/32:udp:143","192.0.2.1/32:udp:145","192.0.2.1/32:udp:147","192.0.2.1/32:udp:149","192.0.2.1/32:udp:151","192.0.2.1/32:udp:153","192.0.2.1/32:udp:155","192.0.2.1/32:udp:157","192.0.2.1/32:udp:159","192.0.2.1/32:udp:161","192.0.2.1/32:udp:163","192.0.2.1/32:udp:165","192.0.2.1/32:udp:167","192.0.2.1/32:udp:169","192.0.2.1/32:udp:171","192.0.2.1/32:udp:173","192.0.2.1/32:udp:175","192.0.2.1/32:udp:177","192.0.2.1/32:udp:179","192.0.2.1/32:udp:181","192.0.2.1/32:udp:183","192.0.2.1/32:udp:185","192.0.2.1/32:udp:187","192.0.2.1/32:udp:189","192.0.2.1/32:udp:191","192.0.2.1/32:udp:193","192.0.2.1/32:udp:195","192.0.2.1/32:udp:197","192.0.2.1/32:udp:199","192.0.2.1/32:udp:201","192.0.2.1/32:udp:203","192.0.2.1/32:udp:205","192.0.2.1/32:udp:207","192.0.2.1/32:udp:209","192.0.2.1/32:udp:211","192.0.2.1/32:udp:213","192.0.2.1/32:udp:215","192.0.2.1/32:udp:217","192.0.2.1/32:udp:219","192.0.2.1/32:udp:221","192.0.2.1/32:udp:223","192.0.2.1/32:udp:225","192.0.2.1/32:udp:227","192.0.2.1/32:udp:229","192.0.2.1/32:udp:231","192.0.2.1/32:udp:233","192.0.2.1/32:udp:235","192.0.2.1/32:udp:237","192.0.2.1/32:udp:239","192.0.2.1/32:udp:241","192.0.2.1/32:udp:243","192.0.2.1/32:udp:245","192.0.2.1/32:udp:247","192.0.2.1/32:udp:249","192.0.2.1/32:udp:251","192.0.2.1/32:udp:253","192.0.2.1/32:udp:255","192.0.2.1/32:udp:257","192.0.2.1/32:udp:259","192.0.2.1/32:udp:261","192.0.2.1/32:udp:263","192.0.2.1/32:udp:265","192.0.2.1/32:udp:267","192.0.2.1/32:udp:269","192.0.2.1/32:udp:271","192.0.2.1/32:udp:273","192.0.2.1/32:udp:275","192.0.2.1/32:udp:277","192.0.2.1/32:udp:279","192.0.2.1/32:udp:281","192.0.2.1/32:udp:283","192.0.2.1/32:udp:285","192.0.2.1/32:udp:287","192.0.2.1/32:udp:289","192.0.2.1/32:udp:291","192.0.2.1/32:udp:293","192.0.2.1/32:udp:295","192.0.2.1/32:udp:297","192.0.2.1/32:udp:299","192.0.2.1/32:udp:301","192.0.2.1/32:udp:303","192.0.2.1/32:udp:305","192.0.2.1/32:udp:307","192.0.2.1/32:udp:309","192.0.2.1/32:udp:311","192.0.2.1/32:udp:313","192.0.2.1/32:udp:315","192.0.2.1/32:udp:317","192.0.2.1/32:udp:319","192.0.2.1/32:udp:321","192.0.2.1/32:udp:323","192.0.2.1/32:udp:325","192.0.2.1/32:udp:327","192.0.2.1/32:udp:329","192.0.2.1/32:udp:331","192.0.2.1/32:udp:333","192.0.2.1/32:udp:335","192.0.2.1/32:udp:337","192.0.2.1/32:udp:339","192.0.2.1/32:udp:341","192.0.2.1/32:udp:343","192.0.2.1/32:udp:345","192.0.2.1/32:udp:347","192.0.2.1/32:udp:349","192.0.2.1/32:udp:351","192.0.2.1/32:udp:353","192.0.2.1/32:udp:355","192.0.2.1/32:udp:357","192.0.2.1/32:udp:359","192.0.2.1/32:udp:361","192.0.2.1/32:udp:363","192.0.2.1/32:udp:365","192.0.2.1/32:udp:367","192.0.2.1/32:udp:369","192.0.2.1/32:udp:371","192.0.2.1/32:udp:373","192.0.2.1/32:udp:375","192.0.2.1/32:udp:377","192.0.2.1/32:udp:379","192.0.2.1/32:udp:381","192.0.2.1/32:udp:383","192.0.2.1/32:udp:385","192.0.2.1/32:udp:387","192.0.2.1/32:udp:389","192.0.2.1/32:udp:391","192.0.2.1/32:udp:393","192.0.2.1/32:udp:395","192.0.2.1/32:udp:397","192.0.2.1/32:udp:399","192.0.2.1/32:udp:401","192.0.2.1/32:udp:403","192.0.2.1/32:udp:405","192.0.2.1/32:udp:407","192.0.2.1/32:udp:409","192.0.2.1/32:udp:411","192.0.2.1/32:udp:413","192.0.2.1/32:udp:415","192.0.2.1/32:udp:417","192.0.2.1/32:udp:419","192.0.2.1/32:udp:421","192.0.2.1/32:udp:423","192.0.2.1/32:udp:425","192.0.2.1/32:udp:427","192.0.2.1/32:udp:429","192.0.2.1/32:udp:431","192.0.2.1/32:udp:433","192.0.2.1/32:udp:435","192.0.2.1/32:udp:437","192.0.2.1/32:udp:439","192.0.2.1/32:udp:441","192.0.2.1/32:udp:443","192.0.2.1/32:udp:445","192.0.2.1/32:udp:447","192.0.2.1/32:udp:449","192.0.2.1/32:udp:451","192.0.2.1/32:udp:453","192.0.2.1/32:udp:455","192.0.2.1/32:udp:457","192.0.2.1/32:udp:459","192.0.2.1/32:udp:461","192.0.2.1/32:udp:463","192.0.2.1/32:udp:465","192.0.2.1/32:udp:467","192.0.2.1/32:udp:469","192.0.2.1/32:udp:471","192.0.2.1/32:udp:473","192.0.2.1/32:udp:475","192.0.2.1/32:udp:477","192.0.2.1/32:udp:479","192.0.2.1/32:udp:481","192.0.2.1/32:udp:483","192.0.2.1/32:udp:485","192.0.2.1/32:udp:487","192.0.2.1/32:udp:489","192.0.2.1/32:udp:491","192.0.2.1/32:udp:493","192.0.2.1/32:udp:495","192.0.2.1/32:udp:497","192.0.2.1/32:udp:499","192.0.2.1/32:udp:501","192.0.2.1/32:udp:503","192.0.2.1/32:udp:505","192.0.2.1/32:udp:507","192.0.2.1/32:udp:509","192.0.2.1/32:udp:511"],"deny":[]}}, + {"name":"limit: 257 separate ports","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.2.1\"}], \"ports\": [{\"port\": 1, \"protocol\": \"udp\"}, {\"port\": 3, \"protocol\": \"udp\"}, {\"port\": 5, \"protocol\": \"udp\"}, {\"port\": 7, \"protocol\": \"udp\"}, {\"port\": 9, \"protocol\": \"udp\"}, {\"port\": 11, \"protocol\": \"udp\"}, {\"port\": 13, \"protocol\": \"udp\"}, {\"port\": 15, \"protocol\": \"udp\"}, {\"port\": 17, \"protocol\": \"udp\"}, {\"port\": 19, \"protocol\": \"udp\"}, {\"port\": 21, \"protocol\": \"udp\"}, {\"port\": 23, \"protocol\": \"udp\"}, {\"port\": 25, \"protocol\": \"udp\"}, {\"port\": 27, \"protocol\": \"udp\"}, {\"port\": 29, \"protocol\": \"udp\"}, {\"port\": 31, \"protocol\": \"udp\"}, {\"port\": 33, \"protocol\": \"udp\"}, {\"port\": 35, \"protocol\": \"udp\"}, {\"port\": 37, \"protocol\": \"udp\"}, {\"port\": 39, \"protocol\": \"udp\"}, {\"port\": 41, \"protocol\": \"udp\"}, {\"port\": 43, \"protocol\": \"udp\"}, {\"port\": 45, \"protocol\": \"udp\"}, {\"port\": 47, \"protocol\": \"udp\"}, {\"port\": 49, \"protocol\": \"udp\"}, {\"port\": 51, \"protocol\": \"udp\"}, {\"port\": 53, \"protocol\": \"udp\"}, {\"port\": 55, \"protocol\": \"udp\"}, {\"port\": 57, \"protocol\": \"udp\"}, {\"port\": 59, \"protocol\": \"udp\"}, {\"port\": 61, \"protocol\": \"udp\"}, {\"port\": 63, \"protocol\": \"udp\"}, {\"port\": 65, \"protocol\": \"udp\"}, {\"port\": 67, \"protocol\": \"udp\"}, {\"port\": 69, \"protocol\": \"udp\"}, {\"port\": 71, \"protocol\": \"udp\"}, {\"port\": 73, \"protocol\": \"udp\"}, {\"port\": 75, \"protocol\": \"udp\"}, {\"port\": 77, \"protocol\": \"udp\"}, {\"port\": 79, \"protocol\": \"udp\"}, {\"port\": 81, \"protocol\": \"udp\"}, {\"port\": 83, \"protocol\": \"udp\"}, {\"port\": 85, \"protocol\": \"udp\"}, {\"port\": 87, \"protocol\": \"udp\"}, {\"port\": 89, \"protocol\": \"udp\"}, {\"port\": 91, \"protocol\": \"udp\"}, {\"port\": 93, \"protocol\": \"udp\"}, {\"port\": 95, \"protocol\": \"udp\"}, {\"port\": 97, \"protocol\": \"udp\"}, {\"port\": 99, \"protocol\": \"udp\"}, {\"port\": 101, \"protocol\": \"udp\"}, {\"port\": 103, \"protocol\": \"udp\"}, {\"port\": 105, \"protocol\": \"udp\"}, {\"port\": 107, \"protocol\": \"udp\"}, {\"port\": 109, \"protocol\": \"udp\"}, {\"port\": 111, \"protocol\": \"udp\"}, {\"port\": 113, \"protocol\": \"udp\"}, {\"port\": 115, \"protocol\": \"udp\"}, {\"port\": 117, \"protocol\": \"udp\"}, {\"port\": 119, \"protocol\": \"udp\"}, {\"port\": 121, \"protocol\": \"udp\"}, {\"port\": 123, \"protocol\": \"udp\"}, {\"port\": 125, \"protocol\": \"udp\"}, {\"port\": 127, \"protocol\": \"udp\"}, {\"port\": 129, \"protocol\": \"udp\"}, {\"port\": 131, \"protocol\": \"udp\"}, {\"port\": 133, \"protocol\": \"udp\"}, {\"port\": 135, \"protocol\": \"udp\"}, {\"port\": 137, \"protocol\": \"udp\"}, {\"port\": 139, \"protocol\": \"udp\"}, {\"port\": 141, \"protocol\": \"udp\"}, {\"port\": 143, \"protocol\": \"udp\"}, {\"port\": 145, \"protocol\": \"udp\"}, {\"port\": 147, \"protocol\": \"udp\"}, {\"port\": 149, \"protocol\": \"udp\"}, {\"port\": 151, \"protocol\": \"udp\"}, {\"port\": 153, \"protocol\": \"udp\"}, {\"port\": 155, \"protocol\": \"udp\"}, {\"port\": 157, \"protocol\": \"udp\"}, {\"port\": 159, \"protocol\": \"udp\"}, {\"port\": 161, \"protocol\": \"udp\"}, {\"port\": 163, \"protocol\": \"udp\"}, {\"port\": 165, \"protocol\": \"udp\"}, {\"port\": 167, \"protocol\": \"udp\"}, {\"port\": 169, \"protocol\": \"udp\"}, {\"port\": 171, \"protocol\": \"udp\"}, {\"port\": 173, \"protocol\": \"udp\"}, {\"port\": 175, \"protocol\": \"udp\"}, {\"port\": 177, \"protocol\": \"udp\"}, {\"port\": 179, \"protocol\": \"udp\"}, {\"port\": 181, \"protocol\": \"udp\"}, {\"port\": 183, \"protocol\": \"udp\"}, {\"port\": 185, \"protocol\": \"udp\"}, {\"port\": 187, \"protocol\": \"udp\"}, {\"port\": 189, \"protocol\": \"udp\"}, {\"port\": 191, \"protocol\": \"udp\"}, {\"port\": 193, \"protocol\": \"udp\"}, {\"port\": 195, \"protocol\": \"udp\"}, {\"port\": 197, \"protocol\": \"udp\"}, {\"port\": 199, \"protocol\": \"udp\"}, {\"port\": 201, \"protocol\": \"udp\"}, {\"port\": 203, \"protocol\": \"udp\"}, {\"port\": 205, \"protocol\": \"udp\"}, {\"port\": 207, \"protocol\": \"udp\"}, {\"port\": 209, \"protocol\": \"udp\"}, {\"port\": 211, \"protocol\": \"udp\"}, {\"port\": 213, \"protocol\": \"udp\"}, {\"port\": 215, \"protocol\": \"udp\"}, {\"port\": 217, \"protocol\": \"udp\"}, {\"port\": 219, \"protocol\": \"udp\"}, {\"port\": 221, \"protocol\": \"udp\"}, {\"port\": 223, \"protocol\": \"udp\"}, {\"port\": 225, \"protocol\": \"udp\"}, {\"port\": 227, \"protocol\": \"udp\"}, {\"port\": 229, \"protocol\": \"udp\"}, {\"port\": 231, \"protocol\": \"udp\"}, {\"port\": 233, \"protocol\": \"udp\"}, {\"port\": 235, \"protocol\": \"udp\"}, {\"port\": 237, \"protocol\": \"udp\"}, {\"port\": 239, \"protocol\": \"udp\"}, {\"port\": 241, \"protocol\": \"udp\"}, {\"port\": 243, \"protocol\": \"udp\"}, {\"port\": 245, \"protocol\": \"udp\"}, {\"port\": 247, \"protocol\": \"udp\"}, {\"port\": 249, \"protocol\": \"udp\"}, {\"port\": 251, \"protocol\": \"udp\"}, {\"port\": 253, \"protocol\": \"udp\"}, {\"port\": 255, \"protocol\": \"udp\"}, {\"port\": 257, \"protocol\": \"udp\"}, {\"port\": 259, \"protocol\": \"udp\"}, {\"port\": 261, \"protocol\": \"udp\"}, {\"port\": 263, \"protocol\": \"udp\"}, {\"port\": 265, \"protocol\": \"udp\"}, {\"port\": 267, \"protocol\": \"udp\"}, {\"port\": 269, \"protocol\": \"udp\"}, {\"port\": 271, \"protocol\": \"udp\"}, {\"port\": 273, \"protocol\": \"udp\"}, {\"port\": 275, \"protocol\": \"udp\"}, {\"port\": 277, \"protocol\": \"udp\"}, {\"port\": 279, \"protocol\": \"udp\"}, {\"port\": 281, \"protocol\": \"udp\"}, {\"port\": 283, \"protocol\": \"udp\"}, {\"port\": 285, \"protocol\": \"udp\"}, {\"port\": 287, \"protocol\": \"udp\"}, {\"port\": 289, \"protocol\": \"udp\"}, {\"port\": 291, \"protocol\": \"udp\"}, {\"port\": 293, \"protocol\": \"udp\"}, {\"port\": 295, \"protocol\": \"udp\"}, {\"port\": 297, \"protocol\": \"udp\"}, {\"port\": 299, \"protocol\": \"udp\"}, {\"port\": 301, \"protocol\": \"udp\"}, {\"port\": 303, \"protocol\": \"udp\"}, {\"port\": 305, \"protocol\": \"udp\"}, {\"port\": 307, \"protocol\": \"udp\"}, {\"port\": 309, \"protocol\": \"udp\"}, {\"port\": 311, \"protocol\": \"udp\"}, {\"port\": 313, \"protocol\": \"udp\"}, {\"port\": 315, \"protocol\": \"udp\"}, {\"port\": 317, \"protocol\": \"udp\"}, {\"port\": 319, \"protocol\": \"udp\"}, {\"port\": 321, \"protocol\": \"udp\"}, {\"port\": 323, \"protocol\": \"udp\"}, {\"port\": 325, \"protocol\": \"udp\"}, {\"port\": 327, \"protocol\": \"udp\"}, {\"port\": 329, \"protocol\": \"udp\"}, {\"port\": 331, \"protocol\": \"udp\"}, {\"port\": 333, \"protocol\": \"udp\"}, {\"port\": 335, \"protocol\": \"udp\"}, {\"port\": 337, \"protocol\": \"udp\"}, {\"port\": 339, \"protocol\": \"udp\"}, {\"port\": 341, \"protocol\": \"udp\"}, {\"port\": 343, \"protocol\": \"udp\"}, {\"port\": 345, \"protocol\": \"udp\"}, {\"port\": 347, \"protocol\": \"udp\"}, {\"port\": 349, \"protocol\": \"udp\"}, {\"port\": 351, \"protocol\": \"udp\"}, {\"port\": 353, \"protocol\": \"udp\"}, {\"port\": 355, \"protocol\": \"udp\"}, {\"port\": 357, \"protocol\": \"udp\"}, {\"port\": 359, \"protocol\": \"udp\"}, {\"port\": 361, \"protocol\": \"udp\"}, {\"port\": 363, \"protocol\": \"udp\"}, {\"port\": 365, \"protocol\": \"udp\"}, {\"port\": 367, \"protocol\": \"udp\"}, {\"port\": 369, \"protocol\": \"udp\"}, {\"port\": 371, \"protocol\": \"udp\"}, {\"port\": 373, \"protocol\": \"udp\"}, {\"port\": 375, \"protocol\": \"udp\"}, {\"port\": 377, \"protocol\": \"udp\"}, {\"port\": 379, \"protocol\": \"udp\"}, {\"port\": 381, \"protocol\": \"udp\"}, {\"port\": 383, \"protocol\": \"udp\"}, {\"port\": 385, \"protocol\": \"udp\"}, {\"port\": 387, \"protocol\": \"udp\"}, {\"port\": 389, \"protocol\": \"udp\"}, {\"port\": 391, \"protocol\": \"udp\"}, {\"port\": 393, \"protocol\": \"udp\"}, {\"port\": 395, \"protocol\": \"udp\"}, {\"port\": 397, \"protocol\": \"udp\"}, {\"port\": 399, \"protocol\": \"udp\"}, {\"port\": 401, \"protocol\": \"udp\"}, {\"port\": 403, \"protocol\": \"udp\"}, {\"port\": 405, \"protocol\": \"udp\"}, {\"port\": 407, \"protocol\": \"udp\"}, {\"port\": 409, \"protocol\": \"udp\"}, {\"port\": 411, \"protocol\": \"udp\"}, {\"port\": 413, \"protocol\": \"udp\"}, {\"port\": 415, \"protocol\": \"udp\"}, {\"port\": 417, \"protocol\": \"udp\"}, {\"port\": 419, \"protocol\": \"udp\"}, {\"port\": 421, \"protocol\": \"udp\"}, {\"port\": 423, \"protocol\": \"udp\"}, {\"port\": 425, \"protocol\": \"udp\"}, {\"port\": 427, \"protocol\": \"udp\"}, {\"port\": 429, \"protocol\": \"udp\"}, {\"port\": 431, \"protocol\": \"udp\"}, {\"port\": 433, \"protocol\": \"udp\"}, {\"port\": 435, \"protocol\": \"udp\"}, {\"port\": 437, \"protocol\": \"udp\"}, {\"port\": 439, \"protocol\": \"udp\"}, {\"port\": 441, \"protocol\": \"udp\"}, {\"port\": 443, \"protocol\": \"udp\"}, {\"port\": 445, \"protocol\": \"udp\"}, {\"port\": 447, \"protocol\": \"udp\"}, {\"port\": 449, \"protocol\": \"udp\"}, {\"port\": 451, \"protocol\": \"udp\"}, {\"port\": 453, \"protocol\": \"udp\"}, {\"port\": 455, \"protocol\": \"udp\"}, {\"port\": 457, \"protocol\": \"udp\"}, {\"port\": 459, \"protocol\": \"udp\"}, {\"port\": 461, \"protocol\": \"udp\"}, {\"port\": 463, \"protocol\": \"udp\"}, {\"port\": 465, \"protocol\": \"udp\"}, {\"port\": 467, \"protocol\": \"udp\"}, {\"port\": 469, \"protocol\": \"udp\"}, {\"port\": 471, \"protocol\": \"udp\"}, {\"port\": 473, \"protocol\": \"udp\"}, {\"port\": 475, \"protocol\": \"udp\"}, {\"port\": 477, \"protocol\": \"udp\"}, {\"port\": 479, \"protocol\": \"udp\"}, {\"port\": 481, \"protocol\": \"udp\"}, {\"port\": 483, \"protocol\": \"udp\"}, {\"port\": 485, \"protocol\": \"udp\"}, {\"port\": 487, \"protocol\": \"udp\"}, {\"port\": 489, \"protocol\": \"udp\"}, {\"port\": 491, \"protocol\": \"udp\"}, {\"port\": 493, \"protocol\": \"udp\"}, {\"port\": 495, \"protocol\": \"udp\"}, {\"port\": 497, \"protocol\": \"udp\"}, {\"port\": 499, \"protocol\": \"udp\"}, {\"port\": 501, \"protocol\": \"udp\"}, {\"port\": 503, \"protocol\": \"udp\"}, {\"port\": 505, \"protocol\": \"udp\"}, {\"port\": 507, \"protocol\": \"udp\"}, {\"port\": 509, \"protocol\": \"udp\"}, {\"port\": 511, \"protocol\": \"udp\"}, {\"port\": 513, \"protocol\": \"udp\"}]}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"limit: 128 IPv4 and 128 IPv6 ports","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.1\"}], \"ports\": [{\"port\": 1}, {\"port\": 3}, {\"port\": 5}, {\"port\": 7}, {\"port\": 9}, {\"port\": 11}, {\"port\": 13}, {\"port\": 15}, {\"port\": 17}, {\"port\": 19}, {\"port\": 21}, {\"port\": 23}, {\"port\": 25}, {\"port\": 27}, {\"port\": 29}, {\"port\": 31}, {\"port\": 33}, {\"port\": 35}, {\"port\": 37}, {\"port\": 39}, {\"port\": 41}, {\"port\": 43}, {\"port\": 45}, {\"port\": 47}, {\"port\": 49}, {\"port\": 51}, {\"port\": 53}, {\"port\": 55}, {\"port\": 57}, {\"port\": 59}, {\"port\": 61}, {\"port\": 63}, {\"port\": 65}, {\"port\": 67}, {\"port\": 69}, {\"port\": 71}, {\"port\": 73}, {\"port\": 75}, {\"port\": 77}, {\"port\": 79}, {\"port\": 81}, {\"port\": 83}, {\"port\": 85}, {\"port\": 87}, {\"port\": 89}, {\"port\": 91}, {\"port\": 93}, {\"port\": 95}, {\"port\": 97}, {\"port\": 99}, {\"port\": 101}, {\"port\": 103}, {\"port\": 105}, {\"port\": 107}, {\"port\": 109}, {\"port\": 111}, {\"port\": 113}, {\"port\": 115}, {\"port\": 117}, {\"port\": 119}, {\"port\": 121}, {\"port\": 123}, {\"port\": 125}, {\"port\": 127}, {\"port\": 129}, {\"port\": 131}, {\"port\": 133}, {\"port\": 135}, {\"port\": 137}, {\"port\": 139}, {\"port\": 141}, {\"port\": 143}, {\"port\": 145}, {\"port\": 147}, {\"port\": 149}, {\"port\": 151}, {\"port\": 153}, {\"port\": 155}, {\"port\": 157}, {\"port\": 159}, {\"port\": 161}, {\"port\": 163}, {\"port\": 165}, {\"port\": 167}, {\"port\": 169}, {\"port\": 171}, {\"port\": 173}, {\"port\": 175}, {\"port\": 177}, {\"port\": 179}, {\"port\": 181}, {\"port\": 183}, {\"port\": 185}, {\"port\": 187}, {\"port\": 189}, {\"port\": 191}, {\"port\": 193}, {\"port\": 195}, {\"port\": 197}, {\"port\": 199}, {\"port\": 201}, {\"port\": 203}, {\"port\": 205}, {\"port\": 207}, {\"port\": 209}, {\"port\": 211}, {\"port\": 213}, {\"port\": 215}, {\"port\": 217}, {\"port\": 219}, {\"port\": 221}, {\"port\": 223}, {\"port\": 225}, {\"port\": 227}, {\"port\": 229}, {\"port\": 231}, {\"port\": 233}, {\"port\": 235}, {\"port\": 237}, {\"port\": 239}, {\"port\": 241}, {\"port\": 243}, {\"port\": 245}, {\"port\": 247}, {\"port\": 249}, {\"port\": 251}, {\"port\": 253}, {\"port\": 255}]}, {\"to\": [{\"cidr\": \"2001:db8::1\"}], \"ports\": [{\"port\": 1}, {\"port\": 3}, {\"port\": 5}, {\"port\": 7}, {\"port\": 9}, {\"port\": 11}, {\"port\": 13}, {\"port\": 15}, {\"port\": 17}, {\"port\": 19}, {\"port\": 21}, {\"port\": 23}, {\"port\": 25}, {\"port\": 27}, {\"port\": 29}, {\"port\": 31}, {\"port\": 33}, {\"port\": 35}, {\"port\": 37}, {\"port\": 39}, {\"port\": 41}, {\"port\": 43}, {\"port\": 45}, {\"port\": 47}, {\"port\": 49}, {\"port\": 51}, {\"port\": 53}, {\"port\": 55}, {\"port\": 57}, {\"port\": 59}, {\"port\": 61}, {\"port\": 63}, {\"port\": 65}, {\"port\": 67}, {\"port\": 69}, {\"port\": 71}, {\"port\": 73}, {\"port\": 75}, {\"port\": 77}, {\"port\": 79}, {\"port\": 81}, {\"port\": 83}, {\"port\": 85}, {\"port\": 87}, {\"port\": 89}, {\"port\": 91}, {\"port\": 93}, {\"port\": 95}, {\"port\": 97}, {\"port\": 99}, {\"port\": 101}, {\"port\": 103}, {\"port\": 105}, {\"port\": 107}, {\"port\": 109}, {\"port\": 111}, {\"port\": 113}, {\"port\": 115}, {\"port\": 117}, {\"port\": 119}, {\"port\": 121}, {\"port\": 123}, {\"port\": 125}, {\"port\": 127}, {\"port\": 129}, {\"port\": 131}, {\"port\": 133}, {\"port\": 135}, {\"port\": 137}, {\"port\": 139}, {\"port\": 141}, {\"port\": 143}, {\"port\": 145}, {\"port\": 147}, {\"port\": 149}, {\"port\": 151}, {\"port\": 153}, {\"port\": 155}, {\"port\": 157}, {\"port\": 159}, {\"port\": 161}, {\"port\": 163}, {\"port\": 165}, {\"port\": 167}, {\"port\": 169}, {\"port\": 171}, {\"port\": 173}, {\"port\": 175}, {\"port\": 177}, {\"port\": 179}, {\"port\": 181}, {\"port\": 183}, {\"port\": 185}, {\"port\": 187}, {\"port\": 189}, {\"port\": 191}, {\"port\": 193}, {\"port\": 195}, {\"port\": 197}, {\"port\": 199}, {\"port\": 201}, {\"port\": 203}, {\"port\": 205}, {\"port\": 207}, {\"port\": 209}, {\"port\": 211}, {\"port\": 213}, {\"port\": 215}, {\"port\": 217}, {\"port\": 219}, {\"port\": 221}, {\"port\": 223}, {\"port\": 225}, {\"port\": 227}, {\"port\": 229}, {\"port\": 231}, {\"port\": 233}, {\"port\": 235}, {\"port\": 237}, {\"port\": 239}, {\"port\": 241}, {\"port\": 243}, {\"port\": 245}, {\"port\": 247}, {\"port\": 249}, {\"port\": 251}, {\"port\": 253}, {\"port\": 255}]}]}"}],"expected":{"error":"deny emits at most 256 native rules"}}, + {"name":"limit: 129 IPv4 and 128 IPv6 ports","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.1\"}], \"ports\": [{\"port\": 1}, {\"port\": 3}, {\"port\": 5}, {\"port\": 7}, {\"port\": 9}, {\"port\": 11}, {\"port\": 13}, {\"port\": 15}, {\"port\": 17}, {\"port\": 19}, {\"port\": 21}, {\"port\": 23}, {\"port\": 25}, {\"port\": 27}, {\"port\": 29}, {\"port\": 31}, {\"port\": 33}, {\"port\": 35}, {\"port\": 37}, {\"port\": 39}, {\"port\": 41}, {\"port\": 43}, {\"port\": 45}, {\"port\": 47}, {\"port\": 49}, {\"port\": 51}, {\"port\": 53}, {\"port\": 55}, {\"port\": 57}, {\"port\": 59}, {\"port\": 61}, {\"port\": 63}, {\"port\": 65}, {\"port\": 67}, {\"port\": 69}, {\"port\": 71}, {\"port\": 73}, {\"port\": 75}, {\"port\": 77}, {\"port\": 79}, {\"port\": 81}, {\"port\": 83}, {\"port\": 85}, {\"port\": 87}, {\"port\": 89}, {\"port\": 91}, {\"port\": 93}, {\"port\": 95}, {\"port\": 97}, {\"port\": 99}, {\"port\": 101}, {\"port\": 103}, {\"port\": 105}, {\"port\": 107}, {\"port\": 109}, {\"port\": 111}, {\"port\": 113}, {\"port\": 115}, {\"port\": 117}, {\"port\": 119}, {\"port\": 121}, {\"port\": 123}, {\"port\": 125}, {\"port\": 127}, {\"port\": 129}, {\"port\": 131}, {\"port\": 133}, {\"port\": 135}, {\"port\": 137}, {\"port\": 139}, {\"port\": 141}, {\"port\": 143}, {\"port\": 145}, {\"port\": 147}, {\"port\": 149}, {\"port\": 151}, {\"port\": 153}, {\"port\": 155}, {\"port\": 157}, {\"port\": 159}, {\"port\": 161}, {\"port\": 163}, {\"port\": 165}, {\"port\": 167}, {\"port\": 169}, {\"port\": 171}, {\"port\": 173}, {\"port\": 175}, {\"port\": 177}, {\"port\": 179}, {\"port\": 181}, {\"port\": 183}, {\"port\": 185}, {\"port\": 187}, {\"port\": 189}, {\"port\": 191}, {\"port\": 193}, {\"port\": 195}, {\"port\": 197}, {\"port\": 199}, {\"port\": 201}, {\"port\": 203}, {\"port\": 205}, {\"port\": 207}, {\"port\": 209}, {\"port\": 211}, {\"port\": 213}, {\"port\": 215}, {\"port\": 217}, {\"port\": 219}, {\"port\": 221}, {\"port\": 223}, {\"port\": 225}, {\"port\": 227}, {\"port\": 229}, {\"port\": 231}, {\"port\": 233}, {\"port\": 235}, {\"port\": 237}, {\"port\": 239}, {\"port\": 241}, {\"port\": 243}, {\"port\": 245}, {\"port\": 247}, {\"port\": 249}, {\"port\": 251}, {\"port\": 253}, {\"port\": 255}, {\"port\": 257}]}, {\"to\": [{\"cidr\": \"2001:db8::1\"}], \"ports\": [{\"port\": 1}, {\"port\": 3}, {\"port\": 5}, {\"port\": 7}, {\"port\": 9}, {\"port\": 11}, {\"port\": 13}, {\"port\": 15}, {\"port\": 17}, {\"port\": 19}, {\"port\": 21}, {\"port\": 23}, {\"port\": 25}, {\"port\": 27}, {\"port\": 29}, {\"port\": 31}, {\"port\": 33}, {\"port\": 35}, {\"port\": 37}, {\"port\": 39}, {\"port\": 41}, {\"port\": 43}, {\"port\": 45}, {\"port\": 47}, {\"port\": 49}, {\"port\": 51}, {\"port\": 53}, {\"port\": 55}, {\"port\": 57}, {\"port\": 59}, {\"port\": 61}, {\"port\": 63}, {\"port\": 65}, {\"port\": 67}, {\"port\": 69}, {\"port\": 71}, {\"port\": 73}, {\"port\": 75}, {\"port\": 77}, {\"port\": 79}, {\"port\": 81}, {\"port\": 83}, {\"port\": 85}, {\"port\": 87}, {\"port\": 89}, {\"port\": 91}, {\"port\": 93}, {\"port\": 95}, {\"port\": 97}, {\"port\": 99}, {\"port\": 101}, {\"port\": 103}, {\"port\": 105}, {\"port\": 107}, {\"port\": 109}, {\"port\": 111}, {\"port\": 113}, {\"port\": 115}, {\"port\": 117}, {\"port\": 119}, {\"port\": 121}, {\"port\": 123}, {\"port\": 125}, {\"port\": 127}, {\"port\": 129}, {\"port\": 131}, {\"port\": 133}, {\"port\": 135}, {\"port\": 137}, {\"port\": 139}, {\"port\": 141}, {\"port\": 143}, {\"port\": 145}, {\"port\": 147}, {\"port\": 149}, {\"port\": 151}, {\"port\": 153}, {\"port\": 155}, {\"port\": 157}, {\"port\": 159}, {\"port\": 161}, {\"port\": 163}, {\"port\": 165}, {\"port\": 167}, {\"port\": 169}, {\"port\": 171}, {\"port\": 173}, {\"port\": 175}, {\"port\": 177}, {\"port\": 179}, {\"port\": 181}, {\"port\": 183}, {\"port\": 185}, {\"port\": 187}, {\"port\": 189}, {\"port\": 191}, {\"port\": 193}, {\"port\": 195}, {\"port\": 197}, {\"port\": 199}, {\"port\": 201}, {\"port\": 203}, {\"port\": 205}, {\"port\": 207}, {\"port\": 209}, {\"port\": 211}, {\"port\": 213}, {\"port\": 215}, {\"port\": 217}, {\"port\": 219}, {\"port\": 221}, {\"port\": 223}, {\"port\": 225}, {\"port\": 227}, {\"port\": 229}, {\"port\": 231}, {\"port\": 233}, {\"port\": 235}, {\"port\": 237}, {\"port\": 239}, {\"port\": 241}, {\"port\": 243}, {\"port\": 245}, {\"port\": 247}, {\"port\": 249}, {\"port\": 251}, {\"port\": 253}, {\"port\": 255}]}]}"}],"expected":{"error":"deny emits at most 256 native rules"}}, + {"name":"limit: 128 IPv4 and 129 IPv6 ports","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.1\"}], \"ports\": [{\"port\": 1}, {\"port\": 3}, {\"port\": 5}, {\"port\": 7}, {\"port\": 9}, {\"port\": 11}, {\"port\": 13}, {\"port\": 15}, {\"port\": 17}, {\"port\": 19}, {\"port\": 21}, {\"port\": 23}, {\"port\": 25}, {\"port\": 27}, {\"port\": 29}, {\"port\": 31}, {\"port\": 33}, {\"port\": 35}, {\"port\": 37}, {\"port\": 39}, {\"port\": 41}, {\"port\": 43}, {\"port\": 45}, {\"port\": 47}, {\"port\": 49}, {\"port\": 51}, {\"port\": 53}, {\"port\": 55}, {\"port\": 57}, {\"port\": 59}, {\"port\": 61}, {\"port\": 63}, {\"port\": 65}, {\"port\": 67}, {\"port\": 69}, {\"port\": 71}, {\"port\": 73}, {\"port\": 75}, {\"port\": 77}, {\"port\": 79}, {\"port\": 81}, {\"port\": 83}, {\"port\": 85}, {\"port\": 87}, {\"port\": 89}, {\"port\": 91}, {\"port\": 93}, {\"port\": 95}, {\"port\": 97}, {\"port\": 99}, {\"port\": 101}, {\"port\": 103}, {\"port\": 105}, {\"port\": 107}, {\"port\": 109}, {\"port\": 111}, {\"port\": 113}, {\"port\": 115}, {\"port\": 117}, {\"port\": 119}, {\"port\": 121}, {\"port\": 123}, {\"port\": 125}, {\"port\": 127}, {\"port\": 129}, {\"port\": 131}, {\"port\": 133}, {\"port\": 135}, {\"port\": 137}, {\"port\": 139}, {\"port\": 141}, {\"port\": 143}, {\"port\": 145}, {\"port\": 147}, {\"port\": 149}, {\"port\": 151}, {\"port\": 153}, {\"port\": 155}, {\"port\": 157}, {\"port\": 159}, {\"port\": 161}, {\"port\": 163}, {\"port\": 165}, {\"port\": 167}, {\"port\": 169}, {\"port\": 171}, {\"port\": 173}, {\"port\": 175}, {\"port\": 177}, {\"port\": 179}, {\"port\": 181}, {\"port\": 183}, {\"port\": 185}, {\"port\": 187}, {\"port\": 189}, {\"port\": 191}, {\"port\": 193}, {\"port\": 195}, {\"port\": 197}, {\"port\": 199}, {\"port\": 201}, {\"port\": 203}, {\"port\": 205}, {\"port\": 207}, {\"port\": 209}, {\"port\": 211}, {\"port\": 213}, {\"port\": 215}, {\"port\": 217}, {\"port\": 219}, {\"port\": 221}, {\"port\": 223}, {\"port\": 225}, {\"port\": 227}, {\"port\": 229}, {\"port\": 231}, {\"port\": 233}, {\"port\": 235}, {\"port\": 237}, {\"port\": 239}, {\"port\": 241}, {\"port\": 243}, {\"port\": 245}, {\"port\": 247}, {\"port\": 249}, {\"port\": 251}, {\"port\": 253}, {\"port\": 255}]}, {\"to\": [{\"cidr\": \"2001:db8::1\"}], \"ports\": [{\"port\": 1}, {\"port\": 3}, {\"port\": 5}, {\"port\": 7}, {\"port\": 9}, {\"port\": 11}, {\"port\": 13}, {\"port\": 15}, {\"port\": 17}, {\"port\": 19}, {\"port\": 21}, {\"port\": 23}, {\"port\": 25}, {\"port\": 27}, {\"port\": 29}, {\"port\": 31}, {\"port\": 33}, {\"port\": 35}, {\"port\": 37}, {\"port\": 39}, {\"port\": 41}, {\"port\": 43}, {\"port\": 45}, {\"port\": 47}, {\"port\": 49}, {\"port\": 51}, {\"port\": 53}, {\"port\": 55}, {\"port\": 57}, {\"port\": 59}, {\"port\": 61}, {\"port\": 63}, {\"port\": 65}, {\"port\": 67}, {\"port\": 69}, {\"port\": 71}, {\"port\": 73}, {\"port\": 75}, {\"port\": 77}, {\"port\": 79}, {\"port\": 81}, {\"port\": 83}, {\"port\": 85}, {\"port\": 87}, {\"port\": 89}, {\"port\": 91}, {\"port\": 93}, {\"port\": 95}, {\"port\": 97}, {\"port\": 99}, {\"port\": 101}, {\"port\": 103}, {\"port\": 105}, {\"port\": 107}, {\"port\": 109}, {\"port\": 111}, {\"port\": 113}, {\"port\": 115}, {\"port\": 117}, {\"port\": 119}, {\"port\": 121}, {\"port\": 123}, {\"port\": 125}, {\"port\": 127}, {\"port\": 129}, {\"port\": 131}, {\"port\": 133}, {\"port\": 135}, {\"port\": 137}, {\"port\": 139}, {\"port\": 141}, {\"port\": 143}, {\"port\": 145}, {\"port\": 147}, {\"port\": 149}, {\"port\": 151}, {\"port\": 153}, {\"port\": 155}, {\"port\": 157}, {\"port\": 159}, {\"port\": 161}, {\"port\": 163}, {\"port\": 165}, {\"port\": 167}, {\"port\": 169}, {\"port\": 171}, {\"port\": 173}, {\"port\": 175}, {\"port\": 177}, {\"port\": 179}, {\"port\": 181}, {\"port\": 183}, {\"port\": 185}, {\"port\": 187}, {\"port\": 189}, {\"port\": 191}, {\"port\": 193}, {\"port\": 195}, {\"port\": 197}, {\"port\": 199}, {\"port\": 201}, {\"port\": 203}, {\"port\": 205}, {\"port\": 207}, {\"port\": 209}, {\"port\": 211}, {\"port\": 213}, {\"port\": 215}, {\"port\": 217}, {\"port\": 219}, {\"port\": 221}, {\"port\": 223}, {\"port\": 225}, {\"port\": 227}, {\"port\": 229}, {\"port\": 231}, {\"port\": 233}, {\"port\": 235}, {\"port\": 237}, {\"port\": 239}, {\"port\": 241}, {\"port\": 243}, {\"port\": 245}, {\"port\": 247}, {\"port\": 249}, {\"port\": 251}, {\"port\": 253}, {\"port\": 255}, {\"port\": 257}]}]}"}],"expected":{"error":"deny emits at most 256 native rules"}}, + {"name":"limit: 200 IPv4 and 56 IPv6 ports","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"192.0.2.1\"}], \"ports\": [{\"port\": 1}, {\"port\": 3}, {\"port\": 5}, {\"port\": 7}, {\"port\": 9}, {\"port\": 11}, {\"port\": 13}, {\"port\": 15}, {\"port\": 17}, {\"port\": 19}, {\"port\": 21}, {\"port\": 23}, {\"port\": 25}, {\"port\": 27}, {\"port\": 29}, {\"port\": 31}, {\"port\": 33}, {\"port\": 35}, {\"port\": 37}, {\"port\": 39}, {\"port\": 41}, {\"port\": 43}, {\"port\": 45}, {\"port\": 47}, {\"port\": 49}, {\"port\": 51}, {\"port\": 53}, {\"port\": 55}, {\"port\": 57}, {\"port\": 59}, {\"port\": 61}, {\"port\": 63}, {\"port\": 65}, {\"port\": 67}, {\"port\": 69}, {\"port\": 71}, {\"port\": 73}, {\"port\": 75}, {\"port\": 77}, {\"port\": 79}, {\"port\": 81}, {\"port\": 83}, {\"port\": 85}, {\"port\": 87}, {\"port\": 89}, {\"port\": 91}, {\"port\": 93}, {\"port\": 95}, {\"port\": 97}, {\"port\": 99}, {\"port\": 101}, {\"port\": 103}, {\"port\": 105}, {\"port\": 107}, {\"port\": 109}, {\"port\": 111}, {\"port\": 113}, {\"port\": 115}, {\"port\": 117}, {\"port\": 119}, {\"port\": 121}, {\"port\": 123}, {\"port\": 125}, {\"port\": 127}, {\"port\": 129}, {\"port\": 131}, {\"port\": 133}, {\"port\": 135}, {\"port\": 137}, {\"port\": 139}, {\"port\": 141}, {\"port\": 143}, {\"port\": 145}, {\"port\": 147}, {\"port\": 149}, {\"port\": 151}, {\"port\": 153}, {\"port\": 155}, {\"port\": 157}, {\"port\": 159}, {\"port\": 161}, {\"port\": 163}, {\"port\": 165}, {\"port\": 167}, {\"port\": 169}, {\"port\": 171}, {\"port\": 173}, {\"port\": 175}, {\"port\": 177}, {\"port\": 179}, {\"port\": 181}, {\"port\": 183}, {\"port\": 185}, {\"port\": 187}, {\"port\": 189}, {\"port\": 191}, {\"port\": 193}, {\"port\": 195}, {\"port\": 197}, {\"port\": 199}, {\"port\": 201}, {\"port\": 203}, {\"port\": 205}, {\"port\": 207}, {\"port\": 209}, {\"port\": 211}, {\"port\": 213}, {\"port\": 215}, {\"port\": 217}, {\"port\": 219}, {\"port\": 221}, {\"port\": 223}, {\"port\": 225}, {\"port\": 227}, {\"port\": 229}, {\"port\": 231}, {\"port\": 233}, {\"port\": 235}, {\"port\": 237}, {\"port\": 239}, {\"port\": 241}, {\"port\": 243}, {\"port\": 245}, {\"port\": 247}, {\"port\": 249}, {\"port\": 251}, {\"port\": 253}, {\"port\": 255}, {\"port\": 257}, {\"port\": 259}, {\"port\": 261}, {\"port\": 263}, {\"port\": 265}, {\"port\": 267}, {\"port\": 269}, {\"port\": 271}, {\"port\": 273}, {\"port\": 275}, {\"port\": 277}, {\"port\": 279}, {\"port\": 281}, {\"port\": 283}, {\"port\": 285}, {\"port\": 287}, {\"port\": 289}, {\"port\": 291}, {\"port\": 293}, {\"port\": 295}, {\"port\": 297}, {\"port\": 299}, {\"port\": 301}, {\"port\": 303}, {\"port\": 305}, {\"port\": 307}, {\"port\": 309}, {\"port\": 311}, {\"port\": 313}, {\"port\": 315}, {\"port\": 317}, {\"port\": 319}, {\"port\": 321}, {\"port\": 323}, {\"port\": 325}, {\"port\": 327}, {\"port\": 329}, {\"port\": 331}, {\"port\": 333}, {\"port\": 335}, {\"port\": 337}, {\"port\": 339}, {\"port\": 341}, {\"port\": 343}, {\"port\": 345}, {\"port\": 347}, {\"port\": 349}, {\"port\": 351}, {\"port\": 353}, {\"port\": 355}, {\"port\": 357}, {\"port\": 359}, {\"port\": 361}, {\"port\": 363}, {\"port\": 365}, {\"port\": 367}, {\"port\": 369}, {\"port\": 371}, {\"port\": 373}, {\"port\": 375}, {\"port\": 377}, {\"port\": 379}, {\"port\": 381}, {\"port\": 383}, {\"port\": 385}, {\"port\": 387}, {\"port\": 389}, {\"port\": 391}, {\"port\": 393}, {\"port\": 395}, {\"port\": 397}, {\"port\": 399}]}, {\"to\": [{\"cidr\": \"2001:db8::1\"}], \"ports\": [{\"port\": 1}, {\"port\": 3}, {\"port\": 5}, {\"port\": 7}, {\"port\": 9}, {\"port\": 11}, {\"port\": 13}, {\"port\": 15}, {\"port\": 17}, {\"port\": 19}, {\"port\": 21}, {\"port\": 23}, {\"port\": 25}, {\"port\": 27}, {\"port\": 29}, {\"port\": 31}, {\"port\": 33}, {\"port\": 35}, {\"port\": 37}, {\"port\": 39}, {\"port\": 41}, {\"port\": 43}, {\"port\": 45}, {\"port\": 47}, {\"port\": 49}, {\"port\": 51}, {\"port\": 53}, {\"port\": 55}, {\"port\": 57}, {\"port\": 59}, {\"port\": 61}, {\"port\": 63}, {\"port\": 65}, {\"port\": 67}, {\"port\": 69}, {\"port\": 71}, {\"port\": 73}, {\"port\": 75}, {\"port\": 77}, {\"port\": 79}, {\"port\": 81}, {\"port\": 83}, {\"port\": 85}, {\"port\": 87}, {\"port\": 89}, {\"port\": 91}, {\"port\": 93}, {\"port\": 95}, {\"port\": 97}, {\"port\": 99}, {\"port\": 101}, {\"port\": 103}, {\"port\": 105}, {\"port\": 107}, {\"port\": 109}, {\"port\": 111}]}]}"}],"expected":{"error":"deny emits at most 256 native rules"}}, + {"name":"limit: 0 IPv4 and 257 IPv6 ports","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::1\"}], \"ports\": [{\"port\": 1}, {\"port\": 3}, {\"port\": 5}, {\"port\": 7}, {\"port\": 9}, {\"port\": 11}, {\"port\": 13}, {\"port\": 15}, {\"port\": 17}, {\"port\": 19}, {\"port\": 21}, {\"port\": 23}, {\"port\": 25}, {\"port\": 27}, {\"port\": 29}, {\"port\": 31}, {\"port\": 33}, {\"port\": 35}, {\"port\": 37}, {\"port\": 39}, {\"port\": 41}, {\"port\": 43}, {\"port\": 45}, {\"port\": 47}, {\"port\": 49}, {\"port\": 51}, {\"port\": 53}, {\"port\": 55}, {\"port\": 57}, {\"port\": 59}, {\"port\": 61}, {\"port\": 63}, {\"port\": 65}, {\"port\": 67}, {\"port\": 69}, {\"port\": 71}, {\"port\": 73}, {\"port\": 75}, {\"port\": 77}, {\"port\": 79}, {\"port\": 81}, {\"port\": 83}, {\"port\": 85}, {\"port\": 87}, {\"port\": 89}, {\"port\": 91}, {\"port\": 93}, {\"port\": 95}, {\"port\": 97}, {\"port\": 99}, {\"port\": 101}, {\"port\": 103}, {\"port\": 105}, {\"port\": 107}, {\"port\": 109}, {\"port\": 111}, {\"port\": 113}, {\"port\": 115}, {\"port\": 117}, {\"port\": 119}, {\"port\": 121}, {\"port\": 123}, {\"port\": 125}, {\"port\": 127}, {\"port\": 129}, {\"port\": 131}, {\"port\": 133}, {\"port\": 135}, {\"port\": 137}, {\"port\": 139}, {\"port\": 141}, {\"port\": 143}, {\"port\": 145}, {\"port\": 147}, {\"port\": 149}, {\"port\": 151}, {\"port\": 153}, {\"port\": 155}, {\"port\": 157}, {\"port\": 159}, {\"port\": 161}, {\"port\": 163}, {\"port\": 165}, {\"port\": 167}, {\"port\": 169}, {\"port\": 171}, {\"port\": 173}, {\"port\": 175}, {\"port\": 177}, {\"port\": 179}, {\"port\": 181}, {\"port\": 183}, {\"port\": 185}, {\"port\": 187}, {\"port\": 189}, {\"port\": 191}, {\"port\": 193}, {\"port\": 195}, {\"port\": 197}, {\"port\": 199}, {\"port\": 201}, {\"port\": 203}, {\"port\": 205}, {\"port\": 207}, {\"port\": 209}, {\"port\": 211}, {\"port\": 213}, {\"port\": 215}, {\"port\": 217}, {\"port\": 219}, {\"port\": 221}, {\"port\": 223}, {\"port\": 225}, {\"port\": 227}, {\"port\": 229}, {\"port\": 231}, {\"port\": 233}, {\"port\": 235}, {\"port\": 237}, {\"port\": 239}, {\"port\": 241}, {\"port\": 243}, {\"port\": 245}, {\"port\": 247}, {\"port\": 249}, {\"port\": 251}, {\"port\": 253}, {\"port\": 255}, {\"port\": 257}, {\"port\": 259}, {\"port\": 261}, {\"port\": 263}, {\"port\": 265}, {\"port\": 267}, {\"port\": 269}, {\"port\": 271}, {\"port\": 273}, {\"port\": 275}, {\"port\": 277}, {\"port\": 279}, {\"port\": 281}, {\"port\": 283}, {\"port\": 285}, {\"port\": 287}, {\"port\": 289}, {\"port\": 291}, {\"port\": 293}, {\"port\": 295}, {\"port\": 297}, {\"port\": 299}, {\"port\": 301}, {\"port\": 303}, {\"port\": 305}, {\"port\": 307}, {\"port\": 309}, {\"port\": 311}, {\"port\": 313}, {\"port\": 315}, {\"port\": 317}, {\"port\": 319}, {\"port\": 321}, {\"port\": 323}, {\"port\": 325}, {\"port\": 327}, {\"port\": 329}, {\"port\": 331}, {\"port\": 333}, {\"port\": 335}, {\"port\": 337}, {\"port\": 339}, {\"port\": 341}, {\"port\": 343}, {\"port\": 345}, {\"port\": 347}, {\"port\": 349}, {\"port\": 351}, {\"port\": 353}, {\"port\": 355}, {\"port\": 357}, {\"port\": 359}, {\"port\": 361}, {\"port\": 363}, {\"port\": 365}, {\"port\": 367}, {\"port\": 369}, {\"port\": 371}, {\"port\": 373}, {\"port\": 375}, {\"port\": 377}, {\"port\": 379}, {\"port\": 381}, {\"port\": 383}, {\"port\": 385}, {\"port\": 387}, {\"port\": 389}, {\"port\": 391}, {\"port\": 393}, {\"port\": 395}, {\"port\": 397}, {\"port\": 399}, {\"port\": 401}, {\"port\": 403}, {\"port\": 405}, {\"port\": 407}, {\"port\": 409}, {\"port\": 411}, {\"port\": 413}, {\"port\": 415}, {\"port\": 417}, {\"port\": 419}, {\"port\": 421}, {\"port\": 423}, {\"port\": 425}, {\"port\": 427}, {\"port\": 429}, {\"port\": 431}, {\"port\": 433}, {\"port\": 435}, {\"port\": 437}, {\"port\": 439}, {\"port\": 441}, {\"port\": 443}, {\"port\": 445}, {\"port\": 447}, {\"port\": 449}, {\"port\": 451}, {\"port\": 453}, {\"port\": 455}, {\"port\": 457}, {\"port\": 459}, {\"port\": 461}, {\"port\": 463}, {\"port\": 465}, {\"port\": 467}, {\"port\": 469}, {\"port\": 471}, {\"port\": 473}, {\"port\": 475}, {\"port\": 477}, {\"port\": 479}, {\"port\": 481}, {\"port\": 483}, {\"port\": 485}, {\"port\": 487}, {\"port\": 489}, {\"port\": 491}, {\"port\": 493}, {\"port\": 495}, {\"port\": 497}, {\"port\": 499}, {\"port\": 501}, {\"port\": 503}, {\"port\": 505}, {\"port\": 507}, {\"port\": 509}, {\"port\": 511}, {\"port\": 513}]}]}"}],"expected":{"error":"deny emits at most 256 native rules"}}, + {"name":"limit: ranges of any protocol over the limit","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 1, \"endPort\": 2}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 4, \"endPort\": 5}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 7, \"endPort\": 8}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 10, \"endPort\": 11}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 13, \"endPort\": 14}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 16, \"endPort\": 17}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 19, \"endPort\": 20}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 22, \"endPort\": 23}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 25, \"endPort\": 26}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 28, \"endPort\": 29}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 31, \"endPort\": 32}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 34, \"endPort\": 35}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 37, \"endPort\": 38}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 40, \"endPort\": 41}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 43, \"endPort\": 44}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 46, \"endPort\": 47}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 49, \"endPort\": 50}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 52, \"endPort\": 53}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 55, \"endPort\": 56}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 58, \"endPort\": 59}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 61, \"endPort\": 62}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 64, \"endPort\": 65}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 67, \"endPort\": 68}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 70, \"endPort\": 71}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 73, \"endPort\": 74}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 76, \"endPort\": 77}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 79, \"endPort\": 80}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 82, \"endPort\": 83}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 85, \"endPort\": 86}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 88, \"endPort\": 89}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 91, \"endPort\": 92}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 94, \"endPort\": 95}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 97, \"endPort\": 98}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 100, \"endPort\": 101}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 103, \"endPort\": 104}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 106, \"endPort\": 107}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 109, \"endPort\": 110}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 112, \"endPort\": 113}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 115, \"endPort\": 116}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 118, \"endPort\": 119}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 121, \"endPort\": 122}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 124, \"endPort\": 125}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 127, \"endPort\": 128}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 130, \"endPort\": 131}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 133, \"endPort\": 134}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 136, \"endPort\": 137}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 139, \"endPort\": 140}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 142, \"endPort\": 143}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 145, \"endPort\": 146}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 148, \"endPort\": 149}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 151, \"endPort\": 152}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 154, \"endPort\": 155}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 157, \"endPort\": 158}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 160, \"endPort\": 161}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 163, \"endPort\": 164}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 166, \"endPort\": 167}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 169, \"endPort\": 170}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 172, \"endPort\": 173}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 175, \"endPort\": 176}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 178, \"endPort\": 179}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 181, \"endPort\": 182}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 184, \"endPort\": 185}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 187, \"endPort\": 188}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 190, \"endPort\": 191}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 193, \"endPort\": 194}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 196, \"endPort\": 197}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 199, \"endPort\": 200}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 202, \"endPort\": 203}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 205, \"endPort\": 206}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 208, \"endPort\": 209}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 211, \"endPort\": 212}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 214, \"endPort\": 215}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 217, \"endPort\": 218}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 220, \"endPort\": 221}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 223, \"endPort\": 224}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 226, \"endPort\": 227}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 229, \"endPort\": 230}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 232, \"endPort\": 233}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 235, \"endPort\": 236}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 238, \"endPort\": 239}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 241, \"endPort\": 242}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 244, \"endPort\": 245}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 247, \"endPort\": 248}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 250, \"endPort\": 251}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 253, \"endPort\": 254}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 256, \"endPort\": 257}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 259, \"endPort\": 260}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 262, \"endPort\": 263}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 265, \"endPort\": 266}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 268, \"endPort\": 269}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 271, \"endPort\": 272}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 274, \"endPort\": 275}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 277, \"endPort\": 278}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 280, \"endPort\": 281}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 283, \"endPort\": 284}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 286, \"endPort\": 287}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 289, \"endPort\": 290}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 292, \"endPort\": 293}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 295, \"endPort\": 296}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 298, \"endPort\": 299}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 301, \"endPort\": 302}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 304, \"endPort\": 305}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 307, \"endPort\": 308}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 310, \"endPort\": 311}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 313, \"endPort\": 314}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 316, \"endPort\": 317}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 319, \"endPort\": 320}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 322, \"endPort\": 323}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 325, \"endPort\": 326}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 328, \"endPort\": 329}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 331, \"endPort\": 332}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 334, \"endPort\": 335}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 337, \"endPort\": 338}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 340, \"endPort\": 341}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 343, \"endPort\": 344}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 346, \"endPort\": 347}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 349, \"endPort\": 350}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 352, \"endPort\": 353}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 355, \"endPort\": 356}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 358, \"endPort\": 359}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 361, \"endPort\": 362}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 364, \"endPort\": 365}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 367, \"endPort\": 368}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 370, \"endPort\": 371}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 373, \"endPort\": 374}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 376, \"endPort\": 377}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 379, \"endPort\": 380}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 382, \"endPort\": 383}, {\"cidr\": \"192.0.2.1\", \"protocol\": \"any\", \"port\": 385, \"endPort\": 386}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"limit: fragmented exclusions","input":[{"text":"{\"allow\": [{\"cidr\": \"0.0.0.0/0\", \"except\": [\"0.0.0.1\", \"2.0.0.1\", \"4.0.0.1\", \"6.0.0.1\", \"8.0.0.1\", \"10.0.0.1\", \"12.0.0.1\", \"14.0.0.1\", \"16.0.0.1\", \"18.0.0.1\", \"20.0.0.1\", \"22.0.0.1\", \"24.0.0.1\", \"26.0.0.1\", \"28.0.0.1\", \"30.0.0.1\", \"32.0.0.1\", \"34.0.0.1\", \"36.0.0.1\", \"38.0.0.1\", \"40.0.0.1\", \"42.0.0.1\", \"44.0.0.1\", \"46.0.0.1\", \"48.0.0.1\", \"50.0.0.1\", \"52.0.0.1\", \"54.0.0.1\", \"56.0.0.1\", \"58.0.0.1\", \"60.0.0.1\", \"62.0.0.1\", \"64.0.0.1\", \"66.0.0.1\", \"68.0.0.1\", \"70.0.0.1\", \"72.0.0.1\", \"74.0.0.1\", \"76.0.0.1\", \"78.0.0.1\", \"80.0.0.1\", \"82.0.0.1\", \"84.0.0.1\", \"86.0.0.1\", \"88.0.0.1\", \"90.0.0.1\", \"92.0.0.1\", \"94.0.0.1\", \"96.0.0.1\", \"98.0.0.1\", \"100.0.0.1\", \"102.0.0.1\", \"104.0.0.1\", \"106.0.0.1\", \"108.0.0.1\", \"110.0.0.1\", \"112.0.0.1\", \"114.0.0.1\", \"116.0.0.1\", \"118.0.0.1\", \"120.0.0.1\", \"122.0.0.1\", \"124.0.0.1\", \"126.0.0.1\"]}]}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"limit: fragments covered by a wider rule","input":[{"text":"{\"allow\": [{\"cidr\": \"0.0.0.0/0\", \"except\": [\"0.0.0.1\", \"1.0.0.1\", \"2.0.0.1\", \"3.0.0.1\", \"4.0.0.1\", \"5.0.0.1\", \"6.0.0.1\", \"7.0.0.1\", \"8.0.0.1\", \"9.0.0.1\", \"10.0.0.1\", \"11.0.0.1\", \"12.0.0.1\", \"13.0.0.1\", \"14.0.0.1\", \"15.0.0.1\", \"16.0.0.1\", \"17.0.0.1\", \"18.0.0.1\", \"19.0.0.1\", \"20.0.0.1\", \"21.0.0.1\", \"22.0.0.1\", \"23.0.0.1\", \"24.0.0.1\", \"25.0.0.1\", \"26.0.0.1\", \"27.0.0.1\", \"28.0.0.1\", \"29.0.0.1\", \"30.0.0.1\", \"31.0.0.1\", \"32.0.0.1\", \"33.0.0.1\", \"34.0.0.1\", \"35.0.0.1\", \"36.0.0.1\", \"37.0.0.1\", \"38.0.0.1\", \"39.0.0.1\", \"40.0.0.1\", \"41.0.0.1\", \"42.0.0.1\", \"43.0.0.1\", \"44.0.0.1\", \"45.0.0.1\", \"46.0.0.1\", \"47.0.0.1\", \"48.0.0.1\", \"49.0.0.1\", \"50.0.0.1\", \"51.0.0.1\", \"52.0.0.1\", \"53.0.0.1\", \"54.0.0.1\", \"55.0.0.1\", \"56.0.0.1\", \"57.0.0.1\", \"58.0.0.1\", \"59.0.0.1\", \"60.0.0.1\", \"61.0.0.1\", \"62.0.0.1\", \"63.0.0.1\", \"64.0.0.1\", \"65.0.0.1\", \"66.0.0.1\", \"67.0.0.1\", \"68.0.0.1\", \"69.0.0.1\", \"70.0.0.1\", \"71.0.0.1\", \"72.0.0.1\", \"73.0.0.1\", \"74.0.0.1\", \"75.0.0.1\", \"76.0.0.1\", \"77.0.0.1\", \"78.0.0.1\", \"79.0.0.1\", \"80.0.0.1\", \"81.0.0.1\", \"82.0.0.1\", \"83.0.0.1\", \"84.0.0.1\", \"85.0.0.1\", \"86.0.0.1\", \"87.0.0.1\", \"88.0.0.1\", \"89.0.0.1\", \"90.0.0.1\", \"91.0.0.1\", \"92.0.0.1\", \"93.0.0.1\", \"94.0.0.1\", \"95.0.0.1\", \"96.0.0.1\", \"97.0.0.1\", \"98.0.0.1\", \"99.0.0.1\", \"100.0.0.1\", \"101.0.0.1\", \"102.0.0.1\", \"103.0.0.1\", \"104.0.0.1\", \"105.0.0.1\", \"106.0.0.1\", \"107.0.0.1\", \"108.0.0.1\", \"109.0.0.1\", \"110.0.0.1\", \"111.0.0.1\", \"112.0.0.1\", \"113.0.0.1\", \"114.0.0.1\", \"115.0.0.1\", \"116.0.0.1\", \"117.0.0.1\", \"118.0.0.1\", \"119.0.0.1\", \"120.0.0.1\", \"121.0.0.1\", \"122.0.0.1\", \"123.0.0.1\", \"124.0.0.1\", \"125.0.0.1\", \"126.0.0.1\", \"127.0.0.1\", \"128.0.0.1\", \"129.0.0.1\", \"130.0.0.1\", \"131.0.0.1\", \"132.0.0.1\", \"133.0.0.1\", \"134.0.0.1\", \"135.0.0.1\", \"136.0.0.1\", \"137.0.0.1\", \"138.0.0.1\", \"139.0.0.1\", \"140.0.0.1\", \"141.0.0.1\", \"142.0.0.1\", \"143.0.0.1\", \"144.0.0.1\", \"145.0.0.1\", \"146.0.0.1\", \"147.0.0.1\", \"148.0.0.1\", \"149.0.0.1\", \"150.0.0.1\", \"151.0.0.1\", \"152.0.0.1\", \"153.0.0.1\", \"154.0.0.1\", \"155.0.0.1\", \"156.0.0.1\", \"157.0.0.1\", \"158.0.0.1\", \"159.0.0.1\", \"160.0.0.1\", \"161.0.0.1\", \"162.0.0.1\", \"163.0.0.1\", \"164.0.0.1\", \"165.0.0.1\", \"166.0.0.1\", \"167.0.0.1\", \"168.0.0.1\", \"169.0.0.1\", \"170.0.0.1\", \"171.0.0.1\", \"172.0.0.1\", \"173.0.0.1\", \"174.0.0.1\", \"175.0.0.1\", \"176.0.0.1\", \"177.0.0.1\", \"178.0.0.1\", \"179.0.0.1\", \"180.0.0.1\", \"181.0.0.1\", \"182.0.0.1\", \"183.0.0.1\", \"184.0.0.1\", \"185.0.0.1\", \"186.0.0.1\", \"187.0.0.1\", \"188.0.0.1\", \"189.0.0.1\", \"190.0.0.1\", \"191.0.0.1\", \"192.0.0.1\", \"193.0.0.1\", \"194.0.0.1\", \"195.0.0.1\", \"196.0.0.1\", \"197.0.0.1\", \"198.0.0.1\", \"199.0.0.1\", \"200.0.0.1\", \"201.0.0.1\", \"202.0.0.1\", \"203.0.0.1\", \"204.0.0.1\", \"205.0.0.1\", \"206.0.0.1\", \"207.0.0.1\", \"208.0.0.1\", \"209.0.0.1\", \"210.0.0.1\", \"211.0.0.1\", \"212.0.0.1\", \"213.0.0.1\", \"214.0.0.1\", \"215.0.0.1\", \"216.0.0.1\", \"217.0.0.1\", \"218.0.0.1\", \"219.0.0.1\", \"220.0.0.1\", \"221.0.0.1\", \"222.0.0.1\", \"223.0.0.1\", \"224.0.0.1\", \"225.0.0.1\", \"226.0.0.1\", \"227.0.0.1\", \"228.0.0.1\", \"229.0.0.1\", \"230.0.0.1\", \"231.0.0.1\", \"232.0.0.1\", \"233.0.0.1\", \"234.0.0.1\", \"235.0.0.1\", \"236.0.0.1\", \"237.0.0.1\", \"238.0.0.1\", \"239.0.0.1\", \"240.0.0.1\", \"241.0.0.1\", \"242.0.0.1\", \"243.0.0.1\", \"244.0.0.1\", \"245.0.0.1\", \"246.0.0.1\", \"247.0.0.1\", \"248.0.0.1\", \"249.0.0.1\", \"250.0.0.1\", \"251.0.0.1\", \"252.0.0.1\", \"253.0.0.1\", \"254.0.0.1\", \"255.0.0.1\"]}, {\"cidr\": \"0.0.0.0/0\"}]}"}],"expected":{"allow":["0.0.0.0/0"],"deny":[]}}, + {"name":"size: exactly the limit","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\"}]}"},{"text":" ","repeat":1048539}],"expected":{"allow":["192.0.2.0/24"],"deny":[]}}, + {"name":"size: one byte over the limit","input":[{"text":"{\"allow\": [{\"cidr\": \"192.0.2.0/24\"}]}"},{"text":" ","repeat":1048540}],"expected":{"error":"egress policy file exceeds 1048576-byte limit: {path}"}}, + {"name":"size: invalid and over the limit","input":[{"hex":"ff","repeat":1048577}],"expected":{"error":"egress policy file exceeds 1048576-byte limit: {path}"}}, + {"name":"size: spaces up to the limit","input":[{"text":" ","repeat":1048576}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"random #0","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"23.74.67.220/255.255.255.254\"}, {\"cidr\": \"0.0.0.0/0\"}, {\"cidr\": \"10.0.1.128/30\"}, {\"cidr\": \"2001:db8::28c/118\"}], \"ports\": [{\"port\": 65535, \"endPort\": 65535}, {\"protocol\": \"udp\", \"port\": 5, \"endPort\": 7}]}, {\"to\": [{\"cidr\": \"2001:db8::a6/118\", \"except\": [\"2001:db8::394/126\", \"2001:db8::/118\", \"2001:db8::32d/121\"]}]}, {\"cidr\": \"10.0.2.0/26\", \"except\": [\"10.0.2.43\", \"10.0.2.7\", \"10.0.2.0/26\"], \"protocol\": \"tcp\", \"port\": 65535}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.231/124\", \"except\": [\"2001:db8::e0/124\", \"2001:db8::e8/126\"]}, {\"cidr\": \"80.0.0.0/0004\"}, {\"cidr\": \"10.0.2.0/24\"}, {\"cidr\": \"10.0.2.64/255.255.255.224\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 78}]}], \"deny\": [{\"to\": [{\"cidr\": \"2001:db8::200/122\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"udp\", \"port\": 8}, {\"protocol\": \"tcp\", \"port\": 4}, {\"protocol\": \"any\"}]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.2.216/126\", \"protocol\": \"any\", \"port\": 65532}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0237/127\", \"protocol\": \"udp\", \"port\": 81, \"endPort\": 24324}, {\"to\": [{\"cidr\": \"10.0.2.0/28\"}]}, {\"cidr\": \"10.0.3.0/24\", \"protocol\": \"any\", \"port\": 65532}, {\"cidr\": \"10.0.0.229/32\", \"except\": [], \"protocol\": \"tcp\", \"port\": 445}, {\"ports\": [{\"protocol\": \"tcp\", \"port\": 78, \"endPort\": 42429}, {\"protocol\": \"udp\"}]}, {\"to\": [{\"cidr\": \"2001:DB8::1B0/0124\"}, {\"cidr\": \"231.5.119.194/31\"}, {\"cidr\": \"2001:db8::200/120\"}, {\"cidr\": \"10.0.1.112/0.0.0.15\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 79, \"endPort\": 79}, {\"protocol\": \"any\"}, {\"protocol\": \"tcp\"}]}]}"}],"expected":{"allow":["0.0.0.0/0:tcp:65535","0.0.0.0/0:udp:5-7","0.0.0.0/0:udp:65535","10.0.2.0/24:udp:78","80.0.0.0/4:udp:78","2001:db8::/118:tcp:65535","2001:db8::/118:udp:5-7","2001:db8::/118:udp:65535"],"deny":["0.0.0.0/0:tcp:78-42429","0.0.0.0/0:udp","10.0.1.112/28","10.0.2.0/28","10.0.3.0/24:tcp:65532","231.5.119.194/31","::/0:tcp:78-42429","::/0:udp","2001:db8::1b0/124","2001:db8::200/120"]}}, + {"name":"random #1","input":[{"text":"{\n \"allow\": [\n {\n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 65532\n }, \n {\n \"protocol\": \"tcp\"\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.0/27\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"udp\", \n \"port\": 65532, \n \"endPort\": 65534\n }\n ]\n }, \n {\n \"cidr\": \"2001:DB8::3C0/124\", \n \"except\": [\n \"2001:db8::3cc/128\", \n \"2001:db8::3c0/126\"\n ], \n \"protocol\": \"icmp\"\n }, \n {\n \"ports\": [\n {\n \"port\": 80, \n \"endPort\": 83\n }\n ]\n }\n ], \n \"deny\": []\n}"}],"expected":{"allow":["0.0.0.0/0:tcp","0.0.0.0/0:udp:80-83","10.0.3.0/27:udp:65532-65534","::/0:tcp","::/0:udp:80-83","2001:db8::3c4/126:icmp","2001:db8::3c8/126:icmp","2001:db8::3cd/128:icmp","2001:db8::3ce/127:icmp"],"deny":[]}}, + {"name":"random #2","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.0.72/30\"}, {\"cidr\": \"10.0.1.220/30\"}, {\"cidr\": \"10.0.1.240/29\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"tcp\"}, {\"protocol\": \"tcp\"}]}, {\"cidr\": \"2001:DB8::160/123\", \"except\": [], \"protocol\": \"any\", \"port\": 6, \"endPort\": 9}], \"deny\": []}"}],"expected":{"allow":["10.0.0.72/30:tcp","10.0.0.72/30:udp","10.0.1.220/30:tcp","10.0.1.220/30:udp","10.0.1.240/29:tcp","10.0.1.240/29:udp","2001:db8::160/123:tcp:6-9","2001:db8::160/123:udp:6-9"],"deny":[]}}, + {"name":"random #3","input":[{"text":"{\n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.80/255.255.255.254\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.1.80\", \n\t\t\t\t\t\t\"10.0.1.80/31\", \n\t\t\t\t\t\t\"10.0.1.80/255.255.255.254\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.129/24\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.18/00027\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"0.0.0.0/2\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 442\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 7, \n\t\t\t\t\t\"endPort\": 10\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"10.0.0.64/26\", \n\t\t\t\"except\": []\n\t\t}, \n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 82, \n\t\t\t\t\t\"endPort\": 83\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 444\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":[],"deny":["0.0.0.0/0:tcp","0.0.0.0/0:udp:82-83","0.0.0.0/2","::/0:tcp","::/0:udp:82-83"]}}, + {"name":"random #4","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1c0/122\",\n\t\t\t\"except\": [\n\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:01e4/125\",\n\t\t\t\t\"2001:db8::1c9/122\",\n\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:01d8/125\"\n\t\t\t],\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 2,\n\t\t\t\"endPort\": 33322\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.64/027\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 442\n\t\t},\n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 442,\n\t\t\t\t\t\"endPort\": 443\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.160/255.255.255.240\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.0.160/255.255.255.240\",\n\t\t\t\t\t\t\"10.0.0.164/30\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.61/22\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"port\": 444,\n\t\t\t\t\t\"endPort\": 446\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"endPort\": 0\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.48/28\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 445\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 80,\n\t\t\t\t\t\"endPort\": 9745\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 65533,\n\t\t\t\t\t\"endPort\": 65535\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.30/031\",\n\t\t\t\"except\": [\n\t\t\t\t\"10.0.3.30/31\",\n\t\t\t\t\"10.0.3.31\"\n\t\t\t],\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 65535\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::140/122\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 81,\n\t\t\t\"endPort\": 81\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::300/120\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 65531\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::/120\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"55.48.14.65/0001\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.216/30\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.192/26\",\n\t\t\t\t\t\"except\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::200/121\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"error":"allow[3] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #5","input":[{"text":"{\"allow\": [{\"cidr\": \"128.0.0.0/2\", \"except\": [\"181.0.0.0/8\", \"185.116.28.62/3\", \"128.0.0.0/003\"], \"protocol\": \"tcp\", \"port\": 65532, \"endPort\": 65535}, {\"cidr\": \"176.0.0.0/4\", \"protocol\": \"tcp\", \"port\": 6, \"endPort\": 9}, {\"cidr\": \"10.0.3.128/26\", \"protocol\": \"any\", \"port\": 65531, \"endPort\": 65532}], \"deny\": [{\"cidr\": \"::/1\", \"except\": [], \"protocol\": \"udp\", \"port\": 9, \"endPort\": 9}, {\"to\": [{\"cidr\": \"2001:db8::152/124\"}, {\"cidr\": \"8354:9c55:5a5f:ba2c:8faf:6798:f9f4:7f00/120\"}, {\"cidr\": \"0.0.0.0/0\"}, {\"cidr\": \"2001:db8::200/122\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 444, \"endPort\": 447}]}, {\"cidr\": \"38.55.133.109/2\", \"protocol\": \"tcp\", \"port\": 7, \"endPort\": 11}]}"}],"expected":{"allow":["10.0.3.128/26:tcp:65531-65532","10.0.3.128/26:udp:65531-65532","176.0.0.0/4:tcp:6-9"],"deny":["0.0.0.0/0:tcp:444-447","0.0.0.0/2:tcp:7-11","::/1:udp:9","2001:db8::150/124:tcp:444-447","2001:db8::200/122:tcp:444-447","8354:9c55:5a5f:ba2c:8faf:6798:f9f4:7f00/120:tcp:444-447"]}}, + {"name":"random #6","input":[{"text":"{\"allow\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:01c0/122\",\"protocol\":\"any\",\"port\":8},{\"ports\":[{\"protocol\":\"any\"},{\"protocol\":\"tcp\",\"port\":81,\"endPort\":83},{\"port\":65532},{\"protocol\":\"tcp\"}]},{\"cidr\":\"2001:db8::10f/0128\"},{\"ports\":[{\"protocol\":\"tcp\",\"port\":12,\"endPort\":13},{\"protocol\":\"udp\",\"port\":445,\"endPort\":449},{\"protocol\":\"any\",\"port\":65534}]}],\"deny\":[{\"to\":[{\"cidr\":\"2001:db8::ac\",\"except\":[\"2001:db8::ac\",\"2001:db8::ac/0128\",\"2001:0db8:0000:0000:0000:0000:0000:00ac/128\"]},{\"cidr\":\"10.0.3.176/28\",\"except\":[\"10.0.3.188/30\"]},{\"cidr\":\"10.0.2.171/32\",\"except\":[\"10.0.2.171\",\"10.0.2.171\"]},{\"cidr\":\"2001:db8::3ca/000127\"}],\"ports\":[{\"protocol\":\"any\",\"port\":65531,\"endPort\":65534},{\"protocol\":\"udp\"},{\"protocol\":\"icmp\"}]},{\"to\":[{\"cidr\":\"2001:db8::200/000119\",\"except\":[]},{\"cidr\":\"10.0.2.140/26\"},{\"cidr\":\"10.0.0.0/24\"},{\"cidr\":\"2001:DB8::3F8/125\"}],\"ports\":[{\"protocol\":\"icmp\"}]}]}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["10.0.0.0/24:icmp","10.0.2.128/26:icmp","10.0.3.176/29:icmp","10.0.3.176/29:tcp:65531-65534","10.0.3.176/29:udp","10.0.3.184/30:icmp","10.0.3.184/30:tcp:65531-65534","10.0.3.184/30:udp","2001:db8::200/119:icmp","2001:db8::3ca/127:tcp:65531-65534","2001:db8::3ca/127:udp"]}}, + {"name":"random #7","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8::/119\", \"protocol\": \"udp\", \"port\": 12}, {\"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"udp\"}, {\"protocol\": \"any\", \"port\": 78}, {\"protocol\": \"tcp\", \"port\": 78, \"endPort\": 78}]}, {\"ports\": [{\"protocol\": \"tcp\", \"port\": 443, \"endPort\": 446}, {\"protocol\": \"tcp\", \"port\": 445, \"endPort\": 447}]}, {\"cidr\": \"2001:db8::90/121\", \"protocol\": \"udp\"}, {\"to\": [{\"cidr\": \"16.81.121.51/4\"}], \"ports\": [{\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"109.16.44.171/8\", \"except\": [\"109.224.0.0/13\", \"109.250.0.0/16\", \"109.40.0.0/15\"]}], \"ports\": [{\"protocol\": \"tcp\"}]}, {}, {\"to\": [{\"cidr\": \"2001:db8::200/120\", \"except\": []}, {\"cidr\": \"10.0.1.48/28\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 444, \"endPort\": 446}, {\"port\": 443, \"endPort\": 447}, {\"protocol\": \"any\"}]}, {\"to\": [{\"cidr\": \"2001:DB8::/120\"}, {\"cidr\": \"144.0.0.0/4\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 3, \"endPort\": 7}]}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.2.32/123\"}, {\"cidr\": \"::ffff:11.251.162.71/000104\", \"except\": [\"::ffff:11.64.0.0/107\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65530, \"endPort\": 65534}, {\"protocol\": \"any\"}, {\"port\": 65532, \"endPort\": 65535}, {\"protocol\": \"any\", \"port\": 442, \"endPort\": 446}]}, {\"to\": [{\"cidr\": \"157.85.35.190/31\"}, {\"cidr\": \"10.0.0.143/0.0.1.255\"}, {\"cidr\": \"62.165.173.29/16\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"udp\"}, {\"protocol\": \"any\", \"port\": 4, \"endPort\": 4658}]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0170/124\", \"protocol\": \"any\", \"port\": 8, \"endPort\": 8}, {\"to\": [{\"cidr\": \"10.0.1.0/26\"}, {\"cidr\": \"10.0.1.128/0.0.0.7\", \"except\": [\"10.0.1.132/31\", \"10.0.1.134/32\", \"10.0.1.128/30\"]}]}, {\"to\": [{\"cidr\": \"165.64.138.224/128.0.0.0\"}, {\"cidr\": \"2001:db8::40/123\", \"except\": [\"2001:DB8::50/00124\", \"2001:db8::40/126\", \"2001:db8::40/126\"]}, {\"cidr\": \"10.0.0.100/0.0.0.3\", \"except\": []}, {\"cidr\": \"10.0.3.192/26\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 445}, {\"protocol\": \"tcp\"}, {\"protocol\": \"udp\", \"port\": 79, \"endPort\": 58101}, {\"protocol\": \"icmp\"}]}, {\"cidr\": \"2001:db8::24e/120\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0.0.2.208/124\", \"2001:db8::2ef/122\"], \"protocol\": \"icmp\"}, {\"cidr\": \"2001:DB8::3DA/122\", \"except\": [], \"protocol\": \"tcp\", \"port\": 81}, {\"to\": [{\"cidr\": \"2001:db8::300/120\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:0390/127\", \"2001:db8::340/124\"]}, {\"cidr\": \"2001:db8::371/125\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 444}, {\"protocol\": \"any\", \"port\": 78}]}, {\"cidr\": \"10.0.2.48/28\", \"protocol\": \"tcp\"}, {}, {\"to\": [{\"cidr\": \"2001:DB8::200/122\"}, {\"cidr\": \"10.0.1.0/24\"}, {\"cidr\": \"2001:DB8::174/126\"}]}, {\"to\": [{\"cidr\": \"2001:db8::3c0/122\", \"except\": []}]}, {\"cidr\": \"8682:bcfe:0000:0000:0000:0000:0000:0000/32\", \"protocol\": \"icmp\"}, {\"to\": [{\"cidr\": \"10.0.0.95/31\"}, {\"cidr\": \"10.0.0.0/23\"}, {\"cidr\": \"10.0.2.81/30\"}]}, {\"to\": [{\"cidr\": \"2001:db8::2c1/123\"}, {\"cidr\": \"10.0.2.176/28\"}, {\"cidr\": \"10.0.3.192/28\", \"except\": []}, {\"cidr\": \"128.0.0.0/2\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"port\": 65533, \"endPort\": 65535}, {\"protocol\": \"udp\", \"port\": 6}]}, {\"cidr\": \"24f9:243f:7d7c:fd47:fa64:63cd:425f:9e00/120\", \"except\": [\"24f9:243f:7d7c:fd47:fa64:63cd:425f:9e80/121\", \"24f9:243f:7d7c:fd47:fa64:63cd:425f:9e00/122\"], \"protocol\": \"tcp\"}, {\"cidr\": \"2001:db8::2c0/122\", \"protocol\": \"tcp\", \"port\": 443, \"endPort\": 443}, {\"to\": [{\"cidr\": \"2001:db8::2e6/125\"}, {\"cidr\": \"2001:db8::3df/123\"}, {\"cidr\": \"8000:0000:0000:0000:0000:0000:0000:0000/1\"}, {\"cidr\": \"10.0.1.26/255.255.255.224\", \"except\": [\"10.0.1.4/27\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 79}, {\"port\": 8}, {\"protocol\": \"icmp\"}]}, {}, {\"to\": [{\"cidr\": \"2001:db8::382/125\", \"except\": []}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0366/00128\"}, {\"cidr\": \"52.203.163.0/24\"}, {\"cidr\": \"2001:DB8::/118\"}]}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0178/121\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 6, \"endPort\": 7}, {\"protocol\": \"tcp\", \"port\": 65530, \"endPort\": 65531}, {\"protocol\": \"udp\", \"port\": 81, \"endPort\": 81}]}, {\"cidr\": \"10.0.1.96/27\", \"except\": [\"10.0.1.102\"], \"protocol\": \"udp\", \"port\": 82, \"endPort\": 86}, {\"to\": [{\"cidr\": \"10.0.1.116/031\", \"except\": [\"10.0.1.117\", \"10.0.1.116/31\", \"10.0.1.116/32\"]}, {\"cidr\": \"2001:db8::320/000123\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"any\"}, {\"protocol\": \"icmp\"}]}, {\"ports\": [{\"protocol\": \"any\"}, {\"protocol\": \"tcp\", \"port\": 65531, \"endPort\": 65535}]}, {\"to\": [{\"cidr\": \"10.0.1.28\"}]}, {\"cidr\": \"10.0.1.0/24\", \"protocol\": \"tcp\"}, {\"ports\": [{\"protocol\": \"udp\", \"port\": 444, \"endPort\": 7998}, {\"port\": 440}, {\"protocol\": \"tcp\", \"port\": 12, \"endPort\": 15}]}, {\"to\": [{\"cidr\": \"2001:db8::280/121\"}, {\"cidr\": \"10.0.0.192/29\", \"except\": [\"10.0.0.196/30\", \"10.0.0.199/30\", \"10.0.0.194/31\"]}], \"ports\": [{\"protocol\": \"any\", \"port\": 81, \"endPort\": 83}]}, {\"to\": [{\"cidr\": \"10.0.2.24/25\", \"except\": []}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0380/123\", \"except\": [\"2001:db8::390/124\", \"2001:db8::392/127\"]}, {\"cidr\": \"::/1\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 79}, {\"protocol\": \"any\", \"port\": 79, \"endPort\": 80}, {\"protocol\": \"udp\", \"port\": 440}]}, {\"to\": [{\"cidr\": \"10.0.3.156/030\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 65532, \"endPort\": 65533}, {\"protocol\": \"any\", \"port\": 444, \"endPort\": 56397}, {\"protocol\": \"any\", \"port\": 65530, \"endPort\": 65535}]}, {\"cidr\": \"10.0.0.176/255.255.255.248\", \"protocol\": \"any\"}, {\"cidr\": \"2001:db8::348/125\"}, {\"cidr\": \"10.0.2.104/30\", \"protocol\": \"any\", \"port\": 444, \"endPort\": 447}, {\"cidr\": \"10.0.0.0/255.255.254.0\", \"protocol\": \"tcp\", \"port\": 65533, \"endPort\": 65535}, {\"to\": [{\"cidr\": \"10.0.0.0/22\", \"except\": [\"10.0.0.0/00023\", \"10.0.0.152/25\", \"10.0.2.0/23\"]}, {\"cidr\": \"10.0.0.194\"}, {\"cidr\": \"10.0.0.37/32\"}, {\"cidr\": \"2001:DB8::1CB\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65533, \"endPort\": 65534}, {\"protocol\": \"any\"}, {\"port\": 65534}, {\"protocol\": \"udp\", \"port\": 12}]}, {\"cidr\": \"2001:db8::2f9/127\", \"protocol\": \"any\", \"port\": 82, \"endPort\": 82}, {\"to\": [{\"cidr\": \"c2d6:46cf:7673:1d6:930:d0b5::/96\"}, {\"cidr\": \"10.0.2.0/24\"}, {\"cidr\": \"10.0.2.204/255.255.255.252\"}]}, {\"to\": [{\"cidr\": \"26.175.173.172\"}, {\"cidr\": \"e000:0000:0000:0000:0000:0000:0.0.0.0/3\"}, {\"cidr\": \"183.0.0.0/8\"}, {\"cidr\": \"10.0.1.224/27\", \"except\": [\"10.0.1.239/30\", \"10.0.1.224/00027\", \"10.0.1.246/255.255.255.254\"]}], \"ports\": [{\"protocol\": \"udp\"}]}, {\"to\": [{\"cidr\": \"10.0.1.161/0.0.0.63\"}, {\"cidr\": \"10.0.1.84/32\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"tcp\", \"port\": 9}]}, {\"to\": [{\"cidr\": \"10.0.0.113/24\"}, {\"cidr\": \"10.0.2.30\"}, {\"cidr\": \"2001:db8::1b6/126\"}], \"ports\": [{}]}, {\"cidr\": \"10.0.2.0/23\", \"protocol\": \"udp\"}, {\"cidr\": \"2001:DB8::90/0125\", \"protocol\": \"tcp\"}, {\"to\": [{\"cidr\": \"202.209.202.54/24\"}, {\"cidr\": \"10.0.1.182/30\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"tcp\", \"port\": 81, \"endPort\": 82}]}], \"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/22\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"udp\"}, {\"protocol\": \"tcp\", \"port\": 78}]}, {\"cidr\": \"2001:db8::388/127\", \"except\": [\"::/0\", \"2001:0db8:0000:0000:0000:0000:0.0.3.137/128\"], \"protocol\": \"icmp\"}, {\"to\": [{\"cidr\": \"2001:db8::140/123\", \"except\": [\"2001:db8::148/0128\"]}, {\"cidr\": \"f44d:213d::/32\", \"except\": [\"f44d:213d:58b4:78f2:6752:3908:33bd:3e67/33\", \"f44d:213d:2000::/00038\", \"f44d:213d:4800::/38\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 445}]}, {\"cidr\": \"10.0.0.216/32\", \"protocol\": \"icmp\"}]}"}],"expected":{"error":"deny[1].except[0] must be contained in 2001:db8::388/127"}}, + {"name":"random #8","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"0.0.0.0/0\"\n}, \n{\n\"cidr\": \"10.0.1.40/31\", \n\"except\": [\n\"10.0.1.41/32\"\n]\n}, \n{\n\"cidr\": \"10.0.0.0/23\"\n}, \n{\n\"cidr\": \"2001:db8::380/0121\"\n}\n]\n}\n], \n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"227.184.191.101/32\"\n}, \n{\n\"cidr\": \"160.0.0.0/15.255.255.255\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 81, \n\"endPort\": 84\n}, \n{}, \n{}, \n{\n\"protocol\": \"tcp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:DB8::200/122\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\"\n}, \n{\n\"protocol\": \"any\"\n}\n]\n}\n]\n}"}],"expected":{"allow":["0.0.0.0/0","2001:db8::380/121"],"deny":["160.0.0.0/4","227.184.191.101/32","2001:db8::200/122"]}}, + {"name":"random #9","input":[{"text":"{}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #10","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"123.129.205.123/1\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.0/26\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 445\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"port\": 65533, \n\t\t\t\t\t\"endPort\": 65535\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65535\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.64/26\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::27e/127\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:db8::27e/127\", \n\t\t\t\t\t\t\"2001:db8::27f/128\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.24/31\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 65530\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t], \n\t\"deny\": [\n\t\t{\n\t\t\t\"cidr\": \"227.135.130.0/24\", \n\t\t\t\"protocol\": \"udp\"\n\t\t}\n\t]\n}"}],"expected":{"allow":["0.0.0.0/1:icmp","0.0.0.0/1:tcp:445","0.0.0.0/1:tcp:65533-65535","0.0.0.0/1:udp:65533-65535","10.0.0.24/31:tcp:65530","10.0.0.24/31:udp","10.0.2.64/26:tcp:65530","10.0.2.64/26:udp"],"deny":["227.135.130.0/24:udp"]}}, + {"name":"random #11","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"::10e1:8a38:20d7:cfc0/64\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.244/32\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"144.0.0.0/4\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.76/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":311\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.96/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":381,\n\t\t\t\"endPort\":384\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.200/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":99,\n\t\t\t\"endPort\":99\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.180/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":27\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::314/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":35\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.232/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":321\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.100/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":293\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.232/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":53,\n\t\t\t\"endPort\":55\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3b0/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":17,\n\t\t\t\"endPort\":18\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.184/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":211,\n\t\t\t\"endPort\":215\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.52/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":1,\n\t\t\t\"endPort\":5\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::f/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":81,\n\t\t\t\"endPort\":84\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.136/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":329,\n\t\t\t\"endPort\":329\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.164/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":61\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.137/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":99,\n\t\t\t\"endPort\":101\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.112/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":313\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::308/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":213,\n\t\t\t\"endPort\":215\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.248/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":127,\n\t\t\t\"endPort\":130\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.254/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":209\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2e5/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":127\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::9b/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":299\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.180/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":377,\n\t\t\t\"endPort\":381\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.104/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":5\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.98/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":261\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.232/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":13,\n\t\t\t\"endPort\":16\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.176/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":41\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.100/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":343,\n\t\t\t\"endPort\":346\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.15/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":215\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.50/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":377,\n\t\t\t\"endPort\":381\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.174/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":205\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.200/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":317\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.176/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":351\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3a4/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":149\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::385/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":361\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.92/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":25\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.224/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":395\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.224/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":331\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.158/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":31\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.104/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":381,\n\t\t\t\"endPort\":381\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.6/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":53\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.68/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":57\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.80/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":179\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.16/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":9\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::36e/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":23\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1dd/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":247\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::389/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":377\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.20/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":355\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.80/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":373\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.150/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":123\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.172/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":65\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.188/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":131\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3cc/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":79\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2dc/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":101,\n\t\t\t\"endPort\":102\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1d2/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":341\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::15f/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":289\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::278/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":235\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.148/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":215\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.178/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":29\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.192/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":399\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.0/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":135,\n\t\t\t\"endPort\":139\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::143/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":253\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.224/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":135\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.114/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":363\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.247/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":367,\n\t\t\t\"endPort\":367\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.140/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":397\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.42/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":337\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::d2/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":31\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.176/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":241\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::38f/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":99\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.26/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":63\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.56/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":279\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::28e/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":9,\n\t\t\t\"endPort\":12\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.204/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":9\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.148/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":83\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::14a/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":133\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.60/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":379\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::206/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":3\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::21e/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":343,\n\t\t\t\"endPort\":343\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.252/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":257,\n\t\t\t\"endPort\":260\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::339/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":91\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1a/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":287\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.102/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":329,\n\t\t\t\"endPort\":329\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3fb/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":83\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.180/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":293,\n\t\t\t\"endPort\":295\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.49/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":53\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.145/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":141\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.136/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":21\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2d1/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":67,\n\t\t\t\"endPort\":70\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.190/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":339\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1a/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":267,\n\t\t\t\"endPort\":268\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.152/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":13\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::215/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":105\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.224/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":5\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.48/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":313\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.58/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":95,\n\t\t\t\"endPort\":95\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.94/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":203\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.245/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":241,\n\t\t\t\"endPort\":245\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.96/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":313\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2a8/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":31\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.118/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":85,\n\t\t\t\"endPort\":85\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.144/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":339\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.62/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":309\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::73/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":53,\n\t\t\t\"endPort\":56\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::30a/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":265,\n\t\t\t\"endPort\":269\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::d4/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":143\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.152/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":289\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.136/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":281,\n\t\t\t\"endPort\":283\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.251/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":53\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.180/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":113\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.16/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":119\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.153/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":335\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.9/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":379\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.98/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":339,\n\t\t\t\"endPort\":343\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.32/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":337\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::16c/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":129,\n\t\t\t\"endPort\":129\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::122/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":327\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.189/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":271\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.118/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":159,\n\t\t\t\"endPort\":162\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.64/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":181\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::f/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":111\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.230/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":241\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.180/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":327\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.244/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":325\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::b8/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":245\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1dd/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":385\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1fd/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":77\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.206/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":395,\n\t\t\t\"endPort\":399\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.88/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":335\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.242/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":3,\n\t\t\t\"endPort\":4\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.149/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":291\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::9c/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":267\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3f0/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":139\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.238/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":329\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::7e/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":209\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.20/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":273,\n\t\t\t\"endPort\":273\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.158/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":213\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::114/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":357\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::392/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":191\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.121/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":207,\n\t\t\t\"endPort\":211\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.96/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":361,\n\t\t\t\"endPort\":362\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::166/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":105\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.24/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":207\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.100/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":301,\n\t\t\t\"endPort\":302\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::131/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":239\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.90/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":185\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.184/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":375\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.168/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":21\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::e3/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":279,\n\t\t\t\"endPort\":282\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.80/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":69\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.8/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":267\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::12/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":107\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::376/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":309,\n\t\t\t\"endPort\":309\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.92/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":285\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.32/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":31\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::230/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":203,\n\t\t\t\"endPort\":204\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.162/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":201\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1b7/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":313\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2a3/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":247\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.228/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":105\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::b7/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":173\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.114/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":185\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.32/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":135\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.61/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":387\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1bd/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":165,\n\t\t\t\"endPort\":168\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.2/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":21\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.148/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":175\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3dc/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":227\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.186/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":161\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.46/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":115\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::d6/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":131\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2fa/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":229\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3c2/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":145,\n\t\t\t\"endPort\":148\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::274/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":209\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::251/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":75\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.170/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":137\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.12/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":329\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::14b/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":15\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.128/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":17\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3b/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":263\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.217/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":365\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.199/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":283\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::66/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":195\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::117/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":173\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.0/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":325\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::ad/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":381\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.194/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":49,\n\t\t\t\"endPort\":53\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.40/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":265\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::119/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":357\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.100/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":163\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::10c/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":327,\n\t\t\t\"endPort\":327\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.219/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":295,\n\t\t\t\"endPort\":295\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.160/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":325\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.130/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":15\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.192/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":7\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.168/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":141,\n\t\t\t\"endPort\":145\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.153/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":57,\n\t\t\t\"endPort\":57\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.108/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":381\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.248/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":85\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.184/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":43\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2ca/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":265\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2fc/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":311\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::22d/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":203,\n\t\t\t\"endPort\":203\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.230/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":59\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1a1/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":311\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.220/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":119\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.20/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":271\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.172/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":51,\n\t\t\t\"endPort\":55\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":115\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.207/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":81\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::59/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":33\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.177/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":323,\n\t\t\t\"endPort\":327\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1f9/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":339\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2ea/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":349\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::8/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":135\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.148/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":279\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.138/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":159,\n\t\t\t\"endPort\":163\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.224/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":157\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.120/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":143,\n\t\t\t\"endPort\":147\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.186/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":129,\n\t\t\t\"endPort\":131\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.26/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":193\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.32/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":43,\n\t\t\t\"endPort\":44\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::370/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":257\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::135/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":221\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.176/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":71\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3fa/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":363\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2ef/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":385\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.228/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":203\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::26e/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":37,\n\t\t\t\"endPort\":39\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.148/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":177\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.0/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":253\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.180/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":217\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::7c/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":81,\n\t\t\t\"endPort\":84\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.68/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":315,\n\t\t\t\"endPort\":318\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.26/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":283\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::298/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":27\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.220/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":299\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.140/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":293,\n\t\t\t\"endPort\":297\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.200/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":37\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.122/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":3\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.210/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":81\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.64/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":267,\n\t\t\t\"endPort\":270\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.104/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":261,\n\t\t\t\"endPort\":263\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.210/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":313\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::205/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":227\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.216/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":173,\n\t\t\t\"endPort\":176\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::235/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":59\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.208/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":129\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.95/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":7\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.152/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":9\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.35/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":41\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.80/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":69,\n\t\t\t\"endPort\":71\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1cd/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":287\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.244/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":393,\n\t\t\t\"endPort\":393\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::39a/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":171,\n\t\t\t\"endPort\":174\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::5b/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":45,\n\t\t\t\"endPort\":49\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::253/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":365\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.106/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":225\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::d/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":183,\n\t\t\t\"endPort\":186\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::b7/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":171\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::18d/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":333\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::56/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":97\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1f8/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":285\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.207/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":311\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.192/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":215\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::95/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":371,\n\t\t\t\"endPort\":371\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.21/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":329,\n\t\t\t\"endPort\":330\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.76/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":383\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::f5/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":79\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.88/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":251\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::127/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":301,\n\t\t\t\"endPort\":305\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.176/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":291\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::19f/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":83,\n\t\t\t\"endPort\":84\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.255/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":109\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.220/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":239\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.88/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":263,\n\t\t\t\"endPort\":265\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.192/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":189\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::270/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":349\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.164/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":95\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1a1/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":109\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.26/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":357\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.40/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":45\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::bc/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":231,\n\t\t\t\"endPort\":234\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::16/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":141\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.90/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":109,\n\t\t\t\"endPort\":112\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.167/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":53\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.85/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":263\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.26/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":109\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.136/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":35\n\t\t}\n\t],\n\t\"deny\":[\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.32/255.255.255.240\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":65532,\n\t\t\t\"endPort\":65535\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.250/31\",\n\t\t\t\"except\":[],\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":78,\n\t\t\t\"endPort\":33847\n\t\t}\n\t]\n}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"random #12","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8::280/122\", \"except\": [], \"protocol\": \"any\", \"port\": 80, \"endPort\": 83}, {\"cidr\": \"10.0.2.64/26\", \"protocol\": \"any\", \"port\": 65530}, {\"cidr\": \"10.0.2.0/26\", \"except\": [\"10.0.2.32/28\", \"10.0.2.4/30\"], \"protocol\": \"udp\", \"port\": 10, \"endPort\": 11}, {\"to\": [{\"cidr\": \"10.0.2.119\"}, {\"cidr\": \"2001:db8::194/127\"}], \"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"udp\", \"port\": 4}, {\"protocol\": \"tcp\", \"port\": 65532}]}], \"deny\": [{\"cidr\": \"10.0.3.128/25\", \"except\": [\"10.0.3.128/28\"], \"protocol\": \"udp\", \"port\": 6, \"endPort\": 8}, {\"cidr\": \"42.49.72.171/1\", \"protocol\": \"tcp\", \"port\": 65533, \"endPort\": 65534}]}"}],"expected":{"allow":["10.0.2.0/30:udp:10-11","10.0.2.8/29:udp:10-11","10.0.2.16/28:udp:10-11","10.0.2.48/28:udp:10-11","10.0.2.64/26:tcp:65530","10.0.2.64/26:udp:65530","10.0.2.119/32:tcp","10.0.2.119/32:udp:4","2001:db8::194/127:tcp","2001:db8::194/127:udp:4","2001:db8::280/122:tcp:80-83","2001:db8::280/122:udp:80-83"],"deny":["0.0.0.0/1:tcp:65533-65534","10.0.3.144/28:udp:6-8","10.0.3.160/27:udp:6-8","10.0.3.192/26:udp:6-8"]}}, + {"name":"random #13","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"0.0.0.0/0\"\n}, \n{\n\"cidr\": \"10.0.0.213/27\"\n}, \n{\n\"cidr\": \"10.0.0.84/23\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 443\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65530, \n\"endPort\": 65532\n}, \n{\n\"protocol\": \"tcp\"\n}\n]\n}, \n{\n\"cidr\": \"101.171.32.96\", \n\"protocol\": \"tcp\", \n\"port\": 65531\n}, \n{\n\"cidr\": \"2001:db8::22b/123\", \n\"protocol\": \"icmp\"\n}, \n{\n\"cidr\": \"2001:db8::238/125\", \n\"protocol\": \"tcp\", \n\"port\": 8\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.93/24\"\n}, \n{\n\"cidr\": \"2001:db8::/118\", \n\"except\": [\n\"2001:db8::200/119\", \n\"2001:db8::1a3/123\", \n\"2001:0db8:0000:0000:0000:0000:0000:02a0/125\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 65533, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 444, \n\"endPort\": 445\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 12, \n\"endPort\": 16\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 65533\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/118\", \n\"except\": [\n\"2001:db8::138/126\"\n]\n}\n]\n}\n], \n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:02e4/124\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 65531\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65533\n}\n]\n}, \n{\n\"cidr\": \"10.0.1.32/27\", \n\"except\": [\n\"10.0.1.60/30\", \n\"10.0.1.48/30\", \n\"10.0.1.60/30\"\n], \n\"protocol\": \"any\", \n\"port\": 7\n}\n]\n}"}],"expected":{"allow":["0.0.0.0/0:tcp","0.0.0.0/0:udp:65530-65532","10.0.0.0/24:udp:12-16","2001:db8::/118:tcp:444-445","2001:db8::/118:tcp:65533-65535","2001:db8::/118:udp:12-16","2001:db8::/120","2001:db8::100/123","2001:db8::120/124","2001:db8::130/125","2001:db8::13c/126","2001:db8::140/122","2001:db8::180/121","2001:db8::200/119"],"deny":["10.0.1.32/28:tcp:7","10.0.1.32/28:udp:7","10.0.1.52/30:tcp:7","10.0.1.52/30:udp:7","10.0.1.56/30:tcp:7","10.0.1.56/30:udp:7","2001:db8::2e0/124:udp:65531","2001:db8::2e0/124:udp:65533"]}}, + {"name":"random #14","input":[{"text":"null"}],"expected":{"error":"egress policy must be an object"}}, + {"name":"random #15","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"685a:6abb:5ae6:c85f:f2ad:5c59:8f00:0/104\"},{\"cidr\":\"10.0.3.73/30\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":65532},{\"protocol\":\"icmp\"}]}]}"}],"expected":{"allow":["10.0.3.72/30:icmp","10.0.3.72/30:tcp:65532","685a:6abb:5ae6:c85f:f2ad:5c59:8f00:0/104:icmp","685a:6abb:5ae6:c85f:f2ad:5c59:8f00:0/104:tcp:65532"],"deny":[]}}, + {"name":"random #16","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::330/124\"}, {\"cidr\": \"10.0.1.200/30\"}, {\"cidr\": \"2001:db8::260/126\"}, {\"cidr\": \"8036:e4bb:7d0a:b04d:9b26:b335:4600:0/000104\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"144.0.0.0/15.255.255.255\"}, {\"cidr\": \"10.0.0.0/24\"}, {\"cidr\": \"2001:db8::48/000127\", \"except\": []}]}, {\"cidr\": \"10.0.0.168/29\", \"protocol\": \"any\", \"port\": 2}, {\"cidr\": \"164.72.221.0/24\", \"protocol\": \"udp\", \"port\": 65532}, {\"to\": [{\"cidr\": \"10.0.1.96/0028\"}, {\"cidr\": \"10.0.0.158/00025\"}, {\"cidr\": \"2001:DB8::200/121\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:0261/124\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0263/120\", \"except\": [\"2001:db8::2e0/125\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65535, \"endPort\": 65535}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"10.0.1.74/31\"}, {\"cidr\": \"2001:db8::/118\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"port\": 65531, \"endPort\": 65531}, {\"protocol\": \"any\"}]}], \"deny\": [{\"to\": [{\"cidr\": \"87dc:e63e:601c:436f:9dd6:226c:43df:7700/120\"}, {\"cidr\": \"2001:db8::1a0/123\"}, {\"cidr\": \"2001:db8::332/120\", \"except\": [\"2001:db8::380/00121\", \"2001:db8::3f0/124\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 8}, {\"protocol\": \"udp\"}, {\"port\": 82, \"endPort\": 24693}, {\"protocol\": \"tcp\", \"port\": 12, \"endPort\": 18}]}, {\"to\": [{\"cidr\": \"::/00\"}, {\"cidr\": \"2001:db8::201/118\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 7, \"endPort\": 12}]}]}"}],"expected":{"allow":["10.0.0.0/24","10.0.1.74/31","10.0.1.96/28:tcp","10.0.1.200/30:icmp","10.0.1.200/30:tcp","144.0.0.0/4","164.72.221.0/24:udp:65532","2001:db8::/118","8036:e4bb:7d0a:b04d:9b26:b335:4600:0/104:icmp","8036:e4bb:7d0a:b04d:9b26:b335:4600:0/104:tcp"],"deny":["::/0:tcp:7-12","2001:db8::1a0/123:tcp:13-18","2001:db8::1a0/123:tcp:82-24693","2001:db8::1a0/123:udp","2001:db8::300/121:tcp:13-18","2001:db8::300/121:tcp:82-24693","2001:db8::300/121:udp","87dc:e63e:601c:436f:9dd6:226c:43df:7700/120:tcp:13-18","87dc:e63e:601c:436f:9dd6:226c:43df:7700/120:tcp:82-24693","87dc:e63e:601c:436f:9dd6:226c:43df:7700/120:udp"]}}, + {"name":"random #17","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.2.0/25\", \"protocol\": \"any\"}, {\"cidr\": \"2001:db8::134/127\", \"except\": [\"2001:db8::134/127\", \"2001:db8::134/127\"], \"protocol\": \"tcp\", \"port\": 80, \"endPort\": 86}, {\"to\": [{\"cidr\": \"10.0.3.192/255.255.255.192\"}, {\"cidr\": \"10.0.0.0/22\", \"except\": [\"10.0.0.117/29\", \"10.0.2.24/29\", \"10.0.0.0/22\"]}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"24de:1653:d6d3:934f:b0c9:ccc9:915f:d2ae\", \"except\": []}, {\"cidr\": \"2001:db8::3c2/127\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:03c2/127\", \"2001:db8::3c3\", \"2001:db8::3c3\"]}, {\"cidr\": \"2001:db8::80/122\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 80, \"endPort\": 86}]}, {\"to\": [{\"cidr\": \"10.0.2.240/255.255.255.240\"}]}, {\"cidr\": \"180.214.63.179/12\", \"except\": [\"180.208.0.0/12\", \"180.209.29.38/20\", \"180.221.192.0/255.255.224.0\"], \"protocol\": \"tcp\", \"port\": 1}, {\"to\": [{\"cidr\": \"10.0.3.138/23\"}, {\"cidr\": \"10.0.1.168/30\", \"except\": []}, {\"cidr\": \"10.0.2.136/0031\"}, {\"cidr\": \"10.0.1.128/25\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 445}, {\"protocol\": \"udp\", \"port\": 65534}]}, {\"to\": [{\"cidr\": \"2001:db8::/119\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:002a/127\", \"2001:DB8::113/119\", \"2001:db8::15a/127\"]}, {\"cidr\": \"10.0.2.0/23\", \"except\": []}, {\"cidr\": \"ac71:2d35:69a9:330:c439:61ad:5200:0/104\", \"except\": [\"ac71:2d35:69a9:330:c439:61ad:5200:0/105\", \"AC71:2D35:69A9:330:C439:61AD:5200:0/105\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 441, \"endPort\": 443}, {\"protocol\": \"tcp\", \"port\": 441}, {\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}]}], \"deny\": [{\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0040/122\"}, {\"cidr\": \"fae1:2117:c59b:f7db:52d2:4868::/96\"}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0320/123\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"tcp\"}, {\"protocol\": \"udp\", \"port\": 12, \"endPort\": 9561}]}, {\"to\": [{\"cidr\": \"10.0.1.204/31\"}, {\"cidr\": \"10.0.3.108/023\"}]}, {\"to\": [{\"cidr\": \"10.0.2.0/24\", \"except\": [\"10.0.2.15/25\"]}, {\"cidr\": \"153.47.0.0/16\", \"except\": [\"153.47.150.5/019\", \"153.47.0.0/00016\"]}, {\"cidr\": \"10.0.2.224/29\", \"except\": []}, {\"cidr\": \"e63b:3ea2:2d1:66c::/64\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 2, \"endPort\": 3}]}]}"}],"expected":{"allow":["10.0.1.128/25:tcp:445","10.0.1.128/25:udp:65534","10.0.2.0/23:icmp","10.0.2.0/23:tcp:441-443","10.0.2.0/23:tcp:445","10.0.2.0/23:udp:65534","10.0.2.0/25","10.0.2.240/28","2001:db8::80/122:udp:80-86","24de:1653:d6d3:934f:b0c9:ccc9:915f:d2ae/128:udp:80-86","ac71:2d35:69a9:330:c439:61ad:5280:0/105:icmp","ac71:2d35:69a9:330:c439:61ad:5280:0/105:tcp:441-443"],"deny":["10.0.1.204/31","10.0.2.0/23","2001:db8::40/122:tcp","2001:db8::40/122:udp","2001:db8::320/123:tcp","2001:db8::320/123:udp","e63b:3ea2:2d1:66c::/64:udp:2-3","fae1:2117:c59b:f7db:52d2:4868::/96:tcp","fae1:2117:c59b:f7db:52d2:4868::/96:udp"]}}, + {"name":"random #18","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.0.173/32\"}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:00f8/125\", \"except\": []}, {\"cidr\": \"50.208.0.0/12\"}, {\"cidr\": \"6a98:8f87:c846:8913:adf5:b318:3f00:0/104\"}], \"ports\": [{\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"::/0\"}, {\"cidr\": \"10.0.0.145/26\"}, {\"cidr\": \"10.0.0.43/031\", \"except\": []}], \"ports\": [{\"port\": 79, \"endPort\": 59551}, {\"protocol\": \"tcp\"}]}, {\"cidr\": \"2001:db8::e8/120\", \"protocol\": \"any\", \"port\": 8, \"endPort\": 11}, {\"cidr\": \"2001:db8::/118\"}, {\"to\": [{\"cidr\": \"2001:db8::/00118\"}, {\"cidr\": \"10.0.0.218/255.255.255.224\", \"except\": [\"10.0.0.216/29\", \"10.0.0.209/32\", \"10.0.0.192/29\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 4}, {\"protocol\": \"udp\"}, {\"protocol\": \"tcp\", \"port\": 65530}]}, {\"to\": [{\"cidr\": \"10.0.1.64/26\"}, {\"cidr\": \"2001:db8::300/120\"}]}], \"deny\": [{\"cidr\": \"10.0.2.0/0024\", \"except\": [], \"protocol\": \"tcp\"}, {\"to\": [{\"cidr\": \"10.0.3.0/24\"}, {\"cidr\": \"10.0.3.63/27\"}, {\"cidr\": \"10.0.0.192/27\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 444}]}, {\"to\": [{\"cidr\": \"10.0.0.192/26\", \"except\": []}, {\"cidr\": \"10.0.2.0/026\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 65530}]}, {\"to\": [{\"cidr\": \"10.0.3.172/30\", \"except\": [\"10.0.3.175\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0140/122\"}, {\"cidr\": \"10.0.2.11/026\", \"except\": []}], \"ports\": [{\"protocol\": \"udp\", \"port\": 65533, \"endPort\": 65535}]}, {}, {\"ports\": [{\"port\": 2}]}]}"}],"expected":{"allow":["10.0.0.42/31:tcp","10.0.0.42/31:udp:79-59551","10.0.0.128/26:tcp","10.0.0.128/26:udp:79-59551","10.0.0.200/29:tcp:4","10.0.0.200/29:tcp:65530","10.0.0.200/29:udp","10.0.0.208/32:tcp:4","10.0.0.208/32:tcp:65530","10.0.0.208/32:udp","10.0.0.210/31:tcp:4","10.0.0.210/31:tcp:65530","10.0.0.210/31:udp","10.0.0.212/30:tcp:4","10.0.0.212/30:tcp:65530","10.0.0.212/30:udp","10.0.1.64/26","50.208.0.0/12:tcp","::/0:tcp","::/0:udp:79-59551","2001:db8::/118"],"deny":["0.0.0.0/0","::/0"]}}, + {"name":"random #19","input":[{"text":"{\n\"allow\":[\n{\n\"to\":[\n{\n\"cidr\":\"124.71.0.0/16\"\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"10.0.3.20/31\"\n},\n{\n\"cidr\":\"10.0.0.0/24\"\n},\n{\n\"cidr\":\"2001:db8::3aa/127\",\n\"except\":[\n\"2001:db8::3aa/127\",\n\"2001:db8::3aa/127\",\n\"200"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"random #20","input":[{"text":"{\n\"allow\": [\n{\n\"cidr\": \"2001:db8::2ea/120\", \n\"except\": [\n\"2001:db8::268/127\"\n], \n\"protocol\": \"icmp\"\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::/118\"\n}, \n{\n\"cidr\": \"2001:db8::70/122\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 442\n}\n]\n}, \n{\n\"cidr\": \"10.0.2.235/32\", \n\"protocol\": \"any\", \n\"port\": 3, \n\"endPort\": 9\n}\n], \n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0010/126\"\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/00119\"\n}, \n{\n\"cidr\": \"2001:db8::/120\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 82, \n\"endPort\": 86\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65532\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 82\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/255.255.252.0\", \n\"except\": [\n\"10.0.0.0/22\", \n\"10.0.2.128/26\"\n]\n}, \n{\n\"cidr\": \"10.0.2.228/0029\"\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:01c0/123\"\n}, \n{\n\"cidr\": \"10.0.3.128/26\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 6\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65534, \n\"endPort\": 65535\n}, \n{}, \n{\n\"protocol\": \"udp\", \n\"port\": 78\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.24/30\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"160.112.0.0/12\", \n\"except\": [\n\"160.112.0.0/12\"\n]\n}, \n{\n\"cidr\": \"10.0.1.16/29\"\n}, \n{\n\"cidr\": \"2001:db8::213/121\"\n}, \n{\n\"cidr\": \"10.0.1.0/27\", \n\"except\": [\n\"10.0.1.0/27\"\n]\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:DB8::308/125\"\n}, \n{\n\"cidr\": \"10.0.3.128/26\"\n}, \n{\n\"cidr\": \"2001:DB8::C0/125\"\n}, \n{\n\"cidr\": \"23c6:569d:a646:d784:5f35:7ca6:8452:43ec/096\"\n}\n], \n\"ports\": [\n{\n\"port\": 445, \n\"endPort\": 14651\n}, \n{\n\"protocol\": \"any\", \n\"port\": 5\n}, \n{\n\"protocol\": \"icmp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.184/32\", \n\"except\": [\n\"10.0.0.184\"\n]\n}, \n{\n\"cidr\": \"78.166.151.141\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 79\n}, \n{\n\"port\": 441\n}\n]\n}\n]\n}"}],"expected":{"allow":["10.0.2.235/32:tcp:3-9","10.0.2.235/32:udp:3-9","2001:db8::/118:tcp:442","2001:db8::/118:udp:442","2001:db8::200/122:icmp","2001:db8::240/123:icmp","2001:db8::260/125:icmp","2001:db8::26a/127:icmp","2001:db8::26c/126:icmp","2001:db8::270/124:icmp","2001:db8::280/121:icmp"],"deny":["10.0.1.16/29","10.0.2.224/29","10.0.3.24/30","10.0.3.128/26","78.166.151.141/32:tcp:441","78.166.151.141/32:udp:79","78.166.151.141/32:udp:441","2001:db8::/119:tcp","2001:db8::/119:udp:82","2001:db8::/119:udp:65532","2001:db8::c0/125:icmp","2001:db8::c0/125:udp:5","2001:db8::c0/125:udp:445-14651","2001:db8::1c0/123","2001:db8::200/121","2001:db8::308/125:icmp","2001:db8::308/125:tcp:5","2001:db8::308/125:tcp:445-14651","2001:db8::308/125:udp:5","2001:db8::308/125:udp:445-14651","23c6:569d:a646:d784:5f35:7ca6::/96:icmp","23c6:569d:a646:d784:5f35:7ca6::/96:tcp:5","23c6:569d:a646:d784:5f35:7ca6::/96:tcp:445-14651","23c6:569d:a646:d784:5f35:7ca6::/96:udp:5","23c6:569d:a646:d784:5f35:7ca6::/96:udp:445-14651"]}}, + {"name":"random #21","input":[{"text":"{\n \"allow\": [],\n \"deny\": [\n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::365/120\",\n \"except\": [\n \"2001:DB8::370/124\",\n \"2001:db8::38a/123\"\n ]\n },\n {\n \"cidr\": \"10.0.0.44/30\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"tcp\",\n \"port\": 444\n },\n {\n \"protocol\": \"tcp\"\n },\n {\n \"protocol\": \"udp\",\n \"port\": 82\n },\n {\n \"protocol\": \"udp\"\n }\n ]\n },\n {\n \"to\": [\n {\n \"cidr\": \"64.0.0.0/192.0.0.0\"\n },\n {\n \"cidr\": \"10.0.1.60/30\",\n \"except\": [\n \"10.0.1.60/30\",\n \"10.0.1.60/30\"\n ]\n },\n {\n \"cidr\": \"212.207.126.176/31\"\n }\n ]\n },\n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.208/28\"\n },\n {\n \"cidr\": \"10.0.0.0/24\"\n },\n {\n \"cidr\": \"::ffff:66.229.241.167/128\"\n },\n {\n \"cidr\": \"2001:db8::1c0/124\"\n }\n ]\n }\n ]\n}"}],"expected":{"allow":[],"deny":["10.0.0.0/24","64.0.0.0/2","212.207.126.176/31","::ffff:66.229.241.167/128","2001:db8::1c0/124","2001:db8::300/122:tcp","2001:db8::300/122:udp","2001:db8::340/123:tcp","2001:db8::340/123:udp","2001:db8::360/124:tcp","2001:db8::360/124:udp","2001:db8::3a0/123:tcp","2001:db8::3a0/123:udp","2001:db8::3c0/122:tcp","2001:db8::3c0/122:udp"]}}, + {"name":"random #22","input":[{"text":"null"}],"expected":{"error":"egress policy must be an object"}}, + {"name":"random #23","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.56/125\", \"protocol\": \"any\", \"port\": 65534}, {\"cidr\": \"10.0.0.0/24\", \"protocol\": \"udp\", \"port\": 11, \"endPort\": 13}]}"}],"expected":{"allow":["10.0.0.0/24:udp:11-13","2001:db8::38/125:tcp:65534","2001:db8::38/125:udp:65534"],"deny":[]}}, + {"name":"random #24","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::/119\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0.0.1.128/125\"]}, {\"cidr\": \"93.140.2.127/32\", \"except\": [\"93.140.2.127\", \"93.140.2.127/255.255.255.255\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 444, \"endPort\": 444}]}, {\"to\": [{\"cidr\": \"28.21.64.44/31\"}], \"ports\": [{\"port\": 82, \"endPort\": 84}, {\"protocol\": \"any\", \"port\": 443}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"3320:7deb:4796:b264:9f7:369d:8a21:aca4/00128\"}, {\"cidr\": \"10.0.2.0/24\"}, {\"cidr\": \"210.240.171.178/8\"}, {\"cidr\": \"10.0.0.130\", \"except\": [\"10.0.0.130\", \"10.0.0.130/32\", \"10.0.0.130/32\"]}]}, {\"to\": [{\"cidr\": \"10.0.3.192/0.0.0.63\", \"except\": [\"10.0.3.220/31\"]}, {\"cidr\": \"2001:db8::9d/126\"}, {\"cidr\": \"2001:db8::/118\", \"except\": []}, {\"cidr\": \"10.0.2.156/29\", \"except\": [\"10.0.2.152/00031\", \"10.0.2.153/29\", \"10.0.2.152/29\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 12}], \"cidr\": \"192.0.2.0/24\"}, {\"to\": [{\"cidr\": \"2001:db8::2c0/126\", \"except\": [\"2001:db8::2c0/000128\", \"2001:0db8:0000:0000:0000:0000:0000:02c2\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 445}, {\"protocol\": \"icmp\"}]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0100/120\", \"protocol\": \"any\", \"port\": 5, \"endPort\": 10}]}"}],"expected":{"error":"deny[3] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #25","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.192/28\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::/118\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.104/31\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.39/27\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 9, \n\t\t\t\t\t\"endPort\": 13\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"port\": 8, \n\t\t\t\t\t\"endPort\": 11\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 78\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65531, \n\t\t\t\t\t\"endPort\": 65535\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/0022\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.160/255.255.255.254\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.86/23\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::2e8/127\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 79, \n\t\t\t\t\t\"endPort\": 83\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 65531, \n\t\t\t\t\t\"endPort\": 65532\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"2001:db8::16a/119\", \n\t\t\t\"except\": [\n\t\t\t\t\"2001:db8::8c/126\"\n\t\t\t], \n\t\t\t\"protocol\": \"any\", \n\t\t\t\"port\": 444\n\t\t}\n\t], \n\t\"deny\": [\n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 443\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 7, \n\t\t\t\t\t\"endPort\": 11\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.64/31\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::100/123\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"::ac56:e339:cff7:b80e/064\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 78, \n\t\t\t\t\t\"endPort\": 7012\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"::/0\", \n\t\t\t\"protocol\": \"icmp\"\n\t\t}\n\t]\n}"}],"expected":{"allow":["10.0.0.0/22:tcp:79-83","10.0.0.0/22:udp:65531-65532","10.0.0.32/27:tcp:8-13","10.0.0.32/27:tcp:65531-65535","10.0.0.32/27:udp:8-11","10.0.0.32/27:udp:78","10.0.0.32/27:udp:65533-65535","10.0.1.192/28:tcp:8-13","10.0.1.192/28:tcp:65531-65535","10.0.1.192/28:udp:8-11","10.0.1.192/28:udp:78","10.0.1.192/28:udp:65533-65535","10.0.2.104/31:tcp:8-13","10.0.2.104/31:tcp:65531-65535","10.0.2.104/31:udp:8-11","10.0.2.104/31:udp:78","10.0.2.104/31:udp:65533-65535","2001:db8::/118:tcp:8-13","2001:db8::/118:tcp:65531-65535","2001:db8::/118:udp:8-11","2001:db8::/118:udp:78","2001:db8::/118:udp:65531-65535","2001:db8::/121:tcp:444","2001:db8::/121:udp:444","2001:db8::80/125:tcp:444","2001:db8::80/125:udp:444","2001:db8::88/126:tcp:444","2001:db8::88/126:udp:444","2001:db8::90/124:tcp:444","2001:db8::90/124:udp:444","2001:db8::a0/123:tcp:444","2001:db8::a0/123:udp:444","2001:db8::c0/122:tcp:444","2001:db8::c0/122:udp:444","2001:db8::100/120:tcp:444","2001:db8::100/120:udp:444","2001:db8::2e8/127:tcp:79-83"],"deny":["0.0.0.0/0:tcp","0.0.0.0/0:udp:7-11","0.0.0.0/0:udp:443","10.0.3.64/31:udp","::/0:icmp","::/0:tcp","::/0:udp:7-11","::/0:udp:443","::/64:udp","2001:db8::100/123:udp"]}}, + {"name":"random #26","input":[{"text":"{\n\t\"allow\":[],\n\t\"deny\":[\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.0/24\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.3.216/29\",\n\t\t\t\t\t\t\"10.0.3.0/25\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::218/121\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.1.144/28\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.224/31\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":81\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":3,\n\t\t\t\t\t\"endPort\":9\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":442,\n\t\t\t\t\t\"endPort\":447\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::3c0/122\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.0/24\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.2.16/29\",\n\t\t\t\t\t\t\"10.0.2.128/25\",\n\t\t\t\t\t\t\"10.0.2.128/0.0.0.31\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.0/23\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65530\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65535,\n\t\t\t\t\t\"endPort\":65535\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.253/255.255.255.224\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"255.0.0.0/8\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"6a94:2ed0:8277:bd3e:2b9e:36b4:cc8b:53c3/64\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\",\n\t\t\t\t\t\"port\":1\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.1.176/28\",\n\t\t\t\t\t\t\"10.0.2.0/25\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"error":"deny[4].ports[0].port is not supported with icmp"}}, + {"name":"random #27","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::3be/0125\", \"except\": [\"2001:db8::3b8/125\", \"2001:db8::3b8/126\"]}, {\"cidr\": \"10.0.3.128/25\"}, {\"cidr\": \"2001:db8::190/124\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:0190/126\", \"2001:db8::194/126\"]}], \"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"udp\", \"port\": 441}, {\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"161.45.215.27/8\"}, {\"cidr\": \"::/3\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65531}, {\"protocol\": \"icmp\"}]}, {\"cidr\": \"10.0.2.176/28\", \"protocol\": \"tcp\"}, {\"cidr\": \"10.0.3.151/0.0.0.255\", \"except\": [\"10.0.3.11/30\", \"10.0.3.192/00030\"], \"protocol\": \"any\", \"port\": 81}, {\"to\": [{\"cidr\": \"10.0.2.0/27\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65530, \"endPort\": 65535}, {\"protocol\": \"udp\"}, {\"protocol\": \"tcp\"}, {\"protocol\": \"udp\", \"port\": 444}]}, {\"to\": [{\"cidr\": \"2001:db8::/118\", \"except\": [\"2001:db8::380/121\", \"2001:db8::100/120\", \"2001:db8::2a8/0126\"]}, {\"cidr\": \"10.0.0.0/0024\"}, {\"cidr\": \"::ffff:71.24.10.244\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 444}, {\"protocol\": \"tcp\"}, {\"protocol\": \"udp\", \"port\": 7, \"endPort\": 13}, {\"protocol\": \"any\", \"port\": 65531, \"endPort\": 65535}]}, {\"cidr\": \"10.0.3.208/28\", \"except\": [\"10.0.3.208/31\", \"10.0.3.208/28\"], \"protocol\": \"icmp\", \"port\": 65530}, {\"ports\": [{\"protocol\": \"any\", \"port\": 443, \"endPort\": 449}]}], \"deny\": [{\"to\": [{\"cidr\": \"10.0.1.0/255.255.255.0\"}, {\"cidr\": \"176.0.0.0/4\"}, {\"cidr\": \"2001:db8::/123\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 441, \"endPort\": 444}, {\"protocol\": \"udp\", \"port\": 11}, {\"protocol\": \"udp\", \"port\": 1, \"endPort\": 2}, {\"protocol\": \"udp\"}]}, {\"cidr\": \"2001:db8::37a/127\", \"except\": [\"2001:DB8::37A/128\"], \"protocol\": \"tcp\"}, {\"cidr\": \"10.0.2.32/23\", \"protocol\": \"any\"}, {\"to\": [{\"cidr\": \"2001:db8::/118\", \"except\": []}]}, {\"cidr\": \"10.0.3.128/27\", \"protocol\": \"udp\"}, {\"to\": [{\"cidr\": \"2001:db8::/118\", \"except\": []}, {\"cidr\": \"10.0.3.64/27\", \"except\": [\"10.0.3.78/255.255.255.248\", \"10.0.3.80/28\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:022e/121\"}], \"ports\": [{\"port\": 442}, {\"protocol\": \"udp\", \"port\": 12, \"endPort\": 15}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"2001:db8::f3/125\", \"except\": [\"2001:db8::f0/125\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 78, \"endPort\": 78}, {\"protocol\": \"tcp\", \"port\": 81}]}, {\"to\": [{\"cidr\": \"2001:db8::2f9/128\"}, {\"cidr\": \"10.0.2.64/00026\", \"except\": [\"10.0.2.119/029\", \"10.0.2.64/00026\"]}, {\"cidr\": \"10.0.3.253/29\"}]}]}"}],"expected":{"error":"allow[6].port is not supported with icmp"}}, + {"name":"random #28","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"54.200.177.0/24\"}]},{\"to\":[{\"cidr\":\"10.0.3.48/30\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":9},{\"protocol\":\"udp\"}]},{\"cidr\":\"bd9:afaa:6fa3:fdaa:7e4d:87e6:a1b7:4118/127\",\"protocol\":\"any\",\"port\":441,\"endPort\":445}],\"deny\":[{\"to\":[{\"cidr\":\"2001:db8::3c0/123\"}],\"ports\":[{\"port\":1}]},{\"to\":[{\"cidr\":\"7cd2:8f88:1ffb:c6fb:8729:3774:7c18:e658/3\"}],\"ports\":[{\"protocol\":\"any\",\"port\":10,\"endPort\":13},{\"protocol\":\"any\"}]},{\"to\":[{\"cidr\":\"2"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"random #29","input":[{"text":"{\"deny\":[{\"cidr\":\"10.0.2.136/30\",\"protocol\":\"udp\"},{\"to\":[{\"cidr\":\"10.0.0.0/22\"},{\"cidr\":\"2001:db8::2b4/120\",\"except\":[\"2001:db8::2d8/127\",\"2001:0db8:0000:0000:0000:0000:0000:0230/124\"]}],\"ports\":[{\"protocol\":\"any\"},{\"protocol\":\"udp\",\"port\":10,\"endPort\":14}]},{\"cidr\":\"2001:db8::148/126\"},{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0300/122\",\"except\":[\"2001:db8::31b/000128\"],\"protocol\":\"udp\",\"port\":9},{\"to\":[{\"cidr\":\"2001:db8::390/125\"},{\"cidr\":\"10.0.1.32/28\",\"except\":[]},{\"cidr\":\"10.0.3.27/25\",\"except\":[]},{\"cidr\":\"10.0.1.144/30\",\"except\":[\"10.0.1.145\"]}],\"ports\":[{\"protocol\":\"icmp\"},{\"protocol\":\"tcp\",\"port\":65535,\"endPort\":65535},{\"protocol\":\"icmp\"}]},{\"to\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:021e/124\"}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"icmp\"}]}]}"}],"expected":{"allow":[],"deny":["10.0.0.0/22","2001:db8::148/126","2001:db8::200/123","2001:db8::220/124","2001:db8::240/122","2001:db8::280/122","2001:db8::2c0/124","2001:db8::2d0/125","2001:db8::2da/127","2001:db8::2dc/126","2001:db8::2e0/123","2001:db8::300/124:udp:9","2001:db8::310/125:udp:9","2001:db8::318/127:udp:9","2001:db8::31a/128:udp:9","2001:db8::31c/126:udp:9","2001:db8::320/123:udp:9","2001:db8::390/125:icmp","2001:db8::390/125:tcp:65535"]}}, + {"name":"random #30","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"2001:db8::2a0/0126\", \"except\": []}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 2}, {\"protocol\": \"any\"}, {\"protocol\": \"tcp\", \"port\": 443}, {\"protocol\": \"tcp\", \"port\": 5, \"endPort\": 8}]}, {\"cidr\": \"2001:db8::22f/125\", \"except\": [\"2001:db8::22c/126\", \"2001:0db8:0000:0000:0000:0000:0000:022a/127\"], \"protocol\": \"icmp\"}, {\"to\": [{\"cidr\": \"65.120.242.163/32\"}, {\"cidr\": \"10.0.1.0/0.0.0.63\", \"except\": [\"10.0.1.48/29\"]}], \"ports\": [{\"protocol\": \"any\", \"port\": 65533}, {\"protocol\": \"any\", \"port\": 443, \"endPort\": 444}, {}]}, {\"cidr\": \"2001:db8::340/123\", \"protocol\": \"tcp\", \"port\": 4}, {\"cidr\": \"2001:db8::80/123\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:0080/124\", \"2001:db8::9b/128\"], \"protocol\": \"any\", \"port\": 441, \"endPort\": 446}, {\"cidr\": \"2001:db8::200/120\", \"except\": [\"2001:db8::2c1/127\"], \"protocol\": \"any\", \"port\": 65531, \"endPort\": 65535}, {\"to\": [{\"cidr\": \"10.0.3.192/027\"}], \"ports\": [{\"protocol\": \"any\"}, {\"protocol\": \"tcp\", \"port\": 65532, \"endPort\": 65535}]}, {\"cidr\": \"b188:0db1:55a4:30a1:eae3:4b5f:23f7:c193/120\", \"protocol\": \"udp\"}]}"}],"expected":{"allow":[],"deny":["10.0.1.0/27","10.0.1.32/28","10.0.1.56/29","10.0.3.192/27","65.120.242.163/32","2001:db8::90/125:tcp:441-446","2001:db8::90/125:udp:441-446","2001:db8::98/127:tcp:441-446","2001:db8::98/127:udp:441-446","2001:db8::9a/128:tcp:441-446","2001:db8::9a/128:udp:441-446","2001:db8::9c/126:tcp:441-446","2001:db8::9c/126:udp:441-446","2001:db8::200/121:tcp:65531-65535","2001:db8::200/121:udp:65531-65535","2001:db8::228/127:icmp","2001:db8::280/122:tcp:65531-65535","2001:db8::280/122:udp:65531-65535","2001:db8::2a0/126","2001:db8::2c2/127:tcp:65531-65535","2001:db8::2c2/127:udp:65531-65535","2001:db8::2c4/126:tcp:65531-65535","2001:db8::2c4/126:udp:65531-65535","2001:db8::2c8/125:tcp:65531-65535","2001:db8::2c8/125:udp:65531-65535","2001:db8::2d0/124:tcp:65531-65535","2001:db8::2d0/124:udp:65531-65535","2001:db8::2e0/123:tcp:65531-65535","2001:db8::2e0/123:udp:65531-65535","2001:db8::340/123:tcp:4","b188:db1:55a4:30a1:eae3:4b5f:23f7:c100/120:udp"]}}, + {"name":"random #31","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.3.16/28\", \"protocol\": \"tcp\", \"port\": 79}]}"}],"expected":{"allow":["10.0.3.16/28:tcp:79"],"deny":[]}}, + {"name":"random #32","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3e2/123\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 442\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.235/121\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.228/255.255.255.254\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.0.228\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::190/124\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::3e0/123\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.64/026\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"0fe7:2fe6:f2e7:d03c:0000:0000:0000:0000/64\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 12\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 65531\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 81\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"port\": 79,\n\t\t\t\t\t\"endPort\": 83\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 441\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 79\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.222/23\",\n\t\t\t\"protocol\": \"icmp\"\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.62/23\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.32/0.0.0.15\",\n\t\t\t\"protocol\": \"icmp\"\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.8/30\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 65530\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::6c/126\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 65534\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.119/32\",\n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 65532\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 79,\n\t\t\t\t\t\"endPort\": 79\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::358/127\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.0/26\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"29.95.35.240/31\",\n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"port\": 4,\n\t\t\t\t\t\"endPort\": 10\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 2\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 65532,\n\t\t\t\t\t\"endPort\": 65535\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::220/123\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 78\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.30/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 9\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.128/26\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.62/23\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.3.185/24\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::240/122\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/26\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"::ffff:154.15.6.0/120\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::40/122\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"0.36.0.0/16\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 65531\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 82,\n\t\t\t\t\t\"endPort\": 83\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.58/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 167,\n\t\t\t\"endPort\": 168\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::b7/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 373,\n\t\t\t\"endPort\": 374\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::204/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 217\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.252/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 373,\n\t\t\t\"endPort\": 374\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.50/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 345,\n\t\t\t\"endPort\": 345\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::14d/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 251\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.84/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 29\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.192/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 229\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.32/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 217\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::2b6/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 319\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.252/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 71,\n\t\t\t\"endPort\": 72\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.164/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 363\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.232/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 233\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.248/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 41,\n\t\t\t\"endPort\": 43\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.139/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 259\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.236/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 217\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3fb/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 91,\n\t\t\t\"endPort\": 95\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::21e/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 333\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.40/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 215\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::7e/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 75\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.176/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 85,\n\t\t\t\"endPort\": 88\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1be/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 211,\n\t\t\t\"endPort\": 211\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.186/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 311\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::2df/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 209\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::2ca/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 5\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.216/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 89,\n\t\t\t\"endPort\": 90\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.104/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 11\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.144/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 145\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.89/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 259\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::103/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 197\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::87/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 323\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.208/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 215,\n\t\t\t\"endPort\": 218\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.92/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 351\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.96/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 69\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.30/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 29\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.232/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 239,\n\t\t\t\"endPort\": 243\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.99/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 287\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::304/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 221,\n\t\t\t\"endPort\": 224\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.170/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 325,\n\t\t\t\"endPort\": 326\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.208/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 25,\n\t\t\t\"endPort\": 29\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::321/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 87,\n\t\t\t\"endPort\": 88\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.60/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 25\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::ec/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 99\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.176/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 91\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.5/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 205\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.42/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 311\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.96/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 219\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.166/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 159\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.220/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 353\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::ff/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 149,\n\t\t\t\"endPort\": 151\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.104/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 17\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.212/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 141,\n\t\t\t\"endPort\": 143\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.164/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 161,\n\t\t\t\"endPort\": 161\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::137/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 159\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.124/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 55\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.157/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 143\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.20/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 107\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.230/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 1\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.216/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 67\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.252/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 157\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.178/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 293\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.196/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 327\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.202/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 55\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.40/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 27,\n\t\t\t\"endPort\": 27\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.48/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 13\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.114/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 249,\n\t\t\t\"endPort\": 252\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.212/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 165,\n\t\t\t\"endPort\": 166\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.247/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 221\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.112/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 221\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.224/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 271\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::384/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 319\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.68/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 153,\n\t\t\t\"endPort\": 154\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::327/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 29\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3ec/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 349\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.74/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 83\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::e2/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 109\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.227/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 147,\n\t\t\t\"endPort\": 149\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1e6/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 173\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::a/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 199,\n\t\t\t\"endPort\": 202\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::2f/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 297,\n\t\t\t\"endPort\": 300\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.59/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 15\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.64/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 337,\n\t\t\t\"endPort\": 337\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::32e/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 169\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.82/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 277,\n\t\t\t\"endPort\": 281\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3e4/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 139,\n\t\t\t\"endPort\": 142\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.204/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 251,\n\t\t\t\"endPort\": 254\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.8/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 309\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.242/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 253\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::339/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 217,\n\t\t\t\"endPort\": 218\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.108/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 305\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.118/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 305\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.219/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 349\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.188/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 115\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.53/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 195\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::326/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 297,\n\t\t\t\"endPort\": 299\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.64/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 359\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::310/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 379\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.192/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 255\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.138/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 371\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.204/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 37\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.120/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 273\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.146/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 305\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.240/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 33\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.36/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 205\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.229/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 175,\n\t\t\t\"endPort\": 176\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::24b/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 265\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::298/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 199\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.241/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 173,\n\t\t\t\"endPort\": 174\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.176/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 243,\n\t\t\t\"endPort\": 244\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.172/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 47\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.140/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 225\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.134/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 283\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1a3/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 219\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.117/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 343\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.144/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 383,\n\t\t\t\"endPort\": 385\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.113/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 345,\n\t\t\t\"endPort\": 347\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.84/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 313\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.146/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 213\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.84/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 395,\n\t\t\t\"endPort\": 398\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.56/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 135,\n\t\t\t\"endPort\": 135\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::47/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 309\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1a1/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 147\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.154/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 399\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::2c8/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 371\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::44/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 231\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.54/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 127\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.34/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 185,\n\t\t\t\"endPort\": 187\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::145/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 265\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::131/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 109,\n\t\t\t\"endPort\": 109\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.152/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 129\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.160/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 179\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::2c2/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 377\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.248/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 173\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.4/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 63\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.37/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 119\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.230/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 287\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.228/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 385\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.136/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 135\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.93/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 189,\n\t\t\t\"endPort\": 191\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.27/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 183,\n\t\t\t\"endPort\": 183\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::37f/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 337\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::28f/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 259,\n\t\t\t\"endPort\": 259\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.152/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 317,\n\t\t\t\"endPort\": 321\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.196/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 363\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::32b/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 121\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::293/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 391,\n\t\t\t\"endPort\": 393\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3b7/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 399\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1ff/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 217\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.52/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 109,\n\t\t\t\"endPort\": 110\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::39/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 307\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1f5/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 283\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::31/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 233\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.70/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 35,\n\t\t\t\"endPort\": 38\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::326/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 345\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.81/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 247,\n\t\t\t\"endPort\": 249\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::167/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 275\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.240/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 307\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.176/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 181\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.205/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 71,\n\t\t\t\"endPort\": 75\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1b9/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 135\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.180/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 189\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.68/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 101\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.52/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 119\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.96/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 217\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3e4/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 255\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::6f/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 393\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.70/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 315,\n\t\t\t\"endPort\": 317\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.26/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 107,\n\t\t\t\"endPort\": 109\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.115/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 333,\n\t\t\t\"endPort\": 337\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.240/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 1\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3b5/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 81\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.208/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 335\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.234/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 175\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.148/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 233,\n\t\t\t\"endPort\": 236\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::311/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 271,\n\t\t\t\"endPort\": 275\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.160/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 347\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::17a/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 315,\n\t\t\t\"endPort\": 316\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.124/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 75\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.2/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 277\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::333/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 313,\n\t\t\t\"endPort\": 317\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3cc/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 105,\n\t\t\t\"endPort\": 106\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.138/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 107,\n\t\t\t\"endPort\": 111\n\t\t}\n\t]\n}"}],"expected":{"allow":["0.0.0.0/0:tcp:79-83","0.0.0.0/0:tcp:441","0.0.0.0/0:udp:79-83","0.0.0.0/0:udp:441","10.0.0.8/30:tcp:65530","10.0.0.8/30:udp","10.0.0.32/28:icmp","10.0.0.229/32:icmp","10.0.1.64/26:tcp:65531","10.0.1.64/26:udp:12","10.0.1.64/26:udp:65531","10.0.2.0/23","::/0:tcp:79-83","::/0:tcp:441","::/0:udp:79-83","::/0:udp:441","fe7:2fe6:f2e7:d03c::/64:tcp:65531","fe7:2fe6:f2e7:d03c::/64:udp:12","fe7:2fe6:f2e7:d03c::/64:udp:65531","2001:db8::80/121:icmp","2001:db8::190/124:icmp","2001:db8::3e0/123:icmp","2001:db8::3e0/123:udp:442"],"deny":["0.36.0.0/16","10.0.0.0/26","10.0.0.64/30:tcp:359","10.0.0.84/30:tcp:29","10.0.0.84/30:udp:29","10.0.0.84/32:tcp:395-398","10.0.0.84/32:udp:395-398","10.0.0.89/32:tcp:259","10.0.0.96/31:tcp:217","10.0.0.113/32:udp:345-347","10.0.0.114/32:tcp:249-252","10.0.0.117/32:tcp:343","10.0.0.117/32:udp:343","10.0.0.124/30:udp:55","10.0.0.134/31:tcp:283","10.0.0.138/32:tcp:107-111","10.0.0.138/32:udp:107-111","10.0.0.146/32:tcp:305","10.0.0.164/31:tcp:363","10.0.0.164/31:udp:363","10.0.0.170/32:tcp:325-326","10.0.0.170/32:udp:325-326","10.0.0.176/30:tcp:243-244","10.0.0.176/30:udp:243-244","10.0.0.176/31:tcp:181","10.0.0.180/31:tcp:189","10.0.0.180/31:udp:189","10.0.0.204/31:udp:251-254","10.0.0.205/32:udp:71-75","10.0.0.208/31:tcp:335","10.0.0.208/31:udp:335","10.0.0.228/32:tcp:385","10.0.0.228/32:udp:385","10.0.0.229/32:udp:175-176","10.0.0.240/30:tcp:307","10.0.0.240/30:udp:307","10.0.0.248/30:tcp:41-43","10.0.0.248/30:udp:41-43","10.0.0.252/30:tcp:373-374","10.0.1.28/30:tcp:9","10.0.1.37/32:udp:119","10.0.1.40/30:tcp:215","10.0.1.40/30:udp:215","10.0.1.40/31:udp:27","10.0.1.42/32:udp:311","10.0.1.52/30:tcp:109-110","10.0.1.52/30:udp:109-110","10.0.1.53/32:udp:195","10.0.1.56/30:tcp:135","10.0.1.56/30:udp:135","10.0.1.59/32:udp:15","10.0.1.70/31:udp:315-317","10.0.1.74/31:tcp:83","10.0.1.81/32:udp:247-249","10.0.1.92/30:tcp:351","10.0.1.92/30:udp:351","10.0.1.93/32:tcp:189-191","10.0.1.108/30:udp:305","10.0.1.118/31:tcp:305","10.0.1.119/32:tcp:65532","10.0.1.119/32:udp:79","10.0.1.124/31:tcp:75","10.0.1.138/32:tcp:371","10.0.1.140/31:udp:225","10.0.1.144/31:udp:383-385","10.0.1.154/32:udp:399","10.0.1.160/30:udp:179","10.0.1.160/31:tcp:347","10.0.1.164/30:tcp:161","10.0.1.164/30:udp:161","10.0.1.166/32:tcp:159","10.0.1.166/32:udp:159","10.0.1.176/30:tcp:85-88","10.0.1.178/31:tcp:293","10.0.1.186/32:tcp:311","10.0.1.186/32:udp:311","10.0.1.196/32:udp:363","10.0.1.202/32:tcp:55","10.0.1.208/30:udp:25-29","10.0.1.212/30:udp:141-143","10.0.1.212/32:udp:165-166","10.0.1.220/30:tcp:353","10.0.1.230/31:tcp:287","10.0.1.230/31:udp:1","10.0.1.230/31:udp:287","10.0.1.232/30:tcp:239-243","10.0.1.232/31:tcp:233","10.0.1.232/31:udp:233","10.0.1.234/31:tcp:175","10.0.1.234/31:udp:175","10.0.1.236/31:tcp:217","10.0.1.242/31:tcp:253","10.0.1.252/31:tcp:157","10.0.1.252/31:udp:157","10.0.2.0/24","10.0.3.0/26:tcp","10.0.3.0/26:udp:2","10.0.3.0/26:udp:4-10","10.0.3.20/30:udp:107","10.0.3.27/32:udp:183","10.0.3.30/31:udp:29","10.0.3.68/31:tcp:153-154","10.0.3.82/31:tcp:277-281","10.0.3.84/31:udp:313","10.0.3.96/30:tcp:219","10.0.3.96/30:udp:219","10.0.3.96/31:tcp:69","10.0.3.99/32:tcp:287","10.0.3.104/30:udp:17","10.0.3.115/32:tcp:333-337","10.0.3.120/30:udp:273","10.0.3.128/26","10.0.3.192/30:tcp:229","10.0.3.192/30:tcp:255","10.0.3.192/30:udp:229","10.0.3.196/30:tcp:327","10.0.3.208/30:tcp:215-218","10.0.3.216/30:udp:67","10.0.3.224/31:tcp:271","10.0.3.224/31:udp:271","10.0.3.227/32:tcp:147-149","10.0.3.227/32:udp:147-149","10.0.3.240/30:tcp:1","10.0.3.241/32:udp:173-174","29.95.35.240/31:tcp","29.95.35.240/31:udp:2","29.95.35.240/31:udp:4-10","::ffff:154.15.6.0/120","2001:db8::a/128:udp:199-202","2001:db8::2f/128:tcp:297-300","2001:db8::2f/128:udp:297-300","2001:db8::31/128:tcp:233","2001:db8::31/128:udp:233","2001:db8::39/128:udp:307","2001:db8::40/122","2001:db8::87/128:tcp:323","2001:db8::87/128:udp:323","2001:db8::b7/128:tcp:373-374","2001:db8::b7/128:udp:373-374","2001:db8::e2/128:tcp:109","2001:db8::e2/128:udp:109","2001:db8::ec/128:tcp:99","2001:db8::ec/128:udp:99","2001:db8::ff/128:udp:149-151","2001:db8::103/128:tcp:197","2001:db8::131/128:tcp:109","2001:db8::131/128:udp:109","2001:db8::137/128:udp:159","2001:db8::145/128:tcp:265","2001:db8::14d/128:tcp:251","2001:db8::14d/128:udp:251","2001:db8::167/128:udp:275","2001:db8::17a/128:udp:315-316","2001:db8::1a1/128:tcp:147","2001:db8::1a3/128:tcp:219","2001:db8::1a3/128:udp:219","2001:db8::1b9/128:tcp:135","2001:db8::1be/128:tcp:211","2001:db8::1e6/128:udp:173","2001:db8::1f5/128:udp:283","2001:db8::1ff/128:tcp:217","2001:db8::1ff/128:udp:217","2001:db8::204/128:tcp:217","2001:db8::21e/128:tcp:333","2001:db8::220/123:udp:78","2001:db8::240/122","2001:db8::28f/128:tcp:259","2001:db8::293/128:tcp:391-393","2001:db8::293/128:udp:391-393","2001:db8::298/128:udp:199","2001:db8::2b6/128:udp:319","2001:db8::2c2/128:tcp:377","2001:db8::2c2/128:udp:377","2001:db8::2c8/128:tcp:371","2001:db8::2ca/128:tcp:5","2001:db8::2df/128:tcp:209","2001:db8::2df/128:udp:209","2001:db8::304/128:tcp:221-224","2001:db8::304/128:udp:221-224","2001:db8::310/128:tcp:379","2001:db8::311/128:udp:271-275","2001:db8::321/128:tcp:87-88","2001:db8::321/128:udp:87-88","2001:db8::326/128:tcp:297-299","2001:db8::326/128:tcp:345","2001:db8::326/128:udp:345","2001:db8::327/128:tcp:29","2001:db8::327/128:udp:29","2001:db8::32b/128:tcp:121","2001:db8::32b/128:udp:121","2001:db8::32e/128:udp:169","2001:db8::333/128:tcp:313-317","2001:db8::339/128:tcp:217-218","2001:db8::358/127:tcp","2001:db8::358/127:udp:2","2001:db8::358/127:udp:4-10","2001:db8::37f/128:udp:337","2001:db8::384/128:tcp:319","2001:db8::3b5/128:tcp:81","2001:db8::3b5/128:udp:81","2001:db8::3b7/128:udp:399","2001:db8::3cc/128:tcp:105-106","2001:db8::3e4/128:tcp:139-142","2001:db8::3e4/128:udp:139-142","2001:db8::3e4/128:udp:255","2001:db8::3ec/128:tcp:349","2001:db8::3fb/128:tcp:91-95"]}}, + {"name":"random #33","input":[{"text":"{\n \"allow\":[],\n \"deny\":[\n {\n \"cidr\":\"10.0.2.8/30\",\n \"protocol\":\"udp\",\n \"port\":255\n },\n {\n \"cidr\":\"10.0.1.84/30\",\n \"protocol\":\"udp\",\n \"port\":299,\n \"endPort\":299\n },\n {\n \"cidr\":\"10.0.3.192/30\",\n \"protocol\":\"tcp\",\n \"port\":81\n },\n {\n \"cidr\":\"2001:db8::110/128\",\n \"protocol\":\"udp\",\n \"port\":289\n },\n {\n \"cidr\":\"10.0.3.14/32\",\n \"protocol\":\"any\",\n \"port\":345,\n \"endPort\":346\n },\n {\n \"cidr\":\"2001:db8::1da/128\",\n \"protocol\":\"tcp\",\n \"port\":285,\n \"endPort\":286\n },\n {\n \"cidr\":\"10.0.3.9/32\",\n \"protocol\":\"udp\",\n \"port\":299\n },\n {\n \"cidr\":\"10.0.2.58/31\",\n \"protocol\":\"udp\",\n \"port\":61\n },\n {\n \"cidr\":\"10.0.3.224/30\",\n \"protocol\":\"any\",\n \"port\":353\n },\n {\n \"cidr\":\"10.0.0.136/31\",\n \"protocol\":\"udp\",\n \"port\":345,\n \"endPort\":346\n },\n {\n \"cidr\":\"10.0.1.43/32\",\n \"protocol\":\"udp\",\n \"port\":357\n },\n {\n \"cidr\":\"10.0.1.188/30\",\n \"protocol\":\"udp\",\n \"port\":233\n },\n {\n \"cidr\":\"10.0.2.72/30\",\n \"protocol\":\"any\",\n \"port\":135,\n \"endPort\":138\n },\n {\n \"cidr\":\"2001:db8::1fb/128\",\n \"protocol\":\"udp\",\n \"port\":21\n },\n {\n \"cidr\":\"2001:db8::3c2/128\",\n \"protocol\":\"udp\",\n \"port\":347,\n \"endPort\":351\n },\n {\n \"cidr\":\"10.0.0.0/30\",\n \"protocol\":\"any\",\n \"port\":215,\n \"endPort\":215\n },\n {\n \"cidr\":\"10.0.3.252/31\",\n \"protocol\":\"any\",\n \"port\":287\n },\n {\n \"cidr\":\"2001:db8::2ca/128\",\n \"protocol\":\"udp\",\n \"port\":105,\n \"endPort\":106\n },\n {\n \"cidr\":\"2001:db8::3de/128\",\n \"protocol\":\"udp\",\n \"port\":385\n },\n {\n \"cidr\":\"10.0.2.54/31\",\n \"protocol\":\"any\",\n \"port\":233,\n \"endPort\":234\n },\n {\n \"cidr\":\"10.0.3.212/32\",\n \"protocol\":\"any\",\n \"port\":341\n },\n {\n \"cidr\":\"10.0.2.146/32\",\n \"protocol\":\"any\",\n \"port\":143\n },\n {\n \"cidr\":\"10.0.2.184/30\",\n \"protocol\":\"any\",\n \"port\":129\n },\n {\n \"cidr\":\"2001:db8::b1/128\",\n \"protocol\":\"tcp\",\n \"port\":19\n },\n {\n \"cidr\":\"2001:db8::329/128\",\n \"protocol\":\"udp\",\n \"port\":331,\n \"endPort\":333\n },\n {\n \"cidr\":\"10.0.1.92/31\",\n \"protocol\":\"udp\",\n \"port\":165,\n \"endPort\":165\n },\n {\n \"cidr\":\"10.0.1.60/30\",\n \"protocol\":\"any\",\n \"port\":145\n },\n {\n \"cidr\":\"2001:db8::48/128\",\n \"protocol\":\"tcp\",\n \"port\":253\n },\n {\n \"cidr\":\"10.0.2.52/30\",\n \"protocol\":\"any\",\n \"port\":187,\n \"endPort\":188\n },\n {\n \"cidr\":\"10.0.1.162/31\",\n \"protocol\":\"any\",\n \"port\":143\n },\n {\n \"cidr\":\"10.0.0.240/31\",\n \"protocol\":\"tcp\",\n \"port\":83,\n \"endPort\":86\n },\n {\n \"cidr\":\"2001:db8::be/128\",\n \"protocol\":\"any\",\n \"port\":153\n },\n {\n \"cidr\":\"10.0.3.68/30\",\n \"protocol\":\"tcp\",\n \"port\":207,\n \"endPort\":207\n },\n {\n \"cidr\":\"10.0.0.8/30\",\n \"protocol\":\"tcp\",\n \"port\":207\n },\n {\n \"cidr\":\"10.0.0.24/30\",\n \"protocol\":\"tcp\",\n \"port\":251\n },\n {\n \"cidr\":\"2001:db8::d3/128\",\n \"protocol\":\"udp\",\n \"port\":211\n },\n {\n \"cidr\":\"10.0.1.110/31\",\n \"protocol\":\"tcp\",\n \"port\":3\n },\n {\n \"cidr\":\"10.0.3.24/32\",\n \"protocol\":\"tcp\",\n \"port\":27,\n \"endPort\":27\n },\n {\n \"cidr\":\"2001:db8::1fb/128\",\n \"protocol\":\"tcp\",\n \"port\":39,\n \"endPort\":42\n },\n {\n \"cidr\":\"10.0.0.100/30\",\n \"protocol\":\"any\",\n \"port\":277\n },\n {\n \"cidr\":\"2001:db8::371/128\",\n \"protocol\":\"any\",\n \"port\":143\n },\n {\n \"cidr\":\"10.0.0.205/32\",\n \"protocol\":\"any\",\n \"port\":385\n },\n {\n \"cidr\":\"2001:db8::1b0/128\",\n \"protocol\":\"any\",\n \"port\":109\n },\n {\n \"cidr\":\"2001:db8::1ec/128\",\n \"protocol\":\"tcp\",\n \"port\":163\n },\n {\n \"cidr\":\"10.0.2.146/31\",\n \"protocol\":\"any\",\n \"port\":223\n },\n {\n \"cidr\":\"2001:db8::36/128\",\n \"protocol\":\"any\",\n \"port\":107,\n \"endPort\":108\n },\n {\n \"cidr\":\"10.0.2.252/32\",\n \"protocol\":\"tcp\",\n \"port\":219,\n \"endPort\":223\n },\n {\n \"cidr\":\"2001:db8::11d/128\",\n \"protocol\":\"udp\",\n \"port\":127,\n \"endPort\":128\n },\n {\n \"cidr\":\"2001:db8::280/128\",\n \"protocol\":\"any\",\n \"port\":217\n },\n {\n \"cidr\":\"2001:db8::1fe/128\",\n \"protocol\":\"any\",\n \"port\":263\n },\n {\n \"cidr\":\"2001:db8::bb/128\",\n \"protocol\":\"any\",\n \"port\":253\n },\n {\n \"cidr\":\"2001:db8::40/128\",\n \"protocol\":\"udp\",\n \"port\":341,\n \"endPort\":344\n },\n {\n \"cidr\":\"10.0.1.244/30\",\n \"protocol\":\"udp\",\n \"port\":39\n },\n {\n \"cidr\":\"10.0.0.242/31\",\n \"protocol\":\"udp\",\n \"port\":281\n },\n {\n \"cidr\":\"2001:db8::292/128\",\n \"protocol\":\"udp\",\n \"port\":27\n },\n {\n \"cidr\":\"10.0.0.252/30\",\n \"protocol\":\"any\",\n \"port\":1,\n \"endPort\":3\n },\n {\n \"cidr\":\"2001:db8::29f/128\",\n \"protocol\":\"tcp\",\n \"port\":303\n },\n {\n \"cidr\":\"10.0.0.116/31\",\n \"protocol\":\"any\",\n \"port\":255\n },\n {\n \"cidr\":\"10.0.0.27/32\",\n \"protocol\":\"tcp\",\n \"port\":271,\n \"endPort\":273\n },\n {\n \"cidr\":\"2001:db8::3a7/128\",\n \"protocol\":\"any\",\n \"port\":225,\n \"endPort\":226\n },\n {\n \"cidr\":\"10.0.2.2/32\",\n \"protocol\":\"udp\",\n \"port\":137\n },\n {\n \"cidr\":\"10.0.3.226/31\",\n \"protocol\":\"any\",\n \"port\":121\n },\n {\n \"cidr\":\"10.0.1.88/31\",\n \"protocol\":\"any\",\n \"port\":81,\n \"endPort\":85\n },\n {\n \"cidr\":\"2001:db8::e5/128\",\n \"protocol\":\"any\",\n \"port\":99\n },\n {\n \"cidr\":\"10.0.2.129/32\",\n \"protocol\":\"udp\",\n \"port\":85,\n \"endPort\":89\n },\n {\n \"cidr\":\"10.0.0.204/30\",\n \"protocol\":\"udp\",\n \"port\":87\n },\n {\n \"cidr\":\"2001:db8::194/128\",\n \"protocol\":\"tcp\",\n \"port\":361,\n \"endPort\":361\n },\n {\n \"cidr\":\"10.0.3.236/31\",\n \"protocol\":\"tcp\",\n \"port\":375,\n \"endPort\":378\n },\n {\n \"cidr\":\"10.0.3.60/31\",\n \"protocol\":\"udp\",\n \"port\":263,\n \"endPort\":264\n },\n {\n \"cidr\":\"2001:db8::2b0/128\",\n \"protocol\":\"udp\",\n \"port\":289\n },\n {\n \"cidr\":\"10.0.3.18/31\",\n \"protocol\":\"udp\",\n \"port\":143\n },\n {\n \"cidr\":\"10.0.3.30/31\",\n \"protocol\":\"udp\",\n \"port\":103,\n \"endPort\":105\n },\n {\n \"cidr\":\"10.0.1.40/30\",\n \"protocol\":\"tcp\",\n \"port\":265\n },\n {\n \"cidr\":\"2001:db8::1bb/128\",\n \"protocol\":\"tcp\",\n \"port\":49,\n \"endPort\":51\n },\n {\n \"cidr\":\"10.0.1.144/31\",\n \"protocol\":\"udp\",\n \"port\":323\n },\n {\n \"cidr\":\"10.0.3.48/30\",\n \"protocol\":\"any\",\n \"port\":321,\n \"endPort\":324\n },\n {\n \"cidr\":\"10.0.2.96/31\",\n \"protocol\":\"any\",\n \"port\":43\n },\n {\n \"cidr\":\"10.0.3.72/31\",\n \"protocol\":\"any\",\n \"port\":133\n },\n {\n \"cidr\":\"10.0.1.124/30\",\n \"protocol\":\"any\",\n \"port\":139\n },\n {\n \"cidr\":\"2001:db8::1f8/128\",\n \"protocol\":\"any\",\n \"port\":121\n }\n ]\n}"}],"expected":{"allow":[],"deny":["10.0.0.0/30:tcp:215","10.0.0.0/30:udp:215","10.0.0.8/30:tcp:207","10.0.0.24/30:tcp:251","10.0.0.27/32:tcp:271-273","10.0.0.100/30:tcp:277","10.0.0.100/30:udp:277","10.0.0.116/31:tcp:255","10.0.0.116/31:udp:255","10.0.0.136/31:udp:345-346","10.0.0.204/30:udp:87","10.0.0.205/32:tcp:385","10.0.0.205/32:udp:385","10.0.0.240/31:tcp:83-86","10.0.0.242/31:udp:281","10.0.0.252/30:tcp:1-3","10.0.0.252/30:udp:1-3","10.0.1.40/30:tcp:265","10.0.1.43/32:udp:357","10.0.1.60/30:tcp:145","10.0.1.60/30:udp:145","10.0.1.84/30:udp:299","10.0.1.88/31:tcp:81-85","10.0.1.88/31:udp:81-85","10.0.1.92/31:udp:165","10.0.1.110/31:tcp:3","10.0.1.124/30:tcp:139","10.0.1.124/30:udp:139","10.0.1.144/31:udp:323","10.0.1.162/31:tcp:143","10.0.1.162/31:udp:143","10.0.1.188/30:udp:233","10.0.1.244/30:udp:39","10.0.2.2/32:udp:137","10.0.2.8/30:udp:255","10.0.2.52/30:tcp:187-188","10.0.2.52/30:udp:187-188","10.0.2.54/31:tcp:233-234","10.0.2.54/31:udp:233-234","10.0.2.58/31:udp:61","10.0.2.72/30:tcp:135-138","10.0.2.72/30:udp:135-138","10.0.2.96/31:tcp:43","10.0.2.96/31:udp:43","10.0.2.129/32:udp:85-89","10.0.2.146/31:tcp:223","10.0.2.146/31:udp:223","10.0.2.146/32:tcp:143","10.0.2.146/32:udp:143","10.0.2.184/30:tcp:129","10.0.2.184/30:udp:129","10.0.2.252/32:tcp:219-223","10.0.3.9/32:udp:299","10.0.3.14/32:tcp:345-346","10.0.3.14/32:udp:345-346","10.0.3.18/31:udp:143","10.0.3.24/32:tcp:27","10.0.3.30/31:udp:103-105","10.0.3.48/30:tcp:321-324","10.0.3.48/30:udp:321-324","10.0.3.60/31:udp:263-264","10.0.3.68/30:tcp:207","10.0.3.72/31:tcp:133","10.0.3.72/31:udp:133","10.0.3.192/30:tcp:81","10.0.3.212/32:tcp:341","10.0.3.212/32:udp:341","10.0.3.224/30:tcp:353","10.0.3.224/30:udp:353","10.0.3.226/31:tcp:121","10.0.3.226/31:udp:121","10.0.3.236/31:tcp:375-378","10.0.3.252/31:tcp:287","10.0.3.252/31:udp:287","2001:db8::36/128:tcp:107-108","2001:db8::36/128:udp:107-108","2001:db8::40/128:udp:341-344","2001:db8::48/128:tcp:253","2001:db8::b1/128:tcp:19","2001:db8::bb/128:tcp:253","2001:db8::bb/128:udp:253","2001:db8::be/128:tcp:153","2001:db8::be/128:udp:153","2001:db8::d3/128:udp:211","2001:db8::e5/128:tcp:99","2001:db8::e5/128:udp:99","2001:db8::110/128:udp:289","2001:db8::11d/128:udp:127-128","2001:db8::194/128:tcp:361","2001:db8::1b0/128:tcp:109","2001:db8::1b0/128:udp:109","2001:db8::1bb/128:tcp:49-51","2001:db8::1da/128:tcp:285-286","2001:db8::1ec/128:tcp:163","2001:db8::1f8/128:tcp:121","2001:db8::1f8/128:udp:121","2001:db8::1fb/128:tcp:39-42","2001:db8::1fb/128:udp:21","2001:db8::1fe/128:tcp:263","2001:db8::1fe/128:udp:263","2001:db8::280/128:tcp:217","2001:db8::280/128:udp:217","2001:db8::292/128:udp:27","2001:db8::29f/128:tcp:303","2001:db8::2b0/128:udp:289","2001:db8::2ca/128:udp:105-106","2001:db8::329/128:udp:331-333","2001:db8::371/128:tcp:143","2001:db8::371/128:udp:143","2001:db8::3a7/128:tcp:225-226","2001:db8::3a7/128:udp:225-226","2001:db8::3c2/128:udp:347-351","2001:db8::3de/128:udp:385"]}}, + {"name":"random #34","input":[{"text":"{\n \"allow\": [\n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.0/29\"\n }, \n {\n \"cidr\": \"10.0.1.64/28\", \n \"except\": []\n }, \n {\n \"cidr\": \"10.0.1.80/0.0.0.15\"\n }, \n {\n \"cidr\": \"::ffff:3.0.0.0/104\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"any\", \n \"port\": 65534, \n \"endPort\": 65535\n }\n ]\n }, \n {\n \"cidr\": \"36.53.0.0/16\", \n \"protocol\": \"udp\", \n \"port\": 440, \n \"endPort\": 444\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::2fb/118\"\n }, \n {\n \"cidr\": \"0000:0000:0000:0000:0000:0000:0000:0000/32\"\n }\n ]\n }, \n {\n \"cidr\": \"10.0.2.202/30\", \n \"except\": [], \n \"protocol\": \"tcp\", \n \"port\": 440, \n \"endPort\": 440\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0308/126\", \n \"except\": [\n \"2001:db8::30a/127\", \n \"2001:db8::308/0127\", \n \"2001:db8::30a/127\"\n ]\n }, \n {\n \"cidr\": \"10.0.0.200/30\"\n }, \n {\n \"cidr\": \"214.105.75.196\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"any\", \n \"port\": 440\n }, \n {\n \"protocol\": \"icmp\"\n }\n ]\n }, \n {\n \"cidr\": \"::ffff:80.9.219.205/00120\", \n \"protocol\": \"udp\"\n }, \n {\n \"to\": [\n {\n \"cidr\": \"::/1\"\n }, \n {\n \"cidr\": \"10.0.1.156/25\", \n \"except\": []\n }, \n {\n \"cidr\": \"10.0.2.80/29\"\n }, \n {\n \"cidr\": \"10.0.0.14\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"udp\", \n \"port\": 65532\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 82\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 81\n }, \n {\n \"protocol\": \"any\", \n \"port\": 443\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.2.0/25\"\n }, \n {\n \"cidr\": \"::ffff:0.0.0.0/96\", \n \"except\": [\n \"::ffff:126.173.126.93/102\", \n \"::ffff:45.206.246.157/100\", \n \"::FFFF:18.163.1.89/96\"\n ]\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 65535, \n \"endPort\": 65535\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 5\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/118\", \n \"except\": [\n \"2001:db8::23d/122\", \n \"2001:0db8:0000:0000:0000:0000:0000:0100/122\"\n ]\n }, \n {\n \"cidr\": \"2001:db8::d1\", \n \"except\": [\n \"2001:db8::d1\", \n \"2001:0db8:0000:0000:0000:0000:0.0.0.209/128\"\n ]\n }, \n {\n \"cidr\": \"2001:db8::1e9\"\n }\n ]\n }, \n {\n \"cidr\": \"10.0.3.217/31\", \n \"except\": [\n \"10.0.3.216/31\", \n \"10.0.3.216/31\"\n ], \n \"protocol\": \"udp\", \n \"port\": 1\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"udp\"\n }, \n {\n \"protocol\": \"any\"\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/118\"\n }, \n {\n \"cidr\": \"10.0.3.64/26\", \n \"except\": [\n \"10.0.3.116\", \n \"10.0.3.80/28\"\n ]\n }, \n {\n \"cidr\": \"10.0.0.218\"\n }, \n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0370/0124\", \n \"except\": [\n \"2001:db8::379\", \n \"2001:0db8:0000:0000:0000:0000:0000:0374\", \n \"2001:db8::377\"\n ]\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"udp\"\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 79, \n \"endPort\": 84\n }\n ]\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 444\n }, \n {\n \"protocol\": \"any\", \n \"port\": 4\n }, \n {\n \"protocol\": \"icmp\"\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.0/23\"\n }, \n {\n \"cidr\": \"2001:db8::20/123\"\n }, \n {\n \"cidr\": \"10.0.1.224\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 3\n }\n ]\n }, \n {\n \"cidr\": \"10.0.1.128/255.255.255.128\", \n \"protocol\": \"udp\", \n \"port\": 3, \n \"endPort\": 9\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"any\"\n }\n ]\n }, \n {\n \"cidr\": \"10.0.2.0/255.255.255.240\", \n \"except\": [\n \"10.0.2.0/31\", \n \"10.0.2.1/30\"\n ], \n \"protocol\": \"udp\"\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::200/122\", \n \"except\": [\n \"2001:db8::204/124\"\n ]\n }, \n {\n \"cidr\": \"2001:db8::28b/123\", \n \"except\": [\n \"2001:db8::294/127\"\n ]\n }, \n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0200/119\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"any\"\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 65534, \n \"endPort\": 65535\n }, \n {\n \"protocol\": \"tcp\"\n }, \n {\n \"protocol\": \"tcp\"\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"::ffff:0.0.0.0/96\"\n }, \n {\n \"cidr\": \"::ffff:155.0.0.0/104\", \n \"except\": [\n \"::ffff:155.104.0.0/109\"\n ]\n }, \n {\n \"cidr\": \"2001:db8::/119\"\n }, \n {\n \"cidr\": \"10.0.2.96/27\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\"\n }, \n {\n \"port\": 9\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.138/25\", \n \"except\": []\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"any\"\n }, \n {\n \"protocol\": \"any\", \n \"port\": 4, \n \"endPort\": 7\n }\n ]\n }, \n {\n \"cidr\": \"10.0.0.214/00029\", \n \"except\": [], \n \"protocol\": \"udp\", \n \"port\": 79, \n \"endPort\": 80\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"udp\", \n \"port\": 441\n }\n ]\n }, \n {}, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.182/31\"\n }, \n {\n \"cidr\": \"10.0.1.219\", \n \"except\": [\n \"10.0.1.219/32\"\n ]\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"0.0.0.0/192.0.0.0\"\n }, \n {\n \"cidr\": \"10.0.2.166/31\"\n }, \n {\n \"cidr\": \"10.0.0.224/28\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"icmp\"\n }, \n {\n \"port\": 78\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 65534\n }, \n {\n \"protocol\": \"icmp\"\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::2f3/125\"\n }, \n {\n \"cidr\": \"c57b:887a:be83:b59e:19f3:5ef2:b9c4:beea/0\", \n \"except\": []\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"7e6e:b6b7:7853:f3be:1c89:4a2d::/00096\"\n }, \n {\n \"cidr\": \"2001:db8::1e3/125\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"udp\", \n \"port\": 442, \n \"endPort\": 445\n }, \n {\n \"protocol\": \"any\", \n \"port\": 65535\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 82\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.1.193/22\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 65532\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 3, \n \"endPort\": 6\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 10, \n \"endPort\": 13\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 445\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"61.204.245.90\"\n }\n ]\n }, \n {\n \"cidr\": \"0.0.0.0/0\", \n \"except\": [\n \"112.0.0.0/0004\"\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::340/125\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 65532, \n \"endPort\": 65535\n }\n ]\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"tcp\"\n }, \n {\n \"protocol\": \"tcp\"\n }, \n {\n \"protocol\": \"icmp\"\n }\n ]\n }, \n {\n \"cidr\": \"2001:db8::1c9/126\", \n \"protocol\": \"udp\", \n \"ports\": [\n {\n \"port\": 9, \n \"endPort\": 8\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:01e8/125\"\n }, \n {\n \"cidr\": \"155.207.30.93/4\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"any\", \n \"port\": 4, \n \"endPort\": 6\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 3, \n \"endPort\": 3\n }\n ]\n }, \n {\n \"cidr\": \"b5c4:8c4e:b6d:259b:9db8:49fd:6000:0/104\"\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.2.9/28\"\n }, \n {\n \"cidr\": \"2001:db8::38/125\"\n }, \n {\n \"cidr\": \"4.91.195.19/4\"\n }, \n {\n \"cidr\": \"93.225.0.0/16\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"any\", \n \"port\": 79, \n \"endPort\": 81\n }, \n {\n \"protocol\": \"tcp\"\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 81, \n \"endPort\": 83\n }, \n {\n \"protocol\": \"any\", \n \"port\": 80, \n \"endPort\": 81\n }\n ]\n }, \n {}, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::3cb/128\", \n \"except\": [\n \"2001:db8::3cb/128\", \n \"2001:db8::3cb/128\"\n ]\n }\n ]\n }, \n {\n \"cidr\": \"2001:db8::29e/124\", \n \"protocol\": \"udp\", \n \"port\": 441\n }, \n {\n \"cidr\": \"2001:db8::136/127\", \n \"protocol\": \"tcp\", \n \"port\": 80, \n \"endPort\": 55814\n }, \n {\n \"cidr\": \"192.0.0.0/2\", \n \"protocol\": \"tcp\", \n \"port\": 441, \n \"endPort\": 441\n }, \n {\n \"cidr\": \"2001:db8::342/123\", \n \"protocol\": \"tcp\", \n \"port\": 12, \n \"endPort\": 39236\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0180/124\", \n \"except\": [\n \"2001:db8::180/124\"\n ]\n }, \n {\n \"cidr\": \"10.0.1.166/0.0.0.3\"\n }, \n {\n \"cidr\": \"10.0.0.192/26\"\n }, \n {\n \"cidr\": \"10.0.0.0/25\"\n }\n ], \n \"ports\": [\n {\n \"port\": 441, \n \"endPort\": 445\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 5\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.2.142/32\"\n }\n ]\n }, \n {}, \n {\n \"cidr\": \"2001:db8::1c0/123\", \n \"protocol\": \"udp\", \n \"port\": 65531\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.36/26\", \n \"except\": [\n \"10.0.3.0/26\", \n \"10.0.3.49/0031\"\n ]\n }, \n {\n \"cidr\": \"52.240.0.0/012\", \n \"except\": [\n \"52.246.128.0/18\", \n \"52.255.164.50/255.248.0.0\"\n ]\n }, \n {\n \"cidr\": \"10.0.0.24/30\", \n \"except\": [\n \"10.0.0.26/31\", \n \"10.0.0.25\"\n ]\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"any\", \n \"port\": 443, \n \"endPort\": 444\n }, \n {\n \"protocol\": \"icmp\"\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 80, \n \"endPort\": 83\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 65535\n }\n ]\n }\n ], \n \"deny\": [\n {\n \"cidr\": \"10.0.2.161/29\", \n \"protocol\": \"any\"\n }, \n {\n \"cidr\": \"10.0.0.193/22\", \n \"protocol\": \"any\", \n \"port\": 2\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.2.0/25\", \n \"except\": []\n }, \n {\n \"cidr\": \"2001:db8::/123\", \n \"except\": [\n \"2001:db8::/123\", \n \"2001:0db8:0000:0000:0000:0000:0000:0010/128\", \n \"2001:DB8::/125\"\n ]\n }, \n {\n \"cidr\": \"195.9.149.242/1\"\n }, \n {\n \"cidr\": \"10.0.1.4/255.255.255.0\"\n }\n ]\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"udp\"\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 444\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 440\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:DB8::27A/118\"\n }\n ]\n }, \n {\n \"cidr\": \"10.0.3.0/255.255.255.128\", \n \"protocol\": \"tcp\", \n \"port\": 441, \n \"endPort\": 446\n }\n ]\n}"}],"expected":{"error":"allow[32] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #35","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.3.88/00031\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 80}, {\"protocol\": \"udp\", \"port\": 5}, {\"protocol\": \"udp\", \"port\": 82}]}]}"}],"expected":{"allow":["10.0.3.88/31:udp:5","10.0.3.88/31:udp:80","10.0.3.88/31:udp:82"],"deny":[]}}, + {"name":"random #36","input":[{"text":"{\"allow\": [], \"allow\":[{\"cidr\":\"10.0.3.128/26\",\"protocol\":\"udp\",\"port\":443,\"to\":\"192.0.2.0/24\"}],\"deny\":[{\"cidr\":\"2001:db8::110/124\",\"protocol\":\"udp\",\"port\":65532,\"endPort\":65535}]}"}],"expected":{"error":"duplicate JSON property: allow"}}, + {"name":"random #37","input":[{"text":"{\"allow\": []}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #38","input":[{"text":"{\"allow\":[],\"deny\":[{\"to\":[{\"cidr\":\"2001:db8::241/125\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":65533,\"endPort\":65535}]}]}"}],"expected":{"allow":[],"deny":["2001:db8::240/125:udp:65533-65535"]}}, + {"name":"random #39","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"4.153.169.229\", \n\t\t\t\"except\": [\n\t\t\t\t\"4.153.169.229\", \n\t\t\t\t\"4.153.169.229\", \n\t\t\t\t\"4.153.169.229/32\"\n\t\t\t], \n\t\t\t\"protocol\": \"udp\", \n\t\t\t\"port\": 442\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"10.0.0.42/31\", \n\t\t\t\"protocol\": \"any\"\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0240/122\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:db8::240/00122\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"4ab4:b5ae:9775:79d8:9c97:c1b:2800:0/104\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"1cd8:be1d:e837:b777:d9ef:38bf:79ba:4135/3\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"port\": 8\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65533\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"10.0.3.92/30\"\n\t\t}\n\t], \n\t\"deny\": [\n\t\t{\n\t\t\t\"cidr\": \"::/0\", \n\t\t\t\"protocol\": \"tcp\"\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/023\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 82, \n\t\t\t\t\t\"endPort\": 84\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65530\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.0/25\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.3.58/31\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.0/024\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.150/32\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.2.173\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 80\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.128/027\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.1.138/31\", \n\t\t\t\t\t\t\"10.0.1.128/30\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.140/025\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.2.200/0030\", \n\t\t\t\t\t\t\"x\", \n\t\t\t\t\t\t\"10.0.2.224/28\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"c8ab:a263:d9bf:fd87:669d:6cf:f6b5:c57f\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"d838:ab7c:347f:f0a9:35f5:7377::/96\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"d838:ab7c:347f:f0a9:35f5:7377:f000:0/100\", \n\t\t\t\t\t\t\"d838:ab7c:347f:f0a9:35f5:7377:b400:0/102\", \n\t\t\t\t\t\t\"d838:ab7c:347f:f0a9:35f5:7377:d44a:d645/96\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 65532, \n\t\t\t\t\t\"endPort\": 65532\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 3\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 65533, \n\t\t\t\t\t\"endPort\": 65535\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"196.160.0.0/16\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"239.97.233.82/31\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 440, \n\t\t\t\t\t\"endPort\": 441\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:DB8::BC/123\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:db8::a0/124\", \n\t\t\t\t\t\t\"2001:db8::a0/124\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 442\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"10.0.3.240/28\", \n\t\t\t\"except\": [\n\t\t\t\t\"10.0.3.252/255.255.255.254\", \n\t\t\t\t\"10.0.3.240/29\", \n\t\t\t\t\"10.0.3.253/31\"\n\t\t\t], \n\t\t\t\"protocol\": \"any\", \n\t\t\t\"port\": 78\n\t\t}\n\t]\n}"}],"expected":{"error":"deny[3].to[1].except[1] is not a valid IPv4 or IPv6 CIDR: x"}}, + {"name":"random #40","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"10.0.0.151/255.255.255.128\",\"except\":[\"10.0.0.217\",\"10.0.0.218/31\"]},{\"cidr\":\"10.0.1.30/23\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":65534},{\"protocol\":\"icmp\"}]},{\"to\":[{\"cidr\":\"10.0.0.144/28\",\"except\":[\"10.0.0.155/30\",\"10.0.0.144/255.255.255.252\",\"10.0.0.151\"]},{\"cidr\":\"2001:db8::184/00128\"},{\"cidr\":\"10.0.2.72/29\"}]},{\"cidr\":\"2001:DB8::133/128\",\"protocol\":\"tcp\",\"port\":81,\"endPort\":82},{\"to\":[{\"cidr\":\"10.0.1.32/28\",\"except\":[]},{\"cidr\":\"8da1:715f:6660:81e8:b5f0:2f97:72.165.49.118/127\"}],\"ports\":[{\"protocol\":\"icmp\"},{\"protocol\":\"icmp\"},{\"protocol\":\"any\",\"port\":444},{\"protocol\":\"any\",\"port\":65535}]},{\"to\":[{\"cidr\":\"10.0.1.128/25\",\"except\":[\"10.0.1.210/30\",\"10.0.1.171\",\"10.0.1.128/29\"]},{\"cidr\":\"128.0.0.0/2\",\"except\":[]},{\"cidr\":\"2001:db8::213\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":80,\"endPort\":81},{\"protocol\":\"tcp\"},{\"protocol\":\"any\",\"port\":65530,\"endPort\":65533},{\"protocol\":\"icmp\"}]},{\"to\":[{\"cidr\":\"f65c:9cb7:8f6c:865e:c86c:2c08:9f76:8613\",\"except\":[\"f65c:9cb7:8f6c:865e:c86c:2c08:9f76:8613/128\",\"f65c:9cb7:8f6c:865e:c86c:2c08:9f76:8613\",\"f65c:9cb7:8f6c:865e:c86c:2c08:9f76:8613/128\"]},{\"cidr\":\"10.0.0.0/23\"},{\"cidr\":\"10.0.3.120/255.255.255.254\"}],\"ports\":[{\"port\":2,\"endPort\":2},{\"protocol\":\"tcp\",\"port\":80,\"endPort\":85}]},{\"to\":[{\"cidr\":\"2001:db8::281\"}],\"ports\":[{\"protocol\":\"any\"},{\"protocol\":\"tcp\",\"port\":440},{\"protocol\":\"udp\",\"port\":82,\"endPort\":88},{\"protocol\":\"any\",\"port\":9}]},{\"ports\":[{\"protocol\":\"udp\",\"port\":445,\"endPort\":447},{\"protocol\":\"tcp\",\"port\":65530},{\"protocol\":\"any\",\"port\":443,\"endPort\":446},{\"protocol\":\"udp\"}]},{\"to\":[{\"cidr\":\"220.83.212.0/255.255.255.0\"},{\"cidr\":\"10.0.2.113\",\"except\":[]},{\"cidr\":\"2001:db8::a0/123\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":442,\"endPort\":448},{\"protocol\":\"udp\",\"port\":65531}]},{\"cidr\":\"193.11.7.194/16\",\"except\":[],\"protocol\":\"udp\"},{\"to\":[{\"cidr\":\"10.0.3.10/0.0.0.15\"}],\"ports\":[{\"protocol\":\"any\",\"port\":441},{\"protocol\":\"any\",\"port\":79}]},{\"to\":[{\"cidr\":\"2001:db8::170/124\"},{\"cidr\":\"10.0.2.4/31\",\"except\":[]},{\"cidr\":\"bb4a:e7e7:489f:5c3f:4bf6:7958::/96\",\"except\":[\"bb4a:e7e7:489f:5c3f:4bf6:7958:e000:0/00100\",\"bb4a:e7e7:489f:5c3f:4bf6:7958:1f22:e0c9/101\",\"bb4a:e7e7:489f:5c3f:4bf6:7958::/97\"]}],\"ports\":[{\"protocol\":\"any\",\"port\":444,\"endPort\":44209},{\"protocol\":\"tcp\"},{\"protocol\":\"udp\",\"port\":7},{\"protocol\":\"tcp\"}]},{\"to\":[{\"cidr\":\"10.0.2.128/255.255.255.192\",\"except\":[]}],\"ports\":[{\"protocol\":\"udp\"}]},{\"cidr\":\"41.112.0.0/012\",\"protocol\":\"tcp\",\"port\":65534},{\"to\":[{\"cidr\":\"10.0.3.128/27\"},{\"cidr\":\"10.0.1.0/24\"}],\"ports\":[{\"protocol\":\"any\",\"port\":3}]},{\"cidr\":\"136.115.227.64/12\",\"protocol\":\"any\",\"port\":445},{\"to\":[{\"cidr\":\"10.0.3.0/24\"},{\"cidr\":\"10.0.2.127/29\",\"except\":[\"10.0.2.124/31\"]},{\"cidr\":\"2001:db8::20/124\"},{\"cidr\":\"2001:db8::378/00118\"}],\"ports\":[{\"protocol\":\"any\",\"port\":445,\"endPort\":445},{\"protocol\":\"udp\"},{\"protocol\":\"tcp\",\"port\":79,\"endPort\":79},{\"protocol\":\"tcp\"}]},{\"ports\":[{\"protocol\":\"any\"}]},{\"cidr\":\"10.0.0.0/22\"},{\"to\":[{\"cidr\":\"10.0.2.34/31\"},{\"cidr\":\"2001:db8::300/121\",\"except\":[\"2001:db8::330/124\"]},{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0191/124\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":65531},{\"protocol\":\"tcp\"},{\"protocol\":\"udp\"},{\"protocol\":\"icmp\"}]},{\"cidr\":\"::ffff:0.0.0.0/96\",\"protocol\":\"udp\",\"port\":1}],\"deny\":[{\"to\":[{\"cidr\":\"10.0.3.156/30\"},{\"cidr\":\"10.0.0.34/022\",\"except\":[\"10.0.2.184/29\",\"10.0.0.0/22\"]},{\"cidr\":\"1a9b:a28c:cfe1:bb0c:1176:2acd:141c:1a4c/0\"},{\"cidr\":\"218.41.53.10/1\"}],\"ports\":[{\"protocol\":\"any\",\"port\":4,\"endPort\":6},{\"protocol\":\"tcp\",\"port\":79,\"endPort\":82}]},{\"to\":[{\"cidr\":\"64.0.0.0/4\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":5},{\"protocol\":\"tcp\",\"port\":65530,\"endPort\":65534}]}]}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["10.0.3.156/30:tcp:4-6","10.0.3.156/30:tcp:79-82","10.0.3.156/30:udp:4-6","64.0.0.0/4:tcp:5","64.0.0.0/4:tcp:65530-65534","128.0.0.0/1:tcp:4-6","128.0.0.0/1:tcp:79-82","128.0.0.0/1:udp:4-6","::/0:tcp:4-6","::/0:tcp:79-82","::/0:udp:4-6"]}}, + {"name":"random #41","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:00d0/124\", \"protocol\": \"any\"}, {\"cidr\": \"2001:DB8::200/119\", \"except\": [\"2001:db8::228/122\", \"2001:db8::280/123\", \"2001:db8::2f7/124\"], \"protocol\": \"icmp\"}, {\"ports\": [{\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"55.17.21.41\"}, {\"cidr\": \"2001:db8::1eb/128\"}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.0/120\", \"except\": [\"2001:db8::/0123\"]}], \"ports\": [{\"protocol\": \"any\", \"port\": 6, \"endPort\": 7}]}, {\"cidr\": \"2001:db8::33/128\", \"except\": [\"2001:db8::33/128\", \"2001:0db8:0000:0000:0000:0000:0000:0033/128\", \"2001:db8::33/128\"]}, {\"to\": [{\"cidr\": \"89.32.0.0/12\", \"except\": [\"89.47.0.0/255.255.0.0\", \"89.40.0.0/13\", \"89.44.176.86/17\"]}, {\"cidr\": \"2001:db8::31\"}, {\"cidr\": \"10.0.1.240/28\", \"except\": [\"10.0.1.240/00028\", \"10.0.1.244/30\"]}]}, {\"to\": [{\"cidr\": \"330e:2de6:2235:e665:776:31ea:2c2f:2f33/32\"}, {\"cidr\": \"10.0.3.28/30\"}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/118\"}, {\"cidr\": \"10.0.1.111/27\"}], \"ports\": [{\"protocol\": \"any\"}, {\"protocol\": \"udp\", \"port\": 82}]}, {\"ports\": [{\"protocol\": \"tcp\", \"port\": 65535, \"endPort\": 65535}]}], \"deny\": [{\"cidr\": \"f575:27c8:679f:126a:9f18:a624:6100:0/104\", \"protocol\": \"udp\", \"port\": 78}]}"}],"expected":{"allow":["0.0.0.0/0:icmp","0.0.0.0/0:tcp:65535","10.0.1.96/27","10.0.3.28/30","55.17.21.41/32:tcp:6-7","55.17.21.41/32:udp:6-7","89.32.0.0/13","::/0:icmp","::/0:tcp:65535","2001:db8::/118","330e:2de6::/32"],"deny":["f575:27c8:679f:126a:9f18:a624:6100:0/104:udp:78"]}}, + {"name":"random #42","input":[{"text":"{\"allow\": [], \"deny\": [{\"to\": [{\"cidr\": \"10.0.1.182/28\"}, {\"cidr\": \"96.0.0.0/4\", \"except\": [\"104.0.0.0/3.255.255.255\"]}], \"ports\": [{\"protocol\": \"any\", \"port\": 78}, {\"protocol\": \"tcp\", \"port\": 440, \"endPort\": 446}]}, {\"to\": [{\"cidr\": \"10.0.0.0/23\", \"except\": [\"10.0.0.121/31\", \"10.0.1.0/24\", \"10.0.1.138/0.0.0.3\"]}, {\"cidr\": \"67c3:582b:340c:4f65:8c5f:a321:b14a:9c11/3\", \"except\": []}, {\"cidr\": \"10.0.0.95/26\"}]}, {\"to\": [{\"cidr\": \"10.0.2.0/23\"}, {\"cidr\": \"2001:db8::40/122\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65533}]}, {\"ports\": [{\"port\": 80, \"endPort\": 86}, {\"protocol\": \"udp\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"10.0.0.171/31\", \"except\": [\"10.0.0.170/255.255.255.255\", \"10.0.0.170/31\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 81, \"endPort\": 11029}, {\"port\": 8}]}, {\"cidr\": \"10.0.3.184/29\"}, {\"cidr\": \"134.43.197.42\", \"except\": [], \"protocol\": \"tcp\"}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0204/119\"}], \"ports\": [{\"protocol\": \"any\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"tcp\", \"port\": 65532, \"endPort\": 65534}, {\"protocol\": \"tcp\", \"port\": 80}]}, {\"to\": [{\"cidr\": \"2001:db8::32c/127\"}], \"ports\": [{}]}, {\"to\": [{\"cidr\": \"45ac:a868:8cb7:6c66:71a6:3f42:f232:caaf/00\"}, {\"cidr\": \"10.0.2.234/31\", \"except\": [\"10.0.2.234/31\"]}, {\"cidr\": \"123.177.32.249/0\"}, {\"cidr\": \"10.0.3.130/31\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 442, \"endPort\": 442}, {\"protocol\": \"tcp\"}, {\"protocol\": \"tcp\", \"port\": 441}, {\"protocol\": \"udp\", \"port\": 65532, \"endPort\": 65533}]}, {\"cidr\": \"10.0.0.160/255.255.255.224\", \"except\": [\"10.0.0.160/27\", \"10.0.0.185/28\", \"10.0.0.175/32\"], \"protocol\": \"any\", \"port\": 78}, {\"to\": [{\"cidr\": \"2001:DB8::2FF/126\"}, {\"cidr\": \"10.0.0.194/22\", \"except\": [\"10.0.3.160/27\", \"10.0.2.87/0.0.0.255\"]}, {\"cidr\": \"10.0.3.249/0.0.0.31\"}, {\"cidr\": \"2001:DB8::E0/124\"}]}, {\"to\": [{\"cidr\": \"10.0.1.197/26\", \"except\": [\"10.0.1.206/31\"]}, {\"cidr\": \"203.127.143.163/24\", \"except\": [\"203.127.143.128/255.255.255.240\"]}, {\"cidr\": \"10.0.3.0/24\", \"except\": [\"10.0.3.204/255.255.255.252\"]}, {\"cidr\": \"10.0.0.36/30\"}], \"ports\": [{\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"10.0.1.0/255.255.255.128\"}, {\"cidr\": \"2001:db8::2cb\", \"except\": [\"2001:db8::2cb/128\"]}, {\"cidr\": \"10.0.2.0/00023\"}]}, {\"to\": [{\"cidr\": \"2001:DB8::100/00122\"}, {\"cidr\": \"150.192.0.0/12\"}, {\"cidr\": \"196.51.205.115/32\"}]}, {\"to\": [{\"cidr\": \"10.0.0.47/32\"}, {\"cidr\": \"2001:db8::3f3/123\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 12, \"endPort\": 14}]}, {\"to\": [{\"cidr\": \"5e0e:a4b5:9a3:e15f:7e3a:40ab::/96\", \"except\": []}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"tcp\", \"port\": 441, \"endPort\": 443}]}, {\"cidr\": \"10.0.2.160/27\"}, {\"cidr\": \"::/1\", \"except\": [\"714b:5417:2081:c1d3:9658:b44e:d965:12d9/7\", \"3500::/8\", \"30f4:7a01:e11d:5a71:8afe:24e0:4a7c:e793/4\"], \"protocol\": \"any\", \"port\": 65531, \"endPort\": 65533}, {\"cidr\": \"10.0.2.0/23\", \"protocol\": \"any\", \"port\": 65533}, {\"cidr\": \"2001:DB8::341/122\", \"except\": [], \"protocol\": \"udp\", \"port\": 65535, \"endPort\": 65535}, {\"cidr\": \"88.36.77.186/32\"}, {\"cidr\": \"10.0.1.206/25\", \"except\": [\"10.0.1.252/30\", \"10.0.1.128/25\", \"10.0.1.155/25\"], \"protocol\": \"tcp\", \"port\": 81, \"endPort\": 87}, {\"to\": [{\"cidr\": \"c000:0000:0000:0000:0000:0000:0.0.0.0/3\", \"except\": [\"de04:2db1:973:dbab:a9e5:1c54:f5ad:fd1a/7\", \"d000::/5\"]}, {\"cidr\": \"128.0.0.0/1\", \"except\": [\"128.0.0.0/2\"]}]}, {}, {\"cidr\": \"11.249.150.59/0\", \"except\": [\"32.0.0.0/3\"], \"protocol\": \"icmp\"}, {\"to\": [{\"cidr\": \"2001:db8::306/121\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0.0.3.37/122\", \"2001:db8::36f/126\"]}, {\"cidr\": \"10.0.3.192/30\", \"except\": [\"10.0.3.194/31\", \"10.0.3.192/30\"]}, {\"cidr\": \"2001:DB8::22C/120\"}, {\"cidr\": \"10.0.3.178/31\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 441, \"endPort\": 445}, {\"protocol\": \"any\", \"port\": 79}, {\"protocol\": \"tcp\"}, {\"protocol\": \"tcp\", \"port\": 78, \"endPort\": 79}]}, {\"cidr\": \"2001:db8::298/126\", \"except\": [\"2001:db8::29a/00127\", \"2001:db8::299\", \"2001:db8::29a/127\"], \"protocol\": \"icmp\"}, {\"to\": [{\"cidr\": \"31.255.69.181/00016\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"tcp\"}, {\"protocol\": \"any\", \"port\": 81, \"endPort\": 86}]}, {\"to\": [{\"cidr\": \"10.0.0.166/23\"}, {\"cidr\": \"0.0.0.0/0\", \"except\": []}, {\"cidr\": \"10.0.3.0/26\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 441, \"endPort\": 441}, {\"protocol\": \"tcp\", \"port\": 12}]}, {}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/119\"}, {\"cidr\": \"10.0.2.143\"}, {\"cidr\": \"106.225.35.0/24\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"udp\", \"port\": 65533}]}, {\"to\": [{\"cidr\": \"2001:DB8::/119\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 440}, {\"protocol\": \"udp\", \"port\": 65532, \"endPort\": 65532}, {\"protocol\": \"udp\", \"port\": 444}]}, {\"to\": [{\"cidr\": \"2001:db8::100/120\"}], \"ports\": [{}, {\"protocol\": \"udp\", \"port\": 444, \"endPort\": 446}, {\"port\": 444, \"endPort\": 450}]}, {\"to\": [{\"cidr\": \"2001:db8::330/124\"}, {\"cidr\": \"8000::/1\", \"except\": []}, {\"cidr\": \"ac33:dfbd:988e:f239:c96e:22e6:b800:0/104\"}, {\"cidr\": \"39.0.0.0/8\", \"except\": []}], \"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"tcp\", \"port\": 4}, {\"protocol\": \"udp\"}]}, {}, {\"to\": [{\"cidr\": \"216.83.25.209/0.0.0.1\", \"except\": [\"216.83.25.208/31\", \"216.83.25.208/32\"]}, {\"cidr\": \"227.241.226.0/255.255.255.0\"}, {\"cidr\": \"10.0.2.144/28\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 443}, {\"protocol\": \"udp\", \"port\": 80}, {\"protocol\": \"tcp\", \"port\": 440}, {\"protocol\": \"tcp\", \"port\": 10, \"endPort\": 10}]}, {\"ports\": [{\"protocol\": \"tcp\", \"port\": 78}]}, {\"cidr\": \"::/1\", \"protocol\": \"any\"}, {\"to\": [{\"cidr\": \"e4b3:4d5:2f34:6097:3667:aceb:e833:47ee/128\"}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.0/0120\"}], \"ports\": [{\"port\": 440, \"endPort\": 445}, {\"protocol\": \"any\", \"port\": 65530, \"endPort\": 65531}, {\"protocol\": \"tcp\", \"port\": 2, \"endPort\": 2736}]}, {\"to\": [{\"cidr\": \"2001:db8::2a4/127\"}, {\"cidr\": \"2001:db8::300/121\"}, {\"cidr\": \"10.0.3.13/30\"}, {\"cidr\": \"10.0.0.89/22\"}]}, {\"to\": [{\"cidr\": \"2001:db8::188/121\", \"except\": [\"2001:db8::190/125\"]}, {\"cidr\": \"10.0.3.23/26\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 79, \"endPort\": 84}, {\"protocol\": \"tcp\", \"port\": 444}, {\"port\": 65533}, {\"protocol\": \"tcp\", \"port\": 8, \"endPort\": 63690}]}, {\"cidr\": \"10.0.0.0/23\", \"protocol\": \"tcp\", \"port\": 443}, {\"cidr\": \"10.0.3.0/24\", \"except\": [\"10.0.3.240/29\", \"10.0.3.167/00029\"], \"protocol\": \"any\"}, {\"to\": [{\"cidr\": \"10.0.2.0/23\"}], \"ports\": [{\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"10.0.2.0/024\"}, {\"cidr\": \"10.0.0.148/28\"}]}, {\"to\": [{\"cidr\": \"0.0.0.0/01\", \"except\": [\"17.124.189.197/8\", \"33.0.0.0/8\"]}, {\"cidr\": \"0.0.0.0/0.0.0.0\", \"except\": [\"158.144.137.248/248.0.0.0\", \"128.0.0.0/01\"]}, {\"cidr\": \"2001:db8::/118\", \"except\": [\"2001:db8::313/121\"]}, {\"cidr\": \"10.0.0.0/22\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 79}, {\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 82}, {\"protocol\": \"tcp\", \"port\": 1}]}, {\"cidr\": \"2001:db8::120/123\", \"protocol\": \"udp\", \"port\": 78, \"endPort\": 82}, {}, {\"cidr\": \"2001:db8::/123\", \"protocol\": \"tcp\"}, {\"cidr\": \"10.0.2.104/32\", \"protocol\": \"tcp\", \"port\": 3}, {\"cidr\": \"64.0.0.0/2\", \"except\": [\"89.10.80.183/224.0.0.0\", \"80.0.0.0/8\", \"116.128.0.0/0.127.255.255\"], \"protocol\": \"tcp\"}]}"}],"expected":{"allow":[],"deny":["0.0.0.0/0","::/0"]}}, + {"name":"random #43","input":[{"text":"{\n \"deny\": []\n}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #44","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"10.0.1.174/30\"},{\"cidr\":\"10.0.0.132/30\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":NaN}]},{\"cidr\":\"2001:db8::/118\"},{\"to\":[{\"cidr\":\"10.0.1.0/25\",\"except\":[\"10.0.1.41/0026\",\"10.0.1.57/27\",\"10.0.1.32/255.255.255.224\"]},{\"cidr\":\"10.0.1.144/28\",\"except\":[]},{\"cidr\":\"10.0.2.144/00028\"}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"tcp\",\"port\":443,\"endPort\":448},{\"protocol\":\"udp\",\"port\":9},{\"protocol\":\"udp\",\"port\":445}]},{\"to\":[{\"cidr\":\"30.44.62.94/2\"}]},{\"to\":[{\"cidr\":\"2001:db8::1f4/126\"}]},{\"to\":[{\"cidr\":\"af82:e6a3:b081:4741:9c2f:e26:e600:0/104\",\"except\":[\"af82:e6a3:b081:4741:9c2f:e26:e634:6973/000112\"]},{\"cidr\":\"128.0.0.0/127.255.255.255\",\"except\":[]},{\"cidr\":\"2001:db8::30/000120\"}],\"ports\":[{\"protocol\":\"any\",\"port\":80,\"endPort\":85},{\"protocol\":\"any\",\"port\":65530},{\"protocol\":\"tcp\",\"port\":79,\"endPort\":82},{\"protocol\":\"any\",\"port\":9}]}],\"deny\":[]}"}],"expected":{"error":"allow[0].ports[0].port must be an integer"}}, + {"name":"random #45","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"64.0.0.0/2\"\n},\n{\n\"cidr\": \"10.0.1.72/29\",\n\"except\": [\n\"10.0.1.72/29\",\n\"10.0.1.79/31\",\n\"10.0.0.0/33\"\n]\n}\n],\n\"ports\": [\n{\n\"protocol\": \"tcp\"\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:02e6/122\",\n\"except\": []\n},\n{\n\"cidr\": \"10.0.3.0/24\"\n},\n{\n\"cidr\": \"10.0.0.40/30\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"any\"\n},\n{\n\"protocol\": \"tcp\",\n\"port\": 444\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.0/26\",\n\"except\": []\n},\n{\n\"cidr\": \"2001:db8::255/119\",\n\"except\": [\n\"2001:db8::28d/121\",\n\"2001:db8::280/123\",\n\"2001:db8::308/127\"\n]\n},\n{\n\"cidr\": \"::ffff:53.183.196.0/00120\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"any\"\n}\n]\n},\n{\n\"cidr\": \"10.0.0.110/22\",\n\"except\": [\n\"10.0.1.128/25\",\n\"10.0.2.0/026\"\n],\n\"protocol\": \"any\",\n\"port\": 444\n},\n{\n\"ports\": [\n{\n\"protocol\": \"tcp\",\n\"port\": 4,\n\"endPort\": 9\n},\n{\n\"protocol\": \"any\",\n\"port\": 9\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.1.200/029\"\n},\n{\n\"cidr\": \"72.57.105.0/24\",\n\"except\": [\n\"72.57.105.164/28\",\n\"72.57.105.95/32\"\n]\n},\n{\n\"cidr\": \"10.0.1.192/24\"\n},\n{\n\"cidr\": \"10.0.0.0/23\"\n}\n],\n\"ports\": [\n{},\n{\n\"port\": 65530,\n\"endPort\": 65530\n}\n]\n},\n{\n\"cidr\": \"2001:db8::300/123\",\n\"protocol\": \"icmp\"\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.1.133/32\",\n\"except\": [\n\"10.0.1.133/32\",\n\"10.0.1.133\"\n]\n},\n{\n\"cidr\": \"2001:db8::350/126\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"icmp\"\n},\n{\n\"protocol\": \"tcp\"\n},\n{\n\"protocol\": \"any\"\n},\n{\n\"protocol\": \"any\",\n\"port\": 12,\n\"endPort\": 17\n}\n]\n}\n],\n\"deny\": [\n{\n\"cidr\": \"2001:db8::be/125\",\n\"except\": [\n\"2001:db8::bf/128\",\n\"2001:DB8::BA/00127\",\n\"2001:db8::bb/128\"\n],\n\"protocol\": \"udp\",\n\"port\": 65535,\n\"endPort\": 65535\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/23\",\n\"except\": [\n\"10.0.1.182/24\",\n\"10.0.1.108/31\"\n]\n},\n{\n\"cidr\": \"10.0.3.34/31\"\n},\n{\n\"cidr\": \"198.24.190.118/4\",\n\"except\": [\n\"196.0.0.0/8\",\n\"192.0.0.0/5\"\n]\n}\n]\n}\n]\n}"}],"expected":{"error":"allow[0].to[1].except[2] is not a valid IPv4 or IPv6 CIDR: 10.0.0.0/33"}}, + {"name":"random #46","input":[{"text":"{\n \"allow\":[\n {\n \"to\":[\n {\n \"cidr\":\"2001:0db8:0000:0000:0000:0000:0.0.2.185/127\",\n \"except\":[\n \"2001:db8::2b8\",\n \"2001:db8::2b8/127\"\n ]\n },\n {\n \"cidr\":\"2001:DB8::397/122\"\n },\n {\n \"cidr\":\"10.0.0.0/32\"\n },\n {\n \"cidr\":\"10.0.0.40/024\"\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"10.0.0.59/255.255.255.252\"\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"ff4c:12ce:acd1:6d60:3e99:9fe3:2b35:356/127\"\n },\n {\n \"cidr\":\"4c48:aa07:7d40:380b:1a6:193b::/96\"\n },\n {\n \"cidr\":\"80.0.0.0/0008\"\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"icmp\"\n },\n {\n \"protocol\":\"udp\"\n },\n {\n \"protocol\":\"any\",\n \"port\":442,\n \"endPort\":446\n },\n {\n \"protocol\":\"tcp\",\n \"port\":65535,\n \"endPort\":65535\n }\n ]\n },\n {\n \"cidr\":\"10.0.0.192/255.255.255.192\",\n \"except\":[\n \"10.0.0.198/29\",\n \"10.0.0.192/31\",\n \"10.0.0.195/27\"\n ],\n \"protocol\":\"tcp\"\n }\n ],\n \"deny\":[\n {\n \"cidr\":\"892d:173c:5886:2788::/64\",\n \"protocol\":\"tcp\",\n \"port\":440,\n \"endPort\":440\n },\n {\n \"to\":[\n {\n \"cidr\":\"2001:db8::200/120\"\n },\n {\n \"cidr\":\"10.0.2.172/28\"\n },\n {\n \"cidr\":\"0.0.0.0/0.0.0.0\"\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"any\"\n },\n {\n \"protocol\":\"icmp\"\n },\n {\n \"protocol\":\"udp\",\n \"port\":1,\n \"endPort\":4\n }\n ]\n },\n {\n \"cidr\":\"10.0.3.183/029\",\n \"protocol\":\"any\",\n \"port\":65531\n },\n {\n \"to\":[\n {\n \"cidr\":\"89.226.0.0/16\",\n \"except\":[\n \"89.226.234.128/17\"\n ]\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"tcp\",\n \"port\":6\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"10.0.2.166/22\"\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"tcp\",\n \"port\":80\n },\n {\n \"protocol\":\"tcp\",\n \"port\":65531,\n \"endPort\":65535\n },\n {\n \"protocol\":\"tcp\",\n \"port\":11\n },\n {\n \"protocol\":\"udp\",\n \"port\":4\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"130.187.202.139\"\n },\n {\n \"cidr\":\"2001:db8::100/121\",\n \"except\":[\n \"2001:db8::148/125\",\n \"2001:db8::16b\"\n ]\n },\n {\n \"cidr\":\"2001:DB8::2AE/127\"\n },\n {\n \"cidr\":\"2001:db8::/118\"\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"udp\",\n \"port\":78,\n \"endPort\":83\n },\n {\n \"protocol\":\"udp\"\n },\n {\n \"protocol\":\"tcp\"\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["10.0.0.0/24","80.0.0.0/8:icmp","80.0.0.0/8:tcp:442-446","80.0.0.0/8:tcp:65535","80.0.0.0/8:udp","2001:db8::380/122","4c48:aa07:7d40:380b:1a6:193b::/96:icmp","4c48:aa07:7d40:380b:1a6:193b::/96:tcp:442-446","4c48:aa07:7d40:380b:1a6:193b::/96:tcp:65535","4c48:aa07:7d40:380b:1a6:193b::/96:udp","ff4c:12ce:acd1:6d60:3e99:9fe3:2b35:356/127:icmp","ff4c:12ce:acd1:6d60:3e99:9fe3:2b35:356/127:tcp:442-446","ff4c:12ce:acd1:6d60:3e99:9fe3:2b35:356/127:tcp:65535","ff4c:12ce:acd1:6d60:3e99:9fe3:2b35:356/127:udp"],"deny":["0.0.0.0/0","2001:db8::/118:tcp","2001:db8::/118:udp","2001:db8::200/120","892d:173c:5886:2788::/64:tcp:440"]}}, + {"name":"random #47","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"50d1:d9cf:76e9:8abb:4360:ac7f:563e:c278\"\n}, \n{\n\"cidr\": \"10.0.0.206/0.0.0.63\"\n}\n], \n\"port\": true\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.1.7/24\"\n}, \n{\n\"cidr\": \"2001:DB8::2C0/122\", \n\"except\": []\n}\n], \n\"ports\": [\n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"any\"\n}\n]\n}, \n{\n\"cidr\": \"10.0.1.64/30\", \n\"protocol\": \"tcp\", \n\"port\": 79, \n\"endPort\": 84\n}, \n{\n\"cidr\": \"10.0.2.225\", \n\"protocol\": \"tcp\", \n\"port\": 80, \n\"endPort\": 81\n}\n], \n\"deny\": []\n}"}],"expected":{"error":"allow[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #48","input":[{"text":"{\"deny\": [{\"cidr\": \"10.0.1.22/31\", \"protocol\": \"tcp\"}, {\"to\": [{\"cidr\": \"10.0.0.128/25\"}]}, {\"to\": [{\"cidr\": \"10.0.2.128/255.255.255.128\"}, {\"cidr\": \"::ffff:226.231.246.82/127\"}, {\"cidr\": \"10.0.0.0/0026\", \"except\": []}, {\"cidr\": \"0.0.0.0/0\"}]}, {\"to\": [{\"cidr\": \"2001:db8::2fe/119\", \"except\": [\"2001:db8::300/123\", \"2001:db8::2a0/126\"]}, {\"cidr\": \"2001:db8::313/119\"}, {\"cidr\": \"10.0.3.112/29\", \"except\": [\"10.0.3.112/00030\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 445, \"endPort\": 450}, {\"protocol\": \"tcp\", \"port\": 65530, \"endPort\": 65533}]}, {\"cidr\": \"2001:db8::200/119\", \"except\": [], \"protocol\": \"udp\", \"port\": 65534}, {\"to\": [{\"cidr\": \"10.0.1.192/29\"}]}]}"}],"expected":{"allow":[],"deny":["0.0.0.0/0","::ffff:226.231.246.82/127","2001:db8::200/119:tcp:65530-65533","2001:db8::200/119:udp:445-450","2001:db8::200/119:udp:65534"]}}, + {"name":"random #49","input":[{"text":"{}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #50","input":[{"text":"{\n \"allow\":[\n {\n \"to\":[\n {\n \"cidr\":\"10.0.2.51/22\",\n \"except\":[]\n },\n {\n \"cidr\":\"2001:db8::/118\"\n }\n ],\n \"ports\":[\n {\n \"port\":80.0\n },\n {\n \"protocol\":\"tcp\",\n \"port\":65535\n },\n {\n \"protocol\":\"udp\",\n \"port\":6,\n \"endPort\":7\n }\n ]\n },\n {\n \"cidr\":\"10.0.2.104/30\",\n \"protocol\":\"any\",\n \"port\":109\n },\n {\n \"cidr\":\"2001:db8::2cb/128\",\n \"protocol\":\"udp\",\n \"port\":373,\n \"endPort\":375\n },\n {\n \"cidr\":\"10.0.1.162/32\",\n \"protocol\":\"tcp\",\n \"port\":67,\n \"endPort\":70\n },\n {\n \"cidr\":\"10.0.2.220/30\",\n \"protocol\":\"udp\",\n \"port\":179,\n \"endPort\":181\n },\n {\n \"cidr\":\"10.0.3.130/31\",\n \"protocol\":\"any\",\n \"port\":137\n },\n {\n \"cidr\":\"10.0.0.153/32\",\n \"protocol\":\"any\",\n \"port\":275\n },\n {\n \"cidr\":\"10.0.3.130/31\",\n \"protocol\":\"any\",\n \"port\":261\n },\n {\n \"cidr\":\"2001:db8::10b/128\",\n \"protocol\":\"tcp\",\n \"port\":349,\n \"endPort\":349\n },\n {\n \"cidr\":\"10.0.2.12/30\",\n \"protocol\":\"any\",\n \"port\":209,\n \"endPort\":213\n },\n {\n \"cidr\":\"10.0.2.48/30\",\n \"protocol\":\"udp\",\n \"port\":125\n },\n {\n \"cidr\":\"10.0.1.16/30\",\n \"protocol\":\"udp\",\n \"port\":173\n },\n {\n \"cidr\":\"2001:db8::2a0/128\",\n \"protocol\":\"udp\",\n \"port\":153\n },\n {\n \"cidr\":\"10.0.3.50/31\",\n \"protocol\":\"any\",\n \"port\":35\n },\n {\n \"cidr\":\"10.0.0.52/32\",\n \"protocol\":\"tcp\",\n \"port\":51,\n \"endPort\":55\n },\n {\n \"cidr\":\"10.0.1.218/32\",\n \"protocol\":\"tcp\",\n \"port\":197\n },\n {\n \"cidr\":\"10.0.1.38/31\",\n \"protocol\":\"any\",\n \"port\":215,\n \"endPort\":219\n },\n {\n \"cidr\":\"10.0.0.160/30\",\n \"protocol\":\"any\",\n \"port\":121,\n \"endPort\":125\n },\n {\n \"cidr\":\"2001:db8::3f7/128\",\n \"protocol\":\"tcp\",\n \"port\":141,\n \"endPort\":143\n },\n {\n \"cidr\":\"10.0.0.162/32\",\n \"protocol\":\"any\",\n \"port\":199\n },\n {\n \"cidr\":\"10.0.3.114/31\",\n \"protocol\":\"tcp\",\n \"port\":147\n },\n {\n \"cidr\":\"10.0.2.30/32\",\n \"protocol\":\"any\",\n \"port\":69,\n \"endPort\":69\n },\n {\n \"cidr\":\"10.0.3.206/31\",\n \"protocol\":\"any\",\n \"port\":17\n },\n {\n \"cidr\":\"10.0.2.108/30\",\n \"protocol\":\"any\",\n \"port\":153\n },\n {\n \"cidr\":\"10.0.3.66/31\",\n \"protocol\":\"any\",\n \"port\":19\n },\n {\n \"cidr\":\"10.0.0.114/31\",\n \"protocol\":\"udp\",\n \"port\":37,\n \"endPort\":39\n },\n {\n \"cidr\":\"2001:db8::1/128\",\n \"protocol\":\"udp\",\n \"port\":7\n },\n {\n \"cidr\":\"10.0.3.132/30\",\n \"protocol\":\"udp\",\n \"port\":149\n },\n {\n \"cidr\":\"10.0.0.17/32\",\n \"protocol\":\"tcp\",\n \"port\":177\n },\n {\n \"cidr\":\"10.0.0.176/31\",\n \"protocol\":\"any\",\n \"port\":205,\n \"endPort\":206\n },\n {\n \"cidr\":\"2001:db8::11/128\",\n \"protocol\":\"udp\",\n \"port\":85,\n \"endPort\":88\n },\n {\n \"cidr\":\"2001:db8::294/128\",\n \"protocol\":\"udp\",\n \"port\":285,\n \"endPort\":289\n },\n {\n \"cidr\":\"10.0.0.228/31\",\n \"protocol\":\"udp\",\n \"port\":333\n },\n {\n \"cidr\":\"10.0.0.236/31\",\n \"protocol\":\"tcp\",\n \"port\":275\n },\n {\n \"cidr\":\"10.0.2.144/31\",\n \"protocol\":\"tcp\",\n \"port\":169\n },\n {\n \"cidr\":\"10.0.2.0/30\",\n \"protocol\":\"any\",\n \"port\":143,\n \"endPort\":144\n },\n {\n \"cidr\":\"10.0.1.52/30\",\n \"protocol\":\"tcp\",\n \"port\":153\n },\n {\n \"cidr\":\"10.0.1.172/30\",\n \"protocol\":\"any\",\n \"port\":325,\n \"endPort\":329\n },\n {\n \"cidr\":\"2001:db8::e3/128\",\n \"protocol\":\"tcp\",\n \"port\":273\n },\n {\n \"cidr\":\"10.0.2.210/31\",\n \"protocol\":\"any\",\n \"port\":389\n },\n {\n \"cidr\":\"10.0.3.126/32\",\n \"protocol\":\"any\",\n \"port\":247\n },\n {\n \"cidr\":\"10.0.2.56/30\",\n \"protocol\":\"udp\",\n \"port\":123,\n \"endPort\":127\n },\n {\n \"cidr\":\"10.0.1.228/30\",\n \"protocol\":\"udp\",\n \"port\":149\n },\n {\n \"cidr\":\"10.0.0.250/31\",\n \"protocol\":\"tcp\",\n \"port\":119\n },\n {\n \"cidr\":\"10.0.0.80/31\",\n \"protocol\":\"udp\",\n \"port\":349\n },\n {\n \"cidr\":\"10.0.3.30/32\",\n \"protocol\":\"any\",\n \"port\":39\n },\n {\n \"cidr\":\"10.0.2.26/32\",\n \"protocol\":\"udp\",\n \"port\":293\n },\n {\n \"cidr\":\"10.0.3.80/31\",\n \"protocol\":\"udp\",\n \"port\":33\n },\n {\n \"cidr\":\"10.0.2.79/32\",\n \"protocol\":\"any\",\n \"port\":375,\n \"endPort\":377\n },\n {\n \"cidr\":\"10.0.2.40/30\",\n \"protocol\":\"any\",\n \"port\":341\n },\n {\n \"cidr\":\"2001:db8::162/128\",\n \"protocol\":\"udp\",\n \"port\":249\n },\n {\n \"cidr\":\"10.0.3.62/31\",\n \"protocol\":\"udp\",\n \"port\":275,\n \"endPort\":276\n },\n {\n \"cidr\":\"10.0.2.148/32\",\n \"protocol\":\"any\",\n \"port\":355\n },\n {\n \"cidr\":\"10.0.1.133/32\",\n \"protocol\":\"udp\",\n \"port\":181,\n \"endPort\":185\n },\n {\n \"cidr\":\"10.0.2.214/32\",\n \"protocol\":\"any\",\n \"port\":287,\n \"endPort\":290\n },\n {\n \"cidr\":\"10.0.1.172/30\",\n \"protocol\":\"any\",\n \"port\":283,\n \"endPort\":287\n },\n {\n \"cidr\":\"2001:db8::2bd/128\",\n \"protocol\":\"tcp\",\n \"port\":261\n },\n {\n \"cidr\":\"10.0.2.253/32\",\n \"protocol\":\"tcp\",\n \"port\":141\n },\n {\n \"cidr\":\"10.0.2.241/32\",\n \"protocol\":\"udp\",\n \"port\":195\n },\n {\n \"cidr\":\"10.0.0.217/32\",\n \"protocol\":\"any\",\n \"port\":109\n },\n {\n \"cidr\":\"10.0.1.248/31\",\n \"protocol\":\"any\",\n \"port\":121,\n \"endPort\":121\n },\n {\n \"cidr\":\"2001:db8::26b/128\",\n \"protocol\":\"tcp\",\n \"port\":31\n },\n {\n \"cidr\":\"2001:db8::264/128\",\n \"protocol\":\"tcp\",\n \"port\":65\n },\n {\n \"cidr\":\"10.0.2.132/32\",\n \"protocol\":\"udp\",\n \"port\":17\n },\n {\n \"cidr\":\"10.0.2.8/30\",\n \"protocol\":\"any\",\n \"port\":53\n },\n {\n \"cidr\":\"2001:db8::1e9/128\",\n \"protocol\":\"any\",\n \"port\":331,\n \"endPort\":332\n },\n {\n \"cidr\":\"10.0.2.96/31\",\n \"protocol\":\"any\",\n \"port\":105\n },\n {\n \"cidr\":\"2001:db8::204/128\",\n \"protocol\":\"any\",\n \"port\":345,\n \"endPort\":347\n },\n {\n \"cidr\":\"10.0.0.0/30\",\n \"protocol\":\"udp\",\n \"port\":119\n },\n {\n \"cidr\":\"2001:db8::1a/128\",\n \"protocol\":\"any\",\n \"port\":371\n },\n {\n \"cidr\":\"10.0.2.232/30\",\n \"protocol\":\"udp\",\n \"port\":151\n },\n {\n \"cidr\":\"10.0.3.216/31\",\n \"protocol\":\"tcp\",\n \"port\":339\n },\n {\n \"cidr\":\"2001:db8::398/128\",\n \"protocol\":\"tcp\",\n \"port\":7\n },\n {\n \"cidr\":\"10.0.0.150/31\",\n \"protocol\":\"any\",\n \"port\":107\n },\n {\n \"cidr\":\"10.0.1.88/30\",\n \"protocol\":\"any\",\n \"port\":311\n },\n {\n \"cidr\":\"10.0.0.172/30\",\n \"protocol\":\"any\",\n \"port\":149\n },\n {\n \"cidr\":\"2001:db8::3f0/128\",\n \"protocol\":\"any\",\n \"port\":35\n },\n {\n \"cidr\":\"10.0.0.30/32\",\n \"protocol\":\"tcp\",\n \"port\":167\n },\n {\n \"cidr\":\"2001:db8::e7/128\",\n \"protocol\":\"udp\",\n \"port\":307\n },\n {\n \"cidr\":\"10.0.1.48/31\",\n \"protocol\":\"any\",\n \"port\":285\n },\n {\n \"cidr\":\"10.0.3.2/32\",\n \"protocol\":\"tcp\",\n \"port\":187\n },\n {\n \"cidr\":\"10.0.1.152/30\",\n \"protocol\":\"tcp\",\n \"port\":317\n },\n {\n \"cidr\":\"10.0.0.100/30\",\n \"protocol\":\"tcp\",\n \"port\":223\n },\n {\n \"cidr\":\"10.0.3.176/30\",\n \"protocol\":\"tcp\",\n \"port\":127\n },\n {\n \"cidr\":\"2001:db8::f1/128\",\n \"protocol\":\"tcp\",\n \"port\":383\n },\n {\n \"cidr\":\"2001:db8::336/128\",\n \"protocol\":\"tcp\",\n \"port\":327\n },\n {\n \"cidr\":\"10.0.2.46/31\",\n \"protocol\":\"udp\",\n \"port\":291\n },\n {\n \"cidr\":\"10.0.2.124/30\",\n \"protocol\":\"udp\",\n \"port\":277,\n \"endPort\":280\n },\n {\n \"cidr\":\"10.0.1.114/31\",\n \"protocol\":\"tcp\",\n \"port\":193\n },\n {\n \"cidr\":\"2001:db8::ca/128\",\n \"protocol\":\"tcp\",\n \"port\":115\n },\n {\n \"cidr\":\"10.0.2.66/31\",\n \"protocol\":\"any\",\n \"port\":369\n },\n {\n \"cidr\":\"10.0.3.254/32\",\n \"protocol\":\"tcp\",\n \"port\":193\n },\n {\n \"cidr\":\"2001:db8::33c/128\",\n \"protocol\":\"udp\",\n \"port\":193,\n \"endPort\":195\n },\n {\n \"cidr\":\"2001:db8::3b0/128\",\n \"protocol\":\"any\",\n \"port\":111,\n \"endPort\":112\n },\n {\n \"cidr\":\"10.0.2.8/30\",\n \"protocol\":\"tcp\",\n \"port\":385\n },\n {\n \"cidr\":\"2001:db8::1cd/128\",\n \"protocol\":\"tcp\",\n \"port\":111\n },\n {\n \"cidr\":\"2001:db8::3b6/128\",\n \"protocol\":\"udp\",\n \"port\":169\n },\n {\n \"cidr\":\"10.0.0.150/31\",\n \"protocol\":\"any\",\n \"port\":265\n },\n {\n \"cidr\":\"2001:db8::a8/128\",\n \"protocol\":\"tcp\",\n \"port\":35\n },\n {\n \"cidr\":\"10.0.3.40/30\",\n \"protocol\":\"tcp\",\n \"port\":327\n },\n {\n \"cidr\":\"2001:db8::43/128\",\n \"protocol\":\"udp\",\n \"port\":89,\n \"endPort\":91\n },\n {\n \"cidr\":\"10.0.0.28/30\",\n \"protocol\":\"any\",\n \"port\":49,\n \"endPort\":50\n },\n {\n \"cidr\":\"2001:db8::1ee/128\",\n \"protocol\":\"any\",\n \"port\":315,\n \"endPort\":316\n },\n {\n \"cidr\":\"10.0.2.8/30\",\n \"protocol\":\"udp\",\n \"port\":347\n },\n {\n \"cidr\":\"2001:db8::22d/128\",\n \"protocol\":\"any\",\n \"port\":333,\n \"endPort\":333\n },\n {\n \"cidr\":\"10.0.0.220/32\",\n \"protocol\":\"tcp\",\n \"port\":255,\n \"endPort\":255\n },\n {\n \"cidr\":\"2001:db8::307/128\",\n \"protocol\":\"udp\",\n \"port\":89,\n \"endPort\":92\n },\n {\n \"cidr\":\"10.0.2.220/32\",\n \"protocol\":\"any\",\n \"port\":109\n },\n {\n \"cidr\":\"10.0.2.18/31\",\n \"protocol\":\"tcp\",\n \"port\":339,\n \"endPort\":342\n },\n {\n \"cidr\":\"10.0.0.164/32\",\n \"protocol\":\"udp\",\n \"port\":211,\n \"endPort\":215\n },\n {\n \"cidr\":\"2001:db8::357/128\",\n \"protocol\":\"tcp\",\n \"port\":91\n },\n {\n \"cidr\":\"2001:db8::20f/128\",\n \"protocol\":\"udp\",\n \"port\":327,\n \"endPort\":329\n },\n {\n \"cidr\":\"10.0.2.168/30\",\n \"protocol\":\"udp\",\n \"port\":243\n },\n {\n \"cidr\":\"2001:db8::37e/128\",\n \"protocol\":\"udp\",\n \"port\":315\n },\n {\n \"cidr\":\"2001:db8::305/128\",\n \"protocol\":\"any\",\n \"port\":385,\n \"endPort\":388\n },\n {\n \"cidr\":\"10.0.3.213/32\",\n \"protocol\":\"tcp\",\n \"port\":341\n },\n {\n \"cidr\":\"2001:db8::375/128\",\n \"protocol\":\"tcp\",\n \"port\":271\n },\n {\n \"cidr\":\"10.0.2.122/31\",\n \"protocol\":\"udp\",\n \"port\":201,\n \"endPort\":202\n },\n {\n \"cidr\":\"10.0.1.76/30\",\n \"protocol\":\"tcp\",\n \"port\":79,\n \"endPort\":83\n },\n {\n \"cidr\":\"10.0.1.32/30\",\n \"protocol\":\"udp\",\n \"port\":301\n },\n {\n \"cidr\":\"10.0.3.72/32\",\n \"protocol\":\"udp\",\n \"port\":369\n },\n {\n \"cidr\":\"2001:db8::7/128\",\n \"protocol\":\"tcp\",\n \"port\":361,\n \"endPort\":362\n },\n {\n \"cidr\":\"10.0.2.216/32\",\n \"protocol\":\"udp\",\n \"port\":391,\n \"endPort\":391\n },\n {\n \"cidr\":\"2001:db8::19e/128\",\n \"protocol\":\"tcp\",\n \"port\":149\n },\n {\n \"cidr\":\"10.0.2.222/32\",\n \"protocol\":\"udp\",\n \"port\":95\n },\n {\n \"cidr\":\"10.0.0.220/30\",\n \"protocol\":\"udp\",\n \"port\":311\n },\n {\n \"cidr\":\"2001:db8::15b/128\",\n \"protocol\":\"any\",\n \"port\":257\n },\n {\n \"cidr\":\"10.0.0.232/30\",\n \"protocol\":\"udp\",\n \"port\":299,\n \"endPort\":301\n },\n {\n \"cidr\":\"10.0.0.22/31\",\n \"protocol\":\"udp\",\n \"port\":315\n },\n {\n \"cidr\":\"10.0.3.179/32\",\n \"protocol\":\"tcp\",\n \"port\":161\n },\n {\n \"cidr\":\"10.0.2.18/32\",\n \"protocol\":\"udp\",\n \"port\":31\n },\n {\n \"cidr\":\"10.0.1.216/31\",\n \"protocol\":\"any\",\n \"port\":333\n },\n {\n \"cidr\":\"10.0.2.120/31\",\n \"protocol\":\"tcp\",\n \"port\":245\n },\n {\n \"cidr\":\"10.0.0.224/32\",\n \"protocol\":\"any\",\n \"port\":167,\n \"endPort\":167\n },\n {\n \"cidr\":\"10.0.0.12/30\",\n \"protocol\":\"tcp\",\n \"port\":19\n },\n {\n \"cidr\":\"10.0.3.236/31\",\n \"protocol\":\"udp\",\n \"port\":321,\n \"endPort\":321\n },\n {\n \"cidr\":\"10.0.0.113/32\",\n \"protocol\":\"tcp\",\n \"port\":125,\n \"endPort\":128\n },\n {\n \"cidr\":\"10.0.2.176/30\",\n \"protocol\":\"tcp\",\n \"port\":339\n },\n {\n \"cidr\":\"10.0.1.48/30\",\n \"protocol\":\"any\",\n \"port\":303,\n \"endPort\":306\n },\n {\n \"cidr\":\"2001:db8::15f/128\",\n \"protocol\":\"udp\",\n \"port\":327\n },\n {\n \"cidr\":\"10.0.1.140/30\",\n \"protocol\":\"any\",\n \"port\":55\n },\n {\n \"cidr\":\"10.0.1.36/31\",\n \"protocol\":\"udp\",\n \"port\":69\n },\n {\n \"cidr\":\"10.0.2.10/31\",\n \"protocol\":\"any\",\n \"port\":171,\n \"endPort\":175\n },\n {\n \"cidr\":\"2001:db8::34a/128\",\n \"protocol\":\"udp\",\n \"port\":67\n },\n {\n \"cidr\":\"10.0.3.144/31\",\n \"protocol\":\"tcp\",\n \"port\":89\n },\n {\n \"cidr\":\"10.0.2.16/30\",\n \"protocol\":\"udp\",\n \"port\":333\n },\n {\n \"cidr\":\"10.0.2.87/32\",\n \"protocol\":\"tcp\",\n \"port\":171\n },\n {\n \"cidr\":\"2001:db8::2b6/128\",\n \"protocol\":\"udp\",\n \"port\":299\n },\n {\n \"cidr\":\"10.0.1.56/30\",\n \"protocol\":\"tcp\",\n \"port\":257,\n \"endPort\":257\n },\n {\n \"cidr\":\"10.0.2.28/31\",\n \"protocol\":\"udp\",\n \"port\":107\n },\n {\n \"cidr\":\"10.0.1.234/31\",\n \"protocol\":\"udp\",\n \"port\":193\n },\n {\n \"cidr\":\"10.0.2.48/31\",\n \"protocol\":\"udp\",\n \"port\":171\n },\n {\n \"cidr\":\"10.0.0.152/30\",\n \"protocol\":\"any\",\n \"port\":345\n },\n {\n \"cidr\":\"2001:db8::bd/128\",\n \"protocol\":\"any\",\n \"port\":113,\n \"endPort\":115\n },\n {\n \"cidr\":\"2001:db8::7/128\",\n \"protocol\":\"tcp\",\n \"port\":281\n },\n {\n \"cidr\":\"10.0.3.150/31\",\n \"protocol\":\"udp\",\n \"port\":387\n },\n {\n \"cidr\":\"10.0.2.71/32\",\n \"protocol\":\"any\",\n \"port\":93\n },\n {\n \"cidr\":\"2001:db8::10b/128\",\n \"protocol\":\"udp\",\n \"port\":205\n },\n {\n \"cidr\":\"10.0.3.46/32\",\n \"protocol\":\"tcp\",\n \"port\":199\n },\n {\n \"cidr\":\"10.0.2.47/32\",\n \"protocol\":\"udp\",\n \"port\":335\n },\n {\n \"cidr\":\"10.0.1.128/31\",\n \"protocol\":\"udp\",\n \"port\":177\n },\n {\n \"cidr\":\"2001:db8::18c/128\",\n \"protocol\":\"udp\",\n \"port\":395\n },\n {\n \"cidr\":\"10.0.0.100/31\",\n \"protocol\":\"tcp\",\n \"port\":221\n },\n {\n \"cidr\":\"2001:db8::3a2/128\",\n \"protocol\":\"any\",\n \"port\":31\n },\n {\n \"cidr\":\"2001:db8::19d/128\",\n \"protocol\":\"tcp\",\n \"port\":111\n }\n ],\n \"deny\":[\n {\n \"cidr\":\"21a5:3abf:6df4:43b0:e677:e60d:41aa:3f07/0\",\n \"except\":[\n \"::/0\",\n \"ea7:dc24:70a5:8bc8:ca34:e8b0:a11b:290d/3\",\n \"b800::/5\"\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"2001:db8::1e4/126\"\n },\n {\n \"cidr\":\"247.139.55.75/8\",\n \"except\":[\n \"247.40.160.139/12\",\n \"247.46.212.221/15\",\n \"247.72.0.0/255.254.0.0\"\n ]\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"10.0.2.236\"\n },\n {\n \"cidr\":\"208.0.0.0/4\"\n },\n {\n \"cidr\":\"2001:db8::3a6\",\n \"except\":[\n \"2001:db8::3a6\"\n ]\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"any\",\n \"port\":12,\n \"endPort\":18\n },\n {\n \"protocol\":\"any\",\n \"port\":1\n },\n {\n \"protocol\":\"tcp\",\n \"port\":12\n },\n {\n \"protocol\":\"udp\",\n \"port\":79,\n \"endPort\":81\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"10.0.1.96/0027\"\n },\n {\n \"cidr\":\"98.182.67.50/255.255.0.0\"\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"a53d:74b4:4180:7239:fbf9:f191:9b79:d95/3\"\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"10.0.3.216/25\"\n },\n {\n \"cidr\":\"10.0.3.128/25\",\n \"except\":[]\n },\n {\n \"cidr\":\"10.0.1.245/32\"\n }\n ]\n }\n ]\n}"}],"expected":{"error":"allow[0].ports[0].port must be an integer"}}, + {"name":"random #51","input":[{"text":"{\"deny\":[{\"cidr\":\"42a1:6d8:d9d2:5197:6234:cc97:9646:acb3/3\",\"protocol\":\"any\"},{\"to\":[{\"cidr\":\"::ffff:55.35.254.0/120\"}],\"ports\":[{\"port\":443,\"endPort\":443},{\"protocol\":\"udp\",\"port\":65531}]},{\"to\":[{\"cidr\":\"10.0.2.0/24\",\"except\":[]},{\"cidr\":\"10.0.3.0/29\"},{\"cidr\":\"192.0.0.0/2\",\"except\":[\"198.0.0.0/07\",\"207.0.0.0/8\",\"240.0.0.0/240.0.0.0\"]}]},{\"to\":[{\"cidr\":\"10.0.2.103/27\"},{\"cidr\":\"25.139.174.39\"}],\"ports\":[{},{\"protocol\":\"tcp\",\"port\":80,\"endPort\":45339},{\"protocol\":\"udp\",\"port\":9,\"endPort\":15}]}]}"}],"expected":{"allow":[],"deny":["10.0.2.0/24","10.0.3.0/29","25.139.174.39/32","192.0.0.0/6","196.0.0.0/7","200.0.0.0/6","204.0.0.0/7","206.0.0.0/8","208.0.0.0/4","224.0.0.0/4","::ffff:55.35.254.0/120:tcp:443","::ffff:55.35.254.0/120:udp:443","::ffff:55.35.254.0/120:udp:65531","4000::/3"]}}, + {"name":"random #52","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.3.32/029\"}, {\"cidr\": \"2001:db8::2f0/126\", \"except\": [\"2001:db8::2f1\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 79, \"endPort\": 81}]}, {\"to\": [{\"cidr\": \"2001:db8::3a\", \"except\": [\"2001:db8::3a\"]}]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0200/119\", \"protocol\": \"tcp\", \"port\": 79, \"endPort\": 83}, {\"to\": [{\"cidr\": \"2001:db8::360/123\", \"except\": [\"10.0.1.72/255.255.255.248\"]}, {\"cidr\": \"10.0.2.36/31\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 442}, {\"protocol\": \"udp\", \"port\": 7, \"endPort\": 9}, {\"protocol\": \"udp\"}, {\"protocol\": \"udp\", \"port\": 65535}]}, {\"cidr\": \"10.0.2.208/0.0.0.15\", \"protocol\": \"tcp\", \"port\": 445}, {\"cidr\": \"2001:DB8::197/126\", \"except\": []}]}"}],"expected":{"error":"deny[3].to[0].except[0] must be contained in 2001:db8::360/123"}}, + {"name":"random #53","input":[{"text":"{\n \"allow\": [], \n \"deny\": [\n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::3f4/126\"\n }, \n {\n \"cidr\": \"10.0.3.161/25\"\n }, \n {\n \"cidr\": \"10.0.0.0/24\"\n }, \n {\n \"cidr\": \"235.145.88.199/31\"\n }\n ], \n \"ports\": [\n {\n \"port\": 12\n }\n ]\n }\n ]\n}"}],"expected":{"allow":[],"deny":["10.0.0.0/24:tcp:12","10.0.0.0/24:udp:12","10.0.3.128/25:tcp:12","10.0.3.128/25:udp:12","235.145.88.198/31:tcp:12","235.145.88.198/31:udp:12","2001:db8::3f4/126:tcp:12","2001:db8::3f4/126:udp:12"]}}, + {"name":"random #54","input":[{"text":"{\n\t\"allow\": [], \n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::2c0/125\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.160/28\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 65534\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 79\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":[],"deny":["10.0.1.160/28:tcp:79","10.0.1.160/28:tcp:65534","2001:db8::2c0/125:tcp:79","2001:db8::2c0/125:tcp:65534"]}}, + {"name":"random #55","input":[{"text":"{}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #56","input":[{"text":"{\n\"allow\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0274\"\n}\n]\n}"}],"expected":{"allow":["2001:db8::274/128"],"deny":[]}}, + {"name":"random #57","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"2001:DB8::25C/118\",\"except\":[\"2001:db8::a8/125\",\"2001:db8::40/124\"]},{\"cidr\":\"10.0.2.32/27\"}],\"ports\":[{\"protocol\":\"udp\"}]},{\"to\":[{\"cidr\":\"c4f4:fdd6:6758:eeec:b3da:478a:45bd:c75c/127\"},{\"cidr\":\"10.0.2.128/24\",\"except\":[\"10.0.2.40/255.255.255.248\"]},{\"cidr\":\"10.0.0.148/031\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":65534},{\"protocol\":\"any\"},{\"protocol\":\"any\",\"port\":65532,\"endPort\":65535},{\"protocol\":\"any\",\"port\":444}]},{\"ports\":[{\"protocol\":\"any\",\"port\":442},{\"protocol\":\"tcp\",\"port\":12}]}],\"deny\":[{\"to\":[{\"cidr\":\"237.0.0.0/12\"},{\"cidr\":\"2001:db8::300/122\"}],\"ports\":[{\"protocol\":\"any\",\"port\":445},{\"protocol\":\"tcp\",\"port\":65532}]},{\"to\":[{\"cidr\":\"192.0.0.0/15.255.255.255\"},{\"cidr\":\"e12e:bf95:4038:93c5:51a7:b068:99f7:ed11/0\"},{\"cidr\":\"10.0.0.0/23\"},{\"cidr\":\"10.0.0.0/23\"}],\"ports\":[{\"port\":442},{\"protocol\":\"any\",\"port\":65535,\"endPort\":65535}]},{\"to\":[{\"cidr\":\"10.0.0.8/255.255.255.192\"},{\"cidr\":\"2001:db8::247/119\"},{\"cidr\":\"2001:db8::294/126\"}],\"ports\":[{\"protocol\":\"tcp\"}]},{\"to\":[{\"cidr\":\"10.0.1.84/255.255.255.254\"},{\"cidr\":\"86.197.155.27/1\",\"except\":[\"87.128.0.0/9\"]},{\"cidr\":\"10.0.2.0/23\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":10,\"endPort\":46931},{\"protocol\":\"tcp\"},{\"protocol\":\"udp\",\"port\":7}]},{\"cidr\":\"10.0.3.96/30\",\"except\":[],\"protocol\":\"udp\",\"port\":6,\"endPort\":64587},{\"cidr\":\"10.0.2.152/29\",\"protocol\":\"any\",\"port\":65531,\"endPort\":65533}]}"}],"expected":{"allow":["0.0.0.0/0:tcp:12","0.0.0.0/0:tcp:442","0.0.0.0/0:udp:442","10.0.0.148/31","10.0.2.0/24:udp","10.0.2.0/27","10.0.2.32/29","10.0.2.48/28","10.0.2.64/26","10.0.2.128/25","::/0:tcp:12","::/0:tcp:442","::/0:udp:442","2001:db8::/122:udp","2001:db8::50/124:udp","2001:db8::60/123:udp","2001:db8::80/123:udp","2001:db8::a0/125:udp","2001:db8::b0/124:udp","2001:db8::c0/122:udp","2001:db8::100/120:udp","2001:db8::200/119:udp","c4f4:fdd6:6758:eeec:b3da:478a:45bd:c75c/127"],"deny":["0.0.0.0/2:tcp","0.0.0.0/2:udp:7","10.0.0.0/23:udp:442","10.0.0.0/23:udp:65535","10.0.2.152/29:udp:65531-65533","10.0.3.96/30:udp:6-64587","64.0.0.0/4:tcp","64.0.0.0/4:udp:7","80.0.0.0/6:tcp","80.0.0.0/6:udp:7","84.0.0.0/7:tcp","84.0.0.0/7:udp:7","86.0.0.0/8:tcp","86.0.0.0/8:udp:7","87.0.0.0/9:tcp","87.0.0.0/9:udp:7","88.0.0.0/5:tcp","88.0.0.0/5:udp:7","96.0.0.0/3:tcp","96.0.0.0/3:udp:7","192.0.0.0/4:tcp:442","192.0.0.0/4:tcp:65535","192.0.0.0/4:udp:442","192.0.0.0/4:udp:65535","237.0.0.0/12:tcp:445","237.0.0.0/12:tcp:65532","237.0.0.0/12:udp:445","::/0:tcp:442","::/0:tcp:65535","::/0:udp:442","::/0:udp:65535","2001:db8::200/119:tcp","2001:db8::300/122:udp:445"]}}, + {"name":"random #58","input":[{"text":"{\n \"allow\": [\n {\n \"cidr\": \"10.0.1.128/0027\", \n \"except\": [\n \"10.0.1.137/0032\", \n \"10.0.1.135/32\", \n \"10.0.1.128/27\"\n ], \n \"protocol\": \"udp\", \n \"port\": 9, \n \"endPort\": 52106\n }, \n {\n \"cidr\": \"10.0.0.0/22\", \n \"protocol\": \"tcp\", \n \"port\": 441, \n \"endPort\": 445\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 80\n }, \n {\n \"protocol\": \"any\"\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"4e93:db03:5723:b1a1:40e1:2729:1b4f:8e7c/128\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"udp\", \n \"port\": 3, \n \"endPort\": 7\n }, \n {\n \"port\": 440\n }, \n {\n \"protocol\": \"tcp\"\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"679f:d470:c677:dea4:ba81:d73f:c6ab:255a/127\", \n \"except\": [\n \"679f:d470:c677:dea4:ba81:d73f:c6ab:255a/127\", \n \"679f:d470:c677:dea4:ba81:d73f:198.171.37.90/127\", \n \"679f:d470:c677:dea4:ba81:d73f:c6ab:255a/128\"\n ]\n }, \n {\n \"cidr\": \"2001:db8::/00121\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"icmp\"\n }, \n {\n \"protocol\": \"udp\"\n }\n ]\n }, \n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0200/121\", \n \"protocol\": \"tcp\"\n }, \n {\n \"cidr\": \"128.0.0.0/1\", \n \"except\": [\n \"128.0.0.0/0002\"\n ], \n \"protocol\": \"tcp\", \n \"port\": 80, \n \"endPort\": 83\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:DB8::200/00119\"\n }, \n {\n \"cidr\": \"10.0.3.110/27\"\n }\n ], \n \"ports\": [\n {\n \"port\": 12, \n \"endPort\": 12\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 65531, \n \"endPort\": 65531\n }\n ]\n }\n ], \n \"deny\": [\n {\n \"ports\": [\n {\n \"protocol\": \"udp\", \n \"port\": 441, \n \"endPort\": 447\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 2\n }\n ]\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 440\n }, \n {\n \"protocol\": \"any\", \n \"port\": 65534, \n \"endPort\": 65535\n }, \n {\n \"protocol\": \"any\", \n \"port\": 8, \n \"endPort\": 14\n }\n ]\n }, \n {\n \"cidr\": \"10.0.0.64/00026\", \n \"except\": [\n \"10.0.0.96/27\"\n ], \n \"protocol\": \"tcp\", \n \"port\": 82\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.37/27\"\n }, \n {\n \"cidr\": \"10.0.1.128/25\"\n }, \n {\n \"cidr\": \"65af:8ce5:3468:bb28::/64\"\n }, \n {\n \"cidr\": \"10.0.0.195\", \n \"except\": [\n \"10.0.0.195\"\n ]\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["0.0.0.0/0:tcp:2","0.0.0.0/0:tcp:8-14","0.0.0.0/0:tcp:440","0.0.0.0/0:tcp:65534-65535","0.0.0.0/0:udp:8-14","0.0.0.0/0:udp:441-447","0.0.0.0/0:udp:65534-65535","10.0.0.64/27:tcp:82","10.0.1.128/25","10.0.3.32/27","::/0:tcp:2","::/0:tcp:8-14","::/0:tcp:440","::/0:tcp:65534-65535","::/0:udp:8-14","::/0:udp:441-447","::/0:udp:65534-65535","65af:8ce5:3468:bb28::/64"]}}, + {"name":"random #59","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0.0.0.0/118\",\"except\":[\"2001:db8::40/122\",\"2001:db8::28a/119\"]},{\"cidr\":\"20a3:2087:9c8b:505:d7d0:fa3c::/96\",\"except\":[\"20a3:2087:9c8b:0505:d7d0:fa3c:8800:0000/0101\"]}],\"ports\":[{}]},{\"to\":[{\"cidr\":\"10.0.2.152/22\",\"except\":[\"10.0.1.0/24\"]},{\"cidr\":\"10.0.1.19/23\",\"except\":[\"10.0.0.108/255.255.255.192\",\"10.0.1.96/27\"]}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"tcp\"},{\"protocol\":\"tcp\",\"port\":65533,\"endPort\":65534},{\"protocol\":\"tcp\"}]},{\"to\":[{\"cidr\":\"1ef:5762:a2c3:7878:d7f:c7ae:c117:2500/120\",\"except\":[]},{\"cidr\":\"2001:db8::/118\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":65530},{\"protocol\":\"any\",\"port\":65534},{\"protocol\":\"udp\",\"port\":440,\"endPort\":444},{\"protocol\":\"udp\",\"port\":443}]},{\"cidr\":\"2001:db8::231/128\",\"protocol\":\"udp\",\"port\":209},{\"cidr\":\"10.0.2.238/32\",\"protocol\":\"tcp\",\"port\":389},{\"cidr\":\"10.0.2.244/30\",\"protocol\":\"any\",\"port\":53},{\"cidr\":\"10.0.1.92/30\",\"protocol\":\"any\",\"port\":163},{\"cidr\":\"10.0.1.218/31\",\"protocol\":\"any\",\"port\":67},{\"cidr\":\"2001:db8::318/128\",\"protocol\":\"udp\",\"port\":235},{\"cidr\":\"2001:db8::171/128\",\"protocol\":\"any\",\"port\":297},{\"cidr\":\"2001:db8::25e/128\",\"protocol\":\"tcp\",\"port\":47,\"endPort\":49},{\"cidr\":\"10.0.3.144/31\",\"protocol\":\"udp\",\"port\":295,\"endPort\":297},{\"cidr\":\"10.0.2.172/31\",\"protocol\":\"any\",\"port\":119},{\"cidr\":\"2001:db8::1c4/128\",\"protocol\":\"any\",\"port\":109,\"endPort\":112},{\"cidr\":\"2001:db8::22d/128\",\"protocol\":\"tcp\",\"port\":141},{\"cidr\":\"10.0.0.184/30\",\"protocol\":\"udp\",\"port\":265,\"endPort\":266},{\"cidr\":\"10.0.2.24/30\",\"protocol\":\"tcp\",\"port\":89,\"endPort\":90},{\"cidr\":\"10.0.1.162/31\",\"protocol\":\"any\",\"port\":33,\"endPort\":35},{\"cidr\":\"10.0.3.53/32\",\"protocol\":\"udp\",\"port\":341},{\"cidr\":\"2001:db8::ec/128\",\"protocol\":\"udp\",\"port\":241},{\"cidr\":\"10.0.2.96/30\",\"protocol\":\"any\",\"port\":37,\"endPort\":37},{\"cidr\":\"2001:db8::26e/128\",\"protocol\":\"any\",\"port\":397},{\"cidr\":\"2001:db8::1c8/128\",\"protocol\":\"tcp\",\"port\":69},{\"cidr\":\"2001:db8::16e/128\",\"protocol\":\"any\",\"port\":59,\"endPort\":59},{\"cidr\":\"10.0.0.20/31\",\"protocol\":\"any\",\"port\":219},{\"cidr\":\"2001:db8::2bb/128\",\"protocol\":\"udp\",\"port\":9},{\"cidr\":\"10.0.0.146/31\",\"protocol\":\"tcp\",\"port\":125},{\"cidr\":\"2001:db8::96/128\",\"protocol\":\"any\",\"port\":203},{\"cidr\":\"10.0.3.160/30\",\"protocol\":\"tcp\",\"port\":283},{\"cidr\":\"10.0.1.208/30\",\"protocol\":\"udp\",\"port\":113},{\"cidr\":\"10.0.3.80/31\",\"protocol\":\"udp\",\"port\":21},{\"cidr\":\"10.0.0.145/32\",\"protocol\":\"tcp\",\"port\":339},{\"cidr\":\"10.0.1.47/32\",\"protocol\":\"tcp\",\"port\":9,\"endPort\":13},{\"cidr\":\"10.0.1.80/31\",\"protocol\":\"udp\",\"port\":143,\"endPort\":147},{\"cidr\":\"2001:db8::350/128\",\"protocol\":\"tcp\",\"port\":271},{\"cidr\":\"10.0.1.140/31\",\"protocol\":\"any\",\"port\":257,\"endPort\":257},{\"cidr\":\"10.0.3.58/32\",\"protocol\":\"udp\",\"port\":233},{\"cidr\":\"10.0.3.10/31\",\"protocol\":\"udp\",\"port\":333},{\"cidr\":\"10.0.3.190/31\",\"protocol\":\"tcp\",\"port\":277},{\"cidr\":\"10.0.3.188/31\",\"protocol\":\"any\",\"port\":79},{\"cidr\":\"10.0.0.78/32\",\"protocol\":\"tcp\",\"port\":147},{\"cidr\":\"10.0.2.216/30\",\"protocol\":\"tcp\",\"port\":91},{\"cidr\":\"2001:db8::1b0/128\",\"protocol\":\"any\",\"port\":155,\"endPort\":157},{\"cidr\":\"10.0.1.92/30\",\"protocol\":\"tcp\",\"port\":211},{\"cidr\":\"10.0.3.198/32\",\"protocol\":\"any\",\"port\":57,\"endPort\":58},{\"cidr\":\"2001:db8::d5/128\",\"protocol\":\"udp\",\"port\":301},{\"cidr\":\"10.0.2.84/30\",\"protocol\":\"tcp\",\"port\":395},{\"cidr\":\"10.0.2.162/31\",\"protocol\":\"tcp\",\"port\":201},{\"cidr\":\"10.0.0.252/31\",\"protocol\":\"any\",\"port\":201},{\"cidr\":\"2001:db8::68/128\",\"protocol\":\"tcp\",\"port\":301,\"endPort\":305},{\"cidr\":\"10.0.3.150/31\",\"protocol\":\"tcp\",\"port\":81},{\"cidr\":\"10.0.0.162/32\",\"protocol\":\"udp\",\"port\":135,\"endPort\":135},{\"cidr\":\"10.0.0.44/30\",\"protocol\":\"tcp\",\"port\":133},{\"cidr\":\"2001:db8::3f2/128\",\"protocol\":\"udp\",\"port\":339},{\"cidr\":\"10.0.0.254/32\",\"protocol\":\"any\",\"port\":121},{\"cidr\":\"10.0.3.16/30\",\"protocol\":\"tcp\",\"port\":139},{\"cidr\":\"10.0.0.181/32\",\"protocol\":\"udp\",\"port\":241,\"endPort\":241},{\"cidr\":\"2001:db8::138/128\",\"protocol\":\"any\",\"port\":155},{\"cidr\":\"10.0.1.28/30\",\"protocol\":\"tcp\",\"port\":351,\"endPort\":355},{\"cidr\":\"10.0.3.42/32\",\"protocol\":\"tcp\",\"port\":389},{\"cidr\":\"2001:db8::284/128\",\"protocol\":\"any\",\"port\":395},{\"cidr\":\"2001:db8::31e/128\",\"protocol\":\"udp\",\"port\":201},{\"cidr\":\"10.0.0.249/32\",\"protocol\":\"udp\",\"port\":35},{\"cidr\":\"2001:db8::271/128\",\"protocol\":\"any\",\"port\":197},{\"cidr\":\"2001:db8::202/128\",\"protocol\":\"tcp\",\"port\":225,\"endPort\":229},{\"cidr\":\"10.0.3.209/32\",\"protocol\":\"any\",\"port\":133,\"endPort\":137},{\"cidr\":\"2001:db8::20b/128\",\"protocol\":\"udp\",\"port\":105},{\"cidr\":\"10.0.2.216/31\",\"protocol\":\"udp\",\"port\":109},{\"cidr\":\"10.0.1.96/30\",\"protocol\":\"tcp\",\"port\":79},{\"cidr\":\"2001:db8::1d1/128\",\"protocol\":\"udp\",\"port\":377},{\"cidr\":\"10.0.2.115/32\",\"protocol\":\"udp\",\"port\":107},{\"cidr\":\"10.0.0.128/30\",\"protocol\":\"any\",\"port\":117},{\"cidr\":\"2001:db8::7/128\",\"protocol\":\"udp\",\"port\":265},{\"cidr\":\"2001:db8::21c/128\",\"protocol\":\"any\",\"port\":239},{\"cidr\":\"2001:db8::377/128\",\"protocol\":\"udp\",\"port\":25},{\"cidr\":\"10.0.1.112/30\",\"protocol\":\"tcp\",\"port\":77,\"endPort\":80},{\"cidr\":\"10.0.0.200/30\",\"protocol\":\"udp\",\"port\":213},{\"cidr\":\"2001:db8::287/128\",\"protocol\":\"tcp\",\"port\":63},{\"cidr\":\"10.0.1.18/31\",\"protocol\":\"any\",\"port\":67},{\"cidr\":\"10.0.1.13/32\",\"protocol\":\"udp\",\"port\":35},{\"cidr\":\"2001:db8::16a/128\",\"protocol\":\"any\",\"port\":357},{\"cidr\":\"2001:db8::15/128\",\"protocol\":\"tcp\",\"port\":265},{\"cidr\":\"10.0.2.18/32\",\"protocol\":\"tcp\",\"port\":321},{\"cidr\":\"10.0.0.94/32\",\"protocol\":\"tcp\",\"port\":109},{\"cidr\":\"10.0.0.26/31\",\"protocol\":\"any\",\"port\":15,\"endPort\":16},{\"cidr\":\"2001:db8::1a1/128\",\"protocol\":\"tcp\",\"port\":311},{\"cidr\":\"2001:db8::1c3/128\",\"protocol\":\"any\",\"port\":5},{\"cidr\":\"10.0.3.148/30\",\"protocol\":\"udp\",\"port\":173},{\"cidr\":\"10.0.3.60/32\",\"protocol\":\"any\",\"port\":91},{\"cidr\":\"10.0.3.244/31\",\"protocol\":\"udp\",\"port\":289},{\"cidr\":\"10.0.0.128/32\",\"protocol\":\"any\",\"port\":111},{\"cidr\":\"10.0.1.36/31\",\"protocol\":\"any\",\"port\":221,\"endPort\":225},{\"cidr\":\"2001:db8::12a/128\",\"protocol\":\"udp\",\"port\":147,\"endPort\":148},{\"cidr\":\"10.0.3.58/31\",\"protocol\":\"udp\",\"port\":29},{\"cidr\":\"2001:db8::1b3/128\",\"protocol\":\"any\",\"port\":351},{\"cidr\":\"2001:db8::3a8/128\",\"protocol\":\"tcp\",\"port\":51},{\"cidr\":\"10.0.1.150/32\",\"protocol\":\"tcp\",\"port\":297},{\"cidr\":\"10.0.0.98/31\",\"protocol\":\"udp\",\"port\":277},{\"cidr\":\"10.0.1.7/32\",\"protocol\":\"tcp\",\"port\":283},{\"cidr\":\"2001:db8::42/128\",\"protocol\":\"any\",\"port\":165,\"endPort\":167},{\"cidr\":\"10.0.2.68/30\",\"protocol\":\"any\",\"port\":77,\"endPort\":77},{\"cidr\":\"10.0.0.120/30\",\"protocol\":\"udp\",\"port\":33},{\"cidr\":\"10.0.1.56/30\",\"protocol\":\"udp\",\"port\":301},{\"cidr\":\"10.0.2.82/31\",\"protocol\":\"udp\",\"port\":393,\"endPort\":393},{\"cidr\":\"10.0.3.64/31\",\"protocol\":\"any\",\"port\":281,\"endPort\":281},{\"cidr\":\"2001:db8::28/128\",\"protocol\":\"udp\",\"port\":157},{\"cidr\":\"10.0.3.224/31\",\"protocol\":\"tcp\",\"port\":91},{\"cidr\":\"10.0.2.148/31\",\"protocol\":\"any\",\"port\":99},{\"cidr\":\"2001:db8::3db/128\",\"protocol\":\"udp\",\"port\":191},{\"cidr\":\"2001:db8::3a4/128\",\"protocol\":\"tcp\",\"port\":277,\"endPort\":279},{\"cidr\":\"10.0.1.192/32\",\"protocol\":\"any\",\"port\":333,\"endPort\":337},{\"cidr\":\"2001:db8::254/128\",\"protocol\":\"any\",\"port\":35},{\"cidr\":\"2001:db8::d0/128\",\"protocol\":\"tcp\",\"port\":391,\"endPort\":394},{\"cidr\":\"10.0.3.184/30\",\"protocol\":\"tcp\",\"port\":259},{\"cidr\":\"10.0.3.22/31\",\"protocol\":\"tcp\",\"port\":229},{\"cidr\":\"10.0.0.160/30\",\"protocol\":\"any\",\"port\":163},{\"cidr\":\"10.0.2.124/31\",\"protocol\":\"udp\",\"port\":277},{\"cidr\":\"10.0.2.140/30\",\"protocol\":\"tcp\",\"port\":121},{\"cidr\":\"10.0.3.37/32\",\"protocol\":\"udp\",\"port\":137,\"endPort\":141},{\"cidr\":\"10.0.2.247/32\",\"protocol\":\"tcp\",\"port\":269},{\"cidr\":\"10.0.1.220/31\",\"protocol\":\"udp\",\"port\":361},{\"cidr\":\"10.0.2.140/30\",\"protocol\":\"udp\",\"port\":395},{\"cidr\":\"10.0.0.36/32\",\"protocol\":\"tcp\",\"port\":59,\"endPort\":60},{\"cidr\":\"10.0.2.116/30\",\"protocol\":\"udp\",\"port\":105},{\"cidr\":\"10.0.3.48/31\",\"protocol\":\"any\",\"port\":121},{\"cidr\":\"10.0.0.60/30\",\"protocol\":\"tcp\",\"port\":309,\"endPort\":313},{\"cidr\":\"10.0.3.72/30\",\"protocol\":\"udp\",\"port\":45,\"endPort\":45},{\"cidr\":\"10.0.2.178/31\",\"protocol\":\"udp\",\"port\":337,\"endPort\":339},{\"cidr\":\"10.0.0.0/30\",\"protocol\":\"tcp\",\"port\":369},{\"cidr\":\"2001:db8::33b/128\",\"protocol\":\"any\",\"port\":323,\"endPort\":324},{\"cidr\":\"10.0.1.248/30\",\"protocol\":\"udp\",\"port\":287},{\"cidr\":\"10.0.1.56/30\",\"protocol\":\"tcp\",\"port\":295},{\"cidr\":\"2001:db8::329/128\",\"protocol\":\"udp\",\"port\":379},{\"cidr\":\"2001:db8::13b/128\",\"protocol\":\"tcp\",\"port\":375},{\"cidr\":\"2001:db8::26a/128\",\"protocol\":\"tcp\",\"port\":297},{\"cidr\":\"10.0.2.100/31\",\"protocol\":\"tcp\",\"port\":21},{\"cidr\":\"2001:db8::b5/128\",\"protocol\":\"tcp\",\"port\":31,\"endPort\":35},{\"cidr\":\"2001:db8::221/128\",\"protocol\":\"tcp\",\"port\":105},{\"cidr\":\"10.0.3.96/30\",\"protocol\":\"udp\",\"port\":187,\"endPort\":187},{\"cidr\":\"10.0.1.152/30\",\"protocol\":\"udp\",\"port\":81},{\"cidr\":\"10.0.2.100/30\",\"protocol\":\"udp\",\"port\":95},{\"cidr\":\"10.0.1.220/30\",\"protocol\":\"tcp\",\"port\":311,\"endPort\":312},{\"cidr\":\"10.0.1.196/30\",\"protocol\":\"udp\",\"port\":325},{\"cidr\":\"10.0.0.53/32\",\"protocol\":\"udp\",\"port\":25,\"endPort\":29}]}"}],"expected":{"allow":["10.0.0.0/24:tcp","10.0.0.20/31:udp:219","10.0.0.26/31:udp:15-16","10.0.0.53/32:udp:25-29","10.0.0.98/31:udp:277","10.0.0.120/30:udp:33","10.0.0.128/30:udp:117","10.0.0.128/32:udp:111","10.0.0.160/30:udp:163","10.0.0.162/32:udp:135","10.0.0.181/32:udp:241","10.0.0.184/30:udp:265-266","10.0.0.200/30:udp:213","10.0.0.249/32:udp:35","10.0.0.252/31:udp:201","10.0.0.254/32:udp:121","10.0.1.0/26:tcp","10.0.1.13/32:udp:35","10.0.1.18/31:udp:67","10.0.1.36/31:udp:221-225","10.0.1.56/30:udp:301","10.0.1.64/27:tcp","10.0.1.80/31:udp:143-147","10.0.1.92/30:udp:163","10.0.1.96/30:tcp:79","10.0.1.112/30:tcp:77-80","10.0.1.128/25:tcp","10.0.1.140/31:udp:257","10.0.1.152/30:udp:81","10.0.1.162/31:udp:33-35","10.0.1.192/32:udp:333-337","10.0.1.196/30:udp:325","10.0.1.208/30:udp:113","10.0.1.218/31:udp:67","10.0.1.220/31:udp:361","10.0.1.248/30:udp:287","10.0.2.0/23:tcp","10.0.2.68/30:udp:77","10.0.2.82/31:udp:393","10.0.2.96/30:udp:37","10.0.2.100/30:udp:95","10.0.2.115/32:udp:107","10.0.2.116/30:udp:105","10.0.2.124/31:udp:277","10.0.2.140/30:udp:395","10.0.2.148/31:udp:99","10.0.2.172/31:udp:119","10.0.2.178/31:udp:337-339","10.0.2.216/31:udp:109","10.0.2.244/30:udp:53","10.0.3.10/31:udp:333","10.0.3.37/32:udp:137-141","10.0.3.48/31:udp:121","10.0.3.53/32:udp:341","10.0.3.58/31:udp:29","10.0.3.58/32:udp:233","10.0.3.60/32:udp:91","10.0.3.64/31:udp:281","10.0.3.72/30:udp:45","10.0.3.80/31:udp:21","10.0.3.96/30:udp:187","10.0.3.144/31:udp:295-297","10.0.3.148/30:udp:173","10.0.3.188/31:udp:79","10.0.3.198/32:udp:57-58","10.0.3.209/32:udp:133-137","10.0.3.244/31:udp:289","1ef:5762:a2c3:7878:d7f:c7ae:c117:2500/120:tcp:65530","1ef:5762:a2c3:7878:d7f:c7ae:c117:2500/120:tcp:65534","1ef:5762:a2c3:7878:d7f:c7ae:c117:2500/120:udp:440-444","1ef:5762:a2c3:7878:d7f:c7ae:c117:2500/120:udp:65534","2001:db8::/118:tcp:65530","2001:db8::/118:tcp:65534","2001:db8::/118:udp:440-444","2001:db8::/118:udp:65534","2001:db8::/122","2001:db8::42/128:tcp:165-167","2001:db8::42/128:udp:165-167","2001:db8::68/128:tcp:301-305","2001:db8::80/121","2001:db8::100/120","2001:db8::202/128:tcp:225-229","2001:db8::20b/128:udp:105","2001:db8::21c/128:tcp:239","2001:db8::21c/128:udp:239","2001:db8::221/128:tcp:105","2001:db8::22d/128:tcp:141","2001:db8::231/128:udp:209","2001:db8::254/128:tcp:35","2001:db8::254/128:udp:35","2001:db8::25e/128:tcp:47-49","2001:db8::26a/128:tcp:297","2001:db8::26e/128:tcp:397","2001:db8::26e/128:udp:397","2001:db8::271/128:tcp:197","2001:db8::271/128:udp:197","2001:db8::284/128:tcp:395","2001:db8::284/128:udp:395","2001:db8::287/128:tcp:63","2001:db8::2bb/128:udp:9","2001:db8::318/128:udp:235","2001:db8::31e/128:udp:201","2001:db8::329/128:udp:379","2001:db8::33b/128:tcp:323-324","2001:db8::33b/128:udp:323-324","2001:db8::350/128:tcp:271","2001:db8::377/128:udp:25","2001:db8::3a4/128:tcp:277-279","2001:db8::3a8/128:tcp:51","2001:db8::3db/128:udp:191","2001:db8::3f2/128:udp:339","20a3:2087:9c8b:505:d7d0:fa3c::/97","20a3:2087:9c8b:505:d7d0:fa3c:8000:0/101","20a3:2087:9c8b:505:d7d0:fa3c:9000:0/100","20a3:2087:9c8b:505:d7d0:fa3c:a000:0/99","20a3:2087:9c8b:505:d7d0:fa3c:c000:0/98"],"deny":[]}}, + {"name":"random #60","input":[{"text":"{}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #61","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.96/00029\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:DB8::250/125\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.172/255.255.255.252\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.217/31\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"114.113.0.0/16\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0368/125\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65532\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 79, \n\t\t\t\t\t\"endPort\": 80\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 3\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":["10.0.0.96/29","10.0.1.172/30","10.0.3.216/31","114.113.0.0/16","2001:db8::250/125","2001:db8::368/125"],"deny":[]}}, + {"name":"random #62","input":[{"text":"{\"allow\": []}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #63","input":[{"text":"{\"allow\":[{\"protocol\":\"udp\",\"port\":78,\"endPort\":81},{\"cidr\":\"10.0.0.0/23\",\"protocol\":\"tcp\"},{},{\"to\":[{\"cidr\":\"10.0.2.32/0027\",\"except\":[\"10.0.2.32/27\"]},{\"cidr\":\"2001:db8::200/119\",\"except\":[\"2001:db8::32f/127\",\"2001:0db8:0000:0000:0000:0000:0000:0390/000124\"]},{\"cidr\":\"10.0.0.0/22\",\"except\":[\"10.0.0.128/25\"]},{\"cidr\":\"10.0.3.124/31\",\"except\":[\"10.0.3.125\"]}],\"ports\":[{\"protocol\":\"tcp\",\"port\":80,\"endPort\":85}]}],\"deny\":[]}"}],"expected":{"error":"allow[0].cidr is required"}}, + {"name":"random #64","input":[{"text":"{\n\"allow\":[\n{\n\"cidr\":\"10.0.2.245/0030\",\n\"protocol\":\"udp\",\n\"port\":78,\n\"endPort\":81\n}\n],\n\"deny\":[]\n}"}],"expected":{"allow":["10.0.2.244/30:udp:78-81"],"deny":[]}}, + {"name":"random #65","input":[{"text":"{\"allow\": [], \"allow\": [{\"to\": [{\"cidr\": \"10.0.3.64/27\"}, {\"cidr\": \"2001:db8::/118\"}, {\"cidr\": \"10.0.3.23/32\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 79, \"endPort\": 85}, {\"protocol\": \"tcp\", \"port\": 79}, {\"protocol\": \"any\"}]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:030a/127\", \"except\": [\"2001:db8::30b/128\", \"2001:DB8::30B/128\"]}, {\"cidr\": \"10.0.0.128/25\", \"protocol\": \"tcp\", \"port\": 82, \"endPort\": 85}, {\"cidr\": \"d7e:aee2:7423:73d0::/64\", \"protocol\": \"tcp\", \"port\": 443, \"endPort\": 446}, {\"to\": [{\"cidr\": \"10.0.2.64/26\"}, {\"cidr\": \"2001:db8::200/120\", \"except\": [\"2001:db8::240/122\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65532, \"endPort\": 65535}]}, {\"ports\": [{\"protocol\": \"tcp\", \"port\": 12, \"endPort\": 16}, {\"protocol\": \"tcp\", \"port\": 78}, {\"protocol\": \"tcp\", \"port\": 8}]}, {\"to\": [{\"cidr\": \"10.0.3.223/30\", \"except\": []}, {\"cidr\": \"68.82.198.45/4\", \"except\": [\"71.10.165.254/11\", \"64.0.0.0/4\", \"73.144.0.0/12\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 441}]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:02e0/126\", \"protocol\": \"udp\", \"port\": 80}, {\"cidr\": \"2001:db8::c0/123\", \"protocol\": \"icmp\"}, {\"cidr\": \"10.0.0.199/25\", \"except\": [], \"protocol\": \"tcp\", \"port\": 443}, {\"to\": [{\"cidr\": \"10.0.3.240/255.255.255.255\", \"except\": [\"10.0.3.240/32\", \"10.0.3.240\", \"10.0.3.240/32\"]}, {\"cidr\": \"10.0.0.136/30\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"any\", \"port\": 8, \"endPort\": 53807}, {\"protocol\": \"any\"}, {\"protocol\": \"icmp\"}]}, {\"cidr\": \"2001:db8::3a0/124\", \"protocol\": \"udp\", \"port\": 440}, {\"to\": [{\"cidr\": \"238.97.210.57/8\", \"except\": [\"238.32.0.0/0011\", \"238.0.0.0/0.255.255.255\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 80}, {\"protocol\": \"tcp\", \"port\": 65533, \"endPort\": 65534}]}, {\"to\": [{\"cidr\": \"10.0.1.0/24\"}, {\"cidr\": \"10.0.1.55/255.255.255.254\"}, {\"cidr\": \"2001:db8::324/126\", \"except\": [\"2001:db8::327/128\", \"2001:db8::327/128\", \"2001:db8::326/127\"]}, {\"cidr\": \"2001:db8::2a0/127\", \"except\": [\"2001:db8::2a0\", \"2001:db8::2a1\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 443}, {\"protocol\": \"tcp\"}, {\"protocol\": \"udp\"}]}, {\"to\": [{\"cidr\": \"10.0.2.24/022\"}, {\"cidr\": \"0.0.0.0/0\"}, {\"cidr\": \"2001:db8::280/124\", \"except\": [\"2001:db8::289\"]}], \"ports\": [{\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"10.0.2.242/032\", \"except\": [\"10.0.2.242/32\", \"10.0.2.242\", \"10.0.2.242\"]}, {\"cidr\": \"2001:db8::/00118\"}, {\"cidr\": \"10.0.1.23/22\", \"except\": []}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 81, \"endPort\": 12861}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"64.0.0.0/2\"}, {\"cidr\": \"10.0.0.0/23\"}, {\"cidr\": \"10.0.2.143/255.255.255.248\"}]}, {\"to\": [{\"cidr\": \"10.0.2.0/25\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65534}, {\"protocol\": \"udp\"}]}, {\"to\": [{\"cidr\": \"2001:DB8::300/120\", \"except\": [\"2001:db8::300/120\", \"2001:db8::373/126\", \"2001:db8::3cb/123\"]}], \"ports\": [{\"protocol\": \"any\", \"port\": 65532, \"endPort\": 65534}]}, {\"to\": [{\"cidr\": \"10.0.1.72/30\", \"except\": [\"10.0.1.72/31\"]}]}, {\"cidr\": \"2.13.177.0/24\"}, {\"to\": [{\"cidr\": \"10.0.2.0/00024\"}, {\"cidr\": \"10.0.0.0/22\"}, {\"cidr\": \"10.0.1.128/26\", \"except\": [\"10.0.1.168/30\"]}, {\"cidr\": \"2001:db8::/118\"}]}, {\"cidr\": \"10.0.0.158/0.0.0.63\", \"protocol\": \"any\"}, {\"to\": [{\"cidr\": \"10.0.1.220/32\"}, {\"cidr\": \"10.0.3.213/28\"}, {\"cidr\": \"::/1\"}, {\"cidr\": \"7041:8e5c:d691:dc99:404a:5afe:332f:4200/120\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 445, \"endPort\": 448}, {\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"udp\"}, {\"protocol\": \"tcp\", \"port\": 81}]}, {\"to\": [{\"cidr\": \"10.0.0.150/27\", \"except\": []}, {\"cidr\": \"10.0.2.32/28\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65533}]}, {\"to\": [{\"cidr\": \"10.0.2.240/29\", \"except\": []}, {\"cidr\": \"2001:db8::/119\", \"except\": []}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 81, \"endPort\": 84}, {\"protocol\": \"any\", \"port\": 80}, {\"port\": 82}]}, {\"to\": [{\"cidr\": \"b0c8:6cf5:62f5:3d45:6c6a:2a08:ee00:0/104\", \"except\": [\"b0c8:6cf5:62f5:3d45:6c6a:2a08:ee30:0/108\", \"b0c8:6cf5:62f5:3d45:6c6a:2a08:ee68:e6a4/112\"]}]}, {\"cidr\": \"10.0.2.0/255.255.255.0\", \"protocol\": \"udp\", \"port\": 65531, \"endPort\": 65535}, {\"to\": [{\"cidr\": \"10.0.1.0/24\"}, {\"cidr\": \"0.0.0.0/0\"}, {\"cidr\": \"2001:db8::74/126\"}, {\"cidr\": \"10.0.0.0/22\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 442}, {\"port\": 5}]}, {\"to\": [{\"cidr\": \"c000::/3\", \"except\": []}, {\"cidr\": \"10.0.0.32/27\"}, {\"cidr\": \"10.0.2.0/25\", \"except\": [\"10.0.2.64/0.0.0.63\"]}, {\"cidr\": \"10.0.0.0/0.0.1.255\", \"except\": []}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 441, \"endPort\": 443}, {\"protocol\": \"any\", \"port\": 444}, {\"protocol\": \"any\", \"port\": 65532, \"endPort\": 65535}, {}]}, {\"to\": [{\"cidr\": \"243.95.181.206/1\"}, {\"cidr\": \"5ba5:168c:f289:a649:9763:2922:895a:216a/127\"}, {\"cidr\": \"5.0.0.0/255.240.0.0\"}]}, {\"to\": [{\"cidr\": \"10.0.3.219/30\"}, {\"cidr\": \"10.0.3.162/23\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"udp\", \"port\": 443, \"endPort\": 449}, {}]}, {\"to\": [{\"cidr\": \"10.0.3.66/28\"}, {\"cidr\": \"2001:db8::/118\", \"except\": []}, {\"cidr\": \"10.0.0.0/23\"}]}, {\"cidr\": \"10.0.0.0/255.255.252.0\", \"protocol\": \"tcp\", \"port\": 78}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0200/119\"}, {\"cidr\": \"10.0.2.128/25\"}, {\"cidr\": \"2001:db8::228/125\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 9, \"endPort\": 15}]}, {\"cidr\": \"5e3a:5bc8:d088:950a:3707:d119:e3ad:4de7/1\", \"protocol\": \"any\", \"port\": 79, \"endPort\": 79}, {\"cidr\": \"2001:db8::340/122\", \"protocol\": \"icmp\"}, {\"cidr\": \"10.0.2.128/24\", \"protocol\": \"any\", \"port\": 441}, {\"to\": [{\"cidr\": \"17.182.59.0/31\"}, {\"cidr\": \"b127:2eeb:60b7:b773:3951:2ca9:5300:0/104\", \"except\": [\"b127:2eeb:60b7:b773:3951:2ca9:5360:0000/108\", \"b127:2eeb:60b7:b773:3951:2ca9:53f8:0/109\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0180/121\"}, {\"cidr\": \"10.0.2.98/31\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"udp\", \"port\": 7}, {\"protocol\": \"any\", \"port\": 65535, \"endPort\": 65535}]}], \"deny\": [{\"cidr\": \"2001:DB8::340/123\", \"except\": [], \"protocol\": \"tcp\", \"port\": 78}, {\"cidr\": \"10.0.1.32/27\", \"protocol\": \"tcp\", \"port\": 65533}, {\"to\": [{\"cidr\": \"10.0.1.98\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 80, \"endPort\": 81}]}, {\"cidr\": \"225.166.241.163\", \"except\": [\"225.166.241.163\"], \"protocol\": \"tcp\"}]}"}],"expected":{"error":"duplicate JSON property: allow"}}, + {"name":"random #66","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"10.0.2.44/25\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":9},{\"protocol\":\"icmp\"},{\"protocol\":\"tcp\",\"port\":80,\"endPort\":81}]},{\"to\":[{\"cidr\":\"3.188.163.65/0\"}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"any\",\"port\":81,\"endPort\":82},{\"protocol\":\"any\",\"port\":81,\"endPort\":81},{\"protocol\":\"any\",\"port\":5,\"endPort\":7}]},{\"to\":[{\"cidr\":\"4004:a5da:24c2:8a6e:b313:af45:3153:7cae/3\",\"except\":[\"5000::/5\"]}],\"ports\":[{\"protocol\":\"udp\",\"port\":65533,\"endPort\":65535}]},{\"to\":[{\"cidr\":\"0.0.0.0/128.0.0.0\"},{\"cidr\":\"110.85.110.95/16\"},{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0.0.2.0/119\"},{\"cidr\":\"10.0.2.0/025\",\"except\":[\"10.0.2.32/27\",\"10.0.2.80/28\",\"10.0.2.100/31\"]}],\"ports\":[{\"protocol\":\"tcp\",\"port\":65530},{\"protocol\":\"any\",\"port\":2,\"endPort\":3975}]},{\"ports\":[{\"protocol\":\"any\",\"port\":82},{\"protocol\":\"any\",\"port\":7,\"endPort\":11},{\"protocol\":\"icmp\"},{\"protocol\":\"tcp\",\"port\":65532,\"endPort\":65534}]},{},{\"to\":[{\"cidr\":\"2001:db8::200/00119\"}]},{\"to\":[{\"cidr\":\"10.0.3.0/24\"},{\"cidr\":\"10.0.2.0/23\"}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"udp\",\"port\":3},{\"protocol\":\"udp\",\"port\":6}]},{\"cidr\":\"10.0.0.32/32\",\"protocol\":\"tcp\",\"port\":213},{\"cidr\":\"10.0.1.196/31\",\"protocol\":\"tcp\",\"port\":231},{\"cidr\":\"10.0.1.99/32\",\"protocol\":\"tcp\",\"port\":5},{\"cidr\":\"10.0.1.180/31\",\"protocol\":\"tcp\",\"port\":195},{\"cidr\":\"10.0.0.60/30\",\"protocol\":\"any\",\"port\":25},{\"cidr\":\"2001:db8::2be/128\",\"protocol\":\"tcp\",\"port\":97,\"endPort\":101},{\"cidr\":\"10.0.3.17/32\",\"protocol\":\"any\",\"port\":57},{\"cidr\":\"2001:db8::1c1/128\",\"protocol\":\"tcp\",\"port\":215},{\"cidr\":\"2001:db8::299/128\",\"protocol\":\"any\",\"port\":157},{\"cidr\":\"10.0.2.108/30\",\"protocol\":\"udp\",\"port\":15},{\"cidr\":\"10.0.3.148/30\",\"protocol\":\"udp\",\"port\":205},{\"cidr\":\"10.0.3.7/32\",\"protocol\":\"any\",\"port\":317,\"endPort\":319},{\"cidr\":\"2001:db8::1f5/128\",\"protocol\":\"udp\",\"port\":153},{\"cidr\":\"10.0.0.182/32\",\"protocol\":\"udp\",\"port\":371},{\"cidr\":\"2001:db8::157/128\",\"protocol\":\"udp\",\"port\":335,\"endPort\":339},{\"cidr\":\"10.0.1.180/31\",\"protocol\":\"tcp\",\"port\":193},{\"cidr\":\"10.0.3.220/30\",\"protocol\":\"tcp\",\"port\":335},{\"cidr\":\"10.0.3.127/32\",\"protocol\":\"tcp\",\"port\":321},{\"cidr\":\"10.0.3.128/31\",\"protocol\":\"tcp\",\"port\":59,\"endPort\":61},{\"cidr\":\"2001:db8::32c/128\",\"protocol\":\"udp\",\"port\":265},{\"cidr\":\"10.0.1.108/31\",\"protocol\":\"any\",\"port\":199},{\"cidr\":\"10.0.0.235/32\",\"protocol\":\"any\",\"port\":363,\"endPort\":364},{\"cidr\":\"2001:db8::1a0/128\",\"protocol\":\"tcp\",\"port\":275},{\"cidr\":\"10.0.3.100/30\",\"protocol\":\"udp\",\"port\":275},{\"cidr\":\"2001:db8::be/128\",\"protocol\":\"udp\",\"port\":205},{\"cidr\":\"10.0.1.24/30\",\"protocol\":\"any\",\"port\":345,\"endPort\":349},{\"cidr\":\"10.0.1.158/32\",\"protocol\":\"udp\",\"port\":243,\"endPort\":247},{\"cidr\":\"10.0.1.213/32\",\"protocol\":\"udp\",\"port\":289},{\"cidr\":\"2001:db8::2b7/128\",\"protocol\":\"any\",\"port\":11,\"endPort\":15},{\"cidr\":\"2001:db8::18/128\",\"protocol\":\"udp\",\"port\":301,\"endPort\":304},{\"cidr\":\"10.0.2.148/30\",\"protocol\":\"udp\",\"port\":251},{\"cidr\":\"10.0.0.152/30\",\"protocol\":\"tcp\",\"port\":113},{\"cidr\":\"2001:db8::b8/128\",\"protocol\":\"tcp\",\"port\":59},{\"cidr\":\"2001:db8::293/128\",\"protocol\":\"any\",\"port\":215},{\"cidr\":\"10.0.0.196/30\",\"protocol\":\"udp\",\"port\":273},{\"cidr\":\"10.0.3.210/31\",\"protocol\":\"udp\",\"port\":235},{\"cidr\":\"2001:db8::2e3/128\",\"protocol\":\"tcp\",\"port\":327},{\"cidr\":\"10.0.2.192/31\",\"protocol\":\"any\",\"port\":209},{\"cidr\":\"10.0.2.160/30\",\"protocol\":\"tcp\",\"port\":315},{\"cidr\":\"10.0.3.46/32\",\"protocol\":\"tcp\",\"port\":367},{\"cidr\":\"10.0.0.40/30\",\"protocol\":\"any\",\"port\":121},{\"cidr\":\"10.0.3.73/32\",\"protocol\":\"udp\",\"port\":365},{\"cidr\":\"2001:db8::172/128\",\"protocol\":\"any\",\"port\":307},{\"cidr\":\"2001:db8::379/128\",\"protocol\":\"tcp\",\"port\":301},{\"cidr\":\"2001:db8::be/128\",\"protocol\":\"any\",\"port\":73},{\"cidr\":\"10.0.0.6/31\",\"protocol\":\"udp\",\"port\":29},{\"cidr\":\"10.0.2.136/31\",\"protocol\":\"any\",\"port\":39},{\"cidr\":\"2001:db8::10f/128\",\"protocol\":\"any\",\"port\":327},{\"cidr\":\"10.0.3.144/30\",\"protocol\":\"any\",\"port\":209},{\"cidr\":\"10.0.1.180/30\",\"protocol\":\"tcp\",\"port\":273},{\"cidr\":\"10.0.3.166/31\",\"protocol\":\"any\",\"port\":103},{\"cidr\":\"10.0.0.240/31\",\"protocol\":\"any\",\"port\":193},{\"cidr\":\"2001:db8::3b0/128\",\"protocol\":\"any\",\"port\":183},{\"cidr\":\"10.0.2.128/31\",\"protocol\":\"udp\",\"port\":27,\"endPort\":27},{\"cidr\":\"10.0.2.145/32\",\"protocol\":\"any\",\"port\":221},{\"cidr\":\"2001:db8::2f7/128\",\"protocol\":\"tcp\",\"port\":153},{\"cidr\":\"10.0.0.80/30\",\"protocol\":\"any\",\"port\":291},{\"cidr\":\"2001:db8::11e/128\",\"protocol\":\"tcp\",\"port\":175},{\"cidr\":\"2001:db8::29a/128\",\"protocol\":\"tcp\",\"port\":391,\"endPort\":393},{\"cidr\":\"2001:db8::31a/128\",\"protocol\":\"tcp\",\"port\":233},{\"cidr\":\"10.0.1.144/32\",\"protocol\":\"udp\",\"port\":93},{\"cidr\":\"10.0.0.180/30\",\"protocol\":\"udp\",\"port\":263,\"endPort\":267},{\"cidr\":\"10.0.2.64/30\",\"protocol\":\"udp\",\"port\":319,\"endPort\":320},{\"cidr\":\"10.0.0.116/30\",\"protocol\":\"any\",\"port\":337},{\"cidr\":\"10.0.2.36/30\",\"protocol\":\"any\",\"port\":109},{\"cidr\":\"10.0.2.224/32\",\"protocol\":\"udp\",\"port\":61,\"endPort\":64},{\"cidr\":\"2001:db8::150/128\",\"protocol\":\"udp\",\"port\":393},{\"cidr\":\"10.0.2.56/30\",\"protocol\":\"udp\",\"port\":169},{\"cidr\":\"2001:db8::27a/128\",\"protocol\":\"tcp\",\"port\":389},{\"cidr\":\"10.0.2.84/30\",\"protocol\":\"any\",\"port\":307},{\"cidr\":\"2001:db8::3f8/128\",\"protocol\":\"tcp\",\"port\":345},{\"cidr\":\"2001:db8::e9/128\",\"protocol\":\"any\",\"port\":229},{\"cidr\":\"10.0.2.240/30\",\"protocol\":\"tcp\",\"port\":243},{\"cidr\":\"10.0.2.164/31\",\"protocol\":\"udp\",\"port\":237},{\"cidr\":\"2001:db8::10c/128\",\"protocol\":\"any\",\"port\":89},{\"cidr\":\"2001:db8::bc/128\",\"protocol\":\"tcp\",\"port\":313},{\"cidr\":\"10.0.3.12/30\",\"protocol\":\"any\",\"port\":51,\"endPort\":52},{\"cidr\":\"10.0.0.241/32\",\"protocol\":\"any\",\"port\":177},{\"cidr\":\"10.0.1.168/32\",\"protocol\":\"any\",\"port\":191,\"endPort\":195},{\"cidr\":\"10.0.2.184/30\",\"protocol\":\"any\",\"port\":361,\"endPort\":364},{\"cidr\":\"10.0.0.113/32\",\"protocol\":\"any\",\"port\":257,\"endPort\":258},{\"cidr\":\"10.0.3.32/30\",\"protocol\":\"tcp\",\"port\":287},{\"cidr\":\"2001:db8::2d1/128\",\"protocol\":\"any\",\"port\":231},{\"cidr\":\"10.0.0.226/32\",\"protocol\":\"udp\",\"port\":201},{\"cidr\":\"10.0.1.220/30\",\"protocol\":\"udp\",\"port\":111,\"endPort\":115},{\"cidr\":\"10.0.2.41/32\",\"protocol\":\"any\",\"port\":77},{\"cidr\":\"10.0.1.88/30\",\"protocol\":\"udp\",\"port\":165},{\"cidr\":\"10.0.0.128/30\",\"protocol\":\"any\",\"port\":393},{\"cidr\":\"10.0.2.168/30\",\"protocol\":\"udp\",\"port\":13},{\"cidr\":\"2001:db8::2df/128\",\"protocol\":\"udp\",\"port\":137,\"endPort\":137},{\"cidr\":\"2001:db8::1c6/128\",\"protocol\":\"udp\",\"port\":261},{\"cidr\":\"2001:db8::1c7/128\",\"protocol\":\"tcp\",\"port\":53},{\"cidr\":\"10.0.0.250/31\",\"protocol\":\"any\",\"port\":307,\"endPort\":307},{\"cidr\":\"2001:db8::338/128\",\"protocol\":\"udp\",\"port\":185},{\"cidr\":\"10.0.0.6/32\",\"protocol\":\"tcp\",\"port\":89},{\"cidr\":\"10.0.1.76/30\",\"protocol\":\"any\",\"port\":311},{\"cidr\":\"10.0.1.236/30\",\"protocol\":\"udp\",\"port\":231},{\"cidr\":\"10.0.0.124/30\",\"protocol\":\"tcp\",\"port\":227,\"endPort\":230},{\"cidr\":\"2001:db8::2c9/128\",\"protocol\":\"any\",\"port\":353},{\"cidr\":\"10.0.1.212/30\",\"protocol\":\"any\",\"port\":379,\"endPort\":383},{\"cidr\":\"2001:db8::36c/128\",\"protocol\":\"any\",\"port\":313},{\"cidr\":\"10.0.0.240/31\",\"protocol\":\"tcp\",\"port\":43},{\"cidr\":\"2001:db8::e3/128\",\"protocol\":\"any\",\"port\":53},{\"cidr\":\"2001:db8::34c/128\",\"protocol\":\"any\",\"port\":197,\"endPort\":197},{\"cidr\":\"10.0.3.200/30\",\"protocol\":\"tcp\",\"port\":315},{\"cidr\":\"2001:db8::f9/128\",\"protocol\":\"udp\",\"port\":273},{\"cidr\":\"10.0.1.137/32\",\"protocol\":\"udp\",\"port\":125,\"endPort\":129},{\"cidr\":\"2001:db8::172/128\",\"protocol\":\"any\",\"port\":257},{\"cidr\":\"2001:db8::f2/128\",\"protocol\":\"any\",\"port\":59},{\"cidr\":\"10.0.0.152/31\",\"protocol\":\"any\",\"port\":191},{\"cidr\":\"2001:db8::65/128\",\"protocol\":\"any\",\"port\":105},{\"cidr\":\"10.0.3.210/32\",\"protocol\":\"udp\",\"port\":309},{\"cidr\":\"10.0.2.204/30\",\"protocol\":\"any\",\"port\":225,\"endPort\":227},{\"cidr\":\"2001:db8::308/128\",\"protocol\":\"udp\",\"port\":203},{\"cidr\":\"2001:db8::/128\",\"protocol\":\"any\",\"port\":115},{\"cidr\":\"10.0.3.54/32\",\"protocol\":\"any\",\"port\":37,\"endPort\":40},{\"cidr\":\"10.0.3.147/32\",\"protocol\":\"udp\",\"port\":383},{\"cidr\":\"10.0.3.201/32\",\"protocol\":\"udp\",\"port\":341},{\"cidr\":\"10.0.0.123/32\",\"protocol\":\"any\",\"port\":49},{\"cidr\":\"10.0.3.180/30\",\"protocol\":\"udp\",\"port\":313},{\"cidr\":\"2001:db8::393/128\",\"protocol\":\"any\",\"port\":61},{\"cidr\":\"2001:db8::2ac/128\",\"protocol\":\"udp\",\"port\":1},{\"cidr\":\"10.0.2.62/31\",\"protocol\":\"any\",\"port\":245},{\"cidr\":\"10.0.1.212/30\",\"protocol\":\"tcp\",\"port\":225},{\"cidr\":\"10.0.3.102/32\",\"protocol\":\"udp\",\"port\":193},{\"cidr\":\"10.0.0.132/31\",\"protocol\":\"any\",\"port\":253},{\"cidr\":\"10.0.2.156/30\",\"protocol\":\"any\",\"port\":291,\"endPort\":291},{\"cidr\":\"10.0.2.95/32\",\"protocol\":\"udp\",\"port\":295},{\"cidr\":\"10.0.3.32/32\",\"protocol\":\"tcp\",\"port\":381},{\"cidr\":\"10.0.3.156/30\",\"protocol\":\"tcp\",\"port\":29},{\"cidr\":\"10.0.3.16/30\",\"protocol\":\"udp\",\"port\":67},{\"cidr\":\"10.0.2.120/31\",\"protocol\":\"tcp\",\"port\":387},{\"cidr\":\"10.0.1.24/31\",\"protocol\":\"any\",\"port\":83},{\"cidr\":\"10.0.2.184/31\",\"protocol\":\"any\",\"port\":185},{\"cidr\":\"10.0.3.189/32\",\"protocol\":\"any\",\"port\":51,\"endPort\":53},{\"cidr\":\"10.0.3.84/30\",\"protocol\":\"udp\",\"port\":267},{\"cidr\":\"10.0.1.223/32\",\"protocol\":\"tcp\",\"port\":131},{\"cidr\":\"10.0.1.43/32\",\"protocol\":\"any\",\"port\":251,\"endPort\":253},{\"cidr\":\"2001:db8::218/128\",\"protocol\":\"tcp\",\"port\":193,\"endPort\":195},{\"cidr\":\"10.0.1.100/31\",\"protocol\":\"any\",\"port\":211},{\"cidr\":\"10.0.3.140/30\",\"protocol\":\"any\",\"port\":195},{\"cidr\":\"10.0.0.152/30\",\"protocol\":\"any\",\"port\":203,\"endPort\":207},{\"cidr\":\"10.0.1.72/30\",\"protocol\":\"tcp\",\"port\":101},{\"cidr\":\"10.0.1.4/30\",\"protocol\":\"tcp\",\"port\":179},{\"cidr\":\"10.0.3.88/31\",\"protocol\":\"udp\",\"port\":17,\"endPort\":17},{\"cidr\":\"2001:db8::143/128\",\"protocol\":\"udp\",\"port\":115},{\"cidr\":\"10.0.2.238/31\",\"protocol\":\"any\",\"port\":147,\"endPort\":149},{\"cidr\":\"2001:db8::99/128\",\"protocol\":\"tcp\",\"port\":303},{\"cidr\":\"10.0.1.84/32\",\"protocol\":\"any\",\"port\":227},{\"cidr\":\"10.0.3.44/30\",\"protocol\":\"tcp\",\"port\":325},{\"cidr\":\"2001:db8::2b8/128\",\"protocol\":\"tcp\",\"port\":197},{\"cidr\":\"2001:db8::1d2/128\",\"protocol\":\"udp\",\"port\":29},{\"cidr\":\"10.0.1.192/30\",\"protocol\":\"tcp\",\"port\":261},{\"cidr\":\"10.0.2.140/31\",\"protocol\":\"any\",\"port\":287},{\"cidr\":\"2001:db8::2e8/128\",\"protocol\":\"udp\",\"port\":235,\"endPort\":237},{\"cidr\":\"10.0.1.120/31\",\"protocol\":\"any\",\"port\":119},{\"cidr\":\"10.0.3.59/32\",\"protocol\":\"udp\",\"port\":333},{\"cidr\":\"10.0.1.164/30\",\"protocol\":\"any\",\"port\":25,\"endPort\":26},{\"cidr\":\"10.0.1.60/31\",\"protocol\":\"tcp\",\"port\":353,\"endPort\":353},{\"cidr\":\"2001:db8::35a/128\",\"protocol\":\"any\",\"port\":163},{\"cidr\":\"10.0.1.96/31\",\"protocol\":\"tcp\",\"port\":77},{\"cidr\":\"2001:db8::29/128\",\"protocol\":\"tcp\",\"port\":159,\"endPort\":162},{\"cidr\":\"10.0.3.60/30\",\"protocol\":\"udp\",\"port\":399},{\"cidr\":\"10.0.2.48/30\",\"protocol\":\"any\",\"port\":65},{\"cidr\":\"10.0.0.198/31\",\"protocol\":\"tcp\",\"port\":129},{\"cidr\":\"10.0.1.220/30\",\"protocol\":\"any\",\"port\":139},{\"cidr\":\"10.0.0.8/30\",\"protocol\":\"udp\",\"port\":185,\"endPort\":185},{\"cidr\":\"10.0.1.18/31\",\"protocol\":\"udp\",\"port\":349},{\"cidr\":\"10.0.2.92/30\",\"protocol\":\"udp\",\"port\":363},{\"cidr\":\"10.0.0.60/30\",\"protocol\":\"udp\",\"port\":319,\"endPort\":322},{\"cidr\":\"2001:db8::287/128\",\"protocol\":\"tcp\",\"port\":241,\"endPort\":242},{\"cidr\":\"2001:db8::cc/128\",\"protocol\":\"tcp\",\"port\":211,\"endPort\":213},{\"cidr\":\"10.0.2.228/30\",\"protocol\":\"udp\",\"port\":113},{\"cidr\":\"2001:db8::371/128\",\"protocol\":\"udp\",\"port\":377},{\"cidr\":\"10.0.3.228/31\",\"protocol\":\"any\",\"port\":347},{\"cidr\":\"10.0.0.217/32\",\"protocol\":\"tcp\",\"port\":267},{\"cidr\":\"10.0.2.184/32\",\"protocol\":\"any\",\"port\":29},{\"cidr\":\"2001:db8::190/128\",\"protocol\":\"tcp\",\"port\":257,\"endPort\":258},{\"cidr\":\"2001:db8::210/128\",\"protocol\":\"udp\",\"port\":13},{\"cidr\":\"2001:db8::fe/128\",\"protocol\":\"any\",\"port\":61},{\"cidr\":\"10.0.0.198/31\",\"protocol\":\"tcp\",\"port\":257},{\"cidr\":\"10.0.3.108/30\",\"protocol\":\"udp\",\"port\":45,\"endPort\":47},{\"cidr\":\"10.0.1.167/32\",\"protocol\":\"udp\",\"port\":297},{\"cidr\":\"10.0.3.94/32\",\"protocol\":\"tcp\",\"port\":307},{\"cidr\":\"10.0.3.58/31\",\"protocol\":\"tcp\",\"port\":23},{\"cidr\":\"10.0.0.200/32\",\"protocol\":\"any\",\"port\":103,\"endPort\":105},{\"cidr\":\"10.0.3.13/32\",\"protocol\":\"tcp\",\"port\":37,\"endPort\":40},{\"cidr\":\"10.0.0.62/31\",\"protocol\":\"udp\",\"port\":267},{\"cidr\":\"10.0.1.194/32\",\"protocol\":\"any\",\"port\":269},{\"cidr\":\"10.0.1.156/31\",\"protocol\":\"tcp\",\"port\":3},{\"cidr\":\"10.0.1.24/30\",\"protocol\":\"udp\",\"port\":47,\"endPort\":48},{\"cidr\":\"10.0.1.36/32\",\"protocol\":\"any\",\"port\":253},{\"cidr\":\"2001:db8::323/128\",\"protocol\":\"any\",\"port\":31,\"endPort\":34},{\"cidr\":\"10.0.2.169/32\",\"protocol\":\"udp\",\"port\":183},{\"cidr\":\"10.0.1.218/31\",\"protocol\":\"udp\",\"port\":47},{\"cidr\":\"10.0.1.120/31\",\"protocol\":\"any\",\"port\":53},{\"cidr\":\"2001:db8::160/128\",\"protocol\":\"any\",\"port\":147},{\"cidr\":\"10.0.0.183/32\",\"protocol\":\"tcp\",\"port\":75},{\"cidr\":\"10.0.2.60/30\",\"protocol\":\"udp\",\"port\":115},{\"cidr\":\"10.0.1.154/31\",\"protocol\":\"tcp\",\"port\":285,\"endPort\":288},{\"cidr\":\"10.0.1.138/31\",\"protocol\":\"tcp\",\"port\":295},{\"cidr\":\"2001:db8::2ac/128\",\"protocol\":\"udp\",\"port\":143},{\"cidr\":\"10.0.3.212/30\",\"protocol\":\"any\",\"port\":125},{\"cidr\":\"2001:db8::6e/128\",\"protocol\":\"any\",\"port\":247,\"endPort\":248},{\"cidr\":\"2001:db8::4/128\",\"protocol\":\"udp\",\"port\":111},{\"cidr\":\"2001:db8::114/128\",\"protocol\":\"tcp\",\"port\":97},{\"cidr\":\"10.0.3.192/31\",\"protocol\":\"any\",\"port\":315},{\"cidr\":\"10.0.1.37/32\",\"protocol\":\"tcp\",\"port\":115},{\"cidr\":\"10.0.0.160/31\",\"protocol\":\"any\",\"port\":67},{\"cidr\":\"10.0.2.136/32\",\"protocol\":\"any\",\"port\":385},{\"cidr\":\"2001:db8::12f/128\",\"protocol\":\"any\",\"port\":239,\"endPort\":242},{\"cidr\":\"2001:db8::1f3/128\",\"protocol\":\"tcp\",\"port\":179},{\"cidr\":\"10.0.1.20/30\",\"protocol\":\"tcp\",\"port\":35},{\"cidr\":\"2001:db8::173/128\",\"protocol\":\"udp\",\"port\":133,\"endPort\":137},{\"cidr\":\"10.0.0.224/32\",\"protocol\":\"udp\",\"port\":61,\"endPort\":63},{\"cidr\":\"10.0.2.20/31\",\"protocol\":\"any\",\"port\":191},{\"cidr\":\"10.0.3.240/31\",\"protocol\":\"udp\",\"port\":157,\"endPort\":160},{\"cidr\":\"10.0.3.10/32\",\"protocol\":\"any\",\"port\":167},{\"cidr\":\"10.0.1.70/31\",\"protocol\":\"any\",\"port\":39,\"endPort\":39},{\"cidr\":\"2001:db8::35e/128\",\"protocol\":\"udp\",\"port\":265},{\"cidr\":\"2001:db8::36d/128\",\"protocol\":\"any\",\"port\":111,\"endPort\":115},{\"cidr\":\"2001:db8::168/128\",\"protocol\":\"tcp\",\"port\":221},{\"cidr\":\"10.0.2.190/31\",\"protocol\":\"tcp\",\"port\":157,\"endPort\":160},{\"cidr\":\"2001:db8::283/128\",\"protocol\":\"any\",\"port\":251,\"endPort\":252}]}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":[]}}, + {"name":"random #67","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8::25c/126\", \"protocol\": \"udp\", \"port\": 1, \"endPort\": 5}, {\"cidr\": \"::/1\", \"protocol\": \"icmp\"}, {\"to\": [{\"cidr\": \"128.0.0.0/2\"}]}, {\"to\": [{\"cidr\": \"209.166.168.72/31\"}, {\"cidr\": \"2001:db8::340/123\", \"except\": [\"2001:db8::35f/000124\"]}, {\"cidr\": \"10.0.2.132\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"udp\"}, {\"protocol\": \"any\"}]}, {\"ports\": [{\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"211.104.110.205/8\"}, {\"cidr\": \"2001:db8::229/119\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0.0.2.28/126\", \"2001:db8::37a/124\"]}, {\"cidr\": \"ee62:a27c:f997:8459:8dc5:4be1:e430:9500/120\"}, {\"cidr\": \"b159:2591::/00032\", \"except\": [\"B159:2591:8000::/33\", \"b159:2591:9000::/36\", \"b159:2591:6000:0000:0000:0000:0.0.0.0/0036\"]}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"tcp\", \"port\": 7}]}, {\"to\": [{\"cidr\": \"10.0.0.0/0022\"}, {\"cidr\": \"10.0.3.56/30\", \"except\": [\"10.0.3.56/30\", \"10.0.3.58/32\"]}, {\"cidr\": \"2001:db8::80/122\", \"except\": [\"2001:db8::9f/124\", \"2001:db8::94/125\", \"2001:db8::98\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 440, \"endPort\": 446}]}, {\"cidr\": \"10.0.3.43\", \"except\": [], \"protocol\": \"any\", \"port\": 9, \"endPort\": 11}], \"deny\": [{\"ports\": [{\"protocol\": \"tcp\"}]}, {\"cidr\": \"10.0.1.0/27\", \"protocol\": \"udp\"}, {\"to\": [{\"cidr\": \"cbf9:5332:74d2:5fe9:0fa9:043b:3b00:0000/104\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 442, \"endPort\": 443}]}]}"}],"expected":{"allow":["0.0.0.0/0:icmp","10.0.0.0/22:udp:440-446","10.0.2.132/32","10.0.3.43/32:tcp:9-11","10.0.3.43/32:udp:9-11","128.0.0.0/2","209.166.168.72/31","211.0.0.0/8:tcp:7","::/0:icmp","2001:db8::80/124:udp:440-446","2001:db8::a0/123:udp:440-446","2001:db8::200/124:tcp:7","2001:db8::210/125:tcp:7","2001:db8::218/126:tcp:7","2001:db8::220/123:tcp:7","2001:db8::240/122:tcp:7","2001:db8::25c/126:udp:1-5","2001:db8::280/121:tcp:7","2001:db8::300/122:tcp:7","2001:db8::340/123:tcp:7","2001:db8::340/124","2001:db8::360/124:tcp:7","2001:db8::380/121:tcp:7","b159:2591::/34:tcp:7","b159:2591:4000::/35:tcp:7","b159:2591:7000::/36:tcp:7","ee62:a27c:f997:8459:8dc5:4be1:e430:9500/120:tcp:7"],"deny":["0.0.0.0/0:tcp","10.0.1.0/27:udp","::/0:tcp","cbf9:5332:74d2:5fe9:fa9:43b:3b00:0/104:udp:442-443"]}}, + {"name":"random #68","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"::/0\"}, {\"cidr\": \"f1e1:0610:d725:0df6:0000:0000:0000:0000/0064\", \"except\": [\"f1e1:0610:d725:0df6:a000:0000:0000:0000/67\", \"f1e1:610:d725:df6:ce00::/71\", \"F1E1:610:D725:DF6:8000::/65\"]}, {\"cidr\": \"2001:db8::3e0/124\", \"except\": []}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65533}, {\"protocol\": \"udp\", \"port\": 442}]}, {\"cidr\": \"2001:db8::1a3/000118\", \"protocol\": \"any\", \"port\": 65533}, {\"to\": [{\"cidr\": \"2001:db8::20/123\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 78, \"endPort\": 78}, {\"protocol\": \"udp\", \"port\": 441}, {\"protocol\": \"udp\"}]}, {\"to\": [{\"cidr\": \"234.41.212.0/255.255.255.0\"}, {\"cidr\": \"2001:db8::202/127\"}, {\"cidr\": \"10.0.2.128/25\", \"except\": [\"10.0.2.198/27\", \"10.0.2.216/030\"]}, {\"cidr\": \"10.0.3.224/31\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 445, \"endPort\": 448}, {\"protocol\": \"udp\", \"port\": 79, \"endPort\": 17672}, {\"protocol\": \"udp\"}]}, {\"to\": [{\"cidr\": \"2001:db8::3d8/125\"}, {\"cidr\": \"10.0.1.120/29\"}, {\"cidr\": \"10.0.0.0/28\"}, {\"cidr\": \"10.0.3.97/0.0.0.15\"}], \"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"tcp\"}, {\"protocol\": \"any\", \"port\": 80}]}, {\"to\": [{\"cidr\": \"50.61.64.71/1\", \"except\": [\"19.0.0.0/8\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.3.63/126\"}]}, {\"to\": [{\"cidr\": \"bd70:e3d3:c594:70c8:49a9:ad94:b572:d05e/64\"}, {\"cidr\": \"10.0.2.32/27\"}, {\"cidr\": \"2001:db8::290\"}]}, {\"to\": [{\"cidr\": \"10.0.3.32/29\"}, {\"cidr\": \"10.0.2.192/26\"}, {\"cidr\": \"10.0.2.224/27\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"port\": 78, \"endPort\": 78}, {\"protocol\": \"udp\", \"port\": 81}]}], \"deny\": [{\"to\": [{\"cidr\": \"10.0.1.144/255.255.255.240\"}, {\"cidr\": \"0000:0000:0000:0000:0000:ffff:168.0.0.0/104\"}, {\"cidr\": \"3f98:69a5:5327:b051:4c9a:3255:d7cd:a68d/128\", \"except\": [\"3f98:69a5:5327:b051:4c9a:3255:d7cd:a68d/00128\", \"3f98:69a5:5327:b051:4c9a:3255:d7cd:a68d\"]}, {\"cidr\": \"2001:db8::240/128\"}], \"ports\": [{\"protocol\": \"tcp\"}]}]}"}],"expected":{"allow":["0.0.0.0/4","16.0.0.0/7","18.0.0.0/8","20.0.0.0/6","24.0.0.0/5","32.0.0.0/3","64.0.0.0/2","234.41.212.0/24:tcp:445-448","234.41.212.0/24:udp","::/0:tcp:65533","::/0:udp:442","2001:db8::/118:udp:65533","2001:db8::20/123:tcp:78","2001:db8::20/123:udp","2001:db8::202/127:tcp:445-448","2001:db8::202/127:udp","2001:db8::290/128","2001:db8::33c/126","2001:db8::3d8/125:tcp","2001:db8::3d8/125:udp:80","bd70:e3d3:c594:70c8::/64"],"deny":["10.0.1.144/28:tcp","::ffff:168.0.0.0/104:tcp","2001:db8::240/128:tcp"]}}, + {"name":"random #69","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::2ab/128\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"2001:DB8::2AB/128\",\n\t\t\t\t\t\t\"2001:db8::2ab\",\n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:02ab\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"::/0\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.112/028\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.128/25\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.3.128/25\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":1,\n\t\t\t\t\t\"endPort\":1\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":82\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:DB8::DE\",\n\t\t\t\"except\":[\n\t\t\t\t\"2001:DB8::DE\"\n\t\t\t],\n\t\t\t\"protocol\":\"any\"\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"119.152.73.116/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":81\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:DB8::320/123\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.112/29\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"7.18.28.0/0.0.0.255\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"7.18.28.45/255.255.255.224\",\n\t\t\t\t\t\t\"7.18.28.149/26\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.1.0/25\",\n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:0000/119\",\n\t\t\t\t\t\t\"10.0.3.241/255.255.255.224\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"port\":65533\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"855:41a0:2051:1630:c287:8214:5350:3cea/104\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"0855:41a0:2051:1630:c287:8214:53d6:bd21/108\",\n\t\t\t\t\t\t\"855:41a0:2051:1630:c287:8214:5319:853c/107\",\n\t\t\t\t\t\t\"855:41a0:2051:1630:c287:8214:53d4:1fa2/110\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.112/255.255.255.240\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::200/120\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":79,\n\t\t\t\t\t\"endPort\":80\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.224/27\",\n\t\t\t\"except\":[\n\t\t\t\t\"10.0.0.240/27\"\n\t\t\t],\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":10\n\t\t}\n\t],\n\t\"deny\":[\n\t\t{\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.255\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.131\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::/118\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.1.102/030\",\n\t\t\t\t\t\t\"10.0.3.0/24\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.221/0.0.0.63\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.3.238/31\",\n\t\t\t\t\t\t\"10.0.3.220/30\",\n\t\t\t\t\t\t\"10.0.3.192/255.255.255.192\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"error":"allow[3].to[3].except[1] must be contained in 10.0.0.0/22"}}, + {"name":"random #70","input":[{"text":"{\"allow\":[{\"cidr\":\"10.0.2.43/26\",\"except\":[\"10.0.2.48/29\"],\"protocol\":\"any\"},{\"to\":[{\"cidr\":\"10.0.3.0/25\",\"except\":[\"::ffff:216.30.143.225\",\"10.0.3.12/30\"]},{\"cidr\":\"10.0.2.224/00027\",\"except\":[\"10.0.2.224/27\",\"10.0.2.231/0029\",\"10.0.2.224/28\"]},{\"cidr\":\"10.0.0.0/22\"},{\"cidr\":\"10.0.0.0/24\"}]},{\"ports\":[{\"protocol\":\"udp\",\"port\":79},{\"protocol\":\"icmp\"},{\"protocol\":\"any\",\"port\":441,\"endPort\":443}]},{\"cidr\":\"10.0.2.118/25\",\"except\":[],\"protocol\":\"tcp\",\"port\":445},{\"cidr\":\"10.0.0.200/28\",\"except\":[\"10.0.0.192/29\"],\"protocol\":\"udp\"},{\"cidr\":\"143.93.111.62\",\"protocol\":\"any\",\"port\":445}],\"deny\":[{\"cidr\":\"0.0.0.0/01\",\"except\":[\"44.0.0.0/6\",\"9.51.130.105/8\"]},{\"to\":[{\"cidr\":\"10.0.3.63/30\",\"except\":[\"10.0.3.62/31\",\"10.0.3.63/32\",\"10.0.3.60/30\"]}],\"ports\":[{\"protocol\":\"tcp\",\"port\":82,\"endPort\":82},{\"protocol\":\"tcp\",\"port\":79}]},{\"cidr\":\"10.0.2.144/29\",\"protocol\":\"TCP\",\"port\":7,\"endPort\":9},{\"to\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0.0.1.208/127\"},{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:01e9/120\"}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"any\",\"port\":444,\"endPort\":449}]},{\"to\":[{\"cidr\":\"2001:db8::118/125\"},{\"cidr\":\"0.0.0.0/0\",\"except\":[\"12.0.0.0/6\"]},{\"cidr\":\"10.0.0.0/255.255.254.0\"},{\"cidr\":\"10.0.3.192/27\"}]},{\"cidr\":\"10.0.2.17/29\",\"protocol\":\"any\"},{\"cidr\":\"10.0.1.0/24\",\"protocol\":\"icmp\"},{\"cidr\":\"10.0.0.207/26\",\"protocol\":\"any\"}]}"}],"expected":{"error":"allow[1].to[0].except[0] must be contained in 10.0.3.0/25"}}, + {"name":"random #71","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.3.64/27\"}, {\"cidr\": \"10.0.1.12/30\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 65534}]}, {\"ports\": [{\"protocol\": \"udp\", \"port\": 65534}]}, {\"to\": [{\"cidr\": \"10.0.0.0/255.255.255.192\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 2, \"endPort\": 4}, {\"protocol\": \"udp\", \"port\": 444, \"endPort\": 448}, {\"protocol\": \"any\", \"port\": 444}]}], \"deny\": []}"}],"expected":{"allow":["0.0.0.0/0:udp:65534","10.0.0.0/26:tcp:2-4","10.0.0.0/26:tcp:444","10.0.0.0/26:udp:2-4","10.0.0.0/26:udp:444-448","10.0.1.12/30:tcp:65534","10.0.3.64/27:tcp:65534","::/0:udp:65534"],"deny":[]}}, + {"name":"random #72","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"10.0.0.214/29\"},{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0192/124\"},{\"cidr\":\"10.0.2.64/27\",\"except\":[\"10.0.2.82/30\",\"10.0.2.65/30\"]},{\"cidr\":\"2001:db8::100/120\"}],\"ports\":[{\"protocol\":\"tcp\"}]},{\"to\":[{\"cidr\":\"10.0.3.212/31\"},{\"cidr\":\"187.7.219.204/31\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":8,\"endPort\":11},{\"protocol\":\"any\",\"port\":2,\"endPort\":54369}]},{\"cidr\":\"10.0.2.0/25\",\"protocol\":\"icmp\"},{\"cidr\":\"2001:db8::100/120\",\"protocol\":\"any\"},{\"cidr\":\"10.0.2.88/23\",\"protocol\":\"tcp\"},{\"cidr\":\"10.0.1.163/25\",\"protocol\":\"tcp\",\"port\":9},{\"to\":[{\"cidr\":\"197.76.177.206/16\",\"except\":[\"197.76.248.0/0023\"]}],\"ports\":[{\"protocol\":\"icmp\"}]},{\"to\":[{\"cidr\":\"10.0.1.0/255.255.255.224\",\"except\":[]},{\"cidr\":\"10.0.3.64/27\"},{\"cidr\":\"213.232.227.87/128.0.0.0\"}],\"ports\":[{\"protocol\":\"udp\"},{\"protocol\":\"udp\",\"port\":79,\"endPort\":83}]},{\"to\":[{\"cidr\":\"10.0.1.0/255.255.255.0\",\"except\":[\"10.0.1.192/27\",\"10.0.1.144/31\"]}],\"ports\":[{\"protocol\":\"any\",\"port\":7,\"endPort\":11}]},{\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"any\",\"port\":65535},{\"protocol\":\"udp\",\"port\":82}]},{\"cidr\":\"2001:db8::/118\"},{\"to\":[{\"cidr\":\"2001:db8::3a/119\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":7,\"endPort\":12},{\"protocol\":\"tcp\",\"port\":65532,\"endPort\":65534},{\"protocol\":\"any\"},{\"protocol\":\"any\",\"port\":12,\"endPort\":13}]},{\"to\":[{\"cidr\":\"2001:db8::240/122\",\"except\":[]},{\"cidr\":\"0.20.38.21/1\"},{\"cidr\":\"10.0.1.112/30\"},{\"cidr\":\"2001:db8::115/127\"}]},{\"to\":[{\"cidr\":\"10.0.1.110/0028\"},{\"cidr\":\"2001:DB8::2BA/119\"}]},{\"to\":[{\"cidr\":\"4000::/3\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":65533,\"endPort\":65535}]},{\"cidr\":\"10.0.1.0/0.0.0.255\",\"protocol\":\"tcp\"},{\"to\":[{\"cidr\":\"f19:599::/32\"},{\"cidr\":\"10.0.2.0/0.0.1.255\"},{\"cidr\":\"10.0.0.0/23\"},{\"cidr\":\"e876:5afc:bf77:3056:e371:159f:26.243.212.238/127\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":445,\"endPort\":445},{\"port\":65530,\"endPort\":65530},{\"protocol\":\"tcp\",\"port\":9,\"endPort\":13}]},{\"cidr\":\"2001:db8::300/122\",\"except\":[\"2001:db8::331/123\",\"2001:db8::300/123\"],\"protocol\":\"tcp\",\"port\":4},{\"to\":[{\"cidr\":\"2b09:3066:fb6b:9706:d84:bbf0:9de1:eebf/000127\"},{\"cidr\":\"10.0.0.0/24\"},{\"cidr\":\"10.0.0.88/29\",\"except\":[\"10.0.0.94\",\"10.0.0.88/00029\",\"10.0.0.93/31\"]},{\"cidr\":\"10.0.3.192/26\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":441}]},{\"to\":[{\"cidr\":\"10.0.2.0/0024\"},{\"cidr\":\"10.0.2.179/0.0.3.255\"},{\"cidr\":\"10.0.0.81/22\"},{\"cidr\":\"10.0.0.0/0024\"}],\"ports\":[{\"port\":65531,\"endPort\":65535},{\"protocol\":\"udp\",\"port\":78,\"endPort\":79},{\"port\":10,\"endPort\":15},{\"protocol\":\"any\",\"port\":79}]},{\"cidr\":\"2001:db8::2ec/125\"},{\"to\":[{\"cidr\":\"10.0.3.56/23\"},{\"cidr\":\"10.0.0.216/00028\"},{\"cidr\":\"10.0.1.168/00031\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":82},{\"protocol\":\"icmp\"},{\"protocol\":\"udp\"},{\"protocol\":\"tcp\"}]},{\"cidr\":\"efd7:83b3:37c:8fe2:e639:e5cd:a6d:cc21/64\",\"except\":[\"EFD7:83B3:37C:8FE2:C800::/71\",\"efd7:83b3:037c:8fe2:5569:8d6d:675e:c9b5/68\"],\"protocol\":\"any\",\"port\":443,\"endPort\":447},{\"to\":[{\"cidr\":\"c2ce:a716:280d:3c08:bd0a:a4c:8a4d:4067/127\"},{\"cidr\":\"215.80.0.0/255.240.0.0\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":65530,\"endPort\":65532},{\"port\":82},{\"protocol\":\"tcp\"},{\"protocol\":\"udp\",\"port\":5,\"endPort\":11}]},{\"cidr\":\"10.0.2.175/26\"},{\"to\":[{\"cidr\":\"10.0.0.134\"},{\"cidr\":\"10.0.0.96/255.255.255.240\"}],\"ports\":[{\"protocol\":\"icmp\"},{\"protocol\":\"udp\",\"port\":444,\"endPort\":444}]},{\"to\":[{\"cidr\":\"10.0.0.0/23\"},{\"cidr\":\"10.0.2.32/27\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":444,\"endPort\":444}]},{\"to\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:012a/124\"},{\"cidr\":\"249.201.109.251/00\"},{\"cidr\":\"10.0.0.224/028\"},{\"cidr\":\"137.15.252.210/255.240.0.0\",\"except\":[\"137.8.0.0/13\",\"137.7.0.0/17\",\"137.3.128.0/17\"]}],\"ports\":[{\"protocol\":\"udp\"},{\"port\":9}]},{\"to\":[{\"cidr\":\"10.0.0.0/22\"},{\"cidr\":\"10.0.1.112/28\",\"except\":[\"10.0.1.112/29\",\"10.0.3.160/255.255.255.224\"]}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"udp\"}]},{\"ports\":[{\"protocol\":\"any\",\"port\":442}]},{\"to\":[{\"cidr\":\"74.255.82.232/31\"},{\"cidr\":\"10.0.2.128/00029\",\"except\":[\"10.0.2.133\",\"10.0.2.128/29\",\"10.0.2.128/29\"]},{\"cidr\":\"131.249.0.0/16\",\"except\":[\"131.249.0.0/21\"]},{\"cidr\":\"2001:DB8::180/121\"}]},{\"to\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0.0.0.128/122\",\"except\":[]},{\"cidr\":\"2001:db8::280/124\"}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"udp\"},{\"protocol\":\"udp\",\"port\":80}]},{\"to\":[{\"cidr\":\"a136:ff78:a29e:f63a:8dd9:ff2f:25dd:8700/120\"},{\"cidr\":\"0000:0000:0000:0000:0000:ffff:56.52.247.51\",\"except\":[\"x\",\"::ffff:56.52.247.51/128\",\"0000:0000:0000:0000:0000:ffff:56.52.247.51/128\"]},{\"cidr\":\"2001:db8::/120\"},{\"cidr\":\"10.0.3.0/26\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":444,\"endPort\":446}]},{\"to\":[{\"cidr\":\"2001:db8::1e0/123\",\"except\":[\"fe80::1%eth0\",\"2001:0db8:0000:0000:0000:0000:0000:01ec/126\",\"2001:db8::1f5/128\"]}],\"ports\":[{\"protocol\":\"any\",\"port\":78},{\"protocol\":\"any\"},{\"protocol\":\"tcp\",\"port\":441},{\"protocol\":\"tcp\",\"port\":65532}]},{\"to\":[{\"cidr\":\"125.84.246.233/16\"},{\"cidr\":\"10.0.0.94/0031\"}]},{\"to\":[{\"cidr\":\"0b95:d855:64ad:52d9:cf4a:28c2:253.252.188.45\",\"except\":[]}],\"ports\":[{\"protocol\":\"tcp\",\"port\":65531,\"endPort\":65534},{\"port\":79,\"endPort\":80},{\"protocol\":\"tcp\",\"port\":441}]},{},{\"to\":[{\"cidr\":\"2001:db8::313/122\",\"except\":[\"2001:db8::324/127\",\"2001:db8::300/00122\",\"2001:db8::306\"]},{\"cidr\":\"8000:0000:0000:0000:0000:0000:0.0.0.0/1\",\"except\":[\"9369:188a:2abb:24c:cf0b:7f73:13c7:ddbb/4\",\"c800:0000:0000:0000:0000:0000:0000:0000/5\"]},{\"cidr\":\"10.0.2.222/24\"},{\"cidr\":\"10.0.3.100/22\"}]},{\"to\":[{\"cidr\":\"108.0.0.0/8\"},{\"cidr\":\"10.0.3.40/255.255.255.252\"},{\"cidr\":\"10.0.2.48/29\"},{\"cidr\":\"10.0.2.255\",\"except\":[\"10.0.2.255/32\",\"10.0.2.255/32\",\"10.0.2.255/255.255.255.255\"]}]},{\"cidr\":\"2001:db8::3b2/127\",\"protocol\":\"tcp\",\"port\":79}],\"deny\":[{\"to\":[{\"cidr\":\"10.0.0.219/25\"},{\"cidr\":\"2001:db8::30a/125\"},{\"cidr\":\"10.0.1.171\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":80},{\"protocol\":\"tcp\",\"port\":81,\"endPort\":85},{\"protocol\":\"icmp\"},{\"protocol\":\"any\",\"port\":5}]},{\"ports\":[{\"protocol\":\"any\",\"port\":65535,\"endPort\":65535},{\"protocol\":\"udp\",\"port\":65534,\"endPort\":65535},{\"protocol\":\"tcp\"}]},{\"ports\":[{\"protocol\":\"any\",\"port\":78},{\"protocol\":\"tcp\"}]},{\"cidr\":\"110.126.130.170/255.255.255.0\",\"protocol\":\"any\",\"port\":79,\"endPort\":83},{\"to\":[{\"cidr\":\"118.188.53.30/12\"}],\"ports\":[{\"port\":65531,\"endPort\":65531},{\"protocol\":\"icmp\"}]},{\"ports\":[{\"protocol\":\"udp\",\"port\":65531},{\"protocol\":\"tcp\"}]},{\"cidr\":\"10.0.3.0/25\",\"except\":[\"10.0.3.0/26\"],\"protocol\":\"udp\"},{\"to\":[{\"cidr\":\"10.0.3.25/31\",\"except\":[\"10.0.3.24/31\",\"10.0.3.35/255.255.254.0\"]},{\"cidr\":\"129.121.13.205/4\"},{\"cidr\":\"10.0.0.78/27\",\"except\":[\"10.0.0.69/255.255.255.240\",\"10.0.0.64/29\"]}],\"ports\":[{\"protocol\":\"udp\",\"port\":440,\"endPort\":444},{\"protocol\":\"udp\",\"port\":445}]}]}"}],"expected":{"error":"allow[28].to[1].except[1] must be contained in 10.0.1.112/28"}}, + {"name":"random #73","input":[{"text":"{\n \"allow\": [], \n \"deny\": [\n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.16/32\"\n }, \n {\n \"cidr\": \"2001:db8::1cc/000124\"\n }, \n {\n \"cidr\": \"10.0.2.128/25\"\n }, \n {\n \"cidr\": \"10.0.2.106\", \n \"except\": [\n \"10.0.2.106/32\"\n ]\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 4\n }, \n {\n \"protocol\": \"any\", \n \"port\": 7, \n \"endPort\": 10\n }, \n {\n \"protocol\": \"any\", \n \"port\": 440, \n \"endPort\": 443\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 9\n }\n ]\n }, \n {\n \"cidr\": \"27.62.139.132/32\", \n \"protocol\": \"icmp\"\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.19/255.255.255.254\"\n }, \n {\n \"cidr\": \"::/0\", \n \"except\": [\n \"9a92:5186:51a0:5212:245d:a646:8223:f6b7/5\", \n \"::/1\", \n \"1ab0:2980:c9c6:8d27:3085:7725:577f:c50c/7\"\n ]\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"udp\"\n }\n ]\n }\n ]\n}"}],"expected":{"allow":[],"deny":["10.0.0.18/31:udp","10.0.2.128/25:tcp:4","10.0.2.128/25:tcp:7-10","10.0.2.128/25:tcp:440-443","10.0.2.128/25:udp:7-10","10.0.2.128/25:udp:440-443","10.0.3.16/32:tcp:4","10.0.3.16/32:tcp:7-10","10.0.3.16/32:tcp:440-443","10.0.3.16/32:udp:7-10","10.0.3.16/32:udp:440-443","27.62.139.132/32:icmp","2001:db8::1c0/124:tcp:4","2001:db8::1c0/124:tcp:7-10","2001:db8::1c0/124:tcp:440-443","2001:db8::1c0/124:udp:7-10","2001:db8::1c0/124:udp:440-443","8000::/4:udp","9000::/5:udp","a000::/3:udp","c000::/2:udp"]}}, + {"name":"random #74","input":[{"text":"{\"deny\":[{\"cidr\":\"226.200.242.0/24\",\"except\":[],\"protocol\":\"udp\"}]}"}],"expected":{"allow":[],"deny":["226.200.242.0/24:udp"]}}, + {"name":"random #75","input":[{"text":"{\"allow\":[],\"deny\":[{\"cidr\":\"10.0.1.64/26\",\"protocol\":\"udp\",\"port\":3,\"endPort\":3}]}"}],"expected":{"allow":[],"deny":["10.0.1.64/26:udp:3"]}}, + {"name":"random #76","input":[{"text":"{\n \"allow\": [\n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::144/127\",\n \"except\": [\n \"2001:db8::144/127\",\n \"2001:db8::145\",\n \"2001:db8::144/127\"\n ]\n },\n {\n \"cidr\": \"10.0.2.105/23\"\n },\n {\n \"cidr\": \"10.0.0.0/22\",\n \"except\": [\n \"10.0.2.0/23\",\n \"10.0.2.0/23\"\n ]\n },\n {\n \"cidr\": \"32.0.0.0/4\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"tcp\",\n \"port\": 441\n },\n {\n \"port\": 65535,\n \"endPort\": 65535\n },\n {\n \"protocol\": \"icmp\"\n },\n {\n \"protocol\": \"any\",\n \"port\": 443,\n \"endPort\": 445\n }\n ]\n },\n {\n \"to\": [\n {\n \"cidr\": \"10.0.1.220/31\"\n },\n {\n \"cidr\": \"10.0.3.0/24\",\n \"except\": [\n \"10.0.3.42/0.0.0.0\",\n \"10.0.3.0/027\",\n \"10.0.3.101/25\"\n ]\n },\n {\n \"cidr\": \"77.17.17.40/255.255.255.254\",\n \"except\": [\n \"77.17.17.41\"\n ]\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"tcp\"\n },\n {\n \"protocol\": \"udp\",\n \"port\": 79,\n \"endPort\": 83\n },\n {\n \"protocol\": \"any\",\n \"port\": 65532\n },\n {\n \"protocol\": \"tcp\",\n \"port\": 444,\n \"endPort\": 449\n }\n ]\n },\n {\n \"cidr\": \"80.0.0.0/4\",\n \"except\": [],\n \"protocol\": \"any\",\n \"port\": 82,\n \"endPort\": 83\n },\n {\n \"to\": [\n {\n \"cidr\": \"213.90.229.0/0.0.0.255\"\n },\n {\n \"cidr\": \"10.0.3.96/28\",\n \"except\": [\n \"10.0.3.102/31\",\n \"10.0.3.108/30\",\n \"10.0.3.107/255.255.255.255\"\n ]\n }\n ]\n },\n {\n \"cidr\": \"2001:DB8::/118\",\n \"protocol\": \"tcp\"\n },\n {\n \"to\": [\n {\n \"cidr\": \"10.0.1.208/28\",\n \"except\": []\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"udp\",\n \"port\": 65531,\n \"endPort\": 65534\n },\n {\n \"protocol\": \"icmp\"\n }\n ]\n }\n ],\n \"deny\": [\n {\n \"to\": [\n {\n \"cidr\": \"::/1\"\n },\n {\n \"cidr\": \"2001:db8::248/000123\"\n },\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/118\",\n \"except\": [\n \"2001:DB8::200/119\",\n \"2001:DB8::/121\"\n ]\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"tcp\",\n \"port\": 65530\n },\n {\n \"protocol\": \"udp\",\n \"port\": 78,\n \"endPort\": 80\n },\n {},\n {\n \"port\": 65530\n }\n ]\n },\n {\n \"to\": [\n {\n \"cidr\": \"10.0.1.192/26\"\n },\n {\n \"cidr\": \"40.161.139.255/192.0.0.0\",\n \"except\": [\n \"0.0.0.0/2\"\n ]\n },\n {\n \"cidr\": \"10.0.0.0/23\",\n \"except\": []\n }\n ]\n },\n {\n \"cidr\": \"10.0.0.140/25\",\n \"protocol\": \"icmp\"\n },\n {\n \"cidr\": \"10.0.0.110/023\",\n \"protocol\": \"tcp\",\n \"port\": 65530\n },\n {\n \"cidr\": \"10.0.3.64/27\",\n \"except\": [],\n \"protocol\": \"udp\",\n \"port\": 78\n },\n {\n \"cidr\": \"41.12.201.130/012\",\n \"protocol\": \"tcp\",\n \"port\": 65535,\n \"endPort\": 65535\n }\n ]\n}"}],"expected":{"error":"allow[1].to[1].except[0] must be contained in 10.0.3.0/24"}}, + {"name":"random #77","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"128.0.0.0/4\"},{\"cidr\":\"10.0.2.64/00028\"},{\"cidr\":\"0.0.0.0/128.0.0.0\"},{\"cidr\":\"2001:db8::153/122\",\"except\":[\"2001:0db8:0000:0000:0000:0000:0000:0170/124\",\"2001:db8::160/125\"]}],\"ports\":[{\"protocol\":\"udp\"},{}]},{\"to\":[{\"cidr\":\"240.255.178.243/4\"},{\"cidr\":\"10.0.2.0/24\"}]},{\"cidr\":\"10.0.2.44/31\",\"protocol\":\"any\",\"port\":11,\"endPort\":17},{\"to\":[{\"cidr\":\"::/3\"},{\"cidr\":\"2001:db8::280/121\"},{\"cidr\":\"224.105.208.28/32\"}],\"ports\":[{\"protocol\":\"udp\"},{\"protocol\":\"tcp\",\"port\":81},{\"protocol\":\"tcp\",\"port\":81},{\"protocol\":\"tcp\",\"port\":442,\"endPort\":442}]},{\"cidr\":\"2001:db8::3f5/126\"},{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0171/119\",\"protocol\":\"any\",\"port\":78}],\"deny\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0.0.0.50/128\",\"except\":[\"2001:DB8::32\",\"2001:0db8:0000:0000:0000:0000:0.0.0.50\"],\"protocol\":\"tcp\",\"port\":82}]}"}],"expected":{"allow":["0.0.0.0/1","128.0.0.0/4","224.105.208.28/32:tcp:81","224.105.208.28/32:tcp:442","224.105.208.28/32:udp","240.0.0.0/4","::/3:tcp:81","::/3:tcp:442","::/3:udp","2001:db8::/119:tcp:78","2001:db8::/119:udp:78","2001:db8::140/123","2001:db8::168/125","2001:db8::280/121:tcp:81","2001:db8::280/121:tcp:442","2001:db8::280/121:udp","2001:db8::3f4/126"],"deny":[]}}, + {"name":"random #78","input":[{"text":"{\"allow\": [{}, {\"cidr\": \"10.0.1.88/29\", \"except\": [\"10.0.1.91/31\", \"10.0.1.89\", \"10.0.1.88/255.255.255.248\"], \"protocol\": \"tcp\", \"port\": 78, \"endPort\": 81}, {\"ports\": [{\"port\": 79, \"endPort\": 80}, {\"protocol\": \"udp\"}]}], \"deny\": [{\"to\": [{\"cidr\": \"64.0.0.0/2\", \"except\": [\"116.128.0.0/9\"]}, {\"cidr\": \"10.0.0.0/23\", \"except\": [\"10.0.1.160/29\"]}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"any\", \"port\": 443}]}, {\"cidr\": \"10.0.0.118/0.0.0.63\", \"protocol\": \"tcp\", \"port\": 65533, \"endPort\": 65535}, {\"to\": [{\"cidr\": \"0.0.0.0/0\"}, {\"cidr\": \"58.97.0.0/16\"}, {\"cidr\": \"::/1\"}, {\"cidr\": \"10.0.0.0/23\"}]}]}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["0.0.0.0/0","::/1"]}}, + {"name":"random #79","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.2.148/29\"}, {\"cidr\": \"10.0.1.32/00027\", \"except\": [\"10.0.1.48/28\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0256/127\", \"except\": []}, {\"cidr\": \"2001:DB8::330/00125\"}]}]}"}],"expected":{"allow":["10.0.1.32/28","10.0.2.144/29","2001:db8::256/127","2001:db8::330/125"],"deny":[]}}, + {"name":"random #80","input":[{"text":"{\"allow\":[],\"deny\":[{\"to\":[{\"cidr\":\"10.0.1.0/24\",\"except\":[]},{\"cidr\":\"2001:db8::3c8/120\",\"except\":[\"10.0.1.0/24\",\"2001:db8::36e/123\"]},{\"cidr\":\"10.0.0.0/23\",\"except\":[]}]},{\"cidr\":\"bee9:4354::/32\",\"protocol\":\"any\",\"port\":9,\"endPort\":14},{\"cidr\":\"938d:babd:1752:fdf1:04d9:c621:0990:c684/127\",\"except\":[\"938d:babd:1752:fdf1:4d9:c621:990:c684/127\",\"10.0.0.0/33\"],\"protocol\":\"tcp\"},{\"to\":[{\"cidr\":\"186.0.0.0/8\"},{\"cidr\":\"2001:db8::180/122\",\"except\":[]},{\"cidr\":\"65.229.126.156/24\",\"except\":[\"65.229.126.136/29\",\"65.229.126.168/29\",\"65.229.126.64/0029\"]}],\"ports\":[{\"protocol\":\"any\",\"port\":6},{\"protocol\":\"tcp\"}]},{\"to\":[{\"cidr\":\"228.0.0.0/8\"},{\"cidr\":\"9f1b:204c:dd98:6ca3:8b4d:6990:377c:387c/127\"},{\"cidr\":\"2001:DB8::1C7/118\"}]},{\"cidr\":\"10.0.2.217\",\"except\":[\"10.0.2.217\",\"10.0.2.217/32\"],\"protocol\":\"tcp\",\"port\":442},{\"cidr\":\"2001:db8::2d/00123\",\"protocol\":\"icmp\"},{\"to\":[{\"cidr\":\"10.0.1.128/26\"},{\"cidr\":\"10.0.0.48/29\"},{\"cidr\":\"10.0.0.0/0.0.0.31\"},{\"cidr\":\"10.0.2.248/29\"}],\"ports\":[{\"protocol\":\"udp\"},{\"port\":78}]}]}"}],"expected":{"error":"deny[0].to[1].except[0] must be contained in 2001:db8::300/120"}}, + {"name":"random #81","input":[{"text":"{\n\t\"deny\":[]\n}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #82","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.56/0.0.0.7\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 65533\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"5d41:66bf:327e:9273:f581:3abb::/96\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 443,\n\t\t\t\"endPort\": 443\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.128/25\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.1.248/029\",\n\t\t\t\t\t\t\"10.0.1.176/30\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.64/00030\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 441\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 2\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.125/29\",\n\t\t\t\"except\": [],\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 441\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"b29a:49c:e895:7807:388b:931d:2c8e:b600/120\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::138/127\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:db8::138/128\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/24\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.0.124/31\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/24\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.208/31\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.80/29\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.224/27\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.47\",\n\t\t\t\"except\": [\n\t\t\t\t\"10.0.1.47\",\n\t\t\t\t\"10.0.1.47\",\n\t\t\t\t\"10.0.1.47\"\n\t\t\t],\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 2\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"128.0.0.0/1\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"71a4:245c:ae13:2db4:fefe:8649:4493:4900/120\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"71a4:245c:ae13:2db4:fefe:8649:4493:4988/126\",\n\t\t\t\t\t\t\"71a4:245c:ae13:2db4:fefe:8649:4493:4930/124\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.220/00032\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.192/29\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 81\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 4\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.164/30\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.225/26\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.0.224/28\",\n\t\t\t\t\t\t\"10.0.0.192/27\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.224/28\",\n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.239\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 443,\n\t\t\t\t\t\"endPort\": 449\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 1,\n\t\t\t\t\t\"endPort\": 6\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 80\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.224/28\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 65531,\n\t\t\t\"endPort\": 65531\n\t\t}\n\t]\n}"}],"expected":{"allow":["10.0.0.0/24","10.0.1.56/29:udp:65533","10.0.1.120/29:udp:441","10.0.1.128/27","10.0.1.160/28","10.0.1.180/30","10.0.1.184/29","10.0.1.192/27","10.0.1.224/28","10.0.1.240/29","10.0.2.192/29:tcp","10.0.2.192/29:udp","10.0.3.224/27","128.0.0.0/1:tcp","128.0.0.0/1:udp","2001:db8::139/128:icmp","2001:db8::139/128:tcp","5d41:66bf:327e:9273:f581:3abb::/96:udp:443","71a4:245c:ae13:2db4:fefe:8649:4493:4900/123:tcp","71a4:245c:ae13:2db4:fefe:8649:4493:4900/123:udp","71a4:245c:ae13:2db4:fefe:8649:4493:4920/124:tcp","71a4:245c:ae13:2db4:fefe:8649:4493:4920/124:udp","71a4:245c:ae13:2db4:fefe:8649:4493:4940/122:tcp","71a4:245c:ae13:2db4:fefe:8649:4493:4940/122:udp","71a4:245c:ae13:2db4:fefe:8649:4493:4980/125:tcp","71a4:245c:ae13:2db4:fefe:8649:4493:4980/125:udp","71a4:245c:ae13:2db4:fefe:8649:4493:498c/126:tcp","71a4:245c:ae13:2db4:fefe:8649:4493:498c/126:udp","71a4:245c:ae13:2db4:fefe:8649:4493:4990/124:tcp","71a4:245c:ae13:2db4:fefe:8649:4493:4990/124:udp","71a4:245c:ae13:2db4:fefe:8649:4493:49a0/123:tcp","71a4:245c:ae13:2db4:fefe:8649:4493:49a0/123:udp","71a4:245c:ae13:2db4:fefe:8649:4493:49c0/122:tcp","71a4:245c:ae13:2db4:fefe:8649:4493:49c0/122:udp","b29a:49c:e895:7807:388b:931d:2c8e:b600/120:icmp","b29a:49c:e895:7807:388b:931d:2c8e:b600/120:tcp"],"deny":["0.0.0.0/0:tcp:4","10.0.0.224/27:tcp","10.0.0.224/27:udp","10.0.2.164/30:tcp","10.0.2.164/30:udp","10.0.3.239/32:icmp","10.0.3.239/32:tcp:80","10.0.3.239/32:udp:1-6","10.0.3.239/32:udp:443-449","::/0:tcp:4"]}}, + {"name":"random #83","input":[{"text":"{\n\"allow\":[\n{\n\"to\":[\n{\n\"cidr\":\"60.0.0.0/8\"\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"10.0.2.176/29\"\n},\n{\n\"cidr\":\"10.0.0.0/23\",\n\"except\":[\n\"10.0.0.16/28\",\n\"10.0.0.0/23\"\n]\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"192.0.2.0/24\",\n\"except\":[\n\"10.0.0.0/8\"\n]\n}\n],\n\"ports\":[\n{\n\"protocol\":\"tcp\"\n}\n]\n},\n{}\n],\n\"deny\":[\n{\n\"to\":[\n{\n\"cidr\":\"10.0.2.30/0026\"\n},\n{\n\"cidr\":\"2001:db8::380/122\",\n\"except\":[\n\"2001:db8::3a0/123\"\n]\n}\n],\n\"ports\":[\n{\n\"protocol\":\"udp\",\n\"port\":10,\n\"endPort\":13\n},\n{\n\"protocol\":\"udp\",\n\"port\":65535\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"2001:db8::30/125\"\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"10.0.2.159/28\"\n},\n{\n\"cidr\":\"10.0.1.128/25\"\n},\n{\n\"cidr\":\"E81B:7AED:5B6C:CF67:16FF:5142:589F:D91B/120\"\n},\n{\n\"cidr\":\"149.219.141.0/24\",\n\"except\":[\n\"149.219.141.128/25\",\n\"149.219.141.176/28\",\n\"149.219.141.153/29\"\n]\n}\n],\n\"ports\":[\n{\n\"protocol\":\"any\"\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"2001:db8::89/119\"\n},\n{\n\"cidr\":\"10.0.0.0/24\"\n},\n{\n\"cidr\":\"10.0.2.128/26\"\n},\n{\n\"cidr\":\"10.0.0.0/22\"\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"10.0.1.0/24\"\n},\n{\n\"cidr\":\"10.0.1.11/32\"\n},\n{\n\"cidr\":\"2001:db8::191/122\"\n},\n{\n\"cidr\":\"10.0.3.133/26\"\n}\n],\n\"ports\":[\n{\n\"protocol\":\"icmp\"\n},\n{\n\"protocol\":\"tcp\"\n}\n]\n},\n{\n\"cidr\":\"2001:db8::200/119\",\n\"protocol\":\"any\",\n\"port\":442,\n\"endPort\":444\n}\n]\n}"}],"expected":{"error":"allow[2].to[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"random #84","input":[{"text":"{\"allow\": [], \"deny\": [{\"cidr\": \"176.0.0.0/4\", \"protocol\": \"any\"}, {\"to\": [{\"cidr\": \"224.205.16.60/031\"}]}, {\"to\": [{\"cidr\": \"10.0.3.0/26\"}, {\"cidr\": \"2001:db8::24/126\", \"except\": [\"2001:DB8::24/0126\", \"2001:db8::26/127\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.3.32/123\"}, {\"cidr\": \"10.0.1.0/24\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"any\", \"port\": 10}, {\"protocol\": \"tcp\", \"port\": 78, \"endPort\": 82}]}, {\"to\": [{\"cidr\": \"10.0.0.0/26\"}, {\"cidr\": \"10.0.1.0/24\", \"except\": []}, {\"cidr\": \"10.0.0.0/25\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 65534, \"endPort\": 65535}]}, {\"to\": [{\"cidr\": \"10.0.0.192/0026\"}, {\"cidr\": \"2001:DB8::390/125\"}]}, {\"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"any\", \"port\": 78, \"endPort\": 79}, {\"protocol\": \"tcp\", \"port\": 443, \"endPort\": 29025}, {\"port\": 65531}]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:000d\", \"protocol\": \"tcp\", \"port\": 79}, {\"ports\": [{\"port\": 11}, {\"protocol\": \"tcp\", \"port\": 79, \"endPort\": 85}]}]}"}],"expected":{"allow":[],"deny":["0.0.0.0/0:tcp:11","0.0.0.0/0:tcp:78-85","0.0.0.0/0:tcp:443-29025","0.0.0.0/0:tcp:65531","0.0.0.0/0:udp","10.0.0.192/26","10.0.1.0/24:icmp","10.0.1.0/24:tcp:10-11","10.0.3.0/26:icmp","10.0.3.0/26:tcp:10-11","176.0.0.0/4","224.205.16.60/31","::/0:tcp:11","::/0:tcp:78-85","::/0:tcp:443-29025","::/0:tcp:65531","::/0:udp","2001:db8::320/123:icmp","2001:db8::320/123:tcp:10-11","2001:db8::390/125"]}}, + {"name":"random #85","input":[{"text":"{\n\t\"allow\": [], \n\t\"deny\": []\n}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #86","input":[{"text":"{\"allow\": [{\"cidr\": \"128.0.0.0/4\", \"except\": [\"143.240.0.0/12\", \"142.0.0.0/7\"], \"protocol\": \"udp\", \"port\": 78, \"endPort\": 84}, {\"cidr\": \"a000:0000:0000:0000:0000:0000:0000:0000/3\", \"protocol\": \"any\", \"port\": 444, \"endPort\": 449}, {\"cidr\": \"2001:db8::/000118\", \"protocol\": \"tcp\"}], \"deny\": [{\"to\": [{\"cidr\": \"0000:0000:0000:0000:0000:ffff:0.0.0.0/96\"}, {\"cidr\": \"2001:db8::/118\", \"except\": [\"::/0\", \"2001:0db8:0000:0000:0000:0000:0.0.2.0/120\"]}], \"ports\": [{}, {\"protocol\": \"icmp\"}, {\"protocol\": \"any\", \"port\": 82, \"endPort\": 82}]}, {\"to\": [{\"cidr\": \"e0e6:d63a:823b:b849:761b:b624::/96\", \"except\": [\"e0e6:d63a:823b:b849:761b:b624:4000:0/101\", \"e0e6:d63a:823b:b849:761b:b624::/98\"]}, {\"cidr\": \"2001:db8::a/125\"}, {\"cidr\": \"10.0.2.91\"}, {\"cidr\": \"2001:db8::300/120\"}]}, {\"to\": [{\"cidr\": \"10.0.3.128/0.0.0.63\"}, {\"cidr\": \"2001:db8::200/120\"}, {\"cidr\": \"2001:db8::64/122\", \"except\": []}, {\"cidr\": \"10.0.0.96/27\"}], \"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"icmp\"}]}]}"}],"expected":{"error":"deny[0].to[1].except[0] must be contained in 2001:db8::/118"}}, + {"name":"random #87","input":[{"text":"{\n\t\"deny\": [\n\t\t{\n\t\t\t\"cidr\": \"218.161.246.185/31\",\n\t\t\t\"except\": [\n\t\t\t\t\"218.161.246.184/31\"\n\t\t\t],\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"192.0.2.0/24\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.0.0/8\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"0.0.0.0/2\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 79\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/255.255.252.0\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::100/0120\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0.0.1.232/125\",\n\t\t\t\t\t\t\"2001:db8::140/122\",\n\t\t\t\t\t\t\"2001:db8::1d0/126\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 65532\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::f8/00125\",\n\t\t\t\"except\": [\n\t\t\t\t\"2001:db8::fc/127\",\n\t\t\t\t\"2001:db8::fc/126\"\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"error":"deny[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #88","input":[{"text":"{\n\"allow\": [\n{\n\"cidr\": \"10.0.1.0/25\"\n}\n],\n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"2001:DB8::12A/118\",\n\"except\": [\n\"2001:DB8::100/123\",\n\"2001:DB8::360/121\"\n]\n},\n{\n\"cidr\": \"2001:db8::80/121\",\n\"except\": [\n\"2001:0db8:0000:0000:0000:0000:0000:00a0/126\"\n]\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::20c/124\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"udp\"\n},\n{\n\"port\": 444\n},\n{\n\"protocol\": \"any\",\n\"port\": 65533,\n\"endPort\": 65535\n},\n{\n\"protocol\": \"any\",\n\"port\": 443\n}\n]\n},\n{\n\"cidr\": \"10.0.3.215/28\",\n\"protocol\": \"tcp\",\n\"port\": 78,\n\"endPort\": 82\n},\n{\n\"cidr\": \"2001:db8::180/121\",\n\"protocol\": \"tcp\",\n\"port\": 445,\n\"endPort\": 449\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/00022\"\n},\n{\n\"cidr\": \"10.0.1.0/24\"\n},\n{\n\"cidr\": \"76.16.197.226/31\"\n},\n{\n\"cidr\": \"0.0.0.0/2\"\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::1c5/125\",\n\"except\": [\n\"2001:db8::1c1\",\n\"2001:db8::1c0/125\",\n\"2001:db8::1c1\"\n]\n},\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/121\"\n},\n{\n\"cidr\": \"10.0.0.0/24\"\n},\n{\n\"cidr\": \"::/3\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"tcp\",\n\"port\": 8,\n\"endPort\": 12\n},\n{\n\"protocol\": \"udp\"\n}\n]\n},\n{\n\"cidr\": \"2001:db8::388/128\",\n\"protocol\": \"any\",\n\"port\": 315,\n\"endPort\": 317\n},\n{\n\"cidr\": \"10.0.2.48/30\",\n\"protocol\": \"tcp\",\n\"port\": 303,\n\"endPort\": 303\n},\n{\n\"cidr\": \"10.0.1.204/31\",\n\"protocol\": \"udp\",\n\"port\": 221\n},\n{\n\"cidr\": \"10.0.3.158/31\",\n\"protocol\": \"tcp\",\n\"port\": 163\n},\n{\n\"cidr\": \"10.0.0.28/32\",\n\"protocol\": \"udp\",\n\"port\": 217\n},\n{\n\"cidr\": \"10.0.1.252/31\",\n\"protocol\": \"tcp\",\n\"port\": 125,\n\"endPort\": 129\n},\n{\n\"cidr\": \"10.0.1.132/31\",\n\"protocol\": \"tcp\",\n\"port\": 105,\n\"endPort\": 109\n},\n{\n\"cidr\": \"10.0.3.198/31\",\n\"protocol\": \"any\",\n\"port\": 151\n},\n{\n\"cidr\": \"10.0.1.100/30\",\n\"protocol\": \"any\",\n\"port\": 199\n},\n{\n\"cidr\": \"10.0.2.154/31\",\n\"protocol\": \"any\",\n\"port\": 111\n},\n{\n\"cidr\": \"10.0.1.196/30\",\n\"protocol\": \"tcp\",\n\"port\": 199\n},\n{\n\"cidr\": \"10.0.1.36/30\",\n\"protocol\": \"tcp\",\n\"port\": 225,\n\"endPort\": 227\n},\n{\n\"cidr\": \"10.0.1.0/31\",\n\"protocol\": \"udp\",\n\"port\": 385,\n\"endPort\": 387\n},\n{\n\"cidr\": \"10.0.2.56/30\",\n\"protocol\": \"udp\",\n\"port\": 333\n},\n{\n\"cidr\": \"2001:db8::d0/128\",\n\"protocol\": \"any\",\n\"port\": 387,\n\"endPort\": 390\n},\n{\n\"cidr\": \"10.0.3.46/31\",\n\"protocol\": \"udp\",\n\"port\": 131\n},\n{\n\"cidr\": \"10.0.2.144/30\",\n\"protocol\": \"tcp\",\n\"port\": 371\n},\n{\n\"cidr\": \"10.0.0.134/32\",\n\"protocol\": \"udp\",\n\"port\": 243,\n\"endPort\": 246\n},\n{\n\"cidr\": \"10.0.0.56/31\",\n\"protocol\": \"any\",\n\"port\": 19\n},\n{\n\"cidr\": \"10.0.3.168/30\",\n\"protocol\": \"any\",\n\"port\": 5\n},\n{\n\"cidr\": \"2001:db8::1c3/128\",\n\"protocol\": \"any\",\n\"port\": 395\n},\n{\n\"cidr\": \"10.0.1.212/31\",\n\"protocol\": \"any\",\n\"port\": 371\n},\n{\n\"cidr\": \"10.0.3.132/30\",\n\"protocol\": \"udp\",\n\"port\": 379\n},\n{\n\"cidr\": \"10.0.0.174/32\",\n\"protocol\": \"tcp\",\n\"port\": 21\n},\n{\n\"cidr\": \"10.0.3.174/31\",\n\"protocol\": \"any\",\n\"port\": 107,\n\"endPort\": 107\n},\n{\n\"cidr\": \"10.0.1.144/31\",\n\"protocol\": \"tcp\",\n\"port\": 255,\n\"endPort\": 257\n},\n{\n\"cidr\": \"2001:db8::da/128\",\n\"protocol\": \"any\",\n\"port\": 279\n},\n{\n\"cidr\": \"10.0.0.96/30\",\n\"protocol\": \"tcp\",\n\"port\": 85,\n\"endPort\": 88\n},\n{\n\"cidr\": \"10.0.3.160/30\",\n\"protocol\": \"udp\",\n\"port\": 103\n},\n{\n\"cidr\": \"10.0.2.74/31\",\n\"protocol\": \"any\",\n\"port\": 379\n},\n{\n\"cidr\": \"10.0.1.96/30\",\n\"protocol\": \"any\",\n\"port\": 247,\n\"endPort\": 248\n},\n{\n\"cidr\": \"2001:db8::183/128\",\n\"protocol\": \"tcp\",\n\"port\": 243,\n\"endPort\": 244\n},\n{\n\"cidr\": \"2001:db8::17/128\",\n\"protocol\": \"any\",\n\"port\": 355\n},\n{\n\"cidr\": \"2001:db8::120/128\",\n\"protocol\": \"udp\",\n\"port\": 209\n},\n{\n\"cidr\": \"2001:db8::1ab/128\",\n\"protocol\": \"any\",\n\"port\": 339\n},\n{\n\"cidr\": \"2001:db8::24c/128\",\n\"protocol\": \"tcp\",\n\"port\": 315,\n\"endPort\": 317\n},\n{\n\"cidr\": \"10.0.0.152/32\",\n\"protocol\": \"udp\",\n\"port\": 201\n},\n{\n\"cidr\": \"2001:db8::369/128\",\n\"protocol\": \"any\",\n\"port\": 155\n},\n{\n\"cidr\": \"2001:db8::242/128\",\n\"protocol\": \"tcp\",\n\"port\": 25\n},\n{\n\"cidr\": \"2001:db8::2e0/128\",\n\"protocol\": \"any\",\n\"port\": 223\n},\n{\n\"cidr\": \"2001:db8::2d9/128\",\n\"protocol\": \"tcp\",\n\"port\": 381,\n\"endPort\": 384\n},\n{\n\"cidr\": \"10.0.1.35/32\",\n\"protocol\": \"udp\",\n\"port\": 15\n},\n{\n\"cidr\": \"2001:db8::262/128\",\n\"protocol\": \"tcp\",\n\"port\": 335,\n\"endPort\": 339\n},\n{\n\"cidr\": \"10.0.1.132/30\",\n\"protocol\": \"udp\",\n\"port\": 95,\n\"endPort\": 99\n},\n{\n\"cidr\": \"10.0.3.216/30\",\n\"protocol\": \"udp\",\n\"port\": 199\n},\n{\n\"cidr\": \"10.0.2.79/32\",\n\"protocol\": \"udp\",\n\"port\": 139,\n\"endPort\": 140\n},\n{\n\"cidr\": \"10.0.2.162/31\",\n\"protocol\": \"udp\",\n\"port\": 397,\n\"endPort\": 401\n},\n{\n\"cidr\": \"10.0.1.80/30\",\n\"protocol\": \"tcp\",\n\"port\": 107,\n\"endPort\": 107\n},\n{\n\"cidr\": \"10.0.1.90/32\",\n\"protocol\": \"tcp\",\n\"port\": 277\n},\n{\n\"cidr\": \"2001:db8::54/128\",\n\"protocol\": \"any\",\n\"port\": 227\n},\n{\n\"cidr\": \"2001:db8::36e/128\",\n\"protocol\": \"any\",\n\"port\": 239,\n\"endPort\": 241\n},\n{\n\"cidr\": \"2001:db8::3b5/128\",\n\"protocol\": \"tcp\",\n\"port\": 347\n},\n{\n\"cidr\": \"10.0.2.46/31\",\n\"protocol\": \"udp\",\n\"port\": 33\n},\n{\n\"cidr\": \"10.0.3.145/32\",\n\"protocol\": \"tcp\",\n\"port\": 209\n},\n{\n\"cidr\": \"10.0.3.249/32\",\n\"protocol\": \"any\",\n\"port\": 381\n},\n{\n\"cidr\": \"10.0.1.168/30\",\n\"protocol\": \"tcp\",\n\"port\": 351\n},\n{\n\"cidr\": \"10.0.0.26/31\",\n\"protocol\": \"any\",\n\"port\": 381\n},\n{\n\"cidr\": \"10.0.1.208/30\",\n\"protocol\": \"any\",\n\"port\": 317\n},\n{\n\"cidr\": \"10.0.3.216/31\",\n\"protocol\": \"any\",\n\"port\": 363,\n\"endPort\": 366\n},\n{\n\"cidr\": \"10.0.3.16/30\",\n\"protocol\": \"any\",\n\"port\": 41\n},\n{\n\"cidr\": \"10.0.2.108/30\",\n\"protocol\": \"tcp\",\n\"port\": 341,\n\"endPort\": 344\n},\n{\n\"cidr\": \"10.0.3.248/30\",\n\"protocol\": \"udp\",\n\"port\": 145,\n\"endPort\": 146\n},\n{\n\"cidr\": \"10.0.3.176/31\",\n\"protocol\": \"any\",\n\"port\": 289\n},\n{\n\"cidr\": \"10.0.0.179/32\",\n\"protocol\": \"tcp\",\n\"port\": 247,\n\"endPort\": 250\n},\n{\n\"cidr\": \"10.0.0.128/30\",\n\"protocol\": \"udp\",\n\"port\": 243\n},\n{\n\"cidr\": \"10.0.1.207/32\",\n\"protocol\": \"udp\",\n\"port\": 315\n},\n{\n\"cidr\": \"10.0.1.242/31\",\n\"protocol\": \"any\",\n\"port\": 245,\n\"endPort\": 245\n},\n{\n\"cidr\": \"2001:db8::11a/128\",\n\"protocol\": \"udp\",\n\"port\": 305\n},\n{\n\"cidr\": \"10.0.1.232/30\",\n\"protocol\": \"tcp\",\n\"port\": 167,\n\"endPort\": 170\n},\n{\n\"cidr\": \"10.0.1.221/32\",\n\"protocol\": \"udp\",\n\"port\": 371\n},\n{\n\"cidr\": \"2001:db8::32e/128\",\n\"protocol\": \"tcp\",\n\"port\": 237\n},\n{\n\"cidr\": \"10.0.1.92/31\",\n\"protocol\": \"udp\",\n\"port\": 319\n},\n{\n\"cidr\": \"2001:db8::140/128\",\n\"protocol\": \"any\",\n\"port\": 163\n},\n{\n\"cidr\": \"10.0.0.200/30\",\n\"protocol\": \"tcp\",\n\"port\": 25\n},\n{\n\"cidr\": \"2001:db8::15c/128\",\n\"protocol\": \"udp\",\n\"port\": 101,\n\"endPort\": 102\n},\n{\n\"cidr\": \"10.0.1.104/30\",\n\"protocol\": \"any\",\n\"port\": 141\n},\n{\n\"cidr\": \"10.0.1.216/31\",\n\"protocol\": \"udp\",\n\"port\": 275\n},\n{\n\"cidr\": \"10.0.1.14/31\",\n\"protocol\": \"udp\",\n\"port\": 301\n},\n{\n\"cidr\": \"2001:db8::245/128\",\n\"protocol\": \"tcp\",\n\"port\": 135\n},\n{\n\"cidr\": \"10.0.3.184/30\",\n\"protocol\": \"udp\",\n\"port\": 239,\n\"endPort\": 243\n},\n{\n\"cidr\": \"10.0.2.10/32\",\n\"protocol\": \"udp\",\n\"port\": 95,\n\"endPort\": 98\n},\n{\n\"cidr\": \"2001:db8::23e/128\",\n\"protocol\": \"tcp\",\n\"port\": 43\n},\n{\n\"cidr\": \"2001:db8::1aa/128\",\n\"protocol\": \"tcp\",\n\"port\": 361\n},\n{\n\"cidr\": \"10.0.1.81/32\",\n\"protocol\": \"any\",\n\"port\": 83\n},\n{\n\"cidr\": \"10.0.1.92/31\",\n\"protocol\": \"udp\",\n\"port\": 45,\n\"endPort\": 48\n},\n{\n\"cidr\": \"10.0.3.168/30\",\n\"protocol\": \"any\",\n\"port\": 123\n},\n{\n\"cidr\": \"10.0.3.105/32\",\n\"protocol\": \"udp\",\n\"port\": 317\n},\n{\n\"cidr\": \"10.0.1.52/32\",\n\"protocol\": \"tcp\",\n\"port\": 375\n},\n{\n\"cidr\": \"2001:db8::3b1/128\",\n\"protocol\": \"udp\",\n\"port\": 271,\n\"endPort\": 272\n},\n{\n\"cidr\": \"10.0.1.212/30\",\n\"protocol\": \"udp\",\n\"port\": 215,\n\"endPort\": 218\n},\n{\n\"cidr\": \"10.0.1.18/31\",\n\"protocol\": \"tcp\",\n\"port\": 213\n},\n{\n\"cidr\": \"10.0.2.156/31\",\n\"protocol\": \"udp\",\n\"port\": 31\n},\n{\n\"cidr\": \"2001:db8::355/128\",\n\"protocol\": \"any\",\n\"port\": 217\n},\n{\n\"cidr\": \"2001:db8::4a/128\",\n\"protocol\": \"any\",\n\"port\": 333\n},\n{\n\"cidr\": \"10.0.1.10/32\",\n\"protocol\": \"any\",\n\"port\": 19\n},\n{\n\"cidr\": \"10.0.2.208/30\",\n\"protocol\": \"tcp\",\n\"port\": 393\n},\n{\n\"cidr\": \"10.0.1.40/31\",\n\"protocol\": \"udp\",\n\"port\": 159\n},\n{\n\"cidr\": \"10.0.0.230/31\",\n\"protocol\": \"any\",\n\"port\": 387\n},\n{\n\"cidr\": \"2001:db8::143/128\",\n\"protocol\": \"any\",\n\"port\": 201\n},\n{\n\"cidr\": \"10.0.0.172/30\",\n\"protocol\": \"udp\",\n\"port\": 183,\n\"endPort\": 183\n},\n{\n\"cidr\": \"2001:db8::25c/128\",\n\"protocol\": \"udp\",\n\"port\": 29\n},\n{\n\"cidr\": \"2001:db8::20e/128\",\n\"protocol\": \"tcp\",\n\"port\": 323\n},\n{\n\"cidr\": \"10.0.2.182/32\",\n\"protocol\": \"udp\",\n\"port\": 387\n},\n{\n\"cidr\": \"10.0.1.100/30\",\n\"protocol\": \"tcp\",\n\"port\": 275\n},\n{\n\"cidr\": \"10.0.3.248/31\",\n\"protocol\": \"tcp\",\n\"port\": 193\n},\n{\n\"cidr\": \"10.0.2.244/30\",\n\"protocol\": \"tcp\",\n\"port\": 47\n},\n{\n\"cidr\": \"10.0.2.58/31\",\n\"protocol\": \"udp\",\n\"port\": 281\n},\n{\n\"cidr\": \"10.0.0.247/32\",\n\"protocol\": \"any\",\n\"port\": 165\n},\n{\n\"cidr\": \"2001:db8::22d/128\",\n\"protocol\": \"tcp\",\n\"port\": 283\n},\n{\n\"cidr\": \"10.0.0.218/31\",\n\"protocol\": \"udp\",\n\"port\": 203\n},\n{\n\"cidr\": \"2001:db8::63/128\",\n\"protocol\": \"udp\",\n\"port\": 303,\n\"endPort\": 303\n},\n{\n\"cidr\": \"10.0.0.112/32\",\n\"protocol\": \"any\",\n\"port\": 389\n},\n{\n\"cidr\": \"10.0.1.228/30\",\n\"protocol\": \"any\",\n\"port\": 311\n},\n{\n\"cidr\": \"10.0.2.48/32\",\n\"protocol\": \"any\",\n\"port\": 255\n},\n{\n\"cidr\": \"2001:db8::100/128\",\n\"protocol\": \"any\",\n\"port\": 97\n},\n{\n\"cidr\": \"10.0.0.95/32\",\n\"protocol\": \"any\",\n\"port\": 319\n},\n{\n\"cidr\": \"2001:db8::340/128\",\n\"protocol\": \"any\",\n\"port\": 229\n},\n{\n\"cidr\": \"2001:db8::155/128\",\n\"protocol\": \"tcp\",\n\"port\": 333\n},\n{\n\"cidr\": \"10.0.1.104/30\",\n\"protocol\": \"any\",\n\"port\": 341,\n\"endPort\": 342\n},\n{\n\"cidr\": \"10.0.1.235/32\",\n\"protocol\": \"udp\",\n\"port\": 103\n},\n{\n\"cidr\": \"10.0.2.204/30\",\n\"protocol\": \"any\",\n\"port\": 205\n},\n{\n\"cidr\": \"2001:db8::1d7/128\",\n\"protocol\": \"tcp\",\n\"port\": 249,\n\"endPort\": 250\n},\n{\n\"cidr\": \"10.0.1.243/32\",\n\"protocol\": \"tcp\",\n\"port\": 33\n},\n{\n\"cidr\": \"10.0.3.146/31\",\n\"protocol\": \"any\",\n\"port\": 163\n},\n{\n\"cidr\": \"10.0.3.92/30\",\n\"protocol\": \"tcp\",\n\"port\": 167\n},\n{\n\"cidr\": \"10.0.1.14/32\",\n\"protocol\": \"any\",\n\"port\": 325,\n\"endPort\": 328\n},\n{\n\"cidr\": \"2001:db8::16a/128\",\n\"protocol\": \"udp\",\n\"port\": 195,\n\"endPort\": 196\n},\n{\n\"cidr\": \"2001:db8::77/128\",\n\"protocol\": \"any\",\n\"port\": 269\n},\n{\n\"cidr\": \"10.0.2.44/30\",\n\"protocol\": \"udp\",\n\"port\": 279\n},\n{\n\"cidr\": \"10.0.0.246/31\",\n\"protocol\": \"tcp\",\n\"port\": 213\n},\n{\n\"cidr\": \"10.0.2.204/31\",\n\"protocol\": \"udp\",\n\"port\": 395\n},\n{\n\"cidr\": \"2001:db8::5e/128\",\n\"protocol\": \"tcp\",\n\"port\": 117,\n\"endPort\": 117\n},\n{\n\"cidr\": \"10.0.1.76/31\",\n\"protocol\": \"any\",\n\"port\": 331\n},\n{\n\"cidr\": \"2001:db8::3d4/128\",\n\"protocol\": \"tcp\",\n\"port\": 191\n},\n{\n\"cidr\": \"2001:db8::95/128\",\n\"protocol\": \"any\",\n\"port\": 93\n}\n]\n}"}],"expected":{"allow":["10.0.1.0/25"],"deny":["0.0.0.0/2","76.16.197.226/31","::/3:tcp:8-12","::/3:udp","2001:db8::/120","2001:db8::100/128:tcp:97","2001:db8::100/128:udp:97","2001:db8::11a/128:udp:305","2001:db8::120/123","2001:db8::140/122","2001:db8::180/121","2001:db8::200/120","2001:db8::32e/128:tcp:237","2001:db8::340/128:tcp:229","2001:db8::340/128:udp:229","2001:db8::355/128:tcp:217","2001:db8::355/128:udp:217","2001:db8::369/128:tcp:155","2001:db8::369/128:udp:155","2001:db8::36e/128:tcp:239-241","2001:db8::36e/128:udp:239-241","2001:db8::380/121"]}}, + {"name":"random #89","input":[{"text":"{\n\"allow\": [\n{\n\"cidr\": \"0.0.0.0/0\",\n\"protocol\": \"tcp\",\n\"port\": 81\n},\n{\n\"to\": [\n{\n\"cidr\": \"4d60:752c:80c1:1d30:1e45:a4b1:f988:ea12/127\",\n\"except\": []\n},\n{\n\"cidr\": \"2001:db8::3b8/125\",\n\"except\": [\n\"2001:0db8:0000:0000:0000:0000:0000:03be/127\",\n\"2001:db8::3be/127\",\n\"2001:0db8:0000:0000:0000:0000:0.0.3.188/000127\"\n]\n},\n{\n\"cidr\": \"2001:db8::200/119\"\n},\n{\n\"cidr\": \"2001:db8::2c8\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"any\",\n\"port\": 82\n},\n{\n\"protocol\": \"udp\",\n\"port\": 65535\n}\n]\n},\n{\n\"cidr\": \"10.0.0.96/27\",\n\"protocol\": \"tcp\",\n\"port\": 81,\n\"endPort\": 85\n}\n],\n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0300/125\",\n\"except\": [\n\"2001:db8::307\",\n\"2001:db8::301/128\"\n]\n},\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0100/120\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"tcp\",\n\"port\": 65534\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"128.0.0.0/01\"\n}\n]\n}\n]\n}"}],"expected":{"allow":["0.0.0.0/0:tcp:81","10.0.0.96/27:tcp:82-85","2001:db8::200/119:tcp:82","2001:db8::200/119:udp:82","2001:db8::200/119:udp:65535","4d60:752c:80c1:1d30:1e45:a4b1:f988:ea12/127:tcp:82","4d60:752c:80c1:1d30:1e45:a4b1:f988:ea12/127:udp:82","4d60:752c:80c1:1d30:1e45:a4b1:f988:ea12/127:udp:65535"],"deny":["128.0.0.0/1","2001:db8::100/120:tcp:65534","2001:db8::300/128:tcp:65534","2001:db8::302/127:tcp:65534","2001:db8::304/127:tcp:65534","2001:db8::306/128:tcp:65534"]}}, + {"name":"random #90","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"port\":65532\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":444\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65530\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":65531,\n\t\t\t\t\t\"endPort\":65535\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.1.128/27\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.230/31\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.32/255.255.255.224\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.0/255.255.255.254\",\n\t\t\t\"except\":[]\n\t\t},\n\t\t{\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":80,\n\t\t\t\t\t\"endPort\":80\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":82\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65534,\n\t\t\t\t\t\"endPort\":65534\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":79,\n\t\t\t\t\t\"endPort\":85\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.0/24\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.80/24\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.122/022\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::198/125\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.0.0/22\",\n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:019c/126\",\n\t\t\t\t\t\t\"2001:db8::198/127\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":78\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:DB8::2BC/127\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"2001:DB8::2BC/127\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::190\",\n\t\t\t\"except\":[\n\t\t\t\t\"2001:db8::190/128\"\n\t\t\t],\n\t\t\t\"protocol\":\"tcp\"\n\t\t}\n\t]\n}"}],"expected":{"error":"allow[5].to[3].except[0] must be contained in 2001:db8::198/125"}}, + {"name":"random #91","input":[{"text":"{\"allow\":[],\"deny\":[{\"to\":[{\"cidr\":\"10.0.3.86/26\"}]},{\"to\":[{\"cidr\":\"10.0.3.30/31\"},{\"cidr\":\"c291:3f54:4979:88b:61db:d08f:ade8:591f/64\"},{\"cidr\":\"205.19.0.0/16\"}],\"ports\":[{\"protocol\":\"any\"},{\"protocol\":\"udp\"},{\"protocol\":\"any\",\"port\":65530,\"endPort\":65531}]},{\"cidr\":\"10.0.0.192/26\",\"protocol\":\"udp\"},{\"cidr\":\"10.0.0.32/27\",\"protocol\":\"TCP\"}]}"}],"expected":{"error":"deny[3].protocol must be tcp, udp, icmp, or any"}}, + {"name":"random #92","input":[{"text":"{\"allow\": [], \"deny\": [{\"cidr\": \"2001:db8::f0/000126\", \"protocol\": \"udp\"}, {\"cidr\": \"10.0.0.128/26\", \"except\": [\"10.0.0.189/29\", \"10.0.0.128/27\"], \"protocol\": \"any\", \"port\": 11}]}"}],"expected":{"allow":[],"deny":["10.0.0.160/28:tcp:11","10.0.0.160/28:udp:11","10.0.0.176/29:tcp:11","10.0.0.176/29:udp:11","2001:db8::f0/126:udp"]}}, + {"name":"random #93","input":[{"text":"{\n \"allow\":[\n {\n \"cidr\":\"10.0.0.0/22\",\n \"protocol\":\"any\",\n \"ports\":[]\n }\n ],\n \"deny\":[\n {\n \"cidr\":\"145.253.164.154/31\",\n \"protocol\":\"icmp\"\n },\n {\n \"to\":[\n {\n \"cidr\":\"10.0.0.168/255.255.255.252\",\n \"except\":[\n \"10.0.0.168/31\",\n \"10.0.0.168/30\",\n \"10.0.0.171/0.0.0.0\"\n ]\n },\n {\n \"cidr\":\"10.0.3.0/25\",\n \"except\":[\n \"10.0.3.12/31\",\n \"10.0.3.0/255.255.255.128\",\n \"10.0.3.64/26\"\n ]\n },\n {\n \"cidr\":\"10.0.0.0/22\"\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"tcp\"\n },\n {\n \"protocol\":\"any\",\n \"port\":78,\n \"endPort\":79\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"2001:db8::280/121\"\n },\n {\n \"cidr\":\"10.0.2.0/23\"\n },\n {\n \"cidr\":\"767c:759:5355:cd44::/64\"\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"any\",\n \"port\":444,\n \"endPort\":447\n },\n {\n \"port\":9\n },\n {\n \"protocol\":\"udp\",\n \"port\":442,\n \"endPort\":32197\n }\n ]\n }\n ]\n}"}],"expected":{"error":"allow[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #94","input":[{"text":"{\"allow\":[{\"ports\":[{\"port\":79,\"endPort\":82},{\"protocol\":\"tcp\",\"port\":81,\"endPort\":58590}]},{\"cidr\":\"2001:db8::62/118\",\"except\":[\"2001:db8::/118\"],\"protocol\":\"udp\",\"port\":442,\"endPort\":442},{\"to\":[{\"cidr\":\"10.0.2.0/0.0.0.127\"}]}],\"deny\":[{\"to\":[{\"cidr\":\"10.0.0.160/28\"}],\"ports\":[{\"protocol\":\"any\",\"port\":79},{\"protocol\":\"any\",\"port\":82,\"endPort\":82}]},{\"to\":[{\"cidr\":\"240.161.235.0/24\"},{\"cidr\":\"10.0.3.200/30\"}],\"ports\":[{\"protocol\":\"udp\"},{\"protocol\":\"udp\",\"port\":8,\"endPort\":13},{\"protocol\":\"udp\",\"port\":7}]},{\"cidr\":\"10.0.3.160/27\",\"except\":[\"10.0.3.176/28\",\"10.0.3.184/29\"],\"protocol\":\"udp\",\"port\":11},{\"cidr\":\"2001:db8::200/0120\",\"protocol\":\"udp\",\"port\":2,\"endPort\":4},{\"to\":[{\"cidr\":\"2001:DB8::37C/126\"},{\"cidr\":\"10.0.3.116/30\"},{\"cidr\":\"10.0.3.197/0029\"}],\"ports\":[{},{\"protocol\":\"udp\",\"port\":65531},{\"protocol\":\"udp\",\"port\":7},{\"port\":80,\"endPort\":84}]},{\"cidr\":\"10.0.0.0/28\"},{\"cidr\":\"10.0.0.33/22\",\"except\":[],\"protocol\":\"udp\"},{\"cidr\":\"8396:9384:b06f:f995:1035:f398::/96\",\"protocol\":\"any\",\"port\":78,\"endPort\":80}]}"}],"expected":{"allow":["0.0.0.0/0:tcp:79-58590","0.0.0.0/0:udp:79-82","10.0.2.0/25","::/0:tcp:79-58590","::/0:udp:79-82"],"deny":["10.0.0.0/22:udp","10.0.0.0/28","10.0.0.160/28:tcp:79","10.0.0.160/28:tcp:82","10.0.3.116/30","10.0.3.192/29","240.161.235.0/24:udp","2001:db8::200/120:udp:2-4","2001:db8::37c/126","8396:9384:b06f:f995:1035:f398::/96:tcp:78-80","8396:9384:b06f:f995:1035:f398::/96:udp:78-80"]}}, + {"name":"random #95","input":[{"text":"{\n\"allow\": [\n{\n\"cidr\": \"10.0.2.64/255.255.255.192\", \n\"protocol\": \"tcp\", \n\"port\": 441, \n\"ports\": [\n{\n\"protocol\": \"icmp\", \n\"port\": 1\n}\n]\n}\n], \n\"deny\": []\n}"}],"expected":{"error":"allow[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #96","input":[{"text":"{\"allow\": [{\"ports\": [{\"port\": 82, \"endPort\": 83}, {\"port\": 9, \"endPort\": 14}, {\"protocol\": \"any\", \"port\": 81, \"endPort\": 81}]}, {\"cidr\": \"10.0.1.20/31\", \"protocol\": \"tcp\", \"port\": 3}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0042/127\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:0043\", \"2001:db8::43\"]}, {\"cidr\": \"200.0.0.0/008\", \"except\": [\"200.219.254.158/13\", \"200.0.0.0/8\"]}, {\"cidr\": \"10.0.1.250/27\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 440}, {\"port\": 65530}, {\"protocol\": \"tcp\"}, {\"protocol\": \"any\", \"port\": 3, \"endPort\": 15387}]}, {\"ports\": [{\"protocol\": \"tcp\"}]}], \"deny\": []}"}],"expected":{"allow":["0.0.0.0/0:tcp","0.0.0.0/0:udp:9-14","0.0.0.0/0:udp:81-83","10.0.1.224/27:udp:3-15387","10.0.1.224/27:udp:65530","::/0:tcp","::/0:udp:9-14","::/0:udp:81-83","2001:db8::42/128:udp:3-15387","2001:db8::42/128:udp:65530"],"deny":[]}}, + {"name":"random #97","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"10.0.2.85/23\"},{\"cidr\":\"2001:db8::95/118\",\"except\":[\"2001:0db8:0000:0000:0000:0000:0000:0108/126\",\"2001:db8::1a0/124\",\"2001:db8::ba/125\"]},{\"cidr\":\"2001:db8::260/123\",\"except\":[\"2001:db8::278/125\",\"2001:db8::260/124\",\"2001:db8::267/126\"]},{\"cidr\":\"2001:db8::100/121\",\"except\":[\"2001:db8::160/123\",\"2001:db8::154/127\"]}],\"ports\":[{\"protocol\":\"icmp\"},{\"protocol\":\"udp\",\"port\":443,\"endPort\":447}]},{\"to\":[{\"ci"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"random #98","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"9c1b:f9f4:ef87:a197:7624:2a0:b280:cf92/104\"},{\"cidr\":\"2001:db8::30/124\"},{\"cidr\":\"10.0.3.0/255.255.255.0\"},{\"cidr\":\"2001:db8::/118\"}],\"ports\":[{\"protocol\":\"any\"}]},{\"cidr\":\"10.0.2.176/28\",\"protocol\":\"tcp\",\"port\":440,\"endPort\":446},{\"protocol\":\"icmp\"}],\"deny\":[{\"to\":[{\"cidr\":\"10.0.2.73/32\"}],\"ports\":[{\"port\":1,\"endPort\":7},{\"protocol\":\"icmp\"}]},{\"to\":[{\"cidr\":\"10.0.3.192/26\",\"except\":[]},{\"cidr\":\"b233:a14e:5048:8ca4:0000:0000:0000:0000/64\",\"except\":[\"b233:a14e:5048:8ca4::/0064\",\"b233:a14e:5048:8ca4:6800::/69\"]},{\"cidr\":\"2001:db8::200/119\"},{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0.0.1.226/127\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":65530,\"endPort\":65533},{\"protocol\":\"udp\",\"port\":65532,\"endPort\":65535},{\"protocol\":\"tcp\",\"port\":445,\"endPort\":451}]},{\"to\":[{\"cidr\":\"0.0.0.0/0\",\"except\":[]},{\"cidr\":\"10.0.0.192/26\"}]},{\"to\":[{\"cidr\":\"10.0.2.0/24\"},{\"cidr\":\"10.0.3.29/29\",\"except\":[\"10.0.3.28/31\"]},{\"cidr\":\"10.0.3.192/27\"}],\"ports\":[{\"port\":79,\"endPort\":81},{\"protocol\":\"tcp\",\"port\":65531,\"endPort\":65535}]},{\"to\":[{\"cidr\":\"10.0.2.161/27\"},{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0040/123\"},{\"cidr\":\"10.0.3.209/25\",\"except\":[\"10.0.3.250/31\",\"10.0.3.157/26\"]},{\"cidr\":\"44.147.211.46/255.255.255.254\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":65532,\"endPort\":65533},{\"protocol\":\"udp\"}]},{\"to\":[{\"cidr\":\"c802:c7cc:d8:a487:3515:7fde:219b:1227/3\",\"except\":[]}],\"ports\":[{\"protocol\":\"udp\"}]},{\"to\":[{\"cidr\":\"224.0.0.0/240.0.0.0\",\"except\":[]},{\"cidr\":\"10.0.1.220/26\",\"except\":[\"10.0.1.197\",\"10.0.1.236\",\"10.0.1.208/255.255.255.192\"]},{\"cidr\":\"2001:DB8::300/121\",\"except\":[\"2001:0db8:0000:0000:0000:0000:0000:0320/123\",\"2001:db8::340/122\"]}],\"ports\":[{\"protocol\":\"tcp\",\"port\":10,\"endPort\":14},{\"protocol\":\"any\"},{\"protocol\":\"any\",\"port\":11,\"endPort\":11}]},{}]}"}],"expected":{"error":"allow[2].cidr is required"}}, + {"name":"random #99","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1a0/123\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 9\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"159.221.32.166/24\",\n\t\t\t\t\t\"except\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.19/30\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.136/28\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"port\": 440\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 443,\n\t\t\t\t\t\"endPort\": 448\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 80\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::40/123\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"40c6:ef4f:34e2:54b2:2871:a0ea:1a00:0/104\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"192.0.0.0/2\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.20/0127\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 65531\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.119/22\",\n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 8\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::320/123\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"b2dc:4245:2daa:9303:d1dd:b79:a68e:9dd7/127\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/22\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.1.160/28\",\n\t\t\t\t\t\t\"10.0.1.234/26\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.88/29\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.194/31\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.2.195\",\n\t\t\t\t\t\t\"10.0.2.194/32\",\n\t\t\t\t\t\t\"10.0.2.195/32\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 65531\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":["10.0.2.16/30:tcp:80","10.0.2.16/30:tcp:440","10.0.2.16/30:tcp:443-448","10.0.2.16/30:udp","10.0.2.128/28:tcp:80","10.0.2.128/28:tcp:440","10.0.2.128/28:tcp:443-448","10.0.2.128/28:udp","159.221.32.0/24:tcp:80","159.221.32.0/24:tcp:440","159.221.32.0/24:tcp:443-448","159.221.32.0/24:udp","192.0.0.0/2","2001:db8::14/127","2001:db8::40/123","2001:db8::1a0/123:tcp:9","40c6:ef4f:34e2:54b2:2871:a0ea:1a00:0/104"],"deny":["10.0.0.0/22","2001:db8::320/123","b2dc:4245:2daa:9303:d1dd:b79:a68e:9dd6/127"]}}, + {"name":"random #100","input":[{"text":"{\"allow\": [{\"cidr\": \"232.253.205.217/63.255.255.255\", \"protocol\": \"udp\"}, {\"cidr\": \"2001:db8::a6/127\", \"except\": [\"2001:db8::a7\", \"2001:db8::a6/127\", \"2001:0db8:0000:0000:0000:0000:0.0.0.167/128\"], \"protocol\": \"udp\"}]}"}],"expected":{"allow":["192.0.0.0/2:udp"],"deny":[]}}, + {"name":"random #101","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"216.132.128.102/0032\"}, {\"cidr\": \"2001:db8::3b1/123\"}, {\"cidr\": \"2001:db8::12e/124\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 9}, {\"protocol\": \"udp\", \"port\": 65535, \"endPort\": 65535}, {\"protocol\": \"tcp\"}, {\"protocol\": \"tcp\", \"port\": 65531, \"endPort\": 65532}]}, {\"to\": [{\"cidr\": \"2001:db8::180/122\"}, {\"cidr\": \"4727:c883:3177:b7d6:669e:8d94:bc84:50b8/127\"}, {\"cidr\": \"2001:db8::c0/123\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65535}]}]}"}],"expected":{"allow":[],"deny":["216.132.128.102/32:tcp","216.132.128.102/32:udp:65535","2001:db8::c0/123:tcp:65535","2001:db8::120/124:tcp","2001:db8::120/124:udp:65535","2001:db8::180/122:tcp:65535","2001:db8::3a0/123:tcp","2001:db8::3a0/123:udp:65535","4727:c883:3177:b7d6:669e:8d94:bc84:50b8/127:tcp:65535"]}}, + {"name":"random #102","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.1.1/32\", \"except\": [\"10.0.1.1\"], \"protocol\": \"udp\"}, {\"to\": [{\"cidr\": \"90.215.74.220/2\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 1}]}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0024/122\"}, {\"cidr\": \"171.0.0.0/8\", \"except\": [\"171.0.0.0/0011\", \"171.128.0.0/9\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 443}, {\"protocol\": \"icmp\", \"port\": 6, \"endPort\": 6}, {\"protocol\": \"udp\", \"port\": 2, \"endPort\": 4}]}, {\"cidr\": \"76.137.248.255/31\", \"protocol\": \"tcp\", \"port\": 78, \"endPort\": 81}], \"deny\": []}"}],"expected":{"error":"allow[2].ports[1].port is not supported with icmp"}}, + {"name":"random #103","input":[{"text":"{\"allow\": [], \"allow\": [{\"to\": [{\"cidr\": \"10.0.3.192/29\", \"except\": [\"10.0.3.192/29\"]}, {\"cidr\": \"10.0.0.122/22\"}]}, {\"to\": [{\"cidr\": \"2001:db8::1e/127\"}, {\"cidr\": \"10.0.3.0/255.255.255.0\", \"except\": []}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65534}, {\"protocol\": \"tcp\"}, {\"port\": 65533, \"endPort\": 65535}]}, {\"to\": [{\"cidr\": \"10.0.2.0/24\"}, {\"cidr\": \"5720:ce40:88ca:4d4a:23bf:9ace:32aa:d322/64\"}, {\"cidr\": \"10.0.0.0/255.255.254.0\", \"except\": [\"10.0.1.128/25\", \"10.0.1.177/27\", \"10.0.1.120/0031\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 443, \"endPort\": 449}, {\"protocol\": \"udp\", \"port\": 78}]}, {\"to\": [{\"cidr\": \"118.243.0.0/16\"}, {\"cidr\": \"2001:db8::1bb/0123\"}]}], \"deny\": [{\"to\": [{\"cidr\": \"64.0.0.0/192.0.0.0\"}], \"ports\": [{\"port\": 80}, {\"protocol\": \"tcp\", \"port\": 1}, {\"protocol\": \"tcp\", \"port\": 7, \"endPort\": 9}, {\"protocol\": \"tcp\", \"port\": 78, \"endPort\": 81}]}, {\"to\": [{\"cidr\": \"10.0.0.59/30\"}, {\"cidr\": \"2001:db8::/0118\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:0100/120\"]}], \"ports\": [{\"port\": 65531, \"endPort\": 65535}, {\"protocol\": \"icmp\"}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"2001:db8::/119\"}, {\"cidr\": \"10.0.0.0/23\"}]}, {\"cidr\": \"10.0.0.128/25\", \"protocol\": \"udp\", \"port\": 442, \"endPort\": 446}, {\"cidr\": \"2001:db8::74/128\", \"protocol\": \"udp\", \"port\": 267, \"endPort\": 268}, {\"cidr\": \"2001:db8::1ed/128\", \"protocol\": \"any\", \"port\": 135}, {\"cidr\": \"2001:db8::31e/128\", \"protocol\": \"udp\", \"port\": 395, \"endPort\": 399}, {\"cidr\": \"2001:db8::226/128\", \"protocol\": \"udp\", \"port\": 179}, {\"cidr\": \"2001:db8::3d5/128\", \"protocol\": \"tcp\", \"port\": 325}, {\"cidr\": \"10.0.1.53/32\", \"protocol\": \"tcp\", \"port\": 201}, {\"cidr\": \"10.0.0.244/30\", \"protocol\": \"udp\", \"port\": 305, \"endPort\": 306}, {\"cidr\": \"10.0.2.48/31\", \"protocol\": \"udp\", \"port\": 263}, {\"cidr\": \"10.0.0.208/31\", \"protocol\": \"udp\", \"port\": 399, \"endPort\": 402}, {\"cidr\": \"2001:db8::74/128\", \"protocol\": \"any\", \"port\": 305}, {\"cidr\": \"10.0.2.126/31\", \"protocol\": \"udp\", \"port\": 339}, {\"cidr\": \"10.0.0.150/31\", \"protocol\": \"any\", \"port\": 209, \"endPort\": 211}, {\"cidr\": \"2001:db8::35d/128\", \"protocol\": \"tcp\", \"port\": 237}, {\"cidr\": \"10.0.0.55/32\", \"protocol\": \"udp\", \"port\": 237}, {\"cidr\": \"2001:db8::1a3/128\", \"protocol\": \"udp\", \"port\": 385}, {\"cidr\": \"2001:db8::384/128\", \"protocol\": \"udp\", \"port\": 279}, {\"cidr\": \"10.0.3.2/32\", \"protocol\": \"udp\", \"port\": 113}, {\"cidr\": \"2001:db8::158/128\", \"protocol\": \"any\", \"port\": 175}, {\"cidr\": \"10.0.0.163/32\", \"protocol\": \"any\", \"port\": 79}, {\"cidr\": \"2001:db8::151/128\", \"protocol\": \"tcp\", \"port\": 11, \"endPort\": 13}, {\"cidr\": \"10.0.2.228/30\", \"protocol\": \"udp\", \"port\": 295}, {\"cidr\": \"10.0.0.178/32\", \"protocol\": \"udp\", \"port\": 379}, {\"cidr\": \"10.0.2.85/32\", \"protocol\": \"udp\", \"port\": 95}, {\"cidr\": \"10.0.1.195/32\", \"protocol\": \"any\", \"port\": 277}, {\"cidr\": \"10.0.3.218/31\", \"protocol\": \"tcp\", \"port\": 385, \"endPort\": 389}, {\"cidr\": \"10.0.0.190/31\", \"protocol\": \"udp\", \"port\": 375, \"endPort\": 375}, {\"cidr\": \"10.0.0.216/31\", \"protocol\": \"udp\", \"port\": 209}, {\"cidr\": \"10.0.0.189/32\", \"protocol\": \"tcp\", \"port\": 5}, {\"cidr\": \"10.0.2.26/31\", \"protocol\": \"any\", \"port\": 73}, {\"cidr\": \"10.0.0.2/31\", \"protocol\": \"udp\", \"port\": 43, \"endPort\": 44}, {\"cidr\": \"10.0.2.48/30\", \"protocol\": \"udp\", \"port\": 341}, {\"cidr\": \"2001:db8::33/128\", \"protocol\": \"any\", \"port\": 67}, {\"cidr\": \"10.0.3.16/31\", \"protocol\": \"tcp\", \"port\": 213}, {\"cidr\": \"10.0.3.222/31\", \"protocol\": \"udp\", \"port\": 25, \"endPort\": 27}, {\"cidr\": \"10.0.0.188/31\", \"protocol\": \"any\", \"port\": 251, \"endPort\": 254}, {\"cidr\": \"10.0.2.80/30\", \"protocol\": \"udp\", \"port\": 213, \"endPort\": 214}, {\"cidr\": \"10.0.0.240/31\", \"protocol\": \"tcp\", \"port\": 69}, {\"cidr\": \"10.0.3.252/30\", \"protocol\": \"any\", \"port\": 313}, {\"cidr\": \"2001:db8::bd/128\", \"protocol\": \"any\", \"port\": 113}, {\"cidr\": \"10.0.2.116/31\", \"protocol\": \"udp\", \"port\": 17, \"endPort\": 19}, {\"cidr\": \"10.0.2.109/32\", \"protocol\": \"tcp\", \"port\": 63}, {\"cidr\": \"10.0.3.107/32\", \"protocol\": \"udp\", \"port\": 381}, {\"cidr\": \"2001:db8::2b4/128\", \"protocol\": \"any\", \"port\": 165}, {\"cidr\": \"10.0.3.192/30\", \"protocol\": \"any\", \"port\": 351}, {\"cidr\": \"10.0.2.16/30\", \"protocol\": \"tcp\", \"port\": 357}, {\"cidr\": \"2001:db8::51/128\", \"protocol\": \"udp\", \"port\": 187}, {\"cidr\": \"10.0.3.191/32\", \"protocol\": \"any\", \"port\": 201}, {\"cidr\": \"2001:db8::389/128\", \"protocol\": \"udp\", \"port\": 385, \"endPort\": 385}, {\"cidr\": \"10.0.3.248/30\", \"protocol\": \"udp\", \"port\": 255}, {\"cidr\": \"2001:db8::21c/128\", \"protocol\": \"udp\", \"port\": 273}, {\"cidr\": \"10.0.3.192/32\", \"protocol\": \"tcp\", \"port\": 335, \"endPort\": 338}, {\"cidr\": \"2001:db8::3fa/128\", \"protocol\": \"tcp\", \"port\": 7}, {\"cidr\": \"2001:db8::3eb/128\", \"protocol\": \"any\", \"port\": 109}, {\"cidr\": \"10.0.0.168/30\", \"protocol\": \"tcp\", \"port\": 175}, {\"cidr\": \"10.0.3.212/30\", \"protocol\": \"udp\", \"port\": 79}, {\"cidr\": \"10.0.2.126/31\", \"protocol\": \"udp\", \"port\": 193, \"endPort\": 193}, {\"cidr\": \"10.0.1.46/31\", \"protocol\": \"any\", \"port\": 369}, {\"cidr\": \"10.0.2.0/31\", \"protocol\": \"tcp\", \"port\": 355, \"endPort\": 357}, {\"cidr\": \"2001:db8::266/128\", \"protocol\": \"tcp\", \"port\": 341}, {\"cidr\": \"10.0.0.138/31\", \"protocol\": \"tcp\", \"port\": 65}, {\"cidr\": \"10.0.2.246/32\", \"protocol\": \"udp\", \"port\": 33}, {\"cidr\": \"10.0.3.84/30\", \"protocol\": \"any\", \"port\": 151}, {\"cidr\": \"2001:db8::12c/128\", \"protocol\": \"tcp\", \"port\": 327}, {\"cidr\": \"10.0.0.18/31\", \"protocol\": \"any\", \"port\": 169}, {\"cidr\": \"2001:db8::100/128\", \"protocol\": \"udp\", \"port\": 99}, {\"cidr\": \"10.0.2.198/31\", \"protocol\": \"tcp\", \"port\": 263}, {\"cidr\": \"10.0.0.7/32\", \"protocol\": \"udp\", \"port\": 85}, {\"cidr\": \"10.0.1.129/32\", \"protocol\": \"any\", \"port\": 375}, {\"cidr\": \"10.0.2.52/31\", \"protocol\": \"any\", \"port\": 5, \"endPort\": 6}, {\"cidr\": \"2001:db8::5d/128\", \"protocol\": \"tcp\", \"port\": 273}, {\"cidr\": \"2001:db8::242/128\", \"protocol\": \"any\", \"port\": 393}, {\"cidr\": \"10.0.3.160/32\", \"protocol\": \"tcp\", \"port\": 59, \"endPort\": 63}, {\"cidr\": \"10.0.1.81/32\", \"protocol\": \"udp\", \"port\": 69}, {\"cidr\": \"10.0.2.68/32\", \"protocol\": \"udp\", \"port\": 67}, {\"cidr\": \"10.0.3.128/30\", \"protocol\": \"udp\", \"port\": 347}, {\"cidr\": \"2001:db8::88/128\", \"protocol\": \"udp\", \"port\": 223}, {\"cidr\": \"10.0.1.52/30\", \"protocol\": \"any\", \"port\": 217, \"endPort\": 217}, {\"cidr\": \"10.0.2.116/31\", \"protocol\": \"any\", \"port\": 27, \"endPort\": 28}, {\"cidr\": \"2001:db8::34b/128\", \"protocol\": \"tcp\", \"port\": 203}, {\"cidr\": \"2001:db8::291/128\", \"protocol\": \"tcp\", \"port\": 65}, {\"cidr\": \"10.0.1.232/30\", \"protocol\": \"any\", \"port\": 263}, {\"cidr\": \"10.0.1.240/31\", \"protocol\": \"any\", \"port\": 53}, {\"cidr\": \"10.0.0.44/30\", \"protocol\": \"any\", \"port\": 185, \"endPort\": 187}, {\"cidr\": \"10.0.3.236/32\", \"protocol\": \"tcp\", \"port\": 369}, {\"cidr\": \"2001:db8::21a/128\", \"protocol\": \"tcp\", \"port\": 13}, {\"cidr\": \"10.0.3.120/30\", \"protocol\": \"any\", \"port\": 9, \"endPort\": 10}, {\"cidr\": \"10.0.1.16/30\", \"protocol\": \"tcp\", \"port\": 55, \"endPort\": 56}, {\"cidr\": \"2001:db8::320/128\", \"protocol\": \"any\", \"port\": 89}, {\"cidr\": \"10.0.3.214/31\", \"protocol\": \"tcp\", \"port\": 263}, {\"cidr\": \"2001:db8::268/128\", \"protocol\": \"any\", \"port\": 237}, {\"cidr\": \"2001:db8::9c/128\", \"protocol\": \"tcp\", \"port\": 397}, {\"cidr\": \"2001:db8::23e/128\", \"protocol\": \"any\", \"port\": 381}, {\"cidr\": \"2001:db8::293/128\", \"protocol\": \"udp\", \"port\": 247, \"endPort\": 248}, {\"cidr\": \"10.0.3.108/30\", \"protocol\": \"tcp\", \"port\": 55, \"endPort\": 59}, {\"cidr\": \"10.0.0.162/31\", \"protocol\": \"any\", \"port\": 81, \"endPort\": 83}, {\"cidr\": \"10.0.0.242/31\", \"protocol\": \"any\", \"port\": 255}, {\"cidr\": \"2001:db8::234/128\", \"protocol\": \"tcp\", \"port\": 313, \"endPort\": 317}, {\"cidr\": \"10.0.3.109/32\", \"protocol\": \"udp\", \"port\": 367, \"endPort\": 371}, {\"cidr\": \"10.0.0.162/31\", \"protocol\": \"udp\", \"port\": 165}, {\"cidr\": \"2001:db8::3a5/128\", \"protocol\": \"any\", \"port\": 5}, {\"cidr\": \"10.0.3.212/31\", \"protocol\": \"any\", \"port\": 91, \"endPort\": 91}, {\"cidr\": \"10.0.0.140/30\", \"protocol\": \"udp\", \"port\": 3}, {\"cidr\": \"10.0.3.180/30\", \"protocol\": \"any\", \"port\": 103, \"endPort\": 107}, {\"cidr\": \"2001:db8::e9/128\", \"protocol\": \"udp\", \"port\": 263, \"endPort\": 264}, {\"cidr\": \"10.0.0.234/32\", \"protocol\": \"tcp\", \"port\": 361}, {\"cidr\": \"2001:db8::394/128\", \"protocol\": \"tcp\", \"port\": 65}, {\"cidr\": \"10.0.3.247/32\", \"protocol\": \"any\", \"port\": 253}, {\"cidr\": \"10.0.0.225/32\", \"protocol\": \"udp\", \"port\": 307}, {\"cidr\": \"2001:db8::41/128\", \"protocol\": \"any\", \"port\": 179}, {\"cidr\": \"10.0.3.112/32\", \"protocol\": \"udp\", \"port\": 295, \"endPort\": 298}, {\"cidr\": \"2001:db8::2b/128\", \"protocol\": \"udp\", \"port\": 231}, {\"cidr\": \"10.0.2.7/32\", \"protocol\": \"tcp\", \"port\": 233}, {\"cidr\": \"10.0.2.64/30\", \"protocol\": \"tcp\", \"port\": 69}, {\"cidr\": \"10.0.3.20/31\", \"protocol\": \"tcp\", \"port\": 193}, {\"cidr\": \"2001:db8::2/128\", \"protocol\": \"udp\", \"port\": 59}, {\"cidr\": \"2001:db8::2cb/128\", \"protocol\": \"tcp\", \"port\": 343}, {\"cidr\": \"10.0.1.176/30\", \"protocol\": \"tcp\", \"port\": 129}, {\"cidr\": \"2001:db8::35d/128\", \"protocol\": \"any\", \"port\": 165}, {\"cidr\": \"10.0.0.138/32\", \"protocol\": \"tcp\", \"port\": 299}, {\"cidr\": \"10.0.0.80/31\", \"protocol\": \"any\", \"port\": 397}, {\"cidr\": \"10.0.0.156/31\", \"protocol\": \"udp\", \"port\": 133, \"endPort\": 134}, {\"cidr\": \"10.0.2.130/31\", \"protocol\": \"any\", \"port\": 253, \"endPort\": 256}, {\"cidr\": \"10.0.3.200/30\", \"protocol\": \"any\", \"port\": 297, \"endPort\": 299}, {\"cidr\": \"10.0.2.156/31\", \"protocol\": \"tcp\", \"port\": 327}, {\"cidr\": \"10.0.0.224/30\", \"protocol\": \"udp\", \"port\": 317}, {\"cidr\": \"10.0.0.10/31\", \"protocol\": \"tcp\", \"port\": 243}, {\"cidr\": \"10.0.0.12/32\", \"protocol\": \"udp\", \"port\": 251}, {\"cidr\": \"2001:db8::3cc/128\", \"protocol\": \"any\", \"port\": 319}, {\"cidr\": \"10.0.3.115/32\", \"protocol\": \"udp\", \"port\": 181}, {\"cidr\": \"10.0.1.160/32\", \"protocol\": \"tcp\", \"port\": 31}, {\"cidr\": \"10.0.2.149/32\", \"protocol\": \"any\", \"port\": 321}, {\"cidr\": \"10.0.2.144/30\", \"protocol\": \"any\", \"port\": 397}, {\"cidr\": \"2001:db8::3ed/128\", \"protocol\": \"udp\", \"port\": 135}, {\"cidr\": \"10.0.3.237/32\", \"protocol\": \"tcp\", \"port\": 345, \"endPort\": 347}, {\"cidr\": \"10.0.2.190/31\", \"protocol\": \"udp\", \"port\": 117}, {\"cidr\": \"10.0.2.22/32\", \"protocol\": \"any\", \"port\": 41}, {\"cidr\": \"10.0.1.242/32\", \"protocol\": \"tcp\", \"port\": 347}, {\"cidr\": \"10.0.1.166/32\", \"protocol\": \"udp\", \"port\": 215}, {\"cidr\": \"10.0.2.116/32\", \"protocol\": \"tcp\", \"port\": 241, \"endPort\": 241}, {\"cidr\": \"10.0.3.10/31\", \"protocol\": \"any\", \"port\": 311, \"endPort\": 311}, {\"cidr\": \"2001:db8::129/128\", \"protocol\": \"udp\", \"port\": 125}, {\"cidr\": \"10.0.0.112/30\", \"protocol\": \"udp\", \"port\": 133}, {\"cidr\": \"2001:db8::1dd/128\", \"protocol\": \"any\", \"port\": 295}, {\"cidr\": \"2001:db8::c3/128\", \"protocol\": \"tcp\", \"port\": 35}, {\"cidr\": \"2001:db8::293/128\", \"protocol\": \"udp\", \"port\": 385}, {\"cidr\": \"10.0.3.170/32\", \"protocol\": \"tcp\", \"port\": 255}, {\"cidr\": \"10.0.2.89/32\", \"protocol\": \"udp\", \"port\": 337, \"endPort\": 337}, {\"cidr\": \"2001:db8::14e/128\", \"protocol\": \"tcp\", \"port\": 193, \"endPort\": 194}, {\"cidr\": \"10.0.0.110/31\", \"protocol\": \"tcp\", \"port\": 305, \"endPort\": 308}, {\"cidr\": \"10.0.2.46/31\", \"protocol\": \"any\", \"port\": 133}, {\"cidr\": \"10.0.0.150/31\", \"protocol\": \"udp\", \"port\": 73, \"endPort\": 76}, {\"cidr\": \"2001:db8::c5/128\", \"protocol\": \"tcp\", \"port\": 115, \"endPort\": 115}, {\"cidr\": \"10.0.1.216/30\", \"protocol\": \"tcp\", \"port\": 253}, {\"cidr\": \"10.0.3.158/31\", \"protocol\": \"udp\", \"port\": 143}, {\"cidr\": \"10.0.1.72/30\", \"protocol\": \"tcp\", \"port\": 49}, {\"cidr\": \"10.0.3.180/30\", \"protocol\": \"any\", \"port\": 135}, {\"cidr\": \"10.0.0.196/30\", \"protocol\": \"any\", \"port\": 13}, {\"cidr\": \"10.0.3.22/31\", \"protocol\": \"udp\", \"port\": 41}, {\"cidr\": \"10.0.2.70/32\", \"protocol\": \"tcp\", \"port\": 135, \"endPort\": 136}, {\"cidr\": \"10.0.2.136/30\", \"protocol\": \"tcp\", \"port\": 169}, {\"cidr\": \"10.0.2.92/30\", \"protocol\": \"tcp\", \"port\": 379}, {\"cidr\": \"10.0.0.64/32\", \"protocol\": \"any\", \"port\": 171, \"endPort\": 174}, {\"cidr\": \"10.0.3.96/30\", \"protocol\": \"tcp\", \"port\": 379, \"endPort\": 380}, {\"cidr\": \"2001:db8::214/128\", \"protocol\": \"tcp\", \"port\": 117, \"endPort\": 118}, {\"cidr\": \"10.0.1.48/30\", \"protocol\": \"any\", \"port\": 27, \"endPort\": 27}, {\"cidr\": \"2001:db8::3c8/128\", \"protocol\": \"tcp\", \"port\": 305}, {\"cidr\": \"2001:db8::208/128\", \"protocol\": \"any\", \"port\": 95}, {\"cidr\": \"10.0.1.173/32\", \"protocol\": \"any\", \"port\": 59}, {\"cidr\": \"10.0.3.88/31\", \"protocol\": \"tcp\", \"port\": 47}, {\"cidr\": \"2001:db8::22b/128\", \"protocol\": \"tcp\", \"port\": 215}, {\"cidr\": \"2001:db8::30b/128\", \"protocol\": \"any\", \"port\": 341, \"endPort\": 345}, {\"cidr\": \"2001:db8::322/128\", \"protocol\": \"any\", \"port\": 257}, {\"cidr\": \"10.0.1.98/31\", \"protocol\": \"any\", \"port\": 31}, {\"cidr\": \"10.0.1.12/30\", \"protocol\": \"udp\", \"port\": 179}, {\"cidr\": \"10.0.2.150/31\", \"protocol\": \"tcp\", \"port\": 39}, {\"cidr\": \"10.0.1.175/32\", \"protocol\": \"udp\", \"port\": 331}, {\"cidr\": \"2001:db8::1ee/128\", \"protocol\": \"any\", \"port\": 333}, {\"cidr\": \"10.0.1.184/31\", \"protocol\": \"tcp\", \"port\": 185}, {\"cidr\": \"2001:db8::87/128\", \"protocol\": \"udp\", \"port\": 183}, {\"cidr\": \"10.0.2.204/30\", \"protocol\": \"tcp\", \"port\": 41}, {\"cidr\": \"10.0.1.76/30\", \"protocol\": \"tcp\", \"port\": 53, \"endPort\": 54}, {\"cidr\": \"10.0.1.65/32\", \"protocol\": \"tcp\", \"port\": 181}, {\"cidr\": \"2001:db8::11f/128\", \"protocol\": \"tcp\", \"port\": 125}, {\"cidr\": \"10.0.3.192/30\", \"protocol\": \"any\", \"port\": 357, \"endPort\": 360}, {\"cidr\": \"10.0.3.8/30\", \"protocol\": \"any\", \"port\": 41}, {\"cidr\": \"10.0.3.0/31\", \"protocol\": \"tcp\", \"port\": 275, \"endPort\": 276}, {\"cidr\": \"10.0.2.27/32\", \"protocol\": \"any\", \"port\": 83}, {\"cidr\": \"2001:db8::169/128\", \"protocol\": \"tcp\", \"port\": 81}, {\"cidr\": \"2001:db8::cc/128\", \"protocol\": \"any\", \"port\": 93}, {\"cidr\": \"10.0.0.32/30\", \"protocol\": \"any\", \"port\": 87, \"endPort\": 87}, {\"cidr\": \"10.0.2.92/30\", \"protocol\": \"tcp\", \"port\": 79}, {\"cidr\": \"10.0.3.144/32\", \"protocol\": \"udp\", \"port\": 93}, {\"cidr\": \"10.0.2.124/30\", \"protocol\": \"any\", \"port\": 173, \"endPort\": 177}, {\"cidr\": \"2001:db8::23d/128\", \"protocol\": \"tcp\", \"port\": 195}, {\"cidr\": \"10.0.3.28/31\", \"protocol\": \"udp\", \"port\": 201}, {\"cidr\": \"10.0.3.84/32\", \"protocol\": \"any\", \"port\": 45}, {\"cidr\": \"10.0.0.151/32\", \"protocol\": \"tcp\", \"port\": 83}, {\"cidr\": \"2001:db8::b5/128\", \"protocol\": \"tcp\", \"port\": 265}, {\"cidr\": \"10.0.1.42/32\", \"protocol\": \"udp\", \"port\": 99, \"endPort\": 99}, {\"cidr\": \"2001:db8::29b/128\", \"protocol\": \"tcp\", \"port\": 313, \"endPort\": 316}, {\"cidr\": \"10.0.0.208/30\", \"protocol\": \"tcp\", \"port\": 247, \"endPort\": 249}, {\"cidr\": \"2001:db8::357/128\", \"protocol\": \"udp\", \"port\": 169, \"endPort\": 173}, {\"cidr\": \"2001:db8::39f/128\", \"protocol\": \"udp\", \"port\": 309}, {\"cidr\": \"2001:db8::149/128\", \"protocol\": \"any\", \"port\": 47}, {\"cidr\": \"10.0.2.92/30\", \"protocol\": \"tcp\", \"port\": 229, \"endPort\": 230}, {\"cidr\": \"10.0.1.160/32\", \"protocol\": \"udp\", \"port\": 299, \"endPort\": 300}, {\"cidr\": \"2001:db8::17a/128\", \"protocol\": \"any\", \"port\": 45}, {\"cidr\": \"2001:db8::1af/128\", \"protocol\": \"any\", \"port\": 35, \"endPort\": 35}, {\"cidr\": \"2001:db8::13f/128\", \"protocol\": \"any\", \"port\": 309}, {\"cidr\": \"10.0.2.194/32\", \"protocol\": \"any\", \"port\": 229}, {\"cidr\": \"10.0.0.228/30\", \"protocol\": \"any\", \"port\": 95}, {\"cidr\": \"10.0.1.181/32\", \"protocol\": \"any\", \"port\": 101}, {\"cidr\": \"10.0.0.86/31\", \"protocol\": \"any\", \"port\": 215}, {\"cidr\": \"10.0.0.196/32\", \"protocol\": \"any\", \"port\": 239}, {\"cidr\": \"10.0.3.123/32\", \"protocol\": \"udp\", \"port\": 281}, {\"cidr\": \"10.0.1.16/30\", \"protocol\": \"udp\", \"port\": 187, \"endPort\": 190}, {\"cidr\": \"10.0.1.136/30\", \"protocol\": \"any\", \"port\": 293}, {\"cidr\": \"2001:db8::203/128\", \"protocol\": \"tcp\", \"port\": 227}, {\"cidr\": \"2001:db8::35d/128\", \"protocol\": \"tcp\", \"port\": 263}, {\"cidr\": \"10.0.3.6/31\", \"protocol\": \"tcp\", \"port\": 355}, {\"cidr\": \"10.0.1.216/32\", \"protocol\": \"tcp\", \"port\": 191, \"endPort\": 191}, {\"cidr\": \"10.0.3.73/32\", \"protocol\": \"any\", \"port\": 299}, {\"cidr\": \"10.0.0.128/30\", \"protocol\": \"tcp\", \"port\": 163}, {\"cidr\": \"10.0.0.124/30\", \"protocol\": \"any\", \"port\": 303}, {\"cidr\": \"10.0.2.86/31\", \"protocol\": \"tcp\", \"port\": 11}, {\"cidr\": \"2001:db8::1bb/128\", \"protocol\": \"tcp\", \"port\": 247}, {\"cidr\": \"10.0.0.178/31\", \"protocol\": \"tcp\", \"port\": 323, \"endPort\": 324}, {\"cidr\": \"10.0.2.134/31\", \"protocol\": \"tcp\", \"port\": 249, \"endPort\": 253}, {\"cidr\": \"2001:db8::2be/128\", \"protocol\": \"tcp\", \"port\": 61}, {\"cidr\": \"10.0.1.40/30\", \"protocol\": \"udp\", \"port\": 69}, {\"cidr\": \"10.0.2.227/32\", \"protocol\": \"udp\", \"port\": 125}, {\"cidr\": \"10.0.0.240/30\", \"protocol\": \"udp\", \"port\": 355}, {\"cidr\": \"10.0.0.238/31\", \"protocol\": \"udp\", \"port\": 37}, {\"cidr\": \"2001:db8::30f/128\", \"protocol\": \"tcp\", \"port\": 165}, {\"cidr\": \"10.0.2.136/30\", \"protocol\": \"udp\", \"port\": 227}, {\"cidr\": \"10.0.0.44/32\", \"protocol\": \"any\", \"port\": 139}, {\"cidr\": \"2001:db8::1cf/128\", \"protocol\": \"tcp\", \"port\": 329, \"endPort\": 329}, {\"cidr\": \"2001:db8::9d/128\", \"protocol\": \"any\", \"port\": 339}, {\"cidr\": \"10.0.2.76/31\", \"protocol\": \"udp\", \"port\": 275}, {\"cidr\": \"2001:db8::77/128\", \"protocol\": \"any\", \"port\": 85}, {\"cidr\": \"10.0.1.212/30\", \"protocol\": \"udp\", \"port\": 167}, {\"cidr\": \"2001:db8::287/128\", \"protocol\": \"tcp\", \"port\": 115}, {\"cidr\": \"10.0.1.73/32\", \"protocol\": \"tcp\", \"port\": 93, \"endPort\": 93}, {\"cidr\": \"2001:db8::331/128\", \"protocol\": \"any\", \"port\": 209}, {\"cidr\": \"10.0.1.112/32\", \"protocol\": \"udp\", \"port\": 73}, {\"cidr\": \"2001:db8::85/128\", \"protocol\": \"tcp\", \"port\": 19}, {\"cidr\": \"10.0.1.72/32\", \"protocol\": \"udp\", \"port\": 205, \"endPort\": 208}, {\"cidr\": \"10.0.0.46/31\", \"protocol\": \"any\", \"port\": 201}, {\"cidr\": \"10.0.0.189/32\", \"protocol\": \"tcp\", \"port\": 223}, {\"cidr\": \"10.0.0.240/30\", \"protocol\": \"tcp\", \"port\": 15}, {\"cidr\": \"10.0.0.101/32\", \"protocol\": \"any\", \"port\": 9}, {\"cidr\": \"2001:db8::184/128\", \"protocol\": \"udp\", \"port\": 255, \"endPort\": 258}, {\"cidr\": \"10.0.1.51/32\", \"protocol\": \"any\", \"port\": 343, \"endPort\": 346}, {\"cidr\": \"10.0.0.220/30\", \"protocol\": \"tcp\", \"port\": 163}, {\"cidr\": \"10.0.0.190/32\", \"protocol\": \"tcp\", \"port\": 393}, {\"cidr\": \"10.0.1.144/30\", \"protocol\": \"any\", \"port\": 257}, {\"cidr\": \"2001:db8::3fc/128\", \"protocol\": \"udp\", \"port\": 193}, {\"cidr\": \"10.0.0.72/30\", \"protocol\": \"udp\", \"port\": 285}, {\"cidr\": \"2001:db8::3cc/128\", \"protocol\": \"any\", \"port\": 387}, {\"cidr\": \"10.0.1.116/30\", \"protocol\": \"any\", \"port\": 373, \"endPort\": 374}, {\"cidr\": \"2001:db8::3c7/128\", \"protocol\": \"tcp\", \"port\": 41}, {\"cidr\": \"10.0.1.150/32\", \"protocol\": \"tcp\", \"port\": 215}, {\"cidr\": \"10.0.2.68/32\", \"protocol\": \"udp\", \"port\": 259}, {\"cidr\": \"10.0.2.184/30\", \"protocol\": \"tcp\", \"port\": 75}, {\"cidr\": \"2001:db8::1f9/128\", \"protocol\": \"udp\", \"port\": 109}, {\"cidr\": \"10.0.3.190/32\", \"protocol\": \"udp\", \"port\": 323, \"endPort\": 327}, {\"cidr\": \"2001:db8::1d5/128\", \"protocol\": \"any\", \"port\": 5}, {\"cidr\": \"10.0.1.95/32\", \"protocol\": \"udp\", \"port\": 95, \"endPort\": 96}, {\"cidr\": \"10.0.3.172/30\", \"protocol\": \"any\", \"port\": 31}, {\"cidr\": \"10.0.2.232/31\", \"protocol\": \"tcp\", \"port\": 129}, {\"cidr\": \"10.0.3.66/31\", \"protocol\": \"tcp\", \"port\": 69}, {\"cidr\": \"10.0.3.255/32\", \"protocol\": \"tcp\", \"port\": 303, \"endPort\": 306}, {\"cidr\": \"10.0.0.84/31\", \"protocol\": \"any\", \"port\": 293}, {\"cidr\": \"10.0.1.152/30\", \"protocol\": \"udp\", \"port\": 373, \"endPort\": 373}, {\"cidr\": \"2001:db8::39b/128\", \"protocol\": \"udp\", \"port\": 231, \"endPort\": 234}, {\"cidr\": \"2001:db8::313/128\", \"protocol\": \"tcp\", \"port\": 43, \"endPort\": 43}]}"}],"expected":{"error":"duplicate JSON property: allow"}}, + {"name":"random #104","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::2e7/00121\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.80/28\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 11\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.205/25\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 79\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::204/126\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"4934:b28:63bd:8bc5:8ea8:6ad:1600:0/104\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"4934:b28:63bd:8bc5:8ea8:6ad:16b9:0/112\",\n\t\t\t\t\t\t\"4934:b28:63bd:8bc5:8ea8:6ad:1610:0/108\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.64/00027\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.64/026\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 6\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 441\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 78\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 80,\n\t\t\t\t\t\"endPort\": 84\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 10\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.0/23\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::300/121\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:db8::320/00123\",\n\t\t\t\t\t\t\"10.0.1.224/27\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.0/24\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.32/24\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 444,\n\t\t\t\"endPort\": 445\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.0/23\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"error":"deny[3].to[1].except[1] must be contained in 2001:db8::300/121"}}, + {"name":"random #105","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.1.128/025\"}, {\"cidr\": \"10.0.0.0/22\"}], \"ports\": [{\"port\": 441}, {\"port\": 11, \"endPort\": 14}, {\"protocol\": \"any\"}]}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:036a/127\"}, {\"cidr\": \"2001:db8::13f/123\"}, {\"cidr\": \"2001:db8::3f/120\"}, {\"cidr\": \"10.0.0.0/22\", \"except\": [\"10.0.0.17/24\"]}], \"ports\": [{\"protocol\": \"any\", \"port\": 5, \"endPort\": 20751}, {\"protocol\": \"tcp\", \"port\": 65531, \"endPort\": 65532}, {\"protocol\": \"any\"}]}], \"deny\": []}"}],"expected":{"allow":["10.0.0.0/22","2001:db8::/120","2001:db8::120/123","2001:db8::36a/127"],"deny":[]}}, + {"name":"random #106","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/23\"}, {\"cidr\": \"176.189.184.144/24\"}], \"ports\": [{\"port\": 3, \"endPort\": 7}, {\"protocol\": \"tcp\", \"port\": 78}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"10.0.0.0/22\"}, {\"cidr\": \"10.0.0.122/31\"}, {\"cidr\": \"FAF4:22E2:7E57:64EA:900A:F6A5:6182:8AD0/120\"}]}, {\"cidr\": \"10.0.3.162/29\", \"protocol\": \"udp\", \"port\": 443, \"endPort\": 447}, {\"to\": [{\"cidr\": \"2001:db8::240/124\"}, {\"cidr\": \"10.0.2.88/29\"}, {\"cidr\": \"10.0.3.57/24\", \"except\": []}], \"ports\": [{\"protocol\": \"udp\", \"port\": 444, \"endPort\": 446}, {\"protocol\": \"tcp\", \"port\": 440}]}, {\"cidr\": \"10.0.1.170/255.255.255.254\", \"except\": [\"10.0.1.171\"], \"protocol\": \"udp\"}, {}, {\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.0/124\"}, {\"cidr\": \"2001:db8::3f1\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65535, \"endPort\": 65535}]}, {\"to\": [{\"cidr\": \"3.150.135.185/1\"}]}]}"}],"expected":{"allow":[],"deny":["0.0.0.0/0","::/0"]}}, + {"name":"random #107","input":[{"text":"{\n \"allow\": [],\n \"deny\": [\n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.0/22\"\n },\n {\n \"cidr\": \"10.0.0.237/32\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"any\",\n \"port\": NaN\n },\n {\n \"protocol\": \"tcp\",\n \"port\": 80,\n \"endPort\": 85\n },\n {\n \"protocol\": \"tcp\"\n },\n {\n \"protocol\": \"tcp\"\n }\n ]\n },\n {\n \"cidr\": \"eb79:61c6:c624:ae92::/064\",\n \"protocol\": \"any\",\n \"port\": 65530\n },\n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::300/120\",\n \"except\": [\n \"2001:0db8:0000:0000:0000:0000:0000:0300/121\"\n ]\n }\n ]\n }\n ]\n}"}],"expected":{"error":"deny[0].ports[0].port must be an integer"}}, + {"name":"random #108","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.64/00026\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.3.93/29\",\n\t\t\t\t\t\t\"10.0.3.64/27\",\n\t\t\t\t\t\t\"10.0.3.89/32\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"8670:f370:b3ec:ee0d:9211:6b4f:0000:0000/96\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 78,\n\t\t\t\t\t\"endPort\": 45206\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"::ffff:0.0.0.0/096\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.192/26\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 442,\n\t\t\t\t\t\"endPort\": 443\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.0/27\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"848a:9271:2d98:c528:c70a:4828:ac56:b9e4/127\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"848a:9271:2d98:c528:c70a:4828:ac56:b9e4/127\",\n\t\t\t\t\t\t\"848a:9271:2d98:c528:c70a:4828:ac56:b9e4/127\",\n\t\t\t\t\t\t\"848a:9271:2d98:c528:c70a:4828:ac56:b9e4\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0225/126\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"4bba:36b9:8765:8425:9266:6f08:96e9:620e/1\",\n\t\t\t\"protocol\": \"icmp\"\n\t\t}\n\t]\n}"}],"expected":{"allow":["10.0.3.96/27:tcp:78-45206","10.0.3.96/27:udp:78-45206","8670:f370:b3ec:ee0d:9211:6b4f::/96:tcp:78-45206","8670:f370:b3ec:ee0d:9211:6b4f::/96:udp:78-45206"],"deny":["10.0.2.0/27","10.0.3.192/26:tcp:442-443","10.0.3.192/26:udp:442-443","::/1:icmp","::ffff:0.0.0.0/96:tcp:442-443","::ffff:0.0.0.0/96:udp:442-443","2001:db8::224/126"]}}, + {"name":"random #109","input":[{"text":"{\n\"allow\":[\n{\n\"to\":[\n{\n\"cidr\":\"192.0.2.0/24\",\n\"except\":[\n\"10.0.0.0/8\"\n]\n}\n],\n\"ports\":[\n{\n\"protocol\":\"udp\",\n\"port\":65531\n}\n]\n}\n]\n}"}],"expected":{"error":"allow[0].to[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"random #110","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::d8/126\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":7\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/0022\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65532\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":81\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.168/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":12,\n\t\t\t\"endPort\":15\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"43.0.0.0/8\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"73.213.75.193/12\",\n\t\t\t\"except\":[\n\t\t\t\t\"73.219.0.0/16\"\n\t\t\t],\n\t\t\t\"protocol\":\"any\"\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/255.255.252.0\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::200/124\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.128/25\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":80\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"port\":78\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"port\":65533,\n\t\t\t\t\t\"endPort\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":65535\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"::ffff:0.0.0.0/096\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"::ffff:148.0.0.0/102\",\n\t\t\t\t\t\t\"::ffff:250.70.249.87/98\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":65533,\n\t\t\t\t\t\"endPort\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":80\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"cb51:c873:70a3:df87:2f6b:e7a7:b365:9cb/32\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"::ffff:125.0.0.0/104\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"::ffff:125.196.0.0/110\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/0\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"225.242.96.226/8\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::a7/121\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":8\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::d9/119\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::fb/120\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:02cd/123\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":445\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::352/127\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"9a0:4eca:a840:9594:654e:2357::/96\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"9a0:4eca:a840:9594:654e:2357:e000:0/101\",\n\t\t\t\t\t\t\"9a0:4eca:a840:9594:654e:2357:bfc3:d768/97\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":80,\n\t\t\t\t\t\"endPort\":81\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::177/126\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:03c7/124\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/1\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":81,\n\t\t\t\t\t\"endPort\":83\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.20\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.40/31\",\n\t\t\t\"except\":[],\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":65532\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::1e0/123\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.0/24\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":441,\n\t\t\t\t\t\"endPort\":445\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/2\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"7c7d:1206:3dec:f736:c839:fff2:5a2a:3c7f/96\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"7c7d:1206:3dec:f736:c839:fff2:4000:0/100\",\n\t\t\t\t\t\t\"7c7d:1206:3dec:f736:c839:fff2:6a00:0000/103\",\n\t\t\t\t\t\t\"7C7D:1206:3DEC:F736:C839:FFF2:6C14:63AF/98\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::350/0124\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":10\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"port\":12\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65535,\n\t\t\t\t\t\"endPort\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":7,\n\t\t\t\t\t\"endPort\":12\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::c0/125\",\n\t\t\t\"except\":[\n\t\t\t\t\"2001:db8::c1/128\",\n\t\t\t\t\"2001:db8::c0/00126\",\n\t\t\t\t\"2001:db8::c0/125\"\n\t\t\t],\n\t\t\t\"protocol\":\"tcp\"\n\t\t},\n\t\t{\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"0.0.0.0/240.0.0.0\",\n\t\t\t\"protocol\":\"any\"\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::a8/128\",\n\t\t\t\"except\":[\n\t\t\t\t\"2001:db8::a8/128\",\n\t\t\t\t\"2001:db8::a8\"\n\t\t\t],\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":65535\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.192/27\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":445,\n\t\t\t\"endPort\":451\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.1.103/255.255.255.248\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.1.98/31\",\n\t\t\t\t\t\t\"10.0.1.102/31\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.0/23\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":11\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"9933:97dd:821d:1335:3f0c:af1f:eef:985/128\",\n\t\t\t\"protocol\":\"udp\"\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"::/64\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65535,\n\t\t\t\t\t\"endPort\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":78,\n\t\t\t\t\t\"endPort\":79\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":2\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":3,\n\t\t\t\t\t\"endPort\":5\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.0/23\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.29/23\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.1.22/255.255.255.248\",\n\t\t\t\t\t\t\"10.0.1.45/0.0.0.1\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"a6c8:8fef:0f06:4e44:d440:d4de:0100:0000/104\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"51.101.1.0/24\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":5\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.0/23\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.155/0029\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"deny\":[\n\t\t{}\n\t]\n}"}],"expected":{"allow":["0.0.0.0/0","::/0:icmp","::/0:tcp","::/0:udp","::ffff:125.0.0.0/105","::ffff:125.128.0.0/106","::ffff:125.192.0.0/110","::ffff:125.200.0.0/109","::ffff:125.208.0.0/108","::ffff:125.224.0.0/107","a6c8:8fef:f06:4e44:d440:d4de:100:0/104","cb51:c873::/32"],"deny":["0.0.0.0/0","::/0"]}}, + {"name":"random #111","input":[{"text":"{\n \"allow\": [\n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.75/32\"\n }, \n {\n \"cidr\": \"2001:db8::22e/118\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 78, \n \"endPort\": 82\n }, \n {\n \"protocol\": \"icmp\"\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 65535, \n \"endPort\": 65535\n }\n ]\n }, \n {\n \"cidr\": \"86.246.226.227\", \n \"protocol\": \"tcp\", \n \"port\": 441, \n \"endPort\": 445\n }\n ]\n}"}],"expected":{"allow":["10.0.3.75/32:icmp","10.0.3.75/32:tcp:78-82","10.0.3.75/32:udp:65535","86.246.226.227/32:tcp:441-445","2001:db8::/118:icmp","2001:db8::/118:tcp:78-82","2001:db8::/118:udp:65535"],"deny":[]}}, + {"name":"random #112","input":[{"text":"{\n \"allow\": [\n {}, \n {\n \"ports\": [\n {\n \"protocol\": \"any\"\n }, \n {\n \"protocol\": \"udp\"\n }, \n {\n \"protocol\": \"tcp\"\n }\n ]\n }, \n {\n \"cidr\": \"2001:db8::37/120\", \n \"protocol\": \"icmp\"\n }\n ], \n \"deny\": [\n {\n \"cidr\": \"2001:db8::28b/124\", \n \"except\": [\n \"2001:db8::289/124\", \n \"2001:db8::287/125\"\n ], \n \"protocol\": \"any\"\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.2.190/31\"\n }, \n {\n \"cidr\": \"10.0.3.48/28\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\"\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["10.0.2.190/31:tcp","10.0.3.48/28:tcp"]}}, + {"name":"random #113","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"842e:f963:4f0d:465e::/64\", \n\t\t\t\"protocol\": \"icmp\"\n\t\t}, \n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"10.0.0.160/255.255.255.224\", \n\t\t\t\"protocol\": \"any\"\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::194/128\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::16f/119\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/22\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.2.152/29\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.12/31\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 445\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.147/26\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.3.140/31\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.160/28\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/22\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.1.28/30\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"fa98:d637::/32\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 4, \n\t\t\t\t\t\"endPort\": 9\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 440\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"unknown\": 1\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.17/27\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.144/30\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.64/0.0.0.63\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 65532\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"error":"allow[4] has unknown field 'unknown'"}}, + {"name":"random #114","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0180/121\"}, {\"cidr\": \"10.0.2.128/26\"}, {\"cidr\": \"10.0.1.47/255.255.255.224\", \"except\": [\"10.0.1.62/29\"]}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"udp\"}, {\"protocol\": \"icmp\"}]}, {\"cidr\": \"2001:db8::200/121\", \"protocol\": \"udp\", \"port\": 80, \"endPort\": 84}], \"deny\": [{\"to\": [{\"cidr\": \"8b82:0446:ef00:8ab4:0000:0000:0000:0000/0064\"}], \"ports\": [{\"port\": 65534, \"endPort\": 65535}]}, {\"cidr\": \"10.0.0.107/28\", \"protocol\": \"udp\", \"port\": 82}, {\"to\": [{\"cidr\": \"0.0.0.0/1\"}], \"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"tcp\"}]}]}"}],"expected":{"allow":["10.0.1.32/28:icmp","10.0.1.32/28:udp","10.0.1.48/29:icmp","10.0.1.48/29:udp","10.0.2.128/26:icmp","10.0.2.128/26:udp","2001:db8::180/121:icmp","2001:db8::180/121:udp","2001:db8::200/121:udp:80-84"],"deny":["0.0.0.0/1:tcp","10.0.0.96/28:udp:82","8b82:446:ef00:8ab4::/64:tcp:65534-65535","8b82:446:ef00:8ab4::/64:udp:65534-65535"]}}, + {"name":"random #115","input":[{"text":"{\n \"allow\": [\n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::a1/123\"\n }, \n {\n \"cidr\": \"189.16.129.215/4\"\n }, \n {\n \"cidr\": \"10.0.1.0/00025\", \n \"except\": [\n \"10.0.1.84/31\", \n \"10.0.1.0/25\"\n ]\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["176.0.0.0/4","2001:db8::a0/123"],"deny":[]}}, + {"name":"random #116","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::19f/128\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"::/1\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 10\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 445\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"10.0.3.128/25\", \n\t\t\t\"protocol\": \"tcp\", \n\t\t\t\"port\": 4, \n\t\t\t\"endPort\": 37064\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.2/31\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"43f9:238e::/32\", \n\t\t\t\"protocol\": \"any\"\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"10.0.1.247/255.255.254.0\", \n\t\t\t\"except\": []\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"193.204.123.212/32\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.2.0/127\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t], \n\t\"deny\": [\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.0/255.255.254.0\", \n\t\t\t\"protocol\": \"udp\"\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"205.164.89.57/4\", \n\t\t\t\"protocol\": \"udp\", \n\t\t\t\"port\": 80, \n\t\t\t\"endPort\": 86\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"183.0.0.0/8\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:01be/124\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.96/27\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/22\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 11, \n\t\t\t\t\t\"endPort\": 15\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"port\": 65532, \n\t\t\t\t\t\"endPort\": 65535\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 5\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"2001:db8::23/124\", \n\t\t\t\"protocol\": \"tcp\", \n\t\t\t\"port\": 82\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:DB8::390/125\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.3.124/30\", \n\t\t\t\t\t\t\"2001:db8::392/000128\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"::/3\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"120.141.5.198/31\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.44/32\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 3, \n\t\t\t\t\t\"endPort\": 6\n\t\t\t\t}, \n\t\t\t\t{}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"error":"deny[4].to[0].except[0] must be contained in 2001:db8::390/125"}}, + {"name":"random #117","input":[{"text":"{\n \"allow\": [\n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.245/22\"\n }\n ]\n },\n {\n \"cidr\": \"10.0.0.113\",\n \"except\": [],\n \"protocol\": \"tcp\",\n \"port\": 65531,\n \"endPort\": 65533\n },\n {\n \"cidr\": \"56.255.0.0/16\"\n },\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.90/127\",\n \"protocol\": \"icmp\"\n },\n {\n \"cidr\": \"2001:db8::/118\",\n \"except\": [\n \"2001:db8::/121\"\n ],\n \"protocol\": \"udp\",\n \"port\": 82,\n \"endPort\": 85\n },\n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.160/27\",\n \"except\": [\n \"10.0.0.160/31\",\n \"10.0.0.168/0.0.0.0\",\n \"10.0.0.181/32\"\n ]\n },\n {\n \"cidr\": \"0.0.0.0/1\",\n \"except\": []\n },\n {\n \"cidr\": \"10.0.0.0/00022\"\n },\n {\n \"cidr\": \"2001:db8::98/127\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"udp\",\n \"port\": 11,\n \"endPort\": 55205\n },\n {\n \"protocol\": \"any\"\n },\n {\n \"port\": 65534\n }\n ]\n },\n {\n \"cidr\": \"2001:db8::196/127\",\n \"protocol\": \"udp\",\n \"port\": 442\n },\n {}\n ]\n}"}],"expected":{"error":"allow[5].to[0].except[1] must be contained in 10.0.0.160/27"}}, + {"name":"random #118","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.0/255.255.252.0\",\n\t\t\t\"except\":[\n\t\t\t\t\"10.0.2.0/23\"\n\t\t\t],\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":7\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"80.0.0.0/4\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":78,\n\t\t\t\t\t\"endPort\":79\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"16.3.149.192/31\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/24\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:03d0/124\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"ec1a:62ea:d900:6520:d038:7f57:a800:0000/104\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"port\":65534,\n\t\t\t\t\t\"endPort\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65531,\n\t\t\t\t\t\"endPort\":65533\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":65531\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::338/125\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"port\":5,\n\t\t\t\t\t\"endPort\":10\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/002\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"138.10.95.187/63.255.255.255\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"152.0.0.0/6\",\n\t\t\t\t\t\t\"137.201.104.135/2\",\n\t\t\t\t\t\t\"128.0.0.0/4\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/255.255.252.0\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":78,\n\t\t\t\t\t\"endPort\":79\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.64/255.255.255.248\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0090/125\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":440,\n\t\t\t\t\t\"endPort\":445\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.144/0030\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.96/255.255.255.248\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"232.165.205.15/1\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"128.0.0.0/1\",\n\t\t\t\t\t\t\"2001:db8::/118\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"240.0.0.0/240.0.0.0\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"::ffff:204.24.92.156/96\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"::ffff:0.0.0.0/96\",\n\t\t\t\t\t\t\"::ffff:0.0.0.0/99\",\n\t\t\t\t\t\t\"::FFFF:0.0.0.0/97\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.192/26\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"deny\":[\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.180/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":82,\n\t\t\t\"endPort\":85\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/23\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.224/29\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":78\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/01\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"78.81.41.194/5\",\n\t\t\t\t\t\t\"64.0.0.0/3\",\n\t\t\t\t\t\t\"70.8.101.243/2\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":445\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":81,\n\t\t\t\t\t\"endPort\":87\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:DB8::240/123\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::1a0/126\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"2001:db8::1a0/128\",\n\t\t\t\t\t\t\"2001:db8::1a0/126\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.204/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":79\n\t\t},\n\t\t{\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":442\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/0\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"128.0.0.0/1\",\n\t\t\t\t\t\t\"116.0.0.0/6\",\n\t\t\t\t\t\t\"0.0.0.0/0\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":443\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":65531\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":7\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":79\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:DB8::300/120\",\n\t\t\t\"except\":[\n\t\t\t\t\"2001:db8::300/121\",\n\t\t\t\t\"2001:db8::340/122\",\n\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:037e/124\"\n\t\t\t],\n\t\t\t\"protocol\":\"udp\"\n\t\t}\n\t]\n}"}],"expected":{"error":"allow[6].to[2].except[1] must be contained in 128.0.0.0/1"}}, + {"name":"random #119","input":[{"text":"{\"allow\":[],\"deny\":[{\"cidr\":\"10.0.0.224/27\",\"protocol\":\"udp\",\"port\":78,\"endPort\":84},{\"cidr\":\"10.0.3.88/29\",\"protocol\":\"udp\",\"port\":444,\"endPort\":38486}]}"}],"expected":{"allow":[],"deny":["10.0.0.224/27:udp:78-84","10.0.3.88/29:udp:444-38486"]}}, + {"name":"random #120","input":[{"text":"{\"deny\": [{\"to\": [{\"cidr\": \"c70b:a38:761e:c36f:6ca4:b19e:aa0:b00/120\"}, {\"cidr\": \"10.0.2.64/031\", \"except\": [\"10.0.2.65\", \"10.0.2.65\", \"10.0.2.64/31\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 80, \"endPort\": 80}, {\"protocol\": \"tcp\", \"port\": 82, \"endPort\": 86}, {\"protocol\": \"icmp\"}]}]}"}],"expected":{"allow":[],"deny":["c70b:a38:761e:c36f:6ca4:b19e:aa0:b00/120:icmp","c70b:a38:761e:c36f:6ca4:b19e:aa0:b00/120:tcp:82-86","c70b:a38:761e:c36f:6ca4:b19e:aa0:b00/120:udp:80"]}}, + {"name":"random #121","input":[{"text":"{\n\t\"allow\": [], \n\t\"deny\": []\n}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #122","input":[{"text":"{\n \"allow\": [\n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::24/126\",\n \"except\": []\n },\n {\n \"cidr\": \"0.24.198.179/004\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"icmp\"\n }\n ]\n }\n ],\n \"deny\": [\n {\n \"to\": [\n {\n \"cidr\": \"2001:DB8::280/121\"\n },\n {\n \"cidr\": \"10.0.0.144/28\",\n \"except\": [\n \"10.0.0.153/255.255.255.254\",\n \"10.0.0.148/31\",\n \"10.0.0.159/255.255.255.254\"\n ]\n },\n {\n \"cidr\": \"1549:d01a:3e62:b620:b907:9f02:98ff:5c98/127\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"icmp\"\n },\n {\n \"protocol\": \"tcp\",\n \"port\": 82,\n \"endPort\": 82\n },\n {\n \"protocol\": \"icmp\"\n }\n ]\n },\n {\n \"to\": [\n {\n \"cidr\": \"48B9:6D55:9EF0:1B64:F56B:5A57:E9D0:A741/001\"\n },\n {\n \"cidr\": \"10.0.1.65\"\n },\n {\n \"cidr\": \"11.84.142.53/4\",\n \"except\": [\n \"5.0.0.0/8\",\n \"12.129.131.112/8\",\n \"0.0.0.0/4\"\n ]\n },\n {\n \"cidr\": \"10.0.3.128/26\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"udp\"\n },\n {\n \"protocol\": \"tcp\",\n \"port\": 65533\n },\n {\n \"port\": 5,\n \"endPort\": 7\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["0.0.0.0/4:icmp","2001:db8::24/126:icmp"],"deny":["10.0.0.144/30:icmp","10.0.0.144/30:tcp:82","10.0.0.150/31:icmp","10.0.0.150/31:tcp:82","10.0.0.154/31:icmp","10.0.0.154/31:tcp:82","10.0.0.156/31:icmp","10.0.0.156/31:tcp:82","10.0.1.65/32:tcp:5-7","10.0.1.65/32:tcp:65533","10.0.1.65/32:udp","10.0.3.128/26:tcp:5-7","10.0.3.128/26:tcp:65533","10.0.3.128/26:udp","::/1:tcp:5-7","::/1:tcp:65533","::/1:udp","1549:d01a:3e62:b620:b907:9f02:98ff:5c98/127:icmp","1549:d01a:3e62:b620:b907:9f02:98ff:5c98/127:tcp:82","2001:db8::280/121:icmp","2001:db8::280/121:tcp:82"]}}, + {"name":"random #123","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::200/120\"\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.14/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 353,\n\t\t\t\"endPort\": 353\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.122/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 77\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.44/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 259\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.76/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 259\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::12a/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 179,\n\t\t\t\"endPort\": 180\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.93/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 255\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.190/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 303\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.12/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 263\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1d1/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 347,\n\t\t\t\"endPort\": 347\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.32/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 335\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::143/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 277\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::31/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 95\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::2c/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 51\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.64/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 381\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.153/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 45\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::97/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 17\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::47/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 215,\n\t\t\t\"endPort\": 219\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.178/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 341\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3be/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 333,\n\t\t\t\"endPort\": 335\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::ff/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 187\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.241/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 69\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::41/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 117\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.200/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 235\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.248/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 203\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.248/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 345\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.92/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 323,\n\t\t\t\"endPort\": 326\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.114/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 351\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.32/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 9\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.238/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 371\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::80/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 325\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.44/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 193\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.56/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 357,\n\t\t\t\"endPort\": 359\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1ed/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 55,\n\t\t\t\"endPort\": 58\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::180/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 381,\n\t\t\t\"endPort\": 381\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.48/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 189\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.222/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 399,\n\t\t\t\"endPort\": 401\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.4/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 237\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.64/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 397,\n\t\t\t\"endPort\": 397\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::254/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 123,\n\t\t\t\"endPort\": 127\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.158/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 91\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.27/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 195\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.189/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 215,\n\t\t\t\"endPort\": 216\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.52/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 335,\n\t\t\t\"endPort\": 335\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.218/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 129\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.80/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 397\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::d4/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 111,\n\t\t\t\"endPort\": 115\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.244/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 107,\n\t\t\t\"endPort\": 111\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.4/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 215\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::394/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 275\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::185/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 173\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::289/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 141\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::154/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 329,\n\t\t\t\"endPort\": 332\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.212/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 379\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.164/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 259\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::65/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 219\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.12/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 399\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.194/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 271\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.118/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 109\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.194/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 233,\n\t\t\t\"endPort\": 237\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.92/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 47\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.108/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 115,\n\t\t\t\"endPort\": 115\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::29b/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 85\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::23f/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 23\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.212/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 101,\n\t\t\t\"endPort\": 103\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::234/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 201,\n\t\t\t\"endPort\": 202\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::241/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 139,\n\t\t\t\"endPort\": 141\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::381/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 27,\n\t\t\t\"endPort\": 30\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.228/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 397,\n\t\t\t\"endPort\": 399\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.191/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 199\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.136/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 135,\n\t\t\t\"endPort\": 135\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::301/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 219\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1f6/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 249,\n\t\t\t\"endPort\": 250\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.69/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 379\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.6/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 65,\n\t\t\t\"endPort\": 66\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.74/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 265\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.168/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 279\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.198/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 325\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.168/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 113\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.9/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 93\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::cb/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 23\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.154/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 195\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.176/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 365,\n\t\t\t\"endPort\": 369\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.24/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 111,\n\t\t\t\"endPort\": 115\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.49/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 239,\n\t\t\t\"endPort\": 240\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::146/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 175,\n\t\t\t\"endPort\": 178\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::3c6/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 251\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.108/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 11,\n\t\t\t\"endPort\": 11\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.54/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 297\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.159/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 181,\n\t\t\t\"endPort\": 183\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.192/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 299\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.148/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 89,\n\t\t\t\"endPort\": 93\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.176/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 121,\n\t\t\t\"endPort\": 124\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::2a4/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 303,\n\t\t\t\"endPort\": 303\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.243/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 229\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.52/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 181\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.184/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 259,\n\t\t\t\"endPort\": 261\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::34a/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 91\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.214/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 139\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.80/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 65\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.126/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 303,\n\t\t\t\"endPort\": 305\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.226/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 49\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.32/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 139,\n\t\t\t\"endPort\": 140\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.128/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 329\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.16/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 141\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.22/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 127,\n\t\t\t\"endPort\": 131\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::327/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 129\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1de/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 239\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.132/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 223\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.182/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 157\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::85/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 309,\n\t\t\t\"endPort\": 309\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::6d/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 189,\n\t\t\t\"endPort\": 192\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.40/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 205\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::f1/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 187\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.227/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 355\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.167/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 183\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.96/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 163,\n\t\t\t\"endPort\": 165\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::a1/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 39\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.144/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 83,\n\t\t\t\"endPort\": 87\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::18e/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 235\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::ed/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 307\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.179/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 283\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.105/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 73,\n\t\t\t\"endPort\": 77\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::352/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 21,\n\t\t\t\"endPort\": 23\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.0/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 99,\n\t\t\t\"endPort\": 100\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.52/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 231\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.236/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 271,\n\t\t\t\"endPort\": 273\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.72/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 339\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.150/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 143\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::f5/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 129\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.30/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 233\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.44/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 247,\n\t\t\t\"endPort\": 247\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.214/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 235\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::2c7/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 65\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.238/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 21,\n\t\t\t\"endPort\": 25\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.44/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 265,\n\t\t\t\"endPort\": 267\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.88/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 185\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.184/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 347\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.134/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 399\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.174/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 179,\n\t\t\t\"endPort\": 182\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.142/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 283\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.36/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 155\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.10/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 125\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.14/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 261\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.188/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 117\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.140/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 31\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.174/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 175\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::fd/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 141,\n\t\t\t\"endPort\": 145\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::70/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 325\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::12d/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 137\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::159/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 285,\n\t\t\t\"endPort\": 285\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.67/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 389,\n\t\t\t\"endPort\": 391\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.176/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 29\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.8/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 221,\n\t\t\t\"endPort\": 221\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.42/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 251\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::119/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 93\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.97/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 327\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.0.126/31\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 215\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::21d/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 361\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.54/31\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 179\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.60/32\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 5\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.80/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 337\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.192/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 135\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.176/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 91\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.201/32\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 71\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::388/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 201\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::8e/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 327\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.143/32\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 59\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.0/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 239\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::12d/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 297,\n\t\t\t\"endPort\": 299\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::73/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 307\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.10/31\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 95\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::e0/128\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 139\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::353/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 167,\n\t\t\t\"endPort\": 168\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.120/30\",\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 139,\n\t\t\t\"endPort\": 139\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1e4/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 35\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.12/30\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 209,\n\t\t\t\"endPort\": 210\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.8/30\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 275,\n\t\t\t\"endPort\": 279\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::f2/128\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 145\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::338/128\",\n\t\t\t\"protocol\": \"udp\",\n\t\t\t\"port\": 391\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.128/027\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.195/22\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 2\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 65534,\n\t\t\t\t\t\"endPort\": 65534\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.0/118\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.32/27\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":["10.0.0.8/31:tcp:221","10.0.0.10/32:tcp:125","10.0.0.10/32:udp:125","10.0.0.14/32:udp:261","10.0.0.14/32:udp:353","10.0.0.22/31:tcp:127-131","10.0.0.22/31:udp:127-131","10.0.0.32/30:udp:335","10.0.0.44/30:udp:265-267","10.0.0.52/30:tcp:181","10.0.0.52/30:udp:181","10.0.0.52/31:tcp:231","10.0.0.54/32:tcp:297","10.0.0.54/32:udp:297","10.0.0.74/32:tcp:265","10.0.0.74/32:udp:265","10.0.0.80/30:tcp:397","10.0.0.108/32:tcp:11","10.0.0.126/31:udp:215","10.0.0.150/31:tcp:143","10.0.0.153/32:tcp:45","10.0.0.153/32:udp:45","10.0.0.158/32:udp:91","10.0.0.168/30:udp:113","10.0.0.174/31:tcp:179-182","10.0.0.174/31:udp:179-182","10.0.0.174/32:tcp:175","10.0.0.176/30:tcp:121-124","10.0.0.176/30:udp:29","10.0.0.176/30:udp:121-124","10.0.0.179/32:udp:283","10.0.0.194/32:udp:271","10.0.0.200/31:tcp:235","10.0.0.200/31:udp:235","10.0.0.212/30:tcp:101-103","10.0.0.212/32:tcp:379","10.0.0.212/32:udp:379","10.0.0.218/32:udp:129","10.0.0.226/31:tcp:49","10.0.1.0/30:udp:99-100","10.0.1.10/31:tcp:95","10.0.1.16/32:tcp:141","10.0.1.16/32:udp:141","10.0.1.24/30:tcp:111-115","10.0.1.24/30:udp:111-115","10.0.1.42/31:udp:251","10.0.1.44/30:tcp:193","10.0.1.44/30:udp:193","10.0.1.48/32:tcp:189","10.0.1.48/32:udp:189","10.0.1.60/32:udp:5","10.0.1.72/30:udp:339","10.0.1.114/32:tcp:351","10.0.1.132/31:udp:223","10.0.1.134/32:udp:399","10.0.1.140/30:udp:31","10.0.1.142/31:tcp:283","10.0.1.142/31:udp:283","10.0.1.144/30:udp:83-87","10.0.1.148/31:tcp:89-93","10.0.1.182/32:udp:157","10.0.1.184/31:tcp:347","10.0.1.184/31:udp:347","10.0.1.191/32:tcp:199","10.0.1.192/31:tcp:135","10.0.1.192/32:tcp:299","10.0.1.201/32:tcp:71","10.0.1.201/32:udp:71","10.0.1.227/32:udp:355","10.0.1.241/32:udp:69","10.0.1.244/31:tcp:107-111","10.0.2.0/30:tcp:239","10.0.2.6/32:udp:65-66","10.0.2.12/30:udp:399","10.0.2.12/32:tcp:263","10.0.2.12/32:udp:263","10.0.2.27/32:tcp:195","10.0.2.30/31:tcp:233","10.0.2.32/32:tcp:139-140","10.0.2.32/32:udp:139-140","10.0.2.49/32:tcp:239-240","10.0.2.49/32:udp:239-240","10.0.2.54/31:tcp:179","10.0.2.54/31:udp:179","10.0.2.56/30:udp:357-359","10.0.2.64/31:tcp:397","10.0.2.80/30:tcp:337","10.0.2.80/30:udp:337","10.0.2.93/32:tcp:255","10.0.2.97/32:tcp:327","10.0.2.105/32:tcp:73-77","10.0.2.108/32:tcp:115","10.0.2.108/32:udp:115","10.0.2.118/31:tcp:109","10.0.2.120/30:tcp:139","10.0.2.120/30:udp:139","10.0.2.143/32:tcp:59","10.0.2.154/32:udp:195","10.0.2.159/32:udp:181-183","10.0.2.164/30:udp:259","10.0.2.168/30:tcp:279","10.0.2.178/31:tcp:341","10.0.2.178/31:udp:341","10.0.2.184/31:tcp:259-261","10.0.2.214/31:tcp:139","10.0.2.222/32:tcp:399-401","10.0.2.228/32:tcp:397-399","10.0.2.228/32:udp:397-399","10.0.2.236/32:tcp:271-273","10.0.2.238/32:tcp:371","10.0.2.238/32:udp:371","10.0.2.243/32:tcp:229","10.0.2.248/30:tcp:203","10.0.3.4/30:tcp:237","10.0.3.4/30:udp:237","10.0.3.4/31:udp:215","10.0.3.8/30:udp:275-279","10.0.3.9/32:tcp:93","10.0.3.9/32:udp:93","10.0.3.12/30:tcp:209-210","10.0.3.32/31:tcp:9","10.0.3.36/31:tcp:155","10.0.3.36/31:udp:155","10.0.3.40/32:tcp:205","10.0.3.40/32:udp:205","10.0.3.44/31:tcp:259","10.0.3.44/31:udp:247","10.0.3.52/31:tcp:335","10.0.3.64/30:udp:381","10.0.3.67/32:tcp:389-391","10.0.3.67/32:udp:389-391","10.0.3.69/32:udp:379","10.0.3.76/30:tcp:259","10.0.3.80/32:udp:65","10.0.3.88/30:tcp:185","10.0.3.88/30:udp:185","10.0.3.92/30:tcp:323-326","10.0.3.92/30:udp:323-326","10.0.3.92/31:tcp:47","10.0.3.92/31:udp:47","10.0.3.96/30:udp:163-165","10.0.3.122/32:tcp:77","10.0.3.126/32:tcp:303-305","10.0.3.126/32:udp:303-305","10.0.3.128/31:udp:329","10.0.3.136/31:tcp:135","10.0.3.136/31:udp:135","10.0.3.167/32:udp:183","10.0.3.176/30:tcp:91","10.0.3.176/30:udp:91","10.0.3.176/31:tcp:365-369","10.0.3.188/31:tcp:117","10.0.3.189/32:tcp:215-216","10.0.3.189/32:udp:215-216","10.0.3.190/31:tcp:303","10.0.3.190/31:udp:303","10.0.3.194/31:tcp:233-237","10.0.3.194/31:udp:233-237","10.0.3.198/32:tcp:325","10.0.3.198/32:udp:325","10.0.3.214/31:udp:235","10.0.3.238/32:tcp:21-25","10.0.3.248/31:tcp:345","10.0.3.248/31:udp:345","2001:db8::2c/128:tcp:51","2001:db8::31/128:tcp:95","2001:db8::31/128:udp:95","2001:db8::41/128:udp:117","2001:db8::47/128:tcp:215-219","2001:db8::65/128:udp:219","2001:db8::6d/128:udp:189-192","2001:db8::70/128:udp:325","2001:db8::73/128:tcp:307","2001:db8::80/128:tcp:325","2001:db8::85/128:udp:309","2001:db8::8e/128:udp:327","2001:db8::97/128:udp:17","2001:db8::a1/128:tcp:39","2001:db8::cb/128:udp:23","2001:db8::d4/128:tcp:111-115","2001:db8::d4/128:udp:111-115","2001:db8::e0/128:tcp:139","2001:db8::e0/128:udp:139","2001:db8::ed/128:tcp:307","2001:db8::f1/128:udp:187","2001:db8::f2/128:tcp:145","2001:db8::f5/128:tcp:129","2001:db8::f5/128:udp:129","2001:db8::fd/128:tcp:141-145","2001:db8::ff/128:udp:187","2001:db8::119/128:udp:93","2001:db8::12a/128:tcp:179-180","2001:db8::12d/128:tcp:297-299","2001:db8::12d/128:udp:137","2001:db8::12d/128:udp:297-299","2001:db8::143/128:udp:277","2001:db8::146/128:tcp:175-178","2001:db8::146/128:udp:175-178","2001:db8::154/128:tcp:329-332","2001:db8::154/128:udp:329-332","2001:db8::159/128:tcp:285","2001:db8::159/128:udp:285","2001:db8::180/128:tcp:381","2001:db8::185/128:tcp:173","2001:db8::18e/128:udp:235","2001:db8::1d1/128:tcp:347","2001:db8::1de/128:tcp:239","2001:db8::1de/128:udp:239","2001:db8::1e4/128:tcp:35","2001:db8::1ed/128:tcp:55-58","2001:db8::1ed/128:udp:55-58","2001:db8::1f6/128:tcp:249-250","2001:db8::200/120","2001:db8::301/128:udp:219","2001:db8::327/128:tcp:129","2001:db8::327/128:udp:129","2001:db8::338/128:udp:391","2001:db8::34a/128:tcp:91","2001:db8::34a/128:udp:91","2001:db8::352/128:tcp:21-23","2001:db8::353/128:udp:167-168","2001:db8::381/128:udp:27-30","2001:db8::388/128:tcp:201","2001:db8::388/128:udp:201","2001:db8::394/128:udp:275","2001:db8::3be/128:udp:333-335","2001:db8::3c6/128:tcp:251"],"deny":["10.0.0.0/22:tcp:2","10.0.0.0/22:tcp:65534","10.0.1.32/27","2001:db8::/118"]}}, + {"name":"random #124","input":[{"text":"{"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"random #125","input":[{"text":"{\"allow\": [], \"deny\": [{\"to\": [{\"cidr\": \"10.0.0.36/31\"}, {\"cidr\": \"10.0.0.102/0.0.0.127\"}, {\"cidr\": \"10.0.0.80/29\"}]}]}"}],"expected":{"allow":[],"deny":["10.0.0.0/25"]}}, + {"name":"random #126","input":[{"text":"{}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #127","input":[{"text":"{\"deny\": []}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #128","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"1286:2A2A:62E1:EC1C:F009:22E1:C571:403/3\"}, {\"cidr\": \"52.159.199.254/12\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 80, \"endPort\": 81}, {\"protocol\": \"udp\"}]}, {\"cidr\": \"10.0.1.212/30\", \"protocol\": \"tcp\", \"port\": 12, \"endPort\": 18}, {\"to\": [{\"cidr\": \"21ff:1592:4d4d:bf8c::/64\"}, {\"cidr\": \"10.0.2.192/26\"}, {\"cidr\": \"10.0.0.220/255.255.255.252\", \"except\": [\"10.0.0.222/31\", \"10.0.0.220/30\", \"10.0.0.221/32\"]}, {\"cidr\": \"10.0.2.32/27\"}], \"ports\": [{\"port\": 79}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"2001:db8::249/119\", \"except\": []}, {\"cidr\": \"10.0.3.0/24\"}, {\"cidr\": \"10.0.3.0/255.255.255.0\"}, {\"cidr\": \"219.16.0.0/16\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65535, \"endPort\": 65535}]}, {\"cidr\": \"21.48.29.119\", \"except\": [\"21.48.29.119\", \"21.48.29.119/32\", \"21.48.29.119\"], \"protocol\": \"tcp\"}, {\"to\": [{\"cidr\": \"76.127.0.0/16\"}]}], \"deny\": [{\"to\": [{\"cidr\": \"2001:db8::1c0/122\"}, {\"cidr\": \"bb1:d489:ec25:33f2:683e:37ed:cac8:7e06/127\"}, {\"cidr\": \"2001:db8::1c8/125\"}]}, {\"to\": [{\"cidr\": \"10.0.0.0/22\", \"except\": [\"10.0.2.0/24\", \"10.0.0.192/28\"]}, {\"cidr\": \"2001:db8::353/127\"}, {\"cidr\": \"::/0\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 79, \"endPort\": 83}, {\"protocol\": \"udp\", \"port\": 81}, {\"protocol\": \"tcp\", \"port\": 65535, \"endPort\": 65535}, {\"protocol\": \"tcp\", \"port\": 78, \"endPort\": 79}]}, {\"to\": [{\"cidr\": \"192.0.2.0/24\", \"except\": [\"10.0.0.0/8\"]}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"tcp\"}]}, {\"cidr\": \"10.0.1.222/255.255.252.0\", \"protocol\": \"tcp\", \"port\": 82, \"endPort\": 85}, {\"to\": [{\"cidr\": \"10.0.2.0/24\"}, {\"cidr\": \"2001:db8::200/120\"}, {\"cidr\": \"10.0.0.0/25\"}, {\"cidr\": \"10.0.2.192/26\"}], \"ports\": [{}, {\"protocol\": \"tcp\", \"port\": 2}, {\"protocol\": \"any\", \"port\": 82}]}, {\"to\": [{\"cidr\": \"2001:db8::340/126\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65530, \"endPort\": 65535}, {\"protocol\": \"tcp\"}, {\"protocol\": \"tcp\", \"port\": 65534, \"endPort\": 65535}, {\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"10.0.1.224/27\"}, {\"cidr\": \"130.112.0.0/0.15.255.255\", \"except\": [\"130.126.68.135/18\", \"130.127.64.130/17\", \"130.114.195.186/255.255.0.0\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/121\"}], \"ports\": [{\"protocol\": \"tcp\"}]}, {\"cidr\": \"0.0.0.0/0\", \"protocol\": \"tcp\"}]}"}],"expected":{"error":"deny[2].to[0].except[0] must be contained in 192.0.2.0/24"}}, + {"name":"random #129","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.2.128/25\"}, {\"cidr\": \"2001:db8::228/125\", \"except\": [\"2001:db8::228/0125\"]}, {\"cidr\": \"234.233.242.109/32\", \"except\": []}]}], \"deny\": [{\"to\": [{\"cidr\": \"4c2d:a949:f33a:cd0d:878e:ef5:b400:0/104\"}]}, {\"ports\": [{\"port\": 445}]}, {\"cidr\": \"10.0.0.0/22\", \"except\": [\"10.0.3.0/26\", \"10.0.2.12/25\"], \"protocol\": \"udp\"}, {\"cidr\": \"10.0.3.244/31\", \"protocol\": \"udp\", \"port\": 5, \"endPort\": 7}, {\"cidr\": \"10.0.1.254/31\", \"protocol\": \"udp\", \"port\": 269}, {\"cidr\": \"2001:db8::2f3/128\", \"protocol\": \"tcp\", \"port\": 187, \"endPort\": 189}, {\"cidr\": \"10.0.1.184/31\", \"protocol\": \"tcp\", \"port\": 35}, {\"cidr\": \"10.0.2.248/30\", \"protocol\": \"udp\", \"port\": 137}, {\"cidr\": \"2001:db8::19f/128\", \"protocol\": \"any\", \"port\": 327, \"endPort\": 329}, {\"cidr\": \"10.0.1.128/30\", \"protocol\": \"tcp\", \"port\": 27}, {\"cidr\": \"10.0.3.112/30\", \"protocol\": \"any\", \"port\": 239, \"endPort\": 242}, {\"cidr\": \"10.0.2.74/32\", \"protocol\": \"any\", \"port\": 361}, {\"cidr\": \"10.0.2.65/32\", \"protocol\": \"any\", \"port\": 289}, {\"cidr\": \"10.0.2.172/32\", \"protocol\": \"any\", \"port\": 237, \"endPort\": 239}, {\"cidr\": \"10.0.0.234/31\", \"protocol\": \"udp\", \"port\": 223}, {\"cidr\": \"10.0.1.54/31\", \"protocol\": \"udp\", \"port\": 167}, {\"cidr\": \"10.0.1.244/31\", \"protocol\": \"tcp\", \"port\": 169}, {\"cidr\": \"2001:db8::d4/128\", \"protocol\": \"any\", \"port\": 283, \"endPort\": 284}, {\"cidr\": \"10.0.3.72/31\", \"protocol\": \"any\", \"port\": 315}, {\"cidr\": \"10.0.0.4/30\", \"protocol\": \"udp\", \"port\": 229}, {\"cidr\": \"10.0.0.38/31\", \"protocol\": \"tcp\", \"port\": 339}, {\"cidr\": \"10.0.0.152/30\", \"protocol\": \"tcp\", \"port\": 281, \"endPort\": 281}, {\"cidr\": \"10.0.1.14/31\", \"protocol\": \"tcp\", \"port\": 223, \"endPort\": 226}, {\"cidr\": \"10.0.1.42/31\", \"protocol\": \"udp\", \"port\": 29}, {\"cidr\": \"10.0.1.20/32\", \"protocol\": \"udp\", \"port\": 125}, {\"cidr\": \"10.0.2.119/32\", \"protocol\": \"udp\", \"port\": 51}, {\"cidr\": \"10.0.2.156/30\", \"protocol\": \"udp\", \"port\": 179}, {\"cidr\": \"2001:db8::c5/128\", \"protocol\": \"any\", \"port\": 155}, {\"cidr\": \"10.0.2.168/30\", \"protocol\": \"any\", \"port\": 5, \"endPort\": 6}, {\"cidr\": \"10.0.1.88/32\", \"protocol\": \"udp\", \"port\": 137, \"endPort\": 141}, {\"cidr\": \"10.0.3.32/30\", \"protocol\": \"tcp\", \"port\": 167}, {\"cidr\": \"2001:db8::135/128\", \"protocol\": \"udp\", \"port\": 51}, {\"cidr\": \"10.0.2.180/32\", \"protocol\": \"udp\", \"port\": 381}, {\"cidr\": \"10.0.3.98/31\", \"protocol\": \"tcp\", \"port\": 121}, {\"cidr\": \"10.0.2.216/30\", \"protocol\": \"tcp\", \"port\": 293}, {\"cidr\": \"10.0.2.108/32\", \"protocol\": \"any\", \"port\": 387}, {\"cidr\": \"2001:db8::2b5/128\", \"protocol\": \"tcp\", \"port\": 265, \"endPort\": 266}, {\"cidr\": \"2001:db8::332/128\", \"protocol\": \"any\", \"port\": 57, \"endPort\": 58}, {\"cidr\": \"10.0.3.32/30\", \"protocol\": \"udp\", \"port\": 83}, {\"cidr\": \"10.0.2.250/31\", \"protocol\": \"any\", \"port\": 303, \"endPort\": 305}, {\"cidr\": \"10.0.3.66/32\", \"protocol\": \"any\", \"port\": 177, \"endPort\": 181}, {\"cidr\": \"10.0.2.244/30\", \"protocol\": \"tcp\", \"port\": 339}, {\"cidr\": \"10.0.3.60/30\", \"protocol\": \"any\", \"port\": 215, \"endPort\": 216}, {\"cidr\": \"10.0.3.41/32\", \"protocol\": \"tcp\", \"port\": 57, \"endPort\": 59}, {\"cidr\": \"10.0.2.151/32\", \"protocol\": \"any\", \"port\": 207}, {\"cidr\": \"10.0.0.144/32\", \"protocol\": \"udp\", \"port\": 183, \"endPort\": 185}, {\"cidr\": \"10.0.1.144/31\", \"protocol\": \"any\", \"port\": 197, \"endPort\": 198}, {\"cidr\": \"10.0.2.143/32\", \"protocol\": \"tcp\", \"port\": 213, \"endPort\": 217}, {\"cidr\": \"10.0.0.126/32\", \"protocol\": \"tcp\", \"port\": 305}, {\"cidr\": \"2001:db8::368/128\", \"protocol\": \"tcp\", \"port\": 331, \"endPort\": 333}, {\"cidr\": \"2001:db8::33/128\", \"protocol\": \"any\", \"port\": 171}, {\"cidr\": \"2001:db8::283/128\", \"protocol\": \"any\", \"port\": 355}, {\"cidr\": \"10.0.2.150/31\", \"protocol\": \"tcp\", \"port\": 329}, {\"cidr\": \"10.0.1.178/31\", \"protocol\": \"any\", \"port\": 375, \"endPort\": 378}, {\"cidr\": \"2001:db8::2f5/128\", \"protocol\": \"udp\", \"port\": 311, \"endPort\": 313}, {\"cidr\": \"2001:db8::291/128\", \"protocol\": \"any\", \"port\": 65, \"endPort\": 66}, {\"cidr\": \"10.0.1.144/30\", \"protocol\": \"tcp\", \"port\": 229}, {\"cidr\": \"10.0.2.228/32\", \"protocol\": \"udp\", \"port\": 43, \"endPort\": 43}, {\"cidr\": \"10.0.2.120/30\", \"protocol\": \"tcp\", \"port\": 225}, {\"cidr\": \"2001:db8::3b/128\", \"protocol\": \"any\", \"port\": 329, \"endPort\": 330}, {\"cidr\": \"10.0.0.252/30\", \"protocol\": \"udp\", \"port\": 95}, {\"cidr\": \"2001:db8::e6/128\", \"protocol\": \"tcp\", \"port\": 253}, {\"cidr\": \"10.0.2.160/30\", \"protocol\": \"any\", \"port\": 1}, {\"cidr\": \"10.0.2.106/31\", \"protocol\": \"any\", \"port\": 297}, {\"cidr\": \"2001:db8::1cf/128\", \"protocol\": \"any\", \"port\": 171}, {\"cidr\": \"10.0.0.244/31\", \"protocol\": \"tcp\", \"port\": 305}, {\"cidr\": \"2001:db8::8/128\", \"protocol\": \"any\", \"port\": 343}, {\"cidr\": \"10.0.3.112/31\", \"protocol\": \"any\", \"port\": 133}, {\"cidr\": \"2001:db8::1b8/128\", \"protocol\": \"any\", \"port\": 291}, {\"cidr\": \"10.0.2.198/32\", \"protocol\": \"tcp\", \"port\": 275, \"endPort\": 279}, {\"cidr\": \"10.0.1.180/31\", \"protocol\": \"tcp\", \"port\": 43, \"endPort\": 46}, {\"cidr\": \"10.0.0.164/30\", \"protocol\": \"tcp\", \"port\": 265}, {\"cidr\": \"2001:db8::3fe/128\", \"protocol\": \"any\", \"port\": 143}, {\"cidr\": \"2001:db8::2fb/128\", \"protocol\": \"udp\", \"port\": 41}, {\"cidr\": \"10.0.1.134/32\", \"protocol\": \"any\", \"port\": 107}, {\"cidr\": \"10.0.1.128/31\", \"protocol\": \"tcp\", \"port\": 259}, {\"cidr\": \"2001:db8::2ac/128\", \"protocol\": \"tcp\", \"port\": 71}, {\"cidr\": \"2001:db8::295/128\", \"protocol\": \"udp\", \"port\": 151, \"endPort\": 153}, {\"cidr\": \"10.0.1.176/31\", \"protocol\": \"any\", \"port\": 331}, {\"cidr\": \"10.0.2.144/31\", \"protocol\": \"udp\", \"port\": 95, \"endPort\": 96}, {\"cidr\": \"2001:db8::399/128\", \"protocol\": \"tcp\", \"port\": 357}, {\"cidr\": \"10.0.1.154/32\", \"protocol\": \"udp\", \"port\": 73}, {\"cidr\": \"10.0.3.153/32\", \"protocol\": \"any\", \"port\": 47, \"endPort\": 48}, {\"cidr\": \"2001:db8::8c/128\", \"protocol\": \"any\", \"port\": 91}, {\"cidr\": \"10.0.3.60/30\", \"protocol\": \"tcp\", \"port\": 225}, {\"cidr\": \"10.0.1.46/31\", \"protocol\": \"tcp\", \"port\": 5, \"endPort\": 8}, {\"cidr\": \"10.0.3.204/30\", \"protocol\": \"udp\", \"port\": 199, \"endPort\": 203}, {\"cidr\": \"10.0.2.52/32\", \"protocol\": \"any\", \"port\": 325}, {\"cidr\": \"10.0.1.104/31\", \"protocol\": \"tcp\", \"port\": 389}, {\"cidr\": \"10.0.1.252/30\", \"protocol\": \"udp\", \"port\": 109, \"endPort\": 112}, {\"cidr\": \"10.0.0.6/31\", \"protocol\": \"udp\", \"port\": 265, \"endPort\": 266}, {\"cidr\": \"10.0.2.154/31\", \"protocol\": \"any\", \"port\": 389}, {\"cidr\": \"10.0.2.146/31\", \"protocol\": \"any\", \"port\": 379}, {\"cidr\": \"10.0.2.42/31\", \"protocol\": \"udp\", \"port\": 1}, {\"cidr\": \"2001:db8::e3/128\", \"protocol\": \"udp\", \"port\": 75}, {\"cidr\": \"10.0.0.2/31\", \"protocol\": \"tcp\", \"port\": 295}, {\"cidr\": \"2001:db8::214/128\", \"protocol\": \"any\", \"port\": 71}, {\"cidr\": \"10.0.0.136/30\", \"protocol\": \"udp\", \"port\": 45}, {\"cidr\": \"10.0.1.12/30\", \"protocol\": \"tcp\", \"port\": 345, \"endPort\": 345}, {\"cidr\": \"2001:db8::7b/128\", \"protocol\": \"tcp\", \"port\": 231}, {\"cidr\": \"2001:db8::38a/128\", \"protocol\": \"udp\", \"port\": 15, \"endPort\": 16}, {\"cidr\": \"10.0.3.0/31\", \"protocol\": \"udp\", \"port\": 293, \"endPort\": 294}, {\"cidr\": \"10.0.3.173/32\", \"protocol\": \"any\", \"port\": 87, \"endPort\": 89}, {\"cidr\": \"10.0.1.12/30\", \"protocol\": \"any\", \"port\": 169}, {\"cidr\": \"10.0.1.220/32\", \"protocol\": \"any\", \"port\": 159, \"endPort\": 162}, {\"cidr\": \"10.0.2.60/30\", \"protocol\": \"tcp\", \"port\": 275, \"endPort\": 276}, {\"cidr\": \"10.0.1.12/31\", \"protocol\": \"udp\", \"port\": 41, \"endPort\": 41}, {\"cidr\": \"10.0.2.83/32\", \"protocol\": \"udp\", \"port\": 53}, {\"cidr\": \"10.0.0.133/32\", \"protocol\": \"tcp\", \"port\": 127, \"endPort\": 131}, {\"cidr\": \"2001:db8::1f1/128\", \"protocol\": \"any\", \"port\": 111, \"endPort\": 113}, {\"cidr\": \"10.0.1.52/30\", \"protocol\": \"any\", \"port\": 377}, {\"cidr\": \"2001:db8::118/128\", \"protocol\": \"tcp\", \"port\": 335}, {\"cidr\": \"10.0.0.124/30\", \"protocol\": \"udp\", \"port\": 325}, {\"cidr\": \"2001:db8::1e7/128\", \"protocol\": \"udp\", \"port\": 5, \"endPort\": 8}, {\"cidr\": \"10.0.3.120/32\", \"protocol\": \"tcp\", \"port\": 359}, {\"cidr\": \"10.0.2.44/30\", \"protocol\": \"udp\", \"port\": 239}, {\"cidr\": \"2001:db8::12b/128\", \"protocol\": \"tcp\", \"port\": 1, \"endPort\": 5}, {\"cidr\": \"10.0.2.64/31\", \"protocol\": \"tcp\", \"port\": 195}, {\"cidr\": \"10.0.0.20/30\", \"protocol\": \"any\", \"port\": 377}, {\"cidr\": \"2001:db8::3d6/128\", \"protocol\": \"any\", \"port\": 347}, {\"cidr\": \"2001:db8::34a/128\", \"protocol\": \"udp\", \"port\": 59}, {\"cidr\": \"2001:db8::242/128\", \"protocol\": \"any\", \"port\": 145}, {\"cidr\": \"2001:db8::b5/128\", \"protocol\": \"udp\", \"port\": 347}, {\"cidr\": \"10.0.3.124/30\", \"protocol\": \"tcp\", \"port\": 123}, {\"cidr\": \"10.0.2.180/31\", \"protocol\": \"tcp\", \"port\": 297}, {\"cidr\": \"10.0.0.56/32\", \"protocol\": \"udp\", \"port\": 247}, {\"cidr\": \"10.0.1.152/30\", \"protocol\": \"tcp\", \"port\": 367}, {\"cidr\": \"10.0.1.52/32\", \"protocol\": \"any\", \"port\": 121}, {\"cidr\": \"2001:db8::20c/128\", \"protocol\": \"any\", \"port\": 103}, {\"cidr\": \"2001:db8::115/128\", \"protocol\": \"udp\", \"port\": 341, \"endPort\": 342}, {\"cidr\": \"10.0.1.84/30\", \"protocol\": \"udp\", \"port\": 27, \"endPort\": 31}, {\"cidr\": \"10.0.3.94/32\", \"protocol\": \"any\", \"port\": 303}, {\"cidr\": \"10.0.0.60/30\", \"protocol\": \"udp\", \"port\": 75}, {\"cidr\": \"2001:db8::e1/128\", \"protocol\": \"tcp\", \"port\": 389}, {\"cidr\": \"2001:db8::1ad/128\", \"protocol\": \"any\", \"port\": 161}, {\"cidr\": \"10.0.1.99/32\", \"protocol\": \"tcp\", \"port\": 87}, {\"cidr\": \"10.0.1.124/30\", \"protocol\": \"udp\", \"port\": 227}, {\"cidr\": \"10.0.1.28/30\", \"protocol\": \"udp\", \"port\": 203, \"endPort\": 207}, {\"cidr\": \"10.0.3.20/30\", \"protocol\": \"any\", \"port\": 383}, {\"cidr\": \"2001:db8::2db/128\", \"protocol\": \"tcp\", \"port\": 15}, {\"cidr\": \"2001:db8::3d3/128\", \"protocol\": \"any\", \"port\": 75, \"endPort\": 76}, {\"cidr\": \"2001:db8::e1/128\", \"protocol\": \"tcp\", \"port\": 371, \"endPort\": 375}, {\"cidr\": \"10.0.0.186/32\", \"protocol\": \"any\", \"port\": 211, \"endPort\": 214}, {\"cidr\": \"10.0.3.157/32\", \"protocol\": \"udp\", \"port\": 243, \"endPort\": 244}, {\"cidr\": \"2001:db8::152/128\", \"protocol\": \"tcp\", \"port\": 221, \"endPort\": 225}, {\"cidr\": \"2001:db8::203/128\", \"protocol\": \"any\", \"port\": 359}, {\"cidr\": \"10.0.2.109/32\", \"protocol\": \"tcp\", \"port\": 7}, {\"cidr\": \"10.0.1.164/31\", \"protocol\": \"tcp\", \"port\": 51}, {\"cidr\": \"10.0.3.156/32\", \"protocol\": \"tcp\", \"port\": 123}, {\"cidr\": \"10.0.3.228/30\", \"protocol\": \"udp\", \"port\": 321}, {\"cidr\": \"2001:db8::4/128\", \"protocol\": \"tcp\", \"port\": 161}, {\"cidr\": \"10.0.3.156/30\", \"protocol\": \"any\", \"port\": 243}, {\"cidr\": \"10.0.3.162/32\", \"protocol\": \"udp\", \"port\": 395, \"endPort\": 398}, {\"cidr\": \"10.0.1.28/30\", \"protocol\": \"tcp\", \"port\": 375}, {\"cidr\": \"10.0.3.50/32\", \"protocol\": \"any\", \"port\": 223, \"endPort\": 226}, {\"cidr\": \"10.0.2.171/32\", \"protocol\": \"tcp\", \"port\": 145}, {\"cidr\": \"10.0.2.42/32\", \"protocol\": \"tcp\", \"port\": 11, \"endPort\": 15}, {\"cidr\": \"10.0.1.200/32\", \"protocol\": \"udp\", \"port\": 73}, {\"cidr\": \"10.0.0.211/32\", \"protocol\": \"tcp\", \"port\": 345}, {\"cidr\": \"10.0.1.212/30\", \"protocol\": \"udp\", \"port\": 357}, {\"cidr\": \"10.0.3.214/31\", \"protocol\": \"tcp\", \"port\": 141, \"endPort\": 145}, {\"cidr\": \"10.0.3.242/31\", \"protocol\": \"any\", \"port\": 41}, {\"cidr\": \"2001:db8::361/128\", \"protocol\": \"tcp\", \"port\": 5, \"endPort\": 9}, {\"cidr\": \"10.0.2.70/32\", \"protocol\": \"tcp\", \"port\": 155, \"endPort\": 156}, {\"cidr\": \"2001:db8::285/128\", \"protocol\": \"udp\", \"port\": 341, \"endPort\": 343}, {\"cidr\": \"2001:db8::1f2/128\", \"protocol\": \"any\", \"port\": 131}, {\"cidr\": \"10.0.3.184/30\", \"protocol\": \"udp\", \"port\": 393}, {\"cidr\": \"2001:db8::25c/128\", \"protocol\": \"any\", \"port\": 33}, {\"cidr\": \"2001:db8::7c/128\", \"protocol\": \"udp\", \"port\": 135}, {\"cidr\": \"2001:db8::80/128\", \"protocol\": \"udp\", \"port\": 391}, {\"cidr\": \"10.0.0.53/32\", \"protocol\": \"tcp\", \"port\": 351}, {\"cidr\": \"10.0.2.20/30\", \"protocol\": \"tcp\", \"port\": 77}, {\"cidr\": \"10.0.3.212/31\", \"protocol\": \"any\", \"port\": 379}, {\"cidr\": \"10.0.1.210/31\", \"protocol\": \"udp\", \"port\": 141}, {\"cidr\": \"2001:db8::3a2/128\", \"protocol\": \"udp\", \"port\": 365}, {\"cidr\": \"10.0.3.124/30\", \"protocol\": \"any\", \"port\": 43}, {\"cidr\": \"2001:db8::125/128\", \"protocol\": \"udp\", \"port\": 315, \"endPort\": 319}, {\"cidr\": \"10.0.1.44/30\", \"protocol\": \"any\", \"port\": 159}, {\"cidr\": \"10.0.1.224/32\", \"protocol\": \"any\", \"port\": 379}, {\"cidr\": \"10.0.0.196/31\", \"protocol\": \"tcp\", \"port\": 113, \"endPort\": 115}, {\"cidr\": \"2001:db8::d7/128\", \"protocol\": \"any\", \"port\": 21}, {\"cidr\": \"10.0.0.136/30\", \"protocol\": \"any\", \"port\": 335, \"endPort\": 339}, {\"cidr\": \"10.0.1.224/30\", \"protocol\": \"tcp\", \"port\": 171, \"endPort\": 173}, {\"cidr\": \"10.0.3.13/32\", \"protocol\": \"udp\", \"port\": 203}, {\"cidr\": \"2001:db8::238/128\", \"protocol\": \"udp\", \"port\": 311, \"endPort\": 315}, {\"cidr\": \"2001:db8::129/128\", \"protocol\": \"any\", \"port\": 391}, {\"cidr\": \"10.0.1.72/31\", \"protocol\": \"udp\", \"port\": 105, \"endPort\": 106}, {\"cidr\": \"10.0.0.123/32\", \"protocol\": \"any\", \"port\": 369}, {\"cidr\": \"10.0.1.252/31\", \"protocol\": \"any\", \"port\": 377}, {\"cidr\": \"10.0.2.224/31\", \"protocol\": \"any\", \"port\": 49, \"endPort\": 49}, {\"cidr\": \"10.0.0.114/31\", \"protocol\": \"tcp\", \"port\": 197}, {\"cidr\": \"10.0.0.104/30\", \"protocol\": \"tcp\", \"port\": 215}, {\"cidr\": \"10.0.3.28/30\", \"protocol\": \"udp\", \"port\": 103}, {\"cidr\": \"2001:db8::3f8/128\", \"protocol\": \"udp\", \"port\": 69}, {\"cidr\": \"10.0.3.233/32\", \"protocol\": \"any\", \"port\": 311}, {\"cidr\": \"2001:db8::1d8/128\", \"protocol\": \"any\", \"port\": 133}, {\"cidr\": \"2001:db8::3e7/128\", \"protocol\": \"tcp\", \"port\": 201, \"endPort\": 201}, {\"cidr\": \"10.0.2.122/32\", \"protocol\": \"any\", \"port\": 93}, {\"cidr\": \"10.0.2.156/30\", \"protocol\": \"any\", \"port\": 91}, {\"cidr\": \"2001:db8::249/128\", \"protocol\": \"udp\", \"port\": 9}, {\"cidr\": \"2001:db8::36c/128\", \"protocol\": \"tcp\", \"port\": 283, \"endPort\": 286}, {\"cidr\": \"10.0.1.148/31\", \"protocol\": \"udp\", \"port\": 119, \"endPort\": 122}, {\"cidr\": \"10.0.1.20/30\", \"protocol\": \"any\", \"port\": 169, \"endPort\": 172}, {\"cidr\": \"10.0.2.156/31\", \"protocol\": \"any\", \"port\": 163}, {\"cidr\": \"10.0.3.59/32\", \"protocol\": \"udp\", \"port\": 121, \"endPort\": 125}, {\"cidr\": \"10.0.2.88/30\", \"protocol\": \"tcp\", \"port\": 237}, {\"cidr\": \"2001:db8::3d/128\", \"protocol\": \"tcp\", \"port\": 273}, {\"cidr\": \"10.0.3.234/31\", \"protocol\": \"udp\", \"port\": 83}, {\"cidr\": \"10.0.2.82/31\", \"protocol\": \"tcp\", \"port\": 151}, {\"cidr\": \"10.0.2.251/32\", \"protocol\": \"tcp\", \"port\": 187}, {\"cidr\": \"10.0.0.26/31\", \"protocol\": \"any\", \"port\": 49}, {\"cidr\": \"10.0.1.212/31\", \"protocol\": \"tcp\", \"port\": 311}, {\"cidr\": \"10.0.3.179/32\", \"protocol\": \"udp\", \"port\": 101}, {\"cidr\": \"10.0.1.235/32\", \"protocol\": \"udp\", \"port\": 139}, {\"cidr\": \"10.0.2.66/31\", \"protocol\": \"any\", \"port\": 55, \"endPort\": 57}, {\"cidr\": \"10.0.3.228/32\", \"protocol\": \"any\", \"port\": 95}, {\"cidr\": \"10.0.3.176/30\", \"protocol\": \"udp\", \"port\": 137, \"endPort\": 140}, {\"cidr\": \"2001:db8::2d7/128\", \"protocol\": \"tcp\", \"port\": 23}, {\"cidr\": \"10.0.0.142/31\", \"protocol\": \"tcp\", \"port\": 345}, {\"cidr\": \"10.0.2.78/31\", \"protocol\": \"udp\", \"port\": 335}, {\"cidr\": \"10.0.3.49/32\", \"protocol\": \"udp\", \"port\": 323}, {\"cidr\": \"10.0.2.76/30\", \"protocol\": \"tcp\", \"port\": 341}, {\"cidr\": \"2001:db8::9a/128\", \"protocol\": \"udp\", \"port\": 101}, {\"cidr\": \"10.0.3.104/30\", \"protocol\": \"udp\", \"port\": 27}, {\"cidr\": \"10.0.1.92/31\", \"protocol\": \"tcp\", \"port\": 331}, {\"cidr\": \"10.0.2.128/30\", \"protocol\": \"tcp\", \"port\": 213, \"endPort\": 215}, {\"cidr\": \"2001:db8::25/128\", \"protocol\": \"udp\", \"port\": 85}, {\"cidr\": \"2001:db8::58/128\", \"protocol\": \"any\", \"port\": 351, \"endPort\": 351}, {\"cidr\": \"10.0.2.232/31\", \"protocol\": \"udp\", \"port\": 185}, {\"cidr\": \"10.0.2.238/31\", \"protocol\": \"tcp\", \"port\": 307}, {\"cidr\": \"10.0.1.228/31\", \"protocol\": \"udp\", \"port\": 151}, {\"cidr\": \"2001:db8::167/128\", \"protocol\": \"any\", \"port\": 353}, {\"cidr\": \"10.0.2.216/31\", \"protocol\": \"tcp\", \"port\": 23, \"endPort\": 26}, {\"cidr\": \"10.0.2.12/30\", \"protocol\": \"any\", \"port\": 79, \"endPort\": 82}, {\"cidr\": \"10.0.1.248/32\", \"protocol\": \"any\", \"port\": 99, \"endPort\": 99}, {\"cidr\": \"10.0.0.21/32\", \"protocol\": \"any\", \"port\": 313}, {\"cidr\": \"10.0.0.74/31\", \"protocol\": \"udp\", \"port\": 91, \"endPort\": 91}, {\"cidr\": \"2001:db8::372/128\", \"protocol\": \"tcp\", \"port\": 95}, {\"cidr\": \"2001:db8::18b/128\", \"protocol\": \"udp\", \"port\": 145}, {\"cidr\": \"10.0.2.94/31\", \"protocol\": \"any\", \"port\": 341, \"endPort\": 342}, {\"cidr\": \"10.0.3.52/30\", \"protocol\": \"udp\", \"port\": 285, \"endPort\": 285}, {\"cidr\": \"10.0.0.150/32\", \"protocol\": \"tcp\", \"port\": 15, \"endPort\": 15}, {\"cidr\": \"10.0.2.248/31\", \"protocol\": \"udp\", \"port\": 197}, {\"cidr\": \"10.0.2.56/30\", \"protocol\": \"any\", \"port\": 387}, {\"cidr\": \"10.0.2.39/32\", \"protocol\": \"tcp\", \"port\": 221, \"endPort\": 221}, {\"cidr\": \"10.0.3.180/30\", \"protocol\": \"tcp\", \"port\": 125}, {\"cidr\": \"10.0.0.66/31\", \"protocol\": \"tcp\", \"port\": 121}, {\"cidr\": \"10.0.0.212/30\", \"protocol\": \"any\", \"port\": 349}, {\"cidr\": \"2001:db8::3fe/128\", \"protocol\": \"udp\", \"port\": 31}, {\"cidr\": \"2001:db8::38/128\", \"protocol\": \"udp\", \"port\": 83, \"endPort\": 87}, {\"cidr\": \"10.0.0.196/31\", \"protocol\": \"tcp\", \"port\": 83, \"endPort\": 87}, {\"cidr\": \"10.0.1.80/32\", \"protocol\": \"any\", \"port\": 285, \"endPort\": 286}, {\"cidr\": \"2001:db8::293/128\", \"protocol\": \"tcp\", \"port\": 179}, {\"cidr\": \"10.0.2.104/30\", \"protocol\": \"udp\", \"port\": 341, \"endPort\": 342}, {\"cidr\": \"2001:db8::126/128\", \"protocol\": \"tcp\", \"port\": 123}, {\"cidr\": \"10.0.3.212/30\", \"protocol\": \"any\", \"port\": 361, \"endPort\": 363}, {\"cidr\": \"2001:db8::12/128\", \"protocol\": \"any\", \"port\": 11, \"endPort\": 11}, {\"cidr\": \"10.0.1.240/31\", \"protocol\": \"any\", \"port\": 119, \"endPort\": 121}, {\"cidr\": \"10.0.0.118/31\", \"protocol\": \"tcp\", \"port\": 227}, {\"cidr\": \"2001:db8::19c/128\", \"protocol\": \"udp\", \"port\": 31, \"endPort\": 33}, {\"cidr\": \"10.0.1.236/32\", \"protocol\": \"any\", \"port\": 47}, {\"cidr\": \"2001:db8::10b/128\", \"protocol\": \"udp\", \"port\": 37}, {\"cidr\": \"10.0.1.64/31\", \"protocol\": \"any\", \"port\": 87}, {\"cidr\": \"10.0.0.172/30\", \"protocol\": \"any\", \"port\": 277}, {\"cidr\": \"2001:db8::9/128\", \"protocol\": \"udp\", \"port\": 7, \"endPort\": 9}, {\"cidr\": \"2001:db8::67/128\", \"protocol\": \"any\", \"port\": 369, \"endPort\": 373}, {\"cidr\": \"2001:db8::24f/128\", \"protocol\": \"tcp\", \"port\": 143}, {\"cidr\": \"10.0.3.68/30\", \"protocol\": \"tcp\", \"port\": 277, \"endPort\": 281}, {\"cidr\": \"2001:db8::2b/128\", \"protocol\": \"tcp\", \"port\": 67}, {\"cidr\": \"10.0.1.98/31\", \"protocol\": \"any\", \"port\": 397}, {\"cidr\": \"10.0.3.208/30\", \"protocol\": \"any\", \"port\": 161, \"endPort\": 162}, {\"cidr\": \"10.0.3.39/32\", \"protocol\": \"udp\", \"port\": 227}, {\"cidr\": \"2001:db8::28d/128\", \"protocol\": \"tcp\", \"port\": 143}, {\"cidr\": \"2001:db8::224/128\", \"protocol\": \"any\", \"port\": 211}, {\"cidr\": \"10.0.3.217/32\", \"protocol\": \"udp\", \"port\": 261, \"endPort\": 262}, {\"cidr\": \"10.0.2.8/31\", \"protocol\": \"any\", \"port\": 351}, {\"cidr\": \"2001:db8::19e/128\", \"protocol\": \"udp\", \"port\": 351, \"endPort\": 354}, {\"cidr\": \"2001:db8::3ed/128\", \"protocol\": \"udp\", \"port\": 161}, {\"cidr\": \"2001:db8::39f/128\", \"protocol\": \"tcp\", \"port\": 389, \"endPort\": 392}, {\"cidr\": \"10.0.2.236/30\", \"protocol\": \"udp\", \"port\": 235}, {\"cidr\": \"10.0.0.74/31\", \"protocol\": \"any\", \"port\": 69}, {\"cidr\": \"2001:db8::3a4/128\", \"protocol\": \"udp\", \"port\": 239}, {\"cidr\": \"10.0.0.118/32\", \"protocol\": \"udp\", \"port\": 167}, {\"cidr\": \"2001:db8::38d/128\", \"protocol\": \"udp\", \"port\": 361}, {\"cidr\": \"10.0.2.202/32\", \"protocol\": \"any\", \"port\": 3}, {\"cidr\": \"2001:db8::d1/128\", \"protocol\": \"tcp\", \"port\": 229}, {\"cidr\": \"10.0.3.112/30\", \"protocol\": \"tcp\", \"port\": 219}, {\"cidr\": \"10.0.0.124/30\", \"protocol\": \"any\", \"port\": 115}, {\"cidr\": \"10.0.3.252/30\", \"protocol\": \"any\", \"port\": 11, \"endPort\": 11}, {\"cidr\": \"2001:db8::219/128\", \"protocol\": \"tcp\", \"port\": 351, \"endPort\": 355}, {\"cidr\": \"10.0.1.120/30\", \"protocol\": \"any\", \"port\": 379}]}"}],"expected":{"error":"deny emits at most 256 native rules"}}, + {"name":"random #130","input":[{"text":"{\n\"allow\": [\n{\n\"cidr\": \"::/32\", \n\"protocol\": \"tcp\", \n\"port\": 9999999999999999999999999999999999999999999999999999999999999999\n}\n], \n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::d0/124\"\n}\n]\n}, \n{\n\"cidr\": \"10.0.0.161/27\", \n\"except\": [], \n\"protocol\": \"udp\", \n\"port\": 65534\n}, \n{\n\"cidr\": \"10.0.3.144/28\", \n\"except\": [\n\"10.0.3.155/30\"\n], \n\"protocol\": \"udp\"\n}, \n{\n\"cidr\": \"2001:db8::160/000125\"\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65532\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 3\n}, \n{\n\"port\": 80\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.0/26\"\n}, \n{\n\"cidr\": \"6000::/3\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"any\", \n\"port\": 11, \n\"endPort\": 56135\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.18/31\"\n}, \n{\n\"cidr\": \"10.0.1.64/29\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 65532\n}, \n{\n\"protocol\": \"icmp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"227.154.134.106/32\"\n}, \n{\n\"cidr\": \"6705:926e:d13a:c278:415e:40e6:bd21:4711\", \n\"except\": []\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 2, \n\"endPort\": 5\n}\n]\n}, \n{\n\"cidr\": \"10.0.2.64/26\", \n\"protocol\": \"any\"\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.1.36/031\"\n}, \n{\n\"cidr\": \"10.0.2.184/31\"\n}, \n{\n\"cidr\": \"10.0.0.56/0029\"\n}, \n{\n\"cidr\": \"2001:db8::80/121\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.128/28\"\n}, \n{\n\"cidr\": \"::/64\", \n\"except\": [\n\"::2D92:ADEA:4EC1:AE7A/0070\", \n\"::E000:0:0:0/67\"\n]\n}\n]\n}, \n{\n\"cidr\": \"95bf:f070:3ea0:63b9:af10:8188:1ea3:2427\", \n\"protocol\": \"any\", \n\"port\": 1\n}, \n{\n\"cidr\": \"216.47.0.0/16\", \n\"except\": [], \n\"protocol\": \"udp\", \n\"port\": 80, \n\"endPort\": 86\n}, \n{\n\"cidr\": \"2001:db8::145/120\", \n\"protocol\": \"udp\", \n\"port\": 65534, \n\"endPort\": 65535\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.141/24\"\n}, \n{\n\"cidr\": \"2001:db8::100/120\"\n}, \n{\n\"cidr\": \"222.27.212.0/24\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"any\", \n\"port\": 65532, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 79, \n\"endPort\": 80\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/255.255.252.0\", \n\"except\": [\n\"10.0.0.0/22\", \n\"10.0.0.223/0.0.0.63\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 65535, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"icmp\", \n\"port\": 5\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"c000:0000:0000:0000:0000:0000:0000:0000/3\"\n}, \n{\n\"cidr\": \"442d:98cc:b895:c2a:a934:6fca:aab3:ba37/0\"\n}, \n{\n\"cidr\": \"10.0.0.76/30\", \n\"except\": [\n\"10.0.0.79/255.255.255.255\", \n\"10.0.0.77/31\"\n]\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::2a0/123\", \n\"except\": [\n\"2001:0db8:0000:0000:0000:0000:0000:02b0/125\", \n\"2001:db8::2b4/127\", \n\"2001:db8::2be/125\"\n]\n}, \n{\n\"cidr\": \"10.0.0.0/23\"\n}, \n{\n\"cidr\": \"2B61:597F:9846:5528:B88A:E217:6600:0/104\", \n\"except\": [\n\"2b61:597f:9846:5528:b88a:e217:66f0:0/109\"\n]\n}, \n{\n\"cidr\": \"10.0.2.0/23\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 65535, \n\"endPort\": 65535\n}\n]\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:00e0/124\", \n\"protocol\": \"tcp\"\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.6/255.255.255.192\"\n}, \n{\n\"cidr\": \"69.0.0.0/8\"\n}, \n{\n\"cidr\": \"10.0.0.0/22\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 3, \n\"endPort\": 9\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 7\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 5\n}, \n{\n\"protocol\": \"any\", \n\"port\": 440\n}\n]\n}, \n{\n\"cidr\": \"b:3838:f9ee:49f4:d149:b0e:5847:f4fe\", \n\"protocol\": \"tcp\", \n\"port\": 65531, \n\"endPort\": 65533\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::300/120\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"tcp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/24\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 65534\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"64.44.20.136\", \n\"except\": [\n\"64.44.20.136\"\n]\n}, \n{\n\"cidr\": \"10.0.2.64/0.0.0.31\", \n\"except\": [\n\"10.0.2.64/29\"\n]\n}\n]\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 65533\n}\n]\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 10, \n\"endPort\": 14\n}, \n{\n\"protocol\": \"any\", \n\"port\": 65534, \n\"endPort\": 65535\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/22\"\n}, \n{\n\"cidr\": \"8000::/1\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 82, \n\"endPort\": 84\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 4, \n\"endPort\": 9\n}\n]\n}, \n{\n\"cidr\": \"2001:db8::309/120\", \n\"protocol\": \"tcp\", \n\"port\": 441\n}, \n{\n\"cidr\": \"10.0.1.146/24\", \n\"protocol\": \"tcp\", \n\"port\": 65534, \n\"endPort\": 65535\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::3e0/124\"\n}, \n{\n\"cidr\": \"f8b5:723:4796:ed65:cedf:2eab:6e26:a125/64\", \n\"except\": [\n\"f8b5:0723:4796:ed65:0000:0000:0.0.0.0/65\", \n\"f8b5:0723:4796:ed65:0000:0000:0.0.0.0/064\"\n]\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.3.32/123\", \n\"except\": [\n\"2001:db8::330/124\"\n]\n}, \n{\n\"cidr\": \"0.0.0.0/0\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 441, \n\"endPort\": 445\n}, \n{\n\"protocol\": \"any\", \n\"port\": 65535\n}, \n{\n\"protocol\": \"any\", \n\"port\": 2, \n\"endPort\": 7\n}, \n{\n\"protocol\": \"any\", \n\"port\": 65530\n}\n]\n}, \n{\n\"cidr\": \"2001:DB8::/120\", \n\"protocol\": \"icmp\"\n}, \n{\n\"cidr\": \"240.0.0.0/4\", \n\"except\": [\n\"241.169.26.67/5\"\n], \n\"protocol\": \"tcp\"\n}, \n{\n\"to\": \"192.0.2.0/24\", \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 78, \n\"endPort\": 37796\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 65530\n}, \n{\n\"protocol\": \"any\", \n\"port\": 443\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"149.193.0.0/16\"\n}, \n{\n\"cidr\": \"e5a4:b32f:0000:0000:0000:0000:0.0.0.0/32\"\n}, \n{\n\"cidr\": \"d4fe:b275::/32\", \n\"except\": [\n\"d4fe:b275:3000:0000:0000:0000:0.0.0.0/038\", \n\"d4fe:b275:b000::/36\"\n]\n}, \n{\n\"cidr\": \"2001:db8::300/121\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"192.0.0.0/2\"\n}, \n{\n\"cidr\": \"10.0.0.165/32\"\n}, \n{\n\"cidr\": \"2001:db8::17c/123\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.1.50/30\"\n}, \n{\n\"cidr\": \"::ffff:247.203.249.0/120\", \n\"except\": [\n\"::ffff:247.203.249.128/121\", \n\"0000:0000:0000:0000:0000:ffff:247.203.249.221/124\"\n]\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:01cc/124\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 8, \n\"endPort\": 14\n}, \n{\n\"protocol\": \"udp\"\n}, \n{\n\"protocol\": \"any\", \n\"port\": 5, \n\"endPort\": 34444\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65534, \n\"endPort\": 65534\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"136.0.0.0/8\"\n}, \n{\n\"cidr\": \"10.0.3.224/255.255.255.224\"\n}, \n{\n\"cidr\": \"10.0.3.0/24\"\n}, \n{\n\"cidr\": \"10.0.3.0/24\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 80, \n\"endPort\": 83\n}, \n{\n\"protocol\": \"udp\"\n}\n]\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.0/118\"\n}, \n{\n\"cidr\": \"e6be:5c8a:6310:f068:15b4:0f5d:166a:04cb/120\", \n\"except\": [\n\"e6be:5c8a:6310:f068:15b4:0f5d:166a:0440/124\"\n], \n\"protocol\": \"tcp\", \n\"port\": 65532\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::/119\"\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/118\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 440\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 445\n}, \n{\n\"protocol\": \"any\", \n\"port\": 65530\n}, \n{\n\"protocol\": \"tcp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::c0/123\"\n}, \n{\n\"cidr\": \"10.0.2.27/24\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 6\n}, \n{\n\"protocol\": \"any\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"20.24.61.91/1\"\n}, \n{\n\"cidr\": \"168.218.114.146/2\"\n}, \n{\n\"cidr\": \"10.0.0.64/26\"\n}, \n{\n\"cidr\": \"2001:db8::1b4/126\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.192/26\"\n}, \n{\n\"cidr\": \"::FFFF:215.0.0.0/104\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.160/255.255.255.248\"\n}, \n{\n\"cidr\": \"83.54.227.214/31\", \n\"except\": [\n\"83.54.227.214/31\"\n]\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.1.14/123\", \n\"except\": []\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 441\n}\n]\n}, \n{\n\"cidr\": \"10.0.3.68/31\", \n\"protocol\": \"udp\", \n\"port\": 79\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.128/25\"\n}, \n{\n\"cidr\": \"10.0.3.199/31\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 78\n}, \n{\n\"protocol\": \"icmp\"\n}\n]\n}, \n{\n\"cidr\": \"2001:db8::200/119\", \n\"protocol\": \"udp\", \n\"port\": 7\n}, \n{\n\"cidr\": \"212.238.161.60/32\", \n\"protocol\": \"icmp\"\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::2ff/125\"\n}, \n{\n\"cidr\": \"2001:db8::/118\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\"\n}, \n{\n\"protocol\": \"udp\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 81, \n\"endPort\": 87\n}, \n{\n\"protocol\": \"any\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.54/24\"\n}, \n{\n\"cidr\": \"10.0.3.128/25\"\n}, \n{\n\"cidr\": \"2001:db8::380/119\", \n\"except\": [\n\"2001:db8::356/127\", \n\"2001:DB8::3B6/124\"\n]\n}\n], \n\"ports\": [\n{\n\"port\": 65532\n}, \n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"icmp\"\n}\n]\n}, \n{\n\"cidr\": \"2001:DB8::/000118\", \n\"protocol\": \"udp\", \n\"port\": 7\n}, \n{\n\"to\": [\n{\n\"cidr\": \"200.39.243.117/192.0.0.0\", \n\"except\": [\n\"224.0.0.0/240.0.0.0\", \n\"220.240.16.26/224.0.0.0\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 443, \n\"endPort\": 26229\n}, \n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65530\n}, \n{\n\"port\": 65532, \n\"endPort\": 65534\n}\n]\n}\n]\n}"}],"expected":{"error":"allow[0].port must be between 1 and 65535"}}, + {"name":"random #131","input":[{"text":"{\"allow\":[{\"cidr\":\"2001:db8::/118\",\"except\":[\"2001:DB8::3F0/124\"]},{\"to\":[{\"cidr\":\"10.0.2.93/255.255.255.0\"},{\"cidr\":\"8000:0000:0000:0000:0000:0000:0000:0000/01\"},{\"cidr\":\"10.0.1.128/26\",\"except\":[\"10.0.1.154/255.255.255.224\"]}]},{\"cidr\":\"2001:db8::5c/126\",\"protocol\":\"any\",\"port\":2,\"endPort\":5},{\"to\":[{\"cidr\":\"10.0.0.151/26\",\"except\":[\"10.0.0.128/28\",\"10.0.0.146/00031\",\"10.0.0.174/31\"]},{\"cidr\":\"10.0.3.40/27\"},{\"cidr\":\"190.204.69.173\"}],\"ports\":[{\"protocol\":\"udp\"}]},{\"to\":[{\"cidr\":\"2001:db8::395/123\"}],\"ports\":[{\"protocol\":\"any\",\"port\":1}]},{\"cidr\":\"10.0.0.183/24\",\"protocol\":\"icmp\"}],\"deny\":[{\"to\":[{\"cidr\":\"10.0.3.238/22\"},{\"cidr\":\"10.0.2.29/0.0.0.0\"},{\"cidr\":\"2001:db8::/0118\"},{\"cidr\":\"165.103.215.145/255.0.0.0\",\"except\":[]}]}]}"}],"expected":{"allow":["10.0.0.0/24:icmp","10.0.0.144/31:udp","10.0.0.148/30:udp","10.0.0.152/29:udp","10.0.0.160/29:udp","10.0.0.168/30:udp","10.0.0.172/31:udp","10.0.0.176/28:udp","10.0.1.160/27","10.0.2.0/24","10.0.3.32/27:udp","190.204.69.173/32:udp","2001:db8::/119","2001:db8::200/120","2001:db8::300/121","2001:db8::380/122","2001:db8::3c0/123","2001:db8::3e0/124","8000::/1"],"deny":["0.0.0.0/0","2001:db8::/118"]}}, + {"name":"random #132","input":[{"text":"{\"allow\":[{},{\"cidr\":\"2001:db8::1a0/124\",\"except\":[\"2001:db8::1a8/125\",\"2001:db8::1a0/124\",\"2001:db8::1a3/126\"]},{\"to\":[{\"cidr\":\"2001:db8::300/000121\",\"except\":[\"2001:db8::30a\"]}]},{\"to\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0040/123\"}]},{\"cidr\":\"10.0.0.160/28\",\"except\":[\"10.0.0.164/30\"],\"protocol\":\"udp\"},{\"to\":[{\"cidr\":\"2001:db8::180/123\"},{\"cidr\":\"224.0.0.0/4\"}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"tcp\",\"port\":444}]}],\"deny\":[{\"to\":[{\"cidr\":\"10.0.2.128/26\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":65531,\"endPort\":65534},{\"protocol\":\"any\",\"port\":65534,\"endPort\":65535},{\"protocol\":\"tcp\",\"port\":65530}]},{\"to\":[{\"cidr\":\"10.0.0.0/0.0.3.255\"},{\"cidr\":\"2001:db8::100/123\",\"except\":[\"2001:0db8:0000:0000:0000:0000:0000:0100/123\",\"2001:db8::10c/126\",\"2001:0db8:0000:0000:0000:0000:0000:0110/00125\"]}],\"ports\":[{\"protocol\":\"udp\"},{\"protocol\":\"udp\"},{\"protocol\":\"any\",\"port\":65532,\"endPort\":65532},{\"protocol\":\"udp\",\"port\":78}]},{\"ports\":[{\"protocol\":\"tcp\",\"port\":79,\"endPort\":79},{\"protocol\":\"tcp\"},{\"protocol\":\"tcp\"}]},{\"cidr\":\"2001:db8::259/119\"}]}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["0.0.0.0/0:tcp","10.0.0.0/22:udp","::/0:tcp","2001:db8::200/119"]}}, + {"name":"random #133","input":[{"text":"{\n\"allow\":[\n{\n\"to\":[\n{\n\"cidr\":\"10.0.0.246/255.255.255.252\"\n},\n{\n\"cidr\":\"7A15:BB1D:3806:843B:5B2D:70F1:7B00:0/00104\"\n},\n{\n\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:03c4/126\"\n},\n{\n\"cidr\":\"10.0.0.64/28\"\n}\n],\n\"ports\":[\n{\n\"protocol\":\"udp\"\n},\n{\n\"protocol\":\"any\",\n\"port\":445,\n\"endPort\":446\n},\n{\n\"protocol\":\"udp\",\n\"port\":10\n},\n{\n\"protocol\":\"tcp\",\n\"port\":65534,\n\"endPort\":65535\n}\n]\n}\n],\n\"deny\":[\n{\n\"to\":[\n{\n\"cidr\":\"10.0.3.214/255.255.255.192\",\n\"except\":[]\n},\n{\n\"cidr\":\"2001:DB8::60/124\"\n},\n{\n\"cidr\":\"10.0.1.15/255.255.255.192\"\n},\n{\n\"cidr\":\"10.0.3.224/0027\"\n}\n],\n\"ports\":[\n{\n\"protocol\":\"icmp\"\n},\n{\n\"protocol\":\"udp\",\n\"port\":442\n},\n{\n\"protocol\":\"tcp\",\n\"port\":65532\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"2001:db8::8f/125\"\n}\n],\n\"ports\":[\n{\n\"protocol\":\"tcp\",\n\"port\":440\n},\n{\n\"protocol\":\"udp\",\n\"port\":65533\n},\n{\n\"protocol\":\"tcp\",\n\"port\":6,\n\"endPort\":12\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"2001:db8::14f/125\",\n\"except\":[\n\"2001:0db8:0000:0000:0000:0000:0000:014c/126\"\n]\n},\n{\n\"cidr\":\"9073:f12:5fb4:22a6:e06b:5246:468:f3f6/128\",\n\"except\":[]\n},\n{\n\"cidr\":\"2001:db8::28c/123\"\n},\n{\n\"cidr\":\"10.0.3.192/26\",\n\"except\":[\n\"10.0.3.200/29\",\n\"10.0.3.224/29\",\n\"10.0.3.212/31\"\n]\n}\n]\n},\n{\n\"to\":[\n{\n\"cidr\":\"2001:db8::/124\"\n},\n{\n\"cidr\":\"0.0.0.0/1\",\n\"except\":[\n\"120.0.0.0/9\",\n\"6.0.0.0/8\",\n\"96.0.0.0/3\"\n]\n}\n],\n\"ports\":[\n{\n\"protocol\":\"any\",\n\"port\":82,\n\"endPort\":88\n}\n]\n},\n{\n\"cidr\":\"142.203.39.93\",\n\"protocol\":\"tcp\"\n},\n{\n\"to\":[\n{\n\"cidr\":\"10.0.2.0/23\"\n}\n],\n\"ports\":[\n{\n\"protocol\":\"tcp\",\n\"port\":443\n},\n{\n\"port\":441,\n\"endPort\":444\n},\n{\n\"protocol\":\"tcp\"\n}\n]\n},\n{\n\"cidr\":\"2001:db8::37c/127\",\n\"protocol\":\"any\"\n},\n{\n\"to\":[\n{\n\"cidr\":\"10.0.0.0/22\"\n}\n],\n\"ports\":[\n{\n\"protocol\":\"icmp\"\n},\n{\n\"protocol\":\"tcp\",\n\"port\":443,\n\"endPort\":447\n},\n{\n\"protocol\":\"tcp\",\n\"port\":5,\n\"endPort\":7\n},\n{\n\"protocol\":\"udp\",\n\"port\":440,\n\"endPort\":440\n}\n]\n}\n]\n}"}],"expected":{"allow":["10.0.0.64/28:tcp:445-446","10.0.0.64/28:tcp:65534-65535","10.0.0.64/28:udp","10.0.0.244/30:tcp:445-446","10.0.0.244/30:tcp:65534-65535","10.0.0.244/30:udp","2001:db8::3c4/126:tcp:445-446","2001:db8::3c4/126:tcp:65534-65535","2001:db8::3c4/126:udp","7a15:bb1d:3806:843b:5b2d:70f1:7b00:0/104:tcp:445-446","7a15:bb1d:3806:843b:5b2d:70f1:7b00:0/104:tcp:65534-65535","7a15:bb1d:3806:843b:5b2d:70f1:7b00:0/104:udp"],"deny":["0.0.0.0/6:tcp:82-88","0.0.0.0/6:udp:82-88","4.0.0.0/7:tcp:82-88","4.0.0.0/7:udp:82-88","7.0.0.0/8:tcp:82-88","7.0.0.0/8:udp:82-88","8.0.0.0/5:tcp:82-88","8.0.0.0/5:udp:82-88","10.0.0.0/22:icmp","10.0.0.0/22:tcp:5-7","10.0.0.0/22:tcp:443-447","10.0.0.0/22:udp:440","10.0.1.0/26:tcp:65532","10.0.1.0/26:udp:442","10.0.2.0/23:tcp","10.0.2.0/23:udp:441-444","10.0.3.192/29","10.0.3.208/30","10.0.3.214/31","10.0.3.216/29","10.0.3.232/29","10.0.3.240/28","16.0.0.0/4:tcp:82-88","16.0.0.0/4:udp:82-88","32.0.0.0/3:tcp:82-88","32.0.0.0/3:udp:82-88","64.0.0.0/3:tcp:82-88","64.0.0.0/3:udp:82-88","142.203.39.93/32:tcp","2001:db8::/124:tcp:82-88","2001:db8::/124:udp:82-88","2001:db8::60/124:icmp","2001:db8::60/124:tcp:65532","2001:db8::60/124:udp:442","2001:db8::88/125:tcp:6-12","2001:db8::88/125:tcp:440","2001:db8::88/125:udp:65533","2001:db8::148/126","2001:db8::280/123","2001:db8::37c/127","9073:f12:5fb4:22a6:e06b:5246:468:f3f6/128"]}}, + {"name":"random #134","input":[{"text":"{\n \"allow\": [\n {\n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 1e400, \n \"endPort\": 12159\n }\n ]\n }, \n {\n \"cidr\": \"2001:db8::2d0/125\", \n \"protocol\": \"tcp\", \n \"port\": 65532\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.1.216/29\"\n }, \n {\n \"cidr\": \"10.0.1.16/29\"\n }, \n {\n \"cidr\": \"10.0.1.0/24\", \n \"except\": [\n \"10.0.1.128/26\", \n \"10.0.1.37/255.255.255.0\"\n ]\n }, \n {\n \"cidr\": \"10.0.3.216/30\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"udp\", \n \"port\": 5, \n \"endPort\": 6\n }, \n {\n \"port\": 1\n }, \n {\n \"protocol\": \"any\", \n \"port\": 444\n }\n ]\n }\n ], \n \"deny\": [\n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.80/28\"\n }, \n {\n \"cidr\": \"10.0.3.232/31\", \n \"except\": []\n }, \n {\n \"cidr\": \"64.0.0.0/4\", \n \"except\": [\n \"77.179.29.136/10\", \n \"74.225.121.163/0011\"\n ]\n }, \n {\n \"cidr\": \"2001:db8::300/121\", \n \"except\": [\n \"2001:0db8:0000:0000:0000:0000:0000:0300/123\", \n \"2001:db8::32d/125\"\n ]\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::261/120\"\n }, \n {\n \"cidr\": \"2001:db8::301/127\"\n }, \n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0015/124\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"icmp\"\n }, \n {\n \"protocol\": \"tcp\"\n }, \n {\n \"protocol\": \"icmp\"\n }\n ]\n }, \n {\n \"cidr\": \"10.0.3.215/26\", \n \"protocol\": \"tcp\", \n \"port\": 65531, \n \"endPort\": 65535\n }, \n {\n \"cidr\": \"2001:db8::28/123\", \n \"except\": [\n \"2001:0db8:0000:0000:0000:0000:0.0.0.47/123\", \n \"2001:db8::3a\", \n \"2001:0db8:0000:0000:0000:0000:0000:0027/124\"\n ], \n \"protocol\": \"icmp\"\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.0/23\", \n \"except\": [\n \"10.0.0.128/26\", \n \"10.0.1.237/0026\", \n \"10.0.1.128/25\"\n ]\n }, \n {\n \"cidr\": \"10.0.2.55/0.0.0.3\"\n }, \n {\n \"cidr\": \"2001:DB8::/119\"\n }, \n {\n \"cidr\": \"2001:db8::260/123\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\"\n }, \n {\n \"protocol\": \"icmp\"\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:DB8::2B8/118\", \n \"except\": []\n }, \n {\n \"cidr\": \"10.0.1.0/0024\"\n }, \n {\n \"cidr\": \"2001:db8::1c0/123\", \n \"except\": [\n \"10.0.1.169/26\", \n \"2001:0db8:0000:0000:0000:0000:0000:01c0/123\", \n \"2001:0db8:0000:0000:0000:0000:0.0.1.192/123\"\n ]\n }, \n {\n \"cidr\": \"ad6d:3cad:1118:5dea:ae5c:e18e:253.14.162.96/127\"\n }\n ]\n }, \n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/120\", \n \"except\": [\n \"2001:0db8:0000:0000:0000:0000:0000:00d9\", \n \"2001:db8::10/124\", \n \"2001:db8::e9/126\"\n ], \n \"protocol\": \"any\", \n \"port\": 440, \n \"endPort\": 445\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"icmp\"\n }, \n {\n \"protocol\": \"any\"\n }, \n {\n \"protocol\": \"icmp\"\n }, \n {\n \"port\": 440\n }\n ]\n }\n ]\n}"}],"expected":{"error":"allow[0].ports[0].port must be an integer"}}, + {"name":"random #135","input":[{"text":"{\n \"allow\":[\n {\n \"to\":[\n {\n \"cidr\":\"230.98.33.78\"\n },\n {\n \"cidr\":\"10.0.1.252/32\"\n }\n ],\n \"ports\":[\n {\n \"port\":81\n },\n {\n \"protocol\":\"any\",\n \"port\":440,\n \"endPort\":445\n },\n {\n \"protocol\":\"tcp\",\n \"port\":8\n },\n {\n \"protocol\":\"any\",\n \"port\":10\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"10.0.2.48/29\"\n },\n {\n \"cidr\":\"10.0.1.241/31\",\n \"except\":[\n \"10.0.1.240/31\"\n ]\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"10.0.3.64/28\"\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"udp\"\n },\n {\n \"protocol\":\"any\"\n },\n {\n \"protocol\":\"tcp\",\n \"port\":81\n }\n ]\n },\n {\n \"ports\":[\n {\n \"protocol\":\"udp\",\n \"port\":65533,\n \"endPort\":65535\n },\n {\n \"protocol\":\"any\",\n \"port\":65531\n },\n {\n \"protocol\":\"tcp\",\n \"port\":5,\n \"endPort\":9\n },\n {\n \"protocol\":\"icmp\"\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"2001:DB8::378/127\"\n },\n {\n \"cidr\":\"10.0.2.30/00032\"\n },\n {\n \"cidr\":\"10.0.1.236/31\"\n },\n {\n \"cidr\":\"10.0.1.128/25\"\n }\n ],\n \"ports\":[\n {\n \"protocol\":\"udp\",\n \"port\":3\n },\n {\n \"protocol\":\"tcp\"\n },\n {\n \"protocol\":\"udp\",\n \"port\":11,\n \"endPort\":12\n }\n ]\n },\n {\n \"cidr\":\"10.0.0.128/255.255.255.128\",\n \"protocol\":\"any\",\n \"port\":440,\n \"endPort\":445\n }\n ],\n \"deny\":[\n {\n \"to\":[\n {\n \"cidr\":\"10.0.0.0/0.0.1.255\"\n },\n {\n \"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0300/120\",\n \"except\":[\n \"2001:db8::33e/127\",\n \"2001:db8::39d\",\n \"2001:db8::3c0/124\"\n ]\n }\n ]\n },\n {\n \"to\":[\n {\n \"cidr\":\"10.0.3.76/30\"\n },\n {\n \"cidr\":\"10.0.0.182/32\"\n },\n {\n \"cidr\":\"2001:db8::80/122\"\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["0.0.0.0/0:icmp","0.0.0.0/0:tcp:5-9","0.0.0.0/0:tcp:65531","0.0.0.0/0:udp:65531","0.0.0.0/0:udp:65533-65535","10.0.0.128/25:tcp:440-445","10.0.0.128/25:udp:440-445","10.0.1.128/25:tcp","10.0.1.128/25:udp:3","10.0.1.128/25:udp:11-12","10.0.1.252/32:udp:10-12","10.0.1.252/32:udp:81","10.0.1.252/32:udp:440-445","10.0.2.30/32:tcp","10.0.2.30/32:udp:3","10.0.2.30/32:udp:11-12","10.0.2.48/29","10.0.3.64/28","230.98.33.78/32:tcp:10","230.98.33.78/32:tcp:81","230.98.33.78/32:tcp:440-445","230.98.33.78/32:udp:10","230.98.33.78/32:udp:81","230.98.33.78/32:udp:440-445","::/0:icmp","::/0:tcp:5-9","::/0:tcp:65531","::/0:udp:65531","::/0:udp:65533-65535","2001:db8::378/127:tcp","2001:db8::378/127:udp:3","2001:db8::378/127:udp:11-12"],"deny":["10.0.0.0/23","10.0.3.76/30","2001:db8::80/122","2001:db8::300/123","2001:db8::320/124","2001:db8::330/125","2001:db8::338/126","2001:db8::33c/127","2001:db8::340/122","2001:db8::380/124","2001:db8::390/125","2001:db8::398/126","2001:db8::39c/128","2001:db8::39e/127","2001:db8::3a0/123","2001:db8::3d0/124","2001:db8::3e0/123"]}}, + {"name":"random #136","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::200/120\"\n},\n{\n\"cidr\": \"10.0.0.187/29\"\n},\n{\n\"cidr\": \"121.37.116.20/1\",\n\"except\": [\n\"84.0.0.0/3.255.255.255\",\n\"32.0.0.0/0003\",\n\"0.0.0.0/1\"\n]\n},\n{\n\"cidr\": \"10.0.1.192/28\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"any\",\n\"port\": 6,\n\"endPort\": 6\n},\n{\n\"protocol\": \"tcp\",\n\"port\": 65533\n},\n{\n\"protocol\": \"udp\",\n\"port\": 441,\n\"endPort\": 443\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.10/32\"\n},\n{\n\"cidr\": \"235.175.230.156/0.0.0.1\"\n},\n{\n\"cidr\": \"2001:db8::3e7/127\"\n},\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.3.96/0127\",\n\"except\": [\n\"2001:db8::360/127\",\n\"2001:db8::361\",\n\"2001:db8::360\"\n]\n}\n]\n},\n{\n\"cidr\": \"10.0.0.60/30\",\n\"protocol\": \"udp\",\n\"port\": 65535,\n\"endPort\": 65535\n}\n],\n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"160.0.0.0/4\"\n},\n{\n\"cidr\": \"10.0.2.24/255.255.255.252\",\n\"except\": [\n\"10.0.2.25/32\",\n\"10.0.2.24/255.255.255.252\",\n\"10.0.2.24/255.255.255.252\"\n]\n},\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/120\"\n},\n{\n\"cidr\": \"10.0.0.0/022\"\n}\n],\n\"ports\": [\n{\n\"port\": 7,\n\"endPort\": 27348\n},\n{}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::/120\"\n},\n{\n\"cidr\": \"::/00\",\n\"except\": [\n\"a000::/7\",\n\"8000::/1\",\n\"d895:78b8:48bc:1d2e:b22:cc1d:2850:d2d8/8\"\n]\n},\n{\n\"cidr\": \"10.0.0.0/25\"\n},\n{\n\"cidr\": \"64.0.0.0/2\"\n}\n],\n\"ports\": [\n{\n\"port\": 65530,\n\"endPort\": 65535\n},\n{\n\"protocol\": \"udp\",\n\"port\": 81\n}\n],\n\"cidr\": \"192.0.2.0/24\"\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.89/27\",\n\"except\": [\n\"10.0.3.87/30\",\n\"10.0.3.86/0.0.0.1\",\n\"10.0.3.68\"\n]\n},\n{\n\"cidr\": \"10.0.1.128/25\"\n},\n{\n\"cidr\": \"2001:db8::11c/122\",\n\"except\": [\n\"2001:0db8:0000:0000:0000:0000:0000:010c\",\n\"2001:db8::11b/127\"\n]\n},\n{\n\"cidr\": \"10.0.3.64/26\"\n}\n]\n}\n]\n}"}],"expected":{"error":"deny[1] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #137","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.16/28\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::13d/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":78,\n\t\t\t\"endPort\":83\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/02\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0050/124\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"::/3\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::199/121\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":80,\n\t\t\t\t\t\"endPort\":85\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":65532,\n\t\t\t\t\t\"endPort\":65534\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::300/120\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.19/0.0.1.255\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"e515:bd82:4b3f:8659:a392:74af:2957:abce/127\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.1.128/26\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":65534,\n\t\t\t\t\t\"endPort\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":5,\n\t\t\t\t\t\"endPort\":8\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":442,\n\t\t\t\t\t\"endPort\":444\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::160/124\",\n\t\t\t\"protocol\":\"icmp\"\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"192.0.0.0/2\",\n\t\t\t\"protocol\":\"tcp\"\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"232.65.66.70\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":445\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:DB8::/118\",\n\t\t\t\"protocol\":\"udp\"\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.50/32\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.8/255.255.255.248\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/23\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"3d30:149d:4ad5:0bde:9da8:db21:2500:0000/104\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"3d30:149d:4ad5:bde:9da8:db21:2500:0/106\",\n\t\t\t\t\t\t\"3d30:149d:4ad5:bde:9da8:db21:2560:9495/110\",\n\t\t\t\t\t\t\"3d30:149d:4ad5:bde:9da8:db21:2547:4bac/105\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.0/29\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.180/26\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::295/119\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":65533,\n\t\t\t\t\t\"endPort\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::164/128\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::19a/124\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::380/122\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"2001:db8::380/123\",\n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:03b0/124\",\n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:0380/122\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.0/24\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":65534,\n\t\t\t\"endPort\":65535\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::3c0/122\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::c1/119\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"39.134.14.87/0\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.160/255.255.255.224\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/0000\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.160/27\",\n\t\t\t\"protocol\":\"udp\"\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::1c4/127\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":65530,\n\t\t\t\t\t\"endPort\":65532\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"4000::/0003\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:03a0/123\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::139/122\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"208.0.0.0/8\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":82,\n\t\t\t\t\t\"endPort\":87\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"cef4:e6ce:9bd5:2da7:0000:0000:0000:0000/0064\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"13.227.35.80/24\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"::ffff:0.0.0.0/96\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"::ffff:0.0.0.0/96\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::200/120\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::191/120\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"128.0.0.0/1\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":78\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::37c/127\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.1.39/26\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:DB8::240/122\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":78\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"bc97:a452::/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":65535,\n\t\t\t\"endPort\":65535\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::236/126\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"9997:7ebe:c06f:7ac8:8d37:df38:5932:3000/120\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::/000118\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.1.140/30\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":79\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"port\":1,\n\t\t\t\t\t\"endPort\":1\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":6\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.16/0.0.0.3\"\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"::ffff:54.14.205.118/000127\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":4\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::f0/124\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.128/00025\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.0.160/027\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::3c0/123\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":65530\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":78,\n\t\t\t\t\t\"endPort\":83\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"110.0.0.0/8\",\n\t\t\t\"protocol\":\"tcp\"\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.0/255.255.255.192\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.138/31\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.2.138/31\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/23\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"128.0.0.0/2\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":5,\n\t\t\t\t\t\"endPort\":7\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.0/23\",\n\t\t\t\"except\":[],\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":65534,\n\t\t\t\"endPort\":65535\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/24\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.1.232/30\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.128/255.255.255.128\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":80,\n\t\t\t\t\t\"endPort\":80\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":444\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":441,\n\t\t\t\t\t\"endPort\":446\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"deny\":[\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"7902:65A0:A833:4B7F:B61D:4BB1:BF60:630B/64\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"91.69.191.0/24\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"91.69.191.24/29\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.80/28\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::180/120\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:0100/000122\",\n\t\t\t\t\t\t\"2001:db8::17c/121\",\n\t\t\t\t\t\t\"2001:db8::130/124\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65533\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":6,\n\t\t\t\t\t\"endPort\":10\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":78,\n\t\t\t\t\t\"endPort\":36596\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::200/119\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":440\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["10.0.3.80/28:icmp","10.0.3.80/28:tcp:6-10","10.0.3.80/28:tcp:78-36596","10.0.3.80/28:tcp:65533","91.69.191.0/28:icmp","91.69.191.0/28:tcp:6-10","91.69.191.0/28:tcp:78-36596","91.69.191.0/28:tcp:65533","91.69.191.16/29:icmp","91.69.191.16/29:tcp:6-10","91.69.191.16/29:tcp:78-36596","91.69.191.16/29:tcp:65533","91.69.191.32/27:icmp","91.69.191.32/27:tcp:6-10","91.69.191.32/27:tcp:78-36596","91.69.191.32/27:tcp:65533","91.69.191.64/26:icmp","91.69.191.64/26:tcp:6-10","91.69.191.64/26:tcp:78-36596","91.69.191.64/26:tcp:65533","91.69.191.128/25:icmp","91.69.191.128/25:tcp:6-10","91.69.191.128/25:tcp:78-36596","91.69.191.128/25:tcp:65533","2001:db8::180/121:icmp","2001:db8::180/121:tcp:6-10","2001:db8::180/121:tcp:78-36596","2001:db8::180/121:tcp:65533","2001:db8::200/119:icmp","2001:db8::200/119:tcp:440","7902:65a0:a833:4b7f::/64:icmp","7902:65a0:a833:4b7f::/64:tcp:6-10","7902:65a0:a833:4b7f::/64:tcp:78-36596","7902:65a0:a833:4b7f::/64:tcp:65533"]}}, + {"name":"random #138","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:db8::/121\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 445}]}, {\"to\": [{\"cidr\": \"2001:db8::380/121\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 65532}, {\"protocol\": \"udp\"}, {\"protocol\": \"tcp\", \"port\": 1}]}, {\"cidr\": \"2001:DB8::3F0/125\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:03f0/00128\", \"2001:db8::3f0/128\", \"2001:db8::3f4/000126\"], \"protocol\": \"any\", \"port\": 443}, {\"to\": [{\"cidr\": \"2001:db8::380/121\"}, {\"cidr\": \"2001:DB8::282/127\"}, {\"cidr\": \"10.0.1.97/28\", \"except\": [\"10.0.1.101/31\", \"10.0.1.98/32\", \"10.0.1.96/28\"]}, {\"cidr\": \"115.181.251.142/31\"}], \"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"any\", \"port\": 65533, \"endPort\": 65534}, {\"protocol\": \"udp\", \"port\": 79}]}], \"deny\": [{\"cidr\": \"ac8b:1a18:1239:8105:d767:1885:802f:4700/120\", \"protocol\": \"udp\", \"port\": 445}, {\"to\": [{\"cidr\": \"10.0.3.0/26\", \"except\": []}], \"ports\": [{\"protocol\": \"udp\", \"port\": 78, \"endPort\": 80}]}, {\"to\": [{\"cidr\": \"2001:db8::/000119\", \"except\": [\"2001:DB8::100/120\"]}, {\"cidr\": \"10.0.2.228/31\"}], \"ports\": [{\"protocol\": \"any\", \"port\": 5, \"endPort\": 10}]}, {\"ports\": [{\"protocol\": \"udp\", \"port\": 81}]}, {\"cidr\": \"2001:DB8::2A0/123\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:02a0/125\", \"2001:db8::2b8/125\"], \"protocol\": \"any\", \"port\": 10, \"endPort\": 12}, {\"cidr\": \"2001:db8::224/126\", \"except\": [\"2001:db8::224/126\", \"2001:db8::226/127\", \"2001:db8::226/127\"], \"protocol\": \"udp\", \"port\": 445, \"endPort\": 451}, {\"to\": [{\"cidr\": \"10.0.0.136/29\"}, {\"cidr\": \"10.0.3.0/255.255.255.0\"}, {\"cidr\": \"89c7:d136:e435:6985:ce1a:b28d:424a:ef00/120\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 81}, {\"protocol\": \"udp\", \"port\": 79}]}, {\"cidr\": \"10.0.2.0/23\", \"protocol\": \"udp\", \"port\": 440}]}"}],"expected":{"allow":["115.181.251.142/31:tcp","115.181.251.142/31:udp:79","115.181.251.142/31:udp:65533-65534","2001:db8::/121:udp:445","2001:db8::282/127:tcp","2001:db8::282/127:udp:79","2001:db8::282/127:udp:65533-65534","2001:db8::380/121:tcp","2001:db8::380/121:udp"],"deny":["0.0.0.0/0:udp:81","10.0.0.136/29:tcp:81","10.0.0.136/29:udp:79","10.0.2.0/23:udp:440","10.0.2.228/31:tcp:5-10","10.0.2.228/31:udp:5-10","10.0.3.0/24:tcp:81","10.0.3.0/24:udp:79","10.0.3.0/26:udp:78-81","::/0:udp:81","2001:db8::/120:tcp:5-10","2001:db8::/120:udp:5-10","2001:db8::2a8/125:tcp:10-12","2001:db8::2a8/125:udp:10-12","2001:db8::2b0/125:tcp:10-12","2001:db8::2b0/125:udp:10-12","89c7:d136:e435:6985:ce1a:b28d:424a:ef00/120:tcp:81","89c7:d136:e435:6985:ce1a:b28d:424a:ef00/120:udp:79","ac8b:1a18:1239:8105:d767:1885:802f:4700/120:udp:445"]}}, + {"name":"random #139","input":[{"text":"{\n \"allow\": [\n {\n \"cidr\": \"2001:db8::/118\"\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"icmp\"\n }, \n {\n \"protocol\": \"icmp\"\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 80, \n \"endPort\": 53977\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 82, \n \"endPort\": 88\n }\n ]\n }\n ], \n \"deny\": [\n {\n \"to\": [\n {\n \"cidr\": \"30.107.0.0/255.255.0.0\"\n }, \n {\n \"cidr\": \"81.92.35.235/32\", \n \"except\": [\n \"81.92.35.235\"\n ]\n }, \n {\n \"cidr\": \"10.0.3.128/26\"\n }, \n {\n \"cidr\": \"10.0.0.0/22\"\n }\n ]\n }, \n {\n \"cidr\": \"19.80.0.0/012\", \n \"protocol\": \"tcp\", \n \"port\": 65534\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.70/0.0.0.1\"\n }, \n {\n \"cidr\": \"10.0.0.128/00025\"\n }, \n {\n \"cidr\": \"10.0.3.0/25\", \n \"except\": [\n \"10.0.3.0/00025\", \n \"10.0.3.0/26\", \n \"10.0.3.40/0.0.0.3\"\n ]\n }, \n {\n \"cidr\": \"192.179.178.0/00024\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"udp\", \n \"port\": 80\n }, \n {\n \"protocol\": \"tcp\"\n }, \n {\n \"protocol\": \"any\", \n \"port\": 10, \n \"endPort\": 15\n }, \n {\n \"protocol\": \"icmp\"\n }\n ]\n }, \n {\n \"ports\": [\n {}, \n {\n \"protocol\": \"tcp\", \n \"port\": 441, \n \"endPort\": 443\n }, \n {\n \"protocol\": \"tcp\"\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 8\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["0.0.0.0/0:icmp","0.0.0.0/0:tcp:80-53977","0.0.0.0/0:udp:82-88","::/0:icmp","::/0:tcp:80-53977","::/0:udp:82-88","2001:db8::/118"],"deny":["0.0.0.0/0","::/0"]}}, + {"name":"random #140","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"10.0.3.96/27\",\n\t\t\t\"protocol\": \"tcp\"\n\t\t}\n\t],\n\t\"deny\": []\n}"}],"expected":{"allow":["10.0.3.96/27:tcp"],"deny":[]}}, + {"name":"random #141","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:03bb/118\"}, {\"to\": [{\"cidr\": \"10.0.2.0/23\", \"except\": [\"10.0.3.60/31\", \"10.0.2.0/24\"]}, {\"cidr\": \"2001:db8::3e7/120\"}, {\"cidr\": \"10.0.0.0/022\", \"except\": [\"10.0.3.98/30\", \"10.0.0.0/22\", \"10.0.3.64/0.0.0.31\"]}, {\"cidr\": \"192.0.0.0/15.255.255.255\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 9, \"endPort\": 9}, {\"protocol\": \"tcp\", \"port\": 65531, \"endPort\": 65534}, {\"protocol\": \"tcp\", \"port\": 441, \"endPort\": 441}]}], \"deny\": [{\"cidr\": \"0.0.0.0/01\", \"protocol\": \"tcp\", \"port\": 79, \"endPort\": 84}, {\"ports\": [{\"protocol\": \"udp\", \"port\": 78, \"endPort\": 82}, {\"protocol\": \"tcp\", \"port\": 440}]}]}"}],"expected":{"allow":["10.0.3.0/27:tcp:441","10.0.3.0/27:tcp:65531-65534","10.0.3.0/27:udp:9","10.0.3.32/28:tcp:441","10.0.3.32/28:tcp:65531-65534","10.0.3.32/28:udp:9","10.0.3.48/29:tcp:441","10.0.3.48/29:tcp:65531-65534","10.0.3.48/29:udp:9","10.0.3.56/30:tcp:441","10.0.3.56/30:tcp:65531-65534","10.0.3.56/30:udp:9","10.0.3.62/31:tcp:441","10.0.3.62/31:tcp:65531-65534","10.0.3.62/31:udp:9","10.0.3.64/26:tcp:441","10.0.3.64/26:tcp:65531-65534","10.0.3.64/26:udp:9","10.0.3.128/25:tcp:441","10.0.3.128/25:tcp:65531-65534","10.0.3.128/25:udp:9","192.0.0.0/4:tcp:441","192.0.0.0/4:tcp:65531-65534","192.0.0.0/4:udp:9","2001:db8::/118"],"deny":["0.0.0.0/0:tcp:440","0.0.0.0/0:udp:78-82","0.0.0.0/1:tcp:79-84","::/0:tcp:440","::/0:udp:78-82"]}}, + {"name":"random #142","input":[{"text":"{\"allow\":[{\"cidr\":\"::/1\",\"protocol\":\"tcp\",\"port\":65530,\"endPort\":65530},{\"to\":[{\"cidr\":\"2001:db8::242/119\"},{\"cidr\":\"10.0.1.0/24\"}],\"ports\":[{\"protocol\":\"any\",\"port\":443,\"endPort\":445},{\"protocol\":\"tcp\",\"port\":65535,\"endPort\":65535}]},{\"cidr\":\"2001:DB8::90/125\",\"except\":[\"2001:db8::92/127\",\"2001:db8::94/126\"],\"protocol\":\"udp\"},{\"cidr\":\"2001:db8::201/128\",\"protocol\":\"tcp\",\"port\":441}],\"deny\":[{\"cidr\":\"10.0.1.177/27\",\"protocol\":\"tcp\",\"port\":65535,\"endPort\":65535},{\"cidr\":\"::/0001\",\"protocol\":\"tcp\",\"port\":443,\"endPort\":449},{\"to\":[{\"cidr\":\"8000::/0001\"},{\"cidr\":\"2001:db8::/118\",\"except\":[]},{\"cidr\":\"10.0.0.0/22\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":81,\"endPort\":85},{\"protocol\":\"tcp\",\"port\":10},{\"protocol\":\"icmp\"},{\"protocol\":\"any\"}]},{\"to\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:03c0/122\"},{\"cidr\":\"10.0.1.216/29\",\"except\":[\"10.0.1.221/32\",\"10.0.1.218\"]}],\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"icmp\"}]},{\"cidr\":\"10.0.0.56/30\",\"protocol\":\"udp\",\"port\":275,\"endPort\":275},{\"cidr\":\"10.0.2.207/32\",\"protocol\":\"tcp\",\"port\":251},{\"cidr\":\"2001:db8::2aa/128\",\"protocol\":\"any\",\"port\":149},{\"cidr\":\"10.0.1.4/30\",\"protocol\":\"udp\",\"port\":141},{\"cidr\":\"10.0.0.170/32\",\"protocol\":\"udp\",\"port\":115},{\"cidr\":\"10.0.2.107/32\",\"protocol\":\"tcp\",\"port\":103},{\"cidr\":\"2001:db8::1c8/128\",\"protocol\":\"any\",\"port\":315},{\"cidr\":\"10.0.2.63/32\",\"protocol\":\"any\",\"port\":215},{\"cidr\":\"10.0.3.166/31\",\"protocol\":\"udp\",\"port\":77},{\"cidr\":\"2001:db8::1c4/128\",\"protocol\":\"any\",\"port\":143,\"endPort\":143},{\"cidr\":\"10.0.2.55/32\",\"protocol\":\"any\",\"port\":397},{\"cidr\":\"10.0.1.97/32\",\"protocol\":\"udp\",\"port\":121},{\"cidr\":\"10.0.3.231/32\",\"protocol\":\"any\",\"port\":273,\"endPort\":274},{\"cidr\":\"10.0.1.113/32\",\"protocol\":\"udp\",\"port\":45,\"endPort\":46},{\"cidr\":\"10.0.2.96/32\",\"protocol\":\"tcp\",\"port\":359,\"endPort\":361},{\"cidr\":\"10.0.3.32/32\",\"protocol\":\"tcp\",\"port\":65},{\"cidr\":\"10.0.3.31/32\",\"protocol\":\"tcp\",\"port\":11,\"endPort\":13},{\"cidr\":\"10.0.0.102/31\",\"protocol\":\"udp\",\"port\":289},{\"cidr\":\"10.0.3.218/32\",\"protocol\":\"udp\",\"port\":217},{\"cidr\":\"2001:db8::3f6/128\",\"protocol\":\"udp\",\"port\":81,\"endPort\":81},{\"cidr\":\"2001:db8::10d/128\",\"protocol\":\"any\",\"port\":157},{\"cidr\":\"10.0.3.216/30\",\"protocol\":\"tcp\",\"port\":73,\"endPort\":73},{\"cidr\":\"2001:db8::250/128\",\"protocol\":\"tcp\",\"port\":303},{\"cidr\":\"2001:db8::34f/128\",\"protocol\":\"tcp\",\"port\":231},{\"cidr\":\"10.0.0.225/32\",\"protocol\":\"tcp\",\"port\":279},{\"cidr\":\"10.0.1.180/30\",\"protocol\":\"udp\",\"port\":131},{\"cidr\":\"2001:db8::b7/128\",\"protocol\":\"udp\",\"port\":241},{\"cidr\":\"10.0.0.116/30\",\"protocol\":\"udp\",\"port\":33},{\"cidr\":\"10.0.3.160/30\",\"protocol\":\"udp\",\"port\":257},{\"cidr\":\"10.0.0.48/30\",\"protocol\":\"any\",\"port\":205,\"endPort\":206},{\"cidr\":\"10.0.0.12/31\",\"protocol\":\"udp\",\"port\":289},{\"cidr\":\"2001:db8::283/128\",\"protocol\":\"udp\",\"port\":9},{\"cidr\":\"2001:db8::3a8/128\",\"protocol\":\"tcp\",\"port\":95},{\"cidr\":\"10.0.3.46/31\",\"protocol\":\"any\",\"port\":5,\"endPort\":9},{\"cidr\":\"10.0.0.148/30\",\"protocol\":\"udp\",\"port\":27,\"endPort\":27},{\"cidr\":\"2001:db8::379/128\",\"protocol\":\"tcp\",\"port\":81},{\"cidr\":\"10.0.2.20/30\",\"protocol\":\"any\",\"port\":341},{\"cidr\":\"10.0.0.48/30\",\"protocol\":\"tcp\",\"port\":239,\"endPort\":240},{\"cidr\":\"10.0.0.168/32\",\"protocol\":\"tcp\",\"port\":205,\"endPort\":206},{\"cidr\":\"10.0.3.0/30\",\"protocol\":\"udp\",\"port\":231},{\"cidr\":\"10.0.2.120/31\",\"protocol\":\"tcp\",\"port\":273},{\"cidr\":\"10.0.3.92/30\",\"protocol\":\"any\",\"port\":251,\"endPort\":252},{\"cidr\":\"10.0.2.24/30\",\"protocol\":\"tcp\",\"port\":261},{\"cidr\":\"10.0.1.80/30\",\"protocol\":\"tcp\",\"port\":287},{\"cidr\":\"10.0.1.0/30\",\"protocol\":\"any\",\"port\":333,\"endPort\":335},{\"cidr\":\"10.0.1.208/31\",\"protocol\":\"tcp\",\"port\":115,\"endPort\":117},{\"cidr\":\"10.0.3.114/31\",\"protocol\":\"tcp\",\"port\":19,\"endPort\":23},{\"cidr\":\"10.0.0.112/30\",\"protocol\":\"udp\",\"port\":69},{\"cidr\":\"10.0.0.136/30\",\"protocol\":\"tcp\",\"port\":185},{\"cidr\":\"2001:db8::30d/128\",\"protocol\":\"any\",\"port\":391,\"endPort\":394},{\"cidr\":\"2001:db8::11a/128\",\"protocol\":\"udp\",\"port\":321},{\"cidr\":\"10.0.1.72/30\",\"protocol\":\"tcp\",\"port\":75,\"endPort\":78},{\"cidr\":\"10.0.1.108/30\",\"protocol\":\"any\",\"port\":391,\"endPort\":395},{\"cidr\":\"2001:db8::398/128\",\"protocol\":\"udp\",\"port\":85,\"endPort\":85},{\"cidr\":\"10.0.1.168/32\",\"protocol\":\"any\",\"port\":261},{\"cidr\":\"10.0.2.155/32\",\"protocol\":\"tcp\",\"port\":257,\"endPort\":257},{\"cidr\":\"10.0.3.152/31\",\"protocol\":\"udp\",\"port\":383},{\"cidr\":\"10.0.3.104/31\",\"protocol\":\"udp\",\"port\":217},{\"cidr\":\"2001:db8::22c/128\",\"protocol\":\"tcp\",\"port\":325},{\"cidr\":\"2001:db8::36e/128\",\"protocol\":\"any\",\"port\":255},{\"cidr\":\"2001:db8::33c/128\",\"protocol\":\"any\",\"port\":281,\"endPort\":282},{\"cidr\":\"2001:db8::3f9/128\",\"protocol\":\"any\",\"port\":285},{\"cidr\":\"2001:db8::2db/128\",\"protocol\":\"udp\",\"port\":275},{\"cidr\":\"10.0.1.4/30\",\"protocol\":\"any\",\"port\":327},{\"cidr\":\"10.0.3.176/30\",\"protocol\":\"tcp\",\"port\":95},{\"cidr\":\"10.0.3.42/31\",\"protocol\":\"tcp\",\"port\":167},{\"cidr\":\"2001:db8::158/128\",\"protocol\":\"udp\",\"port\":69},{\"cidr\":\"10.0.2.176/31\",\"protocol\":\"tcp\",\"port\":237,\"endPort\":237},{\"cidr\":\"10.0.3.176/30\",\"protocol\":\"tcp\",\"port\":377},{\"cidr\":\"10.0.0.7/32\",\"protocol\":\"any\",\"port\":175},{\"cidr\":\"10.0.0.72/30\",\"protocol\":\"udp\",\"port\":265},{\"cidr\":\"10.0.0.190/32\",\"protocol\":\"tcp\",\"port\":381},{\"cidr\":\"2001:db8::289/128\",\"protocol\":\"udp\",\"port\":263},{\"cidr\":\"10.0.1.212/30\",\"protocol\":\"any\",\"port\":211},{\"cidr\":\"10.0.0.218/31\",\"protocol\":\"any\",\"port\":179},{\"cidr\":\"10.0.0.244/30\",\"protocol\":\"udp\",\"port\":331},{\"cidr\":\"10.0.0.16/30\",\"protocol\":\"tcp\",\"port\":191},{\"cidr\":\"2001:db8::267/128\",\"protocol\":\"any\",\"port\":193,\"endPort\":195},{\"cidr\":\"10.0.2.24/30\",\"protocol\":\"udp\",\"port\":193,\"endPort\":197},{\"cidr\":\"10.0.1.83/32\",\"protocol\":\"tcp\",\"port\":177},{\"cidr\":\"10.0.1.188/32\",\"protocol\":\"any\",\"port\":367,\"endPort\":370},{\"cidr\":\"2001:db8::1dd/128\",\"protocol\":\"tcp\",\"port\":307},{\"cidr\":\"10.0.2.17/32\",\"protocol\":\"udp\",\"port\":55},{\"cidr\":\"10.0.0.129/32\",\"protocol\":\"udp\",\"port\":17},{\"cidr\":\"10.0.2.148/30\",\"protocol\":\"any\",\"port\":111},{\"cidr\":\"2001:db8::2a4/128\",\"protocol\":\"tcp\",\"port\":379},{\"cidr\":\"10.0.1.108/30\",\"protocol\":\"any\",\"port\":335},{\"cidr\":\"2001:db8::101/128\",\"protocol\":\"udp\",\"port\":343},{\"cidr\":\"10.0.1.32/30\",\"protocol\":\"any\",\"port\":275},{\"cidr\":\"10.0.0.68/30\",\"protocol\":\"any\",\"port\":81,\"endPort\":81},{\"cidr\":\"10.0.1.3/32\",\"protocol\":\"tcp\",\"port\":151},{\"cidr\":\"10.0.3.108/30\",\"protocol\":\"any\",\"port\":43},{\"cidr\":\"10.0.2.143/32\",\"protocol\":\"tcp\",\"port\":301},{\"cidr\":\"2001:db8::21d/128\",\"protocol\":\"tcp\",\"port\":381,\"endPort\":381},{\"cidr\":\"10.0.0.190/31\",\"protocol\":\"udp\",\"port\":199},{\"cidr\":\"10.0.0.244/31\",\"protocol\":\"udp\",\"port\":215,\"endPort\":217},{\"cidr\":\"10.0.0.104/31\",\"protocol\":\"udp\",\"port\":149},{\"cidr\":\"10.0.3.157/32\",\"protocol\":\"udp\",\"port\":5},{\"cidr\":\"10.0.2.96/32\",\"protocol\":\"tcp\",\"port\":211},{\"cidr\":\"10.0.0.221/32\",\"protocol\":\"tcp\",\"port\":57,\"endPort\":60},{\"cidr\":\"10.0.2.0/31\",\"protocol\":\"any\",\"port\":67},{\"cidr\":\"10.0.1.0/30\",\"protocol\":\"any\",\"port\":389},{\"cidr\":\"2001:db8::158/128\",\"protocol\":\"tcp\",\"port\":291,\"endPort\":292},{\"cidr\":\"2001:db8::167/128\",\"protocol\":\"tcp\",\"port\":77},{\"cidr\":\"10.0.1.136/30\",\"protocol\":\"udp\",\"port\":277},{\"cidr\":\"10.0.3.128/31\",\"protocol\":\"tcp\",\"port\":127},{\"cidr\":\"2001:db8::328/128\",\"protocol\":\"any\",\"port\":227},{\"cidr\":\"10.0.3.176/32\",\"protocol\":\"tcp\",\"port\":355},{\"cidr\":\"10.0.0.162/31\",\"protocol\":\"any\",\"port\":173},{\"cidr\":\"10.0.1.72/31\",\"protocol\":\"tcp\",\"port\":87},{\"cidr\":\"10.0.0.60/30\",\"protocol\":\"any\",\"port\":285},{\"cidr\":\"10.0.1.24/30\",\"protocol\":\"udp\",\"port\":121,\"endPort\":123},{\"cidr\":\"2001:db8::251/128\",\"protocol\":\"tcp\",\"port\":229,\"endPort\":233},{\"cidr\":\"10.0.1.250/32\",\"protocol\":\"tcp\",\"port\":247},{\"cidr\":\"10.0.2.152/31\",\"protocol\":\"udp\",\"port\":7},{\"cidr\":\"10.0.1.193/32\",\"protocol\":\"tcp\",\"port\":1},{\"cidr\":\"2001:db8::6c/128\",\"protocol\":\"udp\",\"port\":157,\"endPort\":159},{\"cidr\":\"2001:db8::296/128\",\"protocol\":\"any\",\"port\":313,\"endPort\":317},{\"cidr\":\"10.0.1.170/32\",\"protocol\":\"tcp\",\"port\":273},{\"cidr\":\"10.0.3.254/32\",\"protocol\":\"tcp\",\"port\":249},{\"cidr\":\"10.0.0.10/31\",\"protocol\":\"any\",\"port\":85},{\"cidr\":\"10.0.2.16/32\",\"protocol\":\"tcp\",\"port\":361},{\"cidr\":\"10.0.2.188/30\",\"protocol\":\"any\",\"port\":159},{\"cidr\":\"10.0.3.168/32\",\"protocol\":\"udp\",\"port\":243},{\"cidr\":\"10.0.0.120/30\",\"protocol\":\"tcp\",\"port\":301},{\"cidr\":\"10.0.3.216/30\",\"protocol\":\"udp\",\"port\":389},{\"cidr\":\"10.0.1.219/32\",\"protocol\":\"any\",\"port\":143},{\"cidr\":\"10.0.0.144/30\",\"protocol\":\"udp\",\"port\":367},{\"cidr\":\"10.0.2.124/30\",\"protocol\":\"tcp\",\"port\":219,\"endPort\":222},{\"cidr\":\"10.0.0.208/32\",\"protocol\":\"tcp\",\"port\":323},{\"cidr\":\"2001:db8::49/128\",\"protocol\":\"any\",\"port\":25},{\"cidr\":\"2001:db8::32e/128\",\"protocol\":\"udp\",\"port\":349,\"endPort\":350},{\"cidr\":\"10.0.3.184/30\",\"protocol\":\"tcp\",\"port\":209},{\"cidr\":\"10.0.3.108/30\",\"protocol\":\"any\",\"port\":197},{\"cidr\":\"2001:db8::28c/128\",\"protocol\":\"any\",\"port\":true},{\"cidr\":\"2001:db8::c8/128\",\"protocol\":\"tcp\",\"port\":263},{\"cidr\":\"10.0.1.204/30\",\"protocol\":\"tcp\",\"port\":109},{\"cidr\":\"2001:db8::47/128\",\"protocol\":\"tcp\",\"port\":231},{\"cidr\":\"2001:db8::1cd/128\",\"protocol\":\"any\",\"port\":65},{\"cidr\":\"10.0.3.36/30\",\"protocol\":\"any\",\"port\":211,\"endPort\":211},{\"cidr\":\"10.0.0.68/30\",\"protocol\":\"tcp\",\"port\":19},{\"cidr\":\"2001:db8::23c/128\",\"protocol\":\"any\",\"port\":11},{\"cidr\":\"10.0.0.232/31\",\"protocol\":\"any\",\"port\":73},{\"cidr\":\"2001:db8::1ed/128\",\"protocol\":\"any\",\"port\":383},{\"cidr\":\"10.0.0.77/32\",\"protocol\":\"udp\",\"port\":247,\"endPort\":249},{\"cidr\":\"10.0.0.235/32\",\"protocol\":\"any\",\"port\":245},{\"cidr\":\"2001:db8::376/128\",\"protocol\":\"tcp\",\"port\":101,\"endPort\":102},{\"cidr\":\"10.0.0.192/32\",\"protocol\":\"any\",\"port\":57,\"endPort\":57},{\"cidr\":\"10.0.2.30/31\",\"protocol\":\"any\",\"port\":57,\"endPort\":60},{\"cidr\":\"10.0.3.10/32\",\"protocol\":\"any\",\"port\":313},{\"cidr\":\"2001:db8::25b/128\",\"protocol\":\"udp\",\"port\":385},{\"cidr\":\"2001:db8::3b8/128\",\"protocol\":\"tcp\",\"port\":49},{\"cidr\":\"10.0.2.156/31\",\"protocol\":\"tcp\",\"port\":205},{\"cidr\":\"2001:db8::189/128\",\"protocol\":\"any\",\"port\":331},{\"cidr\":\"10.0.1.44/30\",\"protocol\":\"udp\",\"port\":291},{\"cidr\":\"2001:db8::3a5/128\",\"protocol\":\"udp\",\"port\":377,\"endPort\":377},{\"cidr\":\"10.0.3.36/31\",\"protocol\":\"tcp\",\"port\":395,\"endPort\":395},{\"cidr\":\"10.0.1.168/30\",\"protocol\":\"tcp\",\"port\":67},{\"cidr\":\"2001:db8::368/128\",\"protocol\":\"udp\",\"port\":177,\"endPort\":181},{\"cidr\":\"10.0.3.54/31\",\"protocol\":\"udp\",\"port\":141,\"endPort\":145},{\"cidr\":\"10.0.1.202/32\",\"protocol\":\"udp\",\"port\":85},{\"cidr\":\"10.0.0.113/32\",\"protocol\":\"any\",\"port\":381},{\"cidr\":\"10.0.1.116/31\",\"protocol\":\"any\",\"port\":389},{\"cidr\":\"10.0.1.238/31\",\"protocol\":\"any\",\"port\":261},{\"cidr\":\"10.0.0.240/30\",\"protocol\":\"any\",\"port\":301},{\"cidr\":\"10.0.2.15/32\",\"protocol\":\"tcp\",\"port\":323},{\"cidr\":\"2001:db8::e2/128\",\"protocol\":\"udp\",\"port\":233},{\"cidr\":\"10.0.3.93/32\",\"protocol\":\"tcp\",\"port\":215,\"endPort\":215},{\"cidr\":\"10.0.0.172/30\",\"protocol\":\"tcp\",\"port\":277},{\"cidr\":\"10.0.3.112/30\",\"protocol\":\"udp\",\"port\":221},{\"cidr\":\"10.0.0.72/30\",\"protocol\":\"tcp\",\"port\":217},{\"cidr\":\"10.0.1.54/31\",\"protocol\":\"any\",\"port\":385,\"endPort\":389},{\"cidr\":\"10.0.3.36/30\",\"protocol\":\"any\",\"port\":41},{\"cidr\":\"10.0.2.128/30\",\"protocol\":\"udp\",\"port\":363,\"endPort\":366},{\"cidr\":\"2001:db8::3ec/128\",\"protocol\":\"any\",\"port\":167},{\"cidr\":\"10.0.3.175/32\",\"protocol\":\"any\",\"port\":153},{\"cidr\":\"10.0.0.12/32\",\"protocol\":\"any\",\"port\":205,\"endPort\":208},{\"cidr\":\"10.0.1.22/31\",\"protocol\":\"any\",\"port\":69,\"endPort\":72},{\"cidr\":\"2001:db8::132/128\",\"protocol\":\"any\",\"port\":349,\"endPort\":353},{\"cidr\":\"10.0.2.35/32\",\"protocol\":\"any\",\"port\":265},{\"cidr\":\"10.0.0.244/31\",\"protocol\":\"udp\",\"port\":121},{\"cidr\":\"10.0.2.160/30\",\"protocol\":\"any\",\"port\":23},{\"cidr\":\"2001:db8::2da/128\",\"protocol\":\"tcp\",\"port\":123,\"endPort\":126},{\"cidr\":\"10.0.3.130/31\",\"protocol\":\"udp\",\"port\":29,\"endPort\":32},{\"cidr\":\"10.0.3.211/32\",\"protocol\":\"udp\",\"port\":111,\"endPort\":113},{\"cidr\":\"10.0.0.223/32\",\"protocol\":\"any\",\"port\":29},{\"cidr\":\"2001:db8::ff/128\",\"protocol\":\"tcp\",\"port\":123},{\"cidr\":\"10.0.1.222/32\",\"protocol\":\"tcp\",\"port\":337},{\"cidr\":\"10.0.2.48/32\",\"protocol\":\"udp\",\"port\":139},{\"cidr\":\"10.0.3.236/30\",\"protocol\":\"any\",\"port\":373},{\"cidr\":\"10.0.0.204/31\",\"protocol\":\"any\",\"port\":195},{\"cidr\":\"2001:db8::1c7/128\",\"protocol\":\"udp\",\"port\":239},{\"cidr\":\"2001:db8::15a/128\",\"protocol\":\"udp\",\"port\":327},{\"cidr\":\"10.0.0.218/31\",\"protocol\":\"tcp\",\"port\":319,\"endPort\":322},{\"cidr\":\"10.0.0.208/31\",\"protocol\":\"any\",\"port\":61,\"endPort\":65},{\"cidr\":\"2001:db8::3f6/128\",\"protocol\":\"tcp\",\"port\":255},{\"cidr\":\"10.0.0.72/30\",\"protocol\":\"tcp\",\"port\":235},{\"cidr\":\"2001:db8::3b3/128\",\"protocol\":\"udp\",\"port\":341,\"endPort\":345},{\"cidr\":\"10.0.2.140/30\",\"protocol\":\"tcp\",\"port\":87},{\"cidr\":\"10.0.0.240/31\",\"protocol\":\"tcp\",\"port\":225},{\"cidr\":\"10.0.0.188/31\",\"protocol\":\"udp\",\"port\":107},{\"cidr\":\"2001:db8::2d8/128\",\"protocol\":\"tcp\",\"port\":337},{\"cidr\":\"10.0.0.255/32\",\"protocol\":\"tcp\",\"port\":293},{\"cidr\":\"10.0.1.99/32\",\"protocol\":\"udp\",\"port\":363},{\"cidr\":\"10.0.2.150/31\",\"protocol\":\"udp\",\"port\":321,\"endPort\":324},{\"cidr\":\"2001:db8::18f/128\",\"protocol\":\"udp\",\"port\":147,\"endPort\":150},{\"cidr\":\"2001:db8::f/128\",\"protocol\":\"tcp\",\"port\":99},{\"cidr\":\"10.0.1.4/31\",\"protocol\":\"any\",\"port\":341},{\"cidr\":\"10.0.2.248/30\",\"protocol\":\"tcp\",\"port\":103},{\"cidr\":\"10.0.1.170/32\",\"protocol\":\"udp\",\"port\":13},{\"cidr\":\"10.0.2.113/32\",\"protocol\":\"tcp\",\"port\":147},{\"cidr\":\"10.0.0.208/30\",\"protocol\":\"any\",\"port\":343},{\"cidr\":\"2001:db8::ce/128\",\"protocol\":\"any\",\"port\":253,\"endPort\":254},{\"cidr\":\"10.0.1.232/30\",\"protocol\":\"udp\",\"port\":37,\"endPort\":39},{\"cidr\":\"10.0.2.4/30\",\"protocol\":\"tcp\",\"port\":247,\"endPort\":250},{\"cidr\":\"10.0.1.216/30\",\"protocol\":\"any\",\"port\":177},{\"cidr\":\"2001:db8::107/128\",\"protocol\":\"any\",\"port\":33,\"endPort\":37},{\"cidr\":\"10.0.2.185/32\",\"protocol\":\"tcp\",\"port\":305},{\"cidr\":\"10.0.3.228/31\",\"protocol\":\"tcp\",\"port\":31},{\"cidr\":\"10.0.2.48/30\",\"protocol\":\"tcp\",\"port\":361,\"endPort\":365},{\"cidr\":\"2001:db8::2da/128\",\"protocol\":\"tcp\",\"port\":117},{\"cidr\":\"2001:db8::272/128\",\"protocol\":\"udp\",\"port\":75,\"endPort\":77},{\"cidr\":\"10.0.2.112/32\",\"protocol\":\"udp\",\"port\":89,\"endPort\":92},{\"cidr\":\"10.0.1.155/32\",\"protocol\":\"udp\",\"port\":257},{\"cidr\":\"10.0.1.12/30\",\"protocol\":\"any\",\"port\":301,\"endPort\":303},{\"cidr\":\"2001:db8::389/128\",\"protocol\":\"tcp\",\"port\":69},{\"cidr\":\"10.0.1.124/31\",\"protocol\":\"udp\",\"port\":113},{\"cidr\":\"2001:db8::284/128\",\"protocol\":\"tcp\",\"port\":119},{\"cidr\":\"10.0.3.161/32\",\"protocol\":\"any\",\"port\":129},{\"cidr\":\"10.0.3.190/31\",\"protocol\":\"any\",\"port\":17,\"endPort\":21},{\"cidr\":\"10.0.1.132/31\",\"protocol\":\"any\",\"port\":299,\"endPort\":302},{\"cidr\":\"10.0.1.100/30\",\"protocol\":\"tcp\",\"port\":73,\"endPort\":73},{\"cidr\":\"10.0.2.132/31\",\"protocol\":\"any\",\"port\":175,\"endPort\":178},{\"cidr\":\"10.0.0.240/30\",\"protocol\":\"tcp\",\"port\":375,\"endPort\":378},{\"cidr\":\"10.0.0.200/31\",\"protocol\":\"any\",\"port\":105,\"endPort\":107},{\"cidr\":\"10.0.1.244/30\",\"protocol\":\"any\",\"port\":155,\"endPort\":157},{\"cidr\":\"2001:db8::a9/128\",\"protocol\":\"tcp\",\"port\":343},{\"cidr\":\"2001:db8::55/128\",\"protocol\":\"any\",\"port\":127,\"endPort\":128},{\"cidr\":\"10.0.2.200/30\",\"protocol\":\"any\",\"port\":231},{\"cidr\":\"10.0.3.216/30\",\"protocol\":\"any\",\"port\":241},{\"cidr\":\"2001:db8::361/128\",\"protocol\":\"any\",\"port\":147},{\"cidr\":\"10.0.1.84/32\",\"protocol\":\"tcp\",\"port\":207},{\"cidr\":\"2001:db8::23c/128\",\"protocol\":\"tcp\",\"port\":31,\"endPort\":31},{\"cidr\":\"10.0.1.135/32\",\"protocol\":\"any\",\"port\":249,\"endPort\":252},{\"cidr\":\"10.0.3.68/30\",\"protocol\":\"tcp\",\"port\":95},{\"cidr\":\"2001:db8::20c/128\",\"protocol\":\"tcp\",\"port\":73},{\"cidr\":\"10.0.3.244/30\",\"protocol\":\"tcp\",\"port\":333},{\"cidr\":\"10.0.1.24/30\",\"protocol\":\"udp\",\"port\":187},{\"cidr\":\"2001:db8::1f2/128\",\"protocol\":\"udp\",\"port\":159,\"endPort\":160},{\"cidr\":\"2001:db8::2a5/128\",\"protocol\":\"tcp\",\"port\":195},{\"cidr\":\"2001:db8::391/128\",\"protocol\":\"tcp\",\"port\":167,\"endPort\":169},{\"cidr\":\"10.0.2.248/30\",\"protocol\":\"udp\",\"port\":395},{\"cidr\":\"10.0.1.159/32\",\"protocol\":\"udp\",\"port\":175,\"endPort\":177},{\"cidr\":\"10.0.3.42/31\",\"protocol\":\"udp\",\"port\":57},{\"cidr\":\"10.0.0.196/31\",\"protocol\":\"any\",\"port\":241,\"endPort\":243},{\"cidr\":\"2001:db8::2cd/128\",\"protocol\":\"udp\",\"port\":283},{\"cidr\":\"2001:db8::2a8/128\",\"protocol\":\"tcp\",\"port\":267},{\"cidr\":\"2001:db8::265/128\",\"protocol\":\"tcp\",\"port\":307,\"endPort\":310},{\"cidr\":\"10.0.2.194/31\",\"protocol\":\"tcp\",\"port\":135,\"endPort\":139},{\"cidr\":\"10.0.2.84/31\",\"protocol\":\"any\",\"port\":55},{\"cidr\":\"10.0.1.120/31\",\"protocol\":\"udp\",\"port\":213},{\"cidr\":\"2001:db8::3f4/128\",\"protocol\":\"udp\",\"port\":243,\"endPort\":243},{\"cidr\":\"10.0.2.216/30\",\"protocol\":\"tcp\",\"port\":307},{\"cidr\":\"10.0.1.176/32\",\"protocol\":\"tcp\",\"port\":225},{\"cidr\":\"2001:db8::1af/128\",\"protocol\":\"udp\",\"port\":397},{\"cidr\":\"10.0.1.64/31\",\"protocol\":\"tcp\",\"port\":263},{\"cidr\":\"2001:db8::9c/128\",\"protocol\":\"any\",\"port\":147},{\"cidr\":\"2001:db8::3ff/128\",\"protocol\":\"any\",\"port\":155,\"endPort\":157},{\"cidr\":\"2001:db8::338/128\",\"protocol\":\"tcp\",\"port\":21,\"endPort\":25},{\"cidr\":\"10.0.1.113/32\",\"protocol\":\"any\",\"port\":251},{\"cidr\":\"10.0.2.172/31\",\"protocol\":\"tcp\",\"port\":157,\"endPort\":159},{\"cidr\":\"10.0.2.68/31\",\"protocol\":\"udp\",\"port\":119,\"endPort\":123},{\"cidr\":\"10.0.1.232/30\",\"protocol\":\"any\",\"port\":341},{\"cidr\":\"10.0.2.236/30\",\"protocol\":\"udp\",\"port\":109},{\"cidr\":\"10.0.1.67/32\",\"protocol\":\"any\",\"port\":261},{\"cidr\":\"10.0.1.240/30\",\"protocol\":\"any\",\"port\":31},{\"cidr\":\"10.0.2.176/30\",\"protocol\":\"udp\",\"port\":55},{\"cidr\":\"10.0.3.56/30\",\"protocol\":\"tcp\",\"port\":177},{\"cidr\":\"10.0.2.28/30\",\"protocol\":\"udp\",\"port\":347},{\"cidr\":\"10.0.3.250/31\",\"protocol\":\"tcp\",\"port\":27},{\"cidr\":\"10.0.2.58/32\",\"protocol\":\"any\",\"port\":135},{\"cidr\":\"10.0.2.172/30\",\"protocol\":\"any\",\"port\":389},{\"cidr\":\"10.0.1.172/30\",\"protocol\":\"any\",\"port\":149},{\"cidr\":\"10.0.0.140/31\",\"protocol\":\"udp\",\"port\":387},{\"cidr\":\"10.0.2.48/30\",\"protocol\":\"tcp\",\"port\":171,\"endPort\":174},{\"cidr\":\"2001:db8::105/128\",\"protocol\":\"tcp\",\"port\":47},{\"cidr\":\"10.0.3.214/32\",\"protocol\":\"tcp\",\"port\":355,\"endPort\":358},{\"cidr\":\"2001:db8::2bf/128\",\"protocol\":\"any\",\"port\":137,\"endPort\":137},{\"cidr\":\"10.0.2.90/31\",\"protocol\":\"any\",\"port\":135},{\"cidr\":\"10.0.2.197/32\",\"protocol\":\"tcp\",\"port\":309},{\"cidr\":\"10.0.1.18/31\",\"protocol\":\"any\",\"port\":271,\"endPort\":275}]}"}],"expected":{"error":"deny[138].port must be an integer"}}, + {"name":"random #143","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.36/32\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 442, \n\t\t\t\t\t\"endPort\": 443\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.38/25\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"port\": 440\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:DB8::180/124\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/22\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 9, \n\t\t\t\t\t\"endPort\": 10\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 11\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t], \n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::3a0/123\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.144/30\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.145/32\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"0.0.0.0/2\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"0.0.0.0/2\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.74\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.49/255.255.255.248\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 442\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"10.0.2.196/255.255.255.128\", \n\t\t\t\"except\": [\n\t\t\t\t\"10.0.3.16/28\"\n\t\t\t], \n\t\t\t\"protocol\": \"tcp\"\n\t\t}\n\t]\n}"}],"expected":{"error":"deny[2].except[0] must be contained in 10.0.2.128/25"}}, + {"name":"random #144","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.0.0/0024\"}, {\"cidr\": \"2001:db8::118/127\"}], \"ports\": [{}, {\"protocol\": \"udp\", \"port\": 441, \"endPort\": 446}, {\"protocol\": \"any\", \"port\": 443, \"endPort\": 60858}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"10.0.2.1/30\", \"except\": [\"10.0.2.2/32\", \"10.0.2.2/0.0.0.1\"]}, {\"cidr\": \"10.0.0.128/29\"}, {\"cidr\": \"10.0.3.131/31\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 5}, {}, {\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"2001:DB8::2C4/127\", \"except\": [\"2001:db8::2c4/128\"]}]}], \"deny\": [{\"to\": [{\"cidr\": \"10.0.2.128/26\", \"except\": [\"10.0.2.188/30\", \"10.0.2.143/31\"]}, {\"cidr\": \"10.0.1.160/27\"}]}, {\"ports\": [{\"protocol\": \"udp\", \"port\": 444, \"endPort\": 3435}, {\"protocol\": \"tcp\", \"port\": 82}, {\"protocol\": \"any\", \"port\": 81}]}, {\"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"tcp\", \"port\": 81, \"endPort\": 87}]}, {\"cidr\": \"2001:db8::40/122\", \"protocol\": \"any\"}, {\"cidr\": \"10.0.0.46/32\", \"protocol\": \"any\", \"port\": 225}, {\"cidr\": \"10.0.2.84/30\", \"protocol\": \"tcp\", \"port\": 19}, {\"cidr\": \"2001:db8::125/128\", \"protocol\": \"tcp\", \"port\": 85}, {\"cidr\": \"10.0.1.248/30\", \"protocol\": \"udp\", \"port\": 291}, {\"cidr\": \"10.0.3.146/31\", \"protocol\": \"any\", \"port\": 7}, {\"cidr\": \"10.0.2.230/31\", \"protocol\": \"udp\", \"port\": 265}, {\"cidr\": \"10.0.3.170/31\", \"protocol\": \"tcp\", \"port\": 191, \"endPort\": 191}, {\"cidr\": \"2001:db8::188/128\", \"protocol\": \"udp\", \"port\": 321, \"endPort\": 324}, {\"cidr\": \"10.0.1.113/32\", \"protocol\": \"udp\", \"port\": 327, \"endPort\": 331}, {\"cidr\": \"2001:db8::246/128\", \"protocol\": \"tcp\", \"port\": 61}, {\"cidr\": \"2001:db8::56/128\", \"protocol\": \"udp\", \"port\": 129}, {\"cidr\": \"2001:db8::3fd/128\", \"protocol\": \"tcp\", \"port\": 297}, {\"cidr\": \"10.0.2.76/31\", \"protocol\": \"tcp\", \"port\": 85, \"endPort\": 89}, {\"cidr\": \"10.0.3.214/32\", \"protocol\": \"any\", \"port\": 199, \"endPort\": 203}, {\"cidr\": \"10.0.0.92/31\", \"protocol\": \"tcp\", \"port\": 289, \"endPort\": 292}, {\"cidr\": \"2001:db8::3da/128\", \"protocol\": \"udp\", \"port\": 273}, {\"cidr\": \"10.0.1.96/31\", \"protocol\": \"udp\", \"port\": 189}, {\"cidr\": \"10.0.0.122/31\", \"protocol\": \"udp\", \"port\": 249}, {\"cidr\": \"10.0.0.62/31\", \"protocol\": \"tcp\", \"port\": 189, \"endPort\": 192}, {\"cidr\": \"10.0.0.232/30\", \"protocol\": \"tcp\", \"port\": 265, \"endPort\": 265}, {\"cidr\": \"10.0.1.50/31\", \"protocol\": \"tcp\", \"port\": 179}, {\"cidr\": \"10.0.0.11/32\", \"protocol\": \"udp\", \"port\": 223}, {\"cidr\": \"2001:db8::a0/128\", \"protocol\": \"udp\", \"port\": 287}, {\"cidr\": \"2001:db8::2a3/128\", \"protocol\": \"tcp\", \"port\": 281}, {\"cidr\": \"10.0.1.144/30\", \"protocol\": \"tcp\", \"port\": 53}, {\"cidr\": \"2001:db8::382/128\", \"protocol\": \"udp\", \"port\": 219, \"endPort\": 223}, {\"cidr\": \"10.0.2.108/32\", \"protocol\": \"udp\", \"port\": 179}, {\"cidr\": \"2001:db8::383/128\", \"protocol\": \"tcp\", \"port\": 253}, {\"cidr\": \"10.0.1.40/30\", \"protocol\": \"udp\", \"port\": 25}, {\"cidr\": \"10.0.2.232/32\", \"protocol\": \"tcp\", \"port\": 205, \"endPort\": 209}, {\"cidr\": \"10.0.3.28/32\", \"protocol\": \"any\", \"port\": 279}, {\"cidr\": \"10.0.0.124/30\", \"protocol\": \"udp\", \"port\": 243}, {\"cidr\": \"10.0.1.5/32\", \"protocol\": \"tcp\", \"port\": 245, \"endPort\": 245}, {\"cidr\": \"10.0.1.46/32\", \"protocol\": \"udp\", \"port\": 239}, {\"cidr\": \"10.0.0.198/31\", \"protocol\": \"udp\", \"port\": 365}, {\"cidr\": \"10.0.1.152/31\", \"protocol\": \"udp\", \"port\": 247}, {\"cidr\": \"10.0.0.170/32\", \"protocol\": \"udp\", \"port\": 79, \"endPort\": 82}, {\"cidr\": \"10.0.3.90/31\", \"protocol\": \"any\", \"port\": 97, \"endPort\": 98}, {\"cidr\": \"10.0.3.84/31\", \"protocol\": \"any\", \"port\": 333}, {\"cidr\": \"10.0.0.228/30\", \"protocol\": \"tcp\", \"port\": 133, \"endPort\": 137}, {\"cidr\": \"2001:db8::83/128\", \"protocol\": \"udp\", \"port\": 51, \"endPort\": 51}, {\"cidr\": \"10.0.2.111/32\", \"protocol\": \"udp\", \"port\": 19}, {\"cidr\": \"2001:db8::113/128\", \"protocol\": \"udp\", \"port\": 193}, {\"cidr\": \"10.0.2.138/32\", \"protocol\": \"udp\", \"port\": 189}, {\"cidr\": \"10.0.2.140/30\", \"protocol\": \"tcp\", \"port\": 67}, {\"cidr\": \"2001:db8::288/128\", \"protocol\": \"any\", \"port\": 89}, {\"cidr\": \"10.0.2.96/30\", \"protocol\": \"tcp\", \"port\": 307}, {\"cidr\": \"10.0.1.216/30\", \"protocol\": \"tcp\", \"port\": 131, \"endPort\": 131}, {\"cidr\": \"10.0.2.184/31\", \"protocol\": \"udp\", \"port\": 201}, {\"cidr\": \"2001:db8::3c/128\", \"protocol\": \"any\", \"port\": 295, \"endPort\": 298}, {\"cidr\": \"2001:db8::331/128\", \"protocol\": \"tcp\", \"port\": 131}, {\"cidr\": \"10.0.3.36/30\", \"protocol\": \"any\", \"port\": 263}, {\"cidr\": \"10.0.3.154/32\", \"protocol\": \"any\", \"port\": 31}, {\"cidr\": \"10.0.1.236/30\", \"protocol\": \"tcp\", \"port\": 217}, {\"cidr\": \"10.0.2.210/31\", \"protocol\": \"tcp\", \"port\": 383, \"endPort\": 384}, {\"cidr\": \"10.0.0.140/30\", \"protocol\": \"tcp\", \"port\": 397}, {\"cidr\": \"2001:db8::1dd/128\", \"protocol\": \"udp\", \"port\": 395}, {\"cidr\": \"10.0.0.184/30\", \"protocol\": \"tcp\", \"port\": 155}, {\"cidr\": \"2001:db8::200/128\", \"protocol\": \"any\", \"port\": 169, \"endPort\": 169}, {\"cidr\": \"10.0.2.136/30\", \"protocol\": \"tcp\", \"port\": 109}, {\"cidr\": \"2001:db8::1a9/128\", \"protocol\": \"udp\", \"port\": 239}, {\"cidr\": \"2001:db8::38a/128\", \"protocol\": \"udp\", \"port\": 25}, {\"cidr\": \"2001:db8::129/128\", \"protocol\": \"any\", \"port\": 49, \"endPort\": 53}, {\"cidr\": \"10.0.2.46/32\", \"protocol\": \"any\", \"port\": 93}, {\"cidr\": \"10.0.0.87/32\", \"protocol\": \"tcp\", \"port\": 95, \"endPort\": 97}, {\"cidr\": \"2001:db8::1e6/128\", \"protocol\": \"udp\", \"port\": 239}, {\"cidr\": \"10.0.2.236/31\", \"protocol\": \"tcp\", \"port\": 199, \"endPort\": 199}, {\"cidr\": \"10.0.2.56/30\", \"protocol\": \"udp\", \"port\": 135}, {\"cidr\": \"2001:db8::2c7/128\", \"protocol\": \"any\", \"port\": 111, \"endPort\": 114}, {\"cidr\": \"10.0.0.225/32\", \"protocol\": \"udp\", \"port\": 117, \"endPort\": 121}, {\"cidr\": \"10.0.1.196/32\", \"protocol\": \"any\", \"port\": 15}, {\"cidr\": \"2001:db8::3c4/128\", \"protocol\": \"tcp\", \"port\": 229}, {\"cidr\": \"10.0.2.242/31\", \"protocol\": \"any\", \"port\": 393}, {\"cidr\": \"10.0.0.2/31\", \"protocol\": \"tcp\", \"port\": 11}, {\"cidr\": \"10.0.3.230/31\", \"protocol\": \"udp\", \"port\": 313, \"endPort\": 314}, {\"cidr\": \"10.0.3.247/32\", \"protocol\": \"udp\", \"port\": 297}, {\"cidr\": \"10.0.0.28/30\", \"protocol\": \"udp\", \"port\": 85}, {\"cidr\": \"10.0.1.92/30\", \"protocol\": \"udp\", \"port\": 393}, {\"cidr\": \"10.0.1.129/32\", \"protocol\": \"tcp\", \"port\": 371, \"endPort\": 373}, {\"cidr\": \"10.0.2.54/32\", \"protocol\": \"udp\", \"port\": 143}, {\"cidr\": \"10.0.2.40/30\", \"protocol\": \"any\", \"port\": 253}, {\"cidr\": \"10.0.0.24/31\", \"protocol\": \"tcp\", \"port\": 55}, {\"cidr\": \"10.0.1.252/30\", \"protocol\": \"any\", \"port\": 95}, {\"cidr\": \"10.0.0.14/31\", \"protocol\": \"any\", \"port\": 33}, {\"cidr\": \"2001:db8::378/128\", \"protocol\": \"udp\", \"port\": 381}, {\"cidr\": \"10.0.2.184/30\", \"protocol\": \"udp\", \"port\": 335}, {\"cidr\": \"2001:db8::2e/128\", \"protocol\": \"tcp\", \"port\": 85}, {\"cidr\": \"10.0.2.118/31\", \"protocol\": \"tcp\", \"port\": 117}, {\"cidr\": \"10.0.0.0/32\", \"protocol\": \"udp\", \"port\": 299}, {\"cidr\": \"2001:db8::372/128\", \"protocol\": \"tcp\", \"port\": 257}, {\"cidr\": \"10.0.1.52/30\", \"protocol\": \"any\", \"port\": 103}, {\"cidr\": \"10.0.2.68/30\", \"protocol\": \"any\", \"port\": 69}, {\"cidr\": \"10.0.1.171/32\", \"protocol\": \"any\", \"port\": 365}, {\"cidr\": \"10.0.2.101/32\", \"protocol\": \"tcp\", \"port\": 113}, {\"cidr\": \"10.0.0.23/32\", \"protocol\": \"tcp\", \"port\": 273}, {\"cidr\": \"10.0.2.218/32\", \"protocol\": \"tcp\", \"port\": 157, \"endPort\": 158}, {\"cidr\": \"10.0.0.232/30\", \"protocol\": \"udp\", \"port\": 131}, {\"cidr\": \"10.0.1.204/31\", \"protocol\": \"tcp\", \"port\": 157}, {\"cidr\": \"2001:db8::186/128\", \"protocol\": \"tcp\", \"port\": 191}, {\"cidr\": \"2001:db8::187/128\", \"protocol\": \"tcp\", \"port\": 361}, {\"cidr\": \"10.0.3.34/31\", \"protocol\": \"udp\", \"port\": 5}, {\"cidr\": \"10.0.1.5/32\", \"protocol\": \"tcp\", \"port\": 383}, {\"cidr\": \"10.0.3.113/32\", \"protocol\": \"udp\", \"port\": 155, \"endPort\": 157}, {\"cidr\": \"2001:db8::2/128\", \"protocol\": \"any\", \"port\": 173}, {\"cidr\": \"2001:db8::1d2/128\", \"protocol\": \"tcp\", \"port\": 189}, {\"cidr\": \"10.0.1.4/32\", \"protocol\": \"tcp\", \"port\": 37, \"endPort\": 38}, {\"cidr\": \"10.0.1.2/31\", \"protocol\": \"tcp\", \"port\": 277, \"endPort\": 281}, {\"cidr\": \"2001:db8::f9/128\", \"protocol\": \"tcp\", \"port\": 351}, {\"cidr\": \"10.0.0.228/30\", \"protocol\": \"any\", \"port\": 145}, {\"cidr\": \"10.0.2.212/30\", \"protocol\": \"udp\", \"port\": 89}, {\"cidr\": \"10.0.0.76/30\", \"protocol\": \"any\", \"port\": 379, \"endPort\": 380}, {\"cidr\": \"2001:db8::2ed/128\", \"protocol\": \"udp\", \"port\": 155}, {\"cidr\": \"10.0.0.236/31\", \"protocol\": \"udp\", \"port\": 187}, {\"cidr\": \"10.0.2.160/32\", \"protocol\": \"tcp\", \"port\": 155}, {\"cidr\": \"10.0.1.18/31\", \"protocol\": \"tcp\", \"port\": 233}, {\"cidr\": \"10.0.0.2/32\", \"protocol\": \"udp\", \"port\": 13}, {\"cidr\": \"2001:db8::91/128\", \"protocol\": \"any\", \"port\": 209}, {\"cidr\": \"10.0.2.168/30\", \"protocol\": \"any\", \"port\": 315, \"endPort\": 317}, {\"cidr\": \"10.0.1.60/30\", \"protocol\": \"udp\", \"port\": 329}, {\"cidr\": \"10.0.3.176/30\", \"protocol\": \"tcp\", \"port\": 45}, {\"cidr\": \"10.0.0.24/30\", \"protocol\": \"any\", \"port\": 7, \"endPort\": 8}, {\"cidr\": \"10.0.0.71/32\", \"protocol\": \"tcp\", \"port\": 117}, {\"cidr\": \"10.0.0.47/32\", \"protocol\": \"any\", \"port\": 163}, {\"cidr\": \"10.0.1.73/32\", \"protocol\": \"tcp\", \"port\": 227}, {\"cidr\": \"10.0.1.32/30\", \"protocol\": \"tcp\", \"port\": 147}, {\"cidr\": \"10.0.0.60/30\", \"protocol\": \"any\", \"port\": 67}, {\"cidr\": \"2001:db8::126/128\", \"protocol\": \"any\", \"port\": 229, \"endPort\": 233}, {\"cidr\": \"10.0.2.232/31\", \"protocol\": \"tcp\", \"port\": 127}, {\"cidr\": \"2001:db8::236/128\", \"protocol\": \"udp\", \"port\": 49, \"endPort\": 52}, {\"cidr\": \"10.0.3.194/31\", \"protocol\": \"any\", \"port\": 395}, {\"cidr\": \"10.0.0.184/30\", \"protocol\": \"tcp\", \"port\": 55}, {\"cidr\": \"10.0.0.112/30\", \"protocol\": \"udp\", \"port\": 149}, {\"cidr\": \"2001:db8::2cf/128\", \"protocol\": \"any\", \"port\": 279, \"endPort\": 281}, {\"cidr\": \"10.0.3.132/30\", \"protocol\": \"tcp\", \"port\": 85, \"endPort\": 86}, {\"cidr\": \"10.0.0.90/31\", \"protocol\": \"udp\", \"port\": 21, \"endPort\": 22}, {\"cidr\": \"2001:db8::1f7/128\", \"protocol\": \"tcp\", \"port\": 143}, {\"cidr\": \"10.0.0.172/32\", \"protocol\": \"any\", \"port\": 363}, {\"cidr\": \"10.0.2.8/31\", \"protocol\": \"tcp\", \"port\": 85, \"endPort\": 87}, {\"cidr\": \"10.0.0.169/32\", \"protocol\": \"tcp\", \"port\": 219, \"endPort\": 219}, {\"cidr\": \"10.0.3.192/31\", \"protocol\": \"any\", \"port\": 343}, {\"cidr\": \"2001:db8::3c9/128\", \"protocol\": \"tcp\", \"port\": 209}, {\"cidr\": \"2001:db8::3c0/128\", \"protocol\": \"udp\", \"port\": 63, \"endPort\": 65}, {\"cidr\": \"10.0.2.0/30\", \"protocol\": \"udp\", \"port\": 71}, {\"cidr\": \"10.0.3.56/30\", \"protocol\": \"any\", \"port\": 139}, {\"cidr\": \"2001:db8::236/128\", \"protocol\": \"tcp\", \"port\": 361, \"endPort\": 365}, {\"cidr\": \"10.0.2.8/31\", \"protocol\": \"tcp\", \"port\": 257}, {\"cidr\": \"10.0.1.18/32\", \"protocol\": \"tcp\", \"port\": 355}, {\"cidr\": \"2001:db8::11d/128\", \"protocol\": \"tcp\", \"port\": 65, \"endPort\": 69}, {\"cidr\": \"10.0.0.98/31\", \"protocol\": \"udp\", \"port\": 179}, {\"cidr\": \"10.0.0.186/31\", \"protocol\": \"tcp\", \"port\": 157}, {\"cidr\": \"10.0.1.124/31\", \"protocol\": \"tcp\", \"port\": 133, \"endPort\": 137}, {\"cidr\": \"10.0.0.210/32\", \"protocol\": \"any\", \"port\": 61, \"endPort\": 61}, {\"cidr\": \"10.0.0.96/30\", \"protocol\": \"any\", \"port\": 241}, {\"cidr\": \"10.0.0.28/31\", \"protocol\": \"tcp\", \"port\": 335, \"endPort\": 339}, {\"cidr\": \"2001:db8::69/128\", \"protocol\": \"tcp\", \"port\": 203, \"endPort\": 207}, {\"cidr\": \"10.0.1.26/31\", \"protocol\": \"tcp\", \"port\": 389}, {\"cidr\": \"10.0.1.76/30\", \"protocol\": \"any\", \"port\": 219, \"endPort\": 220}, {\"cidr\": \"10.0.1.230/31\", \"protocol\": \"any\", \"port\": 203, \"endPort\": 204}, {\"cidr\": \"10.0.0.74/31\", \"protocol\": \"any\", \"port\": 65}, {\"cidr\": \"10.0.3.152/30\", \"protocol\": \"udp\", \"port\": 145}, {\"cidr\": \"10.0.3.10/31\", \"protocol\": \"tcp\", \"port\": 371}, {\"cidr\": \"10.0.1.134/32\", \"protocol\": \"udp\", \"port\": 3}, {\"cidr\": \"10.0.3.107/32\", \"protocol\": \"udp\", \"port\": 57}, {\"cidr\": \"2001:db8::2d5/128\", \"protocol\": \"any\", \"port\": 333}, {\"cidr\": \"10.0.1.185/32\", \"protocol\": \"tcp\", \"port\": 119}, {\"cidr\": \"10.0.2.42/32\", \"protocol\": \"any\", \"port\": 23}, {\"cidr\": \"10.0.0.65/32\", \"protocol\": \"tcp\", \"port\": 187, \"endPort\": 189}, {\"cidr\": \"10.0.2.244/31\", \"protocol\": \"any\", \"port\": 89}, {\"cidr\": \"2001:db8::3b9/128\", \"protocol\": \"tcp\", \"port\": 333}, {\"cidr\": \"10.0.3.220/31\", \"protocol\": \"udp\", \"port\": 103}, {\"cidr\": \"10.0.3.240/32\", \"protocol\": \"udp\", \"port\": 283, \"endPort\": 286}, {\"cidr\": \"10.0.3.40/31\", \"protocol\": \"tcp\", \"port\": 133}, {\"cidr\": \"10.0.0.230/31\", \"protocol\": \"any\", \"port\": 191}, {\"cidr\": \"2001:db8::3cf/128\", \"protocol\": \"tcp\", \"port\": 381, \"endPort\": 384}, {\"cidr\": \"2001:db8::172/128\", \"protocol\": \"udp\", \"port\": 395}, {\"cidr\": \"10.0.3.236/30\", \"protocol\": \"any\", \"port\": 95}, {\"cidr\": \"10.0.1.192/30\", \"protocol\": \"udp\", \"port\": 33}, {\"cidr\": \"10.0.0.148/31\", \"protocol\": \"any\", \"port\": 57, \"endPort\": 57}, {\"cidr\": \"10.0.2.202/32\", \"protocol\": \"any\", \"port\": 389}, {\"cidr\": \"10.0.3.16/31\", \"protocol\": \"tcp\", \"port\": 85}, {\"cidr\": \"10.0.2.98/31\", \"protocol\": \"udp\", \"port\": 183}, {\"cidr\": \"10.0.3.36/31\", \"protocol\": \"udp\", \"port\": 271, \"endPort\": 273}, {\"cidr\": \"10.0.3.234/31\", \"protocol\": \"tcp\", \"port\": 387}, {\"cidr\": \"10.0.3.146/31\", \"protocol\": \"any\", \"port\": 375}, {\"cidr\": \"2001:db8::ae/128\", \"protocol\": \"any\", \"port\": 365}, {\"cidr\": \"10.0.0.200/30\", \"protocol\": \"tcp\", \"port\": 239}, {\"cidr\": \"2001:db8::3e6/128\", \"protocol\": \"tcp\", \"port\": 29}, {\"cidr\": \"10.0.0.104/32\", \"protocol\": \"any\", \"port\": 257}, {\"cidr\": \"2001:db8::15b/128\", \"protocol\": \"any\", \"port\": 53}, {\"cidr\": \"10.0.0.86/31\", \"protocol\": \"any\", \"port\": 297, \"endPort\": 300}, {\"cidr\": \"2001:db8::2cc/128\", \"protocol\": \"tcp\", \"port\": 73}, {\"cidr\": \"10.0.2.3/32\", \"protocol\": \"udp\", \"port\": 3}, {\"cidr\": \"10.0.3.196/31\", \"protocol\": \"udp\", \"port\": 135}, {\"cidr\": \"10.0.1.216/30\", \"protocol\": \"any\", \"port\": 15}, {\"cidr\": \"2001:db8::6f/128\", \"protocol\": \"udp\", \"port\": 21}, {\"cidr\": \"2001:db8::3cc/128\", \"protocol\": \"udp\", \"port\": 169}, {\"cidr\": \"10.0.2.124/30\", \"protocol\": \"any\", \"port\": 81}, {\"cidr\": \"2001:db8::6d/128\", \"protocol\": \"udp\", \"port\": 79}, {\"cidr\": \"10.0.0.205/32\", \"protocol\": \"tcp\", \"port\": 243}, {\"cidr\": \"10.0.0.244/32\", \"protocol\": \"udp\", \"port\": 389, \"endPort\": 389}, {\"cidr\": \"10.0.3.228/30\", \"protocol\": \"udp\", \"port\": 43, \"endPort\": 43}, {\"cidr\": \"10.0.1.188/31\", \"protocol\": \"any\", \"port\": 165}, {\"cidr\": \"2001:db8::2ec/128\", \"protocol\": \"any\", \"port\": 39}, {\"cidr\": \"10.0.2.240/32\", \"protocol\": \"any\", \"port\": 259, \"endPort\": 260}, {\"cidr\": \"10.0.3.142/31\", \"protocol\": \"tcp\", \"port\": 365}, {\"cidr\": \"10.0.2.195/32\", \"protocol\": \"any\", \"port\": 213}, {\"cidr\": \"2001:db8::2bd/128\", \"protocol\": \"any\", \"port\": 139}, {\"cidr\": \"2001:db8::3a9/128\", \"protocol\": \"udp\", \"port\": 7}, {\"cidr\": \"10.0.1.104/30\", \"protocol\": \"tcp\", \"port\": 135}, {\"cidr\": \"10.0.1.56/30\", \"protocol\": \"any\", \"port\": 341}, {\"cidr\": \"10.0.1.47/32\", \"protocol\": \"udp\", \"port\": 227}, {\"cidr\": \"2001:db8::3a7/128\", \"protocol\": \"tcp\", \"port\": 161, \"endPort\": 162}, {\"cidr\": \"2001:db8::20a/128\", \"protocol\": \"udp\", \"port\": 337}, {\"cidr\": \"2001:db8::144/128\", \"protocol\": \"any\", \"port\": 133, \"endPort\": 133}, {\"cidr\": \"10.0.0.128/31\", \"protocol\": \"udp\", \"port\": 197}, {\"cidr\": \"2001:db8::1d4/128\", \"protocol\": \"tcp\", \"port\": 337, \"endPort\": 340}, {\"cidr\": \"10.0.2.198/32\", \"protocol\": \"tcp\", \"port\": 381}, {\"cidr\": \"2001:db8::259/128\", \"protocol\": \"tcp\", \"port\": 23, \"endPort\": 24}, {\"cidr\": \"10.0.1.202/31\", \"protocol\": \"tcp\", \"port\": 19}, {\"cidr\": \"10.0.2.88/30\", \"protocol\": \"udp\", \"port\": 15}, {\"cidr\": \"10.0.0.232/30\", \"protocol\": \"tcp\", \"port\": 217}, {\"cidr\": \"10.0.1.28/30\", \"protocol\": \"udp\", \"port\": 191, \"endPort\": 191}, {\"cidr\": \"10.0.1.116/32\", \"protocol\": \"udp\", \"port\": 301, \"endPort\": 305}, {\"cidr\": \"2001:db8::225/128\", \"protocol\": \"udp\", \"port\": 325}, {\"cidr\": \"2001:db8::339/128\", \"protocol\": \"tcp\", \"port\": 297}, {\"cidr\": \"10.0.2.116/30\", \"protocol\": \"udp\", \"port\": 287}, {\"cidr\": \"10.0.0.232/31\", \"protocol\": \"any\", \"port\": 277}, {\"cidr\": \"10.0.0.56/30\", \"protocol\": \"any\", \"port\": 167, \"endPort\": 171}, {\"cidr\": \"10.0.2.62/31\", \"protocol\": \"udp\", \"port\": 353}, {\"cidr\": \"10.0.2.254/31\", \"protocol\": \"udp\", \"port\": 393}, {\"cidr\": \"10.0.2.6/32\", \"protocol\": \"udp\", \"port\": 247}, {\"cidr\": \"10.0.2.190/31\", \"protocol\": \"any\", \"port\": 227, \"endPort\": 231}, {\"cidr\": \"2001:db8::2d3/128\", \"protocol\": \"any\", \"port\": 23}, {\"cidr\": \"10.0.1.100/32\", \"protocol\": \"udp\", \"port\": 279, \"endPort\": 281}, {\"cidr\": \"10.0.0.196/30\", \"protocol\": \"tcp\", \"port\": 9, \"endPort\": 11}, {\"cidr\": \"10.0.2.128/31\", \"protocol\": \"udp\", \"port\": 343}, {\"cidr\": \"2001:db8::377/128\", \"protocol\": \"udp\", \"port\": 83}, {\"cidr\": \"2001:db8::c5/128\", \"protocol\": \"udp\", \"port\": 199}, {\"cidr\": \"2001:db8::2ea/128\", \"protocol\": \"any\", \"port\": 21}, {\"cidr\": \"10.0.1.156/31\", \"protocol\": \"tcp\", \"port\": 179}, {\"cidr\": \"2001:db8::340/128\", \"protocol\": \"any\", \"port\": 301}, {\"cidr\": \"10.0.0.248/30\", \"protocol\": \"tcp\", \"port\": 119}, {\"cidr\": \"10.0.1.176/30\", \"protocol\": \"udp\", \"port\": 139}, {\"cidr\": \"10.0.1.198/32\", \"protocol\": \"tcp\", \"port\": 121}, {\"cidr\": \"10.0.1.112/31\", \"protocol\": \"tcp\", \"port\": 145}, {\"cidr\": \"2001:db8::4f/128\", \"protocol\": \"tcp\", \"port\": 295}, {\"cidr\": \"2001:db8::4/128\", \"protocol\": \"udp\", \"port\": 199, \"endPort\": 203}, {\"cidr\": \"10.0.0.80/31\", \"protocol\": \"any\", \"port\": 323}, {\"cidr\": \"10.0.0.250/31\", \"protocol\": \"udp\", \"port\": 79}, {\"cidr\": \"10.0.3.26/31\", \"protocol\": \"any\", \"port\": 301, \"endPort\": 301}, {\"cidr\": \"2001:db8::1dd/128\", \"protocol\": \"udp\", \"port\": 339, \"endPort\": 340}, {\"cidr\": \"10.0.1.45/32\", \"protocol\": \"any\", \"port\": 141}, {\"cidr\": \"2001:db8::f8/128\", \"protocol\": \"udp\", \"port\": 59}, {\"cidr\": \"2001:db8::ab/128\", \"protocol\": \"any\", \"port\": 293, \"endPort\": 293}, {\"cidr\": \"2001:db8::f7/128\", \"protocol\": \"udp\", \"port\": 33}, {\"cidr\": \"10.0.2.36/32\", \"protocol\": \"any\", \"port\": 371, \"endPort\": 372}, {\"cidr\": \"2001:db8::2c6/128\", \"protocol\": \"tcp\", \"port\": 351, \"endPort\": 352}, {\"cidr\": \"2001:db8::1c3/128\", \"protocol\": \"any\", \"port\": 145}, {\"cidr\": \"2001:db8::28e/128\", \"protocol\": \"tcp\", \"port\": 299}, {\"cidr\": \"10.0.3.148/30\", \"protocol\": \"tcp\", \"port\": 287}, {\"cidr\": \"10.0.1.160/32\", \"protocol\": \"any\", \"port\": 395, \"endPort\": 397}, {\"cidr\": \"10.0.2.228/30\", \"protocol\": \"tcp\", \"port\": 119}, {\"cidr\": \"10.0.0.116/31\", \"protocol\": \"udp\", \"port\": 153, \"endPort\": 155}, {\"cidr\": \"10.0.1.56/31\", \"protocol\": \"any\", \"port\": 1, \"endPort\": 4}, {\"cidr\": \"2001:db8::11d/128\", \"protocol\": \"any\", \"port\": 71}, {\"cidr\": \"2001:db8::3a1/128\", \"protocol\": \"any\", \"port\": 341, \"endPort\": 342}, {\"cidr\": \"10.0.3.180/31\", \"protocol\": \"tcp\", \"port\": 213}, {\"cidr\": \"10.0.2.102/31\", \"protocol\": \"any\", \"port\": 385}, {\"cidr\": \"10.0.3.104/30\", \"protocol\": \"tcp\", \"port\": 377}, {\"cidr\": \"2001:db8::1c7/128\", \"protocol\": \"any\", \"port\": 237}, {\"cidr\": \"10.0.3.89/32\", \"protocol\": \"any\", \"port\": 173}, {\"cidr\": \"2001:db8::cd/128\", \"protocol\": \"udp\", \"port\": 77}, {\"cidr\": \"2001:db8::3d4/128\", \"protocol\": \"udp\", \"port\": 397}, {\"cidr\": \"2001:db8::244/128\", \"protocol\": \"udp\", \"port\": 77}, {\"cidr\": \"10.0.1.169/32\", \"protocol\": \"tcp\", \"port\": 279, \"endPort\": 283}, {\"cidr\": \"10.0.3.86/31\", \"protocol\": \"udp\", \"port\": 17}, {\"cidr\": \"2001:db8::55/128\", \"protocol\": \"udp\", \"port\": 13}, {\"cidr\": \"2001:db8::21b/128\", \"protocol\": \"udp\", \"port\": 355}, {\"cidr\": \"10.0.1.208/31\", \"protocol\": \"tcp\", \"port\": 123}, {\"cidr\": \"10.0.1.184/30\", \"protocol\": \"any\", \"port\": 191}, {\"cidr\": \"2001:db8::111/128\", \"protocol\": \"any\", \"port\": 355}, {\"cidr\": \"2001:db8::f1/128\", \"protocol\": \"tcp\", \"port\": 395}, {\"cidr\": \"10.0.1.244/30\", \"protocol\": \"tcp\", \"port\": 27}, {\"cidr\": \"10.0.1.12/30\", \"protocol\": \"any\", \"port\": 241}, {\"cidr\": \"10.0.3.16/31\", \"protocol\": \"udp\", \"port\": 333, \"endPort\": 334}, {\"cidr\": \"10.0.2.252/30\", \"protocol\": \"tcp\", \"port\": 167}, {\"cidr\": \"10.0.0.170/32\", \"protocol\": \"udp\", \"port\": 311}, {\"cidr\": \"10.0.3.46/31\", \"protocol\": \"any\", \"port\": 55}, {\"cidr\": \"2001:db8::156/128\", \"protocol\": \"udp\", \"port\": 311}, {\"cidr\": \"10.0.3.252/31\", \"protocol\": \"any\", \"port\": 177, \"endPort\": 179}, {\"cidr\": \"2001:db8::58/128\", \"protocol\": \"udp\", \"port\": 137, \"endPort\": 140}, {\"cidr\": \"10.0.2.10/31\", \"protocol\": \"udp\", \"port\": 259}, {\"cidr\": \"10.0.3.64/31\", \"protocol\": \"udp\", \"port\": 189, \"endPort\": 189}, {\"cidr\": \"10.0.0.49/32\", \"protocol\": \"tcp\", \"port\": 123}]}"}],"expected":{"allow":["10.0.0.0/24","10.0.2.0/31","10.0.3.130/31","2001:db8::118/127","2001:db8::2c5/128"],"deny":["0.0.0.0/0:icmp","0.0.0.0/0:tcp","0.0.0.0/0:udp:81","0.0.0.0/0:udp:444-3435","10.0.0.0/32:udp:299","10.0.0.2/32:udp:13","10.0.0.11/32:udp:223","10.0.0.14/31:udp:33","10.0.0.24/30:udp:7-8","10.0.0.28/30:udp:85","10.0.0.46/32:udp:225","10.0.0.47/32:udp:163","10.0.0.56/30:udp:167-171","10.0.0.60/30:udp:67","10.0.0.74/31:udp:65","10.0.0.76/30:udp:379-380","10.0.0.80/31:udp:323","10.0.0.86/31:udp:297-300","10.0.0.90/31:udp:21-22","10.0.0.96/30:udp:241","10.0.0.98/31:udp:179","10.0.0.104/32:udp:257","10.0.0.112/30:udp:149","10.0.0.116/31:udp:153-155","10.0.0.122/31:udp:249","10.0.0.124/30:udp:243","10.0.0.128/31:udp:197","10.0.0.148/31:udp:57","10.0.0.170/32:udp:79-82","10.0.0.170/32:udp:311","10.0.0.172/32:udp:363","10.0.0.198/31:udp:365","10.0.0.210/32:udp:61","10.0.0.225/32:udp:117-121","10.0.0.228/30:udp:145","10.0.0.230/31:udp:191","10.0.0.232/30:udp:131","10.0.0.232/31:udp:277","10.0.0.236/31:udp:187","10.0.0.244/32:udp:389","10.0.0.250/31:udp:79","10.0.1.12/30:udp:241","10.0.1.28/30:udp:191","10.0.1.40/30:udp:25","10.0.1.45/32:udp:141","10.0.1.46/32:udp:239","10.0.1.47/32:udp:227","10.0.1.52/30:udp:103","10.0.1.56/30:udp:341","10.0.1.56/31:udp:1-4","10.0.1.60/30:udp:329","10.0.1.76/30:udp:219-220","10.0.1.92/30:udp:393","10.0.1.96/31:udp:189","10.0.1.100/32:udp:279-281","10.0.1.113/32:udp:327-331","10.0.1.116/32:udp:301-305","10.0.1.134/32:udp:3","10.0.1.152/31:udp:247","10.0.1.160/27","10.0.1.192/30:udp:33","10.0.1.196/32:udp:15","10.0.1.216/30:udp:15","10.0.1.230/31:udp:203-204","10.0.1.248/30:udp:291","10.0.1.252/30:udp:95","10.0.2.0/30:udp:71","10.0.2.3/32:udp:3","10.0.2.6/32:udp:247","10.0.2.10/31:udp:259","10.0.2.36/32:udp:371-372","10.0.2.40/30:udp:253","10.0.2.42/32:udp:23","10.0.2.46/32:udp:93","10.0.2.54/32:udp:143","10.0.2.56/30:udp:135","10.0.2.62/31:udp:353","10.0.2.68/30:udp:69","10.0.2.88/30:udp:15","10.0.2.98/31:udp:183","10.0.2.102/31:udp:385","10.0.2.108/32:udp:179","10.0.2.111/32:udp:19","10.0.2.116/30:udp:287","10.0.2.128/29","10.0.2.136/30","10.0.2.140/31","10.0.2.144/28","10.0.2.160/28","10.0.2.176/29","10.0.2.184/30","10.0.2.190/31:udp:227-231","10.0.2.195/32:udp:213","10.0.2.202/32:udp:389","10.0.2.212/30:udp:89","10.0.2.230/31:udp:265","10.0.2.240/32:udp:259-260","10.0.2.242/31:udp:393","10.0.2.244/31:udp:89","10.0.2.254/31:udp:393","10.0.3.16/31:udp:333-334","10.0.3.26/31:udp:301","10.0.3.28/32:udp:279","10.0.3.34/31:udp:5","10.0.3.36/30:udp:263","10.0.3.36/31:udp:271-273","10.0.3.46/31:udp:55","10.0.3.56/30:udp:139","10.0.3.64/31:udp:189","10.0.3.84/31:udp:333","10.0.3.86/31:udp:17","10.0.3.89/32:udp:173","10.0.3.90/31:udp:97-98","10.0.3.107/32:udp:57","10.0.3.113/32:udp:155-157","10.0.3.146/31:udp:7","10.0.3.146/31:udp:375","10.0.3.152/30:udp:145","10.0.3.154/32:udp:31","10.0.3.192/31:udp:343","10.0.3.194/31:udp:395","10.0.3.196/31:udp:135","10.0.3.214/32:udp:199-203","10.0.3.220/31:udp:103","10.0.3.228/30:udp:43","10.0.3.230/31:udp:313-314","10.0.3.236/30:udp:95","10.0.3.240/32:udp:283-286","10.0.3.247/32:udp:297","10.0.3.252/31:udp:177-179","::/0:icmp","::/0:tcp","::/0:udp:81","::/0:udp:444-3435","2001:db8::2/128:udp:173","2001:db8::4/128:udp:199-203","2001:db8::3c/128:udp:295-298","2001:db8::40/122","2001:db8::83/128:udp:51","2001:db8::91/128:udp:209","2001:db8::a0/128:udp:287","2001:db8::ab/128:udp:293","2001:db8::ae/128:udp:365","2001:db8::c5/128:udp:199","2001:db8::cd/128:udp:77","2001:db8::f7/128:udp:33","2001:db8::f8/128:udp:59","2001:db8::111/128:udp:355","2001:db8::113/128:udp:193","2001:db8::11d/128:udp:71","2001:db8::126/128:udp:229-233","2001:db8::129/128:udp:49-53","2001:db8::144/128:udp:133","2001:db8::156/128:udp:311","2001:db8::15b/128:udp:53","2001:db8::172/128:udp:395","2001:db8::188/128:udp:321-324","2001:db8::1a9/128:udp:239","2001:db8::1c3/128:udp:145","2001:db8::1c7/128:udp:237","2001:db8::1dd/128:udp:339-340","2001:db8::1dd/128:udp:395","2001:db8::1e6/128:udp:239","2001:db8::200/128:udp:169","2001:db8::20a/128:udp:337","2001:db8::21b/128:udp:355","2001:db8::225/128:udp:325","2001:db8::236/128:udp:49-52","2001:db8::244/128:udp:77","2001:db8::288/128:udp:89","2001:db8::2bd/128:udp:139","2001:db8::2c7/128:udp:111-114","2001:db8::2cf/128:udp:279-281","2001:db8::2d3/128:udp:23","2001:db8::2d5/128:udp:333","2001:db8::2ea/128:udp:21","2001:db8::2ec/128:udp:39","2001:db8::2ed/128:udp:155","2001:db8::340/128:udp:301","2001:db8::377/128:udp:83","2001:db8::378/128:udp:381","2001:db8::382/128:udp:219-223","2001:db8::38a/128:udp:25","2001:db8::3a1/128:udp:341-342","2001:db8::3a9/128:udp:7","2001:db8::3c0/128:udp:63-65","2001:db8::3cc/128:udp:169","2001:db8::3d4/128:udp:397","2001:db8::3da/128:udp:273"]}}, + {"name":"random #145","input":[{"text":"{\"deny\":[]}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #146","input":[{"text":"{\"allow\": [], \"allow\": [{\"cidr\": \"c045:a4e3:d0d0:372f:484e:11:e92b:5e00/120\", \"protocol\": \"tcp\", \"port\": 443, \"endPort\": 45408}, {\"cidr\": \"250.35.11.17/0\"}, {\"to\": [{\"cidr\": \"178.0.0.0/8\"}]}, {\"to\": [{\"cidr\": \"2001:db8::224/128\"}, {\"cidr\": \"10.0.2.128/00027\", \"except\": [\"10.0.2.144/28\", \"10.0.2.128/27\", \"10.0.2.128/28\"]}, {\"cidr\": \"e000::/3\", \"except\": [\"fb9e:4235:aae5:632e:669f:f403:de0:6414/08\"]}, {\"cidr\": \"10.0.0.0/00022\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 7, \"endPort\": 8568}, {\"protocol\": \"udp\"}, {\"protocol\": \"tcp\", \"port\": 65534}]}], \"deny\": [{\"to\": [{\"cidr\": \"10.0.0.0/25\"}, {\"cidr\": \"117.212.204.0/24\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"tcp\", \"port\": 65531, \"endPort\": 65531}]}]}"}],"expected":{"error":"duplicate JSON property: allow"}}, + {"name":"random #147","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 445, \n\t\t\t\t\t\"endPort\": 450\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::161/125\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.192/26\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.1.224/27\", \n\t\t\t\t\t\t\"10.0.1.192/27\", \n\t\t\t\t\t\t\"10.0.1.254/31\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"454D:6B9D:7AE1:FC58:D963:EE21:245E:40C2/120\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::300/00121\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::336/128\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::120/126\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:db8::123\", \n\t\t\t\t\t\t\"2001:db8::120/126\", \n\t\t\t\t\t\t\"2001:db8::122/128\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65535\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t], \n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.26/00031\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.32/28\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"162.104.211.148/1\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":["0.0.0.0/0:tcp","::/0:tcp","2001:db8::160/125","2001:db8::300/121:icmp","2001:db8::300/121:udp:65535","454d:6b9d:7ae1:fc58:d963:ee21:245e:4000/120:icmp","454d:6b9d:7ae1:fc58:d963:ee21:245e:4000/120:udp:65535"],"deny":["10.0.0.26/31","10.0.0.32/28","128.0.0.0/1"]}}, + {"name":"random #148","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"96.0.0.0/4\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.32/28\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\",\n\t\t\t\t\t\"port\": 10\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"4220:2525:3b7a:b024:40e0:4161:6ec5:c300/120\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 65535\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::130/124\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.189/22\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\",\n\t\t\t\t\t\"port\": 81,\n\t\t\t\t\t\"endPort\": 83\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 65530,\n\t\t\t\t\t\"endPort\": 65534\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 6,\n\t\t\t\t\t\"endPort\": 9\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 80\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 3\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.2.240/255.255.255.224\",\n\t\t\t\"except\": [\n\t\t\t\t\"10.0.2.249\"\n\t\t\t],\n\t\t\t\"protocol\": \"any\",\n\t\t\t\"port\": 80,\n\t\t\t\"endPort\": 82\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::d0/126\",\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 442\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.32/28\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 7\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"port\": 2,\n\t\t\t\t\t\"endPort\": 2\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.104\",\n\t\t\t\"except\": [\n\t\t\t\t\"10.0.1.104/32\"\n\t\t\t],\n\t\t\t\"protocol\": \"tcp\",\n\t\t\t\"port\": 65533\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.0/24\",\n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 3,\n\t\t\t\t\t\"endPort\": 5\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"125.6.187.218/0.0.0.1\",\n\t\t\t\"protocol\": \"tcp\"\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0280/121\",\n\t\t\t\"protocol\": \"tcp\"\n\t\t}\n\t]\n}"}],"expected":{"error":"allow[2].ports[0].port is not supported with icmp"}}, + {"name":"random #149","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"885b:4af7::/32\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 443\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.0/24\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.1.65\", \n\t\t\t\t\t\t\"10.0.1.188/30\", \n\t\t\t\t\t\t\"10.0.1.252/32\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.188\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.9/31\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.144/29\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.2.144/29\", \n\t\t\t\t\t\t\"10.0.2.150/31\", \n\t\t\t\t\t\t\"10.0.2.149/031\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"port\": 79\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"port\": 65530\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 79, \n\t\t\t\t\t\"endPort\": 9326\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.147/30\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 65531\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 441\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 11, \n\t\t\t\t\t\"endPort\": 14\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"2001:DB8::3AA/122\", \n\t\t\t\"protocol\": \"any\"\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"518d:43af:6a29:c3b:c27f:24fa::/96\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"518d:43af:6a29:0c3b:c27f:24fa:0000:0000/97\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.128/28\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.37.26.30\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"145.123.126.13/31\", \n\t\t\t\"protocol\": \"tcp\", \n\t\t\t\"port\": 82\n\t\t}\n\t], \n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"23c1:b37c:a7ca:91f8:d4fb:1710:b26:4272/1\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"5000::/4\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"0.0.0.0/1\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 440, \n\t\t\t\t\t\"endPort\": 55152\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 443, \n\t\t\t\t\t\"endPort\": 445\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65534, \n\t\t\t\t\t\"endPort\": 65535\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 1, \n\t\t\t\t\t\"endPort\": 4\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.105/28\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::100/000121\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"128.0.0.0/240.0.0.0\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.156/30\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65530\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 1\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":["10.0.0.144/30:tcp:11-14","10.0.0.144/30:tcp:65531","10.0.0.144/30:udp:11-14","10.0.0.144/30:udp:441","10.0.1.0/26","10.0.1.64/32","10.0.1.66/31","10.0.1.68/30","10.0.1.72/29","10.0.1.80/28","10.0.1.96/27","10.0.1.128/27","10.0.1.160/28","10.0.1.176/29","10.0.1.184/30","10.0.1.192/27","10.0.1.224/28","10.0.1.240/29","10.0.1.248/30","10.0.1.253/32","10.0.1.254/31","10.0.2.188/32","10.0.3.8/31","10.37.26.30/32","145.123.126.12/31:tcp:82","2001:db8::380/122","518d:43af:6a29:c3b:c27f:24fa:8000:0/97","885b:4af7::/32:icmp","885b:4af7::/32:tcp"],"deny":["0.0.0.0/0:tcp","0.0.0.0/0:udp:65534-65535","0.0.0.0/1:udp:443-445","10.0.0.156/30:icmp","10.0.0.156/30:udp:65530","10.0.3.96/28:icmp","10.0.3.96/28:udp:65530","128.0.0.0/4:icmp","128.0.0.0/4:udp:65530","::/0:tcp","::/0:udp:65534-65535","::/2:udp:443-445","2001:db8::100/121:icmp","2001:db8::100/121:udp:65530","4000::/4:udp:443-445","6000::/3:udp:443-445"]}}, + {"name":"random #150","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"2001:DB8::300/123\"}, {\"cidr\": \"2001:db8::10/126\", \"except\": [\"2001:db8::12/127\", \"2001:DB8::11\"]}, {\"cidr\": \"10.0.1.212/32\"}], \"ports\": [{\"protocol\": \"udp\", \"port\": 11, \"endPort\": 13}, {\"protocol\": \"tcp\", \"port\": 442, \"endPort\": 443}]}, {\"cidr\": \"10.0.0.0/22\", \"protocol\": \"tcp\", \"port\": 79}, {\"cidr\": \"10.0.1.92/30\", \"protocol\": \"any\", \"port\": 4}, {\"cidr\": \"2001:DB8::70/124\", \"protocol\": \"udp\"}, {\"to\": [{\"cidr\": \"10.0.2.64/26\", \"except\": [\"10.0.2.104/0.0.0.7\"]}, {\"cidr\": \"2001:DB8::140/122\"}, {\"cidr\": \"8aae:1c17:5def:95fb:3efa:5079:b0f3:ec00/120\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 443}, {\"protocol\": \"udp\", \"port\": 8}]}, {}], \"deny\": [{\"to\": [{\"cidr\": \"10.0.2.32/027\"}, {\"cidr\": \"10.0.0.0/255.255.254.0\", \"except\": []}, {\"cidr\": \"10.0.1.224/27\"}]}, {\"cidr\": \"10.0.0.201\", \"except\": [\"10.0.0.201\", \"10.0.0.201\", \"10.0.0.201\"], \"protoc"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"random #151","input":[{"text":"{\n \"allow\": [\n {\n \"to\": [\n {\n \"cidr\": \"c179:f3fc:c0b8:b589:bdcf:49a4:5100:0/000104\"\n },\n {\n \"cidr\": \"2001:db8::/118\"\n },\n {\n \"cidr\": \"10.0.1.0/255.255.255.0\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"any\",\n \"port\": 442,\n \"endPort\": 446\n }\n ]\n },\n {\n \"cidr\": \"217.153.117.185/2\",\n \"except\": [],\n \"protocol\": \"udp\"\n },\n {\n \"cidr\": \"2001:db8::180/121\",\n \"except\": [\n \"2001:db8::1ca/127\",\n \"2001:db8::1c0/124\"\n ],\n \"protocol\": \"tcp\"\n },\n {\n \"cidr\": \"7e3e:abe8:18b2:dc19:7b44:f0e::/96\",\n \"except\": [\n \"7e3e:abe8:18b2:dc19:7b44:f0e:8000:0/97\",\n \"7e3e:abe8:18b2:dc19:7b44:f0e:2200:0/103\"\n ],\n \"protocol\": \"tcp\"\n },\n {\n \"cidr\": \"2001:DB8::360/126\"\n },\n {\n \"ports\": [\n {\n \"protocol\": \"tcp\",\n \"port\": 5,\n \"endPort\": 6\n },\n {\n \"protocol\": \"any\"\n },\n {\n \"protocol\": \"tcp\",\n \"port\": 444,\n \"endPort\": 447\n },\n {\n \"protocol\": \"icmp\"\n }\n ]\n },\n {\n \"cidr\": \"2001:db8::1e0/123\",\n \"except\": [\n \"2001:db8::1e0/124\"\n ],\n \"protocol\": \"icmp\"\n },\n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::/119\",\n \"except\": [\n \"2001:db8::1a1/124\",\n \"2001:db8::1ae/127\"\n ]\n },\n {\n \"cidr\": \"10.0.3.98/255.255.255.254\"\n },\n {\n \"cidr\": \"10.0.2.105/24\"\n },\n {\n \"cidr\": \"10.0.2.210/255.255.255.255\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"any\",\n \"port\": 444\n },\n {\n \"protocol\": \"icmp\"\n },\n {}\n ]\n }\n ],\n \"deny\": [\n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.30/00031\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"tcp\",\n \"port\": 65533\n },\n {\n \"protocol\": \"any\",\n \"port\": 12\n }\n ]\n },\n {\n \"to\": [\n {\n \"cidr\": \"10.0.1.128/25\",\n \"except\": [\n \"10.0.1.192/29\"\n ]\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"udp\",\n \"port\": 441\n },\n {\n \"protocol\": \"udp\"\n },\n {\n \"protocol\": \"udp\",\n \"port\": 65534\n },\n {\n \"protocol\": \"tcp\"\n }\n ]\n },\n {\n \"cidr\": \"10.0.1.138/026\",\n \"protocol\": \"any\",\n \"port\": 82\n },\n {\n \"cidr\": \"144.175.239.16/31\",\n \"except\": [\n \"144.175.239.16\"\n ]\n },\n {\n \"cidr\": \"161.210.189.160/4\",\n \"protocol\": \"udp\"\n },\n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.132/31\",\n \"except\": []\n },\n {\n \"cidr\": \"2001:db8::/119\"\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"tcp\"\n },\n {\n \"protocol\": \"any\",\n \"port\": 80\n }\n ]\n },\n {\n \"to\": [\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:01b0/124\"\n },\n {\n \"cidr\": \"10.0.1.0/26\",\n \"except\": []\n }\n ],\n \"ports\": [\n {\n \"protocol\": \"any\"\n },\n {\n \"protocol\": \"tcp\",\n \"port\": 445\n },\n {\n \"port\": 78,\n \"endPort\": 84\n }\n ]\n },\n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.139/32\"\n },\n {\n \"cidr\": \"8354:b120:72a8:d18b:b30f:c658:f5d1:df72/0000\",\n \"except\": []\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["10.0.1.0/26","10.0.1.128/26:tcp","10.0.1.128/26:udp","10.0.1.200/29:tcp","10.0.1.200/29:udp","10.0.1.208/28:tcp","10.0.1.208/28:udp","10.0.1.224/27:tcp","10.0.1.224/27:udp","10.0.3.30/31:tcp:12","10.0.3.30/31:tcp:65533","10.0.3.30/31:udp:12","10.0.3.132/31:tcp","10.0.3.132/31:udp:80","10.0.3.139/32","144.175.239.17/32","160.0.0.0/4:udp","::/0"]}}, + {"name":"random #152","input":[{"text":"{}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #153","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"tcp\", \"port\": NaN}, {\"protocol\": \"udp\", \"port\": 445}, {\"protocol\": \"any\", \"port\": 82, \"endPort\": 86}]}, {\"cidr\": \"10.0.0.0/24\", \"protocol\": \"icmp\"}, {\"cidr\": \"a000::/3\", \"protocol\": \"udp\", \"port\": 78, \"endPort\": 84}, {\"to\": [{\"cidr\": \"0000:0000:0000:0000:0000:0000:0000:0000/1\"}, {\"cidr\": \"10.0.1.245/28\", \"except\": []}, {\"cidr\": \"10.0.0.0/23\", \"except\": [\"10.0.0.92/0.0.0.3\", \"10.0.1.164/30\"]}, {\"cidr\": \"10.0.2.192/27\", \"except\": [\"10.0.2.192/29\"]}]}, {\"to\": [{\"cidr\": \"10.0.1.126/32\"}, {\"cidr\": \"10.0.0.160/255.255.255.248\"}]}, {\"cidr\": \"10.0.1.157/25\", \"protocol\": \"any\"}, {\"to\": [{\"cidr\": \"2001:DB8::200/121\", \"except\": [\"2001:db8::262/127\", \"2001:db8::26f/126\", \"2001:db8::268/000125\"]}, {\"cidr\": \"10.0.2.60/30\"}, {\"cidr\": \"10.0.3.64/28\"}, {\"cidr\": \"128.0.0.0/1\", \"except\": [\"238.0.0.0/7\", \"169.0.0.0/8\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 65531, \"endPort\": 65535}]}, {\"to\": [{\"cidr\": \"10.0.1.184/30\"}, {\"cidr\": \"10.0.2.4/30\"}], \"ports\": [{\"protocol\": \"tcp\"}]}]}"}],"expected":{"error":"allow[0].ports[0].port must be an integer"}}, + {"name":"random #154","input":[{"text":"{\n \"allow\": [\n {\n \"to\": [\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:016c\"\n },\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0280/122\"\n },\n {\n \"cidr\": \"207.132.31.128/255.255.255.0\",\n \"except\": []\n }\n ]\n }\n ],\n \"deny\": []\n}"}],"expected":{"allow":["207.132.31.0/24","2001:db8::16c/128","2001:db8::280/122"],"deny":[]}}, + {"name":"random #155","input":[{"text":"{\"allow\":[{\"ports\":[{\"protocol\":\"udp\",\"port\":441,\"endPort\":447},{\"protocol\":\"icmp\"},{\"protocol\":\"tcp\",\"port\":81},{\"protocol\":\"any\"}]},{\"cidr\":\"10.0.1.128/255.255.255.192\",\"except\":[\"10.0.1.170\",\"10.0.1.184/0.0.0.7\"],\"protocol\":\"udp\"},{\"to\":[{\"cidr\":\"2001:db8::/118\"},{\"cidr\":\"2001:db8::1f0/125\"},{\"cidr\":\"80.16.115.0/24\"},{\"cidr\":\"2001:db8::31f/118\"}]},{\"cidr\":\"87.33.76.82\",\"protocol\":\"tcp\",\"port\":5},{\"cidr\":\"10.0.0.120/29\",\"protocol\":\"tcp\"},{\"to\":[{\"cidr\":\"10.0.1.0/26\"},{\"cidr\":\"6e9a:1990:300:2231:e753:ea37:e25:2f23/128\"},{\"cidr\":\"10.0.1.128/25\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":5},{\"protocol\":\"tcp\"},{\"protocol\":\"udp\",\"port\":445}]}],\"deny\":[{\"to\":[{\"cidr\":\"10.0.3.128/25\",\"except\":[\"10.0.3.178/032\"]}],\"ports\":[{\"protocol\":\"udp\",\"port\":7}]}]}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["10.0.3.128/27:udp:7","10.0.3.160/28:udp:7","10.0.3.176/31:udp:7","10.0.3.179/32:udp:7","10.0.3.180/30:udp:7","10.0.3.184/29:udp:7","10.0.3.192/26:udp:7"]}}, + {"name":"random #156","input":[{"text":"{\"allow\": [], \"deny\": [{\"to\": [{\"cidr\": \"31.37.208.219/4\"}, {\"cidr\": \"10.0.2.193/27\"}, {\"cidr\": \"10.0.3.152/29\"}, {\"cidr\": \"25.187.177.169/0\"}], \"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"any\", \"port\": 12}, {\"protocol\": \"icmp\"}]}, {}, {\"to\": [{\"cidr\": \"10.0.0.0/22\"}, {\"cidr\": \"10.0.0.0/22\", \"except\": [\"10.0.0.224/255.255.255.224\"]}, {\"cidr\": \"10.0.1.72/31\"}]}, {\"to\": [{\"cidr\": \"10.0.2.198/31\", \"except\": []}, {\"cidr\": \"10.0.0.140/22\"}]"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"random #157","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.0.160/27\"}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:00f0/124\"}, {\"cidr\": \"10.0.1.99/32\", \"except\": [\"10.0.1.99\", \"10.0.1.99/32\"]}], \"ports\": [{\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"2001:db8::/121\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"udp\"}]}, {\"to\": [{\"cidr\": \"10.0.2.0/23\"}], \"ports\": [{\"protocol\": \"udp\"}, {\"protocol\": \"udp\", \"port\": 80}, {\"protocol\": \"icmp\"}]}, {\"to\": [{\"cidr\": \"175.171.0.0/16\"}, {\"cidr\": \"10.0.2.0/0.0.1.255\", \"except\": [\"::/0\", \"10.0.2.88/29\"]}, {\"cidr\": \"::ffff:39.0.0.0/104\", \"except\": []}, {\"cidr\": \"2001:db8::/118\"}]}], \"deny\": [{\"to\": [{\"cidr\": \"10.0.3.228/22\", \"except\": [\"10.0.3.183/0.0.0.15\", \"10.0.1.64/26\", \"10.0.3.32/27\"]}, {\"cidr\": \"10.0.0.0/25\"}, {\"cidr\": \"10.0.1.64/255.255.255.224\", \"except\": [\"10.0.1.69/00032\"]}, {\"cidr\": \"34.213.155.127/24\", \"except\": []}], \"ports\": [{\"protocol\": \"any\", \"port\": 80, \"endPort\": 86}, {\"protocol\": \"udp\", \"port\": 443}]}]}"}],"expected":{"error":"allow[3].to[1].except[0] must be contained in 10.0.2.0/23"}}, + {"name":"random #158","input":[{"text":"{\n\"deny\": [\n{\n\"cidr\": \"f1fa:3bac:e784:7a7c:66c6:3c88:900d:9cd3/0001\",\n\"except\": []\n},\n{\n\"to\": [\n{\n\"cidr\": \"0.0.0.0/2\"\n},\n{\n\"cidr\": \"10.0.1.168/31\"\n},\n{\n\"cidr\": \"10.0.2.143\",\n\"except\": [\n\"10.0.2.143\"\n]\n},\n{\n\"cidr\": \"2001:db8::44\"\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::280/121\"\n},\n{\n\"cidr\": \"b146:bd3c:1da9:fb7c:4c7a:6a46:7e3c:9f00/120\"\n},\n{\n\"cidr\": \"2001:db8::3d3/128\"\n},\n{\n\"cidr\": \"2001:db8::1d9/127\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"any\",\n\"port\": 78\n}\n]\n}\n]\n}"}],"expected":{"allow":[],"deny":["0.0.0.0/2","2001:db8::44/128","2001:db8::1d8/127:tcp:78","2001:db8::1d8/127:udp:78","2001:db8::280/121:tcp:78","2001:db8::280/121:udp:78","2001:db8::3d3/128:tcp:78","2001:db8::3d3/128:udp:78","8000::/1"]}}, + {"name":"random #159","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.252/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":7\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::313/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":85\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.100/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":265\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.236/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":139\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::5c/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":297\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1b2/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":33,\n\t\t\t\"endPort\":34\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.252/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":301\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.247/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":57\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.194/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":157,\n\t\t\t\"endPort\":160\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.138/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":55\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.116/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":69\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::91/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":295\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.38/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":399,\n\t\t\t\"endPort\":400\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.6/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":13\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.20/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":341\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.35/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":333,\n\t\t\t\"endPort\":337\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.88/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":217,\n\t\t\t\"endPort\":221\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.218/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":39,\n\t\t\t\"endPort\":43\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.140/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":199\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1cc/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":257,\n\t\t\t\"endPort\":261\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2cd/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":263\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.178/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":241,\n\t\t\t\"endPort\":241\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2c8/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":35\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::37b/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":271\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.166/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":385\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.48/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":341,\n\t\t\t\"endPort\":343\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.104/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":59,\n\t\t\t\"endPort\":61\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.216/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":89\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::22a/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":63\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::df/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":29\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.204/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":255,\n\t\t\t\"endPort\":255\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::de/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":367\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.89/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":353,\n\t\t\t\"endPort\":353\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.112/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":293\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::385/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":99\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::115/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":197\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.208/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":29,\n\t\t\t\"endPort\":32\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.240/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":87,\n\t\t\t\"endPort\":91\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::97/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":59\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.68/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":361\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.175/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":11,\n\t\t\t\"endPort\":12\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.128/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":219,\n\t\t\t\"endPort\":223\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.218/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":149\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.168/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":139,\n\t\t\t\"endPort\":142\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1cc/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":143\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.47/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":289\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.20/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":167,\n\t\t\t\"endPort\":167\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.192/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":37\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::78/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":277\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.8/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":385,\n\t\t\t\"endPort\":386\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2f3/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":399\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.184/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":239\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.164/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":161\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.136/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":13\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.226/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":259\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.154/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":329\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.202/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":321,\n\t\t\t\"endPort\":325\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.58/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":299\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.10/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":123\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.78/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":1\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.154/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":147,\n\t\t\t\"endPort\":151\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.124/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":369,\n\t\t\t\"endPort\":370\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::e4/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":87,\n\t\t\t\"endPort\":89\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.228/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":365\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::ad/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":83\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.38/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":119,\n\t\t\t\"endPort\":122\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.248/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":207,\n\t\t\t\"endPort\":207\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::349/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":99\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.178/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":359\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.132/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":365,\n\t\t\t\"endPort\":369\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::89/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":333,\n\t\t\t\"endPort\":334\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.210/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":337\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.132/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":391\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.86/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":261,\n\t\t\t\"endPort\":265\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.60/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":279\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.181/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":265\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.48/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":41\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.180/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":119\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.188/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":91\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.4/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":307\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.222/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":317\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2e/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":13,\n\t\t\t\"endPort\":13\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.54/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":357,\n\t\t\t\"endPort\":359\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.147/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":211\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.192/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":363,\n\t\t\t\"endPort\":366\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.204/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":309\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.80/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":333\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.111/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":267\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.226/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":209\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.238/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":145,\n\t\t\t\"endPort\":146\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.11/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":105\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.128/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":217\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.93/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":131\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.24/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":163,\n\t\t\t\"endPort\":166\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.248/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":35,\n\t\t\t\"endPort\":36\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2d9/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":175,\n\t\t\t\"endPort\":177\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.104/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":113\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.17/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":199\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.132/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":305,\n\t\t\t\"endPort\":305\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.175/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":351\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.117/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":245\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.161/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":379\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.196/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":119\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.34/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":383\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.190/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":359,\n\t\t\t\"endPort\":362\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.104/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":205\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.30/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":89\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.144/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":35,\n\t\t\t\"endPort\":39\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.56/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":119\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::256/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":103\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.232/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":35,\n\t\t\t\"endPort\":37\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.88/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":287\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::56/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":397,\n\t\t\t\"endPort\":399\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.201/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":49\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.243/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":391,\n\t\t\t\"endPort\":394\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.220/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":225,\n\t\t\t\"endPort\":229\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.229/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":213\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.168/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":187\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.190/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":49,\n\t\t\t\"endPort\":53\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.231/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":295,\n\t\t\t\"endPort\":296\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.166/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":5,\n\t\t\t\"endPort\":8\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.0/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":51\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.6/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":97,\n\t\t\t\"endPort\":98\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.89/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":103\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.219/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":223,\n\t\t\t\"endPort\":225\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.252/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":351,\n\t\t\t\"endPort\":354\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.143/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":325,\n\t\t\t\"endPort\":327\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::bc/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":105\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.236/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":255\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::19/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":167\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.240/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":51\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::30b/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":197\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.30/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":355\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.217/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":285,\n\t\t\t\"endPort\":286\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3e0/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":223\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.124/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":283\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.192/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":9\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.56/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":283\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.92/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":225,\n\t\t\t\"endPort\":226\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.88/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":57\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.190/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":369,\n\t\t\t\"endPort\":373\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.182/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":327\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1b/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":365\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::246/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":367,\n\t\t\t\"endPort\":370\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.252/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":21\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.193/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":233\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.100/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":89\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.84/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":283,\n\t\t\t\"endPort\":285\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.204/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":313\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.203/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":155,\n\t\t\t\"endPort\":156\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2fc/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":139,\n\t\t\t\"endPort\":143\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.230/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":77\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.132/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":357\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.68/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":357\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::23d/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":15\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::369/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":253\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::36e/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":1,\n\t\t\t\"endPort\":3\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.245/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":83\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.170/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":249\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.133/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":137\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.198/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":165\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.184/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":69\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::43/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":361\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.136/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":189\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::374/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":363\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.108/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":391\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.4/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":347\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.192/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":343\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.104/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":227\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.16/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":135\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.246/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":229,\n\t\t\t\"endPort\":230\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.4/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":309\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::3d/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":17\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.60/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":149,\n\t\t\t\"endPort\":149\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.179/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":315\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.118/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":275\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.74/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":327,\n\t\t\t\"endPort\":328\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::117/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":105\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::6f/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":7\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1c4/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":265\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.220/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":371\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.10/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":9,\n\t\t\t\"endPort\":10\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1af/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":321,\n\t\t\t\"endPort\":324\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::186/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":215\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.119/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":397\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.68/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":63,\n\t\t\t\"endPort\":65\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.64/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":193,\n\t\t\t\"endPort\":194\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.30/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":233\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.101/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":319\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.156/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":109\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.132/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":367,\n\t\t\t\"endPort\":371\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.232/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":41\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.26/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":297,\n\t\t\t\"endPort\":301\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.132/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":347\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.188/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":173\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.145/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":395\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.52/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":139\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.184/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":83\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.166/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":127\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1fd/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":273\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.12/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":317\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.149/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":265\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.184/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":71\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2eb/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":307\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::182/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":397\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.55/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":163\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.96/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":175\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.116/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":357,\n\t\t\t\"endPort\":360\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::e4/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":397\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::16d/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":25\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.235/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":311,\n\t\t\t\"endPort\":314\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.128/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":51\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::298/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":241,\n\t\t\t\"endPort\":243\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.47/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":123,\n\t\t\t\"endPort\":125\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.132/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":267\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.240/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":271,\n\t\t\t\"endPort\":272\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.212/30\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":387\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.48/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":161\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::10e/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":263\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.140/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":259\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.220/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":83\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.16/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":21\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.219/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":207\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.92/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":89\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.250/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":331\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.201/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":205\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.242/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":219\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":363\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.91/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":17\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.70/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":125,\n\t\t\t\"endPort\":126\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1e5/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":261\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.12/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":95\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.4/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":345,\n\t\t\t\"endPort\":349\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.168/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":129\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2c/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":113,\n\t\t\t\"endPort\":117\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.14/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":309\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.192/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":103\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::236/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":221\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.156/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":35,\n\t\t\t\"endPort\":35\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.182/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":85\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2cd/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":121\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1a3/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":339\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.4/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":19\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.60/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":157\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.181/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":19\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::385/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":355,\n\t\t\t\"endPort\":355\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.60/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":297,\n\t\t\t\"endPort\":301\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::147/128\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":319\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.16/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":231\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.64/30\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":139\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.108/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":193,\n\t\t\t\"endPort\":197\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.78/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":67\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.192/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":67\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.232/32\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":209\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.20/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":383\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.59/32\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":63\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::67/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":361\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.172/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":327\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::1c/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":141,\n\t\t\t\"endPort\":145\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.92/30\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":223\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.50/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":99\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::299/128\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":81\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.56/31\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":255\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.80/32\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":277\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.1.130/31\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":5,\n\t\t\t\"endPort\":8\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::338/128\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":69\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.122/31\",\n\t\t\t\"protocol\":\"any\",\n\t\t\t\"port\":89,\n\t\t\t\"endPort\":92\n\t\t}\n\t],\n\t\"deny\":[]\n}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"random #160","input":[{"text":"{\"allow\": [{\"ports\": [{\"protocol\": \"tcp\", \"port\": 4}], \"port\": 99999}]}"}],"expected":{"error":"allow[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #161","input":[{"text":"{\"allow\": [{\"cidr\": \"10.0.0.0/0.0.0.127\", \"except\": [], \"protocol\": \"udp\"}, {\"cidr\": \"10.0.3.91/22\", \"except\": [\"10.0.3.27/28\"], \"protocol\": \"udp\", \"port\": 79, \"endPort\": 2998}], \"deny\": [{\"ports\": [{\"protocol\": \"udp\", \"port\": 80}, {\"protocol\": \"udp\", \"port\": 65531}, {\"protocol\": \"tcp\", \"port\": 65530, \"endPort\": 65532}, {\"protocol\": \"udp\", \"port\": 81, \"endPort\": 82}]}, {\"cidr\": \"::ffff:236.106.128.1/128\", \"except\": [], \"protocol\": \"any\", \"port\": 442, \"endPort\": 0}, {\"cidr\": \"10.0.1.96/27\", \"except\": [\"10.0.1.124/031\"]}]}"}],"expected":{"error":"deny[1].endPort must be between 1 and 65535"}}, + {"name":"random #162","input":[{"text":"{}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #163","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.181/30\",\n\"except\": []\n}\n],\n\"ports\": [\n{\n\"protocol\": \"udp\",\n\"port\": 441\n},\n{\n\"protocol\": \"udp\"\n}\n]\n},\n{\n\"cidr\": \"10.0.3.216/29\",\n\"protocol\": \"udp\",\n\"port\": 80,\n\"endPort\": 83\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.80/29\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"any\",\n\"port\": 442,\n\"endPort\": 442\n},\n{\n\"protocol\": \"tcp\"\n}\n]\n},\n{\n\"cidr\": \"2001:db8::100/123\"\n},\n{\n\"to\": [\n{\n\"cidr\": \"250.118.72.143\"\n},\n{\n\"cidr\": \"10.0.3.152/029\"\n},\n{\n\"cidr\": \"::ffff:228.235.82.160\"\n},\n{\n\"cidr\": \"10.0.1.28/32\"\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.0/24\"\n},\n{\n\"cidr\": \"2001:db8::80/121\",\n\"except\": [\n\"2001:db8::e1/126\"\n]\n},\n{\n\"cidr\": \"10.0.3.128/25\",\n\"except\": [\n\"10.0.3.217\",\n\"10.0.3.196/30\",\n\"10.0.3.172/30\"\n]\n},\n{\n\"cidr\": \"2001:db8::1d0/127\"\n}\n]\n}\n]\n}"}],"expected":{"allow":["10.0.1.28/32","10.0.2.0/24","10.0.3.80/29:tcp","10.0.3.80/29:udp:442","10.0.3.128/27","10.0.3.160/29","10.0.3.168/30","10.0.3.176/28","10.0.3.192/30","10.0.3.200/29","10.0.3.208/28:udp:80-83","10.0.3.208/29","10.0.3.216/32","10.0.3.218/31","10.0.3.220/30","10.0.3.224/27","250.118.72.143/32","::ffff:228.235.82.160/128","2001:db8::80/122","2001:db8::c0/123","2001:db8::e4/126","2001:db8::e8/125","2001:db8::f0/124","2001:db8::100/123","2001:db8::1d0/127"],"deny":[]}}, + {"name":"random #164","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"cfa4:7eb4:5dda:1262:1120:c53a:77b:b00e/127\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 445\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::90/125\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:01c3/126\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.29/30\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.41/027\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"73.176.0.0/12\",\n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 65535,\n\t\t\t\t\t\"endPort\": 65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":["2001:db8::90/125:tcp","cfa4:7eb4:5dda:1262:1120:c53a:77b:b00e/127:icmp","cfa4:7eb4:5dda:1262:1120:c53a:77b:b00e/127:tcp:445"],"deny":["10.0.2.28/30:icmp","10.0.2.28/30:tcp:65535","10.0.2.32/27:icmp","10.0.2.32/27:tcp:65535","73.176.0.0/12:icmp","73.176.0.0/12:tcp:65535","2001:db8::1c0/126:icmp","2001:db8::1c0/126:tcp:65535"]}}, + {"name":"random #165","input":[{"text":"{\n\t\"allow\":[],\n\t\"deny\":[\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.224/27\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.3.238/255.255.255.254\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":443,\n\t\t\t\t\t\"endPort\":445\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/0\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.16/32\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"port\":78,\n\t\t\t\t\t\"endPort\":78\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.3.202/32\",\n\t\t\t\"protocol\":\"icmp\"\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.2.16/0.0.0.15\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":6,\n\t\t\t\"endPort\":11\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.148/31\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::e4/127\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"2001:db8::e4/127\",\n\t\t\t\t\t\t\"2001:db8::e4/127\",\n\t\t\t\t\t\t\"2001:db8::e4/127\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::1f7/123\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"2001:db8::1f6/125\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::e0/0123\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":12\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":82\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":1\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.128/25\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":440\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"0.0.0.0/0\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":80,\n\t\t\t\t\t\"endPort\":84\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":[],"deny":["0.0.0.0/0:icmp","0.0.0.0/0:tcp:1","0.0.0.0/0:tcp:12","0.0.0.0/0:tcp:78","0.0.0.0/0:tcp:82","0.0.0.0/0:udp:1","0.0.0.0/0:udp:12","0.0.0.0/0:udp:78","0.0.0.0/0:udp:80-84","10.0.0.128/25:udp:440","10.0.0.148/31","10.0.2.16/28:tcp:6-12","10.0.3.224/29:udp:443-445","10.0.3.232/30:udp:443-445","10.0.3.236/31:udp:443-445","10.0.3.240/28:udp:443-445","::/0:icmp","::/0:tcp:1","::/0:tcp:12","::/0:tcp:82","::/0:udp:1","::/0:udp:12","2001:db8::e0/123","2001:db8::1e0/124","2001:db8::1f8/125"]}}, + {"name":"random #166","input":[{"text":"{\n \"allow\": [\n {\n \"to\": [\n {\n \"cidr\": \"a1b3:24d4:add8:ba25::/64\"\n }, \n {\n \"cidr\": \"10.0.3.192/255.255.255.192\"\n }, \n {\n \"cidr\": \"8000:0000:0000:0000:0000:0000:0000:0000/1\", \n \"except\": [\n \"ac00::/8\"\n ]\n }, \n {\n \"cidr\": \"2001:db8::280/122\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 445, \n \"endPort\": 449\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 12, \n \"endPort\": 18\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"::ffff:0.0.0.0/104\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 440\n }, \n {}, \n {\n \"protocol\": \"tcp\", \n \"port\": 65535\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.0/118\", \n \"except\": []\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 444, \n \"endPort\": 445\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 65534, \n \"endPort\": 65535\n }, \n {\n \"protocol\": \"any\", \n \"port\": 65531\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::33\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 4\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 80, \n \"endPort\": 81\n }, \n {\n \"protocol\": \"udp\"\n }\n ]\n }, \n {\n \"cidr\": \"10.0.3.233/255.255.255.254\", \n \"protocol\": \"any\"\n }, \n {\n \"cidr\": \"10.0.1.117/32\", \n \"except\": [\n \"10.0.1.117/32\"\n ], \n \"protocol\": \"any\"\n }\n ], \n \"deny\": [\n {\n \"cidr\": \"10.0.0.192/27\", \n \"except\": [\n \"10.0.0.208/31\", \n \"10.0.0.195/30\", \n \"10.0.0.220/28\"\n ], \n \"protocol\": \"tcp\", \n \"port\": 441\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::200/120\", \n \"except\": []\n }, \n {\n \"cidr\": \"10.0.2.71/26\"\n }, \n {\n \"cidr\": \"10.0.0.0/24\"\n }, \n {\n \"cidr\": \"2001:db8::d4/000127\"\n }\n ], \n \"ports\": [\n {\n \"port\": 444\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 6, \n \"endPort\": 5271\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 65530, \n \"endPort\": 65535\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.148/30\"\n }, \n {\n \"cidr\": \"10.0.1.19/22\"\n }, \n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0372/121\"\n }, \n {\n \"cidr\": \"10.0.1.65\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"icmp\"\n }, \n {\n \"protocol\": \"udp\"\n }, \n {\n \"port\": 443\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.3.181\"\n }, \n {\n \"cidr\": \"10.0.0.26/31\"\n }, \n {\n \"cidr\": \"2001:db8::1b4/126\"\n }, \n {\n \"cidr\": \"6.187.146.3/4\", \n \"except\": [\n \"10.96.0.0/12\", \n \"4.0.0.0/6\"\n ]\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 82, \n \"endPort\": 87\n }\n ]\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"icmp\"\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"52.1.135.254/31\", \n \"except\": [\n \"52.1.135.255/31\", \n \"52.1.135.255\", \n \"52.1.135.254/31\"\n ]\n }, \n {\n \"cidr\": \"17f8:64b3:854:e286:d856:5bc2::/96\", \n \"except\": [\n \"17f8:64b3:0854:e286:d856:5bc2:7000:0000/101\", \n \"17f8:64b3:854:e286:d856:5bc2::/97\"\n ]\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"any\", \n \"port\": 80, \n \"endPort\": 18065\n }, \n {\n \"protocol\": \"icmp\"\n }, \n {\n \"port\": 440\n }, \n {\n \"protocol\": \"tcp\"\n }\n ]\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 65532\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.48/31\"\n }, \n {\n \"cidr\": \"2001:db8::280/121\"\n }, \n {\n \"cidr\": \"2001:db8::168/127\"\n }, \n {\n \"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0080/121\", \n \"except\": [\n \"2001:0db8:0000:0000:0000:0000:0000:00b1\", \n \"2001:db8::96/123\", \n \"2001:db8::fa/127\"\n ]\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"icmp\"\n }, \n {\n \"protocol\": \"tcp\"\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["10.0.3.192/26:tcp:12-18","10.0.3.192/26:tcp:445-449","10.0.3.232/31","::ffff:0.0.0.0/104","2001:db8::/118:tcp:444-445","2001:db8::/118:tcp:65531","2001:db8::/118:tcp:65534-65535","2001:db8::/118:udp:65531","2001:db8::33/128:tcp:4","2001:db8::33/128:tcp:80-81","2001:db8::33/128:udp","2001:db8::280/122:tcp:12-18","2001:db8::280/122:tcp:446-449","8000::/3:tcp:12-18","8000::/3:tcp:445-449","a000::/5:tcp:12-18","a000::/5:tcp:445-449","a800::/6:tcp:12-18","a800::/6:tcp:445-449","ad00::/8:tcp:12-18","ad00::/8:tcp:445-449","ae00::/7:tcp:12-18","ae00::/7:tcp:445-449","b000::/4:tcp:12-18","b000::/4:tcp:445-449","c000::/2:tcp:12-18","c000::/2:tcp:445-449"],"deny":["0.0.0.0/0:icmp","0.0.0.0/0:tcp:65532","0.0.0.0/6:tcp:82-87","8.0.0.0/7:tcp:82-87","10.0.0.0/10:tcp:82-87","10.0.0.0/22:tcp:443","10.0.0.0/22:udp","10.0.0.0/24:tcp:6-5271","10.0.0.0/24:tcp:65530-65535","10.0.0.48/31:tcp","10.0.2.64/26:tcp:6-5271","10.0.2.64/26:tcp:65530-65535","10.64.0.0/11:tcp:82-87","10.112.0.0/12:tcp:82-87","10.128.0.0/9:tcp:82-87","11.0.0.0/8:tcp:82-87","12.0.0.0/6:tcp:82-87","::/0:icmp","::/0:tcp:65532","17f8:64b3:854:e286:d856:5bc2:8000:0/97:tcp","17f8:64b3:854:e286:d856:5bc2:8000:0/97:udp:80-18065","2001:db8::a0/124:tcp","2001:db8::b0/128:tcp","2001:db8::b2/127:tcp","2001:db8::b4/126:tcp","2001:db8::b8/125:tcp","2001:db8::c0/123:tcp","2001:db8::d4/127:udp:444","2001:db8::e0/124:tcp","2001:db8::f0/125:tcp","2001:db8::f8/127:tcp","2001:db8::fc/126:tcp","2001:db8::168/127:tcp","2001:db8::1b4/126:tcp:82-87","2001:db8::200/120:tcp:6-5271","2001:db8::200/120:tcp:65530-65535","2001:db8::200/120:udp:444","2001:db8::280/121:tcp","2001:db8::300/121:tcp:443","2001:db8::300/121:udp"]}}, + {"name":"random #167","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8::/118\", \"protocol\": \"tcp\", \"port\": 65531}], \"deny\": [{\"to\": [{\"cidr\": \"10.0.0.224/28\"}, {\"cidr\": \"10.0.3.0/25\"}, {\"cidr\": \"2001:DB8::140/122\", \"except\": [\"2001:db8::170/125\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0190/127\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:0190/127\", \"2001:db8::190/127\"]}]}, {\"to\": [{\"cidr\": \"6.107.20.162/0\"}]}]}"}],"expected":{"allow":["2001:db8::/118:tcp:65531"],"deny":["0.0.0.0/0","2001:db8::140/123","2001:db8::160/124","2001:db8::178/125"]}}, + {"name":"random #168","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0030/118\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.0/23\"\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.3.112/124\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 2\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0238/125\"\n}, \n{\n\"cidr\": \"10.0.1.53\"\n}, \n{\n\"cidr\": \"10.0.2.15/26\"\n}\n], \n\"ports\": [\n{}, \n{\n\"protocol\": \"udp\", \n\"port\": 65532\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65534\n}, \n{\n\"protocol\": \"any\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.244/29\"\n}, \n{\n\"cidr\": \"c826:ec3b:c713:157b:bfff:a036:691a:c9da/127\", \n\"except\": [\n\"C826:EC3B:C713:157B:BFFF:A036:691A:C9DA\", \n\"c826:ec3b:c713:157b:bfff:a036:691a:c9db/127\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 6\n}\n]\n}\n], \n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.64/26\", \n\"except\": [\n\"10.0.2.64/26\", \n\"10.0.2.67/26\", \n\"10.0.2.87/28\"\n]\n}, \n{\n\"cidr\": \"2001:db8::130/124\"\n}, \n{\n\"cidr\": \"2001:db8::e0/122\"\n}, \n{\n\"cidr\": \"2001:db8::100/120\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 442\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/23\"\n}, \n{\n\"cidr\": \"::ffff:120.0.0.0/104\", \n\"except\": [\n\"::ffff:120.119.166.15/108\", \n\"0000:0000:0000:0000:0000:ffff:120.16.0.0/108\", \n\"::ffff:120.16.0.0/108\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 1\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 65535\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"210.37.0.0/255.255.0.0\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 65531\n}, \n{\n\"protocol\": \"any\", \n\"port\": 65531, \n\"endPort\": 65535\n}\n]\n}, \n{\n\"cidr\": \"::644e:d548:e18f:f035:a2f7:3f4a/32\", \n\"protocol\": \"any\", \n\"port\": 80, \n\"endPort\": 86\n}, \n{\n\"cidr\": \"0.0.0.0/0\", \n\"protocol\": \"tcp\", \n\"port\": 78\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.88/29\", \n\"except\": [\n\"10.0.2.92/0032\", \n\"10.0.2.88/29\"\n]\n}\n]\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"any\"\n}\n]\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.2.252/128\", \n\"except\": [\n\"2001:0db8:0000:0000:0000:0000:0000:02fc\", \n\"2001:db8::2fc/128\"\n], \n\"protocol\": \"any\", \n\"port\": 443\n}\n]\n}"}],"expected":{"allow":["10.0.0.240/29:tcp:6","10.0.1.53/32","10.0.2.0/23:icmp","10.0.2.0/23:udp:2","10.0.2.0/26","2001:db8::/118"],"deny":["0.0.0.0/0","::/0"]}}, + {"name":"random #169","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:01bd/121\"\n}, \n{\n\"cidr\": \"10.0.3.120/29\", \n\"except\": []\n}, \n{\n\"cidr\": \"10.0.0.161/255.255.255.248\", \n\"except\": [\n\"10.0.0.160/0030\"\n]\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::3eb/128\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\"\n}, \n{}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::200/119\", \n\"except\": [\n\"2001:0db8:0000:0000:0000:0000:0000:0200/119\", \n\"2001:db8::318/000125\", \n\"2001:db8::3e0/123\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 442\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 65533\n}, \n{\n\"protocol\": \"any\", \n\"port\": 79, \n\"endPort\": 81\n}\n]\n}\n], \n\"deny\": []\n}"}],"expected":{"allow":["10.0.0.164/30","10.0.3.120/29","2001:db8::180/121","2001:db8::3eb/128"],"deny":[]}}, + {"name":"random #170","input":[{"text":"{\"allow\":[{\"cidr\":\"10.0.3.31/29\",\"protocol\":\"udp\",\"port\":5,\"endPort\":6},{\"cidr\":\"10.0.1.160/28\",\"protocol\":\"tcp\"}],\"deny\":[{\"cidr\":\"28.164.95.147/32\",\"protocol\":\"icmp\"}]}"}],"expected":{"allow":["10.0.1.160/28:tcp","10.0.3.24/29:udp:5-6"],"deny":["28.164.95.147/32:icmp"]}}, + {"name":"random #171","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::274/126\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":3,\n\t\t\t\"endPort\":8\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:008c/126\",\n\t\t\t\"except\":[\n\t\t\t\t\"2001:DB8::8E/127\",\n\t\t\t\t\"2001:db8::8e/00127\",\n\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0.0.0.143/126\"\n\t\t\t],\n\t\t\t\"protocol\":\"udp\"\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::40/122\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::353/123\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.0/22\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":3\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.128/25\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.3.192/0026\",\n\t\t\t\t\t\t\"10.0.3.230/31\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":441,\n\t\t\t\t\t\"endPort\":445\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":65535\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:DB8::6D/118\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.1.0/24\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.28/255.255.255.254\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.3.28/255.255.255.254\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.203/255.255.255.254\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":441\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::/121\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":3\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"0000:0000:0000:0000:0000:ffff:123.204.82.35/000127\",\n\t\t\t\"except\":[\n\t\t\t\t\"0000:0000:0000:0000:0000:ffff:123.204.82.34\",\n\t\t\t\t\"::ffff:123.204.82.34\"\n\t\t\t],\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":2\n\t\t},\n\t\t{\n\t\t\t\"to\":\"192.0.2.0/24\"\n\t\t}\n\t],\n\t\"deny\":[\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"88bd:807f:f73c:8470:7051:1896::/96\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"88bd:807f:f73c:8470:7051:1896::/99\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:01a0/124\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"2001:db8::1ad/128\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.112/27\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":440,\n\t\t\t\t\t\"endPort\":440\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"b81f:d565:925d:6cf9:05dc:5510:d9d4:3dc9/32\",\n\t\t\t\"protocol\":\"any\"\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"2001:db8::200/119\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"any\",\n\t\t\t\t\t\"port\":80\n\t\t\t\t},\n\t\t\t\t{}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.97/32\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\",\n\t\t\t\t\t\"port\":441,\n\t\t\t\t\t\"endPort\":51586\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::2d3/125\",\n\t\t\t\"protocol\":\"tcp\",\n\t\t\t\"port\":81\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"::/32\",\n\t\t\t\t\t\"except\":[]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.2.127/25\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.2.64/26\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"error":"allow[7].to must be an array"}}, + {"name":"random #172","input":[{"text":"{\"allow\": []}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #173","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"192.0.0.0/4\"}, {\"cidr\": \"2001:db8::/118\"}, {\"cidr\": \"2001:db8::229/128\"}]}, {\"to\": [{\"cidr\": \"2001:DB8::90\"}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 82}]}, {\"cidr\": \"10.0.2.0/23\", \"protocol\": \"udp\", \"port\": 80, \"endPort\": 85}, {\"cidr\": \"10.0.2.0/23\"}, {\"to\": [{\"cidr\": \"10.0.3.196/31\"}, {\"cidr\": \"10.0.0.0/22\"}]}, {\"to\": [{\"cidr\": \"10.0.3.0/255.255.255.128\", \"except\": [\"10.0.3.42/26\"]}, {\"cidr\": \"10.0.1.128/26\", \"except\": []}]}], \"deny\": [{\"to\": [{\"cidr\": \"10.0.1.0/25\"}, {\"cidr\": \"2001:db8::331/126\"}, {\"cidr\": \"8000::/1\", \"except\": [\"969c:9ef6:341:c381:cbfb:20d9:8b39:e594/9\", \"f0c8:839d:f0b:e0b6:8dd6:988:4418:541a/2\", \"dc00::/7\"]}]}, {\"to\": [{\"cidr\": \"587a:9b56:a8a8:9f8c:472e:979e:f3ea:f700/120\", \"except\": [\"587a:9b56:a8a8:9f8c:472e:979e:f3ea:f720/123\", \"587a:9b56:a8a8:9f8c:472e:979e:f3ea:f713/128\", \"587a:9b56:a8a8:9f8c:472e:979e:f3ea:f710/124\"]}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"tcp\"}, {\"protocol\": \"udp\"}, {\"protocol\": \"tcp\", \"port\": 79, \"endPort\": 84}]}, {\"cidr\": \"2001:db8::3bc/122\", \"except\": [\"2001:db8::387\"], \"protocol\": \"icmp\"}, {\"cidr\": \"10.0.0.202/29\", \"except\": [\"10.0.0.204/30\", \"10.0.0.200/30\"]}, {\"to\": [{\"cidr\": \"10.0.0.112/29\"}], \"ports\": [{\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"9e24:fd81:5ef5:f590:3c9:e095:e607:e937/32\", \"except\": []}]}]}"}],"expected":{"allow":["10.0.0.0/22","192.0.0.0/4","2001:db8::/118"],"deny":["10.0.0.112/29:tcp","10.0.1.0/25","2001:db8::330/126","2001:db8::380/126:icmp","2001:db8::384/127:icmp","2001:db8::386/128:icmp","2001:db8::388/125:icmp","2001:db8::390/124:icmp","2001:db8::3a0/123:icmp","587a:9b56:a8a8:9f8c:472e:979e:f3ea:f700/124:icmp","587a:9b56:a8a8:9f8c:472e:979e:f3ea:f700/124:tcp","587a:9b56:a8a8:9f8c:472e:979e:f3ea:f700/124:udp","587a:9b56:a8a8:9f8c:472e:979e:f3ea:f740/122:icmp","587a:9b56:a8a8:9f8c:472e:979e:f3ea:f740/122:tcp","587a:9b56:a8a8:9f8c:472e:979e:f3ea:f740/122:udp","587a:9b56:a8a8:9f8c:472e:979e:f3ea:f780/121:icmp","587a:9b56:a8a8:9f8c:472e:979e:f3ea:f780/121:tcp","587a:9b56:a8a8:9f8c:472e:979e:f3ea:f780/121:udp","8000::/4","9000::/6","9400::/7","9600::/9","9700::/8","9800::/5","a000::/3"]}}, + {"name":"random #174","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.48/28\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\",\n\t\t\t\t\t\"port\":441\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::10/124\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":65534\n\t\t}\n\t],\n\t\"deny\":[\n\t\t{\n\t\t\t\"cidr\":\"10.0.0.0/023\",\n\t\t\t\"protocol\":\"icmp\",\n\t\t\t\"port\":443\n\t\t}\n\t]\n}"}],"expected":{"error":"deny[0].port is not supported with icmp"}}, + {"name":"random #175","input":[{"text":"{\n\t\"deny\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::/118\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.104/31\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"22ff:42f:b495:a7b3::/64\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 440, \n\t\t\t\t\t\"endPort\": 444\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 65534\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 80\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 445\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0383\", \n\t\t\t\"except\": [\n\t\t\t\t\"2001:db8::383\", \n\t\t\t\t\"2001:db8::383/128\"\n\t\t\t], \n\t\t\t\"protocol\": \"udp\", \n\t\t\t\"port\": 65532, \n\t\t\t\"endPort\": 65535\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:02e0/125\", \n\t\t\t\"except\": [\n\t\t\t\t\"2001:db8::2e5/126\", \n\t\t\t\t\"2001:db8::2e4/000128\", \n\t\t\t\t\"x\"\n\t\t\t], \n\t\t\t\"protocol\": \"tcp\", \n\t\t\t\"port\": 3\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.136/29\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.7/28\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.240/23\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.41/29\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 441\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.32/30\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"::ffff:0.0.0.0/96\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.0.0/120\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.140/23\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 11, \n\t\t\t\t\t\"endPort\": 12\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"0000:0000:0000:0000:0000:ffff:72.137.106.237/96\", \n\t\t\t\"protocol\": \"tcp\"\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::140/122\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.0/25\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0240/122\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"f3a0:dc63:e2c9:3a1f:472b:5aff:261:e3c/127\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65530, \n\t\t\t\t\t\"endPort\": 65532\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 81, \n\t\t\t\t\t\"endPort\": 85\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 9, \n\t\t\t\t\t\"endPort\": 13076\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::311\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:0311/128\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::3e0/00126\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 1\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 6, \n\t\t\t\t\t\"endPort\": 52291\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 65531\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"error":"deny[2].except[2] is not a valid IPv4 or IPv6 CIDR: x"}}, + {"name":"random #176","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"2001:db8::3bd/128\",\"except\":[]},{\"cidr\":\"2001:DB8::86\",\"except\":[\"2001:db8::86/128\",\"2001:db8::86\",\"2001:db8::86\"]},{\"cidr\":\"10.0.2.144/29\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":5},{\"protocol\":\"icmp\"},{\"protocol\":\"udp\"}]},{\"cidr\":\"2001:db8::42/123\",\"except\":[\"2001:db8::50/124\"],\"protocol\":\"udp\",\"port\":65535,\"endPort\":65535},{\"cidr\":\"10.0.0.128/25\",\"except\":[],\"protocol\":\"udp\"},{\"to\":[{\"cidr\":\"10.0.0.199/26\"}],\"ports\":[{\"protocol\":\"any\"},{\"protocol\":\"udp\"}]},{\"cidr\":\"10.0.3.240/29\",\"except\":[\"10.0.3.247/31\"],\"protocol\":\"icmp\"},{\"to\":[{\"cidr\":\"215.171.134.127/1\"},{\"cidr\":\"10.0.3.0/26\"},{\"cidr\":\"::/0\",\"except\":[]}],\"ports\":[{\"protocol\":\"tcp\",\"port\":440},{\"protocol\":\"tcp\"},{\"protocol\":\"udp\"}]},{\"to\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0.0.1.0/121\"},{\"cidr\":\"10.0.3.72/26\",\"except\":[]},{\"cidr\":\"10.0.0.224/255.255.255.252\"}]},{\"to\":[{\"cidr\":\"10.0.2.37/29\",\"except\":[]},{\"cidr\":\"B6FA:AF44::/32\"}],\"ports\":[{\"protocol\":\"any\",\"port\":81,\"endPort\":82},{\"protocol\":\"udp\"},{\"protocol\":\"any\",\"port\":79}]}]}"}],"expected":{"allow":["10.0.0.128/25:udp","10.0.0.192/26","10.0.2.32/29:tcp:79","10.0.2.32/29:tcp:81-82","10.0.2.32/29:udp","10.0.2.144/29:icmp","10.0.2.144/29:tcp:5","10.0.2.144/29:udp","10.0.3.0/25:tcp","10.0.3.0/25:udp","10.0.3.64/26","10.0.3.240/30:icmp","10.0.3.244/31:icmp","128.0.0.0/1:tcp","128.0.0.0/1:udp","::/0:tcp","::/0:udp","2001:db8::100/121","2001:db8::3bd/128:icmp"],"deny":[]}}, + {"name":"random #177","input":[{"text":"{\n\"allow\": [\n{\n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 11\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 441, \n\"endPort\": 444\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.1.128/28\", \n\"except\": [\n\"10.0.1.132/31\", \n\"10.0.1.136/255.255.255.252\"\n]\n}, \n{\n\"cidr\": \"10.0.1.241/255.255.255.0\"\n}, \n{\n\"cidr\": \"10.0.0.25/31\", \n\"except\": [\n\"10.0.0.24/0031\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 65530, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 65531, \n\"endPort\": 65535\n}\n]\n}, \n{\n\"cidr\": \"10.0.0.0/22\", \n\"protocol\": \"tcp\", \n\"port\": 4\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::3c0/123\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"97.126.129.53/16\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 65535\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65533\n}\n]\n}, \n{\n\"cidr\": \"2001:db8::22f/128\", \n\"except\": [], \n\"protocol\": \"any\", \n\"port\": 445\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.200/29\"\n}, \n{\n\"cidr\": \"10.0.2.220/31\", \n\"except\": [\n\"10.0.2.220/255.255.255.254\"\n]\n}, \n{\n\"cidr\": \"2001:db8::398/124\"\n}, \n{\n\"cidr\": \"10.0.2.0/27\", \n\"except\": [\n\"10.0.2.12/30\", \n\"10.0.2.0/27\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 65533, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 12, \n\"endPort\": 17\n}, \n{\n\"protocol\": \"any\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::349/119\"\n}, \n{\n\"cidr\": \"::ffff:218.114.245.79/128\"\n}, \n{\n\"cidr\": \"10.0.3.64/26\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"udp\"\n}, \n{\n\"protocol\": \"tcp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"38.176.0.0/12\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 442\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 5, \n\"endPort\": 5\n}\n]\n}, \n{\n\"cidr\": \"10.0.3.96/30\", \n\"except\": [\n\"10.0.3.96/255.255.255.254\", \n\"10.0.3.96/031\"\n], \n\"protocol\": \"tcp\", \n\"port\": 6, \n\"endPort\": 20816\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::110/00124\", \n\"except\": [\n\"2001:db8::11a/128\", \n\"2001:db8::113/126\", \n\"2001:db8::11e/00127\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 65535, \n\"endPort\": 65535\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:00f7/125\", \n\"except\": []\n}\n], \n\"ports\": [\n{\n\"port\": 2, \n\"endPort\": 6\n}, \n{\n\"protocol\": \"any\", \n\"port\": 443\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 82, \n\"endPort\": 84\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.232\", \n\"except\": [\n\"10.0.3.232\"\n]\n}, \n{\n\"cidr\": \"10.0.1.0/0024\"\n}, \n{\n\"cidr\": \"2001:db8::2e8/127\", \n\"except\": [\n\"2001:db8::2e8/127\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 11\n}\n]\n}, \n{\n\"cidr\": \"10.0.3.77/25\", \n\"except\": [\n\"10.0.3.104/0.0.0.7\"\n], \n\"protocol\": \"tcp\", \n\"port\": 80, \n\"endPort\": 86\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/255.255.255.0\"\n}, \n{\n\"cidr\": \"2001:DB8::340/122\"\n}, \n{\n\"cidr\": \"10.0.0.31/24\"\n}, \n{\n\"cidr\": \"10.0.3.144/31\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 65534\n}, \n{\n\"protocol\": \"udp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/24\", \n\"except\": []\n}, \n{\n\"cidr\": \"10.0.2.183/25\"\n}, \n{\n\"cidr\": \"2001:db8::180/122\", \n\"except\": [\n\"2001:db8::180/122\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 65534\n}, \n{\n\"port\": 1, \n\"endPort\": 7\n}, \n{\n\"protocol\": \"any\", \n\"port\": 3\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::100/126\", \n\"except\": [\n\"2001:db8::103/000127\", \n\"2001:DB8::102/127\", \n\"2001:DB8::100/126\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 65533, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 441, \n\"endPort\": 441\n}\n]\n}, \n{\n\"cidr\": \"2001:db8::239/118\", \n\"protocol\": \"icmp\"\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::174/000127\"\n}, \n{\n\"cidr\": \"10.0.3.168/31\"\n}, \n{\n\"cidr\": \"10.0.2.0/25\", \n\"except\": []\n}, \n{\n\"cidr\": \"2001:db8::160/126\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 12, \n\"endPort\": 18\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 81\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 65535, \n\"endPort\": 65535\n}\n]\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"udp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::28/000125\", \n\"except\": [\n\"2001:db8::28/127\"\n]\n}, \n{\n\"cidr\": \"3c49:0773:0000:0000:0000:0000:0000:0000/32\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 65532, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"any\", \n\"port\": 81\n}, \n{\n\"protocol\": \"icmp\"\n}\n]\n}, \n{\n\"cidr\": \"10.0.2.0/00023\", \n\"protocol\": \"udp\"\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:00a4/00119\", \n\"protocol\": \"any\", \n\"port\": 65532\n}, \n{\n\"cidr\": \"2001:db8::c0/124\", \n\"protocol\": \"tcp\", \n\"port\": 8\n}, \n{\n\"cidr\": \"246.240.0.0/255.240.0.0\", \n\"except\": [\n\"246.252.0.0/15\"\n], \n\"protocol\": \"tcp\", \n\"port\": 443\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"icmp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.3.41/120\"\n}, \n{\n\"cidr\": \"10.0.1.64/00026\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 65533, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 82, \n\"endPort\": 83\n}, \n{\n\"protocol\": \"udp\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 65532\n}\n]\n}, \n{\n\"cidr\": \"::ffff:155.211.100.240/0128\", \n\"protocol\": \"any\", \n\"port\": 81, \n\"endPort\": 85\n}, \n{\n\"cidr\": \"2001:db8::b4/121\", \n\"protocol\": \"tcp\"\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:019f/128\"\n}, \n{\n\"cidr\": \"10.0.3.192/26\", \n\"except\": [\n\"10.0.3.193/29\"\n]\n}, \n{\n\"cidr\": \"96.0.0.0/8\"\n}, \n{\n\"cidr\": \"10.0.3.253/29\"\n}\n], \n\"ports\": [\n{\n\"port\": 442\n}\n]\n}, \n{\n\"cidr\": \"10.0.3.192/32\", \n\"protocol\": \"udp\", \n\"port\": 10, \n\"endPort\": 11\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.48/00028\", \n\"except\": [\n\"10.0.2.60/0.0.0.3\", \n\"10.0.2.54/31\", \n\"10.0.2.57/00028\"\n]\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0000/0121\"\n}\n], \n\"ports\": [\n{}\n]\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 65533, \n\"endPort\": 65533\n}, \n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"icmp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:DB8::AF/127\", \n\"except\": [\n\"2001:db8::af/128\", \n\"2001:db8::ae/127\", \n\"2001:db8::ae/127\"\n]\n}, \n{\n\"cidr\": \"2001:db8::140/122\"\n}, \n{\n\"cidr\": \"::/3\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\"\n}\n]\n}, \n{}, \n{\n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 445\n}, \n{\n\"protocol\": \"udp\"\n}, \n{\n\"protocol\": \"icmp\"\n}\n]\n}, \n{\n\"cidr\": \"2001:db8::340/123\", \n\"except\": [\n\"2001:db8::356/123\"\n], \n\"protocol\": \"tcp\", \n\"port\": 442, \n\"endPort\": 445\n}, \n{\n\"cidr\": \"10.0.0.0/255.255.254.0\", \n\"protocol\": \"tcp\", \n\"port\": 78, \n\"endPort\": 83\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/0.0.3.255\"\n}, \n{\n\"cidr\": \"10.0.2.43/0.0.0.15\"\n}, \n{\n\"cidr\": \"10.0.3.192/26\"\n}, \n{\n\"cidr\": \"8000::/3\", \n\"except\": [\n\"9000::/4\", \n\"88C0::/11\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"9b7d:98a0:af68:ca7b:e8c1:e385::/96\"\n}, \n{\n\"cidr\": \"41.0.0.0/8\"\n}, \n{\n\"cidr\": \"2001:db8::90/125\", \n\"except\": [\n\"2001:db8::90/125\"\n]\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"6238:3e86:3901:a97b:5a2f:6754:8ea7:bda8/1\"\n}, \n{\n\"cidr\": \"10.0.2.148/28\"\n}\n]\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"any\", \n\"port\": 65532\n}, \n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 4, \n\"endPort\": 8\n}\n]\n}, \n{\n\"cidr\": \"10.0.2.64/24\", \n\"except\": []\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.0/0.0.0.255\"\n}, \n{\n\"cidr\": \"49.53.57.212/0016\"\n}, \n{\n\"cidr\": \"10.0.0.0/22\"\n}, \n{\n\"cidr\": \"2001:db8::/119\", \n\"except\": [\n\"2001:db8::15a/121\", \n\"2001:db8::b0/124\", \n\"2001:db8::40/122\"\n]\n}\n]\n}, \n{\n\"cidr\": \"2001:db8::3e0/123\", \n\"except\": [], \n\"protocol\": \"udp\", \n\"port\": 9\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"udp\"\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 444, \n\"endPort\": 56756\n}\n]\n}\n], \n\"deny\": [\n{\n\"cidr\": \"2000::/3\", \n\"protocol\": \"any\"\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.100/22\", \n\"except\": [\n\"10.0.0.134/24\"\n]\n}, \n{\n\"cidr\": \"b60c:21d6:c79e:83c2:381d:c4c7:1229:9235/32\"\n}, \n{\n\"cidr\": \"10.0.0.0/25\"\n}\n], \n\"ports\": [\n{\n\"port\": 65531, \n\"endPort\": 65532\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 441\n}\n]\n}, \n{\n\"cidr\": \"10.0.1.160/27\", \n\"except\": [\n\"10.0.1.184/32\", \n\"10.0.1.176/29\"\n], \n\"protocol\": \"any\", \n\"port\": 440\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.1.192/26\"\n}, \n{\n\"cidr\": \"64.0.0.0/2\", \n\"except\": [\n\"76.0.0.0/6\", \n\"84.0.0.0/6\", \n\"89.128.0.0/10\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\"\n}, \n{\n\"protocol\": \"any\", \n\"port\": 65534\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:03b8/125\", \n\"except\": [\n\"2001:db8::3bc/126\"\n]\n}, \n{\n\"cidr\": \"10.0.0.0/22\"\n}, \n{\n\"cidr\": \"10.0.0.0/22\"\n}, \n{\n\"cidr\": \"10.0.3.56/29\", \n\"except\": [\n\"10.0.3.58/31\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"tcp\", \n\"port\": 65532, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 11, \n\"endPort\": 49274\n}, \n{\n\"protocol\": \"icmp\"\n}\n]\n}, \n{\n\"cidr\": \"10.0.1.32/28\", \n\"protocol\": \"any\", \n\"port\": 65530\n}, \n{\n\"to\": [\n{\n\"cidr\": \"0.0.0.0/0.0.0.0\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.2.0/255.255.254.0\"\n}, \n{\n\"cidr\": \"2001:db8::226/127\"\n}, \n{\n\"cidr\": \"10.0.2.8/30\", \n\"except\": [\n\"10.0.2.9\", \n\"10.0.2.10/30\", \n\"10.0.2.10/0032\"\n]\n}, \n{\n\"cidr\": \"10.0.2.250/24\"\n}\n], \n\"ports\": [\n{\n\"protocol\": \"udp\", \n\"port\": 65532, \n\"endPort\": 65535\n}, \n{\n\"protocol\": \"any\", \n\"port\": 11, \n\"endPort\": 15\n}\n]\n}\n]\n}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":["0.0.0.0/0","2000::/3","b60c:21d6::/32:tcp:65531-65532","b60c:21d6::/32:udp:441","b60c:21d6::/32:udp:65531-65532"]}}, + {"name":"random #178","input":[{"text":"{\n \"allow\": [\n {\n \"ports\": [\n {\n \"protocol\": \"any\",\n \"port\": 441,\n \"endPort\": 447\n },\n {\n \"protocol\": \"udp\"\n }\n ]\n }\n ]\n}"}],"expected":{"allow":["0.0.0.0/0:tcp:441-447","0.0.0.0/0:udp","::/0:tcp:441-447","::/0:udp"],"deny":[]}}, + {"name":"random #179","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 441, \n\t\t\t\t\t\"endPort\": 446\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65532\n\t\t\t\t}, \n\t\t\t\t{}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 442\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":["0.0.0.0/0","::/0"],"deny":[]}}, + {"name":"random #180","input":[{"text":"{\n\t\"allow\":[\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::300/120\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":9\n\t\t},\n\t\t{\n\t\t\t\"to\":[\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.131/28\",\n\t\t\t\t\t\"except\":[\n\t\t\t\t\t\t\"10.0.0.141\"\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.0.212/30\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\":\"10.0.3.72/30\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\":[\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"udp\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\":\"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\":\"2001:db8::240/122\",\n\t\t\t\"protocol\":\"udp\",\n\t\t\t\"port\":9\n\t\t}\n\t]\n}"}],"expected":{"allow":["10.0.0.128/29:icmp","10.0.0.128/29:udp","10.0.0.136/30:icmp","10.0.0.136/30:udp","10.0.0.140/32:icmp","10.0.0.140/32:udp","10.0.0.142/31:icmp","10.0.0.142/31:udp","10.0.0.212/30:icmp","10.0.0.212/30:udp","10.0.3.72/30:icmp","10.0.3.72/30:udp","2001:db8::240/122:udp:9","2001:db8::300/120:udp:9"],"deny":[]}}, + {"name":"random #181","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"10.0.3.132/255.255.252.0\", \"except\": []}, {\"cidr\": \"2001:db8::d/126\"}]}, {\"to\": [{\"cidr\": \"10.0.0.109/32\"}, {\"cidr\": \"10.0.1.0/25\"}, {\"cidr\": \"10.0.0.152/30\"}, {\"cidr\": \"::ffff:185.242.110.69/128\"}], \"ports\": [{\"protocol\": \"udp\"}]}, {\"cidr\": \"2001:db8::358/125\", \"except\": [], \"protocol\": \"tcp\", \"port\": 81, \"endPort\": 81}, {\"ports\": [{\"protocol\": \"udp\", \"port\": 65533}, {\"protocol\": \"udp\"}, {\"protocol\": \"tcp\"}]}], \"deny\": [{\"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"udp\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"any\", \"port\": 82, \"endPort\": 87}]}, {\"cidr\": \"167.64.167.38/12\", \"protocol\": \"any\", \"port\": 3}, {\"cidr\": \"10.0.0.0/22\", \"except\": [\"10.0.2.120/29\", \"10.0.3.160/255.255.255.224\", \"10.0.0.160/027\"], \"protocol\": \"any\", \"port\": 3}]}"}],"expected":{"allow":["0.0.0.0/0:tcp","0.0.0.0/0:udp","10.0.0.0/22","::/0:tcp","::/0:udp","2001:db8::c/126"],"deny":["0.0.0.0/0:icmp","0.0.0.0/0:tcp:82-87","0.0.0.0/0:udp","10.0.0.0/25:tcp:3","10.0.0.128/27:tcp:3","10.0.0.192/26:tcp:3","10.0.1.0/24:tcp:3","10.0.2.0/26:tcp:3","10.0.2.64/27:tcp:3","10.0.2.96/28:tcp:3","10.0.2.112/29:tcp:3","10.0.2.128/25:tcp:3","10.0.3.0/25:tcp:3","10.0.3.128/27:tcp:3","10.0.3.192/26:tcp:3","167.64.0.0/12:tcp:3","::/0:icmp","::/0:tcp:82-87","::/0:udp"]}}, + {"name":"random #182","input":[{"text":"{\"allow\":[{\"ports\":[{\"protocol\":\"tcp\"},{\"protocol\":\"tcp\",\"port\":80},{\"protocol\":\"udp\"}]},{\"to\":[{\"cidr\":\"0.0.0.0/1\",\"except\":[]},{\"cidr\":\"2001:db8::1a/126\"},{\"cidr\":\"0.0.0.0/001\"},{\"cidr\":\"10.0.1.114/30\"}],\"ports\":[{\"protocol\":\"udp\",\"port\":79},{\"port\":81,\"endPort\":4510},{\"protocol\":\"any\",\"port\":65534}]},{\"cidr\":\"184.0.0.0/255.0.0.0\",\"except\":[\"184.0.0.0/8\"],\"protocol\":\"tcp\",\"port\":9},{\"to\":[{\"cidr\":\"10.0.1.128/25\"},{\"cidr\":\"2001:DB8::/118\",\"except\":[]}],\"ports\":[{\"protocol\":\"icmp\"}]}],\"deny\":[{\"to\":[{\"cidr\":\"10.0.1.253/32\"},{\"cidr\":\"0.0.0.0/1\"},{\"cidr\":\"::ffff:0.0.0.0/96\"},{\"cidr\":\"2001:db8::/122\"}],\"ports\":[{\"protocol\":\"tcp\"}]},{\"cidr\":\"10.0.0.0/22\",\"protocol\":\"any\"},{\"to\":[{\"cidr\":\"2001:0db8:0000:0000:0000:0000:0000:0200/120\"},{\"cidr\":\"71.93.0.0/16\"}]}]}"}],"expected":{"allow":["0.0.0.0/0:tcp","0.0.0.0/0:udp","10.0.1.128/25:icmp","::/0:tcp","::/0:udp","2001:db8::/118:icmp"],"deny":["0.0.0.0/1:tcp","10.0.0.0/22","71.93.0.0/16","::ffff:0.0.0.0/96:tcp","2001:db8::/122:tcp","2001:db8::200/120"]}}, + {"name":"random #183","input":[{"text":"{\"deny\": []}"}],"expected":{"allow":[],"deny":[]}}, + {"name":"random #184","input":[{"text":"{\n \"allow\": [\n {\n \"cidr\": \"2001:db8::ca/127\", \n \"protocol\": \"any\", \n \"port\": 78\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::/119\", \n \"except\": [\n \"2001:0db8:0000:0000:0000:0000:0000:00c0/00122\"\n ]\n }, \n {\n \"cidr\": \"10.0.2.24/0029\", \n \"except\": []\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"tcp\", \n \"port\": 65534, \n \"endPort\": 65535\n }, \n {\n \"protocol\": \"tcp\"\n }, \n {\n \"protocol\": \"any\", \n \"port\": 3\n }, \n {\n \"protocol\": \"icmp\"\n }\n ]\n }, \n {\n \"ports\": [\n {\n \"protocol\": \"any\", \n \"port\": 65535\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 81, \n \"endPort\": 83\n }, \n {\n \"protocol\": \"udp\", \n \"port\": 445\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::274/127\", \n \"except\": [\n \"2001:db8::274\", \n \"2001:db8::275/128\", \n \"2001:0db8:0000:0000:0000:0000:0000:0275\"\n ]\n }, \n {\n \"cidr\": \"5ac9:371c:13e7:b7f5:64f6:eede:0000:0000/96\"\n }, \n {\n \"cidr\": \"10.0.3.128/25\"\n }\n ], \n \"ports\": [\n {\n \"port\": 2, \n \"endPort\": 3\n }\n ]\n }, \n {\n \"cidr\": \"2001:db8::3d5/128\", \n \"protocol\": \"any\", \n \"port\": 19\n }, \n {\n \"cidr\": \"10.0.1.224/31\", \n \"protocol\": \"any\", \n \"port\": 137\n }, \n {\n \"cidr\": \"10.0.2.188/31\", \n \"protocol\": \"udp\", \n \"port\": 275\n }, \n {\n \"cidr\": \"10.0.1.220/31\", \n \"protocol\": \"tcp\", \n \"port\": 319\n }, \n {\n \"cidr\": \"10.0.3.0/31\", \n \"protocol\": \"tcp\", \n \"port\": 167\n }, \n {\n \"cidr\": \"2001:db8::3e7/128\", \n \"protocol\": \"any\", \n \"port\": 13, \n \"endPort\": 15\n }, \n {\n \"cidr\": \"10.0.2.240/30\", \n \"protocol\": \"udp\", \n \"port\": 27, \n \"endPort\": 30\n }, \n {\n \"cidr\": \"10.0.1.32/30\", \n \"protocol\": \"udp\", \n \"port\": 277, \n \"endPort\": 277\n }, \n {\n \"cidr\": \"10.0.3.212/30\", \n \"protocol\": \"tcp\", \n \"port\": 45\n }, \n {\n \"cidr\": \"10.0.0.184/30\", \n \"protocol\": \"udp\", \n \"port\": 155\n }, \n {\n \"cidr\": \"2001:db8::39f/128\", \n \"protocol\": \"tcp\", \n \"port\": 125\n }, \n {\n \"cidr\": \"10.0.0.102/32\", \n \"protocol\": \"tcp\", \n \"port\": 233\n }, \n {\n \"cidr\": \"10.0.2.246/31\", \n \"protocol\": \"udp\", \n \"port\": 29\n }, \n {\n \"cidr\": \"10.0.0.244/30\", \n \"protocol\": \"tcp\", \n \"port\": 121\n }, \n {\n \"cidr\": \"10.0.3.124/30\", \n \"protocol\": \"tcp\", \n \"port\": 301\n }, \n {\n \"cidr\": \"10.0.0.184/32\", \n \"protocol\": \"udp\", \n \"port\": 175\n }, \n {\n \"cidr\": \"2001:db8::384/128\", \n \"protocol\": \"udp\", \n \"port\": 73\n }, \n {\n \"cidr\": \"10.0.2.99/32\", \n \"protocol\": \"tcp\", \n \"port\": 377, \n \"endPort\": 379\n }, \n {\n \"cidr\": \"10.0.1.204/30\", \n \"protocol\": \"udp\", \n \"port\": 243, \n \"endPort\": 245\n }, \n {\n \"cidr\": \"10.0.3.26/31\", \n \"protocol\": \"any\", \n \"port\": 365, \n \"endPort\": 365\n }, \n {\n \"cidr\": \"2001:db8::18/128\", \n \"protocol\": \"any\", \n \"port\": 37, \n \"endPort\": 40\n }, \n {\n \"cidr\": \"10.0.3.142/31\", \n \"protocol\": \"any\", \n \"port\": 77\n }, \n {\n \"cidr\": \"10.0.3.246/32\", \n \"protocol\": \"udp\", \n \"port\": 157\n }, \n {\n \"cidr\": \"10.0.2.151/32\", \n \"protocol\": \"udp\", \n \"port\": 203\n }, \n {\n \"cidr\": \"10.0.3.100/30\", \n \"protocol\": \"udp\", \n \"port\": 111, \n \"endPort\": 115\n }, \n {\n \"cidr\": \"2001:db8::14c/128\", \n \"protocol\": \"tcp\", \n \"port\": 161, \n \"endPort\": 161\n }, \n {\n \"cidr\": \"2001:db8::2db/128\", \n \"protocol\": \"udp\", \n \"port\": 317\n }, \n {\n \"cidr\": \"10.0.1.78/31\", \n \"protocol\": \"udp\", \n \"port\": 161\n }, \n {\n \"cidr\": \"10.0.0.248/32\", \n \"protocol\": \"udp\", \n \"port\": 309, \n \"endPort\": 311\n }, \n {\n \"cidr\": \"10.0.3.20/31\", \n \"protocol\": \"tcp\", \n \"port\": 29, \n \"endPort\": 31\n }, \n {\n \"cidr\": \"10.0.0.158/31\", \n \"protocol\": \"any\", \n \"port\": 185, \n \"endPort\": 186\n }, \n {\n \"cidr\": \"10.0.1.216/32\", \n \"protocol\": \"tcp\", \n \"port\": 195, \n \"endPort\": 195\n }, \n {\n \"cidr\": \"10.0.2.236/31\", \n \"protocol\": \"tcp\", \n \"port\": 295\n }, \n {\n \"cidr\": \"2001:db8::261/128\", \n \"protocol\": \"any\", \n \"port\": 113, \n \"endPort\": 116\n }, \n {\n \"cidr\": \"10.0.2.247/32\", \n \"protocol\": \"tcp\", \n \"port\": 35\n }, \n {\n \"cidr\": \"10.0.2.200/30\", \n \"protocol\": \"udp\", \n \"port\": 147, \n \"endPort\": 151\n }, \n {\n \"cidr\": \"10.0.2.184/30\", \n \"protocol\": \"udp\", \n \"port\": 363\n }, \n {\n \"cidr\": \"2001:db8::321/128\", \n \"protocol\": \"tcp\", \n \"port\": 7\n }, \n {\n \"cidr\": \"10.0.0.88/30\", \n \"protocol\": \"tcp\", \n \"port\": 93\n }, \n {\n \"cidr\": \"2001:db8::231/128\", \n \"protocol\": \"tcp\", \n \"port\": 319\n }, \n {\n \"cidr\": \"10.0.3.122/31\", \n \"protocol\": \"udp\", \n \"port\": 379\n }, \n {\n \"cidr\": \"10.0.1.8/30\", \n \"protocol\": \"tcp\", \n \"port\": 361\n }, \n {\n \"cidr\": \"2001:db8::1e5/128\", \n \"protocol\": \"tcp\", \n \"port\": 177\n }, \n {\n \"cidr\": \"10.0.0.51/32\", \n \"protocol\": \"udp\", \n \"port\": 287, \n \"endPort\": 288\n }, \n {\n \"cidr\": \"10.0.0.172/30\", \n \"protocol\": \"tcp\", \n \"port\": 121\n }, \n {\n \"cidr\": \"10.0.1.24/30\", \n \"protocol\": \"udp\", \n \"port\": 211, \n \"endPort\": 214\n }, \n {\n \"cidr\": \"10.0.3.207/32\", \n \"protocol\": \"any\", \n \"port\": 149\n }, \n {\n \"cidr\": \"2001:db8::2b3/128\", \n \"protocol\": \"udp\", \n \"port\": 27\n }, \n {\n \"cidr\": \"2001:db8::395/128\", \n \"protocol\": \"udp\", \n \"port\": 123, \n \"endPort\": 125\n }, \n {\n \"cidr\": \"2001:db8::5f/128\", \n \"protocol\": \"any\", \n \"port\": 107\n }, \n {\n \"cidr\": \"10.0.0.187/32\", \n \"protocol\": \"udp\", \n \"port\": 25\n }, \n {\n \"cidr\": \"10.0.3.87/32\", \n \"protocol\": \"udp\", \n \"port\": 45\n }, \n {\n \"cidr\": \"10.0.1.0/30\", \n \"protocol\": \"any\", \n \"port\": 123\n }, \n {\n \"cidr\": \"10.0.2.152/31\", \n \"protocol\": \"tcp\", \n \"port\": 353\n }, \n {\n \"cidr\": \"2001:db8::370/128\", \n \"protocol\": \"udp\", \n \"port\": 297\n }, \n {\n \"cidr\": \"10.0.0.158/31\", \n \"protocol\": \"tcp\", \n \"port\": 317\n }, \n {\n \"cidr\": \"10.0.1.149/32\", \n \"protocol\": \"any\", \n \"port\": 263\n }, \n {\n \"cidr\": \"10.0.2.52/30\", \n \"protocol\": \"udp\", \n \"port\": 363\n }, \n {\n \"cidr\": \"10.0.3.254/31\", \n \"protocol\": \"udp\", \n \"port\": 153\n }, \n {\n \"cidr\": \"10.0.2.226/32\", \n \"protocol\": \"udp\", \n \"port\": 15\n }, \n {\n \"cidr\": \"2001:db8::2d4/128\", \n \"protocol\": \"tcp\", \n \"port\": 255\n }, \n {\n \"cidr\": \"2001:db8::335/128\", \n \"protocol\": \"any\", \n \"port\": 203, \n \"endPort\": 204\n }, \n {\n \"cidr\": \"10.0.3.44/30\", \n \"protocol\": \"tcp\", \n \"port\": 395\n }, \n {\n \"cidr\": \"10.0.0.241/32\", \n \"protocol\": \"tcp\", \n \"port\": 183\n }, \n {\n \"cidr\": \"2001:db8::269/128\", \n \"protocol\": \"tcp\", \n \"port\": 239, \n \"endPort\": 239\n }, \n {\n \"cidr\": \"2001:db8::2b5/128\", \n \"protocol\": \"tcp\", \n \"port\": 173\n }, \n {\n \"cidr\": \"2001:db8::122/128\", \n \"protocol\": \"tcp\", \n \"port\": 291\n }, \n {\n \"cidr\": \"10.0.2.40/30\", \n \"protocol\": \"any\", \n \"port\": 105, \n \"endPort\": 108\n }, \n {\n \"cidr\": \"10.0.1.252/30\", \n \"protocol\": \"tcp\", \n \"port\": 177\n }, \n {\n \"cidr\": \"2001:db8::370/128\", \n \"protocol\": \"udp\", \n \"port\": 281\n }, \n {\n \"cidr\": \"10.0.3.204/30\", \n \"protocol\": \"any\", \n \"port\": 269, \n \"endPort\": 271\n }, \n {\n \"cidr\": \"10.0.1.56/31\", \n \"protocol\": \"udp\", \n \"port\": 349\n }, \n {\n \"cidr\": \"2001:db8::36b/128\", \n \"protocol\": \"udp\", \n \"port\": 17\n }, \n {\n \"cidr\": \"10.0.3.152/31\", \n \"protocol\": \"any\", \n \"port\": 177\n }, \n {\n \"cidr\": \"2001:db8::36b/128\", \n \"protocol\": \"tcp\", \n \"port\": 307\n }, \n {\n \"cidr\": \"10.0.0.94/31\", \n \"protocol\": \"udp\", \n \"port\": 327\n }, \n {\n \"cidr\": \"10.0.2.168/30\", \n \"protocol\": \"udp\", \n \"port\": 355\n }, \n {\n \"cidr\": \"10.0.3.160/32\", \n \"protocol\": \"udp\", \n \"port\": 19\n }, \n {\n \"cidr\": \"10.0.2.98/32\", \n \"protocol\": \"any\", \n \"port\": 177\n }, \n {\n \"cidr\": \"10.0.1.137/32\", \n \"protocol\": \"any\", \n \"port\": 41, \n \"endPort\": 42\n }, \n {\n \"cidr\": \"2001:db8::250/128\", \n \"protocol\": \"tcp\", \n \"port\": 95, \n \"endPort\": 97\n }, \n {\n \"cidr\": \"10.0.0.195/32\", \n \"protocol\": \"udp\", \n \"port\": 173\n }, \n {\n \"cidr\": \"10.0.2.245/32\", \n \"protocol\": \"tcp\", \n \"port\": 353\n }, \n {\n \"cidr\": \"10.0.0.32/30\", \n \"protocol\": \"any\", \n \"port\": 83\n }, \n {\n \"cidr\": \"10.0.0.188/32\", \n \"protocol\": \"any\", \n \"port\": 121\n }, \n {\n \"cidr\": \"10.0.0.236/30\", \n \"protocol\": \"any\", \n \"port\": 233\n }, \n {\n \"cidr\": \"2001:db8::137/128\", \n \"protocol\": \"any\", \n \"port\": 45\n }, \n {\n \"cidr\": \"2001:db8::a6/128\", \n \"protocol\": \"udp\", \n \"port\": 235, \n \"endPort\": 237\n }, \n {\n \"cidr\": \"10.0.3.162/31\", \n \"protocol\": \"tcp\", \n \"port\": 271, \n \"endPort\": 274\n }, \n {\n \"cidr\": \"10.0.2.98/31\", \n \"protocol\": \"any\", \n \"port\": 165, \n \"endPort\": 167\n }, \n {\n \"cidr\": \"10.0.3.164/32\", \n \"protocol\": \"any\", \n \"port\": 367\n }, \n {\n \"cidr\": \"10.0.1.204/31\", \n \"protocol\": \"any\", \n \"port\": 295\n }, \n {\n \"cidr\": \"2001:db8::1ac/128\", \n \"protocol\": \"any\", \n \"port\": 347\n }, \n {\n \"cidr\": \"10.0.3.200/30\", \n \"protocol\": \"udp\", \n \"port\": 119\n }, \n {\n \"cidr\": \"2001:db8::6b/128\", \n \"protocol\": \"any\", \n \"port\": 307\n }, \n {\n \"cidr\": \"10.0.1.90/31\", \n \"protocol\": \"any\", \n \"port\": 259, \n \"endPort\": 261\n }, \n {\n \"cidr\": \"10.0.3.80/30\", \n \"protocol\": \"any\", \n \"port\": 63\n }, \n {\n \"cidr\": \"10.0.2.34/31\", \n \"protocol\": \"any\", \n \"port\": 43\n }, \n {\n \"cidr\": \"10.0.1.104/32\", \n \"protocol\": \"tcp\", \n \"port\": 55\n }, \n {\n \"cidr\": \"10.0.3.184/30\", \n \"protocol\": \"tcp\", \n \"port\": 271\n }, \n {\n \"cidr\": \"10.0.1.198/31\", \n \"protocol\": \"udp\", \n \"port\": 389\n }, \n {\n \"cidr\": \"10.0.2.153/32\", \n \"protocol\": \"any\", \n \"port\": 65, \n \"endPort\": 68\n }, \n {\n \"cidr\": \"2001:db8::15d/128\", \n \"protocol\": \"any\", \n \"port\": 345, \n \"endPort\": 349\n }, \n {\n \"cidr\": \"10.0.3.104/30\", \n \"protocol\": \"any\", \n \"port\": 275, \n \"endPort\": 275\n }, \n {\n \"cidr\": \"10.0.2.12/30\", \n \"protocol\": \"any\", \n \"port\": 293\n }, \n {\n \"cidr\": \"2001:db8::15c/128\", \n \"protocol\": \"udp\", \n \"port\": 37\n }, \n {\n \"cidr\": \"2001:db8::337/128\", \n \"protocol\": \"tcp\", \n \"port\": 203\n }, \n {\n \"cidr\": \"10.0.1.236/30\", \n \"protocol\": \"udp\", \n \"port\": 167\n }, \n {\n \"cidr\": \"10.0.3.250/31\", \n \"protocol\": \"udp\", \n \"port\": 231\n }, \n {\n \"cidr\": \"10.0.1.224/31\", \n \"protocol\": \"any\", \n \"port\": 399, \n \"endPort\": 399\n }, \n {\n \"cidr\": \"10.0.1.215/32\", \n \"protocol\": \"any\", \n \"port\": 71\n }, \n {\n \"cidr\": \"2001:db8::1d2/128\", \n \"protocol\": \"tcp\", \n \"port\": 349\n }, \n {\n \"cidr\": \"10.0.1.234/32\", \n \"protocol\": \"udp\", \n \"port\": 93\n }, \n {\n \"cidr\": \"2001:db8::39b/128\", \n \"protocol\": \"udp\", \n \"port\": 15\n }, \n {\n \"cidr\": \"10.0.1.36/31\", \n \"protocol\": \"any\", \n \"port\": 265, \n \"endPort\": 266\n }, \n {\n \"cidr\": \"10.0.2.92/31\", \n \"protocol\": \"udp\", \n \"port\": 315\n }, \n {\n \"cidr\": \"10.0.3.88/30\", \n \"protocol\": \"udp\", \n \"port\": 223\n }, \n {\n \"cidr\": \"10.0.3.212/30\", \n \"protocol\": \"any\", \n \"port\": 267\n }, \n {\n \"cidr\": \"10.0.0.65/32\", \n \"protocol\": \"tcp\", \n \"port\": 193\n }, \n {\n \"cidr\": \"10.0.3.1/32\", \n \"protocol\": \"tcp\", \n \"port\": 337\n }, \n {\n \"cidr\": \"10.0.2.105/32\", \n \"protocol\": \"any\", \n \"port\": 205\n }, \n {\n \"cidr\": \"2001:db8::7b/128\", \n \"protocol\": \"any\", \n \"port\": 205\n }, \n {\n \"cidr\": \"10.0.3.42/32\", \n \"protocol\": \"udp\", \n \"port\": 1\n }, \n {\n \"cidr\": \"10.0.1.11/32\", \n \"protocol\": \"tcp\", \n \"port\": 31\n }, \n {\n \"cidr\": \"10.0.2.233/32\", \n \"protocol\": \"udp\", \n \"port\": 45\n }, \n {\n \"cidr\": \"10.0.1.244/31\", \n \"protocol\": \"udp\", \n \"port\": 175\n }, \n {\n \"cidr\": \"10.0.2.4/31\", \n \"protocol\": \"tcp\", \n \"port\": 323\n }, \n {\n \"cidr\": \"10.0.3.136/30\", \n \"protocol\": \"tcp\", \n \"port\": 7\n }, \n {\n \"cidr\": \"2001:db8::155/128\", \n \"protocol\": \"any\", \n \"port\": 93, \n \"endPort\": 95\n }, \n {\n \"cidr\": \"10.0.1.220/32\", \n \"protocol\": \"tcp\", \n \"port\": 263, \n \"endPort\": 266\n }, \n {\n \"cidr\": \"2001:db8::11b/128\", \n \"protocol\": \"udp\", \n \"port\": 333\n }, \n {\n \"cidr\": \"10.0.0.92/31\", \n \"protocol\": \"any\", \n \"port\": 27, \n \"endPort\": 27\n }, \n {\n \"cidr\": \"10.0.0.63/32\", \n \"protocol\": \"any\", \n \"port\": 61\n }, \n {\n \"cidr\": \"10.0.2.64/30\", \n \"protocol\": \"any\", \n \"port\": 259\n }, \n {\n \"cidr\": \"10.0.0.76/30\", \n \"protocol\": \"tcp\", \n \"port\": 215\n }, \n {\n \"cidr\": \"10.0.3.198/31\", \n \"protocol\": \"tcp\", \n \"port\": 371, \n \"endPort\": 373\n }, \n {\n \"cidr\": \"2001:db8::18/128\", \n \"protocol\": \"udp\", \n \"port\": 169\n }, \n {\n \"cidr\": \"10.0.0.44/32\", \n \"protocol\": \"tcp\", \n \"port\": 219\n }, \n {\n \"cidr\": \"10.0.2.168/30\", \n \"protocol\": \"tcp\", \n \"port\": 123\n }, \n {\n \"cidr\": \"2001:db8::46/128\", \n \"protocol\": \"udp\", \n \"port\": 177\n }, \n {\n \"cidr\": \"10.0.2.28/32\", \n \"protocol\": \"tcp\", \n \"port\": 217\n }, \n {\n \"cidr\": \"2001:db8::23e/128\", \n \"protocol\": \"tcp\", \n \"port\": 263\n }, \n {\n \"cidr\": \"2001:db8::2bd/128\", \n \"protocol\": \"udp\", \n \"port\": 189, \n \"endPort\": 191\n }, \n {\n \"cidr\": \"10.0.2.8/31\", \n \"protocol\": \"any\", \n \"port\": 127, \n \"endPort\": 131\n }, \n {\n \"cidr\": \"10.0.3.2/31\", \n \"protocol\": \"udp\", \n \"port\": 199, \n \"endPort\": 199\n }, \n {\n \"cidr\": \"2001:db8::9d/128\", \n \"protocol\": \"udp\", \n \"port\": 41\n }, \n {\n \"cidr\": \"10.0.0.52/30\", \n \"protocol\": \"udp\", \n \"port\": 371, \n \"endPort\": 372\n }, \n {\n \"cidr\": \"10.0.0.180/30\", \n \"protocol\": \"tcp\", \n \"port\": 297, \n \"endPort\": 298\n }, \n {\n \"cidr\": \"2001:db8::3b9/128\", \n \"protocol\": \"any\", \n \"port\": 225\n }, \n {\n \"cidr\": \"10.0.0.238/31\", \n \"protocol\": \"tcp\", \n \"port\": 335\n }, \n {\n \"cidr\": \"10.0.3.191/32\", \n \"protocol\": \"tcp\", \n \"port\": 287, \n \"endPort\": 288\n }, \n {\n \"cidr\": \"2001:db8::1c4/128\", \n \"protocol\": \"any\", \n \"port\": 335\n }, \n {\n \"cidr\": \"10.0.2.222/31\", \n \"protocol\": \"tcp\", \n \"port\": 83\n }, \n {\n \"cidr\": \"10.0.3.38/31\", \n \"protocol\": \"udp\", \n \"port\": 43\n }, \n {\n \"cidr\": \"2001:db8::1e7/128\", \n \"protocol\": \"tcp\", \n \"port\": 161, \n \"endPort\": 162\n }, \n {\n \"cidr\": \"10.0.0.84/31\", \n \"protocol\": \"udp\", \n \"port\": 267\n }, \n {\n \"cidr\": \"2001:db8::42/128\", \n \"protocol\": \"udp\", \n \"port\": 343\n }, \n {\n \"cidr\": \"10.0.1.26/31\", \n \"protocol\": \"tcp\", \n \"port\": 89\n }, \n {\n \"cidr\": \"10.0.2.232/30\", \n \"protocol\": \"any\", \n \"port\": 269\n }, \n {\n \"cidr\": \"10.0.3.171/32\", \n \"protocol\": \"any\", \n \"port\": 373\n }, \n {\n \"cidr\": \"2001:db8::34e/128\", \n \"protocol\": \"udp\", \n \"port\": 319\n }, \n {\n \"cidr\": \"10.0.2.100/30\", \n \"protocol\": \"udp\", \n \"port\": 255, \n \"endPort\": 255\n }, \n {\n \"cidr\": \"10.0.3.20/30\", \n \"protocol\": \"tcp\", \n \"port\": 399\n }, \n {\n \"cidr\": \"10.0.0.96/31\", \n \"protocol\": \"udp\", \n \"port\": 245\n }, \n {\n \"cidr\": \"10.0.0.44/31\", \n \"protocol\": \"any\", \n \"port\": 233, \n \"endPort\": 235\n }, \n {\n \"cidr\": \"10.0.0.208/31\", \n \"protocol\": \"tcp\", \n \"port\": 131\n }, \n {\n \"cidr\": \"10.0.0.96/31\", \n \"protocol\": \"tcp\", \n \"port\": 71\n }, \n {\n \"cidr\": \"10.0.1.12/30\", \n \"protocol\": \"any\", \n \"port\": 325, \n \"endPort\": 329\n }, \n {\n \"cidr\": \"10.0.2.114/31\", \n \"protocol\": \"any\", \n \"port\": 11, \n \"endPort\": 15\n }, \n {\n \"cidr\": \"10.0.2.160/31\", \n \"protocol\": \"udp\", \n \"port\": 35\n }, \n {\n \"cidr\": \"2001:db8::d6/128\", \n \"protocol\": \"udp\", \n \"port\": 283, \n \"endPort\": 286\n }, \n {\n \"cidr\": \"10.0.3.20/31\", \n \"protocol\": \"any\", \n \"port\": 215\n }, \n {\n \"cidr\": \"2001:db8::2e6/128\", \n \"protocol\": \"any\", \n \"port\": 137, \n \"endPort\": 139\n }, \n {\n \"cidr\": \"10.0.0.220/30\", \n \"protocol\": \"udp\", \n \"port\": 283\n }, \n {\n \"cidr\": \"2001:db8::3d0/128\", \n \"protocol\": \"tcp\", \n \"port\": 175\n }, \n {\n \"cidr\": \"10.0.3.114/32\", \n \"protocol\": \"any\", \n \"port\": 89\n }, \n {\n \"cidr\": \"10.0.0.8/30\", \n \"protocol\": \"tcp\", \n \"port\": 29\n }, \n {\n \"cidr\": \"2001:db8::3da/128\", \n \"protocol\": \"tcp\", \n \"port\": 251\n }, \n {\n \"cidr\": \"10.0.2.14/31\", \n \"protocol\": \"tcp\", \n \"port\": 231, \n \"endPort\": 234\n }, \n {\n \"cidr\": \"2001:db8::89/128\", \n \"protocol\": \"any\", \n \"port\": 113\n }, \n {\n \"cidr\": \"10.0.1.37/32\", \n \"protocol\": \"udp\", \n \"port\": 219, \n \"endPort\": 222\n }, \n {\n \"cidr\": \"10.0.2.78/32\", \n \"protocol\": \"any\", \n \"port\": 303\n }, \n {\n \"cidr\": \"10.0.0.36/30\", \n \"protocol\": \"any\", \n \"port\": 67, \n \"endPort\": 67\n }, \n {\n \"cidr\": \"10.0.0.148/30\", \n \"protocol\": \"any\", \n \"port\": 209\n }, \n {\n \"cidr\": \"10.0.1.251/32\", \n \"protocol\": \"udp\", \n \"port\": 307, \n \"endPort\": 311\n }, \n {\n \"cidr\": \"10.0.0.9/32\", \n \"protocol\": \"udp\", \n \"port\": 323, \n \"endPort\": 324\n }, \n {\n \"cidr\": \"10.0.1.19/32\", \n \"protocol\": \"udp\", \n \"port\": 337\n }, \n {\n \"cidr\": \"10.0.2.226/32\", \n \"protocol\": \"udp\", \n \"port\": 9\n }, \n {\n \"cidr\": \"2001:db8::3be/128\", \n \"protocol\": \"any\", \n \"port\": 193, \n \"endPort\": 194\n }, \n {\n \"cidr\": \"10.0.3.111/32\", \n \"protocol\": \"tcp\", \n \"port\": 241, \n \"endPort\": 245\n }, \n {\n \"cidr\": \"10.0.2.232/31\", \n \"protocol\": \"any\", \n \"port\": 311\n }, \n {\n \"cidr\": \"10.0.3.195/32\", \n \"protocol\": \"tcp\", \n \"port\": 341\n }, \n {\n \"cidr\": \"10.0.2.110/31\", \n \"protocol\": \"tcp\", \n \"port\": 185\n }, \n {\n \"cidr\": \"10.0.3.234/31\", \n \"protocol\": \"udp\", \n \"port\": 183\n }, \n {\n \"cidr\": \"10.0.1.92/32\", \n \"protocol\": \"any\", \n \"port\": 163\n }, \n {\n \"cidr\": \"10.0.1.44/32\", \n \"protocol\": \"udp\", \n \"port\": 51, \n \"endPort\": 51\n }, \n {\n \"cidr\": \"2001:db8::246/128\", \n \"protocol\": \"any\", \n \"port\": 243\n }, \n {\n \"cidr\": \"2001:db8::51/128\", \n \"protocol\": \"udp\", \n \"port\": 263, \n \"endPort\": 264\n }, \n {\n \"cidr\": \"10.0.3.88/30\", \n \"protocol\": \"udp\", \n \"port\": 219, \n \"endPort\": 220\n }, \n {\n \"cidr\": \"2001:db8::193/128\", \n \"protocol\": \"udp\", \n \"port\": 285\n }, \n {\n \"cidr\": \"10.0.0.32/30\", \n \"protocol\": \"any\", \n \"port\": 305\n }, \n {\n \"cidr\": \"10.0.3.88/30\", \n \"protocol\": \"tcp\", \n \"port\": 43, \n \"endPort\": 45\n }, \n {\n \"cidr\": \"2001:db8::107/128\", \n \"protocol\": \"tcp\", \n \"port\": 263\n }, \n {\n \"cidr\": \"10.0.1.112/31\", \n \"protocol\": \"any\", \n \"port\": 19\n }, \n {\n \"cidr\": \"2001:db8::3d5/128\", \n \"protocol\": \"udp\", \n \"port\": 159\n }, \n {\n \"cidr\": \"2001:db8::3d/128\", \n \"protocol\": \"udp\", \n \"port\": 121\n }, \n {\n \"cidr\": \"10.0.1.92/32\", \n \"protocol\": \"tcp\", \n \"port\": 81, \n \"endPort\": 82\n }, \n {\n \"cidr\": \"10.0.2.82/32\", \n \"protocol\": \"udp\", \n \"port\": 75\n }, \n {\n \"cidr\": \"10.0.3.120/32\", \n \"protocol\": \"udp\", \n \"port\": 71\n }, \n {\n \"cidr\": \"10.0.0.155/32\", \n \"protocol\": \"udp\", \n \"port\": 87\n }, \n {\n \"cidr\": \"10.0.0.140/32\", \n \"protocol\": \"any\", \n \"port\": 249\n }, \n {\n \"cidr\": \"10.0.3.184/30\", \n \"protocol\": \"udp\", \n \"port\": 239, \n \"endPort\": 240\n }, \n {\n \"cidr\": \"10.0.3.84/30\", \n \"protocol\": \"tcp\", \n \"port\": 215\n }, \n {\n \"cidr\": \"10.0.0.184/30\", \n \"protocol\": \"any\", \n \"port\": 249\n }, \n {\n \"cidr\": \"10.0.0.20/30\", \n \"protocol\": \"tcp\", \n \"port\": 153\n }, \n {\n \"cidr\": \"10.0.0.68/30\", \n \"protocol\": \"udp\", \n \"port\": 389, \n \"endPort\": 389\n }, \n {\n \"cidr\": \"10.0.2.136/31\", \n \"protocol\": \"tcp\", \n \"port\": 189\n }, \n {\n \"cidr\": \"2001:db8::161/128\", \n \"protocol\": \"any\", \n \"port\": 293\n }, \n {\n \"cidr\": \"10.0.2.197/32\", \n \"protocol\": \"tcp\", \n \"port\": 81, \n \"endPort\": 83\n }, \n {\n \"cidr\": \"10.0.2.74/31\", \n \"protocol\": \"tcp\", \n \"port\": 389\n }, \n {\n \"cidr\": \"10.0.2.152/30\", \n \"protocol\": \"udp\", \n \"port\": 327, \n \"endPort\": 328\n }, \n {\n \"cidr\": \"10.0.1.114/31\", \n \"protocol\": \"any\", \n \"port\": 145\n }\n ], \n \"deny\": [\n {\n \"cidr\": \"10.0.2.0/25\", \n \"except\": [\n \"10.0.2.96/027\", \n \"10.0.2.103/31\"\n ], \n \"protocol\": \"tcp\", \n \"port\": 82, \n \"endPort\": 86\n }, \n {\n \"cidr\": \"2001:db8::284/126\"\n }, \n {\n \"to\": [\n {\n \"cidr\": \"10.0.0.86/255.255.255.254\"\n }, \n {\n \"cidr\": \"10.0.1.0/255.255.255.0\", \n \"except\": [\n \"10.0.1.58\", \n \"0.0.0.0/0\"\n ]\n }, \n {\n \"cidr\": \"10.0.3.88/29\"\n }, \n {\n \"cidr\": \"540a:fe1:4f51:3550:347f:4f7b:b4b2:6bd8/0\", \n \"except\": []\n }\n ]\n }, \n {\n \"to\": [\n {\n \"cidr\": \"2001:db8::398/127\"\n }, \n {\n \"cidr\": \"2001:db8::77/119\"\n }\n ]\n }, \n {\n \"cidr\": \"2001:db8::36/128\", \n \"except\": [\n \"2001:db8::36\"\n ], \n \"protocol\": \"tcp\"\n }, \n {\n \"to\": [\n {\n \"cidr\": \"0.0.0.0/0\"\n }\n ], \n \"ports\": [\n {\n \"protocol\": \"udp\"\n }, \n {\n \"protocol\": \"tcp\", \n \"port\": 81, \n \"endPort\": 86\n }, \n {\n \"protocol\": \"any\"\n }, \n {\n \"protocol\": \"icmp\"\n }\n ]\n }\n ]\n}"}],"expected":{"error":"allow emits at most 256 native rules"}}, + {"name":"random #185","input":[{"text":"{\"allow\": [{\"cidr\": \"2001:db8::2d0/124\", \"except\": [], \"protocol\": \"tcp\"}, {\"to\": [{\"cidr\": \"176.2.200.102/4\", \"except\": [\"177.97.93.166/255.0.0.0\", \"184.0.0.0/7\"]}, {\"cidr\": \"2001:db8::/121\"}, {\"cidr\": \"110.91.37.163/32\", \"except\": [\"110.91.37.163/255.255.255.255\"]}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0260/0127\", \"except\": [\"2001:db8::261/127\", \"2001:0db8:0000:0000:0000:0000:0000:0260/127\"]}], \"ports\": [{\"protocol\": \"tcp\", \"port\": 65530, \"endPort\": 65534}, {\"protocol\": \"udp\"}]}, {\"cidr\": \"2001:db8::180/0123\", \"protocol\": \"udp\"}, {\"to\": [{\"cidr\": \"10.0.0.0/23\", \"except\": []}, {\"cidr\": \"10.0.0.0/24\", \"except\": []}]}]}"}],"expected":{"allow":["10.0.0.0/23","176.0.0.0/8:tcp:65530-65534","176.0.0.0/8:udp","178.0.0.0/7:tcp:65530-65534","178.0.0.0/7:udp","180.0.0.0/6:tcp:65530-65534","180.0.0.0/6:udp","186.0.0.0/7:tcp:65530-65534","186.0.0.0/7:udp","188.0.0.0/6:tcp:65530-65534","188.0.0.0/6:udp","2001:db8::/121:tcp:65530-65534","2001:db8::/121:udp","2001:db8::180/123:udp","2001:db8::2d0/124:tcp"],"deny":[]}}, + {"name":"random #186","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"2001:db8::1b4/127\", \n\t\t\t\"except\": [\n\t\t\t\t\"2001:db8::1b5\"\n\t\t\t], \n\t\t\t\"protocol\": \"tcp\", \n\t\t\t\"port\": 6\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"143.0.0.0/08\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"143.119.152.25/00013\", \n\t\t\t\t\t\t\"143.128.0.0/255.192.0.0\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"36.32.0.0/12\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"36.32.0.0/12\", \n\t\t\t\t\t\t\"36.44.0.0/14\", \n\t\t\t\t\t\t\"36.34.33.169/17\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.212/22\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::/119\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::/118\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:db8::f/124\", \n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0000:01a9/0120\", \n\t\t\t\t\t\t\"2001:db8::220/121\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 442\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"}],"expected":{"allow":["10.0.0.0/22:tcp","143.0.0.0/10","143.64.0.0/11","143.96.0.0/12","143.120.0.0/13","143.192.0.0/10","2001:db8::/119:tcp","2001:db8::280/121:tcp","2001:db8::300/120:tcp"],"deny":[]}}, + {"name":"random #187","input":[{"text":"{\"allow\":[{\"to\":[{\"cidr\":\"10.0.0.0/22\"},{\"cidr\":\"10.0.3.90/31\",\"except\":[]},{\"cidr\":\"2001:db8::280/0123\"},{\"cidr\":\"192.0.0.0/2\",\"except\":[\"212.128.0.0/9\"]}],\"ports\":[{\"protocol\":\"any\"},{\"protocol\":\"tcp\"},{\"protocol\":\"any\",\"port\":444},{\"protocol\":\"any\"}]},{\"ports\":[{\"protocol\":\"any\"},{\"protocol\":\"tcp\",\"port\":7,\"endPort\":13}]},{\"cidr\":\"10.0.3.208/28\",\"protocol\":\"udp\",\"port\":445},{\"to\":[{\"cidr\":\"10.0.2.0/23\"},{\"cidr\":\"10.0.2.0/255.255.254.0\",\"except\":[\"10.0.2.128/26\"]},{\"cidr\":\"2001:db8::/123\",\"except\":[]},{\"cidr\":\"2001:db8::/118\"}],\"ports\":[{\"protocol\":\"tcp\",\"port\":79}]}],\"deny\":[{\"cidr\":\"2001:db8::28f/00124\",\"protocol\":\"icmp\",\"ports\":[]},{\"to\":[{\"cidr\":\"::ffff:15.224.251.152/128\"},{\"cidr\":\"2001:db8::c0/122\"},{\"cidr\":\"225.30.0.0/16\"}]},{\"cidr\":\"10.0.1.0/25\",\"protocol\":\"tcp\",\"port\":80}]}"}],"expected":{"error":"deny[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #188","input":[{"text":"{\n \"allow\": [], \n \"deny\": [\n {\n \"cidr\": \"128.0.0.0/2\", \n \"except\": [], \n \"protocol\": \"tcp\", \n \"port\": 9, \n \"endPort\": 12, \n \"to\": []\n }\n ]\n}"}],"expected":{"error":"deny[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #189","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0380/123\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"2001:db8::394/127\", \n\t\t\t\t\t\t\"2001:0db8:0000:0000:0000:0000:0.0.3.128/124\", \n\t\t\t\t\t\t\"2001:db8::380/123\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"64.0.0.0/4\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"66.0.0.0/8\", \n\t\t\t\t\t\t\"69.236.148.130/10\", \n\t\t\t\t\t\t\"77.241.88.244/6\"\n\t\t\t\t\t]\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.65/32\", \n\t\t\t\t\t\"except\": []\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 82\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 1, \n\t\t\t\t\t\"endPort\": 6\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 65534, \n\t\t\t\t\t\"endPort\": 65535\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"cidr\": \"2001:db8::/121\", \n\t\t\t\"protocol\": \"icmp\"\n\t\t}, \n\t\t{\n\t\t\t\"to\": \"192.0.2.0/24\"\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.3.84/030\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::e0/124\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 445\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"port\": 65530, \n\t\t\t\t\t\"endPort\": 65530\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"icmp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\", \n\t\t\t\t\t\"port\": 442, \n\t\t\t\t\t\"endPort\": 444\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\"\n\t\t\t\t}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.0/25\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"20.201.181.0/24\", \n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"20.201.181.16/29\", \n\t\t\t\t\t\t\"20.201.181.220/30\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{}\n\t\t\t]\n\t\t}, \n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::ea/125\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:01f7/127\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:db8::/118\"\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"128.0.0.0/1\"\n\t\t\t\t}\n\t\t\t], \n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 443\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 79\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\", \n\t\t\t\t\t\"port\": 79\n\t\t\t\t}, \n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"any\", \n\t\t\t\t\t\"port\": 442, \n\t\t\t\t\t\"endPort\": 447\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t], \n\t\"deny\": []\n}"}],"expected":{"error":"allow[2].to must be an array"}}, + {"name":"random #190","input":[{"text":"{\"deny\": [{\"cidr\": \"10.0.1.126/32\", \"protocol\": \"tcp\", \"port\": 65531}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:006c/125\", \"protocol\": \"tcp\", \"port\": 65530, \"endPort\": 65535}, {\"cidr\": \"2001:db8::e0/123\", \"except\": [\"2001:db8::ea/127\"], \"protocol\": \"any\"}, {\"to\": [{\"cidr\": \"164.149.0.0/16\"}, {\"cidr\": \"10.0.1.18/255.255.254.0\"}], \"ports\": [{\"port\": 65532}]}, {\"ports\": [{\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"2001:db8::3cc/126\", \"except\": [\"2001:0db8:0000:0000:0000:0000:0000:03ce/127\", \"2001:db8::3cc/128\"]}, {\"cidr\": \"10.0.3.24/30\"}], \"ports\": [{}, {\"protocol\": \"tcp\", \"port\": 1}]}, {\"cidr\": \"32.0.0.0/4\", \"except\": [\"32.0.0.0/9\"], \"protocol\": \"icmp\"}, {\"to\": [{\"cidr\": \"2001:db8::3d0/124\", \"except\": [\"2001:db8::3d8/128\", \"2001:db8::3d0/124\", \"2001:db8::3d0/124\"]}, {\"cidr\": \"10.0.0.0/24\"}]}]}"}],"expected":{"allow":[],"deny":["0.0.0.0/0:tcp","10.0.0.0/23:udp:65532","10.0.0.0/24","10.0.3.24/30","32.128.0.0/9:icmp","33.0.0.0/8:icmp","34.0.0.0/7:icmp","36.0.0.0/6:icmp","40.0.0.0/5:icmp","164.149.0.0/16:udp:65532","::/0:tcp","2001:db8::e0/125","2001:db8::e8/127","2001:db8::ec/126","2001:db8::f0/124","2001:db8::3cd/128"]}}, + {"name":"random #191","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.249/30\"\n},\n{\n\"cidr\": \"10.0.1.216/30\"\n},\n{\n\"cidr\": \"2001:db8::360/124\",\n\"except\": [\n\"2001:DB8::362/124\",\n\"2001:db8::200/121\"\n]\n}\n]\n}\n],\n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"195.5.0.0/16\"\n},\n{\n\"cidr\": \"10.0.2.0/026\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"udp\",\n\"port\": 440\n},\n{\n\"protocol\": \"any\",\n\"port\": 6\n},\n{\n\"protocol\": \"any\",\n\"port\": 65531,\n\"endPort\": 65534\n},\n{\n\"protocol\": \"tcp\",\n\"port\": 440\n}\n]\n},\n{\n\"cidr\": \"2001:db8::278/125\",\n\"except\": [],\n\"protocol\": \"any\",\n\"port\": true\n},\n{\n\"cidr\": \"10.0.3.192/26\",\n\"protocol\": \"tcp\"\n},\n{\n\"to\": [\n{\n\"cidr\": \"0000:0000:0000:0000:0000:0000:0000:0000/0\"\n},\n{\n\"cidr\": \"10.0.3.158/31\",\n\"except\": [\n\"10.0.3.158/31\",\n\"10.0.3.159/32\",\n\"10.0.3.158/31\"\n]\n},\n{\n\"cidr\": \"10.0.3.104/30\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"any\",\n\"port\": 442,\n\"endPort\": 442\n},\n{\n\"protocol\": \"any\",\n\"port\": 81\n},\n{\n\"protocol\": \"any\"\n},\n{\n\"protocol\": \"tcp\",\n\"port\": 5,\n\"endPort\": 6\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::60/125\"\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"3.32.0.0/12\"\n},\n{\n\"cidr\": \"10.0.0.116\"\n},\n{\n\"cidr\": \"10.0.1.8/0029\"\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.0/22\",\n\"except\": [\n\"10.0.2.32/27\",\n\"10.0.0.60/23\"\n]\n},\n{\n\"cidr\": \"10.0.1.104/29\",\n\"except\": []\n},\n{\n\"cidr\": \"10.0.0.255/0.0.0.1\"\n},\n{\n\"cidr\": \"0000:0000:0000:0000:0000:ffff:238.120.161.0/120\"\n}\n],\n\"ports\": [\n{},\n{\n\"protocol\": \"tcp\",\n\"port\": 81\n}\n]\n},\n{\n\"cidr\": \"0.0.0.0/1\",\n\"except\": [\n\"0.0.0.0/2\",\n\"0.0.0.0/1\"\n],\n\"protocol\": \"tcp\"\n}\n]\n}"}],"expected":{"error":"allow[0].to[2].except[1] must be contained in 2001:db8::360/124"}}, + {"name":"random #192","input":[{"text":""}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"random #193","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.135/00022\",\n\"except\": [\n\"10.0.0.64/27\",\n\"10.0.0.136/30\",\n\"10.0.3.68/30\"\n]\n},\n{\n\"cidr\": \"39.213.0.0/16\"\n},\n{\n\"cidr\": \"22ec:3cf2:61e7:925e:b54:6cbb:9133:9a3a/32\"\n},\n{\n\"cidr\": \"172.162.230.0/24\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"icmp\"\n},\n{\n\"protocol\": \"tcp\"\n},\n{\n\"protocol\": \"any\",\n\"port\": 12\n},\n{\n\"protocol\": \"udp\",\n\"port\": 65531,\n\"endPort\": 65535\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"::/0\",\n\"except\": [\n\"9000::/4\",\n\"4e00::/7\"\n]\n},\n{\n\"cidr\": \"2001:db8::bb/122\"\n},\n{\n\"cidr\": \"2001:DB8::300/122\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"tcp\"\n},\n{\n\"protocol\": \"tcp\"\n},\n{\n\"protocol\": \"udp\",\n\"port\": 442,\n\"endPort\": 447\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"0.0.0.0/0\",\n\"except\": [\n\"88.0.0.0/5\",\n\"0.0.0.0/00\",\n\"107.54.244.201/2\"\n]\n},\n{\n\"cidr\": \"10.0.0.0/22\"\n},\n{\n\"cidr\": \"10.0.0.208/30\"\n}\n],\n\"ports\": [\n{\n\"port\": 65535\n},\n{\n\"protocol\": \"any\",\n\"port\": 65534\n}\n]\n},\n{\n\"to\": [\n{\n\"cidr\": \"2001:db8::3e8/126\"\n},\n{\n\"cidr\": \"e034:404:dd8c:7901:3e3b:dd4:4000:0/104\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"any\",\n\"port\": 5,\n\"endPort\": 10\n},\n{\n\"protocol\": \"tcp\",\n\"port\": 65534,\n\"endPort\": 65534\n},\n{\n\"protocol\": \"udp\",\n\"port\": 79,\n\"endPort\": 80\n}\n]\n}\n],\n\"deny\": []\n}"}],"expected":{"allow":["10.0.0.0/22:tcp:65534-65535","10.0.0.0/22:udp:65534-65535","10.0.0.0/26:icmp","10.0.0.0/26:tcp","10.0.0.0/26:udp:12","10.0.0.0/26:udp:65531-65535","10.0.0.96/27:icmp","10.0.0.96/27:tcp","10.0.0.96/27:udp:12","10.0.0.96/27:udp:65531-65535","10.0.0.128/29:icmp","10.0.0.128/29:tcp","10.0.0.128/29:udp:12","10.0.0.128/29:udp:65531-65535","10.0.0.140/30:icmp","10.0.0.140/30:tcp","10.0.0.140/30:udp:12","10.0.0.140/30:udp:65531-65535","10.0.0.144/28:icmp","10.0.0.144/28:tcp","10.0.0.144/28:udp:12","10.0.0.144/28:udp:65531-65535","10.0.0.160/27:icmp","10.0.0.160/27:tcp","10.0.0.160/27:udp:12","10.0.0.160/27:udp:65531-65535","10.0.0.192/26:icmp","10.0.0.192/26:tcp","10.0.0.192/26:udp:12","10.0.0.192/26:udp:65531-65535","10.0.1.0/24:icmp","10.0.1.0/24:tcp","10.0.1.0/24:udp:12","10.0.1.0/24:udp:65531-65535","10.0.2.0/24:icmp","10.0.2.0/24:tcp","10.0.2.0/24:udp:12","10.0.2.0/24:udp:65531-65535","10.0.3.0/26:icmp","10.0.3.0/26:tcp","10.0.3.0/26:udp:12","10.0.3.0/26:udp:65531-65535","10.0.3.64/30:icmp","10.0.3.64/30:tcp","10.0.3.64/30:udp:12","10.0.3.64/30:udp:65531-65535","10.0.3.72/29:icmp","10.0.3.72/29:tcp","10.0.3.72/29:udp:12","10.0.3.72/29:udp:65531-65535","10.0.3.80/28:icmp","10.0.3.80/28:tcp","10.0.3.80/28:udp:12","10.0.3.80/28:udp:65531-65535","10.0.3.96/27:icmp","10.0.3.96/27:tcp","10.0.3.96/27:udp:12","10.0.3.96/27:udp:65531-65535","10.0.3.128/25:icmp","10.0.3.128/25:tcp","10.0.3.128/25:udp:12","10.0.3.128/25:udp:65531-65535","39.213.0.0/16:icmp","39.213.0.0/16:tcp","39.213.0.0/16:udp:12","39.213.0.0/16:udp:65531-65535","172.162.230.0/24:icmp","172.162.230.0/24:tcp","172.162.230.0/24:udp:12","172.162.230.0/24:udp:65531-65535","::/2:tcp","::/2:udp:442-447","2001:db8::3e8/126:udp:5-10","2001:db8::3e8/126:udp:79-80","22ec:3cf2::/32:icmp","22ec:3cf2::/32:udp:12","22ec:3cf2::/32:udp:65531-65535","4000::/5:tcp","4000::/5:udp:442-447","4800::/6:tcp","4800::/6:udp:442-447","4c00::/7:tcp","4c00::/7:udp:442-447","5000::/4:tcp","5000::/4:udp:442-447","6000::/3:tcp","6000::/3:udp:442-447","8000::/4:tcp","8000::/4:udp:442-447","a000::/3:tcp","a000::/3:udp:442-447","c000::/2:tcp","c000::/2:udp:442-447","e034:404:dd8c:7901:3e3b:dd4:4000:0/104:udp:5-10","e034:404:dd8c:7901:3e3b:dd4:4000:0/104:udp:79-80"],"deny":[]}}, + {"name":"random #194","input":[{"text":"{\n\"allow\": [\n{\n\"to\": [\n{\n\"cidr\": \"18.2.73.194/31\"\n},\n{\n\"cidr\": \"2001:DB8::B4/00126\"\n},\n{\n\"cidr\": \"10.0.2.185/22\",\n\"except\": [\n\"10.0.3.208/28\"\n]\n}\n],\n\"ports\": [\n{\n\"port\": 65531,\n\"endPort\": 65531\n},\n{\n\"protocol\": \"udp\",\n\"port\": 82,\n\"endPort\": 84\n},\n{\n\"protocol\": \"tcp\",\n\"port\": 80,\n\"endPort\": 44049\n}\n]\n},\n{\n\"cidr\": \"2001:db8::2bd/121\",\n\"protocol\": \"tcp\",\n\"port\": 81,\n\"endPort\": 87\n}\n],\n\"deny\": [\n{\n\"cidr\": \"::ffff:84.255.47.0/120\",\n\"protocol\": \"any\",\n\"port\": 5,\n\"endPort\": 9\n},\n{\n\"cidr\": \"2001:db8::302/127\",\n\"protocol\": \"any\",\n\"port\": true\n}\n]\n}"}],"expected":{"error":"deny[1].port must be an integer"}}, + {"name":"random #195","input":[{"text":"{\"allow\":[{\"cidr\":\"10.0.2.216/31\",\"protocol\":\"udp\",\"port\":79,\"endPort\":80,\"to\":[]}]}"}],"expected":{"error":"allow[0] cannot mix MXC to/ports fields with legacy flat fields"}}, + {"name":"random #196","input":[{"text":"{\"allow\": [{\"to\": [{\"cidr\": \"136.192.0.0/0012\"}, {\"cidr\": \"8b0e:92c6:e873:1168:9c33:7673:9c31:ec40/127\", \"except\": []}, {\"cidr\": \"faee:0f51:7481:ae48:15fe:ece0:18.222.77.207/128\"}], \"ports\": [{\"protocol\": \"any\"}]}, {\"cidr\": \"10.0.0.0/22\", \"protocol\": \"tcp\", \"port\": 9999999999999999999999999999999999999999999999999999999999999999}, {\"cidr\": \"2001:db8::116/126\", \"except\": [\"2001:db8::117/127\", \"2001:0db8:0000:0000:0000:0000:0000:0115/000126\"], \"protocol\": \"any\"}], \"deny\": [{\"cidr\": \"10.0.1.192/26\", \"protocol\": \"udp\", \"port\": 65534, \"endPort\": 65535}, {\"to\": [{\"cidr\": \"10.0.0.101/29\", \"except\": [\"10.0.0.96/29\", \"10.0.0.96/30\", \"10.0.0.102/31\"]}], \"ports\": [{\"protocol\": \"udp\", \"port\": 65530, \"endPort\": 65534}, {\"port\": 65534}, {\"protocol\": \"any\", \"port\": 440, \"endPort\": 440}]}, {\"to\": [{\"cidr\": \"10.0.0.0/23\"}, {\"cidr\": \"10.0.0.40/24\"}], \"ports\": [{\"protocol\": \"any\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"udp\", \"port\": 65532}, {\"protocol\": \"tcp\"}]}, {\"to\": [{\"cidr\": \"2001:db8::12e/126\"}, {\"cidr\": \"2001:0db8:0000:0000:0000:0000:0.0.2.192/123\"}], \"ports\": [{\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"any\", \"port\": 78}]}, {\"cidr\": \"10.0.2.66/255.255.255.254\", \"protocol\": \"udp\", \"port\": 78}, {\"to\": [{\"cidr\": \"2001:db8::/121\"}, {\"cidr\": \"2001:db8::2ae/000119\"}, {\"cidr\": \"10.0.2.0/24\"}, {\"cidr\": \"10.0.0.0/23\"}], \"ports\": [{\"port\": 444}, {\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}, {\"protocol\": \"icmp\"}]}, {}, {\"to\": [{\"cidr\": \"2001:DB8::38E/128\", \"except\": [\"2001:db8::38e\", \"2001:db8::38e/128\", \"2001:db8::38e/128\"]}, {\"cidr\": \"10.0.3.201/32\", \"except\": [\"10.0.3.201\", \"10.0.3.201\"]}, {\"cidr\": \"2001:db8::1b9/123\"}, {\"cidr\": \"2001:db8::200/120\"}], \"ports\": [{\"protocol\": \"tcp\"}, {\"protocol\": \"tcp\", \"port\": 65532}, {\"protocol\": \"udp\"}]}]}"}],"expected":{"error":"allow[1].port must be between 1 and 65535"}}, + {"name":"random #197","input":[{"text":"{\n\"allow\": [],\n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"2001:DB8::280/121\"\n},\n{\n\"cidr\": \"26.57.158.174/0000\"\n}\n],\n\"ports\": [\n{\n\"protocol\": \"udp\",\n\"port\": 442\n},\n{\n\"port\": 9,\n\"endPort\": 14\n},\n{\n\"protocol\": \"icmp\"\n}\n]\n},\n{\n\"cidr\": \"108.147.67.94/16\",\n\"except\": [\n\"108.147.151.11/24\",\n\"108.147.192.0/255.255.192.0\"\n],\n\"protocol\": \"icmp\"\n},\n{\n\"to\": [\n{\n\"cidr\": \"10.0.0.164/30\"\n},\n{\n\"cidr\": \"176.32.156.242/32\"\n},\n{\n\"cidr\": \"10.0.3.4/255.255.255.252\",\n\"except\": [\n\"10.0.3.4/31\",\n\"10.0.3.6/32\"\n]\n}\n],\n\"ports\": [\n{\n\"protocol\": \"udp\",\n\"port\": 82,\n\"endPort\": 83\n}\n]\n}\n]\n}"}],"expected":{"allow":[],"deny":["0.0.0.0/0:icmp","0.0.0.0/0:tcp:9-14","0.0.0.0/0:udp:9-14","0.0.0.0/0:udp:442","10.0.0.164/30:udp:82-83","10.0.3.7/32:udp:82-83","176.32.156.242/32:udp:82-83","2001:db8::280/121:icmp","2001:db8::280/121:tcp:9-14","2001:db8::280/121:udp:9-14","2001:db8::280/121:udp:442"]}}, + {"name":"random #198","input":[{"text":"{\n\t\"allow\": [\n\t\t{\n\t\t\t\"cidr\": \"10.0.1.232/29\",\n\t\t\t\"except\": [\n\t\t\t\t\"10.0.1.232/29\",\n\t\t\t\t\"10.0.1.232/30\"\n\t\t\t],\n\t\t\t\"protocol\": \"udp\"\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:DB8::200/121\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.32/27\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:0326/127\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.60/27\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.232/0.0.3.255\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.240/00031\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.1.240/31\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 80,\n\t\t\t\t\t\"endPort\": 86\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"port\": 442,\n\t\t\t\t\t\"endPort\": 442\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.2.160/28\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.0.129/23\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 3,\n\t\t\t\t\t\"endPort\": 5\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"tcp\",\n\t\t\t\t\t\"port\": 65531,\n\t\t\t\t\t\"endPort\": 65532\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t\t\t\"cidr\": \"10.0.1.0/00024\",\n\t\t\t\t\t\"except\": [\n\t\t\t\t\t\t\"10.0.1.140/30\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"ports\": [\n\t\t\t\t{\n\t\t\t\t\t\"protocol\": \"udp\",\n\t\t\t\t\t\"port\": 7,\n\t\t\t\t\t\"endPort\": 10\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"cidr\": \"0ff3:68ab:9816:4ccc:c343:b540:73.151.255.80/32\",\n\t\t\t\"protocol\": \"tcp\"\n\t\t}\n\t],\n\t\"deny\": [\n\t\t{\n\t\t\t\"cidr\": \"7e7a:e248:9cbd:4ca8:a684:96ce:715f:2ef8/104\"\n\t\t},\n\t\t{\n\t\t\t\"to\": [\n\t\t\t\t{\n\t\t\t"}],"expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"random #199","input":[{"text":"{\n\"allow\": [\n{\n\"cidr\": \"2001:db8::/00118\", \n\"except\": [\n\"2001:DB8::350/125\", \n\"2001:db8::109/125\", \n\"2001:db8::380/121\"\n], \n\"protocol\": \"tcp\"\n}, \n{\n\"to\": [\n{\n\"cidr\": \"0.0.0.0/0002\"\n}, \n{\n\"cidr\": \"2001:db8::fa/127\"\n}, \n{\n\"cidr\": \"10.0.1.222/27\"\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:01c0/122\", \n\"except\": []\n}\n]\n}\n], \n\"deny\": [\n{\n\"to\": [\n{\n\"cidr\": \"10.0.1.0/25\"\n}, \n{\n\"cidr\": \"85.3.238.37/32\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:036c/127\"\n}, \n{\n\"cidr\": \"2001:db8::320/124\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"087a:2cc3:0000:0000:0000:0000:0000:0000/00032\"\n}, \n{\n\"cidr\": \"10.0.0.224/27\"\n}, \n{\n\"cidr\": \"10.0.2.0/23\", \n\"except\": [\n\"10.0.3.41/27\", \n\"10.0.2.0/23\", \n\"10.0.3.42/31\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"any\", \n\"port\": 440\n}\n]\n}, \n{\n\"ports\": [\n{\n\"protocol\": \"tcp\"\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 9\n}, \n{\n\"protocol\": \"tcp\"\n}, \n{\n\"protocol\": \"tcp\", \n\"port\": 2, \n\"endPort\": 4\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"30d5:8b1b:4b5e:5c9b:35ee:bd04:50a9:c870/1\", \n\"except\": [\n\"4c00::/7\", \n\"2D80::/09\"\n]\n}, \n{\n\"cidr\": \"ec75:6447:0000:0000:0000:0000:0000:0000/32\"\n}\n]\n}, \n{\n\"to\": [\n{\n\"cidr\": \"10.0.3.104/30\"\n}, \n{\n\"cidr\": \"2001:0db8:0000:0000:0000:0000:0000:00a0/126\", \n\"except\": [\n\"2001:0db8:0000:0000:0000:0000:0000:00a0/127\", \n\"2001:db8::a0/128\", \n\"2001:db8::a0/128\"\n]\n}\n], \n\"ports\": [\n{\n\"protocol\": \"any\"\n}, \n{\n\"protocol\": \"icmp\"\n}, \n{\n\"protocol\": \"udp\", \n\"port\": 9\n}, \n{\n\"protocol\": \"icmp\"\n}\n]\n}\n]\n}"}],"expected":{"allow":["0.0.0.0/2","2001:db8::/120:tcp","2001:db8::fa/127","2001:db8::100/125:tcp","2001:db8::110/124:tcp","2001:db8::120/123:tcp","2001:db8::140/122:tcp","2001:db8::180/121:tcp","2001:db8::1c0/122","2001:db8::200/120:tcp","2001:db8::300/122:tcp","2001:db8::340/124:tcp","2001:db8::358/125:tcp","2001:db8::360/123:tcp"],"deny":["0.0.0.0/0:tcp","0.0.0.0/0:udp:9","10.0.0.224/27","10.0.1.0/25","10.0.3.104/30","85.3.238.37/32","::/0:tcp","::/0:udp:9","::/3","2000::/5","2800::/6","2c00::/8","2d00::/9","2e00::/7","3000::/4","4000::/5","4800::/6","4e00::/7","5000::/4","6000::/3","ec75:6447::/32"]}} + ], + "files": [ + {"name":"missing","expected":{"error":"failed to read egress policy file: {path}"}}, + {"name":"directory","expected":{"error":"failed to read egress policy file: {path}"}} + ], + "paths": [ + {"path":"","posix":".","windows":"."}, + {"path":".","posix":".","windows":"."}, + {"path":"./","posix":".","windows":"."}, + {"path":"a","posix":"a","windows":"a"}, + {"path":"a/","posix":"a","windows":"a"}, + {"path":"a//b","posix":"a/b","windows":"a\\b"}, + {"path":"a/./b/.","posix":"a/b","windows":"a\\b"}, + {"path":"./a","posix":"a","windows":"a"}, + {"path":"../a","posix":"../a","windows":"..\\a"}, + {"path":"a/../b","posix":"a/../b","windows":"a\\..\\b"}, + {"path":"/","posix":"/","windows":"\\"}, + {"path":"//","posix":"//","windows":"\\\\"}, + {"path":"///","posix":"/","windows":"\\\\\\"}, + {"path":"////a","posix":"/a","windows":"\\\\\\\\a"}, + {"path":"//a/b","posix":"//a/b","windows":"\\\\a\\b\\"}, + {"path":"///a/b","posix":"/a/b","windows":"\\\\\\a\\b"}, + {"path":"a\\b","posix":"a\\b","windows":"a\\b"}, + {"path":"c:","posix":"c:","windows":"c:"}, + {"path":"c:a","posix":"c:a","windows":"c:a"}, + {"path":"c:/a","posix":"c:/a","windows":"c:\\a"}, + {"path":"c:\\a\\","posix":"c:\\a\\","windows":"c:\\a"}, + {"path":"C:/","posix":"C:","windows":"C:\\"}, + {"path":"/a","posix":"/a","windows":"\\a"}, + {"path":"\\a","posix":"\\a","windows":"\\a"}, + {"path":"\\\\server","posix":"\\\\server","windows":"\\\\server"}, + {"path":"\\\\server\\share","posix":"\\\\server\\share","windows":"\\\\server\\share\\"}, + {"path":"\\\\server\\share\\x","posix":"\\\\server\\share\\x","windows":"\\\\server\\share\\x"}, + {"path":"//server/share/x/","posix":"//server/share/x","windows":"\\\\server\\share\\x"}, + {"path":"\\\\?\\c:\\x","posix":"\\\\?\\c:\\x","windows":"\\\\?\\c:\\x"}, + {"path":"\\\\?\\UNC\\server\\share\\x","posix":"\\\\?\\UNC\\server\\share\\x","windows":"\\\\?\\UNC\\server\\share\\x"}, + {"path":"\\\\?\\unc\\server\\share","posix":"\\\\?\\unc\\server\\share","windows":"\\\\?\\unc\\server\\share\\"}, + {"path":"\\\\.\\device\\x","posix":"\\\\.\\device\\x","windows":"\\\\.\\device\\x"}, + {"path":"\\\\\\share","posix":"\\\\\\share","windows":"\\\\\\share"}, + {"path":"\\\\?.\\x\\y","posix":"\\\\?.\\x\\y","windows":"\\\\?.\\x\\y"}, + {"path":"./c:foo","posix":"c:foo","windows":".\\c:foo"}, + {"path":"c:foo/./bar","posix":"c:foo/bar","windows":"c:foo\\bar"}, + {"path":"1:x","posix":"1:x","windows":"1:x"}, + {"path":"::x","posix":"::x","windows":"::x"}, + {"path":"a:b:c","posix":"a:b:c","windows":"a:b:c"}, + {"path":"\\\\server\\share\\","posix":"\\\\server\\share\\","windows":"\\\\server\\share\\"}, + {"path":"//?/UNC/a","posix":"//?/UNC/a","windows":"\\\\?\\UNC\\a"}, + {"path":"//?/UNC/a/b","posix":"//?/UNC/a/b","windows":"\\\\?\\UNC\\a\\b\\"}, + {"path":"//?/UNC/a/b/c","posix":"//?/UNC/a/b/c","windows":"\\\\?\\UNC\\a\\b\\c"}, + {"path":"\\\\?\\","posix":"\\\\?\\","windows":"\\\\?\\"}, + {"path":"\\\\.\\","posix":"\\\\.\\","windows":"\\\\.\\"}, + {"path":"\\\\?\\x","posix":"\\\\?\\x","windows":"\\\\?\\x"}, + {"path":"a/.","posix":"a","windows":"a"}, + {"path":".\\.\\","posix":".\\.\\","windows":"."}, + {"path":"x\\.\\y","posix":"x\\.\\y","windows":"x\\y"}, + {"path":"\\\\a\\b\\c\\d\\e","posix":"\\\\a\\b\\c\\d\\e","windows":"\\\\a\\b\\c\\d\\e"}, + {"path":" a ","posix":" a ","windows":" a "}, + {"path":"a /b","posix":"a /b","windows":"a \\b"}, + {"path":"\u00e9:x","posix":"\u00e9:x","windows":"\u00e9:x"}, + {"path":"dir/\u00e9.json","posix":"dir/\u00e9.json","windows":"dir\\\u00e9.json"}, + {"path":"policy.json","posix":"policy.json","windows":"policy.json"} + ] +} diff --git a/nvx-rs/tests/fixtures/python.json b/nvx-rs/tests/fixtures/python.json new file mode 100644 index 000000000..b1df5d562 --- /dev/null +++ b/nvx-rs/tests/fixtures/python.json @@ -0,0 +1,2753 @@ +{ +"generator": "scripts/nvx_rs_fixtures.py, retired by microsoft/nvx#359", +"python": "3.14", +"unicode": "16.0.0", +"space": [[9, 13], [28, 32], [133, 133], [160, 160], [5760, 5760], [8192, 8202], [8232, 8233], [8239, 8239], [8287, 8287], [12288, 12288]], +"decimal_starts": [48, 1632, 1776, 1984, 2406, 2534, 2662, 2790, 2918, 3046, 3174, 3302, 3430, 3558, 3664, 3792, 3872, 4160, 4240, 6112, 6160, 6470, 6608, 6784, 6800, 6992, 7088, 7232, 7248, 42528, 43216, 43264, 43472, 43504, 43600, 44016, 65296, 66720, 68912, 68928, 69734, 69872, 69942, 70096, 70384, 70736, 70864, 71248, 71360, 71376, 71386, 71472, 71904, 72016, 72688, 72784, 73040, 73120, 73552, 90416, 92768, 92864, 93008, 93552, 118000, 120782, 120792, 120802, 120812, 120822, 123200, 123632, 124144, 124401, 125264, 130032], +"non_printable": [[128, 160], [173, 173], [888, 889], [896, 899], [907, 907], [909, 909], [930, 930], [1328, 1328], [1367, 1368], [1419, 1420], [1424, 1424], [1480, 1487], [1515, 1518], [1525, 1541], [1564, 1564], [1757, 1757], [1806, 1807], [1867, 1868], [1970, 1983], [2043, 2044], [2094, 2095], [2111, 2111], [2140, 2141], [2143, 2143], [2155, 2159], [2191, 2198], [2274, 2274], [2436, 2436], [2445, 2446], [2449, 2450], [2473, 2473], [2481, 2481], [2483, 2485], [2490, 2491], [2501, 2502], [2505, 2506], [2511, 2518], [2520, 2523], [2526, 2526], [2532, 2533], [2559, 2560], [2564, 2564], [2571, 2574], [2577, 2578], [2601, 2601], [2609, 2609], [2612, 2612], [2615, 2615], [2618, 2619], [2621, 2621], [2627, 2630], [2633, 2634], [2638, 2640], [2642, 2648], [2653, 2653], [2655, 2661], [2679, 2688], [2692, 2692], [2702, 2702], [2706, 2706], [2729, 2729], [2737, 2737], [2740, 2740], [2746, 2747], [2758, 2758], [2762, 2762], [2766, 2767], [2769, 2783], [2788, 2789], [2802, 2808], [2816, 2816], [2820, 2820], [2829, 2830], [2833, 2834], [2857, 2857], [2865, 2865], [2868, 2868], [2874, 2875], [2885, 2886], [2889, 2890], [2894, 2900], [2904, 2907], [2910, 2910], [2916, 2917], [2936, 2945], [2948, 2948], [2955, 2957], [2961, 2961], [2966, 2968], [2971, 2971], [2973, 2973], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3005], [3011, 3013], [3017, 3017], [3022, 3023], [3025, 3030], [3032, 3045], [3067, 3071], [3085, 3085], [3089, 3089], [3113, 3113], [3130, 3131], [3141, 3141], [3145, 3145], [3150, 3156], [3159, 3159], [3163, 3164], [3166, 3167], [3172, 3173], [3184, 3190], [3213, 3213], [3217, 3217], [3241, 3241], [3252, 3252], [3258, 3259], [3269, 3269], [3273, 3273], [3278, 3284], [3287, 3292], [3295, 3295], [3300, 3301], [3312, 3312], [3316, 3327], [3341, 3341], [3345, 3345], [3397, 3397], [3401, 3401], [3408, 3411], [3428, 3429], [3456, 3456], [3460, 3460], [3479, 3481], [3506, 3506], [3516, 3516], [3518, 3519], [3527, 3529], [3531, 3534], [3541, 3541], [3543, 3543], [3552, 3557], [3568, 3569], [3573, 3584], [3643, 3646], [3676, 3712], [3715, 3715], [3717, 3717], [3723, 3723], [3748, 3748], [3750, 3750], [3774, 3775], [3781, 3781], [3783, 3783], [3791, 3791], [3802, 3803], [3808, 3839], [3912, 3912], [3949, 3952], [3992, 3992], [4029, 4029], [4045, 4045], [4059, 4095], [4294, 4294], [4296, 4300], [4302, 4303], [4681, 4681], [4686, 4687], [4695, 4695], [4697, 4697], [4702, 4703], [4745, 4745], [4750, 4751], [4785, 4785], [4790, 4791], [4799, 4799], [4801, 4801], [4806, 4807], [4823, 4823], [4881, 4881], [4886, 4887], [4955, 4956], [4989, 4991], [5018, 5023], [5110, 5111], [5118, 5119], [5760, 5760], [5789, 5791], [5881, 5887], [5910, 5918], [5943, 5951], [5972, 5983], [5997, 5997], [6001, 6001], [6004, 6015], [6110, 6111], [6122, 6127], [6138, 6143], [6158, 6158], [6170, 6175], [6265, 6271], [6315, 6319], [6390, 6399], [6431, 6431], [6444, 6447], [6460, 6463], [6465, 6467], [6510, 6511], [6517, 6527], [6572, 6575], [6602, 6607], [6619, 6621], [6684, 6685], [6751, 6751], [6781, 6782], [6794, 6799], [6810, 6815], [6830, 6831], [6863, 6911], [6989, 6989], [7156, 7163], [7224, 7226], [7242, 7244], [7307, 7311], [7355, 7356], [7368, 7375], [7419, 7423], [7958, 7959], [7966, 7967], [8006, 8007], [8014, 8015], [8024, 8024], [8026, 8026], [8028, 8028], [8030, 8030], [8062, 8063], [8117, 8117], [8133, 8133], [8148, 8149], [8156, 8156], [8176, 8177], [8181, 8181], [8191, 8207], [8232, 8239], [8287, 8303], [8306, 8307], [8335, 8335], [8349, 8351], [8385, 8399], [8433, 8447], [8588, 8591], [9258, 9279], [9291, 9311], [11124, 11125], [11158, 11158], [11508, 11512], [11558, 11558], [11560, 11564], [11566, 11567], [11624, 11630], [11633, 11646], [11671, 11679], [11687, 11687], [11695, 11695], [11703, 11703], [11711, 11711], [11719, 11719], [11727, 11727], [11735, 11735], [11743, 11743], [11870, 11903], [11930, 11930], [12020, 12031], [12246, 12271], [12288, 12288], [12352, 12352], [12439, 12440], [12544, 12548], [12592, 12592], [12687, 12687], [12774, 12782], [12831, 12831], [42125, 42127], [42183, 42191], [42540, 42559], [42744, 42751], [42958, 42959], [42962, 42962], [42964, 42964], [42973, 42993], [43053, 43055], [43066, 43071], [43128, 43135], [43206, 43213], [43226, 43231], [43348, 43358], [43389, 43391], [43470, 43470], [43482, 43485], [43519, 43519], [43575, 43583], [43598, 43599], [43610, 43611], [43715, 43738], [43767, 43776], [43783, 43784], [43791, 43792], [43799, 43807], [43815, 43815], [43823, 43823], [43884, 43887], [44014, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 63743], [64110, 64111], [64218, 64255], [64263, 64274], [64280, 64284], [64311, 64311], [64317, 64317], [64319, 64319], [64322, 64322], [64325, 64325], [64451, 64466], [64912, 64913], [64968, 64974], [64976, 65007], [65050, 65055], [65107, 65107], [65127, 65127], [65132, 65135], [65141, 65141], [65277, 65280], [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497], [65501, 65503], [65511, 65511], [65519, 65531], [65534, 65535], [65548, 65548], [65575, 65575], [65595, 65595], [65598, 65598], [65614, 65615], [65630, 65663], [65787, 65791], [65795, 65798], [65844, 65846], [65935, 65935], [65949, 65951], [65953, 65999], [66046, 66175], [66205, 66207], [66257, 66271], [66300, 66303], [66340, 66348], [66379, 66383], [66427, 66431], [66462, 66462], [66500, 66503], [66518, 66559], [66718, 66719], [66730, 66735], [66772, 66775], [66812, 66815], [66856, 66863], [66916, 66926], [66939, 66939], [66955, 66955], [66963, 66963], [66966, 66966], [66978, 66978], [66994, 66994], [67002, 67002], [67005, 67007], [67060, 67071], [67383, 67391], [67414, 67423], [67432, 67455], [67462, 67462], [67505, 67505], [67515, 67583], [67590, 67591], [67593, 67593], [67638, 67638], [67641, 67643], [67645, 67646], [67670, 67670], [67743, 67750], [67760, 67807], [67827, 67827], [67830, 67834], [67868, 67870], [67898, 67902], [67904, 67967], [68024, 68027], [68048, 68049], [68100, 68100], [68103, 68107], [68116, 68116], [68120, 68120], [68150, 68151], [68155, 68158], [68169, 68175], [68185, 68191], [68256, 68287], [68327, 68330], [68343, 68351], [68406, 68408], [68438, 68439], [68467, 68471], [68498, 68504], [68509, 68520], [68528, 68607], [68681, 68735], [68787, 68799], [68851, 68857], [68904, 68911], [68922, 68927], [68966, 68968], [68998, 69005], [69008, 69215], [69247, 69247], [69290, 69290], [69294, 69295], [69298, 69313], [69317, 69371], [69416, 69423], [69466, 69487], [69514, 69551], [69580, 69599], [69623, 69631], [69710, 69713], [69750, 69758], [69821, 69821], [69827, 69839], [69865, 69871], [69882, 69887], [69941, 69941], [69960, 69967], [70007, 70015], [70112, 70112], [70133, 70143], [70162, 70162], [70210, 70271], [70279, 70279], [70281, 70281], [70286, 70286], [70302, 70302], [70314, 70319], [70379, 70383], [70394, 70399], [70404, 70404], [70413, 70414], [70417, 70418], [70441, 70441], [70449, 70449], [70452, 70452], [70458, 70458], [70469, 70470], [70473, 70474], [70478, 70479], [70481, 70486], [70488, 70492], [70500, 70501], [70509, 70511], [70517, 70527], [70538, 70538], [70540, 70541], [70543, 70543], [70582, 70582], [70593, 70593], [70595, 70596], [70598, 70598], [70603, 70603], [70614, 70614], [70617, 70624], [70627, 70655], [70748, 70748], [70754, 70783], [70856, 70863], [70874, 71039], [71094, 71095], [71134, 71167], [71237, 71247], [71258, 71263], [71277, 71295], [71354, 71359], [71370, 71375], [71396, 71423], [71451, 71452], [71468, 71471], [71495, 71679], [71740, 71839], [71923, 71934], [71943, 71944], [71946, 71947], [71956, 71956], [71959, 71959], [71990, 71990], [71993, 71994], [72007, 72015], [72026, 72095], [72104, 72105], [72152, 72153], [72165, 72191], [72264, 72271], [72355, 72367], [72441, 72447], [72458, 72639], [72674, 72687], [72698, 72703], [72713, 72713], [72759, 72759], [72774, 72783], [72813, 72815], [72848, 72849], [72872, 72872], [72887, 72959], [72967, 72967], [72970, 72970], [73015, 73017], [73019, 73019], [73022, 73022], [73032, 73039], [73050, 73055], [73062, 73062], [73065, 73065], [73103, 73103], [73106, 73106], [73113, 73119], [73130, 73439], [73465, 73471], [73489, 73489], [73531, 73533], [73563, 73647], [73649, 73663], [73714, 73726], [74650, 74751], [74863, 74863], [74869, 74879], [75076, 77711], [77811, 77823], [78896, 78911], [78934, 78943], [82939, 82943], [83527, 90367], [90426, 92159], [92729, 92735], [92767, 92767], [92778, 92781], [92863, 92863], [92874, 92879], [92910, 92911], [92918, 92927], [92998, 93007], [93018, 93018], [93026, 93026], [93048, 93052], [93072, 93503], [93562, 93759], [93851, 93951], [94027, 94030], [94088, 94094], [94112, 94175], [94181, 94191], [94194, 94207], [100344, 100351], [101590, 101630], [101641, 110575], [110580, 110580], [110588, 110588], [110591, 110591], [110883, 110897], [110899, 110927], [110931, 110932], [110934, 110947], [110952, 110959], [111356, 113663], [113771, 113775], [113789, 113791], [113801, 113807], [113818, 113819], [113824, 117759], [118010, 118015], [118452, 118527], [118574, 118575], [118599, 118607], [118724, 118783], [119030, 119039], [119079, 119080], [119155, 119162], [119275, 119295], [119366, 119487], [119508, 119519], [119540, 119551], [119639, 119647], [119673, 119807], [119893, 119893], [119965, 119965], [119968, 119969], [119971, 119972], [119975, 119976], [119981, 119981], [119994, 119994], [119996, 119996], [120004, 120004], [120070, 120070], [120075, 120076], [120085, 120085], [120093, 120093], [120122, 120122], [120127, 120127], [120133, 120133], [120135, 120137], [120145, 120145], [120486, 120487], [120780, 120781], [121484, 121498], [121504, 121504], [121520, 122623], [122655, 122660], [122667, 122879], [122887, 122887], [122905, 122906], [122914, 122914], [122917, 122917], [122923, 122927], [122990, 123022], [123024, 123135], [123181, 123183], [123198, 123199], [123210, 123213], [123216, 123535], [123567, 123583], [123642, 123646], [123648, 124111], [124154, 124367], [124411, 124414], [124416, 124895], [124903, 124903], [124908, 124908], [124911, 124911], [124927, 124927], [125125, 125126], [125143, 125183], [125260, 125263], [125274, 125277], [125280, 126064], [126133, 126208], [126270, 126463], [126468, 126468], [126496, 126496], [126499, 126499], [126501, 126502], [126504, 126504], [126515, 126515], [126520, 126520], [126522, 126522], [126524, 126529], [126531, 126534], [126536, 126536], [126538, 126538], [126540, 126540], [126544, 126544], [126547, 126547], [126549, 126550], [126552, 126552], [126554, 126554], [126556, 126556], [126558, 126558], [126560, 126560], [126563, 126563], [126565, 126566], [126571, 126571], [126579, 126579], [126584, 126584], [126589, 126589], [126591, 126591], [126602, 126602], [126620, 126624], [126628, 126628], [126634, 126634], [126652, 126703], [126706, 126975], [127020, 127023], [127124, 127135], [127151, 127152], [127168, 127168], [127184, 127184], [127222, 127231], [127406, 127461], [127491, 127503], [127548, 127551], [127561, 127567], [127570, 127583], [127590, 127743], [128728, 128731], [128749, 128751], [128765, 128767], [128887, 128890], [128986, 128991], [129004, 129007], [129009, 129023], [129036, 129039], [129096, 129103], [129114, 129119], [129160, 129167], [129198, 129199], [129212, 129215], [129218, 129279], [129620, 129631], [129646, 129647], [129661, 129663], [129674, 129678], [129735, 129741], [129757, 129758], [129770, 129775], [129785, 129791], [129939, 129939], [130042, 131071], [173792, 173823], [177978, 177983], [178206, 178207], [183970, 183983], [191457, 191471], [192094, 194559], [195102, 196607], [201547, 201551], [205744, 917759], [918000, 1114111]], +"strings": [ +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":" ","repr":"' '","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ' '"}, +{"text":"007","repr":"'007'","split":["007"],"strip":"007","uuid":null,"int":"7"}, +{"text":"+5","repr":"'+5'","split":["+5"],"strip":"+5","uuid":null,"int":"5"}, +{"text":"-5","repr":"'-5'","split":["-5"],"strip":"-5","uuid":null,"int":"-5"}, +{"text":" 5 ","repr":"' 5 '","split":["5"],"strip":"5","uuid":null,"int":"5"}, +{"text":"5_000","repr":"'5_000'","split":["5_000"],"strip":"5_000","uuid":null,"int":"5000"}, +{"text":"_5","repr":"'_5'","split":["_5"],"strip":"_5","uuid":null,"int_error":"invalid literal for int() with base 10: '_5'"}, +{"text":"5_","repr":"'5_'","split":["5_"],"strip":"5_","uuid":null,"int_error":"invalid literal for int() with base 10: '5_'"}, +{"text":"5__0","repr":"'5__0'","split":["5__0"],"strip":"5__0","uuid":null,"int_error":"invalid literal for int() with base 10: '5__0'"}, +{"text":"0x5","repr":"'0x5'","split":["0x5"],"strip":"0x5","uuid":null,"int_error":"invalid literal for int() with base 10: '0x5'"}, +{"text":"1e3","repr":"'1e3'","split":["1e3"],"strip":"1e3","uuid":null,"int_error":"invalid literal for int() with base 10: '1e3'"}, +{"text":"\u20035\u3000","repr":"'\\u20035\\u3000'","split":["5"],"strip":"5","uuid":null,"int":"5"}, +{"text":"\uff15","repr":"'\uff15'","split":["\uff15"],"strip":"\uff15","uuid":null,"int":"5"}, +{"text":"\u0664\u0662","repr":"'\u0664\u0662'","split":["\u0664\u0662"],"strip":"\u0664\u0662","uuid":null,"int":"42"}, +{"text":"\u001c42\u001f","repr":"'\\x1c42\\x1f'","split":["42"],"strip":"42","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1c42\\x1f'"}, +{"text":"1\u00002","repr":"'1\\x002'","split":["1\u00002"],"strip":"1\u00002","uuid":null,"int_error":"invalid literal for int() with base 10: '1\\x002'"}, +{"text":"it's","repr":"\"it's\"","split":["it's"],"strip":"it's","uuid":null,"int_error":"invalid literal for int() with base 10: \"it's\""}, +{"text":"say \"hi\"","repr":"'say \"hi\"'","split":["say","\"hi\""],"strip":"say \"hi\"","uuid":null,"int_error":"invalid literal for int() with base 10: 'say \"hi\"'"}, +{"text":"both ' and \"","repr":"'both \\' and \"'","split":["both","'","and","\""],"strip":"both ' and \"","uuid":null,"int_error":"invalid literal for int() with base 10: 'both \\' and \"'"}, +{"text":"a\\b","repr":"'a\\\\b'","split":["a\\b"],"strip":"a\\b","uuid":null,"int_error":"invalid literal for int() with base 10: 'a\\\\b'"}, +{"text":"\t\n\r\u0000\u007f\u001b","repr":"'\\t\\n\\r\\x00\\x7f\\x1b'","split":["\u0000\u007f\u001b"],"strip":"\u0000\u007f\u001b","uuid":null,"int_error":"invalid literal for int() with base 10: '\\t\\n\\r\\x00\\x7f\\x1b'"}, +{"text":"\u00e9\u00a0\u00ad","repr":"'\u00e9\\xa0\\xad'","split":["\u00e9","\u00ad"],"strip":"\u00e9\u00a0\u00ad","uuid":null,"int_error":"invalid literal for int() with base 10: '\u00e9\\xa0\\xad'"}, +{"text":"\u2028\u200b\u0378","repr":"'\\u2028\\u200b\\u0378'","split":["\u200b\u0378"],"strip":"\u200b\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2028\\u200b\\u0378'"}, +{"text":"\ud83d\ude00\udb40\udc01","repr":"'\ud83d\ude00\\U000e0001'","split":["\ud83d\ude00\udb40\udc01"],"strip":"\ud83d\ude00\udb40\udc01","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud83d\ude00\\U000e0001'"}, +{"text":"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","repr":"'1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'","split":["1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"],"strip":"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","uuid":null,"int":"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"}, +{"text":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001","repr":"'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001'","split":["00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"],"strip":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001","uuid":null,"int_error":"Exceeds the limit (4300 digits) for integer string conversion: value has 4301 digits; use sys.set_int_max_str_digits() to increase the limit"}, +{"text":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","repr":"'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'","split":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"strip":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","uuid":null,"int_error":"invalid literal for int() with base 10: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"uuuuid:id:12345678123456781234567812345678","repr":"'uuuuid:id:12345678123456781234567812345678'","split":["uuuuid:id:12345678123456781234567812345678"],"strip":"uuuuid:id:12345678123456781234567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'uuuuid:id:12345678123456781234567812345678'"}, +{"text":"{urn:uuid:12345678-1234-5678-1234-567812345678}","repr":"'{urn:uuid:12345678-1234-5678-1234-567812345678}'","split":["{urn:uuid:12345678-1234-5678-1234-567812345678}"],"strip":"{urn:uuid:12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{urn:uuid:12345678-1234-5678-1234-567812345678}'"}, +{"text":"0x111111111111111111111111111111","repr":"'0x111111111111111111111111111111'","split":["0x111111111111111111111111111111"],"strip":"0x111111111111111111111111111111","uuid":"00111111-1111-1111-1111-111111111111","int_error":"invalid literal for int() with base 10: '0x111111111111111111111111111111'"}, +{"text":"+1111111111111111111111111111111","repr":"'+1111111111111111111111111111111'","split":["+1111111111111111111111111111111"],"strip":"+1111111111111111111111111111111","uuid":"01111111-1111-1111-1111-111111111111","int":"1111111111111111111111111111111"}, +{"text":"-1111111111111111111111111111111","repr":"'-1111111111111111111111111111111'","split":["-1111111111111111111111111111111"],"strip":"-1111111111111111111111111111111","uuid":null,"int":"-1111111111111111111111111111111"}, +{"text":" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","repr":"' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'","split":["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],"strip":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","uuid":"0aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","int_error":"invalid literal for int() with base 10: ' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'"}, +{"text":"1_111111111111111111111111111111","repr":"'1_111111111111111111111111111111'","split":["1_111111111111111111111111111111"],"strip":"1_111111111111111111111111111111","uuid":"01111111-1111-1111-1111-111111111111","int":"1111111111111111111111111111111"}, +{"text":"\u06610000000000000000000000000000000","repr":"'\u06610000000000000000000000000000000'","split":["\u06610000000000000000000000000000000"],"strip":"\u06610000000000000000000000000000000","uuid":"10000000-0000-0000-0000-000000000000","int":"10000000000000000000000000000000"}, +{"text":"0b111111111111111111111111111111","repr":"'0b111111111111111111111111111111'","split":["0b111111111111111111111111111111"],"strip":"0b111111111111111111111111111111","uuid":"0b111111-1111-1111-1111-111111111111","int_error":"invalid literal for int() with base 10: '0b111111111111111111111111111111'"}, +{"text":"\ufeff","repr":"'\\ufeff'","split":["\ufeff"],"strip":"\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '\\ufeff'"}, +{"text":"}\udb40\udc01{\u00a0\u007f1","repr":"'}\\U000e0001{\\xa0\\x7f1'","split":["}\udb40\udc01{","\u007f1"],"strip":"}\udb40\udc01{\u00a0\u007f1","uuid":null,"int_error":"invalid literal for int() with base 10: '}\\U000e0001{\\xa0\\x7f1'"}, +{"text":"Fuuid:x\t7=0x9","repr":"'Fuuid:x\\t7=0x9'","split":["Fuuid:x","7=0x9"],"strip":"Fuuid:x\t7=0x9","uuid":null,"int_error":"invalid literal for int() with base 10: 'Fuuid:x\\t7=0x9'"}, +{"text":"\u2028+9}\"urn:\n\ud835\udfce\u001c\t\\","repr":"'\\u2028+9}\"urn:\\n\ud835\udfce\\x1c\\t\\\\'","split":["+9}\"urn:","\ud835\udfce","\\"],"strip":"+9}\"urn:\n\ud835\udfce\u001c\t\\","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2028+9}\"urn:\\n\ud835\udfce\\x1c\\t\\\\'"}, +{"text":",uuid:\u00ad\uff11uuid:F\ud835\udfce0'\n\u30000x","repr":"\",uuid:\\xad\uff11uuid:F\ud835\udfce0'\\n\\u30000x\"","split":[",uuid:\u00ad\uff11uuid:F\ud835\udfce0'","0x"],"strip":",uuid:\u00ad\uff11uuid:F\ud835\udfce0'\n\u30000x","uuid":null,"int_error":"invalid literal for int() with base 10: \",uuid:\\xad\uff11uuid:F\ud835\udfce0'\\n\\u30000x\""}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"urn:0uuid:{x","repr":"'urn:0uuid:{x'","split":["urn:0uuid:{x"],"strip":"urn:0uuid:{x","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:0uuid:{x'"}, +{"text":"_\u03780 ","repr":"'_\\u03780 '","split":["_\u03780"],"strip":"_\u03780","uuid":null,"int_error":"invalid literal for int() with base 10: '_\\u03780 '"}, +{"text":"\u0085\u00e90x\u00ad\u00a0,=","repr":"'\\x85\u00e90x\\xad\\xa0,='","split":["\u00e90x\u00ad",",="],"strip":"\u00e90x\u00ad\u00a0,=","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x85\u00e90x\\xad\\xa0,='"}, +{"text":"1\ud835\udfce\udb40\udc01\u0663\u00a0","repr":"'1\ud835\udfce\\U000e0001\u0663\\xa0'","split":["1\ud835\udfce\udb40\udc01\u0663"],"strip":"1\ud835\udfce\udb40\udc01\u0663","uuid":null,"int_error":"invalid literal for int() with base 10: '1\ud835\udfce\\U000e0001\u0663\\xa0'"}, +{"text":"\ufeff0x\nX\u200b","repr":"'\\ufeff0x\\nX\\u200b'","split":["\ufeff0x","X\u200b"],"strip":"\ufeff0x\nX\u200b","uuid":null,"int_error":"invalid literal for int() with base 10: '\\ufeff0x\\nX\\u200b'"}, +{"text":"\u3000\u2028\\\\\ufeff","repr":"'\\u3000\\u2028\\\\\\\\\\ufeff'","split":["\\\\\ufeff"],"strip":"\\\\\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000\\u2028\\\\\\\\\\ufeff'"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"'\u00007-x0xF\udb40\udc01\uff11","repr":"\"'\\x007-x0xF\\U000e0001\uff11\"","split":["'\u00007-x0xF\udb40\udc01\uff11"],"strip":"'\u00007-x0xF\udb40\udc01\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: \"'\\x007-x0xF\\U000e0001\uff11\""}, +{"text":"a=\ud835\udfce\u000b\u00e9urn:9\ufeff}\u0378","repr":"'a=\ud835\udfce\\x0b\u00e9urn:9\\ufeff}\\u0378'","split":["a=\ud835\udfce","\u00e9urn:9\ufeff}\u0378"],"strip":"a=\ud835\udfce\u000b\u00e9urn:9\ufeff}\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: 'a=\ud835\udfce\\x0b\u00e9urn:9\\ufeff}\\u0378'"}, +{"text":"\u0085\u0663-1\uff11\udb40\udc01\u000b1\\X","repr":"'\\x85\u0663-1\uff11\\U000e0001\\x0b1\\\\X'","split":["\u0663-1\uff11\udb40\udc01","1\\X"],"strip":"\u0663-1\uff11\udb40\udc01\u000b1\\X","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x85\u0663-1\uff11\\U000e0001\\x0b1\\\\X'"}, +{"text":"\"{\t\u00e9\u001c\u00e9","repr":"'\"{\\t\u00e9\\x1c\u00e9'","split":["\"{","\u00e9","\u00e9"],"strip":"\"{\t\u00e9\u001c\u00e9","uuid":null,"int_error":"invalid literal for int() with base 10: '\"{\\t\u00e9\\x1c\u00e9'"}, +{"text":"\u00e9}\u007f\u00a0\ud83d\ude00 \t\u000b","repr":"'\u00e9}\\x7f\\xa0\ud83d\ude00 \\t\\x0b'","split":["\u00e9}\u007f","\ud83d\ude00"],"strip":"\u00e9}\u007f\u00a0\ud83d\ude00","uuid":null,"int_error":"invalid literal for int() with base 10: '\u00e9}\\x7f\\xa0\ud83d\ude00 \\t\\x0b'"}, +{"text":"\uff11\u0085\u2003urn:x","repr":"'\uff11\\x85\\u2003urn:x'","split":["\uff11","urn:x"],"strip":"\uff11\u0085\u2003urn:x","uuid":null,"int_error":"invalid literal for int() with base 10: '\uff11\\x85\\u2003urn:x'"}, +{"text":"\u001c\ud83d\ude00=\u2028}0X\u200burn:a\u00e91","repr":"'\\x1c\ud83d\ude00=\\u2028}0X\\u200burn:a\u00e91'","split":["\ud83d\ude00=","}0X\u200burn:a\u00e91"],"strip":"\ud83d\ude00=\u2028}0X\u200burn:a\u00e91","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1c\ud83d\ude00=\\u2028}0X\\u200burn:a\u00e91'"}, +{"text":"uuid:-0x\u0378","repr":"'uuid:-0x\\u0378'","split":["uuid:-0x\u0378"],"strip":"uuid:-0x\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: 'uuid:-0x\\u0378'"}, +{"text":"F'9\u000b0\u0378+\u001c\u007f-","repr":"\"F'9\\x0b0\\u0378+\\x1c\\x7f-\"","split":["F'9","0\u0378+","\u007f-"],"strip":"F'9\u000b0\u0378+\u001c\u007f-","uuid":null,"int_error":"invalid literal for int() with base 10: \"F'9\\x0b0\\u0378+\\x1c\\x7f-\""}, +{"text":"F-\u2003\ud835\udfce\u0378:}Xa","repr":"'F-\\u2003\ud835\udfce\\u0378:}Xa'","split":["F-","\ud835\udfce\u0378:}Xa"],"strip":"F-\u2003\ud835\udfce\u0378:}Xa","uuid":null,"int_error":"invalid literal for int() with base 10: 'F-\\u2003\ud835\udfce\\u0378:}Xa'"}, +{"text":"{\u2028\ud835\udfce7=","repr":"'{\\u2028\ud835\udfce7='","split":["{","\ud835\udfce7="],"strip":"{\u2028\ud835\udfce7=","uuid":null,"int_error":"invalid literal for int() with base 10: '{\\u2028\ud835\udfce7='"}, +{"text":"{\ud835\udfce\ud83d\ude00uuid:x\u2028","repr":"'{\ud835\udfce\ud83d\ude00uuid:x\\u2028'","split":["{\ud835\udfce\ud83d\ude00uuid:x"],"strip":"{\ud835\udfce\ud83d\ude00uuid:x","uuid":null,"int_error":"invalid literal for int() with base 10: '{\ud835\udfce\ud83d\ude00uuid:x\\u2028'"}, +{"text":"7X\u00ad\u001f9\ufeff\u000b\ufeff","repr":"'7X\\xad\\x1f9\\ufeff\\x0b\\ufeff'","split":["7X\u00ad","9\ufeff","\ufeff"],"strip":"7X\u00ad\u001f9\ufeff\u000b\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '7X\\xad\\x1f9\\ufeff\\x0b\\ufeff'"}, +{"text":"\n\u2003=X","repr":"'\\n\\u2003=X'","split":["=X"],"strip":"=X","uuid":null,"int_error":"invalid literal for int() with base 10: '\\n\\u2003=X'"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"\u007f\n\u200b\u007f\u2003a_\u0085\u0663\u0663a","repr":"'\\x7f\\n\\u200b\\x7f\\u2003a_\\x85\u0663\u0663a'","split":["\u007f","\u200b\u007f","a_","\u0663\u0663a"],"strip":"\u007f\n\u200b\u007f\u2003a_\u0085\u0663\u0663a","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x7f\\n\\u200b\\x7f\\u2003a_\\x85\u0663\u0663a'"}, +{"text":"\u3000\u000bF","repr":"'\\u3000\\x0bF'","split":["F"],"strip":"F","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000\\x0bF'"}, +{"text":"}=","repr":"'}='","split":["}="],"strip":"}=","uuid":null,"int_error":"invalid literal for int() with base 10: '}='"}, +{"text":"a\udb40\udc01uuid:\u0085 \ud83d\ude00\\{","repr":"'a\\U000e0001uuid:\\x85 \ud83d\ude00\\\\{'","split":["a\udb40\udc01uuid:","\ud83d\ude00\\{"],"strip":"a\udb40\udc01uuid:\u0085 \ud83d\ude00\\{","uuid":null,"int_error":"invalid literal for int() with base 10: 'a\\U000e0001uuid:\\x85 \ud83d\ude00\\\\{'"}, +{"text":"\\}=","repr":"'\\\\}='","split":["\\}="],"strip":"\\}=","uuid":null,"int_error":"invalid literal for int() with base 10: '\\\\}='"}, +{"text":"\u0378:0x,\u00a0\u3000=","repr":"'\\u0378:0x,\\xa0\\u3000='","split":["\u0378:0x,","="],"strip":"\u0378:0x,\u00a0\u3000=","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u0378:0x,\\xa0\\u3000='"}, +{"text":",x:\u0000\udb40\udc019'}\u20289","repr":"\",x:\\x00\\U000e00019'}\\u20289\"","split":[",x:\u0000\udb40\udc019'}","9"],"strip":",x:\u0000\udb40\udc019'}\u20289","uuid":null,"int_error":"invalid literal for int() with base 10: \",x:\\x00\\U000e00019'}\\u20289\""}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"x\n\u001f","repr":"'x\\n\\x1f'","split":["x"],"strip":"x","uuid":null,"int_error":"invalid literal for int() with base 10: 'x\\n\\x1f'"}, +{"text":":\u200b=\u2003}\u3000X:\n\u0085","repr":"':\\u200b=\\u2003}\\u3000X:\\n\\x85'","split":[":\u200b=","}","X:"],"strip":":\u200b=\u2003}\u3000X:","uuid":null,"int_error":"invalid literal for int() with base 10: ':\\u200b=\\u2003}\\u3000X:\\n\\x85'"}, +{"text":"-0x\ufeff\u2028","repr":"'-0x\\ufeff\\u2028'","split":["-0x\ufeff"],"strip":"-0x\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '-0x\\ufeff\\u2028'"}, +{"text":"\ud835\udfce\u2028}\t","repr":"'\ud835\udfce\\u2028}\\t'","split":["\ud835\udfce","}"],"strip":"\ud835\udfce\u2028}","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud835\udfce\\u2028}\\t'"}, +{"text":"\ud83d\ude00,\ud83d\ude00 a,\"7\\9+\u0378","repr":"'\ud83d\ude00,\ud83d\ude00 a,\"7\\\\9+\\u0378'","split":["\ud83d\ude00,\ud83d\ude00","a,\"7\\9+\u0378"],"strip":"\ud83d\ude00,\ud83d\ude00 a,\"7\\9+\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud83d\ude00,\ud83d\ude00 a,\"7\\\\9+\\u0378'"}, +{"text":"9\uff11\u200b","repr":"'9\uff11\\u200b'","split":["9\uff11\u200b"],"strip":"9\uff11\u200b","uuid":null,"int_error":"invalid literal for int() with base 10: '9\uff11\\u200b'"}, +{"text":",","repr":"','","split":[","],"strip":",","uuid":null,"int_error":"invalid literal for int() with base 10: ','"}, +{"text":"0\u00a0F{\u2028uuid:\u0663\\\u00ad","repr":"'0\\xa0F{\\u2028uuid:\u0663\\\\\\xad'","split":["0","F{","uuid:\u0663\\\u00ad"],"strip":"0\u00a0F{\u2028uuid:\u0663\\\u00ad","uuid":null,"int_error":"invalid literal for int() with base 10: '0\\xa0F{\\u2028uuid:\u0663\\\\\\xad'"}, +{"text":"\u001c\u00a0{'\ud83d\ude00","repr":"\"\\x1c\\xa0{'\ud83d\ude00\"","split":["{'\ud83d\ude00"],"strip":"{'\ud83d\ude00","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\x1c\\xa0{'\ud83d\ude00\""}, +{"text":"\u001c\u3000=\"X\u3000\u0378\t\n}\udb40\udc01","repr":"'\\x1c\\u3000=\"X\\u3000\\u0378\\t\\n}\\U000e0001'","split":["=\"X","\u0378","}\udb40\udc01"],"strip":"=\"X\u3000\u0378\t\n}\udb40\udc01","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1c\\u3000=\"X\\u3000\\u0378\\t\\n}\\U000e0001'"}, +{"text":"\u0085,\u0085 \u007f\ud83d\ude00\u2003Xuuid:',","repr":"\"\\x85,\\x85 \\x7f\ud83d\ude00\\u2003Xuuid:',\"","split":[",","\u007f\ud83d\ude00","Xuuid:',"],"strip":",\u0085 \u007f\ud83d\ude00\u2003Xuuid:',","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\x85,\\x85 \\x7f\ud83d\ude00\\u2003Xuuid:',\""}, +{"text":"urn:\u200burn:uuid:\u00ad}'-","repr":"\"urn:\\u200burn:uuid:\\xad}'-\"","split":["urn:\u200burn:uuid:\u00ad}'-"],"strip":"urn:\u200burn:uuid:\u00ad}'-","uuid":null,"int_error":"invalid literal for int() with base 10: \"urn:\\u200burn:uuid:\\xad}'-\""}, +{"text":"{F\u007f\uff11\u0378","repr":"'{F\\x7f\uff11\\u0378'","split":["{F\u007f\uff11\u0378"],"strip":"{F\u007f\uff11\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: '{F\\x7f\uff11\\u0378'"}, +{"text":"\nF\n\u200b\u001f\u0000:\n\u0085\u20280x\u001c","repr":"'\\nF\\n\\u200b\\x1f\\x00:\\n\\x85\\u20280x\\x1c'","split":["F","\u200b","\u0000:","0x"],"strip":"F\n\u200b\u001f\u0000:\n\u0085\u20280x","uuid":null,"int_error":"invalid literal for int() with base 10: '\\nF\\n\\u200b\\x1f\\x00:\\n\\x85\\u20280x\\x1c'"}, +{"text":"xa\u0663\u2028-{\u0663\u000b90x+","repr":"'xa\u0663\\u2028-{\u0663\\x0b90x+'","split":["xa\u0663","-{\u0663","90x+"],"strip":"xa\u0663\u2028-{\u0663\u000b90x+","uuid":null,"int_error":"invalid literal for int() with base 10: 'xa\u0663\\u2028-{\u0663\\x0b90x+'"}, +{"text":"\u0085\u00a0urn:{=\udb40\udc01\u2028\u200b\uff11\udb40\udc01uuid:","repr":"'\\x85\\xa0urn:{=\\U000e0001\\u2028\\u200b\uff11\\U000e0001uuid:'","split":["urn:{=\udb40\udc01","\u200b\uff11\udb40\udc01uuid:"],"strip":"urn:{=\udb40\udc01\u2028\u200b\uff11\udb40\udc01uuid:","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x85\\xa0urn:{=\\U000e0001\\u2028\\u200b\uff11\\U000e0001uuid:'"}, +{"text":"\turn:_',Xurn:\u0378\ud835\udfce\u0085x","repr":"\"\\turn:_',Xurn:\\u0378\ud835\udfce\\x85x\"","split":["urn:_',Xurn:\u0378\ud835\udfce","x"],"strip":"urn:_',Xurn:\u0378\ud835\udfce\u0085x","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\turn:_',Xurn:\\u0378\ud835\udfce\\x85x\""}, +{"text":"\ta\"1\n\u0663\uff11\u00adF0x","repr":"'\\ta\"1\\n\u0663\uff11\\xadF0x'","split":["a\"1","\u0663\uff11\u00adF0x"],"strip":"a\"1\n\u0663\uff11\u00adF0x","uuid":null,"int_error":"invalid literal for int() with base 10: '\\ta\"1\\n\u0663\uff11\\xadF0x'"}, +{"text":"\u0000uuid:\ud83d\ude00=\u2003\u000b\ufeff\u3000x \t\u001c","repr":"'\\x00uuid:\ud83d\ude00=\\u2003\\x0b\\ufeff\\u3000x \\t\\x1c'","split":["\u0000uuid:\ud83d\ude00=","\ufeff","x"],"strip":"\u0000uuid:\ud83d\ude00=\u2003\u000b\ufeff\u3000x","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x00uuid:\ud83d\ude00=\\u2003\\x0b\\ufeff\\u3000x \\t\\x1c'"}, +{"text":"\u00000x","repr":"'\\x000x'","split":["\u00000x"],"strip":"\u00000x","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x000x'"}, +{"text":" ","repr":"' '","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ' '"}, +{"text":"\t1x9\u00e9\udb40\udc01\n0\u0663","repr":"'\\t1x9\u00e9\\U000e0001\\n0\u0663'","split":["1x9\u00e9\udb40\udc01","0\u0663"],"strip":"1x9\u00e9\udb40\udc01\n0\u0663","uuid":null,"int_error":"invalid literal for int() with base 10: '\\t1x9\u00e9\\U000e0001\\n0\u0663'"}, +{"text":",uuid:{\ud83d\ude00\u007f\n","repr":"',uuid:{\ud83d\ude00\\x7f\\n'","split":[",uuid:{\ud83d\ude00\u007f"],"strip":",uuid:{\ud83d\ude00\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: ',uuid:{\ud83d\ude00\\x7f\\n'"}, +{"text":"\ud835\udfcea -\u001c_","repr":"'\ud835\udfcea -\\x1c_'","split":["\ud835\udfcea","-","_"],"strip":"\ud835\udfcea -\u001c_","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud835\udfcea -\\x1c_'"}, +{"text":"\u0378urn:0x","repr":"'\\u0378urn:0x'","split":["\u0378urn:0x"],"strip":"\u0378urn:0x","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u0378urn:0x'"}, +{"text":"\"9F\u000b\ud835\udfce'\u00a0{=urn:\u00e9","repr":"'\"9F\\x0b\ud835\udfce\\'\\xa0{=urn:\u00e9'","split":["\"9F","\ud835\udfce'","{=urn:\u00e9"],"strip":"\"9F\u000b\ud835\udfce'\u00a0{=urn:\u00e9","uuid":null,"int_error":"invalid literal for int() with base 10: '\"9F\\x0b\ud835\udfce\\'\\xa0{=urn:\u00e9'"}, +{"text":" \uff11:\\","repr":"' \uff11:\\\\'","split":["\uff11:\\"],"strip":"\uff11:\\","uuid":null,"int_error":"invalid literal for int() with base 10: ' \uff11:\\\\'"}, +{"text":"'-x \u0000\ufeff\uff11","repr":"\"'-x \\x00\\ufeff\uff11\"","split":["'-x","\u0000\ufeff\uff11"],"strip":"'-x \u0000\ufeff\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: \"'-x \\x00\\ufeff\uff11\""}, +{"text":"\u001f70x\ud83d\ude00","repr":"'\\x1f70x\ud83d\ude00'","split":["70x\ud83d\ude00"],"strip":"70x\ud83d\ude00","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f70x\ud83d\ude00'"}, +{"text":"\u00ad\u00a00x\udb40\udc01\u2028\u007f\u001c\u000b,","repr":"'\\xad\\xa00x\\U000e0001\\u2028\\x7f\\x1c\\x0b,'","split":["\u00ad","0x\udb40\udc01","\u007f",","],"strip":"\u00ad\u00a00x\udb40\udc01\u2028\u007f\u001c\u000b,","uuid":null,"int_error":"invalid literal for int() with base 10: '\\xad\\xa00x\\U000e0001\\u2028\\x7f\\x1c\\x0b,'"}, +{"text":"\u000b7\u000b-,F\u00a07\u001c\u00e99","repr":"'\\x0b7\\x0b-,F\\xa07\\x1c\u00e99'","split":["7","-,F","7","\u00e99"],"strip":"7\u000b-,F\u00a07\u001c\u00e99","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x0b7\\x0b-,F\\xa07\\x1c\u00e99'"}, +{"text":"x0,_9:=urn:urn::\ud83d\ude00\ud83d\ude00","repr":"'x0,_9:=urn:urn::\ud83d\ude00\ud83d\ude00'","split":["x0,_9:=urn:urn::\ud83d\ude00\ud83d\ude00"],"strip":"x0,_9:=urn:urn::\ud83d\ude00\ud83d\ude00","uuid":null,"int_error":"invalid literal for int() with base 10: 'x0,_9:=urn:urn::\ud83d\ude00\ud83d\ude00'"}, +{"text":"\u200b{\udb40\udc01\udb40\udc01X1}","repr":"'\\u200b{\\U000e0001\\U000e0001X1}'","split":["\u200b{\udb40\udc01\udb40\udc01X1}"],"strip":"\u200b{\udb40\udc01\udb40\udc01X1}","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u200b{\\U000e0001\\U000e0001X1}'"}, +{"text":"\u00a0\u001c","repr":"'\\xa0\\x1c'","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\xa0\\x1c'"}, +{"text":"\u2003\u0085\u3000\u0663\ud835\udfce\u0085_\n\u0378a\\0","repr":"'\\u2003\\x85\\u3000\u0663\ud835\udfce\\x85_\\n\\u0378a\\\\0'","split":["\u0663\ud835\udfce","_","\u0378a\\0"],"strip":"\u0663\ud835\udfce\u0085_\n\u0378a\\0","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2003\\x85\\u3000\u0663\ud835\udfce\\x85_\\n\\u0378a\\\\0'"}, +{"text":"\ud835\udfce\uff11urn:","repr":"'\ud835\udfce\uff11urn:'","split":["\ud835\udfce\uff11urn:"],"strip":"\ud835\udfce\uff11urn:","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud835\udfce\uff11urn:'"}, +{"text":"F_XXuuid:\u200b","repr":"'F_XXuuid:\\u200b'","split":["F_XXuuid:\u200b"],"strip":"F_XXuuid:\u200b","uuid":null,"int_error":"invalid literal for int() with base 10: 'F_XXuuid:\\u200b'"}, +{"text":"\u007f\u00009\u007f\u00ada0\u000b\uff11","repr":"'\\x7f\\x009\\x7f\\xada0\\x0b\uff11'","split":["\u007f\u00009\u007f\u00ada0","\uff11"],"strip":"\u007f\u00009\u007f\u00ada0\u000b\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x7f\\x009\\x7f\\xada0\\x0b\uff11'"}, +{"text":"\u001f\u007f\u0378:F\ud835\udfce\\uuid:\u200b\u00a0\"\u000b","repr":"'\\x1f\\x7f\\u0378:F\ud835\udfce\\\\uuid:\\u200b\\xa0\"\\x0b'","split":["\u007f\u0378:F\ud835\udfce\\uuid:\u200b","\""],"strip":"\u007f\u0378:F\ud835\udfce\\uuid:\u200b\u00a0\"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f\\x7f\\u0378:F\ud835\udfce\\\\uuid:\\u200b\\xa0\"\\x0b'"}, +{"text":":\ud83d\ude00\u007f\u007fX0 }0\u0000\u200bX","repr":"':\ud83d\ude00\\x7f\\x7fX0 }0\\x00\\u200bX'","split":[":\ud83d\ude00\u007f\u007fX0","}0\u0000\u200bX"],"strip":":\ud83d\ude00\u007f\u007fX0 }0\u0000\u200bX","uuid":null,"int_error":"invalid literal for int() with base 10: ':\ud83d\ude00\\x7f\\x7fX0 }0\\x00\\u200bX'"}, +{"text":"\ufeff\u0000x\u0000\ud835\udfce\u2003{","repr":"'\\ufeff\\x00x\\x00\ud835\udfce\\u2003{'","split":["\ufeff\u0000x\u0000\ud835\udfce","{"],"strip":"\ufeff\u0000x\u0000\ud835\udfce\u2003{","uuid":null,"int_error":"invalid literal for int() with base 10: '\\ufeff\\x00x\\x00\ud835\udfce\\u2003{'"}, +{"text":"\u00ad\u00e9\u3000x\u00ad","repr":"'\\xad\u00e9\\u3000x\\xad'","split":["\u00ad\u00e9","x\u00ad"],"strip":"\u00ad\u00e9\u3000x\u00ad","uuid":null,"int_error":"invalid literal for int() with base 10: '\\xad\u00e9\\u3000x\\xad'"}, +{"text":"\\\u3000\u0378\u0663\n\u00aduuid:,","repr":"'\\\\\\u3000\\u0378\u0663\\n\\xaduuid:,'","split":["\\","\u0378\u0663","\u00aduuid:,"],"strip":"\\\u3000\u0378\u0663\n\u00aduuid:,","uuid":null,"int_error":"invalid literal for int() with base 10: '\\\\\\u3000\\u0378\u0663\\n\\xaduuid:,'"}, +{"text":"1}\u00e9\u2003\ud83d\ude00\u00a0\"_\u00ad=9","repr":"'1}\u00e9\\u2003\ud83d\ude00\\xa0\"_\\xad=9'","split":["1}\u00e9","\ud83d\ude00","\"_\u00ad=9"],"strip":"1}\u00e9\u2003\ud83d\ude00\u00a0\"_\u00ad=9","uuid":null,"int_error":"invalid literal for int() with base 10: '1}\u00e9\\u2003\ud83d\ude00\\xa0\"_\\xad=9'"}, +{"text":"7\u001fa\u001c0}","repr":"'7\\x1fa\\x1c0}'","split":["7","a","0}"],"strip":"7\u001fa\u001c0}","uuid":null,"int_error":"invalid literal for int() with base 10: '7\\x1fa\\x1c0}'"}, +{"text":"}\u0378","repr":"'}\\u0378'","split":["}\u0378"],"strip":"}\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: '}\\u0378'"}, +{"text":"urn:\u00001\u00ad\u001c \u3000+","repr":"'urn:\\x001\\xad\\x1c \\u3000+'","split":["urn:\u00001\u00ad","+"],"strip":"urn:\u00001\u00ad\u001c \u3000+","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:\\x001\\xad\\x1c \\u3000+'"}, +{"text":"\u2028\u0085\u000bx9\u001c\u001f","repr":"'\\u2028\\x85\\x0bx9\\x1c\\x1f'","split":["x9"],"strip":"x9","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2028\\x85\\x0bx9\\x1c\\x1f'"}, +{"text":"7\"\u2028}\u3000","repr":"'7\"\\u2028}\\u3000'","split":["7\"","}"],"strip":"7\"\u2028}","uuid":null,"int_error":"invalid literal for int() with base 10: '7\"\\u2028}\\u3000'"}, +{"text":"-\u001f0x7-\u200b\ufeff\u000b\\\"\u0378","repr":"'-\\x1f0x7-\\u200b\\ufeff\\x0b\\\\\"\\u0378'","split":["-","0x7-\u200b\ufeff","\\\"\u0378"],"strip":"-\u001f0x7-\u200b\ufeff\u000b\\\"\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: '-\\x1f0x7-\\u200b\\ufeff\\x0b\\\\\"\\u0378'"}, +{"text":"\u0000-\udb40\udc01\ud835\udfce0x\u000b\u2003\u0663\u001c","repr":"'\\x00-\\U000e0001\ud835\udfce0x\\x0b\\u2003\u0663\\x1c'","split":["\u0000-\udb40\udc01\ud835\udfce0x","\u0663"],"strip":"\u0000-\udb40\udc01\ud835\udfce0x\u000b\u2003\u0663","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x00-\\U000e0001\ud835\udfce0x\\x0b\\u2003\u0663\\x1c'"}, +{"text":"\ud835\udfce\\\uff11\ufeff:+\u001f","repr":"'\ud835\udfce\\\\\uff11\\ufeff:+\\x1f'","split":["\ud835\udfce\\\uff11\ufeff:+"],"strip":"\ud835\udfce\\\uff11\ufeff:+","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud835\udfce\\\\\uff11\\ufeff:+\\x1f'"}, +{"text":"0x-\u200b\u0085\ud83d\ude00:+aX7\uff11","repr":"'0x-\\u200b\\x85\ud83d\ude00:+aX7\uff11'","split":["0x-\u200b","\ud83d\ude00:+aX7\uff11"],"strip":"0x-\u200b\u0085\ud83d\ude00:+aX7\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: '0x-\\u200b\\x85\ud83d\ude00:+aX7\uff11'"}, +{"text":"\u00e9\t-","repr":"'\u00e9\\t-'","split":["\u00e9","-"],"strip":"\u00e9\t-","uuid":null,"int_error":"invalid literal for int() with base 10: '\u00e9\\t-'"}, +{"text":"\\\ufeff","repr":"'\\\\\\ufeff'","split":["\\\ufeff"],"strip":"\\\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '\\\\\\ufeff'"}, +{"text":"\uff11}","repr":"'\uff11}'","split":["\uff11}"],"strip":"\uff11}","uuid":null,"int_error":"invalid literal for int() with base 10: '\uff11}'"}, +{"text":"\n1 a","repr":"'\\n1 a'","split":["1","a"],"strip":"1 a","uuid":null,"int_error":"invalid literal for int() with base 10: '\\n1 a'"}, +{"text":"\\:\ud83d\ude00\u3000\u0000:\na\u00ad\uff11\ud835\udfce","repr":"'\\\\:\ud83d\ude00\\u3000\\x00:\\na\\xad\uff11\ud835\udfce'","split":["\\:\ud83d\ude00","\u0000:","a\u00ad\uff11\ud835\udfce"],"strip":"\\:\ud83d\ude00\u3000\u0000:\na\u00ad\uff11\ud835\udfce","uuid":null,"int_error":"invalid literal for int() with base 10: '\\\\:\ud83d\ude00\\u3000\\x00:\\na\\xad\uff11\ud835\udfce'"}, +{"text":"a\u0085\tx\u200b=","repr":"'a\\x85\\tx\\u200b='","split":["a","x\u200b="],"strip":"a\u0085\tx\u200b=","uuid":null,"int_error":"invalid literal for int() with base 10: 'a\\x85\\tx\\u200b='"}, +{"text":"\u200ba{","repr":"'\\u200ba{'","split":["\u200ba{"],"strip":"\u200ba{","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u200ba{'"}, +{"text":"}\u001f\udb40\udc01","repr":"'}\\x1f\\U000e0001'","split":["}","\udb40\udc01"],"strip":"}\u001f\udb40\udc01","uuid":null,"int_error":"invalid literal for int() with base 10: '}\\x1f\\U000e0001'"}, +{"text":"a7\u00a0\"\u0000\t\u0378\u0378\u0000\\\u001f","repr":"'a7\\xa0\"\\x00\\t\\u0378\\u0378\\x00\\\\\\x1f'","split":["a7","\"\u0000","\u0378\u0378\u0000\\"],"strip":"a7\u00a0\"\u0000\t\u0378\u0378\u0000\\","uuid":null,"int_error":"invalid literal for int() with base 10: 'a7\\xa0\"\\x00\\t\\u0378\\u0378\\x00\\\\\\x1f'"}, +{"text":"\u00ad+-}X1","repr":"'\\xad+-}X1'","split":["\u00ad+-}X1"],"strip":"\u00ad+-}X1","uuid":null,"int_error":"invalid literal for int() with base 10: '\\xad+-}X1'"}, +{"text":"\u001f7\u00ad\"\ufeff","repr":"'\\x1f7\\xad\"\\ufeff'","split":["7\u00ad\"\ufeff"],"strip":"7\u00ad\"\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f7\\xad\"\\ufeff'"}, +{"text":"\ud83d\ude00X1\"\u001f\ud83d\ude00\ufeff\n,'\u0663","repr":"'\ud83d\ude00X1\"\\x1f\ud83d\ude00\\ufeff\\n,\\'\u0663'","split":["\ud83d\ude00X1\"","\ud83d\ude00\ufeff",",'\u0663"],"strip":"\ud83d\ude00X1\"\u001f\ud83d\ude00\ufeff\n,'\u0663","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud83d\ude00X1\"\\x1f\ud83d\ude00\\ufeff\\n,\\'\u0663'"}, +{"text":":uuid:","repr":"':uuid:'","split":[":uuid:"],"strip":":uuid:","uuid":null,"int_error":"invalid literal for int() with base 10: ':uuid:'"}, +{"text":"\u2003","repr":"'\\u2003'","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2003'"}, +{"text":"_\u000b0x\u2003auuid:'\u001c","repr":"\"_\\x0b0x\\u2003auuid:'\\x1c\"","split":["_","0x","auuid:'"],"strip":"_\u000b0x\u2003auuid:'","uuid":null,"int_error":"invalid literal for int() with base 10: \"_\\x0b0x\\u2003auuid:'\\x1c\""}, +{"text":"a\u30000=\u007f\u001c+_","repr":"'a\\u30000=\\x7f\\x1c+_'","split":["a","0=\u007f","+_"],"strip":"a\u30000=\u007f\u001c+_","uuid":null,"int_error":"invalid literal for int() with base 10: 'a\\u30000=\\x7f\\x1c+_'"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"\"","repr":"'\"'","split":["\""],"strip":"\"","uuid":null,"int_error":"invalid literal for int() with base 10: '\"'"}, +{"text":"\u001f\u00001\u2003\u0000\n\u001f\uff119==\u00a0","repr":"'\\x1f\\x001\\u2003\\x00\\n\\x1f\uff119==\\xa0'","split":["\u00001","\u0000","\uff119=="],"strip":"\u00001\u2003\u0000\n\u001f\uff119==","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f\\x001\\u2003\\x00\\n\\x1f\uff119==\\xa0'"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"\u0000\u00ad\u300009 F\u0378\u001c\ud835\udfce\u2003","repr":"'\\x00\\xad\\u300009 F\\u0378\\x1c\ud835\udfce\\u2003'","split":["\u0000\u00ad","09","F\u0378","\ud835\udfce"],"strip":"\u0000\u00ad\u300009 F\u0378\u001c\ud835\udfce","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x00\\xad\\u300009 F\\u0378\\x1c\ud835\udfce\\u2003'"}, +{"text":"\u2003","repr":"'\\u2003'","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2003'"}, +{"text":"}F\\\u2003,\u001f\n7\t\"","repr":"'}F\\\\\\u2003,\\x1f\\n7\\t\"'","split":["}F\\",",","7","\""],"strip":"}F\\\u2003,\u001f\n7\t\"","uuid":null,"int_error":"invalid literal for int() with base 10: '}F\\\\\\u2003,\\x1f\\n7\\t\"'"}, +{"text":"F\u0085{\u2003'\u007f\ufeff\u001f","repr":"\"F\\x85{\\u2003'\\x7f\\ufeff\\x1f\"","split":["F","{","'\u007f\ufeff"],"strip":"F\u0085{\u2003'\u007f\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: \"F\\x85{\\u2003'\\x7f\\ufeff\\x1f\""}, +{"text":"\u2028\u00ad_F\u3000X","repr":"'\\u2028\\xad_F\\u3000X'","split":["\u00ad_F","X"],"strip":"\u00ad_F\u3000X","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2028\\xad_F\\u3000X'"}, +{"text":"=_\ud835\udfce","repr":"'=_\ud835\udfce'","split":["=_\ud835\udfce"],"strip":"=_\ud835\udfce","uuid":null,"int_error":"invalid literal for int() with base 10: '=_\ud835\udfce'"}, +{"text":",\n\u000b9","repr":"',\\n\\x0b9'","split":[",","9"],"strip":",\n\u000b9","uuid":null,"int_error":"invalid literal for int() with base 10: ',\\n\\x0b9'"}, +{"text":":\u00ad\ud835\udfce'","repr":"\":\\xad\ud835\udfce'\"","split":[":\u00ad\ud835\udfce'"],"strip":":\u00ad\ud835\udfce'","uuid":null,"int_error":"invalid literal for int() with base 10: \":\\xad\ud835\udfce'\""}, +{"text":"\u0085","repr":"'\\x85'","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x85'"}, +{"text":"a\u00e90","repr":"'a\u00e90'","split":["a\u00e90"],"strip":"a\u00e90","uuid":null,"int_error":"invalid literal for int() with base 10: 'a\u00e90'"}, +{"text":"10x}\u0663a,\ufeffX=","repr":"'10x}\u0663a,\\ufeffX='","split":["10x}\u0663a,\ufeffX="],"strip":"10x}\u0663a,\ufeffX=","uuid":null,"int_error":"invalid literal for int() with base 10: '10x}\u0663a,\\ufeffX='"}, +{"text":"0+","repr":"'0+'","split":["0+"],"strip":"0+","uuid":null,"int_error":"invalid literal for int() with base 10: '0+'"}, +{"text":" \u2003+\u200bXurn:\u0663}","repr":"' \\u2003+\\u200bXurn:\u0663}'","split":["+\u200bXurn:\u0663}"],"strip":"+\u200bXurn:\u0663}","uuid":null,"int_error":"invalid literal for int() with base 10: ' \\u2003+\\u200bXurn:\u0663}'"}, +{"text":"=\uff11","repr":"'=\uff11'","split":["=\uff11"],"strip":"=\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: '=\uff11'"}, +{"text":"\u001c\u06630-+x\udb40\udc01=uuid:","repr":"'\\x1c\u06630-+x\\U000e0001=uuid:'","split":["\u06630-+x\udb40\udc01=uuid:"],"strip":"\u06630-+x\udb40\udc01=uuid:","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1c\u06630-+x\\U000e0001=uuid:'"}, +{"text":"\u0663\turn:',\u007f","repr":"\"\u0663\\turn:',\\x7f\"","split":["\u0663","urn:',\u007f"],"strip":"\u0663\turn:',\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: \"\u0663\\turn:',\\x7f\""}, +{"text":"F\u000b\\\u000b\u2003\udb40\udc01","repr":"'F\\x0b\\\\\\x0b\\u2003\\U000e0001'","split":["F","\\","\udb40\udc01"],"strip":"F\u000b\\\u000b\u2003\udb40\udc01","uuid":null,"int_error":"invalid literal for int() with base 10: 'F\\x0b\\\\\\x0b\\u2003\\U000e0001'"}, +{"text":"\u0378X\u001fuuid:\u2028+\u0000\ud83d\ude00\u2003 \u0085'","repr":"\"\\u0378X\\x1fuuid:\\u2028+\\x00\ud83d\ude00\\u2003 \\x85'\"","split":["\u0378X","uuid:","+\u0000\ud83d\ude00","'"],"strip":"\u0378X\u001fuuid:\u2028+\u0000\ud83d\ude00\u2003 \u0085'","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\u0378X\\x1fuuid:\\u2028+\\x00\ud83d\ude00\\u2003 \\x85'\""}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"\u007f{,1a\u0085\uff11a","repr":"'\\x7f{,1a\\x85\uff11a'","split":["\u007f{,1a","\uff11a"],"strip":"\u007f{,1a\u0085\uff11a","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x7f{,1a\\x85\uff11a'"}, +{"text":",\uff11\ud83d\ude00uuid:a\u0000\u0000\\","repr":"',\uff11\ud83d\ude00uuid:a\\x00\\x00\\\\'","split":[",\uff11\ud83d\ude00uuid:a\u0000\u0000\\"],"strip":",\uff11\ud83d\ude00uuid:a\u0000\u0000\\","uuid":null,"int_error":"invalid literal for int() with base 10: ',\uff11\ud83d\ude00uuid:a\\x00\\x00\\\\'"}, +{"text":"\u0378a\u00ad","repr":"'\\u0378a\\xad'","split":["\u0378a\u00ad"],"strip":"\u0378a\u00ad","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u0378a\\xad'"}, +{"text":"'","repr":"\"'\"","split":["'"],"strip":"'","uuid":null,"int_error":"invalid literal for int() with base 10: \"'\""}, +{"text":"\u3000\u200b_x0\u0663\u20039F","repr":"'\\u3000\\u200b_x0\u0663\\u20039F'","split":["\u200b_x0\u0663","9F"],"strip":"\u200b_x0\u0663\u20039F","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000\\u200b_x0\u0663\\u20039F'"}, +{"text":"1,\ufeff\u00a000x0x","repr":"'1,\\ufeff\\xa000x0x'","split":["1,\ufeff","00x0x"],"strip":"1,\ufeff\u00a000x0x","uuid":null,"int_error":"invalid literal for int() with base 10: '1,\\ufeff\\xa000x0x'"}, +{"text":"\u007f\u3000\u00e9_x-a\u007f","repr":"'\\x7f\\u3000\u00e9_x-a\\x7f'","split":["\u007f","\u00e9_x-a\u007f"],"strip":"\u007f\u3000\u00e9_x-a\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x7f\\u3000\u00e9_x-a\\x7f'"}, +{"text":"\ufeff\u0085\u000bF\u001f\u001f\u3000\ufeff","repr":"'\\ufeff\\x85\\x0bF\\x1f\\x1f\\u3000\\ufeff'","split":["\ufeff","F","\ufeff"],"strip":"\ufeff\u0085\u000bF\u001f\u001f\u3000\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '\\ufeff\\x85\\x0bF\\x1f\\x1f\\u3000\\ufeff'"}, +{"text":"+-\u0000\"\u007f\u0000\u0378\uff11\u3000\u001f","repr":"'+-\\x00\"\\x7f\\x00\\u0378\uff11\\u3000\\x1f'","split":["+-\u0000\"\u007f\u0000\u0378\uff11"],"strip":"+-\u0000\"\u007f\u0000\u0378\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: '+-\\x00\"\\x7f\\x00\\u0378\uff11\\u3000\\x1f'"}, +{"text":"\ud83d\ude00\n\ud83d\ude00x\u0000\ufeff\\","repr":"'\ud83d\ude00\\n\ud83d\ude00x\\x00\\ufeff\\\\'","split":["\ud83d\ude00","\ud83d\ude00x\u0000\ufeff\\"],"strip":"\ud83d\ude00\n\ud83d\ude00x\u0000\ufeff\\","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud83d\ude00\\n\ud83d\ude00x\\x00\\ufeff\\\\'"}, +{"text":"-\u3000\u0085:F\u00ad","repr":"'-\\u3000\\x85:F\\xad'","split":["-",":F\u00ad"],"strip":"-\u3000\u0085:F\u00ad","uuid":null,"int_error":"invalid literal for int() with base 10: '-\\u3000\\x85:F\\xad'"}, +{"text":"9\ufeff","repr":"'9\\ufeff'","split":["9\ufeff"],"strip":"9\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '9\\ufeff'"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"}\u001cF0x","repr":"'}\\x1cF0x'","split":["}","F0x"],"strip":"}\u001cF0x","uuid":null,"int_error":"invalid literal for int() with base 10: '}\\x1cF0x'"}, +{"text":"\"X9'\u001f\\\u2003","repr":"'\"X9\\'\\x1f\\\\\\u2003'","split":["\"X9'","\\"],"strip":"\"X9'\u001f\\","uuid":null,"int_error":"invalid literal for int() with base 10: '\"X9\\'\\x1f\\\\\\u2003'"}, +{"text":"\u200b\u2003,","repr":"'\\u200b\\u2003,'","split":["\u200b",","],"strip":"\u200b\u2003,","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u200b\\u2003,'"}, +{"text":"}+","repr":"'}+'","split":["}+"],"strip":"}+","uuid":null,"int_error":"invalid literal for int() with base 10: '}+'"}, +{"text":"\u00a0\ud835\udfce0_\t11+\u00a0","repr":"'\\xa0\ud835\udfce0_\\t11+\\xa0'","split":["\ud835\udfce0_","11+"],"strip":"\ud835\udfce0_\t11+","uuid":null,"int_error":"invalid literal for int() with base 10: '\\xa0\ud835\udfce0_\\t11+\\xa0'"}, +{"text":"\\\ud83d\ude00a\ufeffx\uff11\u00e9''=urn:uuid:","repr":"\"\\\\\ud83d\ude00a\\ufeffx\uff11\u00e9''=urn:uuid:\"","split":["\\\ud83d\ude00a\ufeffx\uff11\u00e9''=urn:uuid:"],"strip":"\\\ud83d\ude00a\ufeffx\uff11\u00e9''=urn:uuid:","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\\\\ud83d\ude00a\\ufeffx\uff11\u00e9''=urn:uuid:\""}, +{"text":"\u007f0x\u200b_","repr":"'\\x7f0x\\u200b_'","split":["\u007f0x\u200b_"],"strip":"\u007f0x\u200b_","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x7f0x\\u200b_'"}, +{"text":"\ud83d\ude00\ud835\udfce\u00e90x\u00ad Fa\u0378","repr":"'\ud83d\ude00\ud835\udfce\u00e90x\\xad Fa\\u0378'","split":["\ud83d\ude00\ud835\udfce\u00e90x\u00ad","Fa\u0378"],"strip":"\ud83d\ude00\ud835\udfce\u00e90x\u00ad Fa\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud83d\ude00\ud835\udfce\u00e90x\\xad Fa\\u0378'"}, +{"text":"+\u00ad\u3000\u200b\udb40\udc01\"","repr":"'+\\xad\\u3000\\u200b\\U000e0001\"'","split":["+\u00ad","\u200b\udb40\udc01\""],"strip":"+\u00ad\u3000\u200b\udb40\udc01\"","uuid":null,"int_error":"invalid literal for int() with base 10: '+\\xad\\u3000\\u200b\\U000e0001\"'"}, +{"text":"\u2003-x\\\u06630x\u00e9{{","repr":"'\\u2003-x\\\\\u06630x\u00e9{{'","split":["-x\\\u06630x\u00e9{{"],"strip":"-x\\\u06630x\u00e9{{","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2003-x\\\\\u06630x\u00e9{{'"}, +{"text":":urn:\u00ad\"\u00ad\u0378\"\uff117-","repr":"':urn:\\xad\"\\xad\\u0378\"\uff117-'","split":[":urn:\u00ad\"\u00ad\u0378\"\uff117-"],"strip":":urn:\u00ad\"\u00ad\u0378\"\uff117-","uuid":null,"int_error":"invalid literal for int() with base 10: ':urn:\\xad\"\\xad\\u0378\"\uff117-'"}, +{"text":"\u001f\u0000\u2003+\u001c}\ufeff=\u0085\uff11","repr":"'\\x1f\\x00\\u2003+\\x1c}\\ufeff=\\x85\uff11'","split":["\u0000","+","}\ufeff=","\uff11"],"strip":"\u0000\u2003+\u001c}\ufeff=\u0085\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f\\x00\\u2003+\\x1c}\\ufeff=\\x85\uff11'"}, +{"text":"\u200b\u000b 1","repr":"'\\u200b\\x0b 1'","split":["\u200b","1"],"strip":"\u200b\u000b 1","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u200b\\x0b 1'"}, +{"text":"+\u2028\u000b\u00e9\u00ad+\\\u007f","repr":"'+\\u2028\\x0b\u00e9\\xad+\\\\\\x7f'","split":["+","\u00e9\u00ad+\\\u007f"],"strip":"+\u2028\u000b\u00e9\u00ad+\\\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '+\\u2028\\x0b\u00e9\\xad+\\\\\\x7f'"}, +{"text":"\\{\u00ad_9,{\uff117\u001f\ud835\udfce,","repr":"'\\\\{\\xad_9,{\uff117\\x1f\ud835\udfce,'","split":["\\{\u00ad_9,{\uff117","\ud835\udfce,"],"strip":"\\{\u00ad_9,{\uff117\u001f\ud835\udfce,","uuid":null,"int_error":"invalid literal for int() with base 10: '\\\\{\\xad_9,{\uff117\\x1f\ud835\udfce,'"}, +{"text":"-F\u00e9\u001c\u03787\u0000\u06630__","repr":"'-F\u00e9\\x1c\\u03787\\x00\u06630__'","split":["-F\u00e9","\u03787\u0000\u06630__"],"strip":"-F\u00e9\u001c\u03787\u0000\u06630__","uuid":null,"int_error":"invalid literal for int() with base 10: '-F\u00e9\\x1c\\u03787\\x00\u06630__'"}, +{"text":"+\ud83d\ude00}0x\u3000}\u2028, _}\u2028","repr":"'+\ud83d\ude00}0x\\u3000}\\u2028, _}\\u2028'","split":["+\ud83d\ude00}0x","}",",","_}"],"strip":"+\ud83d\ude00}0x\u3000}\u2028, _}","uuid":null,"int_error":"invalid literal for int() with base 10: '+\ud83d\ude00}0x\\u3000}\\u2028, _}\\u2028'"}, +{"text":"9\u001f\u0663\\F","repr":"'9\\x1f\u0663\\\\F'","split":["9","\u0663\\F"],"strip":"9\u001f\u0663\\F","uuid":null,"int_error":"invalid literal for int() with base 10: '9\\x1f\u0663\\\\F'"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"=\u2028\u0000_=\ud835\udfce0\n}\ufeff'-","repr":"\"=\\u2028\\x00_=\ud835\udfce0\\n}\\ufeff'-\"","split":["=","\u0000_=\ud835\udfce0","}\ufeff'-"],"strip":"=\u2028\u0000_=\ud835\udfce0\n}\ufeff'-","uuid":null,"int_error":"invalid literal for int() with base 10: \"=\\u2028\\x00_=\ud835\udfce0\\n}\\ufeff'-\""}, +{"text":"\u03787}1","repr":"'\\u03787}1'","split":["\u03787}1"],"strip":"\u03787}1","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u03787}1'"}, +{"text":"90x\u2028\u001c\u0085\u000b\u0000","repr":"'90x\\u2028\\x1c\\x85\\x0b\\x00'","split":["90x","\u0000"],"strip":"90x\u2028\u001c\u0085\u000b\u0000","uuid":null,"int_error":"invalid literal for int() with base 10: '90x\\u2028\\x1c\\x85\\x0b\\x00'"}, +{"text":"a:_\u001c","repr":"'a:_\\x1c'","split":["a:_"],"strip":"a:_","uuid":null,"int_error":"invalid literal for int() with base 10: 'a:_\\x1c'"}, +{"text":"urn:9\u001c\u007furn:,\u0378_\uff11","repr":"'urn:9\\x1c\\x7furn:,\\u0378_\uff11'","split":["urn:9","\u007furn:,\u0378_\uff11"],"strip":"urn:9\u001c\u007furn:,\u0378_\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:9\\x1c\\x7furn:,\\u0378_\uff11'"}, +{"text":"}\u0000\u0000urn:{9","repr":"'}\\x00\\x00urn:{9'","split":["}\u0000\u0000urn:{9"],"strip":"}\u0000\u0000urn:{9","uuid":null,"int_error":"invalid literal for int() with base 10: '}\\x00\\x00urn:{9'"}, +{"text":"\u0378_\u007f=\u007f\u00a0\u000b","repr":"'\\u0378_\\x7f=\\x7f\\xa0\\x0b'","split":["\u0378_\u007f=\u007f"],"strip":"\u0378_\u007f=\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u0378_\\x7f=\\x7f\\xa0\\x0b'"}, +{"text":"9\ufeff7uuid:7-=\u2028\ud835\udfce\u30009","repr":"'9\\ufeff7uuid:7-=\\u2028\ud835\udfce\\u30009'","split":["9\ufeff7uuid:7-=","\ud835\udfce","9"],"strip":"9\ufeff7uuid:7-=\u2028\ud835\udfce\u30009","uuid":null,"int_error":"invalid literal for int() with base 10: '9\\ufeff7uuid:7-=\\u2028\ud835\udfce\\u30009'"}, +{"text":"\u00e9+9\\\t7","repr":"'\u00e9+9\\\\\\t7'","split":["\u00e9+9\\","7"],"strip":"\u00e9+9\\\t7","uuid":null,"int_error":"invalid literal for int() with base 10: '\u00e9+9\\\\\\t7'"}, +{"text":"\u007f\ufeff\n\u0378X\u001fF","repr":"'\\x7f\\ufeff\\n\\u0378X\\x1fF'","split":["\u007f\ufeff","\u0378X","F"],"strip":"\u007f\ufeff\n\u0378X\u001fF","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x7f\\ufeff\\n\\u0378X\\x1fF'"}, +{"text":"\n7","repr":"'\\n7'","split":["7"],"strip":"7","uuid":null,"int":"7"}, +{"text":"_'+,\u200b\u0378uuid:x","repr":"\"_'+,\\u200b\\u0378uuid:x\"","split":["_'+,\u200b\u0378uuid:x"],"strip":"_'+,\u200b\u0378uuid:x","uuid":null,"int_error":"invalid literal for int() with base 10: \"_'+,\\u200b\\u0378uuid:x\""}, +{"text":":urn:\u200b0x\t9-\u0085","repr":"':urn:\\u200b0x\\t9-\\x85'","split":[":urn:\u200b0x","9-"],"strip":":urn:\u200b0x\t9-","uuid":null,"int_error":"invalid literal for int() with base 10: ':urn:\\u200b0x\\t9-\\x85'"}, +{"text":"X\u00a0\udb40\udc01\u0378","repr":"'X\\xa0\\U000e0001\\u0378'","split":["X","\udb40\udc01\u0378"],"strip":"X\u00a0\udb40\udc01\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: 'X\\xa0\\U000e0001\\u0378'"}, +{"text":",uuid:7X\tuuid:\u00a0\u000b\u2003 {","repr":"',uuid:7X\\tuuid:\\xa0\\x0b\\u2003 {'","split":[",uuid:7X","uuid:","{"],"strip":",uuid:7X\tuuid:\u00a0\u000b\u2003 {","uuid":null,"int_error":"invalid literal for int() with base 10: ',uuid:7X\\tuuid:\\xa0\\x0b\\u2003 {'"}, +{"text":"\u00e90\u001c","repr":"'\u00e90\\x1c'","split":["\u00e90"],"strip":"\u00e90","uuid":null,"int_error":"invalid literal for int() with base 10: '\u00e90\\x1c'"}, +{"text":"\u00e9x \u00a0\u001f\u200b","repr":"'\u00e9x \\xa0\\x1f\\u200b'","split":["\u00e9x","\u200b"],"strip":"\u00e9x \u00a0\u001f\u200b","uuid":null,"int_error":"invalid literal for int() with base 10: '\u00e9x \\xa0\\x1f\\u200b'"}, +{"text":"={\udb40\udc01=7-\ud835\udfce0x 0x\u000b","repr":"'={\\U000e0001=7-\ud835\udfce0x 0x\\x0b'","split":["={\udb40\udc01=7-\ud835\udfce0x","0x"],"strip":"={\udb40\udc01=7-\ud835\udfce0x 0x","uuid":null,"int_error":"invalid literal for int() with base 10: '={\\U000e0001=7-\ud835\udfce0x 0x\\x0b'"}, +{"text":"\n\udb40\udc01xx0x}\ud835\udfce{\ud835\udfce1","repr":"'\\n\\U000e0001xx0x}\ud835\udfce{\ud835\udfce1'","split":["\udb40\udc01xx0x}\ud835\udfce{\ud835\udfce1"],"strip":"\udb40\udc01xx0x}\ud835\udfce{\ud835\udfce1","uuid":null,"int_error":"invalid literal for int() with base 10: '\\n\\U000e0001xx0x}\ud835\udfce{\ud835\udfce1'"}, +{"text":"urn:=","repr":"'urn:='","split":["urn:="],"strip":"urn:=","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:='"}, +{"text":"\ud835\udfce\\\u007f\u00ad\u0378\u0000\uff11\u2028F0\u2003\u007f","repr":"'\ud835\udfce\\\\\\x7f\\xad\\u0378\\x00\uff11\\u2028F0\\u2003\\x7f'","split":["\ud835\udfce\\\u007f\u00ad\u0378\u0000\uff11","F0","\u007f"],"strip":"\ud835\udfce\\\u007f\u00ad\u0378\u0000\uff11\u2028F0\u2003\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud835\udfce\\\\\\x7f\\xad\\u0378\\x00\uff11\\u2028F0\\u2003\\x7f'"}, +{"text":"\u00a0","repr":"'\\xa0'","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\xa0'"}, +{"text":"\u0000","repr":"'\\x00'","split":["\u0000"],"strip":"\u0000","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x00'"}, +{"text":"\u3000\\\u30007\u00e9\u000b\u2003x9}F\u00a0","repr":"'\\u3000\\\\\\u30007\u00e9\\x0b\\u2003x9}F\\xa0'","split":["\\","7\u00e9","x9}F"],"strip":"\\\u30007\u00e9\u000b\u2003x9}F","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000\\\\\\u30007\u00e9\\x0b\\u2003x9}F\\xa0'"}, +{"text":"\u0000\u0085\u3000","repr":"'\\x00\\x85\\u3000'","split":["\u0000"],"strip":"\u0000","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x00\\x85\\u3000'"}, +{"text":"7\udb40\udc0117\u3000\udb40\udc01\u0663\u00a0\u001c","repr":"'7\\U000e000117\\u3000\\U000e0001\u0663\\xa0\\x1c'","split":["7\udb40\udc0117","\udb40\udc01\u0663"],"strip":"7\udb40\udc0117\u3000\udb40\udc01\u0663","uuid":null,"int_error":"invalid literal for int() with base 10: '7\\U000e000117\\u3000\\U000e0001\u0663\\xa0\\x1c'"}, +{"text":" \\\t\t{F\n","repr":"' \\\\\\t\\t{F\\n'","split":["\\","{F"],"strip":"\\\t\t{F","uuid":null,"int_error":"invalid literal for int() with base 10: ' \\\\\\t\\t{F\\n'"}, +{"text":"0xF\u0085urn:0","repr":"'0xF\\x85urn:0'","split":["0xF","urn:0"],"strip":"0xF\u0085urn:0","uuid":null,"int_error":"invalid literal for int() with base 10: '0xF\\x85urn:0'"}, +{"text":"\uff11","repr":"'\uff11'","split":["\uff11"],"strip":"\uff11","uuid":null,"int":"1"}, +{"text":"\u00a0\u200b71\uff111\u007f9'\u0000\u001f,","repr":"\"\\xa0\\u200b71\uff111\\x7f9'\\x00\\x1f,\"","split":["\u200b71\uff111\u007f9'\u0000",","],"strip":"\u200b71\uff111\u007f9'\u0000\u001f,","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\xa0\\u200b71\uff111\\x7f9'\\x00\\x1f,\""}, +{"text":"_\u0378\u2003\u007f","repr":"'_\\u0378\\u2003\\x7f'","split":["_\u0378","\u007f"],"strip":"_\u0378\u2003\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '_\\u0378\\u2003\\x7f'"}, +{"text":"\u200b\u200b\uff11'\\\ufeff=\u3000'","repr":"\"\\u200b\\u200b\uff11'\\\\\\ufeff=\\u3000'\"","split":["\u200b\u200b\uff11'\\\ufeff=","'"],"strip":"\u200b\u200b\uff11'\\\ufeff=\u3000'","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\u200b\\u200b\uff11'\\\\\\ufeff=\\u3000'\""}, +{"text":"-\t\\-={\u2028\t0x+\n}","repr":"'-\\t\\\\-={\\u2028\\t0x+\\n}'","split":["-","\\-={","0x+","}"],"strip":"-\t\\-={\u2028\t0x+\n}","uuid":null,"int_error":"invalid literal for int() with base 10: '-\\t\\\\-={\\u2028\\t0x+\\n}'"}, +{"text":"\ud83d\ude00\u000b9'\uff11F\u001fuuid:\u000b{\ufeff7","repr":"\"\ud83d\ude00\\x0b9'\uff11F\\x1fuuid:\\x0b{\\ufeff7\"","split":["\ud83d\ude00","9'\uff11F","uuid:","{\ufeff7"],"strip":"\ud83d\ude00\u000b9'\uff11F\u001fuuid:\u000b{\ufeff7","uuid":null,"int_error":"invalid literal for int() with base 10: \"\ud83d\ude00\\x0b9'\uff11F\\x1fuuid:\\x0b{\\ufeff7\""}, +{"text":"\u007f0","repr":"'\\x7f0'","split":["\u007f0"],"strip":"\u007f0","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x7f0'"}, +{"text":"urn:1\ufeff\u0085","repr":"'urn:1\\ufeff\\x85'","split":["urn:1\ufeff"],"strip":"urn:1\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:1\\ufeff\\x85'"}, +{"text":"0x\u001f}uuid:,a\ud83d\ude00\u00a0","repr":"'0x\\x1f}uuid:,a\ud83d\ude00\\xa0'","split":["0x","}uuid:,a\ud83d\ude00"],"strip":"0x\u001f}uuid:,a\ud83d\ude00","uuid":null,"int_error":"invalid literal for int() with base 10: '0x\\x1f}uuid:,a\ud83d\ude00\\xa0'"}, +{"text":"\u03789\udb40\udc01{\ud83d\ude00=0x-\u007f\u00a00","repr":"'\\u03789\\U000e0001{\ud83d\ude00=0x-\\x7f\\xa00'","split":["\u03789\udb40\udc01{\ud83d\ude00=0x-\u007f","0"],"strip":"\u03789\udb40\udc01{\ud83d\ude00=0x-\u007f\u00a00","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u03789\\U000e0001{\ud83d\ude00=0x-\\x7f\\xa00'"}, +{"text":"\u2028","repr":"'\\u2028'","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2028'"}, +{"text":"\ud835\udfceuuid:'\udb40\udc01\\","repr":"\"\ud835\udfceuuid:'\\U000e0001\\\\\"","split":["\ud835\udfceuuid:'\udb40\udc01\\"],"strip":"\ud835\udfceuuid:'\udb40\udc01\\","uuid":null,"int_error":"invalid literal for int() with base 10: \"\ud835\udfceuuid:'\\U000e0001\\\\\""}, +{"text":"\ufeff\u0663\u3000\uff11\u200b","repr":"'\\ufeff\u0663\\u3000\uff11\\u200b'","split":["\ufeff\u0663","\uff11\u200b"],"strip":"\ufeff\u0663\u3000\uff11\u200b","uuid":null,"int_error":"invalid literal for int() with base 10: '\\ufeff\u0663\\u3000\uff11\\u200b'"}, +{"text":"\udb40\udc011","repr":"'\\U000e00011'","split":["\udb40\udc011"],"strip":"\udb40\udc011","uuid":null,"int_error":"invalid literal for int() with base 10: '\\U000e00011'"}, +{"text":"\ud835\udfce,\\\ufeff\uff11\u00e9\ud83d\ude00\u2028\ufeff","repr":"'\ud835\udfce,\\\\\\ufeff\uff11\u00e9\ud83d\ude00\\u2028\\ufeff'","split":["\ud835\udfce,\\\ufeff\uff11\u00e9\ud83d\ude00","\ufeff"],"strip":"\ud835\udfce,\\\ufeff\uff11\u00e9\ud83d\ude00\u2028\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud835\udfce,\\\\\\ufeff\uff11\u00e9\ud83d\ude00\\u2028\\ufeff'"}, +{"text":"\u001fuuid:\"\udb40\udc01","repr":"'\\x1fuuid:\"\\U000e0001'","split":["uuid:\"\udb40\udc01"],"strip":"uuid:\"\udb40\udc01","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1fuuid:\"\\U000e0001'"}, +{"text":"\u2028","repr":"'\\u2028'","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2028'"}, +{"text":"X+\u0000\ufeff\ud83d\ude00\uff11=urn:\udb40\udc01:","repr":"'X+\\x00\\ufeff\ud83d\ude00\uff11=urn:\\U000e0001:'","split":["X+\u0000\ufeff\ud83d\ude00\uff11=urn:\udb40\udc01:"],"strip":"X+\u0000\ufeff\ud83d\ude00\uff11=urn:\udb40\udc01:","uuid":null,"int_error":"invalid literal for int() with base 10: 'X+\\x00\\ufeff\ud83d\ude00\uff11=urn:\\U000e0001:'"}, +{"text":"\u0085\u0663\u0663\udb40\udc01\u00a0\ufeff+Fa\u00ad","repr":"'\\x85\u0663\u0663\\U000e0001\\xa0\\ufeff+Fa\\xad'","split":["\u0663\u0663\udb40\udc01","\ufeff+Fa\u00ad"],"strip":"\u0663\u0663\udb40\udc01\u00a0\ufeff+Fa\u00ad","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x85\u0663\u0663\\U000e0001\\xa0\\ufeff+Fa\\xad'"}, +{"text":"\n\u0000\u000b0x","repr":"'\\n\\x00\\x0b0x'","split":["\u0000","0x"],"strip":"\u0000\u000b0x","uuid":null,"int_error":"invalid literal for int() with base 10: '\\n\\x00\\x0b0x'"}, +{"text":"\u0000\\0x,9\u0000=:","repr":"'\\x00\\\\0x,9\\x00=:'","split":["\u0000\\0x,9\u0000=:"],"strip":"\u0000\\0x,9\u0000=:","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x00\\\\0x,9\\x00=:'"}, +{"text":"0x\n\u0085X1\u00e9urn: ","repr":"'0x\\n\\x85X1\u00e9urn: '","split":["0x","X1\u00e9urn:"],"strip":"0x\n\u0085X1\u00e9urn:","uuid":null,"int_error":"invalid literal for int() with base 10: '0x\\n\\x85X1\u00e9urn: '"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"\u0085F \"\u0085\u200b\ufeff,","repr":"'\\x85F \"\\x85\\u200b\\ufeff,'","split":["F","\"","\u200b\ufeff,"],"strip":"F \"\u0085\u200b\ufeff,","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x85F \"\\x85\\u200b\\ufeff,'"}, +{"text":"\u000b\u20030x{\u0663x","repr":"'\\x0b\\u20030x{\u0663x'","split":["0x{\u0663x"],"strip":"0x{\u0663x","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x0b\\u20030x{\u0663x'"}, +{"text":"'\ud835\udfce:1urn:\"","repr":"'\\'\ud835\udfce:1urn:\"'","split":["'\ud835\udfce:1urn:\""],"strip":"'\ud835\udfce:1urn:\"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\'\ud835\udfce:1urn:\"'"}, +{"text":"\uff11\u001f{xuuid:urn:\n","repr":"'\uff11\\x1f{xuuid:urn:\\n'","split":["\uff11","{xuuid:urn:"],"strip":"\uff11\u001f{xuuid:urn:","uuid":null,"int_error":"invalid literal for int() with base 10: '\uff11\\x1f{xuuid:urn:\\n'"}, +{"text":"_\n{-\t\u0663","repr":"'_\\n{-\\t\u0663'","split":["_","{-","\u0663"],"strip":"_\n{-\t\u0663","uuid":null,"int_error":"invalid literal for int() with base 10: '_\\n{-\\t\u0663'"}, +{"text":"\u2028\u000b' 0=","repr":"\"\\u2028\\x0b' 0=\"","split":["'","0="],"strip":"' 0=","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\u2028\\x0b' 0=\""}, +{"text":"\uff11,=0xuuid:'","repr":"\"\uff11,=0xuuid:'\"","split":["\uff11,=0xuuid:'"],"strip":"\uff11,=0xuuid:'","uuid":null,"int_error":"invalid literal for int() with base 10: \"\uff11,=0xuuid:'\""}, +{"text":"\u3000\u00ad, \n","repr":"'\\u3000\\xad, \\n'","split":["\u00ad,"],"strip":"\u00ad,","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000\\xad, \\n'"}, +{"text":"\u0085\u00ad\n\u2028=\u2003_\u00e9a\u007f_","repr":"'\\x85\\xad\\n\\u2028=\\u2003_\u00e9a\\x7f_'","split":["\u00ad","=","_\u00e9a\u007f_"],"strip":"\u00ad\n\u2028=\u2003_\u00e9a\u007f_","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x85\\xad\\n\\u2028=\\u2003_\u00e9a\\x7f_'"}, +{"text":"}\u200b\u00ad-\uff11","repr":"'}\\u200b\\xad-\uff11'","split":["}\u200b\u00ad-\uff11"],"strip":"}\u200b\u00ad-\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: '}\\u200b\\xad-\uff11'"}, +{"text":"\u0378\u00a0+\\\na","repr":"'\\u0378\\xa0+\\\\\\na'","split":["\u0378","+\\","a"],"strip":"\u0378\u00a0+\\\na","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u0378\\xa0+\\\\\\na'"}, +{"text":"91x_\u0378\u0085\udb40\udc01\udb40\udc01a\u00e9\u3000","repr":"'91x_\\u0378\\x85\\U000e0001\\U000e0001a\u00e9\\u3000'","split":["91x_\u0378","\udb40\udc01\udb40\udc01a\u00e9"],"strip":"91x_\u0378\u0085\udb40\udc01\udb40\udc01a\u00e9","uuid":null,"int_error":"invalid literal for int() with base 10: '91x_\\u0378\\x85\\U000e0001\\U000e0001a\u00e9\\u3000'"}, +{"text":"+\u200b++\u007f\u3000\\0xX}","repr":"'+\\u200b++\\x7f\\u3000\\\\0xX}'","split":["+\u200b++\u007f","\\0xX}"],"strip":"+\u200b++\u007f\u3000\\0xX}","uuid":null,"int_error":"invalid literal for int() with base 10: '+\\u200b++\\x7f\\u3000\\\\0xX}'"}, +{"text":"\u001fa\"\u00ad'","repr":"'\\x1fa\"\\xad\\''","split":["a\"\u00ad'"],"strip":"a\"\u00ad'","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1fa\"\\xad\\''"}, +{"text":"\u0000\ufeff\u00009\u0085","repr":"'\\x00\\ufeff\\x009\\x85'","split":["\u0000\ufeff\u00009"],"strip":"\u0000\ufeff\u00009","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x00\\ufeff\\x009\\x85'"}, +{"text":"urn:\ud835\udfce\n=:\u0378","repr":"'urn:\ud835\udfce\\n=:\\u0378'","split":["urn:\ud835\udfce","=:\u0378"],"strip":"urn:\ud835\udfce\n=:\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:\ud835\udfce\\n=:\\u0378'"}, +{"text":" :Fuuid:\u001f=,","repr":"' :Fuuid:\\x1f=,'","split":[":Fuuid:","=,"],"strip":":Fuuid:\u001f=,","uuid":null,"int_error":"invalid literal for int() with base 10: ' :Fuuid:\\x1f=,'"}, +{"text":"\u001f01\u2028\u0663\u0085","repr":"'\\x1f01\\u2028\u0663\\x85'","split":["01","\u0663"],"strip":"01\u2028\u0663","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f01\\u2028\u0663\\x85'"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"X9\n\ud835\udfce","repr":"'X9\\n\ud835\udfce'","split":["X9","\ud835\udfce"],"strip":"X9\n\ud835\udfce","uuid":null,"int_error":"invalid literal for int() with base 10: 'X9\\n\ud835\udfce'"}, +{"text":"\u001f\u00ad_\n\u007f\u3000''","repr":"\"\\x1f\\xad_\\n\\x7f\\u3000''\"","split":["\u00ad_","\u007f","''"],"strip":"\u00ad_\n\u007f\u3000''","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\x1f\\xad_\\n\\x7f\\u3000''\""}, +{"text":"=xx\ufeff\t\u007f=","repr":"'=xx\\ufeff\\t\\x7f='","split":["=xx\ufeff","\u007f="],"strip":"=xx\ufeff\t\u007f=","uuid":null,"int_error":"invalid literal for int() with base 10: '=xx\\ufeff\\t\\x7f='"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"uuid:\u0378}0x=","repr":"'uuid:\\u0378}0x='","split":["uuid:\u0378}0x="],"strip":"uuid:\u0378}0x=","uuid":null,"int_error":"invalid literal for int() with base 10: 'uuid:\\u0378}0x='"}, +{"text":"X\ud83d\ude00\u0378xF\n{\ud83d\ude00\u0085","repr":"'X\ud83d\ude00\\u0378xF\\n{\ud83d\ude00\\x85'","split":["X\ud83d\ude00\u0378xF","{\ud83d\ude00"],"strip":"X\ud83d\ude00\u0378xF\n{\ud83d\ude00","uuid":null,"int_error":"invalid literal for int() with base 10: 'X\ud83d\ude00\\u0378xF\\n{\ud83d\ude00\\x85'"}, +{"text":"0x:,\u2028\u007f\u007f\u000b\u0663=7X","repr":"'0x:,\\u2028\\x7f\\x7f\\x0b\u0663=7X'","split":["0x:,","\u007f\u007f","\u0663=7X"],"strip":"0x:,\u2028\u007f\u007f\u000b\u0663=7X","uuid":null,"int_error":"invalid literal for int() with base 10: '0x:,\\u2028\\x7f\\x7f\\x0b\u0663=7X'"}, +{"text":"++","repr":"'++'","split":["++"],"strip":"++","uuid":null,"int_error":"invalid literal for int() with base 10: '++'"}, +{"text":"\u3000\udb40\udc01}\ud835\udfce_\ufeff\\-\u200buuid:","repr":"'\\u3000\\U000e0001}\ud835\udfce_\\ufeff\\\\-\\u200buuid:'","split":["\udb40\udc01}\ud835\udfce_\ufeff\\-\u200buuid:"],"strip":"\udb40\udc01}\ud835\udfce_\ufeff\\-\u200buuid:","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000\\U000e0001}\ud835\udfce_\\ufeff\\\\-\\u200buuid:'"}, +{"text":"\u001f\u0378\"x","repr":"'\\x1f\\u0378\"x'","split":["\u0378\"x"],"strip":"\u0378\"x","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f\\u0378\"x'"}, +{"text":"'-\u00a0\u001f\u0378urn:9\ud83d\ude00\ud835\udfce","repr":"\"'-\\xa0\\x1f\\u0378urn:9\ud83d\ude00\ud835\udfce\"","split":["'-","\u0378urn:9\ud83d\ude00\ud835\udfce"],"strip":"'-\u00a0\u001f\u0378urn:9\ud83d\ude00\ud835\udfce","uuid":null,"int_error":"invalid literal for int() with base 10: \"'-\\xa0\\x1f\\u0378urn:9\ud83d\ude00\ud835\udfce\""}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"\u0085}+\u200b\u0663\u001f+\u00e9\u2003urn:\u00a0","repr":"'\\x85}+\\u200b\u0663\\x1f+\u00e9\\u2003urn:\\xa0'","split":["}+\u200b\u0663","+\u00e9","urn:"],"strip":"}+\u200b\u0663\u001f+\u00e9\u2003urn:","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x85}+\\u200b\u0663\\x1f+\u00e9\\u2003urn:\\xa0'"}, +{"text":"\u0085\u2028","repr":"'\\x85\\u2028'","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x85\\u2028'"}, +{"text":"\u2003\ud83d\ude00,\u001c\u000b1uuid:'\u3000","repr":"\"\\u2003\ud83d\ude00,\\x1c\\x0b1uuid:'\\u3000\"","split":["\ud83d\ude00,","1uuid:'"],"strip":"\ud83d\ude00,\u001c\u000b1uuid:'","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\u2003\ud83d\ude00,\\x1c\\x0b1uuid:'\\u3000\""}, +{"text":"\u00ad","repr":"'\\xad'","split":["\u00ad"],"strip":"\u00ad","uuid":null,"int_error":"invalid literal for int() with base 10: '\\xad'"}, +{"text":"\u001f9\u200b\u000b\u200b\u2003urn:\t","repr":"'\\x1f9\\u200b\\x0b\\u200b\\u2003urn:\\t'","split":["9\u200b","\u200b","urn:"],"strip":"9\u200b\u000b\u200b\u2003urn:","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f9\\u200b\\x0b\\u200b\\u2003urn:\\t'"}, +{"text":"\n\u0378- \u0000\u001f\ud83d\ude00\ud83d\ude00+X,","repr":"'\\n\\u0378- \\x00\\x1f\ud83d\ude00\ud83d\ude00+X,'","split":["\u0378-","\u0000","\ud83d\ude00\ud83d\ude00+X,"],"strip":"\u0378- \u0000\u001f\ud83d\ude00\ud83d\ude00+X,","uuid":null,"int_error":"invalid literal for int() with base 10: '\\n\\u0378- \\x00\\x1f\ud83d\ude00\ud83d\ude00+X,'"}, +{"text":"\"\ud835\udfce,\u007f","repr":"'\"\ud835\udfce,\\x7f'","split":["\"\ud835\udfce,\u007f"],"strip":"\"\ud835\udfce,\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '\"\ud835\udfce,\\x7f'"}, +{"text":"\u00e9'\u00a0X\\:a\u2003urn:\u2028","repr":"\"\u00e9'\\xa0X\\\\:a\\u2003urn:\\u2028\"","split":["\u00e9'","X\\:a","urn:"],"strip":"\u00e9'\u00a0X\\:a\u2003urn:","uuid":null,"int_error":"invalid literal for int() with base 10: \"\u00e9'\\xa0X\\\\:a\\u2003urn:\\u2028\""}, +{"text":":","repr":"':'","split":[":"],"strip":":","uuid":null,"int_error":"invalid literal for int() with base 10: ':'"}, +{"text":"F\u0000:=\u200b\u001f_\u00e9\"x","repr":"'F\\x00:=\\u200b\\x1f_\u00e9\"x'","split":["F\u0000:=\u200b","_\u00e9\"x"],"strip":"F\u0000:=\u200b\u001f_\u00e9\"x","uuid":null,"int_error":"invalid literal for int() with base 10: 'F\\x00:=\\u200b\\x1f_\u00e9\"x'"}, +{"text":"7\u00adurn:\uff11","repr":"'7\\xadurn:\uff11'","split":["7\u00adurn:\uff11"],"strip":"7\u00adurn:\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: '7\\xadurn:\uff11'"}, +{"text":"a{,0+\"\u20280x:{\n","repr":"'a{,0+\"\\u20280x:{\\n'","split":["a{,0+\"","0x:{"],"strip":"a{,0+\"\u20280x:{","uuid":null,"int_error":"invalid literal for int() with base 10: 'a{,0+\"\\u20280x:{\\n'"}, +{"text":"\u00e9\ud83d\ude00\u00e9\udb40\udc01\udb40\udc01\u0663\"\udb40\udc01\u3000-","repr":"'\u00e9\ud83d\ude00\u00e9\\U000e0001\\U000e0001\u0663\"\\U000e0001\\u3000-'","split":["\u00e9\ud83d\ude00\u00e9\udb40\udc01\udb40\udc01\u0663\"\udb40\udc01","-"],"strip":"\u00e9\ud83d\ude00\u00e9\udb40\udc01\udb40\udc01\u0663\"\udb40\udc01\u3000-","uuid":null,"int_error":"invalid literal for int() with base 10: '\u00e9\ud83d\ude00\u00e9\\U000e0001\\U000e0001\u0663\"\\U000e0001\\u3000-'"}, +{"text":"\u2028x7 uuid:09","repr":"'\\u2028x7 uuid:09'","split":["x7","uuid:09"],"strip":"x7 uuid:09","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2028x7 uuid:09'"}, +{"text":"97\u0000\u00e9\n\u007f \u007f","repr":"'97\\x00\u00e9\\n\\x7f \\x7f'","split":["97\u0000\u00e9","\u007f","\u007f"],"strip":"97\u0000\u00e9\n\u007f \u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '97\\x00\u00e9\\n\\x7f \\x7f'"}, +{"text":"\ufeff\u2003\"x\ufeff\u200b0\u00ad\ufeffa\u007f","repr":"'\\ufeff\\u2003\"x\\ufeff\\u200b0\\xad\\ufeffa\\x7f'","split":["\ufeff","\"x\ufeff\u200b0\u00ad\ufeffa\u007f"],"strip":"\ufeff\u2003\"x\ufeff\u200b0\u00ad\ufeffa\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '\\ufeff\\u2003\"x\\ufeff\\u200b0\\xad\\ufeffa\\x7f'"}, +{"text":"0xF","repr":"'0xF'","split":["0xF"],"strip":"0xF","uuid":null,"int_error":"invalid literal for int() with base 10: '0xF'"}, +{"text":"\u001c\uff11-\uff11=uuid:\"\u0000\u0085\u001f+","repr":"'\\x1c\uff11-\uff11=uuid:\"\\x00\\x85\\x1f+'","split":["\uff11-\uff11=uuid:\"\u0000","+"],"strip":"\uff11-\uff11=uuid:\"\u0000\u0085\u001f+","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1c\uff11-\uff11=uuid:\"\\x00\\x85\\x1f+'"}, +{"text":"0x","repr":"'0x'","split":["0x"],"strip":"0x","uuid":null,"int_error":"invalid literal for int() with base 10: '0x'"}, +{"text":"\u200b+\u06630x\ud83d\ude00\u00e9}","repr":"'\\u200b+\u06630x\ud83d\ude00\u00e9}'","split":["\u200b+\u06630x\ud83d\ude00\u00e9}"],"strip":"\u200b+\u06630x\ud83d\ude00\u00e9}","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u200b+\u06630x\ud83d\ude00\u00e9}'"}, +{"text":"","repr":"''","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: ''"}, +{"text":"7\"{","repr":"'7\"{'","split":["7\"{"],"strip":"7\"{","uuid":null,"int_error":"invalid literal for int() with base 10: '7\"{'"}, +{"text":"=1\u001c\u0000","repr":"'=1\\x1c\\x00'","split":["=1","\u0000"],"strip":"=1\u001c\u0000","uuid":null,"int_error":"invalid literal for int() with base 10: '=1\\x1c\\x00'"}, +{"text":"\ufeff\u000b","repr":"'\\ufeff\\x0b'","split":["\ufeff"],"strip":"\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: '\\ufeff\\x0b'"}, +{"text":"770x","repr":"'770x'","split":["770x"],"strip":"770x","uuid":null,"int_error":"invalid literal for int() with base 10: '770x'"}, +{"text":"+\u00ad\u00ad\u03781\u00e9\u0000","repr":"'+\\xad\\xad\\u03781\u00e9\\x00'","split":["+\u00ad\u00ad\u03781\u00e9\u0000"],"strip":"+\u00ad\u00ad\u03781\u00e9\u0000","uuid":null,"int_error":"invalid literal for int() with base 10: '+\\xad\\xad\\u03781\u00e9\\x00'"}, +{"text":"9:\u00e9\u000b1\uff11+\uff11\ud835\udfce\n","repr":"'9:\u00e9\\x0b1\uff11+\uff11\ud835\udfce\\n'","split":["9:\u00e9","1\uff11+\uff11\ud835\udfce"],"strip":"9:\u00e9\u000b1\uff11+\uff11\ud835\udfce","uuid":null,"int_error":"invalid literal for int() with base 10: '9:\u00e9\\x0b1\uff11+\uff11\ud835\udfce\\n'"}, +{"text":"\u200b-\u000b{\u2003\u0000\u30000+:","repr":"'\\u200b-\\x0b{\\u2003\\x00\\u30000+:'","split":["\u200b-","{","\u0000","0+:"],"strip":"\u200b-\u000b{\u2003\u0000\u30000+:","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u200b-\\x0b{\\u2003\\x00\\u30000+:'"}, +{"text":"x\u00a0\t\u0000\u200bx\ufeff","repr":"'x\\xa0\\t\\x00\\u200bx\\ufeff'","split":["x","\u0000\u200bx\ufeff"],"strip":"x\u00a0\t\u0000\u200bx\ufeff","uuid":null,"int_error":"invalid literal for int() with base 10: 'x\\xa0\\t\\x00\\u200bx\\ufeff'"}, +{"text":"\\\u00e9\\a\t\u00a0\udb40\udc01,urn:auuid:}","repr":"'\\\\\u00e9\\\\a\\t\\xa0\\U000e0001,urn:auuid:}'","split":["\\\u00e9\\a","\udb40\udc01,urn:auuid:}"],"strip":"\\\u00e9\\a\t\u00a0\udb40\udc01,urn:auuid:}","uuid":null,"int_error":"invalid literal for int() with base 10: '\\\\\u00e9\\\\a\\t\\xa0\\U000e0001,urn:auuid:}'"}, +{"text":"\\\uff11\uff11a7F\tX\u0378","repr":"'\\\\\uff11\uff11a7F\\tX\\u0378'","split":["\\\uff11\uff11a7F","X\u0378"],"strip":"\\\uff11\uff11a7F\tX\u0378","uuid":null,"int_error":"invalid literal for int() with base 10: '\\\\\uff11\uff11a7F\\tX\\u0378'"}, +{"text":"xF0","repr":"'xF0'","split":["xF0"],"strip":"xF0","uuid":null,"int_error":"invalid literal for int() with base 10: 'xF0'"}, +{"text":"\u3000uuid:\u2003\uff11\n{uuid:0x+","repr":"'\\u3000uuid:\\u2003\uff11\\n{uuid:0x+'","split":["uuid:","\uff11","{uuid:0x+"],"strip":"uuid:\u2003\uff11\n{uuid:0x+","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000uuid:\\u2003\uff11\\n{uuid:0x+'"}, +{"text":"\u0663\uff11\u000b\u00009urn:\u0085\u00ad","repr":"'\u0663\uff11\\x0b\\x009urn:\\x85\\xad'","split":["\u0663\uff11","\u00009urn:","\u00ad"],"strip":"\u0663\uff11\u000b\u00009urn:\u0085\u00ad","uuid":null,"int_error":"invalid literal for int() with base 10: '\u0663\uff11\\x0b\\x009urn:\\x85\\xad'"}, +{"text":"7+","repr":"'7+'","split":["7+"],"strip":"7+","uuid":null,"int_error":"invalid literal for int() with base 10: '7+'"}, +{"text":"\ud83d\ude00\"\u007f\u001c\u001c0x\u0000\\\u00ad \uff11","repr":"'\ud83d\ude00\"\\x7f\\x1c\\x1c0x\\x00\\\\\\xad \uff11'","split":["\ud83d\ude00\"\u007f","0x\u0000\\\u00ad","\uff11"],"strip":"\ud83d\ude00\"\u007f\u001c\u001c0x\u0000\\\u00ad \uff11","uuid":null,"int_error":"invalid literal for int() with base 10: '\ud83d\ude00\"\\x7f\\x1c\\x1c0x\\x00\\\\\\xad \uff11'"}, +{"text":"\u0378++\u200b+\ufeff}\u0085\"\udb40\udc01\uff11X","repr":"'\\u0378++\\u200b+\\ufeff}\\x85\"\\U000e0001\uff11X'","split":["\u0378++\u200b+\ufeff}","\"\udb40\udc01\uff11X"],"strip":"\u0378++\u200b+\ufeff}\u0085\"\udb40\udc01\uff11X","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u0378++\\u200b+\\ufeff}\\x85\"\\U000e0001\uff11X'"}, +{"text":"\u0663x\u3000\ud83d\ude00\u0000\u00ad{\u0378urn:{F9","repr":"'\u0663x\\u3000\ud83d\ude00\\x00\\xad{\\u0378urn:{F9'","split":["\u0663x","\ud83d\ude00\u0000\u00ad{\u0378urn:{F9"],"strip":"\u0663x\u3000\ud83d\ude00\u0000\u00ad{\u0378urn:{F9","uuid":null,"int_error":"invalid literal for int() with base 10: '\u0663x\\u3000\ud83d\ude00\\x00\\xad{\\u0378urn:{F9'"}, +{"text":"\u2003\udb40\udc01:\u0378'X","repr":"\"\\u2003\\U000e0001:\\u0378'X\"","split":["\udb40\udc01:\u0378'X"],"strip":"\udb40\udc01:\u0378'X","uuid":null,"int_error":"invalid literal for int() with base 10: \"\\u2003\\U000e0001:\\u0378'X\""}, +{"text":"\u007f_\u00e9","repr":"'\\x7f_\u00e9'","split":["\u007f_\u00e9"],"strip":"\u007f_\u00e9","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x7f_\u00e9'"}, +{"text":"\u00007\ufeff\ud835\udfce,\n\u00e9\u2028\u2003-\ud835\udfce\u000b","repr":"'\\x007\\ufeff\ud835\udfce,\\n\u00e9\\u2028\\u2003-\ud835\udfce\\x0b'","split":["\u00007\ufeff\ud835\udfce,","\u00e9","-\ud835\udfce"],"strip":"\u00007\ufeff\ud835\udfce,\n\u00e9\u2028\u2003-\ud835\udfce","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x007\\ufeff\ud835\udfce,\\n\u00e9\\u2028\\u2003-\ud835\udfce\\x0b'"}, +{"text":"\u2003_\u001f0x7","repr":"'\\u2003_\\x1f0x7'","split":["_","0x7"],"strip":"_\u001f0x7","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u2003_\\x1f0x7'"}, +{"text":"\u3000+\u00ad\ud83d\ude00\u0663\u00e9","repr":"'\\u3000+\\xad\ud83d\ude00\u0663\u00e9'","split":["+\u00ad\ud83d\ude00\u0663\u00e9"],"strip":"+\u00ad\ud83d\ude00\u0663\u00e9","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000+\\xad\ud83d\ude00\u0663\u00e9'"}, +{"text":"0xX\u000b{\u3000}x+X}","repr":"'0xX\\x0b{\\u3000}x+X}'","split":["0xX","{","}x+X}"],"strip":"0xX\u000b{\u3000}x+X}","uuid":null,"int_error":"invalid literal for int() with base 10: '0xX\\x0b{\\u3000}x+X}'"}, +{"text":"\u3000\u0000","repr":"'\\u3000\\x00'","split":["\u0000"],"strip":"\u0000","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000\\x00'"}, +{"text":"\u001f\t\u007furn:+\udb40\udc01\u001f\ufeff\uff117","repr":"'\\x1f\\t\\x7furn:+\\U000e0001\\x1f\\ufeff\uff117'","split":["\u007furn:+\udb40\udc01","\ufeff\uff117"],"strip":"\u007furn:+\udb40\udc01\u001f\ufeff\uff117","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f\\t\\x7furn:+\\U000e0001\\x1f\\ufeff\uff117'"}, +{"text":"-\ufeff\u00e9\u0085\n\u3000uuid:\u0663'9_urn:","repr":"\"-\\ufeff\u00e9\\x85\\n\\u3000uuid:\u0663'9_urn:\"","split":["-\ufeff\u00e9","uuid:\u0663'9_urn:"],"strip":"-\ufeff\u00e9\u0085\n\u3000uuid:\u0663'9_urn:","uuid":null,"int_error":"invalid literal for int() with base 10: \"-\\ufeff\u00e9\\x85\\n\\u3000uuid:\u0663'9_urn:\""}, +{"text":"0-\ufeff\u001c-_\"\udb40\udc01\u00e9\t,0x","repr":"'0-\\ufeff\\x1c-_\"\\U000e0001\u00e9\\t,0x'","split":["0-\ufeff","-_\"\udb40\udc01\u00e9",",0x"],"strip":"0-\ufeff\u001c-_\"\udb40\udc01\u00e9\t,0x","uuid":null,"int_error":"invalid literal for int() with base 10: '0-\\ufeff\\x1c-_\"\\U000e0001\u00e9\\t,0x'"}, +{"text":"a\u200b\u00ad\u000b\u2003urn:a","repr":"'a\\u200b\\xad\\x0b\\u2003urn:a'","split":["a\u200b\u00ad","urn:a"],"strip":"a\u200b\u00ad\u000b\u2003urn:a","uuid":null,"int_error":"invalid literal for int() with base 10: 'a\\u200b\\xad\\x0b\\u2003urn:a'"}, +{"text":"\u001c=\u007f","repr":"'\\x1c=\\x7f'","split":["=\u007f"],"strip":"=\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1c=\\x7f'"}, +{"text":"\n\u00a0","repr":"'\\n\\xa0'","split":[],"strip":"","uuid":null,"int_error":"invalid literal for int() with base 10: '\\n\\xa0'"}, +{"text":"F\u2003\udb40\udc01,\u2003\t\n\u2028\"","repr":"'F\\u2003\\U000e0001,\\u2003\\t\\n\\u2028\"'","split":["F","\udb40\udc01,","\""],"strip":"F\u2003\udb40\udc01,\u2003\t\n\u2028\"","uuid":null,"int_error":"invalid literal for int() with base 10: 'F\\u2003\\U000e0001,\\u2003\\t\\n\\u2028\"'"}, +{"text":"\u0663\\\uff11\u001f","repr":"'\u0663\\\\\uff11\\x1f'","split":["\u0663\\\uff11"],"strip":"\u0663\\\uff11","uuid":null,"int_error":"invalid literal for int() with base 10: '\u0663\\\\\uff11\\x1f'"}, +{"text":"\u3000\u00007_\u00a00x70\udb40\udc01","repr":"'\\u3000\\x007_\\xa00x70\\U000e0001'","split":["\u00007_","0x70\udb40\udc01"],"strip":"\u00007_\u00a00x70\udb40\udc01","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u3000\\x007_\\xa00x70\\U000e0001'"}, +{"text":"}","repr":"'}'","split":["}"],"strip":"}","uuid":null,"int_error":"invalid literal for int() with base 10: '}'"}, +{"text":"\u0378_x,: 0x9","repr":"'\\u0378_x,: 0x9'","split":["\u0378_x,:","0x9"],"strip":"\u0378_x,: 0x9","uuid":null,"int_error":"invalid literal for int() with base 10: '\\u0378_x,: 0x9'"}, +{"text":"12345678-1234-5678-1234-5678112345678","repr":"'12345678-1234-5678-1234-5678112345678'","split":["12345678-1234-5678-1234-5678112345678"],"strip":"12345678-1234-5678-1234-5678112345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-5678112345678'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-56712345678","repr":"'urn:uuid:12345678-1234-5678-1234-56712345678'","split":["urn:uuid:12345678-1234-5678-1234-56712345678"],"strip":"urn:uuid:12345678-1234-5678-1234-56712345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-56712345678'"}, +{"text":"{1234568-1234-5678-1234-567812345678}","repr":"'{1234568-1234-5678-1234-567812345678}'","split":["{1234568-1234-5678-1234-567812345678}"],"strip":"{1234568-1234-5678-1234-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{1234568-1234-5678-1234-567812345678}'"}, +{"text":"12345678-12384-5678-1 34-57812345678","repr":"'12345678-12384-5678-1 34-57812345678'","split":["12345678-12384-5678-1","34-57812345678"],"strip":"12345678-12384-5678-1 34-57812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-12384-5678-1 34-57812345678'"}, +{"text":"{12345678-12x4-567\u00a08-1234-56781234568}","repr":"'{12345678-12x4-567\\xa08-1234-56781234568}'","split":["{12345678-12x4-567","8-1234-56781234568}"],"strip":"{12345678-12x4-567\u00a08-1234-56781234568}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-12x4-567\\xa08-1234-56781234568}'"}, +{"text":"{12345678-1234-5678\u001f-1234-5678123455678_","repr":"'{12345678-1234-5678\\x1f-1234-5678123455678_'","split":["{12345678-1234-5678","-1234-5678123455678_"],"strip":"{12345678-1234-5678\u001f-1234-5678123455678_","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5678\\x1f-1234-5678123455678_'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678uuid:1234-5678-1234-567812345678","repr":"'12345678uuid:1234-5678-1234-567812345678'","split":["12345678uuid:1234-5678-1234-567812345678"],"strip":"12345678uuid:1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678uuid:1234-5678-1234-567812345678'"}, +{"text":"12345678\u001f-1234-5678-124-56812345678","repr":"'12345678\\x1f-1234-5678-124-56812345678'","split":["12345678","-1234-5678-124-56812345678"],"strip":"12345678\u001f-1234-5678-124-56812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678\\x1f-1234-5678-124-56812345678'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-56781234578","repr":"'urn:uuid:12345678-1234-5678-1234-56781234578'","split":["urn:uuid:12345678-1234-5678-1234-56781234578"],"strip":"urn:uuid:12345678-1234-5678-1234-56781234578","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-56781234578'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"urn:uuid:12345678-124-5678-1234-567812345678","repr":"'urn:uuid:12345678-124-5678-1234-567812345678'","split":["urn:uuid:12345678-124-5678-1234-567812345678"],"strip":"urn:uuid:12345678-124-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-124-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-2349-5678112345678","repr":"'12345678-1234-5678-2349-5678112345678'","split":["12345678-1234-5678-2349-5678112345678"],"strip":"12345678-1234-5678-2349-5678112345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-2349-5678112345678'"}, +{"text":"1234567812345678123456781345678","repr":"'1234567812345678123456781345678'","split":["1234567812345678123456781345678"],"strip":"1234567812345678123456781345678","uuid":null,"int":"1234567812345678123456781345678"}, +{"text":"7rn:uuid:12345678-12345678-1234-56781234B678","repr":"'7rn:uuid:12345678-12345678-1234-56781234B678'","split":["7rn:uuid:12345678-12345678-1234-56781234B678"],"strip":"7rn:uuid:12345678-12345678-1234-56781234B678","uuid":null,"int_error":"invalid literal for int() with base 10: '7rn:uuid:12345678-12345678-1234-56781234B678'"}, +{"text":"1234578123456781F34567812345678","repr":"'1234578123456781F34567812345678'","split":["1234578123456781F34567812345678"],"strip":"1234578123456781F34567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234578123456781F34567812345678'"}, +{"text":"1234567812345678123456781245678","repr":"'1234567812345678123456781245678'","split":["1234567812345678123456781245678"],"strip":"1234567812345678123456781245678","uuid":null,"int":"1234567812345678123456781245678"}, +{"text":"12345678-124-5678-1234-56781\u00ad2345678","repr":"'12345678-124-5678-1234-56781\\xad2345678'","split":["12345678-124-5678-1234-56781\u00ad2345678"],"strip":"12345678-124-5678-1234-56781\u00ad2345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-124-5678-1234-56781\\xad2345678'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-1234-56781245678","repr":"'12345678-1234-5678-1234-56781245678'","split":["12345678-1234-5678-1234-56781245678"],"strip":"12345678-1234-5678-1234-56781245678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-56781245678'"}, +{"text":"1234567812456781\ud835\udfce34567812345678","repr":"'1234567812456781\ud835\udfce34567812345678'","split":["1234567812456781\ud835\udfce34567812345678"],"strip":"1234567812456781\ud835\udfce34567812345678","uuid":null,"int":"1234567812456781034567812345678"}, +{"text":"{12345678-1234-5\u2028678-1234-56781234}78}","repr":"'{12345678-1234-5\\u2028678-1234-56781234}78}'","split":["{12345678-1234-5","678-1234-56781234}78}"],"strip":"{12345678-1234-5\u2028678-1234-56781234}78}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5\\u2028678-1234-56781234}78}'"}, +{"text":"urn:uuid:12345698-1234-5678-1234-567812345678","repr":"'urn:uuid:12345698-1234-5678-1234-567812345678'","split":["urn:uuid:12345698-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345698-1234-5678-1234-567812345678","uuid":"12345698-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345698-1234-5678-1234-567812345678'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-56812345678","repr":"'urn:uuid:12345678-1234-5678-1234-56812345678'","split":["urn:uuid:12345678-1234-5678-1234-56812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-56812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-56812345678'"}, +{"text":"urn:uu3d:2345678-1234-5678B-1234-567812345678","repr":"'urn:uu3d:2345678-1234-5678B-1234-567812345678'","split":["urn:uu3d:2345678-1234-5678B-1234-567812345678"],"strip":"urn:uu3d:2345678-1234-5678B-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uu3d:2345678-1234-5678B-1234-567812345678'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-1534-56782345678","repr":"'12345678-1234-5678-1534-56782345678'","split":["12345678-1234-5678-1534-56782345678"],"strip":"12345678-1234-5678-1534-56782345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1534-56782345678'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"12345678123456781234567812a45678","repr":"'12345678123456781234567812a45678'","split":["12345678123456781234567812a45678"],"strip":"12345678123456781234567812a45678","uuid":"12345678-1234-5678-1234-567812a45678","int_error":"invalid literal for int() with base 10: '12345678123456781234567812a45678'"}, +{"text":"123456781234567823456781234\u0378567\udb40\udc018","repr":"'123456781234567823456781234\\u0378567\\U000e00018'","split":["123456781234567823456781234\u0378567\udb40\udc018"],"strip":"123456781234567823456781234\u0378567\udb40\udc018","uuid":null,"int_error":"invalid literal for int() with base 10: '123456781234567823456781234\\u0378567\\U000e00018'"}, +{"text":"{1245678-1234-5678-1234-567812345678}","repr":"'{1245678-1234-5678-1234-567812345678}'","split":["{1245678-1234-5678-1234-567812345678}"],"strip":"{1245678-1234-5678-1234-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{1245678-1234-5678-1234-567812345678}'"}, +{"text":"urn\\uuid:12345678-1234-5678\u20281234-567812=45678","repr":"'urn\\\\uuid:12345678-1234-5678\\u20281234-567812=45678'","split":["urn\\uuid:12345678-1234-5678","1234-567812=45678"],"strip":"urn\\uuid:12345678-1234-5678\u20281234-567812=45678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn\\\\uuid:12345678-1234-5678\\u20281234-567812=45678'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"123456778-1234-5678-1234-567812345678","repr":"'123456778-1234-5678-1234-567812345678'","split":["123456778-1234-5678-1234-567812345678"],"strip":"123456778-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '123456778-1234-5678-1234-567812345678'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"12345678\u00a01234-5678-1234-567812-345678","repr":"'12345678\\xa01234-5678-1234-567812-345678'","split":["12345678","1234-5678-1234-567812-345678"],"strip":"12345678\u00a01234-5678-1234-567812-345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678\\xa01234-5678-1234-567812-345678'"}, +{"text":"1234567812345678123456781234568","repr":"'1234567812345678123456781234568'","split":["1234567812345678123456781234568"],"strip":"1234567812345678123456781234568","uuid":null,"int":"1234567812345678123456781234568"}, +{"text":"123456781234567812567812345678","repr":"'123456781234567812567812345678'","split":["123456781234567812567812345678"],"strip":"123456781234567812567812345678","uuid":null,"int":"123456781234567812567812345678"}, +{"text":"x123456781234678123456781234678","repr":"'x123456781234678123456781234678'","split":["x123456781234678123456781234678"],"strip":"x123456781234678123456781234678","uuid":null,"int_error":"invalid literal for int() with base 10: 'x123456781234678123456781234678'"}, +{"text":"12345678-1234\u00a0567\ufeff8-123c4-567812345678","repr":"'12345678-1234\\xa0567\\ufeff8-123c4-567812345678'","split":["12345678-1234","567\ufeff8-123c4-567812345678"],"strip":"12345678-1234\u00a0567\ufeff8-123c4-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234\\xa0567\\ufeff8-123c4-567812345678'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"123456\u066378\u00851234-568-1234-567812345678","repr":"'123456\u066378\\x851234-568-1234-567812345678'","split":["123456\u066378","1234-568-1234-567812345678"],"strip":"123456\u066378\u00851234-568-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '123456\u066378\\x851234-568-1234-567812345678'"}, +{"text":"urn:uuid\udb40\udc0113456788-1234-5678-1234-567812345678","repr":"'urn:uuid\\U000e000113456788-1234-5678-1234-567812345678'","split":["urn:uuid\udb40\udc0113456788-1234-5678-1234-567812345678"],"strip":"urn:uuid\udb40\udc0113456788-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid\\U000e000113456788-1234-5678-1234-567812345678'"}, +{"text":"123456778-1234-5678-1234-567812345678","repr":"'123456778-1234-5678-1234-567812345678'","split":["123456778-1234-5678-1234-567812345678"],"strip":"123456778-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '123456778-1234-5678-1234-567812345678'"}, +{"text":"12345678-123467-1234-567812345678","repr":"'12345678-123467-1234-567812345678'","split":["12345678-123467-1234-567812345678"],"strip":"12345678-123467-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-123467-1234-567812345678'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"12345678-12 34-5678-1234-5678\ufeff2345678","repr":"'12345678-12 34-5678-1234-5678\\ufeff2345678'","split":["12345678-12","34-5678-1234-5678\ufeff2345678"],"strip":"12345678-12 34-5678-1234-5678\ufeff2345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-12 34-5678-1234-5678\\ufeff2345678'"}, +{"text":"1234568-12\ufeff34-5678-1234-5\ufeff67812345678","repr":"'1234568-12\\ufeff34-5678-1234-5\\ufeff67812345678'","split":["1234568-12\ufeff34-5678-1234-5\ufeff67812345678"],"strip":"1234568-12\ufeff34-5678-1234-5\ufeff67812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234568-12\\ufeff34-5678-1234-5\\ufeff67812345678'"}, +{"text":"123456781234567812345678123458","repr":"'123456781234567812345678123458'","split":["123456781234567812345678123458"],"strip":"123456781234567812345678123458","uuid":null,"int":"123456781234567812345678123458"}, +{"text":"{12345678-1234-5678-1234-56781236 45678}","repr":"'{12345678-1234-5678-1234-56781236 45678}'","split":["{12345678-1234-5678-1234-56781236","45678}"],"strip":"{12345678-1234-5678-1234-56781236 45678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-56781236 45678}'"}, +{"text":"{12345678-1234-5678-1234-56781234567'}","repr":"\"{12345678-1234-5678-1234-56781234567'}\"","split":["{12345678-1234-5678-1234-56781234567'}"],"strip":"{12345678-1234-5678-1234-56781234567'}","uuid":null,"int_error":"invalid literal for int() with base 10: \"{12345678-1234-5678-1234-56781234567'}\""}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"urn:uuid:1\u000045678-1234-5678-123uuid:-567812345678","repr":"'urn:uuid:1\\x0045678-1234-5678-123uuid:-567812345678'","split":["urn:uuid:1\u000045678-1234-5678-123uuid:-567812345678"],"strip":"urn:uuid:1\u000045678-1234-5678-123uuid:-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:1\\x0045678-1234-5678-123uuid:-567812345678'"}, +{"text":"1234567812345678123467812345678","repr":"'1234567812345678123467812345678'","split":["1234567812345678123467812345678"],"strip":"1234567812345678123467812345678","uuid":null,"int":"1234567812345678123467812345678"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"312345678-1234-5678-1234-567812345678}","repr":"'312345678-1234-5678-1234-567812345678}'","split":["312345678-1234-5678-1234-567812345678}"],"strip":"312345678-1234-5678-1234-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '312345678-1234-5678-1234-567812345678}'"}, +{"text":"123456781234567812345678\u00a0234568","repr":"'123456781234567812345678\\xa0234568'","split":["123456781234567812345678","234568"],"strip":"123456781234567812345678\u00a0234568","uuid":null,"int_error":"invalid literal for int() with base 10: '123456781234567812345678\\xa0234568'"}, +{"text":"1235678-1234-5678-1234-567812345678","repr":"'1235678-1234-5678-1234-567812345678'","split":["1235678-1234-5678-1234-567812345678"],"strip":"1235678-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1235678-1234-5678-1234-567812345678'"}, +{"text":"12347678-1234-5678-2349-567812345678","repr":"'12347678-1234-5678-2349-567812345678'","split":["12347678-1234-5678-2349-567812345678"],"strip":"12347678-1234-5678-2349-567812345678","uuid":"12347678-1234-5678-2349-567812345678","int_error":"invalid literal for int() with base 10: '12347678-1234-5678-2349-567812345678'"}, +{"text":"12345678-1234-5678-1f34-567812345678","repr":"'12345678-1234-5678-1f34-567812345678'","split":["12345678-1234-5678-1f34-567812345678"],"strip":"12345678-1234-5678-1f34-567812345678","uuid":"12345678-1234-5678-1f34-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1f34-567812345678'"}, +{"text":"123456781234567812345678123456718","repr":"'123456781234567812345678123456718'","split":["123456781234567812345678123456718"],"strip":"123456781234567812345678123456718","uuid":null,"int":"123456781234567812345678123456718"}, +{"text":"{12345678-1uuid:34,-5678-1234-567812345678}","repr":"'{12345678-1uuid:34,-5678-1234-567812345678}'","split":["{12345678-1uuid:34,-5678-1234-567812345678}"],"strip":"{12345678-1uuid:34,-5678-1234-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1uuid:34,-5678-1234-567812345678}'"}, +{"text":"{12345678-12,4-567 -1234-5678123456478}","repr":"'{12345678-12,4-567 -1234-5678123456478}'","split":["{12345678-12,4-567","-1234-5678123456478}"],"strip":"{12345678-12,4-567 -1234-5678123456478}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-12,4-567 -1234-5678123456478}'"}, +{"text":"urn:uuid:123f5678-1234-5678-1234-5678123{5678","repr":"'urn:uuid:123f5678-1234-5678-1234-5678123{5678'","split":["urn:uuid:123f5678-1234-5678-1234-5678123{5678"],"strip":"urn:uuid:123f5678-1234-5678-1234-5678123{5678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:123f5678-1234-5678-1234-5678123{5678'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"123456781234567812456781234567\u00e98","repr":"'123456781234567812456781234567\u00e98'","split":["123456781234567812456781234567\u00e98"],"strip":"123456781234567812456781234567\u00e98","uuid":null,"int_error":"invalid literal for int() with base 10: '123456781234567812456781234567\u00e98'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"12-345678-1234-5678d1234-567812345678","repr":"'12-345678-1234-5678d1234-567812345678'","split":["12-345678-1234-5678d1234-567812345678"],"strip":"12-345678-1234-5678d1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12-345678-1234-5678d1234-567812345678'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"12345678-1234-5678-\ud83d\ude00234-567812345678","repr":"'12345678-1234-5678-\ud83d\ude00234-567812345678'","split":["12345678-1234-5678-\ud83d\ude00234-567812345678"],"strip":"12345678-1234-5678-\ud83d\ude00234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-\ud83d\ude00234-567812345678'"}, +{"text":"1234568-1234-5678-1234567812345678","repr":"'1234568-1234-5678-1234567812345678'","split":["1234568-1234-5678-1234567812345678"],"strip":"1234568-1234-5678-1234567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234568-1234-5678-1234567812345678'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-1234-\uff11567812345678","repr":"'12345678-1234-5678-1234-\uff11567812345678'","split":["12345678-1234-5678-1234-\uff11567812345678"],"strip":"12345678-1234-5678-1234-\uff11567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-\uff11567812345678'"}, +{"text":"1234B678-1234-5678-1234-5627812345678","repr":"'1234B678-1234-5678-1234-5627812345678'","split":["1234B678-1234-5678-1234-5627812345678"],"strip":"1234B678-1234-5678-1234-5627812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234B678-1234-5678-1234-5627812345678'"}, +{"text":"urn:uuid:12345678-12-4-\uff11678-1234-567812345678","repr":"'urn:uuid:12345678-12-4-\uff11678-1234-567812345678'","split":["urn:uuid:12345678-12-4-\uff11678-1234-567812345678"],"strip":"urn:uuid:12345678-12-4-\uff11678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-12-4-\uff11678-1234-567812345678'"}, +{"text":"12345678-1234-678-123\u007f4-567812345678","repr":"'12345678-1234-678-123\\x7f4-567812345678'","split":["12345678-1234-678-123\u007f4-567812345678"],"strip":"12345678-1234-678-123\u007f4-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-678-123\\x7f4-567812345678'"}, +{"text":"{12345678-1234-5678-1\u001c234-567812345\u2003678}","repr":"'{12345678-1234-5678-1\\x1c234-567812345\\u2003678}'","split":["{12345678-1234-5678-1","234-567812345","678}"],"strip":"{12345678-1234-5678-1\u001c234-567812345\u2003678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1\\x1c234-567812345\\u2003678}'"}, +{"text":"urn:uuid:1234567\\8-1234-56738-1234-5678123456\"8","repr":"'urn:uuid:1234567\\\\8-1234-56738-1234-5678123456\"8'","split":["urn:uuid:1234567\\8-1234-56738-1234-5678123456\"8"],"strip":"urn:uuid:1234567\\8-1234-56738-1234-5678123456\"8","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:1234567\\\\8-1234-56738-1234-5678123456\"8'"}, +{"text":"12345678-1234-5678-123}-567812345678","repr":"'12345678-1234-5678-123}-567812345678'","split":["12345678-1234-5678-123}-567812345678"],"strip":"12345678-1234-5678-123}-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-123}-567812345678'"}, +{"text":"12345678-12\u00a034-5678-1234-567812345678","repr":"'12345678-12\\xa034-5678-1234-567812345678'","split":["12345678-12","34-5678-1234-567812345678"],"strip":"12345678-12\u00a034-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-12\\xa034-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-1234-567812345\u0085678","repr":"'12345678-1234-5678-1234-567812345\\x85678'","split":["12345678-1234-5678-1234-567812345","678"],"strip":"12345678-1234-5678-1234-567812345\u0085678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345\\x85678'"}, +{"text":"urn:uuid:12345678-12\u001f4-5678-1234-567812345678","repr":"'urn:uuid:12345678-12\\x1f4-5678-1234-567812345678'","split":["urn:uuid:12345678-12","4-5678-1234-567812345678"],"strip":"urn:uuid:12345678-12\u001f4-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-12\\x1f4-5678-1234-567812345678'"}, +{"text":"{12345678-1234-5678-\u3000123-56781-345678}","repr":"'{12345678-1234-5678-\\u3000123-56781-345678}'","split":["{12345678-1234-5678-","123-56781-345678}"],"strip":"{12345678-1234-5678-\u3000123-56781-345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-\\u3000123-56781-345678}'"}, +{"text":"{12345678-1234-5678-1b234-567812345678}","repr":"'{12345678-1234-5678-1b234-567812345678}'","split":["{12345678-1234-5678-1b234-567812345678}"],"strip":"{12345678-1234-5678-1b234-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1b234-567812345678}'"}, +{"text":"urn:uuid:2345678-1234-5678-1234-567\t12345678","repr":"'urn:uuid:2345678-1234-5678-1234-567\\t12345678'","split":["urn:uuid:2345678-1234-5678-1234-567","12345678"],"strip":"urn:uuid:2345678-1234-5678-1234-567\t12345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:2345678-1234-5678-1234-567\\t12345678'"}, +{"text":"urn:uuid:12345678-\u00a0234-5678-1234-567812345678","repr":"'urn:uuid:12345678-\\xa0234-5678-1234-567812345678'","split":["urn:uuid:12345678-","234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-\u00a0234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-\\xa0234-5678-1234-567812345678'"}, +{"text":"12345678-134-5678-1234-5678123456\u000b7","repr":"'12345678-134-5678-1234-5678123456\\x0b7'","split":["12345678-134-5678-1234-5678123456","7"],"strip":"12345678-134-5678-1234-5678123456\u000b7","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-134-5678-1234-5678123456\\x0b7'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"{1245678-1234-5678-1234-567812345678}","repr":"'{1245678-1234-5678-1234-567812345678}'","split":["{1245678-1234-5678-1234-567812345678}"],"strip":"{1245678-1234-5678-1234-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{1245678-1234-5678-1234-567812345678}'"}, +{"text":"123456781234-5678-1234-5678123\uff115678","repr":"'123456781234-5678-1234-5678123\uff115678'","split":["123456781234-5678-1234-5678123\uff115678"],"strip":"123456781234-5678-1234-5678123\uff115678","uuid":"12345678-1234-5678-1234-567812315678","int_error":"invalid literal for int() with base 10: '123456781234-5678-1234-5678123\uff115678'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"12345678\u0000-1234-5678-1234-567812345678","repr":"'12345678\\x00-1234-5678-1234-567812345678'","split":["12345678\u0000-1234-5678-1234-567812345678"],"strip":"12345678\u0000-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678\\x00-1234-5678-1234-567812345678'"}, +{"text":"{1345678-1234-5678-1234-56782345678-","repr":"'{1345678-1234-5678-1234-56782345678-'","split":["{1345678-1234-5678-1234-56782345678-"],"strip":"{1345678-1234-5678-1234-56782345678-","uuid":null,"int_error":"invalid literal for int() with base 10: '{1345678-1234-5678-1234-56782345678-'"}, +{"text":"12345678-1234-5678-1234-56781245678","repr":"'12345678-1234-5678-1234-56781245678'","split":["12345678-1234-5678-1234-56781245678"],"strip":"12345678-1234-5678-1234-56781245678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-56781245678'"}, +{"text":"12345678-1234-567-1234-56812345678","repr":"'12345678-1234-567-1234-56812345678'","split":["12345678-1234-567-1234-56812345678"],"strip":"12345678-1234-567-1234-56812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-567-1234-56812345678'"}, +{"text":"12345678_-1234-5678-1234-56781\uff11345678","repr":"'12345678_-1234-5678-1234-56781\uff11345678'","split":["12345678_-1234-5678-1234-56781\uff11345678"],"strip":"12345678_-1234-5678-1234-56781\uff11345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678_-1234-5678-1234-56781\uff11345678'"}, +{"text":"1214578-1234-5678-12a4-567812345678","repr":"'1214578-1234-5678-12a4-567812345678'","split":["1214578-1234-5678-12a4-567812345678"],"strip":"1214578-1234-5678-12a4-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1214578-1234-5678-12a4-567812345678'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"urnuuid:12345678-1234-567A8-1234-567812345678","repr":"'urnuuid:12345678-1234-567A8-1234-567812345678'","split":["urnuuid:12345678-1234-567A8-1234-567812345678"],"strip":"urnuuid:12345678-1234-567A8-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urnuuid:12345678-1234-567A8-1234-567812345678'"}, +{"text":"1234 5678e123456781234567812345678","repr":"'1234 5678e123456781234567812345678'","split":["1234","5678e123456781234567812345678"],"strip":"1234 5678e123456781234567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234 5678e123456781234567812345678'"}, +{"text":"1\u0663345678-1234-b678-1734-567812345678","repr":"'1\u0663345678-1234-b678-1734-567812345678'","split":["1\u0663345678-1234-b678-1734-567812345678"],"strip":"1\u0663345678-1234-b678-1734-567812345678","uuid":"13345678-1234-b678-1734-567812345678","int_error":"invalid literal for int() with base 10: '1\u0663345678-1234-b678-1734-567812345678'"}, +{"text":"123456781234567812346781345678","repr":"'123456781234567812346781345678'","split":["123456781234567812346781345678"],"strip":"123456781234567812346781345678","uuid":null,"int":"123456781234567812346781345678"}, +{"text":"{12345678-1234-56768-1234-56781234E56d78}","repr":"'{12345678-1234-56768-1234-56781234E56d78}'","split":["{12345678-1234-56768-1234-56781234E56d78}"],"strip":"{12345678-1234-56768-1234-56781234E56d78}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-56768-1234-56781234E56d78}'"}, +{"text":"{12345678-1234-5678-1234-567812f45678}","repr":"'{12345678-1234-5678-1234-567812f45678}'","split":["{12345678-1234-5678-1234-567812f45678}"],"strip":"{12345678-1234-5678-1234-567812f45678}","uuid":"12345678-1234-5678-1234-567812f45678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812f45678}'"}, +{"text":"urn}:uuid:C2345678-1234-5678-1234-5670812345678","repr":"'urn}:uuid:C2345678-1234-5678-1234-5670812345678'","split":["urn}:uuid:C2345678-1234-5678-1234-5670812345678"],"strip":"urn}:uuid:C2345678-1234-5678-1234-5670812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn}:uuid:C2345678-1234-5678-1234-5670812345678'"}, +{"text":"{123\u007f5678-123-5678-1_234-567812345678}","repr":"'{123\\x7f5678-123-5678-1_234-567812345678}'","split":["{123\u007f5678-123-5678-1_234-567812345678}"],"strip":"{123\u007f5678-123-5678-1_234-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{123\\x7f5678-123-5678-1_234-567812345678}'"}, +{"text":"{12345678-234-5678-124-567812345678}","repr":"'{12345678-234-5678-124-567812345678}'","split":["{12345678-234-5678-124-567812345678}"],"strip":"{12345678-234-5678-124-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-234-5678-124-567812345678}'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"1234678-1234-5678-1234-567812345678","repr":"'1234678-1234-5678-1234-567812345678'","split":["1234678-1234-5678-1234-567812345678"],"strip":"1234678-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234678-1234-5678-1234-567812345678'"}, +{"text":"1234567812X4567812345678162345678","repr":"'1234567812X4567812345678162345678'","split":["1234567812X4567812345678162345678"],"strip":"1234567812X4567812345678162345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234567812X4567812345678162345678'"}, +{"text":"1234578-1234-5678-123{4-\u000067812345678","repr":"'1234578-1234-5678-123{4-\\x0067812345678'","split":["1234578-1234-5678-123{4-\u000067812345678"],"strip":"1234578-1234-5678-123{4-\u000067812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234578-1234-5678-123{4-\\x0067812345678'"}, +{"text":"13\u00ad5678123456781234567812345678","repr":"'13\\xad5678123456781234567812345678'","split":["13\u00ad5678123456781234567812345678"],"strip":"13\u00ad5678123456781234567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '13\\xad5678123456781234567812345678'"}, +{"text":"{12345678-1234-5678-1234-56782345678}","repr":"'{12345678-1234-5678-1234-56782345678}'","split":["{12345678-1234-5678-1234-56782345678}"],"strip":"{12345678-1234-5678-1234-56782345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-56782345678}'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"urn:uuid:123456789-1234-e678-1234-567812345678","repr":"'urn:uuid:123456789-1234-e678-1234-567812345678'","split":["urn:uuid:123456789-1234-e678-1234-567812345678"],"strip":"urn:uuid:123456789-1234-e678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:123456789-1234-e678-1234-567812345678'"}, +{"text":"urn:uu3id:1234e678-123D-5678-1234-567812345678","repr":"'urn:uu3id:1234e678-123D-5678-1234-567812345678'","split":["urn:uu3id:1234e678-123D-5678-1234-567812345678"],"strip":"urn:uu3id:1234e678-123D-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uu3id:1234e678-123D-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-123-567812345678","repr":"'12345678-1234-5678-123-567812345678'","split":["12345678-1234-5678-123-567812345678"],"strip":"12345678-1234-5678-123-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-123-567812345678'"}, +{"text":"12345678-B234-5678-1234-567812345678","repr":"'12345678-B234-5678-1234-567812345678'","split":["12345678-B234-5678-1234-567812345678"],"strip":"12345678-B234-5678-1234-567812345678","uuid":"12345678-b234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-B234-5678-1234-567812345678'"}, +{"text":"{123456c78-1234-5678-1234-567812345678}","repr":"'{123456c78-1234-5678-1234-567812345678}'","split":["{123456c78-1234-5678-1234-567812345678}"],"strip":"{123456c78-1234-5678-1234-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{123456c78-1234-5678-1234-567812345678}'"}, +{"text":"12345678-134-5678-1234-5'7812345678","repr":"\"12345678-134-5678-1234-5'7812345678\"","split":["12345678-134-5678-1234-5'7812345678"],"strip":"12345678-134-5678-1234-5'7812345678","uuid":null,"int_error":"invalid literal for int() with base 10: \"12345678-134-5678-1234-5'7812345678\""}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"urn:uuid:12345678-1234-678-1234-06781245678","repr":"'urn:uuid:12345678-1234-678-1234-06781245678'","split":["urn:uuid:12345678-1234-678-1234-06781245678"],"strip":"urn:uuid:12345678-1234-678-1234-06781245678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-678-1234-06781245678'"}, +{"text":"12345678-1204-5678-1234-567812345678","repr":"'12345678-1204-5678-1234-567812345678'","split":["12345678-1204-5678-1234-567812345678"],"strip":"12345678-1204-5678-1234-567812345678","uuid":"12345678-1204-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1204-5678-1234-567812345678'"}, +{"text":"c2345678-1234-5678-1234-567812345678","repr":"'c2345678-1234-5678-1234-567812345678'","split":["c2345678-1234-5678-1234-567812345678"],"strip":"c2345678-1234-5678-1234-567812345678","uuid":"c2345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'c2345678-1234-5678-1234-567812345678'"}, +{"text":"{12345678-1234,-5678-1234-567812345678}","repr":"'{12345678-1234,-5678-1234-567812345678}'","split":["{12345678-1234,-5678-1234-567812345678}"],"strip":"{12345678-1234,-5678-1234-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234,-5678-1234-567812345678}'"}, +{"text":"2345678-1234-5678-1234-5681234578","repr":"'2345678-1234-5678-1234-5681234578'","split":["2345678-1234-5678-1234-5681234578"],"strip":"2345678-1234-5678-1234-5681234578","uuid":null,"int_error":"invalid literal for int() with base 10: '2345678-1234-5678-1234-5681234578'"}, +{"text":"1234567871234-5678-1234-567812345678","repr":"'1234567871234-5678-1234-567812345678'","split":["1234567871234-5678-1234-567812345678"],"strip":"1234567871234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234567871234-5678-1234-567812345678'"}, +{"text":"{12345678-1234-5668-1234-567812345678","repr":"'{12345678-1234-5668-1234-567812345678'","split":["{12345678-1234-5668-1234-567812345678"],"strip":"{12345678-1234-5668-1234-567812345678","uuid":"12345678-1234-5668-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5668-1234-567812345678'"}, +{"text":"12845678-1234-5678-1234-5\u06637812345678","repr":"'12845678-1234-5678-1234-5\u06637812345678'","split":["12845678-1234-5678-1234-5\u06637812345678"],"strip":"12845678-1234-5678-1234-5\u06637812345678","uuid":"12845678-1234-5678-1234-537812345678","int_error":"invalid literal for int() with base 10: '12845678-1234-5678-1234-5\u06637812345678'"}, +{"text":"1245678-1234-5678-123456781234567","repr":"'1245678-1234-5678-123456781234567'","split":["1245678-1234-5678-123456781234567"],"strip":"1245678-1234-5678-123456781234567","uuid":null,"int_error":"invalid literal for int() with base 10: '1245678-1234-5678-123456781234567'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"{12345678-124-5678-1234-5678123456{8}","repr":"'{12345678-124-5678-1234-5678123456{8}'","split":["{12345678-124-5678-1234-5678123456{8}"],"strip":"{12345678-124-5678-1234-5678123456{8}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-124-5678-1234-5678123456{8}'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"12345678-12\u200b4-5678-1234-567812345678","repr":"'12345678-12\\u200b4-5678-1234-567812345678'","split":["12345678-12\u200b4-5678-1234-567812345678"],"strip":"12345678-12\u200b4-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-12\\u200b4-5678-1234-567812345678'"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"12345678-1\u202834-5678-1234-567812345678","repr":"'12345678-1\\u202834-5678-1234-567812345678'","split":["12345678-1","34-5678-1234-567812345678"],"strip":"12345678-1\u202834-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1\\u202834-5678-1234-567812345678'"}, +{"text":"12345678a234567812345678B12345678","repr":"'12345678a234567812345678B12345678'","split":["12345678a234567812345678B12345678"],"strip":"12345678a234567812345678B12345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678a234567812345678B12345678'"}, +{"text":"1234578-1234-5678-91234-56X812345678","repr":"'1234578-1234-5678-91234-56X812345678'","split":["1234578-1234-5678-91234-56X812345678"],"strip":"1234578-1234-5678-91234-56X812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234578-1234-5678-91234-56X812345678'"}, +{"text":"urn:uuid:12\u001cC45678-1234-5678-1234-57812345678","repr":"'urn:uuid:12\\x1cC45678-1234-5678-1234-57812345678'","split":["urn:uuid:12","C45678-1234-5678-1234-57812345678"],"strip":"urn:uuid:12\u001cC45678-1234-5678-1234-57812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12\\x1cC45678-1234-5678-1234-57812345678'"}, +{"text":"urn:uuid:12345,678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345,678-1234-5678-1234-567812345678'","split":["urn:uuid:12345,678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345,678-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345,678-1234-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-1234-56781345678}","repr":"'12345678-1234-5678-1234-56781345678}'","split":["12345678-1234-5678-1234-56781345678}"],"strip":"12345678-1234-5678-1234-56781345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-56781345678}'"}, +{"text":"{123456\u300078-1234-5678-123D-x67812345678}","repr":"'{123456\\u300078-1234-5678-123D-x67812345678}'","split":["{123456","78-1234-5678-123D-x67812345678}"],"strip":"{123456\u300078-1234-5678-123D-x67812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{123456\\u300078-1234-5678-123D-x67812345678}'"}, +{"text":"12345678-1234-508-1234-567812345678","repr":"'12345678-1234-508-1234-567812345678'","split":["12345678-1234-508-1234-567812345678"],"strip":"12345678-1234-508-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-508-1234-567812345678'"}, +{"text":"1\u2003a2345678-1234-5678-1234-567812345678","repr":"'1\\u2003a2345678-1234-5678-1234-567812345678'","split":["1","a2345678-1234-5678-1234-567812345678"],"strip":"1\u2003a2345678-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1\\u2003a2345678-1234-5678-1234-567812345678'"}, +{"text":"1234567812345c67812345678123245678","repr":"'1234567812345c67812345678123245678'","split":["1234567812345c67812345678123245678"],"strip":"1234567812345c67812345678123245678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234567812345c67812345678123245678'"}, +{"text":"urn:uuid:12345678-1234-567\n-1234-567812345678","repr":"'urn:uuid:12345678-1234-567\\n-1234-567812345678'","split":["urn:uuid:12345678-1234-567","-1234-567812345678"],"strip":"urn:uuid:12345678-1234-567\n-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-567\\n-1234-567812345678'"}, +{"text":"1834E78123456781234567812345678","repr":"'1834E78123456781234567812345678'","split":["1834E78123456781234567812345678"],"strip":"1834E78123456781234567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1834E78123456781234567812345678'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"1234567-1234-\u20035678-123-567812345678","repr":"'1234567-1234-\\u20035678-123-567812345678'","split":["1234567-1234-","5678-123-567812345678"],"strip":"1234567-1234-\u20035678-123-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1234567-1234-\\u20035678-123-567812345678'"}, +{"text":"123456781\"23456d8123456781235678","repr":"'123456781\"23456d8123456781235678'","split":["123456781\"23456d8123456781235678"],"strip":"123456781\"23456d8123456781235678","uuid":null,"int_error":"invalid literal for int() with base 10: '123456781\"23456d8123456781235678'"}, +{"text":"12345678-1234-=678-1234-5673812345678","repr":"'12345678-1234-=678-1234-5673812345678'","split":["12345678-1234-=678-1234-5673812345678"],"strip":"12345678-1234-=678-1234-5673812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-=678-1234-5673812345678'"}, +{"text":"123568123\u300056781234567812345678","repr":"'123568123\\u300056781234567812345678'","split":["123568123","56781234567812345678"],"strip":"123568123\u300056781234567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '123568123\\u300056781234567812345678'"}, +{"text":"12345678-12314-56785-1234-567812345678","repr":"'12345678-12314-56785-1234-567812345678'","split":["12345678-12314-56785-1234-567812345678"],"strip":"12345678-12314-56785-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-12314-56785-1234-567812345678'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"12345678-1234-5678-1234567812345678","repr":"'12345678-1234-5678-1234567812345678'","split":["12345678-1234-5678-1234567812345678"],"strip":"12345678-1234-5678-1234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234567812345678'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678123456781234567812345678","repr":"'12345678123456781234567812345678'","split":["12345678123456781234567812345678"],"strip":"12345678123456781234567812345678","uuid":"12345678-1234-5678-1234-567812345678","int":"12345678123456781234567812345678"}, +{"text":"{12345678-1234-5678-1234-567812345678}","repr":"'{12345678-1234-5678-1234-567812345678}'","split":["{12345678-1234-5678-1234-567812345678}"],"strip":"{12345678-1234-5678-1234-567812345678}","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-567812345678}'"}, +{"text":"1234567812345678123567812345678","repr":"'1234567812345678123567812345678'","split":["1234567812345678123567812345678"],"strip":"1234567812345678123567812345678","uuid":null,"int":"1234567812345678123567812345678"}, +{"text":"{12345678-1234-5674-1234-567812345a78}","repr":"'{12345678-1234-5674-1234-567812345a78}'","split":["{12345678-1234-5674-1234-567812345a78}"],"strip":"{12345678-1234-5674-1234-567812345a78}","uuid":"12345678-1234-5674-1234-567812345a78","int_error":"invalid literal for int() with base 10: '{12345678-1234-5674-1234-567812345a78}'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"1234567812346781234567812345678","repr":"'1234567812346781234567812345678'","split":["1234567812346781234567812345678"],"strip":"1234567812346781234567812345678","uuid":null,"int":"1234567812346781234567812345678"}, +{"text":"1245678-1234-5678-1234-567812345678","repr":"'1245678-1234-5678-1234-567812345678'","split":["1245678-1234-5678-1234-567812345678"],"strip":"1245678-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '1245678-1234-5678-1234-567812345678'"}, +{"text":"urn:uuid:1234578-1234-5678-1234-567812345678","repr":"'urn:uuid:1234578-1234-5678-1234-567812345678'","split":["urn:uuid:1234578-1234-5678-1234-567812345678"],"strip":"urn:uuid:1234578-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:1234578-1234-5678-1234-567812345678'"}, +{"text":"urn:uuid\uff11:123459B78-1234-5678-1234-567812345678","repr":"'urn:uuid\uff11:123459B78-1234-5678-1234-567812345678'","split":["urn:uuid\uff11:123459B78-1234-5678-1234-567812345678"],"strip":"urn:uuid\uff11:123459B78-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid\uff11:123459B78-1234-5678-1234-567812345678'"}, +{"text":"12345678-\ud83d\ude00\\234-5678-1234-567812345678","repr":"'12345678-\ud83d\ude00\\\\234-5678-1234-567812345678'","split":["12345678-\ud83d\ude00\\234-5678-1234-567812345678"],"strip":"12345678-\ud83d\ude00\\234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-\ud83d\ude00\\\\234-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-1234-56781234567","repr":"'12345678-1234-5678-1234-56781234567'","split":["12345678-1234-5678-1234-56781234567"],"strip":"12345678-1234-5678-1234-56781234567","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-56781234567'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"12345678-12\\4-5678-1234-5675812345678","repr":"'12345678-12\\\\4-5678-1234-5675812345678'","split":["12345678-12\\4-5678-1234-5675812345678"],"strip":"12345678-12\\4-5678-1234-5675812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-12\\\\4-5678-1234-5675812345678'"}, +{"text":"12345678-1234-5678-1234-56782345D78","repr":"'12345678-1234-5678-1234-56782345D78'","split":["12345678-1234-5678-1234-56782345D78"],"strip":"12345678-1234-5678-1234-56782345D78","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-56782345D78'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"urn:uuid:12345678-1234-56781234-567812345678","repr":"'urn:uuid:12345678-1234-56781234-567812345678'","split":["urn:uuid:12345678-1234-56781234-567812345678"],"strip":"urn:uuid:12345678-1234-56781234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-56781234-567812345678'"}, +{"text":"urn:uuid:123{45678-1234-5678-234-'567812345678","repr":"\"urn:uuid:123{45678-1234-5678-234-'567812345678\"","split":["urn:uuid:123{45678-1234-5678-234-'567812345678"],"strip":"urn:uuid:123{45678-1234-5678-234-'567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: \"urn:uuid:123{45678-1234-5678-234-'567812345678\""}, +{"text":"12\u2028\\45678-1234-5678-1234-567812\u2028345678","repr":"'12\\u2028\\\\45678-1234-5678-1234-567812\\u2028345678'","split":["12","\\45678-1234-5678-1234-567812","345678"],"strip":"12\u2028\\45678-1234-5678-1234-567812\u2028345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12\\u2028\\\\45678-1234-5678-1234-567812\\u2028345678'"}, +{"text":"12345\\678123406781234567812345678","repr":"'12345\\\\678123406781234567812345678'","split":["12345\\678123406781234567812345678"],"strip":"12345\\678123406781234567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345\\\\678123406781234567812345678'"}, +{"text":"{123456c78-1234-568-1234-567812345678","repr":"'{123456c78-1234-568-1234-567812345678'","split":["{123456c78-1234-568-1234-567812345678"],"strip":"{123456c78-1234-568-1234-567812345678","uuid":"123456c7-8123-4568-1234-567812345678","int_error":"invalid literal for int() with base 10: '{123456c78-1234-568-1234-567812345678'"}, +{"text":"urn:uui:12345678-1234-5678-1234a56781234678","repr":"'urn:uui:12345678-1234-5678-1234a56781234678'","split":["urn:uui:12345678-1234-5678-1234a56781234678"],"strip":"urn:uui:12345678-1234-5678-1234a56781234678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uui:12345678-1234-5678-1234a56781234678'"}, +{"text":"12345678-1234-5678-1234-56781234678","repr":"'12345678-1234-5678-1234-56781234678'","split":["12345678-1234-5678-1234-56781234678"],"strip":"12345678-1234-5678-1234-56781234678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-56781234678'"}, +{"text":"urn:uuid:12345678-234-5678-234-5678123\u20035678","repr":"'urn:uuid:12345678-234-5678-234-5678123\\u20035678'","split":["urn:uuid:12345678-234-5678-234-5678123","5678"],"strip":"urn:uuid:12345678-234-5678-234-5678123\u20035678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-234-5678-234-5678123\\u20035678'"}, +{"text":"12345678-1234-568-1234-567812345678","repr":"'12345678-1234-568-1234-567812345678'","split":["12345678-1234-568-1234-567812345678"],"strip":"12345678-1234-568-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-568-1234-567812345678'"}, +{"text":"urn:uui\ud83d\ude00d:1234578-1234-5678-1234-567812345678","repr":"'urn:uui\ud83d\ude00d:1234578-1234-5678-1234-567812345678'","split":["urn:uui\ud83d\ude00d:1234578-1234-5678-1234-567812345678"],"strip":"urn:uui\ud83d\ude00d:1234578-1234-5678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uui\ud83d\ude00d:1234578-1234-5678-1234-567812345678'"}, +{"text":"12345678-1234-5678-1234-567812345678","repr":"'12345678-1234-5678-1234-567812345678'","split":["12345678-1234-5678-1234-567812345678"],"strip":"12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: '12345678-1234-5678-1234-567812345678'"}, +{"text":"12345x78-1234-5678-1234-56782345678","repr":"'12345x78-1234-5678-1234-56782345678'","split":["12345x78-1234-5678-1234-56782345678"],"strip":"12345x78-1234-5678-1234-56782345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345x78-1234-5678-1234-56782345678'"}, +{"text":"{12345678-1234-5678-1234-56=8:1234\ud83d\ude005678}","repr":"'{12345678-1234-5678-1234-56=8:1234\ud83d\ude005678}'","split":["{12345678-1234-5678-1234-56=8:1234\ud83d\ude005678}"],"strip":"{12345678-1234-5678-1234-56=8:1234\ud83d\ude005678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-56=8:1234\ud83d\ude005678}'"}, +{"text":"12345678-1234-5678-123\u00854-57812345678","repr":"'12345678-1234-5678-123\\x854-57812345678'","split":["12345678-1234-5678-123","4-57812345678"],"strip":"12345678-1234-5678-123\u00854-57812345678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-1234-5678-123\\x854-57812345678'"}, +{"text":"12345678-123\u000b4-5678-1234-56781245678","repr":"'12345678-123\\x0b4-5678-1234-56781245678'","split":["12345678-123","4-5678-1234-56781245678"],"strip":"12345678-123\u000b4-5678-1234-56781245678","uuid":null,"int_error":"invalid literal for int() with base 10: '12345678-123\\x0b4-5678-1234-56781245678'"}, +{"text":"urn:uuid:12345678-1234-56478-1934-56712345678","repr":"'urn:uuid:12345678-1234-56478-1934-56712345678'","split":["urn:uuid:12345678-1234-56478-1934-56712345678"],"strip":"urn:uuid:12345678-1234-56478-1934-56712345678","uuid":"12345678-1234-5647-8193-456712345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-56478-1934-56712345678'"}, +{"text":"urn:uuid:1c234678-1234-\ud83d\ude005678-1234-567812345678","repr":"'urn:uuid:1c234678-1234-\ud83d\ude005678-1234-567812345678'","split":["urn:uuid:1c234678-1234-\ud83d\ude005678-1234-567812345678"],"strip":"urn:uuid:1c234678-1234-\ud83d\ude005678-1234-567812345678","uuid":null,"int_error":"invalid literal for int() with base 10: 'urn:uuid:1c234678-1234-\ud83d\ude005678-1234-567812345678'"}, +{"text":"{12345678-1234-5678-1234-5678+12345678}","repr":"'{12345678-1234-5678-1234-5678+12345678}'","split":["{12345678-1234-5678-1234-5678+12345678}"],"strip":"{12345678-1234-5678-1234-5678+12345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5678-1234-5678+12345678}'"}, +{"text":"urn:uuid:12345678-1234-5678-1234-567812345678","repr":"'urn:uuid:12345678-1234-5678-1234-567812345678'","split":["urn:uuid:12345678-1234-5678-1234-567812345678"],"strip":"urn:uuid:12345678-1234-5678-1234-567812345678","uuid":"12345678-1234-5678-1234-567812345678","int_error":"invalid literal for int() with base 10: 'urn:uuid:12345678-1234-5678-1234-567812345678'"}, +{"text":"{12345678-1234-5D678-1234\u00e9-567812345678}","repr":"'{12345678-1234-5D678-1234\u00e9-567812345678}'","split":["{12345678-1234-5D678-1234\u00e9-567812345678}"],"strip":"{12345678-1234-5D678-1234\u00e9-567812345678}","uuid":null,"int_error":"invalid literal for int() with base 10: '{12345678-1234-5D678-1234\u00e9-567812345678}'"}, +{"text":"0x9\u00ad650722769","repr":"'0x9\\xad650722769'","split":["0x9\u00ad650722769"],"strip":"0x9\u00ad650722769","uuid":null,"int_error":"invalid literal for int() with base 10: '0x9\\xad650722769'"}, +{"text":"46013807112","repr":"'46013807112'","split":["46013807112"],"strip":"46013807112","uuid":null,"int":"46013807112"}, +{"text":"32\u000b92610527","repr":"'32\\x0b92610527'","split":["32","92610527"],"strip":"32\u000b92610527","uuid":null,"int_error":"invalid literal for int() with base 10: '32\\x0b92610527'"}, +{"text":"1a9\u00ad2557146414","repr":"'1a9\\xad2557146414'","split":["1a9\u00ad2557146414"],"strip":"1a9\u00ad2557146414","uuid":null,"int_error":"invalid literal for int() with base 10: '1a9\\xad2557146414'"}, +{"text":"896743'31684\u2003X","repr":"\"896743'31684\\u2003X\"","split":["896743'31684","X"],"strip":"896743'31684\u2003X","uuid":null,"int_error":"invalid literal for int() with base 10: \"896743'31684\\u2003X\""}, +{"text":"-8\udb40\udc016259_259\u001f3","repr":"'-8\\U000e00016259_259\\x1f3'","split":["-8\udb40\udc016259_259","3"],"strip":"-8\udb40\udc016259_259\u001f3","uuid":null,"int_error":"invalid literal for int() with base 10: '-8\\U000e00016259_259\\x1f3'"}, +{"text":"-66997\u00a0921\u00008\ud83d\ude0072","repr":"'-66997\\xa0921\\x008\ud83d\ude0072'","split":["-66997","921\u00008\ud83d\ude0072"],"strip":"-66997\u00a0921\u00008\ud83d\ude0072","uuid":null,"int_error":"invalid literal for int() with base 10: '-66997\\xa0921\\x008\ud83d\ude0072'"}, +{"text":"19183994030","repr":"'19183994030'","split":["19183994030"],"strip":"19183994030","uuid":null,"int":"19183994030"}, +{"text":"-838508502324","repr":"'-838508502324'","split":["-838508502324"],"strip":"-838508502324","uuid":null,"int":"-838508502324"}, +{"text":"11070909980","repr":"'11070909980'","split":["11070909980"],"strip":"11070909980","uuid":null,"int":"11070909980"}, +{"text":"77871628769","repr":"'77871628769'","split":["77871628769"],"strip":"77871628769","uuid":null,"int":"77871628769"}, +{"text":"388350446'50","repr":"\"388350446'50\"","split":["388350446'50"],"strip":"388350446'50","uuid":null,"int_error":"invalid literal for int() with base 10: \"388350446'50\""}, +{"text":"4931441646\n63","repr":"'4931441646\\n63'","split":["4931441646","63"],"strip":"4931441646\n63","uuid":null,"int_error":"invalid literal for int() with base 10: '4931441646\\n63'"}, +{"text":"66541504\u00a058","repr":"'66541504\\xa058'","split":["66541504","58"],"strip":"66541504\u00a058","uuid":null,"int_error":"invalid literal for int() with base 10: '66541504\\xa058'"}, +{"text":"67729uuid:2642","repr":"'67729uuid:2642'","split":["67729uuid:2642"],"strip":"67729uuid:2642","uuid":null,"int_error":"invalid literal for int() with base 10: '67729uuid:2642'"}, +{"text":"-19257646598","repr":"'-19257646598'","split":["-19257646598"],"strip":"-19257646598","uuid":null,"int":"-19257646598"}, +{"text":"-45174965334urn:","repr":"'-45174965334urn:'","split":["-45174965334urn:"],"strip":"-45174965334urn:","uuid":null,"int_error":"invalid literal for int() with base 10: '-45174965334urn:'"}, +{"text":"265264768798","repr":"'265264768798'","split":["265264768798"],"strip":"265264768798","uuid":null,"int":"265264768798"}, +{"text":"2\uff11995720\u037870","repr":"'2\uff11995720\\u037870'","split":["2\uff11995720\u037870"],"strip":"2\uff11995720\u037870","uuid":null,"int_error":"invalid literal for int() with base 10: '2\uff11995720\\u037870'"}, +{"text":"829741\u0663972045","repr":"'829741\u0663972045'","split":["829741\u0663972045"],"strip":"829741\u0663972045","uuid":null,"int":"8297413972045"}, +{"text":"337785023071","repr":"'337785023071'","split":["337785023071"],"strip":"337785023071","uuid":null,"int":"337785023071"}, +{"text":"93609769032\u00a0","repr":"'93609769032\\xa0'","split":["93609769032"],"strip":"93609769032","uuid":null,"int":"93609769032"}, +{"text":"-12513667488","repr":"'-12513667488'","split":["-12513667488"],"strip":"-12513667488","uuid":null,"int":"-12513667488"}, +{"text":"311003738848","repr":"'311003738848'","split":["311003738848"],"strip":"311003738848","uuid":null,"int":"311003738848"}, +{"text":"58\u0085722765078","repr":"'58\\x85722765078'","split":["58","722765078"],"strip":"58\u0085722765078","uuid":null,"int_error":"invalid literal for int() with base 10: '58\\x85722765078'"}, +{"text":"\u007f9\\6205283276","repr":"'\\x7f9\\\\6205283276'","split":["\u007f9\\6205283276"],"strip":"\u007f9\\6205283276","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x7f9\\\\6205283276'"}, +{"text":"2\u007f8\u3000846249137","repr":"'2\\x7f8\\u3000846249137'","split":["2\u007f8","846249137"],"strip":"2\u007f8\u3000846249137","uuid":null,"int_error":"invalid literal for int() with base 10: '2\\x7f8\\u3000846249137'"}, +{"text":"-4\"28452002774","repr":"'-4\"28452002774'","split":["-4\"28452002774"],"strip":"-4\"28452002774","uuid":null,"int_error":"invalid literal for int() with base 10: '-4\"28452002774'"}, +{"text":"-7}57081050375","repr":"'-7}57081050375'","split":["-7}57081050375"],"strip":"-7}57081050375","uuid":null,"int_error":"invalid literal for int() with base 10: '-7}57081050375'"}, +{"text":"8888304769721+","repr":"'8888304769721+'","split":["8888304769721+"],"strip":"8888304769721+","uuid":null,"int_error":"invalid literal for int() with base 10: '8888304769721+'"}, +{"text":"501610631217","repr":"'501610631217'","split":["501610631217"],"strip":"501610631217","uuid":null,"int":"501610631217"}, +{"text":"7760914931","repr":"'7760914931'","split":["7760914931"],"strip":"7760914931","uuid":null,"int":"7760914931"}, +{"text":"-530\u00ad5950967","repr":"'-530\\xad5950967'","split":["-530\u00ad5950967"],"strip":"-530\u00ad5950967","uuid":null,"int_error":"invalid literal for int() with base 10: '-530\\xad5950967'"}, +{"text":"22503057420","repr":"'22503057420'","split":["22503057420"],"strip":"22503057420","uuid":null,"int":"22503057420"}, +{"text":"-674916167x04","repr":"'-674916167x04'","split":["-674916167x04"],"strip":"-674916167x04","uuid":null,"int_error":"invalid literal for int() with base 10: '-674916167x04'"}, +{"text":"-141 3161uuid:261","repr":"'-141 3161uuid:261'","split":["-141","3161uuid:261"],"strip":"-141 3161uuid:261","uuid":null,"int_error":"invalid literal for int() with base 10: '-141 3161uuid:261'"}, +{"text":"-413319852305","repr":"'-413319852305'","split":["-413319852305"],"strip":"-413319852305","uuid":null,"int":"-413319852305"}, +{"text":"-684517309300","repr":"'-684517309300'","split":["-684517309300"],"strip":"-684517309300","uuid":null,"int":"-684517309300"}, +{"text":"256026534354","repr":"'256026534354'","split":["256026534354"],"strip":"256026534354","uuid":null,"int":"256026534354"}, +{"text":"-99701250982","repr":"'-99701250982'","split":["-99701250982"],"strip":"-99701250982","uuid":null,"int":"-99701250982"}, +{"text":"-761948360465","repr":"'-761948360465'","split":["-761948360465"],"strip":"-761948360465","uuid":null,"int":"-761948360465"}, +{"text":"-879044390077","repr":"'-879044390077'","split":["-879044390077"],"strip":"-879044390077","uuid":null,"int":"-879044390077"}, +{"text":"-\u00003582581822uuid:","repr":"'-\\x003582581822uuid:'","split":["-\u00003582581822uuid:"],"strip":"-\u00003582581822uuid:","uuid":null,"int_error":"invalid literal for int() with base 10: '-\\x003582581822uuid:'"}, +{"text":"-\uff114758978624","repr":"'-\uff114758978624'","split":["-\uff114758978624"],"strip":"-\uff114758978624","uuid":null,"int":"-14758978624"}, +{"text":"-336782347001","repr":"'-336782347001'","split":["-336782347001"],"strip":"-336782347001","uuid":null,"int":"-336782347001"}, +{"text":"-742702785001","repr":"'-742702785001'","split":["-742702785001"],"strip":"-742702785001","uuid":null,"int":"-742702785001"}, +{"text":"-579043\udb40\udc011\n9369","repr":"'-579043\\U000e00011\\n9369'","split":["-579043\udb40\udc011","9369"],"strip":"-579043\udb40\udc011\n9369","uuid":null,"int_error":"invalid literal for int() with base 10: '-579043\\U000e00011\\n9369'"}, +{"text":"149215335646","repr":"'149215335646'","split":["149215335646"],"strip":"149215335646","uuid":null,"int":"149215335646"}, +{"text":"\n804871\ud83d\ude00994849","repr":"'\\n804871\ud83d\ude00994849'","split":["804871\ud83d\ude00994849"],"strip":"804871\ud83d\ude00994849","uuid":null,"int_error":"invalid literal for int() with base 10: '\\n804871\ud83d\ude00994849'"}, +{"text":"-639174390393","repr":"'-639174390393'","split":["-639174390393"],"strip":"-639174390393","uuid":null,"int":"-639174390393"}, +{"text":"-994666487267","repr":"'-994666487267'","split":["-994666487267"],"strip":"-994666487267","uuid":null,"int":"-994666487267"}, +{"text":"309248238212","repr":"'309248238212'","split":["309248238212"],"strip":"309248238212","uuid":null,"int":"309248238212"}, +{"text":"21396329102","repr":"'21396329102'","split":["21396329102"],"strip":"21396329102","uuid":null,"int":"21396329102"}, +{"text":"-889223071220","repr":"'-889223071220'","split":["-889223071220"],"strip":"-889223071220","uuid":null,"int":"-889223071220"}, +{"text":"-8119133770959","repr":"'-8119133770959'","split":["-8119133770959"],"strip":"-8119133770959","uuid":null,"int":"-8119133770959"}, +{"text":"3:0192094036","repr":"'3:0192094036'","split":["3:0192094036"],"strip":"3:0192094036","uuid":null,"int_error":"invalid literal for int() with base 10: '3:0192094036'"}, +{"text":"60,05111687","repr":"'60,05111687'","split":["60,05111687"],"strip":"60,05111687","uuid":null,"int_error":"invalid literal for int() with base 10: '60,05111687'"}, +{"text":"-84131943205","repr":"'-84131943205'","split":["-84131943205"],"strip":"-84131943205","uuid":null,"int":"-84131943205"}, +{"text":"61-9328\ud835\udfce5888","repr":"'61-9328\ud835\udfce5888'","split":["61-9328\ud835\udfce5888"],"strip":"61-9328\ud835\udfce5888","uuid":null,"int_error":"invalid literal for int() with base 10: '61-9328\ud835\udfce5888'"}, +{"text":"233_73944120","repr":"'233_73944120'","split":["233_73944120"],"strip":"233_73944120","uuid":null,"int":"23373944120"}, +{"text":"22 060729665\ud835\udfce\u3000","repr":"'22 060729665\ud835\udfce\\u3000'","split":["22","060729665\ud835\udfce"],"strip":"22 060729665\ud835\udfce","uuid":null,"int_error":"invalid literal for int() with base 10: '22 060729665\ud835\udfce\\u3000'"}, +{"text":"-716904258302","repr":"'-716904258302'","split":["-716904258302"],"strip":"-716904258302","uuid":null,"int":"-716904258302"}, +{"text":"-28\u000079528668","repr":"'-28\\x0079528668'","split":["-28\u000079528668"],"strip":"-28\u000079528668","uuid":null,"int_error":"invalid literal for int() with base 10: '-28\\x0079528668'"}, +{"text":"76893\u00a0280537","repr":"'76893\\xa0280537'","split":["76893","280537"],"strip":"76893\u00a0280537","uuid":null,"int_error":"invalid literal for int() with base 10: '76893\\xa0280537'"}, +{"text":"919229745054","repr":"'919229745054'","split":["919229745054"],"strip":"919229745054","uuid":null,"int":"919229745054"}, +{"text":"962913985{813","repr":"'962913985{813'","split":["962913985{813"],"strip":"962913985{813","uuid":null,"int_error":"invalid literal for int() with base 10: '962913985{813'"}, +{"text":"804951522645","repr":"'804951522645'","split":["804951522645"],"strip":"804951522645","uuid":null,"int":"804951522645"}, +{"text":"5\n,72749725","repr":"'5\\n,72749725'","split":["5",",72749725"],"strip":"5\n,72749725","uuid":null,"int_error":"invalid literal for int() with base 10: '5\\n,72749725'"}, +{"text":"-278875215825","repr":"'-278875215825'","split":["-278875215825"],"strip":"-278875215825","uuid":null,"int":"-278875215825"}, +{"text":"73=6623496\ufeff1\u007f","repr":"'73=6623496\\ufeff1\\x7f'","split":["73=6623496\ufeff1\u007f"],"strip":"73=6623496\ufeff1\u007f","uuid":null,"int_error":"invalid literal for int() with base 10: '73=6623496\\ufeff1\\x7f'"}, +{"text":"-54366433473","repr":"'-54366433473'","split":["-54366433473"],"strip":"-54366433473","uuid":null,"int":"-54366433473"}, +{"text":"-171859479060","repr":"'-171859479060'","split":["-171859479060"],"strip":"-171859479060","uuid":null,"int":"-171859479060"}, +{"text":"-20937540480","repr":"'-20937540480'","split":["-20937540480"],"strip":"-20937540480","uuid":null,"int":"-20937540480"}, +{"text":"\ufeff6494\u06635565667","repr":"'\\ufeff6494\u06635565667'","split":["\ufeff6494\u06635565667"],"strip":"\ufeff6494\u06635565667","uuid":null,"int_error":"invalid literal for int() with base 10: '\\ufeff6494\u06635565667'"}, +{"text":"237887762762","repr":"'237887762762'","split":["237887762762"],"strip":"237887762762","uuid":null,"int":"237887762762"}, +{"text":"4698577991862","repr":"'4698577991862'","split":["4698577991862"],"strip":"4698577991862","uuid":null,"int":"4698577991862"}, +{"text":"973214\u008528106","repr":"'973214\\x8528106'","split":["973214","28106"],"strip":"973214\u008528106","uuid":null,"int_error":"invalid literal for int() with base 10: '973214\\x8528106'"}, +{"text":"75{902351664","repr":"'75{902351664'","split":["75{902351664"],"strip":"75{902351664","uuid":null,"int_error":"invalid literal for int() with base 10: '75{902351664'"}, +{"text":"394129792377","repr":"'394129792377'","split":["394129792377"],"strip":"394129792377","uuid":null,"int":"394129792377"}, +{"text":"859233865321","repr":"'859233865321'","split":["859233865321"],"strip":"859233865321","uuid":null,"int":"859233865321"}, +{"text":"-98270495909","repr":"'-98270495909'","split":["-98270495909"],"strip":"-98270495909","uuid":null,"int":"-98270495909"}, +{"text":"367191268911","repr":"'367191268911'","split":["367191268911"],"strip":"367191268911","uuid":null,"int":"367191268911"}, +{"text":"01253803 2","repr":"'01253803 2'","split":["01253803","2"],"strip":"01253803 2","uuid":null,"int_error":"invalid literal for int() with base 10: '01253803 2'"}, +{"text":"-9:3629517,7","repr":"'-9:3629517,7'","split":["-9:3629517,7"],"strip":"-9:3629517,7","uuid":null,"int_error":"invalid literal for int() with base 10: '-9:3629517,7'"}, +{"text":"-292679502194","repr":"'-292679502194'","split":["-292679502194"],"strip":"-292679502194","uuid":null,"int":"-292679502194"}, +{"text":"-28\u00ad636\u3000929984","repr":"'-28\\xad636\\u3000929984'","split":["-28\u00ad636","929984"],"strip":"-28\u00ad636\u3000929984","uuid":null,"int_error":"invalid literal for int() with base 10: '-28\\xad636\\u3000929984'"}, +{"text":"265635844293","repr":"'265635844293'","split":["265635844293"],"strip":"265635844293","uuid":null,"int":"265635844293"}, +{"text":"-479084439094","repr":"'-479084439094'","split":["-479084439094"],"strip":"-479084439094","uuid":null,"int":"-479084439094"}, +{"text":"6004446285","repr":"'6004446285'","split":["6004446285"],"strip":"6004446285","uuid":null,"int":"6004446285"}, +{"text":"536\u000b429207=793","repr":"'536\\x0b429207=793'","split":["536","429207=793"],"strip":"536\u000b429207=793","uuid":null,"int_error":"invalid literal for int() with base 10: '536\\x0b429207=793'"}, +{"text":"-191677754742","repr":"'-191677754742'","split":["-191677754742"],"strip":"-191677754742","uuid":null,"int":"-191677754742"}, +{"text":"-6{985899844","repr":"'-6{985899844'","split":["-6{985899844"],"strip":"-6{985899844","uuid":null,"int_error":"invalid literal for int() with base 10: '-6{985899844'"}, +{"text":"-18126393020","repr":"'-18126393020'","split":["-18126393020"],"strip":"-18126393020","uuid":null,"int":"-18126393020"}, +{"text":"3662300155","repr":"'3662300155'","split":["3662300155"],"strip":"3662300155","uuid":null,"int":"3662300155"}, +{"text":"-44542647427","repr":"'-44542647427'","split":["-44542647427"],"strip":"-44542647427","uuid":null,"int":"-44542647427"}, +{"text":"64702080689","repr":"'64702080689'","split":["64702080689"],"strip":"64702080689","uuid":null,"int":"64702080689"}, +{"text":"-\ud83d\ude009054046675","repr":"'-\ud83d\ude009054046675'","split":["-\ud83d\ude009054046675"],"strip":"-\ud83d\ude009054046675","uuid":null,"int_error":"invalid literal for int() with base 10: '-\ud83d\ude009054046675'"}, +{"text":"4002a10076815","repr":"'4002a10076815'","split":["4002a10076815"],"strip":"4002a10076815","uuid":null,"int_error":"invalid literal for int() with base 10: '4002a10076815'"}, +{"text":"-65680450772","repr":"'-65680450772'","split":["-65680450772"],"strip":"-65680450772","uuid":null,"int":"-65680450772"}, +{"text":"-357153504400","repr":"'-357153504400'","split":["-357153504400"],"strip":"-357153504400","uuid":null,"int":"-357153504400"}, +{"text":"93736956714","repr":"'93736956714'","split":["93736956714"],"strip":"93736956714","uuid":null,"int":"93736956714"}, +{"text":"60\u001f1548369'61","repr":"\"60\\x1f1548369'61\"","split":["60","1548369'61"],"strip":"60\u001f1548369'61","uuid":null,"int_error":"invalid literal for int() with base 10: \"60\\x1f1548369'61\""}, +{"text":"-48576099635","repr":"'-48576099635'","split":["-48576099635"],"strip":"-48576099635","uuid":null,"int":"-48576099635"}, +{"text":"-61130950168","repr":"'-61130950168'","split":["-61130950168"],"strip":"-61130950168","uuid":null,"int":"-61130950168"}, +{"text":"-3\ud83d\ude00306a2530167","repr":"'-3\ud83d\ude00306a2530167'","split":["-3\ud83d\ude00306a2530167"],"strip":"-3\ud83d\ude00306a2530167","uuid":null,"int_error":"invalid literal for int() with base 10: '-3\ud83d\ude00306a2530167'"}, +{"text":"51584385212\n","repr":"'51584385212\\n'","split":["51584385212"],"strip":"51584385212","uuid":null,"int":"51584385212"}, +{"text":"727442921843","repr":"'727442921843'","split":["727442921843"],"strip":"727442921843","uuid":null,"int":"727442921843"}, +{"text":"-972888118726","repr":"'-972888118726'","split":["-972888118726"],"strip":"-972888118726","uuid":null,"int":"-972888118726"}, +{"text":"95\u037835\u001f3064516","repr":"'95\\u037835\\x1f3064516'","split":["95\u037835","3064516"],"strip":"95\u037835\u001f3064516","uuid":null,"int_error":"invalid literal for int() with base 10: '95\\u037835\\x1f3064516'"}, +{"text":"9470900740x18","repr":"'9470900740x18'","split":["9470900740x18"],"strip":"9470900740x18","uuid":null,"int_error":"invalid literal for int() with base 10: '9470900740x18'"}, +{"text":"704244478357","repr":"'704244478357'","split":["704244478357"],"strip":"704244478357","uuid":null,"int":"704244478357"}, +{"text":"5082425\u001c0503","repr":"'5082425\\x1c0503'","split":["5082425","0503"],"strip":"5082425\u001c0503","uuid":null,"int_error":"invalid literal for int() with base 10: '5082425\\x1c0503'"}, +{"text":"12133045756","repr":"'12133045756'","split":["12133045756"],"strip":"12133045756","uuid":null,"int":"12133045756"}, +{"text":"965257810854","repr":"'965257810854'","split":["965257810854"],"strip":"965257810854","uuid":null,"int":"965257810854"}, +{"text":"-51159660235","repr":"'-51159660235'","split":["-51159660235"],"strip":"-51159660235","uuid":null,"int":"-51159660235"}, +{"text":"-1919844\n8877\u20037\t","repr":"'-1919844\\n8877\\u20037\\t'","split":["-1919844","8877","7"],"strip":"-1919844\n8877\u20037","uuid":null,"int_error":"invalid literal for int() with base 10: '-1919844\\n8877\\u20037\\t'"}, +{"text":"21227534358","repr":"'21227534358'","split":["21227534358"],"strip":"21227534358","uuid":null,"int":"21227534358"}, +{"text":"-505713\u007f16454","repr":"'-505713\\x7f16454'","split":["-505713\u007f16454"],"strip":"-505713\u007f16454","uuid":null,"int_error":"invalid literal for int() with base 10: '-505713\\x7f16454'"}, +{"text":"298982975","repr":"'298982975'","split":["298982975"],"strip":"298982975","uuid":null,"int":"298982975"}, +{"text":"33119:63\ufeff300","repr":"'33119:63\\ufeff300'","split":["33119:63\ufeff300"],"strip":"33119:63\ufeff300","uuid":null,"int_error":"invalid literal for int() with base 10: '33119:63\\ufeff300'"}, +{"text":"\u001f2\ufeff081526678","repr":"'\\x1f2\\ufeff081526678'","split":["2\ufeff081526678"],"strip":"2\ufeff081526678","uuid":null,"int_error":"invalid literal for int() with base 10: '\\x1f2\\ufeff081526678'"}, +{"text":"18350x95981963","repr":"'18350x95981963'","split":["18350x95981963"],"strip":"18350x95981963","uuid":null,"int_error":"invalid literal for int() with base 10: '18350x95981963'"}, +{"text":"-961677106128","repr":"'-961677106128'","split":["-961677106128"],"strip":"-961677106128","uuid":null,"int":"-961677106128"}, +{"text":"-507042-7257\udb40\udc010","repr":"'-507042-7257\\U000e00010'","split":["-507042-7257\udb40\udc010"],"strip":"-507042-7257\udb40\udc010","uuid":null,"int_error":"invalid literal for int() with base 10: '-507042-7257\\U000e00010'"}, +{"text":"720584196749","repr":"'720584196749'","split":["720584196749"],"strip":"720584196749","uuid":null,"int":"720584196749"}, +{"text":"694355,4\u03787618urn:","repr":"'694355,4\\u03787618urn:'","split":["694355,4\u03787618urn:"],"strip":"694355,4\u03787618urn:","uuid":null,"int_error":"invalid literal for int() with base 10: '694355,4\\u03787618urn:'"}, +{"text":"3925610813","repr":"'3925610813'","split":["3925610813"],"strip":"3925610813","uuid":null,"int":"3925610813"}, +{"text":"-5\u00e9834827235","repr":"'-5\u00e9834827235'","split":["-5\u00e9834827235"],"strip":"-5\u00e9834827235","uuid":null,"int_error":"invalid literal for int() with base 10: '-5\u00e9834827235'"}, +{"text":"8624572016","repr":"'8624572016'","split":["8624572016"],"strip":"8624572016","uuid":null,"int":"8624572016"}, +{"text":"-35385868754","repr":"'-35385868754'","split":["-35385868754"],"strip":"-35385868754","uuid":null,"int":"-35385868754"}, +{"text":"-849205320x76","repr":"'-849205320x76'","split":["-849205320x76"],"strip":"-849205320x76","uuid":null,"int_error":"invalid literal for int() with base 10: '-849205320x76'"}, +{"text":"162159\u300030","repr":"'162159\\u300030'","split":["162159","30"],"strip":"162159\u300030","uuid":null,"int_error":"invalid literal for int() with base 10: '162159\\u300030'"}, +{"text":"-970960a3,291","repr":"'-970960a3,291'","split":["-970960a3,291"],"strip":"-970960a3,291","uuid":null,"int_error":"invalid literal for int() with base 10: '-970960a3,291'"}, +{"text":"-21452716796","repr":"'-21452716796'","split":["-21452716796"],"strip":"-21452716796","uuid":null,"int":"-21452716796"}, +{"text":"352145050F543","repr":"'352145050F543'","split":["352145050F543"],"strip":"352145050F543","uuid":null,"int_error":"invalid literal for int() with base 10: '352145050F543'"}, +{"text":"372200030527","repr":"'372200030527'","split":["372200030527"],"strip":"372200030527","uuid":null,"int":"372200030527"}, +{"text":"-\u2003\udb40\udc0145980474084","repr":"'-\\u2003\\U000e000145980474084'","split":["-","\udb40\udc0145980474084"],"strip":"-\u2003\udb40\udc0145980474084","uuid":null,"int_error":"invalid literal for int() with base 10: '-\\u2003\\U000e000145980474084'"}, +{"text":"8\"79879861","repr":"'8\"79879861'","split":["8\"79879861"],"strip":"8\"79879861","uuid":null,"int_error":"invalid literal for int() with base 10: '8\"79879861'"}, +{"text":"-77982739\"55","repr":"'-77982739\"55'","split":["-77982739\"55"],"strip":"-77982739\"55","uuid":null,"int_error":"invalid literal for int() with base 10: '-77982739\"55'"}, +{"text":"7426281774","repr":"'7426281774'","split":["7426281774"],"strip":"7426281774","uuid":null,"int":"7426281774"}, +{"text":"\u2028164507150645","repr":"'\\u2028164507150645'","split":["164507150645"],"strip":"164507150645","uuid":null,"int":"164507150645"}, +{"text":"-47462861502","repr":"'-47462861502'","split":["-47462861502"],"strip":"-47462861502","uuid":null,"int":"-47462861502"}, +{"text":"-521954399042","repr":"'-521954399042'","split":["-521954399042"],"strip":"-521954399042","uuid":null,"int":"-521954399042"}, +{"text":"-941420302204","repr":"'-941420302204'","split":["-941420302204"],"strip":"-941420302204","uuid":null,"int":"-941420302204"}, +{"text":"-8571506896","repr":"'-8571506896'","split":["-8571506896"],"strip":"-8571506896","uuid":null,"int":"-8571506896"}, +{"text":"66772}42667","repr":"'66772}42667'","split":["66772}42667"],"strip":"66772}42667","uuid":null,"int_error":"invalid literal for int() with base 10: '66772}42667'"}, +{"text":"722218285673","repr":"'722218285673'","split":["722218285673"],"strip":"722218285673","uuid":null,"int":"722218285673"}, +{"text":"-493302351164","repr":"'-493302351164'","split":["-493302351164"],"strip":"-493302351164","uuid":null,"int":"-493302351164"}, +{"text":"\"-68\u0378782778303","repr":"'\"-68\\u0378782778303'","split":["\"-68\u0378782778303"],"strip":"\"-68\u0378782778303","uuid":null,"int_error":"invalid literal for int() with base 10: '\"-68\\u0378782778303'"}, +{"text":"90954464\u00a082","repr":"'90954464\\xa082'","split":["90954464","82"],"strip":"90954464\u00a082","uuid":null,"int_error":"invalid literal for int() with base 10: '90954464\\xa082'"} +], +"paths": [ +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":".","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"./a","str":"a","parts":["a"],"absolute":false,"name":"a","parents":["."]}, +{"flavor":"posix","text":"a/./b/","str":"a/b","parts":["a","b"],"absolute":false,"name":"b","parents":["a","."]}, +{"flavor":"posix","text":"a//b","str":"a/b","parts":["a","b"],"absolute":false,"name":"b","parents":["a","."]}, +{"flavor":"posix","text":"a/../b","str":"a/../b","parts":["a","..","b"],"absolute":false,"name":"b","parents":["a/..","a","."]}, +{"flavor":"posix","text":"/a","str":"/a","parts":["/","a"],"absolute":true,"name":"a","parents":["/"]}, +{"flavor":"posix","text":"//a","str":"//a","parts":["//","a"],"absolute":true,"name":"a","parents":["//"]}, +{"flavor":"posix","text":"///a","str":"/a","parts":["/","a"],"absolute":true,"name":"a","parents":["/"]}, +{"flavor":"posix","text":"a\\b","str":"a\\b","parts":["a\\b"],"absolute":false,"name":"a\\b","parents":["."]}, +{"flavor":"posix","text":"c:x","str":"c:x","parts":["c:x"],"absolute":false,"name":"c:x","parents":["."]}, +{"flavor":"posix","text":"C:/a/b/","str":"C:/a/b","parts":["C:","a","b"],"absolute":false,"name":"b","parents":["C:/a","C:","."]}, +{"flavor":"posix","text":"c:","str":"c:","parts":["c:"],"absolute":false,"name":"c:","parents":["."]}, +{"flavor":"posix","text":"./c:x","str":"c:x","parts":["c:x"],"absolute":false,"name":"c:x","parents":["."]}, +{"flavor":"posix","text":"//server/share","str":"//server/share","parts":["//","server","share"],"absolute":true,"name":"share","parents":["//server","//"]}, +{"flavor":"posix","text":"//server/share/x","str":"//server/share/x","parts":["//","server","share","x"],"absolute":true,"name":"x","parents":["//server/share","//server","//"]}, +{"flavor":"posix","text":"\\\\?\\C:\\x","str":"\\\\?\\C:\\x","parts":["\\\\?\\C:\\x"],"absolute":false,"name":"\\\\?\\C:\\x","parents":["."]}, +{"flavor":"posix","text":"//?/UNC/server/share","str":"//?/UNC/server/share","parts":["//","?","UNC","server","share"],"absolute":true,"name":"share","parents":["//?/UNC/server","//?/UNC","//?","//"]}, +{"flavor":"posix","text":"//?/UNC/server","str":"//?/UNC/server","parts":["//","?","UNC","server"],"absolute":true,"name":"server","parents":["//?/UNC","//?","//"]}, +{"flavor":"posix","text":"\\\\.\\device\\x","str":"\\\\.\\device\\x","parts":["\\\\.\\device\\x"],"absolute":false,"name":"\\\\.\\device\\x","parents":["."]}, +{"flavor":"posix","text":"\\a","str":"\\a","parts":["\\a"],"absolute":false,"name":"\\a","parents":["."]}, +{"flavor":"posix","text":"\u00e9:x","str":"\u00e9:x","parts":["\u00e9:x"],"absolute":false,"name":"\u00e9:x","parents":["."]}, +{"flavor":"posix","text":"\\\\\\x","str":"\\\\\\x","parts":["\\\\\\x"],"absolute":false,"name":"\\\\\\x","parents":["."]}, +{"flavor":"posix","text":"//?/","str":"//?","parts":["//","?"],"absolute":true,"name":"?","parents":["//"]}, +{"flavor":"posix","text":"x:y:z","str":"x:y:z","parts":["x:y:z"],"absolute":false,"name":"x:y:z","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"uncb..x,y/\\/","str":"uncb..x,y/\\","parts":["uncb..x,y","\\"],"absolute":false,"name":"\\","parents":["uncb..x,y","."]}, +{"flavor":"posix","text":"\u00e9/","str":"\u00e9","parts":["\u00e9"],"absolute":false,"name":"\u00e9","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":";\u0000?\u00e9","str":";\u0000?\u00e9","parts":[";\u0000?\u00e9"],"absolute":false,"name":";\u0000?\u00e9","parents":["."]}, +{"flavor":"posix","text":"x,y//.?\\..\u0000","str":"x,y/.?\\..\u0000","parts":["x,y",".?\\..\u0000"],"absolute":false,"name":".?\\..\u0000","parents":["x,y","."]}, +{"flavor":"posix","text":"x,y\u0000;..","str":"x,y\u0000;..","parts":["x,y\u0000;.."],"absolute":false,"name":"x,y\u0000;..","parents":["."]}, +{"flavor":"posix","text":"\\\\..uncC","str":"\\\\..uncC","parts":["\\\\..uncC"],"absolute":false,"name":"\\\\..uncC","parents":["."]}, +{"flavor":"posix","text":"a\\\\uncb//","str":"a\\\\uncb","parts":["a\\\\uncb"],"absolute":false,"name":"a\\\\uncb","parents":["."]}, +{"flavor":"posix","text":":a\u0000","str":":a\u0000","parts":[":a\u0000"],"absolute":false,"name":":a\u0000","parents":["."]}, +{"flavor":"posix","text":";UNC\\\\bUNC","str":";UNC\\\\bUNC","parts":[";UNC\\\\bUNC"],"absolute":false,"name":";UNC\\\\bUNC","parents":["."]}, +{"flavor":"posix","text":"\u00e9c?;aCb\u0000","str":"\u00e9c?;aCb\u0000","parts":["\u00e9c?;aCb\u0000"],"absolute":false,"name":"\u00e9c?;aCb\u0000","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":":..","str":":..","parts":[":.."],"absolute":false,"name":":..","parents":["."]}, +{"flavor":"posix","text":":C\u0000.\u0000.","str":":C\u0000.\u0000.","parts":[":C\u0000.\u0000."],"absolute":false,"name":":C\u0000.\u0000.","parents":["."]}, +{"flavor":"posix","text":"C:...","str":"C:...","parts":["C:..."],"absolute":false,"name":"C:...","parents":["."]}, +{"flavor":"posix","text":"C\u00e9;\\\\Cunc\u00e9/","str":"C\u00e9;\\\\Cunc\u00e9","parts":["C\u00e9;\\\\Cunc\u00e9"],"absolute":false,"name":"C\u00e9;\\\\Cunc\u00e9","parents":["."]}, +{"flavor":"posix","text":"\\\\","str":"\\\\","parts":["\\\\"],"absolute":false,"name":"\\\\","parents":["."]}, +{"flavor":"posix","text":":C\u0000 \u00e9","str":":C\u0000 \u00e9","parts":[":C\u0000 \u00e9"],"absolute":false,"name":":C\u0000 \u00e9","parents":["."]}, +{"flavor":"posix","text":"UNC\\\\\\..cc","str":"UNC\\\\\\..cc","parts":["UNC\\\\\\..cc"],"absolute":false,"name":"UNC\\\\\\..cc","parents":["."]}, +{"flavor":"posix","text":" ////","str":" ","parts":[" "],"absolute":false,"name":" ","parents":["."]}, +{"flavor":"posix","text":".?..","str":".?..","parts":[".?.."],"absolute":false,"name":".?..","parents":["."]}, +{"flavor":"posix","text":"?UNC//","str":"?UNC","parts":["?UNC"],"absolute":false,"name":"?UNC","parents":["."]}, +{"flavor":"posix","text":"..//\u00e9\u0000\u0000unc\u00e9","str":"../\u00e9\u0000\u0000unc\u00e9","parts":["..","\u00e9\u0000\u0000unc\u00e9"],"absolute":false,"name":"\u00e9\u0000\u0000unc\u00e9","parents":["..","."]}, +{"flavor":"posix","text":".bb\u0000 //\\\\a","str":".bb\u0000 /\\\\a","parts":[".bb\u0000 ","\\\\a"],"absolute":false,"name":"\\\\a","parents":[".bb\u0000 ","."]}, +{"flavor":"posix","text":"bbx,yx,yx,y/ ","str":"bbx,yx,yx,y/ ","parts":["bbx,yx,yx,y"," "],"absolute":false,"name":" ","parents":["bbx,yx,yx,y","."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"\\?:","str":"\\?:","parts":["\\?:"],"absolute":false,"name":"\\?:","parents":["."]}, +{"flavor":"posix","text":"unc;\u00e9;..\\\\","str":"unc;\u00e9;..\\\\","parts":["unc;\u00e9;..\\\\"],"absolute":false,"name":"unc;\u00e9;..\\\\","parents":["."]}, +{"flavor":"posix","text":"/?\\\u00e9?:","str":"/?\\\u00e9?:","parts":["/","?\\\u00e9?:"],"absolute":true,"name":"?\\\u00e9?:","parents":["/"]}, +{"flavor":"posix","text":" ac..Cx,y","str":" ac..Cx,y","parts":[" ac..Cx,y"],"absolute":false,"name":" ac..Cx,y","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":" ?cx,yC","str":" ?cx,yC","parts":[" ?cx,yC"],"absolute":false,"name":" ?cx,yC","parents":["."]}, +{"flavor":"posix","text":"C: C\u00e9unc;unc","str":"C: C\u00e9unc;unc","parts":["C: C\u00e9unc;unc"],"absolute":false,"name":"C: C\u00e9unc;unc","parents":["."]}, +{"flavor":"posix","text":"\u0000uncUNC?","str":"\u0000uncUNC?","parts":["\u0000uncUNC?"],"absolute":false,"name":"\u0000uncUNC?","parents":["."]}, +{"flavor":"posix","text":"\\\\b/c","str":"\\\\b/c","parts":["\\\\b","c"],"absolute":false,"name":"c","parents":["\\\\b","."]}, +{"flavor":"posix","text":"\\\\ C","str":"\\\\ C","parts":["\\\\ C"],"absolute":false,"name":"\\\\ C","parents":["."]}, +{"flavor":"posix","text":"C\\;unc\\\\;","str":"C\\;unc\\\\;","parts":["C\\;unc\\\\;"],"absolute":false,"name":"C\\;unc\\\\;","parents":["."]}, +{"flavor":"posix","text":"?bunc\u0000c?unc","str":"?bunc\u0000c?unc","parts":["?bunc\u0000c?unc"],"absolute":false,"name":"?bunc\u0000c?unc","parents":["."]}, +{"flavor":"posix","text":"b?","str":"b?","parts":["b?"],"absolute":false,"name":"b?","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"\\\\Cbx,y;c","str":"\\\\Cbx,y;c","parts":["\\\\Cbx,y;c"],"absolute":false,"name":"\\\\Cbx,y;c","parents":["."]}, +{"flavor":"posix","text":"\u0000Cunc ;x,y","str":"\u0000Cunc ;x,y","parts":["\u0000Cunc ;x,y"],"absolute":false,"name":"\u0000Cunc ;x,y","parents":["."]}, +{"flavor":"posix","text":"..c\u0000\\\\..\u0000","str":"..c\u0000\\\\..\u0000","parts":["..c\u0000\\\\..\u0000"],"absolute":false,"name":"..c\u0000\\\\..\u0000","parents":["."]}, +{"flavor":"posix","text":"\\\\a","str":"\\\\a","parts":["\\\\a"],"absolute":false,"name":"\\\\a","parents":["."]}, +{"flavor":"posix","text":"/UNCb // ","str":"/UNCb / ","parts":["/","UNCb "," "],"absolute":true,"name":" ","parents":["/UNCb ","/"]}, +{"flavor":"posix","text":":uncuncUNC\\\\\\","str":":uncuncUNC\\\\\\","parts":[":uncuncUNC\\\\\\"],"absolute":false,"name":":uncuncUNC\\\\\\","parents":["."]}, +{"flavor":"posix","text":"\\\\C..b\u0000a","str":"\\\\C..b\u0000a","parts":["\\\\C..b\u0000a"],"absolute":false,"name":"\\\\C..b\u0000a","parents":["."]}, +{"flavor":"posix","text":"\u0000\u00e9C//uncC:","str":"\u0000\u00e9C/uncC:","parts":["\u0000\u00e9C","uncC:"],"absolute":false,"name":"uncC:","parents":["\u0000\u00e9C","."]}, +{"flavor":"posix","text":";","str":";","parts":[";"],"absolute":false,"name":";","parents":["."]}, +{"flavor":"posix","text":"//x,y","str":"//x,y","parts":["//","x,y"],"absolute":true,"name":"x,y","parents":["//"]}, +{"flavor":"posix","text":"/unc","str":"/unc","parts":["/","unc"],"absolute":true,"name":"unc","parents":["/"]}, +{"flavor":"posix","text":".. c","str":".. c","parts":[".. c"],"absolute":false,"name":".. c","parents":["."]}, +{"flavor":"posix","text":"..\\\\a\u0000","str":"..\\\\a\u0000","parts":["..\\\\a\u0000"],"absolute":false,"name":"..\\\\a\u0000","parents":["."]}, +{"flavor":"posix","text":"/CCUNC:","str":"/CCUNC:","parts":["/","CCUNC:"],"absolute":true,"name":"CCUNC:","parents":["/"]}, +{"flavor":"posix","text":";C\u0000","str":";C\u0000","parts":[";C\u0000"],"absolute":false,"name":";C\u0000","parents":["."]}, +{"flavor":"posix","text":":\u0000:.\\\\uncunc;","str":":\u0000:.\\\\uncunc;","parts":[":\u0000:.\\\\uncunc;"],"absolute":false,"name":":\u0000:.\\\\uncunc;","parents":["."]}, +{"flavor":"posix","text":"uncUNC;.\u00e9\u00e9uncb","str":"uncUNC;.\u00e9\u00e9uncb","parts":["uncUNC;.\u00e9\u00e9uncb"],"absolute":false,"name":"uncUNC;.\u00e9\u00e9uncb","parents":["."]}, +{"flavor":"posix","text":"?C.. \\\\","str":"?C.. \\\\","parts":["?C.. \\\\"],"absolute":false,"name":"?C.. \\\\","parents":["."]}, +{"flavor":"posix","text":"\u0000\\\\cb:","str":"\u0000\\\\cb:","parts":["\u0000\\\\cb:"],"absolute":false,"name":"\u0000\\\\cb:","parents":["."]}, +{"flavor":"posix","text":"\u0000: /Cx,yx,y..","str":"\u0000: /Cx,yx,y..","parts":["\u0000: ","Cx,yx,y.."],"absolute":false,"name":"Cx,yx,y..","parents":["\u0000: ","."]}, +{"flavor":"posix","text":"\\x,y..;b.","str":"\\x,y..;b.","parts":["\\x,y..;b."],"absolute":false,"name":"\\x,y..;b.","parents":["."]}, +{"flavor":"posix","text":"\u00e9 b..:","str":"\u00e9 b..:","parts":["\u00e9 b..:"],"absolute":false,"name":"\u00e9 b..:","parents":["."]}, +{"flavor":"posix","text":"\u00e9","str":"\u00e9","parts":["\u00e9"],"absolute":false,"name":"\u00e9","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"x,y","str":"x,y","parts":["x,y"],"absolute":false,"name":"x,y","parents":["."]}, +{"flavor":"posix","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"\\","parents":["."]}, +{"flavor":"posix","text":"?;bc ","str":"?;bc ","parts":["?;bc "],"absolute":false,"name":"?;bc ","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"C","str":"C","parts":["C"],"absolute":false,"name":"C","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"\u0000C... \\\\//","str":"\u0000C... \\\\","parts":["\u0000C... \\\\"],"absolute":false,"name":"\u0000C... \\\\","parents":["."]}, +{"flavor":"posix","text":"unc/\u00e9x,yC/c","str":"unc/\u00e9x,yC/c","parts":["unc","\u00e9x,yC","c"],"absolute":false,"name":"c","parents":["unc/\u00e9x,yC","unc","."]}, +{"flavor":"posix","text":"C..////C\\","str":"C../C\\","parts":["C..","C\\"],"absolute":false,"name":"C\\","parents":["C..","."]}, +{"flavor":"posix","text":"..x,yUNC\\.a::","str":"..x,yUNC\\.a::","parts":["..x,yUNC\\.a::"],"absolute":false,"name":"..x,yUNC\\.a::","parents":["."]}, +{"flavor":"posix","text":"UNCC..ax,y","str":"UNCC..ax,y","parts":["UNCC..ax,y"],"absolute":false,"name":"UNCC..ax,y","parents":["."]}, +{"flavor":"posix","text":"UNCunc\\\\:.. :","str":"UNCunc\\\\:.. :","parts":["UNCunc\\\\:.. :"],"absolute":false,"name":"UNCunc\\\\:.. :","parents":["."]}, +{"flavor":"posix","text":"/? a","str":"/? a","parts":["/","? a"],"absolute":true,"name":"? a","parents":["/"]}, +{"flavor":"posix","text":"/\\","str":"/\\","parts":["/","\\"],"absolute":true,"name":"\\","parents":["/"]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"uncC\\:ca. ","str":"uncC\\:ca. ","parts":["uncC\\:ca. "],"absolute":false,"name":"uncC\\:ca. ","parents":["."]}, +{"flavor":"posix","text":";b\\\\?b?:.","str":";b\\\\?b?:.","parts":[";b\\\\?b?:."],"absolute":false,"name":";b\\\\?b?:.","parents":["."]}, +{"flavor":"posix","text":"\\\\ a;c","str":"\\\\ a;c","parts":["\\\\ a;c"],"absolute":false,"name":"\\\\ a;c","parents":["."]}, +{"flavor":"posix","text":"b? ///","str":"b? ","parts":["b? "],"absolute":false,"name":"b? ","parents":["."]}, +{"flavor":"posix","text":"\u00e9","str":"\u00e9","parts":["\u00e9"],"absolute":false,"name":"\u00e9","parents":["."]}, +{"flavor":"posix","text":" :\\\\\u00e9Cuncx,y\u0000","str":" :\\\\\u00e9Cuncx,y\u0000","parts":[" :\\\\\u00e9Cuncx,y\u0000"],"absolute":false,"name":" :\\\\\u00e9Cuncx,y\u0000","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"\\\\.","str":"\\\\.","parts":["\\\\."],"absolute":false,"name":"\\\\.","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"\\","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"..","str":"..","parts":[".."],"absolute":false,"name":"..","parents":["."]}, +{"flavor":"posix","text":"//","str":"//","parts":["//"],"absolute":true,"name":"","parents":[]}, +{"flavor":"posix","text":"C\\\\","str":"C\\\\","parts":["C\\\\"],"absolute":false,"name":"C\\\\","parents":["."]}, +{"flavor":"posix","text":"uncb\u0000unc","str":"uncb\u0000unc","parts":["uncb\u0000unc"],"absolute":false,"name":"uncb\u0000unc","parents":["."]}, +{"flavor":"posix","text":"..c..a\u0000","str":"..c..a\u0000","parts":["..c..a\u0000"],"absolute":false,"name":"..c..a\u0000","parents":["."]}, +{"flavor":"posix","text":"\\\\\\\\x,yunc\\\\\u00e9?","str":"\\\\\\\\x,yunc\\\\\u00e9?","parts":["\\\\\\\\x,yunc\\\\\u00e9?"],"absolute":false,"name":"\\\\\\\\x,yunc\\\\\u00e9?","parents":["."]}, +{"flavor":"posix","text":"x,yax,y/UNC:.","str":"x,yax,y/UNC:.","parts":["x,yax,y","UNC:."],"absolute":false,"name":"UNC:.","parents":["x,yax,y","."]}, +{"flavor":"posix","text":"/","str":"/","parts":["/"],"absolute":true,"name":"","parents":[]}, +{"flavor":"posix","text":"a.:\\/","str":"a.:\\","parts":["a.:\\"],"absolute":false,"name":"a.:\\","parents":["."]}, +{"flavor":"posix","text":"Cc;\u0000","str":"Cc;\u0000","parts":["Cc;\u0000"],"absolute":false,"name":"Cc;\u0000","parents":["."]}, +{"flavor":"posix","text":"\\\\ac//\\\\","str":"\\\\ac/\\\\","parts":["\\\\ac","\\\\"],"absolute":false,"name":"\\\\","parents":["\\\\ac","."]}, +{"flavor":"posix","text":"\u0000b x,yUNC\\","str":"\u0000b x,yUNC\\","parts":["\u0000b x,yUNC\\"],"absolute":false,"name":"\u0000b x,yUNC\\","parents":["."]}, +{"flavor":"posix","text":":CcCx,y//\\\\","str":":CcCx,y/\\\\","parts":[":CcCx,y","\\\\"],"absolute":false,"name":"\\\\","parents":[":CcCx,y","."]}, +{"flavor":"posix","text":"?C\u0000;C/ ","str":"?C\u0000;C/ ","parts":["?C\u0000;C"," "],"absolute":false,"name":" ","parents":["?C\u0000;C","."]}, +{"flavor":"posix","text":"\\\\//;a\\\\UNC","str":"\\\\/;a\\\\UNC","parts":["\\\\",";a\\\\UNC"],"absolute":false,"name":";a\\\\UNC","parents":["\\\\","."]}, +{"flavor":"posix","text":";;.b","str":";;.b","parts":[";;.b"],"absolute":false,"name":";;.b","parents":["."]}, +{"flavor":"posix","text":"x,y\\\\bunc:;","str":"x,y\\\\bunc:;","parts":["x,y\\\\bunc:;"],"absolute":false,"name":"x,y\\\\bunc:;","parents":["."]}, +{"flavor":"posix","text":"\\\\C:\u00e9","str":"\\\\C:\u00e9","parts":["\\\\C:\u00e9"],"absolute":false,"name":"\\\\C:\u00e9","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"unc\\..c?unc","str":"unc\\..c?unc","parts":["unc\\..c?unc"],"absolute":false,"name":"unc\\..c?unc","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"unc","str":"unc","parts":["unc"],"absolute":false,"name":"unc","parents":["."]}, +{"flavor":"posix","text":". ?x,y","str":". ?x,y","parts":[". ?x,y"],"absolute":false,"name":". ?x,y","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"\u0000\\\\","str":"\u0000\\\\","parts":["\u0000\\\\"],"absolute":false,"name":"\u0000\\\\","parents":["."]}, +{"flavor":"posix","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"\\","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"UNC/\u0000C","str":"UNC/\u0000C","parts":["UNC","\u0000C"],"absolute":false,"name":"\u0000C","parents":["UNC","."]}, +{"flavor":"posix","text":" uncunc","str":" uncunc","parts":[" uncunc"],"absolute":false,"name":" uncunc","parents":["."]}, +{"flavor":"posix","text":"\u0000c.?..bC..","str":"\u0000c.?..bC..","parts":["\u0000c.?..bC.."],"absolute":false,"name":"\u0000c.?..bC..","parents":["."]}, +{"flavor":"posix","text":".UNC//;UNCCc//","str":".UNC/;UNCCc","parts":[".UNC",";UNCCc"],"absolute":false,"name":";UNCCc","parents":[".UNC","."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":".x,y","str":".x,y","parts":[".x,y"],"absolute":false,"name":".x,y","parents":["."]}, +{"flavor":"posix","text":": :","str":": :","parts":[": :"],"absolute":false,"name":": :","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"unca:?ba\u0000unc","str":"unca:?ba\u0000unc","parts":["unca:?ba\u0000unc"],"absolute":false,"name":"unca:?ba\u0000unc","parents":["."]}, +{"flavor":"posix","text":":b/:","str":":b/:","parts":[":b",":"],"absolute":false,"name":":","parents":[":b","."]}, +{"flavor":"posix","text":"UNC\u00e9.\u00e9","str":"UNC\u00e9.\u00e9","parts":["UNC\u00e9.\u00e9"],"absolute":false,"name":"UNC\u00e9.\u00e9","parents":["."]}, +{"flavor":"posix","text":"c\\\\UNCunc\\\u00e9/x,y","str":"c\\\\UNCunc\\\u00e9/x,y","parts":["c\\\\UNCunc\\\u00e9","x,y"],"absolute":false,"name":"x,y","parents":["c\\\\UNCunc\\\u00e9","."]}, +{"flavor":"posix","text":"x,y\u00e9aUNC \\\\C","str":"x,y\u00e9aUNC \\\\C","parts":["x,y\u00e9aUNC \\\\C"],"absolute":false,"name":"x,y\u00e9aUNC \\\\C","parents":["."]}, +{"flavor":"posix","text":" ","str":" ","parts":[" "],"absolute":false,"name":" ","parents":["."]}, +{"flavor":"posix","text":"..b.a","str":"..b.a","parts":["..b.a"],"absolute":false,"name":"..b.a","parents":["."]}, +{"flavor":"posix","text":"\\\\.C","str":"\\\\.C","parts":["\\\\.C"],"absolute":false,"name":"\\\\.C","parents":["."]}, +{"flavor":"posix","text":" // /","str":" / ","parts":[" "," "],"absolute":false,"name":" ","parents":[" ","."]}, +{"flavor":"posix","text":"\u0000? cCUNC;","str":"\u0000? cCUNC;","parts":["\u0000? cCUNC;"],"absolute":false,"name":"\u0000? cCUNC;","parents":["."]}, +{"flavor":"posix","text":"?\u00e9 \\\\","str":"?\u00e9 \\\\","parts":["?\u00e9 \\\\"],"absolute":false,"name":"?\u00e9 \\\\","parents":["."]}, +{"flavor":"posix","text":" ","str":" ","parts":[" "],"absolute":false,"name":" ","parents":["."]}, +{"flavor":"posix","text":"cunc//","str":"cunc","parts":["cunc"],"absolute":false,"name":"cunc","parents":["."]}, +{"flavor":"posix","text":"\\\\\u00e9","str":"\\\\\u00e9","parts":["\\\\\u00e9"],"absolute":false,"name":"\\\\\u00e9","parents":["."]}, +{"flavor":"posix","text":"unc\u00e9\\\\","str":"unc\u00e9\\\\","parts":["unc\u00e9\\\\"],"absolute":false,"name":"unc\u00e9\\\\","parents":["."]}, +{"flavor":"posix","text":"\u0000","str":"\u0000","parts":["\u0000"],"absolute":false,"name":"\u0000","parents":["."]}, +{"flavor":"posix","text":"UNC;x,y","str":"UNC;x,y","parts":["UNC;x,y"],"absolute":false,"name":"UNC;x,y","parents":["."]}, +{"flavor":"posix","text":"UNC.c ","str":"UNC.c ","parts":["UNC.c "],"absolute":false,"name":"UNC.c ","parents":["."]}, +{"flavor":"posix","text":"a \u00e9..///","str":"a \u00e9..","parts":["a \u00e9.."],"absolute":false,"name":"a \u00e9..","parents":["."]}, +{"flavor":"posix","text":"x,y\u00e9x,y/ \\\u0000","str":"x,y\u00e9x,y/ \\\u0000","parts":["x,y\u00e9x,y"," \\\u0000"],"absolute":false,"name":" \\\u0000","parents":["x,y\u00e9x,y","."]}, +{"flavor":"posix","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"\\","parents":["."]}, +{"flavor":"posix","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"\\","parents":["."]}, +{"flavor":"posix","text":" b.","str":" b.","parts":[" b."],"absolute":false,"name":" b.","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":";..//Ca;","str":";../Ca;","parts":[";..","Ca;"],"absolute":false,"name":"Ca;","parents":[";..","."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"C\\/?UNC","str":"C\\/?UNC","parts":["C\\","?UNC"],"absolute":false,"name":"?UNC","parents":["C\\","."]}, +{"flavor":"posix","text":".c","str":".c","parts":[".c"],"absolute":false,"name":".c","parents":["."]}, +{"flavor":"posix","text":"C","str":"C","parts":["C"],"absolute":false,"name":"C","parents":["."]}, +{"flavor":"posix","text":"..;UNCx,y;UNC/","str":"..;UNCx,y;UNC","parts":["..;UNCx,y;UNC"],"absolute":false,"name":"..;UNCx,y;UNC","parents":["."]}, +{"flavor":"posix","text":":/\\;//x,y","str":":/\\;/x,y","parts":[":","\\;","x,y"],"absolute":false,"name":"x,y","parents":[":/\\;",":","."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"aC ///","str":"aC ","parts":["aC "],"absolute":false,"name":"aC ","parents":["."]}, +{"flavor":"posix","text":"/C","str":"/C","parts":["/","C"],"absolute":true,"name":"C","parents":["/"]}, +{"flavor":"posix","text":"x,yUNC??C?","str":"x,yUNC??C?","parts":["x,yUNC??C?"],"absolute":false,"name":"x,yUNC??C?","parents":["."]}, +{"flavor":"posix","text":"Cbb\u0000UNC","str":"Cbb\u0000UNC","parts":["Cbb\u0000UNC"],"absolute":false,"name":"Cbb\u0000UNC","parents":["."]}, +{"flavor":"posix","text":"a/","str":"a","parts":["a"],"absolute":false,"name":"a","parents":["."]}, +{"flavor":"posix","text":"cuncUNC\\\\...a","str":"cuncUNC\\\\...a","parts":["cuncUNC\\\\...a"],"absolute":false,"name":"cuncUNC\\\\...a","parents":["."]}, +{"flavor":"posix","text":"uncUNC.","str":"uncUNC.","parts":["uncUNC."],"absolute":false,"name":"uncUNC.","parents":["."]}, +{"flavor":"posix","text":"/\u00e9\\bCUNC;","str":"/\u00e9\\bCUNC;","parts":["/","\u00e9\\bCUNC;"],"absolute":true,"name":"\u00e9\\bCUNC;","parents":["/"]}, +{"flavor":"posix","text":";","str":";","parts":[";"],"absolute":false,"name":";","parents":["."]}, +{"flavor":"posix","text":"\u00e9 UNCaC:","str":"\u00e9 UNCaC:","parts":["\u00e9 UNCaC:"],"absolute":false,"name":"\u00e9 UNCaC:","parents":["."]}, +{"flavor":"posix","text":".?","str":".?","parts":[".?"],"absolute":false,"name":".?","parents":["."]}, +{"flavor":"posix","text":"..","str":"..","parts":[".."],"absolute":false,"name":"..","parents":["."]}, +{"flavor":"posix","text":"x,yx,y","str":"x,yx,y","parts":["x,yx,y"],"absolute":false,"name":"x,yx,y","parents":["."]}, +{"flavor":"posix","text":"?/a/;UNCx,y","str":"?/a/;UNCx,y","parts":["?","a",";UNCx,y"],"absolute":false,"name":";UNCx,y","parents":["?/a","?","."]}, +{"flavor":"posix","text":"\\Cb..","str":"\\Cb..","parts":["\\Cb.."],"absolute":false,"name":"\\Cb..","parents":["."]}, +{"flavor":"posix","text":":?/\u00e9\\\\\u0000b","str":":?/\u00e9\\\\\u0000b","parts":[":?","\u00e9\\\\\u0000b"],"absolute":false,"name":"\u00e9\\\\\u0000b","parents":[":?","."]}, +{"flavor":"posix","text":"/","str":"/","parts":["/"],"absolute":true,"name":"","parents":[]}, +{"flavor":"posix","text":"?\\\\a;","str":"?\\\\a;","parts":["?\\\\a;"],"absolute":false,"name":"?\\\\a;","parents":["."]}, +{"flavor":"posix","text":"//","str":"//","parts":["//"],"absolute":true,"name":"","parents":[]}, +{"flavor":"posix","text":"?","str":"?","parts":["?"],"absolute":false,"name":"?","parents":["."]}, +{"flavor":"posix","text":"?;/\u00e9UNC\\","str":"?;/\u00e9UNC\\","parts":["?;","\u00e9UNC\\"],"absolute":false,"name":"\u00e9UNC\\","parents":["?;","."]}, +{"flavor":"posix","text":" ?c..b","str":" ?c..b","parts":[" ?c..b"],"absolute":false,"name":" ?c..b","parents":["."]}, +{"flavor":"posix","text":";/C\u00e9.","str":";/C\u00e9.","parts":[";","C\u00e9."],"absolute":false,"name":"C\u00e9.","parents":[";","."]}, +{"flavor":"posix","text":";;bUNC\\\\..","str":";;bUNC\\\\..","parts":[";;bUNC\\\\.."],"absolute":false,"name":";;bUNC\\\\..","parents":["."]}, +{"flavor":"posix","text":" \\ \\//","str":" \\ \\","parts":[" \\ \\"],"absolute":false,"name":" \\ \\","parents":["."]}, +{"flavor":"posix","text":"cca\\\\ ","str":"cca\\\\ ","parts":["cca\\\\ "],"absolute":false,"name":"cca\\\\ ","parents":["."]}, +{"flavor":"posix","text":"/.\\\\cCUNC//","str":"/.\\\\cCUNC","parts":["/",".\\\\cCUNC"],"absolute":true,"name":".\\\\cCUNC","parents":["/"]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"b/c","str":"b/c","parts":["b","c"],"absolute":false,"name":"c","parents":["b","."]}, +{"flavor":"posix","text":"UNCx,y? unc","str":"UNCx,y? unc","parts":["UNCx,y? unc"],"absolute":false,"name":"UNCx,y? unc","parents":["."]}, +{"flavor":"posix","text":"UNC.unc\\.","str":"UNC.unc\\.","parts":["UNC.unc\\."],"absolute":false,"name":"UNC.unc\\.","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"//cC//..x,yC","str":"//cC/..x,yC","parts":["//","cC","..x,yC"],"absolute":true,"name":"..x,yC","parents":["//cC","//"]}, +{"flavor":"posix","text":"x,y ....","str":"x,y ....","parts":["x,y ...."],"absolute":false,"name":"x,y ....","parents":["."]}, +{"flavor":"posix","text":"; \\\\/:\u0000?","str":"; \\\\/:\u0000?","parts":["; \\\\",":\u0000?"],"absolute":false,"name":":\u0000?","parents":["; \\\\","."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"UNC?a\\\\","str":"UNC?a\\\\","parts":["UNC?a\\\\"],"absolute":false,"name":"UNC?a\\\\","parents":["."]}, +{"flavor":"posix","text":"/unc","str":"/unc","parts":["/","unc"],"absolute":true,"name":"unc","parents":["/"]}, +{"flavor":"posix","text":"/a\\..c?","str":"/a\\..c?","parts":["/","a\\..c?"],"absolute":true,"name":"a\\..c?","parents":["/"]}, +{"flavor":"posix","text":"//;:a","str":"//;:a","parts":["//",";:a"],"absolute":true,"name":";:a","parents":["//"]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"\u00e9/b?:;","str":"\u00e9/b?:;","parts":["\u00e9","b?:;"],"absolute":false,"name":"b?:;","parents":["\u00e9","."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"unc.//\u00e9/ :;","str":"unc./\u00e9/ :;","parts":["unc.","\u00e9"," :;"],"absolute":false,"name":" :;","parents":["unc./\u00e9","unc.","."]}, +{"flavor":"posix","text":" //;b?c.:","str":" /;b?c.:","parts":[" ",";b?c.:"],"absolute":false,"name":";b?c.:","parents":[" ","."]}, +{"flavor":"posix","text":"b\\a\\..?.","str":"b\\a\\..?.","parts":["b\\a\\..?."],"absolute":false,"name":"b\\a\\..?.","parents":["."]}, +{"flavor":"posix","text":"//","str":"//","parts":["//"],"absolute":true,"name":"","parents":[]}, +{"flavor":"posix","text":"//..\\C/..\u0000\u00e9","str":"//..\\C/..\u0000\u00e9","parts":["//","..\\C","..\u0000\u00e9"],"absolute":true,"name":"..\u0000\u00e9","parents":["//..\\C","//"]}, +{"flavor":"posix","text":"\\\\x,ya;bc","str":"\\\\x,ya;bc","parts":["\\\\x,ya;bc"],"absolute":false,"name":"\\\\x,ya;bc","parents":["."]}, +{"flavor":"posix","text":"UNCunc\\\\;.","str":"UNCunc\\\\;.","parts":["UNCunc\\\\;."],"absolute":false,"name":"UNCunc\\\\;.","parents":["."]}, +{"flavor":"posix","text":"cx,yc\\b","str":"cx,yc\\b","parts":["cx,yc\\b"],"absolute":false,"name":"cx,yc\\b","parents":["."]}, +{"flavor":"posix","text":"aCa\u0000C?","str":"aCa\u0000C?","parts":["aCa\u0000C?"],"absolute":false,"name":"aCa\u0000C?","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"x,y","str":"x,y","parts":["x,y"],"absolute":false,"name":"x,y","parents":["."]}, +{"flavor":"posix","text":"UNCx,y\u00e9","str":"UNCx,y\u00e9","parts":["UNCx,y\u00e9"],"absolute":false,"name":"UNCx,y\u00e9","parents":["."]}, +{"flavor":"posix","text":"a ","str":"a ","parts":["a "],"absolute":false,"name":"a ","parents":["."]}, +{"flavor":"posix","text":"?unc x,y//;a","str":"?unc x,y/;a","parts":["?unc x,y",";a"],"absolute":false,"name":";a","parents":["?unc x,y","."]}, +{"flavor":"posix","text":".../\u0000\\c","str":".../\u0000\\c","parts":["...","\u0000\\c"],"absolute":false,"name":"\u0000\\c","parents":["...","."]}, +{"flavor":"posix","text":"a?/\u00e9CCb?","str":"a?/\u00e9CCb?","parts":["a?","\u00e9CCb?"],"absolute":false,"name":"\u00e9CCb?","parents":["a?","."]}, +{"flavor":"posix","text":"b","str":"b","parts":["b"],"absolute":false,"name":"b","parents":["."]}, +{"flavor":"posix","text":"UNCUNC\\\\","str":"UNCUNC\\\\","parts":["UNCUNC\\\\"],"absolute":false,"name":"UNCUNC\\\\","parents":["."]}, +{"flavor":"posix","text":"b.","str":"b.","parts":["b."],"absolute":false,"name":"b.","parents":["."]}, +{"flavor":"posix","text":":UNC\u00e9\u00e9a/..","str":":UNC\u00e9\u00e9a/..","parts":[":UNC\u00e9\u00e9a",".."],"absolute":false,"name":"..","parents":[":UNC\u00e9\u00e9a","."]}, +{"flavor":"posix","text":"C?x,yx,y.. \\?","str":"C?x,yx,y.. \\?","parts":["C?x,yx,y.. \\?"],"absolute":false,"name":"C?x,yx,y.. \\?","parents":["."]}, +{"flavor":"posix","text":"c:b cC..","str":"c:b cC..","parts":["c:b cC.."],"absolute":false,"name":"c:b cC..","parents":["."]}, +{"flavor":"posix","text":"///..\u0000 \u0000//","str":"/..\u0000 \u0000","parts":["/","..\u0000 \u0000"],"absolute":true,"name":"..\u0000 \u0000","parents":["/"]}, +{"flavor":"posix","text":";;a?x,y\\\\ ?","str":";;a?x,y\\\\ ?","parts":[";;a?x,y\\\\ ?"],"absolute":false,"name":";;a?x,y\\\\ ?","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"C","str":"C","parts":["C"],"absolute":false,"name":"C","parents":["."]}, +{"flavor":"posix","text":"cx,ya//unc?","str":"cx,ya/unc?","parts":["cx,ya","unc?"],"absolute":false,"name":"unc?","parents":["cx,ya","."]}, +{"flavor":"posix","text":"//","str":"//","parts":["//"],"absolute":true,"name":"","parents":[]}, +{"flavor":"posix","text":":/","str":":","parts":[":"],"absolute":false,"name":":","parents":["."]}, +{"flavor":"posix","text":";b\u00e9..;.:\u0000","str":";b\u00e9..;.:\u0000","parts":[";b\u00e9..;.:\u0000"],"absolute":false,"name":";b\u00e9..;.:\u0000","parents":["."]}, +{"flavor":"posix","text":"c;","str":"c;","parts":["c;"],"absolute":false,"name":"c;","parents":["."]}, +{"flavor":"posix","text":"//","str":"//","parts":["//"],"absolute":true,"name":"","parents":[]}, +{"flavor":"posix","text":" //..\u0000 a","str":" /..\u0000 a","parts":[" ","..\u0000 a"],"absolute":false,"name":"..\u0000 a","parents":[" ","."]}, +{"flavor":"posix","text":":cC.","str":":cC.","parts":[":cC."],"absolute":false,"name":":cC.","parents":["."]}, +{"flavor":"posix","text":"uncb\u00e9..\\\\a/..","str":"uncb\u00e9..\\\\a/..","parts":["uncb\u00e9..\\\\a",".."],"absolute":false,"name":"..","parents":["uncb\u00e9..\\\\a","."]}, +{"flavor":"posix","text":"\u00e9..\\\\b. UNCUNC","str":"\u00e9..\\\\b. UNCUNC","parts":["\u00e9..\\\\b. UNCUNC"],"absolute":false,"name":"\u00e9..\\\\b. UNCUNC","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"c\\ \u0000","str":"c\\ \u0000","parts":["c\\ \u0000"],"absolute":false,"name":"c\\ \u0000","parents":["."]}, +{"flavor":"posix","text":":","str":":","parts":[":"],"absolute":false,"name":":","parents":["."]}, +{"flavor":"posix","text":" ","str":" ","parts":[" "],"absolute":false,"name":" ","parents":["."]}, +{"flavor":"posix","text":"?C...","str":"?C...","parts":["?C..."],"absolute":false,"name":"?C...","parents":["."]}, +{"flavor":"posix","text":"cunc?\u0000","str":"cunc?\u0000","parts":["cunc?\u0000"],"absolute":false,"name":"cunc?\u0000","parents":["."]}, +{"flavor":"posix","text":" \\c\u00e9\\\\\\//","str":" \\c\u00e9\\\\\\","parts":[" \\c\u00e9\\\\\\"],"absolute":false,"name":" \\c\u00e9\\\\\\","parents":["."]}, +{"flavor":"posix","text":"UNCx,y","str":"UNCx,y","parts":["UNCx,y"],"absolute":false,"name":"UNCx,y","parents":["."]}, +{"flavor":"posix","text":"cUNC; UNC?.c","str":"cUNC; UNC?.c","parts":["cUNC; UNC?.c"],"absolute":false,"name":"cUNC; UNC?.c","parents":["."]}, +{"flavor":"posix","text":"?..","str":"?..","parts":["?.."],"absolute":false,"name":"?..","parents":["."]}, +{"flavor":"posix","text":" b ac../","str":" b ac..","parts":[" b ac.."],"absolute":false,"name":" b ac..","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"..\\?:x,y;\\\\\u00e9","str":"..\\?:x,y;\\\\\u00e9","parts":["..\\?:x,y;\\\\\u00e9"],"absolute":false,"name":"..\\?:x,y;\\\\\u00e9","parents":["."]}, +{"flavor":"posix","text":"\\.unc/..","str":"\\.unc/..","parts":["\\.unc",".."],"absolute":false,"name":"..","parents":["\\.unc","."]}, +{"flavor":"posix","text":"/....\\\\ a\u00e9","str":"/....\\\\ a\u00e9","parts":["/","....\\\\ a\u00e9"],"absolute":true,"name":"....\\\\ a\u00e9","parents":["/"]}, +{"flavor":"posix","text":"/\u00e9UNCc..\\b","str":"/\u00e9UNCc..\\b","parts":["/","\u00e9UNCc..\\b"],"absolute":true,"name":"\u00e9UNCc..\\b","parents":["/"]}, +{"flavor":"posix","text":"\\b","str":"\\b","parts":["\\b"],"absolute":false,"name":"\\b","parents":["."]}, +{"flavor":"posix","text":".:x,y::UNCb","str":".:x,y::UNCb","parts":[".:x,y::UNCb"],"absolute":false,"name":".:x,y::UNCb","parents":["."]}, +{"flavor":"posix","text":"?:;b\u0000a","str":"?:;b\u0000a","parts":["?:;b\u0000a"],"absolute":false,"name":"?:;b\u0000a","parents":["."]}, +{"flavor":"posix","text":"bunc//:x,y","str":"bunc/:x,y","parts":["bunc",":x,y"],"absolute":false,"name":":x,y","parents":["bunc","."]}, +{"flavor":"posix","text":"?\u0000\u0000/ \u00e9\u0000","str":"?\u0000\u0000/ \u00e9\u0000","parts":["?\u0000\u0000"," \u00e9\u0000"],"absolute":false,"name":" \u00e9\u0000","parents":["?\u0000\u0000","."]}, +{"flavor":"posix","text":"//C.\u00e9.","str":"//C.\u00e9.","parts":["//","C.\u00e9."],"absolute":true,"name":"C.\u00e9.","parents":["//"]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"CUNC","str":"CUNC","parts":["CUNC"],"absolute":false,"name":"CUNC","parents":["."]}, +{"flavor":"posix","text":"a unc? C\\\\","str":"a unc? C\\\\","parts":["a unc? C\\\\"],"absolute":false,"name":"a unc? C\\\\","parents":["."]}, +{"flavor":"posix","text":"\\.","str":"\\.","parts":["\\."],"absolute":false,"name":"\\.","parents":["."]}, +{"flavor":"posix","text":"b","str":"b","parts":["b"],"absolute":false,"name":"b","parents":["."]}, +{"flavor":"posix","text":"x,yc;unc.. ","str":"x,yc;unc.. ","parts":["x,yc;unc.. "],"absolute":false,"name":"x,yc;unc.. ","parents":["."]}, +{"flavor":"posix","text":".x,y","str":".x,y","parts":[".x,y"],"absolute":false,"name":".x,y","parents":["."]}, +{"flavor":"posix","text":"\\\u0000.","str":"\\\u0000.","parts":["\\\u0000."],"absolute":false,"name":"\\\u0000.","parents":["."]}, +{"flavor":"posix","text":";..C ","str":";..C ","parts":[";..C "],"absolute":false,"name":";..C ","parents":["."]}, +{"flavor":"posix","text":"\\unc;..?: ..","str":"\\unc;..?: ..","parts":["\\unc;..?: .."],"absolute":false,"name":"\\unc;..?: ..","parents":["."]}, +{"flavor":"posix","text":" cunc \\","str":" cunc \\","parts":[" cunc \\"],"absolute":false,"name":" cunc \\","parents":["."]}, +{"flavor":"posix","text":"b","str":"b","parts":["b"],"absolute":false,"name":"b","parents":["."]}, +{"flavor":"posix","text":" c\u0000","str":" c\u0000","parts":[" c\u0000"],"absolute":false,"name":" c\u0000","parents":["."]}, +{"flavor":"posix","text":"\\\\","str":"\\\\","parts":["\\\\"],"absolute":false,"name":"\\\\","parents":["."]}, +{"flavor":"posix","text":"C \\?","str":"C \\?","parts":["C \\?"],"absolute":false,"name":"C \\?","parents":["."]}, +{"flavor":"posix","text":"ca//C","str":"ca/C","parts":["ca","C"],"absolute":false,"name":"C","parents":["ca","."]}, +{"flavor":"posix","text":"..","str":"..","parts":[".."],"absolute":false,"name":"..","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"\u0000\u0000\\C\u00e9","str":"\u0000\u0000\\C\u00e9","parts":["\u0000\u0000\\C\u00e9"],"absolute":false,"name":"\u0000\u0000\\C\u00e9","parents":["."]}, +{"flavor":"posix","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"posix","text":"UNCbUNC","str":"UNCbUNC","parts":["UNCbUNC"],"absolute":false,"name":"UNCbUNC","parents":["."]}, +{"flavor":"posix","text":"ca\u00e9a","str":"ca\u00e9a","parts":["ca\u00e9a"],"absolute":false,"name":"ca\u00e9a","parents":["."]}, +{"flavor":"posix","text":";//;UNC","str":";/;UNC","parts":[";",";UNC"],"absolute":false,"name":";UNC","parents":[";","."]}, +{"flavor":"posix","text":"cCx,yunc","str":"cCx,yunc","parts":["cCx,yunc"],"absolute":false,"name":"cCx,yunc","parents":["."]}, +{"flavor":"posix","text":"C","str":"C","parts":["C"],"absolute":false,"name":"C","parents":["."]}, +{"flavor":"posix","text":".//\u00e9?\u00e9b\u00e9","str":"\u00e9?\u00e9b\u00e9","parts":["\u00e9?\u00e9b\u00e9"],"absolute":false,"name":"\u00e9?\u00e9b\u00e9","parents":["."]}, +{"flavor":"posix","text":"\u00e9","str":"\u00e9","parts":["\u00e9"],"absolute":false,"name":"\u00e9","parents":["."]}, +{"flavor":"posix","text":":\\\\CUNCa:?unc","str":":\\\\CUNCa:?unc","parts":[":\\\\CUNCa:?unc"],"absolute":false,"name":":\\\\CUNCa:?unc","parents":["."]}, +{"flavor":"posix","text":"UNC","str":"UNC","parts":["UNC"],"absolute":false,"name":"UNC","parents":["."]}, +{"flavor":"posix","text":" a..;/a ","str":" a..;/a ","parts":[" a..;","a "],"absolute":false,"name":"a ","parents":[" a..;","."]}, +{"flavor":"posix","text":"\u00e9.c","str":"\u00e9.c","parts":["\u00e9.c"],"absolute":false,"name":"\u00e9.c","parents":["."]}, +{"flavor":"posix","text":"...//UNCb/a","str":".../UNCb/a","parts":["...","UNCb","a"],"absolute":false,"name":"a","parents":[".../UNCb","...","."]}, +{"flavor":"posix","text":"////;\\\\;\\\\//c","str":"/;\\\\;\\\\/c","parts":["/",";\\\\;\\\\","c"],"absolute":true,"name":"c","parents":["/;\\\\;\\\\","/"]}, +{"flavor":"posix","text":"b;..\u0000UNC\u0000unc","str":"b;..\u0000UNC\u0000unc","parts":["b;..\u0000UNC\u0000unc"],"absolute":false,"name":"b;..\u0000UNC\u0000unc","parents":["."]}, +{"flavor":"posix","text":"?","str":"?","parts":["?"],"absolute":false,"name":"?","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":".","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"./a","str":"a","parts":["a"],"absolute":false,"name":"a","parents":["."]}, +{"flavor":"windows","text":"a/./b/","str":"a\\b","parts":["a","b"],"absolute":false,"name":"b","parents":["a","."]}, +{"flavor":"windows","text":"a//b","str":"a\\b","parts":["a","b"],"absolute":false,"name":"b","parents":["a","."]}, +{"flavor":"windows","text":"a/../b","str":"a\\..\\b","parts":["a","..","b"],"absolute":false,"name":"b","parents":["a\\..","a","."]}, +{"flavor":"windows","text":"/a","str":"\\a","parts":["\\","a"],"absolute":false,"name":"a","parents":["\\"]}, +{"flavor":"windows","text":"//a","str":"\\\\a","parts":["\\\\a"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"///a","str":"\\\\\\a","parts":["\\\\\\a"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"a\\b","str":"a\\b","parts":["a","b"],"absolute":false,"name":"b","parents":["a","."]}, +{"flavor":"windows","text":"c:x","str":"c:x","parts":["c:","x"],"absolute":false,"name":"x","parents":["c:"]}, +{"flavor":"windows","text":"C:/a/b/","str":"C:\\a\\b","parts":["C:\\","a","b"],"absolute":true,"name":"b","parents":["C:\\a","C:\\"]}, +{"flavor":"windows","text":"c:","str":"c:","parts":["c:"],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"./c:x","str":".\\c:x","parts":["c:x"],"absolute":false,"name":"c:x","parents":["."]}, +{"flavor":"windows","text":"//server/share","str":"\\\\server\\share\\","parts":["\\\\server\\share\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"//server/share/x","str":"\\\\server\\share\\x","parts":["\\\\server\\share\\","x"],"absolute":true,"name":"x","parents":["\\\\server\\share\\"]}, +{"flavor":"windows","text":"\\\\?\\C:\\x","str":"\\\\?\\C:\\x","parts":["\\\\?\\C:\\","x"],"absolute":true,"name":"x","parents":["\\\\?\\C:\\"]}, +{"flavor":"windows","text":"//?/UNC/server/share","str":"\\\\?\\UNC\\server\\share\\","parts":["\\\\?\\UNC\\server\\share\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"//?/UNC/server","str":"\\\\?\\UNC\\server","parts":["\\\\?\\UNC\\server"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"\\\\.\\device\\x","str":"\\\\.\\device\\x","parts":["\\\\.\\device\\","x"],"absolute":true,"name":"x","parents":["\\\\.\\device\\"]}, +{"flavor":"windows","text":"\\a","str":"\\a","parts":["\\","a"],"absolute":false,"name":"a","parents":["\\"]}, +{"flavor":"windows","text":"\u00e9:x","str":"\u00e9:x","parts":["\u00e9:","x"],"absolute":false,"name":"x","parents":["\u00e9:"]}, +{"flavor":"windows","text":"\\\\\\x","str":"\\\\\\x","parts":["\\\\\\x"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"//?/","str":"\\\\?\\","parts":["\\\\?\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"x:y:z","str":"x:y:z","parts":["x:","y:z"],"absolute":false,"name":"y:z","parents":["x:"]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"uncb..x,y/\\/","str":"uncb..x,y","parts":["uncb..x,y"],"absolute":false,"name":"uncb..x,y","parents":["."]}, +{"flavor":"windows","text":"\u00e9/","str":"\u00e9","parts":["\u00e9"],"absolute":false,"name":"\u00e9","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":";\u0000?\u00e9","str":";\u0000?\u00e9","parts":[";\u0000?\u00e9"],"absolute":false,"name":";\u0000?\u00e9","parents":["."]}, +{"flavor":"windows","text":"x,y//.?\\..\u0000","str":"x,y\\.?\\..\u0000","parts":["x,y",".?","..\u0000"],"absolute":false,"name":"..\u0000","parents":["x,y\\.?","x,y","."]}, +{"flavor":"windows","text":"x,y\u0000;..","str":"x,y\u0000;..","parts":["x,y\u0000;.."],"absolute":false,"name":"x,y\u0000;..","parents":["."]}, +{"flavor":"windows","text":"\\\\..uncC","str":"\\\\..uncC","parts":["\\\\..uncC"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"a\\\\uncb//","str":"a\\uncb","parts":["a","uncb"],"absolute":false,"name":"uncb","parents":["a","."]}, +{"flavor":"windows","text":":a\u0000","str":":a\u0000","parts":[":a\u0000"],"absolute":false,"name":":a\u0000","parents":["."]}, +{"flavor":"windows","text":";UNC\\\\bUNC","str":";UNC\\bUNC","parts":[";UNC","bUNC"],"absolute":false,"name":"bUNC","parents":[";UNC","."]}, +{"flavor":"windows","text":"\u00e9c?;aCb\u0000","str":"\u00e9c?;aCb\u0000","parts":["\u00e9c?;aCb\u0000"],"absolute":false,"name":"\u00e9c?;aCb\u0000","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":":..","str":":..","parts":[":.."],"absolute":false,"name":":..","parents":["."]}, +{"flavor":"windows","text":":C\u0000.\u0000.","str":":C\u0000.\u0000.","parts":[":C\u0000.\u0000."],"absolute":false,"name":":C\u0000.\u0000.","parents":["."]}, +{"flavor":"windows","text":"C:...","str":"C:...","parts":["C:","..."],"absolute":false,"name":"...","parents":["C:"]}, +{"flavor":"windows","text":"C\u00e9;\\\\Cunc\u00e9/","str":"C\u00e9;\\Cunc\u00e9","parts":["C\u00e9;","Cunc\u00e9"],"absolute":false,"name":"Cunc\u00e9","parents":["C\u00e9;","."]}, +{"flavor":"windows","text":"\\\\","str":"\\\\","parts":["\\\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":":C\u0000 \u00e9","str":":C\u0000 \u00e9","parts":[":C\u0000 \u00e9"],"absolute":false,"name":":C\u0000 \u00e9","parents":["."]}, +{"flavor":"windows","text":"UNC\\\\\\..cc","str":"UNC\\..cc","parts":["UNC","..cc"],"absolute":false,"name":"..cc","parents":["UNC","."]}, +{"flavor":"windows","text":" ////","str":" ","parts":[" "],"absolute":false,"name":" ","parents":["."]}, +{"flavor":"windows","text":".?..","str":".?..","parts":[".?.."],"absolute":false,"name":".?..","parents":["."]}, +{"flavor":"windows","text":"?UNC//","str":"?UNC","parts":["?UNC"],"absolute":false,"name":"?UNC","parents":["."]}, +{"flavor":"windows","text":"..//\u00e9\u0000\u0000unc\u00e9","str":"..\\\u00e9\u0000\u0000unc\u00e9","parts":["..","\u00e9\u0000\u0000unc\u00e9"],"absolute":false,"name":"\u00e9\u0000\u0000unc\u00e9","parents":["..","."]}, +{"flavor":"windows","text":".bb\u0000 //\\\\a","str":".bb\u0000 \\a","parts":[".bb\u0000 ","a"],"absolute":false,"name":"a","parents":[".bb\u0000 ","."]}, +{"flavor":"windows","text":"bbx,yx,yx,y/ ","str":"bbx,yx,yx,y\\ ","parts":["bbx,yx,yx,y"," "],"absolute":false,"name":" ","parents":["bbx,yx,yx,y","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"\\?:","str":"\\?:","parts":["\\","?:"],"absolute":false,"name":"?:","parents":["\\"]}, +{"flavor":"windows","text":"unc;\u00e9;..\\\\","str":"unc;\u00e9;..","parts":["unc;\u00e9;.."],"absolute":false,"name":"unc;\u00e9;..","parents":["."]}, +{"flavor":"windows","text":"/?\\\u00e9?:","str":"\\?\\\u00e9?:","parts":["\\","?","\u00e9?:"],"absolute":false,"name":"\u00e9?:","parents":["\\?","\\"]}, +{"flavor":"windows","text":" ac..Cx,y","str":" ac..Cx,y","parts":[" ac..Cx,y"],"absolute":false,"name":" ac..Cx,y","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":" ?cx,yC","str":" ?cx,yC","parts":[" ?cx,yC"],"absolute":false,"name":" ?cx,yC","parents":["."]}, +{"flavor":"windows","text":"C: C\u00e9unc;unc","str":"C: C\u00e9unc;unc","parts":["C:"," C\u00e9unc;unc"],"absolute":false,"name":" C\u00e9unc;unc","parents":["C:"]}, +{"flavor":"windows","text":"\u0000uncUNC?","str":"\u0000uncUNC?","parts":["\u0000uncUNC?"],"absolute":false,"name":"\u0000uncUNC?","parents":["."]}, +{"flavor":"windows","text":"\\\\b/c","str":"\\\\b\\c\\","parts":["\\\\b\\c\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"\\\\ C","str":"\\\\ C","parts":["\\\\ C"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"C\\;unc\\\\;","str":"C\\;unc\\;","parts":["C",";unc",";"],"absolute":false,"name":";","parents":["C\\;unc","C","."]}, +{"flavor":"windows","text":"?bunc\u0000c?unc","str":"?bunc\u0000c?unc","parts":["?bunc\u0000c?unc"],"absolute":false,"name":"?bunc\u0000c?unc","parents":["."]}, +{"flavor":"windows","text":"b?","str":"b?","parts":["b?"],"absolute":false,"name":"b?","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"\\\\Cbx,y;c","str":"\\\\Cbx,y;c","parts":["\\\\Cbx,y;c"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"\u0000Cunc ;x,y","str":"\u0000Cunc ;x,y","parts":["\u0000Cunc ;x,y"],"absolute":false,"name":"\u0000Cunc ;x,y","parents":["."]}, +{"flavor":"windows","text":"..c\u0000\\\\..\u0000","str":"..c\u0000\\..\u0000","parts":["..c\u0000","..\u0000"],"absolute":false,"name":"..\u0000","parents":["..c\u0000","."]}, +{"flavor":"windows","text":"\\\\a","str":"\\\\a","parts":["\\\\a"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"/UNCb // ","str":"\\UNCb \\ ","parts":["\\","UNCb "," "],"absolute":false,"name":" ","parents":["\\UNCb ","\\"]}, +{"flavor":"windows","text":":uncuncUNC\\\\\\","str":":uncuncUNC","parts":[":uncuncUNC"],"absolute":false,"name":":uncuncUNC","parents":["."]}, +{"flavor":"windows","text":"\\\\C..b\u0000a","str":"\\\\C..b\u0000a","parts":["\\\\C..b\u0000a"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"\u0000\u00e9C//uncC:","str":"\u0000\u00e9C\\uncC:","parts":["\u0000\u00e9C","uncC:"],"absolute":false,"name":"uncC:","parents":["\u0000\u00e9C","."]}, +{"flavor":"windows","text":";","str":";","parts":[";"],"absolute":false,"name":";","parents":["."]}, +{"flavor":"windows","text":"//x,y","str":"\\\\x,y","parts":["\\\\x,y"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"/unc","str":"\\unc","parts":["\\","unc"],"absolute":false,"name":"unc","parents":["\\"]}, +{"flavor":"windows","text":".. c","str":".. c","parts":[".. c"],"absolute":false,"name":".. c","parents":["."]}, +{"flavor":"windows","text":"..\\\\a\u0000","str":"..\\a\u0000","parts":["..","a\u0000"],"absolute":false,"name":"a\u0000","parents":["..","."]}, +{"flavor":"windows","text":"/CCUNC:","str":"\\CCUNC:","parts":["\\","CCUNC:"],"absolute":false,"name":"CCUNC:","parents":["\\"]}, +{"flavor":"windows","text":";C\u0000","str":";C\u0000","parts":[";C\u0000"],"absolute":false,"name":";C\u0000","parents":["."]}, +{"flavor":"windows","text":":\u0000:.\\\\uncunc;","str":":\u0000:.\\uncunc;","parts":[":\u0000:.","uncunc;"],"absolute":false,"name":"uncunc;","parents":[":\u0000:.","."]}, +{"flavor":"windows","text":"uncUNC;.\u00e9\u00e9uncb","str":"uncUNC;.\u00e9\u00e9uncb","parts":["uncUNC;.\u00e9\u00e9uncb"],"absolute":false,"name":"uncUNC;.\u00e9\u00e9uncb","parents":["."]}, +{"flavor":"windows","text":"?C.. \\\\","str":"?C.. ","parts":["?C.. "],"absolute":false,"name":"?C.. ","parents":["."]}, +{"flavor":"windows","text":"\u0000\\\\cb:","str":"\u0000\\cb:","parts":["\u0000","cb:"],"absolute":false,"name":"cb:","parents":["\u0000","."]}, +{"flavor":"windows","text":"\u0000: /Cx,yx,y..","str":"\u0000: \\Cx,yx,y..","parts":["\u0000:"," ","Cx,yx,y.."],"absolute":false,"name":"Cx,yx,y..","parents":["\u0000: ","\u0000:"]}, +{"flavor":"windows","text":"\\x,y..;b.","str":"\\x,y..;b.","parts":["\\","x,y..;b."],"absolute":false,"name":"x,y..;b.","parents":["\\"]}, +{"flavor":"windows","text":"\u00e9 b..:","str":"\u00e9 b..:","parts":["\u00e9 b..:"],"absolute":false,"name":"\u00e9 b..:","parents":["."]}, +{"flavor":"windows","text":"\u00e9","str":"\u00e9","parts":["\u00e9"],"absolute":false,"name":"\u00e9","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"x,y","str":"x,y","parts":["x,y"],"absolute":false,"name":"x,y","parents":["."]}, +{"flavor":"windows","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"?;bc ","str":"?;bc ","parts":["?;bc "],"absolute":false,"name":"?;bc ","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"C","str":"C","parts":["C"],"absolute":false,"name":"C","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"\u0000C... \\\\//","str":"\u0000C... ","parts":["\u0000C... "],"absolute":false,"name":"\u0000C... ","parents":["."]}, +{"flavor":"windows","text":"unc/\u00e9x,yC/c","str":"unc\\\u00e9x,yC\\c","parts":["unc","\u00e9x,yC","c"],"absolute":false,"name":"c","parents":["unc\\\u00e9x,yC","unc","."]}, +{"flavor":"windows","text":"C..////C\\","str":"C..\\C","parts":["C..","C"],"absolute":false,"name":"C","parents":["C..","."]}, +{"flavor":"windows","text":"..x,yUNC\\.a::","str":"..x,yUNC\\.a::","parts":["..x,yUNC",".a::"],"absolute":false,"name":".a::","parents":["..x,yUNC","."]}, +{"flavor":"windows","text":"UNCC..ax,y","str":"UNCC..ax,y","parts":["UNCC..ax,y"],"absolute":false,"name":"UNCC..ax,y","parents":["."]}, +{"flavor":"windows","text":"UNCunc\\\\:.. :","str":"UNCunc\\:.. :","parts":["UNCunc",":.. :"],"absolute":false,"name":":.. :","parents":["UNCunc","."]}, +{"flavor":"windows","text":"/? a","str":"\\? a","parts":["\\","? a"],"absolute":false,"name":"? a","parents":["\\"]}, +{"flavor":"windows","text":"/\\","str":"\\\\","parts":["\\\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"uncC\\:ca. ","str":"uncC\\:ca. ","parts":["uncC",":ca. "],"absolute":false,"name":":ca. ","parents":["uncC","."]}, +{"flavor":"windows","text":";b\\\\?b?:.","str":";b\\?b?:.","parts":[";b","?b?:."],"absolute":false,"name":"?b?:.","parents":[";b","."]}, +{"flavor":"windows","text":"\\\\ a;c","str":"\\\\ a;c","parts":["\\\\ a;c"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"b? ///","str":"b? ","parts":["b? "],"absolute":false,"name":"b? ","parents":["."]}, +{"flavor":"windows","text":"\u00e9","str":"\u00e9","parts":["\u00e9"],"absolute":false,"name":"\u00e9","parents":["."]}, +{"flavor":"windows","text":" :\\\\\u00e9Cuncx,y\u0000","str":" :\\\u00e9Cuncx,y\u0000","parts":[" :\\","\u00e9Cuncx,y\u0000"],"absolute":true,"name":"\u00e9Cuncx,y\u0000","parents":[" :\\"]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"\\\\.","str":"\\\\.","parts":["\\\\."],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"..","str":"..","parts":[".."],"absolute":false,"name":"..","parents":["."]}, +{"flavor":"windows","text":"//","str":"\\\\","parts":["\\\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"C\\\\","str":"C","parts":["C"],"absolute":false,"name":"C","parents":["."]}, +{"flavor":"windows","text":"uncb\u0000unc","str":"uncb\u0000unc","parts":["uncb\u0000unc"],"absolute":false,"name":"uncb\u0000unc","parents":["."]}, +{"flavor":"windows","text":"..c..a\u0000","str":"..c..a\u0000","parts":["..c..a\u0000"],"absolute":false,"name":"..c..a\u0000","parents":["."]}, +{"flavor":"windows","text":"\\\\\\\\x,yunc\\\\\u00e9?","str":"\\\\\\\\x,yunc\\\u00e9?","parts":["\\\\\\\\","x,yunc","\u00e9?"],"absolute":true,"name":"\u00e9?","parents":["\\\\\\\\x,yunc","\\\\\\\\"]}, +{"flavor":"windows","text":"x,yax,y/UNC:.","str":"x,yax,y\\UNC:.","parts":["x,yax,y","UNC:."],"absolute":false,"name":"UNC:.","parents":["x,yax,y","."]}, +{"flavor":"windows","text":"/","str":"\\","parts":["\\"],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"a.:\\/","str":"a.:","parts":["a.:"],"absolute":false,"name":"a.:","parents":["."]}, +{"flavor":"windows","text":"Cc;\u0000","str":"Cc;\u0000","parts":["Cc;\u0000"],"absolute":false,"name":"Cc;\u0000","parents":["."]}, +{"flavor":"windows","text":"\\\\ac//\\\\","str":"\\\\ac\\\\","parts":["\\\\ac\\\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"\u0000b x,yUNC\\","str":"\u0000b x,yUNC","parts":["\u0000b x,yUNC"],"absolute":false,"name":"\u0000b x,yUNC","parents":["."]}, +{"flavor":"windows","text":":CcCx,y//\\\\","str":":CcCx,y","parts":[":CcCx,y"],"absolute":false,"name":":CcCx,y","parents":["."]}, +{"flavor":"windows","text":"?C\u0000;C/ ","str":"?C\u0000;C\\ ","parts":["?C\u0000;C"," "],"absolute":false,"name":" ","parents":["?C\u0000;C","."]}, +{"flavor":"windows","text":"\\\\//;a\\\\UNC","str":"\\\\\\\\;a\\UNC","parts":["\\\\\\\\",";a","UNC"],"absolute":true,"name":"UNC","parents":["\\\\\\\\;a","\\\\\\\\"]}, +{"flavor":"windows","text":";;.b","str":";;.b","parts":[";;.b"],"absolute":false,"name":";;.b","parents":["."]}, +{"flavor":"windows","text":"x,y\\\\bunc:;","str":"x,y\\bunc:;","parts":["x,y","bunc:;"],"absolute":false,"name":"bunc:;","parents":["x,y","."]}, +{"flavor":"windows","text":"\\\\C:\u00e9","str":"\\\\C:\u00e9","parts":["\\\\C:\u00e9"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"unc\\..c?unc","str":"unc\\..c?unc","parts":["unc","..c?unc"],"absolute":false,"name":"..c?unc","parents":["unc","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"unc","str":"unc","parts":["unc"],"absolute":false,"name":"unc","parents":["."]}, +{"flavor":"windows","text":". ?x,y","str":". ?x,y","parts":[". ?x,y"],"absolute":false,"name":". ?x,y","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"\u0000\\\\","str":"\u0000","parts":["\u0000"],"absolute":false,"name":"\u0000","parents":["."]}, +{"flavor":"windows","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"UNC/\u0000C","str":"UNC\\\u0000C","parts":["UNC","\u0000C"],"absolute":false,"name":"\u0000C","parents":["UNC","."]}, +{"flavor":"windows","text":" uncunc","str":" uncunc","parts":[" uncunc"],"absolute":false,"name":" uncunc","parents":["."]}, +{"flavor":"windows","text":"\u0000c.?..bC..","str":"\u0000c.?..bC..","parts":["\u0000c.?..bC.."],"absolute":false,"name":"\u0000c.?..bC..","parents":["."]}, +{"flavor":"windows","text":".UNC//;UNCCc//","str":".UNC\\;UNCCc","parts":[".UNC",";UNCCc"],"absolute":false,"name":";UNCCc","parents":[".UNC","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":".x,y","str":".x,y","parts":[".x,y"],"absolute":false,"name":".x,y","parents":["."]}, +{"flavor":"windows","text":": :","str":": :","parts":[": :"],"absolute":false,"name":": :","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"unca:?ba\u0000unc","str":"unca:?ba\u0000unc","parts":["unca:?ba\u0000unc"],"absolute":false,"name":"unca:?ba\u0000unc","parents":["."]}, +{"flavor":"windows","text":":b/:","str":":b\\:","parts":[":b",":"],"absolute":false,"name":":","parents":[":b","."]}, +{"flavor":"windows","text":"UNC\u00e9.\u00e9","str":"UNC\u00e9.\u00e9","parts":["UNC\u00e9.\u00e9"],"absolute":false,"name":"UNC\u00e9.\u00e9","parents":["."]}, +{"flavor":"windows","text":"c\\\\UNCunc\\\u00e9/x,y","str":"c\\UNCunc\\\u00e9\\x,y","parts":["c","UNCunc","\u00e9","x,y"],"absolute":false,"name":"x,y","parents":["c\\UNCunc\\\u00e9","c\\UNCunc","c","."]}, +{"flavor":"windows","text":"x,y\u00e9aUNC \\\\C","str":"x,y\u00e9aUNC \\C","parts":["x,y\u00e9aUNC ","C"],"absolute":false,"name":"C","parents":["x,y\u00e9aUNC ","."]}, +{"flavor":"windows","text":" ","str":" ","parts":[" "],"absolute":false,"name":" ","parents":["."]}, +{"flavor":"windows","text":"..b.a","str":"..b.a","parts":["..b.a"],"absolute":false,"name":"..b.a","parents":["."]}, +{"flavor":"windows","text":"\\\\.C","str":"\\\\.C","parts":["\\\\.C"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":" // /","str":" \\ ","parts":[" "," "],"absolute":false,"name":" ","parents":[" ","."]}, +{"flavor":"windows","text":"\u0000? cCUNC;","str":"\u0000? cCUNC;","parts":["\u0000? cCUNC;"],"absolute":false,"name":"\u0000? cCUNC;","parents":["."]}, +{"flavor":"windows","text":"?\u00e9 \\\\","str":"?\u00e9 ","parts":["?\u00e9 "],"absolute":false,"name":"?\u00e9 ","parents":["."]}, +{"flavor":"windows","text":" ","str":" ","parts":[" "],"absolute":false,"name":" ","parents":["."]}, +{"flavor":"windows","text":"cunc//","str":"cunc","parts":["cunc"],"absolute":false,"name":"cunc","parents":["."]}, +{"flavor":"windows","text":"\\\\\u00e9","str":"\\\\\u00e9","parts":["\\\\\u00e9"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"unc\u00e9\\\\","str":"unc\u00e9","parts":["unc\u00e9"],"absolute":false,"name":"unc\u00e9","parents":["."]}, +{"flavor":"windows","text":"\u0000","str":"\u0000","parts":["\u0000"],"absolute":false,"name":"\u0000","parents":["."]}, +{"flavor":"windows","text":"UNC;x,y","str":"UNC;x,y","parts":["UNC;x,y"],"absolute":false,"name":"UNC;x,y","parents":["."]}, +{"flavor":"windows","text":"UNC.c ","str":"UNC.c ","parts":["UNC.c "],"absolute":false,"name":"UNC.c ","parents":["."]}, +{"flavor":"windows","text":"a \u00e9..///","str":"a \u00e9..","parts":["a \u00e9.."],"absolute":false,"name":"a \u00e9..","parents":["."]}, +{"flavor":"windows","text":"x,y\u00e9x,y/ \\\u0000","str":"x,y\u00e9x,y\\ \\\u0000","parts":["x,y\u00e9x,y"," ","\u0000"],"absolute":false,"name":"\u0000","parents":["x,y\u00e9x,y\\ ","x,y\u00e9x,y","."]}, +{"flavor":"windows","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"\\","str":"\\","parts":["\\"],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":" b.","str":" b.","parts":[" b."],"absolute":false,"name":" b.","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":";..//Ca;","str":";..\\Ca;","parts":[";..","Ca;"],"absolute":false,"name":"Ca;","parents":[";..","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"C\\/?UNC","str":"C\\?UNC","parts":["C","?UNC"],"absolute":false,"name":"?UNC","parents":["C","."]}, +{"flavor":"windows","text":".c","str":".c","parts":[".c"],"absolute":false,"name":".c","parents":["."]}, +{"flavor":"windows","text":"C","str":"C","parts":["C"],"absolute":false,"name":"C","parents":["."]}, +{"flavor":"windows","text":"..;UNCx,y;UNC/","str":"..;UNCx,y;UNC","parts":["..;UNCx,y;UNC"],"absolute":false,"name":"..;UNCx,y;UNC","parents":["."]}, +{"flavor":"windows","text":":/\\;//x,y","str":":\\;\\x,y","parts":[":",";","x,y"],"absolute":false,"name":"x,y","parents":[":\\;",":","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"aC ///","str":"aC ","parts":["aC "],"absolute":false,"name":"aC ","parents":["."]}, +{"flavor":"windows","text":"/C","str":"\\C","parts":["\\","C"],"absolute":false,"name":"C","parents":["\\"]}, +{"flavor":"windows","text":"x,yUNC??C?","str":"x,yUNC??C?","parts":["x,yUNC??C?"],"absolute":false,"name":"x,yUNC??C?","parents":["."]}, +{"flavor":"windows","text":"Cbb\u0000UNC","str":"Cbb\u0000UNC","parts":["Cbb\u0000UNC"],"absolute":false,"name":"Cbb\u0000UNC","parents":["."]}, +{"flavor":"windows","text":"a/","str":"a","parts":["a"],"absolute":false,"name":"a","parents":["."]}, +{"flavor":"windows","text":"cuncUNC\\\\...a","str":"cuncUNC\\...a","parts":["cuncUNC","...a"],"absolute":false,"name":"...a","parents":["cuncUNC","."]}, +{"flavor":"windows","text":"uncUNC.","str":"uncUNC.","parts":["uncUNC."],"absolute":false,"name":"uncUNC.","parents":["."]}, +{"flavor":"windows","text":"/\u00e9\\bCUNC;","str":"\\\u00e9\\bCUNC;","parts":["\\","\u00e9","bCUNC;"],"absolute":false,"name":"bCUNC;","parents":["\\\u00e9","\\"]}, +{"flavor":"windows","text":";","str":";","parts":[";"],"absolute":false,"name":";","parents":["."]}, +{"flavor":"windows","text":"\u00e9 UNCaC:","str":"\u00e9 UNCaC:","parts":["\u00e9 UNCaC:"],"absolute":false,"name":"\u00e9 UNCaC:","parents":["."]}, +{"flavor":"windows","text":".?","str":".?","parts":[".?"],"absolute":false,"name":".?","parents":["."]}, +{"flavor":"windows","text":"..","str":"..","parts":[".."],"absolute":false,"name":"..","parents":["."]}, +{"flavor":"windows","text":"x,yx,y","str":"x,yx,y","parts":["x,yx,y"],"absolute":false,"name":"x,yx,y","parents":["."]}, +{"flavor":"windows","text":"?/a/;UNCx,y","str":"?\\a\\;UNCx,y","parts":["?","a",";UNCx,y"],"absolute":false,"name":";UNCx,y","parents":["?\\a","?","."]}, +{"flavor":"windows","text":"\\Cb..","str":"\\Cb..","parts":["\\","Cb.."],"absolute":false,"name":"Cb..","parents":["\\"]}, +{"flavor":"windows","text":":?/\u00e9\\\\\u0000b","str":":?\\\u00e9\\\u0000b","parts":[":?","\u00e9","\u0000b"],"absolute":false,"name":"\u0000b","parents":[":?\\\u00e9",":?","."]}, +{"flavor":"windows","text":"/","str":"\\","parts":["\\"],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"?\\\\a;","str":"?\\a;","parts":["?","a;"],"absolute":false,"name":"a;","parents":["?","."]}, +{"flavor":"windows","text":"//","str":"\\\\","parts":["\\\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"?","str":"?","parts":["?"],"absolute":false,"name":"?","parents":["."]}, +{"flavor":"windows","text":"?;/\u00e9UNC\\","str":"?;\\\u00e9UNC","parts":["?;","\u00e9UNC"],"absolute":false,"name":"\u00e9UNC","parents":["?;","."]}, +{"flavor":"windows","text":" ?c..b","str":" ?c..b","parts":[" ?c..b"],"absolute":false,"name":" ?c..b","parents":["."]}, +{"flavor":"windows","text":";/C\u00e9.","str":";\\C\u00e9.","parts":[";","C\u00e9."],"absolute":false,"name":"C\u00e9.","parents":[";","."]}, +{"flavor":"windows","text":";;bUNC\\\\..","str":";;bUNC\\..","parts":[";;bUNC",".."],"absolute":false,"name":"..","parents":[";;bUNC","."]}, +{"flavor":"windows","text":" \\ \\//","str":" \\ ","parts":[" "," "],"absolute":false,"name":" ","parents":[" ","."]}, +{"flavor":"windows","text":"cca\\\\ ","str":"cca\\ ","parts":["cca"," "],"absolute":false,"name":" ","parents":["cca","."]}, +{"flavor":"windows","text":"/.\\\\cCUNC//","str":"\\cCUNC","parts":["\\","cCUNC"],"absolute":false,"name":"cCUNC","parents":["\\"]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"b/c","str":"b\\c","parts":["b","c"],"absolute":false,"name":"c","parents":["b","."]}, +{"flavor":"windows","text":"UNCx,y? unc","str":"UNCx,y? unc","parts":["UNCx,y? unc"],"absolute":false,"name":"UNCx,y? unc","parents":["."]}, +{"flavor":"windows","text":"UNC.unc\\.","str":"UNC.unc","parts":["UNC.unc"],"absolute":false,"name":"UNC.unc","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"//cC//..x,yC","str":"\\\\cC\\\\..x,yC","parts":["\\\\cC\\\\","..x,yC"],"absolute":true,"name":"..x,yC","parents":["\\\\cC\\\\"]}, +{"flavor":"windows","text":"x,y ....","str":"x,y ....","parts":["x,y ...."],"absolute":false,"name":"x,y ....","parents":["."]}, +{"flavor":"windows","text":"; \\\\/:\u0000?","str":"; \\:\u0000?","parts":["; ",":\u0000?"],"absolute":false,"name":":\u0000?","parents":["; ","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"UNC?a\\\\","str":"UNC?a","parts":["UNC?a"],"absolute":false,"name":"UNC?a","parents":["."]}, +{"flavor":"windows","text":"/unc","str":"\\unc","parts":["\\","unc"],"absolute":false,"name":"unc","parents":["\\"]}, +{"flavor":"windows","text":"/a\\..c?","str":"\\a\\..c?","parts":["\\","a","..c?"],"absolute":false,"name":"..c?","parents":["\\a","\\"]}, +{"flavor":"windows","text":"//;:a","str":"\\\\;:a","parts":["\\\\;:a"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"\u00e9/b?:;","str":"\u00e9\\b?:;","parts":["\u00e9","b?:;"],"absolute":false,"name":"b?:;","parents":["\u00e9","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"unc.//\u00e9/ :;","str":"unc.\\\u00e9\\ :;","parts":["unc.","\u00e9"," :;"],"absolute":false,"name":" :;","parents":["unc.\\\u00e9","unc.","."]}, +{"flavor":"windows","text":" //;b?c.:","str":" \\;b?c.:","parts":[" ",";b?c.:"],"absolute":false,"name":";b?c.:","parents":[" ","."]}, +{"flavor":"windows","text":"b\\a\\..?.","str":"b\\a\\..?.","parts":["b","a","..?."],"absolute":false,"name":"..?.","parents":["b\\a","b","."]}, +{"flavor":"windows","text":"//","str":"\\\\","parts":["\\\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"//..\\C/..\u0000\u00e9","str":"\\\\..\\C\\..\u0000\u00e9","parts":["\\\\..\\C\\","..\u0000\u00e9"],"absolute":true,"name":"..\u0000\u00e9","parents":["\\\\..\\C\\"]}, +{"flavor":"windows","text":"\\\\x,ya;bc","str":"\\\\x,ya;bc","parts":["\\\\x,ya;bc"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"UNCunc\\\\;.","str":"UNCunc\\;.","parts":["UNCunc",";."],"absolute":false,"name":";.","parents":["UNCunc","."]}, +{"flavor":"windows","text":"cx,yc\\b","str":"cx,yc\\b","parts":["cx,yc","b"],"absolute":false,"name":"b","parents":["cx,yc","."]}, +{"flavor":"windows","text":"aCa\u0000C?","str":"aCa\u0000C?","parts":["aCa\u0000C?"],"absolute":false,"name":"aCa\u0000C?","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"x,y","str":"x,y","parts":["x,y"],"absolute":false,"name":"x,y","parents":["."]}, +{"flavor":"windows","text":"UNCx,y\u00e9","str":"UNCx,y\u00e9","parts":["UNCx,y\u00e9"],"absolute":false,"name":"UNCx,y\u00e9","parents":["."]}, +{"flavor":"windows","text":"a ","str":"a ","parts":["a "],"absolute":false,"name":"a ","parents":["."]}, +{"flavor":"windows","text":"?unc x,y//;a","str":"?unc x,y\\;a","parts":["?unc x,y",";a"],"absolute":false,"name":";a","parents":["?unc x,y","."]}, +{"flavor":"windows","text":".../\u0000\\c","str":"...\\\u0000\\c","parts":["...","\u0000","c"],"absolute":false,"name":"c","parents":["...\\\u0000","...","."]}, +{"flavor":"windows","text":"a?/\u00e9CCb?","str":"a?\\\u00e9CCb?","parts":["a?","\u00e9CCb?"],"absolute":false,"name":"\u00e9CCb?","parents":["a?","."]}, +{"flavor":"windows","text":"b","str":"b","parts":["b"],"absolute":false,"name":"b","parents":["."]}, +{"flavor":"windows","text":"UNCUNC\\\\","str":"UNCUNC","parts":["UNCUNC"],"absolute":false,"name":"UNCUNC","parents":["."]}, +{"flavor":"windows","text":"b.","str":"b.","parts":["b."],"absolute":false,"name":"b.","parents":["."]}, +{"flavor":"windows","text":":UNC\u00e9\u00e9a/..","str":":UNC\u00e9\u00e9a\\..","parts":[":UNC\u00e9\u00e9a",".."],"absolute":false,"name":"..","parents":[":UNC\u00e9\u00e9a","."]}, +{"flavor":"windows","text":"C?x,yx,y.. \\?","str":"C?x,yx,y.. \\?","parts":["C?x,yx,y.. ","?"],"absolute":false,"name":"?","parents":["C?x,yx,y.. ","."]}, +{"flavor":"windows","text":"c:b cC..","str":"c:b cC..","parts":["c:","b cC.."],"absolute":false,"name":"b cC..","parents":["c:"]}, +{"flavor":"windows","text":"///..\u0000 \u0000//","str":"\\\\\\..\u0000 \u0000\\","parts":["\\\\\\..\u0000 \u0000\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":";;a?x,y\\\\ ?","str":";;a?x,y\\ ?","parts":[";;a?x,y"," ?"],"absolute":false,"name":" ?","parents":[";;a?x,y","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"C","str":"C","parts":["C"],"absolute":false,"name":"C","parents":["."]}, +{"flavor":"windows","text":"cx,ya//unc?","str":"cx,ya\\unc?","parts":["cx,ya","unc?"],"absolute":false,"name":"unc?","parents":["cx,ya","."]}, +{"flavor":"windows","text":"//","str":"\\\\","parts":["\\\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":":/","str":":","parts":[":"],"absolute":false,"name":":","parents":["."]}, +{"flavor":"windows","text":";b\u00e9..;.:\u0000","str":";b\u00e9..;.:\u0000","parts":[";b\u00e9..;.:\u0000"],"absolute":false,"name":";b\u00e9..;.:\u0000","parents":["."]}, +{"flavor":"windows","text":"c;","str":"c;","parts":["c;"],"absolute":false,"name":"c;","parents":["."]}, +{"flavor":"windows","text":"//","str":"\\\\","parts":["\\\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":" //..\u0000 a","str":" \\..\u0000 a","parts":[" ","..\u0000 a"],"absolute":false,"name":"..\u0000 a","parents":[" ","."]}, +{"flavor":"windows","text":":cC.","str":":cC.","parts":[":cC."],"absolute":false,"name":":cC.","parents":["."]}, +{"flavor":"windows","text":"uncb\u00e9..\\\\a/..","str":"uncb\u00e9..\\a\\..","parts":["uncb\u00e9..","a",".."],"absolute":false,"name":"..","parents":["uncb\u00e9..\\a","uncb\u00e9..","."]}, +{"flavor":"windows","text":"\u00e9..\\\\b. UNCUNC","str":"\u00e9..\\b. UNCUNC","parts":["\u00e9..","b. UNCUNC"],"absolute":false,"name":"b. UNCUNC","parents":["\u00e9..","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"c\\ \u0000","str":"c\\ \u0000","parts":["c"," \u0000"],"absolute":false,"name":" \u0000","parents":["c","."]}, +{"flavor":"windows","text":":","str":":","parts":[":"],"absolute":false,"name":":","parents":["."]}, +{"flavor":"windows","text":" ","str":" ","parts":[" "],"absolute":false,"name":" ","parents":["."]}, +{"flavor":"windows","text":"?C...","str":"?C...","parts":["?C..."],"absolute":false,"name":"?C...","parents":["."]}, +{"flavor":"windows","text":"cunc?\u0000","str":"cunc?\u0000","parts":["cunc?\u0000"],"absolute":false,"name":"cunc?\u0000","parents":["."]}, +{"flavor":"windows","text":" \\c\u00e9\\\\\\//","str":" \\c\u00e9","parts":[" ","c\u00e9"],"absolute":false,"name":"c\u00e9","parents":[" ","."]}, +{"flavor":"windows","text":"UNCx,y","str":"UNCx,y","parts":["UNCx,y"],"absolute":false,"name":"UNCx,y","parents":["."]}, +{"flavor":"windows","text":"cUNC; UNC?.c","str":"cUNC; UNC?.c","parts":["cUNC; UNC?.c"],"absolute":false,"name":"cUNC; UNC?.c","parents":["."]}, +{"flavor":"windows","text":"?..","str":"?..","parts":["?.."],"absolute":false,"name":"?..","parents":["."]}, +{"flavor":"windows","text":" b ac../","str":" b ac..","parts":[" b ac.."],"absolute":false,"name":" b ac..","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"..\\?:x,y;\\\\\u00e9","str":"..\\?:x,y;\\\u00e9","parts":["..","?:x,y;","\u00e9"],"absolute":false,"name":"\u00e9","parents":["..\\?:x,y;","..","."]}, +{"flavor":"windows","text":"\\.unc/..","str":"\\.unc\\..","parts":["\\",".unc",".."],"absolute":false,"name":"..","parents":["\\.unc","\\"]}, +{"flavor":"windows","text":"/....\\\\ a\u00e9","str":"\\....\\ a\u00e9","parts":["\\","...."," a\u00e9"],"absolute":false,"name":" a\u00e9","parents":["\\....","\\"]}, +{"flavor":"windows","text":"/\u00e9UNCc..\\b","str":"\\\u00e9UNCc..\\b","parts":["\\","\u00e9UNCc..","b"],"absolute":false,"name":"b","parents":["\\\u00e9UNCc..","\\"]}, +{"flavor":"windows","text":"\\b","str":"\\b","parts":["\\","b"],"absolute":false,"name":"b","parents":["\\"]}, +{"flavor":"windows","text":".:x,y::UNCb","str":".:x,y::UNCb","parts":[".:","x,y::UNCb"],"absolute":false,"name":"x,y::UNCb","parents":[".:"]}, +{"flavor":"windows","text":"?:;b\u0000a","str":"?:;b\u0000a","parts":["?:",";b\u0000a"],"absolute":false,"name":";b\u0000a","parents":["?:"]}, +{"flavor":"windows","text":"bunc//:x,y","str":"bunc\\:x,y","parts":["bunc",":x,y"],"absolute":false,"name":":x,y","parents":["bunc","."]}, +{"flavor":"windows","text":"?\u0000\u0000/ \u00e9\u0000","str":"?\u0000\u0000\\ \u00e9\u0000","parts":["?\u0000\u0000"," \u00e9\u0000"],"absolute":false,"name":" \u00e9\u0000","parents":["?\u0000\u0000","."]}, +{"flavor":"windows","text":"//C.\u00e9.","str":"\\\\C.\u00e9.","parts":["\\\\C.\u00e9."],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"CUNC","str":"CUNC","parts":["CUNC"],"absolute":false,"name":"CUNC","parents":["."]}, +{"flavor":"windows","text":"a unc? C\\\\","str":"a unc? C","parts":["a unc? C"],"absolute":false,"name":"a unc? C","parents":["."]}, +{"flavor":"windows","text":"\\.","str":"\\","parts":["\\"],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"b","str":"b","parts":["b"],"absolute":false,"name":"b","parents":["."]}, +{"flavor":"windows","text":"x,yc;unc.. ","str":"x,yc;unc.. ","parts":["x,yc;unc.. "],"absolute":false,"name":"x,yc;unc.. ","parents":["."]}, +{"flavor":"windows","text":".x,y","str":".x,y","parts":[".x,y"],"absolute":false,"name":".x,y","parents":["."]}, +{"flavor":"windows","text":"\\\u0000.","str":"\\\u0000.","parts":["\\","\u0000."],"absolute":false,"name":"\u0000.","parents":["\\"]}, +{"flavor":"windows","text":";..C ","str":";..C ","parts":[";..C "],"absolute":false,"name":";..C ","parents":["."]}, +{"flavor":"windows","text":"\\unc;..?: ..","str":"\\unc;..?: ..","parts":["\\","unc;..?: .."],"absolute":false,"name":"unc;..?: ..","parents":["\\"]}, +{"flavor":"windows","text":" cunc \\","str":" cunc ","parts":[" cunc "],"absolute":false,"name":" cunc ","parents":["."]}, +{"flavor":"windows","text":"b","str":"b","parts":["b"],"absolute":false,"name":"b","parents":["."]}, +{"flavor":"windows","text":" c\u0000","str":" c\u0000","parts":[" c\u0000"],"absolute":false,"name":" c\u0000","parents":["."]}, +{"flavor":"windows","text":"\\\\","str":"\\\\","parts":["\\\\"],"absolute":true,"name":"","parents":[]}, +{"flavor":"windows","text":"C \\?","str":"C \\?","parts":["C ","?"],"absolute":false,"name":"?","parents":["C ","."]}, +{"flavor":"windows","text":"ca//C","str":"ca\\C","parts":["ca","C"],"absolute":false,"name":"C","parents":["ca","."]}, +{"flavor":"windows","text":"..","str":"..","parts":[".."],"absolute":false,"name":"..","parents":["."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"\u0000\u0000\\C\u00e9","str":"\u0000\u0000\\C\u00e9","parts":["\u0000\u0000","C\u00e9"],"absolute":false,"name":"C\u00e9","parents":["\u0000\u0000","."]}, +{"flavor":"windows","text":"","str":".","parts":[],"absolute":false,"name":"","parents":[]}, +{"flavor":"windows","text":"UNCbUNC","str":"UNCbUNC","parts":["UNCbUNC"],"absolute":false,"name":"UNCbUNC","parents":["."]}, +{"flavor":"windows","text":"ca\u00e9a","str":"ca\u00e9a","parts":["ca\u00e9a"],"absolute":false,"name":"ca\u00e9a","parents":["."]}, +{"flavor":"windows","text":";//;UNC","str":";\\;UNC","parts":[";",";UNC"],"absolute":false,"name":";UNC","parents":[";","."]}, +{"flavor":"windows","text":"cCx,yunc","str":"cCx,yunc","parts":["cCx,yunc"],"absolute":false,"name":"cCx,yunc","parents":["."]}, +{"flavor":"windows","text":"C","str":"C","parts":["C"],"absolute":false,"name":"C","parents":["."]}, +{"flavor":"windows","text":".//\u00e9?\u00e9b\u00e9","str":"\u00e9?\u00e9b\u00e9","parts":["\u00e9?\u00e9b\u00e9"],"absolute":false,"name":"\u00e9?\u00e9b\u00e9","parents":["."]}, +{"flavor":"windows","text":"\u00e9","str":"\u00e9","parts":["\u00e9"],"absolute":false,"name":"\u00e9","parents":["."]}, +{"flavor":"windows","text":":\\\\CUNCa:?unc","str":":\\CUNCa:?unc","parts":[":","CUNCa:?unc"],"absolute":false,"name":"CUNCa:?unc","parents":[":","."]}, +{"flavor":"windows","text":"UNC","str":"UNC","parts":["UNC"],"absolute":false,"name":"UNC","parents":["."]}, +{"flavor":"windows","text":" a..;/a ","str":" a..;\\a ","parts":[" a..;","a "],"absolute":false,"name":"a ","parents":[" a..;","."]}, +{"flavor":"windows","text":"\u00e9.c","str":"\u00e9.c","parts":["\u00e9.c"],"absolute":false,"name":"\u00e9.c","parents":["."]}, +{"flavor":"windows","text":"...//UNCb/a","str":"...\\UNCb\\a","parts":["...","UNCb","a"],"absolute":false,"name":"a","parents":["...\\UNCb","...","."]}, +{"flavor":"windows","text":"////;\\\\;\\\\//c","str":"\\\\\\\\;\\;\\c","parts":["\\\\\\\\",";",";","c"],"absolute":true,"name":"c","parents":["\\\\\\\\;\\;","\\\\\\\\;","\\\\\\\\"]}, +{"flavor":"windows","text":"b;..\u0000UNC\u0000unc","str":"b;..\u0000UNC\u0000unc","parts":["b;..\u0000UNC\u0000unc"],"absolute":false,"name":"b;..\u0000UNC\u0000unc","parents":["."]}, +{"flavor":"windows","text":"?","str":"?","parts":["?"],"absolute":false,"name":"?","parents":["."]} +], +"joins": [ +{"flavor":"posix","left":"/a/b","right":"c","str":"/a/b/c"}, +{"flavor":"posix","left":"/a/b","right":"/c","str":"/c"}, +{"flavor":"posix","left":"C:\\cwd","right":"a/b","str":"C:\\cwd/a/b"}, +{"flavor":"posix","left":"C:\\cwd","right":"C:x","str":"C:\\cwd/C:x"}, +{"flavor":"posix","left":"C:\\cwd","right":"c:x","str":"C:\\cwd/c:x"}, +{"flavor":"posix","left":"C:\\cwd","right":"D:x","str":"C:\\cwd/D:x"}, +{"flavor":"posix","left":"C:\\cwd","right":"\\x","str":"C:\\cwd/\\x"}, +{"flavor":"posix","left":"//server/share","right":"x","str":"//server/share/x"}, +{"flavor":"posix","left":"C:","right":"x","str":"C:/x"}, +{"flavor":"posix","left":"","right":"x","str":"x"}, +{"flavor":"posix","left":"a","right":"","str":"a"}, +{"flavor":"posix","left":"","right":"a","str":"a"}, +{"flavor":"posix","left":"\\\\","right":"Cb","str":"\\\\/Cb"}, +{"flavor":"posix","left":"/","right":"a\\\\..","str":"/a\\\\.."}, +{"flavor":"posix","left":"b/CcUNC","right":"//x,yba","str":"//x,yba"}, +{"flavor":"posix","left":"b..unc","right":"c\u0000\u00e9//","str":"b..unc/c\u0000\u00e9"}, +{"flavor":"posix","left":"x,y","right":".\u00e9..\u0000","str":"x,y/.\u00e9..\u0000"}, +{"flavor":"posix","left":"?","right":"","str":"?"}, +{"flavor":"posix","left":"/://\u00e9:c","right":"cCa","str":"/:/\u00e9:c/cCa"}, +{"flavor":"posix","left":" ","right":"\\\\CUNCCunc","str":" /\\\\CUNCCunc"}, +{"flavor":"posix","left":".;aUNC..x,y","right":"c..\u0000:;","str":".;aUNC..x,y/c..\u0000:;"}, +{"flavor":"posix","left":"Cc....;..","right":"\\\\;x,y\\\\unc;","str":"Cc....;../\\\\;x,y\\\\unc;"}, +{"flavor":"posix","left":"a","right":"\u00e9/","str":"a/\u00e9"}, +{"flavor":"posix","left":"/UNCuncC?unc","right":"//unc\u00e9??","str":"//unc\u00e9??"}, +{"flavor":"posix","left":"/\u00e9;b\u0000\u00e9","right":";Cb","str":"/\u00e9;b\u0000\u00e9/;Cb"}, +{"flavor":"posix","left":" a\\.//","right":"cbC/","str":" a\\./cbC"}, +{"flavor":"posix","left":":","right":"","str":":"}, +{"flavor":"posix","left":"/.unc","right":"C","str":"/.unc/C"}, +{"flavor":"posix","left":"\\\\//?","right":"\\\\","str":"\\\\/?/\\\\"}, +{"flavor":"posix","left":"cx,y....\\:","right":"//","str":"//"}, +{"flavor":"posix","left":"b;\\\\unc","right":"\u00e9b//:.","str":"b;\\\\unc/\u00e9b/:."}, +{"flavor":"posix","left":".b\\\\","right":"a?unc/:b","str":".b\\\\/a?unc/:b"}, +{"flavor":"posix","left":"\u00e9;\\uncb..","right":"a..:","str":"\u00e9;\\uncb../a..:"}, +{"flavor":"posix","left":"cunca","right":":","str":"cunca/:"}, +{"flavor":"posix","left":" \u0000?//","right":"\u00e9?..;","str":" \u0000?/\u00e9?..;"}, +{"flavor":"posix","left":"","right":";//UNCx,ya","str":";/UNCx,ya"}, +{"flavor":"posix","left":"","right":"\\\u0000\u00e9.","str":"\\\u0000\u00e9."}, +{"flavor":"posix","left":"\\\\\\\\\u0000","right":";\u0000 ","str":"\\\\\\\\\u0000/;\u0000 "}, +{"flavor":"posix","left":"unc","right":".\\?//","str":"unc/.\\?"}, +{"flavor":"posix","left":"\u00e9.b\u00e9\\","right":";.../.","str":"\u00e9.b\u00e9\\/;..."}, +{"flavor":"posix","left":"unc:","right":"c\\\\","str":"unc:/c\\\\"}, +{"flavor":"posix","left":"unc/","right":"","str":"unc"}, +{"flavor":"posix","left":"cUNCC\\.","right":"","str":"cUNCC\\."}, +{"flavor":"posix","left":"","right":"x,yac","str":"x,yac"}, +{"flavor":"posix","left":"\\\\\u00e9;unc/","right":"a\\\u0000x,y..","str":"\\\\\u00e9;unc/a\\\u0000x,y.."}, +{"flavor":"posix","left":"..;//\u0000x,y","right":"/C;b\u00e9","str":"/C;b\u00e9"}, +{"flavor":"posix","left":"..;x,y//","right":"\u0000","str":"..;x,y/\u0000"}, +{"flavor":"posix","left":"\u0000","right":"aa//x,y","str":"\u0000/aa/x,y"}, +{"flavor":"posix","left":"?/","right":"\\\\\\\\;","str":"?/\\\\\\\\;"}, +{"flavor":"posix","left":"\u0000","right":"\\","str":"\u0000/\\"}, +{"flavor":"posix","left":"\u0000/\u0000UNC\u0000","right":"\u0000?....\u00e9C","str":"\u0000/\u0000UNC\u0000/\u0000?....\u00e9C"}, +{"flavor":"posix","left":"\u00e9//","right":".///x,y\u00e9.","str":"\u00e9/x,y\u00e9."}, +{"flavor":"posix","left":";","right":"..","str":";/.."}, +{"flavor":"posix","left":".","right":"unc\\\\unc\\\\UNC\\","str":"unc\\\\unc\\\\UNC\\"}, +{"flavor":"posix","left":"..","right":"/;;","str":"/;;"}, +{"flavor":"posix","left":"..","right":"\\","str":"../\\"}, +{"flavor":"posix","left":"c//C\\\\","right":"//a","str":"//a"}, +{"flavor":"posix","left":"unc:","right":"c","str":"unc:/c"}, +{"flavor":"posix","left":"?//.","right":"unc","str":"?/unc"}, +{"flavor":"posix","left":"\u00e9a//c\\\\","right":"C","str":"\u00e9a/c\\\\/C"}, +{"flavor":"posix","left":"","right":"\\","str":"\\"}, +{"flavor":"posix","left":":c:","right":". ","str":":c:/. "}, +{"flavor":"posix","left":"","right":"...;?","str":"...;?"}, +{"flavor":"posix","left":":\\b","right":"UNC\\///a","str":":\\b/UNC\\/a"}, +{"flavor":"posix","left":"c..;b","right":"C\u0000c","str":"c..;b/C\u0000c"}, +{"flavor":"posix","left":"::UNC.b","right":"x,y;","str":"::UNC.b/x,y;"}, +{"flavor":"posix","left":"Cx,yca","right":":.cC","str":"Cx,yca/:.cC"}, +{"flavor":"posix","left":"b.","right":"","str":"b."}, +{"flavor":"posix","left":"C..//","right":"...//\u00e9\u00e9","str":"C../.../\u00e9\u00e9"}, +{"flavor":"posix","left":"..x,y","right":"\u00e9:\\x,yUNC","str":"..x,y/\u00e9:\\x,yUNC"}, +{"flavor":"posix","left":".//..C?","right":"\u0000a","str":"..C?/\u0000a"}, +{"flavor":"posix","left":"unc\u00e9;.uncunc","right":" ..b:a","str":"unc\u00e9;.uncunc/ ..b:a"}, +{"flavor":"posix","left":" \u00e9\\\\\\\\ca","right":"\u00e9\u0000","str":" \u00e9\\\\\\\\ca/\u00e9\u0000"}, +{"flavor":"posix","left":"..\u0000?Cb.","right":"\u00e9UNC","str":"..\u0000?Cb./\u00e9UNC"}, +{"flavor":"posix","left":"\\\u00e9//unc","right":"///\u00e9\\..","str":"/\u00e9\\.."}, +{"flavor":"posix","left":"","right":"\u0000::","str":"\u0000::"}, +{"flavor":"posix","left":"UNC\\\\x,y","right":"a\\unc\u0000","str":"UNC\\\\x,y/a\\unc\u0000"}, +{"flavor":"posix","left":"","right":" ","str":" "}, +{"flavor":"posix","left":"..\\\\/.unc","right":"\\\\..C;","str":"..\\\\/.unc/\\\\..C;"}, +{"flavor":"posix","left":"","right":":a?Cc","str":":a?Cc"}, +{"flavor":"posix","left":".:","right":"\u00e9?","str":".:/\u00e9?"}, +{"flavor":"posix","left":"","right":"aCx,y","str":"aCx,y"}, +{"flavor":"posix","left":"aunc","right":"","str":"aunc"}, +{"flavor":"posix","left":"unc\u00e9;unc","right":"..\u0000\u00e9","str":"unc\u00e9;unc/..\u0000\u00e9"}, +{"flavor":"posix","left":"a. ","right":".b../b","str":"a. /.b../b"}, +{"flavor":"posix","left":"bc\\\\\u0000","right":"..\u0000 .\u0000","str":"bc\\\\\u0000/..\u0000 .\u0000"}, +{"flavor":"posix","left":"x,y","right":"b\\?/","str":"x,y/b\\?"}, +{"flavor":"posix","left":":x,ya","right":"?UNC..unc","str":":x,ya/?UNC..unc"}, +{"flavor":"posix","left":"...UNCunc","right":"\u0000\u00e9\\a","str":"...UNCunc/\u0000\u00e9\\a"}, +{"flavor":"posix","left":"\\\\:UNC\u0000?b","right":"","str":"\\\\:UNC\u0000?b"}, +{"flavor":"posix","left":"cx,y:a: ","right":" UNCUNC\u0000","str":"cx,y:a: / UNCUNC\u0000"}, +{"flavor":"posix","left":"a bx,y\\\\","right":"b:","str":"a bx,y\\\\/b:"}, +{"flavor":"posix","left":":","right":"C\u0000unc;c","str":":/C\u0000unc;c"}, +{"flavor":"posix","left":"\u00e9UNC","right":" C..a\\\\\u00e9","str":"\u00e9UNC/ C..a\\\\\u00e9"}, +{"flavor":"posix","left":"C..?//","right":"c:c\u00e9","str":"C..?/c:c\u00e9"}, +{"flavor":"posix","left":"\\:c?\\\\","right":"..;;c\u0000 ","str":"\\:c?\\\\/..;;c\u0000 "}, +{"flavor":"posix","left":"\\x,y. ; ","right":"unc","str":"\\x,y. ; /unc"}, +{"flavor":"posix","left":"\\\\\\..","right":"","str":"\\\\\\.."}, +{"flavor":"posix","left":"c.\u0000","right":"cx,yC","str":"c.\u0000/cx,yC"}, +{"flavor":"posix","left":"uncb","right":"b//UNC .;","str":"uncb/b/UNC .;"}, +{"flavor":"posix","left":"?/","right":"\\","str":"?/\\"}, +{"flavor":"posix","left":"\\\u0000b","right":";UNC\\//","str":"\\\u0000b/;UNC\\"}, +{"flavor":"posix","left":"x,yuncUNC","right":"\u00e9\\\\UNC","str":"x,yuncUNC/\u00e9\\\\UNC"}, +{"flavor":"posix","left":"\u0000","right":"?\u0000a?","str":"\u0000/?\u0000a?"}, +{"flavor":"posix","left":"","right":";C\\\\UNC","str":";C\\\\UNC"}, +{"flavor":"posix","left":"?/x,y","right":"a//","str":"?/x,y/a"}, +{"flavor":"posix","left":".\u00e9.;unc","right":"\u0000","str":".\u00e9.;unc/\u0000"}, +{"flavor":"posix","left":"unccunc \\UNC","right":"","str":"unccunc \\UNC"}, +{"flavor":"posix","left":"caUNC;..UNC","right":"UNCx,y:.\u0000","str":"caUNC;..UNC/UNCx,y:.\u0000"}, +{"flavor":"posix","left":"","right":"UNC;\\:\u00e9","str":"UNC;\\:\u00e9"}, +{"flavor":"posix","left":"b?c","right":"\u0000","str":"b?c/\u0000"}, +{"flavor":"posix","left":"a","right":"\\\\\\b\\\\?","str":"a/\\\\\\b\\\\?"}, +{"flavor":"posix","left":":\u0000..c","right":"....","str":":\u0000..c/...."}, +{"flavor":"posix","left":"?\u0000\\\\unc","right":":\\a\u00e9unc","str":"?\u0000\\\\unc/:\\a\u00e9unc"}, +{"flavor":"posix","left":"?UNC.","right":"","str":"?UNC."}, +{"flavor":"posix","left":"x,y\u00e9","right":"x,y","str":"x,y\u00e9/x,y"}, +{"flavor":"posix","left":"//;b\\b","right":"\u0000\u0000\\\\x,y.c","str":"//;b\\b/\u0000\u0000\\\\x,y.c"}, +{"flavor":"posix","left":"UNC\\","right":" x,y:?","str":"UNC\\/ x,y:?"}, +{"flavor":"posix","left":"","right":" x,y?\\","str":" x,y?\\"}, +{"flavor":"posix","left":";/\\//","right":"x,y","str":";/\\/x,y"}, +{"flavor":"posix","left":";\u0000\\/;","right":"b/a","str":";\u0000\\/;/b/a"}, +{"flavor":"posix","left":"aac/","right":"...cUNC","str":"aac/...cUNC"}, +{"flavor":"posix","left":"b\u0000","right":"c\u00e9","str":"b\u0000/c\u00e9"}, +{"flavor":"posix","left":"","right":"\\C\u00e9\\\\CC","str":"\\C\u00e9\\\\CC"}, +{"flavor":"posix","left":"\u00e9","right":" UNC/uncc","str":"\u00e9/ UNC/uncc"}, +{"flavor":"posix","left":"","right":" \\\\;","str":" \\\\;"}, +{"flavor":"posix","left":"C\\UNC","right":"","str":"C\\UNC"}, +{"flavor":"posix","left":" //\\:unc","right":"UNCb;..;","str":" /\\:unc/UNCb;..;"}, +{"flavor":"posix","left":":","right":"unc\\uncunc","str":":/unc\\uncunc"}, +{"flavor":"posix","left":"/UNC\\\\b:","right":"?x,ybc\\\\UNC","str":"/UNC\\\\b:/?x,ybc\\\\UNC"}, +{"flavor":"posix","left":"ac\u00e9/c","right":"","str":"ac\u00e9/c"}, +{"flavor":"posix","left":"","right":"","str":"."}, +{"flavor":"posix","left":"x,yunc","right":"\\\\//////","str":"x,yunc/\\\\"}, +{"flavor":"posix","left":"///","right":"x,y////UNCb","str":"/x,y/UNCb"}, +{"flavor":"posix","left":"b","right":"\\\\Cunc\u0000","str":"b/\\\\Cunc\u0000"}, +{"flavor":"posix","left":"\\\\:a","right":"/b..\\/","str":"/b..\\"}, +{"flavor":"posix","left":"..a..x,yunc","right":"..\\\\;//\u00e9unc","str":"..a..x,yunc/..\\\\;/\u00e9unc"}, +{"flavor":"posix","left":"","right":";\u0000","str":";\u0000"}, +{"flavor":"posix","left":"c/\\","right":":","str":"c/\\/:"}, +{"flavor":"posix","left":"x,y","right":"x,y:","str":"x,y/x,y:"}, +{"flavor":"posix","left":"","right":"x,y:.c","str":"x,y:.c"}, +{"flavor":"posix","left":"","right":"..?b","str":"..?b"}, +{"flavor":"posix","left":"..;uncaa","right":"\\\\uncuncb\u00e9","str":"..;uncaa/\\\\uncuncb\u00e9"}, +{"flavor":"posix","left":"","right":"\\\\..x,yc","str":"\\\\..x,yc"}, +{"flavor":"posix","left":"\u00e9unc\\??","right":"unc","str":"\u00e9unc\\??/unc"}, +{"flavor":"posix","left":"UNCC\u00e9","right":"://\\UNCUNC","str":"UNCC\u00e9/:/\\UNCUNC"}, +{"flavor":"posix","left":"\\ ","right":" \u0000a..","str":"\\ / \u0000a.."}, +{"flavor":"posix","left":"unc\\?","right":"\\\\?b ","str":"unc\\?/\\\\?b "}, +{"flavor":"posix","left":" ///.unc","right":"/x,y..","str":"/x,y.."}, +{"flavor":"posix","left":":aC","right":"://a//\\\\","str":":aC/:/a/\\\\"}, +{"flavor":"posix","left":" ::","right":"","str":" ::"}, +{"flavor":"posix","left":" UNC:\u00e9","right":"","str":" UNC:\u00e9"}, +{"flavor":"posix","left":"\\x,y..","right":"\\ .:UNC","str":"\\x,y../\\ .:UNC"}, +{"flavor":"posix","left":" \u0000x,y;;unc","right":"unc\\","str":" \u0000x,y;;unc/unc\\"}, +{"flavor":"posix","left":"","right":"\\\\;uncCx,y.","str":"\\\\;uncCx,y."}, +{"flavor":"posix","left":"","right":";bx,y\u0000//UNC","str":";bx,y\u0000/UNC"}, +{"flavor":"posix","left":"","right":"?.C..","str":"?.C.."}, +{"flavor":"posix","left":"","right":"bC\\\\/\\\\","str":"bC\\\\/\\\\"}, +{"flavor":"posix","left":";C","right":":","str":";C/:"}, +{"flavor":"posix","left":"//a\\\\uncb?","right":".\u00e9b\u00e9?\u0000","str":"//a\\\\uncb?/.\u00e9b\u00e9?\u0000"}, +{"flavor":"posix","left":"\\\u00e9b","right":"\\b","str":"\\\u00e9b/\\b"}, +{"flavor":"posix","left":";:;:","right":"","str":";:;:"}, +{"flavor":"posix","left":"\u00e9UNC","right":"\\\\:\\\\\u00e9\u00e9","str":"\u00e9UNC/\\\\:\\\\\u00e9\u00e9"}, +{"flavor":"posix","left":"\\/\\","right":"","str":"\\/\\"}, +{"flavor":"posix","left":"a","right":" ","str":"a/ "}, +{"flavor":"posix","left":".","right":"?\\UNCUNC\\","str":"?\\UNCUNC\\"}, +{"flavor":"posix","left":"b\u00e9","right":"","str":"b\u00e9"}, +{"flavor":"posix","left":"//C","right":"C","str":"//C/C"}, +{"flavor":"posix","left":"unc// ","right":"C","str":"unc/ /C"}, +{"flavor":"posix","left":".c\u0000","right":"C","str":".c\u0000/C"}, +{"flavor":"posix","left":"/","right":"...C\u0000a","str":"/...C\u0000a"}, +{"flavor":"posix","left":"x,yC","right":"b;cccc","str":"x,yC/b;cccc"}, +{"flavor":"posix","left":"?.","right":"//\\x,y;x,y","str":"//\\x,y;x,y"}, +{"flavor":"posix","left":"//C ?cx,y","right":"?","str":"//C ?cx,y/?"}, +{"flavor":"posix","left":" \\\\","right":"\u00e9\\\\\u0000C\\..","str":" \\\\/\u00e9\\\\\u0000C\\.."}, +{"flavor":"posix","left":"\u0000:b","right":"unc","str":"\u0000:b/unc"}, +{"flavor":"posix","left":";a;;?","right":"uncx,yb:","str":";a;;?/uncx,yb:"}, +{"flavor":"posix","left":"\u0000.","right":"c\\","str":"\u0000./c\\"}, +{"flavor":"posix","left":" ?//","right":"\\x,y/","str":" ?/\\x,y"}, +{"flavor":"posix","left":" ","right":"\\\\b://\u0000\\\\","str":" /\\\\b:/\u0000\\\\"}, +{"flavor":"posix","left":";","right":"x,y\\\\","str":";/x,y\\\\"}, +{"flavor":"posix","left":"\\\\x,y/c.","right":"c\\\\\\","str":"\\\\x,y/c./c\\\\\\"}, +{"flavor":"posix","left":"\u0000c?;","right":"\u00e9unc/\u00e9x,y","str":"\u0000c?;/\u00e9unc/\u00e9x,y"}, +{"flavor":"posix","left":" C","right":"","str":" C"}, +{"flavor":"posix","left":"","right":";;a","str":";;a"}, +{"flavor":"posix","left":".","right":"b..","str":"b.."}, +{"flavor":"posix","left":"","right":"\u00e9/unc","str":"\u00e9/unc"}, +{"flavor":"posix","left":"x,yUNC","right":"c\u00e9\u0000","str":"x,yUNC/c\u00e9\u0000"}, +{"flavor":"posix","left":"; \\\\b","right":"","str":"; \\\\b"}, +{"flavor":"posix","left":"","right":":b;","str":":b;"}, +{"flavor":"posix","left":"\u00e9\\\\..","right":"///","str":"/"}, +{"flavor":"posix","left":"\\x,y\u0000ax,y","right":"unc.\\","str":"\\x,y\u0000ax,y/unc.\\"}, +{"flavor":"posix","left":"","right":"","str":"."}, +{"flavor":"posix","left":"unc","right":"UNCbUNCb:\u0000","str":"unc/UNCbUNCb:\u0000"}, +{"flavor":"posix","left":"..UNC..UNCb","right":"\\;C/","str":"..UNC..UNCb/\\;C"}, +{"flavor":"posix","left":"\u0000..\\;///","right":"/a?","str":"/a?"}, +{"flavor":"posix","left":"..UNC?","right":"","str":"..UNC?"}, +{"flavor":"posix","left":"/ x,y\\","right":"\u00e9..a","str":"/ x,y\\/\u00e9..a"}, +{"flavor":"posix","left":"\\ab","right":";x,yb/","str":"\\ab/;x,yb"}, +{"flavor":"posix","left":";UNC//","right":"\u0000 ","str":";UNC/\u0000 "}, +{"flavor":"posix","left":"UNC?UNCca;","right":"UNC","str":"UNC?UNCca;/UNC"}, +{"flavor":"posix","left":"b","right":"//C","str":"//C"}, +{"flavor":"posix","left":"uncc /UNC:","right":" Cc","str":"uncc /UNC:/ Cc"}, +{"flavor":"posix","left":".;?","right":":C\\\\;/","str":".;?/:C\\\\;"}, +{"flavor":"posix","left":"x,yc;//","right":"\u00e9\u00e9x,y","str":"x,yc;/\u00e9\u00e9x,y"}, +{"flavor":"posix","left":"b?a.\u0000","right":"//\u00e9..\\;?","str":"//\u00e9..\\;?"}, +{"flavor":"posix","left":"?:///.\\\\","right":"\u0000:a/","str":"?:/.\\\\/\u0000:a"}, +{"flavor":"posix","left":"","right":"..Cunc..","str":"..Cunc.."}, +{"flavor":"posix","left":";","right":"UNCc","str":";/UNCc"}, +{"flavor":"posix","left":"","right":"C.;unc\\","str":"C.;unc\\"}, +{"flavor":"posix","left":";.x,y..","right":"//\u00e9UNC;?","str":"//\u00e9UNC;?"}, +{"flavor":"windows","left":"/a/b","right":"c","str":"\\a\\b\\c"}, +{"flavor":"windows","left":"/a/b","right":"/c","str":"\\c"}, +{"flavor":"windows","left":"C:\\cwd","right":"a/b","str":"C:\\cwd\\a\\b"}, +{"flavor":"windows","left":"C:\\cwd","right":"C:x","str":"C:\\cwd\\x"}, +{"flavor":"windows","left":"C:\\cwd","right":"c:x","str":"c:\\cwd\\x"}, +{"flavor":"windows","left":"C:\\cwd","right":"D:x","str":"D:x"}, +{"flavor":"windows","left":"C:\\cwd","right":"\\x","str":"C:\\x"}, +{"flavor":"windows","left":"//server/share","right":"x","str":"\\\\server\\share\\x"}, +{"flavor":"windows","left":"C:","right":"x","str":"C:x"}, +{"flavor":"windows","left":"","right":"x","str":"x"}, +{"flavor":"windows","left":"a","right":"","str":"a"}, +{"flavor":"windows","left":"","right":"a","str":"a"}, +{"flavor":"windows","left":"\\\\","right":"Cb","str":"\\\\Cb"}, +{"flavor":"windows","left":"/","right":"a\\\\..","str":"\\a\\.."}, +{"flavor":"windows","left":"b/CcUNC","right":"//x,yba","str":"\\\\x,yba"}, +{"flavor":"windows","left":"b..unc","right":"c\u0000\u00e9//","str":"b..unc\\c\u0000\u00e9"}, +{"flavor":"windows","left":"x,y","right":".\u00e9..\u0000","str":"x,y\\.\u00e9..\u0000"}, +{"flavor":"windows","left":"?","right":"","str":"?"}, +{"flavor":"windows","left":"/://\u00e9:c","right":"cCa","str":"\\:\\\u00e9:c\\cCa"}, +{"flavor":"windows","left":" ","right":"\\\\CUNCCunc","str":"\\\\CUNCCunc"}, +{"flavor":"windows","left":".;aUNC..x,y","right":"c..\u0000:;","str":".;aUNC..x,y\\c..\u0000:;"}, +{"flavor":"windows","left":"Cc....;..","right":"\\\\;x,y\\\\unc;","str":"\\\\;x,y\\\\unc;"}, +{"flavor":"windows","left":"a","right":"\u00e9/","str":"a\\\u00e9"}, +{"flavor":"windows","left":"/UNCuncC?unc","right":"//unc\u00e9??","str":"\\\\unc\u00e9??"}, +{"flavor":"windows","left":"/\u00e9;b\u0000\u00e9","right":";Cb","str":"\\\u00e9;b\u0000\u00e9\\;Cb"}, +{"flavor":"windows","left":" a\\.//","right":"cbC/","str":" a\\cbC"}, +{"flavor":"windows","left":":","right":"","str":":"}, +{"flavor":"windows","left":"/.unc","right":"C","str":"\\.unc\\C"}, +{"flavor":"windows","left":"\\\\//?","right":"\\\\","str":"\\\\"}, +{"flavor":"windows","left":"cx,y....\\:","right":"//","str":"\\\\"}, +{"flavor":"windows","left":"b;\\\\unc","right":"\u00e9b//:.","str":"b;\\unc\\\u00e9b\\:."}, +{"flavor":"windows","left":".b\\\\","right":"a?unc/:b","str":".b\\a?unc\\:b"}, +{"flavor":"windows","left":"\u00e9;\\uncb..","right":"a..:","str":"\u00e9;\\uncb..\\a..:"}, +{"flavor":"windows","left":"cunca","right":":","str":"cunca\\:"}, +{"flavor":"windows","left":" \u0000?//","right":"\u00e9?..;","str":" \u0000?\\\u00e9?..;"}, +{"flavor":"windows","left":"","right":";//UNCx,ya","str":";\\UNCx,ya"}, +{"flavor":"windows","left":"","right":"\\\u0000\u00e9.","str":"\\\u0000\u00e9."}, +{"flavor":"windows","left":"\\\\\\\\\u0000","right":";\u0000 ","str":"\\\\\\\\\u0000\\;\u0000 "}, +{"flavor":"windows","left":"unc","right":".\\?//","str":"unc\\?"}, +{"flavor":"windows","left":"\u00e9.b\u00e9\\","right":";.../.","str":"\u00e9.b\u00e9\\;..."}, +{"flavor":"windows","left":"unc:","right":"c\\\\","str":"unc:\\c"}, +{"flavor":"windows","left":"unc/","right":"","str":"unc"}, +{"flavor":"windows","left":"cUNCC\\.","right":"","str":"cUNCC"}, +{"flavor":"windows","left":"","right":"x,yac","str":"x,yac"}, +{"flavor":"windows","left":"\\\\\u00e9;unc/","right":"a\\\u0000x,y..","str":"\\\\\u00e9;unc\\a\\\u0000x,y.."}, +{"flavor":"windows","left":"..;//\u0000x,y","right":"/C;b\u00e9","str":"\\C;b\u00e9"}, +{"flavor":"windows","left":"..;x,y//","right":"\u0000","str":"..;x,y\\\u0000"}, +{"flavor":"windows","left":"\u0000","right":"aa//x,y","str":"\u0000\\aa\\x,y"}, +{"flavor":"windows","left":"?/","right":"\\\\\\\\;","str":"\\\\\\\\;"}, +{"flavor":"windows","left":"\u0000","right":"\\","str":"\\"}, +{"flavor":"windows","left":"\u0000/\u0000UNC\u0000","right":"\u0000?....\u00e9C","str":"\u0000\\\u0000UNC\u0000\\\u0000?....\u00e9C"}, +{"flavor":"windows","left":"\u00e9//","right":".///x,y\u00e9.","str":"\u00e9\\x,y\u00e9."}, +{"flavor":"windows","left":";","right":"..","str":";\\.."}, +{"flavor":"windows","left":".","right":"unc\\\\unc\\\\UNC\\","str":"unc\\unc\\UNC"}, +{"flavor":"windows","left":"..","right":"/;;","str":"\\;;"}, +{"flavor":"windows","left":"..","right":"\\","str":"\\"}, +{"flavor":"windows","left":"c//C\\\\","right":"//a","str":"\\\\a"}, +{"flavor":"windows","left":"unc:","right":"c","str":"unc:\\c"}, +{"flavor":"windows","left":"?//.","right":"unc","str":"?\\unc"}, +{"flavor":"windows","left":"\u00e9a//c\\\\","right":"C","str":"\u00e9a\\c\\C"}, +{"flavor":"windows","left":"","right":"\\","str":"\\"}, +{"flavor":"windows","left":":c:","right":". ","str":":c:\\. "}, +{"flavor":"windows","left":"","right":"...;?","str":"...;?"}, +{"flavor":"windows","left":":\\b","right":"UNC\\///a","str":":\\b\\UNC\\a"}, +{"flavor":"windows","left":"c..;b","right":"C\u0000c","str":"c..;b\\C\u0000c"}, +{"flavor":"windows","left":"::UNC.b","right":"x,y;","str":"::UNC.b\\x,y;"}, +{"flavor":"windows","left":"Cx,yca","right":":.cC","str":"Cx,yca\\:.cC"}, +{"flavor":"windows","left":"b.","right":"","str":"b."}, +{"flavor":"windows","left":"C..//","right":"...//\u00e9\u00e9","str":"C..\\...\\\u00e9\u00e9"}, +{"flavor":"windows","left":"..x,y","right":"\u00e9:\\x,yUNC","str":"\u00e9:\\x,yUNC"}, +{"flavor":"windows","left":".//..C?","right":"\u0000a","str":"..C?\\\u0000a"}, +{"flavor":"windows","left":"unc\u00e9;.uncunc","right":" ..b:a","str":"unc\u00e9;.uncunc\\ ..b:a"}, +{"flavor":"windows","left":" \u00e9\\\\\\\\ca","right":"\u00e9\u0000","str":" \u00e9\\ca\\\u00e9\u0000"}, +{"flavor":"windows","left":"..\u0000?Cb.","right":"\u00e9UNC","str":"..\u0000?Cb.\\\u00e9UNC"}, +{"flavor":"windows","left":"\\\u00e9//unc","right":"///\u00e9\\..","str":"\\\\\\\u00e9\\.."}, +{"flavor":"windows","left":"","right":"\u0000::","str":"\u0000::"}, +{"flavor":"windows","left":"UNC\\\\x,y","right":"a\\unc\u0000","str":"UNC\\x,y\\a\\unc\u0000"}, +{"flavor":"windows","left":"","right":" ","str":" "}, +{"flavor":"windows","left":"..\\\\/.unc","right":"\\\\..C;","str":"\\\\..C;"}, +{"flavor":"windows","left":"","right":":a?Cc","str":":a?Cc"}, +{"flavor":"windows","left":".:","right":"\u00e9?","str":".:\u00e9?"}, +{"flavor":"windows","left":"","right":"aCx,y","str":"aCx,y"}, +{"flavor":"windows","left":"aunc","right":"","str":"aunc"}, +{"flavor":"windows","left":"unc\u00e9;unc","right":"..\u0000\u00e9","str":"unc\u00e9;unc\\..\u0000\u00e9"}, +{"flavor":"windows","left":"a. ","right":".b../b","str":"a. \\.b..\\b"}, +{"flavor":"windows","left":"bc\\\\\u0000","right":"..\u0000 .\u0000","str":"bc\\\u0000\\..\u0000 .\u0000"}, +{"flavor":"windows","left":"x,y","right":"b\\?/","str":"x,y\\b\\?"}, +{"flavor":"windows","left":":x,ya","right":"?UNC..unc","str":":x,ya\\?UNC..unc"}, +{"flavor":"windows","left":"...UNCunc","right":"\u0000\u00e9\\a","str":"...UNCunc\\\u0000\u00e9\\a"}, +{"flavor":"windows","left":"\\\\:UNC\u0000?b","right":"","str":"\\\\:UNC\u0000?b"}, +{"flavor":"windows","left":"cx,y:a: ","right":" UNCUNC\u0000","str":"cx,y:a: \\ UNCUNC\u0000"}, +{"flavor":"windows","left":"a bx,y\\\\","right":"b:","str":"b:"}, +{"flavor":"windows","left":":","right":"C\u0000unc;c","str":":\\C\u0000unc;c"}, +{"flavor":"windows","left":"\u00e9UNC","right":" C..a\\\\\u00e9","str":"\u00e9UNC\\ C..a\\\u00e9"}, +{"flavor":"windows","left":"C..?//","right":"c:c\u00e9","str":"c:c\u00e9"}, +{"flavor":"windows","left":"\\:c?\\\\","right":"..;;c\u0000 ","str":"\\:c?\\..;;c\u0000 "}, +{"flavor":"windows","left":"\\x,y. ; ","right":"unc","str":"\\x,y. ; \\unc"}, +{"flavor":"windows","left":"\\\\\\..","right":"","str":"\\\\\\.."}, +{"flavor":"windows","left":"c.\u0000","right":"cx,yC","str":"c.\u0000\\cx,yC"}, +{"flavor":"windows","left":"uncb","right":"b//UNC .;","str":"uncb\\b\\UNC .;"}, +{"flavor":"windows","left":"?/","right":"\\","str":"\\"}, +{"flavor":"windows","left":"\\\u0000b","right":";UNC\\//","str":"\\\u0000b\\;UNC"}, +{"flavor":"windows","left":"x,yuncUNC","right":"\u00e9\\\\UNC","str":"x,yuncUNC\\\u00e9\\UNC"}, +{"flavor":"windows","left":"\u0000","right":"?\u0000a?","str":"\u0000\\?\u0000a?"}, +{"flavor":"windows","left":"","right":";C\\\\UNC","str":";C\\UNC"}, +{"flavor":"windows","left":"?/x,y","right":"a//","str":"?\\x,y\\a"}, +{"flavor":"windows","left":".\u00e9.;unc","right":"\u0000","str":".\u00e9.;unc\\\u0000"}, +{"flavor":"windows","left":"unccunc \\UNC","right":"","str":"unccunc \\UNC"}, +{"flavor":"windows","left":"caUNC;..UNC","right":"UNCx,y:.\u0000","str":"caUNC;..UNC\\UNCx,y:.\u0000"}, +{"flavor":"windows","left":"","right":"UNC;\\:\u00e9","str":"UNC;\\:\u00e9"}, +{"flavor":"windows","left":"b?c","right":"\u0000","str":"b?c\\\u0000"}, +{"flavor":"windows","left":"a","right":"\\\\\\b\\\\?","str":"\\\\\\b\\?"}, +{"flavor":"windows","left":":\u0000..c","right":"....","str":":\u0000..c\\...."}, +{"flavor":"windows","left":"?\u0000\\\\unc","right":":\\a\u00e9unc","str":"?\u0000\\unc\\:\\a\u00e9unc"}, +{"flavor":"windows","left":"?UNC.","right":"","str":"?UNC."}, +{"flavor":"windows","left":"x,y\u00e9","right":"x,y","str":"x,y\u00e9\\x,y"}, +{"flavor":"windows","left":"//;b\\b","right":"\u0000\u0000\\\\x,y.c","str":"\\\\;b\\b\\\u0000\u0000\\x,y.c"}, +{"flavor":"windows","left":"UNC\\","right":" x,y:?","str":"UNC\\ x,y:?"}, +{"flavor":"windows","left":"","right":" x,y?\\","str":" x,y?"}, +{"flavor":"windows","left":";/\\//","right":"x,y","str":";\\x,y"}, +{"flavor":"windows","left":";\u0000\\/;","right":"b/a","str":";\u0000\\;\\b\\a"}, +{"flavor":"windows","left":"aac/","right":"...cUNC","str":"aac\\...cUNC"}, +{"flavor":"windows","left":"b\u0000","right":"c\u00e9","str":"b\u0000\\c\u00e9"}, +{"flavor":"windows","left":"","right":"\\C\u00e9\\\\CC","str":"\\C\u00e9\\CC"}, +{"flavor":"windows","left":"\u00e9","right":" UNC/uncc","str":"\u00e9\\ UNC\\uncc"}, +{"flavor":"windows","left":"","right":" \\\\;","str":" \\;"}, +{"flavor":"windows","left":"C\\UNC","right":"","str":"C\\UNC"}, +{"flavor":"windows","left":" //\\:unc","right":"UNCb;..;","str":" \\:unc\\UNCb;..;"}, +{"flavor":"windows","left":":","right":"unc\\uncunc","str":":\\unc\\uncunc"}, +{"flavor":"windows","left":"/UNC\\\\b:","right":"?x,ybc\\\\UNC","str":"\\UNC\\b:\\?x,ybc\\UNC"}, +{"flavor":"windows","left":"ac\u00e9/c","right":"","str":"ac\u00e9\\c"}, +{"flavor":"windows","left":"","right":"","str":"."}, +{"flavor":"windows","left":"x,yunc","right":"\\\\//////","str":"\\\\\\\\"}, +{"flavor":"windows","left":"///","right":"x,y////UNCb","str":"\\\\\\x,y\\UNCb"}, +{"flavor":"windows","left":"b","right":"\\\\Cunc\u0000","str":"\\\\Cunc\u0000"}, +{"flavor":"windows","left":"\\\\:a","right":"/b..\\/","str":"\\\\:a\\b..\\"}, +{"flavor":"windows","left":"..a..x,yunc","right":"..\\\\;//\u00e9unc","str":"..a..x,yunc\\..\\;\\\u00e9unc"}, +{"flavor":"windows","left":"","right":";\u0000","str":";\u0000"}, +{"flavor":"windows","left":"c/\\","right":":","str":"c\\:"}, +{"flavor":"windows","left":"x,y","right":"x,y:","str":"x,y\\x,y:"}, +{"flavor":"windows","left":"","right":"x,y:.c","str":"x,y:.c"}, +{"flavor":"windows","left":"","right":"..?b","str":"..?b"}, +{"flavor":"windows","left":"..;uncaa","right":"\\\\uncuncb\u00e9","str":"\\\\uncuncb\u00e9"}, +{"flavor":"windows","left":"","right":"\\\\..x,yc","str":"\\\\..x,yc"}, +{"flavor":"windows","left":"\u00e9unc\\??","right":"unc","str":"\u00e9unc\\??\\unc"}, +{"flavor":"windows","left":"UNCC\u00e9","right":"://\\UNCUNC","str":"UNCC\u00e9\\:\\UNCUNC"}, +{"flavor":"windows","left":"\\ ","right":" \u0000a..","str":"\\ \\ \u0000a.."}, +{"flavor":"windows","left":"unc\\?","right":"\\\\?b ","str":"\\\\?b "}, +{"flavor":"windows","left":" ///.unc","right":"/x,y..","str":"\\x,y.."}, +{"flavor":"windows","left":":aC","right":"://a//\\\\","str":":aC\\:\\a"}, +{"flavor":"windows","left":" ::","right":"","str":" ::"}, +{"flavor":"windows","left":" UNC:\u00e9","right":"","str":" UNC:\u00e9"}, +{"flavor":"windows","left":"\\x,y..","right":"\\ .:UNC","str":"\\ .:UNC"}, +{"flavor":"windows","left":" \u0000x,y;;unc","right":"unc\\","str":" \u0000x,y;;unc\\unc"}, +{"flavor":"windows","left":"","right":"\\\\;uncCx,y.","str":"\\\\;uncCx,y."}, +{"flavor":"windows","left":"","right":";bx,y\u0000//UNC","str":";bx,y\u0000\\UNC"}, +{"flavor":"windows","left":"","right":"?.C..","str":"?.C.."}, +{"flavor":"windows","left":"","right":"bC\\\\/\\\\","str":"bC"}, +{"flavor":"windows","left":";C","right":":","str":";C\\:"}, +{"flavor":"windows","left":"//a\\\\uncb?","right":".\u00e9b\u00e9?\u0000","str":"\\\\a\\\\uncb?\\.\u00e9b\u00e9?\u0000"}, +{"flavor":"windows","left":"\\\u00e9b","right":"\\b","str":"\\b"}, +{"flavor":"windows","left":";:;:","right":"","str":";:;:"}, +{"flavor":"windows","left":"\u00e9UNC","right":"\\\\:\\\\\u00e9\u00e9","str":"\\\\:\\\\\u00e9\u00e9"}, +{"flavor":"windows","left":"\\/\\","right":"","str":"\\\\\\"}, +{"flavor":"windows","left":"a","right":" ","str":"a\\ "}, +{"flavor":"windows","left":".","right":"?\\UNCUNC\\","str":"?\\UNCUNC"}, +{"flavor":"windows","left":"b\u00e9","right":"","str":"b\u00e9"}, +{"flavor":"windows","left":"//C","right":"C","str":"\\\\C\\C\\"}, +{"flavor":"windows","left":"unc// ","right":"C","str":"unc\\ \\C"}, +{"flavor":"windows","left":".c\u0000","right":"C","str":".c\u0000\\C"}, +{"flavor":"windows","left":"/","right":"...C\u0000a","str":"\\...C\u0000a"}, +{"flavor":"windows","left":"x,yC","right":"b;cccc","str":"x,yC\\b;cccc"}, +{"flavor":"windows","left":"?.","right":"//\\x,y;x,y","str":"\\\\\\x,y;x,y"}, +{"flavor":"windows","left":"//C ?cx,y","right":"?","str":"\\\\C ?cx,y\\?\\"}, +{"flavor":"windows","left":" \\\\","right":"\u00e9\\\\\u0000C\\..","str":" \\\u00e9\\\u0000C\\.."}, +{"flavor":"windows","left":"\u0000:b","right":"unc","str":"\u0000:b\\unc"}, +{"flavor":"windows","left":";a;;?","right":"uncx,yb:","str":";a;;?\\uncx,yb:"}, +{"flavor":"windows","left":"\u0000.","right":"c\\","str":"\u0000.\\c"}, +{"flavor":"windows","left":" ?//","right":"\\x,y/","str":"\\x,y"}, +{"flavor":"windows","left":" ","right":"\\\\b://\u0000\\\\","str":"\\\\b:\\\\\u0000"}, +{"flavor":"windows","left":";","right":"x,y\\\\","str":";\\x,y"}, +{"flavor":"windows","left":"\\\\x,y/c.","right":"c\\\\\\","str":"\\\\x,y\\c.\\c"}, +{"flavor":"windows","left":"\u0000c?;","right":"\u00e9unc/\u00e9x,y","str":"\u0000c?;\\\u00e9unc\\\u00e9x,y"}, +{"flavor":"windows","left":" C","right":"","str":" C"}, +{"flavor":"windows","left":"","right":";;a","str":";;a"}, +{"flavor":"windows","left":".","right":"b..","str":"b.."}, +{"flavor":"windows","left":"","right":"\u00e9/unc","str":"\u00e9\\unc"}, +{"flavor":"windows","left":"x,yUNC","right":"c\u00e9\u0000","str":"x,yUNC\\c\u00e9\u0000"}, +{"flavor":"windows","left":"; \\\\b","right":"","str":"; \\b"}, +{"flavor":"windows","left":"","right":":b;","str":":b;"}, +{"flavor":"windows","left":"\u00e9\\\\..","right":"///","str":"\\\\\\"}, +{"flavor":"windows","left":"\\x,y\u0000ax,y","right":"unc.\\","str":"\\x,y\u0000ax,y\\unc."}, +{"flavor":"windows","left":"","right":"","str":"."}, +{"flavor":"windows","left":"unc","right":"UNCbUNCb:\u0000","str":"unc\\UNCbUNCb:\u0000"}, +{"flavor":"windows","left":"..UNC..UNCb","right":"\\;C/","str":"\\;C"}, +{"flavor":"windows","left":"\u0000..\\;///","right":"/a?","str":"\\a?"}, +{"flavor":"windows","left":"..UNC?","right":"","str":"..UNC?"}, +{"flavor":"windows","left":"/ x,y\\","right":"\u00e9..a","str":"\\ x,y\\\u00e9..a"}, +{"flavor":"windows","left":"\\ab","right":";x,yb/","str":"\\ab\\;x,yb"}, +{"flavor":"windows","left":";UNC//","right":"\u0000 ","str":";UNC\\\u0000 "}, +{"flavor":"windows","left":"UNC?UNCca;","right":"UNC","str":"UNC?UNCca;\\UNC"}, +{"flavor":"windows","left":"b","right":"//C","str":"\\\\C"}, +{"flavor":"windows","left":"uncc /UNC:","right":" Cc","str":"uncc \\UNC:\\ Cc"}, +{"flavor":"windows","left":".;?","right":":C\\\\;/","str":".;?\\:C\\;"}, +{"flavor":"windows","left":"x,yc;//","right":"\u00e9\u00e9x,y","str":"x,yc;\\\u00e9\u00e9x,y"}, +{"flavor":"windows","left":"b?a.\u0000","right":"//\u00e9..\\;?","str":"\\\\\u00e9..\\;?\\"}, +{"flavor":"windows","left":"?:///.\\\\","right":"\u0000:a/","str":"\u0000:a"}, +{"flavor":"windows","left":"","right":"..Cunc..","str":"..Cunc.."}, +{"flavor":"windows","left":";","right":"UNCc","str":";\\UNCc"}, +{"flavor":"windows","left":"","right":"C.;unc\\","str":"C.;unc"}, +{"flavor":"windows","left":";.x,y..","right":"//\u00e9UNC;?","str":"\\\\\u00e9UNC;?"} +], +"equality": [ +{"left":"C:\\A","right":"c:/a","equal":true}, +{"left":"C:\\a","right":"C:\\b","equal":false}, +{"left":"\\\\S\\x\\Y","right":"//s/X/y","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"?","right":"?","equal":true}, +{"left":" ;","right":" ;","equal":true}, +{"left":"aa//B/","right":"aa//B/","equal":true}, +{"left":"a\u0000c","right":"a\u0000c","equal":true}, +{"left":"\\c","right":"\\c","equal":true}, +{"left":"\\\\/","right":"\\\\/","equal":true}, +{"left":"Cc","right":"cc","equal":true}, +{"left":".x,yaUNC;","right":".x,yaUNC;","equal":true}, +{"left":"CUNC","right":"CUNC","equal":true}, +{"left":"?:","right":"?:","equal":true}, +{"left":"aUNC.","right":"AUNC.","equal":true}, +{"left":"UNCB\\\\\\\u0000","right":"uncb\\\\\\\u0000","equal":true}, +{"left":".B","right":".B","equal":true}, +{"left":"\u00e9//;;\u0000","right":"\u00c9//;;\u0000","equal":true}, +{"left":"..\u00e9C","right":"..\u00e9c","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"BB","right":"BB","equal":true}, +{"left":"//","right":"//","equal":true}, +{"left":"a","right":"a","equal":true}, +{"left":"\\\\/;.:","right":"\\\\/;.:","equal":true}, +{"left":" ://","right":" ://","equal":true}, +{"left":";.;.unc","right":";.;.unc","equal":true}, +{"left":"b\u00e9/","right":"b\u00e9/","equal":true}, +{"left":" /x,y/","right":" /x,y/","equal":true}, +{"left":";A","right":";A","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"ax,y","right":"ax,y","equal":true}, +{"left":" .","right":" .","equal":true}, +{"left":"B","right":"B","equal":true}, +{"left":"x,y","right":"X,Y","equal":true}, +{"left":"c\u0000?","right":"C\u0000?","equal":true}, +{"left":"UNC/\u00e9","right":"UNC/\u00e9","equal":true}, +{"left":"c:A\\","right":"C:A\\","equal":true}, +{"left":"\u00e9ba/?","right":"\u00e9ba/?","equal":true}, +{"left":"//CB","right":"//cb","equal":true}, +{"left":"C\u00e9","right":"c\u00e9","equal":true}, +{"left":"..","right":"..","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"C","right":"c","equal":true}, +{"left":"//A\u0000//\\\\","right":"//A\u0000//\\\\","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"CCc","right":"CCc","equal":true}, +{"left":"b.","right":"b.","equal":true}, +{"left":"C;","right":"c;","equal":true}, +{"left":" ///\\\\UNC","right":" ///\\\\unc","equal":true}, +{"left":"x,y..:.","right":"x,y..:.","equal":true}, +{"left":".","right":".","equal":true}, +{"left":"\u00e9\\\\A/?","right":"\u00e9\\\\A/?","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"\u00e9unc?A","right":"\u00e9unc?A","equal":true}, +{"left":"\\\\","right":"\\\\","equal":true}, +{"left":"unc","right":"UNC","equal":true}, +{"left":"\u0000;//","right":"\u0000;//","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"\u00e9\u00e9","right":"\u00e9\u00e9","equal":true}, +{"left":"\u0000\u0000","right":"\u0000\u0000","equal":true}, +{"left":"\u00e9","right":"\u00e9","equal":true}, +{"left":"..\\\\//.. ","right":"..\\\\//.. ","equal":true}, +{"left":"unc","right":"unc","equal":true}, +{"left":"\\","right":"\\","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"Aa\u0000;","right":"Aa\u0000;","equal":true}, +{"left":"","right":"","equal":true}, +{"left":";c//b\u0000","right":";c//b\u0000","equal":true}, +{"left":"\u00e9\\\\unc","right":"\u00e9\\\\unc","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"uncaC","right":"uncaC","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"A","right":"a","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"x,ya","right":"x,ya","equal":true}, +{"left":"a\u00e9//","right":"a\u00e9//","equal":true}, +{"left":"/c","right":"/c","equal":true}, +{"left":" ;;\u0000","right":" ;;\u0000","equal":true}, +{"left":"A\\ ","right":"A\\ ","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"C","right":"c","equal":true}, +{"left":"../unc\\\\\\\\","right":"../UNC\\\\\\\\","equal":true}, +{"left":"?..x,ya\u0000","right":"?..X,YA\u0000","equal":true}, +{"left":"UNC","right":"UNC","equal":true}, +{"left":"C/;bc","right":"C/;BC","equal":true}, +{"left":"\\\\","right":"\\\\","equal":true}, +{"left":"..a?","right":"..A?","equal":true}, +{"left":"?x,y","right":"?X,Y","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"//c","right":"//C","equal":true}, +{"left":"///aA","right":"///aa","equal":true}, +{"left":"acB","right":"ACB","equal":true}, +{"left":"","right":"","equal":true}, +{"left":"x,yb","right":"x,yb","equal":true}, +{"left":":A","right":":A","equal":true}, +{"left":"/x,y","right":"/X,Y","equal":true}, +{"left":"c","right":"C","equal":true}, +{"left":"","right":"","equal":true} +], +"normpath": [ +{"text":"","normpath":"."}, +{"text":".","normpath":"."}, +{"text":"a","normpath":"a"}, +{"text":"a\\","normpath":"a"}, +{"text":"A//B","normpath":"A\\B"}, +{"text":"A/./B","normpath":"A\\B"}, +{"text":"A/foo/../B","normpath":"A\\B"}, +{"text":"C:\\","normpath":"C:\\"}, +{"text":"C:","normpath":"C:"}, +{"text":"C:a\\..\\..","normpath":"C:.."}, +{"text":"C:\\..\\a","normpath":"C:\\a"}, +{"text":"..\\..\\a","normpath":"..\\..\\a"}, +{"text":"a\\..\\..\\b","normpath":"..\\b"}, +{"text":"\\\\server\\share\\","normpath":"\\\\server\\share\\"}, +{"text":"\\\\server\\share\\a\\..","normpath":"\\\\server\\share\\"}, +{"text":".\\a\\.\\b\\","normpath":"a\\b"}, +{"text":"\\","normpath":"\\"}, +{"text":"./..","normpath":".."}, +{"text":"C:./x","normpath":"C:x"}, +{"text":"\\Cb ","normpath":"\\Cb "}, +{"text":"\u00e9\u00e9:/?x,yunc\\;C","normpath":"\u00e9\u00e9:\\?x,yunc\\;C"}, +{"text":"unc.:\u00e9;bunc\\","normpath":"unc.:\u00e9;bunc"}, +{"text":"C;c\u00e9b\u0000","normpath":"C;c\u00e9b\u0000"}, +{"text":";.x,y\\/abCbunc","normpath":";.x,y\\abCbunc"}, +{"text":"UNC?//C","normpath":"UNC?\\C"}, +{"text":"/UNC;ba","normpath":"\\UNC;ba"}, +{"text":"unc//","normpath":"unc"}, +{"text":"C \\\\ unc//\\\\x,yunc","normpath":"C \\ unc\\x,yunc"}, +{"text":"\\;UNC","normpath":"\\;UNC"}, +{"text":"Cb","normpath":"Cb"}, +{"text":"..aunc\u0000","normpath":"..aunc\u0000"}, +{"text":"b ;/ ","normpath":"b ;\\ "}, +{"text":"//UNC:\\\\/.","normpath":"\\\\UNC:\\\\"}, +{"text":"","normpath":"."}, +{"text":"a","normpath":"a"}, +{"text":"aCbb?;\\unc\u00e9","normpath":"aCbb?;\\unc\u00e9"}, +{"text":"unc\u0000;x,y/c?\\\\","normpath":"unc\u0000;x,y\\c?"}, +{"text":"\\;","normpath":"\\;"}, +{"text":"..?UNC","normpath":"..?UNC"}, +{"text":" :\u0000\\ :\u0000?:","normpath":" :\u0000\\ :\u0000?:"}, +{"text":"\\\\/cunc\\:","normpath":"\\\\\\cunc\\:"}, +{"text":"\u00e9c","normpath":"\u00e9c"}, +{"text":"/bunc\\?","normpath":"\\bunc\\?"}, +{"text":"a","normpath":"a"}, +{"text":"b","normpath":"b"}, +{"text":" C...aUNCc\\","normpath":" C...aUNCc"}, +{"text":"x,y/ b","normpath":"x,y\\ b"}, +{"text":"cx,y?.:","normpath":"cx,y?.:"}, +{"text":" UNC\u00e9///C./; ","normpath":" UNC\u00e9\\C.\\; "}, +{"text":"//....;\u0000a","normpath":"\\\\....;\u0000a"}, +{"text":"/x,y\\.;\\UNC ","normpath":"\\x,y\\.;\\UNC "}, +{"text":"/b;C\\\\x,y","normpath":"\\b;C\\x,y"}, +{"text":"\u00e9ca:\u00e9C\u00e9\\\\\\","normpath":"\u00e9ca:\u00e9C\u00e9"}, +{"text":"ba/;:x,yUNCUNC//","normpath":"ba\\;:x,yUNCUNC"}, +{"text":"a","normpath":"a"}, +{"text":"c.C/bC","normpath":"c.C\\bC"}, +{"text":"uncunc?;.\\\\","normpath":"uncunc?;."}, +{"text":"unc;/;\u00e9","normpath":"unc;\\;\u00e9"}, +{"text":"uncUNC bc /UNCunc\\","normpath":"uncUNC bc \\UNCunc"}, +{"text":" unc/","normpath":" unc"}, +{"text":" unc","normpath":" unc"}, +{"text":"\\\\UNC","normpath":"\\\\UNC"}, +{"text":"aUNCCx,y\\\\\\\\\u00e9bUNC","normpath":"aUNCCx,y\\\u00e9bUNC"}, +{"text":"","normpath":"."}, +{"text":"c\\b\u00e9\u00e9","normpath":"c\\b\u00e9\u00e9"}, +{"text":"C;.UNCunc","normpath":"C;.UNCunc"}, +{"text":" ?UNCunc/unc ","normpath":" ?UNCunc\\unc "}, +{"text":"UNCUNC:b\u00e9\\C.c","normpath":"UNCUNC:b\u00e9\\C.c"}, +{"text":"Cunc//UNC ///....","normpath":"Cunc\\UNC \\...."}, +{"text":"b\\c\\","normpath":"b\\c"}, +{"text":"","normpath":"."}, +{"text":"//","normpath":"\\\\"}, +{"text":"\u00e9","normpath":"\u00e9"}, +{"text":"? x,y","normpath":"? x,y"}, +{"text":"...","normpath":"..."}, +{"text":"b/..\u0000x,y\\","normpath":"b\\..\u0000x,y"}, +{"text":"\\?","normpath":"\\?"}, +{"text":"UNC////aC?.?:","normpath":"UNC\\aC?.?:"}, +{"text":"b:.\\.;unc\\\\unc","normpath":"b:.;unc\\unc"}, +{"text":":; ///","normpath":":; "}, +{"text":"\u0000b\u0000:\u0000x,y","normpath":"\u0000b\u0000:\u0000x,y"}, +{"text":"c\\\\;.?\\b?UNC;","normpath":"c\\;.?\\b?UNC;"}, +{"text":"bc\u00e9?? \u0000","normpath":"bc\u00e9?? \u0000"}, +{"text":"/auncb","normpath":"\\auncb"}, +{"text":" \\/a","normpath":" \\a"}, +{"text":"","normpath":"."}, +{"text":"\\unc\u00e9??","normpath":"\\unc\u00e9??"}, +{"text":":unc?b\u0000a","normpath":":unc?b\u0000a"}, +{"text":";?\\c\\\\ ","normpath":";?\\c\\ "}, +{"text":"\\:\\//uncx,y\u0000:\\//","normpath":"\\:\\uncx,y\u0000:"}, +{"text":"b //.","normpath":"b "}, +{"text":"\u0000//C:\u00e9:b","normpath":"\u0000\\C:\u00e9:b"}, +{"text":"","normpath":"."}, +{"text":"/unc\\\\.;","normpath":"\\unc\\.;"}, +{"text":"\\\\b\\\\a?unc?.\\","normpath":"\\\\b\\\\a?unc?."}, +{"text":"b\\\\UNC://?\\\\..x,y","normpath":"b\\UNC:\\?\\..x,y"}, +{"text":"bUNC ../.","normpath":"bUNC .."}, +{"text":"c//c\u0000 bb/","normpath":"c\\c\u0000 bb"}, +{"text":"Cx,y?\u0000uncUNC..","normpath":"Cx,y?\u0000uncUNC.."}, +{"text":".a;b x,y\u00e9//CUNC","normpath":".a;b x,y\u00e9\\CUNC"}, +{"text":" ","normpath":" "}, +{"text":"\\;","normpath":"\\;"}, +{"text":"\u00e9cC\\\\?","normpath":"\u00e9cC\\?"}, +{"text":"?C\u0000","normpath":"?C\u0000"}, +{"text":"","normpath":"."}, +{"text":"b:UNC","normpath":"b:UNC"}, +{"text":"","normpath":"."}, +{"text":"//c//?bx,y;;a","normpath":"\\\\c\\\\?bx,y;;a"}, +{"text":".;","normpath":".;"}, +{"text":" ?..C.","normpath":" ?..C."}, +{"text":";UNCCc;Ca..?C","normpath":";UNCCc;Ca..?C"}, +{"text":"x,yb","normpath":"x,yb"}, +{"text":"","normpath":"."}, +{"text":"\\\\unc\\\\\u00e9c/","normpath":"\\\\unc\\\\\u00e9c"}, +{"text":" //bb","normpath":" \\bb"}, +{"text":"C/:unc/","normpath":"C\\:unc"}, +{"text":"x,y\\\\","normpath":"x,y"}, +{"text":"x,y..unc//unc","normpath":"x,y..unc\\unc"}, +{"text":"UNC..uncbx,y\\\\b","normpath":"UNC..uncbx,y\\b"}, +{"text":"\\x,y// .","normpath":"\\x,y\\ ."}, +{"text":"\u0000//.a C","normpath":"\u0000\\.a C"}, +{"text":"aUNC","normpath":"aUNC"}, +{"text":"\\\\c. ////\\\\","normpath":"\\\\c. \\\\"}, +{"text":"../.\\\\\u00e9;","normpath":"..\\\u00e9;"}, +{"text":"","normpath":"."}, +{"text":"a\\/UNC.cc","normpath":"a\\UNC.cc"}, +{"text":"\\/// ;","normpath":"\\\\\\\\ ;"}, +{"text":"C","normpath":"C"}, +{"text":"..\u0000:","normpath":"..\u0000:"}, +{"text":"C;\u00e9b...?x,y;UNC","normpath":"C;\u00e9b...?x,y;UNC"}, +{"text":"/;\u0000.UNC..","normpath":"\\;\u0000.UNC.."}, +{"text":"//b////:;:Cb..","normpath":"\\\\b\\\\:;:Cb.."}, +{"text":"\\\u0000UNC/ccuncunc","normpath":"\\\u0000UNC\\ccuncunc"}, +{"text":" \\//\\\u00e9","normpath":" \\\u00e9"}, +{"text":"\u00e9UNCb;UNCUNC.//c.","normpath":"\u00e9UNCb;UNCUNC.\\c."}, +{"text":":\\\\\\\\C\\c","normpath":":\\C\\c"}, +{"text":"C..\u00e9","normpath":"C..\u00e9"}, +{"text":"","normpath":"."}, +{"text":".cuncb","normpath":".cuncb"}, +{"text":" \u00e9 x,y?unc","normpath":" \u00e9 x,y?unc"}, +{"text":"..;c ..x,y\u0000unc\\\\","normpath":"..;c ..x,y\u0000unc"}, +{"text":"ax,y:..x,yCunc: ","normpath":"ax,y:..x,yCunc: "}, +{"text":"...","normpath":"..."}, +{"text":"..c..\\\\//x,yb","normpath":"..c..\\x,yb"}, +{"text":"","normpath":"."}, +{"text":"//;C/./:","normpath":"\\\\;C\\.\\:"}, +{"text":"UNC\\:x,ybc/a.C","normpath":"UNC\\:x,ybc\\a.C"}, +{"text":"c\u00e9;C","normpath":"c\u00e9;C"}, +{"text":"\\\\:unc..; ..//","normpath":"\\\\:unc..; ..\\\\"}, +{"text":"x,y\u00e9\u0000.UNC..","normpath":"x,y\u00e9\u0000.UNC.."}, +{"text":"","normpath":"."}, +{"text":"/","normpath":"\\"}, +{"text":"aca.bC\\\u00e9\u0000\\\\","normpath":"aca.bC\\\u00e9\u0000"}, +{"text":"a//unc\\\\/;c","normpath":"a\\unc\\;c"}, +{"text":":uncC///?","normpath":":uncC\\?"}, +{"text":"// x,y\u00e9c","normpath":"\\\\ x,y\u00e9c"}, +{"text":"\\\u0000C","normpath":"\\\u0000C"}, +{"text":"//;?","normpath":"\\\\;?"}, +{"text":".\\\u0000C:a ","normpath":"\u0000C:a "}, +{"text":"\\\\\\\\:x,yc :a","normpath":"\\\\\\\\:x,yc :a"}, +{"text":"/\u0000x,yx,y/","normpath":"\\\u0000x,yx,y"}, +{"text":"","normpath":"."}, +{"text":"\\;.\u0000","normpath":"\\;.\u0000"}, +{"text":"\\// :c?x,y\u0000/","normpath":"\\\\\\ :c?x,y\u0000\\"}, +{"text":"\\\\\\://\u00e9\\\\\\a","normpath":"\\\\\\:\\\u00e9\\a"}, +{"text":"/C?C.\u0000UNCunc","normpath":"\\C?C.\u0000UNCunc"}, +{"text":"?\u00e9//?cx,yUNC;\\\\","normpath":"?\u00e9\\?cx,yUNC;"}, +{"text":"?:\u0000\\..\u0000:x,y..","normpath":"?:\u0000\\..\u0000:x,y.."}, +{"text":":c;//a\\;\\\\bc","normpath":":c;\\a\\;\\bc"}, +{"text":"uncUNC///b..unc","normpath":"uncUNC\\b..unc"}, +{"text":"?c;c//","normpath":"?c;c"}, +{"text":" Ca;.?/","normpath":" Ca;.?"}, +{"text":"\\","normpath":"\\"}, +{"text":"a\\.c","normpath":"a\\.c"}, +{"text":"x,yc\u0000bunc","normpath":"x,yc\u0000bunc"}, +{"text":"../\u0000x,y?C;\u0000..","normpath":"..\\\u0000x,y?C;\u0000.."}, +{"text":"// x,yC\u0000UNC////","normpath":"\\\\ x,yC\u0000UNC\\\\"}, +{"text":"..C?/..c","normpath":"..C?\\..c"}, +{"text":".?Caa:/","normpath":".?Caa:"}, +{"text":" ","normpath":" "}, +{"text":"\u0000\\;\u00e9.x,y.","normpath":"\u0000\\;\u00e9.x,y."}, +{"text":".//UNC;c;.unc","normpath":"UNC;c;.unc"}, +{"text":"bx,y: /c?Cbb","normpath":"bx,y: \\c?Cbb"}, +{"text":"unc:?\u0000//c\u0000//?\\","normpath":"unc:?\u0000\\c\u0000\\?"}, +{"text":"\u00e9\u00e9\u0000ba","normpath":"\u00e9\u00e9\u0000ba"}, +{"text":"UNC\\\\..aUNC;","normpath":"UNC\\..aUNC;"}, +{"text":"C\\?\\;\u00e9c","normpath":"C\\?\\;\u00e9c"}, +{"text":"\\/ . //.C","normpath":"\\\\ . \\\\.C"}, +{"text":"x,y","normpath":"x,y"}, +{"text":"//\\\\\u00e9?\\","normpath":"\\\\\\\\\u00e9?"}, +{"text":"/\\ \\C","normpath":"\\\\ \\C"}, +{"text":" x,yCc?.x,yc","normpath":" x,yCc?.x,yc"}, +{"text":":CUNC://x,yC\\","normpath":":CUNC:\\x,yC"}, +{"text":"/ x,y\u0000??c b/","normpath":"\\ x,y\u0000??c b"}, +{"text":"\u00e9\u0000..\\","normpath":"\u00e9\u0000.."}, +{"text":"UNC\u0000CbUNC\\\\:.C","normpath":"UNC\u0000CbUNC\\:.C"}, +{"text":"?","normpath":"?"}, +{"text":"..;\u0000C:.\u0000..\\","normpath":"..;\u0000C:.\u0000.."}, +{"text":"","normpath":"."}, +{"text":"","normpath":"."}, +{"text":"\u00e9","normpath":"\u00e9"}, +{"text":"","normpath":"."}, +{"text":"\u0000x,yx,yb??b?x,y\u0000","normpath":"\u0000x,yx,yb??b?x,y\u0000"}, +{"text":"\\\\a\\\\b a\u00e9x,yCunc","normpath":"\\\\a\\\\b a\u00e9x,yCunc"}, +{"text":":\u0000..c.;c\\unc\\","normpath":":\u0000..c.;c\\unc"}, +{"text":" c.unc\\\\? ","normpath":" c.unc\\? "}, +{"text":"...b//.x,y //","normpath":"...b\\.x,y "}, +{"text":";b\\/\u00e9a...:","normpath":";b\\\u00e9a...:"}, +{"text":"\\","normpath":"\\"}, +{"text":"a\u0000","normpath":"a\u0000"}, +{"text":"b","normpath":"b"}, +{"text":"\\\\\u00e9\\\\ x,y\\\\/\\\\","normpath":"\\\\\u00e9\\\\ x,y"}, +{"text":"x,y\u00e9C?C","normpath":"x,y\u00e9C?C"}, +{"text":"?\\","normpath":"?"}, +{"text":" a\u0000Cb","normpath":" a\u0000Cb"}, +{"text":"/? \\b..","normpath":"\\? \\b.."}, +{"text":"\u00e9\u0000.///Cc.x,y","normpath":"\u00e9\u0000.\\Cc.x,y"}, +{"text":"//x,y\u00e9\u00e9\\\\..a","normpath":"\\\\x,y\u00e9\u00e9\\\\..a"}, +{"text":"x,yUNCunc\\\\\\ \\unc ","normpath":"x,yUNCunc\\ \\unc "}, +{"text":"","normpath":"."}, +{"text":"","normpath":"."}, +{"text":"","normpath":"."}, +{"text":"cUNC:\\\\?//","normpath":"cUNC:\\?"}, +{"text":"\u00e9a","normpath":"\u00e9a"}, +{"text":"?c","normpath":"?c"}, +{"text":"// uncc\u00e9;","normpath":"\\\\ uncc\u00e9;"}, +{"text":"/x,yx,y","normpath":"\\x,yx,y"}, +{"text":"","normpath":"."}, +{"text":"\\\u0000\\\\b./uncx,y","normpath":"\\\u0000\\b.\\uncx,y"}, +{"text":"// bb. : ","normpath":"\\\\ bb. : "}, +{"text":"unc;.;","normpath":"unc;.;"}, +{"text":"b\u00e9UNC;;.b?","normpath":"b\u00e9UNC;;.b?"}, +{"text":"","normpath":"."}, +{"text":"C\\\\\\\\\\\\UNCC..\\","normpath":"C\\UNCC.."}, +{"text":"x,yC//x,yUNC?C","normpath":"x,yC\\x,yUNC?C"}, +{"text":"","normpath":"."}, +{"text":"\u0000//\\\\.UNCunc\u00e9","normpath":"\u0000\\.UNCunc\u00e9"}, +{"text":"a..:","normpath":"a..:"}, +{"text":"a.;b\u0000","normpath":"a.;b\u0000"}, +{"text":";\u0000Cx,y..;..//\u0000:","normpath":";\u0000Cx,y..;..\\\u0000:"}, +{"text":"\u0000\\\\x,ybb..;","normpath":"\u0000\\x,ybb..;"}, +{"text":"","normpath":"."}, +{"text":"","normpath":"."}, +{"text":"/ \u00e9UNC.?..ca","normpath":"\\ \u00e9UNC.?..ca"}, +{"text":"unc////.","normpath":"unc"}, +{"text":"\\unc..\u00e9C\u00e9","normpath":"\\unc..\u00e9C\u00e9"}, +{"text":"/c//","normpath":"\\c"}, +{"text":"","normpath":"."}, +{"text":";\u0000\\\\:x,y\\b?","normpath":";\u0000\\:x,y\\b?"}, +{"text":"??/C;UNCCaUNC ","normpath":"??\\C;UNCCaUNC "}, +{"text":"b\\\\\u0000\u00e9?","normpath":"b\\\u0000\u00e9?"}, +{"text":"?/ ?","normpath":"?\\ ?"}, +{"text":":","normpath":":"}, +{"text":"?\\\\\\","normpath":"?"}, +{"text":"","normpath":"."}, +{"text":"C\u0000x,y","normpath":"C\u0000x,y"}, +{"text":"b\u0000../////","normpath":"b\u0000.."}, +{"text":"\\\\//x,yb","normpath":"\\\\\\\\x,yb"}, +{"text":"\u0000","normpath":"\u0000"}, +{"text":"\u00e9a.//Cc :?a","normpath":"\u00e9a.\\Cc :?a"}, +{"text":"","normpath":"."}, +{"text":"\u0000","normpath":"\u0000"}, +{"text":"UNCb\\\\//;:?:a","normpath":"UNCb\\;:?:a"}, +{"text":"?","normpath":"?"}, +{"text":"\u0000bc ","normpath":"\u0000bc "}, +{"text":"//x,yC/\\\\\u0000?x,yC?","normpath":"\\\\x,yC\\\\\u0000?x,yC?"}, +{"text":"\u00e9","normpath":"\u00e9"}, +{"text":"UNC;","normpath":"UNC;"}, +{"text":"/;c?unc/ ","normpath":"\\;c?unc\\ "} +], +"json": [ +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"[]","value":["list",[]]}, +{"text":"{}","value":["dict",[]]}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"[1","decode_error":"Expecting ',' delimiter: line 1 column 3 (char 2)"}, +{"text":"[1,]","decode_error":"Illegal trailing comma before end of array: line 1 column 3 (char 2)"}, +{"text":"{\"a\":1,}","decode_error":"Illegal trailing comma before end of object: line 1 column 7 (char 6)"}, +{"text":"{1:2}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"{\"a\" 1}","decode_error":"Expecting ':' delimiter: line 1 column 6 (char 5)"}, +{"text":"\"abc","decode_error":"Unterminated string starting at: line 1 column 1 (char 0)"}, +{"text":"\"a\tb\"","decode_error":"Invalid control character at: line 1 column 3 (char 2)"}, +{"text":"\"\\x\"","decode_error":"Invalid \\escape: line 1 column 2 (char 1)"}, +{"text":"\"\\u12\"","decode_error":"Invalid \\uXXXX escape: line 1 column 3 (char 2)"}, +{"text":"\"\\u12x4\"","decode_error":"Invalid \\uXXXX escape: line 1 column 3 (char 2)"}, +{"text":"\"\\ud800\\u12x4\"","decode_error":"Invalid \\uXXXX escape: line 1 column 9 (char 8)"}, +{"text":"[]\n x","decode_error":"Extra data: line 2 column 2 (char 4)"}, +{"text":"\ufeff[]","decode_error":"Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)"}, +{"text":"-","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"01","decode_error":"Extra data: line 1 column 2 (char 1)"}, +{"text":"1.","decode_error":"Extra data: line 1 column 2 (char 1)"}, +{"text":"1e","decode_error":"Extra data: line 1 column 2 (char 1)"}, +{"text":"1e+","decode_error":"Extra data: line 1 column 2 (char 1)"}, +{"text":"-Infinityx","decode_error":"Extra data: line 1 column 10 (char 9)"}, +{"text":"nul","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]","value":["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[["list",[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]}, +{"text":"11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","value_error":"Exceeds the limit (4300 digits) for integer string conversion: value has 4301 digits; use sys.set_int_max_str_digits() to increase the limit"}, +{"text":"[1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111]","value":["list",[["int","1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"]]]}, +{"text":"1.11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","value":["float","3ff1c71c71c71c72"]}, +{"text":"{\"a\": [true, false, nul0], \"a\":}{\"b\": \"\\n\\t\\\\\\\"\\/\"}}true","decode_error":"Expecting value: line 1 column 21 (char 20)"}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", }\"C\u00e9\\ud3d\\ude00\"]","decode_error":"Expecting value: line 1 column 21 (char 20)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"{\"a\": [utrue, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 8 (char 7)"}, +{"text":"{}","value":["dict",[]]}, +{"text":"[\"A=1\", \"B=\\]00e9\", 1C=\\ud83d\\ude00\"]","decode_error":"Invalid \\escape: line 1 column 12 (char 11)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"\"d800{}","decode_error":"Unterminated string starting at: line 1 column 1 (char 0)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"{\"a\": [true, false, null]true\"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting ',' delimiter: line 1 column 26 (char 25)"}, +{"text":"[\\]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"{[]","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"\"\\ud800\\udc00\\ud:c00\\ud80\u001fx\"","decode_error":"Invalid \\uXXXX escape: line 1 column 15 (char 14)"}, +{"text":"{}","value":["dict",[]]}, +{"text":" \t\r\n[1 , 2 ]Infinity","decode_error":"Extra data: line 2 column 9 (char 12)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"{}","value":["dict",[]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3 [","decode_error":"Expecting ',' delimiter: line 1 column 54 (char 53)"}, +{"text":"{}","value":["dict",[]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"[NaN, Infinity, -:Infin\u0000ity, 1e400, -0.0, 1E-5, 10 -3]","decode_error":"Expecting value: line 1 column 17 (char 16)"}, +{"text":" \t\r\n[1 , 2 ","decode_error":"Expecting ',' delimiter: line 2 column 9 (char 12)"}, +{"text":"{\"a\": [rue, false, null], \"a\": {\"b\":\t\"\n\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 8 (char 7)"}, +{"text":"[NaN, Infinity, 1Infinity, 1e400, -0.0, 1E-5, 10, -3]","decode_error":"Expecting ',' delimiter: line 1 column 18 (char 17)"}, +{"text":"{\"a\": [true, fa.se, null.], \"a\"\u0000: {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 14 (char 13)"}, +{"text":"{}","value":["dict",[]]}, +{"text":"{","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"[]","value":["list",[]]}, +{"text":"\"\\ud800\\udc00\\udc00\\u800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 21 (char 20)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"[NaN, Infinity, -Infixnity,\"1e400, -0.0, 1E-true, 10, -3]","decode_error":"Expecting value: line 1 column 17 (char 16)"}, +{"text":"[\"A=1\"\n \"Bu=\\u00e9\", \"C=\\ud83d]\\ude00\"]","decode_error":"Expecting ',' delimiter: line 2 column 2 (char 8)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"][","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":" \t\r\n[1 ,x\ufeff ]","decode_error":"Expecting value: line 2 column 5 (char 8)"}, +{"text":"\"\\ud800\\truedc00\\ud[00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 18 (char 17)"}, +{"text":" \r{]\n[1 , 2 ] ","decode_error":"Expecting property name enclosed in double quotes: line 1 column 4 (char 3)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"\"\\ud80+0\\udc00 udc00\\ud8-0x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 3 (char 2)"}, +{"text":"[NaN, Infinity, -In.inity, 1e400, -0.0, 1E-5, 10, -]\ufeff","decode_error":"Expecting value: line 1 column 17 (char 16)"}, +{"text":"","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"\"\\ud8-00\\udc00NaN\\udc00\\u800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 3 (char 2)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"\"\\ud800\\u}dc00\\udc00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 9 (char 8)"}, +{"text":"{NaN}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"\r[","decode_error":"Expecting value: line 1 column 3 (char 2)"}, +{"text":"[]","value":["list",[]]}, +{"text":" \r\n[1 , 2 ]","value":["list",[["int","1"],["int","2"]]]}, +{"text":"[]","value":["list",[]]}, +{"text":"{\"a\":null [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting ',' delimiter: line 1 column 11 (char 10)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"pack\nges\": [{\"name\": \"systemd\"}]}","decode_error":"Invalid control character at: line 1 column 43 (char 42)"}, +{"text":"{\"format\": NaN1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting ',' delimiter: line 1 column 15 (char 14)"}, +{"text":"1}","decode_error":"Extra data: line 1 column 2 (char 1)"}, +{"text":"\t\r[1 , 2] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"[}","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":" \t\r\n[1 , 2 ]d800","decode_error":"Extra data: line 2 column 9 (char 12)"}, +{"text":"{\"format\": 1 \"artifaxt\": \"a.erofs\" \"packages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting ',' delimiter: line 1 column 15 (char 14)"}, +{"text":" \r\n[1 , 2 +","decode_error":"Expecting ',' delimiter: line 2 column 9 (char 11)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, x3]","decode_error":"Expecting value: line 1 column 51 (char 50)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud}00x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 21 (char 20)"}, +{"text":"{\t\r\n[1 , 2 ] ","decode_error":"Expecting property name enclosed in double quotes: line 2 column 1 (char 4)"}, +{"text":"{e0}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"{\u00e9\n","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"\"ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[117,100,56,48,48,56320,56320,55296,120]]}, +{"text":"[N0aN, Infinity, -Infinity, 1e4000, -0.0, 1E-5, 10, -3]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"\"\\ud800\\udc00\\udc00\\ude800x\"","value":["code_points",[65536,56320,56960,48,120]]}, +{"text":" \u00e9\r\n[1 , 2 ]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":" \t\r\n[1 , 2] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"]\u001f","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":" \t\r[Infinity, 2 ] ","value":["list",[["float","7ff0000000000000"],["int","2"]]]}, +{"text":"[","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"\"\\ud800\\udc00\\udc00\\ud8\u000000x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 21 (char 20)"}, +{"text":"\"\\ud800\\\u00e9udc00\nInfinitydc00\\ud800x\"","decode_error":"Invalid \\escape: line 1 column 8 (char 7)"}, +{"text":"[","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":" \t\r\n[e , ] ","decode_error":"Expecting value: line 2 column 2 (char 5)"}, +{"text":"[\u0000]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"[NaN,Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"\"ud800\\udc00\\udc00ud800x\"","value":["code_points",[117,100,56,48,48,56320,56320,117,100,56,48,48,120]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -+.0, 1E-5, 10, -3]","decode_error":"Expecting value: line 1 column 35 (char 34)"}, +{"text":"{}","value":["dict",[]]}, +{"text":"{}x","decode_error":"Extra data: line 1 column 3 (char 2)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"\t\r\n[1 , 2true ] ","decode_error":"Expecting ',' delimiter: line 2 column 7 (char 9)"}, +{"text":"{\"format\": 1, \"artifact\": \"Infinity.erofs\", \"packages\": [{\"nam\u0000\": \"systemd\"}]}","decode_error":"Invalid control character at: line 1 column 63 (char 62)"}, +{"text":"[\"d800A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","d800A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"\t}u","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"true}","decode_error":"Extra data: line 1 column 5 (char 4)"}, +{"text":"[]","value":["list",[]]}, +{"text":"{}","value":["dict",[]]}, +{"text":"\ufeff0","decode_error":"Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)"}, +{"text":"}:","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"[n\\t\\d800\\\\\"\\/\"}}","decode_error":"Invalid \\escape: line 1 column 44 (char 43)"}, +{"text":"\"\\ud\\800\\udc00\\udc00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 3 (char 2)"}, +{"text":" \"A=1\", \"B=u00e9\", \"C=\\ud83d\\ude00\"]","decode_error":"Extra data: line 1 column 7 (char 6)"}, +{"text":"\u001f{\"\normat\": 1, \"artifact\": \"a.eofs\", \"packages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[\"A=1\", \"B=\\00e9\", \"C=\\ud83d\\ude00\"]","decode_error":"Invalid \\escape: line 1 column 12 (char 11)"}, +{"text":"Infinity}","decode_error":"Extra data: line 1 column 9 (char 8)"}, +{"text":"{\"a\": [true, false, null], \"a[\": {\"b\": \"\\n\\t\\\\\"\\/\"}}","decode_error":"Expecting ',' delimiter: line 1 column 48 (char 47)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"[]","value":["list",[]]}, +{"text":"\u0000[]","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"\"\\ud800\\udc00+\\uuc00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 16 (char 15)"}, +{"text":"[\"A=1\", \"B=\\u,00e9\",\u00e9\"C=\\ud83d\\ude00\"","decode_error":"Invalid \\uXXXX escape: line 1 column 13 (char 12)"}, +{"text":" \t\r[1 2 ] ","decode_error":"Expecting ',' delimiter: line 1 column 8 (char 7)"}, +{"text":"[NaN,\u001f Infinity, -Infinity, 1e400, -0.0, E-5, 10, -3]","decode_error":"Expecting value: line 1 column 6 (char 5)"}, +{"text":"]","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"}+","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"\"\ufeff\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65279,65536,56320,55296,120]]}, +{"text":"\"\\udx800\\uc00\\udc00\\u800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 3 (char 2)"}, +{"text":"}","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":".\r","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"\"{\\ud800\\udc00eudc00\\ud800x\"","value":["code_points",[123,65536,101,117,100,99,48,48,55296,120]]}, +{"text":"[NaN, Infinity, -I finity, 1e400, -0.0, 1E-5, 10, -3]","decode_error":"Expecting value: line 1 column 17 (char 16)"}, +{"text":"{\"forat\": 1, \"artifact\": \"aerofs\"\n, \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","forat"],["int","1"]],[["str","artifact"],["str","aerofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"\t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"[NaN, Infinity, -Infinity\n 1e400, -0.0, 1E-5, 10,-3]","decode_error":"Expecting ',' delimiter: line 2 column 2 (char 27)"}, +{"text":"[\"A[1\", \"B=\\u001e9\", \"C=\\d83d\\ude00\"]","decode_error":"Invalid \\escape: line 1 column 25 (char 24)"}, +{"text":" \t\r\n[1\u00e9, 2 ] ","decode_error":"Expecting ',' delimiter: line 2 column 3 (char 6)"}, +{"text":"[:x]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erof\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erof"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"}\\]","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"{\"a\": [true, false, null], \"a}\": {\"bInfinity: \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting ':' delimiter: line 1 column 48 (char 47)"}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"[NaN, Infinity, -Inf\ninity, 1e400, -0.0, 1E-5, ]10, -3]","decode_error":"Expecting value: line 1 column 17 (char 16)"}, +{"text":"{}","value":["dict",[]]}, +{"text":"\"\\ud800\\udc00\\udtrue00\\ud80d8000x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 15 (char 14)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"\"\\ud800\\u}dc00 udc00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 9 (char 8)"}, +{"text":"{\"a: [true, false, null], \"a\": {\"b\": \"\\\\t\\\\\\\"\\/\ufeff}}","decode_error":"Expecting ':' delimiter: line 1 column 28 (char 27)"}, +{"text":"[NaN, Infinity, -Infinity,1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":" \t\r\n[1}, 2 ] ","decode_error":"Expecting ',' delimiter: line 2 column 3 (char 6)"}, +{"text":"\t\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":" \r\n[1 ,] 2 ] ","decode_error":"Illegal trailing comma before end of array: line 2 column 4 (char 6)"}, +{"text":"[],NaN","decode_error":"Extra data: line 1 column 3 (char 2)"}, +{"text":" \t\r\n[1 \u0000, 2 [] :","decode_error":"Expecting ',' delimiter: line 2 column 4 (char 7)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"\"[]","decode_error":"Unterminated string starting at: line 1 column 1 (char 0)"}, +{"text":"{\"format\": \"1,0\"artifact\": \"a.erofs\", \"packages\": [{\"na+e\": \"systemd\"}]}","decode_error":"Expecting ',' delimiter: line 1 column 17 (char 16)"}, +{"text":"[NaN, Infinity, -[nfinity, 1e40u, -0.0, 1E-5, 10, -3]","decode_error":"Expecting value: line 1 column 17 (char 16)"}, +{"text":"[\"A=1\",\"B=\\u00e9\", \"Cud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["code_points",[67,117,100,56,51,100,56832]]]]}, +{"text":" \t\r\n[1 , 2 ]","value":["list",[["int","1"],["int","2"]]]}, +{"text":"\n\t\r\n[1 true 2 ] ","decode_error":"Expecting ',' delimiter: line 3 column 4 (char 7)"}, +{"text":"[\"A=1\", \"B[\\u00e9\", \"C=\\d83d\\ue00\"]","decode_error":"Invalid \\escape: line 1 column 24 (char 23)"}, +{"text":"{\"a\": [true, false,Infinitynul{], \"a\": \"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting ',' delimiter: line 1 column 28 (char 27)"}, +{"text":"[NaN, Infinity, -Infinity\t, 1e4\"0, -0.0, 1E-5, 10, -3]","decode_error":"Expecting ',' delimiter: line 1 column 32 (char 31)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"{\"format\": 1, \"artifact\": \"a.eros\", \"packas\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.eros"]],[["str","packas"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"1 \t\r\n[1null ], 2 ] ","decode_error":"Extra data: line 2 column 1 (char 5)"}, +{"text":"[]\r","value":["list",[]]}, +{"text":"\"\\ud800\\udc00\\udc00\\ud8NaN00\"","decode_error":"Invalid \\uXXXX escape: line 1 column 21 (char 20)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\udu00\"]","decode_error":"Invalid \\uXXXX escape: line 1 column 31 (char 30)"}, +{"text":" \t\r\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"\u00e9[]","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":" \t\r\n1 , NaN] ","decode_error":"Extra data: line 2 column 3 (char 6)"}, +{"text":"{\"a\": [trd800ue, false, null], \"\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 8 (char 7)"}, +{"text":" \t\r\n[\u00e9 , 2 ] ","decode_error":"Expecting value: line 2 column 2 (char 5)"}, +{"text":"{NaNa\": [true, false, null], \"a\": {\"b\": \\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"{}","value":["dict",[]]}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}}","decode_error":"Expecting ',' delimiter: line 1 column 70 (char 69)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud8true\\ude00\"]","decode_error":"Invalid \\uXXXX escape: line 1 column 25 (char 24)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\1de00\"]","decode_error":"Invalid \\escape: line 1 column 30 (char 29)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\,\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\,\"/"]]]]]]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"{\"a\" [t]rue, false[, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting ':' delimiter: line 1 column 7 (char 6)"}, +{"text":" \t\r\n[1 , 2Infinity ] ","decode_error":"Expecting ',' delimiter: line 2 column 7 (char 10)"}, +{"text":"[]","value":["list",[]]}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"{\"forma{t\": 1, \"artifact\" \"a.erofs\", \"packag.es\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting ':' delimiter: line 1 column 27 (char 26)"}, +{"text":" \t\r\n[1,2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"[\"A\u001f1\", \"B=\\u00e9\", \"C=\\ud83d.ude00\"]","decode_error":"Invalid control character at: line 1 column 4 (char 3)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"puackages\": [{\"name\"[ \"systemd\"}]}","decode_error":"Expecting ':' delimiter: line 1 column 59 (char 58)"}, +{"text":"-\t\r\n[1 , 2 ] ","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"{","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"{\"a\": [true, false,null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"{\"format\" 1, \"artifact\": \"a.er.ofs\", \"packages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting ':' delimiter: line 1 column 11 (char 10)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"{\"a\": [true, alse, null], \"a\": 0{\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 14 (char 13)"}, +{"text":"{\"for\"mat\": 1, \"artifact\": \"a.erofs\", \u0000\"packages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting ':' delimiter: line 1 column 7 (char 6)"}, +{"text":"\"A=1\", \"B=\\u00e9\", \"C=\ufeff\\ud83d\\ude00\"]","decode_error":"Extra data: line 1 column 6 (char 5)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud8\rx\"","decode_error":"Invalid \\uXXXX escape: line 1 column 21 (char 20)"}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"[\"A=1\", \"B=\\\ufeffu00e9\", \"C=\\u{d83d\\ude00\"]","decode_error":"Invalid \\escape: line 1 column 12 (char 11)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"[\"A=null\"\", \"B=\\u00e9\", \"C=ud83d\\ude00\"]","decode_error":"Expecting ',' delimiter: line 1 column 10 (char 9)"}, +{"text":"[NaN, Infinity, -Infinit, 1e400, -0.0, 1E5, 10, -3]","decode_error":"Expecting value: line 1 column 17 (char 16)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83dnull\\udex0\"]","decode_error":"Invalid \\uXXXX escape: line 1 column 35 (char 34)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"{format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"{}","value":["dict",[]]}, +{"text":"]","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, d8000, -3]","decode_error":"Expecting value: line 1 column 47 (char 46)"}, +{"text":"[[","decode_error":"Expecting value: line 1 column 3 (char 2)"}, +{"text":"{} ","value":["dict",[]]}, +{"text":"{\"a\": [,rue, flse, null], \"a\": {\"b\": \"\\n\\t\t\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 8 (char 7)"}, +{"text":"Infinity}","decode_error":"Extra data: line 1 column 9 (char 8)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"[\"A1\"], \"B=\\u00e9\", \"C=\\ud83d\\ude00 ]","decode_error":"Extra data: line 1 column 7 (char 6)"}, +{"text":" \t\t1x\n[1 , 2 ] ","decode_error":"Extra data: line 1 column 5 (char 4)"}, +{"text":"{\"]Infinityformat\"::1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting value: line 1 column 20 (char 19)"}, +{"text":"\"}","decode_error":"Unterminated string starting at: line 1 column 1 (char 0)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"e=\\ud83d\"ude00\"]","decode_error":"Expecting ',' delimiter: line 1 column 31 (char 30)"}, +{"text":"} \t\r\n,1 2 ] ","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[]","value":["list",[]]}, +{"text":"NaN[]","decode_error":"Extra data: line 1 column 4 (char 3)"}, +{"text":"{nulla\": [true, false, null], \"a\": {\"b : \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":" NaN\n[1 , 2 ]","decode_error":"Extra data: line 2 column 1 (char 5)"}, +{"text":" \t\r\n[1 , NaN [ ","decode_error":"Expecting ',' delimiter: line 2 column 10 (char 13)"}, +{"text":"[\"Au=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","Au=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"\"a\": [true, false, null], \"e\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Extra data: line 1 column 4 (char 3)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"NaN\r\\n\\t\\\\+\"\\/\"}}","decode_error":"Invalid control character at: line 1 column 43 (char 42)"}, +{"text":",]]","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"\"\\ud800udc00\\udc00\\ud800xe\"","value":["code_points",[55296,117,100,99,48,48,56320,55296,120,101]]}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"[.NaN, Infinity, -Infinity, ,e400, -0.0, 1E-5, 10, -3]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":" d800\r\n[1 ,\"2 ] ","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, 0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","0000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", e\"C=\\ud83d\\ude00\"]","decode_error":"Expecting value: line 1 column 21 (char 20)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \u0000\"C=\\ud83d\\unull\ufeff00\"]","decode_error":"Expecting value: line 1 column 21 (char 20)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":" \t\n[1NaN, 2 ] ","decode_error":"Expecting ',' delimiter: line 2 column 3 (char 5)"}, +{"text":"{\"format\": 1, \"atifact\": \"x.erofs\", \"packages\": [{\"nuame\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","atifact"],["str","x.erofs"]],[["str","packages"],["list",[["dict",[[["str","nuame"],["str","systemd"]]]]]]]]]}, +{"text":"{\"a\": [tr\"ue, f\\alse, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 8 (char 7)"}, +{"text":"[\"A=1\", \"B=\\NaNu00e\", \"\tC=\\ud83d\\ude00\"]","decode_error":"Invalid \\escape: line 1 column 12 (char 11)"}, +{"text":"{true}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"\"\\ud800\\udc00\\unulldc00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 15 (char 14)"}, +{"text":"[NaN","decode_error":"Expecting ',' delimiter: line 1 column 5 (char 4)"}, +{"text":"{\\","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"[\"A=1\", B=\\u00e9\", \"C=\\uud83d\\ude00\"]","decode_error":"Expecting value: line 1 column 9 (char 8)"}, +{"text":"\"\\ud800\\udc0\\udc00\\u800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 9 (char 8)"}, +{"text":"[NaN, Infinity, -Infinit\u0000, 1e4\ufeff00, -0.0, 1E-5, 10, -3]","decode_error":"Expecting value: line 1 column 17 (char 16)"}, +{"text":"\t\r\n[1\\ , 2 ]- ","decode_error":"Expecting ',' delimiter: line 2 column 3 (char 5)"}, +{"text":"{,\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"{},","decode_error":"Extra data: line 1 column 3 (char 2)"}, +{"text":"{\"a\": [true, false, nul], \"a\": {\"b\\: \"\\n\t\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 21 (char 20)"}, +{"text":"","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"-}\\ud800\\u c00\\udc00\\ud800x\"","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"[]","value":["list",[]]}, +{"text":"[A=1\", \"B=\\u0[e9\", \"C=\\ud83d\\ude00\"]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"{\"a\": [true, fals\ufeffe,null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 14 (char 13)"}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":" {\r\n[1 ,2 ] ","decode_error":"Expecting property name enclosed in double quotes: line 2 column 1 (char 4)"}, +{"text":"{\\}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":" \t\r\n[1 , 2 ] ","value":["list",[["int","1"],["int","2"]]]}, +{"text":"[\"A+=1\", \"B=\\u0\u00e9e9\", \"C=\\ud83d\\ude00\"]","decode_error":"Invalid \\uXXXX escape: line 1 column 14 (char 13)"}, +{"text":"]]\n","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"{\"a\": [\rrue, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 9 (char 8)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\-\"\\/\"}}","decode_error":"Expecting ',' delimiter: line 1 column 48 (char 47)"}, +{"text":"\"\\ud800\\u\u0000dc00\\udc00\\ud80,0x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 9 (char 8)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"{eNaN","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"[\"A=1\", \"=\\u00e9\", \"C=\\ud83true\\ude00\"]","decode_error":"Invalid \\uXXXX escape: line 1 column 24 (char 23)"}, +{"text":"[ \t\n[1 2 ] ","decode_error":"Expecting ',' delimiter: line 2 column 5 (char 8)"}, +{"text":"[]","value":["list",[]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"{\"a\": truetr:ue, false, null], \"a\"null {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting ',' delimiter: line 1 column 11 (char 10)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00{]","decode_error":"Unterminated string starting at: line 1 column 21 (char 20)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400,x -0.0, 1E-5, 10,-3]","decode_error":"Expecting value: line 1 column 34 (char 33)"}, +{"text":"+]","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"\"\\ud800d800\\udc00\\udc00\\ud800x\"","value":["code_points",[55296,100,56,48,48,56320,56320,55296,120]]}, +{"text":"\"\\ud800\\dc00\\udc0\\ud800x\"","decode_error":"Invalid \\escape: line 1 column 8 (char 7)"}, +{"text":"","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[]","value":["list",[]]}, +{"text":"\u0000","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[\"A=1\",\r\"B=\\u00e9\", \"C=\\ud83d\\ud\u000000\"]","decode_error":"Invalid \\uXXXX escape: line 1 column 31 (char 30)"}, +{"text":"\"\\d800\\udc00\\udc00\\ud8e00x","decode_error":"Invalid \\escape: line 1 column 2 (char 1)"}, +{"text":"[\"A=1\", \"d800B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","d800B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"[\"A=1\"Infinity, \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","decode_error":"Expecting ',' delimiter: line 1 column 7 (char 6)"}, +{"text":"[]\u00e9","decode_error":"Extra data: line 1 column 3 (char 2)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"{}","value":["dict",[]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1 -5:, 10, -3]","decode_error":"Expecting ',' delimiter: line 1 column 43 (char 42)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"{\"a\": true, false, n0ll], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 13 (char 12)"}, +{"text":"{\"\normat\": 1, \"arttrueifact\": \"a.erofs\", \"packages\": [{\"name\": \"systmd\"}]}","decode_error":"Invalid control character at: line 1 column 3 (char 2)"}, +{"text":"[]","value":["list",[]]}, +{"text":"\"\\ud800\\+udc00\\udc\t0\\ud800x\"","decode_error":"Invalid \\escape: line 1 column 8 (char 7)"}, +{"text":" \t\r\n1 , ]\ufeff","decode_error":"Extra data: line 2 column 3 (char 6)"}, +{"text":"\"\\ud800\\u+dc00\\udc00\\ud00xnull\"","decode_error":"Invalid \\uXXXX escape: line 1 column 9 (char 8)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"{\"a\": [true, false, null], \"a\"+:{\"b\"NaN \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting ':' delimiter: line 1 column 31 (char 30)"}, +{"text":"\"\\ud800\\udc00\\udc00\\u800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 21 (char 20)"}, +{"text":"[NaN, Infinity, -Infinity, 1e4\u001f00, -0.0, 1E-5, 10, -3]","decode_error":"Expecting ',' delimiter: line 1 column 31 (char 30)"}, +{"text":"\"\\ud800\\udc00\\udc0u0\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 15 (char 14)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erof\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erof"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"\"\\ud800\\udc00\\udc00\\u800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 21 (char 20)"}, +{"text":"{\"a\": [true, d800false, null], \"a\": {\"b\": \"\\nt:\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 14 (char 13)"}, +{"text":"","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[.e","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"[]","value":["list",[]]}, +{"text":"[\"A=1u\", \"B=\\u00e9\",\"C=\\ud83dInfinity\\ude00\"]","value":["list",[["str","A=1u"],["str","B=\u00e9"],["code_points",[67,61,55357,73,110,102,105,110,105,116,121,56832]]]]}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str","\n\t\\\"/"]]]]]]]}, +{"text":"{}","value":["dict",[]]}, +{"text":"{\"format\"0 1, \"artifact\": \"a.erofs\", \"packages\"\t [{\"name\": \"systemd\"}]}","decode_error":"Expecting ':' delimiter: line 1 column 10 (char 9)"}, +{"text":"{\n","decode_error":"Expecting property name enclosed in double quotes: line 2 column 1 (char 2)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3:","decode_error":"Expecting ',' delimiter: line 1 column 53 (char 52)"}, +{"text":"[NaN, Infinity, Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","7ff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud8-d\\ude00\"]","decode_error":"Invalid \\uXXXX escape: line 1 column 25 (char 24)"}, +{"text":"]\\1","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"]","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\",\u001f\"packages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting property name enclosed in double quotes: line 1 column 37 (char 36)"}, +{"text":"[","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"{\"a\": [true,, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 13 (char 12)"}, +{"text":"\"\\ud80\\udc00\\uInfinityc00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 3 (char 2)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"{e","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"{\"a\": [true, fa.l0se,null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 14 (char 13)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"\"\\ud800\\udc00\\udc00null\\ud800x\"","value":["code_points",[65536,56320,110,117,108,108,55296,120]]}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs1, \"packages\": [{\"name\": \"systemd\"}]}\u001f","decode_error":"Expecting ',' delimiter: line 1 column 39 (char 38)"}, +{"text":"Infinity][,]","decode_error":"Extra data: line 1 column 9 (char 8)"}, +{"text":"[]","value":["list",[]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"[]","value":["list",[]]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":" \t\r\n[1[ true 2 ] ","decode_error":"Expecting ',' delimiter: line 2 column 3 (char 6)"}, +{"text":"\u0000}","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[NaN, Infinity, -InfNaNnity, 1e400, -0.0 1E-5,Infinity10, -3]","decode_error":"Expecting value: line 1 column 17 (char 16)"}, +{"text":"[]","value":["list",[]]}, +{"text":"\"\\ud8}\\udc00\\udc00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 3 (char 2)"}, +{"text":"[NaN, Infinit, -Infinity, 1e400, -0.0, 1E-5, 1, -3]","decode_error":"Expecting value: line 1 column 7 (char 6)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=\u00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"{}","value":["dict",[]]}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\true\r\\\\\\\"\\/\"}}","decode_error":"Invalid control character at: line 1 column 47 (char 46)"}, +{"text":" true\t\r\n[1\t , 2 ] ","decode_error":"Extra data: line 2 column 1 (char 8)"}, +{"text":"{}","value":["dict",[]]}, +{"text":"[Na, Infinity, -Infinity, 1e400, -0.0, 1E-5, 010, -3]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":" \"\r\n[1 . 2 ] ","decode_error":"Invalid control character at: line 1 column 3 (char 2)"}, +{"text":"\n}","decode_error":"Expecting value: line 2 column 1 (char 1)"}, +{"text":"[NaN, Infinity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","value":["list",[["float","7ff8000000000000"],["float","7ff0000000000000"],["float","fff0000000000000"],["float","7ff0000000000000"],["float","8000000000000000"],["float","3ee4f8b588e368f1"],["int","10"],["int","-3"]]]}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\\t/\"}}","decode_error":"Invalid \\escape: line 1 column 48 (char 47)"}, +{"text":"{}","value":["dict",[]]}, +{"text":"[NaN Infinity, -Infinity, 1e0400+ -0.0, 1E-5, 10, -3]","decode_error":"Expecting ',' delimiter: line 1 column 6 (char 5)"}, +{"text":"{\"a\": +true, false, +ull], \"a\": {\"b\":\"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 7 (char 6)"}, +{"text":"","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \\C=\\ud83d\\ude0[0\"]","decode_error":"Expecting value: line 1 column 21 (char 20)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"[NauN, In{inity, -Infinity, 1e400, -0.0, 1E-5, 10, -3]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"\"0\\ud800\\udc0\t\\udc00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 10 (char 9)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"package+\": [{\"name\": \"systemd}]}","decode_error":"Unterminated string starting at: line 1 column 60 (char 59)"}, +{"text":"{}}","decode_error":"Extra data: line 1 column 3 (char 2)"}, +{"text":"{}","value":["dict",[]]}, +{"text":"[","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"{\"format\": 1null, \"artifact\": \"a.erofs\", \"pd800ack\ufeffages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting ',' delimiter: line 1 column 13 (char 12)"}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \" n\\t\\\\\\\"\\/\"}}","value":["dict",[[["str","a"],["dict",[[["str","b"],["str"," n\t\\\"/"]]]]]]]}, +{"text":"{\"a\": [true, false, null], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\t\"}}","decode_error":"Invalid control character at: line 1 column 50 (char 49)"}, +{"text":"true]","decode_error":"Extra data: line 1 column 5 (char 4)"}, +{"text":"\"\\ud8:0\\udc00\\xdc00\\ud800x\"","decode_error":"Invalid \\uXXXX escape: line 1 column 3 (char 2)"}, +{"text":"\r]","decode_error":"Expecting value: line 1 column 2 (char 1)"}, +{"text":"}","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"\"\\ud800\\ud00\\udc00\\ud80truex\"","decode_error":"Invalid \\uXXXX escape: line 1 column 9 (char 8)"}, +{"text":"{\"format\": 1 \"artifact\": \"a.erofs\",\"\"packages\": [{\\\"name\": \"systemd\"}]}","decode_error":"Expecting ',' delimiter: line 1 column 14 (char 13)"}, +{"text":"\"\\ud800\\udc00\\udc00\\ud800x\"","value":["code_points",[65536,56320,55296,120]]}, +{"text":"[\"A=1\", \"B=u[00e9\", \"C=\\ud83d\\ude00\"]","value":["list",[["str","A=1"],["str","B=u[00e9"],["str","C=\ud83d\ude00"]]]}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"ptrueckages: 0[{\"name\": \"systemd\"}]}","decode_error":"Expecting ':' delimiter: line 1 column 56 (char 55)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.\u00e9rofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.\u00e9rofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"{}","value":["dict",[]]}, +{"text":"{.}e","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":" \r\r\n[11 , 2 ]\r","value":["list",[["int","11"],["int","2"]]]}, +{"text":"[\"A=1\",]\"B=\\u00e9\", \"C=\\u\"83,d\\ude00\"]","decode_error":"Illegal trailing comma before end of array: line 1 column 7 (char 6)"}, +{"text":"{","decode_error":"Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"text":"[]","value":["list",[]]}, +{"text":"{\nforNaNat\":. 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","decode_error":"Expecting property name enclosed in double quotes: line 2 column 1 (char 2)"}, +{"text":"{\"A=1\", \"B=\\u00e9\"e \"nullC=\\ud83d\\ude00\"]","decode_error":"Expecting ':' delimiter: line 1 column 7 (char 6)"}, +{"text":"[\taN, InfiniInfinityy, -Infinity, 1e400, -0.0, 1E-5, 10 -3]","decode_error":"Expecting value: line 1 column 3 (char 2)"}, +{"text":"{\"format\": 1, \"artifact\":\"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"]","decode_error":"Expecting value: line 1 column 1 (char 0)"}, +{"text":"{\"a\": [tr.e, false, nu[l], \"a\": {\"b\": \"\\n\\t\\\\\\\"\\/\"}}","decode_error":"Expecting value: line 1 column 8 (char 7)"}, +{"text":" \t\r\n[1]. 2 ] ","decode_error":"Extra data: line 2 column 4 (char 7)"}, +{"text":"{\"format\": 1, \"artifact\": \"a.erofs\", \"packages\": [{\"name\": \"systemd\"}]}","value":["dict",[[["str","format"],["int","1"]],[["str","artifact"],["str","a.erofs"]],[["str","packages"],["list",[["dict",[[["str","name"],["str","systemd"]]]]]]]]]}, +{"text":"{\"","decode_error":"Unterminated string starting at: line 1 column 2 (char 1)"} +], +"utf8": [ +{"bytes":"6f6b","error":null}, +{"bytes":"ff","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"61e282","error":"'utf-8' codec can't decode bytes in position 1-2: unexpected end of data"}, +{"bytes":"e228","error":"'utf-8' codec can't decode byte 0xe2 in position 0: invalid continuation byte"}, +{"bytes":"f09028","error":"'utf-8' codec can't decode bytes in position 0-1: invalid continuation byte"}, +{"bytes":"eda080","error":"'utf-8' codec can't decode byte 0xed in position 0: invalid continuation byte"}, +{"bytes":"c080","error":"'utf-8' codec can't decode byte 0xc0 in position 0: invalid start byte"}, +{"bytes":"f4908080","error":"'utf-8' codec can't decode byte 0xf4 in position 0: invalid continuation byte"}, +{"bytes":"e08080","error":"'utf-8' codec can't decode byte 0xe0 in position 0: invalid continuation byte"}, +{"bytes":"f0808080","error":"'utf-8' codec can't decode byte 0xf0 in position 0: invalid continuation byte"}, +{"bytes":"f09f98","error":"'utf-8' codec can't decode bytes in position 0-2: unexpected end of data"}, +{"bytes":"8061f09f","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"c3a9c3e282ac6161","error":"'utf-8' codec can't decode byte 0xc3 in position 2: invalid continuation byte"}, +{"bytes":"ff61c3a980e282ac","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"80","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"c3a9","error":null}, +{"bytes":"","error":null}, +{"bytes":"6180e261","error":"'utf-8' codec can't decode byte 0x80 in position 1: invalid start byte"}, +{"bytes":"f09f988061e282acff","error":"'utf-8' codec can't decode byte 0xff in position 8: invalid start byte"}, +{"bytes":"80e282ace282acffff","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"f09f9880e282ac80","error":"'utf-8' codec can't decode byte 0x80 in position 7: invalid start byte"}, +{"bytes":"","error":null}, +{"bytes":"616161","error":null}, +{"bytes":"e282ace282acf09f9880e282ace282acc3a9","error":null}, +{"bytes":"e282ac9f9880e282acff","error":"'utf-8' codec can't decode byte 0x9f in position 3: invalid start byte"}, +{"bytes":"","error":null}, +{"bytes":"8080c3a9ffc3a961","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"c3a961","error":null}, +{"bytes":"e282ac","error":null}, +{"bytes":"","error":null}, +{"bytes":"","error":null}, +{"bytes":"e282","error":"'utf-8' codec can't decode bytes in position 0-1: unexpected end of data"}, +{"bytes":"c3a9e282ac","error":null}, +{"bytes":"c361e282ac","error":"'utf-8' codec can't decode byte 0xc3 in position 0: invalid continuation byte"}, +{"bytes":"c3a980f09f9880","error":"'utf-8' codec can't decode byte 0x80 in position 2: invalid start byte"}, +{"bytes":"e282acf09f988080e2","error":"'utf-8' codec can't decode byte 0x80 in position 7: invalid start byte"}, +{"bytes":"e282ace2acff80c3a9","error":"'utf-8' codec can't decode bytes in position 3-4: invalid continuation byte"}, +{"bytes":"","error":null}, +{"bytes":"","error":null}, +{"bytes":"e282ac","error":null}, +{"bytes":"80c3a9c3a9","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"f09f9880c3a9e282ac61ff","error":"'utf-8' codec can't decode byte 0xff in position 10: invalid start byte"}, +{"bytes":"e282acf09f","error":"'utf-8' codec can't decode bytes in position 3-4: unexpected end of data"}, +{"bytes":"ff6161ff","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"f09f9880e282e282acc3a9","error":"'utf-8' codec can't decode bytes in position 4-5: invalid continuation byte"}, +{"bytes":"e282acfff09f9880ff","error":"'utf-8' codec can't decode byte 0xff in position 3: invalid start byte"}, +{"bytes":"ffffc3a9f09f9880c3a9f09f9880","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"c3a9e282ac61e2e282ac","error":"'utf-8' codec can't decode byte 0xe2 in position 6: invalid continuation byte"}, +{"bytes":"8061e282ac","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"c3a9","error":null}, +{"bytes":"ffff80","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"80c3a9","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"61ff","error":"'utf-8' codec can't decode byte 0xff in position 1: invalid start byte"}, +{"bytes":"c3a9","error":null}, +{"bytes":"61ffff","error":"'utf-8' codec can't decode byte 0xff in position 1: invalid start byte"}, +{"bytes":"","error":null}, +{"bytes":"c3a961ff","error":"'utf-8' codec can't decode byte 0xff in position 3: invalid start byte"}, +{"bytes":"61806180","error":"'utf-8' codec can't decode byte 0x80 in position 1: invalid start byte"}, +{"bytes":"acf09f9880e282ac6180","error":"'utf-8' codec can't decode byte 0xac in position 0: invalid start byte"}, +{"bytes":"e282acf09f9880","error":null}, +{"bytes":"8061","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"c3a9f09f9880e282ac","error":null}, +{"bytes":"61e282ac","error":null}, +{"bytes":"e282c3a9f09f9880c3a9e282acc3a9","error":"'utf-8' codec can't decode bytes in position 0-1: invalid continuation byte"}, +{"bytes":"80f09f9880e2ac61c3a9","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"f09f9880e282acc3a9e282ac61","error":null}, +{"bytes":"61ac80f09f9880","error":"'utf-8' codec can't decode byte 0xac in position 1: invalid start byte"}, +{"bytes":"618061a9","error":"'utf-8' codec can't decode byte 0x80 in position 1: invalid start byte"}, +{"bytes":"c3a9","error":null}, +{"bytes":"","error":null}, +{"bytes":"ffe282ac","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"","error":null}, +{"bytes":"80ff80","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"f09f9880","error":null}, +{"bytes":"c3a980c3a9c3a9","error":"'utf-8' codec can't decode byte 0x80 in position 2: invalid start byte"}, +{"bytes":"ffa980","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"c3a9","error":null}, +{"bytes":"e282ac","error":null}, +{"bytes":"80ff80ff","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"e282acf09f9880e282ac","error":null}, +{"bytes":"6161","error":null}, +{"bytes":"e282ace282acc3a9c3a9c3a9","error":null}, +{"bytes":"82ac8061e282ac","error":"'utf-8' codec can't decode byte 0x82 in position 0: invalid start byte"}, +{"bytes":"f09880e282ac","error":"'utf-8' codec can't decode bytes in position 0-2: invalid continuation byte"}, +{"bytes":"f09f9880f09f988061618061","error":"'utf-8' codec can't decode byte 0x80 in position 10: invalid start byte"}, +{"bytes":"619880f09f9880","error":"'utf-8' codec can't decode byte 0x98 in position 1: invalid start byte"}, +{"bytes":"e282acc3a9f09f9880fff09880","error":"'utf-8' codec can't decode byte 0xff in position 9: invalid start byte"}, +{"bytes":"61e282ac809880c3a9f09f9880","error":"'utf-8' codec can't decode byte 0x80 in position 4: invalid start byte"}, +{"bytes":"fff09f988061f09f9880f09f98","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"c3a9","error":null}, +{"bytes":"9f9880e282ac","error":"'utf-8' codec can't decode byte 0x9f in position 0: invalid start byte"}, +{"bytes":"","error":null}, +{"bytes":"c3a9fff09f9880c3a9c3a9f09f9880","error":"'utf-8' codec can't decode byte 0xff in position 2: invalid start byte"}, +{"bytes":"c3a98061","error":"'utf-8' codec can't decode byte 0x80 in position 2: invalid start byte"}, +{"bytes":"ff80f080","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"c3a9","error":null}, +{"bytes":"","error":null}, +{"bytes":"ff6180ffff","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"c3a9","error":null}, +{"bytes":"8061f09f9880618080","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"c3a961c3a961","error":null}, +{"bytes":"61f09f988080c3a9","error":"'utf-8' codec can't decode byte 0x80 in position 5: invalid start byte"}, +{"bytes":"ff","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"e282ac80c3a9c3a9","error":"'utf-8' codec can't decode byte 0x80 in position 3: invalid start byte"}, +{"bytes":"f0988061","error":"'utf-8' codec can't decode bytes in position 0-2: invalid continuation byte"}, +{"bytes":"ff8061f09f9880c3a9a9","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"c3a96180fff09f98","error":"'utf-8' codec can't decode byte 0x80 in position 3: invalid start byte"}, +{"bytes":"f09f9880f09f9880f080c3a9c3a9","error":"'utf-8' codec can't decode byte 0xf0 in position 8: invalid continuation byte"}, +{"bytes":"82acc3a9ff","error":"'utf-8' codec can't decode byte 0x82 in position 0: invalid start byte"}, +{"bytes":"ff61ace282ac","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"e282ace282ac98808061e282ac","error":"'utf-8' codec can't decode byte 0x98 in position 6: invalid start byte"}, +{"bytes":"c3a980e2ace282ac","error":"'utf-8' codec can't decode byte 0x80 in position 2: invalid start byte"}, +{"bytes":"6180","error":"'utf-8' codec can't decode byte 0x80 in position 1: invalid start byte"}, +{"bytes":"ffc3a980e282ac80c3a9","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"80c3a9e282acf09f9880f09f988061","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"61f09f98808061ff","error":"'utf-8' codec can't decode byte 0x80 in position 5: invalid start byte"}, +{"bytes":"e2acf09f9880","error":"'utf-8' codec can't decode bytes in position 0-1: invalid continuation byte"}, +{"bytes":"ff","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"80","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"82ac","error":"'utf-8' codec can't decode byte 0x82 in position 0: invalid start byte"}, +{"bytes":"ff6161f09f9880c3a9","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"e282acfff09f9880","error":"'utf-8' codec can't decode byte 0xff in position 3: invalid start byte"}, +{"bytes":"ffc3a980","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"80c3a9","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"f0988080ff","error":"'utf-8' codec can't decode byte 0xff in position 4: invalid start byte"}, +{"bytes":"80c3a9c3a9","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"6182ac","error":"'utf-8' codec can't decode byte 0x82 in position 1: invalid start byte"}, +{"bytes":"","error":null}, +{"bytes":"61c3a9fff09f9880ff","error":"'utf-8' codec can't decode byte 0xff in position 3: invalid start byte"}, +{"bytes":"8080","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"616180","error":"'utf-8' codec can't decode byte 0x80 in position 2: invalid start byte"}, +{"bytes":"","error":null}, +{"bytes":"e282ac61","error":null}, +{"bytes":"","error":null}, +{"bytes":"e282ac8080","error":"'utf-8' codec can't decode byte 0x80 in position 3: invalid start byte"}, +{"bytes":"f09f988080ffe282acc3ff","error":"'utf-8' codec can't decode byte 0x80 in position 4: invalid start byte"}, +{"bytes":"80fff09f98","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"61f09f988061","error":null}, +{"bytes":"e282acc3a9e282ac80ffc3","error":"'utf-8' codec can't decode byte 0x80 in position 8: invalid start byte"}, +{"bytes":"fff09f988061","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"ff61","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"c3a9e282acc3a961fff09f9880","error":"'utf-8' codec can't decode byte 0xff in position 8: invalid start byte"}, +{"bytes":"","error":null}, +{"bytes":"f09f988061e282ac8061ff","error":"'utf-8' codec can't decode byte 0x80 in position 8: invalid start byte"}, +{"bytes":"c3a9e282ace2ac8061","error":null}, +{"bytes":"","error":null}, +{"bytes":"618080","error":"'utf-8' codec can't decode byte 0x80 in position 1: invalid start byte"}, +{"bytes":"c3a96161e2ac","error":"'utf-8' codec can't decode bytes in position 4-5: unexpected end of data"}, +{"bytes":"","error":null}, +{"bytes":"ffe282ace282e282ac6161","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"e282acc3a9f09f988061f09f9880e282ac","error":null}, +{"bytes":"f09f9880","error":null}, +{"bytes":"f09f9880e282acffc3a9","error":"'utf-8' codec can't decode byte 0xff in position 7: invalid start byte"}, +{"bytes":"61e282","error":"'utf-8' codec can't decode bytes in position 1-2: unexpected end of data"}, +{"bytes":"f09f98","error":"'utf-8' codec can't decode bytes in position 0-2: unexpected end of data"}, +{"bytes":"8061ff","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}, +{"bytes":"ffa9f09f9880f09f9880","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"ffc3a9f09f9880","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"ff","error":"'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"}, +{"bytes":"c3a9ff80f09f98","error":"'utf-8' codec can't decode byte 0xff in position 2: invalid start byte"}, +{"bytes":"80c3a9f09f9880","error":"'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"} +] +} diff --git a/nvx-rs/tests/fixtures/sandbox.json b/nvx-rs/tests/fixtures/sandbox.json new file mode 100644 index 000000000..5056226e4 --- /dev/null +++ b/nvx-rs/tests/fixtures/sandbox.json @@ -0,0 +1,1431 @@ +{ +"generator": "scripts/nvx_rs_fixtures.py, retired by microsoft/nvx#359", +"python": "3.14", +"cwds": {"posix": "/fixture/cwd", "windows": "C:\\fixture\\cwd"}, +"identities": [ +{"value":"1000:1001","identity":[1000,1001]}, +{"value":"0:0","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"1000","error":"--workload-user must be UID:GID"}, +{"value":"1:2:3","error":"--workload-user must be UID:GID"}, +{"value":"a:1","error":"--workload-user must contain numeric UID and GID"}, +{"value":"1:","error":"--workload-user must contain numeric UID and GID"}, +{"value":" +1_000 :\uff11\uff10","identity":[1000,10]}, +{"value":"4294967295:1","identity":[4294967295,1]}, +{"value":"4294967296:1","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"-1:1","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"1:0","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"1:0x10","error":"--workload-user must contain numeric UID and GID"}, +{"value":"11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111:1","error":"--workload-user must contain numeric UID and GID"}, +{"value":"_9:","error":"--workload-user must contain numeric UID and GID"}, +{"value":"9","error":"--workload-user must be UID:GID"}, +{"value":"5\u30005191x1+ ","error":"--workload-user must be UID:GID"}, +{"value":"\u3000\u0661","error":"--workload-user must be UID:GID"}, +{"value":"- _:5_+_9","error":"--workload-user must contain numeric UID and GID"}, +{"value":" 1:x9","error":"--workload-user must contain numeric UID and GID"}, +{"value":"-_1\u0661\u30005","error":"--workload-user must be UID:GID"}, +{"value":"\u30000++_5: 1","error":"--workload-user must contain numeric UID and GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"\u0661115+1","error":"--workload-user must be UID:GID"}, +{"value":"+91:-59 1: 1","error":"--workload-user must be UID:GID"}, +{"value":"_\u06610x\u0661\u3000 _ 9x9","error":"--workload-user must be UID:GID"}, +{"value":"15:0x-\u06611_0","error":"--workload-user must contain numeric UID and GID"}, +{"value":"+0\u06615\u300015_","error":"--workload-user must be UID:GID"}, +{"value":" \u06615_00+:","error":"--workload-user must contain numeric UID and GID"}, +{"value":"5:\u0661","identity":[5,1]}, +{"value":" x__--","error":"--workload-user must be UID:GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"1-\u06611_0:\u066199","error":"--workload-user must contain numeric UID and GID"}, +{"value":":1","error":"--workload-user must contain numeric UID and GID"}, +{"value":"\u0661+\u30001-_9_","error":"--workload-user must be UID:GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"_\u0661 :\u3000","error":"--workload-user must contain numeric UID and GID"}, +{"value":":1:-:\u30001_","error":"--workload-user must be UID:GID"}, +{"value":"5_","error":"--workload-user must be UID:GID"}, +{"value":"0:\u0661001x","error":"--workload-user must contain numeric UID and GID"}, +{"value":"0-9","error":"--workload-user must be UID:GID"}, +{"value":":5++1-1: 5__","error":"--workload-user must be UID:GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"5_x","error":"--workload-user must be UID:GID"}, +{"value":"5-_\u3000-\u0661","error":"--workload-user must be UID:GID"}, +{"value":" 0\u3000 ","error":"--workload-user must be UID:GID"}, +{"value":"_\u300011+\u0661 5 x \u3000","error":"--workload-user must be UID:GID"}, +{"value":"_\u0661","error":"--workload-user must be UID:GID"}, +{"value":"-x","error":"--workload-user must be UID:GID"}, +{"value":"\u0661\u300011 -5-","error":"--workload-user must be UID:GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"5","error":"--workload-user must be UID:GID"}, +{"value":" :5\u3000_\u06615 0x59","error":"--workload-user must contain numeric UID and GID"}, +{"value":"1","error":"--workload-user must be UID:GID"}, +{"value":"+-_90:+ x","error":"--workload-user must contain numeric UID and GID"}, +{"value":"_5-0\u3000:","error":"--workload-user must contain numeric UID and GID"}, +{"value":"++0x\u0661\u3000-_:\u0661","error":"--workload-user must contain numeric UID and GID"}, +{"value":"91:00-","error":"--workload-user must contain numeric UID and GID"}, +{"value":"\u3000 \u06619:+","error":"--workload-user must contain numeric UID and GID"}, +{"value":"0\u0661_","error":"--workload-user must be UID:GID"}, +{"value":"1x\u0661\u0661::","error":"--workload-user must be UID:GID"}, +{"value":"+_\u3000","error":"--workload-user must be UID:GID"}, +{"value":"__-\u0661- _-x_ ","error":"--workload-user must be UID:GID"}, +{"value":"\u0661_15\u0661:x_\u3000_5","error":"--workload-user must contain numeric UID and GID"}, +{"value":"x9_\u3000::","error":"--workload-user must be UID:GID"}, +{"value":" 99","error":"--workload-user must be UID:GID"}, +{"value":"x055:++","error":"--workload-user must contain numeric UID and GID"}, +{"value":"\u0661-+ ","error":"--workload-user must be UID:GID"}, +{"value":"111-\u0661+\u0661\u3000","error":"--workload-user must be UID:GID"}, +{"value":"00\u06611","error":"--workload-user must be UID:GID"}, +{"value":" _0\u3000_ \u3000-","error":"--workload-user must be UID:GID"}, +{"value":"xxx0_-\u06610\u3000+","error":"--workload-user must be UID:GID"}, +{"value":"\u06611x -5","error":"--workload-user must be UID:GID"}, +{"value":"0 \u30001-_5 5\u3000 ","error":"--workload-user must be UID:GID"}, +{"value":"5:5-x+_5","error":"--workload-user must contain numeric UID and GID"}, +{"value":"\u0661x","error":"--workload-user must be UID:GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"-5:+\u066109\u0661-","error":"--workload-user must contain numeric UID and GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"_0\u3000_5+\u0661\u0661","error":"--workload-user must be UID:GID"}, +{"value":":-1\u0661 \u0661 ","error":"--workload-user must contain numeric UID and GID"}, +{"value":"9-5","error":"--workload-user must be UID:GID"}, +{"value":"\u0661\u3000x9\u3000\u0661 0:x","error":"--workload-user must contain numeric UID and GID"}, +{"value":"\u0661++ ","error":"--workload-user must be UID:GID"}, +{"value":" 0+_:x1x_","error":"--workload-user must contain numeric UID and GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"\u3000\u06611\u06615 +9","error":"--workload-user must be UID:GID"}, +{"value":"1\u06610","error":"--workload-user must be UID:GID"}, +{"value":"1","error":"--workload-user must be UID:GID"}, +{"value":"-","error":"--workload-user must be UID:GID"}, +{"value":"\u0661-\u3000\u0661\u3000\u3000+_\u3000x","error":"--workload-user must be UID:GID"}, +{"value":" 1055x","error":"--workload-user must be UID:GID"}, +{"value":"::0_0 +::0 :","error":"--workload-user must be UID:GID"}, +{"value":"1- 9\u3000 +x-","error":"--workload-user must be UID:GID"}, +{"value":"99__:","error":"--workload-user must contain numeric UID and GID"}, +{"value":"++","error":"--workload-user must be UID:GID"}, +{"value":" 0+:1\u3000 xx","error":"--workload-user must contain numeric UID and GID"}, +{"value":"9\u30000\u0661:_","error":"--workload-user must contain numeric UID and GID"}, +{"value":"+5+ \u06611 ","error":"--workload-user must be UID:GID"}, +{"value":"-\u0661x:+\u0661- ","error":"--workload-user must contain numeric UID and GID"}, +{"value":"0\u06611 5+x++x9","error":"--workload-user must be UID:GID"}, +{"value":"__\u3000","error":"--workload-user must be UID:GID"}, +{"value":"99 ","error":"--workload-user must be UID:GID"}, +{"value":"","error":"--workload-user must be UID:GID"}, +{"value":"9_\u0661_\u0661 9_","error":"--workload-user must be UID:GID"}, +{"value":"\u3000151","error":"--workload-user must be UID:GID"}, +{"value":"90","error":"--workload-user must be UID:GID"}, +{"value":"\u30000 +x+0\u3000x_ ","error":"--workload-user must be UID:GID"}, +{"value":"_01 \u3000+\u06619","error":"--workload-user must be UID:GID"}, +{"value":"5-9","error":"--workload-user must be UID:GID"}, +{"value":"5-10+\u0661009 0+","error":"--workload-user must be UID:GID"}, +{"value":"1+\u3000:+5+","error":"--workload-user must contain numeric UID and GID"}, +{"value":"1+:__","error":"--workload-user must contain numeric UID and GID"}, +{"value":"\t67 :\u3000-65534\t","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\u3000-65534\u3000:\t-65534\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":" +40 :\t-073 ","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\u300006_5534:\t065534\u3000","identity":[65534,65534]}, +{"value":"65534 : -65534\u3000","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":" 65534\u3000: 6553_4\t","identity":[65534,65534]}, +{"value":"\u3000027044425_37\t: -172085870 ","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\t-\uff12\uff17\uff16\uff16\uff18\uff13\uff16\uff16\uff18\uff14\u3000: -1376434866","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":" \uff10\uff12\u3000:\u3000-3315681680\u3000","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\u3000-543827860\t:06_1\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":" +4118025408:\t+\uff16\uff15\uff15\uff13_\uff14\t","identity":[4118025408,65534]}, +{"value":"\t+3062_851463\u3000:\u300002357216467\u3000","identity":[3062851463,2357216467]}, +{"value":"\u30001_3\t:053_3716954\t","identity":[13,533716954]}, +{"value":"65534 :\t65534\t","identity":[65534,65534]}, +{"value":"\t6_7\u3000: 65534\t","identity":[67,65534]}, +{"value":" 831927946\u3000:\u3000\uff16\uff15\uff15\uff13\uff14\t","identity":[831927946,65534]}, +{"value":" 3720_501321\t:-72814282 ","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\t-\uff15\uff12 :\t362_7849867\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\u3000+59\u3000: 3399312181\t","identity":[59,3399312181]}, +{"value":" 65534:\t-73 ","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"065534\t: 94 ","identity":[65534,94]}, +{"value":"\u30001377235406\u3000: -\uff13\uff13\uff16\uff19\uff13\uff18\uff17\uff17\uff17\uff18 ","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":" +084:-672843280\u3000","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":" 65534\u3000:\u3000-9_0566714","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"+58\u3000: 04006110698","identity":[58,4006110698]}, +{"value":"65534\t:\t-99","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\t+9_6\t:+65534","identity":[96,65534]}, +{"value":"+3 :+26\u3000","identity":[3,26]}, +{"value":"6_5534\t:\t-65534","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\u300065534\u3000:\u3000-01652675379","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\t+4096015006\t:\u300065534 ","identity":[4096015006,65534]}, +{"value":"\t-1941165337 :\t-03586206692 ","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"+88:\uff19\uff10\uff14\uff12\uff14\uff16\uff17\uff16\uff19\t","identity":[88,904246769]}, +{"value":" 65534\u3000: +\uff16\uff15\uff15\uff13\uff14\t","identity":[65534,65534]}, +{"value":"017 :\t65534\u3000","identity":[17,65534]}, +{"value":"\t+24\u3000:2882191863 ","identity":[24,2882191863]}, +{"value":" -56:\t050\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\u3000+84 : +65534 ","identity":[84,65534]}, +{"value":"\t65534: +62\t","identity":[65534,62]}, +{"value":"\t9_5\u3000: 65534\u3000","identity":[95,65534]}, +{"value":"\uff13_\uff19\u3000: 1822914502\u3000","identity":[39,1822914502]}, +{"value":" 6_5534\u3000: 65534\t","identity":[65534,65534]}, +{"value":"\u3000-65534: 7\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\u300088:\t\uff18\uff17\uff16_\uff13\uff13\uff10\uff14\uff19\uff19 ","identity":[88,876330499]}, +{"value":"-65534\t:+2_1\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\u3000-088\t:\u3000078 ","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\u3000048\t:\u30001319632021\t","identity":[48,1319632021]}, +{"value":"\t-31:\t16\u3000","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\t64: +65534\u3000","identity":[64,65534]}, +{"value":"01182912588:\t9","identity":[1182912588,9]}, +{"value":"\t+26_7838323\t:\t065_534","identity":[267838323,65534]}, +{"value":"\t65534\u3000:\t-03064840839\t","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\u3000+65_534\u3000:\t-6_5534","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":" -2396053624\t: -228038441\u3000","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":" 42\u3000:\t65534\u3000","identity":[42,65534]}, +{"value":"065534\t: +065534","identity":[65534,65534]}, +{"value":"-4012149028\t:-123874810_0\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\t1287511420\t:1\t","identity":[1287511420,1]}, +{"value":"\u3000-\uff19\uff12 :-669413798\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\t25447_90229:\u30008_4\u3000","identity":[2544790229,84]}, +{"value":" -65534\t:\t65534\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\t-01654717838 :\t3767854402 ","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":" 29 : 28 ","identity":[29,28]}, +{"value":" +2004873741 :\u3000-056","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\t+\uff14\uff11 :\u300074\u3000","identity":[41,74]}, +{"value":"2012142218: 3746796062 ","identity":[2012142218,3746796062]}, +{"value":"\t4143267344\u3000: +065534 ","identity":[4143267344,65534]}, +{"value":" +5_31508052:\t079\t","identity":[531508052,79]}, +{"value":"065534:\u3000+65534","identity":[65534,65534]}, +{"value":"\u3000+065534\t:\t-65534 ","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\u3000-4207239492 :+194143544\u3000","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"+58: -331_481346","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"+8_8: 022\t","identity":[88,22]}, +{"value":"2955026462\u3000:\t-21021507_74 ","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\u300065534\u3000: -366775534\u3000","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"-18 :+46","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\uff10\uff12\uff16\uff13\uff12\uff10\uff12\uff10\uff19\uff14\uff14\t: 3714053507\t","identity":[2632020944,3714053507]}, +{"value":"\u30001310933740:02717772865 ","identity":[1310933740,2717772865]}, +{"value":"\u3000+65534\t: -5_9\t","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"065534\t:\t+046","identity":[65534,46]}, +{"value":" +60\u3000: 65534\t","identity":[60,65534]}, +{"value":"\t1065765648: -65534\u3000","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"-65534\t: 1861159687 ","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\u3000+69 :\u300065534","identity":[69,65534]}, +{"value":"+0735370997\t: +65534 ","identity":[735370997,65534]}, +{"value":"6_5534 :\t-6_4","error":"--workload-user GID must be between 1 and 4294967295"}, +{"value":"\t4 :\t+099","identity":[4,99]}, +{"value":"+\uff16_\uff15\uff15\uff13\uff14 :\t+19\t","identity":[65534,19]}, +{"value":"\u3000+21:\t+65534\t","identity":[21,65534]}, +{"value":"\t065_534:\t65534","identity":[65534,65534]}, +{"value":" -065534\t:\u3000+2272821736\u3000","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\u3000+22\u3000:\t2232960263\u3000","identity":[22,2232960263]}, +{"value":" -4_8\t:065534","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"-065534\t:\u3000-2499768029\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\u300003640810574 :\u300065534\t","identity":[3640810574,65534]}, +{"value":" +82\u3000:\t\uff16\uff15\uff15\uff13_\uff14\u3000","identity":[82,65534]}, +{"value":"\u3000-40\u3000:\t+65534\t","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"\t-71\t:\t51 ","error":"--workload-user UID must be between 1 and 4294967295"}, +{"value":"38\t: 72\t","identity":[38,72]}, +{"value":" -21_4345071\u3000:\u3000+65534\t","error":"--workload-user UID must be between 1 and 4294967295"} +], +"layers": [ +{"flavor":"posix","value":"distro,distro.erofs,11111111-1111-1111-1111-111111111111","role":"distro","path":"distro.erofs","uuid":"11111111-1111-1111-1111-111111111111"}, +{"flavor":"posix","value":"custom,layers//custom.erofs,{22222222-2222-2222-2222-22222222222A}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,C:/l/d.erofs,urn:uuid:11111111111111111111111111111111","role":"distro","path":"C:/l/d.erofs","uuid":"11111111-1111-1111-1111-111111111111"}, +{"flavor":"posix","value":"unknown,layer.erofs,11111111-1111-1111-1111-111111111111","error":"unsupported layer role 'unknown'; choose distro"}, +{"flavor":"posix","value":"distro,layer.erofs,not-a-uuid","error":"distro layer UUID is invalid: 'not-a-uuid'"}, +{"flavor":"posix","value":"distro,,1","error":"distro layer path is empty"}, +{"flavor":"posix","value":"distro","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":"distro,a","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":"distro,a,b,c","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":"runtime,a\\b/./c,11111111111111111111111111111111","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,\u00e9\\\\\u00e9\u0000;,d47370eae5cbA75C863be3F8793fa3c475","error":"distro layer UUID is invalid: 'd47370eae5cbA75C863be3F8793fa3c475'"}, +{"flavor":"posix","value":"custom,l.erofs,urn:u9id:be784076-4505-74e5-8157-10ede7f6ae309","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,09c3919ad790d44963d356c2b8caff37","role":"distro","path":"l.erofs","uuid":"09c3919a-d790-d449-63d3-56c2b8caff37"}, +{"flavor":"posix","value":"runtime,l.erofs,059cad0b9662c2f15313cdd92c8a467b","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,urn:uuid:ce910a31-87b3-d154-8786-e88b5197ca97","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,{688670ec-11af-4ecc-227c-2b8fb80b46e5}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,ce3c4752bdde2f319f46da597f9f1dd","error":"distro layer UUID is invalid: 'ce3c4752bdde2f319f46da597f9f1dd'"}, +{"flavor":"posix","value":"distro,l.erofs,CDDC6C8C-280D-C429-581C-6EB1818605BD","role":"distro","path":"l.erofs","uuid":"cddc6c8c-280d-c429-581c-6eb1818605bd"}, +{"flavor":"posix","value":"distro,l.erofs,un:uuid:4d19dc82-d6a2f-3d39-390e-c575209d4642","error":"distro layer UUID is invalid: 'un:uuid:4d19dc82-d6a2f-3d39-390e-c575209d4642'"}, +{"flavor":"posix","value":"distro,\\.UNC ,A2C08FB6-B847-57F5-E521-BE7BB7B8CAC1","role":"distro","path":"\\.UNC ","uuid":"a2c08fb6-b847-57f5-e521-be7bb7b8cac1"}, +{"flavor":"posix","value":"runtime,l.erofs,{aca5b0e7-2b23-36aa-b0d1-c358ade55f90}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,{e31ccd09-a38d-6b01-a567-18d353583623}","role":"distro","path":"l.erofs","uuid":"e31ccd09-a38d-6b01-a567-18d353583623"}, +{"flavor":"posix","value":"custom,\\:,df3dbf88-8e54-b194-bc16-f6d0a60cc0b0","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,2c98e43b-97d0-9883-4ca0-5268c800e130","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,771B1D8B-8AEE-67C9-8375-24C24FABCA5C","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,urn:uuid:2a36bc7f-8009-2884-8cac-6f04ec9098d77","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,fe517c55-3205-b18d-a793-552d9476ccab","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,urn:uuid:08adf337-b699-488b-db394ee894ce6bfd","role":"distro","path":"l.erofs","uuid":"08adf337-b699-488b-db39-4ee894ce6bfd"}, +{"flavor":"posix","value":"runtime,l.erofs,00862bd0-83cc-6f70-456c-9eb}05cc8f258","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,6a021be8-23cc-a543-4b02-649acf462468","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,31d4082d-965c-b9ad-72f9-279472d4ea6d","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,b400187b5f62a2f5790b46db686356656","error":"distro layer UUID is invalid: 'b400187b5f62a2f5790b46db686356656'"}, +{"flavor":"posix","value":"runtime, \u0000,c511e4b9-b92a-2a47-9616-d0549f9ab320","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":",l.erofs,urn:uuid:08ea33c3-151 5-6fad-9054-dd4193856a4","error":"unsupported layer role ''; choose distro"}, +{"flavor":"posix","value":"runtime,ac,{f3ec549b-58ee-1773-8fe5-dae60ceef11e}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,{a24de61c-c46-beb7-37239-ce87b43f0ebd}","role":"distro","path":"l.erofs","uuid":"a24de61c-c46b-eb73-7239-ce87b43f0ebd"}, +{"flavor":"posix","value":"runtime,l.erofs,DC5891E0-CE44-CD84-500B-0750EC9F4A92","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"runtime,unc\\\\../,548f4264ae4ee06aad6c0006a3b9e092","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,{4c9badba-2f47-59db-12b0-e199df1a9608}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,urn:uuid:b36fe728-b5af-5551-b421-018b3a4de53a","role":"distro","path":"l.erofs","uuid":"b36fe728-b5af-5551-b421-018b3a4de53a"}, +{"flavor":"posix","value":"runtime,l.erofs,{a110fcca-a69e-5161-53c2-bc0ca0e94e09}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,4fc70d2cc{4dbff4abb5658f7b4722deb","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,D2E71CE1-30E5-BF2A-8903-75506A9F274F","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,urn:uuid:83439e1b-2664-4a3f-276e-0f9e75587d3b","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,d5be81ce-6e0e-53b9-f5d0-6a9b9bceb265","role":"distro","path":"l.erofs","uuid":"d5be81ce-6e0e-53b9-f5d0-6a9b9bceb265"}, +{"flavor":"posix","value":"distro,l.erofs,95cc2409-bbca-2a82-de91-7632852d212c","role":"distro","path":"l.erofs","uuid":"95cc2409-bbca-2a82-de91-7632852d212c"}, +{"flavor":"posix","value":"runtime,l.erofs,92A33C76-9BC5-5182bBA0B-7ED7406EC40","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,{2a882789-795e-5c58-9680-67e62f3e13f6}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,ccafea27-8d70-dabf-4951-3e2dadf7c698","role":"distro","path":"l.erofs","uuid":"ccafea27-8d70-dabf-4951-3e2dadf7c698"}, +{"flavor":"posix","value":"distro,l.erofs,532C3ADE-D6A5-E14-CF21-14B9798B420A","error":"distro layer UUID is invalid: '532C3ADE-D6A5-E14-CF21-14B9798B420A'"}, +{"flavor":"posix","value":"custom,l.erofs,6C9CBB32-DE23-D3F5-D660-D3AB216169C1","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,0960b882-3da7-6b8e-3a06-e525149dc739","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,37b3c3f6-5d11-b3f7-9326-7a57ac15d0fd","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,;: x,y,{3a551b21-d4e9-146e-7603-67059ab821fe}","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":",l.erofs,urn:uuid:cda20c9b-83c7-98da-d1b2-003c3aea0b7e","error":"unsupported layer role ''; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,{aecbe287-14eb-56a4-af8a-2e3745acd76d}","role":"distro","path":"l.erofs","uuid":"aecbe287-14eb-56a4-af8a-2e3745acd76d"}, +{"flavor":"posix","value":"custom,l.erofs,88bf5b5cb00a47a137d2fa6aafd047cf","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,urn:uuid:dc3d63b-190Cd-dca1-7683-2ea1b6e5364c","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"runtime,\u00e9\\\\\\unc:,a02af7acae132fbaccbddfc4f090167dd8","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,{6515471f-2973-ae1e-8d7f-fb7b51df5998}","role":"distro","path":"l.erofs","uuid":"6515471f-2973-ae1e-8d7f-fb7b51df5998"}, +{"flavor":"posix","value":"distro,;\u00e9\u00e9\\\\UNC,b55261e86c80c039671e2b6884b446f3","role":"distro","path":";\u00e9\u00e9\\\\UNC","uuid":"b55261e8-6c80-c039-671e-2b6884b446f3"}, +{"flavor":"posix","value":"other,l.erofs,{9a0d92b2-47da-59a0-f9c3-78e5d4078715}","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"posix","value":"custom,\u0000,292dd182-f2ee-70a7-aa01-45edda6d9076","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,d34dc48b7b0e451b774c5412c547a1f3","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,{2c354ec2-09cb-3185-72b4-5c3c57b2288a}","role":"distro","path":"l.erofs","uuid":"2c354ec2-09cb-3185-72b4-5c3c57b2288a"}, +{"flavor":"posix","value":"distro,l.erofs,{0fc1d4b1-c50a-5882-4869-495975691dad}","role":"distro","path":"l.erofs","uuid":"0fc1d4b1-c50a-5882-4869-495975691dad"}, +{"flavor":"posix","value":"distro,l.erofs,{2c6d75df-348c-7ede-46f5-f9680a4475d0}","role":"distro","path":"l.erofs","uuid":"2c6d75df-348c-7ede-46f5-f9680a4475d0"}, +{"flavor":"posix","value":"custom,l.erofs,urn:uuid:5b3d3301-7ae3-39bd-0593-11c1ffc3e1e8","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,x,y,222e5703-5db7-9b5d-fa80-0cfdfea56a27","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":"runtime,;,{24eb406e-8f63-1ab2-d6d5-55607c0bc6b3}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,b,ee04d4f8-683a-571a-bf6f-b4a044dc79b3","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,1b1b0886-7c07-7f9e-7570-a6631c0154ab","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,{e1a7ad9b-ac25-d8c2-f9b3-45d7fd8b23b7}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"Distro,c\u00e9?,{bcdadb4-d20-dd18-5e4-b59dd2055185}","error":"unsupported layer role 'Distro'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,37bf70eb-eb85-8145-6327-592a25c4f115","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,32378E3C-73C8-EF04-1DA6-DF849450B1A9","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,urn:uuid:0b03c9dd-401f-7bfb-e3e3-b332640a35d5","role":"distro","path":"l.erofs","uuid":"0b03c9dd-401f-7bfb-e3e3-b332640a35d5"}, +{"flavor":"posix","value":"runtime,l.erofs,6230FAF7-410A-2FF7-9CD9-D7362210D4B6","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"runtime,/\u0000?,0ca2a217497f43dd10aa3a290eb115f","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,b316badf-2319-c480-c888-02f1bd605338","role":"distro","path":"l.erofs","uuid":"b316badf-2319-c480-c888-02f1bd605338"}, +{"flavor":"posix","value":"distro,.C/ ,25ca1a68-1ab9-902a-e7b6-4273181c79cb","role":"distro","path":".C/ ","uuid":"25ca1a68-1ab9-902a-e7b6-4273181c79cb"}, +{"flavor":"posix","value":"distro,l.erofs,3688118-C599-1453-3290-C4A7C34B7CD","error":"distro layer UUID is invalid: '3688118-C599-1453-3290-C4A7C34B7CD'"}, +{"flavor":"posix","value":"distro,uncx,yb\u00e9:,{1ffead7c-9a77-85b1-7a68-33c85b062c65}","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":"other,l.erofs,urn:uuid:bf546440-63c6-cbc5-7ff2-5e82b9fbfa19","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,dabedd032c6f656a12c6f617fc6e3007","role":"distro","path":"l.erofs","uuid":"dabedd03-2c6f-656a-12c6-f617fc6e3007"}, +{"flavor":"posix","value":",l.erofs,urn:uuid:499f619b-9bc1-b3c5-bb3c-eda7afb2700b","error":"unsupported layer role ''; choose distro"}, +{"flavor":"posix","value":"distro,UNCx,yCuncc,AB614B36-E4EA-9EB3-C0D1-A65F1C50518D","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":"custom,l.erofs,6e0eb82a1495c6a6c4c7881e9f692a65","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,.b,05478E4-95B9-8391-B322-65B77F1F4A513","role":"distro","path":".b","uuid":"05478e49-5b98-391b-3226-5b77f1f4a513"}, +{"flavor":"posix","value":"other,l.erofs,6EFB9A12-7711-5D46-4A71-2AAF3151BB08","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,urn:uuid:672efe58-44dc-82e3-070e-01970a44155e","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,b1399c0c-cc0e-ed26-d7a8-59f0f85ba50e","role":"distro","path":"l.erofs","uuid":"b1399c0c-cc0e-ed26-d7a8-59f0f85ba50e"}, +{"flavor":"posix","value":"custom,;,2b8c983b48fe117a9c752b6ae4beba0","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,ad196266f44e84b43ca4cd55774d7149","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,EAD3B99C-9987-7DE9-A7EF-82F85E07AF86","role":"distro","path":"l.erofs","uuid":"ead3b99c-9987-7de9-a7ef-82f85e07af86"}, +{"flavor":"posix","value":"runtime,\\\\c\\\u00e9,urn:uuid:4092f28d-dc50-8b06-9ecc-a48ed0ad6507","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,\u0000unc\\\\;..,825f732fd850a36d6c4c597220227130","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,F6731AAF-D316-01D8-4BF1-68CC72AF9909","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,77CA0BFCE-0314-EC44E-3C4C-E3BAAD63E9A","error":"distro layer UUID is invalid: '77CA0BFCE-0314-EC44E-3C4C-E3BAAD63E9A'"}, +{"flavor":"posix","value":"custom,l.erofs,{c68d0969-de02-5426-06d3-0be6e6d1dabf}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,ab32e5242d8e30fE35381179bf9ceD2d","role":"distro","path":"l.erofs","uuid":"ab32e524-2d8e-30fe-3538-1179bf9ced2d"}, +{"flavor":"posix","value":"runtime,l.erofs,{2af58845-a26d-8c0f-b4bb-e39fccfc3b4c}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,bfb97fd5-3a84-a7e8-b479-5a0ddf6db5cc","role":"distro","path":"l.erofs","uuid":"bfb97fd5-3a84-a7e8-b479-5a0ddf6db5cc"}, +{"flavor":"posix","value":"runtime,l.erofs,6d049}f8d-829d-c9a6-73b7-09bc366dc5b42","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,4F4CF90B-B81F-A867-93B5-8E73C239F190","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,{522bca4d-8403-77d2-0839-e91d7ad9e24c}","role":"distro","path":"l.erofs","uuid":"522bca4d-8403-77d2-0839-e91d7ad9e24c"}, +{"flavor":"posix","value":"distro,l.erofs,urn:uuid:0c40a365-6158-b360-fca2-35d13fe588bd","role":"distro","path":"l.erofs","uuid":"0c40a365-6158-b360-fca2-35d13fe588bd"}, +{"flavor":"posix","value":"runtime,.. ?\\\\,c57eaeca-2945-0e90-b1d3-b2a40d0f30bd","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,e7daea43-ef9c-113b-0418-37eba9114810","role":"distro","path":"l.erofs","uuid":"e7daea43-ef9c-113b-0418-37eba9114810"}, +{"flavor":"posix","value":"custom,unc,{32427f34-de49-89d4-8ded-5dcddb5345c2}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,:,4c2bbdf8-22c8-7cb4-bd0a-3035f04eec23","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,78143d74-3b93-2adb-dfd4-507da43913bc","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,x,yCa:\\\\,30f2a9c6-854c-7403-c4e2-1856023f0be6","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":"runtime,..C ,f1680b112-ad0c-ef25-53dd-3dc0701f1a3","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,{63a4ce9d-86d5-2050-c34e-2b6ee0a06565}","role":"distro","path":"l.erofs","uuid":"63a4ce9d-86d5-2050-c34e-2b6ee0a06565"}, +{"flavor":"posix","value":",.bC;b,EBD3B66D-5AC5-33C5-90EB-7329A3D01D3F","error":"unsupported layer role ''; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,bd0fee6d-efa4-9a7d-1d10-b7e2cc5682b0","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,5ecf5c78-cb7c-dfb4-334d-234d4721b4c9","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,8b0d15bcaf5d90669c821b1112b7e431","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,d17d90e3-0bb6-226b-7a3b-1ab6a8533fe6","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,2F077696-A4D8-2E70-86E7-EB6B0C85650F","role":"distro","path":"l.erofs","uuid":"2f077696-a4d8-2e70-86e7-eb6b0c85650f"}, +{"flavor":"posix","value":"custom,l.erofs,f3728c33-b0ea-7764-d116-71642b05765d","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":",l.erofs,76e7ca06b12f0b32fecde12e68fd412f","error":"unsupported layer role ''; choose distro"}, +{"flavor":"posix","value":"other,l.erofs,{ee87b998-5e87-5c51-c238-028bc639a714}","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"posix","value":"distro,./b,8bc4605cf9e5e49eaadb9e18ee1782b5","role":"distro","path":"b","uuid":"8bc4605c-f9e5-e49e-aadb-9e18ee1782b5"}, +{"flavor":"posix","value":"distro,l.erofs,urn:uuid:79f3ddae-9c07-800f-45cf-54097d035ef8","role":"distro","path":"l.erofs","uuid":"79f3ddae-9c07-800f-45cf-54097d035ef8"}, +{"flavor":"posix","value":"custom,C,47823a6dff65489fcd5be72972fee2cb","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,urn:uuid:854c5327-f769-381a-4a98-d966a4ba29e6","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,b.//:\\\\,476740ac-bafe-c2f2-3ebf-e49cef11b409","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,B277A3B5-87A2-51A4-8EB7-A2691C95D135","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,x,y?ba,869ebbba-6f4c-95c4-1860-dd8f486b24f4","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":"other,\\/;//a,37D0039E-DC2B-80B2-EFA1-333BA95B39FC","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"posix","value":"runtime,:;x,y,1df499fbac7ecdb056a5a31922e139b0","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"posix","value":"custom,l.erofs,{85ba55b0-696e-26c9-fd7e-9f7eead7fe21}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,{91d6acca-835b-23a6-c0b4-4983a1b3b045}","role":"distro","path":"l.erofs","uuid":"91d6acca-835b-23a6-c0b4-4983a1b3b045"}, +{"flavor":"posix","value":"distro,a.UNC\\//,urn:uuid:4e03e884-6e1a-872e-1414-2e8c94f02eb0","role":"distro","path":"a.UNC\\","uuid":"4e03e884-6e1a-872e-1414-2e8c94f02eb0"}, +{"flavor":"posix","value":"custom,l.erofs,urn:uuid:02e32568-fcca-f123-5353-e9b83cef4368","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":",l.erofs,77643A3D-73B4-7A44-4995-17CBD01064761","error":"unsupported layer role ''; choose distro"}, +{"flavor":"posix","value":"runtime,\u00e9\\\\,42e8eb08-f5cd-09c0-b983-a385a69e51db","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,b563db2fe17d97300341346b7fb8a733","role":"distro","path":"l.erofs","uuid":"b563db2f-e17d-9730-0341-346b7fb8a733"}, +{"flavor":"posix","value":"custom,l.erofs,urn:uuid:5ebe3631-3b0a-9e22-7361-c4418f9be13c","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,{2da3af6-33d4-386c-fea5-0146f2803a6f}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,{96064721-c861-cf7c-286d-34728bb8ca13}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,65b24dea-7418-c086-3c04-3cd799cff062","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,\u00e9 ,37a1897c6df95069935c4ead59c96a33","role":"distro","path":"\u00e9 ","uuid":"37a1897c-6df9-5069-935c-4ead59c96a33"}, +{"flavor":"posix","value":"custom,l.erofs,2CE7F754-351A-DB60258f0-E8E032441C57","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,urn:uuid:6ac1a990-d7a6-5989-e871-c7412b190192","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,{5f40518e-0d79-60fd-55c2-da069ddc0ab0}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,d055ce0d-8e03-e2ff-f256-ee19a8413fe8","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,ae127445-baf6-03b0-d399-e6a9f509e85c","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,{fb9ed6a3-cd7e-94f2-cdfb-fb579e78e411}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"posix","value":"runtime,l.erofs,6a7691af-Dafb9-3750c-bff2-baf5274a52c","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"distro,l.erofs,urn:uuid:a0e9b18a-1bd9-32f6-d5d6-ec04d2fc6239","role":"distro","path":"l.erofs","uuid":"a0e9b18a-1bd9-32f6-d5d6-ec04d2fc6239"}, +{"flavor":"posix","value":"distro,UNCUNC;..,caf75663-6c92-8e7a-ecc0-34269711b7a5","role":"distro","path":"UNCUNC;..","uuid":"caf75663-6c92-8e7a-ecc0-34269711b7a5"}, +{"flavor":"posix","value":",\\\\\\\\.a\\,9AF458AB-F246D-B12B-8E8C-3C1F316A616","error":"unsupported layer role ''; choose distro"}, +{"flavor":"posix","value":"distro,\\,50e889b-aec2-298acb99-e98235296bab","error":"distro layer UUID is invalid: '50e889b-aec2-298acb99-e98235296bab'"}, +{"flavor":"posix","value":"distro,l.erofs,a462541d-e859-9321-fb91-8728b6556688","role":"distro","path":"l.erofs","uuid":"a462541d-e859-9321-fb91-8728b6556688"}, +{"flavor":"posix","value":"distro,l.erofs,0f0c1ada-0f7f-793a-94b7-11c02b5dac00","role":"distro","path":"l.erofs","uuid":"0f0c1ada-0f7f-793a-94b7-11c02b5dac00"}, +{"flavor":"posix","value":"runtime,l.erofs,5F35AA7D-2A62-4B24-B9F4-ED94935B156F","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"posix","value":"custom,l.erofs,e3de5cd9dfe45cafe1759b71cb1a3bfa","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,distro.erofs,11111111-1111-1111-1111-111111111111","role":"distro","path":"distro.erofs","uuid":"11111111-1111-1111-1111-111111111111"}, +{"flavor":"windows","value":"custom,layers//custom.erofs,{22222222-2222-2222-2222-22222222222A}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,C:/l/d.erofs,urn:uuid:11111111111111111111111111111111","role":"distro","path":"C:\\l\\d.erofs","uuid":"11111111-1111-1111-1111-111111111111"}, +{"flavor":"windows","value":"unknown,layer.erofs,11111111-1111-1111-1111-111111111111","error":"unsupported layer role 'unknown'; choose distro"}, +{"flavor":"windows","value":"distro,layer.erofs,not-a-uuid","error":"distro layer UUID is invalid: 'not-a-uuid'"}, +{"flavor":"windows","value":"distro,,1","error":"distro layer path is empty"}, +{"flavor":"windows","value":"distro","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":"distro,a","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":"distro,a,b,c","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":"runtime,a\\b/./c,11111111111111111111111111111111","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,\u00e9\\\\\u00e9\u0000;,d47370eae5cbA75C863be3F8793fa3c475","error":"distro layer UUID is invalid: 'd47370eae5cbA75C863be3F8793fa3c475'"}, +{"flavor":"windows","value":"custom,l.erofs,urn:u9id:be784076-4505-74e5-8157-10ede7f6ae309","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,09c3919ad790d44963d356c2b8caff37","role":"distro","path":"l.erofs","uuid":"09c3919a-d790-d449-63d3-56c2b8caff37"}, +{"flavor":"windows","value":"runtime,l.erofs,059cad0b9662c2f15313cdd92c8a467b","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,urn:uuid:ce910a31-87b3-d154-8786-e88b5197ca97","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,{688670ec-11af-4ecc-227c-2b8fb80b46e5}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,ce3c4752bdde2f319f46da597f9f1dd","error":"distro layer UUID is invalid: 'ce3c4752bdde2f319f46da597f9f1dd'"}, +{"flavor":"windows","value":"distro,l.erofs,CDDC6C8C-280D-C429-581C-6EB1818605BD","role":"distro","path":"l.erofs","uuid":"cddc6c8c-280d-c429-581c-6eb1818605bd"}, +{"flavor":"windows","value":"distro,l.erofs,un:uuid:4d19dc82-d6a2f-3d39-390e-c575209d4642","error":"distro layer UUID is invalid: 'un:uuid:4d19dc82-d6a2f-3d39-390e-c575209d4642'"}, +{"flavor":"windows","value":"distro,\\.UNC ,A2C08FB6-B847-57F5-E521-BE7BB7B8CAC1","role":"distro","path":"\\.UNC ","uuid":"a2c08fb6-b847-57f5-e521-be7bb7b8cac1"}, +{"flavor":"windows","value":"runtime,l.erofs,{aca5b0e7-2b23-36aa-b0d1-c358ade55f90}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,{e31ccd09-a38d-6b01-a567-18d353583623}","role":"distro","path":"l.erofs","uuid":"e31ccd09-a38d-6b01-a567-18d353583623"}, +{"flavor":"windows","value":"custom,\\:,df3dbf88-8e54-b194-bc16-f6d0a60cc0b0","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,2c98e43b-97d0-9883-4ca0-5268c800e130","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,771B1D8B-8AEE-67C9-8375-24C24FABCA5C","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,urn:uuid:2a36bc7f-8009-2884-8cac-6f04ec9098d77","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,fe517c55-3205-b18d-a793-552d9476ccab","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,urn:uuid:08adf337-b699-488b-db394ee894ce6bfd","role":"distro","path":"l.erofs","uuid":"08adf337-b699-488b-db39-4ee894ce6bfd"}, +{"flavor":"windows","value":"runtime,l.erofs,00862bd0-83cc-6f70-456c-9eb}05cc8f258","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,6a021be8-23cc-a543-4b02-649acf462468","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,31d4082d-965c-b9ad-72f9-279472d4ea6d","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,b400187b5f62a2f5790b46db686356656","error":"distro layer UUID is invalid: 'b400187b5f62a2f5790b46db686356656'"}, +{"flavor":"windows","value":"runtime, \u0000,c511e4b9-b92a-2a47-9616-d0549f9ab320","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":",l.erofs,urn:uuid:08ea33c3-151 5-6fad-9054-dd4193856a4","error":"unsupported layer role ''; choose distro"}, +{"flavor":"windows","value":"runtime,ac,{f3ec549b-58ee-1773-8fe5-dae60ceef11e}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,{a24de61c-c46-beb7-37239-ce87b43f0ebd}","role":"distro","path":"l.erofs","uuid":"a24de61c-c46b-eb73-7239-ce87b43f0ebd"}, +{"flavor":"windows","value":"runtime,l.erofs,DC5891E0-CE44-CD84-500B-0750EC9F4A92","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"runtime,unc\\\\../,548f4264ae4ee06aad6c0006a3b9e092","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,{4c9badba-2f47-59db-12b0-e199df1a9608}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,urn:uuid:b36fe728-b5af-5551-b421-018b3a4de53a","role":"distro","path":"l.erofs","uuid":"b36fe728-b5af-5551-b421-018b3a4de53a"}, +{"flavor":"windows","value":"runtime,l.erofs,{a110fcca-a69e-5161-53c2-bc0ca0e94e09}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,4fc70d2cc{4dbff4abb5658f7b4722deb","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,D2E71CE1-30E5-BF2A-8903-75506A9F274F","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,urn:uuid:83439e1b-2664-4a3f-276e-0f9e75587d3b","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,d5be81ce-6e0e-53b9-f5d0-6a9b9bceb265","role":"distro","path":"l.erofs","uuid":"d5be81ce-6e0e-53b9-f5d0-6a9b9bceb265"}, +{"flavor":"windows","value":"distro,l.erofs,95cc2409-bbca-2a82-de91-7632852d212c","role":"distro","path":"l.erofs","uuid":"95cc2409-bbca-2a82-de91-7632852d212c"}, +{"flavor":"windows","value":"runtime,l.erofs,92A33C76-9BC5-5182bBA0B-7ED7406EC40","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,{2a882789-795e-5c58-9680-67e62f3e13f6}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,ccafea27-8d70-dabf-4951-3e2dadf7c698","role":"distro","path":"l.erofs","uuid":"ccafea27-8d70-dabf-4951-3e2dadf7c698"}, +{"flavor":"windows","value":"distro,l.erofs,532C3ADE-D6A5-E14-CF21-14B9798B420A","error":"distro layer UUID is invalid: '532C3ADE-D6A5-E14-CF21-14B9798B420A'"}, +{"flavor":"windows","value":"custom,l.erofs,6C9CBB32-DE23-D3F5-D660-D3AB216169C1","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,0960b882-3da7-6b8e-3a06-e525149dc739","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,37b3c3f6-5d11-b3f7-9326-7a57ac15d0fd","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,;: x,y,{3a551b21-d4e9-146e-7603-67059ab821fe}","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":",l.erofs,urn:uuid:cda20c9b-83c7-98da-d1b2-003c3aea0b7e","error":"unsupported layer role ''; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,{aecbe287-14eb-56a4-af8a-2e3745acd76d}","role":"distro","path":"l.erofs","uuid":"aecbe287-14eb-56a4-af8a-2e3745acd76d"}, +{"flavor":"windows","value":"custom,l.erofs,88bf5b5cb00a47a137d2fa6aafd047cf","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,urn:uuid:dc3d63b-190Cd-dca1-7683-2ea1b6e5364c","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"runtime,\u00e9\\\\\\unc:,a02af7acae132fbaccbddfc4f090167dd8","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,{6515471f-2973-ae1e-8d7f-fb7b51df5998}","role":"distro","path":"l.erofs","uuid":"6515471f-2973-ae1e-8d7f-fb7b51df5998"}, +{"flavor":"windows","value":"distro,;\u00e9\u00e9\\\\UNC,b55261e86c80c039671e2b6884b446f3","role":"distro","path":";\u00e9\u00e9\\UNC","uuid":"b55261e8-6c80-c039-671e-2b6884b446f3"}, +{"flavor":"windows","value":"other,l.erofs,{9a0d92b2-47da-59a0-f9c3-78e5d4078715}","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"windows","value":"custom,\u0000,292dd182-f2ee-70a7-aa01-45edda6d9076","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,d34dc48b7b0e451b774c5412c547a1f3","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,{2c354ec2-09cb-3185-72b4-5c3c57b2288a}","role":"distro","path":"l.erofs","uuid":"2c354ec2-09cb-3185-72b4-5c3c57b2288a"}, +{"flavor":"windows","value":"distro,l.erofs,{0fc1d4b1-c50a-5882-4869-495975691dad}","role":"distro","path":"l.erofs","uuid":"0fc1d4b1-c50a-5882-4869-495975691dad"}, +{"flavor":"windows","value":"distro,l.erofs,{2c6d75df-348c-7ede-46f5-f9680a4475d0}","role":"distro","path":"l.erofs","uuid":"2c6d75df-348c-7ede-46f5-f9680a4475d0"}, +{"flavor":"windows","value":"custom,l.erofs,urn:uuid:5b3d3301-7ae3-39bd-0593-11c1ffc3e1e8","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,x,y,222e5703-5db7-9b5d-fa80-0cfdfea56a27","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":"runtime,;,{24eb406e-8f63-1ab2-d6d5-55607c0bc6b3}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,b,ee04d4f8-683a-571a-bf6f-b4a044dc79b3","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,1b1b0886-7c07-7f9e-7570-a6631c0154ab","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,{e1a7ad9b-ac25-d8c2-f9b3-45d7fd8b23b7}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"Distro,c\u00e9?,{bcdadb4-d20-dd18-5e4-b59dd2055185}","error":"unsupported layer role 'Distro'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,37bf70eb-eb85-8145-6327-592a25c4f115","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,32378E3C-73C8-EF04-1DA6-DF849450B1A9","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,urn:uuid:0b03c9dd-401f-7bfb-e3e3-b332640a35d5","role":"distro","path":"l.erofs","uuid":"0b03c9dd-401f-7bfb-e3e3-b332640a35d5"}, +{"flavor":"windows","value":"runtime,l.erofs,6230FAF7-410A-2FF7-9CD9-D7362210D4B6","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"runtime,/\u0000?,0ca2a217497f43dd10aa3a290eb115f","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,b316badf-2319-c480-c888-02f1bd605338","role":"distro","path":"l.erofs","uuid":"b316badf-2319-c480-c888-02f1bd605338"}, +{"flavor":"windows","value":"distro,.C/ ,25ca1a68-1ab9-902a-e7b6-4273181c79cb","role":"distro","path":".C\\ ","uuid":"25ca1a68-1ab9-902a-e7b6-4273181c79cb"}, +{"flavor":"windows","value":"distro,l.erofs,3688118-C599-1453-3290-C4A7C34B7CD","error":"distro layer UUID is invalid: '3688118-C599-1453-3290-C4A7C34B7CD'"}, +{"flavor":"windows","value":"distro,uncx,yb\u00e9:,{1ffead7c-9a77-85b1-7a68-33c85b062c65}","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":"other,l.erofs,urn:uuid:bf546440-63c6-cbc5-7ff2-5e82b9fbfa19","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,dabedd032c6f656a12c6f617fc6e3007","role":"distro","path":"l.erofs","uuid":"dabedd03-2c6f-656a-12c6-f617fc6e3007"}, +{"flavor":"windows","value":",l.erofs,urn:uuid:499f619b-9bc1-b3c5-bb3c-eda7afb2700b","error":"unsupported layer role ''; choose distro"}, +{"flavor":"windows","value":"distro,UNCx,yCuncc,AB614B36-E4EA-9EB3-C0D1-A65F1C50518D","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":"custom,l.erofs,6e0eb82a1495c6a6c4c7881e9f692a65","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,.b,05478E4-95B9-8391-B322-65B77F1F4A513","role":"distro","path":".b","uuid":"05478e49-5b98-391b-3226-5b77f1f4a513"}, +{"flavor":"windows","value":"other,l.erofs,6EFB9A12-7711-5D46-4A71-2AAF3151BB08","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,urn:uuid:672efe58-44dc-82e3-070e-01970a44155e","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,b1399c0c-cc0e-ed26-d7a8-59f0f85ba50e","role":"distro","path":"l.erofs","uuid":"b1399c0c-cc0e-ed26-d7a8-59f0f85ba50e"}, +{"flavor":"windows","value":"custom,;,2b8c983b48fe117a9c752b6ae4beba0","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,ad196266f44e84b43ca4cd55774d7149","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,EAD3B99C-9987-7DE9-A7EF-82F85E07AF86","role":"distro","path":"l.erofs","uuid":"ead3b99c-9987-7de9-a7ef-82f85e07af86"}, +{"flavor":"windows","value":"runtime,\\\\c\\\u00e9,urn:uuid:4092f28d-dc50-8b06-9ecc-a48ed0ad6507","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,\u0000unc\\\\;..,825f732fd850a36d6c4c597220227130","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,F6731AAF-D316-01D8-4BF1-68CC72AF9909","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,77CA0BFCE-0314-EC44E-3C4C-E3BAAD63E9A","error":"distro layer UUID is invalid: '77CA0BFCE-0314-EC44E-3C4C-E3BAAD63E9A'"}, +{"flavor":"windows","value":"custom,l.erofs,{c68d0969-de02-5426-06d3-0be6e6d1dabf}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,ab32e5242d8e30fE35381179bf9ceD2d","role":"distro","path":"l.erofs","uuid":"ab32e524-2d8e-30fe-3538-1179bf9ced2d"}, +{"flavor":"windows","value":"runtime,l.erofs,{2af58845-a26d-8c0f-b4bb-e39fccfc3b4c}","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,bfb97fd5-3a84-a7e8-b479-5a0ddf6db5cc","role":"distro","path":"l.erofs","uuid":"bfb97fd5-3a84-a7e8-b479-5a0ddf6db5cc"}, +{"flavor":"windows","value":"runtime,l.erofs,6d049}f8d-829d-c9a6-73b7-09bc366dc5b42","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,4F4CF90B-B81F-A867-93B5-8E73C239F190","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,{522bca4d-8403-77d2-0839-e91d7ad9e24c}","role":"distro","path":"l.erofs","uuid":"522bca4d-8403-77d2-0839-e91d7ad9e24c"}, +{"flavor":"windows","value":"distro,l.erofs,urn:uuid:0c40a365-6158-b360-fca2-35d13fe588bd","role":"distro","path":"l.erofs","uuid":"0c40a365-6158-b360-fca2-35d13fe588bd"}, +{"flavor":"windows","value":"runtime,.. ?\\\\,c57eaeca-2945-0e90-b1d3-b2a40d0f30bd","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,e7daea43-ef9c-113b-0418-37eba9114810","role":"distro","path":"l.erofs","uuid":"e7daea43-ef9c-113b-0418-37eba9114810"}, +{"flavor":"windows","value":"custom,unc,{32427f34-de49-89d4-8ded-5dcddb5345c2}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,:,4c2bbdf8-22c8-7cb4-bd0a-3035f04eec23","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,78143d74-3b93-2adb-dfd4-507da43913bc","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,x,yCa:\\\\,30f2a9c6-854c-7403-c4e2-1856023f0be6","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":"runtime,..C ,f1680b112-ad0c-ef25-53dd-3dc0701f1a3","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,{63a4ce9d-86d5-2050-c34e-2b6ee0a06565}","role":"distro","path":"l.erofs","uuid":"63a4ce9d-86d5-2050-c34e-2b6ee0a06565"}, +{"flavor":"windows","value":",.bC;b,EBD3B66D-5AC5-33C5-90EB-7329A3D01D3F","error":"unsupported layer role ''; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,bd0fee6d-efa4-9a7d-1d10-b7e2cc5682b0","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,5ecf5c78-cb7c-dfb4-334d-234d4721b4c9","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,8b0d15bcaf5d90669c821b1112b7e431","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,d17d90e3-0bb6-226b-7a3b-1ab6a8533fe6","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,2F077696-A4D8-2E70-86E7-EB6B0C85650F","role":"distro","path":"l.erofs","uuid":"2f077696-a4d8-2e70-86e7-eb6b0c85650f"}, +{"flavor":"windows","value":"custom,l.erofs,f3728c33-b0ea-7764-d116-71642b05765d","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":",l.erofs,76e7ca06b12f0b32fecde12e68fd412f","error":"unsupported layer role ''; choose distro"}, +{"flavor":"windows","value":"other,l.erofs,{ee87b998-5e87-5c51-c238-028bc639a714}","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"windows","value":"distro,./b,8bc4605cf9e5e49eaadb9e18ee1782b5","role":"distro","path":"b","uuid":"8bc4605c-f9e5-e49e-aadb-9e18ee1782b5"}, +{"flavor":"windows","value":"distro,l.erofs,urn:uuid:79f3ddae-9c07-800f-45cf-54097d035ef8","role":"distro","path":"l.erofs","uuid":"79f3ddae-9c07-800f-45cf-54097d035ef8"}, +{"flavor":"windows","value":"custom,C,47823a6dff65489fcd5be72972fee2cb","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,urn:uuid:854c5327-f769-381a-4a98-d966a4ba29e6","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,b.//:\\\\,476740ac-bafe-c2f2-3ebf-e49cef11b409","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,B277A3B5-87A2-51A4-8EB7-A2691C95D135","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,x,y?ba,869ebbba-6f4c-95c4-1860-dd8f486b24f4","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":"other,\\/;//a,37D0039E-DC2B-80B2-EFA1-333BA95B39FC","error":"unsupported layer role 'other'; choose distro"}, +{"flavor":"windows","value":"runtime,:;x,y,1df499fbac7ecdb056a5a31922e139b0","error":"--layer must be ROLE,PATH,EROFS_UUID"}, +{"flavor":"windows","value":"custom,l.erofs,{85ba55b0-696e-26c9-fd7e-9f7eead7fe21}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,{91d6acca-835b-23a6-c0b4-4983a1b3b045}","role":"distro","path":"l.erofs","uuid":"91d6acca-835b-23a6-c0b4-4983a1b3b045"}, +{"flavor":"windows","value":"distro,a.UNC\\//,urn:uuid:4e03e884-6e1a-872e-1414-2e8c94f02eb0","role":"distro","path":"a.UNC","uuid":"4e03e884-6e1a-872e-1414-2e8c94f02eb0"}, +{"flavor":"windows","value":"custom,l.erofs,urn:uuid:02e32568-fcca-f123-5353-e9b83cef4368","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":",l.erofs,77643A3D-73B4-7A44-4995-17CBD01064761","error":"unsupported layer role ''; choose distro"}, +{"flavor":"windows","value":"runtime,\u00e9\\\\,42e8eb08-f5cd-09c0-b983-a385a69e51db","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,b563db2fe17d97300341346b7fb8a733","role":"distro","path":"l.erofs","uuid":"b563db2f-e17d-9730-0341-346b7fb8a733"}, +{"flavor":"windows","value":"custom,l.erofs,urn:uuid:5ebe3631-3b0a-9e22-7361-c4418f9be13c","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,{2da3af6-33d4-386c-fea5-0146f2803a6f}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,{96064721-c861-cf7c-286d-34728bb8ca13}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,65b24dea-7418-c086-3c04-3cd799cff062","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,\u00e9 ,37a1897c6df95069935c4ead59c96a33","role":"distro","path":"\u00e9 ","uuid":"37a1897c-6df9-5069-935c-4ead59c96a33"}, +{"flavor":"windows","value":"custom,l.erofs,2CE7F754-351A-DB60258f0-E8E032441C57","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,urn:uuid:6ac1a990-d7a6-5989-e871-c7412b190192","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,{5f40518e-0d79-60fd-55c2-da069ddc0ab0}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,d055ce0d-8e03-e2ff-f256-ee19a8413fe8","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,ae127445-baf6-03b0-d399-e6a9f509e85c","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,{fb9ed6a3-cd7e-94f2-cdfb-fb579e78e411}","error":"unsupported layer role 'custom'; choose distro"}, +{"flavor":"windows","value":"runtime,l.erofs,6a7691af-Dafb9-3750c-bff2-baf5274a52c","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"distro,l.erofs,urn:uuid:a0e9b18a-1bd9-32f6-d5d6-ec04d2fc6239","role":"distro","path":"l.erofs","uuid":"a0e9b18a-1bd9-32f6-d5d6-ec04d2fc6239"}, +{"flavor":"windows","value":"distro,UNCUNC;..,caf75663-6c92-8e7a-ecc0-34269711b7a5","role":"distro","path":"UNCUNC;..","uuid":"caf75663-6c92-8e7a-ecc0-34269711b7a5"}, +{"flavor":"windows","value":",\\\\\\\\.a\\,9AF458AB-F246D-B12B-8E8C-3C1F316A616","error":"unsupported layer role ''; choose distro"}, +{"flavor":"windows","value":"distro,\\,50e889b-aec2-298acb99-e98235296bab","error":"distro layer UUID is invalid: '50e889b-aec2-298acb99-e98235296bab'"}, +{"flavor":"windows","value":"distro,l.erofs,a462541d-e859-9321-fb91-8728b6556688","role":"distro","path":"l.erofs","uuid":"a462541d-e859-9321-fb91-8728b6556688"}, +{"flavor":"windows","value":"distro,l.erofs,0f0c1ada-0f7f-793a-94b7-11c02b5dac00","role":"distro","path":"l.erofs","uuid":"0f0c1ada-0f7f-793a-94b7-11c02b5dac00"}, +{"flavor":"windows","value":"runtime,l.erofs,5F35AA7D-2A62-4B24-B9F4-ED94935B156F","error":"unsupported layer role 'runtime'; choose distro"}, +{"flavor":"windows","value":"custom,l.erofs,e3de5cd9dfe45cafe1759b71cb1a3bfa","error":"unsupported layer role 'custom'; choose distro"} +], +"mount_values": [ +{"value":"/workspace","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"/workspace,","error":"--mount host path is empty"}, +{"value":"/workspace,,rw","error":"--mount host path is empty"}, +{"value":"/workspace,host,rx","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/workspace,host,rw,extra","error":"unsupported --mount mode 'extra'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/workspace,alpha,beta","error":"unsupported --mount mode 'beta'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/data,/srv/data,rw,ro","target":"/data","host_path":"/srv/data,rw","access":"ro"}, +{"value":"/data,/srv/data,,rw","target":"/data","host_path":"/srv/data,","access":"rw"}, +{"value":"/data,/srv/data,rw","target":"/data","host_path":"/srv/data","access":"rw"}, +{"value":"/data,/srv/data","target":"/data","host_path":"/srv/data","access":"ro"}, +{"value":",","error":"--mount host path is empty"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"c","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"b.unc\u0000b\\\\","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":",CbUNC","target":"","host_path":"CbUNC","access":"ro"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\u00e9\u0000/Cro//","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"/ ;\u00e9a:","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"b","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"UNC //","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"/b:////","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":":unc?..C/","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"C","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"b,uncC;","target":"b","host_path":"uncC;","access":"ro"}, +{"value":" ....:ro:","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\\x,y\\\\:\u00e9","target":"\\x","host_path":"y\\\\:\u00e9","access":"ro"}, +{"value":"rw","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"/","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\\\\//uncrw","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":" \u0000\u0000 ","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"unc .\u00e9?crw ","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"c ","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":",\u00e9\\\\\\\\\u00e9//","target":"","host_path":"\u00e9\\\\\\\\\u00e9//","access":"ro"}, +{"value":"\u00e9rw;\\\\/","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":";?, ","target":";?","host_path":" ","access":"ro"}, +{"value":"?\\C.bc","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"/\\\\b","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"?\u0000rox,y","target":"?\u0000rox","host_path":"y","access":"ro"}, +{"value":"..:b?;ro\u0000..","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"C//C\\\\","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"//.rw?uncC","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"a","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"rox,y////","target":"rox","host_path":"y////","access":"ro"}, +{"value":"a ","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"Cx,y,,x,y\u00e9b\\","error":"unsupported --mount mode 'y\u00e9b\\\\'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"?\u0000x,y,","error":"unsupported --mount mode ''; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":";//aunc:x,yx,y","error":"unsupported --mount mode 'y'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"\\\\","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"unc;x,y;","target":"unc;x","host_path":"y;","access":"ro"}, +{"value":",","error":"--mount host path is empty"}, +{"value":" //\\\\rwUNC","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"b..:;//rw?","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"/;b","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"rw?a;....\u00e9","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"ax,y;x,y\\","error":"unsupported --mount mode 'y\\\\'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"\\\\//a;a.","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"uncunc..","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"a..UNCrw:\\","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"?ro:","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\\\\\\","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"b..unc\\\\","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":",\\\\UNC\u00e9\u00e9..:","target":"","host_path":"\\\\UNC\u00e9\u00e9..:","access":"ro"}, +{"value":"\u0000,\u00e9rw;\\\\.","target":"\u0000","host_path":"\u00e9rw;\\\\.","access":"ro"}, +{"value":",cUNC\u0000?..rw","target":"","host_path":"cUNC\u0000?..rw","access":"ro"}, +{"value":"rwa","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"?b?UNC..","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"C","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"C;aUNC//ro,","error":"--mount host path is empty"}, +{"value":"..\u00e9","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"b//, ..a","target":"b//","host_path":" ..a","access":"ro"}, +{"value":".\u00e9:","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"ro\\ ?//rw//","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\\\\UNC ","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":" rw..unc\u00e9/ro","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\u0000","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"rouncuncc: ","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":":","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":".x,yroUNC//\u0000\u00e9..","target":".x","host_path":"yroUNC//\u0000\u00e9..","access":"ro"}, +{"value":"x,y","target":"x","host_path":"y","access":"ro"}, +{"value":"ro//\u00e9/x,y.ro","target":"ro//\u00e9/x","host_path":"y.ro","access":"ro"}, +{"value":" \u00e9c","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\u0000","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":" C\u00e9arox,y","target":" C\u00e9arox","host_path":"y","access":"ro"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":";\u0000rwCrox,y:\\\\","target":";\u0000rwCrox","host_path":"y:\\\\","access":"ro"}, +{"value":"ro///..x,y;b","target":"ro///..x","host_path":"y;b","access":"ro"}, +{"value":"Ca","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"UNC","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\u0000;..ro?://","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\u0000,uncaunc","target":"\u0000","host_path":"uncaunc","access":"ro"}, +{"value":"/:/\\\\","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"UNCx,y:x,y","error":"unsupported --mount mode 'y'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"rw\\\\:.,// ","target":"rw\\\\:.","host_path":"// ","access":"ro"}, +{"value":"\\/","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"x,yunc","target":"x","host_path":"yunc","access":"ro"}, +{"value":"c;roC","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\u00e9?:..unc;..","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\\UNC.\u0000c ,","error":"--mount host path is empty"}, +{"value":" c:c","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"UNC\\ro","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"UNC..","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"rw","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"uncUNCa?","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"\u0000;b\u0000/","error":"--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]"}, +{"value":"/\u00e9/nvx/srv,aaUNC,ro","target":"/\u00e9/nvx/srv","host_path":"aaUNC","access":"ro"}, +{"value":"/srv/\u00e9,\\\\//,","error":"unsupported --mount mode ''; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/cache/a,aunc\u0000,rw","target":"/cache/a","host_path":"aunc\u0000","access":"rw"}, +{"value":"/nvx,; a,rw","target":"/nvx","host_path":"; a","access":"rw"}, +{"value":"/a/srv/nvx,unc..C,,ro","target":"/a/srv/nvx","host_path":"unc..C,","access":"ro"}, +{"value":"/data-1,\u00e9\u0000,ro","target":"/data-1","host_path":"\u00e9\u0000","access":"ro"}, +{"value":"/\u00e9/etc,\u00e9a//c","target":"/\u00e9/etc","host_path":"\u00e9a//c","access":"ro"}, +{"value":"/nvx,\\uncx,yC,ro","target":"/nvx","host_path":"\\uncx,yC","access":"ro"}, +{"value":"/a/srv/work,a","target":"/a/srv/work","host_path":"a","access":"ro"}, +{"value":"/etc/\u00e9/data-1,.","target":"/etc/\u00e9/data-1","host_path":".","access":"ro"}, +{"value":"/run/srv/work,//\u0000c","target":"/run/srv/work","host_path":"//\u0000c","access":"ro"}, +{"value":"/data-1/srv,\u00e9,rw","target":"/data-1/srv","host_path":"\u00e9","access":"rw"}, +{"value":"/etc,C\u0000;:","target":"/etc","host_path":"C\u0000;:","access":"ro"}, +{"value":"/srv,.","target":"/srv","host_path":".","access":"ro"}, +{"value":"/srv,\u0000?.unc,ro","target":"/srv","host_path":"\u0000?.unc","access":"ro"}, +{"value":"/\u00e9/srv/cache,;b,rw","target":"/\u00e9/srv/cache","host_path":";b","access":"rw"}, +{"value":"/work/srv,unc.//","target":"/work/srv","host_path":"unc.//","access":"ro"}, +{"value":"/\u00e9/nvx,\\\\x,y,rx","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/work,cC,ro","target":"/work","host_path":"cC","access":"ro"}, +{"value":"/data-1/data-1,unc.,rw","target":"/data-1/data-1","host_path":"unc.","access":"rw"}, +{"value":"/etc/run,\\,","error":"unsupported --mount mode ''; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"//etc, ,rw","target":"//etc","host_path":" ","access":"rw"}, +{"value":"/srv/data-1,UNCUNC","target":"/srv/data-1","host_path":"UNCUNC","access":"ro"}, +{"value":"/srv/data-1/srv,/../;,ro","target":"/srv/data-1/srv","host_path":"/../;","access":"ro"}, +{"value":"/data-1/work,/,rw","target":"/data-1/work","host_path":"/","access":"rw"}, +{"value":"/sharesnvx,UNCUNC\\\\unc,rx","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/,UNCc","target":"/","host_path":"UNCc","access":"ro"}, +{"value":"/\u00e9/srv,..,ro","target":"/\u00e9/srv","host_path":"..","access":"ro"}, +{"value":"/nvx/etc,;","target":"/nvx/etc","host_path":";","access":"ro"}, +{"value":"/srv/work/a,.","target":"/srv/work/a","host_path":".","access":"ro"}, +{"value":"/work/data-1,C\u0000,rw","target":"/work/data-1","host_path":"C\u0000","access":"rw"}, +{"value":"/etc/run, uncUNC:,rw","target":"/etc/run","host_path":" uncUNC:","access":"rw"}, +{"value":"///data-1,\\\\C,rw","target":"///data-1","host_path":"\\\\C","access":"rw"}, +{"value":"/run, C,rw","target":"/run","host_path":" C","access":"rw"}, +{"value":"/cache/a/a,UNC// ,ro","target":"/cache/a/a","host_path":"UNC// ","access":"ro"}, +{"value":"/\u00e9/\u00e9,unc//\\x,y,rw","target":"/\u00e9/\u00e9","host_path":"unc//\\x,y","access":"rw"}, +{"value":"/cache/a/etc,\\,rw","target":"/cache/a/etc","host_path":"\\","access":"rw"}, +{"value":"/data-1,..\u00e9,rw","target":"/data-1","host_path":"..\u00e9","access":"rw"}, +{"value":"/\u00e9/etc,\u0000.,ro","target":"/\u00e9/etc","host_path":"\u0000.","access":"ro"}, +{"value":"/run/a,..","target":"/run/a","host_path":"..","access":"ro"}, +{"value":"/nvx,//\\.,rw","target":"/nvx","host_path":"//\\.","access":"rw"}, +{"value":"/nvx/cache/nvx,;..C,ro","target":"/nvx/cache/nvx","host_path":";..C","access":"ro"}, +{"value":"/data-1,:bunc","target":"/data-1","host_path":":bunc","access":"ro"}, +{"value":"/srv,?C,","error":"unsupported --mount mode ''; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/=,x,yb,rw","target":"/=","host_path":"x,yb","access":"rw"}, +{"value":"/nvx/srv,,UNC","error":"--mount host path is empty"}, +{"value":"/etc/run,?,ro","target":"/etc/run","host_path":"?","access":"ro"}, +{"value":"/nvx/run,C\u0000","target":"/nvx/run","host_path":"C\u0000","access":"ro"}, +{"value":"/a/data-1,,unc\\\\,rw","target":"/a/data-1","host_path":",unc\\\\","access":"rw"}, +{"value":"/a/\u00e9/run,unc","target":"/a/\u00e9/run","host_path":"unc","access":"ro"}, +{"value":"/work,//,rw","target":"/work","host_path":"//","access":"rw"}, +{"value":"/\u00e9/nvx,//a ,ro","target":"/\u00e9/nvx","host_path":"//a ","access":"ro"}, +{"value":"/etc,.:..,ro","target":"/etc","host_path":".:..","access":"ro"}, +{"value":"/run/nvx,x,yc..\\,rw","target":"/run/nvx","host_path":"x,yc..\\","access":"rw"}, +{"value":"/cache/data-1,\u0000\\\\x,y,rw","target":"/cache/data-1","host_path":"\u0000\\\\x,y","access":"rw"}, +{"value":"/etc/a,//unc\u0000unc,ro","target":"/etc/a","host_path":"//unc\u0000unc","access":"ro"}, +{"value":"/etc/\u00e9,C//,rw","target":"/etc/\u00e9","host_path":"C//","access":"rw"}, +{"value":"/etc,.uncaa","target":"/etc","host_path":".uncaa","access":"ro"}, +{"value":"/cache/cache/data-1,UNC?..C,rw","target":"/cache/cache/data-1","host_path":"UNC?..C","access":"rw"}, +{"value":"/data-1/run,\\b ,rw","target":"/data-1/run","host_path":"\\b ","access":"rw"}, +{"value":"/ /a,/ .","target":"/ /a","host_path":"/ .","access":"ro"}, +{"value":"/..a//srv, \\.,ro","target":"/..a//srv","host_path":" \\.","access":"ro"}, +{"value":"/data-1/run, \\\\;,rw","target":"/data-1/run","host_path":" \\\\;","access":"rw"}, +{"value":"/etc/a,.x,yunc,","error":"unsupported --mount mode ''; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/srv,b,rw","target":"/srv","host_path":"b","access":"rw"}, +{"value":"/a/etc,. . ,","error":"unsupported --mount mode ''; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/srv/work,b .,,rw","target":"/srv/work","host_path":"b .,","access":"rw"}, +{"value":"/\u00e9/cache/cache,?aUNC\\,rw","target":"/\u00e9/cache/cache","host_path":"?aUNC\\","access":"rw"}, +{"value":"/\u00e9,c//","target":"/\u00e9","host_path":"c//","access":"ro"}, +{"value":"/work/a/run,c?\u00e9,","error":"unsupported --mount mode ''; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/a/\u00e9/cache,x,y","error":"unsupported --mount mode 'y'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/\u00e9/cache/nvx,..,rw","target":"/\u00e9/cache/nvx","host_path":"..","access":"rw"}, +{"value":"/\u00e9/run,.","target":"/\u00e9/run","host_path":".","access":"ro"}, +{"value":"/cache/etc,\\unc\\","target":"/cache/etc","host_path":"\\unc\\","access":"ro"}, +{"value":"/cache,//uncUNC.,ro","target":"/cache","host_path":"//uncUNC.","access":"ro"}, +{"value":"/data-1/cache,c,","error":"unsupported --mount mode ''; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/data-1/srv,?c?,rw","target":"/data-1/srv","host_path":"?c?","access":"rw"}, +{"value":"/etc/data-1/etc,//..//\\,rw","target":"/etc/data-1/etc","host_path":"//..//\\","access":"rw"}, +{"value":"/data-1/\u00e9/a,.\u0000,ro","target":"/data-1/\u00e9/a","host_path":".\u0000","access":"ro"}, +{"value":"/data-1,./ ,rx","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/data-1,..\u00e9x,y.,ro","target":"/data-1","host_path":"..\u00e9x,y.","access":"ro"}, +{"value":"/nvx,unc;/UNC,ro","target":"/nvx","host_path":"unc;/UNC","access":"ro"}, +{"value":"/a/nvx,unc\\\\\\,ro","target":"/a/nvx","host_path":"unc\\\\\\","access":"ro"}, +{"value":"/nvx/data-1/work,c /,rw","target":"/nvx/data-1/work","host_path":"c /","access":"rw"}, +{"value":"/run/\u00e9/a,,b\u0000,ro","target":"/run/\u00e9/a","host_path":",b\u0000","access":"ro"}, +{"value":"/work/etc,a\u0000UNC\u00e9","target":"/work/etc","host_path":"a\u0000UNC\u00e9","access":"ro"}, +{"value":"/a/a/etc,,","error":"--mount host path is empty"}, +{"value":"/run/nvx,x,y.x,y\\\\,ro","target":"/run/nvx","host_path":"x,y.x,y\\\\","access":"ro"}, +{"value":"/work/etc,? x,y,ro","target":"/work/etc","host_path":"? x,y","access":"ro"}, +{"value":"/data-1/srv,UNC.C","target":"/data-1/srv","host_path":"UNC.C","access":"ro"}, +{"value":"/\u00e9/srv,..","target":"/\u00e9/srv","host_path":"..","access":"ro"}, +{"value":"/\u00e9/run,\\","target":"/\u00e9/run","host_path":"\\","access":"ro"}, +{"value":"/cache,a\\\\..,ro","target":"/cache","host_path":"a\\\\..","access":"ro"}, +{"value":"/\u00e9/data-1/srv,x,y//,rw","target":"/\u00e9/data-1/srv","host_path":"x,y//","access":"rw"}, +{"value":"/data-1/etc/etc,b\u00e9:b,rw","target":"/data-1/etc/etc","host_path":"b\u00e9:b","access":"rw"}, +{"value":"/data-1/run,\u0000C,RW","error":"unsupported --mount mode 'RW'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"value":"/nvx/cache,C,rw","target":"/nvx/cache","host_path":"C","access":"rw"}, +{"value":"/run/data-1/srv,:UNC\u0000","target":"/run/data-1/srv","host_path":":UNC\u0000","access":"ro"}, +{"value":"/nvx,C,ro","target":"/nvx","host_path":"C","access":"ro"}, +{"value":"/cache/run/etc,// a,rw","target":"/cache/run/etc","host_path":"// a","access":"rw"} +], +"mount_targets": [ +{"target":"","error":"invalid sandbox mount target '': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/","error":"invalid sandbox mount target '/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"workspace","error":"invalid sandbox mount target 'workspace': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/workspace/","error":"invalid sandbox mount target '/workspace/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work//space","error":"invalid sandbox mount target '/work//space': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work/./space","error":"invalid sandbox mount target '/work/./space': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work/../space","error":"invalid sandbox mount target '/work/../space': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work space","error":"invalid sandbox mount target '/work space': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work=space","error":"invalid sandbox mount target '/work=space': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work\\space","error":"invalid sandbox mount target '/work\\\\space': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"invalid sandbox mount target '/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":null}, +{"target":"/\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"invalid sandbox mount target '/\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/proc","error":"sandbox mount target /proc overlaps the reserved /proc tree"}, +{"target":"/proc/self","error":"sandbox mount target /proc/self overlaps the reserved /proc tree"}, +{"target":"/sys/fs","error":"sandbox mount target /sys/fs overlaps the reserved /sys tree"}, +{"target":"/dev/shm","error":"sandbox mount target /dev/shm overlaps the reserved /dev tree"}, +{"target":"/.nvx-agent/tools","error":"sandbox mount target /.nvx-agent/tools overlaps the reserved /.nvx-agent tree"}, +{"target":"/etc","error":"sandbox mount target /etc is reserved"}, +{"target":"/etc/machine-id","error":"sandbox mount target /etc/machine-id overlaps the reserved /etc/machine-id tree"}, +{"target":"/etc/machine-id/child","error":"sandbox mount target /etc/machine-id/child overlaps the reserved /etc/machine-id tree"}, +{"target":"/workspace","error":null}, +{"target":"/procfs","error":null}, +{"target":"/etc/app","error":null}, +{"target":"/run","error":"sandbox mount target /run is reserved"}, +{"target":"/run/nvx","error":"sandbox mount target /run/nvx is reserved"}, +{"target":"/run/nvx/time","error":"sandbox mount target /run/nvx/time overlaps the reserved /run/nvx/time tree"}, +{"target":"/run/nvx/shares","error":null}, +{"target":"nvxproc\u3000=a\u001c\u00e9","error":"invalid sandbox mount target 'nvxproc\\u3000=a\\x1c\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"a /etc","error":"invalid sandbox mount target 'a /etc': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"=\u0000run","error":"invalid sandbox mount target '=\\x00run': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"..,","error":"invalid sandbox mount target '..,': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"work./","error":"invalid sandbox mount target 'work./': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u001c","error":"invalid sandbox mount target '\\x1c': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"..machine-idetc\u001cetc ","error":"invalid sandbox mount target '..machine-idetc\\x1cetc ': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/\\\u001c\u001cetcnvx","error":"invalid sandbox mount target '/\\\\\\x1c\\x1cetcnvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":".run ","error":"invalid sandbox mount target '.run ': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u0000machine-idproc\\proc/ proc","error":"invalid sandbox mount target '\\x00machine-idproc\\\\proc/ proc': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u001c workwork","error":"invalid sandbox mount target '\\x1c workwork': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":" ","error":"invalid sandbox mount target ' ': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u3000run\u001c,,,\u3000","error":"invalid sandbox mount target '\\u3000run\\x1c,,,\\u3000': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"procetc,.\\\u3000nvxmachine-id","error":"invalid sandbox mount target 'procetc,.\\\\\\u3000nvxmachine-id': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":",\\../\u3000","error":"invalid sandbox mount target ',\\\\../\\u3000': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"work","error":"invalid sandbox mount target 'work': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u3000etc,\u3000..run","error":"invalid sandbox mount target '\\u3000etc,\\u3000..run': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":",runamachine-id\u0000 ,","error":"invalid sandbox mount target ',runamachine-id\\x00 ,': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"nvx\u001c\\a","error":"invalid sandbox mount target 'nvx\\x1c\\\\a': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\\","error":"invalid sandbox mount target '\\\\': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"worketc=work.","error":"invalid sandbox mount target 'worketc=work.': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"nvx","error":"invalid sandbox mount target 'nvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":" \\","error":"invalid sandbox mount target ' \\\\': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":",machine-idnvx","error":"invalid sandbox mount target ',machine-idnvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"","error":"invalid sandbox mount target '': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"etcshares=shares\u001c","error":"invalid sandbox mount target 'etcshares=shares\\x1c': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u3000\u001c.. shares","error":"invalid sandbox mount target '\\u3000\\x1c.. shares': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"a\u3000etcrun..\u00e9.","error":"invalid sandbox mount target 'a\\u3000etcrun..\u00e9.': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"proc.\u00e9\u3000","error":"invalid sandbox mount target 'proc.\u00e9\\u3000': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":",machine-id...=","error":"invalid sandbox mount target ',machine-id...=': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"aaetc\u001ca/","error":"invalid sandbox mount target 'aaetc\\x1ca/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":".\\\u3000/","error":"invalid sandbox mount target '.\\\\\\u3000/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u0000runa","error":"invalid sandbox mount target '\\x00runa': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"etc\u3000\u001c","error":"invalid sandbox mount target 'etc\\u3000\\x1c': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u3000nvx\u0000","error":"invalid sandbox mount target '\\u3000nvx\\x00': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":" ==machine-id.\u0000aa","error":"invalid sandbox mount target ' ==machine-id.\\x00aa': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\\/\u00e9\u3000.\u001cmachine-id","error":"invalid sandbox mount target '\\\\/\u00e9\\u3000.\\x1cmachine-id': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"=\u00e9","error":"invalid sandbox mount target '=\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":" run\u3000work\\","error":"invalid sandbox mount target ' run\\u3000work\\\\': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"=\u3000 \u001c","error":"invalid sandbox mount target '=\\u3000 \\x1c': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\\nvxprocprocwork","error":"invalid sandbox mount target '\\\\nvxprocprocwork': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"a\u001csharesnvxwork","error":"invalid sandbox mount target 'a\\x1csharesnvxwork': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u001c\\\u001c\u0000 \u00e9","error":"invalid sandbox mount target '\\x1c\\\\\\x1c\\x00 \u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"a .shares\u0000=etc","error":"invalid sandbox mount target 'a .shares\\x00=etc': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"etc./","error":"invalid sandbox mount target 'etc./': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u3000","error":"invalid sandbox mount target '\\u3000': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"machine-idnvx","error":"invalid sandbox mount target 'machine-idnvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\\run.procshares","error":"invalid sandbox mount target '\\\\run.procshares': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"..etc/ ","error":"invalid sandbox mount target '..etc/ ': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"machine-ida\u0000 \\arun=","error":"invalid sandbox mount target 'machine-ida\\x00 \\\\arun=': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"=procrunetc\u3000","error":"invalid sandbox mount target '=procrunetc\\u3000': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u00e9nvx\u00e9\u001c\u001c\u3000nvx","error":"invalid sandbox mount target '\u00e9nvx\u00e9\\x1c\\x1c\\u3000nvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\\","error":"invalid sandbox mount target '\\\\': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"","error":"invalid sandbox mount target '': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"etc","error":"invalid sandbox mount target 'etc': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"proc\u001c\u3000\u0000//","error":"invalid sandbox mount target 'proc\\x1c\\u3000\\x00//': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"runnvx ,","error":"invalid sandbox mount target 'runnvx ,': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"machine-idproc...,.\u0000\u3000","error":"invalid sandbox mount target 'machine-idproc...,.\\x00\\u3000': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":" \u0000\\procrun=\u0000\\","error":"invalid sandbox mount target ' \\x00\\\\procrun=\\x00\\\\': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"nvx..,proc/shares,run","error":"invalid sandbox mount target 'nvx..,proc/shares,run': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"nvx../nvxa..","error":"invalid sandbox mount target 'nvx../nvxa..': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u001cnvx","error":"invalid sandbox mount target '\\x1cnvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u0000nvx.","error":"invalid sandbox mount target '\\x00nvx.': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"machine-ida\u3000\u3000work\u3000proc\u00e9","error":"invalid sandbox mount target 'machine-ida\\u3000\\u3000work\\u3000proc\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"sharesmachine-id\u3000a\u00e9","error":"invalid sandbox mount target 'sharesmachine-id\\u3000a\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"nvxshares ..nvx","error":"invalid sandbox mount target 'nvxshares ..nvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u0000proc,run","error":"invalid sandbox mount target '\\x00proc,run': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"nvxproc/run\u001crun","error":"invalid sandbox mount target 'nvxproc/run\\x1crun': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\\\\\u3000/\u00e9=/","error":"invalid sandbox mount target '\\\\\\\\\\u3000/\u00e9=/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"","error":"invalid sandbox mount target '': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"machine-id","error":"invalid sandbox mount target 'machine-id': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"proc/.\u0000work \u00e9a","error":"invalid sandbox mount target 'proc/.\\x00work \u00e9a': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/=shares","error":"invalid sandbox mount target '/=shares': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":" \u001c","error":"invalid sandbox mount target ' \\x1c': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"","error":"invalid sandbox mount target '': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":",..\u00e9,work","error":"invalid sandbox mount target ',..\u00e9,work': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"machine-id\u001c","error":"invalid sandbox mount target 'machine-id\\x1c': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"","error":"invalid sandbox mount target '': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u001cetc=,run proc","error":"invalid sandbox mount target '\\x1cetc=,run proc': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u3000","error":"invalid sandbox mount target '\\u3000': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"..\u0000\u3000nvx","error":"invalid sandbox mount target '..\\x00\\u3000nvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"etcmachine-id.\u001c\u0000sharesnvxrun","error":"invalid sandbox mount target 'etcmachine-id.\\x1c\\x00sharesnvxrun': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"etcnvx\\","error":"invalid sandbox mount target 'etcnvx\\\\': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\\\u0000\\etc.,","error":"invalid sandbox mount target '\\\\\\x00\\\\etc.,': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":".. shares../run","error":"invalid sandbox mount target '.. shares../run': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"work","error":"invalid sandbox mount target 'work': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"","error":"invalid sandbox mount target '': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u001c\u0000\u001c","error":"invalid sandbox mount target '\\x1c\\x00\\x1c': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"","error":"invalid sandbox mount target '': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"proc","error":"invalid sandbox mount target 'proc': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":" .","error":"invalid sandbox mount target ' .': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":" ","error":"invalid sandbox mount target ' ': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/\u3000=\\machine-id\u00e9 a","error":"invalid sandbox mount target '/\\u3000=\\\\machine-id\u00e9 a': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"\u00e9\u001c\\.\\","error":"invalid sandbox mount target '\u00e9\\x1c\\\\.\\\\': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"shares\u3000,\u3000sharesrun","error":"invalid sandbox mount target 'shares\\u3000,\\u3000sharesrun': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"run..shares,\u00e9\u00e9","error":"invalid sandbox mount target 'run..shares,\u00e9\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"etcetc\u0000work\\\u0000run","error":"invalid sandbox mount target 'etcetc\\x00work\\\\\\x00run': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":" asharesnvxnvx","error":"invalid sandbox mount target ' asharesnvxnvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"etc\u0000etcetc..\u0000anvx","error":"invalid sandbox mount target 'etc\\x00etcetc..\\x00anvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"etc","error":"invalid sandbox mount target 'etc': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/nvx/srv/\u00e9","error":null}, +{"target":"/\u00e9/nvx/srv","error":null}, +{"target":"/nvx/cache","error":null}, +{"target":"/\u00e9/run/\u00e9","error":null}, +{"target":"/\u00e9","error":null}, +{"target":"/srv","error":null}, +{"target":"/srv","error":null}, +{"target":"/nvx","error":null}, +{"target":"/nvx","error":null}, +{"target":"//work/\u00e9","error":"invalid sandbox mount target '//work/\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work/srv","error":null}, +{"target":"/run/,etc/data-1","error":"invalid sandbox mount target '/run/,etc/data-1': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/a","error":null}, +{"target":"/etc","error":"sandbox mount target /etc is reserved"}, +{"target":"/run/\u00e9","error":null}, +{"target":"/run","error":"sandbox mount target /run is reserved"}, +{"target":"/nvx/work","error":null}, +{"target":"/a/cache/etc","error":null}, +{"target":"/a/\u001c=","error":"invalid sandbox mount target '/a/\\x1c=': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/srv/run/nvx","error":null}, +{"target":"/work/srv/etc","error":null}, +{"target":"/etc/etc/cache","error":null}, +{"target":"/run","error":"sandbox mount target /run is reserved"}, +{"target":"/srv","error":null}, +{"target":"/srv/work/","error":"invalid sandbox mount target '/srv/work/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/\u00e9/a","error":null}, +{"target":"/data-1/srv","error":null}, +{"target":"/run/\u00e9","error":null}, +{"target":"/nvx/etc","error":null}, +{"target":"/","error":"invalid sandbox mount target '/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work/work","error":null}, +{"target":"/data-1/run","error":null}, +{"target":"/work/work/cache","error":null}, +{"target":"/work/run../data-1","error":null}, +{"target":"/\u00e9/cache","error":null}, +{"target":"/etc/run","error":null}, +{"target":"/run/nvx/\u00e9","error":null}, +{"target":"/run/a/nvx","error":null}, +{"target":"/data-1/srv/data-1","error":null}, +{"target":"/data-1","error":null}, +{"target":"/run/srv/a","error":null}, +{"target":"/nvx","error":null}, +{"target":"/etc","error":"sandbox mount target /etc is reserved"}, +{"target":"/data-1","error":null}, +{"target":"/srv","error":null}, +{"target":"/cache","error":null}, +{"target":"/run/srv","error":null}, +{"target":"/etc/etc","error":null}, +{"target":"/work/work","error":null}, +{"target":"/work/etc/\u3000\u001cmachine-id","error":"invalid sandbox mount target '/work/etc/\\u3000\\x1cmachine-id': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/a/\u00e9/\u00e9","error":null}, +{"target":"/etc/a/work","error":null}, +{"target":"/etc/cache/data-1","error":null}, +{"target":"/\u00e9","error":null}, +{"target":"/nvx","error":null}, +{"target":"/data-1/data-1/work","error":null}, +{"target":"/\u3000","error":"invalid sandbox mount target '/\\u3000': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work/nvx/a","error":null}, +{"target":"/a/run/data-1","error":null}, +{"target":"/run/\u00e9/srv","error":null}, +{"target":"/\u00e9/nvx","error":null}, +{"target":"/run","error":"sandbox mount target /run is reserved"}, +{"target":"/srv/run","error":null}, +{"target":"/run","error":"sandbox mount target /run is reserved"}, +{"target":"/nvx","error":null}, +{"target":"/cache","error":null}, +{"target":"/data-1","error":null}, +{"target":"/work/srv","error":null}, +{"target":"/proc","error":"sandbox mount target /proc overlaps the reserved /proc tree"}, +{"target":"/cache/a","error":null}, +{"target":"/cache","error":null}, +{"target":"/nvx/srv/work","error":null}, +{"target":"/etc/run","error":null}, +{"target":"/nvx/\u00e9","error":null}, +{"target":"/\u00e9","error":null}, +{"target":"/work","error":null}, +{"target":"/srv","error":null}, +{"target":"/etc//data-1","error":"invalid sandbox mount target '/etc//data-1': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/srv/a/\u00e9","error":null}, +{"target":"/etc/nvx","error":null}, +{"target":"/run/\u00e9","error":null}, +{"target":"/run/srv","error":null}, +{"target":"/run","error":"sandbox mount target /run is reserved"}, +{"target":"/run","error":"sandbox mount target /run is reserved"}, +{"target":"/srv","error":null}, +{"target":"/nvx/work","error":null}, +{"target":"/a/data-1","error":null}, +{"target":"/data-1/run/data-1","error":null}, +{"target":"/nvx/data-1/nvx","error":null}, +{"target":"//work/run","error":"invalid sandbox mount target '//work/run': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"target":"/work/nvx/nvx","error":null}, +{"target":"/etc/work/nvx","error":null}, +{"target":"/etc/srv","error":null}, +{"target":"/srv/etc","error":null}, +{"target":"/srv/\u00e9","error":null}, +{"target":"/work","error":null}, +{"target":"/nvx/work/a","error":null}, +{"target":"/work","error":null}, +{"target":"/work/etc","error":null}, +{"target":"/data-1/work","error":null} +], +"child_names": [ +{"index":0,"target":"/workspace","name":"0-c52ddf65534b7b46035084358ab7902b"}, +{"index":1,"target":"/workspace","name":"1-c52ddf65534b7b46035084358ab7902b"}, +{"index":999,"target":"/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","name":"999-cc75cd4428893a920419e41a6f2a00e4"}, +{"index":36,"target":"\u0000,machine-id proc","name":"36-a838dea7fcd4b1001d75151adcb4f1ce"}, +{"index":38,"target":"\u00e9..\u3000\\\u00e9/..","name":"38-13c26a98529e421f416d9d0f4a55805e"}, +{"index":6,"target":"runrunnvxwork ","name":"6-012351eb5c89460f34c10da2d10b5613"}, +{"index":43,"target":" procrun","name":"43-56d00e23134b6d56074b18c361b20dda"}, +{"index":3,"target":"etc\u3000..\u0000..","name":"3-65e3d59d391216b6183b1f837872181c"}, +{"index":50,"target":"machine-idprocnvx \u0000..work","name":"50-7f35b4dd127dafb8c3adc0e244cbfc14"}, +{"index":3,"target":" /amachine-idwork ,nvx","name":"3-b099591d4d744194452f205d859b4fe9"}, +{"index":32,"target":"\u0000.\u0000\u001c","name":"32-5017c11c8eb44e6ee24caff902a0ba67"}, +{"index":16,"target":"aprocrun/ ","name":"16-048031d8dc860fc54d825b7fb48a964a"}, +{"index":31,"target":"\u00e9","name":"31-4a99557e4033c3539de2eb65472017ca"}, +{"index":38,"target":"sharesmachine-id\u00e9\u0000\u0000proc=","name":"38-e400653deaa66bbe8a3280932ad0db6a"}, +{"index":23,"target":"work/\u001ca","name":"23-6c082eefd36340c75c073da5e38c85f5"}, +{"index":16,"target":"etcshares","name":"16-d9aebd32d9b4a90f878f508626e14474"}, +{"index":37,"target":"nvxrun\u00e9/,\u00e9\\","name":"37-da36198ba86793cf1a8fafff5574cce6"}, +{"index":30,"target":"\u001c/\u00e9\u3000shares","name":"30-dd5d4d060454a9b28d3ccb5c91c26082"}, +{"index":48,"target":"\u001c","name":"48-bd4fc42a21f1f860a1030e6eba23d53e"}, +{"index":39,"target":".....a..\\","name":"39-e125453adf94181f3bc359a7d93652ff"}, +{"index":41,"target":"runetcproc..\u3000.\\","name":"41-bf67bfc1b5e7e84572f14d2dd338378c"}, +{"index":13,"target":"\u00e9\\a ","name":"13-db7d12da777d5e1ea2ca24f934c9f38b"}, +{"index":30,"target":"\u00e9sharesproc\u00e9","name":"30-a262c664c07597843b776e35445f40e4"}, +{"index":44,"target":"a\u3000..proc","name":"44-790c04e63985d94f1364a5618f66f880"}, +{"index":24,"target":".\\=\u001c","name":"24-62e2d4b9f332d7aee89decb92dd30ba1"}, +{"index":32,"target":".etcnvx\u00e9\\\u0000","name":"32-6f6bdd93e820d5e3c0f3a7a8182063f1"}, +{"index":7,"target":"work","name":"7-00e13ed7af55b27622f1d6eab5bec014"}, +{"index":24,"target":",..run \u3000,..proc","name":"24-4b355337f01c4b8ea483431c32e9a3d0"}, +{"index":20,"target":"/","name":"20-8a5edab282632443219e051e4ade2d1d"}, +{"index":33,"target":" machine-id\\machine-id","name":"33-5a42917d15923698ee808a82626e8c31"}, +{"index":41,"target":".","name":"41-cdb4ee2aea69cc6a83331bbe96dc2caa"}, +{"index":9,"target":"\u001c..=","name":"9-081ce382fa80ab8fd738aa4144d536cc"}, +{"index":44,"target":"\u00e9work\u0000nvx=work\\","name":"44-465ea7ff674e5d2f57c6a4e0642f06fc"}, +{"index":36,"target":"\\proc..proc\u3000 ","name":"36-562834b582a54a4a6140a1407d7a2dc6"}, +{"index":27,"target":"machine-idsharesmachine-id.sharesrun","name":"27-539b8053a9211f7575a9180e4cb656ae"}, +{"index":30,"target":"nvx\u00e9/.","name":"30-00a51cad797dc02ae8799c4b38afdbab"}, +{"index":40,"target":"etc","name":"40-812de6e718f869feb16b45c6bbcfdb12"}, +{"index":8,"target":"=..run=proc","name":"8-947452e5e75a653fc6f8379c8da140d8"}, +{"index":42,"target":"work..machine-id\\a..,","name":"42-6c1fa9d46082b15f3161fe3fbcf5696b"}, +{"index":49,"target":"=.\u0000","name":"49-0b799fc0366e68b0fe573c69ec829bc5"}, +{"index":26,"target":"shares/work","name":"26-552a31c86bda15bf23d5a836ca2626b5"}, +{"index":7,"target":"=nvxnvx\u00e9","name":"7-004790cb1b6b3f790a8d739f45bd8331"}, +{"index":47,"target":"=\u3000\u3000","name":"47-c5273a1453d7bd93408d21a201af377f"}, +{"index":39,"target":"/\u0000\u3000proc/\u00e9","name":"39-c562e50aa269bbacb287e3a2d4efa82a"}, +{"index":33,"target":"nvxrun=ashares,..run","name":"33-d2104fab3f53feafc75587df245debe1"}, +{"index":8,"target":"workrunprocetc","name":"8-ee964ad063b2c879c80b78d3a52dd198"}, +{"index":37,"target":"etc\u00e9,etcwork=,machine-id","name":"37-b426aa229016764fef4eb415e7bae746"}, +{"index":17,"target":",","name":"17-d03502c43d74a30b936740a9517dc4ea"}, +{"index":45,"target":"a,.","name":"45-5b17b875efaf4bd0e46540d09eb4d9f7"}, +{"index":49,"target":"nvx.work","name":"49-190ca4e761d3a4238de8365ae7148985"}, +{"index":28,"target":"run","name":"28-acba25512100f80b56fc3ccd14c65be5"}, +{"index":30,"target":"etc.etcrun\u001cproc\u0000","name":"30-a1326f38320fd1f16228cb2ecf0a6216"}, +{"index":23,"target":"\u3000\u0000\u00e9aa","name":"23-8cdb4274efbdc4b6c1a83a7818d4c388"} +], +"mounts": [ +{"flavor":"posix","value":"/opt/hostedtoolcache,host-dir,rw","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/opt/hostedtoolcache","host_path":"host-dir","access":"rw","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/opt/hostedtoolcache,host-dir,rw","--mount-deny","logs","--mount-deny","secrets"],"fragment":" virtfs_dir=/opt/hostedtoolcache virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/opt/hostedtoolcache","host_path":"/fixture/cwd/host-dir","access":"rw","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/host-dir/logs","/fixture/cwd/host-dir/secrets"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/tmp/mcp-runtime,host-dir,rw","denied":["mcp-logs"],"allowed":["mcp-logs/payloads"],"writable":["agent","cache"],"owner":"caller","kind":"directory","mount":{"guest_target":"/tmp/mcp-runtime","host_path":"host-dir","access":"rw","denied":["mcp-logs"],"allowed":["mcp-logs/payloads"],"writable":["agent","cache"],"owner":"caller","kind":"directory"},"arguments":["--mount","/tmp/mcp-runtime,host-dir,rw","--mount-deny","mcp-logs","--mount-allow","mcp-logs/payloads","--mount-write","agent","--mount-write","cache","--mount-owner","caller"],"fragment":" virtfs_dir=/tmp/mcp-runtime virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/tmp/mcp-runtime","host_path":"/fixture/cwd/host-dir","access":"rw","denied":["mcp-logs"],"allowed":["mcp-logs/payloads"],"writable":["agent","cache"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/host-dir/mcp-logs"]],["allowed",["/fixture/cwd/host-dir/mcp-logs/payloads"]],["writable",["/fixture/cwd/host-dir/agent","/fixture/cwd/host-dir/cache"]]]}, +{"flavor":"posix","value":"/w,host,ro","denied":[],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory","error":"--mount-write narrows the writes of a read-write share; --mount /w is read-only"}, +{"flavor":"posix","value":"/w,host,rw","denied":[],"allowed":["path-0","path-1","path-2","path-3","path-4","path-5","path-6","path-7","path-8","path-9","path-10","path-11","path-12","path-13","path-14","path-15","path-16","path-17","path-18","path-19","path-20","path-21","path-22","path-23","path-24","path-25","path-26","path-27","path-28","path-29","path-30","path-31","path-32","path-33","path-34","path-35","path-36","path-37","path-38","path-39","path-40","path-41","path-42","path-43","path-44","path-45","path-46","path-47","path-48","path-49","path-50","path-51","path-52","path-53","path-54","path-55","path-56","path-57","path-58","path-59","path-60","path-61","path-62","path-63","path-64","path-65","path-66","path-67","path-68","path-69","path-70","path-71","path-72","path-73","path-74","path-75","path-76","path-77","path-78","path-79","path-80","path-81","path-82","path-83","path-84","path-85","path-86","path-87","path-88","path-89","path-90","path-91","path-92","path-93","path-94","path-95","path-96","path-97","path-98","path-99","path-100","path-101","path-102","path-103","path-104","path-105","path-106","path-107","path-108","path-109","path-110","path-111","path-112","path-113","path-114","path-115","path-116","path-117","path-118","path-119","path-120","path-121","path-122","path-123","path-124","path-125","path-126","path-127","path-128"],"writable":[],"owner":"vmm","kind":"directory","error":"a sandbox mount permits at most 128 allowed paths"}, +{"flavor":"posix","value":"/etc/app/settings.json,settings.json,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/etc/app/settings.json","host_path":"settings.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-3ddd35a70a0d7386a5451cd0d5dc3c39,settings.json,ro,file"],"fragment":" virtfs_dir=/etc/app/settings.json virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/etc/app/settings.json","host_path":"/fixture/cwd/settings.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/a,C:\\data\\x,y,rx","denied":["C:\\abs\\out","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"posix","value":"/nvx,tools/cache,rw","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":["/abs/out","cache"],"owner":"caller","kind":"directory","mount":{"guest_target":"/nvx","host_path":"tools/cache","access":"rw","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":["/abs/out","cache"],"owner":"caller","kind":"directory"},"arguments":["--mount","/nvx,tools/cache,rw","--mount-deny","logs","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-write","/abs/out","--mount-write","cache","--mount-owner","caller"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvx","host_path":"/fixture/cwd/tools/cache","access":"rw","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":["/abs/out","cache"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/tools/cache/logs","/fixture/cwd/tools/cache/secrets"]],["allowed",["/fixture/cwd/tools/cache/logs/payloads"]],["writable",["/abs/out","/fixture/cwd/tools/cache/cache"]]]}, +{"flavor":"posix","value":"/run/a,w//x/.,ro","denied":["secrets","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/a","host_path":"w/x","access":"ro","denied":["secrets","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/a,w/x,ro","--mount-deny","secrets","--mount-deny","/abs/out"],"fragment":" virtfs_dir=/run/a virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/a","host_path":"/fixture/cwd/w/x","access":"ro","denied":["secrets","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/w/x/secrets","/abs/out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/run,tools/cache,rw","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":["/abs/out","cache"],"owner":"caller","kind":"directory","error":"sandbox mount target /run is reserved"}, +{"flavor":"posix","value":"/data-1/data-1/cache,C:\\data\\x,y,rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory","mount":{"guest_target":"/data-1/data-1/cache","host_path":"C:\\data\\x,y","access":"rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory"},"arguments":["--mount","/data-1/data-1/cache,C:\\data\\x,y,rw","--mount-deny","/abs/out","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-write","cache","--mount-owner","caller"],"fragment":" virtfs_dir=/data-1/data-1/cache virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/data-1/data-1/cache","host_path":"/fixture/cwd/C:\\data\\x,y","access":"rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/abs/out","/fixture/cwd/C:\\data\\x,y/secrets"]],["allowed",["/fixture/cwd/C:\\data\\x,y/logs/payloads"]],["writable",["/fixture/cwd/C:\\data\\x,y/cache"]]]}, +{"flavor":"posix","value":"/nvx/\u00e9,a/../b,rw","denied":[],"allowed":[],"writable":["out","cache"],"owner":"caller","kind":"directory","error":"sandbox mount host path contains a parent component: a/../b"}, +{"flavor":"posix","value":"/work,?//a,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work","host_path":"?/a","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work,?/a,rw"],"fragment":" virtfs_dir=/work virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work","host_path":"/fixture/cwd/?/a","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/nvx/data-1/a,a/../b","denied":["logs","C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount host path contains a parent component: a/../b"}, +{"flavor":"posix","value":"/a,C:/w,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/a","host_path":"C:/w","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-6a50dc8584134c7de537c0052ff6d236,C:/w,ro,file","--mount-owner","caller"],"fragment":" virtfs_dir=/a virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/a","host_path":"/fixture/cwd/C:/w","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/work/run,C:/w","denied":["/abs/out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work/run","host_path":"C:/w","access":"ro","denied":["/abs/out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work/run,C:/w,ro","--mount-deny","/abs/out","--mount-deny","logs"],"fragment":" virtfs_dir=/work/run virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/work/run","host_path":"/fixture/cwd/C:/w","access":"ro","denied":["/abs/out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/abs/out","/fixture/cwd/C:/w/logs"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/etc/work/data-1,x,y\u0000x,y,ro","denied":["logs","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount host path contains a NUL character: 'x,y\\x00x,y'"}, +{"flavor":"posix","value":"/srv,C:\\data\\x,y,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv,C:\\data\\x,y,ro","--mount-owner","caller"],"fragment":" virtfs_dir=/srv virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/srv","host_path":"/fixture/cwd/C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/a b,a\u0000b,ro","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/nvx/cache,C:/w,rx","denied":["secrets","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"posix","value":"/work/a,/srv/data,rw","denied":["C:\\abs\\out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory","mount":{"guest_target":"/work/a","host_path":"/srv/data","access":"rw","denied":["C:\\abs\\out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory"},"arguments":["--mount","/work/a,/srv/data,rw","--mount-deny","C:\\abs\\out","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-write","cache","--mount-owner","caller"],"fragment":" virtfs_dir=/work/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work/a","host_path":"/srv/data","access":"rw","denied":["C:\\abs\\out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/srv/data/C:\\abs\\out","/srv/data/secrets"]],["allowed",["/srv/data/logs/payloads"]],["writable",["/srv/data/cache"]]]}, +{"flavor":"posix","value":"/,C:\\data\\x,y,ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/work/a,C:\\data\\x,y,rw","denied":["secrets","C:\\abs\\out"],"allowed":[],"writable":["cache"],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work/a","host_path":"C:\\data\\x,y","access":"rw","denied":["secrets","C:\\abs\\out"],"allowed":[],"writable":["cache"],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work/a,C:\\data\\x,y,rw","--mount-deny","secrets","--mount-deny","C:\\abs\\out","--mount-write","cache"],"fragment":" virtfs_dir=/work/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work/a","host_path":"/fixture/cwd/C:\\data\\x,y","access":"rw","denied":["secrets","C:\\abs\\out"],"allowed":[],"writable":["cache"],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:\\data\\x,y/secrets","/fixture/cwd/C:\\data\\x,y/C:\\abs\\out"]],["allowed",[]],["writable",["/fixture/cwd/C:\\data\\x,y/cache"]]]}, +{"flavor":"posix","value":"/nvx/nvx,work,ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/nvx/nvx","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/nvx/nvx,work,ro","--mount-deny","logs","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/nvx/nvx virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx/nvx","host_path":"/fixture/cwd/work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/work/logs","/fixture/cwd/work/secrets"]],["allowed",["/fixture/cwd/work/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/work,w//x/.,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/work","host_path":"w/x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-0c9a453fad615c83c57d57c2b2be05fa,w/x,ro,file","--mount-owner","caller"],"fragment":" virtfs_dir=/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/work","host_path":"/fixture/cwd/w/x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/\u00e9/srv,C:\\data\\x,y,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/\u00e9/srv","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-b52d86793a80d4b693be52d859a1fd8a,C:\\data\\x,y,ro,file"],"fragment":" virtfs_dir=/\u00e9/srv virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/\u00e9/srv","host_path":"/fixture/cwd/C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/etc,w//x/.","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/a,C:/w,rw","denied":["/abs/out"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory","mount":{"guest_target":"/a","host_path":"C:/w","access":"rw","denied":["/abs/out"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"},"arguments":["--mount","/a,C:/w,rw","--mount-deny","/abs/out","--mount-write","out","--mount-owner","caller"],"fragment":" virtfs_dir=/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/a","host_path":"/fixture/cwd/C:/w","access":"rw","denied":["/abs/out"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/abs/out"]],["allowed",[]],["writable",["/fixture/cwd/C:/w/out"]]]}, +{"flavor":"posix","value":"/a/run,C:/w","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/a/run","host_path":"C:/w","access":"ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/a/run,C:/w,ro","--mount-deny","C:\\abs\\out","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/a/run virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/a/run","host_path":"/fixture/cwd/C:/w","access":"ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:/w/C:\\abs\\out"]],["allowed",["/fixture/cwd/C:/w/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/etc,C:/w,ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/etc,w//x/.,rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/cache/etc/\u00e9,C:\\data\\x,y,rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/cache/etc/\u00e9","host_path":"C:\\data\\x,y","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-3d35e19069b9b96c28742438e4299b52,C:\\data\\x,y,rw,file","--mount-owner","caller"],"fragment":" virtfs_dir=/cache/etc/\u00e9 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/cache/etc/\u00e9","host_path":"/fixture/cwd/C:\\data\\x,y","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/run/shares\u3000work/nvx,C:/w,rw","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/run/shares\\u3000work/nvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/run/\\.\u001c/\u00e9,/srv/data,rw","denied":[],"allowed":[],"writable":["cache","/abs/out"],"owner":"caller","kind":"file","error":"invalid sandbox mount target '/run/\\\\.\\x1c/\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/data-1/cache/\u00e9,C:\\data\\x,y","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"unsupported --mount mode 'y'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"posix","value":"/\u00e9/cache/a,C:\\data\\x,y,rw","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/\u00e9/cache/a","host_path":"C:\\data\\x,y","access":"rw","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/\u00e9/cache/a,C:\\data\\x,y,rw","--mount-deny","/abs/out","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/\u00e9/cache/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/\u00e9/cache/a","host_path":"/fixture/cwd/C:\\data\\x,y","access":"rw","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/abs/out"]],["allowed",["/fixture/cwd/C:\\data\\x,y/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/etc,C:/w,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/\u00e9/cache,/srv/data,rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/\u00e9/cache","host_path":"/srv/data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-93735fe6b8037c02e2ca771a5eb0fbe7,/srv/data,rw,file","--mount-owner","caller"],"fragment":" virtfs_dir=/\u00e9/cache virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/\u00e9/cache","host_path":"/srv/data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/machine-id\u001ca/etc/\u00e9,C:/w","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"file","error":"invalid sandbox mount target '/machine-id\\x1ca/etc/\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/etc/etc,w//x/.,rw","denied":["/abs/out"],"allowed":["logs/payloads","logs"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/etc/etc","host_path":"w/x","access":"rw","denied":["/abs/out"],"allowed":["logs/payloads","logs"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/etc/etc,w/x,rw","--mount-deny","/abs/out","--mount-allow","logs/payloads","--mount-allow","logs"],"fragment":" virtfs_dir=/etc/etc virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/etc/etc","host_path":"/fixture/cwd/w/x","access":"rw","denied":["/abs/out"],"allowed":["logs/payloads","logs"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/abs/out"]],["allowed",["/fixture/cwd/w/x/logs/payloads","/fixture/cwd/w/x/logs"]],["writable",[]]]}, +{"flavor":"posix","value":"/nvx/work/data-1,C:/w,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/nvx/work/data-1","host_path":"C:/w","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-f70c1ff3e0a8554d424014d45d31d57d,C:/w,rw,file"],"fragment":" virtfs_dir=/nvx/work/data-1 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvx/work/data-1","host_path":"/fixture/cwd/C:/w","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/work,C:/w","denied":[],"allowed":["logs"],"writable":[],"owner":"vmm","kind":"directory","error":"--mount-allow exposes a path inside a --mount-deny path of the same share, which has none"}, +{"flavor":"posix","value":"/run/nvx/cache,tools/cache","denied":["/abs/out","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/run/nvx/cache","host_path":"tools/cache","access":"ro","denied":["/abs/out","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/run/nvx/cache,tools/cache,ro","--mount-deny","/abs/out","--mount-deny","logs","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/run/nvx/cache virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/nvx/cache","host_path":"/fixture/cwd/tools/cache","access":"ro","denied":["/abs/out","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/abs/out","/fixture/cwd/tools/cache/logs"]],["allowed",["/fixture/cwd/tools/cache/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/work,C:\\data\\x,y,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work,C:\\data\\x,y,ro"],"fragment":" virtfs_dir=/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/work","host_path":"/fixture/cwd/C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/\u00e9/data-1/\u00e9,C:/w,ro","denied":["logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/\u00e9/data-1/\u00e9","host_path":"C:/w","access":"ro","denied":["logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/\u00e9/data-1/\u00e9,C:/w,ro","--mount-deny","logs","--mount-owner","caller"],"fragment":" virtfs_dir=/\u00e9/data-1/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/\u00e9/data-1/\u00e9","host_path":"/fixture/cwd/C:/w","access":"ro","denied":["logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:/w/logs"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/etc,/srv/data,ro","denied":["secrets","C:\\abs\\out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/srv/srv,C:/w,rw","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv/srv","host_path":"C:/w","access":"rw","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv/srv,C:/w,rw","--mount-deny","/abs/out","--mount-owner","caller"],"fragment":" virtfs_dir=/srv/srv virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/srv/srv","host_path":"/fixture/cwd/C:/w","access":"rw","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/abs/out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/etc,/srv/data,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/etc/\u00e9/\u00e9,tools/cache","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/etc/\u00e9/\u00e9","host_path":"tools/cache","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/etc/\u00e9/\u00e9,tools/cache,ro","--mount-deny","logs","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/etc/\u00e9/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/etc/\u00e9/\u00e9","host_path":"/fixture/cwd/tools/cache","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/tools/cache/logs"]],["allowed",["/fixture/cwd/tools/cache/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/etc/data-1/nvx,w//x/.","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/etc/data-1/nvx","host_path":"w/x","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/etc/data-1/nvx,w/x,ro","--mount-deny","secrets"],"fragment":" virtfs_dir=/etc/data-1/nvx virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/etc/data-1/nvx","host_path":"/fixture/cwd/w/x","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/w/x/secrets"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/a/\u00e9/srv,tools/cache","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/a/\u00e9/srv","host_path":"tools/cache","access":"ro","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/a/\u00e9/srv,tools/cache,ro","--mount-deny","C:\\abs\\out","--mount-deny","/abs/out","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/a/\u00e9/srv virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/a/\u00e9/srv","host_path":"/fixture/cwd/tools/cache","access":"ro","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/tools/cache/C:\\abs\\out","/abs/out"]],["allowed",["/fixture/cwd/tools/cache/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/run/a,tools/cache,rw","denied":[],"allowed":[],"writable":["cache","out","."],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/a","host_path":"tools/cache","access":"rw","denied":[],"allowed":[],"writable":["cache","out","."],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/a,tools/cache,rw","--mount-write","cache","--mount-write","out","--mount-write","."],"fragment":" virtfs_dir=/run/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/run/a","host_path":"/fixture/cwd/tools/cache","access":"rw","denied":[],"allowed":[],"writable":["cache","out","."],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",["/fixture/cwd/tools/cache/cache","/fixture/cwd/tools/cache/out","/fixture/cwd/tools/cache"]]]}, +{"flavor":"posix","value":"/nvx,C:\\data\\x,y,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/nvx","host_path":"C:\\data\\x,y","access":"rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/nvx,C:\\data\\x,y,rw","--mount-deny","secrets"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvx","host_path":"/fixture/cwd/C:\\data\\x,y","access":"rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:\\data\\x,y/secrets"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/a b,a/../b,rw","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/nvx,C:\\data\\x,y,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/nvx","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-30b37e7ae096735b949f4f464412ab3c,C:\\data\\x,y,ro,file"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx","host_path":"/fixture/cwd/C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/\u00e9,C:/w,ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/\u00e9","host_path":"C:/w","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/\u00e9,C:/w,ro","--mount-deny","C:\\abs\\out"],"fragment":" virtfs_dir=/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/\u00e9","host_path":"/fixture/cwd/C:/w","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:/w/C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/nvx/a/work,C:\\data\\x,y","denied":["a\u0000"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'y'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"posix","value":"/etc,C:\\data\\x,y,ro","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/work/\u3000\u001cwork,w//x/.,rw","denied":["logs"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/work/\\u3000\\x1cwork': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/run/\u00e9,w//x/.,ro","denied":["secrets"],"allowed":["out"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/\u00e9","host_path":"w/x","access":"ro","denied":["secrets"],"allowed":["out"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/\u00e9,w/x,ro","--mount-deny","secrets","--mount-allow","out"],"fragment":" virtfs_dir=/run/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/\u00e9","host_path":"/fixture/cwd/w/x","access":"ro","denied":["secrets"],"allowed":["out"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/w/x/secrets"]],["allowed",["/fixture/cwd/w/x/out"]],["writable",[]]]}, +{"flavor":"posix","value":"/cache/run,C:\\data\\x,y,rx","denied":["secrets"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"posix","value":"/data-1/data-1,work","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/data-1/data-1","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-fd1bb3d6d1bce0afe815991bf83f5962,work,ro,file"],"fragment":" virtfs_dir=/data-1/data-1 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1/data-1","host_path":"/fixture/cwd/work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/run/srv/data-1,tools/cache","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/run/srv/data-1","host_path":"tools/cache","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-a8f01611937352bf22a395a8f556cae1,tools/cache,ro,file","--mount-owner","caller"],"fragment":" virtfs_dir=/run/srv/data-1 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/srv/data-1","host_path":"/fixture/cwd/tools/cache","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/data-1/data-1/work,tools/cache,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/data-1/data-1/work","host_path":"tools/cache","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-308e5770e516a27164f075a92cce7fdb,tools/cache,ro,file"],"fragment":" virtfs_dir=/data-1/data-1/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1/data-1/work","host_path":"/fixture/cwd/tools/cache","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/a b,/srv/data","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/nvx,/srv/data,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/nvx","host_path":"/srv/data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-30b37e7ae096735b949f4f464412ab3c,/srv/data,rw,file"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvx","host_path":"/srv/data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/,runshares/run,C:\\data\\x,y","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'y'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"posix","value":"/srv/a,tools/cache,rx","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"posix","value":"/a,a\u0000b,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount host path contains a NUL character: 'a\\x00b'"}, +{"flavor":"posix","value":"/data-1/work,w//x/.","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/data-1/work","host_path":"w/x","access":"ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/data-1/work,w/x,ro","--mount-deny","C:\\abs\\out","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/data-1/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1/work","host_path":"/fixture/cwd/w/x","access":"ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/w/x/C:\\abs\\out"]],["allowed",["/fixture/cwd/w/x/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/etc/a/a,w//x/.","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"file","error":"--mount-deny, --mount-allow, and --mount-write name paths inside a shared directory, but --mount /etc/a/a shares a file, whose mode applies to the whole file"}, +{"flavor":"posix","value":"/\u00e9/etc,w//x/.","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/\u00e9/etc","host_path":"w/x","access":"ro","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/\u00e9/etc,w/x,ro","--mount-deny","C:\\abs\\out","--mount-deny","/abs/out","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/\u00e9/etc virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/\u00e9/etc","host_path":"/fixture/cwd/w/x","access":"ro","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/w/x/C:\\abs\\out","/abs/out"]],["allowed",["/fixture/cwd/w/x/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/etc,tools/cache","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/nvx/data-1,C:\\data\\x,y,ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/nvx/data-1","host_path":"C:\\data\\x,y","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/nvx/data-1,C:\\data\\x,y,ro","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/nvx/data-1 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx/data-1","host_path":"/fixture/cwd/C:\\data\\x,y","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:\\data\\x,y/secrets"]],["allowed",["/fixture/cwd/C:\\data\\x,y/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/etc,tools/cache","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/a b,/srv/data,rw","denied":["secrets","/abs/out"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/run/data-1/etc,tools/cache,rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out","/abs/out"],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/data-1/etc","host_path":"tools/cache","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out","/abs/out"],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/data-1/etc,tools/cache,rw","--mount-deny","logs","--mount-allow","logs/payloads","--mount-write","out","--mount-write","/abs/out"],"fragment":" virtfs_dir=/run/data-1/etc virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/run/data-1/etc","host_path":"/fixture/cwd/tools/cache","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out","/abs/out"],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/tools/cache/logs"]],["allowed",["/fixture/cwd/tools/cache/logs/payloads"]],["writable",["/fixture/cwd/tools/cache/out","/abs/out"]]]}, +{"flavor":"posix","value":"/nvx,/srv/data,ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/nvx","host_path":"/srv/data","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/nvx,/srv/data,ro","--mount-deny","/abs/out"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx","host_path":"/srv/data","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/abs/out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/a b,tools/cache,rw","denied":["secrets","/abs/out","a\u0000"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/run/work,tools/cache,ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/work","host_path":"tools/cache","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/work,tools/cache,ro","--mount-deny","C:\\abs\\out"],"fragment":" virtfs_dir=/run/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/work","host_path":"/fixture/cwd/tools/cache","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/tools/cache/C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"//srv/run,/srv/data","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '//srv/run': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/srv/srv,a/../b,rw","denied":["secrets","C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount host path contains a parent component: a/../b"}, +{"flavor":"posix","value":"/work,w//x/.,rw","denied":["logs","/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/work","host_path":"w/x","access":"rw","denied":["logs","/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/work,w/x,rw","--mount-deny","logs","--mount-deny","/abs/out","--mount-owner","caller"],"fragment":" virtfs_dir=/work virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work","host_path":"/fixture/cwd/w/x","access":"rw","denied":["logs","/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/w/x/logs","/abs/out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/a,work,rw","denied":["/abs/out","."],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory","mount":{"guest_target":"/a","host_path":"work","access":"rw","denied":["/abs/out","."],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"},"arguments":["--mount","/a,work,rw","--mount-deny","/abs/out","--mount-deny",".","--mount-allow","logs/payloads","--mount-write","out","--mount-owner","caller"],"fragment":" virtfs_dir=/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/a","host_path":"/fixture/cwd/work","access":"rw","denied":["/abs/out","."],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/abs/out","/fixture/cwd/work"]],["allowed",["/fixture/cwd/work/logs/payloads"]],["writable",["/fixture/cwd/work/out"]]]}, +{"flavor":"posix","value":"/etc/data-1,w//x/.,rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/etc/data-1","host_path":"w/x","access":"rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/etc/data-1,w/x,rw","--mount-deny","/abs/out","--mount-deny","secrets","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/etc/data-1 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/etc/data-1","host_path":"/fixture/cwd/w/x","access":"rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/abs/out","/fixture/cwd/w/x/secrets"]],["allowed",["/fixture/cwd/w/x/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/srv,w//x/.,rx","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"posix","value":"/\u00e9,/srv/data,rx","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"posix","value":"/srv/data-1/\u00e9,C:\\data\\x,y,ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv/data-1/\u00e9","host_path":"C:\\data\\x,y","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv/data-1/\u00e9,C:\\data\\x,y,ro","--mount-deny","C:\\abs\\out","--mount-owner","caller"],"fragment":" virtfs_dir=/srv/data-1/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/srv/data-1/\u00e9","host_path":"/fixture/cwd/C:\\data\\x,y","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:\\data\\x,y/C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/proc,a\u0000b,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","error":"sandbox mount target /proc overlaps the reserved /proc tree"}, +{"flavor":"posix","value":"/run,/srv/data,ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /run is reserved"}, +{"flavor":"posix","value":"/a/work,w//x/.,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/a/work","host_path":"w/x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/a/work,w/x,ro","--mount-owner","caller"],"fragment":" virtfs_dir=/a/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/a/work","host_path":"/fixture/cwd/w/x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/etc,tools/cache,ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"posix","value":"/nvx/a/a,/srv/data","denied":["/abs/out","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/nvx/a/a","host_path":"/srv/data","access":"ro","denied":["/abs/out","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/nvx/a/a,/srv/data,ro","--mount-deny","/abs/out","--mount-deny","secrets"],"fragment":" virtfs_dir=/nvx/a/a virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx/a/a","host_path":"/srv/data","access":"ro","denied":["/abs/out","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/abs/out","/srv/data/secrets"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/data-1/a,/srv/data","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/data-1/a","host_path":"/srv/data","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-4b25c272fa19ed009dce984c919893ec,/srv/data,ro,file"],"fragment":" virtfs_dir=/data-1/a virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1/a","host_path":"/srv/data","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/nvxrun,/srv/data,rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/nvxrun","host_path":"/srv/data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-6ab51ef8dc55669bf1c3c27372b1f413,/srv/data,rw,file","--mount-owner","caller"],"fragment":" virtfs_dir=/nvxrun virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvxrun","host_path":"/srv/data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/,/srv,tools/cache,rw","denied":[],"allowed":[],"writable":["/abs/out","cache"],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"posix","value":"/run/data-1,C:\\data\\x,y,rw","denied":["secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/data-1","host_path":"C:\\data\\x,y","access":"rw","denied":["secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/data-1,C:\\data\\x,y,rw","--mount-deny","secrets","--mount-write","out"],"fragment":" virtfs_dir=/run/data-1 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/run/data-1","host_path":"/fixture/cwd/C:\\data\\x,y","access":"rw","denied":["secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:\\data\\x,y/secrets"]],["allowed",[]],["writable",["/fixture/cwd/C:\\data\\x,y/out"]]]}, +{"flavor":"posix","value":"/nvx,/srv/data,rw","denied":["C:\\abs\\out","secrets"],"allowed":["logs/payloads"],"writable":["a\u0000"],"owner":"caller","kind":"directory","error":"sandbox mount writable paths must be nonempty"}, +{"flavor":"posix","value":"/work/data-1,C:\\data\\x,y,rw","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work/data-1","host_path":"C:\\data\\x,y","access":"rw","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work/data-1,C:\\data\\x,y,rw","--mount-deny","C:\\abs\\out","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/work/data-1 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work/data-1","host_path":"/fixture/cwd/C:\\data\\x,y","access":"rw","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:\\data\\x,y/C:\\abs\\out"]],["allowed",["/fixture/cwd/C:\\data\\x,y/logs/payloads"]],["writable",[]]]}, +{"flavor":"posix","value":"/srv/a/run,w//x/.,ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv/a/run","host_path":"w/x","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv/a/run,w/x,ro","--mount-deny","/abs/out","--mount-owner","caller"],"fragment":" virtfs_dir=/srv/a/run virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/srv/a/run","host_path":"/fixture/cwd/w/x","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/abs/out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/srv/work,C:/w,rw","denied":["C:\\abs\\out"],"allowed":[],"writable":["cache","out"],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv/work","host_path":"C:/w","access":"rw","denied":["C:\\abs\\out"],"allowed":[],"writable":["cache","out"],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv/work,C:/w,rw","--mount-deny","C:\\abs\\out","--mount-write","cache","--mount-write","out","--mount-owner","caller"],"fragment":" virtfs_dir=/srv/work virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/srv/work","host_path":"/fixture/cwd/C:/w","access":"rw","denied":["C:\\abs\\out"],"allowed":[],"writable":["cache","out"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:/w/C:\\abs\\out"]],["allowed",[]],["writable",["/fixture/cwd/C:/w/cache","/fixture/cwd/C:/w/out"]]]}, +{"flavor":"posix","value":"/data-1,C:/w,ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/data-1","host_path":"C:/w","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/data-1,C:/w,ro","--mount-deny","/abs/out"],"fragment":" virtfs_dir=/data-1 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1","host_path":"/fixture/cwd/C:/w","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["/abs/out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/srv/srv,w//x/.,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/srv/srv","host_path":"w/x","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/srv/srv,w/x,rw"],"fragment":" virtfs_dir=/srv/srv virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/srv/srv","host_path":"/fixture/cwd/w/x","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/work/etc/run,w//x/.","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work/etc/run","host_path":"w/x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work/etc/run,w/x,ro"],"fragment":" virtfs_dir=/work/etc/run virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/work/etc/run","host_path":"/fixture/cwd/w/x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/procmachine-id,C:/w,rw","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/procmachine-id","host_path":"C:/w","access":"rw","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/procmachine-id,C:/w,rw","--mount-deny","C:\\abs\\out","--mount-deny","logs","--mount-owner","caller"],"fragment":" virtfs_dir=/procmachine-id virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/procmachine-id","host_path":"/fixture/cwd/C:/w","access":"rw","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["/fixture/cwd/C:/w/C:\\abs\\out","/fixture/cwd/C:/w/logs"]],["allowed",[]],["writable",[]]]}, +{"flavor":"posix","value":"/srv/nvx/srv,C:/w,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/srv/nvx/srv","host_path":"C:/w","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/srv/nvx/srv,C:/w,rw"],"fragment":" virtfs_dir=/srv/nvx/srv virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/srv/nvx/srv","host_path":"/fixture/cwd/C:/w","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/opt/hostedtoolcache,host-dir,rw","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/opt/hostedtoolcache","host_path":"host-dir","access":"rw","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/opt/hostedtoolcache,host-dir,rw","--mount-deny","logs","--mount-deny","secrets"],"fragment":" virtfs_dir=/opt/hostedtoolcache virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/opt/hostedtoolcache","host_path":"C:\\fixture\\cwd\\host-dir","access":"rw","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\host-dir\\logs","C:\\fixture\\cwd\\host-dir\\secrets"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/tmp/mcp-runtime,host-dir,rw","denied":["mcp-logs"],"allowed":["mcp-logs/payloads"],"writable":["agent","cache"],"owner":"caller","kind":"directory","mount":{"guest_target":"/tmp/mcp-runtime","host_path":"host-dir","access":"rw","denied":["mcp-logs"],"allowed":["mcp-logs/payloads"],"writable":["agent","cache"],"owner":"caller","kind":"directory"},"arguments":["--mount","/tmp/mcp-runtime,host-dir,rw","--mount-deny","mcp-logs","--mount-allow","mcp-logs/payloads","--mount-write","agent","--mount-write","cache","--mount-owner","caller"],"fragment":" virtfs_dir=/tmp/mcp-runtime virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/tmp/mcp-runtime","host_path":"C:\\fixture\\cwd\\host-dir","access":"rw","denied":["mcp-logs"],"allowed":["mcp-logs/payloads"],"writable":["agent","cache"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\host-dir\\mcp-logs"]],["allowed",["C:\\fixture\\cwd\\host-dir\\mcp-logs\\payloads"]],["writable",["C:\\fixture\\cwd\\host-dir\\agent","C:\\fixture\\cwd\\host-dir\\cache"]]]}, +{"flavor":"windows","value":"/w,host,ro","denied":[],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory","error":"--mount-write narrows the writes of a read-write share; --mount /w is read-only"}, +{"flavor":"windows","value":"/w,host,rw","denied":[],"allowed":["path-0","path-1","path-2","path-3","path-4","path-5","path-6","path-7","path-8","path-9","path-10","path-11","path-12","path-13","path-14","path-15","path-16","path-17","path-18","path-19","path-20","path-21","path-22","path-23","path-24","path-25","path-26","path-27","path-28","path-29","path-30","path-31","path-32","path-33","path-34","path-35","path-36","path-37","path-38","path-39","path-40","path-41","path-42","path-43","path-44","path-45","path-46","path-47","path-48","path-49","path-50","path-51","path-52","path-53","path-54","path-55","path-56","path-57","path-58","path-59","path-60","path-61","path-62","path-63","path-64","path-65","path-66","path-67","path-68","path-69","path-70","path-71","path-72","path-73","path-74","path-75","path-76","path-77","path-78","path-79","path-80","path-81","path-82","path-83","path-84","path-85","path-86","path-87","path-88","path-89","path-90","path-91","path-92","path-93","path-94","path-95","path-96","path-97","path-98","path-99","path-100","path-101","path-102","path-103","path-104","path-105","path-106","path-107","path-108","path-109","path-110","path-111","path-112","path-113","path-114","path-115","path-116","path-117","path-118","path-119","path-120","path-121","path-122","path-123","path-124","path-125","path-126","path-127","path-128"],"writable":[],"owner":"vmm","kind":"directory","error":"a sandbox mount permits at most 128 allowed paths"}, +{"flavor":"windows","value":"/etc/app/settings.json,settings.json,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/etc/app/settings.json","host_path":"settings.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-3ddd35a70a0d7386a5451cd0d5dc3c39,settings.json,ro,file"],"fragment":" virtfs_dir=/etc/app/settings.json virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/etc/app/settings.json","host_path":"C:\\fixture\\cwd\\settings.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/a,C:\\data\\x,y,rx","denied":["C:\\abs\\out","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"windows","value":"/nvx,tools/cache,rw","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":["/abs/out","cache"],"owner":"caller","kind":"directory","mount":{"guest_target":"/nvx","host_path":"tools\\cache","access":"rw","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":["/abs/out","cache"],"owner":"caller","kind":"directory"},"arguments":["--mount","/nvx,tools\\cache,rw","--mount-deny","logs","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-write","/abs/out","--mount-write","cache","--mount-owner","caller"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvx","host_path":"C:\\fixture\\cwd\\tools\\cache","access":"rw","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":["/abs/out","cache"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\tools\\cache\\logs","C:\\fixture\\cwd\\tools\\cache\\secrets"]],["allowed",["C:\\fixture\\cwd\\tools\\cache\\logs\\payloads"]],["writable",["C:\\abs\\out","C:\\fixture\\cwd\\tools\\cache\\cache"]]]}, +{"flavor":"windows","value":"/run/a,w//x/.,ro","denied":["secrets","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/a","host_path":"w\\x","access":"ro","denied":["secrets","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/a,w\\x,ro","--mount-deny","secrets","--mount-deny","/abs/out"],"fragment":" virtfs_dir=/run/a virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/a","host_path":"C:\\fixture\\cwd\\w\\x","access":"ro","denied":["secrets","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\w\\x\\secrets","C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/run,tools/cache,rw","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":["/abs/out","cache"],"owner":"caller","kind":"directory","error":"sandbox mount target /run is reserved"}, +{"flavor":"windows","value":"/data-1/data-1/cache,C:\\data\\x,y,rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory","mount":{"guest_target":"/data-1/data-1/cache","host_path":"C:\\data\\x,y","access":"rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory"},"arguments":["--mount","/data-1/data-1/cache,C:\\data\\x,y,rw","--mount-deny","/abs/out","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-write","cache","--mount-owner","caller"],"fragment":" virtfs_dir=/data-1/data-1/cache virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/data-1/data-1/cache","host_path":"C:\\data\\x,y","access":"rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\data\\x,y\\secrets"]],["allowed",["C:\\data\\x,y\\logs\\payloads"]],["writable",["C:\\data\\x,y\\cache"]]]}, +{"flavor":"windows","value":"/nvx/\u00e9,a/../b,rw","denied":[],"allowed":[],"writable":["out","cache"],"owner":"caller","kind":"directory","error":"sandbox mount host path contains a parent component: a\\..\\b"}, +{"flavor":"windows","value":"/work,?//a,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work","host_path":"?\\a","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work,?\\a,rw"],"fragment":" virtfs_dir=/work virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work","host_path":"C:\\fixture\\cwd\\?\\a","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/nvx/data-1/a,a/../b","denied":["logs","C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount host path contains a parent component: a\\..\\b"}, +{"flavor":"windows","value":"/a,C:/w,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/a","host_path":"C:\\w","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-6a50dc8584134c7de537c0052ff6d236,C:\\w,ro,file","--mount-owner","caller"],"fragment":" virtfs_dir=/a virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/a","host_path":"C:\\w","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/work/run,C:/w","denied":["/abs/out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work/run","host_path":"C:\\w","access":"ro","denied":["/abs/out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work/run,C:\\w,ro","--mount-deny","/abs/out","--mount-deny","logs"],"fragment":" virtfs_dir=/work/run virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/work/run","host_path":"C:\\w","access":"ro","denied":["/abs/out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\w\\logs"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/etc/work/data-1,x,y\u0000x,y,ro","denied":["logs","/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount host path contains a NUL character: 'x,y\\x00x,y'"}, +{"flavor":"windows","value":"/srv,C:\\data\\x,y,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv,C:\\data\\x,y,ro","--mount-owner","caller"],"fragment":" virtfs_dir=/srv virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/srv","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/a b,a\u0000b,ro","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/nvx/cache,C:/w,rx","denied":["secrets","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"windows","value":"/work/a,/srv/data,rw","denied":["C:\\abs\\out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory","mount":{"guest_target":"/work/a","host_path":"\\srv\\data","access":"rw","denied":["C:\\abs\\out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory"},"arguments":["--mount","/work/a,\\srv\\data,rw","--mount-deny","C:\\abs\\out","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-write","cache","--mount-owner","caller"],"fragment":" virtfs_dir=/work/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work/a","host_path":"C:\\srv\\data","access":"rw","denied":["C:\\abs\\out","secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\srv\\data\\secrets"]],["allowed",["C:\\srv\\data\\logs\\payloads"]],["writable",["C:\\srv\\data\\cache"]]]}, +{"flavor":"windows","value":"/,C:\\data\\x,y,ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/work/a,C:\\data\\x,y,rw","denied":["secrets","C:\\abs\\out"],"allowed":[],"writable":["cache"],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work/a","host_path":"C:\\data\\x,y","access":"rw","denied":["secrets","C:\\abs\\out"],"allowed":[],"writable":["cache"],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work/a,C:\\data\\x,y,rw","--mount-deny","secrets","--mount-deny","C:\\abs\\out","--mount-write","cache"],"fragment":" virtfs_dir=/work/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work/a","host_path":"C:\\data\\x,y","access":"rw","denied":["secrets","C:\\abs\\out"],"allowed":[],"writable":["cache"],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\data\\x,y\\secrets","C:\\abs\\out"]],["allowed",[]],["writable",["C:\\data\\x,y\\cache"]]]}, +{"flavor":"windows","value":"/nvx/nvx,work,ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/nvx/nvx","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/nvx/nvx,work,ro","--mount-deny","logs","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/nvx/nvx virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx/nvx","host_path":"C:\\fixture\\cwd\\work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\work\\logs","C:\\fixture\\cwd\\work\\secrets"]],["allowed",["C:\\fixture\\cwd\\work\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/work,w//x/.,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/work","host_path":"w\\x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-0c9a453fad615c83c57d57c2b2be05fa,w\\x,ro,file","--mount-owner","caller"],"fragment":" virtfs_dir=/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/work","host_path":"C:\\fixture\\cwd\\w\\x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/\u00e9/srv,C:\\data\\x,y,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/\u00e9/srv","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-b52d86793a80d4b693be52d859a1fd8a,C:\\data\\x,y,ro,file"],"fragment":" virtfs_dir=/\u00e9/srv virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/\u00e9/srv","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/etc,w//x/.","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/a,C:/w,rw","denied":["/abs/out"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory","mount":{"guest_target":"/a","host_path":"C:\\w","access":"rw","denied":["/abs/out"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"},"arguments":["--mount","/a,C:\\w,rw","--mount-deny","/abs/out","--mount-write","out","--mount-owner","caller"],"fragment":" virtfs_dir=/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/a","host_path":"C:\\w","access":"rw","denied":["/abs/out"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",[]],["writable",["C:\\w\\out"]]]}, +{"flavor":"windows","value":"/a/run,C:/w","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/a/run","host_path":"C:\\w","access":"ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/a/run,C:\\w,ro","--mount-deny","C:\\abs\\out","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/a/run virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/a/run","host_path":"C:\\w","access":"ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",["C:\\w\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/etc,C:/w,ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/etc,w//x/.,rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["cache"],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/cache/etc/\u00e9,C:\\data\\x,y,rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/cache/etc/\u00e9","host_path":"C:\\data\\x,y","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-3d35e19069b9b96c28742438e4299b52,C:\\data\\x,y,rw,file","--mount-owner","caller"],"fragment":" virtfs_dir=/cache/etc/\u00e9 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/cache/etc/\u00e9","host_path":"C:\\data\\x,y","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/run/shares\u3000work/nvx,C:/w,rw","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/run/shares\\u3000work/nvx': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/run/\\.\u001c/\u00e9,/srv/data,rw","denied":[],"allowed":[],"writable":["cache","/abs/out"],"owner":"caller","kind":"file","error":"invalid sandbox mount target '/run/\\\\.\\x1c/\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/data-1/cache/\u00e9,C:\\data\\x,y","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"unsupported --mount mode 'y'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"windows","value":"/\u00e9/cache/a,C:\\data\\x,y,rw","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/\u00e9/cache/a","host_path":"C:\\data\\x,y","access":"rw","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/\u00e9/cache/a,C:\\data\\x,y,rw","--mount-deny","/abs/out","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/\u00e9/cache/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/\u00e9/cache/a","host_path":"C:\\data\\x,y","access":"rw","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",["C:\\data\\x,y\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/etc,C:/w,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/\u00e9/cache,/srv/data,rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/\u00e9/cache","host_path":"\\srv\\data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-93735fe6b8037c02e2ca771a5eb0fbe7,\\srv\\data,rw,file","--mount-owner","caller"],"fragment":" virtfs_dir=/\u00e9/cache virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/\u00e9/cache","host_path":"C:\\srv\\data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/machine-id\u001ca/etc/\u00e9,C:/w","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"file","error":"invalid sandbox mount target '/machine-id\\x1ca/etc/\u00e9': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/etc/etc,w//x/.,rw","denied":["/abs/out"],"allowed":["logs/payloads","logs"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/etc/etc","host_path":"w\\x","access":"rw","denied":["/abs/out"],"allowed":["logs/payloads","logs"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/etc/etc,w\\x,rw","--mount-deny","/abs/out","--mount-allow","logs/payloads","--mount-allow","logs"],"fragment":" virtfs_dir=/etc/etc virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/etc/etc","host_path":"C:\\fixture\\cwd\\w\\x","access":"rw","denied":["/abs/out"],"allowed":["logs/payloads","logs"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",["C:\\fixture\\cwd\\w\\x\\logs\\payloads","C:\\fixture\\cwd\\w\\x\\logs"]],["writable",[]]]}, +{"flavor":"windows","value":"/nvx/work/data-1,C:/w,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/nvx/work/data-1","host_path":"C:\\w","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-f70c1ff3e0a8554d424014d45d31d57d,C:\\w,rw,file"],"fragment":" virtfs_dir=/nvx/work/data-1 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvx/work/data-1","host_path":"C:\\w","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/work,C:/w","denied":[],"allowed":["logs"],"writable":[],"owner":"vmm","kind":"directory","error":"--mount-allow exposes a path inside a --mount-deny path of the same share, which has none"}, +{"flavor":"windows","value":"/run/nvx/cache,tools/cache","denied":["/abs/out","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/run/nvx/cache","host_path":"tools\\cache","access":"ro","denied":["/abs/out","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/run/nvx/cache,tools\\cache,ro","--mount-deny","/abs/out","--mount-deny","logs","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/run/nvx/cache virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/nvx/cache","host_path":"C:\\fixture\\cwd\\tools\\cache","access":"ro","denied":["/abs/out","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\fixture\\cwd\\tools\\cache\\logs"]],["allowed",["C:\\fixture\\cwd\\tools\\cache\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/work,C:\\data\\x,y,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work,C:\\data\\x,y,ro"],"fragment":" virtfs_dir=/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/work","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/\u00e9/data-1/\u00e9,C:/w,ro","denied":["logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/\u00e9/data-1/\u00e9","host_path":"C:\\w","access":"ro","denied":["logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/\u00e9/data-1/\u00e9,C:\\w,ro","--mount-deny","logs","--mount-owner","caller"],"fragment":" virtfs_dir=/\u00e9/data-1/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/\u00e9/data-1/\u00e9","host_path":"C:\\w","access":"ro","denied":["logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\w\\logs"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/etc,/srv/data,ro","denied":["secrets","C:\\abs\\out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/srv/srv,C:/w,rw","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv/srv","host_path":"C:\\w","access":"rw","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv/srv,C:\\w,rw","--mount-deny","/abs/out","--mount-owner","caller"],"fragment":" virtfs_dir=/srv/srv virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/srv/srv","host_path":"C:\\w","access":"rw","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/etc,/srv/data,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/etc/\u00e9/\u00e9,tools/cache","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/etc/\u00e9/\u00e9","host_path":"tools\\cache","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/etc/\u00e9/\u00e9,tools\\cache,ro","--mount-deny","logs","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/etc/\u00e9/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/etc/\u00e9/\u00e9","host_path":"C:\\fixture\\cwd\\tools\\cache","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\tools\\cache\\logs"]],["allowed",["C:\\fixture\\cwd\\tools\\cache\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/etc/data-1/nvx,w//x/.","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/etc/data-1/nvx","host_path":"w\\x","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/etc/data-1/nvx,w\\x,ro","--mount-deny","secrets"],"fragment":" virtfs_dir=/etc/data-1/nvx virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/etc/data-1/nvx","host_path":"C:\\fixture\\cwd\\w\\x","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\w\\x\\secrets"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/a/\u00e9/srv,tools/cache","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/a/\u00e9/srv","host_path":"tools\\cache","access":"ro","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/a/\u00e9/srv,tools\\cache,ro","--mount-deny","C:\\abs\\out","--mount-deny","/abs/out","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/a/\u00e9/srv virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/a/\u00e9/srv","host_path":"C:\\fixture\\cwd\\tools\\cache","access":"ro","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\abs\\out"]],["allowed",["C:\\fixture\\cwd\\tools\\cache\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/run/a,tools/cache,rw","denied":[],"allowed":[],"writable":["cache","out","."],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/a","host_path":"tools\\cache","access":"rw","denied":[],"allowed":[],"writable":["cache","out","."],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/a,tools\\cache,rw","--mount-write","cache","--mount-write","out","--mount-write","."],"fragment":" virtfs_dir=/run/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/run/a","host_path":"C:\\fixture\\cwd\\tools\\cache","access":"rw","denied":[],"allowed":[],"writable":["cache","out","."],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",["C:\\fixture\\cwd\\tools\\cache\\cache","C:\\fixture\\cwd\\tools\\cache\\out","C:\\fixture\\cwd\\tools\\cache"]]]}, +{"flavor":"windows","value":"/nvx,C:\\data\\x,y,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/nvx","host_path":"C:\\data\\x,y","access":"rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/nvx,C:\\data\\x,y,rw","--mount-deny","secrets"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvx","host_path":"C:\\data\\x,y","access":"rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\data\\x,y\\secrets"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/a b,a/../b,rw","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/nvx,C:\\data\\x,y,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/nvx","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-30b37e7ae096735b949f4f464412ab3c,C:\\data\\x,y,ro,file"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx","host_path":"C:\\data\\x,y","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/\u00e9,C:/w,ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/\u00e9","host_path":"C:\\w","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/\u00e9,C:\\w,ro","--mount-deny","C:\\abs\\out"],"fragment":" virtfs_dir=/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/\u00e9","host_path":"C:\\w","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/nvx/a/work,C:\\data\\x,y","denied":["a\u0000"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'y'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"windows","value":"/etc,C:\\data\\x,y,ro","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/work/\u3000\u001cwork,w//x/.,rw","denied":["logs"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/work/\\u3000\\x1cwork': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/run/\u00e9,w//x/.,ro","denied":["secrets"],"allowed":["out"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/\u00e9","host_path":"w\\x","access":"ro","denied":["secrets"],"allowed":["out"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/\u00e9,w\\x,ro","--mount-deny","secrets","--mount-allow","out"],"fragment":" virtfs_dir=/run/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/\u00e9","host_path":"C:\\fixture\\cwd\\w\\x","access":"ro","denied":["secrets"],"allowed":["out"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\w\\x\\secrets"]],["allowed",["C:\\fixture\\cwd\\w\\x\\out"]],["writable",[]]]}, +{"flavor":"windows","value":"/cache/run,C:\\data\\x,y,rx","denied":["secrets"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"windows","value":"/data-1/data-1,work","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/data-1/data-1","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-fd1bb3d6d1bce0afe815991bf83f5962,work,ro,file"],"fragment":" virtfs_dir=/data-1/data-1 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1/data-1","host_path":"C:\\fixture\\cwd\\work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/run/srv/data-1,tools/cache","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/run/srv/data-1","host_path":"tools\\cache","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-a8f01611937352bf22a395a8f556cae1,tools\\cache,ro,file","--mount-owner","caller"],"fragment":" virtfs_dir=/run/srv/data-1 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/srv/data-1","host_path":"C:\\fixture\\cwd\\tools\\cache","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/data-1/data-1/work,tools/cache,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/data-1/data-1/work","host_path":"tools\\cache","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-308e5770e516a27164f075a92cce7fdb,tools\\cache,ro,file"],"fragment":" virtfs_dir=/data-1/data-1/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1/data-1/work","host_path":"C:\\fixture\\cwd\\tools\\cache","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/a b,/srv/data","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/nvx,/srv/data,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/nvx","host_path":"\\srv\\data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-30b37e7ae096735b949f4f464412ab3c,\\srv\\data,rw,file"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvx","host_path":"C:\\srv\\data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/,runshares/run,C:\\data\\x,y","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'y'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"windows","value":"/srv/a,tools/cache,rx","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"windows","value":"/a,a\u0000b,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount host path contains a NUL character: 'a\\x00b'"}, +{"flavor":"windows","value":"/data-1/work,w//x/.","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/data-1/work","host_path":"w\\x","access":"ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/data-1/work,w\\x,ro","--mount-deny","C:\\abs\\out","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/data-1/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1/work","host_path":"C:\\fixture\\cwd\\w\\x","access":"ro","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",["C:\\fixture\\cwd\\w\\x\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/etc/a/a,w//x/.","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"file","error":"--mount-deny, --mount-allow, and --mount-write name paths inside a shared directory, but --mount /etc/a/a shares a file, whose mode applies to the whole file"}, +{"flavor":"windows","value":"/\u00e9/etc,w//x/.","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/\u00e9/etc","host_path":"w\\x","access":"ro","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/\u00e9/etc,w\\x,ro","--mount-deny","C:\\abs\\out","--mount-deny","/abs/out","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/\u00e9/etc virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/\u00e9/etc","host_path":"C:\\fixture\\cwd\\w\\x","access":"ro","denied":["C:\\abs\\out","/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\abs\\out"]],["allowed",["C:\\fixture\\cwd\\w\\x\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/etc,tools/cache","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/nvx/data-1,C:\\data\\x,y,ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/nvx/data-1","host_path":"C:\\data\\x,y","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/nvx/data-1,C:\\data\\x,y,ro","--mount-deny","secrets","--mount-allow","logs/payloads","--mount-owner","caller"],"fragment":" virtfs_dir=/nvx/data-1 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx/data-1","host_path":"C:\\data\\x,y","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\data\\x,y\\secrets"]],["allowed",["C:\\data\\x,y\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/etc,tools/cache","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/a b,/srv/data,rw","denied":["secrets","/abs/out"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/run/data-1/etc,tools/cache,rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out","/abs/out"],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/data-1/etc","host_path":"tools\\cache","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out","/abs/out"],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/data-1/etc,tools\\cache,rw","--mount-deny","logs","--mount-allow","logs/payloads","--mount-write","out","--mount-write","/abs/out"],"fragment":" virtfs_dir=/run/data-1/etc virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/run/data-1/etc","host_path":"C:\\fixture\\cwd\\tools\\cache","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out","/abs/out"],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\tools\\cache\\logs"]],["allowed",["C:\\fixture\\cwd\\tools\\cache\\logs\\payloads"]],["writable",["C:\\fixture\\cwd\\tools\\cache\\out","C:\\abs\\out"]]]}, +{"flavor":"windows","value":"/nvx,/srv/data,ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/nvx","host_path":"\\srv\\data","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/nvx,\\srv\\data,ro","--mount-deny","/abs/out"],"fragment":" virtfs_dir=/nvx virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx","host_path":"C:\\srv\\data","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/a b,tools/cache,rw","denied":["secrets","/abs/out","a\u0000"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","error":"invalid sandbox mount target '/a b': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/run/work,tools/cache,ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/work","host_path":"tools\\cache","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/work,tools\\cache,ro","--mount-deny","C:\\abs\\out"],"fragment":" virtfs_dir=/run/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/run/work","host_path":"C:\\fixture\\cwd\\tools\\cache","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"//srv/run,/srv/data","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '//srv/run': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/srv/srv,a/../b,rw","denied":["secrets","C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory","error":"sandbox mount host path contains a parent component: a\\..\\b"}, +{"flavor":"windows","value":"/work,w//x/.,rw","denied":["logs","/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/work","host_path":"w\\x","access":"rw","denied":["logs","/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/work,w\\x,rw","--mount-deny","logs","--mount-deny","/abs/out","--mount-owner","caller"],"fragment":" virtfs_dir=/work virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work","host_path":"C:\\fixture\\cwd\\w\\x","access":"rw","denied":["logs","/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\fixture\\cwd\\w\\x\\logs","C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/a,work,rw","denied":["/abs/out","."],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory","mount":{"guest_target":"/a","host_path":"work","access":"rw","denied":["/abs/out","."],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"},"arguments":["--mount","/a,work,rw","--mount-deny","/abs/out","--mount-deny",".","--mount-allow","logs/payloads","--mount-write","out","--mount-owner","caller"],"fragment":" virtfs_dir=/a virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/a","host_path":"C:\\fixture\\cwd\\work","access":"rw","denied":["/abs/out","."],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\fixture\\cwd\\work"]],["allowed",["C:\\fixture\\cwd\\work\\logs\\payloads"]],["writable",["C:\\fixture\\cwd\\work\\out"]]]}, +{"flavor":"windows","value":"/etc/data-1,w//x/.,rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/etc/data-1","host_path":"w\\x","access":"rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/etc/data-1,w\\x,rw","--mount-deny","/abs/out","--mount-deny","secrets","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/etc/data-1 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/etc/data-1","host_path":"C:\\fixture\\cwd\\w\\x","access":"rw","denied":["/abs/out","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\fixture\\cwd\\w\\x\\secrets"]],["allowed",["C:\\fixture\\cwd\\w\\x\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/srv,w//x/.,rx","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"windows","value":"/\u00e9,/srv/data,rx","denied":["/abs/out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"unsupported --mount mode 'rx'; choose ro or rw (if HOST_PATH contains commas, end the value with ,ro or ,rw)"}, +{"flavor":"windows","value":"/srv/data-1/\u00e9,C:\\data\\x,y,ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv/data-1/\u00e9","host_path":"C:\\data\\x,y","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv/data-1/\u00e9,C:\\data\\x,y,ro","--mount-deny","C:\\abs\\out","--mount-owner","caller"],"fragment":" virtfs_dir=/srv/data-1/\u00e9 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/srv/data-1/\u00e9","host_path":"C:\\data\\x,y","access":"ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/proc,a\u0000b,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","error":"sandbox mount target /proc overlaps the reserved /proc tree"}, +{"flavor":"windows","value":"/run,/srv/data,ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /run is reserved"}, +{"flavor":"windows","value":"/a/work,w//x/.,ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/a/work","host_path":"w\\x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/a/work,w\\x,ro","--mount-owner","caller"],"fragment":" virtfs_dir=/a/work virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/a/work","host_path":"C:\\fixture\\cwd\\w\\x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/etc,tools/cache,ro","denied":["C:\\abs\\out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","error":"sandbox mount target /etc is reserved"}, +{"flavor":"windows","value":"/nvx/a/a,/srv/data","denied":["/abs/out","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/nvx/a/a","host_path":"\\srv\\data","access":"ro","denied":["/abs/out","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/nvx/a/a,\\srv\\data,ro","--mount-deny","/abs/out","--mount-deny","secrets"],"fragment":" virtfs_dir=/nvx/a/a virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/nvx/a/a","host_path":"C:\\srv\\data","access":"ro","denied":["/abs/out","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\srv\\data\\secrets"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/data-1/a,/srv/data","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file","mount":{"guest_target":"/data-1/a","host_path":"\\srv\\data","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-4b25c272fa19ed009dce984c919893ec,\\srv\\data,ro,file"],"fragment":" virtfs_dir=/data-1/a virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1/a","host_path":"C:\\srv\\data","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/nvxrun,/srv/data,rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file","mount":{"guest_target":"/nvxrun","host_path":"\\srv\\data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"arguments":["--mount-aggregate","/run/nvx/shares","--mount-child","0-6ab51ef8dc55669bf1c3c27372b1f413,\\srv\\data,rw,file","--mount-owner","caller"],"fragment":" virtfs_dir=/nvxrun virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/nvxrun","host_path":"C:\\srv\\data","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/,/srv,tools/cache,rw","denied":[],"allowed":[],"writable":["/abs/out","cache"],"owner":"vmm","kind":"directory","error":"invalid sandbox mount target '/': expected an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"flavor":"windows","value":"/run/data-1,C:\\data\\x,y,rw","denied":["secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory","mount":{"guest_target":"/run/data-1","host_path":"C:\\data\\x,y","access":"rw","denied":["secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory"},"arguments":["--mount","/run/data-1,C:\\data\\x,y,rw","--mount-deny","secrets","--mount-write","out"],"fragment":" virtfs_dir=/run/data-1 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/run/data-1","host_path":"C:\\data\\x,y","access":"rw","denied":["secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\data\\x,y\\secrets"]],["allowed",[]],["writable",["C:\\data\\x,y\\out"]]]}, +{"flavor":"windows","value":"/nvx,/srv/data,rw","denied":["C:\\abs\\out","secrets"],"allowed":["logs/payloads"],"writable":["a\u0000"],"owner":"caller","kind":"directory","error":"sandbox mount writable paths must be nonempty"}, +{"flavor":"windows","value":"/work/data-1,C:\\data\\x,y,rw","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work/data-1","host_path":"C:\\data\\x,y","access":"rw","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work/data-1,C:\\data\\x,y,rw","--mount-deny","C:\\abs\\out","--mount-allow","logs/payloads"],"fragment":" virtfs_dir=/work/data-1 virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/work/data-1","host_path":"C:\\data\\x,y","access":"rw","denied":["C:\\abs\\out"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",["C:\\data\\x,y\\logs\\payloads"]],["writable",[]]]}, +{"flavor":"windows","value":"/srv/a/run,w//x/.,ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv/a/run","host_path":"w\\x","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv/a/run,w\\x,ro","--mount-deny","/abs/out","--mount-owner","caller"],"fragment":" virtfs_dir=/srv/a/run virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/srv/a/run","host_path":"C:\\fixture\\cwd\\w\\x","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/srv/work,C:/w,rw","denied":["C:\\abs\\out"],"allowed":[],"writable":["cache","out"],"owner":"caller","kind":"directory","mount":{"guest_target":"/srv/work","host_path":"C:\\w","access":"rw","denied":["C:\\abs\\out"],"allowed":[],"writable":["cache","out"],"owner":"caller","kind":"directory"},"arguments":["--mount","/srv/work,C:\\w,rw","--mount-deny","C:\\abs\\out","--mount-write","cache","--mount-write","out","--mount-owner","caller"],"fragment":" virtfs_dir=/srv/work virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/srv/work","host_path":"C:\\w","access":"rw","denied":["C:\\abs\\out"],"allowed":[],"writable":["cache","out"],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",[]],["writable",["C:\\w\\cache","C:\\w\\out"]]]}, +{"flavor":"windows","value":"/data-1,C:/w,ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/data-1","host_path":"C:\\w","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/data-1,C:\\w,ro","--mount-deny","/abs/out"],"fragment":" virtfs_dir=/data-1 virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/data-1","host_path":"C:\\w","access":"ro","denied":["/abs/out"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/srv/srv,w//x/.,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/srv/srv","host_path":"w\\x","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/srv/srv,w\\x,rw"],"fragment":" virtfs_dir=/srv/srv virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/srv/srv","host_path":"C:\\fixture\\cwd\\w\\x","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/work/etc/run,w//x/.","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/work/etc/run","host_path":"w\\x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/work/etc/run,w\\x,ro"],"fragment":" virtfs_dir=/work/etc/run virtfs_tag=microvm virtfs_mode=ro","absolute":{"guest_target":"/work/etc/run","host_path":"C:\\fixture\\cwd\\w\\x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/procmachine-id,C:/w,rw","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory","mount":{"guest_target":"/procmachine-id","host_path":"C:\\w","access":"rw","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"arguments":["--mount","/procmachine-id,C:\\w,rw","--mount-deny","C:\\abs\\out","--mount-deny","logs","--mount-owner","caller"],"fragment":" virtfs_dir=/procmachine-id virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/procmachine-id","host_path":"C:\\w","access":"rw","denied":["C:\\abs\\out","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},"absolute_policy":[["denied",["C:\\abs\\out","C:\\w\\logs"]],["allowed",[]],["writable",[]]]}, +{"flavor":"windows","value":"/srv/nvx/srv,C:/w,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory","mount":{"guest_target":"/srv/nvx/srv","host_path":"C:\\w","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"arguments":["--mount","/srv/nvx/srv,C:\\w,rw"],"fragment":" virtfs_dir=/srv/nvx/srv virtfs_tag=microvm virtfs_mode=rw","absolute":{"guest_target":"/srv/nvx/srv","host_path":"C:\\w","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},"absolute_policy":[["denied",[]],["allowed",[]],["writable",[]]]} +], +"launches": [ +{"flavor":"posix","layers":["runtime,layer.erofs,a6b34c85-86b9-b646-18dc-28887414f5ab","custom,C:/layers/x.erofs,2089b5af-e1d7-1d94-940d-631f55b9c56a","distro,a//b.erofs,273ccf9a-9d5a-fd30-95b8-921f5d2dc314"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/s h","args":["--","a\u00e9","axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a\u00e9","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,a//b.erofs,a55e4eb1-7eeb-35d5-65b0-f6515125d6da"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["==","---serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=","=\u00e9\u00e9","==","\u00e9axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-","=--serve\u00e9","=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=--serve","a","a\u00e9\u00e9","\u00e9--serve=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9-","a=","--serve","=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=\u00e9"],"hostname":"example","identity":[65534,1001],"memory_max":"268435456","pids_max":"99999999999999999999","mounts":[],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,a//b.erofs,b8ea9084-e245-0269-f314-5038109e52c7","custom,a//b.erofs,84982b13-1f07-8906-6ec1-2925deb75152","distro,C:/layers/x.erofs,d1edc9fd-41f9-e89c-daf1-e00aca033141"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["="],"hostname":"nvx-sandbox","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,7ad560cb-c7d8-f83a-e691-da5182adada5","distro,layer.erofs,82ecd171-e7cf-f834-657d-da4581d68d3f","runtime,C:/layers/x.erofs,a093d2e7-193b-58b1-a44f-cee78f39af18"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":[],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,a//b.erofs,6a18f14b-7162-bf87-25ae-bd7d60a056c1","custom,layer.erofs,d41e6f2e-e259-8f87-f16a-b0eb536f6387"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["=","axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--serve","---servea","=a","\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","---serve-","-a-","--serve","==","\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","---serve--serve","---serve","a","a--","\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","\u00e9\u00e9","-\u00e9--serve"],"hostname":"a-1","identity":[65534,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,layer.erofs,fa3780d0-74bb-4dbb-77d3-8accf1ebeb7a","distro,a//b.erofs,d7ef509b-4a84-cd81-2da1-49e3f43f3b09","custom,layer.erofs,9a364c2e-3436-d5a3-8962-b8d75d6b7d1a"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9a=","a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a=","-=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","a--serve","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-","--serve\u00e9\u00e9","=","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv3,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host4","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host5,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,ecd45898-c6a9-5923-3199-967b9abf7d03","custom,C:/layers/x.erofs,818aa272-63bd-1821-36d6-ccbe0207b303","distro,layer.erofs,35f11375-8399-9dc0-6b2f-0d9545582d31"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["\u00e9-\u00e9","=\u00e9--serve","=a=","---serve=","a=="],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,aed22c49-c733-1bea-fdd4-f8d4204e7498","runtime,a//b.erofs,316b4f19-337d-4146-8519-565eac369b4f"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["=\u00e9","\u00e9\u00e9","a--serve=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","a---serve"],"hostname":"nvx-sandbox","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,a//b.erofs,c7f97bdf-106d-df55-61a9-eb0faa803f3a","runtime,layer.erofs,83f82d7f-2371-9b7c-0e26-943daaa61eff","distro,a//b.erofs,30b5620f-c623-5963-6872-06e17b596643"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["\u00e9a="],"hostname":"-a","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,layer.erofs,22540f15-7117-5e21-5452-55a9556d441a"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"example","identity":[1000,65534],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host3,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxx,/srv4,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxx,/srv5","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,a3e157e4-255a-4257-8176-51c423463089"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":["-\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"nvx-sandbox","identity":[1000,65534],"memory_max":"1000000000000000000000000000000","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,C:/layers/x.erofs,f67d6ca6-1638-7a37-ea24-142db6296541","runtime,layer.erofs,9a0f0e67-9693-840c-31cf-06d86151d54e","custom,C:/layers/x.erofs,ad0acdad-3270-3bc9-0056-44a8918755b1"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":["=axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,979d8723-9b7c-97cb-3c0c-5fce959e3cf4"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","aaa","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a-\u00e9"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/03xxxxxxx,C:/host3","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host4,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv5,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,C:/layers/x.erofs,37358704-42ba-7bc2-f7c8-5e3081e1ce65"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["=\u00e9="],"hostname":"example","identity":[1000,65534],"memory_max":"0","pids_max":"64","mounts":[],"command_line":" quiet loglevel=0 ","error":"--memory-max must be positive"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,45a311d8-bac0-b48e-88e2-c3f5aaf96468"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"example","identity":[1000,1001],"memory_max":"0","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,656a4d6d-68ff-9815-1e85-4d1acbb1a7c1","distro,C:/layers/x.erofs,6717cc9a-5a06-db08-681a-da2a855ad0f8","runtime,layer.erofs,80e671ee-cc09-1bd9-e007-0d2d511a571e","custom,a//b.erofs,cbda55fe-5c2b-a8a6-4394-5ef21b718a2c"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["=="],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":"0","mounts":[],"command_line":"quiet","error":"duplicate sandbox layer role(s): custom"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,cdc95b00-831b-01c1-feae-4724354542f0"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["--serve--serve-","-a"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":"0","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxx,C:/host3","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host4","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host5,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,layer.erofs,580e9941-5f5c-2c30-7ac2-127c4bd8e3c6"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["\u00e9","-"],"hostname":"example","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[],"command_line":"virtfs_dir=/x","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:C:/s/scratch.ext4","--microvm-workload-identity","65534:1001"],"kernel_error":"virtfs_dir is owned by the sandbox --mount option"}, +{"flavor":"posix","layers":["distro,a//b.erofs,8cf91078-9556-d1be-17dc-fae9bcba794b"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":[],"hostname":"Example","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxx,C:/host0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host2,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host3,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host4,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv5","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"sandbox hostname must be a lowercase RFC 1123 label up to 63 characters"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,27c4fb6d-0c0e-4ea4-bbb5-b4d385bb4fe8"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":["--serve-=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9="],"hostname":"a-1","identity":[65534,65534],"memory_max":"268435456","pids_max":null,"mounts":[],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,3cf8bfb6-5fb6-a7bc-1f86-1531a352c85b","runtime,C:/layers/x.erofs,97843d62-9330-fda7-a4fa-ab7eeaac6288","custom,a//b.erofs,7b056600-c055-1ef7-1705-5d36f2d41d94"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["=---serve"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"duplicate sandbox layer role(s): custom"}, +{"flavor":"posix","layers":["runtime,layer.erofs,50f0c385-3492-d186-d292-0a4727a31653","custom,layer.erofs,a40c25c8-1363-d257-5a07-4bbe91024a98"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["-"],"hostname":"example","identity":[1000,1001],"memory_max":"268435456","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,layer.erofs,b8d32eb7-6a38-70fa-b5b1-590ba4479c23"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve","aa","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","--serve"],"hostname":"nvx-sandbox","identity":[1000,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[],"command_line":"quiet","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,layer.erofs,91c1e6d7-63c1-35cc-e117-cd98af9dbee1"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["---serve=","\u00e9-=","--serve=","--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=","\u00e9a\u00e9","=","\u00e9","-","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9--servea","--serve=-","aa\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","a","\u00e9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"nvx-sandbox","identity":[1000,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"nvx_sandbox=0","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:C:/s/scratch.ext4","--microvm-workload-identity","1000:65534","--mount","/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro"],"kernel_error":"nvx_sandbox is owned by the sandbox profile"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,adb35722-f24d-bd71-55fd-5392656d3c19"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["--serve==","\u00e9a"],"hostname":"example","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,b6ef98b9-3131-a98e-89b9-b0273ea7a45c"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa"],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,a//b.erofs,bf51fb7a-ef3c-09cb-432c-5129ae3686bc"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["-\u00e9--serve"],"hostname":"example","identity":[0,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,0a4a8a0b-7cbe-af5d-f52c-55bb4e9761d5","distro,layer.erofs,42660302-8b2f-5d60-e6c6-5c5414b85a76"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["=axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-"],"hostname":"a-1","identity":[1000,65534],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,a//b.erofs,acdd08a1-769a-c3fd-d20b-e8f0b22bdfa5","custom,C:/layers/x.erofs,8cd77740-856f-e40d-b0ce-e8a8dba164b3"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":["=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,layer.erofs,dc6b958d-36cb-e08a-b2f0-6945285a3abe"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":[],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxx,C:/host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,f0916002-b89e-3079-70b4-99fb50379579"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":[],"hostname":"a-1","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,a//b.erofs,0aed6803-962d-9098-90df-c2cdf949064f","distro,layer.erofs,b84c1d3d-670c-bd20-1b15-9e58a57cc159","custom,layer.erofs,386e7680-0139-2cc3-143a-115e64d7b9a8","runtime,a//b.erofs,75b6a5ef-af13-ef61-f7df-0cef00020c8b"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve","=\u00e9a","a=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","--servea","=a","--a","a--serve","=\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a","--servea\u00e9","--servea","\u00e9","--serve=","a","=","-","a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"quiet","error":"duplicate sandbox layer role(s): runtime"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,a54937bf-c3b5-4730-31d0-149d5d967101"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":["--serve"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":"0","mounts":[],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,a//b.erofs,c855b135-ff10-b15f-5bc9-43ebe55f0ef4","runtime,C:/layers/x.erofs,5c3148c0-2bf5-3137-a7dd-cafc0d0283ba"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve\u00e9\u00e9","-","--serve","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[],"command_line":"\u3000quiet\u001c","error":"duplicate sandbox layer role(s): runtime"}, +{"flavor":"posix","layers":["custom,a//b.erofs,bd09dc76-81fd-1b51-4250-14657026e68f","distro,layer.erofs,1ef5a04a-eada-2969-9499-c436a26d1f57","runtime,C:/layers/x.erofs,8e704a5e-c05d-5a80-c3db-d9797c548e73"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["-=a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxx,C:/host2","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host3,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv4,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host5","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,C:/layers/x.erofs,fde517fd-15ae-769b-c408-c4cf745c8c91"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":["aa-"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxx,host1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:C:/layers/x.erofs,ro","--microvm-sandbox-block","scratch:file:C:/s/scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-e1ed8cbca2406f01ea48580ea6726a7d,C:/host0,ro,file","--mount-child","1-1ec7b2c1648b15995214377627d3ac85,host1,ro,file"],"kernel_command_line":"nvx_sandbox=1 nvx_layer=distro,0xd0003000,fde517fd-15ae-769b-c408-c4cf745c8c91 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/usr/bin/python3 nvx_hostname=nvx-sandbox nvx_arg=aa- nvx_pids_max=100000000000000000000 nvx_share=0-e1ed8cbca2406f01ea48580ea6726a7d,/share/00xxxxxxxxx,ro,file nvx_share=1-1ec7b2c1648b15995214377627d3ac85,/share/01xxxxxxxxxxxxxxxxxxx,ro,file"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,3c0d4ab6-c5f5-6f5a-d586-c927ae45ad51"],"scratch":"s//x.ext4","entrypoint":"bin/sh","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"a-1","identity":[65534,1001],"memory_max":"268435456","pids_max":"64","mounts":[],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,a//b.erofs,715d50b1-642e-1b8e-2c2f-7ae3bc2e03ac"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":[],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":"-1","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,2fa9ef3c-075b-32db-6fc2-fdf406368f63"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":[],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,a//b.erofs,c7006cfe-c270-a90c-82a4-23f659021ab5"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["\u00e9\u00e9--serve"],"hostname":"a-1","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxx,/srv1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv3,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host4,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host5","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,layer.erofs,9d716755-a832-c8eb-e326-ccb202412244"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":[""],"hostname":"example","identity":[65534,1001],"memory_max":"268435456","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxx,host1,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host2,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,0fe6f305-a1ba-b6d8-c7c1-e3c4984d3406"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["-"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":"268435456","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,c97e31d1-65d8-53d1-5b65-0eb79ec81bc7"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":"1000000000000000000000000000000","pids_max":"99999999999999999999","mounts":[],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,layer.erofs,42b4357a-f158-1dfb-204c-9322e928c494","custom,a//b.erofs,04648c41-7397-c262-b5f6-6578aaf5bc48","runtime,layer.erofs,e219f7e4-5f5e-87ca-6ab1-bc02ce3b2ecd"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["=","a","=","=a","--serve","\u00e9","--serve-=","\u00e9a\u00e9","--serve","-\u00e9=","=\u00e9","=-a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve-","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","a","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","a\u00e9\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","--serve=--serve"],"hostname":"example","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxx,host1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,layer.erofs,ce8c30ed-7078-382e-a2f5-58d465c41c45","custom,layer.erofs,9637ff70-e851-069f-1aa1-245704a483a0"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"a-1","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxx,/srv2,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxx,C:/host3,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv4","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host5","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,C:/layers/x.erofs,be50e9fe-48e6-328d-afa8-a42b4a9c4183","custom,C:/layers/x.erofs,f09059aa-8b88-3035-b44a-98c496aeeb34"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"example","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,235e05b2-3e18-b2d4-d5c0-9b7f4e1c3d83","distro,C:/layers/x.erofs,0c8d13ee-2896-e0ec-5c63-a29f538beb32","custom,a//b.erofs,3a37bc91-a398-5563-9e33-4af0f1dd6953","runtime,layer.erofs,0cb4e2a4-f2c8-4b56-1e85-fc5b35526df9"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["a--serve","=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9"],"hostname":"example","identity":[1000,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host3,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host4,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host5,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"quiet","error":"duplicate sandbox layer role(s): runtime"}, +{"flavor":"posix","layers":["custom,a//b.erofs,3d2c838c-80d9-57a2-44fa-f475e3836922","runtime,C:/layers/x.erofs,998632ef-57f3-4330-1f76-d691aad62538"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["-\u00e9a","\u00e9=a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9","--serve=--serve","-","\u00e9","\u00e9aa","-=","-","\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","=","\u00e9a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","-a","--serve-","--\u00e9","--serve--serve\u00e9","--serve","\u00e9"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,65534],"memory_max":null,"pids_max":null,"mounts":[],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,eb1dacef-aea8-e610-fb13-5ef9f9df5b30","distro,layer.erofs,a8ba9752-dc18-0619-8bf2-e529f721a7f2"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["\u00e9\u00e9","-\u00e9"],"hostname":"example","identity":[1000,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxx,C:/host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxx,host2,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,7352ea9d-bcbd-8444-c72e-978c92a02f8c"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":[],"hostname":"example","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,b35a836a-d6e8-5246-0bd0-e94bf90f2a1b"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9a","=","a\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"a-1","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":"-5","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxx,host1,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,e422bf00-8ed7-6660-3005-f625faaf5c76","distro,layer.erofs,e60e3700-74a1-87ce-28d6-5850938e08db","runtime,C:/layers/x.erofs,d1cb4c61-cfaf-2d0d-8fa4-53886bd19a99"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["=a\u00e9"],"hostname":"a-1","identity":[1000,1001],"memory_max":"1000000000000000000000000000000","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,C:/layers/x.erofs,b9027c65-8310-af42-e90e-82dd04c81cd6"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["--servea","-a-"],"hostname":"-a","identity":[65534,1001],"memory_max":"268435456","pids_max":null,"mounts":[],"command_line":"\u3000quiet\u001c","error":"sandbox hostname must be a lowercase RFC 1123 label up to 63 characters"}, +{"flavor":"posix","layers":["custom,a//b.erofs,7b96bc5b-5c40-f0bd-23a6-88ecc3303ef6","runtime,C:/layers/x.erofs,29e83381-2324-76cc-fe6a-35dc68bf5bfb"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,26377940-8fb4-1b48-da39-23355972ed05"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["-","a","--serve=","--serve\u00e9-","a","a=-","=--serve--serve","\u00e9","\u00e9--","-aa","=axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a","axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","\u00e9","--serve--serve--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxaa"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":null,"pids_max":"-5","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,1807eecd-9991-7114-f303-34fad13ffd04","custom,C:/layers/x.erofs,bdb0146a-5e85-c2aa-08e0-4e121c0174a2","distro,C:/layers/x.erofs,c7ff0a0d-02de-b93b-6b95-0462c52389eb"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["-","\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=","=-"],"hostname":"a-1","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,9c82caeb-7fbf-649d-b7e1-df9580640bfe"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["a-","\u00e9axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"quiet","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,381f84d2-d206-d7d7-3081-939562e65fcd"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":["\u00e9"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":"0","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,C:/layers/x.erofs,49296bbf-1475-1314-e0fd-e2ac954adb2e"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["a\u00e9","--serve","-","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"," a","--serve---serve","-=-","a=","--","-","=","=--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--serve","\u00e9=\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","-\u00e9","--serve="],"hostname":"example","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxx,host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"sandbox arguments must be nonempty and contain no spaces"}, +{"flavor":"posix","layers":["distro,C:/layers/x.erofs,9b52e70e-70fd-cfb4-72db-c138cb0eca66","custom,C:/layers/x.erofs,2fab978a-97cd-05e9-58ee-1b2d3540ecc4"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9--serve","--serve","--serve\u00e9=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":"1000000000000000000000000000000","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxx,C:/host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxx,/srv2,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,C:/layers/x.erofs,1abe6ab6-c726-b2ef-c5ed-b0021cdeaf8f"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":["\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","="],"hostname":"nvx-sandbox","identity":[1000,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxx,host3,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv4,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host5,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,C:/layers/x.erofs,e1e026dc-6fde-d8b6-16f5-a0408e4582e8","custom,C:/layers/x.erofs,eb7e6df6-d5cd-f69c-346a-e63bdffcf53b","runtime,layer.erofs,7f1f9b36-1d74-9010-3326-834ca2291b5a"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":[],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":"268435456","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxx,host2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv3,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxx,/srv4,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host5,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["runtime,layer.erofs,3d413624-bdca-383d-8cb0-83c4baa05bfe","distro,C:/layers/x.erofs,30c51bc6-62c8-8b78-d2c7-eee2419698b9"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["-a--serve"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":"-1","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,a//b.erofs,8b55b36a-20da-fa71-ffcd-4e51b5e10357","runtime,layer.erofs,59ed1876-a053-3a73-e8e3-a7b96c93fa47","custom,a//b.erofs,e3f5c114-9242-8806-a00c-d69f4977fa56"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["--serve--","="],"hostname":"nvx-sandbox","identity":[0,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxx,host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,a//b.erofs,9e691881-0300-ca52-2c50-e8c83e948dcf","runtime,a//b.erofs,02c9b13d-015e-2e21-3d98-f8a7d27a7b17","distro,a//b.erofs,476b8313-9c4a-1345-0d3b-ec2d9b5bd505"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":["\u00e9\u00e9=","\u00e9"],"hostname":"example","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxx,C:/host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,layer.erofs,9aafe5c8-9f83-3396-72e7-c5c7d3b81854","custom,a//b.erofs,ff999368-15f9-fab7-86fe-020310920f88","runtime,layer.erofs,b7f48d84-d46a-24ec-c6f3-a70eb87faf46"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["a--serve","--serve","--serve--","a","=="],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":"64","mounts":[],"command_line":"quiet","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,layer.erofs,98168019-db31-b55e-c718-6c47562e9e46"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["="],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"quiet","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:s/x.ext4","--microvm-workload-identity","65534:65534"],"kernel_command_line":"quiet nvx_sandbox=1 nvx_layer=distro,0xd0003000,98168019-db31-b55e-c718-6c47562e9e46 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/workload nvx_hostname=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa nvx_arg== nvx_pids_max=100000000000000000000"}, +{"flavor":"posix","layers":["custom,layer.erofs,9ea29769-eccc-6ffc-ed84-a34742514a38","distro,C:/layers/x.erofs,0c405b57-4609-3001-3b79-876fc48932ff"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["=--servea","--serveaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=","a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-a","-\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","-","-=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","=a","--","--serve","--serve","-a","=","--serve--serve","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx="],"hostname":"a-1","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,a//b.erofs,4eb9daf7-6320-096e-1b33-8ff6c19d9fb5"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=--serve=","\u00e9","\u00e9"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:a/b.erofs,ro","--microvm-sandbox-block","scratch:file:s/x.ext4","--microvm-workload-identity","65534:1001","--mount-aggregate","/run/nvx/shares","--mount-child","0-a7738b94c784eb9f25519afb93eb632c,C:/host0,ro,file","--mount-child","1-beb5cac71bf749dd6c188b6843897bcd,host1,rw,file","--mount-child","2-a37404d6fb3eebf56550e9d970c91be4,/srv2,ro,file"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"}, +{"flavor":"posix","layers":["runtime,layer.erofs,3874f07d-eb38-4ccd-d920-f80fbbe3b5f0"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["\u00e9-"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxx,C:/host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,layer.erofs,9b160fb3-4e90-e71d-6ade-9dc9405aa9f5"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01,C:/host1,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"quiet","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:1001","--mount-aggregate","/run/nvx/shares","--mount-child","0-b1b1a0197f57610192a87247301356fd,host0,ro","--mount-child","1-932de0c49c6028fcd06c916b12e85dd0,C:/host1,rw","--mount-deny","/abs/secrets","--mount-deny","/fixture/cwd/C:/host1/secrets"],"kernel_command_line":"quiet nvx_sandbox=1 nvx_layer=distro,0xd0003000,9b160fb3-4e90-e71d-6ade-9dc9405aa9f5 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/usr/bin/python3 nvx_hostname=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa nvx_pids_max=100000000000000000000 nvx_share=0-b1b1a0197f57610192a87247301356fd,/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,ro nvx_share=1-932de0c49c6028fcd06c916b12e85dd0,/share/01,rw"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,40bfe2b3-c3d1-27bb-eaf5-90131bec9b6d","distro,layer.erofs,99d9626a-0b52-f6ea-b8ce-902131f7af0a","runtime,a//b.erofs,54564aca-d5d6-e34f-5382-66bf6146e432"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["\u00e9=","=a","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","=\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=-","=--serve","--serve=--serve","a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","--serve","-a\u00e9","a==","-==","-","\u00e9","-a","=\u00e9\u00e9","\u00e9=\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=\u00e9"],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,a//b.erofs,f42346ad-28e5-1e35-42f2-0362b5f9da80","runtime,a//b.erofs,6f00cdad-33b4-8768-d835-7ca07babe0e5"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"-a","identity":[65534,65534],"memory_max":"-1","pids_max":null,"mounts":[],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,C:/layers/x.erofs,b42d715a-326e-eabe-c3e5-e4c4159865e1","runtime,C:/layers/x.erofs,e187ff8f-bf00-817c-9f43-0131c7c6c5b3","distro,C:/layers/x.erofs,e25afba1-b959-b870-41b2-087b91666dbb"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["\u00e9-=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"example","identity":[65534,1001],"memory_max":"268435456","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxx,/srv1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,542a5777-baad-ecbb-cf4f-c9d582336147","distro,C:/layers/x.erofs,75f56217-c521-b77b-e792-f88901fa3b68","runtime,C:/layers/x.erofs,8cd07922-d2db-a8ef-26e2-fbc9500af744"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["\u00e9","a-=","=a","\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9=--serve","\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-=","\u00e9\u00e9a","axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=--serve","--serve\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve\u00e9","a-\u00e9","a=--serve","\u00e9=-","--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","a"],"hostname":"example","identity":[0,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,6472274c-203d-9af4-5426-bc1fe2a9da7f","distro,layer.erofs,9bd8619b-d800-af1e-737c-24f861ab86f9","runtime,layer.erofs,79294eb6-ba2f-fc82-d85c-ae57c80d9e5f"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["--serve"],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,a//b.erofs,31a4c298-cabd-b7f5-2606-e4c0c083cb44"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":["-\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9\u00e9"],"hostname":"example","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,a//b.erofs,1243acfd-655b-e052-eb75-57e9e5bc6825"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["a-","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,layer.erofs,09795141-58d4-10d2-6c91-f38c58ec1049","distro,C:/layers/x.erofs,1ecbf796-df8f-416b-9d18-ea465ac64fc2","runtime,a//b.erofs,e6ade518-bb51-b59b-9485-62c504e60773"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["=aa"],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["custom,a//b.erofs,5abed48a-edba-04dc-9b45-38bf7d4e05b5","distro,C:/layers/x.erofs,97cc1592-5175-c246-e89e-a9e378e97844"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["a-","\u00e9\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a==","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve\u00e9"],"hostname":"nvx-sandbox","identity":[65534,0],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"posix","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534"],"kernel_command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox"}, +{"flavor":"posix","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"}, +{"flavor":"posix","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/workspace,host,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount","/workspace,host,rw"],"kernel_command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox"}, +{"flavor":"posix","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/workspace,host,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount","/workspace,host,rw"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"}, +{"flavor":"posix","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/workspace,host,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/srv/cache,cache","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-c52ddf65534b7b46035084358ab7902b,host,rw","--mount-child","1-2a93e82e011c66d5c7dbd25da5f2e6c1,cache,ro"],"kernel_command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-c52ddf65534b7b46035084358ab7902b,/workspace,rw nvx_share=1-2a93e82e011c66d5c7dbd25da5f2e6c1,/srv/cache,ro"}, +{"flavor":"posix","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/workspace,host,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/srv/cache,cache","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-c52ddf65534b7b46035084358ab7902b,host,rw","--mount-child","1-2a93e82e011c66d5c7dbd25da5f2e6c1,cache,ro"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"}, +{"flavor":"posix","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/etc/app/settings.json,settings.json","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-3ddd35a70a0d7386a5451cd0d5dc3c39,settings.json,ro,file"],"kernel_command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-3ddd35a70a0d7386a5451cd0d5dc3c39,/etc/app/settings.json,ro,file"}, +{"flavor":"posix","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/etc/app/settings.json,settings.json","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-3ddd35a70a0d7386a5451cd0d5dc3c39,settings.json,ro,file"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"}, +{"flavor":"windows","layers":["runtime,layer.erofs,a6b34c85-86b9-b646-18dc-28887414f5ab","custom,C:/layers/x.erofs,2089b5af-e1d7-1d94-940d-631f55b9c56a","distro,a//b.erofs,273ccf9a-9d5a-fd30-95b8-921f5d2dc314"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/s h","args":["--","a\u00e9","axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a\u00e9","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,a//b.erofs,a55e4eb1-7eeb-35d5-65b0-f6515125d6da"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["==","---serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=","=\u00e9\u00e9","==","\u00e9axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-","=--serve\u00e9","=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=--serve","a","a\u00e9\u00e9","\u00e9--serve=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9-","a=","--serve","=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=\u00e9"],"hostname":"example","identity":[65534,1001],"memory_max":"268435456","pids_max":"99999999999999999999","mounts":[],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,a//b.erofs,b8ea9084-e245-0269-f314-5038109e52c7","custom,a//b.erofs,84982b13-1f07-8906-6ec1-2925deb75152","distro,C:/layers/x.erofs,d1edc9fd-41f9-e89c-daf1-e00aca033141"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["="],"hostname":"nvx-sandbox","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,7ad560cb-c7d8-f83a-e691-da5182adada5","distro,layer.erofs,82ecd171-e7cf-f834-657d-da4581d68d3f","runtime,C:/layers/x.erofs,a093d2e7-193b-58b1-a44f-cee78f39af18"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":[],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,a//b.erofs,6a18f14b-7162-bf87-25ae-bd7d60a056c1","custom,layer.erofs,d41e6f2e-e259-8f87-f16a-b0eb536f6387"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["=","axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--serve","---servea","=a","\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","---serve-","-a-","--serve","==","\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","---serve--serve","---serve","a","a--","\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","\u00e9\u00e9","-\u00e9--serve"],"hostname":"a-1","identity":[65534,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,layer.erofs,fa3780d0-74bb-4dbb-77d3-8accf1ebeb7a","distro,a//b.erofs,d7ef509b-4a84-cd81-2da1-49e3f43f3b09","custom,layer.erofs,9a364c2e-3436-d5a3-8962-b8d75d6b7d1a"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9a=","a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a=","-=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","a--serve","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-","--serve\u00e9\u00e9","=","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv3,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host4","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host5,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,ecd45898-c6a9-5923-3199-967b9abf7d03","custom,C:/layers/x.erofs,818aa272-63bd-1821-36d6-ccbe0207b303","distro,layer.erofs,35f11375-8399-9dc0-6b2f-0d9545582d31"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["\u00e9-\u00e9","=\u00e9--serve","=a=","---serve=","a=="],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,aed22c49-c733-1bea-fdd4-f8d4204e7498","runtime,a//b.erofs,316b4f19-337d-4146-8519-565eac369b4f"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["=\u00e9","\u00e9\u00e9","a--serve=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","a---serve"],"hostname":"nvx-sandbox","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,a//b.erofs,c7f97bdf-106d-df55-61a9-eb0faa803f3a","runtime,layer.erofs,83f82d7f-2371-9b7c-0e26-943daaa61eff","distro,a//b.erofs,30b5620f-c623-5963-6872-06e17b596643"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["\u00e9a="],"hostname":"-a","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,layer.erofs,22540f15-7117-5e21-5452-55a9556d441a"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"example","identity":[1000,65534],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host3,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxx,/srv4,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxx,/srv5","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,a3e157e4-255a-4257-8176-51c423463089"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":["-\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"nvx-sandbox","identity":[1000,65534],"memory_max":"1000000000000000000000000000000","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,C:/layers/x.erofs,f67d6ca6-1638-7a37-ea24-142db6296541","runtime,layer.erofs,9a0f0e67-9693-840c-31cf-06d86151d54e","custom,C:/layers/x.erofs,ad0acdad-3270-3bc9-0056-44a8918755b1"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":["=axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,979d8723-9b7c-97cb-3c0c-5fce959e3cf4"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","aaa","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a-\u00e9"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/03xxxxxxx,C:/host3","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host4,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv5,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,C:/layers/x.erofs,37358704-42ba-7bc2-f7c8-5e3081e1ce65"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["=\u00e9="],"hostname":"example","identity":[1000,65534],"memory_max":"0","pids_max":"64","mounts":[],"command_line":" quiet loglevel=0 ","error":"--memory-max must be positive"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,45a311d8-bac0-b48e-88e2-c3f5aaf96468"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"example","identity":[1000,1001],"memory_max":"0","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,656a4d6d-68ff-9815-1e85-4d1acbb1a7c1","distro,C:/layers/x.erofs,6717cc9a-5a06-db08-681a-da2a855ad0f8","runtime,layer.erofs,80e671ee-cc09-1bd9-e007-0d2d511a571e","custom,a//b.erofs,cbda55fe-5c2b-a8a6-4394-5ef21b718a2c"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["=="],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":"0","mounts":[],"command_line":"quiet","error":"duplicate sandbox layer role(s): custom"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,cdc95b00-831b-01c1-feae-4724354542f0"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["--serve--serve-","-a"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":"0","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxx,C:/host3","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host4","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host5,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,layer.erofs,580e9941-5f5c-2c30-7ac2-127c4bd8e3c6"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["\u00e9","-"],"hostname":"example","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[],"command_line":"virtfs_dir=/x","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:C:\\s\\scratch.ext4","--microvm-workload-identity","65534:1001"],"kernel_error":"virtfs_dir is owned by the sandbox --mount option"}, +{"flavor":"windows","layers":["distro,a//b.erofs,8cf91078-9556-d1be-17dc-fae9bcba794b"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":[],"hostname":"Example","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxx,C:/host0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host2,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host3,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host4,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv5","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"sandbox hostname must be a lowercase RFC 1123 label up to 63 characters"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,27c4fb6d-0c0e-4ea4-bbb5-b4d385bb4fe8"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":["--serve-=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9="],"hostname":"a-1","identity":[65534,65534],"memory_max":"268435456","pids_max":null,"mounts":[],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,3cf8bfb6-5fb6-a7bc-1f86-1531a352c85b","runtime,C:/layers/x.erofs,97843d62-9330-fda7-a4fa-ab7eeaac6288","custom,a//b.erofs,7b056600-c055-1ef7-1705-5d36f2d41d94"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["=---serve"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"duplicate sandbox layer role(s): custom"}, +{"flavor":"windows","layers":["runtime,layer.erofs,50f0c385-3492-d186-d292-0a4727a31653","custom,layer.erofs,a40c25c8-1363-d257-5a07-4bbe91024a98"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["-"],"hostname":"example","identity":[1000,1001],"memory_max":"268435456","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,layer.erofs,b8d32eb7-6a38-70fa-b5b1-590ba4479c23"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve","aa","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","--serve"],"hostname":"nvx-sandbox","identity":[1000,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[],"command_line":"quiet","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,layer.erofs,91c1e6d7-63c1-35cc-e117-cd98af9dbee1"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["---serve=","\u00e9-=","--serve=","--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=","\u00e9a\u00e9","=","\u00e9","-","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9--servea","--serve=-","aa\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","a","\u00e9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"nvx-sandbox","identity":[1000,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"nvx_sandbox=0","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:C:\\s\\scratch.ext4","--microvm-workload-identity","1000:65534","--mount","/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,\\srv0,ro"],"kernel_error":"nvx_sandbox is owned by the sandbox profile"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,adb35722-f24d-bd71-55fd-5392656d3c19"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["--serve==","\u00e9a"],"hostname":"example","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,b6ef98b9-3131-a98e-89b9-b0273ea7a45c"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa"],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,a//b.erofs,bf51fb7a-ef3c-09cb-432c-5129ae3686bc"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["-\u00e9--serve"],"hostname":"example","identity":[0,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,0a4a8a0b-7cbe-af5d-f52c-55bb4e9761d5","distro,layer.erofs,42660302-8b2f-5d60-e6c6-5c5414b85a76"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["=axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-"],"hostname":"a-1","identity":[1000,65534],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,a//b.erofs,acdd08a1-769a-c3fd-d20b-e8f0b22bdfa5","custom,C:/layers/x.erofs,8cd77740-856f-e40d-b0ce-e8a8dba164b3"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":["=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,layer.erofs,dc6b958d-36cb-e08a-b2f0-6945285a3abe"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":[],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxx,C:/host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,f0916002-b89e-3079-70b4-99fb50379579"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":[],"hostname":"a-1","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,a//b.erofs,0aed6803-962d-9098-90df-c2cdf949064f","distro,layer.erofs,b84c1d3d-670c-bd20-1b15-9e58a57cc159","custom,layer.erofs,386e7680-0139-2cc3-143a-115e64d7b9a8","runtime,a//b.erofs,75b6a5ef-af13-ef61-f7df-0cef00020c8b"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve","=\u00e9a","a=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","--servea","=a","--a","a--serve","=\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a","--servea\u00e9","--servea","\u00e9","--serve=","a","=","-","a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"quiet","error":"duplicate sandbox layer role(s): runtime"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,a54937bf-c3b5-4730-31d0-149d5d967101"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":["--serve"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":"0","mounts":[],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,a//b.erofs,c855b135-ff10-b15f-5bc9-43ebe55f0ef4","runtime,C:/layers/x.erofs,5c3148c0-2bf5-3137-a7dd-cafc0d0283ba"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve\u00e9\u00e9","-","--serve","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[],"command_line":"\u3000quiet\u001c","error":"duplicate sandbox layer role(s): runtime"}, +{"flavor":"windows","layers":["custom,a//b.erofs,bd09dc76-81fd-1b51-4250-14657026e68f","distro,layer.erofs,1ef5a04a-eada-2969-9499-c436a26d1f57","runtime,C:/layers/x.erofs,8e704a5e-c05d-5a80-c3db-d9797c548e73"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["-=a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxx,C:/host2","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host3,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv4,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host5","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,C:/layers/x.erofs,fde517fd-15ae-769b-c408-c4cf745c8c91"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":["aa-"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxx,host1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:C:\\layers\\x.erofs,ro","--microvm-sandbox-block","scratch:file:C:\\s\\scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-e1ed8cbca2406f01ea48580ea6726a7d,C:\\host0,ro,file","--mount-child","1-1ec7b2c1648b15995214377627d3ac85,host1,ro,file"],"kernel_command_line":"nvx_sandbox=1 nvx_layer=distro,0xd0003000,fde517fd-15ae-769b-c408-c4cf745c8c91 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/usr/bin/python3 nvx_hostname=nvx-sandbox nvx_arg=aa- nvx_pids_max=100000000000000000000 nvx_share=0-e1ed8cbca2406f01ea48580ea6726a7d,/share/00xxxxxxxxx,ro,file nvx_share=1-1ec7b2c1648b15995214377627d3ac85,/share/01xxxxxxxxxxxxxxxxxxx,ro,file"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,3c0d4ab6-c5f5-6f5a-d586-c927ae45ad51"],"scratch":"s//x.ext4","entrypoint":"bin/sh","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"a-1","identity":[65534,1001],"memory_max":"268435456","pids_max":"64","mounts":[],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,a//b.erofs,715d50b1-642e-1b8e-2c2f-7ae3bc2e03ac"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":[],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":"-1","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,2fa9ef3c-075b-32db-6fc2-fdf406368f63"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":[],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,a//b.erofs,c7006cfe-c270-a90c-82a4-23f659021ab5"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["\u00e9\u00e9--serve"],"hostname":"a-1","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxx,/srv1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv3,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host4,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host5","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,layer.erofs,9d716755-a832-c8eb-e326-ccb202412244"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":[""],"hostname":"example","identity":[65534,1001],"memory_max":"268435456","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxx,host1,ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host2,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,0fe6f305-a1ba-b6d8-c7c1-e3c4984d3406"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["-"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":"268435456","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,c97e31d1-65d8-53d1-5b65-0eb79ec81bc7"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":"1000000000000000000000000000000","pids_max":"99999999999999999999","mounts":[],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,layer.erofs,42b4357a-f158-1dfb-204c-9322e928c494","custom,a//b.erofs,04648c41-7397-c262-b5f6-6578aaf5bc48","runtime,layer.erofs,e219f7e4-5f5e-87ca-6ab1-bc02ce3b2ecd"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["=","a","=","=a","--serve","\u00e9","--serve-=","\u00e9a\u00e9","--serve","-\u00e9=","=\u00e9","=-a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve-","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","a","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","a\u00e9\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","--serve=--serve"],"hostname":"example","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxx,host1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,layer.erofs,ce8c30ed-7078-382e-a2f5-58d465c41c45","custom,layer.erofs,9637ff70-e851-069f-1aa1-245704a483a0"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"a-1","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxx,/srv2,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxx,C:/host3,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv4","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host5","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,C:/layers/x.erofs,be50e9fe-48e6-328d-afa8-a42b4a9c4183","custom,C:/layers/x.erofs,f09059aa-8b88-3035-b44a-98c496aeeb34"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"example","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,235e05b2-3e18-b2d4-d5c0-9b7f4e1c3d83","distro,C:/layers/x.erofs,0c8d13ee-2896-e0ec-5c63-a29f538beb32","custom,a//b.erofs,3a37bc91-a398-5563-9e33-4af0f1dd6953","runtime,layer.erofs,0cb4e2a4-f2c8-4b56-1e85-fc5b35526df9"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["a--serve","=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9"],"hostname":"example","identity":[1000,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host3,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host4,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host5,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"quiet","error":"duplicate sandbox layer role(s): runtime"}, +{"flavor":"windows","layers":["custom,a//b.erofs,3d2c838c-80d9-57a2-44fa-f475e3836922","runtime,C:/layers/x.erofs,998632ef-57f3-4330-1f76-d691aad62538"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["-\u00e9a","\u00e9=a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9","--serve=--serve","-","\u00e9","\u00e9aa","-=","-","\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","=","\u00e9a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","-a","--serve-","--\u00e9","--serve--serve\u00e9","--serve","\u00e9"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,65534],"memory_max":null,"pids_max":null,"mounts":[],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,eb1dacef-aea8-e610-fb13-5ef9f9df5b30","distro,layer.erofs,a8ba9752-dc18-0619-8bf2-e529f721a7f2"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["\u00e9\u00e9","-\u00e9"],"hostname":"example","identity":[1000,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxx,C:/host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxx,host2,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"nvx_sandbox=0","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,7352ea9d-bcbd-8444-c72e-978c92a02f8c"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":[],"hostname":"example","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,b35a836a-d6e8-5246-0bd0-e94bf90f2a1b"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9a","=","a\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"a-1","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":"-5","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxx,host1,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,e422bf00-8ed7-6660-3005-f625faaf5c76","distro,layer.erofs,e60e3700-74a1-87ce-28d6-5850938e08db","runtime,C:/layers/x.erofs,d1cb4c61-cfaf-2d0d-8fa4-53886bd19a99"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["=a\u00e9"],"hostname":"a-1","identity":[1000,1001],"memory_max":"1000000000000000000000000000000","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,C:/layers/x.erofs,b9027c65-8310-af42-e90e-82dd04c81cd6"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["--servea","-a-"],"hostname":"-a","identity":[65534,1001],"memory_max":"268435456","pids_max":null,"mounts":[],"command_line":"\u3000quiet\u001c","error":"sandbox hostname must be a lowercase RFC 1123 label up to 63 characters"}, +{"flavor":"windows","layers":["custom,a//b.erofs,7b96bc5b-5c40-f0bd-23a6-88ecc3303ef6","runtime,C:/layers/x.erofs,29e83381-2324-76cc-fe6a-35dc68bf5bfb"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,26377940-8fb4-1b48-da39-23355972ed05"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["-","a","--serve=","--serve\u00e9-","a","a=-","=--serve--serve","\u00e9","\u00e9--","-aa","=axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a","axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","\u00e9","--serve--serve--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxaa"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":null,"pids_max":"-5","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,1807eecd-9991-7114-f303-34fad13ffd04","custom,C:/layers/x.erofs,bdb0146a-5e85-c2aa-08e0-4e121c0174a2","distro,C:/layers/x.erofs,c7ff0a0d-02de-b93b-6b95-0462c52389eb"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["-","\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=","=-"],"hostname":"a-1","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,9c82caeb-7fbf-649d-b7e1-df9580640bfe"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["a-","\u00e9axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"quiet","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,381f84d2-d206-d7d7-3081-939562e65fcd"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":["\u00e9"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":"0","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,C:/layers/x.erofs,49296bbf-1475-1314-e0fd-e2ac954adb2e"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["a\u00e9","--serve","-","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"," a","--serve---serve","-=-","a=","--","-","=","=--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--serve","\u00e9=\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","-\u00e9","--serve="],"hostname":"example","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxx,host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"sandbox arguments must be nonempty and contain no spaces"}, +{"flavor":"windows","layers":["distro,C:/layers/x.erofs,9b52e70e-70fd-cfb4-72db-c138cb0eca66","custom,C:/layers/x.erofs,2fab978a-97cd-05e9-58ee-1b2d3540ecc4"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9--serve","--serve","--serve\u00e9=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":"1000000000000000000000000000000","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxx,C:/host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxx,/srv2,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u3000quiet\u001c","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,C:/layers/x.erofs,1abe6ab6-c726-b2ef-c5ed-b0021cdeaf8f"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":["\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","="],"hostname":"nvx-sandbox","identity":[1000,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxx,host3,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv4,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host5,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,C:/layers/x.erofs,e1e026dc-6fde-d8b6-16f5-a0408e4582e8","custom,C:/layers/x.erofs,eb7e6df6-d5cd-f69c-346a-e63bdffcf53b","runtime,layer.erofs,7f1f9b36-1d74-9010-3326-834ca2291b5a"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":[],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":"268435456","pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv1,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxx,host2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv3,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/04xxxxxxxxxxxxxxxxxxxx,/srv4,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/05xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host5,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["runtime,layer.erofs,3d413624-bdca-383d-8cb0-83c4baa05bfe","distro,C:/layers/x.erofs,30c51bc6-62c8-8b78-d2c7-eee2419698b9"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["-a--serve"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,1001],"memory_max":"-1","pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,a//b.erofs,8b55b36a-20da-fa71-ffcd-4e51b5e10357","runtime,layer.erofs,59ed1876-a053-3a73-e8e3-a7b96c93fa47","custom,a//b.erofs,e3f5c114-9242-8806-a00c-d69f4977fa56"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["--serve--","="],"hostname":"nvx-sandbox","identity":[0,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxx,host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host2,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,a//b.erofs,9e691881-0300-ca52-2c50-e8c83e948dcf","runtime,a//b.erofs,02c9b13d-015e-2e21-3d98-f8a7d27a7b17","distro,a//b.erofs,476b8313-9c4a-1345-0d3b-ec2d9b5bd505"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":["\u00e9\u00e9=","\u00e9"],"hostname":"example","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[{"value":"/share/00xxxxx,C:/host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,layer.erofs,9aafe5c8-9f83-3396-72e7-c5c7d3b81854","custom,a//b.erofs,ff999368-15f9-fab7-86fe-020310920f88","runtime,layer.erofs,b7f48d84-d46a-24ec-c6f3-a70eb87faf46"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["a--serve","--serve","--serve--","a","=="],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":"1000000000000000000000000000000","pids_max":"64","mounts":[],"command_line":"quiet","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,layer.erofs,98168019-db31-b55e-c718-6c47562e9e46"],"scratch":"s//x.ext4","entrypoint":"/bin/workload","args":["="],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"quiet","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:s\\x.ext4","--microvm-workload-identity","65534:65534"],"kernel_command_line":"quiet nvx_sandbox=1 nvx_layer=distro,0xd0003000,98168019-db31-b55e-c718-6c47562e9e46 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/workload nvx_hostname=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa nvx_arg== nvx_pids_max=100000000000000000000"}, +{"flavor":"windows","layers":["custom,layer.erofs,9ea29769-eccc-6ffc-ed84-a34742514a38","distro,C:/layers/x.erofs,0c405b57-4609-3001-3b79-876fc48932ff"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["=--servea","--serveaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=","a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-a","-\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","-","-=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","=a","--","--serve","--serve","-a","=","--serve--serve","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx="],"hostname":"a-1","identity":[1000,65534],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,a//b.erofs,4eb9daf7-6320-096e-1b33-8ff6c19d9fb5"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=--serve=","\u00e9","\u00e9"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host1,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:a\\b.erofs,ro","--microvm-sandbox-block","scratch:file:s\\x.ext4","--microvm-workload-identity","65534:1001","--mount-aggregate","/run/nvx/shares","--mount-child","0-a7738b94c784eb9f25519afb93eb632c,C:\\host0,ro,file","--mount-child","1-beb5cac71bf749dd6c188b6843897bcd,host1,rw,file","--mount-child","2-a37404d6fb3eebf56550e9d970c91be4,\\srv2,ro,file"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"}, +{"flavor":"windows","layers":["runtime,layer.erofs,3874f07d-eb38-4ccd-d920-f80fbbe3b5f0"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["\u00e9-"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxx,C:/host0","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,layer.erofs,9b160fb3-4e90-e71d-6ade-9dc9405aa9f5"],"scratch":"scratch.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[65534,1001],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01,C:/host1,rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"quiet","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:1001","--mount-aggregate","/run/nvx/shares","--mount-child","0-b1b1a0197f57610192a87247301356fd,host0,ro","--mount-child","1-932de0c49c6028fcd06c916b12e85dd0,C:\\host1,rw","--mount-deny","C:\\abs\\secrets","--mount-deny","C:\\host1\\secrets"],"kernel_command_line":"quiet nvx_sandbox=1 nvx_layer=distro,0xd0003000,9b160fb3-4e90-e71d-6ade-9dc9405aa9f5 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/usr/bin/python3 nvx_hostname=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa nvx_pids_max=100000000000000000000 nvx_share=0-b1b1a0197f57610192a87247301356fd,/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,ro nvx_share=1-932de0c49c6028fcd06c916b12e85dd0,/share/01,rw"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,40bfe2b3-c3d1-27bb-eaf5-90131bec9b6d","distro,layer.erofs,99d9626a-0b52-f6ea-b8ce-902131f7af0a","runtime,a//b.erofs,54564aca-d5d6-e34f-5382-66bf6146e432"],"scratch":"s//x.ext4","entrypoint":"/bin/sh","args":["\u00e9=","=a","--servexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","=\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=-","=--serve","--serve=--serve","a","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-","--serve","-a\u00e9","a==","-==","-","\u00e9","-a","=\u00e9\u00e9","\u00e9=\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=\u00e9"],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,a//b.erofs,f42346ad-28e5-1e35-42f2-0362b5f9da80","runtime,a//b.erofs,6f00cdad-33b4-8768-d835-7ca07babe0e5"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":[],"hostname":"-a","identity":[65534,65534],"memory_max":"-1","pids_max":null,"mounts":[],"command_line":" quiet loglevel=0 ","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,C:/layers/x.erofs,b42d715a-326e-eabe-c3e5-e4c4159865e1","runtime,C:/layers/x.erofs,e187ff8f-bf00-817c-9f43-0131c7c6c5b3","distro,C:/layers/x.erofs,e25afba1-b959-b870-41b2-087b91666dbb"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/workload","args":["\u00e9-=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"hostname":"example","identity":[65534,1001],"memory_max":"268435456","pids_max":"64","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxx,/srv1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv2","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,542a5777-baad-ecbb-cf4f-c9d582336147","distro,C:/layers/x.erofs,75f56217-c521-b77b-e792-f88901fa3b68","runtime,C:/layers/x.erofs,8cd07922-d2db-a8ef-26e2-fbc9500af744"],"scratch":"scratch.ext4","entrypoint":"/bin/workload","args":["\u00e9","a-=","=a","\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00e9=--serve","\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","-=","\u00e9\u00e9a","axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=--serve","--serve\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve\u00e9","a-\u00e9","a=--serve","\u00e9=-","--serve","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","=","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa","a"],"hostname":"example","identity":[0,1001],"memory_max":"1000000000000000000000000000000","pids_max":null,"mounts":[],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,6472274c-203d-9af4-5426-bc1fe2a9da7f","distro,layer.erofs,9bd8619b-d800-af1e-737c-24f861ab86f9","runtime,layer.erofs,79294eb6-ba2f-fc82-d85c-ae57c80d9e5f"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["--serve"],"hostname":"example","identity":[65534,1001],"memory_max":null,"pids_max":"99999999999999999999","mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,/srv0,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/share/01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host1,rw","denied":["/abs/secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9\u00e9","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,a//b.erofs,31a4c298-cabd-b7f5-2606-e4c0c083cb44"],"scratch":"C:/s/scratch.ext4","entrypoint":"/usr/bin/python3","args":["-\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9\u00e9"],"hostname":"example","identity":[65534,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"virtfs_dir=/x","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,a//b.erofs,1243acfd-655b-e052-eb75-57e9e5bc6825"],"scratch":"s//x.ext4","entrypoint":"/usr/bin/python3","args":["a-","-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxa"],"hostname":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","identity":[1000,65534],"memory_max":null,"pids_max":"99999999999999999999","mounts":[],"command_line":"","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,layer.erofs,09795141-58d4-10d2-6c91-f38c58ec1049","distro,C:/layers/x.erofs,1ecbf796-df8f-416b-9d18-ea465ac64fc2","runtime,a//b.erofs,e6ade518-bb51-b59b-9485-62c504e60773"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["=aa"],"hostname":"a-1","identity":[65534,1001],"memory_max":null,"pids_max":null,"mounts":[{"value":"/share/00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,C:/host0,ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"a\u001cnvx_x=1","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["custom,a//b.erofs,5abed48a-edba-04dc-9b45-38bf7d4e05b5","distro,C:/layers/x.erofs,97cc1592-5175-c246-e89e-a9e378e97844"],"scratch":"C:/s/scratch.ext4","entrypoint":"/bin/sh","args":["a-","\u00e9\u00e9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","a==","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","--serve\u00e9"],"hostname":"nvx-sandbox","identity":[65534,0],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"a\u0000b","error":"a sandbox requires exactly one read-only distro layer"}, +{"flavor":"windows","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534"],"kernel_command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox"}, +{"flavor":"windows","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"}, +{"flavor":"windows","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/workspace,host,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount","/workspace,host,rw"],"kernel_command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox"}, +{"flavor":"windows","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/workspace,host,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount","/workspace,host,rw"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"}, +{"flavor":"windows","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/workspace,host,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/srv/cache,cache","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-c52ddf65534b7b46035084358ab7902b,host,rw","--mount-child","1-2a93e82e011c66d5c7dbd25da5f2e6c1,cache,ro"],"kernel_command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-c52ddf65534b7b46035084358ab7902b,/workspace,rw nvx_share=1-2a93e82e011c66d5c7dbd25da5f2e6c1,/srv/cache,ro"}, +{"flavor":"windows","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/workspace,host,rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"value":"/srv/cache,cache","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-c52ddf65534b7b46035084358ab7902b,host,rw","--mount-child","1-2a93e82e011c66d5c7dbd25da5f2e6c1,cache,ro"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"}, +{"flavor":"windows","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/etc/app/settings.json,settings.json","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-3ddd35a70a0d7386a5451cd0d5dc3c39,settings.json,ro,file"],"kernel_command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx nvx_sandbox=1 nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 nvx_scratch=0xd0006000,ext4 nvx_entrypoint=/bin/sh nvx_hostname=nvx-sandbox nvx_share=0-3ddd35a70a0d7386a5451cd0d5dc3c39,/etc/app/settings.json,ro,file"}, +{"flavor":"windows","layers":["distro,layer.erofs,11111111-1111-1111-1111-111111111111"],"scratch":"scratch.ext4","entrypoint":"/bin/sh","args":[],"hostname":"nvx-sandbox","identity":[65534,65534],"memory_max":null,"pids_max":null,"mounts":[{"value":"/etc/app/settings.json,settings.json","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}],"command_line":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","arguments":["--machine","microvm","--microvm-sandbox-block","distro:file:layer.erofs,ro","--microvm-sandbox-block","scratch:file:scratch.ext4","--microvm-workload-identity","65534:65534","--mount-aggregate","/run/nvx/shares","--mount-child","0-3ddd35a70a0d7386a5451cd0d5dc3c39,settings.json,ro,file"],"kernel_error":"sandbox kernel command line exceeds its 1024-byte x86 budget; attach fewer --mount shares or use shorter targets and arguments"} +] +} diff --git a/nvx-rs/tests/fixtures/sandbox_options.json b/nvx-rs/tests/fixtures/sandbox_options.json new file mode 100644 index 000000000..1effa122b --- /dev/null +++ b/nvx-rs/tests/fixtures/sandbox_options.json @@ -0,0 +1,901 @@ +{ +"generator": "scripts/nvx_rs_fixtures.py, retired by microsoft/nvx#359", +"python": "3.14", +"cwds": {"posix": "/fixture/cwd", "windows": "C:\\fixture\\cwd"}, +"run_share_targets": [ +{"targets":["/workspace","/opt/hostedtoolcache","/srv/cache"],"error":null}, +{"targets":["/workspace","/workspace/cache"],"error":"--mount targets /workspace and /workspace/cache overlap; a share cannot hide another"}, +{"targets":["/run/nvx","/b"],"error":"--mount targets /run/nvx/shares and /run/nvx overlap; a share cannot hide another"}, +{"targets":["/run/nvx/shares/x"],"error":"--mount targets /run/nvx/shares and /run/nvx/shares/x overlap; a share cannot hide another"}, +{"targets":["/a b","/b"],"error":"invalid --mount target '/a b': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["relative","/b"],"error":"invalid --mount target 'relative': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"error":null}, +{"targets":[],"error":null}, +{"targets":["\u3000\u3000","/\u001cetcmachine-id"," \u3000\u00e9.\u001c"],"error":"invalid --mount target '\\u3000\\u3000': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u0000\\work\u3000"],"error":"invalid --mount target '\\x00\\\\work\\u3000': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["proc","/,"],"error":"invalid --mount target 'proc': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[".,\u0000 ","\\run","\u3000"],"error":"invalid --mount target '.,\\x00 ': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["run=","\u3000"],"error":"invalid --mount target 'run=': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u00e9",".machine-id","/\\work"],"error":"invalid --mount target '\u00e9': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[".. "],"error":"invalid --mount target '.. ': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[",a","\u0000shares"],"error":"invalid --mount target ',a': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u0000proc",".shares","proc"],"error":"invalid --mount target '\\x00proc': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["nvx"],"error":"invalid --mount target 'nvx': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["nvxprocmachine-id","shares"],"error":"invalid --mount target 'nvxprocmachine-id': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["etca\u001c."],"error":"invalid --mount target 'etca\\x1c.': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u3000shares etcetc,",",proc\u001ca","shares.\u00e9shares \u3000"],"error":"invalid --mount target '\\u3000shares etcetc,': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\\ work"],"error":"invalid --mount target '\\\\ work': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["=.,\u00e9,shares"],"error":"invalid --mount target '=.,\u00e9,shares': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["="],"error":"invalid --mount target '=': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[" \u3000\u3000"],"error":"invalid --mount target ' \\u3000\\u3000': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[".run=proc=","work..\u0000\u0000","nvx/\\..\u0000."],"error":"invalid --mount target '.run=proc=': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u0000\u0000runetc/shares"],"error":"invalid --mount target '\\x00\\x00runetc/shares': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u0000machine-id.","=a "],"error":"invalid --mount target '\\x00machine-id.': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["proc.\u3000runmachine-id","machine-ida,\\a"],"error":"invalid --mount target 'proc.\\u3000runmachine-id': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u00e9.\\\\","\u3000"],"error":"invalid --mount target '\u00e9.\\\\\\\\': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["run"],"error":"invalid --mount target 'run': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["run,.. machine-id","worksharesnvx="],"error":"invalid --mount target 'run,.. machine-id': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u001crunmachine-id\u3000work"],"error":"invalid --mount target '\\x1crunmachine-id\\u3000work': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["awork,\u00e9..,"],"error":"invalid --mount target 'awork,\u00e9..,': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["proc=","work machine-id,\u001c"],"error":"invalid --mount target 'proc=': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["shares"],"error":"invalid --mount target 'shares': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u3000","..\\"],"error":"invalid --mount target '\\u3000': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["nvx,","\u3000 arunaa","work"],"error":"invalid --mount target 'nvx,': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[".,ashares\u00e9 ","/..\\. .."],"error":"invalid --mount target '.,ashares\u00e9 ': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[",nvx\\proc=","run==","work..,="],"error":"invalid --mount target ',nvx\\\\proc=': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[",\u001c"],"error":"invalid --mount target ',\\x1c': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["=\u001cnvx",".","\\"],"error":"invalid --mount target '=\\x1cnvx': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u001cshares\u0000","nvx/machine-id\u0000","run\u00e9\u00e9machine-idmachine-id"],"error":"invalid --mount target '\\x1cshares\\x00': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["worknvxshares",".\u0000","nvxshares=shares"],"error":"invalid --mount target 'worknvxshares': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[",proc.machine-id","\\procawork\u0000"],"error":"invalid --mount target ',proc.machine-id': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":[".nvx",",sharesnvxrun,","/machine-idmachine-id .."],"error":"invalid --mount target '.nvx': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\\etcworknvx","\\etc \u00e9\u001c"],"error":"invalid --mount target '\\\\etcworknvx': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["\u0000nvx","/"],"error":"invalid --mount target '\\x00nvx': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/work","/a/data-1/run","/nvx/\u00e9","/work/data-1/data-1"],"error":"--mount targets /work and /work/data-1/data-1 overlap; a share cannot hide another"}, +{"targets":["/srv"],"error":null}, +{"targets":["/srv","/a/cache/run","/srv/a/srv"],"error":"--mount targets /srv and /srv/a/srv overlap; a share cannot hide another"}, +{"targets":["/data-1","/cache/a/a","/nvx","/etc"],"error":null}, +{"targets":["/srv","/etc/srv"],"error":null}, +{"targets":["/srv/srv/etc","/a/a/cache","/srv/etc/etc","/data-1/cache/etc"],"error":null}, +{"targets":["/nvx/cache/etc"],"error":null}, +{"targets":["/a/a","/work"],"error":null}, +{"targets":["/srv/nvx","/work/nvx/etc","/work","/work/data-1"],"error":"--mount targets /work/nvx/etc and /work overlap; a share cannot hide another"}, +{"targets":["/nvx/nvx/cache","/run"],"error":"--mount targets /run/nvx/shares and /run overlap; a share cannot hide another"}, +{"targets":["/aetc/data-1","/run/\u001c","/srv/etc/\u00e9"],"error":"invalid --mount target '/run/\\x1c': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/cache/data-1","/etc"],"error":null}, +{"targets":["/srv/work/\u00e9","/work/run","/nvx"],"error":null}, +{"targets":["/a/srv","/srv/\u00e9","/srv/work","/nvx/cache/etc"],"error":null}, +{"targets":["/data-1/srv","/cache","/srv","/\u00e9/\u00e9/a"],"error":null}, +{"targets":["/run/run/cache"],"error":null}, +{"targets":["/a//","/cache","/\u00e9","/srv/work"],"error":"invalid --mount target '/a//': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/work/a","/nvx/\u00e9","/work/srv/data-1","/data-1/\u00e9/srv"],"error":null}, +{"targets":["/srv/data-1/\u00e9","/etc/nvx","/srv/cache","/\u001c\\/work"],"error":"invalid --mount target '/\\x1c\\\\/work': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/work/nvx","/srv/data-1","/srv/etc","/\u0000"],"error":"invalid --mount target '/\\x00': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/a/nvx/etc","/run/data-1/\u00e9","/etc/nvx/cache","/etc/work/nvx"],"error":null}, +{"targets":["/\u00e9/etc/etc","/work"],"error":null}, +{"targets":["//srv"],"error":"invalid --mount target '//srv': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/cache/work/nvx"],"error":null}, +{"targets":["/run/run"],"error":null}, +{"targets":["/\\","/cache","/nvx/data-1/\u00e9","/\u00e9/etc"],"error":"invalid --mount target '/\\\\': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/\u00e9","/ /\u00e9","/cache/srv/work","/work"],"error":"invalid --mount target '/ /\u00e9': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/srv/data-1/nvx"],"error":null}, +{"targets":["/data-1","/run"],"error":"--mount targets /run/nvx/shares and /run overlap; a share cannot hide another"}, +{"targets":["/data-1/cache/srv","/\u00e9","/a/data-1/etc"],"error":null}, +{"targets":["/","/data-1/data-1/etc"],"error":"invalid --mount target '/': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/run/a/a","/nvx/a","/\u00e9"],"error":null}, +{"targets":["/nvx/work"],"error":null}, +{"targets":["/data-1/data-1","/a/nvx","/srv"],"error":null}, +{"targets":["/run"],"error":"--mount targets /run/nvx/shares and /run overlap; a share cannot hide another"}, +{"targets":["/srv"],"error":null}, +{"targets":["/.\\","/srv","/work/\u00e9/\u00e9","/=/etc/\u00e9"],"error":"invalid --mount target '/.\\\\': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/nvx/a/a","/\u00e9"],"error":null}, +{"targets":["/a","/data-1/nvx","/etc/srv/run\u001cwork","/work/srv/cache"],"error":"invalid --mount target '/etc/srv/run\\x1cwork': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/data-1/srv","/run/work/cache","/etc/a/\u3000nvx"],"error":"invalid --mount target '/etc/a/\\u3000nvx': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/cache/a/a","/a/etc","/run/srv/work"],"error":null}, +{"targets":["/cache","/nvx/etc/a","/run/run/srv","/cache/data-1/a"],"error":"--mount targets /cache and /cache/data-1/a overlap; a share cannot hide another"}, +{"targets":["/etc","/cache","/work","/data-1/work"],"error":null}, +{"targets":["/etc/\u00e9","/data-1/srv/run","/a/work"],"error":null}, +{"targets":["/cache/srv/data-1","/\u00e9/a/\u00e9","/cache/srv"],"error":"--mount targets /cache/srv/data-1 and /cache/srv overlap; a share cannot hide another"}, +{"targets":["/cache/nvx/\u00e9","/run/a/etc"],"error":null}, +{"targets":["/./nvx","/\u00e9","/work/etc","/nvx/srv"],"error":"invalid --mount target '/./nvx': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/work","/\u00e9/work"],"error":null}, +{"targets":["/\u00e9/srv","/cache/\u0000","/data-1/cache"],"error":"invalid --mount target '/cache/\\x00': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/work/run/run","/work","/etc"],"error":"--mount targets /work/run/run and /work overlap; a share cannot hide another"}, +{"targets":["/etc","/etc/cache/srv","/data-1/srv"],"error":"--mount targets /etc and /etc/cache/srv overlap; a share cannot hide another"}, +{"targets":["/data-1/nvx/nvx","/srv/work"],"error":null}, +{"targets":["/run/run","/work"],"error":null}, +{"targets":["/cache/cache/nvx","//\\","/run/run","/cache/data-1/run"],"error":"invalid --mount target '//\\\\': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/srv/work","/nvx/data-1","/nvx","/etc/a/cache"],"error":"--mount targets /nvx/data-1 and /nvx overlap; a share cannot hide another"}, +{"targets":["/\u00e9","/\\\\","/a/work/srv"],"error":"invalid --mount target '/\\\\\\\\': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/etc/\u00e9","/run/a/etc"],"error":null}, +{"targets":["/etc/a"],"error":null}, +{"targets":["/run/cache","/\u00e9/nvx/data-1","/a/\u00e9","/run/run"],"error":null}, +{"targets":["/nvx/srv","/data-1/run"],"error":null}, +{"targets":["/etc/etc/\u00e9"],"error":null}, +{"targets":["/srv/run/srv","/\u00e9/run/etc","/work/data-1","/nvx/etc"],"error":null}, +{"targets":["/nvx/run/etc","/etc/data-1/run","/cache","/run/a/data-1"],"error":null}, +{"targets":["/\u00e9/work","/nvx/nvx","/etc/run"],"error":null}, +{"targets":["/run/data-1/cache","/procrun\u001c/a/a"],"error":"invalid --mount target '/procrun\\x1c/a/a': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/a/\u00e9/work","/nvx/run"],"error":null}, +{"targets":["/","/cache/cache/run"],"error":"invalid --mount target '/': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"}, +{"targets":["/cache/cache","/data-1/run","/cache/\u00e9"],"error":null}, +{"targets":["/nvx"],"error":null}, +{"targets":["/data-1/work/data-1"],"error":null}, +{"targets":["/cache/srv","/srv"],"error":null}, +{"targets":["/run","/work/run/cache","/cache/srv/cache","/"],"error":"--mount targets /run/nvx/shares and /run overlap; a share cannot hide another"}, +{"targets":["/data-1/etc/srv","/srv/nvx/nvx"],"error":null}, +{"targets":["/\u00e9work","/work/\u00e9/work"],"error":null}, +{"targets":["/data-1/run/cache","/data-1/srv/data-1"],"error":null}, +{"targets":["/cache"],"error":null}, +{"targets":["/work/run"],"error":null}, +{"targets":["/srv/work/nvx"],"error":null}, +{"targets":["/\u00e9","/nvx","/nvx/run","/run/run"],"error":"--mount targets /nvx and /nvx/run overlap; a share cannot hide another"}, +{"targets":["/\u00e9","//"],"error":"invalid --mount target '//': with several shares, or a shared file, each target must be an absolute non-root path without empty, dot, or parent components, whitespace, '\\', '=', or ','"} +], +"sandbox_mounts": [ +{"flavor":"posix","mounts":["/workspace,work,rw","/opt/hostedtoolcache,tools,ro"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"],["writable",0,"out"],["denied",1,"credentials"]],"owner":null,"files":[],"result":[{"guest_target":"/workspace","host_path":"work","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/opt/hostedtoolcache","host_path":"tools","access":"ro","denied":["credentials"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/w,work,rw"],"policy":[["denied",null,"secrets"]],"owner":"caller","files":[],"result":[{"guest_target":"/w","host_path":"work","access":"rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/workspace,work,rw","/opt/hostedtoolcache,tools"],"policy":[["allowed",null,"logs"],["denied",null,"secrets"]],"owner":null,"files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/etc/app/settings.json,settings.json,rw"],"policy":[["writable",0,"settings.json"]],"owner":null,"files":["settings.json"],"error":"--mount-deny, --mount-allow, and --mount-write name paths inside a shared directory, but --mount /etc/app/settings.json shares a file, whose mode applies to the whole file"}, +{"flavor":"posix","mounts":[],"policy":[["denied",null,"x"]],"owner":null,"files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":[],"policy":[["writable",null,"early"]],"owner":null,"files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,f.json"],"policy":[],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,tools,rw","/m1,work","/m2,tools,ro"],"policy":[["writable",null,"early"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"logs"]],"owner":"vmm","files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,tools,rw","/m1,f.json,ro"],"policy":[["writable",0,"out"],["denied",1,"secrets"],["allowed",1,"logs/payloads"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"rw","denied":[],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work,ro"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["allowed",0,""]],"owner":null,"files":["f.json"],"error":"sandbox mount allowed paths must be nonempty"}, +{"flavor":"posix","mounts":["/m0,f.json,rw","/m1,f.json"],"policy":[],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"posix","mounts":["/m0,f.json"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,rw"],"policy":[],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"}]}, +{"flavor":"posix","mounts":["/m0,f.json,rw","/m1,tools"],"policy":[["denied",1,"logs"],["denied",1,"/d"],["allowed",1,"logs/payloads"],["denied",1,""]],"owner":null,"files":["f.json"],"error":"sandbox mount denied paths must be nonempty"}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":[],"result":[]}, +{"flavor":"posix","mounts":["/m0,work,ro"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,C:/x"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":null,"files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":["/m0,C:/x,ro"],"policy":[["denied",0,"/d"],["denied",0,"logs"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["/d","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,C:/x,rw","/m1,work,ro","/m2,C:/x"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["allowed",0,"logs/payloads"],["writable",0,"out"],["denied",2,"/d"],["denied",2,"logs"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"rw","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m2","host_path":"C:/x","access":"ro","denied":["/d","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work,ro","/m1,C:/x,rw"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"logs"],["writable",1,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"C:/x","access":"rw","denied":["logs"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,tools","/m1,tools,rw","/m2,work,rw"],"policy":[["denied",2,"logs"],["denied",2,"secrets"],["writable",2,"out"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"rw","denied":["logs","secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json"],"policy":[],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"caller","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":["/m0,f.json,rw"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["writable",0,"out"]],"owner":"vmm","files":["f.json"],"error":"--mount-deny, --mount-allow, and --mount-write name paths inside a shared directory, but --mount /m0 shares a file, whose mode applies to the whole file"}, +{"flavor":"posix","mounts":["/m0,f.json,rw"],"policy":[["denied",0,"secrets"],["denied",0,"/d"]],"owner":"vmm","files":["f.json"],"error":"--mount-deny, --mount-allow, and --mount-write name paths inside a shared directory, but --mount /m0 shares a file, whose mode applies to the whole file"}, +{"flavor":"posix","mounts":["/m0,work,ro"],"policy":[["denied",0,"/d"],["denied",0,"secrets"],["denied",0,"logs"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["/d","secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,rw"],"policy":[["denied",0,"logs"],["denied",0,"/d"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":["logs","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,rw","/m1,work,ro","/m2,f.json"],"policy":[["denied",0,"/d"],["denied",0,"secrets"],["writable",0,"out"],["denied",1,"logs"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"secrets"],["denied",2,"logs"],["allowed",2,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":["/d","secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work","/m1,f.json","/m2,f.json,ro"],"policy":[["denied",0,"secrets"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["denied",1,"/d"],["allowed",1,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,ro"],"policy":[["denied",0,"logs"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,C:/x","/m1,f.json","/m2,work,ro"],"policy":[["allowed",null,"early"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["allowed",2,""]],"owner":null,"files":["f.json"],"error":"with several --mount options, each --mount-allow must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,work,rw"],"policy":[["denied",0,"secrets"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["writable",0,"out"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"rw","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,ro","/m1,tools,ro"],"policy":[["denied",1,"/d"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,tools","/m1,C:/x","/m2,work,rw"],"policy":[["denied",0,"secrets"],["denied",0,"logs"],["denied",1,"logs"],["denied",1,"/d"],["denied",2,"logs"],["allowed",2,"logs/payloads"],["writable",2,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"C:/x","access":"ro","denied":["logs","/d"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,C:/x,rw"],"policy":[["allowed",null,"early"],["writable",0,"out"]],"owner":null,"files":["f.json"],"error":"--mount-allow exposes a path inside a --mount-deny path of the same share, which has none"}, +{"flavor":"posix","mounts":["/m0,work,rw","/m1,C:/x","/m2,C:/x,rw"],"policy":[["writable",0,"out"],["denied",2,"/d"],["denied",2,""]],"owner":"vmm","files":[],"error":"sandbox mount denied paths must be nonempty"}, +{"flavor":"posix","mounts":["/m0,C:/x","/m1,C:/x,ro"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"],["denied",1,"logs"],["denied",1,"/d"],["allowed",1,"logs"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:/x","access":"ro","denied":["logs","/d"],"allowed":["logs"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[["allowed",null,"early"]],"owner":"caller","files":[],"error":"with several --mount options, each --mount-allow must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,C:/x,ro","/m1,f.json"],"policy":[["denied",0,"secrets"],["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"posix","mounts":["/m0,tools,rw","/m1,work,ro","/m2,tools,rw"],"policy":[["denied",null,"early"],["denied",0,"secrets"],["denied",0,"logs"],["denied",1,"logs"],["denied",2,"logs"],["denied",2,"secrets"],["writable",2,"out"]],"owner":"caller","files":["f.json"],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,f.json,rw"],"policy":[["denied",0,"logs"],["denied",0,"/d"],["writable",0,"out"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":["logs","/d"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work"],"policy":[["denied",0,"secrets"],["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,work,ro"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["writable",0,"logs"],["denied",1,"logs"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":[],"error":"--mount-write narrows the writes of a read-write share; --mount /m0 is read-only"}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":["/m0,work,ro"],"policy":[["denied",null,"early"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["early"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,ro","/m1,f.json,rw"],"policy":[],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,work","/m2,tools,ro"],"policy":[["denied",0,"secrets"],["denied",0,"logs"],["denied",1,"/d"],["allowed",1,"logs/payloads"],["denied",2,"secrets"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"tools","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work,ro","/m1,C:/x,rw","/m2,work,ro"],"policy":[["denied",1,"logs"],["allowed",1,"logs/payloads"],["writable",1,"out"],["denied",2,"logs"],["denied",2,"/d"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:/x","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"ro","denied":["logs","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json"],"policy":[],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":[],"result":[]}, +{"flavor":"posix","mounts":["/m0,work","/m1,work,rw"],"policy":[["denied",0,"secrets"],["denied",1,"logs"],["denied",1,"secrets"],["allowed",1,"logs/payloads"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"rw","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"caller","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":["/m0,work,ro"],"policy":[],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"caller","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":["/m0,work,ro"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,tools,ro","/m1,tools,rw","/m2,f.json,ro"],"policy":[["writable",1,"out"],["allowed",1,""],["denied",2,"/d"]],"owner":"vmm","files":[],"error":"sandbox mount allowed paths must be nonempty"}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":[],"result":[]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,tools,ro"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["denied",1,"secrets"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":[],"result":[]}, +{"flavor":"posix","mounts":["/m0,work,ro","/m1,f.json,ro"],"policy":[["denied",null,"early"],["writable",0,"logs"],["denied",1,"/d"]],"owner":"vmm","files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,C:/x,ro","/m1,C:/x","/m2,C:/x,ro"],"policy":[["denied",0,"secrets"],["denied",0,"/d"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:/x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"C:/x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":[],"policy":[["allowed",null,"early"]],"owner":"caller","files":[],"error":"with several --mount options, each --mount-allow must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,work,ro","/m1,work"],"policy":[["denied",0,"logs"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"logs"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["logs","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,f.json"],"policy":[],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,ro","/m1,f.json,rw","/m2,f.json"],"policy":[["denied",1,"/d"],["denied",1,"secrets"],["denied",2,"logs"],["denied",2,"/d"],["allowed",2,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"rw","denied":["/d","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":["logs","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,rw","/m1,work"],"policy":[["writable",0,"out"],["allowed",0,"logs"]],"owner":"caller","files":["f.json"],"error":"--mount-allow exposes a path inside a --mount-deny path of the same share, which has none"}, +{"flavor":"posix","mounts":["/m0,work,rw","/m1,tools,rw"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["writable",0,"out"],["denied",1,"secrets"],["denied",1,"/d"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"rw","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,tools,ro"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["writable",0,""]],"owner":"caller","files":[],"error":"sandbox mount writable paths must be nonempty"}, +{"flavor":"posix","mounts":["/m0,work,ro"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["allowed",0,"logs"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["secrets"],"allowed":["logs/payloads","logs"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work","/m1,C:/x,rw","/m2,f.json,ro"],"policy":[["denied",null,"early"],["denied",0,"logs"],["denied",0,"secrets"],["denied",1,"/d"],["denied",2,"secrets"],["denied",2,"/d"],["allowed",2,"logs/payloads"]],"owner":"caller","files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,f.json"],"policy":[],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,tools,ro","/m1,f.json,rw"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"posix","mounts":["/m0,C:/x","/m1,tools,rw"],"policy":[["denied",0,"secrets"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["writable",1,"logs"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["logs"],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,C:/x,rw"],"policy":[["denied",1,"secrets"],["allowed",1,"logs/payloads"],["writable",1,"out"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:/x","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,tools,rw","/m1,tools,ro","/m2,C:/x,ro"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["writable",0,"out"],["denied",1,"logs"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"secrets"],["denied",2,"logs"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"C:/x","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,f.json","/m2,work,ro"],"policy":[],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},{"guest_target":"/m2","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,C:/x,rw","/m2,f.json,rw"],"policy":[["denied",1,"secrets"],["denied",1,"/d"],["writable",1,"logs"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"C:/x","access":"rw","denied":["secrets","/d"],"allowed":[],"writable":["logs"],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"posix","mounts":["/m0,f.json,ro"],"policy":[["denied",0,"secrets"],["denied",0,"/d"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,work,ro","/m2,C:/x,ro"],"policy":[["denied",1,"secrets"],["denied",1,"/d"],["denied",2,"secrets"],["denied",2,"/d"],["allowed",2,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"C:/x","access":"ro","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"caller","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"caller","files":[],"result":[]}, +{"flavor":"posix","mounts":["/m0,tools,ro","/m1,work,rw","/m2,f.json"],"policy":[["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"secrets"],["denied",2,"logs"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,C:/x,ro"],"policy":[["denied",0,"logs"],["denied",0,"secrets"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":null,"files":[],"result":[]}, +{"flavor":"posix","mounts":["/m0,work,rw"],"policy":[["allowed",0,""]],"owner":"caller","files":[],"error":"sandbox mount allowed paths must be nonempty"}, +{"flavor":"posix","mounts":["/m0,tools,ro","/m1,tools","/m2,tools,rw"],"policy":[["denied",0,"/d"],["denied",1,"/d"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"secrets"],["denied",2,"/d"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["/d","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"tools","access":"rw","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":null,"files":[],"result":[]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":null,"files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":["/m0,C:/x,rw","/m1,C:/x,rw","/m2,work,rw"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["writable",1,"out"],["denied",1,""],["denied",2,"secrets"],["denied",2,"logs"],["allowed",2,"logs/payloads"],["writable",2,"out"]],"owner":null,"files":[],"error":"sandbox mount denied paths must be nonempty"}, +{"flavor":"posix","mounts":["/m0,C:/x,ro"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work,rw"],"policy":[["denied",null,"early"],["denied",0,"/d"],["allowed",0,"logs/payloads"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"rw","denied":["early","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["allowed",0,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,rw","/m1,work,ro","/m2,tools,rw"],"policy":[["denied",1,"secrets"],["writable",1,""],["denied",2,"secrets"],["allowed",2,"logs/payloads"],["writable",2,"out"],["denied",2,"logs"]],"owner":"caller","files":["f.json"],"error":"sandbox mount writable paths must be nonempty"}, +{"flavor":"posix","mounts":["/m0,work,ro"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,ro","/m1,work,rw","/m2,work,rw"],"policy":[["writable",1,"out"],["denied",2,"secrets"],["denied",2,"logs"],["writable",2,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},{"guest_target":"/m1","host_path":"work","access":"rw","denied":[],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"rw","denied":["secrets","logs"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,rw","/m1,C:/x,rw"],"policy":[["writable",1,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},{"guest_target":"/m1","host_path":"C:/x","access":"rw","denied":[],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,C:/x,ro","/m1,C:/x"],"policy":[["writable",null,"early"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["denied",1,"/d"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,tools,ro"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work","/m1,work,rw","/m2,tools,rw"],"policy":[["writable",null,"early"],["denied",0,"logs"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"logs"],["denied",1,"/d"],["writable",1,"out"],["denied",1,""],["denied",2,"logs"],["writable",2,"out"]],"owner":null,"files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":["/m0,tools","/m1,C:/x,ro"],"policy":[["denied",0,"secrets"],["denied",0,"/d"],["denied",1,"/d"],["denied",1,"secrets"],["allowed",1,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:/x","access":"ro","denied":["/d","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"caller","files":[],"result":[]}, +{"flavor":"posix","mounts":["/m0,C:/x,ro","/m1,f.json,ro"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"/d"],["denied",1,"secrets"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":["/d","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work,rw","/m1,C:/x","/m2,f.json,ro"],"policy":[["denied",null,"early"],["denied",0,"secrets"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["writable",0,"out"]],"owner":"caller","files":["f.json"],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,tools,rw","/m1,tools,ro"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["denied",1,"/d"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json,ro","/m1,C:/x,ro"],"policy":[["writable",null,"early"]],"owner":null,"files":[],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,C:/x,ro","/m2,f.json"],"policy":[["denied",1,"secrets"],["denied",1,"logs"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"C:/x","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":["/m0,f.json,rw","/m1,f.json"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["allowed",0,"logs/payloads"],["denied",1,"/d"],["denied",1,"secrets"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":["/d","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,C:/x","/m1,f.json"],"policy":[["allowed",0,"logs"]],"owner":null,"files":[],"error":"--mount-allow exposes a path inside a --mount-deny path of the same share, which has none"}, +{"flavor":"posix","mounts":["/m0,tools,ro","/m1,tools","/m2,work,rw"],"policy":[["denied",0,"logs"],["denied",0,"/d"],["denied",2,"logs"],["allowed",2,"logs/payloads"],["writable",2,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["logs","/d"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work","/m1,tools"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["denied",1,"logs"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json"],"policy":[],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":null,"files":[],"result":[]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,work,ro"],"policy":[["writable",null,"early"]],"owner":"vmm","files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,tools,rw","/m1,tools"],"policy":[["denied",1,"secrets"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,f.json","/m1,work,ro"],"policy":[["denied",1,"secrets"],["denied",1,"/d"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"posix","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"posix","mounts":["/m0,tools,ro","/m1,tools,ro","/m2,tools"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"],["denied",1,"/d"],["denied",1,"logs"],["allowed",1,"logs/payloads"],["denied",2,"/d"],["denied",2,"logs"],["allowed",2,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"tools","access":"ro","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,work","/m1,C:/x,rw"],"policy":[["denied",null,"early"],["denied",0,"secrets"],["denied",0,"/d"],["denied",1,"logs"]],"owner":null,"files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"posix","mounts":["/m0,C:/x","/m1,tools","/m2,work,ro"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["denied",1,"logs"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"posix","mounts":["/m0,C:/x,rw"],"policy":[["writable",null,"early"],["writable",0,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:/x","access":"rw","denied":[],"allowed":[],"writable":["early","out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/workspace,work,rw","/opt/hostedtoolcache,tools,ro"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"],["writable",0,"out"],["denied",1,"credentials"]],"owner":null,"files":[],"result":[{"guest_target":"/workspace","host_path":"work","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/opt/hostedtoolcache","host_path":"tools","access":"ro","denied":["credentials"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/w,work,rw"],"policy":[["denied",null,"secrets"]],"owner":"caller","files":[],"result":[{"guest_target":"/w","host_path":"work","access":"rw","denied":["secrets"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/workspace,work,rw","/opt/hostedtoolcache,tools"],"policy":[["allowed",null,"logs"],["denied",null,"secrets"]],"owner":null,"files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/etc/app/settings.json,settings.json,rw"],"policy":[["writable",0,"settings.json"]],"owner":null,"files":["settings.json"],"error":"--mount-deny, --mount-allow, and --mount-write name paths inside a shared directory, but --mount /etc/app/settings.json shares a file, whose mode applies to the whole file"}, +{"flavor":"windows","mounts":[],"policy":[["denied",null,"x"]],"owner":null,"files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":[],"policy":[["writable",null,"early"]],"owner":null,"files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,f.json"],"policy":[],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,tools,rw","/m1,work","/m2,tools,ro"],"policy":[["writable",null,"early"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"logs"]],"owner":"vmm","files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,tools,rw","/m1,f.json,ro"],"policy":[["writable",0,"out"],["denied",1,"secrets"],["allowed",1,"logs/payloads"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"rw","denied":[],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work,ro"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["allowed",0,""]],"owner":null,"files":["f.json"],"error":"sandbox mount allowed paths must be nonempty"}, +{"flavor":"windows","mounts":["/m0,f.json,rw","/m1,f.json"],"policy":[],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"windows","mounts":["/m0,f.json"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,rw"],"policy":[],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"}]}, +{"flavor":"windows","mounts":["/m0,f.json,rw","/m1,tools"],"policy":[["denied",1,"logs"],["denied",1,"/d"],["allowed",1,"logs/payloads"],["denied",1,""]],"owner":null,"files":["f.json"],"error":"sandbox mount denied paths must be nonempty"}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":[],"result":[]}, +{"flavor":"windows","mounts":["/m0,work,ro"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,C:/x"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":null,"files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":["/m0,C:/x,ro"],"policy":[["denied",0,"/d"],["denied",0,"logs"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["/d","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,C:/x,rw","/m1,work,ro","/m2,C:/x"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["allowed",0,"logs/payloads"],["writable",0,"out"],["denied",2,"/d"],["denied",2,"logs"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"rw","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m2","host_path":"C:\\x","access":"ro","denied":["/d","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work,ro","/m1,C:/x,rw"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"logs"],["writable",1,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"C:\\x","access":"rw","denied":["logs"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,tools","/m1,tools,rw","/m2,work,rw"],"policy":[["denied",2,"logs"],["denied",2,"secrets"],["writable",2,"out"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"rw","denied":["logs","secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json"],"policy":[],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"caller","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":["/m0,f.json,rw"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["writable",0,"out"]],"owner":"vmm","files":["f.json"],"error":"--mount-deny, --mount-allow, and --mount-write name paths inside a shared directory, but --mount /m0 shares a file, whose mode applies to the whole file"}, +{"flavor":"windows","mounts":["/m0,f.json,rw"],"policy":[["denied",0,"secrets"],["denied",0,"/d"]],"owner":"vmm","files":["f.json"],"error":"--mount-deny, --mount-allow, and --mount-write name paths inside a shared directory, but --mount /m0 shares a file, whose mode applies to the whole file"}, +{"flavor":"windows","mounts":["/m0,work,ro"],"policy":[["denied",0,"/d"],["denied",0,"secrets"],["denied",0,"logs"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["/d","secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,rw"],"policy":[["denied",0,"logs"],["denied",0,"/d"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":["logs","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,rw","/m1,work,ro","/m2,f.json"],"policy":[["denied",0,"/d"],["denied",0,"secrets"],["writable",0,"out"],["denied",1,"logs"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"secrets"],["denied",2,"logs"],["allowed",2,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":["/d","secrets"],"allowed":[],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work","/m1,f.json","/m2,f.json,ro"],"policy":[["denied",0,"secrets"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["denied",1,"/d"],["allowed",1,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,ro"],"policy":[["denied",0,"logs"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,C:/x","/m1,f.json","/m2,work,ro"],"policy":[["allowed",null,"early"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["allowed",2,""]],"owner":null,"files":["f.json"],"error":"with several --mount options, each --mount-allow must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,work,rw"],"policy":[["denied",0,"secrets"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["writable",0,"out"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"rw","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,ro","/m1,tools,ro"],"policy":[["denied",1,"/d"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,tools","/m1,C:/x","/m2,work,rw"],"policy":[["denied",0,"secrets"],["denied",0,"logs"],["denied",1,"logs"],["denied",1,"/d"],["denied",2,"logs"],["allowed",2,"logs/payloads"],["writable",2,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"C:\\x","access":"ro","denied":["logs","/d"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,C:/x,rw"],"policy":[["allowed",null,"early"],["writable",0,"out"]],"owner":null,"files":["f.json"],"error":"--mount-allow exposes a path inside a --mount-deny path of the same share, which has none"}, +{"flavor":"windows","mounts":["/m0,work,rw","/m1,C:/x","/m2,C:/x,rw"],"policy":[["writable",0,"out"],["denied",2,"/d"],["denied",2,""]],"owner":"vmm","files":[],"error":"sandbox mount denied paths must be nonempty"}, +{"flavor":"windows","mounts":["/m0,C:/x","/m1,C:/x,ro"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"],["denied",1,"logs"],["denied",1,"/d"],["allowed",1,"logs"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:\\x","access":"ro","denied":["logs","/d"],"allowed":["logs"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[["allowed",null,"early"]],"owner":"caller","files":[],"error":"with several --mount options, each --mount-allow must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,C:/x,ro","/m1,f.json"],"policy":[["denied",0,"secrets"],["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"windows","mounts":["/m0,tools,rw","/m1,work,ro","/m2,tools,rw"],"policy":[["denied",null,"early"],["denied",0,"secrets"],["denied",0,"logs"],["denied",1,"logs"],["denied",2,"logs"],["denied",2,"secrets"],["writable",2,"out"]],"owner":"caller","files":["f.json"],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,f.json,rw"],"policy":[["denied",0,"logs"],["denied",0,"/d"],["writable",0,"out"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":["logs","/d"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work"],"policy":[["denied",0,"secrets"],["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,work,ro"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["writable",0,"logs"],["denied",1,"logs"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":[],"error":"--mount-write narrows the writes of a read-write share; --mount /m0 is read-only"}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":["/m0,work,ro"],"policy":[["denied",null,"early"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["early"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,ro","/m1,f.json,rw"],"policy":[],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,work","/m2,tools,ro"],"policy":[["denied",0,"secrets"],["denied",0,"logs"],["denied",1,"/d"],["allowed",1,"logs/payloads"],["denied",2,"secrets"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"tools","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work,ro","/m1,C:/x,rw","/m2,work,ro"],"policy":[["denied",1,"logs"],["allowed",1,"logs/payloads"],["writable",1,"out"],["denied",2,"logs"],["denied",2,"/d"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:\\x","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"ro","denied":["logs","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json"],"policy":[],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":[],"result":[]}, +{"flavor":"windows","mounts":["/m0,work","/m1,work,rw"],"policy":[["denied",0,"secrets"],["denied",1,"logs"],["denied",1,"secrets"],["allowed",1,"logs/payloads"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"rw","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"caller","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":["/m0,work,ro"],"policy":[],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"caller","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":["/m0,work,ro"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["allowed",0,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,tools,ro","/m1,tools,rw","/m2,f.json,ro"],"policy":[["writable",1,"out"],["allowed",1,""],["denied",2,"/d"]],"owner":"vmm","files":[],"error":"sandbox mount allowed paths must be nonempty"}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":[],"result":[]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,tools,ro"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["denied",1,"secrets"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["secrets"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":[],"result":[]}, +{"flavor":"windows","mounts":["/m0,work,ro","/m1,f.json,ro"],"policy":[["denied",null,"early"],["writable",0,"logs"],["denied",1,"/d"]],"owner":"vmm","files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,C:/x,ro","/m1,C:/x","/m2,C:/x,ro"],"policy":[["denied",0,"secrets"],["denied",0,"/d"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:\\x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"C:\\x","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":[],"policy":[["allowed",null,"early"]],"owner":"caller","files":[],"error":"with several --mount options, each --mount-allow must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,work,ro","/m1,work"],"policy":[["denied",0,"logs"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"logs"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["logs","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,f.json"],"policy":[],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,ro","/m1,f.json,rw","/m2,f.json"],"policy":[["denied",1,"/d"],["denied",1,"secrets"],["denied",2,"logs"],["denied",2,"/d"],["allowed",2,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"rw","denied":["/d","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":["logs","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,rw","/m1,work"],"policy":[["writable",0,"out"],["allowed",0,"logs"]],"owner":"caller","files":["f.json"],"error":"--mount-allow exposes a path inside a --mount-deny path of the same share, which has none"}, +{"flavor":"windows","mounts":["/m0,work,rw","/m1,tools,rw"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["writable",0,"out"],["denied",1,"secrets"],["denied",1,"/d"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"rw","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,tools,ro"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["writable",0,""]],"owner":"caller","files":[],"error":"sandbox mount writable paths must be nonempty"}, +{"flavor":"windows","mounts":["/m0,work,ro"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["allowed",0,"logs"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["secrets"],"allowed":["logs/payloads","logs"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work","/m1,C:/x,rw","/m2,f.json,ro"],"policy":[["denied",null,"early"],["denied",0,"logs"],["denied",0,"secrets"],["denied",1,"/d"],["denied",2,"secrets"],["denied",2,"/d"],["allowed",2,"logs/payloads"]],"owner":"caller","files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,f.json"],"policy":[],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,tools,ro","/m1,f.json,rw"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"windows","mounts":["/m0,C:/x","/m1,tools,rw"],"policy":[["denied",0,"secrets"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["writable",1,"logs"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["logs"],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,C:/x,rw"],"policy":[["denied",1,"secrets"],["allowed",1,"logs/payloads"],["writable",1,"out"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:\\x","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,tools,rw","/m1,tools,ro","/m2,C:/x,ro"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["writable",0,"out"],["denied",1,"logs"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"secrets"],["denied",2,"logs"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":["out"],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"C:\\x","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,f.json","/m2,work,ro"],"policy":[],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},{"guest_target":"/m2","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,C:/x,rw","/m2,f.json,rw"],"policy":[["denied",1,"secrets"],["denied",1,"/d"],["writable",1,"logs"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"C:\\x","access":"rw","denied":["secrets","/d"],"allowed":[],"writable":["logs"],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"windows","mounts":["/m0,f.json,ro"],"policy":[["denied",0,"secrets"],["denied",0,"/d"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,work,ro","/m2,C:/x,ro"],"policy":[["denied",1,"secrets"],["denied",1,"/d"],["denied",2,"secrets"],["denied",2,"/d"],["allowed",2,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"C:\\x","access":"ro","denied":["secrets","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"caller","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"caller","files":[],"result":[]}, +{"flavor":"windows","mounts":["/m0,tools,ro","/m1,work,rw","/m2,f.json"],"policy":[["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"secrets"],["denied",2,"logs"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,C:/x,ro"],"policy":[["denied",0,"logs"],["denied",0,"secrets"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["logs","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":null,"files":[],"result":[]}, +{"flavor":"windows","mounts":["/m0,work,rw"],"policy":[["allowed",0,""]],"owner":"caller","files":[],"error":"sandbox mount allowed paths must be nonempty"}, +{"flavor":"windows","mounts":["/m0,tools,ro","/m1,tools","/m2,tools,rw"],"policy":[["denied",0,"/d"],["denied",1,"/d"],["denied",1,"secrets"],["allowed",1,"logs/payloads"],["denied",2,"secrets"],["denied",2,"/d"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["/d","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"tools","access":"rw","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":null,"files":[],"result":[]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":null,"files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":["/m0,C:/x,rw","/m1,C:/x,rw","/m2,work,rw"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["writable",1,"out"],["denied",1,""],["denied",2,"secrets"],["denied",2,"logs"],["allowed",2,"logs/payloads"],["writable",2,"out"]],"owner":null,"files":[],"error":"sandbox mount denied paths must be nonempty"}, +{"flavor":"windows","mounts":["/m0,C:/x,ro"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work,rw"],"policy":[["denied",null,"early"],["denied",0,"/d"],["allowed",0,"logs/payloads"]],"owner":null,"files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"rw","denied":["early","/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["allowed",0,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,rw","/m1,work,ro","/m2,tools,rw"],"policy":[["denied",1,"secrets"],["writable",1,""],["denied",2,"secrets"],["allowed",2,"logs/payloads"],["writable",2,"out"],["denied",2,"logs"]],"owner":"caller","files":["f.json"],"error":"sandbox mount writable paths must be nonempty"}, +{"flavor":"windows","mounts":["/m0,work,ro"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,ro","/m1,work,rw","/m2,work,rw"],"policy":[["writable",1,"out"],["denied",2,"secrets"],["denied",2,"logs"],["writable",2,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},{"guest_target":"/m1","host_path":"work","access":"rw","denied":[],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"rw","denied":["secrets","logs"],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,rw","/m1,C:/x,rw"],"policy":[["writable",1,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"file"},{"guest_target":"/m1","host_path":"C:\\x","access":"rw","denied":[],"allowed":[],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,C:/x,ro","/m1,C:/x"],"policy":[["writable",null,"early"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["denied",1,"/d"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,tools,ro"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work","/m1,work,rw","/m2,tools,rw"],"policy":[["writable",null,"early"],["denied",0,"logs"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"logs"],["denied",1,"/d"],["writable",1,"out"],["denied",1,""],["denied",2,"logs"],["writable",2,"out"]],"owner":null,"files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":["/m0,tools","/m1,C:/x,ro"],"policy":[["denied",0,"secrets"],["denied",0,"/d"],["denied",1,"/d"],["denied",1,"secrets"],["allowed",1,"logs/payloads"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"C:\\x","access":"ro","denied":["/d","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"caller","files":[],"result":[]}, +{"flavor":"windows","mounts":["/m0,C:/x,ro","/m1,f.json,ro"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"/d"],["denied",1,"secrets"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":["/d","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work,rw","/m1,C:/x","/m2,f.json,ro"],"policy":[["denied",null,"early"],["denied",0,"secrets"],["denied",0,"/d"],["allowed",0,"logs/payloads"],["writable",0,"out"]],"owner":"caller","files":["f.json"],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,tools,rw","/m1,tools,ro"],"policy":[["denied",0,"secrets"],["allowed",0,"logs/payloads"],["denied",1,"/d"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"rw","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json,ro","/m1,C:/x,ro"],"policy":[["writable",null,"early"]],"owner":null,"files":[],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,C:/x,ro","/m2,f.json"],"policy":[["denied",1,"secrets"],["denied",1,"logs"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"},{"guest_target":"/m1","host_path":"C:\\x","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"file"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":["/m0,f.json,rw","/m1,f.json"],"policy":[["denied",0,"/d"],["denied",0,"logs"],["allowed",0,"logs/payloads"],["denied",1,"/d"],["denied",1,"secrets"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"rw","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"f.json","access":"ro","denied":["/d","secrets"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,C:/x","/m1,f.json"],"policy":[["allowed",0,"logs"]],"owner":null,"files":[],"error":"--mount-allow exposes a path inside a --mount-deny path of the same share, which has none"}, +{"flavor":"windows","mounts":["/m0,tools,ro","/m1,tools","/m2,work,rw"],"policy":[["denied",0,"logs"],["denied",0,"/d"],["denied",2,"logs"],["allowed",2,"logs/payloads"],["writable",2,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["logs","/d"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"rw","denied":["logs"],"allowed":["logs/payloads"],"writable":["out"],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work","/m1,tools"],"policy":[["denied",0,"logs"],["denied",0,"secrets"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["denied",1,"logs"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"work","access":"ro","denied":["logs","secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["secrets","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json"],"policy":[],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":null,"files":[],"result":[]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,work,ro"],"policy":[["writable",null,"early"]],"owner":"vmm","files":["f.json"],"error":"with several --mount options, each --mount-write must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,tools,rw","/m1,tools"],"policy":[["denied",1,"secrets"],["allowed",1,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"rw","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["secrets"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,f.json","/m1,work,ro"],"policy":[["denied",1,"secrets"],["denied",1,"/d"]],"owner":"caller","files":[],"result":[{"guest_target":"/m0","host_path":"f.json","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"caller","kind":"directory"},{"guest_target":"/m1","host_path":"work","access":"ro","denied":["secrets","/d"],"allowed":[],"writable":[],"owner":"caller","kind":"directory"}]}, +{"flavor":"windows","mounts":[],"policy":[],"owner":"vmm","files":["f.json"],"result":[]}, +{"flavor":"windows","mounts":["/m0,tools,ro","/m1,tools,ro","/m2,tools"],"policy":[["denied",0,"logs"],["allowed",0,"logs/payloads"],["denied",1,"/d"],["denied",1,"logs"],["allowed",1,"logs/payloads"],["denied",2,"/d"],["denied",2,"logs"],["allowed",2,"logs/payloads"]],"owner":"vmm","files":[],"result":[{"guest_target":"/m0","host_path":"tools","access":"ro","denied":["logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"tools","access":"ro","denied":["/d","logs"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,work","/m1,C:/x,rw"],"policy":[["denied",null,"early"],["denied",0,"secrets"],["denied",0,"/d"],["denied",1,"logs"]],"owner":null,"files":[],"error":"with several --mount options, each --mount-deny must follow the --mount whose host directory it names"}, +{"flavor":"windows","mounts":["/m0,C:/x","/m1,tools","/m2,work,ro"],"policy":[["denied",0,"/d"],["allowed",0,"logs/payloads"],["denied",1,"secrets"],["denied",1,"logs"]],"owner":null,"files":[],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"ro","denied":["/d"],"allowed":["logs/payloads"],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m1","host_path":"tools","access":"ro","denied":["secrets","logs"],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"},{"guest_target":"/m2","host_path":"work","access":"ro","denied":[],"allowed":[],"writable":[],"owner":"vmm","kind":"directory"}]}, +{"flavor":"windows","mounts":["/m0,C:/x,rw"],"policy":[["writable",null,"early"],["writable",0,"out"]],"owner":"caller","files":["f.json"],"result":[{"guest_target":"/m0","host_path":"C:\\x","access":"rw","denied":[],"allowed":[],"writable":["early","out"],"owner":"caller","kind":"directory"}]} +], +"environments": [ +{"entries":["KEY0=value","KEY1=value","KEY2=value","KEY3=value","KEY4=value","KEY5=value","KEY6=value","KEY7=value","KEY8=value","KEY9=value","KEY10=value","KEY11=value","KEY12=value","KEY13=value","KEY14=value","KEY15=value","KEY16=value","KEY17=value","KEY18=value","KEY19=value","KEY20=value","KEY21=value","KEY22=value","KEY23=value","KEY24=value","KEY25=value","KEY26=value","KEY27=value","KEY28=value","KEY29=value","KEY30=value","KEY31=value","KEY32=value","KEY33=value","KEY34=value","KEY35=value","KEY36=value","KEY37=value","KEY38=value","KEY39=value","KEY40=value","KEY41=value","KEY42=value","KEY43=value","KEY44=value","KEY45=value","KEY46=value","KEY47=value","KEY48=value","KEY49=value","KEY50=value","KEY51=value","KEY52=value","KEY53=value","KEY54=value","KEY55=value","KEY56=value","KEY57=value","KEY58=value","KEY59=value","KEY60=value","KEY61=value","KEY62=value","KEY63=value","KEY64=value","KEY65=value","KEY66=value","KEY67=value","KEY68=value","KEY69=value","KEY70=value","KEY71=value","KEY72=value","KEY73=value","KEY74=value","KEY75=value","KEY76=value","KEY77=value","KEY78=value","KEY79=value","KEY80=value","KEY81=value","KEY82=value","KEY83=value","KEY84=value","KEY85=value","KEY86=value","KEY87=value","KEY88=value","KEY89=value","KEY90=value","KEY91=value","KEY92=value","KEY93=value","KEY94=value","KEY95=value","KEY96=value","KEY97=value","KEY98=value","KEY99=value","KEY100=value","KEY101=value","KEY102=value","KEY103=value","KEY104=value","KEY105=value","KEY106=value","KEY107=value","KEY108=value","KEY109=value","KEY110=value","KEY111=value","KEY112=value","KEY113=value","KEY114=value","KEY115=value","KEY116=value","KEY117=value","KEY118=value","KEY119=value","KEY120=value","KEY121=value","KEY122=value","KEY123=value","KEY124=value","KEY125=value","KEY126=value","KEY127=value","KEY128=value","KEY129=value","KEY130=value","KEY131=value","KEY132=value","KEY133=value","KEY134=value","KEY135=value","KEY136=value","KEY137=value","KEY138=value","KEY139=value","KEY140=value","KEY141=value","KEY142=value","KEY143=value","KEY144=value","KEY145=value","KEY146=value","KEY147=value","KEY148=value","KEY149=value","KEY150=value","KEY151=value","KEY152=value","KEY153=value","KEY154=value","KEY155=value","KEY156=value","KEY157=value","KEY158=value","KEY159=value","KEY160=value","KEY161=value","KEY162=value","KEY163=value","KEY164=value","KEY165=value","KEY166=value","KEY167=value","KEY168=value","KEY169=value","KEY170=value","KEY171=value","KEY172=value","KEY173=value","KEY174=value","KEY175=value","KEY176=value","KEY177=value","KEY178=value","KEY179=value","KEY180=value","KEY181=value","KEY182=value","KEY183=value","KEY184=value","KEY185=value","KEY186=value","KEY187=value","KEY188=value","KEY189=value","KEY190=value","KEY191=value","KEY192=value","KEY193=value","KEY194=value","KEY195=value","KEY196=value","KEY197=value","KEY198=value","KEY199=value","KEY200=value","KEY201=value","KEY202=value","KEY203=value","KEY204=value","KEY205=value","KEY206=value","KEY207=value","KEY208=value","KEY209=value","KEY210=value","KEY211=value","KEY212=value","KEY213=value","KEY214=value","KEY215=value","KEY216=value","KEY217=value","KEY218=value","KEY219=value","KEY220=value","KEY221=value","KEY222=value","KEY223=value","KEY224=value","KEY225=value","KEY226=value","KEY227=value","KEY228=value","KEY229=value","KEY230=value","KEY231=value","KEY232=value","KEY233=value","KEY234=value","KEY235=value","KEY236=value","KEY237=value","KEY238=value","KEY239=value","KEY240=value","KEY241=value","KEY242=value","KEY243=value","KEY244=value","KEY245=value","KEY246=value","KEY247=value","KEY248=value","KEY249=value","KEY250=value","KEY251=value","KEY252=value","KEY253=value","KEY254=value","KEY255=value","KEY256=value"],"error":"managed exec environment exceeds 256 entries"}, +{"entries":["KEY0=value","KEY1=value","KEY2=value","KEY3=value","KEY4=value","KEY5=value","KEY6=value","KEY7=value","KEY8=value","KEY9=value","KEY10=value","KEY11=value","KEY12=value","KEY13=value","KEY14=value","KEY15=value","KEY16=value","KEY17=value","KEY18=value","KEY19=value","KEY20=value","KEY21=value","KEY22=value","KEY23=value","KEY24=value","KEY25=value","KEY26=value","KEY27=value","KEY28=value","KEY29=value","KEY30=value","KEY31=value","KEY32=value","KEY33=value","KEY34=value","KEY35=value","KEY36=value","KEY37=value","KEY38=value","KEY39=value","KEY40=value","KEY41=value","KEY42=value","KEY43=value","KEY44=value","KEY45=value","KEY46=value","KEY47=value","KEY48=value","KEY49=value","KEY50=value","KEY51=value","KEY52=value","KEY53=value","KEY54=value","KEY55=value","KEY56=value","KEY57=value","KEY58=value","KEY59=value","KEY60=value","KEY61=value","KEY62=value","KEY63=value","KEY64=value","KEY65=value","KEY66=value","KEY67=value","KEY68=value","KEY69=value","KEY70=value","KEY71=value","KEY72=value","KEY73=value","KEY74=value","KEY75=value","KEY76=value","KEY77=value","KEY78=value","KEY79=value","KEY80=value","KEY81=value","KEY82=value","KEY83=value","KEY84=value","KEY85=value","KEY86=value","KEY87=value","KEY88=value","KEY89=value","KEY90=value","KEY91=value","KEY92=value","KEY93=value","KEY94=value","KEY95=value","KEY96=value","KEY97=value","KEY98=value","KEY99=value","KEY100=value","KEY101=value","KEY102=value","KEY103=value","KEY104=value","KEY105=value","KEY106=value","KEY107=value","KEY108=value","KEY109=value","KEY110=value","KEY111=value","KEY112=value","KEY113=value","KEY114=value","KEY115=value","KEY116=value","KEY117=value","KEY118=value","KEY119=value","KEY120=value","KEY121=value","KEY122=value","KEY123=value","KEY124=value","KEY125=value","KEY126=value","KEY127=value","KEY128=value","KEY129=value","KEY130=value","KEY131=value","KEY132=value","KEY133=value","KEY134=value","KEY135=value","KEY136=value","KEY137=value","KEY138=value","KEY139=value","KEY140=value","KEY141=value","KEY142=value","KEY143=value","KEY144=value","KEY145=value","KEY146=value","KEY147=value","KEY148=value","KEY149=value","KEY150=value","KEY151=value","KEY152=value","KEY153=value","KEY154=value","KEY155=value","KEY156=value","KEY157=value","KEY158=value","KEY159=value","KEY160=value","KEY161=value","KEY162=value","KEY163=value","KEY164=value","KEY165=value","KEY166=value","KEY167=value","KEY168=value","KEY169=value","KEY170=value","KEY171=value","KEY172=value","KEY173=value","KEY174=value","KEY175=value","KEY176=value","KEY177=value","KEY178=value","KEY179=value","KEY180=value","KEY181=value","KEY182=value","KEY183=value","KEY184=value","KEY185=value","KEY186=value","KEY187=value","KEY188=value","KEY189=value","KEY190=value","KEY191=value","KEY192=value","KEY193=value","KEY194=value","KEY195=value","KEY196=value","KEY197=value","KEY198=value","KEY199=value","KEY200=value","KEY201=value","KEY202=value","KEY203=value","KEY204=value","KEY205=value","KEY206=value","KEY207=value","KEY208=value","KEY209=value","KEY210=value","KEY211=value","KEY212=value","KEY213=value","KEY214=value","KEY215=value","KEY216=value","KEY217=value","KEY218=value","KEY219=value","KEY220=value","KEY221=value","KEY222=value","KEY223=value","KEY224=value","KEY225=value","KEY226=value","KEY227=value","KEY228=value","KEY229=value","KEY230=value","KEY231=value","KEY232=value","KEY233=value","KEY234=value","KEY235=value","KEY236=value","KEY237=value","KEY238=value","KEY239=value","KEY240=value","KEY241=value","KEY242=value","KEY243=value","KEY244=value","KEY245=value","KEY246=value","KEY247=value","KEY248=value","KEY249=value","KEY250=value","KEY251=value","KEY252=value","KEY253=value","KEY254=value","KEY255=value"],"error":null}, +{"entries":["NO_EQUALS"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["=empty-key"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["KEY=embedded\u0000nul"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["KEY=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["K=vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"],"error":null}, +{"entries":["DUPLICATE=one","DUPLICATE=two"],"error":"managed exec environment contains duplicate key: DUPLICATE"}, +{"entries":["A=1","B=2"],"error":null}, +{"entries":[],"error":null}, +{"entries":[],"error":null}, +{"entries":["\u0000"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["Bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u0000"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxB","B"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["","","B",""],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["=\u0000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxA","BA=","\u0000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxABA","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxA=\u00e9"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":[""],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["\u00e9\u00e9B","=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=A="],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["",""],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["==","B","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxB","B="],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["AA","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=\u00e9A","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u0000\u00e9"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["\u0000\u00e9",""],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["B\u0000="],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxA","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxA\u00e9\u00e9A"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["\u00e9AA"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":[],"error":null}, +{"entries":["\u00e9B\u00e9","\u00e9A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["Bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",""],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["AB\u00e9\u0000",""],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["B","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u00e9"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":[],"error":null}, +{"entries":["B\u0000\u0000A\u00e9","xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","=A"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["=\u0000BAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","","=","\u0000A"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["","BB\u0000"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["","\u00e9\u0000\u0000B="],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["AB\u00e9","B"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["","=Bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u0000"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":[""],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u0000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=="],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["\u00c9=\u00e9","A=a=b","K_1=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","PATH=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603","B="],"error":null}, +{"entries":["B=1"],"error":null}, +{"entries":["K_1=1","NO_EQUALS","PATH= spaced "],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["\u00c9=\u00e9","K_1=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"error":null}, +{"entries":["\u00c9=a=b","K_1=","K_1=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603"],"error":"managed exec environment contains duplicate key: K_1"}, +{"entries":["B=\u00e9"],"error":null}, +{"entries":[],"error":null}, +{"entries":[],"error":null}, +{"entries":[],"error":null}, +{"entries":["B= spaced ","B=a=b","=x","K_1=\u00e9","B= spaced "],"error":"managed exec environment contains duplicate key: B"}, +{"entries":["K_1= spaced ","PATH=","A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","B= spaced ","\u00c9=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603"],"error":null}, +{"entries":["\u00c9= spaced ","A=1","K_1= spaced ","A=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603"],"error":"managed exec environment contains duplicate key: A"}, +{"entries":["\u00c9=","B=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603","PATH= spaced ","K_1=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603"],"error":null}, +{"entries":["NO_EQUALS","A="],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["B=\u00e9","A=1","K_1= spaced "],"error":null}, +{"entries":["A=\u00e9"],"error":null}, +{"entries":[],"error":null}, +{"entries":["K_1=a=b","PATH=1","B=1"],"error":null}, +{"entries":["K_1="],"error":null}, +{"entries":["A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00c9= spaced ","A=1"],"error":"managed exec environment contains duplicate key: A"}, +{"entries":[],"error":null}, +{"entries":["A=","K_1=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","B=a=b","\u00c9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","NO_EQUALS"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["\u00c9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","\u00c9="],"error":"managed exec environment contains duplicate key: \u00c9"}, +{"entries":[],"error":null}, +{"entries":["PATH=1","PATH=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"error":"managed exec environment contains duplicate key: PATH"}, +{"entries":["B=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603","A=\u0000","B=\u00e9","B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":["A=","B=a=b","A=a=b","\u00c9=1"],"error":"managed exec environment contains duplicate key: A"}, +{"entries":["K_1=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603"],"error":null}, +{"entries":["\u00c9=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603","K_1= spaced "],"error":null}, +{"entries":["NO_EQUALS"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"}, +{"entries":[],"error":null}, +{"entries":["PATH= spaced "],"error":null}, +{"entries":["B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","A=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603","PATH=a=b"],"error":null}, +{"entries":["B=1"],"error":null}, +{"entries":["A=\u00e9","PATH=a=b","PATH=","B="],"error":"managed exec environment contains duplicate key: PATH"}, +{"entries":[],"error":null}, +{"entries":["K_1=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],"error":null}, +{"entries":["\u00c9=a=b","B=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603","B=\u00e9","K_1=a=b"],"error":"managed exec environment contains duplicate key: B"}, +{"entries":["\u00c9=\u00e9","K_1=\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603\u2603","\u00c9=","\u00c9=1","\u00c9= spaced "],"error":"managed exec environment contains duplicate key: \u00c9"}, +{"entries":["A=\u0000"],"error":"managed exec environment entries must be non-empty KEY=VALUE strings of at most 4096 bytes"} +], +"environment_files": [ +{"text":"[]","environment":[]}, +{"text":"[\"A=1\", \"B=2\"]","environment":["A=1","B=2"]}, +{"text":"[\"DUPLICATE=one\", \"DUPLICATE=two\"]","error":"managed exec environment contains duplicate key: DUPLICATE"}, +{"spaces":1048577,"error":"managed execution environment file exceeds 1048576-byte limit"}, +{"spaces":1048576,"error":"failed to read managed execution environment: {path}"}, +{"text":"{}","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"\"A=1\"","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"[1]","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"[NaN]","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"[[\"A=1\"]]","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"","error":"failed to read managed execution environment: {path}"}, +{"text":"[","error":"failed to read managed execution environment: {path}"}, +{"text":"[\"A=1\",]","error":"failed to read managed execution environment: {path}"}, +{"text":"\ufeff[]","error":"failed to read managed execution environment: {path}"}, +{"bytes":"5b22ff225d","error":"failed to read managed execution environment: {path}"}, +{"text":"[\"A=1\", \"B=\\ud800\"]","error":"'utf-8' codec can't encode character '\\ud800' in position 2: surrogates not allowed"}, +{"text":"[\"B=\\udc00\\ud800x\"]","error":"'utf-8' codec can't encode characters in position 2-3: surrogates not allowed"}, +{"text":"[11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111]","error":"Exceeds the limit (4300 digits) for integer string conversion: value has 4301 digits; use sys.set_int_max_str_digits() to increase the limit"}, +{"text":"[\"A=\\u00e9\"]\r\n","environment":["A=\u00e9"]}, +{"text":"[\"A=11\", \"B=\\u00eNaN9\", \"C=\\ud8\"d\\ude00\"]","error":"failed to read managed execution environment: {path}"}, +{"text":"+[]","error":"failed to read managed execution environment: {path}"}, +{"text":"d800:]","error":"failed to read managed execution environment: {path}"}, +{"text":" \t\r\n[1 , 2\n ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"[u","error":"failed to read managed execution environment: {path}"}, +{"text":"[","error":"failed to read managed execution environment: {path}"}, +{"text":"{}1","error":"failed to read managed execution environment: {path}"}, +{"text":"[]NaN","error":"failed to read managed execution environment: {path}"}, +{"text":" \t\r\n[1 , 2 ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":" \t\r\n[true , 2 ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":" \t\r\ntrue1 { 2 NaN ","error":"failed to read managed execution environment: {path}"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","environment":["A=1","B=\u00e9","C=\ud83d\ude00"]}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude0Infinity0\"]","error":"failed to read managed execution environment: {path}"}, +{"text":"{}","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":" \t\r\n[1 , 2 ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":" \t\r\n01 , 2 ]","error":"failed to read managed execution environment: {path}"}, +{"text":"[\"A=1\", \"B=\\u0\ne9\", \"C=\\ud83d\\ude00\"]","error":"failed to read managed execution environment: {path}"}, +{"text":" \t\r\n[1 , 2 ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":" \t\r\n[1 , 2 ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"[{","error":"failed to read managed execution environment: {path}"}, +{"text":" \u00e9\r\n+[ , 2 ] ","error":"failed to read managed execution environment: {path}"}, +{"text":"[\"A=1\t\", B=\\u{00e9\", \"C=\\ud83d\\ude00\"]","error":"failed to read managed execution environment: {path}"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\+detrue00\"","error":"failed to read managed execution environment: {path}"}, +{"text":"[1","error":"failed to read managed execution environment: {path}"}, +{"text":"-{}","error":"failed to read managed execution environment: {path}"}, +{"text":"{}","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"[]","environment":[]}, +{"text":"[]","environment":[]}, +{"text":" ]\t\r\n[1 , 2null] {","error":"failed to read managed execution environment: {path}"}, +{"text":" \t\r\n[1 , 2 ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","environment":["A=1","B=\u00e9","C=\ud83d\ude00"]}, +{"text":" \t\r\n[u1 , 2 ] ","error":"failed to read managed execution environment: {path}"}, +{"text":"{}","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"{}-","error":"failed to read managed execution environment: {path}"}, +{"text":"{}","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"{}","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":",}","error":"failed to read managed execution environment: {path}"}, +{"text":"0{","error":"failed to read managed execution environment: {path}"}, +{"text":"[e","error":"failed to read managed execution environment: {path}"}, +{"text":"{e","error":"failed to read managed execution environment: {path}"}, +{"text":"{}}","error":"failed to read managed execution environment: {path}"}, +{"text":"{d800","error":"failed to read managed execution environment: {path}"}, +{"text":"[\"A=1\", \"B=\\u00e9\", \"C=\\ud83d\\ude00\"]","environment":["A=1","B=\u00e9","C=\ud83d\ude00"]}, +{"text":"[\"A=1\", \"B=\\u009\", \"C=\\ud83d\\ude00\"]","error":"failed to read managed execution environment: {path}"}, +{"text":" \t\r\n{[1 , 2 ] ","error":"failed to read managed execution environment: {path}"}, +{"text":" \t\r\n[1 , 2 ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":" \t+\n[1 +, 2 ] ","error":"failed to read managed execution environment: {path}"}, +{"text":"[]","environment":[]}, +{"text":" .]","error":"failed to read managed execution environment: {path}"}, +{"text":" {\r","error":"failed to read managed execution environment: {path}"}, +{"text":" \t\n1 , \r2 ] ","error":"failed to read managed execution environment: {path}"}, +{"text":"[]","environment":[]}, +{"text":" \t\r\nd8001true, 2 ]0 ","error":"failed to read managed execution environment: {path}"}, +{"text":"{[","error":"failed to read managed execution environment: {path}"}, +{"text":" \t\r\n[1 , 2 ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"[\"A=1\", \"B=\\u00e\", \"C=\\ud83d\\ude00\"]","error":"failed to read managed execution environment: {path}"}, +{"text":"[]","environment":[]}, +{"text":"{}","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":" \t\r\n[1 , 2 ] ","error":"managed execution environment file must contain a JSON array of KEY=VALUE strings"}, +{"text":"[\"A= \", \"B=\\u00e\", \"C=\\ud83d\\u-e00\"]","error":"failed to read managed execution environment: {path}"}, +{"text":"[\"A=\\u00e9\", \"\\u00c9=\\ud83d\\ude00\", \"B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"\\u00c9=1\"]","error":"managed exec environment contains duplicate key: \u00c9"}, +{"text":"[\"B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"\u00c9=1\"]","error":"managed exec environment contains duplicate key: B"}, +{"text":"[\"\u00c9=\ud83d\ude00\", \"B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"]","environment":["\u00c9=\ud83d\ude00","B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]}, +{"text":"[\"A=\\u00e9\"]","environment":["A=\u00e9"]}, +{"text":"[]","environment":[]}, +{"text":"[\"A=\\u00e9\"]","environment":["A=\u00e9"]}, +{"text":"[\"B=\u00e9\", \"B=\u00e9\", \"\u00c9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"]","error":"managed exec environment contains duplicate key: B"}, +{"text":"[\"B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"B=\u00e9\"]","error":"managed exec environment contains duplicate key: A"}, +{"text":"[]","environment":[]}, +{"text":"[\"A=\u00e9\"]","environment":["A=\u00e9"]}, +{"text":"[\"\\u00c9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"]","environment":["\u00c9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]}, +{"text":"[\"\\u00c9=\\ud83d\\ude00\", \"A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"A=\\ud83d\\ude00\"]","error":"managed exec environment contains duplicate key: A"}, +{"text":"[\"A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"B=\\u00e9\"]","environment":["A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","B=\u00e9"]}, +{"text":"[\"B=1\"]","environment":["B=1"]}, +{"text":"[\"B=\\u00e9\", \"\\u00c9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"]","environment":["B=\u00e9","\u00c9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]}, +{"text":"[\"\u00c9=\u00e9\", \"B=1\"]","environment":["\u00c9=\u00e9","B=1"]}, +{"text":"[\"\u00c9=\ud83d\ude00\", \"\u00c9=\ud83d\ude00\"]","error":"managed exec environment contains duplicate key: \u00c9"}, +{"text":"[\"A=\\u00e9\", \"A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"B=\\ud83d\\ude00\", \"A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"]","error":"managed exec environment contains duplicate key: A"}, +{"text":"[\"B=\\u00e9\", \"A=\\u00e9\", \"\\u00c9=\\ud83d\\ude00\", \"A=\\ud83d\\ude00\"]","error":"managed exec environment contains duplicate key: A"}, +{"text":"[\"A=\ud83d\ude00\", \"B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"]","environment":["A=\ud83d\ude00","B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]}, +{"text":"[]","environment":[]}, +{"text":"[\"B=\ud83d\ude00\"]","environment":["B=\ud83d\ude00"]}, +{"text":"[\"A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"B=\\u00e9\", \"A=\\ud83d\\ude00\", \"\\u00c9=1\"]","error":"managed exec environment contains duplicate key: A"}, +{"text":"[\"B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"B=1\", \"B=\\u00e9\"]","error":"managed exec environment contains duplicate key: B"}, +{"text":"[\"A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"]","environment":["A=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","B=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]}, +{"text":"[\"A=\u00e9\", \"\u00c9=\u00e9\"]","environment":["A=\u00e9","\u00c9=\u00e9"]}, +{"text":"[\"A=\\ud83d\\ude00\"]","environment":["A=\ud83d\ude00"]}, +{"text":"[\"\u00c9=\u00e9\", \"A=\ud83d\ude00\"]","environment":["\u00c9=\u00e9","A=\ud83d\ude00"]}, +{"text":"[]","environment":[]}, +{"text":"[\"\\u00c9=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\", \"\\u00c9=1\", \"A=\\ud83d\\ude00\"]","error":"managed exec environment contains duplicate key: \u00c9"} +], +"distro_layer": "distro", +"distro_manifests": [ +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"systemd images are unsupported by the sandbox security profile"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"systemd images are unsupported by the sandbox security profile"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":null}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1.0, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"systemd images are unsupported by the sandbox security profile"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":null}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": true, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": \"1\", \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 2, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": \"other.erofs\", \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": []}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": {}}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": 1}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": NaN, \"artifact\": 1, \"artifact_sha256\": \"0\", \"packages\": [{\"name\": \"systemd\"}, 1]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"[]"},"error":"invalid sandbox distro manifest: expected a JSON object"}, +{"manifest":{"text":"{\r\n\"format\": }"},"error":"invalid sandbox distro manifest: Expecting value: line 2 column 11 (char 12)"}, +{"manifest":{"bytes":"7bff7d"},"error":"'utf-8' codec can't decode byte 0xff in position 1: invalid start byte"}, +{"manifest":{"text":"\ufeff{}"},"error":"invalid sandbox distro manifest: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"format\": 2}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"},"error":"Exceeds the limit (4300 digits) for integer string conversion: value has 4301 digits; use sys.set_int_max_str_digits() to increase the limit"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6ba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": Infinity3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting ',' delimiter: line 1 column 70 (char 69)"}, +{"manifest":{"text":"{\"format\": 1, \"urtifact\": \"distro.erofs\", \"artifact\u00e9sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sh\t256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Invalid control character at: line 1 column 55 (char 54)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbfb10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": ba\"sh\"}]}"},"error":"invalid sandbox distro manifest: Expecting value: line 1 column 151 (char 150)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\":\u00e9 1, \"artif\tact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting value: line 1 column 11 (char 10)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\":ame\": \"eash\"}]}"},"error":"sandbox distro manifest has invalid package metadata"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distr.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\"d800 \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting ',' delimiter: line 1 column 127 (char 126)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"306\\b5a4993b81fc6b4c6eba8321b7cbfbf5\tb10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Invalid control character at: line 1 column 99 (char 98)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b\u00e90c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": -bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting value: line 1 column 152 (char 151)"}, +{"manifest":{"text":"{\"format\": 1, \"atruetifact:\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\". [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting ':' delimiter: line 1 column 144 (char 143)"}, +{"manifest":{"text":"{\"formatd800\": 1, \"artifact\": \"distro.erofs\", \"art]ifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packaes\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": , \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bd 82b516\", \"packagese\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting value: line 1 column 12 (char 11)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\",NaN\"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting property name enclosed in double quotes: line 1 column 42 (char 41)"}, +{"manifest":{"text":"{\"format\": 1, \"arti0fact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8[d8004e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{]\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"manifest":{"text":"{\"format\": 11, \"artifact\":\"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069NaNc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs:, \"artfact_sha256\": \"3069c5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting ',' delimiter: line 1 column 44 (char 43)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.eros\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artif{act_sha256\": \"306bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b7\u001fbdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Invalid control character at: line 1 column 117 (char 116)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact\u0000_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\"d800, \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Invalid control character at: line 1 column 52 (char 51)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifat_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c\teba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\":- \"bash\"}]}"},"error":"invalid sandbox distro manifest: Invalid control character at: line 1 column 84 (char 83)"}, +{"manifest":{"text":"{\"format: 1, \"ar ifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d9\u00e9b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting ':' delimiter: line 1 column 15 (char 14)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81 c6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{d800\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b51-6\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"pa\u001fckages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Invalid control character at: line 1 column 133 (char 132)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b8\u00e9fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b51\\6\", \"packages\": null[{\"nam\ne\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Invalid \\escape: line 1 column 126 (char 125)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fcb4c6eb\u00008321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"nanulle\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Invalid control character at: line 1 column 86 (char 85)"}, +{"manifest":{"text":"{\"format\": 1, \"artiInfinityact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"\\\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting value: line 1 column 1 (char 0)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifxct_sha25\n\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": .[{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Invalid control character at: line 1 column 58 (char 57)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"nam\"\": \"bah\"}]}"},"error":"invalid sandbox distro manifest: Expecting ':' delimiter: line 1 column 149 (char 148)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4ctrue6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"fo{mat\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash}\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4null7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bas\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a499nullb81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{null\"name\": \"bas\"}]}"},"error":"invalid sandbox distro manifest: Expecting property name enclosed in double quotes: line 1 column 147 (char 146)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_sha256\": \"3069bc5a993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\"1 \"distro.erofs\", \"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"invalid sandbox distro manifest: Expecting ':' delimiter: line 1 column 25 (char 24)"}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\",\"artifact_sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":null}, +{"manifest":{"text":"{\"format\": 1, \"artifact\": \"distro.erofs\", \"artifact_}sha256\": \"3069bc5a4993b81fc6b4c6eba8321b7cbfbf5b10c8e4e7f83d95b79bdd82b516\", \"packages\": [{\"name\": \"bash\"}]}"},"error":"sandbox distro manifest does not match its artifact"} +] +} diff --git a/nvx-rs/tests/managed_lifecycle.rs b/nvx-rs/tests/managed_lifecycle.rs new file mode 100644 index 000000000..6362cf143 --- /dev/null +++ b/nvx-rs/tests/managed_lifecycle.rs @@ -0,0 +1,458 @@ +//! The managed lifecycle of a guest whose init hands the boot to the managed agent, on a real +//! hypervisor: authenticated sessions and reconnects, request-scoped working directories and +//! environments, the workload identity, timeouts, refused working directories, and stop, driven +//! through `nvx::host::session` as `nvx::host::instance` drives it. +//! +//! `scripts/nvx.py test-microvm --scenario managed-lifecycle` runs it for the selected guest and +//! backend, then checks the outcome report that OpenVMM wrote and the guest console. It sets: +//! +//! - `NVX_MANAGED_LIFECYCLE_COMMAND`: OpenVMM's command line, as a JSON array of strings, with the +//! managed lifecycle, the control console, `--microvm-control-auth-stdin`, and the report. +//! - `NVX_MANAGED_LIFECYCLE_ENDPOINT`: the control endpoint that the command listens on. +//! - `NVX_MANAGED_LIFECYCLE_CONSOLE`: the TCP address at which the command's guest console +//! listens, `HOST:PORT`. +//! - `NVX_MANAGED_LIFECYCLE_TIMEOUT`: the longest wait for each operation, in seconds. +//! - `NVX_MANAGED_LIFECYCLE_OUTPUT_DIR`: receives OpenVMM's output, `managed-lifecycle.log`, and +//! the guest console, `managed-lifecycle-guest.log`, which the scenario then scans. +//! +//! The test also records OpenVMM's process ID in `NVX_ADVERSARIAL_OPENVMM_PID_JOURNAL`, if set, +//! as the microVM scenarios do for the adversarial campaigns. +//! +//! These checks were `run_managed_lifecycle` of `scripts/nvx_tools/microvm_tests.py`, which drove +//! the control session from Python until microsoft/nvx#359 left the protocol to nvx-rs alone. +#![cfg(all(feature = "host", any(target_os = "linux", windows)))] + +use std::env; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, Write}; +use std::net::TcpStream; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use nvx::host::instance::generate_capability; +use nvx::host::platform::process_start_time; +use nvx::host::session::{self, ControlSession, ExecEvent}; +use nvx::protocol::{CAPABILITY_LEN, CWD_FAILED, ExitCategory, Workload, WorkloadEnvironment}; + +/// An entry whose value has spaces, an equals sign, and a character beyond ASCII. +const COMPLEX: &str = "COMPLEX=space = \u{2603}"; +/// The file that a workload creates if it runs despite a refused working directory. +const REFUSED_CWD_MARKER: &str = "/tmp/nvx-refused-cwd-ran"; +/// Working directories that the workload cannot enter, each with the Linux error number that the +/// guest refuses it with, which does not depend on the host. +const REFUSED_CWDS: [(&str, &str, i32); 3] = [ + ("missing", "/does-not-exist", 2), + ("file", "/etc/passwd", 20), + ("inaccessible", "/root", 13), +]; +/// The identity of the workloads, which the scenario's command line selects. +const WORKLOAD_IDENTITY: &str = "65534:65534"; + +fn variable(name: &str) -> String { + env::var(name).unwrap_or_else(|_| { + panic!("{name} must be set; run scripts/nvx.py test-microvm --scenario managed-lifecycle") + }) +} + +/// OpenVMM, killed when the test ends before it exits. +struct OpenVmm(Child); + +impl Drop for OpenVmm { + fn drop(&mut self) { + if let Ok(None) = self.0.try_wait() { + let _ = self.0.kill(); + let _ = self.0.wait(); + } + } +} + +/// How a workload ended, and its output. +#[derive(Debug, Default)] +struct Outcome { + /// How it exited, and its status, if it ran. + exit: Option<(ExitCategory, i32)>, + /// The guest's error number and category, if the guest refused it. + refusal: Option<(i32, String)>, + /// Its standard output. + stdout: Vec, + /// Its standard error, or the guest's diagnostic of a refusal. + stderr: Vec, +} + +impl Outcome { + /// Returns whether the workload exited with status 0 and printed exactly `stdout`. + fn printed(&self, stdout: &str) -> bool { + self.exit == Some((ExitCategory::Exit, 0)) + && self.stdout == stdout.as_bytes() + && self.stderr.is_empty() + } +} + +/// The control session of the guest that OpenVMM `pid` runs. +struct Guest { + /// The control endpoint. + endpoint: String, + /// OpenVMM's process ID. + pid: u32, + /// OpenVMM's start time. + start_time: u64, + /// The capability that OpenVMM received. + capability: [u8; CAPABILITY_LEN], + /// The longest wait for each operation. + timeout: Duration, +} + +impl Guest { + /// Opens and authenticates a session. + fn connect(&self) -> ControlSession { + let deadline = Instant::now() + self.timeout; + let transport = session::connect(&self.endpoint, self.pid, self.start_time, deadline) + .expect("the control endpoint accepts a session"); + ControlSession::attach(transport, &self.capability, deadline) + .expect("the control endpoint accepts the capability") + } + + /// Runs `argv` in `session` and returns how it ended. + fn exec( + &self, + session: &mut ControlSession, + argv: &[&str], + timeout_ms: u32, + cwd: Option<&str>, + environment: WorkloadEnvironment<'_>, + ) -> Outcome { + let workload = Workload { + argv: argv.iter().map(|&argument| argument.to_owned()).collect(), + timeout_ms, + cwd, + environment, + }; + // The longest wait bounds the whole exchange, so the workloads must end well before it. + let deadline = Instant::now() + self.timeout; + let request = session + .start_exec(&workload, deadline) + .expect("the guest accepts the request"); + let mut outcome = Outcome::default(); + loop { + match session + .next_exec_event(request, Some(deadline)) + .unwrap_or_else(|error| panic!("{argv:?} did not end: {error}")) + { + ExecEvent::Stdout(chunk) => outcome.stdout.extend(chunk), + ExecEvent::Stderr(chunk) => outcome.stderr.extend(chunk), + ExecEvent::Exit { category, status } => { + outcome.exit = Some((category, status)); + return outcome; + } + ExecEvent::Rejected { status, category } => { + outcome.refusal = Some((status, category)); + return outcome; + } + } + } + } +} + +/// Copies the guest console that listens at `address` into `path` until OpenVMM closes it. +/// +/// The console of a microVM listens on TCP, so the copy connects as soon as OpenVMM listens, as +/// the scenario's console monitor did when it drove the session from Python. +fn capture_console(address: String, path: PathBuf, deadline: Instant) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut console = loop { + match TcpStream::connect(&address) { + Ok(console) => break console, + Err(error) => { + assert!( + Instant::now() < deadline, + "the guest console at {address} did not accept a connection: {error}" + ); + thread::sleep(Duration::from_millis(25)); + } + } + }; + let mut log = File::create(&path).expect("the guest console log opens"); + // A reset, as OpenVMM's exit may cause, ends the console like its end. + let _ = io::copy(&mut console, &mut log); + }) +} + +/// Records OpenVMM's process ID for an adversarial campaign in the journal that +/// `NVX_ADVERSARIAL_OPENVMM_PID_JOURNAL` names, if it is set, as `record_adversarial_openvmm_pid` +/// of the microVM tests does. +fn record_pid(pid: u32) { + if let Some(journal) = env::var_os("NVX_ADVERSARIAL_OPENVMM_PID_JOURNAL") { + append_pid(Path::new(&journal), pid); + } +} + +/// Appends an entry for `pid` to a process ID journal and syncs it. Each entry is a line, +/// `{"pid":PID,"recorded_at_ns":NS}`, which `_read_openvmm_pids` of +/// `scripts/nvx_tools/adversarial_executor.py` reads to check that OpenVMM exited. +fn append_pid(journal: &Path, pid: u32) { + let recorded_at_ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("the clock is after the epoch") + .as_nanos(); + let mut stream = OpenOptions::new() + .create(true) + .append(true) + .open(journal) + .expect("the process ID journal opens"); + writeln!( + stream, + "{{\"pid\":{pid},\"recorded_at_ns\":{recorded_at_ns}}}" + ) + .expect("the process ID journal is writable"); + stream.sync_all().expect("the process ID journal syncs"); +} + +#[test] +fn the_process_id_journal_has_the_entries_that_the_adversarial_executor_reads() { + let directory = tempfile::tempdir().unwrap(); + let journal = directory.path().join("openvmm-pids.jsonl"); + append_pid(&journal, 4321); + append_pid(&journal, u32::MAX); + let text = fs::read_to_string(&journal).unwrap(); + assert!( + text.ends_with('\n') && !text.contains('\r'), + "each entry ends with a line feed: {text:?}" + ); + let pids: Vec = text + .lines() + .map(|line| { + let entry: serde_json::Map = + serde_json::from_str(line).expect("an entry is a JSON object"); + assert_eq!( + entry.keys().collect::>(), + ["pid", "recorded_at_ns"], + "an entry has exactly these properties: {line}" + ); + let recorded_at_ns = entry["recorded_at_ns"] + .as_u64() + .expect("the time is an integer"); + assert!( + (1..=i64::MAX.unsigned_abs()).contains(&recorded_at_ns), + "the time fits the executor's range: {line}" + ); + entry["pid"].as_u64().expect("the process ID is an integer") + }) + .collect(); + assert_eq!(pids, [4321, u64::from(u32::MAX)]); +} + +#[test] +#[ignore = "requires a hypervisor and NVX guest artifacts; run scripts/nvx.py test-microvm \ + --scenario managed-lifecycle"] +fn a_managed_guest_runs_request_scoped_workloads_until_it_stops() { + let command: Vec = serde_json::from_str(&variable("NVX_MANAGED_LIFECYCLE_COMMAND")) + .expect("NVX_MANAGED_LIFECYCLE_COMMAND is a JSON array of strings"); + let timeout = Duration::from_secs_f64( + variable("NVX_MANAGED_LIFECYCLE_TIMEOUT") + .parse() + .expect("NVX_MANAGED_LIFECYCLE_TIMEOUT is a number of seconds"), + ); + let output_dir = PathBuf::from(variable("NVX_MANAGED_LIFECYCLE_OUTPUT_DIR")); + fs::create_dir_all(&output_dir).unwrap(); + let log = File::create(output_dir.join("managed-lifecycle.log")).unwrap(); + let capability = generate_capability().unwrap(); + // OpenVMM reads its capability as soon as it starts, so its input already holds the + // capability and its end, as `nvx::host::process::spawn` prepares it. + let (input, mut writer) = io::pipe().unwrap(); + writer.write_all(&capability).unwrap(); + drop(writer); + let child = Command::new(&command[0]) + .args(&command[1..]) + .stdin(Stdio::from(input)) + .stdout(log.try_clone().unwrap()) + .stderr(log) + .spawn() + .unwrap_or_else(|error| panic!("cannot start {}: {error}", command[0])); + let mut openvmm = OpenVmm(child); + let pid = openvmm.0.id(); + record_pid(pid); + let console = capture_console( + variable("NVX_MANAGED_LIFECYCLE_CONSOLE"), + output_dir.join("managed-lifecycle-guest.log"), + Instant::now() + timeout, + ); + let guest = Guest { + endpoint: variable("NVX_MANAGED_LIFECYCLE_ENDPOINT"), + pid, + start_time: process_start_time(pid) + .unwrap() + .expect("OpenVMM runs after it started"), + capability, + timeout, + }; + + // The first session finds the guest agent ready, and a workload runs in its own working + // directory with exactly the environment that it asks for, an empty value included. + let mut session = guest.connect(); + session + .ping(Instant::now() + timeout) + .expect("the guest agent is ready"); + let exact = vec!["EMPTY=".to_owned(), COMPLEX.to_owned()]; + let first = guest.exec( + &mut session, + &[ + "/bin/sh", + "-c", + "/bin/touch nvx-managed-state; printf '%s|%s|%s' \"$PWD\" \"$EMPTY\" \"$COMPLEX\"", + ], + 5_000, + Some("/tmp"), + WorkloadEnvironment::Replaced(&exact), + ); + assert!( + first.printed("/tmp||space = \u{2603}"), + "the first managed workload returned {first:?}" + ); + drop(session); + + // A second session reaches the same guest. An explicitly empty environment holds no + // variable, and an exact one holds exactly its entries. + let mut session = guest.connect(); + let empty = guest.exec( + &mut session, + &["/usr/bin/env"], + 5_000, + None, + WorkloadEnvironment::Replaced(&[]), + ); + assert!(empty.printed(""), "an empty environment printed {empty:?}"); + let replaced = guest.exec( + &mut session, + &["/usr/bin/env"], + 5_000, + None, + WorkloadEnvironment::Replaced(&exact), + ); + assert!( + replaced.printed("EMPTY=\nCOMPLEX=space = \u{2603}\n"), + "an exact environment printed {replaced:?}" + ); + // Every timeout that fits the protocol's 32 bits is accepted, and a workload that ends first + // finishes normally. + for timeout_ms in [0, 3_600_001, 86_400_000, u32::MAX] { + let outcome = guest.exec( + &mut session, + &["/bin/true"], + timeout_ms, + None, + WorkloadEnvironment::Default, + ); + assert!(outcome.printed(""), "timeout {timeout_ms} ms: {outcome:?}"); + } + // Workloads run as the configured identity. + let identity = guest.exec( + &mut session, + &["/bin/sh", "-c", "printf '%s:%s' \"$(id -u)\" \"$(id -g)\""], + 5_000, + None, + WorkloadEnvironment::Default, + ); + assert!( + identity.printed(WORKLOAD_IDENTITY), + "the workload identity is {identity:?}" + ); + // State persists across requests, while working directories and environments do not, and a + // direct-mode guest has no workload machine ID. + let second = guest.exec( + &mut session, + &[ + "/bin/sh", + "-c", + "pwd; test -f /tmp/nvx-managed-state; test \"${COMPLEX-unset}\" = unset; \ + test ! -e /run/nvx/workload-machine-id; printf second-exec", + ], + 5_000, + None, + WorkloadEnvironment::Default, + ); + assert!( + second.printed("/\nsecond-exec"), + "managed workload state did not survive across requests: {second:?}" + ); + // A workload that outlives its timeout is ended, and the guest stays usable. + let timed_out = guest.exec( + &mut session, + &["/bin/sleep", "5"], + 100, + None, + WorkloadEnvironment::Default, + ); + assert_eq!( + timed_out.exit, + Some((ExitCategory::Timeout, 124)), + "{timed_out:?}" + ); + let after_timeout = guest.exec( + &mut session, + &["/bin/sh", "-c", "printf still-usable"], + 5_000, + None, + WorkloadEnvironment::Default, + ); + assert!( + after_timeout.printed("still-usable"), + "the guest was not usable after a timeout: {after_timeout:?}" + ); + // The guest refuses a working directory that the workload cannot enter, with its error + // number and a diagnostic that names it, before anything of the workload runs. + for (description, cwd, error) in REFUSED_CWDS { + let refused = guest.exec( + &mut session, + &["/bin/touch", REFUSED_CWD_MARKER], + 5_000, + Some(cwd), + WorkloadEnvironment::Default, + ); + let diagnostic = String::from_utf8_lossy(&refused.stderr); + assert!( + refused.exit.is_none() + && refused.refusal == Some((error, CWD_FAILED.to_owned())) + && refused.stdout.is_empty() + && diagnostic.contains(&format!("cannot enter working directory {cwd}: ")), + "the {description} working directory {cwd} was not refused clearly: {refused:?} \ + ({diagnostic})" + ); + } + let after_refusals = guest.exec( + &mut session, + &[ + "/bin/sh", + "-c", + &format!("test ! -e {REFUSED_CWD_MARKER} && printf still-usable"), + ], + 5_000, + None, + WorkloadEnvironment::Default, + ); + assert!( + after_refusals.printed("still-usable"), + "a refused workload ran, or the guest was not usable after refusals: {after_refusals:?}" + ); + + // Stop ends the VM, and OpenVMM exits successfully once it has written its report. + session + .stop(Instant::now() + timeout) + .expect("the guest agent acknowledges stop"); + let deadline = Instant::now() + timeout; + let status = loop { + if let Some(status) = openvmm.0.try_wait().unwrap() { + break status; + } + assert!(Instant::now() < deadline, "OpenVMM did not exit after stop"); + thread::sleep(Duration::from_millis(50)); + }; + console + .join() + .expect("the guest console was copied until its end"); + assert!( + status.success(), + "the managed OpenVMM process exited with {status}" + ); +} diff --git a/scripts/nvx.py b/scripts/nvx.py index 0fb1c94ef..d69877e89 100644 --- a/scripts/nvx.py +++ b/scripts/nvx.py @@ -10,11 +10,10 @@ import subprocess import sys from collections.abc import Sequence -from dataclasses import replace from pathlib import Path -from typing import cast +from typing import Any, cast -from nvx_tools import sandbox_lifecycle +from nvx_tools import nvx_rs from nvx_tools.aci_edge_sandboxes_tests import ( configure_parser as configure_aci_edge_sandboxes_test_parser, ) @@ -23,7 +22,6 @@ configure_parser as configure_agent_bundling_test_parser, ) from nvx_tools.benchmark import configure_parser as configure_benchmark_parser -from nvx_tools.benchmark import positive_float from nvx_tools.benchmark import run as run_benchmark from nvx_tools.build import ( build_all, @@ -45,7 +43,6 @@ OpenVmmBuildConfig, ) from nvx_tools.build_constants import ( - AlpineBuildConstants, BuildConstants, InitramfsBuildConstants, KernelBuildConstants, @@ -72,21 +69,18 @@ from nvx_tools.common import ( ScriptError, artifact_path, - openvmm_binary_path, require_file, - sha256_file, ) -from nvx_tools.control_session import encode_exec_environment from nvx_tools.create_linux_source_archive import ( configure_parser as configure_linux_source_archive_parser, ) from nvx_tools.doctor import configure_parser as configure_doctor_parser from nvx_tools.doctor import host_cpu_signature -from nvx_tools.egress_policy import compile_policy_file from nvx_tools.elf import check_static_executables from nvx_tools.guest_binaries import ( build_guest_binaries, check_guest_binaries, + guest_binary_source_sha256, test_guest_binaries, ) from nvx_tools.guests import GUEST_NAMES, guest_descriptor @@ -104,9 +98,6 @@ MOUNT_OWNERS, MOUNT_POLICY_OPTIONS, SANDBOX_COMMAND_LINE_MAX_SIZE, - SandboxLaunch, - SandboxLayer, - SandboxMount, aggregate_child_argument, aggregate_child_name, aggregate_command_line_fragment, @@ -114,7 +105,6 @@ mount_argument, mount_kind, parse_mount_value, - parse_workload_identity, require_mount_owner_supported, ) from nvx_tools.time_abi import ( @@ -132,8 +122,9 @@ "tcp, udp, or icmp, and for tcp or udp only on PORT or on the inclusive range " "PORT-PORT; repeat to add rules" ) -MAX_ENVIRONMENT_FILE_BYTES = 1024 * 1024 -SYSTEMD_ENTRYPOINTS = frozenset(("/usr/lib/systemd/systemd", "/lib/systemd/systemd")) +SANDBOX_OPERATIONS = ("run", "provision", "start", "exec", "stop", "deprovision") +# The namespace attribute in which the sandbox parser records its options. +SANDBOX_ARGUMENTS = "sandbox_arguments" def _mount_policy_destination(option: str) -> str: @@ -141,9 +132,27 @@ def _mount_policy_destination(option: str) -> str: return option.removeprefix("--").replace("-", "_") -class _MountPolicyPathAction(argparse.Action): - """Record each --mount-deny, --mount-allow, or --mount-write with the index - of the --mount before it.""" +class _SandboxOption(argparse.Action): + """Record an option of `nvx.py sandbox` for nvx-rs, which checks and uses + every value. + + argparse resolves abbreviations and the --option=VALUE form, so nvx-rs + receives each option under its canonical name, in command-line order, + which attributes each --mount-deny, --mount-allow, and --mount-write to the + --mount before it, and with its value attached, so that a value that + starts with "-" stays a value. + """ + + def __init__( + self, + option_strings: Sequence[str], + dest: str, + nargs: int | str | None = None, + **kwargs: Any, + ) -> None: + super().__init__( + option_strings, dest, nargs=nargs, default=argparse.SUPPRESS, **kwargs + ) def __call__( self, @@ -152,50 +161,10 @@ def __call__( values: str | Sequence[object] | None, option_string: str | None = None, ) -> None: - mounts = cast(list[str], getattr(namespace, "mount", None) or []) - paths = list( - cast(list[tuple[int, str]], getattr(namespace, self.dest, None) or []) - ) - paths.append((len(mounts) - 1, cast(str, values))) - setattr(namespace, self.dest, paths) - - -def _sandbox_mounts(args: argparse.Namespace) -> tuple[SandboxMount, ...]: - """Attribute each access-policy path to its share and parse the --mount - options, each of which shares a directory or, when its host path is a - regular file, that file. - - With one --mount, every --mount-deny, --mount-allow, and --mount-write - names a path in it. With several, each names a path in the --mount that - precedes it. - """ - mounts = cast(list[str], args.mount) - attributed: dict[str, list[list[str]]] = {} - for kind, option in MOUNT_POLICY_OPTIONS.items(): - paths: list[list[str]] = [[] for _ in mounts] - destination = _mount_policy_destination(option) - for index, path in cast(list[tuple[int, str]], getattr(args, destination)): - if len(mounts) == 1: - index = 0 - elif index < 0: - raise ScriptError( - f"with several --mount options, each {option} must follow the " - "--mount whose host directory it names" - ) - paths[index].append(path) - attributed[kind] = paths - owner = args.mount_owner or "vmm" - parsed: list[SandboxMount] = [] - for index, value in enumerate(mounts): - mount = SandboxMount.parse( - value, - tuple(attributed["denied"][index]), - owner, - allowed_paths=tuple(attributed["allowed"][index]), - writable_paths=tuple(attributed["writable"][index]), - ) - parsed.append(replace(mount, kind=mount_kind(mount.host_path))) - return tuple(parsed) + name = self.option_strings[0] + argument = name if self.nargs == 0 else f"{name}={cast(str, values)}" + recorded = cast(list[str], getattr(namespace, SANDBOX_ARGUMENTS, None) or []) + setattr(namespace, SANDBOX_ARGUMENTS, [*recorded, argument]) def _validate_run_share_targets(targets: list[str]) -> None: @@ -235,52 +204,6 @@ def _run( subprocess.run(command, cwd=cwd, check=True) -def _validate_sandbox_systemd_policy(launch: SandboxLaunch) -> None: - if launch.entrypoint in SYSTEMD_ENTRYPOINTS: - raise ScriptError( - "systemd entrypoints are unsupported by the sandbox security profile" - ) - distro = next( - (layer for layer in launch.layers if layer.role == "distro"), - None, - ) - if distro is None: - return - manifest = distro.path.with_name( - f"{distro.path.name}{BuildConstants.DISTRO_MANIFEST_SUFFIX}" - ) - if not manifest.exists(): - return - try: - document: object = json.loads(manifest.read_text(encoding="utf-8")) - except (json.JSONDecodeError, OSError) as error: - raise ScriptError(f"invalid sandbox distro manifest: {error}") from error - if not isinstance(document, dict): - raise ScriptError("invalid sandbox distro manifest: expected a JSON object") - manifest_document = cast(dict[str, object], document) - if ( - manifest_document.get("format") != 1 - or manifest_document.get("artifact") != distro.path.name - or manifest_document.get("artifact_sha256") != sha256_file(distro.path) - ): - raise ScriptError("sandbox distro manifest does not match its artifact") - raw_packages = manifest_document.get("packages") - if not isinstance(raw_packages, list): - raise ScriptError("sandbox distro manifest has invalid package metadata") - packages: list[dict[str, object]] = [] - for raw_package in cast(list[object], raw_packages): - if not isinstance(raw_package, dict): - raise ScriptError("sandbox distro manifest has invalid package metadata") - package = cast(dict[str, object], raw_package) - if not isinstance(package.get("name"), str): - raise ScriptError("sandbox distro manifest has invalid package metadata") - packages.append(package) - if any(package["name"] == "systemd" for package in packages): - raise ScriptError( - "systemd images are unsupported by the sandbox security profile" - ) - - def command_init(_: argparse.Namespace) -> None: _run(["git", "submodule", "update", "--init", "--recursive"]) @@ -370,6 +293,11 @@ def command_record_openvmm_provenance(_: argparse.Namespace) -> None: record_openvmm_provenance(OpenVmmBuildConfig()) +def command_record_nvx_rs_provenance(args: argparse.Namespace) -> None: + path = nvx_rs.record_provenance(args.target) + print(f">> recorded the provenance of nvx-rs in {path}") + + def command_materialize_kernel_provenance_inputs(_: argparse.Namespace) -> None: materialize_kernel_provenance_inputs() @@ -393,6 +321,10 @@ def command_kernel_patch_identity(args: argparse.Namespace) -> None: print(kernel_patch_identity(args.root)) +def command_guest_binary_source_identity(args: argparse.Namespace) -> None: + print(guest_binary_source_sha256(args.root)) + + def command_check_required_ci(args: argparse.Namespace) -> None: results = { job: os.environ.get(environment, "") @@ -501,7 +433,7 @@ def _resolve_network_egress_rules( raise ScriptError( "--network-egress is required with --network-egress-policy-file" ) - compiled = compile_policy_file(policy_path) + compiled = nvx_rs.compile_egress_policy(policy_path) return compiled.allow, compiled.deny @@ -556,7 +488,8 @@ def command_run(args: argparse.Namespace) -> None: "--restore-processors cannot exceed --processors capacity" ) network_egress_allow, network_egress_deny = _resolve_network_egress_rules(args) - executable = require_file(openvmm_binary_path(), "OpenVMM release binary") + artifacts = nvx_rs.artifacts() + executable = require_file(artifacts.openvmm, "OpenVMM release binary") command = [ str(executable), "--single-process", @@ -576,13 +509,9 @@ def command_run(args: argparse.Namespace) -> None: if args.restore_ready_path is not None: command.extend(["--restore-ready-path", str(args.restore_ready_path)]) else: - kernel = require_file( - artifact_path(KernelBuildConstants.BINARY_NAME), "Linux direct kernel" - ) - initrd = require_file( - artifact_path(descriptor.initramfs_name), - f"{descriptor.distribution} initramfs", - ) + kernel = require_file(artifacts.kernel, "Linux direct kernel") + guest = artifacts.guest(descriptor.name) + initrd = require_file(guest.initramfs, f"{guest.distribution} initramfs") command.extend( [ "--memory", @@ -699,210 +628,13 @@ def _run_openvmm( return returncode -def command_sandbox(args: argparse.Namespace) -> None: - operation = args.sandbox_operation - exec_environment: tuple[str, ...] | None = None - if args.dry_run and operation != "run": - raise ScriptError("--dry-run is only valid for sandbox run") - if args.environment and args.environment_file is not None: - raise ScriptError("--environment and --environment-file are mutually exclusive") - if operation != "exec" and ( - args.cwd is not None - or args.environment - or args.environment_file is not None - or args.inherit_default_environment - ): - raise ScriptError( - "managed execution options require the sandbox exec operation" - ) - if operation == "exec": - if args.environment_file is not None: - try: - with args.environment_file.open("rb") as stream: - data = stream.read(MAX_ENVIRONMENT_FILE_BYTES + 1) - if len(data) > MAX_ENVIRONMENT_FILE_BYTES: - raise ScriptError( - "managed execution environment file exceeds " - f"{MAX_ENVIRONMENT_FILE_BYTES}-byte limit" - ) - value = json.loads(data.decode("utf-8")) - except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: - raise ScriptError( - f"failed to read managed execution environment: " - f"{args.environment_file}" - ) from error - entries = cast(list[object], value) if isinstance(value, list) else None - if entries is None or not all(isinstance(entry, str) for entry in entries): - raise ScriptError( - "managed execution environment file must contain a JSON " - "array of KEY=VALUE strings" - ) - exec_environment = tuple(cast(list[str], entries)) - elif args.environment: - exec_environment = tuple(args.environment) - if exec_environment is not None: - try: - encode_exec_environment(exec_environment) - except (TypeError, ValueError) as error: - raise ScriptError(str(error)) from error - network_options = ( - args.net, - args.network_profile, - args.network_egress, - args.network_ingress, - args.network_egress_policy_file, - args.host_loopback, - args.network_proxy, - ) - if operation not in ("run", "provision") and ( - any(value is not None for value in network_options) - or args.network_egress_allow - or args.network_egress_deny - or args.host_loopback_forward - ): - raise ScriptError( - "network policy options are only valid for sandbox run or provision" - ) - if operation in ("run", "provision", "exec") and ( - args.entrypoint in SYSTEMD_ENTRYPOINTS - ): - raise ScriptError( - "systemd entrypoints are unsupported by the sandbox security profile" - ) - if args.outcome_report is not None and operation not in ("run", "exec"): - raise ScriptError( - "--outcome-report is only valid for one-shot run or managed exec" - ) - for option in MOUNT_POLICY_OPTIONS.values(): - if getattr(args, _mount_policy_destination(option)) and not args.mount: - raise ScriptError(f"{option} requires --mount") - if args.mount_owner is not None and not args.mount: - raise ScriptError("--mount-owner requires --mount") - if args.mount and operation not in ("run", "provision"): - raise ScriptError("--mount is only valid for sandbox run or provision") - if operation in ("run", "provision"): - if (args.net is None) != (args.network_profile is None): - raise ScriptError("--net and --network-profile must be specified together") - if not args.layer or args.scratch is None: - raise ScriptError(f"sandbox {operation} requires --layer and --scratch") - network_egress_allow, network_egress_deny = _resolve_network_egress_rules(args) - launch = SandboxLaunch( - layers=tuple(args.layer), - scratch=args.scratch, - entrypoint=args.entrypoint, - args=tuple(args.sandbox_arg), - hostname=args.hostname, - workload_identity=args.workload_user, - memory_max=args.memory_max, - pids_max=args.pids_max, - mounts=_sandbox_mounts(args), - ).validated() - _validate_sandbox_systemd_policy(launch) - else: - launch = None - network_egress_allow = () - network_egress_deny = () - - if operation == "provision": - if args.state_dir is None: - raise ScriptError("sandbox provision requires --state-dir") - assert launch is not None - sandbox_lifecycle.provision( - args.state_dir, - launch, - hypervisor=_hypervisor(args.hypervisor), - memory_mib=args.memory_mib, - net=args.net, - network_profile=args.network_profile, - network_egress=args.network_egress, - network_ingress=args.network_ingress, - network_egress_allow=network_egress_allow, - network_egress_deny=network_egress_deny, - host_loopback=args.host_loopback, - network_proxy=args.network_proxy, - host_loopback_forward=tuple(args.host_loopback_forward), - cmdline=args.cmdline, - timeout=args.timeout, - ) - return - if operation == "start": - if args.state_dir is None: - raise ScriptError("sandbox start requires --state-dir") - sandbox_lifecycle.start(args.state_dir, args.timeout) - return - if operation == "exec": - if args.state_dir is None: - raise ScriptError("sandbox exec requires --state-dir") - if args.outcome_report is not None: - sandbox_lifecycle.validate_outcome_destination(args.outcome_report) - result = sandbox_lifecycle.exec_workload( - args.state_dir, - (args.entrypoint, *args.sandbox_arg), - timeout_ms=args.exec_timeout_ms, - response_timeout=args.timeout, - cwd=args.cwd, - environment=exec_environment, - inherit_default_environment=args.inherit_default_environment, - ) - sys.stdout.buffer.write(result.stdout) - sys.stdout.buffer.flush() - sys.stderr.buffer.write(result.stderr) - sys.stderr.buffer.flush() - if args.outcome_report is not None: - sandbox_lifecycle.write_exec_outcome(args.outcome_report, result) - raise SystemExit(result.returncode) - if operation == "stop": - if args.state_dir is None: - raise ScriptError("sandbox stop requires --state-dir") - sandbox_lifecycle.stop(args.state_dir, args.timeout) - return - if operation == "deprovision": - if args.state_dir is None: - raise ScriptError("sandbox deprovision requires --state-dir") - sandbox_lifecycle.deprovision(args.state_dir, args.timeout) - return - if args.state_dir is not None: - raise ScriptError( - "one-shot sandbox execution rejects persistent --state-dir settings" - ) - assert operation == "run" - assert launch is not None - executable = require_file(openvmm_binary_path(), "OpenVMM release binary") - kernel = require_file( - artifact_path(KernelBuildConstants.BINARY_NAME), "Linux direct kernel" +def command_sandbox(args: argparse.Namespace) -> int: + """Run the sandbox command with nvx-rs, which checks its options and runs it + on the standard streams, and return the status that nvx.py exits with.""" + return nvx_rs.run( + ["sandbox", args.sandbox_operation, *getattr(args, SANDBOX_ARGUMENTS)], + program=args.program, ) - initrd = require_file( - artifact_path(AlpineBuildConstants.INITRAMFS_NAME), - "initramfs", - ) - command = [ - str(executable), - *launch.openvmm_arguments(), - "--microvm-lifecycle", - "one-shot", - "--single-process", - "--hypervisor", - _hypervisor(args.hypervisor), - "--memory", - f"{args.memory_mib}M", - "--kernel", - str(kernel), - "--initrd", - str(initrd), - "--cmdline", - launch.kernel_command_line(args.cmdline), - ] - _extend_network_arguments( - command, - args, - network_egress_allow, - network_egress_deny, - ) - if args.outcome_report is not None: - command.extend(["--microvm-report", str(args.outcome_report)]) - print(f">> {_format_command(command)}") - if not args.dry_run: - raise SystemExit(subprocess.run(command).returncode) def command_collect_sources(_: argparse.Namespace) -> None: @@ -1073,6 +805,19 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: ) provenance.set_defaults(handler=command_record_openvmm_provenance) + nvx_rs_provenance = subparsers.add_parser( + "record-nvx-rs-provenance", + help="bind an existing nvx-rs executable to the nvx-rs sources", + ) + nvx_rs_provenance.add_argument( + "--target", + help=( + "Rust target that built nvx-rs (default: the host target that " + "rustc reports)" + ), + ) + nvx_rs_provenance.set_defaults(handler=command_record_nvx_rs_provenance) + kernel_provenance = subparsers.add_parser( "materialize-kernel-provenance-inputs", help="write kernel provenance inputs from raw run-head blobs", @@ -1123,6 +868,20 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: ) patch_identity.set_defaults(handler=command_kernel_patch_identity) + source_identity = subparsers.add_parser( + "guest-binary-source-identity", + help=( + "print the digest of the Rust guest binary source paths and contents " + "that keys the guest image caches" + ), + ) + source_identity.add_argument( + "--root", + type=Path, + default=BuildConstants.REPO_ROOT, + ) + source_identity.set_defaults(handler=command_guest_binary_source_identity) + required_ci = subparsers.add_parser( "check-required-ci", help="validate required GitHub Actions job results", @@ -1341,91 +1100,69 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: help="run or manage workloads over EROFS layers and private ext4 scratch", ) - def sandbox_layer(value: str) -> SandboxLayer: - try: - return SandboxLayer.parse(value) - except ScriptError as error: - raise argparse.ArgumentTypeError(str(error)) from error - - def sandbox_identity(value: str) -> tuple[int, int]: - try: - return parse_workload_identity(value) - except ScriptError as error: - raise argparse.ArgumentTypeError(str(error)) from error + # nvx-rs checks the values and applies the defaults; these options define + # the command line and its help. + def sandbox_option(*names: str, flag: bool = False, **kwargs: Any) -> None: + sandbox.add_argument( + *names, action=_SandboxOption, nargs=0 if flag else None, **kwargs + ) sandbox.add_argument( "sandbox_operation", nargs="?", - choices=("run", "provision", "start", "exec", "stop", "deprovision"), + choices=SANDBOX_OPERATIONS, default="run", ) - sandbox.add_argument( - "--layer", - action="append", - default=[], - type=sandbox_layer, - metavar="ROLE,PATH,EROFS_UUID", - ) - sandbox.add_argument("--scratch", type=Path) - sandbox.add_argument("--state-dir", type=Path) - sandbox.add_argument("--entrypoint", default="/bin/sh") - sandbox.add_argument("--arg", action="append", default=[], dest="sandbox_arg") - sandbox.add_argument("--hostname", default="nvx-sandbox") - sandbox.add_argument( + sandbox_option("--layer", metavar="ROLE,PATH,EROFS_UUID") + sandbox_option("--scratch") + sandbox_option("--state-dir") + sandbox_option("--entrypoint") + sandbox_option("--arg", dest="sandbox_arg") + sandbox_option("--hostname") + sandbox_option( "--workload-user", - type=sandbox_identity, - default=parse_workload_identity("65534:65534"), metavar="UID:GID", help="fixed non-root workload identity (default: 65534:65534)", ) - sandbox.add_argument("--memory-max", type=int) - sandbox.add_argument("--pids-max", type=int) - sandbox.add_argument("--memory-mib", type=int, default=256) - sandbox.add_argument( + sandbox_option("--memory-max") + sandbox_option("--pids-max") + sandbox_option("--memory-mib") + sandbox_option( "--timeout", - type=positive_float, - default=60.0, help=( "control operation timeout, and the longest wait for another " "lifecycle transition of the sandbox, in seconds (default: 60)" ), ) - sandbox.add_argument( + sandbox_option( "--exec-timeout-ms", - type=int, - default=0, help="guest workload timeout in milliseconds; zero disables it", ) - sandbox.add_argument( + sandbox_option( "--cwd", help="absolute guest working directory for managed exec (default: /)", ) - sandbox.add_argument( + sandbox_option( "--environment", - action="append", - default=[], metavar="KEY=VALUE", help="set the exact managed exec environment; repeat for multiple entries", ) - sandbox.add_argument( + sandbox_option( "--environment-file", - type=Path, metavar="PATH", help="read the exact managed exec environment from a JSON string array", ) - sandbox.add_argument( + sandbox_option( "--inherit-default-environment", - action="store_true", + flag=True, help=( "layer the managed exec environment over the guest default " "environment instead of replacing it" ), ) - sandbox.add_argument("--hypervisor", choices=HYPERVISORS, default="auto") - sandbox.add_argument( + sandbox_option("--hypervisor", choices=HYPERVISORS) + sandbox_option( "--mount", - action="append", - default=[], metavar="GUEST_TARGET,HOST_PATH[,ro|rw]", help=( "live-share a host directory, or a single regular host file without " @@ -1435,37 +1172,31 @@ def sandbox_identity(value: str) -> tuple[int, int]: "children of one virtio-fs device" ), ) - sandbox.add_argument( + sandbox_option( "--mount-deny", - action=_MountPolicyPathAction, - default=[], metavar="HOST_PATH", help=( "hide one existing path inside a --mount host directory; with " "several --mount options, it applies to the --mount before it" ), ) - sandbox.add_argument( + sandbox_option( "--mount-allow", - action=_MountPolicyPathAction, - default=[], metavar="HOST_PATH", help=( "expose one existing path inside a --mount-deny path of the same " "share again; the hidden directories on the way list only it" ), ) - sandbox.add_argument( + sandbox_option( "--mount-write", - action=_MountPolicyPathAction, - default=[], metavar="HOST_PATH", help=( "make one existing path one of the only writable parts of a " "read-write --mount; the rest of the share is read-only" ), ) - sandbox.add_argument( + sandbox_option( "--mount-owner", choices=MOUNT_OWNERS, help=( @@ -1474,48 +1205,44 @@ def sandbox_identity(value: str) -> tuple[int, int]: "owner of the shared directory or file (Linux only)" ), ) - sandbox.add_argument("--net", metavar="IPV4/PREFIX") - sandbox.add_argument("--network-profile", choices=NETWORK_PROFILES) - sandbox.add_argument("--network-egress", choices=("allow", "deny")) - sandbox.add_argument("--network-ingress", choices=("allow", "deny")) - sandbox.add_argument( + sandbox_option("--net", metavar="IPV4/PREFIX") + sandbox_option("--network-profile", choices=NETWORK_PROFILES) + sandbox_option("--network-egress", choices=("allow", "deny")) + sandbox_option("--network-ingress", choices=("allow", "deny")) + sandbox_option( "--network-egress-allow", - action="append", - default=[], metavar=NETWORK_EGRESS_RULE_METAVAR, help=NETWORK_EGRESS_RULE_HELP.format(action="allow"), ) - sandbox.add_argument( + sandbox_option( "--network-egress-deny", - action="append", - default=[], metavar=NETWORK_EGRESS_RULE_METAVAR, help=NETWORK_EGRESS_RULE_HELP.format(action="deny"), ) - sandbox.add_argument( + sandbox_option( "--network-egress-policy-file", - type=Path, metavar="PATH", help=( "load bounded IPv4 and IPv6 ranges and rule-local exclusions for " "run/provision" ), ) - sandbox.add_argument("--host-loopback", choices=("allow", "deny")) - sandbox.add_argument("--network-proxy", metavar="IPV4:TCP-PORT") - sandbox.add_argument("--host-loopback-forward", action="append", default=[]) - sandbox.add_argument( + sandbox_option("--host-loopback", choices=("allow", "deny")) + sandbox_option("--network-proxy", metavar="IPV4:TCP-PORT") + sandbox_option("--host-loopback-forward") + sandbox_option( "--outcome-report", - type=Path, help="write a bounded local JSON outcome report for run or exec", ) - sandbox.add_argument("--cmdline", default="") - sandbox.add_argument( + sandbox_option("--cmdline") + sandbox_option( "--dry-run", - action="store_true", + flag=True, help="print the one-shot run command without launching it", ) - sandbox.set_defaults(handler=command_sandbox) + sandbox.set_defaults( + handler=command_sandbox, program=parser.prog, **{SANDBOX_ARGUMENTS: []} + ) benchmark = subparsers.add_parser( "benchmark", diff --git a/scripts/nvx_tools/adversarial_executor.py b/scripts/nvx_tools/adversarial_executor.py index 98e0e67dd..8b2bd2fce 100644 --- a/scripts/nvx_tools/adversarial_executor.py +++ b/scripts/nvx_tools/adversarial_executor.py @@ -14,6 +14,7 @@ from pathlib import Path from typing import cast +from nvx_tools import nvx_rs from nvx_tools.adversarial_broker import ( SCHEMA_VERSION, CaseDefinition, @@ -57,9 +58,15 @@ {"structured-outcome.json", "structured-outcome-rejected.json"} ), } +# The variables that the scenarios inherit. Besides the platform's own, they +# are the paths that nvx-rs needs: NVX_RS, the executable that nvx.py runs, and +# CARGO_HOME and RUSTUP_HOME, the Rust toolchain that builds it and runs the +# managed-lifecycle test. RUSTUP_TOOLCHAIN stays out, so that +# rust-toolchain.toml selects the Rust release, as do Cargo's other settings. _ENVIRONMENT_ALLOWLIST = frozenset( { "APPDATA", + "CARGO_HOME", "COMSPEC", "HOME", "HOMEDRIVE", @@ -69,9 +76,11 @@ "LD_LIBRARY_PATH", "LOCALAPPDATA", "NUMBER_OF_PROCESSORS", + "NVX_RS", "PATH", "PATHEXT", "PROCESSOR_ARCHITECTURE", + "RUSTUP_HOME", "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", @@ -169,6 +178,14 @@ def _artifact_metadata() -> dict[str, object]: artifact_path(OpenVMMBuildConstants.PROVENANCE_NAME), "OpenVMM provenance", ), + # The nvx-rs that the scenarios run through nvx.py, which never + # rebuilds it, so a stale one would run without its provenance check. + "nvx_rs": nvx_rs.find_binary() + or require_file(nvx_rs.binary_path(), "nvx-rs executable"), + "nvx_rs_provenance": require_file( + artifact_path(nvx_rs.PROVENANCE_NAME), + "nvx-rs provenance", + ), } validate_runtime_artifact_provenance() return { @@ -187,6 +204,12 @@ def sanitized_environment() -> dict[str, str]: for key, value in os.environ.items() if key.upper() in _ENVIRONMENT_ALLOWLIST } + # The scenarios run in the repository root, where a relative NVX_RS would + # name another executable than the one that the initialization validated + # in the executor's working directory. + configured = environment.get(nvx_rs.VARIABLE) + if configured: + environment[nvx_rs.VARIABLE] = str(Path(configured).absolute()) environment["PYTHONUNBUFFERED"] = "1" return environment diff --git a/scripts/nvx_tools/archive.py b/scripts/nvx_tools/archive.py index 18163800b..da08cfe9d 100644 --- a/scripts/nvx_tools/archive.py +++ b/scripts/nvx_tools/archive.py @@ -22,6 +22,13 @@ def _normalize_member(member: tarfile.TarInfo) -> tarfile.TarInfo | None: path = PurePosixPath(member.name) if "__pycache__" in path.parts or path.suffix in (".pyc", ".pyo"): return None + for excluded in ReleaseBuildConstants.PROJECT_SOURCE_EXCLUDES: + parts = PurePosixPath(excluded.as_posix()).parts + if any( + path.parts[index : index + len(parts)] == parts + for index in range(len(path.parts) - len(parts) + 1) + ): + return None member.uid = 0 member.gid = 0 member.uname = "" diff --git a/scripts/nvx_tools/azurelinux.py b/scripts/nvx_tools/azurelinux.py index 81e247eb9..549528f33 100644 --- a/scripts/nvx_tools/azurelinux.py +++ b/scripts/nvx_tools/azurelinux.py @@ -17,8 +17,8 @@ ScriptError, download_verified, normalized_sha256_file, - sha256_file, ) +from .guest_binaries import guest_binary_source_files _RPM_FIELD = re.compile(r"[A-Za-z0-9][A-Za-z0-9._+~^-]*") _SHA256 = re.compile(r"[0-9a-f]{64}") @@ -129,8 +129,15 @@ def input_files() -> tuple[Path, ...]: # this digest, so changes to them must also rebuild the artifact. *( root / "scripts" / "nvx_tools" / name - for name in ("azurelinux.py", "build_constants.py", "common.py") + for name in ( + "azurelinux.py", + "build_constants.py", + "common.py", + "elf.py", + "guest_binaries.py", + ) ), + *guest_binary_source_files(), ] for directory in AzureLinuxBuildConstants.GUEST_SOURCE_DIRECTORIES: files.extend(path for path in (root / directory).rglob("*") if path.is_file()) @@ -153,7 +160,7 @@ def input_sha256( "files": [ { "path": path.relative_to(root).as_posix(), - "sha256": sha256_file(path), + "sha256": normalized_sha256_file(path), } for path in input_files() ], diff --git a/scripts/nvx_tools/benchmark.py b/scripts/nvx_tools/benchmark.py index 3c7bcf310..1d801cea1 100644 --- a/scripts/nvx_tools/benchmark.py +++ b/scripts/nvx_tools/benchmark.py @@ -38,10 +38,12 @@ from .build_constants import ( AlpineBuildConstants, BuildConstants, + GuestRustBuildConstants, KernelBuildConstants, OpenVMMBuildConstants, ) from .common import bytes_to_mib, sha256_file +from .guest_binaries import guest_binary_source_sha256, pinned_guest_rust_toolchain from .time_abi import TimeAbiFailure, TimeAbiMonitor, status_script BOOT_MARKER = b"ALPINE-MICROVM-BOOT-OK" @@ -4440,10 +4442,8 @@ def _git_status(repository: Path) -> list[str] | None: def _device_io_helper_provenance( args: argparse.Namespace, initrd: Path ) -> dict[str, str]: - source = require_file( - args.nvx_dir.resolve() / "guest" / "common" / "nvx-device-io.c", - "device I/O helper source", - ) + checkout = args.nvx_dir.resolve() + source = checkout / "nvx-rs" manifest_path = require_file( initrd.with_name(f"{initrd.name}{BuildConstants.PACKAGE_MANIFEST_SUFFIX}"), "initramfs package manifest", @@ -4457,10 +4457,11 @@ def _device_io_helper_provenance( raise ValueError( f"invalid nvx-device-io provenance in {manifest_path}" ) from error - actual_source_sha256 = sha256_file(source) + toolchain = pinned_guest_rust_toolchain(checkout) + actual_source_sha256 = guest_binary_source_sha256(checkout) if source_sha256 != actual_source_sha256: raise ValueError( - "initramfs device I/O helper source does not match the current checkout" + "initramfs Rust guest binary sources do not match the current checkout" ) if not isinstance(binary_sha256, str) or not re.fullmatch( r"[0-9a-f]{64}", binary_sha256 @@ -4470,6 +4471,8 @@ def _device_io_helper_provenance( "source": str(source), "source_sha256": actual_source_sha256, "binary_sha256": binary_sha256, + "toolchain": toolchain, + "target": GuestRustBuildConstants.TARGET, } diff --git a/scripts/nvx_tools/build.py b/scripts/nvx_tools/build.py index 36dbc6238..6dfd9cb72 100644 --- a/scripts/nvx_tools/build.py +++ b/scripts/nvx_tools/build.py @@ -29,6 +29,7 @@ AzureLinuxBuildConstants, BuildConstants, DockerBuildConstants, + GuestRustBuildConstants, InitramfsBuildConstants, KernelBuildConstants, OpenVMMBuildConstants, @@ -39,6 +40,7 @@ artifact_path, download_verified, format_size, + normalized_sha256_file, openvmm_git_state, require_file, require_success, @@ -47,6 +49,11 @@ run_checked, sha256_file, ) +from .guest_binaries import ( + build_guest_binaries, + guest_binary_source_files, + inspect_guest_binaries, +) from .guests import GuestDescriptor, guest_descriptor @@ -347,20 +354,23 @@ def merge_kernel_config_fragment(base: str, fragment: str) -> str: def _initramfs_source_files() -> tuple[Path, ...]: - sources = [ + sources = { BuildConstants.REPO_ROOT / DockerBuildConstants.DOCKERFILE, BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "build.py", BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "build_config.py", BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "build_constants.py", BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "common.py", + BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "elf.py", + BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "guest_binaries.py", BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "guests.py", + *guest_binary_source_files(), *( path for directory in AlpineBuildConstants.GUEST_SOURCE_DIRECTORIES for path in (BuildConstants.REPO_ROOT / directory).rglob("*") if path.is_file() ), - ] + } return tuple( sorted( (require_file(path, "initramfs provenance input") for path in sources), @@ -380,7 +390,7 @@ def initramfs_provenance_inputs() -> dict[str, object]: "source_files": [ { "path": path.relative_to(BuildConstants.REPO_ROOT).as_posix(), - "sha256": sha256_file(path), + "sha256": normalized_sha256_file(path), } for path in _initramfs_source_files() ], @@ -600,53 +610,6 @@ def _install(source: Path, destination: Path) -> dict[str, str]: } -def _build_static_helper( - work: Path, - source: Path, - destination: Path, - cflags: tuple[str, ...] = InitramfsBuildConstants.STATIC_HELPER_CFLAGS, - compiler_name: str = "cc", -) -> dict[str, str]: - compiler = require_tool(compiler_name) - output = work / source.stem - run_checked( - [ - compiler, - *cflags, - "-o", - output, - source, - ] - ) - shutil.copyfile(output, destination) - destination.chmod(0o755) - return { - "source_sha256": sha256_file(source), - "binary_sha256": sha256_file(output), - } - - -def _build_device_io_helper(work: Path, destination: Path) -> dict[str, str]: - compiler = require_tool("cc") - source = BuildConstants.REPO_ROOT / "guest" / "common" / "nvx-device-io.c" - output = work / "nvx-device-io" - run_checked( - [ - compiler, - *InitramfsBuildConstants.DEVICE_IO_CFLAGS, - "-o", - output, - source, - ] - ) - shutil.copyfile(output, destination) - destination.chmod(0o755) - return { - "source_sha256": sha256_file(source), - "binary_sha256": sha256_file(output), - } - - def _apk_add(root: Path, *packages: str) -> None: loader = root / "lib" / "ld-musl-x86_64.so.1" environment = os.environ.copy() @@ -863,31 +826,25 @@ def _install_guest_files( if name == "nvx-bashrc": destination.chmod(0o644) - for name in InitramfsBuildConstants.STATIC_HELPERS: - helpers[name] = _build_static_helper( - config.work, - common / f"{name}.c", - root / "sbin" / name, - ) - for name in InitramfsBuildConstants.MUSL_STATIC_HELPERS: - helpers[name] = _build_static_helper( - config.work, - common / f"{name}.c", - root / "sbin" / name, - compiler_name=InitramfsBuildConstants.MUSL_COMPILER, - ) - probe = InitramfsBuildConstants.TIME_PROBE_NAME - helpers[probe] = _build_static_helper( - config.work, - common / f"{probe}.c", - root / "sbin" / probe, - InitramfsBuildConstants.TIME_PROBE_CFLAGS, - compiler_name=InitramfsBuildConstants.MUSL_COMPILER, - ) - helpers["nvx-device-io"] = _build_device_io_helper( - config.work, - root / "sbin" / "nvx-device-io", - ) + prebuilt = os.environ.get(GuestRustBuildConstants.BINARIES_VARIABLE) + if prebuilt: + binary_directory = Path(prebuilt) + result = inspect_guest_binaries(binary_directory) + else: + binary_directory = config.work / GuestRustBuildConstants.OUTPUT_DIRECTORY_NAME + result = build_guest_binaries(binary_directory) + for name in GuestRustBuildConstants.BINARIES: + source = binary_directory / name + destination = root / "sbin" / name + shutil.copyfile(source, destination) + destination.chmod(0o755) + binary_sha256 = sha256_file(destination) + if binary_sha256 != result.binaries[name]: + raise ScriptError(f"installed Rust guest binary changed: {name}") + helpers[name] = { + "source_sha256": result.source_sha256, + "binary_sha256": binary_sha256, + } return helpers @@ -942,7 +899,7 @@ def _guest_customization_files(descriptor: GuestDescriptor) -> tuple[Path, ...]: ) if path.is_file() ) - return (*common, *alpine) + return (*common, *alpine, *guest_binary_source_files()) def build_initramfs(config: InitramfsBuildConfig) -> None: diff --git a/scripts/nvx_tools/build_constants.py b/scripts/nvx_tools/build_constants.py index 13841b9cb..3419e31ba 100644 --- a/scripts/nvx_tools/build_constants.py +++ b/scripts/nvx_tools/build_constants.py @@ -176,6 +176,8 @@ class GuestRustBuildConstants: BUILD_MANIFEST_NAME: Final = "manifest.json" BUILD_MANIFEST_FORMAT: Final = 1 BUILD_MANIFEST_DOMAIN: Final = "nvx-rust-guest-binary-build" + OUTPUT_DIRECTORY_NAME: Final = "guest-binaries" + SOURCE_PATHS: Final = ("nvx-rs", "rust-toolchain.toml") BINARIES: Final = ( "nvx-console-pending", "nvx-mmio-write", @@ -358,53 +360,14 @@ class InitramfsBuildConstants: TIMESTAMP: Final = 0 CPIO_FORMAT: Final = "newc" GZIP_COMPRESSION_LEVEL: Final = 9 - STATIC_HELPER_CFLAGS: Final = ( - "-static", - "-Os", - "-s", - "-Wall", - "-Wextra", - "-Werror", - ) - DEVICE_IO_CFLAGS: Final = ( - "-nostdlib", - "-static", - "-Os", - "-fno-builtin", - "-fno-pie", - "-fno-stack-protector", - "-no-pie", - "-Wl,--build-id=none", - "-Wl,-z,noexecstack", - "-s", - ) - STATIC_HELPERS: Final = ( - "nvx-reseed", - "nvx-mmio-write", - "nvx-console-pending", - "nvx-managed-agent", - ) - # Time tools, nvx-exit, and nvx-ingress are linked against musl: a static - # glibc binary adds about 700 KB to the initramfs, and every unpacked - # kilobyte delays cold boot. nvx-exit is also a restored guest's usual - # first exec. - MUSL_COMPILER: Final = "musl-gcc" - MUSL_STATIC_HELPERS: Final = ("nvx-time", "nvx-exit", "nvx-ingress") - TIME_PROBE_NAME: Final = "nvx-time-probe" - TIME_PROBE_CFLAGS: Final = ( - "-std=gnu11", - "-static", - "-O2", - "-pthread", - "-s", - "-Wall", - "-Wextra", - "-Werror", - ) class DockerBuildConstants: DOCKERFILE: Final = Path("docker") / "Dockerfile" + RUST_BUILDER_IMAGE: Final = ( + f"rust:{GuestRustBuildConstants.TOOLCHAIN}-slim-bookworm@" + "sha256:d7482085ff5b415f84dba5647ae71606650bdef00db7aeb69f4b3d170c3e4082" + ) ALPINE_TARGET: Final = "artifacts" UBUNTU_TARGET: Final = "ubuntu-guest-artifacts" AZURELINUX_TARGET: Final = "azurelinux-guest-artifacts" @@ -450,6 +413,7 @@ class ReleaseBuildConstants: PROJECT_SOURCE_PATHS: Final = ( ".github/agents/nvx-adversary.md", "guest", + "nvx-rs", "ubuntu", "azurelinux", "data/linux-kvm-virtual-machine.csv", @@ -471,6 +435,7 @@ class ReleaseBuildConstants: "requirements-dev.txt", "rust-toolchain.toml", ) + PROJECT_SOURCE_EXCLUDES: Final = (Path("nvx-rs") / "target",) GUEST_ARTIFACT_NAMES: Final = ( KernelBuildConstants.BINARY_NAME, KernelBuildConstants.CONFIG_NAME, diff --git a/scripts/nvx_tools/ci.py b/scripts/nvx_tools/ci.py index c354a6cf1..0b89a29e3 100644 --- a/scripts/nvx_tools/ci.py +++ b/scripts/nvx_tools/ci.py @@ -131,13 +131,14 @@ # The OpenVMM VMM tests also boot the NVX kernel and Alpine initramfs, so they # add the inputs of those artifacts' cache keys and of the job that builds them. OPENVMM_VMM_TEST_INPUT_PATHS = re.compile( - r"^(openvmm($|/)|rust-toolchain\.toml$|scripts/setup/|" + r"^(openvmm($|/)|nvx-rs/|rust-toolchain\.toml$|scripts/setup/|" r"\.github/workflows/ci\.yml$|" r"\.github/actions/(build-guest-artifacts|checkout-openvmm|sccache|" r"setup-curl|validate-runner)/|" r"scripts/nvx\.py$|" r"scripts/nvx_tools/" - r"(__init__|build|build_config|build_constants|ci|common|guests)\.py$|" + r"(__init__|build|build_config|build_constants|ci|common|elf|" + r"guest_binaries|guests)\.py$|" r"SOURCE-MANIFEST\.json$|docker/Dockerfile$|\.dockerignore$|kernel/|guest/(alpine|common)/)" ) diff --git a/scripts/nvx_tools/control_session.py b/scripts/nvx_tools/control_session.py deleted file mode 100644 index d3d25052c..000000000 --- a/scripts/nvx_tools/control_session.py +++ /dev/null @@ -1,693 +0,0 @@ -"""Authenticated control-session client for managed NVX microVM workloads.""" - -from __future__ import annotations - -import contextlib -import errno -import math -import os -import secrets -import socket -import struct -import time -from collections.abc import Generator -from dataclasses import dataclass -from pathlib import Path -from typing import Any, cast - -from .common import ScriptError, remaining_timeout - -OUTER_HEADER = struct.Struct("<4sHBB16sQQI") -APP_HEADER = struct.Struct("<4sBBHQiI") -OUTER_MAX_PAYLOAD = 65_536 -APP_MAX_ARGUMENTS = 64 -APP_MAX_ARGUMENT_BYTES = 4096 -APP_MAX_ENVIRONMENT = 256 -APP_MAX_ENVIRONMENT_BYTES = 4096 -APP_MAX_CWD_BYTES = 4096 -APP_EXEC_EXTENDED = 1 -APP_EXEC_CWD_PRESENT = 1 -APP_EXEC_ENVIRONMENT_PRESENT = 2 -APP_EXEC_INHERIT_DEFAULT_ENV = 4 - -OUTER_HOST_ATTACH = 2 -OUTER_RESET = 3 -OUTER_DATA = 5 -OUTER_WAIT = 6 -OUTER_READY = 7 -OUTER_ERROR = 8 - -APP_PING = 1 -APP_EXEC = 2 -APP_STOP = 3 -APP_READY = 0x81 -APP_STDOUT = 0x82 -APP_STDERR = 0x83 -APP_EXIT = 0x84 -APP_STOPPED = 0x85 -APP_ERROR = 0xFF -MANAGED_EXIT_CATEGORIES = frozenset( - {"exit", "timeout", "output-limit", "signal", "failed"} -) - - -def encode_exec_environment(environment: tuple[str, ...]) -> tuple[bytes, ...]: - if len(environment) > APP_MAX_ENVIRONMENT: - raise ValueError("managed exec environment exceeds 256 entries") - names: set[str] = set() - encoded: list[bytes] = [] - for entry in environment: - if type(entry) is not str: - raise TypeError("managed exec environment entries must be strings") - name, separator, _value = entry.partition("=") - value = entry.encode("utf-8") - if ( - not separator - or not name - or "\0" in entry - or len(value) > APP_MAX_ENVIRONMENT_BYTES - ): - raise ValueError( - "managed exec environment entries must be non-empty " - "KEY=VALUE strings of at most 4096 bytes" - ) - if name in names: - raise ValueError(f"managed exec environment contains duplicate key: {name}") - names.add(name) - encoded.append(value) - return tuple(encoded) - - -def capability_pipe(capability: bytes) -> int: - """Returns the read end of a pipe that holds `capability` and then end of file. - - OpenVMM reads `--microvm-control-auth-stdin` without blocking as soon as it - starts, and fails unless the whole capability and the end of file are already - there, so a launcher passes this descriptor as OpenVMM's standard input. The - caller closes it once OpenVMM has started. - """ - if len(capability) != 32 or capability == bytes(32): - raise ValueError("control capability must be 32 nonzero bytes") - read, write = os.pipe() - try: - # The capability fits in any pipe's buffer, so these writes cannot block. - remaining = memoryview(capability) - while remaining: - remaining = remaining[os.write(write, remaining) :] - except BaseException: - os.close(read) - raise - finally: - os.close(write) - return read - - -@dataclass(frozen=True) -class ManagedExecResult: - returncode: int - category: str - stdout: bytes - stderr: bytes - - -class ManagedExecRefused(ScriptError): - """The managed guest refused an exec request instead of reporting an exit. - - It keeps the refusal's status and category, and any output that the guest - sent first, such as the diagnostic of a ``cwd-failed`` refusal. - """ - - def __init__( - self, status: int, category: str, stdout: bytes, stderr: bytes - ) -> None: - super().__init__( - f"managed guest rejected exec (status={status}, category={category})" - ) - self.status = status - self.category = category - self.stdout = stdout - self.stderr = stderr - - -class ControlEndpointClosed(ConnectionError): - """OpenVMM closed the managed control endpoint, as it does when it shuts down. - - A session reset, which comes from an OpenVMM that still serves the endpoint, - is a plain ConnectionError instead. - """ - - def __init__(self, message: str = "managed control endpoint closed") -> None: - super().__init__(message) - - -# A peer that closes with data that it has not read resets the connection, and a -# write after the peer closed fails with a broken pipe. -_PEER_CLOSED = (BrokenPipeError, ConnectionResetError) - - -class _SocketStream: - def __init__(self, connection: socket.socket) -> None: - self._connection = connection - - @classmethod - def connect(cls, path: Path, timeout: float) -> _SocketStream: - deadline = time.monotonic() + timeout - family = getattr(socket, "AF_UNIX", None) - if family is None: - raise RuntimeError("Unix-domain sockets are unavailable") - while True: - connection = socket.socket(cast(int, family), socket.SOCK_STREAM) - try: - connection.settimeout(min(0.25, max(0.01, deadline - time.monotonic()))) - connection.connect(os.fspath(path)) - return cls(connection) - except OSError as error: - connection.close() - if time.monotonic() >= deadline: - raise TimeoutError( - f"managed control endpoint did not become available: {path}" - ) from error - time.sleep(0.025) - - def read_exact(self, length: int, deadline: float) -> bytes: - output = bytearray() - while len(output) != length: - remaining = remaining_timeout(deadline) - if remaining <= 0: - raise TimeoutError("managed control response timed out") - self._connection.settimeout(min(remaining, 0.25)) - try: - chunk = self._connection.recv(length - len(output)) - except TimeoutError: - continue - except _PEER_CLOSED as error: - raise ControlEndpointClosed() from error - if not chunk: - raise ControlEndpointClosed() - output.extend(chunk) - return bytes(output) - - def write_all(self, data: bytes, deadline: float) -> None: - unsent = memoryview(data) - while unsent: - remaining = remaining_timeout(deadline) - if remaining <= 0: - raise TimeoutError("managed control request timed out") - self._connection.settimeout(min(remaining, 0.25)) - try: - sent = self._connection.send(unsent) - except TimeoutError: - continue - except _PEER_CLOSED as error: - raise ControlEndpointClosed() from error - if not sent: - raise ControlEndpointClosed() - unsent = unsent[sent:] - - def close(self) -> None: - self._connection.close() - - -if os.name == "nt": - import _winapi - import ctypes - - _cancel_io_ex = ctypes.WinDLL("kernel32", use_last_error=True).CancelIoEx - _cancel_io_ex.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - _cancel_io_ex.restype = ctypes.c_int -else: - _winapi = cast(Any, None) - ctypes = cast(Any, None) - _cancel_io_ex = cast(Any, None) - -# Windows fails a transfer on a pipe that its server closed or disconnected with -# ERROR_BROKEN_PIPE, ERROR_NO_DATA, or ERROR_PIPE_NOT_CONNECTED. -_PIPE_CLOSED_ERRORS = frozenset((109, 232, 233)) -_ERROR_NOT_FOUND = 1168 - - -@contextlib.contextmanager -def _pipe_closure_reported() -> Generator[None]: - try: - yield - except OSError as error: - if getattr(error, "winerror", None) in _PIPE_CLOSED_ERRORS: - raise ControlEndpointClosed() from error - raise - - -def _signaled_before(event: int, deadline: float) -> bool: - while True: - remaining = remaining_timeout(deadline) - if remaining <= 0: - return False - try: - # On the main thread, Ctrl+C ends this wait too. - result = _winapi.WaitForMultipleObjects( - [event], False, math.ceil(min(remaining, 0.25) * 1000) - ) - except InterruptedError: - continue - if result == _winapi.WAIT_OBJECT_0: - return True - - -def _cancel_transfers(handle: int) -> None: - # Overlapped.cancel() marks a transfer as ended at once, so that releasing the - # transfer before GetOverlappedResult() reports its end would free a buffer - # that Windows may still use. CancelIoEx() leaves the transfer pending, and - # releasing a pending transfer waits for its end or leaks its buffer. - if not _cancel_io_ex(handle, None): - error = ctypes.get_last_error() - if error != _ERROR_NOT_FOUND: - raise ctypes.WinError(error) - - -def _finish_transfer(handle: int, transfer: Any, deadline: float, message: str) -> int: - """Returns how many bytes an overlapped pipe transfer moved. - - A transfer that is still pending at `deadline` is cancelled, and fails with a - TimeoutError unless it completed first. Windows uses the transfer's buffer - until the transfer ends, so this waits for that even when it fails. - """ - try: - completed = _signaled_before(transfer.event, deadline) - except BaseException: - with contextlib.suppress(OSError): - _cancel_transfers(handle) - transfer.GetOverlappedResult(True) - raise - if not completed: - _cancel_transfers(handle) - count, status = transfer.GetOverlappedResult(True) - if status == _winapi.ERROR_OPERATION_ABORTED: - raise TimeoutError(message) - return count - - -class _NamedPipeStream: - """A named-pipe client whose transfers end by their deadlines. - - A synchronous write blocks for as long as the endpoint does not drain the - pipe, so the client opens the pipe for overlapped I/O and cancels a transfer - that outlives its deadline. - """ - - def __init__(self, handle: int) -> None: - self._handle = handle - - @classmethod - def connect(cls, path: Path, timeout: float) -> _NamedPipeStream: - normalized = os.fspath(path).replace("/", "\\") - deadline = time.monotonic() + timeout - while True: - try: - return cls( - _winapi.CreateFile( - normalized, - _winapi.GENERIC_READ | _winapi.GENERIC_WRITE, - 0, - _winapi.NULL, - _winapi.OPEN_EXISTING, - _winapi.FILE_FLAG_OVERLAPPED, - _winapi.NULL, - ) - ) - except OSError as error: - if time.monotonic() >= deadline: - raise TimeoutError( - f"managed control endpoint did not become available: {path}" - ) from error - if error.errno not in ( - errno.ENOENT, - errno.EACCES, - errno.EBUSY, - errno.EAGAIN, - # The reconnect transition fails with ERROR_PIPE_BUSY, which - # Python reports as EINVAL. - errno.EINVAL, - ): - raise - time.sleep(0.025) - - def read_exact(self, length: int, deadline: float) -> bytes: - output = bytearray() - while len(output) != length: - if remaining_timeout(deadline) <= 0: - raise TimeoutError("managed control response timed out") - with _pipe_closure_reported(): - transfer, _ = _winapi.ReadFile( - self._handle, length - len(output), overlapped=True - ) - _finish_transfer( - self._handle, - transfer, - deadline, - "managed control response timed out", - ) - chunk = transfer.getbuffer() - if not chunk: - raise ControlEndpointClosed() - output.extend(chunk) - return bytes(output) - - def write_all(self, data: bytes, deadline: float) -> None: - unsent = memoryview(data) - while unsent: - if remaining_timeout(deadline) <= 0: - raise TimeoutError("managed control request timed out") - with _pipe_closure_reported(): - transfer, _ = _winapi.WriteFile(self._handle, unsent, overlapped=True) - count = _finish_transfer( - self._handle, - transfer, - deadline, - "managed control request timed out", - ) - if not count: - raise ControlEndpointClosed() - unsent = unsent[count:] - - def close(self) -> None: - _winapi.CloseHandle(self._handle) - - -class ControlSession: - def __init__(self, stream: _SocketStream | _NamedPipeStream) -> None: - self._stream = stream - self._closed = False - self._instance_id = bytes(16) - self._epoch = 0 - self._send_sequence = 0 - self._receive_sequence = 0 - - @classmethod - def connect( - cls, - endpoint: Path, - capability: bytes, - timeout: float, - ) -> ControlSession: - if len(capability) != 32 or capability == bytes(32): - raise ValueError("control capability must be 32 nonzero bytes") - stream = ( - _NamedPipeStream.connect(endpoint, timeout) - if os.name == "nt" - else _SocketStream.connect(endpoint, timeout) - ) - session = cls(stream) - try: - deadline = time.monotonic() + timeout - session._write_outer( - OUTER_HOST_ATTACH, - bytes(16), - 0, - 0, - capability, - deadline, - ) - while True: - record_type, instance_id, epoch, sequence, payload = ( - session._read_outer(deadline) - ) - if payload: - raise ScriptError( - "control attach response carried an invalid payload" - ) - if record_type == OUTER_WAIT: - continue - if record_type == OUTER_ERROR: - raise ScriptError("control capability authentication failed") - if record_type != OUTER_READY or instance_id == bytes(16) or epoch == 0: - raise ScriptError( - "control endpoint returned an invalid attach response" - ) - session._instance_id = instance_id - session._epoch = epoch - session._receive_sequence = sequence + 1 - return session - except BaseException: - session.close() - raise - - @contextlib.contextmanager - def _transferring_record(self) -> Generator[None]: - """Closes the session if sending or receiving a record fails. - - A failed transfer may leave a record partly sent or partly received, after - which later records would not frame correctly. - """ - if self._closed: - raise ValueError("managed control session is closed") - try: - yield - except BaseException: - self.close() - raise - - def _write_outer( - self, - record_type: int, - instance_id: bytes, - epoch: int, - sequence: int, - payload: bytes, - deadline: float, - ) -> None: - if len(payload) > OUTER_MAX_PAYLOAD: - raise ValueError("control payload exceeds the outer protocol limit") - header = OUTER_HEADER.pack( - b"NVXS", - 1, - record_type, - 0, - instance_id, - epoch, - sequence, - len(payload), - ) - with self._transferring_record(): - self._stream.write_all(header + payload, deadline) - - def _read_outer( - self, - deadline: float, - ) -> tuple[int, bytes, int, int, bytes]: - with self._transferring_record(): - header = self._stream.read_exact(OUTER_HEADER.size, deadline) - magic, version, record_type, flags, instance_id, epoch, sequence, length = ( - OUTER_HEADER.unpack(header) - ) - if ( - magic != b"NVXS" - or version != 1 - or flags != 0 - or length > OUTER_MAX_PAYLOAD - ): - raise ScriptError("control endpoint returned an invalid outer record") - payload = self._stream.read_exact(length, deadline) if length else b"" - return record_type, instance_id, epoch, sequence, payload - - def _send_app( - self, - kind: int, - request_id: int, - deadline: float, - payload: bytes = b"", - ) -> None: - frame = ( - APP_HEADER.pack( - b"NVXC", - 1, - kind, - 0, - request_id, - 0, - len(payload), - ) - + payload - ) - self._write_outer( - OUTER_DATA, - self._instance_id, - self._epoch, - self._send_sequence, - frame, - deadline, - ) - self._send_sequence += 1 - - def _read_app( - self, - deadline: float, - ) -> tuple[int, int, int, bytes]: - record_type, instance_id, epoch, sequence, frame = self._read_outer(deadline) - if record_type == OUTER_RESET: - raise ConnectionError("managed control session was reset") - if ( - record_type != OUTER_DATA - or instance_id != self._instance_id - or epoch != self._epoch - or sequence != self._receive_sequence - or len(frame) < APP_HEADER.size - ): - raise ScriptError("control endpoint returned an invalid data record") - self._receive_sequence += 1 - magic, version, kind, flags, request_id, status, length = APP_HEADER.unpack( - frame[: APP_HEADER.size] - ) - payload = frame[APP_HEADER.size :] - if magic != b"NVXC" or version != 1 or flags != 0 or length != len(payload): - raise ScriptError("control endpoint returned an invalid application frame") - return kind, request_id, status, payload - - @staticmethod - def _request_id() -> int: - return secrets.randbits(64) or 1 - - def ping(self, timeout: float) -> None: - request_id = self._request_id() - deadline = time.monotonic() + timeout - self._send_app(APP_PING, request_id, deadline) - kind, response_id, status, payload = self._read_app(deadline) - if kind != APP_READY or response_id != request_id or status != 0 or payload: - raise ScriptError("managed guest did not acknowledge readiness") - - def exec( - self, - arguments: tuple[str, ...], - *, - timeout_ms: int, - response_timeout: float, - cwd: str | None = None, - environment: tuple[str, ...] | None = None, - inherit_default_environment: bool = False, - ) -> ManagedExecResult: - if not 0 < response_timeout < float("inf"): - raise ValueError( - "managed exec response timeout must be positive and finite" - ) - if not 1 <= len(arguments) <= APP_MAX_ARGUMENTS: - raise ValueError("managed exec requires 1 through 64 arguments") - encoded: list[bytes] = [] - for argument in arguments: - if type(argument) is not str: - raise TypeError("managed exec arguments must be strings") - value = argument.encode("utf-8") - if not value or len(value) > APP_MAX_ARGUMENT_BYTES or b"\0" in value: - raise ValueError("managed exec argument is empty or exceeds 4096 bytes") - encoded.append(struct.pack(" APP_MAX_CWD_BYTES - or b"\0" in encoded_cwd - or not cwd.startswith("/") - ): - raise ValueError( - "managed exec working directory must be an absolute path " - "of at most 4096 bytes" - ) - flags |= APP_EXEC_CWD_PRESENT - - if type(inherit_default_environment) is not bool: - raise TypeError("managed exec environment inheritance must be a boolean") - encoded_environment: list[bytes] = [] - if environment is not None: - for value in encode_exec_environment(environment): - encoded_environment.append(struct.pack(" OUTER_MAX_PAYLOAD: - raise ValueError("managed exec request exceeds the protocol limit") - - request_id = self._request_id() - # The response timeout bounds the whole exchange, so an endpoint that - # stops reading cannot hold up sending the request either. - deadline = time.monotonic() + response_timeout - self._send_app(APP_EXEC, request_id, deadline, payload) - stdout = bytearray() - stderr = bytearray() - while True: - kind, response_id, status, response = self._read_app(deadline) - if response_id != request_id: - raise ScriptError("managed guest returned a mismatched request ID") - if kind == APP_STDOUT: - stdout.extend(response) - elif kind == APP_STDERR: - stderr.extend(response) - elif kind == APP_EXIT: - try: - category = response.decode("ascii") - except UnicodeDecodeError as error: - raise ScriptError( - "managed guest returned an invalid exit category" - ) from error - if category not in MANAGED_EXIT_CATEGORIES: - raise ScriptError( - "managed guest returned an unsupported exit category" - ) - return ManagedExecResult(status, category, bytes(stdout), bytes(stderr)) - elif kind == APP_ERROR: - raise ManagedExecRefused( - status, - response.decode("ascii", "replace"), - bytes(stdout), - bytes(stderr), - ) - else: - raise ScriptError("managed guest returned an invalid exec response") - - def stop(self, timeout: float) -> None: - request_id = self._request_id() - deadline = time.monotonic() + timeout - self._send_app(APP_STOP, request_id, deadline) - kind, response_id, status, payload = self._read_app(deadline) - if kind != APP_STOPPED or response_id != request_id or status != 0 or payload: - raise ScriptError("managed guest did not acknowledge stop") - - def close(self) -> None: - if not self._closed: - self._closed = True - self._stream.close() - - def __enter__(self) -> ControlSession: - return self - - def __exit__(self, *_: object) -> None: - self.close() diff --git a/scripts/nvx_tools/egress_policy.py b/scripts/nvx_tools/egress_policy.py deleted file mode 100644 index 23d071203..000000000 --- a/scripts/nvx_tools/egress_policy.py +++ /dev/null @@ -1,610 +0,0 @@ -"""Compile structured IPv4 and IPv6 egress policies to the native OpenVMM rule -grammar.""" - -from __future__ import annotations - -import ipaddress -import json -from collections import Counter -from collections.abc import Iterable, Iterator -from dataclasses import dataclass -from pathlib import Path -from typing import cast - -from .common import ScriptError, strict_json_object - -MAX_RULES_PER_ACTION = 256 -MAX_POLICY_FILE_SIZE = 1024 * 1024 -_MAX_JSON_INTEGER_DIGITS = 64 -_ROOT_FIELDS = frozenset(("allow", "deny")) -_MXC_RULE_FIELDS = frozenset(("to", "ports")) -_PEER_FIELDS = frozenset(("cidr", "except")) -_PORT_FIELDS = frozenset(("protocol", "port", "endPort")) -_LEGACY_RULE_FIELDS = frozenset(("cidr", "except", "protocol", "port", "endPort")) -_RULE_PROTOCOLS = ("tcp", "udp", "icmp", "any") -# Protocols that a native rule can select on every port, and those with ports. -_NATIVE_PROTOCOLS = ("icmp", "tcp", "udp") -_PORT_PROTOCOLS = ("tcp", "udp") -_IP_VERSIONS = (4, 6) -_AddressInterval = tuple[int, int] -_AddressIntervals = tuple[_AddressInterval, ...] -_Network = ipaddress.IPv4Network | ipaddress.IPv6Network -_PortSelector = tuple[str | None, int | None, int | None] -# Each IP version with its addresses; a rule without destinations matches every -# address of both families. -_Destinations = tuple[tuple[int, _AddressIntervals], ...] -_ALL_ADDRESSES: _Destinations = ( - (4, ((0, (1 << 32) - 1),)), - (6, ((0, (1 << 128) - 1),)), -) -# An inclusive range of destination ports. -_PortRange = tuple[int, int] -# A native rule: a network, and optionally a protocol and its port range. -_NativeRule = tuple[_Network, str | None, _PortRange | None] - - -@dataclass(frozen=True) -class CompiledEgressPolicy: - allow: tuple[str, ...] - deny: tuple[str, ...] - - -@dataclass(frozen=True) -class _Rule: - # IP version of the addresses, each an interval of integers in its family. - version: int - addresses: _AddressIntervals - # None matches every protocol. - protocol: str | None - # None matches every port of the protocol. - start_port: int | None - end_port: int | None - - -def _object(value: object, description: str) -> dict[str, object]: - if not isinstance(value, dict): - raise ScriptError(f"{description} must be an object") - mapping = cast(dict[object, object], value) - if not all(isinstance(key, str) for key in mapping): - raise ScriptError(f"{description} fields must be strings") - return cast(dict[str, object], value) - - -def _array(value: object, description: str) -> list[object]: - if not isinstance(value, list): - raise ScriptError(f"{description} must be an array") - return cast(list[object], value) - - -def _network(value: object, description: str) -> _Network: - if not isinstance(value, str): - raise ScriptError(f"{description} must be an IPv4 or IPv6 CIDR string") - try: - # Clearing host bits rebuilds the network address without its IPv6 - # scope, so parse an interface, which keeps the address as written. - interface = ipaddress.ip_interface(value) - except ValueError as error: - raise ScriptError( - f"{description} is not a valid IPv4 or IPv6 CIDR: {value}" - ) from error - if isinstance(interface, ipaddress.IPv6Interface) and interface.scope_id: - raise ScriptError(f"{description} must not name an IPv6 scope: {value}") - return interface.network - - -def _contains(parent: _Network, child: _Network) -> bool: - if isinstance(parent, ipaddress.IPv4Network) and isinstance( - child, ipaddress.IPv4Network - ): - return child.subnet_of(parent) - if isinstance(parent, ipaddress.IPv6Network) and isinstance( - child, ipaddress.IPv6Network - ): - return child.subnet_of(parent) - return False - - -def _summarize(start: int, end: int, version: int) -> Iterator[_Network]: - if version == 4: - return ipaddress.summarize_address_range( - ipaddress.IPv4Address(start), ipaddress.IPv4Address(end) - ) - return ipaddress.summarize_address_range( - ipaddress.IPv6Address(start), ipaddress.IPv6Address(end) - ) - - -def _port(value: object, description: str) -> int: - if isinstance(value, bool) or not isinstance(value, int): - raise ScriptError(f"{description} must be an integer") - if not 1 <= value <= 65535: - raise ScriptError(f"{description} must be between 1 and 65535") - return value - - -def _bounded_json_integer(value: str) -> int: - if len(value.removeprefix("-")) > _MAX_JSON_INTEGER_DIGITS: - raise ScriptError( - f"JSON integer exceeds {_MAX_JSON_INTEGER_DIGITS}-digit limit" - ) - return int(value) - - -def _merge_intervals(intervals: Iterable[_AddressInterval]) -> _AddressIntervals: - merged: list[_AddressInterval] = [] - for start, end in sorted(intervals): - if merged and start <= merged[-1][1] + 1: - previous_start, previous_end = merged[-1] - merged[-1] = (previous_start, max(previous_end, end)) - else: - merged.append((start, end)) - return tuple(merged) - - -def _subtract_intervals( - sources: _AddressIntervals, - exclusions: _AddressIntervals, -) -> _AddressIntervals: - remaining: list[_AddressInterval] = [] - exclusion_index = 0 - for source_start, source_end in sources: - while ( - exclusion_index < len(exclusions) - and exclusions[exclusion_index][1] < source_start - ): - exclusion_index += 1 - - cursor = source_start - current_index = exclusion_index - while ( - current_index < len(exclusions) - and exclusions[current_index][0] <= source_end - ): - excluded_start, excluded_end = exclusions[current_index] - if cursor < excluded_start: - remaining.append((cursor, excluded_start - 1)) - cursor = max(cursor, excluded_end + 1) - if cursor > source_end: - break - current_index += 1 - if cursor <= source_end: - remaining.append((cursor, source_end)) - return tuple(remaining) - - -def _subtract_exclusions( - parent: _Network, - exclusions: list[object], - description: str, -) -> _AddressIntervals: - parsed: list[_AddressInterval] = [] - for index, value in enumerate(exclusions): - exclusion = _network(value, f"{description}.except[{index}]") - if not _contains(parent, exclusion): - raise ScriptError( - f"{description}.except[{index}] must be contained in {parent}" - ) - parsed.append( - (int(exclusion.network_address), int(exclusion.broadcast_address)) - ) - - parent_interval = ( - int(parent.network_address), - int(parent.broadcast_address), - ) - return _subtract_intervals((parent_interval,), _merge_intervals(parsed)) - - -def _parse_peer(value: object, description: str) -> tuple[int, _AddressIntervals]: - """Return a destination's IP version and the addresses that it selects.""" - peer = _object(value, description) - unknown = sorted(set(peer) - _PEER_FIELDS) - if unknown: - raise ScriptError(f"{description} has unknown field '{unknown[0]}'") - if "cidr" not in peer: - raise ScriptError(f"{description}.cidr is required") - parent = _network(peer["cidr"], f"{description}.cidr") - exclusions = _array(peer.get("except", []), f"{description}.except") - return parent.version, _subtract_exclusions(parent, exclusions, description) - - -def _parse_legacy_rule(rule: dict[str, object], description: str) -> tuple[_Rule, ...]: - unknown = sorted(set(rule) - _LEGACY_RULE_FIELDS) - if unknown: - raise ScriptError(f"{description} has unknown field '{unknown[0]}'") - if "cidr" not in rule: - raise ScriptError(f"{description}.cidr is required") - version, addresses = _parse_peer( - {field: rule[field] for field in ("cidr", "except") if field in rule}, - description, - ) - if "endPort" in rule and "port" not in rule: - raise ScriptError(f"{description}.endPort requires port") - if "protocol" not in rule: - if "port" in rule: - raise ScriptError(f"{description}.port requires protocol") - return (_Rule(version, addresses, None, None, None),) - protocol = rule["protocol"] - if not isinstance(protocol, str) or protocol not in _RULE_PROTOCOLS: - raise ScriptError(f"{description}.protocol must be tcp, udp, icmp, or any") - if "port" not in rule: - return ( - _Rule( - version, - addresses, - None if protocol == "any" else protocol, - None, - None, - ), - ) - if protocol == "icmp": - raise ScriptError(f"{description}.port is not supported with icmp") - start = _port(rule["port"], f"{description}.port") - end = _port(rule.get("endPort", start), f"{description}.endPort") - if end < start: - raise ScriptError(f"{description}.endPort {end} cannot be below port {start}") - protocols = _PORT_PROTOCOLS if protocol == "any" else (protocol,) - return tuple( - _Rule(version, addresses, selected, start, end) for selected in protocols - ) - - -def _parse_mxc_port(value: object, description: str) -> tuple[_PortSelector, ...]: - port = _object(value, description) - unknown = sorted(set(port) - _PORT_FIELDS) - if unknown: - raise ScriptError(f"{description} has unknown field '{unknown[0]}'") - protocol = port.get("protocol", "any") - if not isinstance(protocol, str) or protocol not in _RULE_PROTOCOLS: - raise ScriptError(f"{description}.protocol must be tcp, udp, icmp, or any") - if "port" not in port: - if "endPort" in port: - raise ScriptError(f"{description}.endPort requires port") - return ( - ( - None if protocol == "any" else protocol, - None, - None, - ), - ) - if protocol == "icmp": - raise ScriptError(f"{description}.port is not supported with icmp") - start = _port(port["port"], f"{description}.port") - end = _port(port.get("endPort", start), f"{description}.endPort") - if end < start: - raise ScriptError(f"{description}.endPort {end} cannot be below port {start}") - protocols = _PORT_PROTOCOLS if protocol == "any" else (protocol,) - return tuple((selected, start, end) for selected in protocols) - - -def _parse_mxc_rule( - rule: dict[str, object], - description: str, -) -> tuple[_Rule, ...]: - unknown = sorted(set(rule) - _MXC_RULE_FIELDS) - if unknown: - raise ScriptError(f"{description} has unknown field '{unknown[0]}'") - - destinations = _ALL_ADDRESSES - if "to" in rule: - peers = _array(rule["to"], f"{description}.to") - if not peers: - raise ScriptError(f"{description}.to must contain at least one destination") - parsed = [ - _parse_peer(peer, f"{description}.to[{index}]") - for index, peer in enumerate(peers) - ] - # A native rule names one family, so the peers of each family form - # their own destination set. - destinations = tuple( - ( - version, - _merge_intervals( - interval - for peer_version, intervals in parsed - if peer_version == version - for interval in intervals - ), - ) - for version in _IP_VERSIONS - if any(peer_version == version for peer_version, _ in parsed) - ) - - selectors: list[_PortSelector] = [(None, None, None)] - if "ports" in rule: - ports = _array(rule["ports"], f"{description}.ports") - if not ports: - raise ScriptError(f"{description}.ports must contain at least one selector") - selectors = [ - selector - for index, port in enumerate(ports) - for selector in _parse_mxc_port(port, f"{description}.ports[{index}]") - ] - return tuple( - _Rule(version, addresses, protocol, start_port, end_port) - for version, addresses in destinations - for protocol, start_port, end_port in selectors - ) - - -def _parse_rule(value: object, description: str) -> tuple[_Rule, ...]: - rule = _object(value, description) - legacy = set(rule) & _LEGACY_RULE_FIELDS - mxc = set(rule) & _MXC_RULE_FIELDS - if legacy: - if mxc: - raise ScriptError( - f"{description} cannot mix MXC to/ports fields with legacy flat fields" - ) - return _parse_legacy_rule(rule, description) - return _parse_mxc_rule(rule, description) - - -def _intervals_to_networks( - intervals: _AddressIntervals, - version: int, - maximum: int, - category: str, -) -> tuple[_Network, ...]: - networks: list[_Network] = [] - for start, end in intervals: - summarized = _summarize(start, end, version) - for network in summarized: - if len(networks) >= maximum: - raise ScriptError( - f"{category} emits at most {MAX_RULES_PER_ACTION} native rules" - ) - networks.append(network) - return tuple(networks) - - -def _protocol_intervals_to_networks( - protocol_intervals: _AddressIntervals, - covered: _AddressIntervals, - version: int, - maximum: int, - category: str, -) -> tuple[_Network, ...]: - # Rules over `covered` already match this selector, so widening prefixes - # into it keeps the output compact without changing what matches. - combined = _merge_intervals((*protocol_intervals, *covered)) - networks: list[_Network] = [] - for start, end in combined: - summarized = _summarize(start, end, version) - for network in summarized: - network_interval = ( - int(network.network_address), - int(network.broadcast_address), - ) - if not _subtract_intervals((network_interval,), covered): - continue - if len(networks) >= maximum: - raise ScriptError( - f"{category} emits at most {MAX_RULES_PER_ACTION} native rules" - ) - networks.append(network) - return tuple(networks) - - -def _lower_protocol_rules( - rules: list[_Rule], - protocol: str, - version: int, - category: str, - covered: _AddressIntervals, - remaining_budget: int, -) -> list[_NativeRule]: - events: dict[int, list[tuple[int, _AddressIntervals]]] = {} - for rule in rules: - if ( - rule.protocol != protocol - or rule.start_port is None - or rule.end_port is None - or not rule.addresses - ): - continue - uncovered_addresses = _subtract_intervals(rule.addresses, covered) - if not uncovered_addresses: - continue - events.setdefault(rule.start_port, []).append((1, rule.addresses)) - events.setdefault(rule.end_port + 1, []).append((-1, rule.addresses)) - - active: Counter[_AddressIntervals] = Counter() - port_ranges: dict[_Network, list[_PortRange]] = {} - # Networks whose last range ends just before the current port segment. - open_networks: list[_Network] = [] - rule_count = 0 - previous_port: int | None = None - for port in sorted(events): - if previous_port is not None and not active: - open_networks = [] - if previous_port is not None and previous_port < port and active: - addresses = _merge_intervals( - interval for intervals in active for interval in intervals - ) - available = _merge_intervals((*addresses, *covered)) - kept: list[_Network] = [] - for network in open_networks: - interval = ( - int(network.network_address), - int(network.broadcast_address), - ) - if not _subtract_intervals((interval,), available): - ranges = port_ranges[network] - ranges[-1] = (ranges[-1][0], port - 1) - kept.append(network) - kept_cover = _merge_intervals( - ( - *covered, - *( - ( - int(network.network_address), - int(network.broadcast_address), - ) - for network in kept - ), - ) - ) - networks = _protocol_intervals_to_networks( - addresses, - kept_cover, - version, - remaining_budget - rule_count, - category, - ) - for network in networks: - if rule_count >= remaining_budget: - raise ScriptError( - f"{category} emits at most {MAX_RULES_PER_ACTION} native rules" - ) - port_ranges.setdefault(network, []).append((previous_port, port - 1)) - rule_count += 1 - kept.append(network) - open_networks = kept - for direction, addresses in events[port]: - active[addresses] += direction - if active[addresses] == 0: - del active[addresses] - previous_port = port - return [ - (network, protocol, port_range) - for network, ranges in port_ranges.items() - for port_range in ranges - ] - - -def _native_rule( - network: _Network, protocol: str | None, ports: _PortRange | None -) -> str: - if protocol is None: - return str(network) - if ports is None: - return f"{network}:{protocol}" - start, end = ports - if start == end: - return f"{network}:{protocol}:{start}" - return f"{network}:{protocol}:{start}-{end}" - - -def _compile_family( - rules: list[_Rule], - version: int, - category: str, - budget: int, -) -> list[_NativeRule]: - address_only = _merge_intervals( - interval - for rule in rules - if rule.protocol is None - for interval in rule.addresses - ) - address_only_networks = _intervals_to_networks( - address_only, - version, - budget, - category, - ) - lowered: list[_NativeRule] = [ - (network, None, None) for network in address_only_networks - ] - # Addresses that already match every port of each protocol. - covered = dict.fromkeys(_NATIVE_PROTOCOLS, address_only) - for protocol in _NATIVE_PROTOCOLS: - protocol_wide = _merge_intervals( - interval - for rule in rules - if rule.protocol == protocol and rule.start_port is None - for interval in rule.addresses - ) - if not protocol_wide: - continue - lowered.extend( - (network, protocol, None) - for network in _protocol_intervals_to_networks( - protocol_wide, - address_only, - version, - budget - len(lowered), - category, - ) - ) - covered[protocol] = _merge_intervals((*address_only, *protocol_wide)) - for protocol in _PORT_PROTOCOLS: - lowered.extend( - _lower_protocol_rules( - rules, - protocol, - version, - category, - covered[protocol], - budget - len(lowered), - ) - ) - return lowered - - -def _compile_category(value: object, category: str) -> tuple[str, ...]: - values = _array(value, category) - rules = [ - rule - for index, item in enumerate(values) - for rule in _parse_rule(item, f"{category}[{index}]") - ] - # A native rule matches only its own address family, so each family is - # lowered on its own, within the action's shared rule limit. - lowered: list[_NativeRule] = [] - for version in _IP_VERSIONS: - family = [rule for rule in rules if rule.version == version] - if family: - lowered.extend( - _compile_family( - family, - version, - category, - MAX_RULES_PER_ACTION - len(lowered), - ) - ) - lowered.sort( - key=lambda item: ( - item[0].version, - int(item[0].network_address), - item[0].prefixlen, - "" if item[1] is None else item[1], - (0, 0) if item[2] is None else item[2], - ) - ) - return tuple( - _native_rule(network, protocol, ports) for network, protocol, ports in lowered - ) - - -def compile_policy(value: object) -> CompiledEgressPolicy: - root = _object(value, "egress policy") - unknown = sorted(set(root) - _ROOT_FIELDS) - if unknown: - raise ScriptError(f"egress policy has unknown field '{unknown[0]}'") - return CompiledEgressPolicy( - allow=_compile_category(root.get("allow", []), "allow"), - deny=_compile_category(root.get("deny", []), "deny"), - ) - - -def compile_policy_file(path: Path) -> CompiledEgressPolicy: - try: - with path.open("rb") as stream: - data = stream.read(MAX_POLICY_FILE_SIZE + 1) - except OSError as error: - raise ScriptError(f"failed to read egress policy file: {path}") from error - if len(data) > MAX_POLICY_FILE_SIZE: - raise ScriptError( - f"egress policy file exceeds {MAX_POLICY_FILE_SIZE}-byte limit: {path}" - ) - try: - value = json.loads( - data.decode("utf-8"), - object_pairs_hook=strict_json_object, - parse_int=_bounded_json_integer, - ) - except (UnicodeDecodeError, ValueError) as error: - raise ScriptError(f"failed to read egress policy file: {path}") from error - return compile_policy(value) diff --git a/scripts/nvx_tools/guest_binaries.py b/scripts/nvx_tools/guest_binaries.py index b06c4919d..3b7c4fd1b 100644 --- a/scripts/nvx_tools/guest_binaries.py +++ b/scripts/nvx_tools/guest_binaries.py @@ -14,7 +14,14 @@ from typing import cast from .build_constants import BuildConstants, GuestRustBuildConstants -from .common import ScriptError, require_file, require_tool, run_checked, sha256_file +from .common import ( + ScriptError, + normalized_sha256_file, + require_file, + require_tool, + run_checked, + sha256_file, +) from .elf import check_static_executables @@ -26,58 +33,61 @@ class GuestBinaryBuildResult: binaries: dict[str, str] -def guest_binary_source_files() -> tuple[Path, ...]: +def guest_binary_source_files(root: Path | None = None) -> tuple[Path, ...]: """Return the complete ordered input set of the PR 1 guest executables.""" + root = Path(root or BuildConstants.REPO_ROOT) + crate = root / "nvx-rs" files = { - GuestRustBuildConstants.MANIFEST, - GuestRustBuildConstants.LOCKFILE, - GuestRustBuildConstants.BUILD_SCRIPT, - GuestRustBuildConstants.TOOLCHAIN_FILE, - *GuestRustBuildConstants.BUILD_RECIPE_FILES, + crate / "Cargo.toml", + crate / "Cargo.lock", + crate / "build.rs", + root / "rust-toolchain.toml", + root / "scripts" / "nvx_tools" / "build_constants.py", + root / "scripts" / "nvx_tools" / "guest_binaries.py", } - for directory in GuestRustBuildConstants.SOURCE_DIRECTORIES: - if not directory.is_dir(): - raise ScriptError(f"nvx-rs source directory not found: {directory}") - files.update( - path - for path in directory.rglob("*") - if path.is_file() and "target" not in path.relative_to(directory).parts - ) + source = crate / "src" + if not source.is_dir(): + raise ScriptError(f"nvx-rs source directory not found: {source}") + files.update( + path + for path in source.rglob("*") + if path.is_file() and "target" not in path.relative_to(source).parts + ) return tuple( sorted( (require_file(path, "Rust guest binary source input") for path in files), - key=lambda path: path.relative_to(BuildConstants.REPO_ROOT).as_posix(), + key=lambda path: path.relative_to(root).as_posix(), ) ) -def guest_binary_source_sha256() -> str: +def guest_binary_source_sha256(root: Path | None = None) -> str: """Hash the paths and contents that determine every Rust guest executable.""" + root = Path(root or BuildConstants.REPO_ROOT) inventory = { "domain": GuestRustBuildConstants.SOURCE_DIGEST_DOMAIN, "format": GuestRustBuildConstants.SOURCE_DIGEST_FORMAT, "files": [ { - "path": path.relative_to(BuildConstants.REPO_ROOT).as_posix(), - "sha256": sha256_file(path), + "path": path.relative_to(root).as_posix(), + "sha256": normalized_sha256_file(path), } - for path in guest_binary_source_files() + for path in guest_binary_source_files(root) ], } encoded = json.dumps(inventory, sort_keys=True, separators=(",", ":")).encode() return hashlib.sha256(encoded).hexdigest() -def pinned_guest_rust_toolchain() -> str: +def pinned_guest_rust_toolchain(root: Path | None = None) -> str: """Read and validate the repository's exact Rust release pin.""" - contents = require_file( - GuestRustBuildConstants.TOOLCHAIN_FILE, "Rust toolchain pin" - ).read_text(encoding="utf-8") + toolchain_file = (root or BuildConstants.REPO_ROOT) / "rust-toolchain.toml" + contents = require_file(toolchain_file, "Rust toolchain pin").read_text( + encoding="utf-8" + ) matches = re.findall(r'^channel = "([^"]+)"$', contents, re.MULTILINE) if len(matches) != 1: - raise ScriptError( - f"{GuestRustBuildConstants.TOOLCHAIN_FILE} must define one toolchain channel" - ) + raise ScriptError(f"{toolchain_file} must define one toolchain channel") [toolchain] = matches if toolchain != GuestRustBuildConstants.TOOLCHAIN: raise ScriptError( @@ -289,6 +299,44 @@ def check_guest_binaries(toolchain: str | None = None) -> None: ) +def inspect_guest_binaries(directory: Path) -> GuestBinaryBuildResult: + """Validate and identify a current, previously staged guest build.""" + directory = directory.resolve() + retired = _validate_owned_output(directory) + missing = [ + binary + for binary in GuestRustBuildConstants.BINARIES + if not (directory / binary).is_file() + ] + if retired or missing: + raise ScriptError( + f"guest binary output is not a current build of every executable: " + f"{directory}" + ) + paths = [directory / binary for binary in GuestRustBuildConstants.BINARIES] + check_static_executables(paths) + result = GuestBinaryBuildResult( + source_sha256=guest_binary_source_sha256(), + binaries={ + binary: sha256_file(path) + for binary, path in zip( + GuestRustBuildConstants.BINARIES, paths, strict=True + ) + }, + ) + manifest_path = directory / GuestRustBuildConstants.BUILD_MANIFEST_NAME + manifest = cast( + dict[str, object], + json.loads(manifest_path.read_text(encoding="utf-8")), + ) + if manifest != _manifest(result): + raise ScriptError( + "Rust guest binary build manifest does not match the current " + "sources, build pins, and executables" + ) + return result + + def build_guest_binaries(output: Path) -> GuestBinaryBuildResult: """Build, statically check, digest, and stage a fresh set of executables.""" if sys.platform != "linux": diff --git a/scripts/nvx_tools/microvm_test_scripts/sandbox-blocks.sh b/scripts/nvx_tools/microvm_test_scripts/sandbox-blocks.sh index 2ce10bc28..7f19c212f 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 @@ -14,15 +14,15 @@ for name in vda vdb vdc vdd; do done [ -b "/dev/$name" ] || fail 20 done +[ ! -b /dev/vdc ] || fail 30 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 +# The windows and interrupts of the removed runtime and custom layers stay unassigned. +! grep -q '0xd0004000' /proc/cmdline || fail 31 +! grep -q '0xd0005000' /proc/cmdline || fail 32 [ "$(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 28 +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..247c1d084 100644 --- a/scripts/nvx_tools/microvm_tests.py +++ b/scripts/nvx_tools/microvm_tests.py @@ -9,8 +9,9 @@ import queue import random import re -import secrets import selectors +import shlex +import shutil import socket import struct import subprocess @@ -26,6 +27,7 @@ from stat import S_ISUID from typing import Any, cast +from . import nvx_rs from .benchmark import ( CAPTURE_LOG_FILTER, RESTORE_MARKER, @@ -38,7 +40,6 @@ parse_snapshot_profile_line, positive_float, positive_int, - record_adversarial_openvmm_pid, smp_probe_script, snapshot_restore_command, workload_boot_command, @@ -61,10 +62,9 @@ require_file, sha256_file, ) -from .control_session import ControlSession, ManagedExecRefused, capability_pipe -from .egress_policy import CompiledEgressPolicy, compile_policy_file from .guests import GUEST_NAMES, GuestDescriptor, guest_descriptor from .managed_exec_tests import run_managed_exec_configuration +from .nvx_rs import EgressRules, compile_egress_policy from .openvmm_process import OpenvmmProcess, TcpConsole from .sandbox_lifecycle_tests import run_sandbox_lifecycle from .time_abi import ( @@ -80,7 +80,6 @@ TimeAbiMonitor, check_cpu_budget_us, check_warp_probe, - describe_exit_status, parse_cpu_profile_fallback, parse_fields, parse_marker, @@ -269,14 +268,6 @@ SCRATCH_FRESH_VALUE_PREFIX = b"NVX-SCRATCH-FRESH-VALUE-" SCRATCH_FRESH_VALUE_SUFFIX = b"-END" WORKLOAD_IDENTITY_MARKER = b"NVX-WORKLOAD-IDENTITY-OK uid=65534 gid=65534" -# A direct-mode guest refuses a working directory that the workload cannot -# enter with the Linux error number, which does not depend on the host. -MANAGED_REFUSED_CWDS = ( - ("missing", "/does-not-exist", 2), # ENOENT - ("file", "/etc/passwd", 20), # ENOTDIR - ("inaccessible", "/root", 13), # EACCES -) -MANAGED_REFUSED_CWD_MARKER = "/tmp/nvx-refused-cwd-ran" BOOT_MARKER = b"NVX-GUEST-BOOT-OK:" GUEST_BOOT_COMPLETION_MARKER = b"NVX-GUEST-BOOT-CHECK-OK" GUEST_IDENTITY_COMPLETION_MARKER = b"NVX-GUEST-IDENTITY-OK" @@ -857,24 +848,25 @@ def run_workload_identity( raise RuntimeError("root workload identity was not rejected before boot") -def _refused_managed_exec( - session: ControlSession, cwd: str, *, timeout: float -) -> ManagedExecRefused: - """Runs a workload in an unusable directory; returns the guest's refusal.""" - try: - result = session.exec( - ("/bin/touch", MANAGED_REFUSED_CWD_MARKER), - timeout_ms=5_000, - response_timeout=timeout, - cwd=cwd, - ) - except ManagedExecRefused as refusal: - return refusal - raise RuntimeError( - f"managed working directory {cwd} was not refused: " - f"returncode={result.returncode} category={result.category} " - f"stderr={result.stderr!r}" - ) +# The ignored nvx-rs test that drives the managed lifecycle of a guest whose +# init hands the boot to the managed agent: nvx-rs/tests/managed_lifecycle.rs. +MANAGED_LIFECYCLE_TEST = "managed_lifecycle" + + +def managed_lifecycle_test_command(cargo: str) -> list[str]: + """Return the Cargo command that runs MANAGED_LIFECYCLE_TEST.""" + return [ + cargo, + "test", + "--manifest-path", + os.fspath(nvx_rs.DIRECTORY / "Cargo.toml"), + "--locked", + "--test", + MANAGED_LIFECYCLE_TEST, + "--", + "--ignored", + "--nocapture", + ] def run_managed_lifecycle( @@ -887,20 +879,39 @@ def run_managed_lifecycle( timeout: float, output_dir: Path, ) -> None: + """Run the managed lifecycle of a guest whose init hands the boot to the + managed agent. + + The nvx-rs test MANAGED_LIFECYCLE_TEST starts OpenVMM with the command + built here and drives the guest's control session with nvx-rs, so that no + Python client of the control protocol remains. This checks what the test + leaves: OpenVMM's outcome report and the guest console. + """ + cargo = shutil.which("cargo") + if cargo is None: + raise ScriptError( + "the managed-lifecycle scenario runs the nvx-rs test " + f"{MANAGED_LIFECYCLE_TEST} with Cargo, which is not on PATH; " + "install Rust (https://rustup.rs)" + ) + # Cargo runs the test in the nvx-rs directory, so every path is absolute. + output_dir = output_dir.resolve() with tempfile.TemporaryDirectory(prefix="nvx-managed-lifecycle-") as temporary: - root = Path(temporary) + root = Path(temporary).resolve() endpoint_value = ( str(root / "control.sock"), f"//./pipe/openvmm-microvm-{uuid.uuid4().hex}", )[os.name == "nt"] - boot_console_address = _available_tcp_address() - capability = secrets.token_bytes(32) report_path = root / "managed-outcome.json" + # The test copies the guest console, which listens on TCP, to this log. + console_address = _available_tcp_address() + guest_log_path = output_dir / "managed-lifecycle-guest.log" + guest_log_path.unlink(missing_ok=True) command = workload_boot_command( - executable, + executable.resolve(), backend, - kernel, - initrd, + kernel.resolve(), + initrd.resolve(), memory_mib, "quiet loglevel=0", ) @@ -911,7 +922,7 @@ def run_managed_lifecycle( "--microvm-lifecycle", "managed", "--virtio-console", - f"listen=tcp:{boot_console_address[0]}:{boot_console_address[1]}", + f"listen=tcp:{console_address[0]}:{console_address[1]}", "--microvm-control-console", f"listen={endpoint_value}", "--microvm-control-auth-stdin", @@ -919,255 +930,96 @@ def run_managed_lifecycle( str(report_path), ) ) - log_path = output_dir / "managed-lifecycle.log" - guest_log_path = output_dir / "managed-lifecycle-guest.log" - process: subprocess.Popen[bytes] | None = None - boot_console: TcpConsole | None = None - with log_path.open("wb") as log: - try: - environment = os.environ.copy() - environment["OPENVMM_LOG"] = "off" - # OpenVMM reads its capability as soon as it starts. - capability_input = capability_pipe(capability) - try: - process = subprocess.Popen( - command, - stdin=capability_input, - stdout=log, - stderr=subprocess.STDOUT, - env=environment, - ) - finally: - os.close(capability_input) - record_adversarial_openvmm_pid(process.pid, environment) - # The guest console has no shell to query: init hands the boot - # to the managed agent, which serves the control console. The - # monitor still fails the run on a violation or failed check - # printed there, and a failed boot check powers the guest off - # with status 193, which fails the exit check below. - boot_console = TcpConsole.connect( - boot_console_address, timeout, monitor=TimeAbiMonitor(command) - ) - with ControlSession.connect( - Path(endpoint_value), capability, timeout - ) as session: - session.ping(timeout) - first = session.exec( - ( - "/bin/sh", - "-c", - "/bin/touch nvx-managed-state; " - 'printf \'%s|%s|%s\' "$PWD" "$EMPTY" "$COMPLEX"', - ), - timeout_ms=5_000, - response_timeout=timeout, - cwd="/tmp", - environment=( - "EMPTY=", - "COMPLEX=space = \N{SNOWMAN}", - ), - ) - if ( - first.returncode != 0 - or first.category != "exit" - or first.stdout != "/tmp||space = \N{SNOWMAN}".encode() - or first.stderr - ): - raise RuntimeError( - "first managed workload returned an invalid result" - ) - - with ControlSession.connect( - Path(endpoint_value), capability, timeout - ) as session: - empty_environment = session.exec( - ("/usr/bin/env",), - timeout_ms=5_000, - response_timeout=timeout, - environment=(), - ) - exact_environment = session.exec( - ("/usr/bin/env",), - timeout_ms=5_000, - response_timeout=timeout, - environment=("EMPTY=", "COMPLEX=space = \N{SNOWMAN}"), - ) - if ( - empty_environment.returncode != 0 - or empty_environment.stdout - or empty_environment.stderr - or exact_environment.returncode != 0 - or exact_environment.stderr - or exact_environment.stdout - != "EMPTY=\nCOMPLEX=space = \N{SNOWMAN}\n".encode() - ): - raise RuntimeError( - "managed exec did not preserve the exact exec environment" - ) - for workload_timeout in (0, 3_600_001, 86_400_000, 0xFFFFFFFF): - boundary = session.exec( - ("/bin/true",), - timeout_ms=workload_timeout, - response_timeout=timeout, - ) - if ( - boundary.returncode != 0 - or boundary.category != "exit" - or boundary.stdout - or boundary.stderr - ): - raise RuntimeError( - "managed exec rejected a valid uint32 timeout" - ) - second = session.exec( - ( - "/bin/sh", - "-c", - "pwd; test -f /tmp/nvx-managed-state; " - 'test "${COMPLEX-unset}" = unset; ' - "test ! -e /run/nvx/workload-machine-id; " - "printf second-exec", - ), - timeout_ms=5_000, - response_timeout=timeout, - ) - timed_out = session.exec( - ("/bin/sleep", "5"), - timeout_ms=100, - response_timeout=timeout, - ) - after_timeout = session.exec( - ("/bin/sh", "-c", "printf still-usable"), - timeout_ms=5_000, - response_timeout=timeout, - ) - refused_cwds = [ - ( - description, - cwd, - error, - _refused_managed_exec(session, cwd, timeout=timeout), - ) - for description, cwd, error in MANAGED_REFUSED_CWDS - ] - after_refusals = session.exec( - ( - "/bin/sh", - "-c", - f"test ! -e {MANAGED_REFUSED_CWD_MARKER} && " - "printf still-usable", - ), - timeout_ms=5_000, - response_timeout=timeout, - ) - session.stop(timeout) - if ( - second.returncode != 0 - or second.category != "exit" - or second.stdout != b"/\nsecond-exec" - or second.stderr - ): - raise RuntimeError( - "managed workload state did not survive across exec requests" - ) - if timed_out.returncode != 124 or timed_out.category != "timeout": - raise RuntimeError("managed workload timeout was not reported") - if ( - after_timeout.returncode != 0 - or after_timeout.category != "exit" - or after_timeout.stdout != b"still-usable" - or after_timeout.stderr - ): - raise RuntimeError( - "managed guest was not usable after a workload timeout" - ) - for description, cwd, error, refusal in refused_cwds: - if ( - refusal.category != "cwd-failed" - or refusal.status != error - or refusal.stdout - or f"cannot enter working directory {cwd}: ".encode() - not in refusal.stderr - ): - raise RuntimeError( - f"{description} managed working directory was not " - f"refused clearly: {refusal} stderr={refusal.stderr!r}" - ) - if ( - after_refusals.returncode != 0 - or after_refusals.category != "exit" - or after_refusals.stdout != b"still-usable" - or after_refusals.stderr - ): - raise RuntimeError( - "a refused managed workload ran, or the guest was not " - "usable after refusals" - ) - result = process.wait(timeout=timeout) - if result != 0: - reason = describe_exit_status(result) - raise RuntimeError( - f"managed OpenVMM process exited with status {result}" - + (f": {reason}; see {log_path}" if reason else "") - ) - report = _read_outcome_report(report_path) - _preserve_outcome_report( - output_dir / "managed-outcome.json", - report, - ) - if report["backend"] != backend or report["outcome"] != { - "operation": "managed", - "category": "success", - "status_code": 0, - }: - raise RuntimeError("managed lifecycle outcome report was invalid") - if report["network_policy"]["status"] != "not-requested": - raise RuntimeError( - "managed lifecycle reported an unexpected network policy" - ) - if not all(report["teardown"].values()): - raise RuntimeError("managed lifecycle reported incomplete teardown") - # Keep the guest log, then fail on anything the monitor found - # in the console's tail. - guest_log_path.write_bytes(boot_console.finish(check=False)) - boot_console.finish() - boot_console = None - finally: - if boot_console is not None: - guest_log_path.write_bytes(boot_console.finish(check=False)) - if process is not None and process.poll() is None: - process.terminate() - try: - process.wait(timeout=5) - except subprocess.TimeoutExpired: - process.kill() - process.wait(timeout=5) - - invalid = workload_boot_command( - executable, - backend, - kernel, - initrd, - memory_mib, - "quiet loglevel=0", - ) - invalid.extend( - ( - "--microvm-workload-identity", - "65534:65534", - "--microvm-lifecycle", - "managed", + environment = os.environ.copy() + # The test records OpenVMM's process ID for an adversarial campaign + # as the microVM scenarios do, but from the nvx-rs directory, so a + # relative journal resolves here. + journal = environment.get("NVX_ADVERSARIAL_OPENVMM_PID_JOURNAL") + if journal: + environment["NVX_ADVERSARIAL_OPENVMM_PID_JOURNAL"] = os.path.abspath( + journal + ) + environment.update( + { + "OPENVMM_LOG": "off", + "NVX_MANAGED_LIFECYCLE_COMMAND": json.dumps(command), + "NVX_MANAGED_LIFECYCLE_ENDPOINT": endpoint_value, + "NVX_MANAGED_LIFECYCLE_CONSOLE": ( + f"{console_address[0]}:{console_address[1]}" + ), + "NVX_MANAGED_LIFECYCLE_TIMEOUT": str(timeout), + "NVX_MANAGED_LIFECYCLE_OUTPUT_DIR": os.fspath(output_dir), + } + ) + test_command = managed_lifecycle_test_command(cargo) + print(f">> {shlex.join(test_command)}", flush=True) + result = subprocess.run( + test_command, + cwd=BuildConstants.REPO_ROOT, + env=environment, + check=False, + ) + # The guest console has no shell to query: init hands the boot to the + # managed agent, which serves the control console. The monitor still + # fails the run on a violation or failed check printed there, before + # the test's own failure, which it may explain. A failed boot check + # powers the guest off with status 193, which fails the test. + try: + guest_output = guest_log_path.read_bytes() + except FileNotFoundError: + guest_output = b"" + monitor = TimeAbiMonitor(command) + monitor.feed(guest_output) + monitor.finish() + if result.returncode != 0: + raise RuntimeError( + f"the managed lifecycle test {MANAGED_LIFECYCLE_TEST} failed with " + f"exit status {result.returncode}; see its output above and " + f"{output_dir / 'managed-lifecycle.log'}" ) + report = _read_outcome_report(report_path) + _preserve_outcome_report( + output_dir / "managed-outcome.json", + report, ) - with OpenvmmProcess( - invalid, - output_dir / "managed-lifecycle-invalid-transition.log", - ) as rejected: - result = rejected.wait(timeout) - if result.returncode == 0 or BOOT_MARKER in result.output: + if report["backend"] != backend or report["outcome"] != { + "operation": "managed", + "category": "success", + "status_code": 0, + }: + raise RuntimeError("managed lifecycle outcome report was invalid") + if report["network_policy"]["status"] != "not-requested": raise RuntimeError( - "managed lifecycle without a control endpoint was not rejected before boot" + "managed lifecycle reported an unexpected network policy" ) + if not all(report["teardown"].values()): + raise RuntimeError("managed lifecycle reported incomplete teardown") + + invalid = workload_boot_command( + executable, + backend, + kernel, + initrd, + memory_mib, + "quiet loglevel=0", + ) + invalid.extend( + ( + "--microvm-workload-identity", + "65534:65534", + "--microvm-lifecycle", + "managed", + ) + ) + with OpenvmmProcess( + invalid, + output_dir / "managed-lifecycle-invalid-transition.log", + ) as rejected: + result = rejected.wait(timeout) + if result.returncode == 0 or BOOT_MARKER in result.output: + raise RuntimeError( + "managed lifecycle without a control endpoint was not rejected before boot" + ) def run_structured_outcome( @@ -1901,7 +1753,7 @@ def _bounded_egress_policy( udp_ports: tuple[int, int, int], *, policy_file: Path | None = None, -) -> CompiledEgressPolicy: +) -> EgressRules: allow: list[dict[str, object]] = [] deny: list[dict[str, object]] = [] for protocol, (start, denied, end) in ( @@ -1942,9 +1794,9 @@ def _bounded_egress_policy( ) ) - def compile_file(path: Path) -> CompiledEgressPolicy: + def compile_file(path: Path) -> EgressRules: path.write_text(json.dumps({"allow": allow, "deny": deny}), encoding="utf-8") - return compile_policy_file(path) + return compile_egress_policy(path) if policy_file is not None: return compile_file(policy_file) @@ -2597,7 +2449,7 @@ def run_l3_l4_egress_port_ranges( ) # Each range must reach OpenVMM as one native rule, not as a rule # for each of its ports. - native = compile_policy_file(policy_path) + native = compile_egress_policy(policy_path) if not any( rule.endswith(f":{first}-{last}") for rule in (*native.allow, *native.deny) @@ -3027,7 +2879,7 @@ def run_ipv6_egress_policy( json.dumps({"allow": case.allow, "deny": case.deny}, indent=2) + "\n", encoding="utf-8", ) - native = compile_policy_file(policy_path) + native = compile_egress_policy(policy_path) command = [ sys.executable, str(Path(__file__).parents[1] / "nvx.py"), @@ -4614,7 +4466,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/nvx_rs.py b/scripts/nvx_tools/nvx_rs.py new file mode 100644 index 000000000..6b6d36124 --- /dev/null +++ b/scripts/nvx_tools/nvx_rs.py @@ -0,0 +1,556 @@ +"""Find, build, and run nvx-rs, the Rust command-line interface that nvx.py +delegates to. + +nvx.py runs nvx-rs for `nvx.py sandbox` and for the egress policy and artifact +steps of `nvx.py run` (nvx-rs/README.md). It runs, in this order: + +1. the executable that the NVX_RS environment variable names; +2. nvx-rs/target/release/nvx-rs[.exe] in this checkout, where `cargo build + --release` puts it, and where `nvx.py download` installs the nvx-rs of a + release built from this checkout's nvx-rs sources; or else +3. that executable, after it builds it with Cargo, whose output goes to + standard error so that it never mixes with the output of the command. + +Without Cargo, nvx.py fails and names these choices. + +The provenance of an nvx-rs, build/nvx-rs.provenance.json, binds the digest +of its executable to source_sha256() of the sources that built it, and to the +Rust release, target, and features of the build, as OpenVMM's provenance +binds it to its source revision. nvx.py records it for the nvx-rs that it +builds, and `nvx.py record-nvx-rs-provenance` for one built otherwise. +Packaging and the adversarial executor reject an nvx-rs that it does not +describe as built from this checkout's sources. Releases ship nvx-rs as +bin/nvx-rs[.exe], built for RELEASE_TARGETS with FEATURES, with its +provenance, and `nvx.py download` installs both only when that provenance +names this checkout's sources. +""" + +from __future__ import annotations + +import contextlib +import hashlib +import json +import os +import shlex +import shutil +import signal +import subprocess +import sys +import threading +from collections.abc import Callable, Generator, Sequence +from dataclasses import dataclass +from pathlib import Path +from types import FrameType +from typing import cast + +from .build_constants import BuildConstants, OpenVMMBuildConstants +from .common import ( + ScriptError, + artifact_path, + normalized_sha256_file, + require_file, + sha256_file, +) +from .elf import read_elf_executable +from .guest_binaries import pinned_guest_rust_toolchain + +# The environment variable that names the nvx-rs executable to run. +VARIABLE = "NVX_RS" +DIRECTORY = BuildConstants.REPO_ROOT / "nvx-rs" +# The artifact root that nvx-rs runs with: this checkout, whose build/ and +# openvmm/target/release/ hold the artifacts that nvx.py runs. +ROOT = BuildConstants.REPO_ROOT +BINARY_NAME = "nvx-rs" +# The command that builds the executable that binary_path() names, from the +# repository root, so that rust-toolchain.toml selects the Rust release. The +# target directory is explicit, so that CARGO_TARGET_DIR cannot move it. +BUILD_COMMAND = ( + "cargo", + "build", + "--release", + "--locked", + "--features", + "cli", + "--manifest-path", + "nvx-rs/Cargo.toml", + "--target-dir", + "nvx-rs/target", +) +# The features of the nvx-rs that BUILD_COMMAND builds and that releases ship. +FEATURES = ("cli",) +# The Rust targets of the nvx-rs that releases ship, by operating system, as +# CI's NVX CLI jobs build it: a static musl executable, which runs on every +# x86-64 Linux host, and an MSVC executable that links the C runtime +# statically, which runs without the Visual C++ redistributable. +RELEASE_TARGETS = { + "linux": OpenVMMBuildConstants.MUSL_RUST_TARGET, + "windows": OpenVMMBuildConstants.WINDOWS_RUST_TARGET, +} +# The domain and format of the inventory that source_sha256() hashes. +SOURCE_DIGEST_DOMAIN = "nvx-rs-cli-sources" +SOURCE_DIGEST_FORMAT = 1 +# The provenance of the nvx-rs that nvx.py runs, in build/, beside that of +# OpenVMM, and the format of its record. +PROVENANCE_NAME = "nvx-rs.provenance.json" +PROVENANCE_FORMAT = 1 + + +def binary_path() -> Path: + """Return the nvx-rs of this checkout, nvx-rs/target/release/nvx-rs[.exe], + where BUILD_COMMAND puts it. + + This is also where `nvx.py download` installs a release's bin/nvx-rs[.exe], + as it installs bin/openvmm[.exe] where the OpenVMM build puts it. + """ + name = f"{BINARY_NAME}.exe" if os.name == "nt" else BINARY_NAME + return DIRECTORY / "target" / "release" / name + + +def release_target() -> str: + """Return the Rust target of the nvx-rs that releases for this host's + operating system ship.""" + return RELEASE_TARGETS["windows" if os.name == "nt" else "linux"] + + +def source_files(root: Path | None = None) -> tuple[Path, ...]: + """Return the files that determine the nvx-rs executable of the checkout at + `root`, this one by default, in path order: the crate's manifest, lock + file, build script, and sources, and rust-toolchain.toml, which pins the + Rust release that builds it.""" + root = DIRECTORY.parent if root is None else root + crate = root / DIRECTORY.name + files = [ + crate / "Cargo.toml", + crate / "Cargo.lock", + crate / "build.rs", + root / "rust-toolchain.toml", + ] + sources = crate / "src" + if not sources.is_dir(): + raise ScriptError(f"nvx-rs source directory not found: {sources}") + files.extend(path for path in sources.rglob("*") if path.is_file()) + return tuple( + sorted( + (require_file(path, "nvx-rs source") for path in files), + key=lambda path: path.relative_to(root).as_posix(), + ) + ) + + +def source_sha256(root: Path | None = None) -> str: + """Hash the paths and contents of source_files(root), with line endings + normalized, so that every checkout of the same sources has one digest.""" + root = DIRECTORY.parent if root is None else root + inventory = { + "domain": SOURCE_DIGEST_DOMAIN, + "format": SOURCE_DIGEST_FORMAT, + "files": [ + { + "path": path.relative_to(root).as_posix(), + "sha256": normalized_sha256_file(path), + } + for path in source_files(root) + ], + } + encoded = json.dumps(inventory, sort_keys=True, separators=(",", ":")).encode() + return hashlib.sha256(encoded).hexdigest() + + +def provenance_path() -> Path: + """Return build/nvx-rs.provenance.json, the provenance of the nvx-rs that + nvx.py runs.""" + return artifact_path(PROVENANCE_NAME) + + +def host_target() -> str: + """Return the target that Cargo builds for by default here: the host of + the Rust release that rust-toolchain.toml selects.""" + try: + completed = subprocess.run( + ("rustc", "-vV"), + cwd=BuildConstants.REPO_ROOT, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + check=False, + ) + except OSError as error: + raise ScriptError(f"failed to run rustc for its host target: {error}") from ( + error + ) + if completed.returncode == 0: + for line in completed.stdout.splitlines(): + if line.startswith("host: "): + return line.removeprefix("host: ").strip() + raise ScriptError(f"rustc -vV did not report a host target: {completed.stderr}") + + +def check_target(executable: Path, target: str) -> None: + """Fail unless `executable` is what a build for `target` gives, where the + target makes that checkable: for the Linux release target, a static + executable, which a build for the host's own target is not.""" + if target != RELEASE_TARGETS["linux"]: + return + try: + static = read_elf_executable(executable).static + except (OSError, ValueError) as error: + raise ScriptError(f"invalid {target} nvx-rs executable: {error}") from error + if not static: + raise ScriptError( + f"nvx-rs executable {executable} is not static, but a {target} build " + f"is: build it with `cargo build --release --locked --features cli " + f"--manifest-path nvx-rs/Cargo.toml --target {target}`, and copy " + f"nvx-rs/target/{target}/release/{BINARY_NAME} to {binary_path()}" + ) + + +def _format_command(command: Sequence[str]) -> str: + return subprocess.list2cmdline(command) if os.name == "nt" else shlex.join(command) + + +def _missing_cargo_message() -> str: + return ( + f"nvx-rs is not built at {binary_path()}, and Cargo is not on PATH to " + f"build it: set {VARIABLE} to an nvx-rs executable, install the nvx-rs " + "of a release built from this checkout's nvx-rs sources with " + "`nvx.py download`, or install Rust " + "(https://rustup.rs) and build it from the repository root with " + f"`{_format_command(BUILD_COMMAND)}`" + ) + + +def configured_binary() -> Path | None: + """Return the executable that NVX_RS names, or None if it is unset.""" + configured = os.environ.get(VARIABLE) + if not configured: + return None + path = Path(configured).absolute() + if not path.is_file(): + raise ScriptError(f"{VARIABLE} does not name an nvx-rs executable: {path}") + return path + + +def find_binary() -> Path | None: + """Return the nvx-rs that nvx.py runs without building it, or None.""" + configured = configured_binary() + if configured is not None: + return configured + path = binary_path() + return path if path.is_file() else None + + +def provenance_record(executable: Path, target: str) -> dict[str, object]: + """Return the provenance of the nvx-rs executable at `executable`, built + for `target` with FEATURES from this checkout's sources.""" + return { + "format": PROVENANCE_FORMAT, + "source_sha256": source_sha256(), + "toolchain": pinned_guest_rust_toolchain(DIRECTORY.parent), + "target": target, + "features": list(FEATURES), + "executable_sha256": sha256_file(executable), + } + + +def record_provenance(target: str | None = None) -> Path: + """Record the provenance of the nvx-rs that nvx.py runs without building + it, built from this checkout for `target`, by default the host's, and + return where. + + The caller vouches for the build; the record binds its executable to this + checkout's sources, which is what packaging and the adversarial executor + check. + """ + executable = find_binary() + if executable is None: + raise ScriptError( + f"nvx-rs is not built at {binary_path()}: build it with " + f"`{_format_command(BUILD_COMMAND)}`, or set {VARIABLE} to an " + "nvx-rs executable" + ) + target = target or host_target() + check_target(executable, target) + path = provenance_path() + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(provenance_record(executable, target), indent=2) + "\n", + encoding="utf-8", + ) + return path + + +def _stderr_target() -> int: + """Return the descriptor of standard error, to which a build's output goes, + or DEVNULL if standard error has none, as when a test replaced it.""" + try: + sys.stderr.flush() + return sys.stderr.fileno() + except (AttributeError, OSError, ValueError): + return subprocess.DEVNULL + + +def build_binary() -> Path: + """Build the nvx-rs of this checkout with Cargo and return it.""" + if shutil.which("cargo") is None: + raise ScriptError(_missing_cargo_message()) + print(f">> {_format_command(BUILD_COMMAND)}", file=sys.stderr, flush=True) + try: + completed = subprocess.run( + BUILD_COMMAND, + cwd=BuildConstants.REPO_ROOT, + stdin=subprocess.DEVNULL, + stdout=_stderr_target(), + check=False, + ) + except OSError as error: + raise ScriptError(f"failed to run Cargo to build nvx-rs: {error}") from error + if completed.returncode != 0: + raise ScriptError( + f"Cargo failed to build nvx-rs (exit {completed.returncode}); fix the " + f"build, or set {VARIABLE} to an nvx-rs executable" + ) + path = binary_path() + if not path.is_file(): + raise ScriptError(f"Cargo did not build {path}") + # A Cargo build replaces the executable that an earlier record described. + record_provenance() + return path + + +def require_binary() -> Path: + """Return the nvx-rs that nvx.py runs, and build it first if needed.""" + return find_binary() or build_binary() + + +def _command(arguments: Sequence[str], program: str | None) -> list[str]: + command = [os.fspath(require_binary())] + if program is not None: + command.extend(("--prog", program)) + command.extend(("--root", os.fspath(ROOT), *arguments)) + return command + + +def _forward(process: subprocess.Popen[bytes], signum: int) -> None: + # Popen.send_signal signals nothing once the process has exited. + with contextlib.suppress(OSError): + process.send_signal(signum) + + +@contextlib.contextmanager +def _signals_handed_over() -> Generator[Callable[[subprocess.Popen[bytes]], None]]: + """Hand nvx-rs the signals that would end nvx.py while it runs, and yield + the function that names the nvx-rs process once it has started. + + nvx-rs handles Ctrl-C itself: it prints Interrupted and exits with status + 130, which nvx.py then exits with, rather than printing Interrupted again. + A terminal's Ctrl-C reaches both. On POSIX hosts, a SIGINT, SIGTERM, or + SIGHUP sent to nvx.py alone reaches nvx-rs too, a signal that arrives + before nvx-rs has started reaches it as it starts, and a second SIGINT + changes nothing for nvx-rs. On Windows, where Ctrl-C reaches every process + of the console, nvx.py only ignores it. + """ + started: list[subprocess.Popen[bytes]] = [] + pending: list[int] = [] + + def record(process: subprocess.Popen[bytes]) -> None: + started.append(process) + for signum in pending: + _forward(process, signum) + + if threading.current_thread() is not threading.main_thread(): + yield record + return + names = ("SIGINT",) if os.name == "nt" else ("SIGINT", "SIGTERM", "SIGHUP") + signums = [ + cast(int, getattr(signal, signal_name)) + for signal_name in names + if hasattr(signal, signal_name) + ] + + def hand_over(signum: int, _frame: FrameType | None) -> None: + if os.name == "nt": + return + if started: + _forward(started[0], signum) + else: + pending.append(signum) + + previous: list[tuple[int, Callable[[int, FrameType | None], object] | int]] = [] + try: + for signum in signums: + handler = signal.signal(signum, hand_over) + previous.append((signum, signal.SIG_DFL if handler is None else handler)) + yield record + finally: + for signum, handler in reversed(previous): + signal.signal(signum, handler) + + +def _wait(process: subprocess.Popen[bytes]) -> int: + while True: + try: + return process.wait() + except KeyboardInterrupt: + # A handler other than nvx.py's raised it; nvx-rs, which Ctrl-C + # reaches too, decides how the command ends. + if os.name != "nt": + _forward(process, signal.SIGINT) + + +def exit_status(returncode: int) -> int: + """Return the status that nvx.py exits with for nvx-rs's `returncode`. + + That is nvx-rs's own status, 128 + N if signal N ended nvx-rs, as a shell + reports it, and on Windows, a 32-bit status as the signed value that + SystemExit passes on with every Python release. + """ + if returncode < 0: + return 128 - returncode + if os.name == "nt" and returncode > 0x7FFF_FFFF: + return returncode - (1 << 32) + return returncode + + +def run(arguments: Sequence[str], *, program: str) -> int: + """Run `nvx-rs --prog PROGRAM --root ROOT ARGUMENTS` on nvx.py's standard + streams, and return the status that nvx.py exits with.""" + command = _command(arguments, program) + with _signals_handed_over() as started: + try: + process = subprocess.Popen(command) + except OSError as error: + raise ScriptError(f"failed to run {command[0]}: {error}") from error + started(process) + returncode = _wait(process) + if os.name != "nt" and returncode == -signal.SIGINT: + # Ctrl-C ended nvx-rs before it could handle it, as it would have. + print("Interrupted", file=sys.stderr) + return exit_status(returncode) + + +def _error_message(stderr: bytes, returncode: int) -> str: + text = stderr.decode("utf-8", errors="replace").replace("\r\n", "\n") + text = text.removesuffix("\n") + if text.startswith("error: "): + return text.removeprefix("error: ") + return text or f"nvx-rs exited with status {returncode}" + + +def query(arguments: Sequence[str]) -> object: + """Run `nvx-rs --root ROOT ARGUMENTS`, and return the JSON that it prints. + + A failure raises ScriptError with the message that nvx-rs printed after + `error: `, which nvx.py prints as it printed the message of the Python + code that nvx-rs replaced. + """ + command = _command(arguments, None) + try: + completed = subprocess.run( + command, stdin=subprocess.DEVNULL, capture_output=True, check=False + ) + except OSError as error: + raise ScriptError(f"failed to run {command[0]}: {error}") from error + if completed.returncode != 0: + raise ScriptError(_error_message(completed.stderr, completed.returncode)) + try: + return cast(object, json.loads(completed.stdout.decode("utf-8"))) + except (UnicodeDecodeError, json.JSONDecodeError) as error: + raise ScriptError( + f"nvx-rs {' '.join(arguments)} printed invalid JSON: {error}" + ) from error + + +def _object(value: object, what: str) -> dict[str, object]: + if not isinstance(value, dict): + raise ScriptError(f"nvx-rs printed {what} that is not a JSON object") + return cast(dict[str, object], value) + + +def _string(document: dict[str, object], key: str, what: str) -> str: + value = document.get(key) + if not isinstance(value, str): + raise ScriptError(f"nvx-rs printed {what} without a {key} string") + return value + + +def _strings(document: dict[str, object], key: str, what: str) -> tuple[str, ...]: + value = document.get(key) + if not isinstance(value, list) or not all( + isinstance(item, str) for item in cast(list[object], value) + ): + raise ScriptError(f"nvx-rs printed {what} without a {key} list of strings") + return tuple(cast(list[str], value)) + + +@dataclass(frozen=True) +class EgressRules: + """The native OpenVMM rules that an egress policy file compiles to, in the + order in which OpenVMM receives each action's rules.""" + + allow: tuple[str, ...] + deny: tuple[str, ...] + + +def compile_egress_policy(path: Path) -> EgressRules: + """Compile the egress policy file at `path` with `nvx-rs egress compile`. + + A refused policy raises ScriptError with the compiler's message. + """ + what = "egress rules" + document = _object(query(("egress", "compile", os.fspath(path))), what) + return EgressRules( + allow=_strings(document, "allow", what), + deny=_strings(document, "deny", what), + ) + + +@dataclass(frozen=True) +class GuestArtifacts: + """The artifacts of one guest that `nvx-rs artifacts` lists.""" + + name: str + distribution: str + initramfs: Path + + +@dataclass(frozen=True) +class ArtifactPaths: + """The paths of the artifacts that nvx.py runs, which nvx-rs does not check.""" + + openvmm: Path + kernel: Path + guests: tuple[GuestArtifacts, ...] + + def guest(self, name: str) -> GuestArtifacts: + for guest in self.guests: + if guest.name == name: + return guest + raise ScriptError(f"nvx-rs printed no artifacts of the {name} guest") + + +def artifacts() -> ArtifactPaths: + """Return the paths of the artifacts of ROOT with `nvx-rs artifacts`. + + OpenVMM is the executable that NVX_OPENVMM names, if set, as for nvx.py + sandbox, and otherwise the checkout's. + """ + what = "artifact paths" + document = _object(query(("artifacts",)), what) + raw_guests = document.get("guests") + if not isinstance(raw_guests, list): + raise ScriptError(f"nvx-rs printed {what} without a guests list") + guests: list[GuestArtifacts] = [] + for raw_guest in cast(list[object], raw_guests): + guest = _object(raw_guest, "a guest") + guests.append( + GuestArtifacts( + name=_string(guest, "name", "a guest"), + distribution=_string(guest, "distribution", "a guest"), + initramfs=Path(_string(guest, "initramfs", "a guest")), + ) + ) + return ArtifactPaths( + openvmm=Path(_string(document, "openvmm", what)), + kernel=Path(_string(document, "kernel", what)), + guests=tuple(guests), + ) diff --git a/scripts/nvx_tools/release.py b/scripts/nvx_tools/release.py index 26095eebe..597116946 100644 --- a/scripts/nvx_tools/release.py +++ b/scripts/nvx_tools/release.py @@ -22,6 +22,7 @@ from pathlib import Path, PurePosixPath from typing import cast +from . import nvx_rs from .archive import create_reproducible_release_archive, create_reproducible_tar_gz from .azurelinux import input_sha256 as azurelinux_input_sha256 from .azurelinux import package_lock_sha256 as azurelinux_package_lock_sha256 @@ -36,6 +37,7 @@ AlpineBuildConstants, AzureLinuxBuildConstants, BuildConstants, + GuestRustBuildConstants, InitramfsBuildConstants, KernelBuildConstants, OpenVMMBuildConstants, @@ -59,6 +61,10 @@ write_sha256_sums, ) from .development_release import development_release_tag +from .guest_binaries import ( + guest_binary_source_files, + pinned_guest_rust_toolchain, +) from .ubuntu import converter_input_sha256, customization_files, package_lock_sha256 GITHUB_API_VERSION = "2022-11-28" @@ -495,6 +501,29 @@ def _release_archive_layout( raise ScriptError(f"unsupported release archive: {archive_path.name}") +def release_package_executables(platform: str) -> tuple[str, ...]: + """Return the paths of the executables that every package of the release + platform `platform`, such as linux-kvm, ships: OpenVMM and nvx-rs.""" + suffix = ".exe" if platform.startswith("windows-") else "" + return tuple( + f"bin/{name}{suffix}" + for name in (OpenVMMBuildConstants.BINARY_NAME, nvx_rs.BINARY_NAME) + ) + + +def require_release_package_executables(archive_path: Path, platform: str) -> None: + """Fail unless the release archive at `archive_path` ships the executables + of `platform`, which its member list shows without extracting it.""" + root, layout = _release_archive_layout(archive_path) + missing = [ + path + for path in release_package_executables(platform) + if layout.get(f"{root}/{path}") is not False + ] + if missing: + raise ScriptError(f"{archive_path.name} does not ship {', '.join(missing)}") + + def _extract_release_archive(archive_path: Path, destination: Path) -> None: _release_archive_layout(archive_path) try: @@ -642,6 +671,47 @@ def _packaged_guest_artifact_names(package_root: Path) -> list[str]: return names +def _install_executable(source: Path, destination: Path) -> None: + _replace_runtime_file(source, destination) + if os.name != "nt": + destination.chmod(0o755) + + +def _packaged_nvx_rs( + package_root: Path, package_name: str +) -> tuple[tuple[Path, Path] | None, str]: + """Return the package's nvx-rs and its provenance if the provenance shows + the build of this checkout's nvx-rs sources that nvx.py runs, and + otherwise None, with the reason. + + nvx.py runs the nvx-rs that download installs without rebuilding it, so an + nvx-rs of other sources, such as that of an older release or of a checkout + whose nvx-rs changed, or with other features, would run with this + checkout's nvx.py. A package whose nvx-rs does not match its own + provenance is broken, and fails. + """ + name = nvx_rs.binary_path().name + executable = package_root / "bin" / name + provenance = package_root / "provenance" / nvx_rs.PROVENANCE_NAME + if not executable.is_file(): + return None, f"{package_name} ships no bin/{name}" + if not provenance.is_file(): + return None, f"{package_name} ships no provenance for its bin/{name}" + target = nvx_rs.release_target() + record = _read_json_object(provenance, "packaged nvx-rs build provenance") + if not _describes_checkout_nvx_rs(record, target=target): + return ( + None, + f"the bin/{name} of {package_name} is not the {target} build of this " + "checkout's nvx-rs sources that nvx.py runs", + ) + if record.get("executable_sha256") != sha256_file(executable): + raise ScriptError( + f"the bin/{name} of {package_name} does not match its build provenance" + ) + return (executable, provenance), "" + + def _install_release_archive(archive_path: Path) -> None: with tempfile.TemporaryDirectory(prefix="nvx-release-") as temporary: extraction_root = Path(temporary) @@ -677,7 +747,10 @@ def _install_release_archive(archive_path: Path) -> None: InitramfsBuildConstants.PROVENANCE_NAME, ) } - _replace_runtime_file(binary_source, binary_destination) + packaged_nvx_rs, nvx_rs_skipped = _packaged_nvx_rs( + package_root, archive_path.name + ) + _install_executable(binary_source, binary_destination) for name, source in guest_sources.items(): _replace_runtime_file(source, artifact_path(name)) for name in ReleaseBuildConstants.GUEST_ARTIFACT_NAMES: @@ -685,6 +758,21 @@ def _install_release_archive(archive_path: Path) -> None: artifact_path(name).unlink(missing_ok=True) for name, source in provenance_sources.items(): _replace_runtime_file(source, artifact_path(name)) + nvx_rs_provenance = artifact_path(nvx_rs.PROVENANCE_NAME) + if packaged_nvx_rs is None: + # Remove the nvx-rs that an earlier download or build left, which + # may not match this checkout, so that nvx.py builds one from it. + nvx_rs.binary_path().unlink(missing_ok=True) + nvx_rs_provenance.unlink(missing_ok=True) + print( + f">> {nvx_rs_skipped}, so nvx.py builds nvx-rs from this checkout " + f"with Cargo when a command needs it, unless {nvx_rs.VARIABLE} " + "names one" + ) + else: + executable, provenance = packaged_nvx_rs + _install_executable(executable, nvx_rs.binary_path()) + _replace_runtime_file(provenance, nvx_rs_provenance) def download_checkout_release(repository: str, platform: str) -> None: @@ -1087,7 +1175,64 @@ def _require_runtime_provenance_paths() -> tuple[Path, Path, Path]: ) +def _describes_checkout_nvx_rs( + provenance: dict[str, object], *, target: str | None +) -> bool: + """Return whether the nvx-rs provenance `provenance` describes a build of + this checkout's nvx-rs sources with its features and Rust release, for + `target` if given, whatever executable it names.""" + recorded_target = provenance.get("target") + return ( + provenance.get("format") == nvx_rs.PROVENANCE_FORMAT + and provenance.get("source_sha256") + == nvx_rs.source_sha256(BuildConstants.REPO_ROOT) + and provenance.get("toolchain") == pinned_guest_rust_toolchain() + and provenance.get("features") == list(nvx_rs.FEATURES) + and isinstance(recorded_target, str) + and (target is None or recorded_target == target) + ) + + +def _validate_nvx_rs_provenance( + executable: Path, + provenance_path: Path, + *, + target: str | None, +) -> dict[str, object]: + """Check that the nvx-rs provenance at `provenance_path` describes the + executable at `executable`, built with nvx-rs's features and this + checkout's Rust release from its nvx-rs sources, for `target` if given, + and return it.""" + provenance = _read_json_object(provenance_path, "nvx-rs build provenance") + if not _describes_checkout_nvx_rs(provenance, target=target) or provenance.get( + "executable_sha256" + ) != sha256_file(executable): + build = "a build" if target is None else f"a {target} build" + raise ScriptError( + f"nvx-rs build provenance does not describe {executable} as {build} " + "from the current nvx-rs sources; build nvx-rs from them, and record " + "its provenance with `nvx.py record-nvx-rs-provenance`" + ) + return provenance + + +def _require_runtime_nvx_rs() -> tuple[Path, Path]: + """Return the nvx-rs that nvx.py runs without building it and its + provenance.""" + executable = nvx_rs.find_binary() or require_file( + nvx_rs.binary_path(), "nvx-rs executable" + ) + provenance = require_file( + artifact_path(nvx_rs.PROVENANCE_NAME), "nvx-rs build provenance" + ) + return executable, provenance + + def validate_runtime_artifact_provenance() -> None: + """Check that OpenVMM, the kernel, the Alpine initramfs, and the nvx-rs + that nvx.py runs match their build provenance and this checkout's + sources, so that nothing stale runs, such as an nvx-rs left from before a + change of its sources, which nvx.py never rebuilds.""" binary = require_file(openvmm_binary_path(), "OpenVMM release binary") kernel = require_file( artifact_path(KernelBuildConstants.BINARY_NAME), @@ -1110,6 +1255,7 @@ def validate_runtime_artifact_provenance() -> None: kernel_provenance_path, initramfs_provenance_path, ) = _require_runtime_provenance_paths() + nvx_rs_executable, nvx_rs_provenance_path = _require_runtime_nvx_rs() _validate_openvmm_provenance(binary, openvmm_provenance_path) _validate_kernel_provenance(kernel, kernel_config, kernel_provenance_path) _validate_initramfs_provenance( @@ -1117,6 +1263,22 @@ def validate_runtime_artifact_provenance() -> None: package_manifest, initramfs_provenance_path, ) + _validate_nvx_rs_provenance(nvx_rs_executable, nvx_rs_provenance_path, target=None) + + +def _require_release_nvx_rs() -> tuple[Path, Path, dict[str, object]]: + """Return the nvx-rs that a package ships, its provenance path, and its + provenance, which must show a build for the release target of this + operating system: on Linux, the static musl executable that runs on every + host, which a build for the host's own target is not.""" + executable = require_file(nvx_rs.binary_path(), "nvx-rs release executable") + provenance_path = require_file( + artifact_path(nvx_rs.PROVENANCE_NAME), "nvx-rs build provenance" + ) + target = nvx_rs.release_target() + provenance = _validate_nvx_rs_provenance(executable, provenance_path, target=target) + nvx_rs.check_target(executable, target) + return executable, provenance_path, provenance def _packaged_source_manifest( @@ -1125,6 +1287,8 @@ def _packaged_source_manifest( release_root: Path, openvmm_provenance: dict[str, object], binary_name: str, + nvx_rs_provenance: dict[str, object], + nvx_rs_name: str, *, include_azurelinux: bool, ) -> bytes: @@ -1173,6 +1337,13 @@ def _packaged_source_manifest( ubuntu_section["distro_layer_manifest_sha256"] = sha256_file( release_root / "guest" / UbuntuBuildConstants.DISTRO_MANIFEST_NAME ) + root_manifest["nvx_rs"] = { + "executable_sha256": sha256_file(release_root / "bin" / nvx_rs_name), + **{ + field: nvx_rs_provenance[field] + for field in ("source_sha256", "toolchain", "target", "features") + }, + } if include_azurelinux: azurelinux_section = cast(dict[str, object], azurelinux) azurelinux_section["initramfs_sha256"] = sha256_file( @@ -1231,6 +1402,7 @@ def _validate_source_manifest_metadata( alpine_value = manifest.get("alpine") ubuntu_value = manifest.get("ubuntu") azurelinux_value = manifest.get("azurelinux") + guest_rust_value = manifest.get("guest_rust") source_value = kernel_inputs.get("source") config_value = kernel_inputs.get("input_config") if not all( @@ -1240,6 +1412,7 @@ def _validate_source_manifest_metadata( alpine_value, ubuntu_value, azurelinux_value, + guest_rust_value, source_value, config_value, ) @@ -1249,8 +1422,17 @@ def _validate_source_manifest_metadata( alpine = cast(dict[str, object], alpine_value) ubuntu = cast(dict[str, object], ubuntu_value) azurelinux = cast(dict[str, object], azurelinux_value) + guest_rust = cast(dict[str, object], guest_rust_value) source = cast(dict[str, object], source_value) input_config = cast(dict[str, object], config_value) + expected_guest_rust = { + "toolchain": pinned_guest_rust_toolchain(), + "target": GuestRustBuildConstants.TARGET, + } + if guest_rust != expected_guest_rust: + raise ScriptError( + "SOURCE-MANIFEST.json Rust guest toolchain does not match the build pin" + ) source_patches = source.get("patches") if not isinstance(source_patches, list): raise ScriptError("kernel provenance source patches must be a list") @@ -1295,7 +1477,11 @@ def _validate_source_manifest_metadata( "minirootfs_url": AlpineBuildConstants.MINIROOTFS_URL, "minirootfs_sha256": AlpineBuildConstants.MINIROOTFS_SHA256, "guest_sources": [ - path.as_posix() for path in AlpineBuildConstants.GUEST_SOURCE_DIRECTORIES + *( + path.as_posix() + for path in AlpineBuildConstants.GUEST_SOURCE_DIRECTORIES + ), + *GuestRustBuildConstants.SOURCE_PATHS, ], "package_manifests": [ ( @@ -1329,6 +1515,7 @@ def _validate_source_manifest_metadata( for path in UbuntuBuildConstants.GUEST_SOURCE_DIRECTORIES ), UbuntuBuildConstants.PACKAGE_LOCK_RELATIVE_PATH.as_posix(), + *GuestRustBuildConstants.SOURCE_PATHS, ], "package_manifests": [ ( @@ -1363,6 +1550,7 @@ def _validate_source_manifest_metadata( for path in AzureLinuxBuildConstants.GUEST_SOURCE_DIRECTORIES ), AzureLinuxBuildConstants.PACKAGE_LOCK_RELATIVE_PATH.as_posix(), + *GuestRustBuildConstants.SOURCE_PATHS, ], "package_manifests": [ ( @@ -1506,6 +1694,7 @@ def package_release( ).resolve() _validate_release_replacement(release_destination, force) binary = require_file(openvmm_binary_path(), "OpenVMM release binary") + nvx_rs_binary, nvx_rs_provenance_path, nvx_rs_provenance = _require_release_nvx_rs() kernel = require_file( artifact_path(KernelBuildConstants.BINARY_NAME), "required guest artifact vmlinux", @@ -1564,6 +1753,7 @@ def package_release( preserve_staging = False try: _copy_release_file(binary, staging / "bin" / binary.name) + _copy_release_file(nvx_rs_binary, staging / "bin" / nvx_rs_binary.name) for name in guest_names: _copy_release_file(artifact_path(name), staging / "guest" / name) _copy_release_file( @@ -1578,6 +1768,10 @@ def package_release( initramfs_provenance_path, staging / "provenance" / InitramfsBuildConstants.PROVENANCE_NAME, ) + _copy_release_file( + nvx_rs_provenance_path, + staging / "provenance" / nvx_rs.PROVENANCE_NAME, + ) for name in ("LICENSE", "README.md", "THIRD_PARTY_NOTICES.md"): _copy_release_file(BuildConstants.REPO_ROOT / name, staging / name) _copy_release_file( @@ -1630,6 +1824,13 @@ def package_release( raise ScriptError( "packaged OpenVMM executable does not match its build provenance" ) + if ( + sha256_file(staging / "bin" / nvx_rs_binary.name) + != nvx_rs_provenance["executable_sha256"] + ): + raise ScriptError( + "packaged nvx-rs executable does not match its build provenance" + ) if sha256_file(packaged_kernel) != kernel_provenance["kernel_sha256"]: raise ScriptError("packaged kernel does not match its build provenance") if sha256_file(packaged_config) != kernel_provenance["config_sha256"]: @@ -1668,6 +1869,14 @@ def package_release( != initramfs_provenance ): raise ScriptError("packaged initramfs provenance changed while staging") + if ( + _read_json_object( + staging / "provenance" / nvx_rs.PROVENANCE_NAME, + "packaged nvx-rs build provenance", + ) + != nvx_rs_provenance + ): + raise ScriptError("packaged nvx-rs provenance changed while staging") (staging / "SOURCE-MANIFEST.json").write_bytes( _packaged_source_manifest( root_manifest, @@ -1675,6 +1884,8 @@ def package_release( staging, openvmm_provenance, binary.name, + nvx_rs_provenance, + nvx_rs_binary.name, include_azurelinux=not include_source, ) ) @@ -1699,6 +1910,8 @@ def verify_source_tree() -> None: manifest, kernel_provenance_inputs(), ) + guest_binary_source_files() + pinned_guest_rust_toolchain() required = { BuildConstants.REPO_ROOT / "kernel" diff --git a/scripts/nvx_tools/sandbox.py b/scripts/nvx_tools/sandbox.py index 4a4307524..26ddb978a 100644 --- a/scripts/nvx_tools/sandbox.py +++ b/scripts/nvx_tools/sandbox.py @@ -1,35 +1,27 @@ -"""Validated host-side launch contract for the experimental sandbox profile.""" +"""The live-share arguments of `nvx.py run`: its --mount values, the +aggregate of several shares, and the kernel command-line budget that they +share. + +nvx-rs owns the launch profile of `nvx.py sandbox`, its shares included +(nvx-rs/README.md); `nvx.py run` builds its own OpenVMM command line. +""" from __future__ import annotations import hashlib import os -import re from collections.abc import Sequence -from dataclasses import dataclass from pathlib import Path -from uuid import UUID -from .common import ScriptError, require_file +from .common import ScriptError -LAYER_ROLES = ("distro", "runtime", "custom") -BLOCK_MMIO_BASES = { - "distro": 0xD000_3000, - "runtime": 0xD000_4000, - "custom": 0xD000_5000, - "scratch": 0xD000_6000, -} KERNEL_COMMAND_LINE_MAX_SIZE = 2048 +# OpenVMM keeps this much of the kernel command line for its own tokens. OPENVMM_COMMAND_LINE_RESERVE = 1024 SANDBOX_COMMAND_LINE_MAX_SIZE = ( KERNEL_COMMAND_LINE_MAX_SIZE - OPENVMM_COMMAND_LINE_RESERVE ) -_HOSTNAME = re.compile(r"(?=^.{1,63}$)(?!-)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$") -DEFAULT_WORKLOAD_IDENTITY = (65534, 65534) MOUNT_ACCESS_MODES = ("ro", "rw") -# A share binds a host directory, or a regular host file, which only a child -# of an aggregate can expose, without anything else of its directory. -MOUNT_KINDS = ("directory", "file") # OpenVMM attaches one virtio-fs device, tag `microvm`. One directory share # uses it directly. Several shares, or a file, share it as the children of an # aggregate, named by aggregate_child_name, which the guest mounts at @@ -40,14 +32,6 @@ # `vmm` performs every share operation as OpenVMM; `caller` performs each as the # guest caller's identity, with guest root squashed to the share owner. MOUNT_OWNERS = ("vmm", "caller") -# The container runtime owns these guest paths: it mounts procfs, sysfs, and a -# private /dev over them, stages its tools under /.nvx-agent, and bind-mounts the -# workload machine ID at /etc/machine-id, which it first replaces with a regular -# file. A live share must not shadow or be shadowed by them. -RESERVED_MOUNT_TARGETS = ("/proc", "/sys", "/dev", "/.nvx-agent", "/etc/machine-id") -RESERVED_EXACT_MOUNT_TARGETS = ("/etc",) -# OpenVMM accepts at most this many denied, allowed, and writable paths each. -MAX_MOUNT_POLICY_PATHS = 128 # The option that requests each kind of share access-policy path. `--mount-deny` # hides a path, `--mount-allow` exposes a path inside a denied path again, and # `--mount-write` makes a path one of the only writable parts of its share. @@ -62,21 +46,6 @@ _AGGREGATE_COMMAND_LINE_SUFFIX = " virtfs_aggregate=1" -def parse_workload_identity(value: str) -> tuple[int, int]: - fields = value.split(":") - if len(fields) != 2: - raise ScriptError("--workload-user must be UID:GID") - try: - uid, gid = (int(field, 10) for field in fields) - except ValueError as error: - raise ScriptError("--workload-user must contain numeric UID and GID") from error - if not 1 <= uid <= 0xFFFF_FFFF: - raise ScriptError("--workload-user UID must be between 1 and 4294967295") - if not 1 <= gid <= 0xFFFF_FFFF: - raise ScriptError("--workload-user GID must be between 1 and 4294967295") - return uid, gid - - def parse_mount_value(value: str) -> tuple[str, str, str]: """Split a --mount value, GUEST_TARGET,HOST_PATH[,ro|rw], into its guest target, host path, and access mode, which defaults to ro. @@ -101,55 +70,6 @@ def parse_mount_value(value: str) -> tuple[str, str, str]: return target, host_path, access -@dataclass(frozen=True) -class SandboxLayer: - role: str - path: Path - uuid: str - - @classmethod - def parse(cls, value: str) -> SandboxLayer: - fields = value.split(",") - if len(fields) != 3: - raise ScriptError("--layer must be ROLE,PATH,EROFS_UUID") - role, raw_path, raw_uuid = fields - if role not in LAYER_ROLES: - raise ScriptError( - f"unsupported layer role {role!r}; choose {', '.join(LAYER_ROLES)}" - ) - if not raw_path: - raise ScriptError(f"{role} layer path is empty") - try: - uuid = str(UUID(raw_uuid)) - except ValueError as error: - raise ScriptError(f"{role} layer UUID is invalid: {raw_uuid!r}") from error - return cls(role=role, path=Path(raw_path), uuid=uuid) - - -def validate_mount_target(target: str) -> str: - """Validate a live-share guest target inside the sandbox container rootfs.""" - if ( - not target.startswith("/") - or target == "/" - or len(target.encode("utf-8")) > 4096 - or any(character.isspace() or character in "\0\\=," for character in target) - or any(component in ("", ".", "..") for component in target.split("/")[1:]) - ): - raise ScriptError( - f"invalid sandbox mount target {target!r}: expected an absolute non-root " - "path without empty, dot, or parent components, whitespace, '\\', '=', " - "or ','" - ) - for reserved in RESERVED_MOUNT_TARGETS: - if target == reserved or target.startswith(f"{reserved}/"): - raise ScriptError( - f"sandbox mount target {target} overlaps the reserved {reserved} tree" - ) - if target in RESERVED_EXACT_MOUNT_TARGETS: - raise ScriptError(f"sandbox mount target {target} is reserved") - return target - - def require_mount_owner_supported(owner: str) -> None: """Reject an ownership mode that the host cannot enforce.""" if owner == "caller" and os.name == "nt": @@ -159,253 +79,12 @@ def require_mount_owner_supported(owner: str) -> None: ) -@dataclass(frozen=True) -class SandboxMount: - """A live virtio-fs share mounted inside the sandbox container rootfs. - - `denied_paths` hide paths in the share, `allowed_paths` expose paths - inside denied paths again, and `writable_paths`, when present, are the - only paths of a read-write share that the workload can modify. A share of - `kind` file binds one regular host file, whose mode applies to the whole - file, so it has none of them. - """ - - guest_target: str - host_path: Path - access: str = "ro" - denied_paths: tuple[str, ...] = () - owner: str = "vmm" - allowed_paths: tuple[str, ...] = () - writable_paths: tuple[str, ...] = () - kind: str = "directory" - - @classmethod - def parse( - cls, - value: str, - denied_paths: tuple[str, ...] = (), - owner: str = "vmm", - *, - allowed_paths: tuple[str, ...] = (), - writable_paths: tuple[str, ...] = (), - kind: str = "directory", - ) -> SandboxMount: - guest_target, raw_path, access = parse_mount_value(value) - return cls( - guest_target=guest_target, - host_path=Path(raw_path), - access=access, - denied_paths=denied_paths, - owner=owner, - allowed_paths=allowed_paths, - writable_paths=writable_paths, - kind=kind, - ) - - def __post_init__(self) -> None: - validate_mount_target(self.guest_target) - if self.access not in MOUNT_ACCESS_MODES: - raise ScriptError( - f"unsupported sandbox mount mode {self.access!r}; choose ro or rw" - ) - if self.owner not in MOUNT_OWNERS: - raise ScriptError( - f"unsupported sandbox mount owner {self.owner!r}; choose vmm or caller" - ) - if self.kind not in MOUNT_KINDS: - raise ScriptError( - f"unsupported sandbox mount kind {self.kind!r}; choose directory " - "or file" - ) - raw_path = os.fspath(self.host_path) - if "\0" in raw_path: - raise ScriptError( - f"sandbox mount host path contains a NUL character: {raw_path!r}" - ) - if ".." in self.host_path.parts: - raise ScriptError( - f"sandbox mount host path contains a parent component: {raw_path}" - ) - if self.kind == "file" and any(paths for _, paths in self.policy_paths()): - raise ScriptError( - "--mount-deny, --mount-allow, and --mount-write name paths inside " - f"a shared directory, but --mount {self.guest_target} shares a " - "file, whose mode applies to the whole file" - ) - for kind, paths in self.policy_paths(): - if len(paths) > MAX_MOUNT_POLICY_PATHS: - raise ScriptError( - f"a sandbox mount permits at most {MAX_MOUNT_POLICY_PATHS} " - f"{kind} paths" - ) - for path in paths: - if not path or "\0" in path: - raise ScriptError(f"sandbox mount {kind} paths must be nonempty") - if len(set(paths)) != len(paths): - raise ScriptError(f"sandbox mount {kind} paths must be unique") - if self.allowed_paths and not self.denied_paths: - raise ScriptError( - "--mount-allow exposes a path inside a --mount-deny path of the " - "same share, which has none" - ) - if self.writable_paths and self.access != "rw": - raise ScriptError( - "--mount-write narrows the writes of a read-write share; " - f"--mount {self.guest_target} is read-only" - ) - - def policy_paths(self) -> tuple[tuple[str, tuple[str, ...]], ...]: - """Return each kind of access-policy path with its requested paths.""" - return ( - ("denied", self.denied_paths), - ("allowed", self.allowed_paths), - ("writable", self.writable_paths), - ) - - def validated(self) -> SandboxMount: - require_mount_owner_supported(self.owner) - # The kind was fixed when the share was requested, so a host path that - # became a directory in place of a file is not shared in its place. - if self.kind == "file": - if self.host_path.is_symlink() or not self.host_path.is_file(): - raise ScriptError( - f"sandbox mount host path is not a regular file: {self.host_path}" - ) - elif self.host_path.is_symlink() or not self.host_path.is_dir(): - raise ScriptError( - f"sandbox mount host path is not a plain directory: {self.host_path}" - ) - if self.owner == "caller" and os.name != "nt": - status = self.host_path.stat() - if status.st_uid == 0 or status.st_gid == 0: - raise ScriptError( - "--mount-owner caller squashes guest root to the owner of the " - f"shared {self.kind}, so {self.host_path} must not be owned by " - "UID 0 or GID 0" - ) - return self - - def absolute(self) -> SandboxMount: - # Join instead of normalizing so OpenVMM still sees, and rejects, every - # symbolic-link component of the requested path. - return SandboxMount( - guest_target=self.guest_target, - host_path=( - self.host_path - if self.host_path.is_absolute() - else Path.cwd() / self.host_path - ), - access=self.access, - denied_paths=self.denied_paths, - owner=self.owner, - allowed_paths=self.allowed_paths, - writable_paths=self.writable_paths, - kind=self.kind, - ) - - def _absolute_paths(self, paths: tuple[str, ...]) -> tuple[str, ...]: - host_path = self.absolute().host_path - return tuple( - path if Path(path).is_absolute() else os.fspath(host_path / path) - for path in paths - ) - - def absolute_policy_paths(self) -> tuple[tuple[str, tuple[str, ...]], ...]: - """Return each kind of access-policy path, with relative paths joined - to the share.""" - return tuple( - (kind, self._absolute_paths(paths)) for kind, paths in self.policy_paths() - ) - - def command_line_fragment(self) -> str: - return _MOUNT_COMMAND_LINE_FRAGMENT.format( - self.guest_target, MOUNT_TAG, self.access - ) - - -def _targets_overlap(left: str, right: str) -> bool: - return left == right or left.startswith(f"{right}/") or right.startswith(f"{left}/") - - def mount_kind(host_path: Path) -> str: """Return the kind of share that `host_path` requests: a file for a - regular file, and otherwise a directory, which validation then requires.""" + regular file, and otherwise a directory, which OpenVMM then requires.""" return "file" if host_path.is_file() else "directory" -def mounts_use_aggregate(mounts: Sequence[SandboxMount]) -> bool: - """Return whether `mounts` share the virtio-fs device as the children of - an aggregate: several shares do, and so does a file, because the guest - mounts the device's root, which is a directory.""" - return len(mounts) > 1 or any(mount.kind == "file" for mount in mounts) - - -def validate_mounts(mounts: tuple[SandboxMount, ...]) -> None: - """Validate the live shares that one sandbox attaches, in child order.""" - for index, mount in enumerate(mounts): - for other in mounts[:index]: - if _targets_overlap(other.guest_target, mount.guest_target): - raise ScriptError( - f"sandbox mount targets {other.guest_target} and " - f"{mount.guest_target} overlap; a share cannot hide another" - ) - if len({mount.owner for mount in mounts}) > 1: - raise ScriptError("every sandbox --mount must use the same --mount-owner") - - -def _path_identity(path: Path) -> tuple[int, int]: - status = path.stat() - return status.st_dev, status.st_ino - - -def validate_mount_host_paths(mounts: tuple[SandboxMount, ...]) -> None: - """Reject shares whose host paths overlap, or deny another's paths. A - shared file overlaps only a shared directory that contains it, or another - share of the same file.""" - if len(mounts) < 2: - return - roots = [mount.host_path.resolve() for mount in mounts] - # Resolving follows symbolic links but not bind mounts, so also compare the - # file identities that OpenVMM pins for each share: a root with the identity - # of another root, or of one of its ancestors, is that directory or file, - # such as through a hard link, or lies inside a bind mount of it. On Linux, - # OpenVMM also compares the mount sources that each share reaches, which - # catches a bind mount that exposes part of one share inside the other. - lineages = [ - [_path_identity(path) for path in (root, *root.parents)] for root in roots - ] - for index, root in enumerate(roots): - for other_index, other in enumerate(roots[:index]): - if root == other or other in root.parents or root in other.parents: - raise ScriptError( - f"sandbox mount host paths {other} and {root} overlap; " - "a share could reach files of another under its access mode" - ) - if ( - lineages[index][0] in lineages[other_index] - or lineages[other_index][0] in lineages[index] - ): - raise ScriptError( - f"sandbox mount host paths {other} and {root} name the same " - "directory or file, or one is inside the other through a bind " - "mount" - ) - for mount, root in zip(mounts, roots, strict=True): - for kind, paths in mount.absolute_policy_paths(): - option = MOUNT_POLICY_OPTIONS[kind] - for path in paths: - resolved = Path(path).resolve() - # A denied root hides the share behind its allowed paths, which - # OpenVMM requires; it accepts no other policy path at a root. - if root in resolved.parents or (kind == "denied" and resolved == root): - continue - raise ScriptError( - f"{option} {path} is not inside the host directory of " - f"--mount {mount.guest_target}; with several shares, each " - f"{option} follows the --mount whose directory it names" - ) - - def aggregate_child_name(index: int, target: str) -> str: """Return the name of the aggregate child at `index` that the guest binds at `target`. @@ -434,48 +113,6 @@ def mount_argument(target: str, host_path: str, access: str) -> str: return f"{target},{host_path},{access}" -def mounts_openvmm_arguments(mounts: tuple[SandboxMount, ...]) -> list[str]: - """Return the OpenVMM arguments that attach `mounts`: one directory share - directly, and several shares, or a file, as the children of an aggregate, - in order.""" - arguments: list[str] = [] - aggregate = mounts_use_aggregate(mounts) - if mounts and not aggregate: - mount = mounts[0] - arguments.extend( - ( - "--mount", - mount_argument( - mount.guest_target, os.fspath(mount.host_path), mount.access - ), - ) - ) - elif mounts: - arguments.extend(("--mount-aggregate", AGGREGATE_MOUNT_TARGET)) - for index, mount in enumerate(mounts): - name = aggregate_child_name(index, mount.guest_target) - arguments.extend( - ( - "--mount-child", - aggregate_child_argument( - name, os.fspath(mount.host_path), mount.access, mount.kind - ), - ) - ) - for mount in mounts: - # OpenVMM resolves a relative policy path in the only share, and - # attributes absolute policy paths to the child that contains them. - policy_paths = ( - mount.absolute_policy_paths() if aggregate else mount.policy_paths() - ) - for kind, paths in policy_paths: - for path in paths: - arguments.extend((MOUNT_POLICY_OPTIONS[kind], path)) - if mounts and mounts[0].owner != "vmm": - arguments.extend(("--mount-owner", mounts[0].owner)) - return arguments - - def aggregate_share_tokens(shares: Sequence[tuple[str, str, str]]) -> list[str]: """Return the kernel command-line tokens that tell the guest where to bind each child of an aggregate, given each share's guest target, access, and @@ -496,156 +133,3 @@ def aggregate_command_line_fragment(accesses: Sequence[str]) -> str: _MOUNT_COMMAND_LINE_FRAGMENT.format(AGGREGATE_MOUNT_TARGET, MOUNT_TAG, access) + _AGGREGATE_COMMAND_LINE_SUFFIX ) - - -def mounts_command_line_fragment(mounts: tuple[SandboxMount, ...]) -> str: - """Return the bootstrap tokens that OpenVMM appends for `mounts`.""" - if not mounts: - return "" - if not mounts_use_aggregate(mounts): - return mounts[0].command_line_fragment() - return aggregate_command_line_fragment([mount.access for mount in mounts]) - - -@dataclass(frozen=True) -class SandboxLaunch: - layers: tuple[SandboxLayer, ...] - scratch: Path - entrypoint: str = "/bin/sh" - args: tuple[str, ...] = () - hostname: str = "nvx-sandbox" - workload_identity: tuple[int, int] = DEFAULT_WORKLOAD_IDENTITY - memory_max: int | None = None - pids_max: int | None = None - 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 not self.entrypoint.startswith("/") or any( - character.isspace() for character in self.entrypoint - ): - raise ScriptError( - "sandbox entrypoint must be an absolute path without spaces" - ) - for argument in self.args: - if not argument or any(character.isspace() for character in argument): - raise ScriptError( - "sandbox arguments must be nonempty and contain no spaces" - ) - if _HOSTNAME.fullmatch(self.hostname) is None: - raise ScriptError( - "sandbox hostname must be a lowercase RFC 1123 label up to 63 characters" - ) - uid, gid = self.workload_identity - if not 1 <= uid <= 0xFFFF_FFFF or not 1 <= gid <= 0xFFFF_FFFF: - raise ScriptError("sandbox workload identity must be a non-root UID:GID") - for name, value in ( - ("memory-max", self.memory_max), - ("pids-max", self.pids_max), - ): - if value is not None and value <= 0: - raise ScriptError(f"--{name} must be positive") - validate_mounts(self.mounts) - - def validated(self) -> SandboxLaunch: - for layer in self.layers: - require_file(layer.path, f"{layer.role} EROFS layer") - _reject_disk_path(layer.path) - require_file(self.scratch, "ext4 scratch image") - _reject_disk_path(self.scratch) - for mount in self.mounts: - mount.validated() - validate_mount_host_paths(self.mounts) - return self - - def ordered_layers(self) -> tuple[SandboxLayer, ...]: - by_role = {layer.role: layer for layer in self.layers} - return tuple(by_role[role] for role in LAYER_ROLES if role in by_role) - - def openvmm_arguments(self) -> list[str]: - arguments = ["--machine", "microvm"] - for layer in self.ordered_layers(): - arguments.extend( - ( - "--microvm-sandbox-block", - f"{layer.role}:file:{os.fspath(layer.path)},ro", - ) - ) - arguments.extend( - ( - "--microvm-sandbox-block", - f"scratch:file:{os.fspath(self.scratch)}", - "--microvm-workload-identity", - f"{self.workload_identity[0]}:{self.workload_identity[1]}", - ) - ) - arguments.extend(mounts_openvmm_arguments(self.mounts)) - return arguments - - def kernel_command_line(self, user_command_line: str = "") -> str: - if "\0" in user_command_line: - raise ScriptError("kernel command line contains an embedded NUL") - for token in user_command_line.split(): - if token.startswith("nvx_"): - raise ScriptError( - f"{token.split('=', 1)[0]} is owned by the sandbox profile" - ) - if token.startswith("virtfs_"): - raise ScriptError( - f"{token.split('=', 1)[0]} is owned by the sandbox --mount option" - ) - tokens = [user_command_line.strip(), "nvx_sandbox=1"] - for layer in self.ordered_layers(): - tokens.append( - "nvx_layer=" - f"{layer.role},0x{BLOCK_MMIO_BASES[layer.role]:x},{layer.uuid}" - ) - tokens.extend( - ( - f"nvx_scratch=0x{BLOCK_MMIO_BASES['scratch']:x},ext4", - f"nvx_entrypoint={self.entrypoint}", - f"nvx_hostname={self.hostname}", - ) - ) - tokens.extend(f"nvx_arg={argument}" for argument in self.args) - if self.memory_max is not None: - tokens.append(f"nvx_memory_max={self.memory_max}") - if self.pids_max is not None: - tokens.append(f"nvx_pids_max={self.pids_max + 1}") - if mounts_use_aggregate(self.mounts): - tokens.extend( - aggregate_share_tokens( - [ - (mount.guest_target, mount.access, mount.kind) - for mount in self.mounts - ] - ) - ) - command_line = " ".join(token for token in tokens if token) - # OpenVMM appends the live-share bootstrap tokens after this command line, - # so they consume the same x86 budget. - mount_fragment = mounts_command_line_fragment(self.mounts) - if ( - len(command_line.encode("utf-8")) + len(mount_fragment.encode("utf-8")) + 1 - > SANDBOX_COMMAND_LINE_MAX_SIZE - ): - raise ScriptError( - "sandbox kernel command line exceeds its 1024-byte x86 budget; " - "attach fewer --mount shares or use shorter targets and arguments" - ) - return command_line - - -def _reject_disk_path(path: Path) -> None: - value = os.fspath(path) - if any(character in value for character in ",;"): - raise ScriptError( - f"disk paths containing commas or semicolons are unsupported: {value}" - ) diff --git a/scripts/nvx_tools/sandbox_lifecycle.py b/scripts/nvx_tools/sandbox_lifecycle.py deleted file mode 100644 index 0163216ea..000000000 --- a/scripts/nvx_tools/sandbox_lifecycle.py +++ /dev/null @@ -1,1321 +0,0 @@ -"""Persistent lifecycle operations for managed NVX microVM sandboxes.""" - -from __future__ import annotations - -import contextlib -import errno -import json -import math -import os -import secrets -import stat -import subprocess -import time -import uuid -from pathlib import Path -from typing import Any, cast - -from .build_constants import ( - AlpineBuildConstants, - KernelBuildConstants, -) -from .common import ( - ScriptError, - artifact_path, - openvmm_binary_path, - path_exists, - require_file, -) -from .control_session import ( - MANAGED_EXIT_CATEGORIES, - ControlEndpointClosed, - ControlSession, - ManagedExecResult, - capability_pipe, -) -from .sandbox import SandboxLaunch, SandboxLayer, SandboxMount - -CONFIG_NAME = "config.json" -RUNTIME_NAME = "runtime.json" -CAPABILITY_NAME = "control.capability" -LOG_NAME = "openvmm.log" -CONTROL_SOCKET_NAME = "control.sock" -OUTCOME_NAME = "outcome.json" -LOCK_NAME = "lifecycle.lock" -# Files that exist only from the start that launches an OpenVMM process until the -# stop or failed start that ends it, whether or not the runtime record names it. -RUNTIME_FILE_NAMES = (RUNTIME_NAME, CAPABILITY_NAME, CONTROL_SOCKET_NAME) -# How often a caller retries a lifecycle lock that another caller holds, and the -# removal of a state directory that a removed lock file still keeps. -LOCK_RETRY_INTERVAL = 0.025 -# Deprovision writes this into the lock file before it removes the file, so that a -# waiter that then acquires the removed file knows the directory is going away. -LOCK_TOMBSTONE = b"deprovisioned\n" -# The errors with which rmdir() reports a directory that is not empty. -DIRECTORY_NOT_EMPTY = (errno.ENOTEMPTY, errno.EEXIST) -STATE_FORMAT = 1 -CONFIG_FORMAT = 1 -# Format-1 readers ignore unknown fields, so a configuration with a live share -# uses a format that older NVX releases reject instead of starting without it. -MOUNT_CONFIG_FORMAT = 2 -# Likewise, format-2 readers would start a caller-owned share as the VMM. -OWNER_CONFIG_FORMAT = 3 -# And earlier readers know only the single `mount` share, so a configuration -# with several shares lists them under `mounts` in a format they reject. -MULTI_MOUNT_CONFIG_FORMAT = 4 -# Format-4 readers would ignore allowed and writable paths and start a share -# without them, writable everywhere or with its allowed paths hidden, so a -# configuration with either lists every share under `mounts` in a format that -# they reject. -POLICY_CONFIG_FORMAT = 5 -# Format-5 readers know only directory shares, so a configuration with a file -# share lists every share under `mounts`, with its kind, in a format that they -# reject rather than share a directory that later appears at the file's path. -FILE_MOUNT_CONFIG_FORMAT = 6 -CONFIG_FORMATS = ( - CONFIG_FORMAT, - MOUNT_CONFIG_FORMAT, - OWNER_CONFIG_FORMAT, - MULTI_MOUNT_CONFIG_FORMAT, - POLICY_CONFIG_FORMAT, - FILE_MOUNT_CONFIG_FORMAT, -) -# The formats that list every share under `mounts`. -MOUNTS_CONFIG_FORMATS = ( - MULTI_MOUNT_CONFIG_FORMAT, - POLICY_CONFIG_FORMAT, - FILE_MOUNT_CONFIG_FORMAT, -) -OUTCOME_SCHEMA_VERSION = 1 - - -def _write_json(path: Path, value: dict[str, Any], mode: int = 0o600) -> None: - temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") - try: - temporary.write_text( - json.dumps(value, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - os.chmod(temporary, mode) - temporary.replace(path) - finally: - temporary.unlink(missing_ok=True) - - -def _read_json( - path: Path, - description: str, - *, - version_field: str = "format", - version: int | tuple[int, ...] = STATE_FORMAT, -) -> dict[str, Any]: - try: - value = json.loads(path.read_text(encoding="utf-8")) - except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: - raise ScriptError(f"failed to read {description}: {path}") from error - if not isinstance(value, dict): - raise ScriptError(f"{description} has an unsupported format: {path}") - typed = cast(dict[str, Any], value) - accepted = (version,) if isinstance(version, int) else version - if typed.get(version_field) not in accepted: - raise ScriptError(f"{description} has an unsupported format: {path}") - return typed - - -def _outcome_destination(path: Path) -> Path: - candidate = path if path.is_absolute() else Path.cwd() / path - parent = candidate.parent - if parent.is_symlink() or not parent.is_dir(): - raise ScriptError(f"outcome report parent is not a plain directory: {parent}") - if not candidate.name: - raise ScriptError("outcome report path has no filename") - resolved = parent.resolve() / candidate.name - if path_exists(resolved): - raise ScriptError(f"outcome report already exists: {resolved}") - return resolved - - -def validate_outcome_destination(path: Path) -> None: - _outcome_destination(path) - - -def _write_new_json(path: Path, value: dict[str, Any]) -> None: - resolved = _outcome_destination(path) - temporary = resolved.with_name(f".{resolved.name}.{uuid.uuid4().hex}.tmp") - flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0) - try: - descriptor = os.open(temporary, flags, 0o600) - except FileExistsError as error: - raise ScriptError("failed to reserve an outcome report staging file") from error - try: - with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as output: - json.dump(value, output, indent=2, sort_keys=True) - output.write("\n") - output.flush() - os.fsync(output.fileno()) - try: - os.link(temporary, resolved) - except FileExistsError as error: - raise ScriptError(f"outcome report already exists: {resolved}") from error - except OSError as error: - raise ScriptError( - f"failed to publish outcome report: {resolved}" - ) from error - finally: - temporary.unlink(missing_ok=True) - - -def _read_openvmm_outcome(path: Path) -> dict[str, Any]: - typed = _read_json( - path, - "OpenVMM outcome report", - version_field="schema_version", - version=OUTCOME_SCHEMA_VERSION, - ) - for name in ("outcome", "network_policy", "teardown"): - if not isinstance(typed.get(name), dict): - raise ScriptError( - f"OpenVMM outcome report has an invalid {name} section: {path}" - ) - return typed - - -def write_exec_outcome(path: Path, result: ManagedExecResult) -> None: - if result.category not in MANAGED_EXIT_CATEGORIES: - raise ScriptError("managed workload returned an unsupported outcome category") - if not -(2**31) <= result.returncode < 2**31: - raise ScriptError("managed workload returned an out-of-range status") - _write_new_json( - path, - { - "schema_version": OUTCOME_SCHEMA_VERSION, - "operation_id": secrets.token_hex(16), - "outcome": { - "operation": "exec", - "category": result.category, - "status_code": result.returncode, - }, - }, - ) - - -def _prepare_state_directory(path: Path, *, create: bool) -> Path: - resolved = path.resolve() - if create: - resolved.mkdir(mode=0o700, parents=True, exist_ok=True) - os.chmod(resolved, 0o700) - if resolved.is_symlink() or not resolved.is_dir(): - raise ScriptError(f"sandbox state path is not a plain directory: {resolved}") - return resolved - - -def _open_lock_file(path: Path) -> int: - """Opens the lifecycle lock file of a state directory, creating it if needed. - - Deprovision writes to the file, so it must be a regular file that no other - name links, rather than a link to a file elsewhere. - """ - try: - descriptor = _open_lock_path(path) - except OSError as error: - if error.errno != errno.ELOOP: - raise - raise ScriptError( - f"sandbox lifecycle lock is not a standalone regular file: {path}" - ) from error - try: - status = os.fstat(descriptor) - unsafe = not stat.S_ISREG(status.st_mode) or status.st_nlink != 1 - if os.name == "nt": - # The handle refers to a symbolic link or junction, not its target. - attributes = status.st_file_attributes - unsafe = unsafe or bool(attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT) - if unsafe: - raise ScriptError( - f"sandbox lifecycle lock is not a standalone regular file: {path}" - ) - except BaseException: - os.close(descriptor) - raise - return descriptor - - -def _open_lock_path(path: Path) -> int: - """Opens or creates path, without following a link that it names. - - On Windows, the file is shared for deletion, so that deprovision can remove it - while it holds the lock and other callers wait for it. - """ - if os.name != "nt": - return os.open(path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600) - - import ctypes - import msvcrt - from ctypes import wintypes - - generic_read = 0x80000000 - generic_write = 0x40000000 - file_share_read_write_delete = 0x7 - open_always = 4 - file_attribute_normal = 0x80 - file_flag_open_reparse_point = 0x00200000 - kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) - kernel32.CreateFileW.argtypes = ( - wintypes.LPCWSTR, - wintypes.DWORD, - wintypes.DWORD, - wintypes.LPVOID, - wintypes.DWORD, - wintypes.DWORD, - wintypes.HANDLE, - ) - kernel32.CreateFileW.restype = wintypes.HANDLE - kernel32.CloseHandle.argtypes = (wintypes.HANDLE,) - kernel32.CloseHandle.restype = wintypes.BOOL - handle = kernel32.CreateFileW( - os.fspath(path), - generic_read | generic_write, - file_share_read_write_delete, - None, - open_always, - file_attribute_normal | file_flag_open_reparse_point, - None, - ) - if handle is None or handle == wintypes.HANDLE(-1).value: - raise ctypes.WinError(ctypes.get_last_error()) - try: - return msvcrt.open_osfhandle(handle, os.O_RDWR) - except BaseException: - kernel32.CloseHandle(handle) - raise - - -def _try_lock_file(descriptor: int) -> bool: - """Takes the exclusive lock of an open lock file unless another caller holds it.""" - if os.name != "nt": - import fcntl - - try: - fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) - except BlockingIOError: - return False - return True - - import msvcrt - - try: - # A new descriptor is at offset 0, so this locks the file's first byte. - msvcrt.locking(descriptor, msvcrt.LK_NBLCK, 1) - except OSError as error: - if error.errno in (errno.EACCES, errno.EDEADLOCK): - return False - raise - return True - - -def _unlock_file(descriptor: int) -> None: - if os.name != "nt": - import fcntl - - fcntl.flock(descriptor, fcntl.LOCK_UN) - return - - import msvcrt - - msvcrt.locking(descriptor, msvcrt.LK_UNLCK, 1) - - -def _names_open_file(path: Path, descriptor: int) -> bool: - """Returns whether path still names the file open as descriptor. - - Without POSIX deletion semantics, Windows keeps a removed file in its - directory until its last handle closes, and denies access to it meanwhile. - """ - try: - current = os.stat(path) - except FileNotFoundError: - return False - except PermissionError: - if os.name == "nt": - return False - raise - return os.path.samestat(current, os.fstat(descriptor)) - - -def _read_lock_file(descriptor: int, size: int) -> bytes: - os.lseek(descriptor, 0, os.SEEK_SET) - try: - data = b"" - while len(data) < size: - chunk = os.read(descriptor, size - len(data)) - if not chunk: - break - data += chunk - return data - finally: - os.lseek(descriptor, 0, os.SEEK_SET) - - -def _write_lock_file(descriptor: int, data: bytes) -> None: - os.lseek(descriptor, 0, os.SEEK_SET) - try: - remaining = memoryview(data) - while remaining: - written = os.write(descriptor, remaining) - if not written: - raise OSError( - errno.EIO, "sandbox lifecycle lock write made no progress" - ) - remaining = remaining[written:] - finally: - # msvcrt.locking() unlocks from the file position, as it locked. - os.lseek(descriptor, 0, os.SEEK_SET) - - -def _pause(deadline: float) -> bool: - """Waits for the next retry of a step that deadline bounds. - - Returns False once the deadline has passed, so that no retry starts after it. - """ - remaining = deadline - time.monotonic() - if remaining <= 0: - return False - time.sleep(min(LOCK_RETRY_INTERVAL, remaining)) - return time.monotonic() < deadline - - -def _transition_in_progress() -> TimeoutError: - return TimeoutError( - "another lifecycle transition of the sandbox is still in progress" - ) - - -def _directory_status(path: Path) -> os.stat_result | None: - try: - return os.lstat(path) - except OSError: - return None - - -def _hold_directory(path: Path) -> tuple[os.stat_result, int | None]: - """Returns the identity of a directory, and a descriptor that keeps it if any. - - On POSIX systems, the open descriptor keeps a directory that replaces this one - from reusing its inode number. NTFS and ReFS do not reuse file IDs. - """ - if os.name == "nt": - return os.lstat(path), None - descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) - try: - return os.fstat(descriptor), descriptor - except BaseException: - os.close(descriptor) - raise - - -def _wait_for_handoff( - state_dir: Path, removing: os.stat_result, deadline: float -) -> None: - """Waits while lingering lock files keep the directory that deprovision removes. - - The wait ends once the path names no directory or another one, or the - directory holds anything else, such as a provisioned sandbox, which makes - deprovision fail. Once nothing keeps the directory, the caller removes it - itself, in case deprovision has given up, as it provisions the path again. - """ - while True: - current = _directory_status(state_dir) - if current is None or not os.path.samestat(current, removing): - return - try: - entries = tuple(state_dir.iterdir()) - except OSError: - return - if not all(_lingers(entry) for entry in entries): - return - if not entries: - try: - state_dir.rmdir() - except OSError as error: - if error.errno not in DIRECTORY_NOT_EMPTY: - return - else: - return - if not _pause(deadline): - raise _transition_in_progress() - - -def _lingers(path: Path) -> bool: - """Returns whether path is a removed lock file that a handle still keeps. - - NFS renames a removed file that is still open to `.nfs*`, and Windows file - systems without POSIX deletion keep it under its own name. A transition that - starts after deprovision removed the lock file also holds a lock file there - until it finds the sandbox gone. - """ - return path.name == LOCK_NAME or path.name.startswith(".nfs") - - -class _LifecycleLock: - """Exclusive lock that serializes the lifecycle transitions of a state directory. - - `provision`, `start`, `stop`, and `deprovision` hold it from their first look - at the state until they return, so overlapping transitions take effect one at - a time, and each waits at most its timeout for another to finish. The one - exception is deprovision, which releases it before it retries removing a - directory that a removed lock file still keeps. `exec` does not take it, so a - long workload cannot delay `stop`. - - A provisioned state directory keeps the lock file. A release removes it from - any other directory, such as one that deprovision emptied or that never held a - sandbox. A caller that acquires a lock file that its holder removed meanwhile - holds no lock. Unless it provisions the sandbox, it then retries only if the - sandbox is still provisioned, so that it creates no lock file in a directory - that deprovision is removing. A provision that finds that deprovision removed - the lock file, or the whole state directory, provisions a new directory once - the old one is gone, unless another provision has provisioned it first. - """ - - def __init__(self, state_dir: Path, timeout: float, *, provision: bool = False): - if not 0 < timeout < math.inf: - raise ValueError("lifecycle timeout must be finite and greater than 0") - self._state_dir = state_dir - self._path = state_dir / LOCK_NAME - self._timeout = timeout - self._provision = provision - self._descriptor: int | None = None - - def __enter__(self) -> _LifecycleLock: - deadline = time.monotonic() + self._timeout - while True: - try: - descriptor = _open_lock_file(self._path) - except FileNotFoundError as error: - # Deprovision removed the state directory meanwhile, which only a - # provision recreates. - if not self._provision: - raise ScriptError( - "sandbox state path is not a plain directory: " - f"{self._state_dir}" - ) from error - if time.monotonic() >= deadline: - raise _transition_in_progress() from error - _prepare_state_directory(self._state_dir, create=True) - continue - except PermissionError: - # Without POSIX deletion semantics, Windows denies opening a removed - # lock file until its last handle closes. - if not self._provision or os.name != "nt" or not _pause(deadline): - raise - continue - try: - while not _try_lock_file(descriptor): - if not _pause(deadline): - raise _transition_in_progress() - if _names_open_file(self._path, descriptor): - self._descriptor = descriptor - return self - tombstone = _read_lock_file(descriptor, len(LOCK_TOMBSTONE)) - # Deprovision leaves the mark only if lingering lock files, such as - # the one that this handle may keep, kept it from removing the - # directory, which the path then still names. - removing = ( - _directory_status(self._state_dir) - if self._provision and tombstone == LOCK_TOMBSTONE - else None - ) - except BaseException: - os.close(descriptor) - raise - os.close(descriptor) - if not self._provision: - if not path_exists(self._state_dir / CONFIG_NAME): - raise ScriptError("sandbox is not provisioned") - elif removing is not None: - _wait_for_handoff(self._state_dir, removing, deadline) - if time.monotonic() >= deadline: - raise _transition_in_progress() - - def remove(self) -> None: - """Removes the held lock file, marking it so that waiters know why. - - If the removal fails, the mark goes again, as deprovision then keeps the - directory. - """ - assert self._descriptor is not None - try: - _write_lock_file(self._descriptor, LOCK_TOMBSTONE) - self._path.unlink() - except BaseException: - self.retract() - raise - - def retract(self) -> None: - """Clears the mark of the removed lock file, so that waiters retry at once. - - Deprovision retracts it before it releases the lock, unless lingering lock - files keep the directory, since its waiters then have no removal left to - wait for. - """ - assert self._descriptor is not None - with contextlib.suppress(OSError): - _write_lock_file(self._descriptor, bytes(len(LOCK_TOMBSTONE))) - - def __exit__(self, *_exception: object) -> None: - self.release() - - def release(self) -> None: - descriptor = self._descriptor - if descriptor is None: - return - self._descriptor = None - try: - if not path_exists(self._state_dir / CONFIG_NAME): - with contextlib.suppress(OSError): - if _names_open_file(self._path, descriptor): - self._path.unlink() - finally: - # Closing the descriptor releases the lock even if the unlock fails. - with contextlib.suppress(OSError): - _unlock_file(descriptor) - os.close(descriptor) - - -def _serialize_launch( - launch: SandboxLaunch, - *, - hypervisor: str, - memory_mib: int, - net: str | None, - network_profile: str | None, - network_egress: str | None, - network_ingress: str | None, - network_egress_allow: tuple[str, ...], - network_egress_deny: tuple[str, ...], - host_loopback: str | None, - network_proxy: str | None, - host_loopback_forward: tuple[str, ...], - cmdline: str, -) -> dict[str, Any]: - config: dict[str, Any] = { - "format": _config_format(launch.mounts), - "layers": [ - { - "role": layer.role, - "path": os.fspath(layer.path.resolve()), - "uuid": layer.uuid, - } - for layer in launch.ordered_layers() - ], - "scratch": os.fspath(launch.scratch.resolve()), - "hostname": launch.hostname, - "workload_uid": launch.workload_identity[0], - "workload_gid": launch.workload_identity[1], - "memory_max": launch.memory_max, - "pids_max": launch.pids_max, - "hypervisor": hypervisor, - "memory_mib": memory_mib, - "net": net, - "network_profile": network_profile, - "network_egress": network_egress, - "network_ingress": network_ingress, - "network_egress_allow": list(network_egress_allow), - "network_egress_deny": list(network_egress_deny), - "host_loopback": host_loopback, - "network_proxy": network_proxy, - "host_loopback_forward": list(host_loopback_forward), - "cmdline": cmdline, - } - config_format = _config_format(launch.mounts) - if config_format in MOUNTS_CONFIG_FORMATS: - config["mounts"] = [ - _serialize_mount( - mount, - policy=config_format != MULTI_MOUNT_CONFIG_FORMAT, - kind=config_format == FILE_MOUNT_CONFIG_FORMAT, - ) - for mount in launch.mounts - ] - else: - config["mount"] = _serialize_mount(launch.mounts[0]) if launch.mounts else None - return config - - -def _config_format(mounts: tuple[SandboxMount, ...]) -> int: - if any(mount.kind == "file" for mount in mounts): - return FILE_MOUNT_CONFIG_FORMAT - if any(mount.allowed_paths or mount.writable_paths for mount in mounts): - return POLICY_CONFIG_FORMAT - if len(mounts) > 1: - return MULTI_MOUNT_CONFIG_FORMAT - if len(mounts) == 1 and mounts[0].owner == "caller": - return OWNER_CONFIG_FORMAT - return MOUNT_CONFIG_FORMAT if mounts else CONFIG_FORMAT - - -def _serialize_mount( - mount: SandboxMount, *, policy: bool = False, kind: bool = False -) -> dict[str, Any]: - absolute = mount.absolute() - serialized: dict[str, Any] = { - "guest_target": absolute.guest_target, - "host_path": os.fspath(absolute.host_path), - "access": absolute.access, - "denied_paths": list(absolute.denied_paths), - "owner": absolute.owner, - } - if policy: - serialized["allowed_paths"] = list(absolute.allowed_paths) - serialized["writable_paths"] = list(absolute.writable_paths) - if kind: - serialized["kind"] = absolute.kind - return serialized - - -def _deserialize_paths(mount: dict[str, Any], key: str) -> tuple[str, ...]: - paths = mount[key] - if not isinstance(paths, list): - raise TypeError(f"sandbox mount {key.replace('_', ' ')} must be a list") - return tuple(str(path) for path in cast(list[object], paths)) - - -def _deserialize_mount( - value: object, *, policy: bool = False, kind: bool = False -) -> SandboxMount: - if not isinstance(value, dict): - raise TypeError("sandbox mount configuration must be an object") - mount = cast(dict[str, Any], value) - return SandboxMount( - guest_target=str(mount["guest_target"]), - host_path=Path(str(mount["host_path"])), - access=str(mount["access"]), - denied_paths=_deserialize_paths(mount, "denied_paths"), - # Configurations written before ownership modes ran shares as the VMM. - owner=str(mount.get("owner", "vmm")), - allowed_paths=_deserialize_paths(mount, "allowed_paths") if policy else (), - writable_paths=_deserialize_paths(mount, "writable_paths") if policy else (), - kind=str(mount["kind"]) if kind else "directory", - ) - - -def _deserialize_mounts(config: dict[str, Any]) -> tuple[SandboxMount, ...]: - config_format = config.get("format") - if config_format in MOUNTS_CONFIG_FORMATS: - mounts = config["mounts"] - if not isinstance(mounts, list): - raise TypeError("sandbox mounts configuration must be a list") - return tuple( - _deserialize_mount( - mount, - policy=config_format != MULTI_MOUNT_CONFIG_FORMAT, - kind=config_format == FILE_MOUNT_CONFIG_FORMAT, - ) - for mount in cast(list[object], mounts) - ) - mount = config.get("mount") - return () if mount is None else (_deserialize_mount(mount),) - - -def _deserialize_launch(config: dict[str, Any]) -> SandboxLaunch: - try: - layers = tuple( - SandboxLayer( - role=str(layer["role"]), - path=Path(str(layer["path"])), - uuid=str(layer["uuid"]), - ) - for layer in config["layers"] - ) - identity = (int(config["workload_uid"]), int(config["workload_gid"])) - launch = SandboxLaunch( - layers=layers, - scratch=Path(str(config["scratch"])), - hostname=str(config["hostname"]), - workload_identity=identity, - memory_max=( - None if config["memory_max"] is None else int(config["memory_max"]) - ), - pids_max=None if config["pids_max"] is None else int(config["pids_max"]), - mounts=_deserialize_mounts(config), - ) - except (KeyError, TypeError, ValueError) as error: - raise ScriptError("sandbox configuration is malformed") from error - if config.get("format") != _config_format(launch.mounts): - raise ScriptError("sandbox configuration format does not match its mounts") - return launch.validated() - - -def _linux_start_time(stat: bytes) -> int | None: - # The command name may contain spaces and parentheses, so the fields after it - # start at its last ")", with the state as field 3 and starttime as field 22. - end = stat.rfind(b")") - fields = stat[end + 1 :].split() if end >= 0 else [] - if len(fields) < 20 or not fields[19].isdigit(): - raise ValueError("process status has an unexpected format") - if fields[0] in (b"Z", b"X"): - return None - return int(fields[19]) - - -def _process_start_time(pid: int) -> int | None: - """Returns the start time of a live process, or None if no live process has the ID. - - A zombie counts as exited. On Windows, so does a process that the caller cannot - open: OpenVMM runs as the caller, so such a process cannot be its VM. Raises - OSError or ValueError when the state cannot be determined. - """ - if os.name != "nt": - try: - stat = Path(f"/proc/{pid}/stat").read_bytes() - except (FileNotFoundError, ProcessLookupError): - return None - return _linux_start_time(stat) - - import ctypes - from ctypes import wintypes - - synchronize = 0x00100000 - process_query_limited_information = 0x1000 - error_access_denied = 5 - error_invalid_parameter = 87 - wait_object_0 = 0 - wait_timeout = 0x102 - if pid > 0xFFFFFFFF: - return None - kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) - kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD) - kernel32.OpenProcess.restype = wintypes.HANDLE - kernel32.WaitForSingleObject.argtypes = (wintypes.HANDLE, wintypes.DWORD) - kernel32.WaitForSingleObject.restype = wintypes.DWORD - filetime = ctypes.POINTER(wintypes.FILETIME) - kernel32.GetProcessTimes.argtypes = ( - wintypes.HANDLE, - filetime, - filetime, - filetime, - filetime, - ) - kernel32.GetProcessTimes.restype = wintypes.BOOL - kernel32.CloseHandle.argtypes = (wintypes.HANDLE,) - kernel32.CloseHandle.restype = wintypes.BOOL - handle = kernel32.OpenProcess( - synchronize | process_query_limited_information, False, pid - ) - if not handle: - error = ctypes.get_last_error() - if error in (error_access_denied, error_invalid_parameter): - return None - raise ctypes.WinError(error) - try: - wait = kernel32.WaitForSingleObject(handle, 0) - if wait == wait_object_0: - return None - if wait != wait_timeout: - raise ctypes.WinError(ctypes.get_last_error()) - created = wintypes.FILETIME() - unused = wintypes.FILETIME() - if not kernel32.GetProcessTimes( - handle, - ctypes.byref(created), - ctypes.byref(unused), - ctypes.byref(unused), - ctypes.byref(unused), - ): - raise ctypes.WinError(ctypes.get_last_error()) - return (created.dwHighDateTime << 32) | created.dwLowDateTime - finally: - kernel32.CloseHandle(handle) - - -def _process_running(pid: int, start_time: int | None) -> bool: - """Returns whether the recorded OpenVMM process still runs. - - A record without a start time, written by an earlier NVX version, identifies - OpenVMM by its process ID alone. - """ - if pid <= 0: - return False - try: - current = _process_start_time(pid) - except (OSError, ValueError) as error: - raise ScriptError( - f"cannot determine whether OpenVMM process {pid} is running: {error}" - ) from error - if start_time is None: - return current is not None - return current == start_time - - -def _runtime_process(runtime: dict[str, Any]) -> tuple[int, int | None]: - try: - pid = int(runtime["pid"]) - except (KeyError, TypeError, ValueError) as error: - raise ScriptError("sandbox runtime state has an invalid process ID") from error - start_time = runtime.get("start_time") - if start_time is not None and ( - not isinstance(start_time, int) - or isinstance(start_time, bool) - or start_time < 0 - ): - raise ScriptError("sandbox runtime state has an invalid process start time") - return pid, start_time - - -def runtime_process_running(runtime: dict[str, Any]) -> bool: - """Returns whether the OpenVMM process of a runtime record still runs.""" - return _process_running(*_runtime_process(runtime)) - - -def _load_running(state_dir: Path) -> tuple[dict[str, Any], bytes]: - runtime_path = state_dir / RUNTIME_NAME - if not runtime_path.is_file(): - raise ScriptError("sandbox is not running") - runtime = _read_json(runtime_path, "sandbox runtime state") - if not _process_running(*_runtime_process(runtime)): - raise ScriptError( - "sandbox runtime state is stale because the OpenVMM process is not running" - ) - capability = require_file( - state_dir / CAPABILITY_NAME, "sandbox control capability" - ).read_bytes() - if len(capability) != 32 or capability == bytes(32): - raise ScriptError("sandbox control capability is invalid") - return runtime, capability - - -def _endpoint(runtime: dict[str, Any]) -> Path: - try: - return Path(str(runtime["control_endpoint"])) - except KeyError as error: - raise ScriptError("sandbox runtime state has no control endpoint") from error - - -def _end_failed_start(process: subprocess.Popen[bytes], grace: float) -> int | None: - """Ends the OpenVMM process of a failed start. - - Returns OpenVMM's exit status if it exits on its own within `grace` seconds, - or terminates it and returns None. Terminating OpenVMM while it publishes its - outcome report would leave the report's staging file in the state directory, - which `deprovision` refuses to remove. - """ - try: - if grace > 0: - process.wait(timeout=grace) - except subprocess.TimeoutExpired: - pass - finally: - status = process.poll() - if status is None: - process.terminate() - try: - process.wait(timeout=5) - except subprocess.TimeoutExpired: - process.kill() - process.wait(timeout=5) - return status - - -def _startup_exit_message(outcome_path: Path, status: int) -> str: - """Describes how OpenVMM ended a start, preferring its outcome report. - - The report keeps the guest's own status, such as the one with which it - refuses an unavailable workload identity. - """ - try: - outcome: dict[str, Any] = _read_openvmm_outcome(outcome_path)["outcome"] - except ScriptError: - outcome = {} - category = outcome.get("category") - status_code = outcome.get("status_code") - if isinstance(category, str) and type(status_code) is int: - return f"OpenVMM exited during startup: {category} status {status_code}" - return f"OpenVMM exited during startup with status {status}" - - -def provision( - state_path: Path, - launch: SandboxLaunch, - *, - hypervisor: str, - memory_mib: int, - net: str | None, - network_profile: str | None, - network_egress: str | None, - network_ingress: str | None, - network_egress_allow: tuple[str, ...], - network_egress_deny: tuple[str, ...], - host_loopback: str | None, - network_proxy: str | None, - host_loopback_forward: tuple[str, ...], - cmdline: str, - timeout: float, -) -> None: - state_dir = _prepare_state_directory(state_path, create=True) - config_path = state_dir / CONFIG_NAME - runtime_path = state_dir / RUNTIME_NAME - with _LifecycleLock(state_dir, timeout, provision=True): - if config_path.exists() or runtime_path.exists(): - raise ScriptError("sandbox is already provisioned") - _write_json( - config_path, - _serialize_launch( - launch.validated(), - hypervisor=hypervisor, - memory_mib=memory_mib, - net=net, - network_profile=network_profile, - network_egress=network_egress, - network_ingress=network_ingress, - network_egress_allow=network_egress_allow, - network_egress_deny=network_egress_deny, - host_loopback=host_loopback, - network_proxy=network_proxy, - host_loopback_forward=host_loopback_forward, - cmdline=cmdline, - ), - ) - - -def start(state_path: Path, timeout: float) -> None: - state_dir = _prepare_state_directory(state_path, create=False) - with _LifecycleLock(state_dir, timeout): - _start(state_dir, timeout) - - -def _create_capability(path: Path, capability: bytes) -> None: - """Creates the control capability file of a start, which only it may hold.""" - flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0) - try: - descriptor = os.open(path, flags, 0o600) - except FileExistsError as error: - raise ScriptError( - "sandbox is already running or has stale runtime state" - ) from error - try: - with os.fdopen(descriptor, "wb") as output: - output.write(capability) - except BaseException: - path.unlink(missing_ok=True) - raise - - -def _start(state_dir: Path, timeout: float) -> None: - config = _read_json( - require_file(state_dir / CONFIG_NAME, "sandbox configuration"), - "sandbox configuration", - version=CONFIG_FORMATS, - ) - # Any runtime file belongs to an OpenVMM process that may still run, even - # without a runtime record, as after a start that was killed. Without them, - # every runtime file that appears while this start holds the lifecycle lock - # is its own. - if any(path_exists(state_dir / name) for name in RUNTIME_FILE_NAMES): - raise ScriptError("sandbox is already running or has stale runtime state") - outcome_path = state_dir / OUTCOME_NAME - outcome_path.unlink(missing_ok=True) - launch = _deserialize_launch(config) - executable = require_file(openvmm_binary_path(), "OpenVMM release binary") - kernel = require_file( - artifact_path(KernelBuildConstants.BINARY_NAME), "Linux direct kernel" - ) - initrd = require_file( - artifact_path(AlpineBuildConstants.INITRAMFS_NAME), "initramfs" - ) - capability = secrets.token_bytes(32) - if capability == bytes(32): - raise AssertionError("secrets.token_bytes returned an all-zero capability") - endpoint_value = ( - f"//./pipe/openvmm-microvm-{uuid.uuid4().hex}" - if os.name == "nt" - else os.fspath(state_dir / CONTROL_SOCKET_NAME) - ) - command = [ - os.fspath(executable), - *launch.openvmm_arguments(), - "--microvm-lifecycle", - "managed", - "--single-process", - "--hypervisor", - str(config["hypervisor"]), - "--memory", - f"{int(config['memory_mib'])}M", - "--kernel", - os.fspath(kernel), - "--initrd", - os.fspath(initrd), - "--cmdline", - launch.kernel_command_line(str(config["cmdline"])), - "--virtio-console", - "none", - "--microvm-control-console", - f"listen={endpoint_value}", - "--microvm-control-auth-stdin", - "--microvm-report", - os.fspath(outcome_path), - ] - net = config.get("net") - network_profile = config.get("network_profile") - if net is not None: - command.extend(["--net", str(net), "--network-profile", str(network_profile)]) - for name in ("network_egress", "network_ingress", "host_loopback"): - value = config.get(name) - if value is not None: - command.extend([f"--{name.replace('_', '-')}", str(value)]) - for name in ( - "network_egress_allow", - "network_egress_deny", - "host_loopback_forward", - ): - values = config.get(name, []) - if not isinstance(values, list): - raise ScriptError("sandbox configuration is malformed") - for value in cast(list[object], values): - command.extend([f"--{name.replace('_', '-')}", str(value)]) - network_proxy = config.get("network_proxy") - if network_proxy is not None: - command.extend(["--network-proxy", str(network_proxy)]) - - capability_path = state_dir / CAPABILITY_NAME - _create_capability(capability_path, capability) - log_path = state_dir / LOG_NAME - try: - log = log_path.open("ab", buffering=0) - except BaseException: - capability_path.unlink(missing_ok=True) - raise - creationflags = ( - getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) if os.name == "nt" else 0 - ) - process: subprocess.Popen[bytes] | None = None - try: - # OpenVMM reads its capability as soon as it starts, so the capability is - # in the pipe, and the pipe's write end closed, before OpenVMM starts. - capability_input = capability_pipe(capability) - try: - process = subprocess.Popen( - command, - stdin=capability_input, - stdout=log, - stderr=subprocess.STDOUT, - start_new_session=os.name != "nt", - creationflags=creationflags, - ) - finally: - os.close(capability_input) - # Until this process reaps the child or closes its handle, no other process - # can reuse its ID, so this identifies OpenVMM itself. - try: - start_time = _process_start_time(process.pid) - except (OSError, ValueError) as error: - raise ScriptError( - f"cannot identify the OpenVMM process: {error}" - ) from error - if start_time is None: - raise ScriptError( - "OpenVMM exited during startup" - if process.poll() is not None - else "cannot identify the OpenVMM process" - ) - _write_json( - state_dir / RUNTIME_NAME, - { - "format": STATE_FORMAT, - "pid": process.pid, - "start_time": start_time, - "control_endpoint": endpoint_value, - }, - ) - with ControlSession.connect( - Path(endpoint_value), capability, timeout - ) as session: - session.ping(timeout) - except BaseException as error: - status: int | None = None - ended = process is None - try: - if process is not None: - # OpenVMM closes the control endpoint as it tears the VM down, - # before it publishes its outcome report and exits, so only a - # closed endpoint warrants waiting for it. - status = _end_failed_start( - process, timeout if isinstance(error, ControlEndpointClosed) else 0 - ) - ended = True - finally: - # This start found none of these files and has held the lifecycle - # lock since, so they are its own. They stay while its OpenVMM may - # still run, so that the sandbox does not look stopped. The runtime - # record goes last, so that an interrupted cleanup leaves a record - # that shows OpenVMM gone rather than runtime files that deprovision - # cannot vouch for. - if ended: - (state_dir / CONTROL_SOCKET_NAME).unlink(missing_ok=True) - capability_path.unlink(missing_ok=True) - (state_dir / RUNTIME_NAME).unlink(missing_ok=True) - if status is not None and isinstance(error, Exception): - raise ScriptError(_startup_exit_message(outcome_path, status)) from error - raise - finally: - log.close() - - -def exec_workload( - state_path: Path, - arguments: tuple[str, ...], - *, - timeout_ms: int, - response_timeout: float, - cwd: str | None = None, - environment: tuple[str, ...] | None = None, - inherit_default_environment: bool = False, -) -> ManagedExecResult: - state_dir = _prepare_state_directory(state_path, create=False) - runtime, capability = _load_running(state_dir) - with ControlSession.connect( - _endpoint(runtime), capability, response_timeout - ) as session: - return session.exec( - arguments, - timeout_ms=timeout_ms, - response_timeout=response_timeout, - cwd=cwd, - environment=environment, - inherit_default_environment=inherit_default_environment, - ) - - -def stop(state_path: Path, timeout: float) -> dict[str, Any]: - state_dir = _prepare_state_directory(state_path, create=False) - with _LifecycleLock(state_dir, timeout): - return _stop(state_dir, timeout) - - -def _stop(state_dir: Path, timeout: float) -> dict[str, Any]: - runtime, capability = _load_running(state_dir) - pid, start_time = _runtime_process(runtime) - with ControlSession.connect(_endpoint(runtime), capability, timeout) as session: - session.stop(timeout) - deadline = time.monotonic() + timeout - while _process_running(pid, start_time): - if time.monotonic() >= deadline: - raise TimeoutError("OpenVMM did not terminate after managed stop") - time.sleep(0.025) - try: - outcome = _read_openvmm_outcome(state_dir / OUTCOME_NAME) - finally: - # As in a failed start, the runtime record goes last. - (state_dir / CONTROL_SOCKET_NAME).unlink(missing_ok=True) - (state_dir / CAPABILITY_NAME).unlink(missing_ok=True) - (state_dir / RUNTIME_NAME).unlink(missing_ok=True) - return outcome - - -def deprovision(state_path: Path, timeout: float) -> None: - state_dir = _prepare_state_directory(state_path, create=False) - with _LifecycleLock(state_dir, timeout) as lock: - runtime_path = state_dir / RUNTIME_NAME - if runtime_path.is_file(): - runtime = _read_json(runtime_path, "sandbox runtime state") - if _process_running(*_runtime_process(runtime)): - raise ScriptError("sandbox must be stopped before deprovision") - elif any(path_exists(state_dir / name) for name in RUNTIME_FILE_NAMES): - # As start does, refuse runtime files that no record vouches for. - raise ScriptError( - "sandbox has runtime files but no runtime record, so its OpenVMM " - "process may still run: end any OpenVMM process whose arguments " - f"name the state directory, then remove {CAPABILITY_NAME} and " - f"{CONTROL_SOCKET_NAME}" - ) - for name in ( - RUNTIME_NAME, - CAPABILITY_NAME, - CONTROL_SOCKET_NAME, - OUTCOME_NAME, - LOG_NAME, - CONFIG_NAME, - ): - (state_dir / name).unlink(missing_ok=True) - unknown = tuple(path for path in state_dir.iterdir() if path.name != LOCK_NAME) - if unknown: - raise ScriptError( - "sandbox state directory contains files not owned by NVX: " - + ", ".join(path.name for path in unknown) - ) - # Holding the lock until the directory is gone keeps a waiting transition - # from creating a new lock file in it. - lock.remove() - lingering = False - try: - state_dir.rmdir() - except OSError as error: - if error.errno not in DIRECTORY_NOT_EMPTY: - raise - lingering = True - finally: - if not lingering: - lock.retract() - if lingering: - # A removed lock file that a handle still keeps stays in the directory, - # as do the lock files of transitions that start meanwhile. Each - # waiter closes its handle once it finds the sandbox gone, so the - # directory empties after this call releases the lock. - removing, held = _hold_directory(state_dir) - try: - lock.release() - _remove_emptied_state_directory(state_dir, removing, timeout) - finally: - if held is not None: - os.close(held) - - -def _remove_emptied_state_directory( - state_dir: Path, removing: os.stat_result, timeout: float -) -> None: - """Removes a state directory once no lingering lock file keeps it. - - A provision that waits for the removal may complete it and provision the path - again, which ends this attempt too. Any failure other than the directory not - being empty ends the attempt at once. - """ - deadline = time.monotonic() + timeout - retried_at_once = False - while True: - try: - state_dir.rmdir() - return - except FileNotFoundError: - return - except OSError as error: - current = _directory_status(state_dir) - if current is None or not os.path.samestat(current, removing): - return - try: - remaining = tuple(state_dir.iterdir()) - except FileNotFoundError: - return - unexpected = sorted(path.name for path in remaining if not _lingers(path)) - # A provision that starts after this deprovision removed the lock - # file, and before it removed the directory, provisions it again. - if CONFIG_NAME in unexpected: - raise ScriptError( - "sandbox was provisioned again while deprovision removed it" - ) from error - if unexpected: - raise ScriptError( - "sandbox state directory contains files not owned by NVX: " - + ", ".join(unexpected) - ) from error - if error.errno not in DIRECTORY_NOT_EMPTY: - raise - # Without remaining entries, the last lingering lock file went away - # after rmdir failed, so the next attempt follows at once. Another - # such failure right after it pauses first, so that a file system that - # does not list what keeps the directory cannot make this loop spin. - if not remaining and not retried_at_once: - retried_at_once = True - if time.monotonic() >= deadline: - raise - continue - retried_at_once = False - if not _pause(deadline): - raise diff --git a/scripts/nvx_tools/sandbox_lifecycle_tests.py b/scripts/nvx_tools/sandbox_lifecycle_tests.py index 245459920..fdfe944b1 100644 --- a/scripts/nvx_tools/sandbox_lifecycle_tests.py +++ b/scripts/nvx_tools/sandbox_lifecycle_tests.py @@ -28,7 +28,6 @@ from pathlib import Path from typing import Any, cast -from . import sandbox_lifecycle from .build_constants import BuildConstants from .common import sha256_file from .managed_exec_tests import ( @@ -69,13 +68,14 @@ MANAGED_REFUSAL = {"operation": "managed", "category": "guest-exit", "status_code": 125} REFUSED_START = b"error: OpenVMM exited during startup: guest-exit status 125" -CONFIG = sandbox_lifecycle.CONFIG_NAME -RUNTIME = sandbox_lifecycle.RUNTIME_NAME -CAPABILITY = sandbox_lifecycle.CAPABILITY_NAME -LOG = sandbox_lifecycle.LOG_NAME -SOCKET = sandbox_lifecycle.CONTROL_SOCKET_NAME -OUTCOME = sandbox_lifecycle.OUTCOME_NAME -LOCK = sandbox_lifecycle.LOCK_NAME +# The files of a state directory, as nvx.py sandbox documents them. +CONFIG = "config.json" +RUNTIME = "runtime.json" +CAPABILITY = "control.capability" +LOG = "openvmm.log" +SOCKET = "control.sock" +OUTCOME = "outcome.json" +LOCK = "lifecycle.lock" PROVISIONED_ENTRIES = frozenset({CONFIG, LOCK}) # Windows hosts serve the control endpoint as a named pipe outside the state. RUNNING_ENTRIES = frozenset( @@ -125,6 +125,13 @@ def openvmm_processes(marker: str) -> tuple[int, ...]: return tuple(sorted(processes)) +def openvmm_running(runtime: dict[str, Any], marker: str) -> bool: + """Returns whether the OpenVMM process of a runtime record still runs: an + OpenVMM process with its ID whose arguments contain marker, the name of + the fixture, which no other process names.""" + return int(runtime["pid"]) in openvmm_processes(marker) + + def control_endpoint_present(runtime: dict[str, Any]) -> bool: endpoint = str(runtime["control_endpoint"]) if sys.platform == "win32": @@ -139,17 +146,18 @@ def terminate_process(pid: int) -> None: os.kill(pid, signal.SIGKILL) -def kill_openvmm(runtime: dict[str, Any], timeout: float) -> None: - """Ends the OpenVMM process of a runtime record, as a crash would.""" - if sandbox_lifecycle.runtime_process_running(runtime): +def kill_openvmm(runtime: dict[str, Any], timeout: float, marker: str) -> None: + """Ends the OpenVMM process of a runtime record, as a crash would; marker + names the fixture, as for openvmm_running.""" + if openvmm_running(runtime, marker): try: terminate_process(int(runtime["pid"])) except OSError: # The process may have exited since the check. - if sandbox_lifecycle.runtime_process_running(runtime): + if openvmm_running(runtime, marker): raise deadline = time.monotonic() + timeout - while sandbox_lifecycle.runtime_process_running(runtime): + while openvmm_running(runtime, marker): if time.monotonic() >= deadline: raise RuntimeError(f"OpenVMM process {runtime['pid']} survived its kill") time.sleep(0.05) @@ -172,10 +180,50 @@ def state_entries(state: Path) -> list[str] | None: def managed_outcome(state: Path) -> object: """Returns the outcome section of the OpenVMM report in a state directory.""" + return managed_report(state).get("outcome") + + +def managed_report(state: Path) -> dict[str, object]: + """Returns the OpenVMM report in a state directory, or an empty mapping if + it is not an object.""" report: object = json.loads((state / OUTCOME).read_text(encoding="utf-8")) if not isinstance(report, dict): - return None - return cast(dict[str, object], report).get("outcome") + return {} + return cast(dict[str, object], report) + + +def check_managed_report(report: dict[str, object], backend: str) -> None: + """Checks the report of a managed sandbox that stopped cleanly. + + These are the report checks of the direct-mode managed lifecycle + (nvx-rs/tests/managed_lifecycle.rs) that hold for the layered root too: the + report names the backend, no network policy was requested, and every + teardown step completed. + """ + outcome = report.get("outcome") + if outcome != MANAGED_SUCCESS: + raise RuntimeError(f"the managed outcome is {outcome!r}") + if report.get("backend") != backend: + raise RuntimeError( + f"the managed report names backend {report.get('backend')!r}" + ) + policy = report.get("network_policy") + status = ( + cast(dict[str, object], policy).get("status") + if isinstance(policy, dict) + else None + ) + if status != "not-requested": + raise RuntimeError(f"the managed report has network policy status {status!r}") + teardown = report.get("teardown") + if ( + not isinstance(teardown, dict) + or not teardown + or not all( + value is True for value in cast(dict[str, object], teardown).values() + ) + ): + raise RuntimeError(f"the managed report has incomplete teardown: {teardown!r}") def bounded_evidence(data: bytes, omitted: int = 0) -> bytes: @@ -540,7 +588,7 @@ def require_started(self, description: str) -> dict[str, Any]: self.save_evidence( "runtime.json", (json.dumps(runtime, indent=2) + "\n").encode() ) - if not sandbox_lifecycle.runtime_process_running(runtime): + if not openvmm_running(runtime, self.root.name): raise RuntimeError(f"OpenVMM is not running after the {description}") if not control_endpoint_present(runtime): raise RuntimeError( @@ -667,15 +715,13 @@ def check_deprovision_while_running(self, runtime: dict[str, Any]) -> None: def stop(self, runtime: dict[str, Any], layer_digest: str) -> None: self.invoke("stop", state=self.state) self.require_entries(self.state, STOPPED_ENTRIES, "after stop") - if sandbox_lifecycle.runtime_process_running(runtime): + if openvmm_running(runtime, self.root.name): raise RuntimeError("OpenVMM still runs after stop") if control_endpoint_present(runtime): raise RuntimeError("stop left the control endpoint behind") self.require_processes((), "after stop") self.copy_evidence(self.state / OUTCOME, "outcome.json") - outcome = managed_outcome(self.state) - if outcome != MANAGED_SUCCESS: - raise RuntimeError(f"the managed outcome is {outcome!r}") + check_managed_report(managed_report(self.state), self.backend) # A clean journal shows that stop unmounted the overlay and scratch # before the VM powered off. if not scratch_journal_clean(self.scratch): @@ -695,7 +741,7 @@ def stop(self, runtime: dict[str, Any], layer_digest: str) -> None: def check_crash(self, runtime: dict[str, Any]) -> None: # OpenVMM exits without a stop, so its runtime record goes stale. - kill_openvmm(runtime, self.timeout) + kill_openvmm(runtime, self.timeout, self.root.name) self.require_processes((), "after OpenVMM was killed") if sys.platform == "win32" and control_endpoint_present(runtime): raise RuntimeError("the control pipe outlived OpenVMM") @@ -757,7 +803,7 @@ def clean_up(self) -> list[Exception]: if (state / RUNTIME).is_file(): runtime = read_runtime(state) self.run_cli("stop", state=state) - kill_openvmm(runtime, self.timeout) + kill_openvmm(runtime, self.timeout, self.root.name) except Exception as error: errors.append(error) # Every OpenVMM process whose arguments name the fixture is the diff --git a/scripts/nvx_tools/ubuntu.py b/scripts/nvx_tools/ubuntu.py index 8c3a53cdb..74bb97820 100644 --- a/scripts/nvx_tools/ubuntu.py +++ b/scripts/nvx_tools/ubuntu.py @@ -33,6 +33,7 @@ require_tool, sha256_file, ) +from .guest_binaries import guest_binary_source_files _SHA256 = re.compile(r"[0-9a-f]{64}") _PACKAGE_NAME = re.compile(r"[a-z0-9][a-z0-9+.-]*") @@ -1009,6 +1010,7 @@ def customization_files() -> tuple[Path, ...]: if path.is_file() ), UbuntuBuildConstants.PACKAGE_LOCK, + *guest_binary_source_files(), ) @@ -1026,9 +1028,20 @@ def converter_input_sha256(customization_files: Sequence[Path]) -> str: "customization_files": [ { "path": path.relative_to(BuildConstants.REPO_ROOT).as_posix(), - "sha256": sha256_file(path), + "sha256": normalized_sha256_file(path), } - for path in sorted(customization_files) + # In the order of POSIX paths, whose names compare with case, as + # on the Linux hosts that build the artifacts; Windows paths + # compare without it, which put nvx-rs/build.rs before + # nvx-rs/Cargo.toml and changed the digest. + for path in sorted( + customization_files, + key=lambda path: ( + PurePosixPath( + path.relative_to(BuildConstants.REPO_ROOT).as_posix() + ).parts + ), + ) ], } encoded = json.dumps( diff --git a/scripts/publish_development_release.py b/scripts/publish_development_release.py index d6bde64d3..d143b9d16 100644 --- a/scripts/publish_development_release.py +++ b/scripts/publish_development_release.py @@ -13,6 +13,14 @@ ) from nvx_tools.common import ScriptError, require_file from nvx_tools.development_release import publish_development_release +from nvx_tools.release import require_release_package_executables + +# The release platforms and the archive formats of their packages. +PLATFORMS = ( + ("linux-kvm", ".tar.gz"), + ("linux-mshv", ".tar.gz"), + ("windows-whp", ".zip"), +) def _required_environment(name: str) -> str: @@ -22,22 +30,37 @@ def _required_environment(name: str) -> str: return value +def release_packages(directory: Path, version: str) -> dict[str, Path]: + """Return the package archive of each release platform in `directory`.""" + return { + platform: directory / f"nvx-{version}-{platform}{extension}" + for platform, extension in PLATFORMS + } + + +def check_release_packages(packages: dict[str, Path]) -> None: + """Fail unless every package ships the executables of its platform, + OpenVMM and nvx-rs, which `nvx.py download` installs.""" + for platform, package in packages.items(): + require_file(package, f"development release package {package.name}") + require_release_package_executables(package, platform) + + def main() -> None: version_path = require_file(BuildConstants.REPO_ROOT / "VERSION", "NVX version") try: version = version_path.read_text(encoding="ascii").strip() except OSError as error: raise ScriptError(f"failed to read {version_path}: {error}") from error - packages = ( - Path(ReleaseBuildConstants.DIRECTORY_NAME) / f"nvx-{version}-linux-kvm.tar.gz", - Path(ReleaseBuildConstants.DIRECTORY_NAME) / f"nvx-{version}-linux-mshv.tar.gz", - Path(ReleaseBuildConstants.DIRECTORY_NAME) / f"nvx-{version}-windows-whp.zip", - ) + repository = _required_environment("GH_REPO") + target_sha = _required_environment("GITHUB_SHA") + packages = release_packages(Path(ReleaseBuildConstants.DIRECTORY_NAME), version) + check_release_packages(packages) publish_development_release( - _required_environment("GH_REPO"), - _required_environment("GITHUB_SHA"), + repository, + target_sha, version, - packages, + tuple(packages.values()), ) diff --git a/scripts/setup/setup-linux-mshv.sh b/scripts/setup/setup-linux-mshv.sh index b3cef1ad1..15c95b60c 100644 --- a/scripts/setup/setup-linux-mshv.sh +++ b/scripts/setup/setup-linux-mshv.sh @@ -227,9 +227,14 @@ install_rust_tools() { fi rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal + rustup target add --toolchain "$RUST_TOOLCHAIN" \ + x86_64-unknown-linux-musl rust_version=$(RUSTUP_TOOLCHAIN=$RUST_TOOLCHAIN rustc --version | awk '{print $2}') [ "$rust_version" = "$RUST_TOOLCHAIN" ] || die "Rust ${RUST_TOOLCHAIN} is required, found ${rust_version}" + rustup target list --installed --toolchain "$RUST_TOOLCHAIN" | + grep -Fxq x86_64-unknown-linux-musl || + die "Rust target x86_64-unknown-linux-musl is not installed" if ! command -v cargo-nextest >/dev/null 2>&1 || ! cargo nextest --version | grep -Fq "cargo-nextest ${CARGO_NEXTEST_VERSION}"; then cargo +"$RUST_TOOLCHAIN" install --locked cargo-nextest \ diff --git a/scripts/setup/setup-linux-runner.sh b/scripts/setup/setup-linux-runner.sh index a55a00b2e..4080c150e 100644 --- a/scripts/setup/setup-linux-runner.sh +++ b/scripts/setup/setup-linux-runner.sh @@ -596,8 +596,7 @@ install_rust_tools() { run_as_root env \ CARGO_HOME="$trusted_cargo_home" \ RUSTUP_HOME="$trusted_rustup_home" \ - RUSTUP_TOOLCHAIN="$RUST_TOOLCHAIN" \ - "$rustup" target add \ + "$rustup" target add --toolchain "$RUST_TOOLCHAIN" \ x86_64-unknown-none \ x86_64-unknown-linux-musl \ x86_64-unknown-uefi diff --git a/scripts/test_adversarial.py b/scripts/test_adversarial.py index b06872120..b31dc02cc 100644 --- a/scripts/test_adversarial.py +++ b/scripts/test_adversarial.py @@ -78,6 +78,7 @@ ReleaseBuildConstants, ) from nvx_tools.common import ScriptError +from nvx_tools.nvx_rs import find_binary as find_nvx_rs def _executor_result( @@ -2128,11 +2129,101 @@ def test_executor_environment_drops_controller_credentials(self) -> None: self.assertNotIn("SSH_AUTH_SOCK", environment) self.assertNotIn("UNRELATED_SECRET", environment) + def test_executor_environment_locates_nvx_rs_and_the_rust_toolchain(self) -> None: + # Scenarios run NVX_RS, or build nvx-rs and run the managed-lifecycle + # test with the toolchain that CARGO_HOME and RUSTUP_HOME locate. + # rust-toolchain.toml still selects the Rust release, and no other + # Cargo setting or credential passes. + with patch.dict( + os.environ, + { + "PATH": "test-path", + "NVX_RS": "test-nvx-rs", + "CARGO_HOME": "test-cargo-home", + "RUSTUP_HOME": "test-rustup-home", + "RUSTUP_TOOLCHAIN": "nightly", + "CARGO_TARGET_DIR": "test-target", + "RUSTFLAGS": "-Ctarget-cpu=native", + "CARGO_REGISTRY_TOKEN": "registry-secret", + }, + clear=True, + ): + environment = sanitized_environment() + self.assertEqual( + environment, + { + "PATH": "test-path", + "NVX_RS": str(Path("test-nvx-rs").absolute()), + "CARGO_HOME": "test-cargo-home", + "RUSTUP_HOME": "test-rustup-home", + "PYTHONUNBUFFERED": "1", + }, + ) + + def test_scenarios_run_the_relative_nvx_rs_that_the_executor_validated( + self, + ) -> None: + # The executor validates a relative NVX_RS in its own working + # directory, and the scenarios run in another, the repository root, + # so the environment that they inherit names that executable. + previous = Path.cwd() + self.addCleanup(os.chdir, previous) + with tempfile.TemporaryDirectory() as temporary: + executor_directory = Path(temporary) + executable = executor_directory / "bin" / "nvx-rs" + executable.parent.mkdir() + executable.write_bytes(b"") + os.chdir(executor_directory) + try: + with patch.dict(os.environ, {"NVX_RS": os.path.join("bin", "nvx-rs")}): + validated = find_nvx_rs() + environment = sanitized_environment() + finally: + os.chdir(previous) + completed = subprocess.run( + [ + sys.executable, + "-c", + "from nvx_tools import nvx_rs; print(nvx_rs.find_binary())", + ], + cwd=Path(__file__).resolve().parent, + env=environment, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertEqual(validated, executable.absolute()) + self.assertEqual(completed.stdout.strip(), str(validated)) + + def test_scenarios_find_nvx_rs_with_the_executor_environment(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + nvx_rs = Path(temporary) / "nvx-rs" + nvx_rs.write_bytes(b"") + with patch.dict(os.environ, {"NVX_RS": str(nvx_rs)}): + environment = sanitized_environment() + completed = subprocess.run( + [ + sys.executable, + "-c", + "from nvx_tools import nvx_rs; print(nvx_rs.find_binary())", + ], + cwd=Path(__file__).resolve().parent, + env=environment, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertEqual(completed.stdout.strip(), str(nvx_rs.absolute())) + def test_executor_resolves_build_artifacts_with_shared_helper(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) openvmm = root / OpenVMMBuildConstants.BINARY_NAME openvmm.write_bytes(b"openvmm") + nvx_rs_executable = root / "nvx-rs" + nvx_rs_executable.write_bytes(b"nvx-rs") def artifact_path(name: str) -> Path: path = root / name @@ -2148,11 +2239,30 @@ def artifact_path(name: str) -> Path: "nvx_tools.adversarial_executor.openvmm_binary_path", return_value=openvmm, ), + # The nvx-rs that nvx.py runs, which NVX_RS may name. patch( - "nvx_tools.adversarial_executor.validate_runtime_artifact_provenance" + "nvx_tools.adversarial_executor.nvx_rs.find_binary", + return_value=nvx_rs_executable, ), + patch( + "nvx_tools.adversarial_executor.validate_runtime_artifact_provenance" + ) as validate, ): - _artifact_metadata() + artifacts = _artifact_metadata() + + validate.assert_called_once_with() + self.assertEqual( + artifacts["nvx_rs"], + { + "path": str(nvx_rs_executable), + "sha256": hashlib.sha256(b"nvx-rs").hexdigest(), + "size": len(b"nvx-rs"), + }, + ) + self.assertEqual( + cast(dict[str, object], artifacts["nvx_rs_provenance"])["path"], + str(root / "nvx-rs.provenance.json"), + ) self.assertEqual( [entry.args[0] for entry in resolve.call_args_list], @@ -2163,9 +2273,49 @@ def artifact_path(name: str) -> Path: InitramfsBuildConstants.PROVENANCE_NAME, KernelBuildConstants.PROVENANCE_NAME, OpenVMMBuildConstants.PROVENANCE_NAME, + "nvx-rs.provenance.json", ], ) + def test_executor_requires_the_nvx_rs_that_the_scenarios_run(self) -> None: + # Without NVX_RS, the scenarios run nvx-rs/target/release/nvx-rs, + # which nvx.py would otherwise build during a scenario's timeout. + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + + def artifact_path(name: str) -> Path: + path = root / name + path.write_bytes(name.encode()) + return path + + missing = root / "missing" / "nvx-rs" + with ( + patch( + "nvx_tools.adversarial_executor.artifact_path", + side_effect=artifact_path, + ), + patch( + "nvx_tools.adversarial_executor.openvmm_binary_path", + return_value=artifact_path("openvmm"), + ), + patch( + "nvx_tools.adversarial_executor.nvx_rs.find_binary", + return_value=None, + ), + patch( + "nvx_tools.adversarial_executor.nvx_rs.binary_path", + return_value=missing, + ), + patch( + "nvx_tools.adversarial_executor.validate_runtime_artifact_provenance" + ) as validate, + self.assertRaisesRegex( + ScriptError, f"nvx-rs executable not found: .*{missing.name}" + ), + ): + _artifact_metadata() + validate.assert_not_called() + def test_external_executor_also_drops_controller_credentials(self) -> None: process = MagicMock() process.stdin = MagicMock() diff --git a/scripts/test_control_session.py b/scripts/test_control_session.py deleted file mode 100644 index da11964db..000000000 --- a/scripts/test_control_session.py +++ /dev/null @@ -1,1110 +0,0 @@ -#!/usr/bin/env python3 -# pyright: reportPrivateUsage=false - -import os -import socket -import struct -import sys -import threading -import time -import unittest -import uuid -from pathlib import Path -from types import SimpleNamespace -from typing import Any, cast -from unittest.mock import MagicMock, patch - -sys.path.insert(0, str(Path(__file__).parent)) -from nvx_tools import control_session # noqa: E402 - -if sys.platform == "win32": - import _winapi - - -def _read_like_openvmm(descriptor: int) -> bytes | None: - """Reads a capability pipe as OpenVMM reads --microvm-control-auth-stdin. - - OpenVMM switches the pipe to non-blocking mode and reads it at once, so it - gets the capability only if the capability and the end of file are already - there. Returns what the pipe held, or None if a read would block because the - pipe's write end is still open. - """ - if sys.platform == "win32": - import _winapi - import msvcrt - - handle = msvcrt.get_osfhandle(descriptor) - pipe_nowait = 1 - _winapi.SetNamedPipeHandleState(handle, pipe_nowait, None, None) - - def read() -> bytes | None: - try: - data, _ = _winapi.ReadFile(handle, 64) - except OSError as error: - if error.winerror == _winapi.ERROR_BROKEN_PIPE: - return b"" - if error.winerror == _winapi.ERROR_NO_DATA: - return None - raise - return data - - else: - os.set_blocking(descriptor, False) - - def read() -> bytes | None: - try: - return os.read(descriptor, 64) - except BlockingIOError: - return None - - data = b"" - while True: - chunk = read() - if chunk is None: - return None - if not chunk: - return data - data += chunk - - -def _read_exact(connection: socket.socket, length: int) -> bytes: - output = bytearray() - while len(output) != length: - chunk = connection.recv(length - len(output)) - if not chunk: - raise RuntimeError("test control connection closed") - output.extend(chunk) - return bytes(output) - - -def _read_outer(connection: socket.socket): - header = _read_exact(connection, control_session.OUTER_HEADER.size) - values = control_session.OUTER_HEADER.unpack(header) - payload = _read_exact(connection, values[-1]) if values[-1] else b"" - return (*values[:-1], payload) - - -def _app_record( - *, - instance_id: bytes, - sequence: int, - kind: int, - request_id: int, - status: int, - payload: bytes, -) -> bytes: - app = ( - control_session.APP_HEADER.pack( - b"NVXC", - 1, - kind, - 0, - request_id, - status, - len(payload), - ) - + payload - ) - return ( - control_session.OUTER_HEADER.pack( - b"NVXS", - 1, - control_session.OUTER_DATA, - 0, - instance_id, - 1, - sequence, - len(app), - ) - + app - ) - - -def _write_app( - connection: socket.socket, - *, - instance_id: bytes, - sequence: int, - kind: int, - request_id: int, - status: int, - payload: bytes, -) -> None: - connection.sendall( - _app_record( - instance_id=instance_id, - sequence=sequence, - kind=kind, - request_id=request_id, - status=status, - payload=payload, - ) - ) - - -class _RecordingStream: - """A transport that replays `responses` and records every transfer.""" - - def __init__(self, responses: bytes = b"", *, failure: str | None = None): - self._responses = bytearray(responses) - # The kind of transfer, "write" or "read", that times out. - self._failure = failure - self.transfers: list[tuple[str, float]] = [] - self.closes = 0 - - def _transfer(self, kind: str, deadline: float) -> None: - self.transfers.append((kind, deadline)) - if kind == self._failure: - raise TimeoutError(f"{kind} timed out") - - def write_all(self, data: bytes, deadline: float) -> None: - self._transfer("write", deadline) - - def read_exact(self, length: int, deadline: float) -> bytes: - self._transfer("read", deadline) - data = bytes(self._responses[:length]) - del self._responses[:length] - return data - - def close(self) -> None: - self.closes += 1 - - -if sys.platform == "win32": - - def _create_pipe(quota: int = 65536) -> tuple[Path, int]: - name = rf"\\.\pipe\nvx-control-session-test-{uuid.uuid4().hex}" - server = _winapi.CreateNamedPipe( - name, - _winapi.PIPE_ACCESS_DUPLEX, - _winapi.PIPE_WAIT, - 1, - quota, - quota, - 0, - _winapi.NULL, - ) - return Path(name), server - - def _accept_client(server: int) -> None: - try: - _winapi.ConnectNamedPipe(server) - except OSError as error: - if error.winerror != _winapi.ERROR_PIPE_CONNECTED: - raise - - def _pipe_read_exact(handle: int, length: int) -> bytes: - output = bytearray() - while len(output) != length: - chunk, _ = _winapi.ReadFile(handle, length - len(output)) - output.extend(chunk) - return bytes(output) - - def _accept_attach(server: int, capability: bytes, instance_id: bytes) -> None: - """Authenticates the client of a pipe as OpenVMM's control endpoint does.""" - _accept_client(server) - header = control_session.OUTER_HEADER.unpack( - _pipe_read_exact(server, control_session.OUTER_HEADER.size) - ) - if ( - header[:4] != (b"NVXS", 1, control_session.OUTER_HOST_ATTACH, 0) - or _pipe_read_exact(server, header[-1]) != capability - ): - raise RuntimeError("test endpoint received an invalid attach record") - _winapi.WriteFile( - server, - control_session.OUTER_HEADER.pack( - b"NVXS", 1, control_session.OUTER_READY, 0, instance_id, 1, 0, 0 - ), - ) - - -class ControlSessionTests(unittest.TestCase): - def test_exec_streams_output_and_returns_bounded_status(self): - client, server = socket.socketpair() - instance = bytes.fromhex("11" * 16) - session = control_session.ControlSession(control_session._SocketStream(client)) - session._instance_id = instance - session._epoch = 1 - - def serve() -> None: - ( - magic, - version, - record_type, - flags, - actual_instance, - epoch, - sequence, - frame, - ) = _read_outer(server) - self.assertEqual( - ( - magic, - version, - record_type, - flags, - actual_instance, - epoch, - sequence, - ), - (b"NVXS", 1, control_session.OUTER_DATA, 0, instance, 1, 0), - ) - ( - app_magic, - app_version, - kind, - app_flags, - request_id, - status, - payload_length, - ) = control_session.APP_HEADER.unpack( - frame[: control_session.APP_HEADER.size] - ) - self.assertEqual( - (app_magic, app_version, kind, app_flags, status), - (b"NVXC", 1, control_session.APP_EXEC, 0, 0), - ) - payload = frame[control_session.APP_HEADER.size :] - self.assertEqual(payload_length, len(payload)) - timeout_ms, argc, reserved = struct.unpack(" None: - *_, frame = _read_outer(server) - header = control_session.APP_HEADER.unpack( - frame[: control_session.APP_HEADER.size] - ) - request_id = header[4] - payload = frame[control_session.APP_HEADER.size :] - ( - timeout_ms, - argc, - extension, - flags, - envc, - cwd_len, - ) = struct.unpack(" bytes: - client, server = socket.socketpair() - session = control_session.ControlSession(control_session._SocketStream(client)) - session._instance_id = bytes.fromhex("44" * 16) - session._epoch = 1 - captured = bytearray() - - def serve() -> None: - *_, frame = _read_outer(server) - header = control_session.APP_HEADER.unpack( - frame[: control_session.APP_HEADER.size] - ) - captured.extend(frame[control_session.APP_HEADER.size :]) - _write_app( - server, - instance_id=session._instance_id, - sequence=0, - kind=control_session.APP_EXIT, - request_id=header[4], - status=0, - payload=b"exit", - ) - server.close() - - worker = threading.Thread(target=serve) - worker.start() - session.exec( - ("/bin/true",), - timeout_ms=0, - response_timeout=5, - environment=environment, - inherit_default_environment=inherit_default_environment, - ) - worker.join(timeout=5) - session.close() - return bytes(captured) - - def test_exec_distinguishes_omitted_and_empty_environment(self): - omitted = self._exec_payload(None) - empty = self._exec_payload(()) - self.assertEqual(struct.unpack(" None: - *_, frame = _read_outer(server_socket) - header = control_session.APP_HEADER.unpack( - frame[: control_session.APP_HEADER.size] - ) - payload = frame[control_session.APP_HEADER.size :] - self.assertEqual( - struct.unpack(" None: - *_, frame = _read_outer(server) - request_id = control_session.APP_HEADER.unpack( - frame[: control_session.APP_HEADER.size] - )[4] - _write_app( - server, - instance_id=instance, - sequence=0, - kind=control_session.APP_EXIT, - request_id=request_id, - status=125, - payload=b"guest-provided-detail", - ) - server.close() - - worker = threading.Thread(target=serve) - worker.start() - with self.assertRaisesRegex( - control_session.ScriptError, "unsupported exit category" - ): - session.exec(("/bin/true",), timeout_ms=0, response_timeout=5) - worker.join(timeout=5) - session.close() - - def test_exec_refusal_keeps_status_category_and_diagnostic(self): - client, server = socket.socketpair() - instance = bytes.fromhex("33" * 16) - session = control_session.ControlSession(control_session._SocketStream(client)) - session._instance_id = instance - session._epoch = 1 - diagnostic = ( - b"nvx-managed-agent: cannot enter working directory /missing: " - b"No such file or directory\n" - ) - - def serve() -> None: - *_, frame = _read_outer(server) - request_id = control_session.APP_HEADER.unpack( - frame[: control_session.APP_HEADER.size] - )[4] - _write_app( - server, - instance_id=instance, - sequence=0, - kind=control_session.APP_STDERR, - request_id=request_id, - status=0, - payload=diagnostic, - ) - _write_app( - server, - instance_id=instance, - sequence=1, - kind=control_session.APP_ERROR, - request_id=request_id, - status=2, - payload=b"cwd-failed", - ) - server.close() - - worker = threading.Thread(target=serve) - worker.start() - with self.assertRaises(control_session.ManagedExecRefused) as raised: - session.exec( - ("/bin/true",), timeout_ms=0, response_timeout=5, cwd="/missing" - ) - worker.join(timeout=5) - session.close() - - refusal = raised.exception - self.assertIsInstance(refusal, control_session.ScriptError) - self.assertEqual( - (refusal.status, refusal.category, refusal.stdout, refusal.stderr), - (2, "cwd-failed", b"", diagnostic), - ) - self.assertEqual( - str(refusal), - "managed guest rejected exec (status=2, category=cwd-failed)", - ) - - def test_session_reset_is_not_a_closed_endpoint(self): - # A reset comes from an OpenVMM that still serves the endpoint, so it must - # not look like the closure with which OpenVMM shuts down. - client, server = socket.socketpair() - instance = bytes.fromhex("11" * 16) - session = control_session.ControlSession(control_session._SocketStream(client)) - session._instance_id = instance - session._epoch = 1 - server.sendall( - control_session.OUTER_HEADER.pack( - b"NVXS", 1, control_session.OUTER_RESET, 0, instance, 1, 0, 0 - ) - ) - try: - with self.assertRaisesRegex( - ConnectionError, "^managed control session was reset$" - ) as raised: - session.ping(5) - finally: - session.close() - server.close() - - self.assertNotIsInstance( - raised.exception, control_session.ControlEndpointClosed - ) - - def test_operations_bound_their_requests_by_their_deadlines(self): - # Issue #464: an operation's timeout bounds sending its request as well as - # receiving its response, so attach, ping, exec, and stop each write with - # the deadline with which they then read. - instance = bytes.fromhex("66" * 16) - request_id = 0x1234 - - def response(sequence: int, kind: int, payload: bytes = b"") -> bytes: - return _app_record( - instance_id=instance, - sequence=sequence, - kind=kind, - request_id=request_id, - status=0, - payload=payload, - ) - - stream = _RecordingStream( - control_session.OUTER_HEADER.pack( - b"NVXS", 1, control_session.OUTER_READY, 0, instance, 1, 0, 0 - ) - + response(1, control_session.APP_READY) - + response(2, control_session.APP_EXIT, b"exit") - + response(3, control_session.APP_STOPPED) - ) - transport = ( - control_session._NamedPipeStream - if os.name == "nt" - else control_session._SocketStream - ) - with ( - patch.object(transport, "connect", return_value=stream), - patch.object( - control_session, "time", SimpleNamespace(monotonic=lambda: 1000.0) - ), - patch.object( - control_session.ControlSession, "_request_id", return_value=request_id - ), - ): - session = control_session.ControlSession.connect( - Path("endpoint"), bytes(range(1, 33)), 7 - ) - session.ping(5) - session.exec(("/bin/true",), timeout_ms=0, response_timeout=3) - session.stop(2) - session.close() - - self.assertEqual( - stream.transfers, - [ - ("write", 1007.0), - ("read", 1007.0), - *( - transfer - for deadline in (1005.0, 1003.0, 1002.0) - for transfer in ( - ("write", deadline), - ("read", deadline), - ("read", deadline), - ) - ), - ], - ) - - def test_failed_transfer_closes_the_session(self): - # A transfer that fails, as by timing out, may cut its record short, after - # which the endpoint would frame every later record wrongly. - for failure in ("write", "read"): - with self.subTest(failure=failure): - stream = _RecordingStream(failure=failure) - session = control_session.ControlSession(cast(Any, stream)) - with self.assertRaisesRegex(TimeoutError, f"^{failure} timed out$"): - session.ping(5) - self.assertEqual(stream.closes, 1) - transfers = len(stream.transfers) - with self.assertRaisesRegex( - ValueError, "^managed control session is closed$" - ): - session.exec(("/bin/true",), timeout_ms=0, response_timeout=5) - session.close() - self.assertEqual(len(stream.transfers), transfers) - self.assertEqual(stream.closes, 1) - - -class CapabilityPipeTests(unittest.TestCase): - CAPABILITY = bytes(range(1, 33)) - - def test_pipe_holds_the_capability_and_then_end_of_file(self): - descriptor = control_session.capability_pipe(self.CAPABILITY) - try: - self.assertEqual(_read_like_openvmm(descriptor), self.CAPABILITY) - finally: - os.close(descriptor) - - def test_openvmm_refuses_a_pipe_whose_writer_is_open(self): - # A launcher that writes the capability only after it starts OpenVMM - # leaves the writer open when OpenVMM reads, as before issue #440. - read, write = os.pipe() - try: - os.write(write, self.CAPABILITY) - self.assertIsNone(_read_like_openvmm(read)) - finally: - os.close(read) - os.close(write) - - def test_invalid_capabilities_are_rejected(self): - for capability in (b"", bytes(range(1, 32)), bytes(32), bytes(range(1, 34))): - with ( - self.subTest(capability=capability.hex()), - self.assertRaisesRegex(ValueError, "32 nonzero bytes"), - ): - control_session.capability_pipe(capability) - - -class SocketStreamTests(unittest.TestCase): - def test_write_that_sends_nothing_reports_a_closed_endpoint(self): - connection = MagicMock(spec=socket.socket) - connection.send.return_value = 0 - stream = control_session._SocketStream(connection) - with self.assertRaisesRegex( - control_session.ControlEndpointClosed, - "^managed control endpoint closed$", - ): - stream.write_all(b"record", time.monotonic() + 60) - connection.send.assert_called_once() - - def test_read_reports_a_closed_endpoint(self): - client, server = socket.socketpair() - stream = control_session._SocketStream(client) - server.close() - try: - with self.assertRaisesRegex( - control_session.ControlEndpointClosed, - "^managed control endpoint closed$", - ): - stream.read_exact(1, time.monotonic() + 5) - finally: - stream.close() - - def test_reset_and_broken_pipe_report_a_closed_endpoint(self): - # OpenVMM may close the socket before it reads everything that the client - # sent, which resets the client's reads, and a later write fails with a - # broken pipe. - if sys.platform != "linux": - self.skipTest("Unix-domain socket closure semantics require Linux") - client, server = socket.socketpair() - stream = control_session._SocketStream(client) - stream.write_all(b"unread request", time.monotonic() + 5) - server.close() - try: - with self.assertRaises(control_session.ControlEndpointClosed) as raised: - stream.read_exact(1, time.monotonic() + 5) - self.assertIsInstance(raised.exception.__cause__, ConnectionResetError) - with self.assertRaises(control_session.ControlEndpointClosed) as raised: - stream.write_all(b"record", time.monotonic() + 5) - self.assertIsInstance(raised.exception.__cause__, BrokenPipeError) - finally: - stream.close() - - def test_request_to_a_stalled_endpoint_times_out(self): - # Issue #464: a request that the endpoint does not read must not hold the - # operation past its timeout. - if sys.platform != "linux": - self.skipTest("Unix-domain socket buffering semantics require Linux") - client, server = socket.socketpair() - # The request is several times larger than the socket buffer. - client.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4096) - server.settimeout(5) - session = control_session.ControlSession(control_session._SocketStream(client)) - session._instance_id = bytes.fromhex("77" * 16) - session._epoch = 1 - arguments = ("/bin/true", *("a" * 4000,) * 15) - released = threading.Event() - received = bytearray() - failures: list[BaseException] = [] - - def serve() -> None: - try: - # The peer reads nothing until the client gives up, or long - # after the client's timeout. - released.wait(10) - while chunk := server.recv(65536): - received.extend(chunk) - except BaseException as error: - failures.append(error) - - worker = threading.Thread(target=serve, daemon=True) - worker.start() - timeout = 0.25 - try: - started = time.monotonic() - with self.assertRaisesRegex( - TimeoutError, "^managed control request timed out$" - ): - session.exec(arguments, timeout_ms=0, response_timeout=timeout) - elapsed = time.monotonic() - started - with self.assertRaisesRegex( - ValueError, "^managed control session is closed$" - ): - session.ping(5) - finally: - session.close() - released.set() - worker.join(10) - server.close() - - self.assertFalse(worker.is_alive()) - self.assertEqual(failures, []) - self.assertGreaterEqual(elapsed, timeout) - self.assertLess(elapsed, timeout + 2) - # The endpoint received only part of the request before the end of stream. - self.assertLess(len(received), sum(map(len, arguments))) - - -class NamedPipeStreamTests(unittest.TestCase): - def test_write_that_moves_nothing_reports_a_closed_endpoint(self): - # The stream is never closed, since its handle is not real. - stream = control_session._NamedPipeStream(123) - with ( - patch.object(control_session, "_winapi") as winapi, - patch.object(control_session, "_finish_transfer", return_value=0) as finish, - self.assertRaisesRegex( - control_session.ControlEndpointClosed, - "^managed control endpoint closed$", - ), - ): - winapi.WriteFile.return_value = (MagicMock(), 0) - stream.write_all(b"record", time.monotonic() + 60) - finish.assert_called_once() - - def test_write_reports_a_closed_endpoint(self): - # OpenVMM closes the pipe as it tears the VM down, and the CRT reports a - # later write to it as EINVAL rather than as a broken pipe. - if sys.platform != "win32": - self.skipTest("named pipes require a Windows host") - import _winapi - - name = rf"\\.\pipe\nvx-control-session-test-{uuid.uuid4().hex}" - server = _winapi.CreateNamedPipe( - name, - _winapi.PIPE_ACCESS_DUPLEX, - _winapi.PIPE_WAIT, - 1, - 65536, - 65536, - 0, - _winapi.NULL, - ) - try: - stream = control_session._NamedPipeStream.connect(Path(name), 5) - finally: - _winapi.CloseHandle(server) - try: - with self.assertRaisesRegex( - control_session.ControlEndpointClosed, - "^managed control endpoint closed$", - ): - stream.write_all(b"record", time.monotonic() + 5) - finally: - stream.close() - - def test_read_reports_a_closed_endpoint_after_its_data(self): - if sys.platform != "win32": - self.skipTest("named pipes require a Windows host") - path, server = _create_pipe() - try: - stream = control_session._NamedPipeStream.connect(path, 5) - _accept_client(server) - _winapi.WriteFile(server, b"ok") - finally: - _winapi.CloseHandle(server) - try: - self.assertEqual(stream.read_exact(2, time.monotonic() + 5), b"ok") - with self.assertRaisesRegex( - control_session.ControlEndpointClosed, - "^managed control endpoint closed$", - ): - stream.read_exact(1, time.monotonic() + 5) - finally: - stream.close() - - def test_read_times_out_without_losing_later_data(self): - if sys.platform != "win32": - self.skipTest("named pipes require a Windows host") - path, server = _create_pipe() - self.addCleanup(_winapi.CloseHandle, server) - stream = control_session._NamedPipeStream.connect(path, 5) - self.addCleanup(stream.close) - _accept_client(server) - - started = time.monotonic() - with self.assertRaisesRegex( - TimeoutError, "^managed control response timed out$" - ): - stream.read_exact(2, started + 0.1) - self.assertLess(time.monotonic() - started, 2.1) - _winapi.WriteFile(server, b"ok") - self.assertEqual(stream.read_exact(2, time.monotonic() + 5), b"ok") - - def test_interrupted_write_is_cancelled_before_the_interruption_propagates(self): - if sys.platform != "win32": - self.skipTest("named pipes require a Windows host") - path, server = _create_pipe(1024) - self.addCleanup(_winapi.CloseHandle, server) - stream = control_session._NamedPipeStream.connect(path, 5) - self.addCleanup(stream.close) - _accept_client(server) - - with ( - patch.object( - control_session, "_signaled_before", side_effect=KeyboardInterrupt - ), - self.assertRaises(KeyboardInterrupt), - ): - stream.write_all(b"x" * 4096, time.monotonic() + 5) - # The write, which the pipe could not hold, no longer waits in it. - self.assertEqual(_winapi.PeekNamedPipe(server)[0], 0) - - def test_request_to_a_stalled_endpoint_times_out_and_closes_the_session(self): - # Issue #464: an endpoint that authenticated the session and then stopped - # reading held the request's write, and so the operation, past its timeout. - if sys.platform != "win32": - self.skipTest("named pipes require a Windows host") - # The 4,093-byte request does not fit in the endpoint's 1 KiB quota. - path, server = _create_pipe(1024) - capability = bytes(range(1, 33)) - released = threading.Event() - received = bytearray() - failures: list[BaseException] = [] - - def serve() -> None: - # Type checkers check a nested function apart from its test's skip. - assert sys.platform == "win32" - try: - _accept_attach(server, capability, bytes.fromhex("11" * 16)) - # The endpoint keeps the pipe open but reads nothing more until - # the client gives up, or long after the client's timeout. - released.wait(10) - while True: - chunk, _ = _winapi.ReadFile(server, 65536) - received.extend(chunk) - except BrokenPipeError: - pass - except BaseException as error: - failures.append(error) - - worker = threading.Thread(target=serve, daemon=True) - worker.start() - timeout = 0.25 - try: - with control_session.ControlSession.connect(path, capability, 5) as session: - started = time.monotonic() - with self.assertRaisesRegex( - TimeoutError, "^managed control request timed out$" - ): - session.exec( - ("/bin/true", "a" * 4000), - timeout_ms=0, - response_timeout=timeout, - ) - elapsed = time.monotonic() - started - with self.assertRaisesRegex( - ValueError, "^managed control session is closed$" - ): - session.ping(5) - finally: - released.set() - worker.join(5) - _winapi.CloseHandle(server) - - self.assertFalse(worker.is_alive()) - self.assertEqual(failures, []) - self.assertGreaterEqual(elapsed, timeout) - self.assertLess(elapsed, timeout + 2) - # The endpoint received less than the request before the pipe closed. - self.assertLess(len(received), 4000) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_development_release.py b/scripts/test_development_release.py index 1651dee9d..8ea760e57 100644 --- a/scripts/test_development_release.py +++ b/scripts/test_development_release.py @@ -2,14 +2,22 @@ # pyright: reportPrivateUsage=false import io +import os +import re import sys +import tarfile import tempfile import unittest +import zipfile +from collections.abc import Mapping from pathlib import Path from unittest.mock import call, patch sys.path.insert(0, str(Path(__file__).parent)) +import publish_development_release # noqa: E402 from nvx_tools import development_release # noqa: E402 +from nvx_tools.build_constants import BuildConstants # noqa: E402 +from nvx_tools.common import ScriptError # noqa: E402 class DevelopmentReleaseTests(unittest.TestCase): @@ -478,5 +486,124 @@ def test_new_release_is_created_as_draft_without_assets(self): self.assertFalse(any(item.endswith((".zip", ".tar.gz")) for item in command)) +def _write_package(path: Path, members: Mapping[str, bytes | None]) -> None: + """Write a release archive whose package root holds `members`, files with + their contents, or directories where the contents are None.""" + path.parent.mkdir(parents=True, exist_ok=True) + root = path.name.removesuffix(".tar.gz").removesuffix(".zip") + if path.name.endswith(".tar.gz"): + with tarfile.open(path, "w:gz") as archive: + for name, data in members.items(): + info = tarfile.TarInfo(f"{root}/{name}") + if data is None: + info.type = tarfile.DIRTYPE + archive.addfile(info) + continue + info.size = len(data) + archive.addfile(info, io.BytesIO(data)) + return + with zipfile.ZipFile(path, "w") as archive: + for name, data in members.items(): + archive.writestr( + f"{root}/{name}" + ("/" if data is None else ""), data or b"" + ) + + +class DevelopmentReleasePackageTests(unittest.TestCase): + """The packages that publication checks: each ships OpenVMM and nvx-rs.""" + + def _packages(self, directory: Path) -> dict[str, Path]: + packages = publish_development_release.release_packages(directory, "1.2.3") + for platform, package in packages.items(): + suffix = ".exe" if platform.startswith("windows-") else "" + _write_package( + package, + { + "SHA256SUMS": b"sums", + f"bin/openvmm{suffix}": b"openvmm", + f"bin/nvx-rs{suffix}": b"nvx-rs", + }, + ) + return packages + + def test_packages_are_named_after_their_platforms(self): + self.assertEqual( + publish_development_release.release_packages(Path("dist"), "1.2.3"), + { + "linux-kvm": Path("dist") / "nvx-1.2.3-linux-kvm.tar.gz", + "linux-mshv": Path("dist") / "nvx-1.2.3-linux-mshv.tar.gz", + "windows-whp": Path("dist") / "nvx-1.2.3-windows-whp.zip", + }, + ) + + def test_every_package_must_ship_openvmm_and_nvx_rs(self): + with tempfile.TemporaryDirectory() as temporary: + packages = self._packages(Path(temporary)) + publish_development_release.check_release_packages(packages) + for platform, members, missing in ( + ("linux-kvm", {"bin/openvmm": b"openvmm"}, "bin/nvx-rs"), + ("linux-mshv", {"bin/nvx-rs": b"nvx-rs"}, "bin/openvmm"), + ( + "linux-kvm", + {"bin/openvmm": b"openvmm", "bin/nvx-rs": None}, + "bin/nvx-rs", + ), + # Windows packages ship Windows executables. + ( + "windows-whp", + {"bin/openvmm.exe": b"openvmm", "bin/nvx-rs": b"nvx-rs"}, + "bin/nvx-rs.exe", + ), + ): + with self.subTest(platform=platform, members=sorted(members)): + package = packages[platform] + package.unlink() + _write_package(package, members) + with self.assertRaisesRegex( + ScriptError, + f"^{re.escape(package.name)} does not ship {missing}$", + ): + publish_development_release.check_release_packages(packages) + self._packages(Path(temporary)) + packages["linux-mshv"].unlink() + with self.assertRaisesRegex( + ScriptError, "development release package nvx-1.2.3-linux-mshv.tar.gz" + ): + publish_development_release.check_release_packages(packages) + + def test_publication_checks_the_packages_before_it_publishes(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / "VERSION").write_text("1.2.3\n", encoding="ascii") + packages = self._packages(root / "dist") + environment = {"GH_REPO": "example/nvx", "GITHUB_SHA": "a" * 40} + previous = os.getcwd() + os.chdir(root) + try: + with ( + patch.dict(os.environ, environment), + patch.object(BuildConstants, "REPO_ROOT", root), + patch.object( + publish_development_release, "publish_development_release" + ) as publish, + ): + publish_development_release.main() + publish.assert_called_once_with( + "example/nvx", + "a" * 40, + "1.2.3", + tuple( + Path("dist") / package.name for package in packages.values() + ), + ) + publish.reset_mock() + _write_package(packages["windows-whp"], {"bin/openvmm.exe": b""}) + with self.assertRaisesRegex(ScriptError, "bin/nvx-rs.exe"): + publish_development_release.main() + publish.assert_not_called() + finally: + os.chdir(previous) + + if __name__ == "__main__": unittest.main() diff --git a/scripts/test_egress_policy.py b/scripts/test_egress_policy.py deleted file mode 100644 index 3fbceddc7..000000000 --- a/scripts/test_egress_policy.py +++ /dev/null @@ -1,1104 +0,0 @@ -#!/usr/bin/env python3 -# pyright: reportPrivateUsage=false - -import ipaddress -import json -import re -import sys -import tempfile -import unittest -from collections.abc import Iterable -from pathlib import Path -from unittest import mock - -sys.path.insert(0, str(Path(__file__).parent)) -import nvx_tools.egress_policy as egress_policy # noqa: E402 -from nvx_tools.common import ScriptError # noqa: E402 -from nvx_tools.egress_policy import ( - MAX_POLICY_FILE_SIZE, # noqa: E402 - MAX_RULES_PER_ACTION, # noqa: E402 - compile_policy_file, # noqa: E402 -) - - -class EgressPolicyTests(unittest.TestCase): - def compile(self, value: object): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "policy.json" - path.write_text(json.dumps(value), encoding="utf-8") - return compile_policy_file(path) - - def test_compiles_inclusive_tcp_and_udp_ranges(self): - compiled = self.compile( - { - "allow": [ - { - "to": [{"cidr": "192.0.2.7"}], - "ports": [ - { - "protocol": "tcp", - "port": 8000, - "endPort": 8002, - } - ], - }, - { - "to": [{"cidr": "198.51.100.0/24"}], - "ports": [ - { - "protocol": "udp", - "port": 5000, - "endPort": 5001, - } - ], - }, - ] - } - ) - - self.assertEqual( - compiled.allow, - ( - "192.0.2.7/32:tcp:8000-8002", - "198.51.100.0/24:udp:5000-5001", - ), - ) - self.assertEqual(compiled.deny, ()) - - def test_lowers_each_range_to_one_native_range_rule(self): - ranges: tuple[tuple[dict[str, object], tuple[str, ...]], ...] = ( - ( - {"protocol": "tcp", "port": 8000, "endPort": 8010}, - ("192.0.2.7/32:tcp:8000-8010",), - ), - ( - {"protocol": "udp", "port": 5000, "endPort": 5010}, - ("192.0.2.7/32:udp:5000-5010",), - ), - ( - {"protocol": "tcp", "port": 1, "endPort": 65535}, - ("192.0.2.7/32:tcp:1-65535",), - ), - ( - {"protocol": "any", "port": 1, "endPort": 65535}, - ("192.0.2.7/32:tcp:1-65535", "192.0.2.7/32:udp:1-65535"), - ), - # A range of one port lowers to that port's rule. - ( - {"protocol": "tcp", "port": 8000, "endPort": 8000}, - ("192.0.2.7/32:tcp:8000",), - ), - ) - - for category in ("allow", "deny"): - for selector, expected in ranges: - for rule in ( - {"to": [{"cidr": "192.0.2.7"}], "ports": [selector]}, - {"cidr": "192.0.2.7", **selector}, - ): - with self.subTest(category=category, rule=rule): - compiled = self.compile({category: [rule]}) - self.assertEqual(getattr(compiled, category), expected) - - def test_keeps_a_denied_port_inside_an_allowed_range(self): - compiled = self.compile( - { - "allow": [ - { - "to": [{"cidr": "192.0.2.0/24"}], - "ports": [{"protocol": "tcp", "port": 8000, "endPort": 8010}], - } - ], - "deny": [ - { - "to": [{"cidr": "192.0.2.0/24"}], - "ports": [{"protocol": "tcp", "port": 8005}], - } - ], - } - ) - - # OpenVMM applies deny precedence, so each list keeps its own rule. - self.assertEqual(compiled.allow, ("192.0.2.0/24:tcp:8000-8010",)) - self.assertEqual(compiled.deny, ("192.0.2.0/24:tcp:8005",)) - - def test_merges_adjacent_and_overlapping_ranges_of_one_network(self): - def ranges(*bounds: tuple[int, int]) -> list[dict[str, object]]: - return [ - { - "cidr": "192.0.2.7", - "protocol": "tcp", - "port": start, - "endPort": end, - } - for start, end in bounds - ] - - for rules, expected in ( - (ranges((8000, 8004), (8005, 8010)), ("192.0.2.7/32:tcp:8000-8010",)), - (ranges((8000, 8008), (8003, 8010)), ("192.0.2.7/32:tcp:8000-8010",)), - ( - [ - *ranges((8000, 8010)), - *ranges(*((port, port) for port in range(8000, 8011))), - ], - ("192.0.2.7/32:tcp:8000-8010",), - ), - ( - ranges((8000, 8004), (8006, 8010)), - ("192.0.2.7/32:tcp:8000-8004", "192.0.2.7/32:tcp:8006-8010"), - ), - ): - with self.subTest(rules=rules): - self.assertEqual(self.compile({"allow": rules}).allow, expected) - - # Networks keep their own ranges where their segments differ. - compiled = self.compile( - { - "allow": [ - {"cidr": "192.0.2.7", "protocol": "udp", "port": 1, "endPort": 10}, - { - "cidr": "198.51.100.9", - "protocol": "udp", - "port": 5, - "endPort": 20, - }, - ] - } - ) - self.assertEqual( - compiled.allow, - ("192.0.2.7/32:udp:1-10", "198.51.100.9/32:udp:5-20"), - ) - - def test_keeps_network_range_whole_when_adjacent_rule_changes_union(self): - compiled = self.compile( - { - "allow": [ - { - "cidr": "10.0.0.0/25", - "protocol": "tcp", - "port": 1, - "endPort": 10, - }, - { - "cidr": "10.0.0.128/25", - "protocol": "tcp", - "port": 5, - "endPort": 6, - }, - ] - } - ) - - self.assertEqual( - compiled.allow, - ("10.0.0.0/24:tcp:5-6", "10.0.0.0/25:tcp:1-10"), - ) - - def test_rejects_invalid_port_shapes_and_values(self): - invalid_rules = ( - {"cidr": "192.0.2.0/24", "protocol": "tcp", "endPort": 80}, - {"cidr": "192.0.2.0/24", "protocol": "any", "endPort": 80}, - { - "cidr": "192.0.2.0/24", - "protocol": "tcp", - "port": 81, - "endPort": 80, - }, - {"cidr": "192.0.2.0/24", "protocol": "tcp", "port": 0}, - {"cidr": "192.0.2.0/24", "protocol": "udp", "port": 65536}, - {"cidr": "192.0.2.0/24", "protocol": "tcp", "port": True}, - {"cidr": "192.0.2.0/24", "protocol": "any", "port": 0}, - {"cidr": "192.0.2.0/24", "port": 80}, - {"cidr": "192.0.2.0/24", "protocol": "icmp", "port": 8}, - {"cidr": "192.0.2.0/24", "protocol": "icmp", "endPort": 8}, - {"cidr": "192.0.2.0/24", "protocol": "ICMP"}, - {"cidr": "192.0.2.0/24", "protocol": "sctp"}, - ) - - for rule in invalid_rules: - with self.subTest(rule=rule), self.assertRaises(ScriptError): - self.compile({"allow": [rule]}) - - def test_reports_invalid_port_ranges_clearly(self): - cidr = "192.0.2.0/24" - cases: tuple[tuple[dict[str, object], str], ...] = ( - ( - { - "to": [{"cidr": cidr}], - "ports": [{"protocol": "tcp", "port": 8010, "endPort": 8000}], - }, - r"^deny\[0\]\.ports\[0\]\.endPort 8000 cannot be below port 8010$", - ), - ( - {"ports": [{"protocol": "udp", "endPort": 8010}]}, - r"^deny\[0\]\.ports\[0\]\.endPort requires port$", - ), - ( - {"ports": [{"endPort": 8010}]}, - r"^deny\[0\]\.ports\[0\]\.endPort requires port$", - ), - ( - {"ports": [{"protocol": "tcp", "port": 8000, "endPort": 65536}]}, - r"^deny\[0\]\.ports\[0\]\.endPort must be between 1 and 65535$", - ), - ( - {"cidr": cidr, "protocol": "tcp", "port": 8010, "endPort": 8000}, - r"^deny\[0\]\.endPort 8000 cannot be below port 8010$", - ), - ( - {"cidr": cidr, "protocol": "udp", "endPort": 8010}, - r"^deny\[0\]\.endPort requires port$", - ), - ( - {"cidr": cidr, "endPort": 8010}, - r"^deny\[0\]\.endPort requires port$", - ), - ( - {"cidr": cidr, "protocol": "tcp", "port": 8000, "endPort": 65536}, - r"^deny\[0\]\.endPort must be between 1 and 65535$", - ), - ( - {"cidr": cidr, "protocol": "tcp", "port": 0, "endPort": 8010}, - r"^deny\[0\]\.port must be between 1 and 65535$", - ), - ) - - for rule, message in cases: - with ( - self.subTest(rule=rule), - self.assertRaisesRegex(ScriptError, message), - ): - self.compile({"deny": [rule]}) - - def test_compiles_protocol_selectors_with_and_without_ports(self): - selectors: tuple[tuple[dict[str, object], tuple[str, ...]], ...] = ( - ({"protocol": "tcp"}, ("192.0.2.7/32:tcp",)), - ({"protocol": "udp"}, ("192.0.2.7/32:udp",)), - ({"protocol": "icmp"}, ("192.0.2.7/32:icmp",)), - ({"protocol": "any"}, ("192.0.2.7/32",)), - ({}, ("192.0.2.7/32",)), - ( - {"protocol": "any", "port": 443}, - ("192.0.2.7/32:tcp:443", "192.0.2.7/32:udp:443"), - ), - ( - {"protocol": "any", "port": 443, "endPort": 444}, - ("192.0.2.7/32:tcp:443-444", "192.0.2.7/32:udp:443-444"), - ), - ) - - for category in ("allow", "deny"): - for selector, expected in selectors: - with self.subTest(category=category, selector=selector): - compiled = self.compile( - {category: [{"cidr": "192.0.2.7", **selector}]} - ) - self.assertEqual(getattr(compiled, category), expected) - - def test_protocol_wide_rules_cover_their_port_rules_only(self): - compiled = self.compile( - { - "allow": [ - {"cidr": "192.0.2.7", "protocol": "tcp", "port": 443}, - {"cidr": "192.0.2.0/24", "protocol": "tcp"}, - {"cidr": "192.0.2.7", "protocol": "udp", "port": 443}, - {"cidr": "192.0.2.0/24", "protocol": "icmp"}, - ], - "deny": [ - {"cidr": "198.51.100.0/24", "protocol": "any", "port": 53}, - {"cidr": "198.51.100.0/24", "protocol": "udp"}, - ], - } - ) - - self.assertEqual( - compiled.allow, - ( - "192.0.2.0/24:icmp", - "192.0.2.0/24:tcp", - "192.0.2.7/32:udp:443", - ), - ) - self.assertEqual( - compiled.deny, - ("198.51.100.0/24:tcp:53", "198.51.100.0/24:udp"), - ) - - def test_address_only_rules_cover_protocol_wide_rules(self): - compiled = self.compile( - { - "allow": [ - {"cidr": "192.0.2.0/24", "protocol": "udp"}, - {"cidr": "192.0.2.0/25"}, - {"cidr": "192.0.2.0/25", "protocol": "icmp"}, - {"cidr": "192.0.2.128/25", "protocol": "udp", "port": 53}, - ] - } - ) - - # Widening the UDP rule into the address-only half keeps it to one prefix. - self.assertEqual(compiled.allow, ("192.0.2.0/24:udp", "192.0.2.0/25")) - - def test_protocol_wide_rules_share_the_native_rule_budget(self): - address_only = [ - {"cidr": f"10.0.{index}.1/32"} for index in range(MAX_RULES_PER_ACTION) - ] - accepted = self.compile( - {"allow": [*address_only[:-1], {"cidr": "192.0.2.0/24", "protocol": "tcp"}]} - ) - self.assertEqual(len(accepted.allow), MAX_RULES_PER_ACTION) - self.assertIn("192.0.2.0/24:tcp", accepted.allow) - - with self.assertRaisesRegex(ScriptError, "at most 256"): - self.compile( - { - "allow": [ - *address_only, - {"cidr": "192.0.2.0/24", "protocol": "icmp"}, - ] - } - ) - - # Protocol `any` with a port range lowers to a TCP and a UDP range. - any_range = { - "cidr": "192.0.2.1", - "protocol": "any", - "port": 1, - "endPort": 129, - } - accepted = self.compile({"deny": [*address_only[:-2], any_range]}) - self.assertEqual(len(accepted.deny), MAX_RULES_PER_ACTION) - self.assertIn("192.0.2.1/32:udp:1-129", accepted.deny) - with self.assertRaisesRegex(ScriptError, "at most 256"): - self.compile({"deny": [*address_only[:-1], any_range]}) - - def test_normalizes_host_bits_like_the_native_cidr_parser(self): - compiled = self.compile( - {"allow": [{"cidr": "10.0.0.5/24"}, {"cidr": "fe80::1/64"}]} - ) - self.assertEqual(compiled.allow, ("10.0.0.0/24", "fe80::/64")) - - def test_subtracts_rule_local_cidr_exclusions(self): - compiled = self.compile( - { - "allow": [ - { - "to": [ - { - "cidr": "192.0.2.0/24", - "except": ["192.0.2.128/25"], - } - ], - }, - {"to": [{"cidr": "192.0.2.200/32"}]}, - ], - "deny": [ - { - "to": [ - { - "cidr": "198.51.100.0/24", - "except": ["198.51.100.128/25"], - } - ], - } - ], - } - ) - - self.assertEqual( - compiled.allow, - ("192.0.2.0/25", "192.0.2.200/32"), - ) - self.assertEqual(compiled.deny, ("198.51.100.0/25",)) - - def test_compiles_mxc_destination_and_port_unions(self): - compiled = self.compile( - { - "allow": [ - { - "ports": [ - {"port": 53}, - {"protocol": "icmp"}, - ] - } - ], - "deny": [ - { - "to": [ - {"cidr": "198.51.100.0/25"}, - {"cidr": "198.51.100.128/25"}, - ], - "ports": [ - {"protocol": "tcp", "port": 80}, - {"protocol": "udp", "port": 53}, - ], - } - ], - } - ) - - # A rule without destinations matches every destination of both - # families. - self.assertEqual( - compiled.allow, - ( - "0.0.0.0/0:icmp", - "0.0.0.0/0:tcp:53", - "0.0.0.0/0:udp:53", - "::/0:icmp", - "::/0:tcp:53", - "::/0:udp:53", - ), - ) - self.assertEqual( - compiled.deny, - ("198.51.100.0/24:tcp:80", "198.51.100.0/24:udp:53"), - ) - - def test_deduplicates_overlapping_exclusions_and_collapses_safe_prefixes(self): - compiled = self.compile( - { - "allow": [ - { - "cidr": "192.0.2.0/24", - "except": [ - "192.0.2.128/25", - "192.0.2.128/25", - "192.0.2.192/26", - ], - "protocol": "tcp", - "port": 443, - }, - { - "cidr": "198.51.100.0/25", - "protocol": "udp", - "port": 53, - }, - { - "cidr": "198.51.100.128/25", - "protocol": "udp", - "port": 53, - }, - ] - } - ) - - self.assertEqual( - compiled.allow, - ( - "192.0.2.0/25:tcp:443", - "198.51.100.0/24:udp:53", - ), - ) - - def test_rejects_exclusions_outside_parent_or_wrong_family(self): - for cidr, excluded in ( - ("192.0.2.0/24", "198.51.100.0/24"), - ("192.0.2.0/24", "2001:db8::/32"), - ("2001:db8::/32", "2001:db9::/48"), - ("2001:db8::/32", "192.0.2.0/24"), - ("::/96", "0.0.0.0/8"), - ): - with ( - self.subTest(cidr=cidr, excluded=excluded), - self.assertRaisesRegex(ScriptError, "except"), - ): - self.compile( - { - "allow": [ - { - "to": [ - { - "cidr": cidr, - "except": [excluded], - } - ], - } - ] - } - ) - - def test_compiles_ipv6_networks_with_every_selector(self): - selectors: tuple[tuple[dict[str, object], tuple[str, ...]], ...] = ( - ({"ports": [{"protocol": "tcp"}]}, ("2001:db8::7/128:tcp",)), - ({"ports": [{"protocol": "udp"}]}, ("2001:db8::7/128:udp",)), - ({"ports": [{"protocol": "icmp"}]}, ("2001:db8::7/128:icmp",)), - ({}, ("2001:db8::7/128",)), - ( - {"ports": [{"protocol": "any", "port": 443}]}, - ("2001:db8::7/128:tcp:443", "2001:db8::7/128:udp:443"), - ), - ( - {"ports": [{"protocol": "tcp", "port": 8000, "endPort": 8010}]}, - ("2001:db8::7/128:tcp:8000-8010",), - ), - ) - for category in ("allow", "deny"): - for selector, expected in selectors: - with self.subTest(category=category, selector=selector): - compiled = self.compile( - {category: [{"to": [{"cidr": "2001:db8::7"}], **selector}]} - ) - self.assertEqual(getattr(compiled, category), expected) - - def test_compiles_one_allowed_ipv6_network_with_a_denied_address(self): - compiled = self.compile( - { - "allow": [ - { - "to": [{"cidr": "2001:db8:1::/64"}], - "ports": [{"protocol": "tcp", "port": 443}], - }, - { - "to": [{"cidr": "::/0"}], - "ports": [{"protocol": "udp", "port": 53}], - }, - ], - "deny": [{"to": [{"cidr": "2001:db8:1::123"}]}], - } - ) - self.assertEqual(compiled.allow, ("::/0:udp:53", "2001:db8:1::/64:tcp:443")) - self.assertEqual(compiled.deny, ("2001:db8:1::123/128",)) - - def test_flat_rules_accept_ipv6_networks(self): - compiled = self.compile( - { - "allow": [ - { - "cidr": "2001:db8::/32", - "except": ["2001:db8::/33"], - "protocol": "tcp", - "port": 443, - } - ] - } - ) - self.assertEqual(compiled.allow, ("2001:db8:8000::/33:tcp:443",)) - - def test_keeps_each_address_family_separate(self): - # IPv4 0.0.0.0/0 and IPv6 ::/96 span the same integers, so lowering - # both families together would merge or cover one with the other. - compiled = self.compile( - { - "allow": [ - { - "to": [{"cidr": "::/96"}, {"cidr": "0.0.0.0/0"}], - "ports": [{"protocol": "tcp", "port": 80}], - }, - {"to": [{"cidr": "192.0.2.0/24"}]}, - { - "to": [{"cidr": "::c000:200/120"}], - "ports": [{"protocol": "udp", "port": 53}], - }, - ], - "deny": [ - {"to": [{"cidr": "0.0.0.0/0"}]}, - {"to": [{"cidr": "::/96"}], "ports": [{"protocol": "icmp"}]}, - ], - } - ) - self.assertEqual( - compiled.allow, - ( - "0.0.0.0/0:tcp:80", - "192.0.2.0/24", - "::/96:tcp:80", - "::c000:200/120:udp:53", - ), - ) - self.assertEqual(compiled.deny, ("0.0.0.0/0", "::/96:icmp")) - - def test_lowers_the_destinations_of_each_family_separately(self): - compiled = self.compile( - { - "allow": [ - { - "to": [ - {"cidr": "192.0.2.0/25"}, - {"cidr": "2001:db8::/32", "except": ["2001:db8::/33"]}, - {"cidr": "192.0.2.128/25"}, - ], - "ports": [{"protocol": "tcp", "port": 443}], - } - ] - } - ) - self.assertEqual( - compiled.allow, - ("192.0.2.0/24:tcp:443", "2001:db8:8000::/33:tcp:443"), - ) - - def test_subtracts_ipv6_exclusions(self): - compiled = self.compile( - { - "deny": [ - { - "to": [ - { - "cidr": "2001:db8::/32", - "except": ["2001:db8:1::/48", "2001:db8:1::/64"], - } - ], - "ports": [{"protocol": "tcp", "port": 22}], - } - ] - } - ) - expected = sorted( - ipaddress.IPv6Network("2001:db8::/32").address_exclude( - ipaddress.IPv6Network("2001:db8:1::/48") - ), - key=lambda network: int(network.network_address), - ) - self.assertEqual( - compiled.deny, tuple(f"{network}:tcp:22" for network in expected) - ) - self.assertEqual(len(compiled.deny), 16) - - def test_rejects_ipv6_scopes(self): - # Clearing host bits also clears the scope of the network address, so - # the scoped addresses with host bits must not compile as unscoped. - cidrs = ( - "fe80::%eth0/64", - "fe80::1%1", - "fe80::1%eth0/64", - "fe80::1%1/64", - "::ffff:192.0.2.1%eth0/120", - ) - for category in ("allow", "deny"): - for cidr in cidrs: - for field, rule in ( - ("to[0].cidr", {"to": [{"cidr": cidr}]}), - ("to[0].except[0]", {"to": [{"cidr": "::/0", "except": [cidr]}]}), - ("cidr", {"cidr": cidr}), - ("except[0]", {"cidr": "::/0", "except": [cidr]}), - ): - message = ( - f"{category}[0].{field} must not name an IPv6 scope: {cidr}" - ) - with ( - self.subTest(category=category, rule=rule), - self.assertRaisesRegex(ScriptError, f"^{re.escape(message)}$"), - ): - self.compile({category: [rule]}) - - def test_both_families_share_the_native_rule_budget(self): - # Ports that are not adjacent cannot form one range, so each lowers to - # its own native rule. - def rules(ipv4_ports: int, ipv6_ports: int) -> dict[str, object]: - return { - "allow": [ - { - "to": [{"cidr": cidr}], - "ports": [ - {"protocol": "tcp", "port": 2 * index + 1} - for index in range(ports) - ], - } - for cidr, ports in ( - ("192.0.2.1", ipv4_ports), - ("2001:db8::1", ipv6_ports), - ) - ] - } - - self.assertEqual(len(self.compile(rules(128, 128)).allow), 256) - for ipv4_ports, ipv6_ports in ((129, 128), (128, 129)): - with ( - self.subTest(ipv4_ports=ipv4_ports, ipv6_ports=ipv6_ports), - self.assertRaisesRegex(ScriptError, "at most 256"), - ): - self.compile(rules(ipv4_ports, ipv6_ports)) - - def test_rejects_invalid_mxc_rule_shapes(self): - invalid_rules: tuple[object, ...] = ( - {"to": list[object]()}, - {"to": [dict[str, object]()]}, - {"to": [{"cidr": "192.0.2.0/24", "unknown": True}]}, - {"ports": list[object]()}, - {"ports": [{"protocol": "icmp", "port": 8}]}, - {"ports": [{"protocol": "ICMP"}]}, - {"ports": [{"protocol": "sctp"}]}, - {"ports": [{"endPort": 80}]}, - {"to": [{"cidr": "192.0.2.0/24"}], "cidr": "192.0.2.0/24"}, - ) - - for rule in invalid_rules: - with self.subTest(rule=rule), self.assertRaises(ScriptError): - self.compile({"allow": [rule]}) - - def test_rejects_unknown_and_malformed_fields(self): - invalid: tuple[object, ...] = ( - [], - {"unknown": []}, - {"allow": {}}, - {"allow": ["192.0.2.0/24"]}, - {"allow": [{"cidr": "192.0.2.0/24", "unknown": 1}]}, - {"allow": [{"cidr": 7}]}, - {"allow": [{"cidr": "192.0.2.0/24", "except": "192.0.2.1"}]}, - {"allow": [{"cidr": "192.0.2.0/24", "protocol": None}]}, - ) - - for value in invalid: - with self.subTest(value=value), self.assertRaises(ScriptError): - self.compile(value) - - def test_rejects_duplicate_json_fields(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "policy.json" - for text in ( - '{"deny":[{"cidr":"0.0.0.0/0"}],"deny":[]}', - '{"allow":[{"cidr":"192.0.2.0/24","port":80,"port":443,"protocol":"tcp"}]}', - ): - with self.subTest(text=text): - path.write_text(text, encoding="utf-8") - with self.assertRaisesRegex(ScriptError, "duplicate JSON property"): - compile_policy_file(path) - - def test_empty_destination_does_not_expand_port_range(self): - compiled = self.compile( - { - "allow": [ - { - "cidr": "192.0.2.0/24", - "except": ["192.0.2.0/24"], - "protocol": "tcp", - "port": 1, - "endPort": 65535, - } - ] - } - ) - self.assertEqual(compiled.allow, ()) - self.assertEqual(compiled.deny, ()) - - def test_limits_the_actual_policy_read(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "policy.json" - path.write_bytes(b" " * (MAX_POLICY_FILE_SIZE + 1)) - with mock.patch.object(Path, "stat") as stat: - stat.return_value.st_size = 0 - with self.assertRaisesRegex(ScriptError, "byte limit"): - compile_policy_file(path) - - def test_reports_oversized_json_integer_rejection(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "policy.json" - path.write_text( - '{"allow":[{"cidr":' + ("9" * 10000) + "}]}", - encoding="utf-8", - ) - with self.assertRaisesRegex( - ScriptError, "^JSON integer exceeds 64-digit limit$" - ): - compile_policy_file(path) - - def test_many_source_networks_can_collapse_within_native_budget(self): - start = int(ipaddress.IPv4Address("192.0.2.0")) - compiled = self.compile( - { - "allow": [ - { - "cidr": str(ipaddress.IPv4Address(start + offset)), - "protocol": "tcp", - "port": 80, - } - for offset in range(512) - ] - } - ) - self.assertEqual(compiled.allow, ("192.0.2.0/23:tcp:80",)) - - def test_accepts_exact_256_rule_boundaries(self): - def separate_ranges(cidr: str, protocol: str) -> list[dict[str, object]]: - # Ranges with a port between them cannot merge. - return [ - { - "cidr": cidr, - "protocol": protocol, - "port": 3 * index + 1, - "endPort": 3 * index + 2, - } - for index in range(MAX_RULES_PER_ACTION) - ] - - compiled = self.compile( - { - "allow": separate_ranges("192.0.2.1", "tcp"), - "deny": separate_ranges("198.51.100.1", "udp"), - } - ) - - self.assertEqual(len(compiled.allow), 256) - self.assertEqual(len(compiled.deny), 256) - self.assertEqual( - compiled.allow[:2], ("192.0.2.1/32:tcp:1-2", "192.0.2.1/32:tcp:4-5") - ) - self.assertEqual(compiled.deny[-1], "198.51.100.1/32:udp:766-767") - - def test_rejects_257_rules_before_materializing_them(self): - for category in ("allow", "deny"): - with ( - self.subTest(category=category), - self.assertRaisesRegex(ScriptError, "at most 256"), - ): - self.compile( - { - category: [ - { - "cidr": "192.0.2.1", - "protocol": "tcp", - "port": 2 * index + 1, - } - for index in range(MAX_RULES_PER_ACTION + 1) - ] - } - ) - - def test_overlapping_segments_do_not_inflate_native_rule_budget(self): - internal_ports = range(1000, 1056, 2) - compiled = self.compile( - { - "allow": [ - { - "cidr": "0.0.0.0/0", - "except": ["10.0.0.0/8"], - "protocol": "tcp", - "port": 1, - "endPort": 65535, - }, - *( - { - "cidr": "10.0.0.0/8", - "protocol": "tcp", - "port": port, - } - for port in internal_ports - ), - ] - } - ) - - expected_external = { - f"{cidr}:tcp:1-65535" - for cidr in ( - "0.0.0.0/5", - "8.0.0.0/7", - "11.0.0.0/8", - "12.0.0.0/6", - "16.0.0.0/4", - "32.0.0.0/3", - "64.0.0.0/2", - "128.0.0.0/1", - ) - } - expected_internal = {f"0.0.0.0/0:tcp:{port}" for port in internal_ports} - self.assertEqual(set(compiled.allow), expected_external | expected_internal) - self.assertEqual(len(compiled.allow), 36) - - def test_bounds_exclusions_times_port_segments(self): - def rules(count: int) -> list[dict[str, object]]: - # The exclusion leaves eight networks, and the gap between ports - # keeps each port a segment of its own. - return [ - { - "cidr": "192.0.2.0/24", - "except": ["192.0.2.1/32"], - "protocol": "tcp", - "port": 2 * index + 1, - } - for index in range(count) - ] - - accepted = self.compile({"allow": rules(32)}) - self.assertEqual(len(accepted.allow), 256) - - with self.assertRaisesRegex(ScriptError, "at most 256"): - self.compile({"allow": rules(33)}) - - # Adjacent ports merge into one range for each network. - merged = self.compile( - { - "allow": [ - {**rule, "port": index + 1} for index, rule in enumerate(rules(33)) - ] - } - ) - self.assertEqual(len(merged.allow), 8) - self.assertTrue(all(rule.endswith(":tcp:1-33") for rule in merged.allow)) - - def test_delays_fragment_conversion_until_after_covering_union(self): - exclusions = [ - str(ipaddress.IPv4Address(int(ipaddress.IPv4Address("192.0.0.1")) + 2 * i)) - for i in range(4096) - ] - fragmented = { - "cidr": "192.0.0.0/16", - "except": exclusions, - "protocol": "tcp", - "port": 80, - "endPort": 81, - } - covering = { - "cidr": "192.0.0.0/16", - "protocol": "tcp", - "port": 80, - "endPort": 81, - } - - original_summarize = ipaddress.summarize_address_range - with mock.patch.object( - ipaddress, - "summarize_address_range", - wraps=original_summarize, - ) as summarize: - for rules in ([fragmented, covering], [covering, fragmented]): - with self.subTest(order=rules): - compiled = self.compile({"allow": rules}) - self.assertEqual(compiled.allow, ("192.0.0.0/16:tcp:80-81",)) - - self.assertEqual(summarize.call_count, 2) - - def test_validates_redundant_rules_before_canonicalization(self): - with self.assertRaisesRegex(ScriptError, "unknown field"): - self.compile( - { - "allow": [ - {"cidr": "192.0.2.0/24"}, - {"cidr": "192.0.2.1", "unknown": True}, - ] - } - ) - - def test_address_only_rule_covers_large_protocol_range(self): - for rules in ( - [ - { - "cidr": "192.0.2.0/24", - "protocol": "tcp", - "port": 1, - "endPort": 65535, - }, - {"cidr": "192.0.2.0/24"}, - ], - [ - {"cidr": "192.0.2.0/24"}, - { - "cidr": "192.0.2.0/24", - "protocol": "tcp", - "port": 1, - "endPort": 65535, - }, - ], - ): - with self.subTest(rules=rules): - compiled = self.compile({"allow": rules}) - self.assertEqual(compiled.allow, ("192.0.2.0/24",)) - - def test_address_only_rule_prunes_redundant_port_events_before_sweep(self): - covering = {"cidr": "0.0.0.0/0"} - first_address = int(ipaddress.IPv4Address("192.0.0.0")) - protocol_rules = [ - { - "cidr": f"{ipaddress.IPv4Address(first_address + start)}/32", - "protocol": "tcp", - "port": start, - "endPort": 2001 - start, - } - for start in range(1, 1001) - ] - original_merge = egress_policy._merge_intervals - - for category in ("allow", "deny"): - for rules in ( - [covering, *protocol_rules], - [*protocol_rules, covering], - ): - merged_interval_counts: list[int] = [] - - def record_merge( - intervals: Iterable[tuple[int, int]], - counts: list[int] = merged_interval_counts, - ) -> tuple[tuple[int, int], ...]: - materialized = tuple(intervals) - counts.append(len(materialized)) - return original_merge(materialized) - - with ( - self.subTest( - category=category, - covering_first=rules[0] is covering, - ), - mock.patch.object( - egress_policy, - "_merge_intervals", - side_effect=record_merge, - ), - ): - compiled = self.compile({category: rules}) - self.assertEqual( - getattr(compiled, category), - ("0.0.0.0/0",), - ) - self.assertEqual(sum(merged_interval_counts), 1) - - def test_partial_address_only_coverage_keeps_protocol_union_compact(self): - address_only_rules = [ - {"cidr": f"192.0.0.{2 * index + 1}/32"} for index in range(128) - ] - protocol_rule = { - "cidr": "192.0.0.0/16", - "protocol": "tcp", - "port": 443, - } - - for category in ("allow", "deny"): - compiled_orders: list[tuple[str, ...]] = [] - for rules in ( - [*address_only_rules, protocol_rule], - [protocol_rule, *address_only_rules], - ): - with self.subTest( - category=category, - protocol_first=rules[0] is protocol_rule, - ): - compiled = self.compile({category: rules}) - compiled_rules: tuple[str, ...] = getattr(compiled, category) - self.assertEqual(len(compiled_rules), 129) - self.assertIn("192.0.0.0/16:tcp:443", compiled_rules) - self.assertEqual( - { - rule - for rule in compiled_rules - if not rule.endswith(":tcp:443") - }, - {rule["cidr"] for rule in address_only_rules}, - ) - compiled_orders.append(compiled_rules) - self.assertEqual(*compiled_orders) - - def test_partial_address_only_canonicalization_does_not_bridge_gaps(self): - compiled = self.compile( - { - "allow": [ - {"cidr": "192.0.2.1/32"}, - {"cidr": "192.0.2.0/32", "protocol": "tcp", "port": 443}, - {"cidr": "192.0.2.3/32", "protocol": "tcp", "port": 443}, - ] - } - ) - - self.assertEqual( - compiled.allow, - ( - "192.0.2.0/31:tcp:443", - "192.0.2.1/32", - "192.0.2.3/32:tcp:443", - ), - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_guest_binaries.py b/scripts/test_guest_binaries.py index dc71815c8..f667b76f8 100644 --- a/scripts/test_guest_binaries.py +++ b/scripts/test_guest_binaries.py @@ -100,6 +100,89 @@ def test_source_identity_covers_the_crate_and_the_toolchain(self): GuestRustBuildConstants.TOOLCHAIN, ) + def test_toolchain_pin_must_match_the_guest_build_constants(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / "rust-toolchain.toml").write_text( + '[toolchain]\nchannel = "9.9.9"\n', + encoding="utf-8", + ) + with self.assertRaisesRegex( + guest_binaries.ScriptError, + "Rust guest build toolchain mismatch", + ): + guest_binaries.pinned_guest_rust_toolchain(root) + + def test_source_inventory_preserves_the_callers_root_spelling(self): + with tempfile.TemporaryDirectory( + prefix=".test-guest-binary-root-", + dir=BuildConstants.REPO_ROOT, + ) as temporary: + root = Path(temporary) + relative_root = Path(os.path.relpath(root, Path.cwd())) + crate = root / "nvx-rs" + source = crate / "src" + tools = root / "scripts" / "nvx_tools" + source.mkdir(parents=True) + tools.mkdir(parents=True) + for path in ( + crate / "Cargo.toml", + crate / "Cargo.lock", + crate / "build.rs", + source / "lib.rs", + tools / "build_constants.py", + tools / "guest_binaries.py", + root / "rust-toolchain.toml", + ): + path.write_text(f"{path.name}\n", encoding="utf-8") + + inventory = guest_binaries.guest_binary_source_files(relative_root) + + self.assertTrue(all(not path.is_absolute() for path in inventory)) + self.assertEqual( + guest_binaries.guest_binary_source_sha256(relative_root), + guest_binaries.guest_binary_source_sha256(root), + ) + + def test_source_identity_follows_a_content_preserving_module_move(self): + # The guest image cache keys append this identity because hashFiles + # hashes contents only: moving a module into its own mod.rs keeps every + # file's bytes but changes the source that guest provenance records. + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + crate = root / "nvx-rs" + module = crate / "src" / "guest" / "ingress" + tools = root / "scripts" / "nvx_tools" + module.mkdir(parents=True) + tools.mkdir(parents=True) + for path in ( + crate / "Cargo.toml", + crate / "Cargo.lock", + crate / "build.rs", + crate / "src" / "lib.rs", + module / "encode.rs", + tools / "build_constants.py", + tools / "guest_binaries.py", + root / "rust-toolchain.toml", + ): + path.write_text(f"{path.name}\n", encoding="utf-8") + + def contents() -> list[bytes]: + return [ + path.read_bytes() + for path in guest_binaries.guest_binary_source_files(root) + ] + + original_contents = contents() + original_identity = guest_binaries.guest_binary_source_sha256(root) + (module / "encode").mkdir() + (module / "encode.rs").rename(module / "encode" / "mod.rs") + + self.assertEqual(contents(), original_contents) + self.assertNotEqual( + guest_binaries.guest_binary_source_sha256(root), original_identity + ) + def test_build_uses_fresh_remapped_target_and_writes_manifest(self): with tempfile.TemporaryDirectory( prefix=".test-guest-binaries-", @@ -207,9 +290,16 @@ def run_cargo( guest_binaries.guest_binary_source_sha256(), first_source ) result = guest_binaries.build_guest_binaries(output) + inspected = guest_binaries.inspect_guest_binaries(output) + (source / "lib.rs").write_bytes(b"pub fn fixture() {}\r\n") + self.assertEqual( + guest_binaries.guest_binary_source_sha256(), + result.source_sha256, + ) # One Cargo build produces every binary from the canonical recipe. self.assertEqual(len(commands), 1) + self.assertEqual(inspected, result) [command] = commands self.assertEqual(command[:3], ["cargo", "+1.95.0", "build"]) self.assertEqual( @@ -246,6 +336,7 @@ def run_cargo( [path.name for path in check_static.call_args.args[0]], list(GuestRustBuildConstants.BINARIES), ) + self.assertEqual(check_static.call_count, 2) self.assertEqual( set(result.binaries), set(GuestRustBuildConstants.BINARIES) ) @@ -437,6 +528,37 @@ def test_rebuild_refuses_unrecorded_or_unrelated_files(self): cargo.assert_not_called() self.assertEqual(_contents(output), before) + def test_inspection_refuses_an_earlier_build(self): + # A build upgrades these outputs in place, but none is a current build. + earlier = tuple( + binary + for binary in GuestRustBuildConstants.BINARIES + if binary not in GuestRustBuildConstants.ADDED_BINARIES + ) + retired = (*GuestRustBuildConstants.BINARIES, "nvx-port-io") + for case, binaries in ( + ("build without the added executables", earlier), + ("build from before nvx-port-io retired", retired), + ): + with ( + self.subTest(case), + tempfile.TemporaryDirectory( + prefix=".test-guest-binary-inspect-", + dir=BuildConstants.REPO_ROOT, + ) as temporary, + ): + output = Path(temporary) / "output" + _write_build_output(output, binaries, recorded=binaries) + before = _contents(output) + with ( + _fake_guest_build(), + self.assertRaisesRegex( + guest_binaries.ScriptError, "not a current build" + ), + ): + guest_binaries.inspect_guest_binaries(output) + self.assertEqual(_contents(output), before) + def test_initialization_failure_removes_allocated_work_directory(self): with tempfile.TemporaryDirectory( prefix=".test-guest-binary-initialization-", @@ -608,6 +730,29 @@ def test_test_command_uses_the_selected_directory(self): args.handler(args) test.assert_called_once_with(Path("build/guest-binaries")) + def test_source_identity_command_prints_the_recorded_source_digest(self): + # The guest image cache keys end with this output, so it must be the + # digest that a build records as the binaries' source. + for argv, root in ( + (["guest-binary-source-identity"], BuildConstants.REPO_ROOT), + ( + ["guest-binary-source-identity", "--root", "checkout"], + Path("checkout"), + ), + ): + with self.subTest(argv=argv): + args = nvx.parse_args(argv) + stdout = io.StringIO() + with ( + patch.object( + nvx, "guest_binary_source_sha256", return_value="c" * 64 + ) as identity, + contextlib.redirect_stdout(stdout), + ): + args.handler(args) + identity.assert_called_once_with(root) + self.assertEqual(stdout.getvalue(), "c" * 64 + "\n") + if __name__ == "__main__": unittest.main() diff --git a/scripts/test_guest_ingress.py b/scripts/test_guest_ingress.py deleted file mode 100644 index 0ddd33d5d..000000000 --- a/scripts/test_guest_ingress.py +++ /dev/null @@ -1,416 +0,0 @@ -#!/usr/bin/env python3 -"""Tests for the guest listener announcer, guest/common/nvx-ingress.c. - -The helper's test mode runs synthetic snapshots of the kernel's socket tables -and socket diagnostics through the scans that a guest runs, so these tests -check what it announces without a guest or privileges. -""" - -from __future__ import annotations - -import ipaddress -import shutil -import struct -import subprocess -import sys -import tempfile -import unittest -from dataclasses import dataclass -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[1] -SOURCE = REPO_ROOT / "guest" / "common" / "nvx-ingress.c" - -TCP = 6 -UDP = 17 -IPV4 = 1 -IPV6 = 2 -BOTH = IPV4 | IPV6 -TCP_LISTEN = 0x0A -TCP_ESTABLISHED = 0x01 -UDP_CONNECTED = 0x01 -UDP_UNCONNECTED = 0x07 -UDP_SETTLE_MS = 1000 -# The helper's first source port, which its test mode excludes like its own. -OWN_PORT = 1023 -TABLE_HEADER = ( - " sl local_address rem_address st tx_queue rx_queue tr tm->when " - "retrnsmt uid timeout inode\n" -) -NETLINK_HEADER = struct.Struct("=IHHII") -SOCK_DIAG_BY_FAMILY = 20 -NETLINK_DONE = 3 -INET_DIAG_SKV6ONLY = 11 -AF_INET6 = 10 - - -def kernel_address(address: str) -> str: - """Render an address as the kernel's socket tables print it on x86.""" - packed = ipaddress.ip_address(address).packed - return "".join( - f"{int.from_bytes(packed[offset : offset + 4], 'little'):08X}" - for offset in range(0, len(packed), 4) - ) - - -@dataclass(frozen=True) -class Socket: - protocol: int - address: str - port: int - inode: int - state: int - peer: tuple[str, int] | None = None - - @property - def ipv6(self) -> bool: - return ipaddress.ip_address(self.address).version == 6 - - @property - def table(self) -> str: - return ("tcp" if self.protocol == TCP else "udp") + ("6" if self.ipv6 else "") - - def line(self, index: int) -> str: - remote, remote_port = self.peer or ("::" if self.ipv6 else "0.0.0.0", 0) - return ( - f"{index:4}: {kernel_address(self.address)}:{self.port:04X} " - f"{kernel_address(remote)}:{remote_port:04X} {self.state:02X} " - "00000000:00000000 00:00000000 00000000 0 0 " - f"{self.inode} 2 0000000000000000\n" - ) - - -def tcp_listener(address: str, port: int, inode: int = 1) -> Socket: - return Socket(TCP, address, port, inode, TCP_LISTEN) - - -def udp_socket( - address: str, port: int, inode: int, peer: tuple[str, int] | None = None -) -> Socket: - state = UDP_UNCONNECTED if peer is None else UDP_CONNECTED - return Socket(UDP, address, port, inode, state, peer) - - -def diag_listener(port: int, ipv6_only: bool, address: str = "::") -> bytes: - """Encode a socket diagnostics message for an IPv6 TCP listener.""" - message = struct.pack("=BBBB", AF_INET6, TCP_LISTEN, 0, 0) - message += struct.pack("!HH", port, 0) - message += ipaddress.IPv6Address(address).packed + bytes(16) - message += struct.pack("=III", 0, 0, 0) + struct.pack("=IIIII", 0, 0, 0, 0, 0) - message += struct.pack("=HH", 5, INET_DIAG_SKV6ONLY) + bytes( - (int(ipv6_only), 0, 0, 0) - ) - return ( - NETLINK_HEADER.pack( - NETLINK_HEADER.size + len(message), SOCK_DIAG_BY_FAMILY, 2, 1, 0 - ) - + message - ) - - -def diag_done() -> bytes: - return NETLINK_HEADER.pack(NETLINK_HEADER.size + 4, NETLINK_DONE, 2, 1, 0) + bytes( - 4 - ) - - -@dataclass(frozen=True) -class Snapshot: - time_ms: int - sockets: tuple[Socket, ...] = () - bindv6only: int | None = None - diag: bytes | None = None - - def write(self, root: Path) -> None: - (root / "net").mkdir(parents=True) - for table in ("tcp", "tcp6", "udp", "udp6"): - sockets = [socket for socket in self.sockets if socket.table == table] - (root / "net" / table).write_text( - TABLE_HEADER - + "".join(socket.line(index) for index, socket in enumerate(sockets)), - encoding="utf-8", - ) - if self.bindv6only is not None: - sysctl = root / "sys" / "net" / "ipv6" - sysctl.mkdir(parents=True) - (sysctl / "bindv6only").write_text(f"{self.bindv6only}\n", encoding="utf-8") - if self.diag is not None: - (root / "diag").write_bytes(self.diag) - (root / "time").write_text(f"{self.time_ms}\n", encoding="utf-8") - - -@dataclass(frozen=True) -class Announcement: - payload: bytes - truncated: bool - listeners: dict[tuple[int, int], int] - order: tuple[tuple[int, int], ...] - - @classmethod - def decode(cls, payload: bytes) -> Announcement: - magic, version, flags, count = struct.unpack("!4sBBH", payload[:8]) - if magic != b"NVXL" or version != 1 or len(payload) != 8 + 4 * count: - raise AssertionError(f"malformed announcement {payload.hex()}") - entries = [ - struct.unpack("!BBH", payload[8 + 4 * index : 12 + 4 * index]) - for index in range(count) - ] - return cls( - payload, - bool(flags & 1), - {(protocol, port): families for protocol, families, port in entries}, - tuple((protocol, port) for protocol, _families, port in entries), - ) - - -@unittest.skipUnless(sys.platform.startswith("linux"), "nvx-ingress builds on Linux") -class GuestIngressTests(unittest.TestCase): - binary: Path - directory: tempfile.TemporaryDirectory[str] - - @classmethod - def setUpClass(cls) -> None: - compiler = shutil.which("cc") or shutil.which("gcc") - if compiler is None: - raise unittest.SkipTest("a C compiler is unavailable") - cls.directory = tempfile.TemporaryDirectory() - cls.binary = Path(cls.directory.name) / "nvx-ingress" - subprocess.run( - [ - compiler, - "-O1", - "-Wall", - "-Wextra", - "-Werror", - "-o", - str(cls.binary), - str(SOURCE), - ], - check=True, - capture_output=True, - timeout=120, - ) - - @classmethod - def tearDownClass(cls) -> None: - cls.directory.cleanup() - - def announce(self, *snapshots: Snapshot) -> list[Announcement]: - with tempfile.TemporaryDirectory() as temporary: - for index, snapshot in enumerate(snapshots): - snapshot.write(Path(temporary) / str(index)) - result = subprocess.run( - [str(self.binary), "test", temporary], - capture_output=True, - text=True, - timeout=30, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - lines = result.stdout.splitlines() - self.assertEqual( - [int(line.split()[0]) for line in lines], - [snapshot.time_ms for snapshot in snapshots], - ) - return [Announcement.decode(bytes.fromhex(line.split()[1])) for line in lines] - - def test_announcement_golden_vector(self): - (announcement,) = self.announce( - Snapshot( - 0, - (tcp_listener("0.0.0.0", 8080), tcp_listener("fd00::a00:2", 8443)), - ) - ) - self.assertEqual(announcement.payload.hex(), "4e56584c0100000206011f90060220fb") - - def test_only_listeners_reachable_through_the_gateway_are_announced(self): - sockets = ( - tcp_listener("0.0.0.0", 8080), - tcp_listener("192.0.2.2", 8081), - tcp_listener("127.0.0.1", 8082), - tcp_listener("127.0.0.53", 8083), - tcp_listener("::", 8084), - tcp_listener("fd00::a00:2", 8085), - tcp_listener("::1", 8086), - tcp_listener("fe80::1", 8087), - tcp_listener("::ffff:127.0.0.1", 8088), - tcp_listener("::ffff:192.0.2.2", 8089), - # One port's IPv4 and IPv6 listeners merge their families. - tcp_listener("0.0.0.0", 8090), - tcp_listener("fd00::a00:2", 8090, inode=2), - # Established connections are not listeners. - Socket(TCP, "192.0.2.2", 8091, 3, TCP_ESTABLISHED, ("192.0.2.1", 40000)), - ) - (announcement,) = self.announce(Snapshot(0, sockets)) - self.assertEqual( - announcement.listeners, - { - (TCP, 8080): IPV4, - (TCP, 8081): IPV4, - (TCP, 8084): BOTH, - (TCP, 8085): IPV6, - (TCP, 8089): IPV4, - (TCP, 8090): BOTH, - }, - ) - self.assertFalse(announcement.truncated) - - def test_ipv6_only_tcp_listeners_follow_socket_diagnostics(self): - sockets = tuple( - tcp_listener("::", port, inode=port) for port in range(7001, 7006) - ) - diag = ( - diag_listener(7001, ipv6_only=True) - + diag_listener(7002, ipv6_only=False) - # A port that IPv6-only and dual-stack sockets share takes IPv4. - + diag_listener(7003, ipv6_only=True) - + diag_listener(7003, ipv6_only=False) - # Only listeners on the unspecified address are classified. - + diag_listener(7004, ipv6_only=True, address="fd00::a00:2") - + diag_done() - # Messages after the end of the dump are ignored. - + diag_listener(7005, ipv6_only=True) - ) - default, ipv6_only_default = self.announce( - Snapshot(0, sockets, diag=diag), - Snapshot(100, sockets, bindv6only=1, diag=diag_listener(7002, False)), - ) - self.assertEqual( - default.listeners, - { - (TCP, 7001): IPV6, - (TCP, 7002): BOTH, - (TCP, 7003): BOTH, - (TCP, 7004): BOTH, - (TCP, 7005): BOTH, - }, - ) - # Without diagnostics for a socket, net.ipv6.bindv6only decides. - self.assertEqual( - ipv6_only_default.listeners, - { - (TCP, 7001): IPV6, - (TCP, 7002): BOTH, - (TCP, 7003): IPV6, - (TCP, 7004): IPV6, - (TCP, 7005): IPV6, - }, - ) - - def test_malformed_socket_diagnostics_are_ignored(self): - sockets = (tcp_listener("::", 7101),) - valid = diag_listener(7101, ipv6_only=True) - short_attribute = bytearray(valid) - struct.pack_into("=H", short_attribute, NETLINK_HEADER.size + 72, 2) - long_attribute = bytearray(valid) - struct.pack_into("=H", long_attribute, NETLINK_HEADER.size + 72, 200) - for name, diag in ( - ("garbage", bytes(range(37))), - ("short header", NETLINK_HEADER.pack(8, SOCK_DIAG_BY_FAMILY, 0, 0, 0)), - ( - "overlong message", - NETLINK_HEADER.pack(4096, SOCK_DIAG_BY_FAMILY, 0, 0, 0), - ), - ("truncated message", valid[:40]), - ("short attribute", bytes(short_attribute)), - ("overlong attribute", bytes(long_attribute)), - ): - with self.subTest(name): - (announcement,) = self.announce(Snapshot(0, sockets, diag=diag)) - self.assertEqual(announcement.listeners, {(TCP, 7101): BOTH}) - - def test_udp_sockets_settle_before_they_are_announced(self): - sockets = ( - udp_socket("0.0.0.0", 7201, 10), - udp_socket("::", 7202, 11), - # The helper's own source port is never announced. - udp_socket("0.0.0.0", OWN_PORT, 12), - # Nor is a UDP socket bound to loopback. - udp_socket("127.0.0.1", 7203, 13), - ) - announcements = self.announce( - Snapshot(0, sockets), - Snapshot(UDP_SETTLE_MS - 1, sockets), - Snapshot(UDP_SETTLE_MS, sockets), - Snapshot(UDP_SETTLE_MS + 100, sockets, bindv6only=1), - ) - self.assertEqual(announcements[0].listeners, {}) - self.assertEqual(announcements[1].listeners, {}) - self.assertEqual( - announcements[2].listeners, {(UDP, 7201): IPV4, (UDP, 7202): BOTH} - ) - self.assertEqual( - announcements[3].listeners, {(UDP, 7201): IPV4, (UDP, 7202): IPV6} - ) - - def test_udp_ports_follow_the_identity_of_their_sockets(self): - server = udp_socket("0.0.0.0", 7301, 20) - replacement = udp_socket("0.0.0.0", 7301, 21) - connected = udp_socket("192.0.2.2", 7301, 21, ("198.51.100.7", 5353)) - client = udp_socket("192.0.2.2", 7301, 22, ("192.0.2.1", 53)) - timeline = ( - (0, (server,), None), - (UDP_SETTLE_MS, (server,), IPV4), - # Another socket on the port settles anew. - (UDP_SETTLE_MS + 100, (replacement,), None), - (2 * UDP_SETTLE_MS + 99, (replacement,), None), - (2 * UDP_SETTLE_MS + 100, (replacement,), IPV4), - # A server that connects its announced socket keeps the port. - (2 * UDP_SETTLE_MS + 200, (connected,), IPV4), - (5 * UDP_SETTLE_MS, (connected,), IPV4), - # A connected client that reuses the port never takes it over. - (5 * UDP_SETTLE_MS + 100, (client,), None), - (9 * UDP_SETTLE_MS, (client,), None), - ) - announcements = self.announce( - *(Snapshot(time_ms, sockets) for time_ms, sockets, _ in timeline) - ) - for (time_ms, _sockets, families), announcement in zip( - timeline, announcements, strict=True - ): - with self.subTest(time_ms=time_ms): - expected = {} if families is None else {(UDP, 7301): families} - self.assertEqual(announcement.listeners, expected) - - def test_udp_ports_announce_the_families_of_their_sockets(self): - ipv4 = udp_socket("0.0.0.0", 7401, 30) - ipv6 = udp_socket("::", 7401, 31) - ipv4_connected = udp_socket("192.0.2.2", 7401, 30, ("198.51.100.7", 4000)) - ipv6_connected = udp_socket("fd00::a00:2", 7401, 31, ("fd12::7", 4000)) - timeline = ( - (0, (ipv4, ipv6), None), - (UDP_SETTLE_MS, (ipv4, ipv6), BOTH), - (UDP_SETTLE_MS + 100, (ipv4_connected, ipv6), BOTH), - (UDP_SETTLE_MS + 200, (ipv4_connected, ipv6_connected), BOTH), - (UDP_SETTLE_MS + 300, (ipv6_connected,), IPV6), - (UDP_SETTLE_MS + 400, (), None), - ) - announcements = self.announce( - *( - Snapshot(time_ms, sockets, bindv6only=1) - for time_ms, sockets, _ in timeline - ) - ) - for (time_ms, _sockets, families), announcement in zip( - timeline, announcements, strict=True - ): - with self.subTest(time_ms=time_ms): - expected = {} if families is None else {(UDP, 7401): families} - self.assertEqual(announcement.listeners, expected) - - def test_announcements_carry_the_lowest_64_listeners(self): - sockets = tuple( - tcp_listener("0.0.0.0", port, inode=port) for port in range(2069, 1999, -1) - ) - (announcement,) = self.announce(Snapshot(0, sockets)) - self.assertTrue(announcement.truncated) - self.assertEqual( - announcement.order, tuple((TCP, port) for port in range(2000, 2064)) - ) - (complete,) = self.announce(Snapshot(0, sockets[-64:])) - self.assertFalse(complete.truncated) - self.assertEqual(len(complete.order), 64) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_guest_time.py b/scripts/test_guest_time.py deleted file mode 100644 index 56ef24a53..000000000 --- a/scripts/test_guest_time.py +++ /dev/null @@ -1,943 +0,0 @@ -#!/usr/bin/env python3 -"""Tests for the guest time component, guest/common/nvx-time.c. - -The fixtures encode the guest-visible formats independently of the C decoders. -The restore packet (doc/design/5.5-restore-protocol.md) and the time sample -(doc/design/3.2-clock-and-time.md) follow the guide; the other fixtures, such -as the exhaustive check's lines and the state file, pin the formats that -nvx-time.c defines. -""" - -from __future__ import annotations - -import os -import re -import shutil -import struct -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[1] -SOURCE = REPO_ROOT / "guest" / "common" / "nvx-time.c" - -DOWNTIME_UTC = 0x01 -MEMORY_TARGET = 0x02 -ACK_REQUIRED = 0x04 -TEST_HOOKS = 0x08 -UTC_NS = 1_790_841_600_123_456_789 -ENTROPY = bytes(range(64)) -PREVIOUS_ID = "ff" * 16 -THIRTY_DAYS_NS = 2_592_000 * 1_000_000_000 - - -def encode_packet( - *, - flags: int = 0, - online: int = 0, - ranges: tuple[tuple[int, int], ...] = (), - range_count: int | None = None, - generation: int = 1, - rate_deviation: int = 0, - downtime_ns: int = 0, - utc_ns: int = UTC_NS, - magic: bytes = b"OVR", - version: int = 4, - reserved: int = 0, - entropy: bytes = ENTROPY, -) -> bytes: - """Encode a restore packet v4 (guide: 5.5.5 "Restore Packet").""" - count = len(ranges) if range_count is None else range_count - header = magic + bytes((version, flags, online, count, reserved)) - header += struct.pack(" bytes: - """Encode a time sample (guide: 3.2.9 "Time Samples").""" - return struct.pack(" bool: - """Whether this process may switch from SCHED_IDLE back to SCHED_OTHER. - - That needs CAP_SYS_NICE, which the guest has, or an RLIMIT_NICE of at - least 20; containers and CI runners usually have neither. - """ - status = Path("/proc/self/status").read_text(encoding="utf-8") - capabilities = re.search(r"^CapEff:\s*([0-9a-f]+)$", status, re.MULTILINE) - if capabilities is not None and int(capabilities.group(1), 16) & (1 << 23): - return True - limits = Path("/proc/self/limits").read_text(encoding="utf-8") - nice = re.search(r"^Max nice priority\s+(\S+)", limits, re.MULTILINE) - return nice is not None and ( - nice.group(1) == "unlimited" or int(nice.group(1)) >= 20 - ) - - -TIMER_LIST_CPU = """\ -Tick Device: mode: 1 -Per CPU device: {cpu} -Clock Event Device: {device} - max_delta_ns: 1374389534634 - min_delta_ns: 1000 - mult: 6710886 - shift: 26 - mode: {state} - next_event: 1342000000 nsecs - set_next_event: lapic_next_event - shutdown: lapic_timer_shutdown - periodic: lapic_timer_set_periodic - oneshot: lapic_timer_set_oneshot - oneshot stopped: lapic_timer_shutdown - event_handler: {handler} - retries: 0 - -""" - -TIMER_LIST_BROADCAST = """\ -Tick Device: mode: 1 -Broadcast device -Clock Event Device: {device} -tick_broadcast_mask: 00 -tick_broadcast_oneshot_mask: 00 - -""" - -CPUINFO_CPU = """\ -processor\t: {cpu} -vendor_id\t: GenuineIntel -cpu family\t: 6 -model\t\t: 85 -cpu MHz\t\t: 2194.843 -flags\t\t: fpu vme {flags} -bugs\t\t: spectre_v1 - -""" - -GOOD_FLAGS = ( - "tsc msr pae constant_tsc rdtscp nonstop_tsc tsc_known_freq tsc_reliable " - "hypervisor arat x2apic" -) - - -@unittest.skipUnless(sys.platform.startswith("linux"), "nvx-time builds on Linux") -class GuestTimeTests(unittest.TestCase): - binary: Path - directory: tempfile.TemporaryDirectory[str] - - @classmethod - def setUpClass(cls) -> None: - compiler = shutil.which("cc") or shutil.which("gcc") - if compiler is None: - raise unittest.SkipTest("a C compiler is unavailable") - cls.directory = tempfile.TemporaryDirectory() - cls.binary = Path(cls.directory.name) / "nvx-time" - subprocess.run( - [ - compiler, - "-O1", - "-Wall", - "-Wextra", - "-Werror", - "-o", - str(cls.binary), - str(SOURCE), - ], - check=True, - capture_output=True, - timeout=120, - ) - - @classmethod - def tearDownClass(cls) -> None: - cls.directory.cleanup() - - def run_test(self, *arguments: str) -> str: - result = subprocess.run( - [str(self.binary), "test", *arguments], - capture_output=True, - text=True, - timeout=10, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - return result.stdout - - def fixture(self, name: str, content: bytes | str) -> str: - path = Path(self.directory.name) / name - if isinstance(content, str): - path.write_text(content, encoding="utf-8") - else: - path.write_bytes(content) - return str(path) - - def packet(self, packet: bytes, recorded: int = 0, previous: str = PREVIOUS_ID): - return self.run_test( - "packet", self.fixture("packet.bin", packet), str(recorded), previous - ).strip() - - def test_packet_v4_golden_vector(self): - packet = encode_packet( - flags=DOWNTIME_UTC | ACK_REQUIRED | TEST_HOOKS, - online=4, - generation=3, - rate_deviation=-13_107_200, - downtime_ns=30_000_000_000, - ) - - self.assertEqual(len(packet), 96) - self.assertEqual( - self.packet(packet, recorded=2), - "ok flags=13 online=4 ranges=0 generation=3 " - "rate_deviation=-13107200 frequency=13107200 " - "downtime_ns=30000000000 " - f"utc_ns={UTC_NS} generation_id={ENTROPY[:16].hex()}", - ) - - def test_musl_build_matches_the_reference_build(self): - musl = shutil.which("musl-gcc") - if musl is None: - self.skipTest("musl-gcc is unavailable") - binary = Path(self.directory.name) / "nvx-time-musl" - subprocess.run( - [musl, "-static", "-Os", "-s", "-Wall", "-Wextra", "-Werror"] - + ["-o", str(binary), str(SOURCE)], - check=True, - capture_output=True, - timeout=120, - ) - packet = self.fixture( - "musl-packet.bin", encode_packet(flags=ACK_REQUIRED, generation=2) - ) - arguments = ["test", "packet", packet, "1", PREVIOUS_ID] - result = subprocess.run( - [str(binary), *arguments], - capture_output=True, - text=True, - timeout=10, - check=False, - ) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, self.run_test(*arguments[1:])) - # musl has no sched_setscheduler(); the daemon must still reach - # SCHED_IDLE after a restore and leave it after the bound. - policy = 0 if can_leave_idle_priority() else 5 - for name, expected in ( - ("idle-priority", f"idle=5 normal={policy}\n"), - ("promote", f"promoted caller={policy} thread={policy}\n"), - ): - result = subprocess.run( - [str(binary), "test", name], - capture_output=True, - text=True, - timeout=10, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertTrue(result.stdout.startswith(expected), result.stdout) - self.assertTrue(self.run_test(name).startswith(expected)) - - def test_packet_memory_ranges_follow_the_header(self): - packet = encode_packet( - flags=MEMORY_TARGET, - ranges=((0x8000_0000, 0x0800_0000), (0x1_0000_0000, 0x1000_0000)), - ) - - self.assertEqual(len(packet), 32 + 2 * 16 + 64) - self.assertTrue( - self.packet(packet).endswith( - " range=2147483648:134217728 range=4294967296:268435456" - ) - ) - self.assertIn(" ranges=0 ", self.packet(encode_packet(flags=MEMORY_TARGET))) - - def test_packet_rejections_name_the_repair_code(self): - cases = { - "magic": encode_packet(magic=b"OVX"), - "version 3": encode_packet(version=3), - "reserved flag": encode_packet(flags=0x10), - "reserved byte": encode_packet(reserved=1), - "online count": encode_packet(online=3), - "ranges without target": encode_packet(range_count=1), - "generation zero": encode_packet(generation=0), - "rate above": encode_packet(rate_deviation=16_384_001), - "rate below": encode_packet(rate_deviation=-16_384_001), - "downtime": encode_packet(downtime_ns=THIRTY_DAYS_NS + 1), - "utc zero": encode_packet(utc_ns=0), - "empty range": encode_packet(flags=MEMORY_TARGET, ranges=((0, 0),)), - "range overflow": encode_packet( - flags=MEMORY_TARGET, ranges=((2**64 - 4096, 8192),) - ), - } - for name, packet in cases.items(): - with self.subTest(case=name): - self.assertTrue( - self.packet(packet).startswith("error G_REPAIR_PACKET "), - name, - ) - - def test_packet_bounds_are_inclusive(self): - for packet in ( - encode_packet(rate_deviation=16_384_000), - encode_packet(rate_deviation=-16_384_000), - encode_packet(downtime_ns=THIRTY_DAYS_NS), - encode_packet(online=8), - ): - with self.subTest(packet=packet[:32].hex()): - self.assertTrue(self.packet(packet).startswith("ok ")) - - def test_packet_generation_must_follow_the_recorded_generation(self): - self.assertEqual(self.packet(encode_packet(generation=5), recorded=5), "stale") - self.assertTrue( - self.packet(encode_packet(generation=7), recorded=5).startswith( - "error G_REPAIR_GENERATION " - ) - ) - self.assertTrue( - self.packet( - encode_packet(generation=6), recorded=5, previous=ENTROPY[:16].hex() - ).startswith("error G_REPAIR_GENERATION ") - ) - - def sample(self, sample: bytes, generation: int = 0) -> str: - return self.run_test( - "sample", self.fixture("sample.bin", sample), str(generation) - ).strip() - - def test_time_sample_layout(self): - self.assertEqual(len(encode_sample()), 16) - self.assertEqual( - self.sample(encode_sample(generation=4, flags=TEST_HOOKS), 4), - f"ok flags=8 generation=4 utc_ns={UTC_NS}", - ) - for name, sample in { - "version": encode_sample(version=2), - "flags": encode_sample(flags=0x01), - "reserved": encode_sample(reserved=1), - "utc": encode_sample(utc_ns=0), - "generation": encode_sample(generation=3), - }.items(): - with self.subTest(case=name): - self.assertTrue(self.sample(sample).startswith("error "), name) - - def test_utc_pairing_uses_the_bracket_midpoint(self): - self.assertEqual( - self.run_test("pair", "1000", "3000", "5000").strip(), - "theta_ns=3000 epsilon_ns=1000", - ) - self.assertEqual( - self.run_test("pair", "10000", "10400", "9000").strip(), - "theta_ns=-1200 epsilon_ns=200", - ) - - def test_restore_frequency_compensates_and_clamps(self): - self.assertEqual( - self.run_test("frequency", "13107200").strip(), - "frequency=-13107200 ppb=-200000", - ) - self.assertEqual( - self.run_test("frequency", "-40000000").strip(), - "frequency=32768000 ppb=500000", - ) - self.assertEqual( - self.run_test("frequency", "-65536").strip(), - "frequency=65536 ppb=1000", - ) - - def test_discipline_steps_at_128_ms_and_slews_below(self): - self.assertEqual( - self.run_test("correction", "127999999", "10002", "fast").strip(), - "slew modes=0x203d offset=127999999 status=0x2001 constant=4 " - "maxerror=128011 esterror=11", - ) - self.assertEqual( - self.run_test("correction", "-500", "20", "slow").strip(), - "slew modes=0x203d offset=-500 status=0x2001 constant=6 " - "maxerror=1 esterror=1", - ) - self.assertEqual( - self.run_test("correction", "128000000", "10", "fast").strip(), - "step modes=0x2100 sec=0 nsec=128000000", - ) - self.assertEqual( - self.run_test("correction", "-1128000000", "10", "slow").strip(), - "step modes=0x2100 sec=-2 nsec=872000000", - ) - - def test_watcher_rules_match_linux_6_18_messages(self): - messages = { - "tsc: Marking TSC unstable due to clocksource watchdog": ("G_TSC_UNSTABLE"), - "clocksource: timekeeping watchdog on CPU0: Marking clocksource " - "'tsc-early' as unstable because the skew is too large:": ( - "G_CLOCKSOURCE_UNSTABLE" - ), - "clocksource: Switched to clocksource refined-jiffies": ( - "G_CLOCKSOURCE_SWITCH" - ), - "clocksource: CPUs 1 ahead of CPU 0 for clocksource tsc.": ( - "G_CLOCKSOURCE_SKEW" - ), - "clocksource: CPUs 2-3 behind CPU 0 for clocksource tsc.": ( - "G_CLOCKSOURCE_SKEW" - ), - "TSC synchronization [CPU#0 -> CPU#1]:": "G_TSC_WARP", - "Measured 45 cycles TSC warp between CPUs, turning off TSC clock.": ( - "G_TSC_WARP" - ), - "TSC warped randomly between CPUs": "G_TSC_WARP", - "[Firmware Bug]: TSC ADJUST differs: CPU1 0 --> 100. Restoring": ( - "G_TSC_ADJUST" - ), - "rcu: INFO: rcu_preempt detected stalls on CPUs/tasks:": "G_RCU_STALL", - "rcu: INFO: rcu_preempt self-detected stall on CPU": "G_RCU_STALL", - "rcu: INFO: rcu_preempt detected expedited stalls on CPUs/tasks: " - "{ 1-... }": "G_RCU_STALL", - "rcu: rcu_preempt kthread starved for 2603 jiffies! g345 f0x0": ( - "G_RCU_STARVED" - ), - "rcu: rcu_preempt kthread timer wakeup didn't happen for 2602 " - "jiffies! g345 f0x0": "G_RCU_STARVED", - "watchdog: BUG: soft lockup - CPU#0 stuck for 22s! [sh:123]": ( - "G_SOFT_LOCKUP" - ), - "watchdog: Watchdog detected hard LOCKUP on cpu 1": "G_HARD_LOCKUP", - "INFO: task kworker/0:1:12 blocked for more than 120 seconds.": ( - "G_HUNG_TASK" - ), - "unchecked MSR access error: RDMSR from 0x1ad at rIP: " - "0xffffffff816f11ee (__rdmsr_on_cpu+0x1e/0x30)": "G_UNCHECKED_MSR", - "[Firmware Bug]: CPU 1: APIC ID mismatch. CPUID: 0x0000 " - "APIC: 0x0001": "G_APIC_ID_MISMATCH", - "clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x1fa3": "none", - "Hyper-V: LAPIC Timer Frequency: 0x1e8480": "none", - "rcu: Hierarchical RCU implementation.": "none", - 'NVX-TIME-ABI-VIOLATION: v=1 code=G_UNCHECKED_MSR detail="unchecked ' - 'MSR access error"': "none", - } - output = self.run_test( - "kmsg", self.fixture("kmsg.txt", "\n".join(messages) + "\n") - ) - self.assertEqual(output.splitlines(), list(messages.values())) - - def boot_log(self, lines: list[str]) -> str: - return self.run_test( - "boot-log", self.fixture("boot.txt", "\n".join(lines) + "\n") - ).strip() - - BOOT_LOG = [ - "Hypervisor detected: Microsoft Hyper-V", - "Hyper-V: privilege flags low 0x8860, high 0x0, ext 0x0, hints 0x0, misc 0x100", - "Hyper-V: LAPIC Timer Frequency: 0x1e8480", - "tsc: Detected 2194.843 MHz processor", - "clocksource: Switched to clocksource tsc-early", - "clocksource: Switched to clocksource tsc", - ] - - def test_boot_log_requires_the_identity_and_a_final_switch_to_tsc(self): - self.assertEqual(self.boot_log(self.BOOT_LOG), "ok") - kvm_rate = [line.replace("0x1e8480", "0x989680") for line in self.BOOT_LOG] - self.assertEqual(self.boot_log(kvm_rate), "ok") - failures = { - "missing identity": self.BOOT_LOG[1:], - "wrong LAPIC rate": [ - line.replace("0x1e8480", "0x1e8481") for line in self.BOOT_LOG - ], - "last switch": [ - *self.BOOT_LOG, - "clocksource: Switched to clocksource refined-jiffies", - ], - "kvm-clock": [*self.BOOT_LOG, "kvm-clock: Using msrs 4b564d01"], - "refinement": [ - *self.BOOT_LOG, - "tsc: Refined TSC clocksource calibration: 2194.843 MHz", - ], - "lapic token": [ - *self.BOOT_LOG, - "APIC timer: using supplied frequency 200000000 Hz", - ], - "watcher record": [ - *self.BOOT_LOG, - "tsc: Marking TSC unstable due to clocksource watchdog", - ], - } - for name, lines in failures.items(): - with self.subTest(case=name): - self.assertTrue(self.boot_log(lines).startswith("fail "), name) - - def test_boot_log_reads_syslog_read_all_lines(self): - # SYSLOG_ACTION_READ_ALL prefixes every line with "" and, with - # printk timestamps, "[seconds.micros] ". - lines = [ - f"<6>[{index:5d}.{index * 7919 % 1000000:06d}] {line}" - for index, line in enumerate(self.BOOT_LOG) - ] - lines.insert(1, "<6>[ 0.123456] [drm] a bracketed message is kept") - own_event = ( - "<2>[ 3.000000] NVX-TIME-REPORT-VIOLATION: v=1 code=G_CONFORMANCE_C4 " - 'detail="forbidden record: kvm-clock: Using msrs"' - ) - - def syslog(text_lines: list[str]) -> str: - return self.run_test( - "syslog", self.fixture("syslog.txt", "\n".join(text_lines) + "\n") - ).strip() - - self.assertEqual(syslog(lines), "ok") - self.assertEqual(syslog([*lines, own_event]), "ok") - self.assertTrue( - syslog([*lines, "<6>[ 4.000000] kvm-clock: Using msrs"]).startswith( - "fail forbidden record: kvm-clock" - ) - ) - self.assertTrue(syslog(lines[2:]).startswith("fail no 'Hypervisor")) - - def test_boot_log_reports_a_kernel_wx_mapping_apart_from_c4(self): - # Linux 6.18's CONFIG_DEBUG_WX audit, as logged when the ITS - # mitigation's thunk pages are left writable and executable. - warning = [ - "<4>[ 0.412345] ------------[ cut here ]------------", - "<4>[ 0.412346] x86/mm: Found insecure W+X mapping at address " - "0xffffffffc0000000", - "<4>[ 0.412400] WARNING: CPU: 0 PID: 1 at " - "arch/x86/mm/dump_pagetables.c:246 note_wx+0x5a/0x70", - "<6>[ 0.412500] x86/mm: Checked W+X mappings: FAILED, 4 W+X pages " - "found.", - ] - lines = [f"<6>[ 0.100000] {line}" for line in self.BOOT_LOG] - - def syslog(text_lines: list[str]) -> list[str]: - return ( - self.run_test( - "syslog", - self.fixture("syslog.txt", "\n".join(text_lines) + "\n"), - ) - .strip() - .splitlines() - ) - - self.assertEqual( - syslog([*lines[:2], *warning, *lines[2:]]), - [ - "ok", - "wx x86/mm: Found insecure W+X mapping at address 0xffffffffc0000000", - ], - ) - self.assertEqual( - syslog( - [ - *lines, - "<6>[ 0.412500] x86/mm: Checked W+X mappings: passed, " - "no W+X pages found.", - ] - ), - ["ok"], - ) - failed = syslog([*warning, *lines[1:]]) - self.assertEqual(len(failed), 2) - self.assertTrue(failed[0].startswith("fail no 'Hypervisor")) - self.assertTrue(failed[1].startswith("wx x86/mm: Found insecure W+X")) - own_event = ( - "<2>[ 3.000000] NVX-TIME-REPORT-VIOLATION: v=1 code=G_KERNEL_WX " - 'detail="x86/mm: Found insecure W+X mapping at address 0x0"' - ) - self.assertEqual(syslog([*lines, own_event]), ["ok"]) - - def test_daemon_lines_start_on_a_fresh_console_line(self): - # The daemon prints violation events while a shell may be in the - # middle of a line (its prompt, for example). - line = "NVX-TIME-ABI-VIOLATION: v=1 code=G_RCU_STALL source=watcher\n" - self.assertEqual(self.run_test("console", line, "sync"), line) - self.assertEqual(self.run_test("console", line, "async"), "\n" + line) - - def test_exhaustive_leaf_rules(self): - # X1 and X2: a leaf reads zero or the highest basic leaf's result, - # except the explicit zero leaves, which read zero. - out_of_range = ("0x00000d0b", "0x00000001", "0x00000002", "0x00000003") - zero = ("0", "0", "0", "0") - - def leaf(number: str, value: tuple[str, ...]) -> str: - return self.run_test( - "exhaustive-leaf", number, *value, *out_of_range - ).strip() - - self.assertEqual(leaf("0x40000006", zero), "pass") - self.assertEqual(leaf("0x40000010", out_of_range), "pass") - self.assertEqual(leaf("0x40000100", zero), "pass") - self.assertEqual(leaf("0x40000100", out_of_range), "pass") - explicit = leaf("0x40000081", out_of_range) - self.assertTrue(explicit.startswith("fail leaf 0x40000081 is "), explicit) - self.assertTrue(explicit.endswith("expected zero"), explicit) - # KVM's signature leaf moved to a higher base. - kvm = ("0x40000101", "0x4b4d564b", "0x564b4d56", "0x0000004d") - signature = leaf("0x40000100", kvm) - self.assertIn('(signature "KVMKVMKVM...")', signature) - self.assertTrue(signature.endswith("or the highest basic leaf"), signature) - - def test_exhaustive_report_format(self): - # The exhaustive check prints one line per check and CPU, with an - # escaped detail, then a summary. - line = re.compile( - r"^NVX-TIME-ABI-EXHAUSTIVE: v=1 check=X[1-6] cpu=\d+ " - r'status=(pass|fail) detail="(?:[^"\\]|\\.)*"$' - ) - passed = self.run_test( - "exhaustive-report", "X3", "2", "pass", "ok" - ).splitlines() - self.assertEqual( - passed, - [ - 'NVX-TIME-ABI-EXHAUSTIVE: v=1 check=X3 cpu=2 status=pass detail="ok"', - "failures=0", - ], - ) - failed = self.run_test( - "exhaustive-report", - "X2", - "7", - "fail", - 'base 0x40000100 "KVMKVMKVM" \\ x\ty', - ).splitlines() - self.assertRegex(failed[0], line) - self.assertIn(r'detail="base 0x40000100 \"KVMKVMKVM\" \\ x\x09y"', failed[0]) - self.assertEqual(failed[1], "failures=1") - self.assertEqual( - self.run_test("exhaustive-summary", "4", "0").splitlines(), - ["NVX-TIME-ABI-EXHAUSTIVE: v=1 status=ok cpus=4 failures=0", "exit=0"], - ) - self.assertEqual( - self.run_test("exhaustive-summary", "8", "3").splitlines(), - ["NVX-TIME-ABI-EXHAUSTIVE: v=1 status=fail cpus=8 failures=3", "exit=1"], - ) - - def timer_list( - self, - sections: list[tuple[int, str, int, str, int]], - broadcast: str = "", - cpus: str = "0-1", - ) -> str: - text = TIMER_LIST_BROADCAST.format(device=broadcast) - for cpu, device, state, handler, mode in sections: - text += TIMER_LIST_CPU.format( - cpu=cpu, device=device, state=state, handler=handler - ).replace("Tick Device: mode: 1", f"Tick Device: mode: {mode}") - return self.run_test( - "timer-list", self.fixture("timer_list.txt", text), cpus - ).strip() - - def test_timer_list_requires_one_shot_lapic_ticks(self): - good = [ - (0, "lapic", 3, "hrtimer_interrupt", 1), - (1, "lapic", 4, "hrtimer_interrupt", 1), - ] - self.assertEqual(self.timer_list(good), "ok") - failures = { - "deadline": [good[0], (1, "lapic-deadline", 3, "hrtimer_interrupt", 1)], - "periodic tick": [good[0], (1, "lapic", 3, "hrtimer_interrupt", 0)], - "periodic state": [good[0], (1, "lapic", 2, "hrtimer_interrupt", 1)], - "handler": [good[0], (1, "lapic", 3, "tick_handle_periodic", 1)], - "missing cpu": [good[0]], - } - for name, sections in failures.items(): - with self.subTest(case=name): - self.assertTrue(self.timer_list(sections).startswith("fail ")) - self.assertTrue(self.timer_list(good, broadcast="pit").startswith("fail ")) - - def test_cpuinfo_flags_and_rate(self): - def cpuinfo(flags: str, cpus: int = 2) -> str: - text = "".join( - CPUINFO_CPU.format(cpu=cpu, flags=flags) for cpu in range(cpus) - ) - return self.run_test( - "cpuinfo", self.fixture("cpuinfo.txt", text), "0-1" - ).strip() - - self.assertEqual(cpuinfo(GOOD_FLAGS), "ok cpu0_khz=2194843 cpu1_khz=2194843") - for name, flags in { - "tsc_reliable": GOOD_FLAGS.replace("tsc_reliable ", ""), - "deadline": GOOD_FLAGS + " tsc_deadline_timer", - "adjust": GOOD_FLAGS + " tsc_adjust", - }.items(): - with self.subTest(case=name): - self.assertIn("code=G_CONFORMANCE_C5", cpuinfo(flags)) - self.assertIn("code=G_CONFORMANCE_C5", cpuinfo(GOOD_FLAGS, cpus=1)) - - def test_command_line_rejects_clock_workarounds(self): - accepted = "earlycon=xe9 console=hvc0 quiet clocksource=tsc tsc=reliable" - self.assertEqual(self.run_test("cmdline", accepted).strip(), "ok") - for token in ( - "tsc_early_khz=2194843", - "lapic_timer_hz=200000000", - "notsc", - "nolapic", - "nolapic_timer", - "tsc=unstable", - "hpet=force", - "clocksource=kvm-clock", - ): - with self.subTest(token=token): - self.assertIn( - "code=G_CONFORMANCE_C9", - self.run_test("cmdline", f"{accepted} {token}"), - ) - - def test_clocksource_must_be_tsc_with_only_jiffies_fallbacks(self): - self.assertEqual(self.run_test("clocksource", "tsc\n", "tsc \n").strip(), "ok") - self.assertEqual( - self.run_test( - "clocksource", "tsc\n", "tsc refined-jiffies jiffies\n" - ).strip(), - "ok", - ) - for current, available in ( - ("tsc-early\n", "tsc-early \n"), - ("tsc\n", "tsc kvm-clock \n"), - ("refined-jiffies\n", "refined-jiffies \n"), - ): - with self.subTest(current=current, available=available): - self.assertIn( - "code=G_CONFORMANCE_C6", - self.run_test("clocksource", current, available), - ) - - def test_violation_event_format_escaping_and_limit(self): - line = self.run_test( - "event", - "G_UNCHECKED_MSR", - "watcher", - "runtime", - "3", - "123456789", - 'unchecked "MSR"\\ access\x01', - ) - self.assertEqual( - line, - "NVX-TIME-ABI-VIOLATION: v=1 code=G_UNCHECKED_MSR source=watcher " - "phase=runtime generation=3 boottime_ns=123456789 " - 'detail="unchecked \\"MSR\\"\\\\ access\\x01"\n', - ) - long_line = self.run_test( - "event", "G_HUNG_TASK", "watcher", "runtime", "0", "1", "\x02" * 400 - ) - self.assertLessEqual(len(long_line), 512) - self.assertTrue(long_line.endswith('\\x02"\n')) - - def test_state_file_round_trip(self): - # The fixture lists every key, in the order that nvx-time writes them. - text = ( - "version=1\ngeneration=2\nboot_status=ok\ncapture_status=ok\n" - "restore_status=pending\nboot_cpus=8\ncapture_cpus=8\nrestore_cpus=0\n" - "boot_elapsed_us=2400\ncapture_elapsed_us=310\nrestore_elapsed_us=1830\n" - "boot_cpu_us=1900\ncapture_cpu_us=290\nrestore_cpu_us=640\n" - "capture_generation=1\nrestore_generation=2\ntsc_hz=2194843000\n" - "lapic_hz=200000000\ndiscontinuities=3\nlast_discontinuity=step\n" - "last_step_ns=-200000000\nlast_step_realtime_ns=1790841600000000000\n" - "last_downtime_ns=31000000000\nlast_downtime_source=utc\n" - "synchronized=1\noffset_ns=-42\nuncertainty_ns=1800\n" - "frequency_ppb=-1200\nsamples=17\nrejected_samples=1\n" - "last_sample_error=none\nviolations=0\n" - ) - self.assertEqual(self.run_test("state", self.fixture("state.txt", text)), text) - # Report-only mode adds the failure counts, reserved for it, after the - # generations, and a failed check's status is fail. - report = text.replace("restore_status=pending", "restore_status=fail").replace( - "restore_generation=2\n", - "restore_generation=2\nboot_failures=0\ncapture_failures=0\n" - "restore_failures=3\n", - ) - self.assertEqual( - self.run_test("state", self.fixture("state.txt", report)), report - ) - # A fresh state is the pending boot check; the capture and restore keys - # are absent until those checks first run. - self.assertTrue( - self.run_test("state", self.fixture("state.txt", "version=1\n")).startswith( - "version=1\ngeneration=0\nboot_status=pending\nboot_cpus=0\n" - "boot_elapsed_us=0\nboot_cpu_us=0\ntsc_hz=0\nlapic_hz=0\n" - ) - ) - self.assertEqual( - self.run_test( - "state", self.fixture("state.txt", "unknown=x\nsamples=bad\n") - ).strip(), - "error", - ) - - def status(self, text: str, mode: str = "enforcing") -> list[str]: - return self.run_test( - "status", self.fixture("state.txt", text), mode - ).splitlines() - - def test_status_prints_every_recorded_check_and_the_runtime_line(self): - state = ( - "version=1\ngeneration=2\nboot_status=ok\ncapture_status=ok\n" - "restore_status=ok\nboot_cpus=8\ncapture_cpus=8\nrestore_cpus=4\n" - "boot_elapsed_us=2400\ncapture_elapsed_us=310\nrestore_elapsed_us=1830\n" - "boot_cpu_us=1900\ncapture_cpu_us=290\nrestore_cpu_us=640\n" - "capture_generation=1\nrestore_generation=2\ntsc_hz=2194843000\n" - "lapic_hz=200000000\ndiscontinuities=2\nsynchronized=1\n" - "offset_ns=-42\nuncertainty_ns=1800\nrejected_samples=2\n" - "last_sample_error=G_SAMPLE_UNCERTAIN\n" - ) - rates = "tsc_hz=2194843000 lapic_hz=200000000" - # One line per recorded phase, each with the generation its check ran - # in and its wall and CPU times, then the runtime line; the hook adds - # whether status exits 0. - self.assertEqual( - self.status(state), - [ - f"NVX-TIME-ABI: v=1 phase=boot status=ok cpus=8 {rates} " - "generation=0 elapsed_us=2400 cpu_us=1900", - f"NVX-TIME-ABI: v=1 phase=capture status=ok cpus=8 {rates} " - "generation=1 elapsed_us=310 cpu_us=290", - f"NVX-TIME-ABI: v=1 phase=restore status=ok cpus=4 {rates} " - "generation=2 elapsed_us=1830 cpu_us=640", - "NVX-TIME-ABI: v=1 phase=runtime status=synchronized generation=2 " - "discontinuities=2 offset_ns=-42 uncertainty_ns=1800 " - "rejected_samples=2 last_sample_error=G_SAMPLE_UNCERTAIN", - "ok=1", - ], - ) - # A check still pending after the wait has neither elapsed_us nor - # cpu_us and fails the exit status. A cold boot has a boot line only, - # and a state without a boot record counts as a pending boot check. - pending = state.replace("restore_status=ok", "restore_status=pending") - self.assertEqual( - self.status(pending)[2::2], - [ - f"NVX-TIME-ABI: v=1 phase=restore status=pending cpus=4 {rates} " - "generation=2", - "ok=0", - ], - ) - boot_pending = ( - "NVX-TIME-ABI: v=1 phase=boot status=pending cpus=0 tsc_hz=0 " - "lapic_hz=0 generation=0" - ) - for text in ("version=1\ngeneration=0\nboot_status=pending\n", "version=1\n"): - lines = self.status(text) - self.assertEqual(lines[0], boot_pending, text) - self.assertTrue(lines[1].startswith("NVX-TIME-ABI: v=1 phase=runtime ")) - self.assertEqual(lines[2:], ["ok=0"], text) - # Report-only mode marks every line and appends each check's failure - # count from the state file, where a failed check's status is fail. - report = self.status( - state.replace("restore_status=ok", "restore_status=fail").replace( - "restore_generation=2\n", - "restore_generation=2\nboot_failures=0\nrestore_failures=3\n", - ), - "report-only", - ) - self.assertEqual( - report[0], - f"NVX-TIME-REPORT: v=1 phase=boot status=ok cpus=8 {rates} " - "generation=0 elapsed_us=2400 cpu_us=1900 failures=0", - ) - self.assertEqual( - report[2], - f"NVX-TIME-REPORT: v=1 phase=restore status=fail cpus=4 {rates} " - "generation=2 elapsed_us=1830 cpu_us=640 failures=3", - ) - self.assertTrue(report[3].startswith("NVX-TIME-REPORT: v=1 phase=runtime ")) - self.assertEqual(report[4], "ok=0") - - def test_deferred_work_starts_150_ms_late_and_is_promoted_100_ms_later(self): - # Guest repair step 7 and the asynchronous boot checks start 150 ms - # after the acknowledgement or the boot step and leave SCHED_IDLE - # 100 ms after they start. - self.assertEqual( - self.run_test("deferral"), "deferred_start_ms=150 idle_bound_ms=100\n" - ) - - def test_restore_cpu_time_is_the_deferred_work_with_the_daemons_fork(self): - # The worker's repair step 7 CPU time adds the daemon's preparation - # before the fork, the fork's cost to the daemon, which arrives on a - # pipe after the worker's copy of the timing was taken, and its own. - fields = dict( - field.split("=") - for field in self.run_test("deferred-restore-cpu", "20", "30", "10").split() - ) - self.assertGreaterEqual(int(fields["prior_ms"]), 20) - self.assertLess(int(fields["prior_ms"]), 30) - self.assertGreaterEqual(int(fields["total_ms"]), 60) - self.assertLess(int(fields["total_ms"]), 90) - - def restore_record(self, text: str) -> str: - return self.run_test( - "restore-record", self.fixture("restore.txt", text) - ).strip() - - def test_restore_record_names_the_cpus_that_restore_finish_onlined(self): - # restore-finish records the CPUs that activation brought online. - self.assertEqual( - self.restore_record("generation=3\nnew_cpus=4-6,9\n"), - "generation=3 new_cpus=4,5,6,9 count=4", - ) - self.assertEqual( - self.restore_record("generation=3\nnew_cpus=none\n"), - "generation=3 new_cpus=none count=0", - ) - self.assertEqual( - self.restore_record("generation=3\n"), "generation=3 new_cpus=none count=0" - ) - for text in ( - "", - "new_cpus=4\ngeneration=3\n", - "generation=3\nnew_cpus=7-4\n", - "generation=3\nnew_cpus=64\n", - ): - self.assertEqual(self.restore_record(text), "error", text) - - def test_parallel_checks_merge_every_failure(self): - if sys.platform != "linux": - self.skipTest("CPU affinity is Linux-only") - allowed: list[int] = sorted(os.sched_getaffinity(0)) - # Three side jobs fail 1, 2, and 3 times and the foreground once, - # whether one CPU runs them in the caller or each has a thread. - for size in sorted({1, min(2, len(allowed)), min(4, len(allowed))}): - output = self.run_test("parallel", ",".join(map(str, allowed[:size]))) - self.assertTrue(output.endswith("failures=7\n"), output) - self.assertEqual(output.count("code=G_CONFORMANCE_C4"), 6, output) - self.assertEqual(output.count("code=G_CONFORMANCE_C9"), 1, output) - # A CPU the checks cannot run on fails C1 in its job, and each such - # job's failure counts. - unusable = [cpu for cpu in range(64) if cpu not in allowed] - if not unusable: - self.skipTest("every CPU is usable") - output = self.run_test("parallel", f"{allowed[0]},{unusable[0]}") - self.assertTrue(output.endswith("failures=8\n"), output) - self.assertIn(f"cannot run on cpu {unusable[0]}", output) - if len(unusable) < 2: - self.skipTest("only one CPU is unusable") - cpus = ",".join(map(str, [allowed[0], *unusable[:2]])) - output = self.run_test("parallel", cpus) - self.assertTrue(output.endswith("failures=9\n"), output) - for cpu in unusable[:2]: - self.assertIn(f"cannot run on cpu {cpu}: ", output) - - def test_idle_work_continues_at_normal_priority_after_the_bound(self): - # The asynchronous checks leave SCHED_IDLE (5) for SCHED_OTHER (0) - # 100 ms after they start, every thread included; checks that end - # first stop the promoter without waiting for the bound. - policy = 0 if can_leave_idle_priority() else 5 - promoted, stopped = self.run_test("promote").splitlines() - self.assertEqual(promoted, f"promoted caller={policy} thread={policy}") - fields = dict(field.split("=") for field in stopped.split()[1:]) - self.assertEqual(fields["caller"], "5") - self.assertLess(int(fields["stop_ms"]), 1000) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_managed_agent.py b/scripts/test_managed_agent.py deleted file mode 100644 index 794ff2ffd..000000000 --- a/scripts/test_managed_agent.py +++ /dev/null @@ -1,795 +0,0 @@ -#!/usr/bin/env python3 -"""Compile the existing guest decoder on Linux and test real wire inputs.""" - -import shutil -import struct -import subprocess -import sys -import tempfile -import unittest -from collections.abc import Sequence -from pathlib import Path - - -@unittest.skipUnless(sys.platform.startswith("linux"), "guest decoder requires Linux") -class ManagedAgentDecoderTests(unittest.TestCase): - @classmethod - def setUpClass(cls): - compiler = shutil.which("cc") - if compiler is None: - raise unittest.SkipTest("guest decoder requires the guest build C compiler") - cls.temporary = tempfile.TemporaryDirectory(prefix="nvx-agent-decoder-") - cls.addClassCleanup(cls.temporary.cleanup) - cls.root = Path(cls.temporary.name) - cls.executable = cls.root / "decoder" - cls.fault_executable = cls.root / "fault-injection" - harness = cls.root / "decoder.c" - harness.write_text( - """ -#define main nvx_agent_main -#include "nvx-managed-agent.c" -#undef main -int main(int argc, char **argv) { - unsigned char bytes[65536]; - uint32_t timeout_ms = 0; - char **arguments = NULL; - struct exec_config config; - memset(&config, 0xa5, sizeof(config)); - if (argc > 1 && strcmp(argv[1], "--exec-config-fd") == 0) - return launch_workload(argc, argv); - int launch = argc == 3 && strcmp(argv[1], "--launch") == 0; - if (argc != 2 && !launch) return 2; - FILE *file = fopen(argv[launch ? 2 : 1], "rb"); - if (file == NULL) return 2; - size_t length = fread(bytes, 1, sizeof(bytes), file); - if (ferror(file)) return 2; - fclose(file); - int result = decode_exec_payload( - bytes, (uint32_t)length, &timeout_ms, &arguments, &config); - if (result == 0 && launch) { - int fd = create_exec_config_fd(&config, 0); - if (fd < 0) return 2; - int seals = F_SEAL_WRITE | F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL; - if ((fcntl(fd, F_GET_SEALS) & seals) != seals) return 2; - char fd_text[32]; - snprintf(fd_text, sizeof(fd_text), "%d", fd); - char *helper_arguments[MAX_ARGUMENTS + 5] = { - argv[0], "--exec-config-fd", fd_text, "--" - }; - int count = 0; - while (arguments[count] != NULL) { - helper_arguments[count + 4] = arguments[count]; - count++; - } - helper_arguments[count + 4] = NULL; - return launch_workload(count + 4, helper_arguments); - } - if (result == 0) { - free_arguments(arguments); - free_exec_config(&config); - } - return result == 0 ? 0 : 1; -} -""", - encoding="utf-8", - ) - guest = Path(__file__).resolve().parent.parent / "guest" / "common" - subprocess.run( - [ - compiler, - "-D_GNU_SOURCE", - "-std=c11", - "-I", - str(guest), - str(harness), - "-o", - str(cls.executable), - ], - check=True, - capture_output=True, - timeout=30, - ) - fault_harness = cls.root / "fault-injection.c" - fault_cgroup_root = cls.root / "cgroup" - fault_exec_cgroup = fault_cgroup_root / "nvx-exec" - fault_exec_cgroup.mkdir(parents=True) - (fault_exec_cgroup / "cgroup.procs").write_text("", encoding="utf-8") - fault_harness.write_text( - """ -#define main nvx_agent_main -#include "nvx-managed-agent.c" -#undef main - -static int barrier_reader = -1; - -ssize_t __real_write(int, const void *, size_t); -ssize_t __wrap_write(int fd, const void *buffer, size_t length) { - if (barrier_reader >= 0 && length == 6 && - memcmp(buffer, "start\\n", length) == 0) { - close(barrier_reader); - barrier_reader = -1; - } - return __real_write(fd, buffer, length); -} - -int __real_setenv(const char *, const char *, int); -int __wrap_setenv(const char *name, const char *value, int overwrite) { - const char *failure = getenv("NVX_TEST_FAIL_SETENV"); - if (failure != NULL && strcmp(name, failure) == 0) { - errno = ENOMEM; - return -1; - } - return __real_setenv(name, value, overwrite); -} - -int __wrap_execv(const char *path, char *const argv[]) { - const char *marker = getenv("NVX_TEST_EXEC_MARKER"); - (void)path; - (void)argv; - if (marker != NULL) { - int fd = open(marker, O_WRONLY | O_CREAT | O_TRUNC, 0600); - if (fd >= 0) close(fd); - } - errno = ENOENT; - return -1; -} - -int __wrap_execvp(const char *file, char *const argv[]) { - return __wrap_execv(file, argv); -} - -int main(int argc, char **argv) { - if (argc == 3 && strcmp(argv[1], "--release-closing-barrier") == 0) { - barrier_reader = open(argv[2], O_RDONLY | O_CLOEXEC | O_NONBLOCK); - if (barrier_reader < 0) return 2; - errno = 0; - int result = release_container_barrier(argv[2], -1); - int status = errno; - if (barrier_reader >= 0) close(barrier_reader); - return result == -1 && status == EPIPE ? 0 : 1; - } - if (argc == 3 && strcmp(argv[1], "--release-orphaned-barrier") == 0) { - pid_t child = fork(); - if (child < 0) return 2; - if (child == 0) _exit(125); - int result = release_container_barrier(argv[2], child); - int status = 0; - if (waitpid(child, &status, 0) != child) return 2; - return result == 1 && WIFEXITED(status) ? 0 : 1; - } - if (argc == 3 && strcmp(argv[1], "--release-live-barrier") == 0) { - pid_t child = fork(); - if (child < 0) return 2; - if (child == 0) { - char message[6]; - int fd = open(argv[2], O_RDONLY | O_CLOEXEC); - if (fd < 0 || read(fd, message, sizeof(message)) != sizeof(message)) - _exit(125); - close(fd); - _exit(memcmp(message, "start\\n", sizeof(message)) == 0 ? 0 : 125); - } - int result = release_container_barrier(argv[2], child); - int status = 0; - if (waitpid(child, &status, 0) != child) return 2; - return result == 0 && WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : 1; - } - if (argc != 3) return 2; - struct agent_config config = { - .rootfs = "/rootfs", - .hostname = "sandbox", - .uid = "1000", - .gid = "1000", - .user = "workload", - .home = "/home/workload", - .direct = 0, - }; - char *workload[] = {"/bin/true", NULL}; - if (strcmp(argv[1], "--sandbox") == 0) - exec_sandbox(&config, "/unused", argv[2], workload); - if (strcmp(argv[1], "--direct") == 0) - exec_direct(&config, argv[2], workload); - return 2; -} -""", - encoding="utf-8", - ) - subprocess.run( - [ - compiler, - f'-DCGROUP_ROOT="{fault_cgroup_root}"', - "-D_GNU_SOURCE", - "-std=c11", - "-I", - str(guest), - str(fault_harness), - "-Wl,--wrap=write", - "-Wl,--wrap=setenv", - "-Wl,--wrap=execv", - "-Wl,--wrap=execvp", - "-o", - str(cls.fault_executable), - ], - check=True, - capture_output=True, - timeout=30, - ) - - def decode( - self, - environment: tuple[bytes, ...], - *, - timeout: int = 0, - inherit: bool = False, - ) -> int: - argument = b"/bin/true" - flags = 2 | (4 if inherit else 0) - payload = struct.pack(" subprocess.CompletedProcess[bytes]: - entries = () if environment is None else environment - flags = 1 | (2 if environment is not None else 0) | (4 if inherit else 0) - payload = struct.pack( - " length) return 2; - if (handle_app_request(&session, &config, &mappings, &request) < 0) return 4; - } - printf("%u %u %d\\n", mappings.expected, mappings.applied, mappings.failed); - return 0; -} -""", - encoding="utf-8", - ) - guest = Path(__file__).resolve().parent.parent / "guest" / "common" - subprocess.run( - [ - compiler, - f'-DHOSTFS_DIR="{cls.root / "hostfs"}"', - "-D_GNU_SOURCE", - "-std=c11", - "-I", - str(guest), - str(harness), - "-Wl,--wrap=mount", - "-o", - str(cls.executable), - ], - check=True, - capture_output=True, - timeout=30, - ) - - def setUp(self): - self.case = Path(tempfile.mkdtemp(prefix="case-", dir=self.root)) - export = self.root / "hostfs" / "root" - shutil.rmtree(export, ignore_errors=True) - (export / "0" / "src").mkdir(parents=True) - (export / "0" / "notes.txt").write_text("notes", encoding="utf-8") - (export / "1").mkdir() - self.log = self.case / "mounts.log" - - @staticmethod - def maps( - first: int, - entries: Sequence[tuple[str | bytes, str | bytes, int]], - count: int | None = None, - ) -> bytes: - payload = struct.pack(" tuple[subprocess.CompletedProcess[bytes], list[tuple[int, int, int, bytes]]]: - source = self.case / "requests.bin" - source.write_bytes( - b"".join( - struct.pack(" list[tuple[str, str, int]]: - if not self.log.exists(): - return [] - return [ - (source, target, int(flags)) - for source, target, flags in ( - line.split("|") for line in self.log.read_text().splitlines() - ) - ] - - def test_mappings_mount_in_order_and_unblock_workloads(self): - export = self.root / "hostfs" / "root" - src = str(self.case / "guest" / "src") - notes = str(self.case / "guest" / "src" / "notes.txt") - out = str(self.case / "guest" / "out") - result, answers = self.run_agent( - # A stale token of an older host is ignored. - "quiet nvx_map=0,/stale,rw nvx_maps=3", - [ - (APP_EXEC, b""), - ( - APP_MAPS, - self.maps(0, [("0/src", src, 1), ("0/notes.txt", notes, 0)]), - ), - (APP_EXEC, b""), - (APP_MAPS, self.maps(2, [("1", out, 0)])), - # An empty request is malformed, so it fails only once the mappings are complete. - (APP_EXEC, b""), - ], - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, b"3 3 0\n") - self.assertEqual( - [(kind, status, payload) for kind, _, status, payload in answers], - [ - (APP_ERROR, 11, b"mappings-incomplete"), - (APP_READY, 0, b""), - (APP_ERROR, 11, b"mappings-incomplete"), - (APP_READY, 0, b""), - (APP_ERROR, 22, b"invalid-request"), - ], - ) - self.assertEqual( - self.mounts(), - [ - (str(export / "0" / "src"), src, MS_BIND), - ("-", src, REMOUNT | MS_RDONLY), - (str(export / "0" / "notes.txt"), notes, MS_BIND), - ("-", notes, REMOUNT), - (str(export / "1"), out, MS_BIND), - ("-", out, REMOUNT), - ], - ) - self.assertTrue(Path(src).is_dir()) - self.assertTrue(Path(notes).is_file()) - self.assertTrue(Path(out).is_dir()) - - def test_entries_beyond_the_announcement_or_out_of_order_are_refused(self): - target = str(self.case / "guest" / "src") - entry = [("0/src", target, 0)] - result, answers = self.run_agent( - "nvx_maps=1", - [ - (APP_MAPS, self.maps(1, entry)), - (APP_MAPS, self.maps(0, entry * 2)), - (APP_MAPS, self.maps(0, entry)), - (APP_MAPS, self.maps(1, entry)), - ], - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, b"1 1 0\n") - self.assertEqual( - [(kind, status) for kind, _, status, _ in answers], - [(APP_ERROR, 22), (APP_ERROR, 22), (APP_READY, 0), (APP_ERROR, 22)], - ) - self.assertEqual(len(self.mounts()), 2) - - def test_malformed_tables_mount_nothing(self): - target = str(self.case / "guest" / "x") - good = ("0/src", target, 0) - malformed = [ - self.maps(0, [good, ("0/../1", target, 0)]), - self.maps(0, [good, ("/0/src", target, 0)]), - self.maps(0, [good, ("0/src", "relative", 0)]), - self.maps(0, [good, (".", target, 0)]), - self.maps(0, [good, ("0//src", target, 0)]), - self.maps(0, [good, (b"0/s\0rc", target, 0)]), - self.maps(0, [good, ("0/src", target, 2)]), - self.maps(0, [good, ("", target, 0)]), - self.maps(0, [good]) + b"\0", - self.maps(0, [good], count=2), - self.maps(0, [], count=0), - b"\0\0\0", - ] - result, answers = self.run_agent( - "nvx_maps=2", [(APP_MAPS, payload) for payload in malformed] - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, b"2 0 0\n") - self.assertEqual( - [(kind, status, payload) for kind, _, status, payload in answers], - [(APP_ERROR, 22, b"invalid-request")] * len(malformed), - ) - self.assertEqual(self.mounts(), []) - - def test_without_an_announcement_tables_are_refused(self): - target = str(self.case / "guest" / "src") - result, answers = self.run_agent( - "nvx_map=0,/w,rw", - [(APP_MAPS, self.maps(0, [("0/src", target, 0)])), (APP_EXEC, b"")], - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, b"0 0 0\n") - self.assertEqual( - [(kind, status, payload) for kind, _, status, payload in answers], - [(APP_ERROR, 22, b"invalid-request"), (APP_ERROR, 22, b"invalid-request")], - ) - self.assertEqual(self.mounts(), []) - - def test_a_failed_mount_keeps_refusing_workloads(self): - target = str(self.case / "guest" / "missing") - result, answers = self.run_agent( - "nvx_maps=2", - [ - (APP_MAPS, self.maps(0, [("0/missing", target, 0)])), - (APP_EXEC, b""), - (APP_MAPS, self.maps(0, [("0/src", target, 0)])), - ], - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, b"2 0 1\n") - self.assertEqual( - [(kind, status, payload) for kind, _, status, payload in answers], - [ - (APP_ERROR, 2, b"mapping-failed"), - (APP_ERROR, 11, b"mappings-incomplete"), - (APP_ERROR, 22, b"invalid-request"), - ], - ) - - def test_mapping_counts_are_validated(self): - for command_line in [ - "nvx_maps=0", - "nvx_maps=", - "nvx_maps=4097", - "nvx_maps=99999999999", - "nvx_maps=1x", - "nvx_maps=1 nvx_maps=1", - ]: - result, _ = self.run_agent(command_line, []) - self.assertEqual(result.returncode, 3, command_line) - result, _ = self.run_agent("nvx_maps=4096", []) - self.assertEqual(result.stdout, b"4096 0 0\n") - - def test_features_announce_the_mapping_table_but_not_retired_bit_one(self): - _, answers = self.run_agent("", [(APP_FEATURES, b"")]) - self.assertEqual(len(answers), 1) - kind, _, status, payload = answers[0] - self.assertEqual((kind, status), (APP_READY, 0)) - (features,) = struct.unpack(" nvx_rs.EgressRules: + """Compiles an egress policy with nvx-rs, as nvx.py run compiles its + --network-egress-policy-file.""" + with tempfile.TemporaryDirectory(prefix="nvx-egress-policy-") as temporary: + path = Path(temporary) / "policy.json" + path.write_text(json.dumps(policy), encoding="utf-8") + return nvx_rs.compile_egress_policy(path) + + def _posix_shell() -> str | None: shell = shutil.which("sh") if shell is None: @@ -937,7 +945,12 @@ def _provision( state.mkdir(parents=True, exist_ok=True) (state / "lifecycle.lock").touch() (state / "config.json").write_text( - json.dumps({"user": values.get("--workload-user", "65534:65534")}), + json.dumps( + { + "user": values.get("--workload-user", "65534:65534"), + "hypervisor": values["--hypervisor"], + } + ), encoding="utf-8", ) return 0, b"", b"" @@ -1106,13 +1119,29 @@ def _stop(self, state: Path | None, _: dict[str, str], __: list[str]) -> _CliRes if "stop-leaks-capability" not in self.faults: (state / "control.capability").unlink() (state / "control.sock").unlink(missing_ok=True) + config = json.loads((state / "config.json").read_text(encoding="utf-8")) + teardown = {name: True for name in microvm_tests.OUTCOME_TEARDOWN_FIELDS} + if "stop-reports-incomplete-teardown" in self.faults: + teardown["vm_stopped"] = False (state / "outcome.json").write_text( json.dumps( { "schema_version": 1, + "instance_id": "22" * 16, + "backend": ( + "auto" + if "stop-reports-another-backend" in self.faults + else config["hypervisor"] + ), "outcome": sandbox_lifecycle_tests.MANAGED_SUCCESS, - "network_policy": {}, - "teardown": {}, + "network_policy": { + "status": ( + "applied" + if "stop-reports-a-network-policy" in self.faults + else "not-requested" + ) + }, + "teardown": teardown, } ), encoding="utf-8", @@ -1156,9 +1185,6 @@ def _deprovision( def processes(self, _: str) -> tuple[int, ...]: return tuple(sorted(pid for pid, running in self.running.items() if running)) - def process_running(self, runtime: dict[str, object]) -> bool: - return self.running.get(cast(int, runtime["pid"]), False) - def endpoint_present(self, runtime: dict[str, object]) -> bool: return runtime["control_endpoint"] in self.endpoints.values() @@ -1252,11 +1278,6 @@ def fixture(prefix: str) -> str: "terminate_process", side_effect=fake.terminate, ), - patch.object( - sandbox_lifecycle, - "runtime_process_running", - side_effect=fake.process_running, - ), ): sandbox_lifecycle_tests.run_sandbox_lifecycle( "whp", timeout=2, output_dir=output_dir @@ -1352,6 +1373,13 @@ def test_acceptance_detects_lifecycle_regressions(self): "overwrite-report": "exec returned 0, expected 1", "stop-leaks-capability": "after stop: the state directory holds", "stop-leaks-process": "OpenVMM still runs after stop", + "stop-reports-another-backend": "the managed report names backend 'auto'", + "stop-reports-a-network-policy": ( + "the managed report has network policy status 'applied'" + ), + "stop-reports-incomplete-teardown": ( + "the managed report has incomplete teardown" + ), "restart-loses-state": "the restarted sandbox's state returned status 1", "unserialized-starts": "overlapping public starts returned status 0", "deprovision-removes-foreign": "deprovision returned 0, expected 1", @@ -1449,19 +1477,36 @@ def test_scratch_journal_check_reads_the_ext4_recovery_flag(self): sandbox_lifecycle_tests.scratch_journal_clean(image) def test_kill_ends_the_recorded_process(self): - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"]) + + def processes(marker: str) -> tuple[int, ...]: + # The scan finds the child under its fixture's name while it runs. + self.assertEqual(marker, "nvx-public-lifecycle-kill") + return (child.pid,) if child.poll() is None else () + + runtime: dict[str, object] = { + "format": 1, + "pid": child.pid, + "start_time": 1, + "control_endpoint": "unused", + } try: - runtime: dict[str, object] = { - "format": 1, - "pid": child.pid, - "start_time": sandbox_lifecycle._process_start_time(child.pid), - "control_endpoint": "unused", - } - self.assertTrue(sandbox_lifecycle.runtime_process_running(runtime)) - sandbox_lifecycle_tests.kill_openvmm(runtime, 30) - self.assertFalse(sandbox_lifecycle.runtime_process_running(runtime)) + with patch.object( + sandbox_lifecycle_tests, "openvmm_processes", side_effect=processes + ): + self.assertTrue( + sandbox_lifecycle_tests.openvmm_running( + runtime, "nvx-public-lifecycle-kill" + ) + ) + sandbox_lifecycle_tests.kill_openvmm( + runtime, 30, "nvx-public-lifecycle-kill" + ) + self.assertFalse( + sandbox_lifecycle_tests.openvmm_running( + runtime, "nvx-public-lifecycle-kill" + ) + ) self.assertNotEqual(child.wait(timeout=30), 0) finally: if child.poll() is None: @@ -2589,29 +2634,6 @@ def require(path: Path, _description: str) -> Path: self.assertIn("nvx-exit 0", script) -class ControlSessionTests(unittest.TestCase): - def test_named_pipe_connect_retries_transient_invalid_argument(self): - error = OSError(control_session.errno.EINVAL, "Invalid argument") - with ( - patch.object(control_session, "_winapi") as winapi, - patch.object( - control_session.time, - "monotonic", - side_effect=[0.0, 0.0], - ), - patch.object(control_session.time, "sleep") as sleep, - ): - winapi.CreateFile.side_effect = [error, 123] - stream = control_session._NamedPipeStream.connect( - Path(r"\\.\pipe\nvx-test"), - 1.0, - ) - - self.assertEqual(stream._handle, 123) - self.assertEqual(winapi.CreateFile.call_count, 2) - sleep.assert_called_once_with(0.025) - - class _PortExclusionSockets: """Create sockets that refuse ports excluded per protocol, as Windows does.""" @@ -2668,29 +2690,180 @@ def close() -> None: class MicrovmTests(unittest.TestCase): - def test_managed_lifecycle_spawns_openvmm_with_a_prepared_capability_pipe(self): - # OpenVMM reads its capability as soon as it starts, so the scenario hands - # it a pipe that already holds the capability and the end of file (#440). - prepared: list[int] = [] - spawned: list[object] = [] - - def prepare(capability: bytes) -> int: - descriptor = control_session.capability_pipe(capability) - prepared.append(descriptor) - return descriptor - - def launch(_command: list[str], *, stdin: object, **_options: object) -> None: - # The pipe stays open until OpenVMM has started with it. - self.assertIsInstance(stdin, int) - os.fstat(cast(int, stdin)) - spawned.append(stdin) - raise RuntimeError("stopped after the spawn") + @staticmethod + def _managed_report(backend: str = "kvm") -> dict[str, object]: + return { + "schema_version": 1, + "instance_id": "33" * 16, + "backend": backend, + "outcome": { + "operation": "managed", + "category": "success", + "status_code": 0, + }, + "network_policy": { + "status": "not-requested", + "status_code": 0, + "mode": "none", + "allow_rule_count": 0, + "deny_rule_count": 0, + "host_loopback": False, + }, + "teardown": {name: True for name in microvm_tests.OUTCOME_TEARDOWN_FIELDS}, + } + + def _run_managed_lifecycle( + self, + output_dir: Path, + *, + returncode: int = 0, + console: bytes = b"NVX-GUEST-BOOT-OK: managed\n", + report: dict[str, object] | None = None, + rejected: int = 1, + ) -> list[tuple[list[str], dict[str, str], object]]: + """Runs the scenario with a stand-in for the nvx-rs test, which leaves + `console`, `report`, and `returncode` as the real one would.""" + runs: list[tuple[list[str], dict[str, str], object]] = [] + + def run_test( + command: list[str], *, cwd: object, env: dict[str, str], check: bool + ) -> subprocess.CompletedProcess[bytes]: + self.assertFalse(check) + runs.append((command, env, cwd)) + openvmm = cast(list[str], json.loads(env["NVX_MANAGED_LIFECYCLE_COMMAND"])) + # The test copies the guest console into the output directory. + output_dir = Path(env["NVX_MANAGED_LIFECYCLE_OUTPUT_DIR"]) + (output_dir / "managed-lifecycle-guest.log").write_bytes(console) + report_path = Path(openvmm[openvmm.index("--microvm-report") + 1]) + report_path.write_text( + json.dumps(self._managed_report() if report is None else report), + encoding="utf-8", + ) + return subprocess.CompletedProcess(command, returncode) + with ( + patch.object(microvm_tests.shutil, "which", return_value="cargo"), + patch.object(microvm_tests.subprocess, "run", side_effect=run_test), + patch.object(microvm_tests, "OpenvmmProcess") as process, + ): + process.return_value.__enter__.return_value.wait.return_value = ( + openvmm_process.OpenvmmProcessResult(rejected, b"") + ) + microvm_tests.run_managed_lifecycle( + Path("openvmm"), + Path("vmlinux"), + Path("initramfs.cpio.gz"), + "kvm", + memory_mib=256, + timeout=5, + output_dir=output_dir, + ) + return runs + + def test_managed_lifecycle_runs_the_nvx_rs_test_and_checks_what_it_leaves(self): + # nvx-rs/tests/managed_lifecycle.rs drives the control session; the + # scenario hands it OpenVMM's command, then checks the outcome report + # and the guest console. + with tempfile.TemporaryDirectory() as temporary: + output_dir = Path(temporary) + runs = self._run_managed_lifecycle(output_dir) + preserved = json.loads( + (output_dir / "managed-outcome.json").read_text(encoding="utf-8") + ) + # An adversarial campaign requires this report, with its teardown. + reports, teardown_complete = adversarial_executor._inventory_outcomes( + output_dir, + required_names=adversarial_executor._REQUIRED_OUTCOME_REPORTS[ + "managed-lifecycle" + ], + ) + + self.assertTrue(teardown_complete, reports) + self.assertEqual( + [report["path"] for report in reports], ["managed-outcome.json"] + ) + self.assertEqual(len(runs), 1) + command, environment, cwd = runs[0] + self.assertEqual( + command, + [ + "cargo", + "test", + "--manifest-path", + os.fspath(BuildConstants.REPO_ROOT / "nvx-rs" / "Cargo.toml"), + "--locked", + "--test", + "managed_lifecycle", + "--", + "--ignored", + "--nocapture", + ], + ) + # The repository root selects the pinned Rust release. + self.assertEqual(cwd, BuildConstants.REPO_ROOT) + self.assertEqual(environment["OPENVMM_LOG"], "off") + self.assertEqual(environment["NVX_MANAGED_LIFECYCLE_TIMEOUT"], "5") + self.assertEqual( + environment["NVX_MANAGED_LIFECYCLE_OUTPUT_DIR"], + os.fspath(output_dir.resolve()), + ) + openvmm = json.loads(environment["NVX_MANAGED_LIFECYCLE_COMMAND"]) + # Cargo runs the test in the nvx-rs directory, so every path is absolute. + for option in ("--kernel", "--initrd", "--microvm-report"): + self.assertTrue(Path(openvmm[openvmm.index(option) + 1]).is_absolute()) + self.assertTrue(Path(openvmm[0]).is_absolute()) + # The microVM console listens on TCP, at the address that the test + # copies it from. + self.assertEqual( + openvmm[openvmm.index("--virtio-console") + 1], + f"listen=tcp:{environment['NVX_MANAGED_LIFECYCLE_CONSOLE']}", + ) + self.assertRegex( + environment["NVX_MANAGED_LIFECYCLE_CONSOLE"], r"\A127\.0\.0\.1:[0-9]+\Z" + ) + self.assertEqual( + openvmm[openvmm.index("--microvm-control-console") + 1], + f"listen={environment['NVX_MANAGED_LIFECYCLE_ENDPOINT']}", + ) + for option, value in ( + ("--microvm-workload-identity", "65534:65534"), + ("--microvm-lifecycle", "managed"), + ): + self.assertEqual(openvmm[openvmm.index(option) + 1], value) + self.assertIn("--microvm-control-auth-stdin", openvmm) + self.assertEqual(preserved, self._managed_report()) + + def test_managed_lifecycle_hands_the_test_an_absolute_pid_journal(self): + # The nvx-rs test records OpenVMM in an adversarial campaign's journal + # from the nvx-rs directory, so a relative journal resolves here. + variable = "NVX_ADVERSARIAL_OPENVMM_PID_JOURNAL" + relative = os.path.join("journal", "openvmm-pids.jsonl") + for value, expected in ( + (relative, os.path.abspath(relative)), + (os.path.abspath(relative), os.path.abspath(relative)), + (None, None), + ): + with ( + self.subTest(value=value), + tempfile.TemporaryDirectory() as temporary, + patch.dict(os.environ), + ): + os.environ.pop(variable, None) + if value is not None: + os.environ[variable] = value + runs = self._run_managed_lifecycle(Path(temporary)) + self.assertEqual(runs[0][1].get(variable), expected) + + def test_managed_lifecycle_requires_cargo(self): with ( tempfile.TemporaryDirectory() as temporary, - patch.object(microvm_tests, "capability_pipe", side_effect=prepare), - patch.object(microvm_tests.subprocess, "Popen", side_effect=launch), - self.assertRaisesRegex(RuntimeError, "^stopped after the spawn$"), + patch.object(microvm_tests.shutil, "which", return_value=None), + patch.object(microvm_tests.subprocess, "run") as run, + self.assertRaisesRegex( + common.ScriptError, + "runs the nvx-rs test managed_lifecycle with Cargo, which is not " + "on PATH", + ), ): microvm_tests.run_managed_lifecycle( Path("openvmm"), @@ -2701,12 +2874,46 @@ def launch(_command: list[str], *, stdin: object, **_options: object) -> None: timeout=5, output_dir=Path(temporary), ) + run.assert_not_called() - self.assertEqual(len(prepared), 1) - self.assertEqual(spawned, prepared) - # The scenario closes its copy of the pipe once OpenVMM has it. - with self.assertRaises(OSError): - os.fstat(prepared[0]) + def test_managed_lifecycle_fails_with_the_test_and_with_the_report(self): + violation = f"{time_abi.VIOLATION_PREFIX}code=bogus\n".encode() + failures: tuple[tuple[dict[str, Any], type[Exception], str], ...] = ( + ( + {"returncode": 101}, + RuntimeError, + "the managed lifecycle test managed_lifecycle failed with exit " + "status 101", + ), + # The guest's own explanation comes before the test's failure. + ( + {"returncode": 101, "console": violation}, + time_abi.TimeAbiFailure, + "guest reported time ABI violation", + ), + ( + {"report": self._managed_report(backend="whp")}, + RuntimeError, + "managed lifecycle outcome report was invalid", + ), + ( + {"report": {**self._managed_report(), "teardown": {}}}, + RuntimeError, + "structured outcome report has an invalid teardown outcome", + ), + ( + {"rejected": 0}, + RuntimeError, + "managed lifecycle without a control endpoint was not rejected", + ), + ) + for options, error, message in failures: + with ( + self.subTest(message=message), + tempfile.TemporaryDirectory() as temporary, + self.assertRaisesRegex(error, re.escape(message)), + ): + self._run_managed_lifecycle(Path(temporary), **options) def test_host_loopback_port_candidates_are_spread_across_the_dynamic_range(self): first, last = microvm_tests.HOST_LOOPBACK_PORT_RANGE @@ -4740,13 +4947,29 @@ def test_every_tcp_console_is_monitored_and_cold_boot_shells_are_queried(self): if isinstance(node, ast.Call) and ast.unparse(node.func) == "TcpConsole.connect" ] - # The managed lifecycle, the console-snapshot capture and restore, and - # the snapshot-tier capture, restore, and gate-timeout restore. - self.assertEqual(len(connects), 6) + # The console-snapshot capture and restore, and the snapshot-tier + # capture, restore, and gate-timeout restore. + self.assertEqual(len(connects), 5) for keywords in connects: self.assertRegex( keywords.get("monitor", ""), r"^TimeAbiMonitor\(\w*command\)$" ) + # The managed lifecycle's console is a file, which the same monitor + # scans once the nvx-rs test that drives the guest has ended. + managed = next( + node + for node in ast.walk(tree) + if isinstance(node, ast.FunctionDef) + and node.name == "run_managed_lifecycle" + ) + self.assertIn( + "TimeAbiMonitor(command)", + [ + ast.unparse(node) + for node in ast.walk(managed) + if isinstance(node, ast.Call) + ], + ) # Only the cold boots whose shell is on the virtio console ask for # nvx-time status. Restores never ask, and in the managed lifecycle # init starts the managed agent instead of a shell. @@ -5614,6 +5837,7 @@ def test_egress_port_reservation_closes_tcp_when_udp_fails(self): "_bind_consecutive_ports", side_effect=[endpoints, RuntimeError("UDP unavailable")], ), + patch.object(microvm_tests, "compile_egress_policy") as compile_policy, self.assertRaisesRegex(RuntimeError, "UDP unavailable"), ): microvm_tests.run_l3_l4_egress_policy( @@ -5627,6 +5851,7 @@ def test_egress_port_reservation_closes_tcp_when_udp_fails(self): ) for endpoint in endpoints: endpoint.close.assert_called_once_with() + compile_policy.assert_not_called() def test_bounded_egress_acceptance_policy_lowers_ranges_and_exclusions(self): policy = microvm_tests._bounded_egress_policy( @@ -5696,6 +5921,11 @@ def is_alive(self) -> bool: ), patch.object(microvm_tests, "run_guest_script") as run_guest_script, patch.object(microvm_tests, "OpenvmmProcess") as openvmm_process, + patch.object( + microvm_tests, + "compile_egress_policy", + return_value=nvx_rs.EgressRules(allow=(), deny=()), + ), patch.object( microvm_tests.threading, "Thread", @@ -5847,6 +6077,11 @@ def is_alive(self) -> bool: return_value=(tcp, udp), ), patch.object(microvm_tests, "run_guest_script"), + patch.object( + microvm_tests, + "compile_egress_policy", + return_value=nvx_rs.EgressRules(allow=(), deny=()), + ), patch.object( microvm_tests.threading, "Thread", @@ -6385,7 +6620,7 @@ def test_egress_port_range_cases_lower_each_range_to_one_native_rule(self): "192.0.2.1", 20001, 20002, 20003 ): with self.subTest(case=case.name): - compiled = egress_policy.compile_policy( + compiled = _compile( {"allow": list(case.allow), "deny": list(case.deny)} ) self.assertEqual((compiled.allow, compiled.deny), expected[case.name]) @@ -6464,12 +6699,17 @@ def openvmm_result(name: str, output: bytes) -> MagicMock: return MagicMock(returncode=0, output=microvm_tests.BOOT_MARKER) return MagicMock(returncode=2, output=output) - def public_dry_run(command: list[str], **_kwargs: object) -> MagicMock: + run_process = subprocess.run + + def public_dry_run(command: list[str], **kwargs: Any) -> object: + if "--dry-run" not in command: + # nvx-rs, which compiles each policy. + return cast(object, run_process(command, **kwargs)) if nvx_accepts == policy_name(command): return MagicMock(returncode=0, stderr=b"") path = Path(command[command.index("--network-egress-policy-file") + 1]) try: - egress_policy.compile_policy_file(path) + nvx_rs.compile_egress_policy(path) except common.ScriptError as error: return MagicMock(returncode=1, stderr=f"error: {error}\n".encode()) return MagicMock(returncode=0, stderr=b"") @@ -6596,7 +6836,12 @@ def test_l3_l4_egress_port_ranges_run_every_case_through_public_nvx_run(self): self.assertEqual( rejected, ["192.0.2.1:tcp:20003-20001", "192.0.2.1:udp:-20003"] ) - dry_runs = [call.args[0] for call in nvx_run.call_args_list] + # The other calls are nvx-rs, which compiles each case's policy. + dry_runs = [ + call.args[0] + for call in nvx_run.call_args_list + if "--dry-run" in call.args[0] + ] self.assertEqual(len(dry_runs), 2) for command, name in zip( dry_runs, ("reversed-end-port", "end-port-without-port"), strict=True @@ -6894,9 +7139,7 @@ def test_ipv6_egress_cases_cover_families_and_precedence(self): every_probe = set(microvm_tests.IPV6_EGRESS_PROBES) lowered = {} for case in cases: - compiled = egress_policy.compile_policy( - {"allow": list(case.allow), "deny": list(case.deny)} - ) + compiled = _compile({"allow": list(case.allow), "deny": list(case.deny)}) lowered[case.name] = ( case.default, compiled.allow, @@ -7588,13 +7831,11 @@ 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) + 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 9c621202f..93e2b8d9e 100644 --- a/scripts/test_nvx_tools.py +++ b/scripts/test_nvx_tools.py @@ -4,7 +4,6 @@ import argparse import ast import contextlib -import errno import hashlib import http.client import http.server @@ -30,9 +29,9 @@ import urllib.request import uuid import zipfile -from collections.abc import Callable +from collections.abc import Callable, Generator, Mapping from pathlib import Path, PurePosixPath -from typing import Any, cast +from typing import cast from unittest.mock import MagicMock, call, patch sys.path.insert(0, str(Path(__file__).parent)) @@ -49,12 +48,12 @@ collect_alpine_sources, collect_ubuntu_sources, common, - control_session, elf, + guest_binaries, guests, + nvx_rs, release, sandbox, - sandbox_lifecycle, ubuntu, ) from nvx_tools.build_constants import ( # noqa: E402 @@ -253,6 +252,15 @@ def _write_release_fixture( ): path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(path.name.encode("ascii")) + # The release build of nvx-rs, static as the Linux packages require, and + # its provenance. + nvx_rs_binary = root / "nvx-rs" / "target" / "release" / nvx_rs.binary_path().name + nvx_rs_binary.parent.mkdir(parents=True, exist_ok=True) + nvx_rs_binary.write_bytes(_elf_file()) + (build_dir / nvx_rs.PROVENANCE_NAME).write_text( + json.dumps(nvx_rs.provenance_record(nvx_rs_binary, nvx_rs.release_target())), + encoding="utf-8", + ) for artifact_name, manifest_name in ( ( "initramfs-ubuntu.cpio.gz", @@ -317,6 +325,10 @@ def _write_release_fixture( "control_session_protocol_version": 1, "control_contract_revision": "nvx-microvm-v2-control-v2", }, + "guest_rust": { + "toolchain": GuestRustBuildConstants.TOOLCHAIN, + "target": GuestRustBuildConstants.TARGET, + }, "linux": { "version": "6.18.38", "upstream_url": ( @@ -338,7 +350,11 @@ def _write_release_fixture( "x86_64/alpine-minirootfs-3.24.1-x86_64.tar.gz" ), "minirootfs_sha256": AlpineBuildConstants.MINIROOTFS_SHA256, - "guest_sources": ["guest/common", "guest/alpine"], + "guest_sources": [ + "guest/common", + "guest/alpine", + *GuestRustBuildConstants.SOURCE_PATHS, + ], "package_manifests": ["build/initramfs.cpio.gz.packages.json"], "source_output": "build/sources/alpine", }, @@ -357,6 +373,7 @@ def _write_release_fixture( "guest/common", "guest/ubuntu", "ubuntu/packages.lock.json", + *GuestRustBuildConstants.SOURCE_PATHS, ], "package_manifests": [ "build/initramfs-ubuntu.cpio.gz.packages.json", @@ -378,6 +395,7 @@ def _write_release_fixture( for path in AzureLinuxBuildConstants.GUEST_SOURCE_DIRECTORIES ), "azurelinux/packages.lock.json", + *GuestRustBuildConstants.SOURCE_PATHS, ], "package_manifests": [ ( @@ -446,11 +464,39 @@ def _write_release_fixture( "source": source_dir, "openvmm": openvmm_dir, "binary": binary, + "nvx_rs": nvx_rs_binary, } return paths, kernel_inputs, revision -class CliTests(unittest.TestCase): +def _checkout_artifacts() -> nvx_rs.ArtifactPaths: + """Return the artifact paths of this checkout as `nvx-rs artifacts` prints + them, for tests of `nvx.py run` that need no nvx-rs.""" + return nvx_rs.ArtifactPaths( + openvmm=common.openvmm_binary_path(), + kernel=common.artifact_path(KernelBuildConstants.BINARY_NAME), + guests=tuple( + nvx_rs.GuestArtifacts( + name, + guests.guest_descriptor(name).distribution, + common.artifact_path(guests.guest_descriptor(name).initramfs_name), + ) + for name in guests.GUEST_NAMES + ), + ) + + +class _CheckoutArtifacts(unittest.TestCase): + """Answers the artifact lookups of `nvx.py run` without nvx-rs.""" + + def setUp(self) -> None: + super().setUp() + patcher = patch.object(nvx_rs, "artifacts", side_effect=_checkout_artifacts) + self.artifacts = patcher.start() + self.addCleanup(patcher.stop) + + +class CliTests(_CheckoutArtifacts): def test_usage_documents_every_command_and_option(self): # doc/usage.md is the CLI reference: every command has a row in its # command table and a section that names exactly the options of the @@ -919,429 +965,6 @@ def test_restore_rejects_ubuntu_guest_selection(self): with self.assertRaisesRegex(common.ScriptError, "snapshot already fixes"): nvx.command_run(args) - def test_sandbox_rejects_systemd_entrypoint(self): - for entrypoint in nvx.SYSTEMD_ENTRYPOINTS: - with self.subTest(entrypoint=entrypoint): - args = nvx.parse_args( - [ - "sandbox", - "--entrypoint", - entrypoint, - "--layer", - "distro,distro.erofs,11111111-1111-1111-1111-111111111111", - "--scratch", - "scratch.ext4", - ] - ) - with self.assertRaisesRegex( - common.ScriptError, - "systemd entrypoints", - ): - nvx.command_sandbox(args) - - def test_sandbox_allows_non_systemd_sbin_init(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - layer.write_bytes(b"distro") - scratch.write_bytes(b"scratch") - args = nvx.parse_args( - [ - "sandbox", - "--entrypoint", - "/sbin/init", - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - "--dry-run", - ] - ) - - def require(path: Path, _description: str) -> Path: - return path - - with ( - patch.object(nvx, "require_file", side_effect=require), - patch.object(nvx, "_format_command", return_value="formatted"), - ): - nvx.command_sandbox(args) - - def test_sandbox_rejects_systemd_from_bound_distro_metadata(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - layer.write_bytes(b"distro") - scratch.write_bytes(b"scratch") - layer.with_name(f"{layer.name}.manifest.json").write_text( - json.dumps( - { - "format": 1, - "artifact": layer.name, - "artifact_sha256": common.sha256_file(layer), - "packages": [{"name": "systemd"}], - } - ), - encoding="utf-8", - ) - args = nvx.parse_args( - [ - "sandbox", - "--entrypoint", - "/sbin/init", - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - ] - ) - with self.assertRaisesRegex(common.ScriptError, "systemd images"): - nvx.command_sandbox(args) - - def test_sandbox_command_parses_typed_launch_contract(self): - args = nvx.parse_args( - [ - "sandbox", - "--layer", - "distro,distro.erofs,11111111-1111-1111-1111-111111111111", - "--scratch", - "scratch.ext4", - "--entrypoint", - "/bin/workload", - "--arg=--serve", - "--memory-max", - "268435456", - "--pids-max", - "64", - "--workload-user", - "1000:1001", - ] - ) - - self.assertEqual(args.layer[0].role, "distro") - self.assertEqual(args.scratch, Path("scratch.ext4")) - self.assertEqual(args.sandbox_arg, ["--serve"]) - self.assertEqual(args.memory_max, 268435456) - self.assertEqual(args.pids_max, 64) - self.assertEqual(args.workload_user, (1000, 1001)) - self.assertIs(args.handler, nvx.command_sandbox) - - def test_sandbox_command_parses_state_aware_operations(self): - provision = nvx.parse_args( - [ - "sandbox", - "provision", - "--state-dir", - "state", - "--layer", - "distro,distro.erofs,11111111-1111-1111-1111-111111111111", - "--scratch", - "scratch.ext4", - ] - ) - self.assertEqual(provision.sandbox_operation, "provision") - self.assertEqual(provision.state_dir, Path("state")) - - execute = nvx.parse_args( - [ - "sandbox", - "exec", - "--state-dir", - "state", - "--entrypoint", - "/bin/sh", - "--arg=-c", - "--arg=echo managed", - "--exec-timeout-ms", - "5000", - ] - ) - self.assertEqual(execute.sandbox_operation, "exec") - self.assertEqual(execute.sandbox_arg, ["-c", "echo managed"]) - self.assertEqual(execute.exec_timeout_ms, 5000) - self.assertFalse(execute.inherit_default_environment) - - configured = nvx.parse_args( - [ - "sandbox", - "exec", - "--state-dir", - "state", - "--cwd", - "/work tree", - "--environment", - "EMPTY=", - "--environment", - "VALUE=space = value", - "--inherit-default-environment", - ] - ) - self.assertEqual(configured.cwd, "/work tree") - self.assertEqual( - configured.environment, - ["EMPTY=", "VALUE=space = value"], - ) - self.assertTrue(configured.inherit_default_environment) - - report = nvx.parse_args( - [ - "sandbox", - "exec", - "--state-dir", - "state", - "--outcome-report", - "exec-outcome.json", - ] - ) - self.assertEqual(report.outcome_report, Path("exec-outcome.json")) - - def test_sandbox_timeout_must_be_finite_and_positive(self): - for value in ("0", "-1", "nan", "inf"): - stderr = io.StringIO() - with ( - self.subTest(value=value), - contextlib.redirect_stderr(stderr), - self.assertRaises(SystemExit), - ): - nvx.parse_args( - ["sandbox", "start", "--state-dir", "state", "--timeout", value] - ) - self.assertIn("must be finite and greater than 0", stderr.getvalue()) - - def test_sandbox_managed_operations_reject_dry_run_before_state_access(self): - lifecycle_methods = ( - ("provision", "provision"), - ("start", "start"), - ("exec", "exec_workload"), - ("stop", "stop"), - ("deprovision", "deprovision"), - ) - with tempfile.TemporaryDirectory() as temporary: - state = Path(temporary) / "state" - state.mkdir() - marker = state / "marker" - marker.write_text("preserved", encoding="utf-8") - - for operation, lifecycle_method in lifecycle_methods: - with self.subTest(operation=operation): - args = nvx.parse_args( - [ - "sandbox", - operation, - "--state-dir", - str(state), - "--dry-run", - ] - ) - with ( - patch.object( - sandbox_lifecycle, - lifecycle_method, - ) as lifecycle, - self.assertRaisesRegex( - common.ScriptError, - "--dry-run is only valid for sandbox run", - ), - ): - nvx.command_sandbox(args) - lifecycle.assert_not_called() - self.assertEqual(marker.read_text(encoding="utf-8"), "preserved") - - def test_sandbox_exec_forwards_explicit_empty_environment_and_cwd(self): - with tempfile.TemporaryDirectory() as temporary: - environment_file = Path(temporary) / "environment.json" - environment_file.write_text("[]", encoding="utf-8") - args = nvx.parse_args( - [ - "sandbox", - "exec", - "--state-dir", - "state", - "--cwd", - "/work", - "--environment-file", - str(environment_file), - ] - ) - result = sandbox_lifecycle.ManagedExecResult(0, "exit", b"", b"") - with ( - patch.object( - sandbox_lifecycle, - "exec_workload", - return_value=result, - ) as execute, - self.assertRaises(SystemExit) as exit_context, - ): - nvx.command_sandbox(args) - - self.assertEqual(exit_context.exception.code, 0) - execute.assert_called_once_with( - Path("state"), - ("/bin/sh",), - timeout_ms=0, - response_timeout=60.0, - cwd="/work", - environment=(), - inherit_default_environment=False, - ) - - def test_sandbox_exec_forwards_environment_inheritance(self): - cases: tuple[tuple[list[str], tuple[str, ...] | None], ...] = ( - (["--environment", "A=1"], ("A=1",)), - ( - ["--environment", "A=1", "--environment", "TERM=dumb"], - ("A=1", "TERM=dumb"), - ), - # Without entries the workload gets the defaults whatever the flag says. - ([], None), - ) - for environment_args, environment in cases: - with self.subTest(environment=environment): - args = nvx.parse_args( - [ - "sandbox", - "exec", - "--state-dir", - "state", - *environment_args, - "--inherit-default-environment", - ] - ) - result = sandbox_lifecycle.ManagedExecResult(0, "exit", b"", b"") - with ( - patch.object( - sandbox_lifecycle, - "exec_workload", - return_value=result, - ) as execute, - self.assertRaises(SystemExit), - ): - nvx.command_sandbox(args) - execute.assert_called_once_with( - Path("state"), - ("/bin/sh",), - timeout_ms=0, - response_timeout=60.0, - cwd=None, - environment=environment, - inherit_default_environment=True, - ) - - def test_sandbox_exec_rejects_ambiguous_or_one_shot_environment(self): - both = nvx.parse_args( - [ - "sandbox", - "exec", - "--state-dir", - "state", - "--environment", - "A=1", - "--environment-file", - "environment.json", - ] - ) - with self.assertRaisesRegex(common.ScriptError, "mutually exclusive"): - nvx.command_sandbox(both) - - one_shot = nvx.parse_args(["sandbox", "run", "--environment", "A=1"]) - with self.assertRaisesRegex(common.ScriptError, "require.*exec"): - nvx.command_sandbox(one_shot) - - for operation in ("run", "start"): - inheriting = nvx.parse_args( - ["sandbox", operation, "--inherit-default-environment"] - ) - with ( - self.subTest(operation=operation), - self.assertRaisesRegex(common.ScriptError, "require.*exec"), - ): - nvx.command_sandbox(inheriting) - - def test_sandbox_environment_file_is_bounded_before_state_access(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "environment.json" - path.write_bytes(b" " * (1024 * 1024 + 1)) - args = nvx.parse_args( - [ - "sandbox", - "exec", - "--state-dir", - "absent-state", - "--environment-file", - str(path), - ] - ) - with ( - patch.object(sandbox_lifecycle, "exec_workload") as execute, - self.assertRaisesRegex(common.ScriptError, "exceeds.*byte limit"), - ): - nvx.command_sandbox(args) - execute.assert_not_called() - - def test_sandbox_environment_is_validated_before_state_access(self): - too_many = [ - value - for index in range(257) - for value in ("--environment", f"KEY{index}=value") - ] - cases = ( - (too_many, "exceeds 256 entries"), - (["--environment", "NO_EQUALS"], "non-empty KEY=VALUE"), - (["--environment", "=empty-key"], "non-empty KEY=VALUE"), - (["--environment", "KEY=embedded\0nul"], "non-empty KEY=VALUE"), - ( - ["--environment", "KEY=" + "\N{SNOWMAN}" * 1365], - "at most 4096 bytes", - ), - ( - ["--environment", "DUPLICATE=one", "--environment", "DUPLICATE=two"], - "duplicate key", - ), - ) - for environment_args, message in cases: - with self.subTest(message=message): - args = nvx.parse_args( - [ - "sandbox", - "exec", - "--state-dir", - "absent-state", - *environment_args, - ] - ) - with ( - patch.object(sandbox_lifecycle, "exec_workload") as execute, - self.assertRaisesRegex(common.ScriptError, message), - ): - nvx.command_sandbox(args) - execute.assert_not_called() - - def test_sandbox_environment_file_entries_are_validated_before_state_access(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "environment.json" - path.write_text( - json.dumps(["DUPLICATE=one", "DUPLICATE=two"]), - encoding="utf-8", - ) - args = nvx.parse_args( - [ - "sandbox", - "exec", - "--state-dir", - "absent-state", - "--environment-file", - str(path), - ] - ) - with ( - patch.object(sandbox_lifecycle, "exec_workload") as execute, - self.assertRaisesRegex(common.ScriptError, "duplicate key"), - ): - nvx.command_sandbox(args) - execute.assert_not_called() - def test_network_requires_explicit_portable_profile(self): args = nvx.parse_args( [ @@ -1362,7 +985,7 @@ def test_network_requires_explicit_portable_profile(self): with self.assertRaisesRegex(common.ScriptError, "--net and --network-profile"): nvx.command_run(missing_network) - def test_run_and_sandbox_forward_network_arguments(self): + def test_run_forwards_network_arguments(self): network_arguments = ( "--net 10.0.0.2/24 --network-profile portable " "--network-egress deny --network-ingress deny " @@ -1370,41 +993,73 @@ def test_run_and_sandbox_forward_network_arguments(self): "--network-egress-deny 10.0.0.0/8 --host-loopback allow " "--network-proxy 10.0.0.1:3128 --host-loopback-forward tcp:8080:80" ).split() - commands = [ - ["run", "--dry-run", *network_arguments], + + def require(path: Path, _description: str) -> Path: + return path + + args = nvx.parse_args(["run", "--dry-run", *network_arguments]) + with ( + patch.object(nvx, "require_file", side_effect=require), + patch.object( + nvx, "_format_command", return_value="formatted" + ) as format_command, + ): + args.handler(args) + + command = format_command.call_args.args[0] + self.assertEqual(command[command.index("--net") :], network_arguments) + + def test_run_lowers_structured_egress_policy_before_launch(self): + args = nvx.parse_args( [ - "sandbox", + "run", "--dry-run", - "--layer", - "distro,distro.erofs,11111111-1111-1111-1111-111111111111", - "--scratch", - "scratch.ext4", - *network_arguments, - ], - ] + "--network-egress", + "deny", + "--network-egress-policy-file", + "policy.json", + ] + ) def require(path: Path, _description: str) -> Path: return path - for arguments in commands: - with self.subTest(command=arguments[0]): - args = nvx.parse_args(arguments) - with ( - patch.object(nvx, "require_file", side_effect=require), - patch.object(sandbox, "require_file", side_effect=require), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, - ): - args.handler(args) + rules = nvx_rs.EgressRules( + allow=("192.0.2.0/25:tcp:8000-8001", "192.0.2.200/32"), + deny=("192.0.2.0/24:tcp:8001",), + ) + with ( + patch.object( + nvx_rs, "compile_egress_policy", return_value=rules + ) as compile_policy, + patch.object(nvx, "require_file", side_effect=require), + patch.object( + nvx, "_format_command", return_value="formatted" + ) as format_command, + ): + nvx.command_run(args) - command = format_command.call_args.args[0] - self.assertEqual( - command[command.index("--net") :], - network_arguments, - ) + compile_policy.assert_called_once_with(Path("policy.json")) + command = format_command.call_args.args[0] + self.assertNotIn("--network-egress-policy-file", command) + self.assertEqual( + command[command.index("--network-egress") :], + [ + "--network-egress", + "deny", + "--network-egress-allow", + "192.0.2.0/25:tcp:8000-8001", + "--network-egress-allow", + "192.0.2.200/32", + "--network-egress-deny", + "192.0.2.0/24:tcp:8001", + ], + ) - def test_run_lowers_structured_egress_policy_before_launch(self): + @unittest.skipUnless( + os.environ.get("NVX_RS"), "set NVX_RS to an nvx-rs executable to run" + ) + def test_run_lowers_structured_egress_policy_with_nvx_rs(self): with tempfile.TemporaryDirectory() as temporary: policy = Path(temporary) / "policy.json" policy.write_text( @@ -1459,14 +1114,32 @@ def require(path: Path, _description: str) -> Path: ) as format_command, ): nvx.command_run(args) - - command = format_command.call_args.args[0] - self.assertNotIn("--network-egress-policy-file", command) - self.assertEqual(command.count("--network-egress-allow"), 2) - self.assertIn("192.0.2.0/25:tcp:8000-8001", command) - self.assertIn("192.0.2.200/32", command) - self.assertEqual(command.count("--network-egress-deny"), 1) - self.assertIn("192.0.2.0/24:tcp:8001", command) + command = format_command.call_args.args[0] + self.assertEqual( + command[command.index("--network-egress-allow") :], + [ + "--network-egress-allow", + "192.0.2.0/25:tcp:8000-8001", + "--network-egress-allow", + "192.0.2.200/32", + "--network-egress-deny", + "192.0.2.0/24:tcp:8001", + ], + ) + # A refused policy fails with the compiler's message, before the + # artifacts are looked up. + policy.write_text( + json.dumps({"allow": [{"to": [{"cidr": "fe80::1%eth0/64"}]}]}), + encoding="utf-8", + ) + self.artifacts.reset_mock() + with self.assertRaisesRegex( + common.ScriptError, + r"^allow\[0\]\.to\[0\]\.cidr must not name an IPv6 scope: " + r"fe80::1%eth0/64$", + ): + nvx.command_run(args) + self.artifacts.assert_not_called() def test_policy_file_rejects_ambiguous_flags_before_artifact_access(self): cases = ( @@ -1490,261 +1163,11 @@ def test_policy_file_rejects_ambiguous_flags_before_artifact_access(self): nvx.command_run(args) require.assert_not_called() - def test_sandbox_rejects_policy_mixing_before_validating_layer_files(self): - for operation in ("run", "provision"): - with self.subTest(operation=operation): - args = nvx.parse_args( - [ - "sandbox", - operation, - "--layer", - "distro,absent.erofs,00000000-0000-4000-8000-000000000001", - "--scratch", - "absent.ext4", - "--network-egress", - "deny", - "--network-egress-policy-file", - "absent.json", - "--network-egress-allow", - "192.0.2.1", - ] - ) - with ( - patch.object(nvx.SandboxLaunch, "validated") as validated, - self.assertRaisesRegex(common.ScriptError, "cannot be combined"), - ): - nvx.command_sandbox(args) - validated.assert_not_called() - - def test_policy_file_rejects_scoped_ipv6_before_launch_or_provision(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - policy = root / "policy.json" - policy.write_text( - json.dumps({"allow": [{"to": [{"cidr": "fe80::1%eth0/64"}]}]}), - encoding="utf-8", - ) - options = [ - "--network-egress", - "deny", - "--network-egress-policy-file", - str(policy), - ] - message = ( - r"^allow\[0\]\.to\[0\]\.cidr must not name an IPv6 scope: " - r"fe80::1%eth0/64$" - ) - args = nvx.parse_args(["run", "--dry-run", *options]) - with ( - patch.object(nvx, "require_file") as require, - self.assertRaisesRegex(common.ScriptError, message), - ): - nvx.command_run(args) - require.assert_not_called() - - state = root / "state" - operations: tuple[tuple[str, list[str]], ...] = ( - ("run", []), - ("provision", ["--state-dir", str(state)]), - ) - for operation, extra in operations: - with self.subTest(operation=operation): - args = nvx.parse_args( - [ - "sandbox", - operation, - *extra, - "--layer", - "distro,absent.erofs,00000000-0000-4000-8000-000000000001", - "--scratch", - "absent.ext4", - *options, - ] - ) - with ( - patch.object(nvx.SandboxLaunch, "validated") as validated, - self.assertRaisesRegex(common.ScriptError, message), - ): - nvx.command_sandbox(args) - validated.assert_not_called() - self.assertFalse(state.exists()) - - def test_managed_provision_persists_lowered_policy_not_source_path(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - policy = root / "policy.json" - state = root / "state" - layer.write_bytes(b"layer") - scratch.write_bytes(b"scratch") - policy.write_text( - json.dumps( - { - "allow": [ - { - "to": [ - { - "cidr": "192.0.2.0/24", - "except": ["192.0.2.128/25"], - } - ], - } - ] - } - ), - encoding="utf-8", - ) - args = nvx.parse_args( - [ - "sandbox", - "provision", - "--state-dir", - str(state), - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - "--network-egress", - "deny", - "--network-egress-policy-file", - str(policy), - ] - ) - - nvx.command_sandbox(args) - config = json.loads( - (state / sandbox_lifecycle.CONFIG_NAME).read_text(encoding="utf-8") - ) - policy.unlink() - - self.assertEqual(config["network_egress"], "deny") - self.assertEqual(config["network_egress_allow"], ["192.0.2.0/25"]) - self.assertEqual(config["network_egress_deny"], []) - self.assertNotIn("network_egress_policy_file", config) - - def test_public_cli_provision_persists_policy_after_source_removal(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - policy = root / "policy.json" - state = root / "state" - layer.write_bytes(b"layer") - scratch.write_bytes(b"scratch") - policy.write_text( - json.dumps( - { - "allow": [ - { - "to": [ - { - "cidr": "192.0.2.0/24", - "except": ["192.0.2.128/25"], - } - ], - "ports": [ - { - "protocol": "tcp", - "port": 8000, - "endPort": 8001, - } - ], - } - ], - "deny": [ - { - "to": [{"cidr": "192.0.2.0/24"}], - "ports": [{"protocol": "tcp", "port": 8001}], - } - ], - } - ), - encoding="utf-8", - ) - result = subprocess.run( - [ - sys.executable, - str(Path(nvx.__file__).resolve()), - "sandbox", - "provision", - "--state-dir", - str(state), - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - "--network-egress", - "deny", - "--network-egress-policy-file", - str(policy), - ], - cwd=BuildConstants.REPO_ROOT, - capture_output=True, - timeout=10, - ) - self.assertEqual( - result.returncode, - 0, - result.stderr.decode("utf-8", "replace"), - ) - policy.unlink() - config = json.loads( - (state / sandbox_lifecycle.CONFIG_NAME).read_text(encoding="utf-8") - ) - - self.assertEqual( - config["network_egress_allow"], - ["192.0.2.0/25:tcp:8000-8001"], - ) - self.assertEqual( - config["network_egress_deny"], - ["192.0.2.0/24:tcp:8001"], - ) - self.assertNotIn(str(policy), json.dumps(config)) - - def test_invalid_policy_has_no_managed_state_side_effect(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - policy = root / "policy.json" - state = root / "state" - layer.write_bytes(b"layer") - scratch.write_bytes(b"scratch") - policy.write_text( - '{"allow":[{"to":[{"cidr":"192.0.2.0/24"}],' - '"ports":[{"protocol":"icmp","port":8}]}]}', - encoding="utf-8", - ) - args = nvx.parse_args( - [ - "sandbox", - "provision", - "--state-dir", - str(state), - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - "--network-egress", - "deny", - "--network-egress-policy-file", - str(policy), - ] - ) - - with self.assertRaises(common.ScriptError): - nvx.command_sandbox(args) - self.assertFalse(state.exists()) - - def test_managed_non_launch_operations_reject_network_policy_options(self): + def test_run_reports_policy_errors_before_looking_up_artifacts(self): args = nvx.parse_args( [ - "sandbox", - "start", - "--state-dir", - "state", + "run", + "--dry-run", "--network-egress", "deny", "--network-egress-policy-file", @@ -1752,13 +1175,64 @@ def test_managed_non_launch_operations_reject_network_policy_options(self): ] ) with ( - patch.object(sandbox_lifecycle, "start") as start, + patch.object( + nvx_rs, + "compile_egress_policy", + side_effect=common.ScriptError("egress policy must be an object"), + ), + patch.object(nvx, "require_file") as require, self.assertRaisesRegex( - common.ScriptError, "only valid for sandbox run or provision" + common.ScriptError, "^egress policy must be an object$" + ), + ): + nvx.command_run(args) + self.artifacts.assert_not_called() + require.assert_not_called() + + def test_run_takes_its_artifacts_from_nvx_rs(self): + root = Path("alternate") + artifacts = nvx_rs.ArtifactPaths( + openvmm=root / "openvmm", + kernel=root / "vmlinux", + guests=( + nvx_rs.GuestArtifacts("alpine", "Alpine Linux", root / "alpine.gz"), + nvx_rs.GuestArtifacts("ubuntu", "Ubuntu Base", root / "ubuntu.gz"), ), + ) + self.artifacts.side_effect = None + self.artifacts.return_value = artifacts + + def require(path: Path, _description: str) -> Path: + return path + + args = nvx.parse_args(["run", "--guest", "ubuntu", "--dry-run"]) + with ( + patch.object(nvx, "require_file", side_effect=require) as require_file, + patch.object( + nvx, "_format_command", return_value="formatted" + ) as format_command, + ): + nvx.command_run(args) + command = format_command.call_args.args[0] + self.assertEqual(command[0], str(root / "openvmm")) + self.assertEqual(command[command.index("--kernel") + 1], str(root / "vmlinux")) + self.assertEqual( + command[command.index("--initrd") + 1], str(root / "ubuntu.gz") + ) + # The checks and their messages are those of nvx.py. + self.assertEqual( + require_file.call_args_list, + [ + call(root / "openvmm", "OpenVMM release binary"), + call(root / "vmlinux", "Linux direct kernel"), + call(root / "ubuntu.gz", "Ubuntu Base initramfs"), + ], + ) + with self.assertRaisesRegex( + common.ScriptError, + "^OpenVMM release binary not found: " + re.escape(str(root / "openvmm")), ): - nvx.command_sandbox(args) - start.assert_not_called() + nvx.command_run(args) def test_run_parses_denied_filesystem_paths(self): args = nvx.parse_args( @@ -2452,3094 +1926,3028 @@ def test_record_openvmm_provenance_command_is_exposed(self): self.assertIs(args.handler, nvx.command_record_openvmm_provenance) -class CiTests(unittest.TestCase): - def test_openvmm_unit_tests_run_nextest_and_doctests(self): +SCRIPTS_DIRECTORY = Path(__file__).parent +# A stand-in for nvx-rs that the process tests run in its place: it acts as its +# first argument asks. +FAKE_NVX_RS = r""" +import os +import signal +import sys +import time + +mode = sys.argv[1] +if mode == "streams": + os.write(1, b"standard output\n") + os.write(2, b"standard error\n") + sys.exit(int(sys.argv[2])) +if mode == "interruptible": + # Like nvx-rs, which records each SIGINT, report the first one only. + reported = [] + + def interrupted(signum, frame): + if not reported: + reported.append(signum) + os.write(2, b"Interrupted\n") + os._exit(130) + + signal.signal(signal.SIGINT, interrupted) + open(sys.argv[2], "w").close() + time.sleep(60) + sys.exit(99) +if mode == "signaled": + signum = getattr(signal, sys.argv[2]) + signal.signal(signum, signal.SIG_DFL) + os.kill(os.getpid(), signum) + time.sleep(60) +sys.exit(f"unknown mode {mode}") +""" +# Runs nvx_rs.run in a fresh Python, with the stand-in in place of nvx-rs. +NVX_RS_DRIVER = r""" +import sys + +sys.path.insert(0, sys.argv[1]) +from nvx_tools import nvx_rs + +fake = sys.argv[2] +nvx_rs._command = lambda arguments, program: [sys.executable, fake, *arguments] +raise SystemExit(nvx_rs.run(sys.argv[3:], program="nvx.py")) +""" +# Runs nvx.main in a fresh Python, with the stand-in in place of nvx-rs: the +# arguments before "--" tell the stand-in what to do. +NVX_PY_DRIVER = r""" +import sys + +sys.path.insert(0, sys.argv[1]) +import nvx +from nvx_tools import nvx_rs + +separator = sys.argv.index("--") +fake = [sys.executable, sys.argv[2], *sys.argv[3:separator]] +nvx_rs._command = lambda arguments, program: fake +raise SystemExit(nvx.main(sys.argv[separator + 1 :])) +""" +# Runs nvx.main as nvx.py in a fresh Python, with the nvx-rs that NVX_RS names +# and the artifact root that the second argument names. +NVX_PY_REAL_DRIVER = r""" +import sys +from pathlib import Path + +sys.path.insert(0, sys.argv[1]) +sys.argv[0] = "nvx.py" +import nvx +from nvx_tools import nvx_rs + +nvx_rs.ROOT = Path(sys.argv[2]) +raise SystemExit(nvx.main(sys.argv[3:])) +""" + + +@contextlib.contextmanager +def _without_nvx_rs_variable() -> Generator[None]: + with patch.dict(os.environ): + os.environ.pop(nvx_rs.VARIABLE, None) + yield + + +def _completed( + returncode: int, stdout: bytes = b"", stderr: bytes = b"" +) -> subprocess.CompletedProcess[bytes]: + return subprocess.CompletedProcess(["nvx-rs"], returncode, stdout, stderr) + + +def _signal_process_group(pid: int, name: str) -> None: + """Send the POSIX signal `name` to the process group `pid`, as a terminal + sends Ctrl-C to its foreground group.""" + killpg = cast(Callable[[int, int], None], getattr(os, "killpg")) # noqa: B009 + killpg(pid, cast(int, getattr(signal, name))) + + +class NvxRsTests(unittest.TestCase): + def _fake(self, directory: Path) -> Path: + fake = directory / "fake_nvx_rs.py" + fake.write_text(FAKE_NVX_RS, encoding="utf-8") + return fake + + def _driver(self, fake: Path, *arguments: str) -> list[str]: + return [ + sys.executable, + "-c", + NVX_RS_DRIVER, + str(SCRIPTS_DIRECTORY), + str(fake), + *arguments, + ] + + def test_nvx_rs_variable_names_the_executable(self): with tempfile.TemporaryDirectory() as temporary: - openvmm = Path(temporary) / "openvmm" - openvmm.mkdir() - (openvmm / "Cargo.toml").touch() - fuzz_crates = common.CommandResult( - args=("cargo", "xtask", "fuzz", "list", "--crates"), - returncode=0, - stdout=b"fuzz_alpha\nfuzz_beta\n", - stderr=b"", - ) + binary = Path(temporary) / "nvx-rs" + binary.write_bytes(b"") + with ( + patch.dict(os.environ, {"NVX_RS": str(binary)}), + patch.object(nvx_rs, "build_binary") as build, + ): + self.assertEqual(nvx_rs.require_binary(), binary.absolute()) + build.assert_not_called() + missing = Path(temporary) / "missing" + with ( + patch.dict(os.environ, {"NVX_RS": str(missing)}), + self.assertRaisesRegex( + common.ScriptError, "NVX_RS does not name an nvx-rs executable" + ), + ): + nvx_rs.require_binary() + def test_the_checkout_binary_runs_without_a_build(self): + self.assertEqual( + nvx_rs.binary_path(), + BuildConstants.REPO_ROOT + / "nvx-rs" + / "target" + / "release" + / ("nvx-rs.exe" if os.name == "nt" else "nvx-rs"), + ) + with tempfile.TemporaryDirectory() as temporary: + binary = Path(temporary) / "nvx-rs" + binary.write_bytes(b"") with ( - patch.object(OpenVMMBuildConstants, "DIRECTORY", openvmm), - patch.object(ci, "require_tool", return_value="cargo"), - patch.object(ci, "run_capture", return_value=fuzz_crates) as capture, - patch.object(ci, "run_checked") as run_checked, + _without_nvx_rs_variable(), + patch.object(nvx_rs, "binary_path", return_value=binary), + patch("subprocess.run") as run, ): - ci.run_openvmm_unit_tests() + self.assertEqual(nvx_rs.require_binary(), binary) + run.assert_not_called() - capture.assert_called_once_with( - ["cargo", "xtask", "fuzz", "list", "--crates"], - cwd=openvmm, - ) - self.assertEqual(run_checked.call_count, 3) - self.assertEqual( - run_checked.call_args_list[0], - call( - [ - "cargo", - "xflowey", - "restore-packages", - "--no-compat-igvm", - ], - cwd=openvmm, - ), - ) - command = run_checked.call_args_list[1].args[0] - self.assertEqual( - command[:10], - [ - "cargo", - "nextest", - "run", - "--profile", - "agent", - "--workspace", - "--tests", - "--bins", - "--features", - "ci", - ], - ) - excluded_packages = [ - command[index + 1] - for index, argument in enumerate(command) - if argument == "--exclude" - ] - self.assertEqual( - excluded_packages, - [ - *ci.OPENVMM_UNIT_TEST_EXCLUDED_PACKAGES, - "fuzz_alpha", - "fuzz_beta", - ], - ) - self.assertEqual(run_checked.call_args_list[1].kwargs["cwd"], openvmm) - self.assertEqual( - run_checked.call_args_list[2], - call( - [ - "cargo", - "test", - "--locked", - "--doc", - "--workspace", - "--no-fail-fast", - ], - cwd=openvmm, - ), - ) - - def test_openvmm_unit_tests_stop_when_fuzz_crate_query_fails(self): + def test_cargo_builds_a_missing_binary_onto_standard_error(self): with tempfile.TemporaryDirectory() as temporary: - openvmm = Path(temporary) / "openvmm" - openvmm.mkdir() - (openvmm / "Cargo.toml").touch() - failed_query = common.CommandResult( - args=("cargo", "xtask", "fuzz", "list", "--crates"), - returncode=2, - stdout=b"", - stderr=b"query failed", - ) + binary = Path(temporary) / "nvx-rs" + + def build( + command: tuple[str, ...], **_: object + ) -> subprocess.CompletedProcess[bytes]: + binary.write_bytes(b"") + return _completed(0) + stdout = io.StringIO() + stderr = io.StringIO() with ( - patch.object(OpenVMMBuildConstants, "DIRECTORY", openvmm), - patch.object(ci, "require_tool", return_value="cargo"), - patch.object(ci, "run_capture", return_value=failed_query), - patch.object(ci, "run_checked") as run_checked, - self.assertRaisesRegex( - common.ScriptError, - "OpenVMM fuzz crate query exited 2", - ), + _without_nvx_rs_variable(), + patch.object(nvx_rs, "binary_path", return_value=binary), + patch("shutil.which", return_value="cargo"), + patch("subprocess.run", side_effect=build) as run, + patch.object(nvx_rs, "_stderr_target", return_value=2), + patch.object(nvx_rs, "record_provenance") as record, + contextlib.redirect_stdout(stdout), + contextlib.redirect_stderr(stderr), ): - ci.run_openvmm_unit_tests() + self.assertEqual(nvx_rs.require_binary(), binary) + run.assert_called_once() + # The build records the provenance of what it built, for the host. + record.assert_called_once_with() + self.assertEqual( + run.call_args.args[0], + ( + "cargo", + "build", + "--release", + "--locked", + "--features", + "cli", + "--manifest-path", + "nvx-rs/Cargo.toml", + "--target-dir", + "nvx-rs/target", + ), + ) + # From the repository root, so that rust-toolchain.toml applies, and + # with Cargo's output on standard error, away from the command's. + self.assertEqual(run.call_args.kwargs["cwd"], BuildConstants.REPO_ROOT) + self.assertEqual(run.call_args.kwargs["stdout"], 2) + self.assertEqual(stdout.getvalue(), "") + self.assertEqual( + stderr.getvalue(), + ">> cargo build --release --locked --features cli --manifest-path " + "nvx-rs/Cargo.toml --target-dir nvx-rs/target\n", + ) - run_checked.assert_not_called() + def test_build_output_goes_to_the_descriptor_of_standard_error(self): + stream = MagicMock() + stream.fileno.return_value = 2 + with patch("sys.stderr", stream): + self.assertEqual(nvx_rs._stderr_target(), 2) + stream.flush.assert_called_once_with() + with patch("sys.stderr", new_callable=io.StringIO): + self.assertEqual(nvx_rs._stderr_target(), subprocess.DEVNULL) - def test_openvmm_tests_use_nvx_guest_artifacts(self): + def test_missing_cargo_names_every_way_to_get_nvx_rs(self): with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - openvmm = root / "openvmm" - openvmm.mkdir() - (openvmm / "Cargo.toml").touch() - kernel = root / "vmlinux" - initrd = root / "initramfs.cpio.gz" - kernel.touch() - initrd.touch() - backend = "whp" if os.name == "nt" else "kvm" - installed_targets = common.CommandResult( - args=("rustup", "target", "list"), - returncode=0, - stdout=( - "\n".join(OpenVMMBuildConstants.TEST_RUST_TARGETS[backend]) + "\n" - ).encode(), - stderr=b"", - ) + binary = Path(temporary) / "nvx-rs" + with ( + _without_nvx_rs_variable(), + patch.object(nvx_rs, "binary_path", return_value=binary), + patch("shutil.which", return_value=None), + patch("subprocess.run") as run, + self.assertRaises(common.ScriptError) as raised, + ): + nvx_rs.require_binary() + run.assert_not_called() + message = str(raised.exception) + for expected in ( + f"nvx-rs is not built at {binary}, and Cargo is not on PATH", + "set NVX_RS to an nvx-rs executable", + "`nvx.py download`", + "https://rustup.rs", + "cargo build --release --locked --features cli --manifest-path " + "nvx-rs/Cargo.toml --target-dir nvx-rs/target", + ): + self.assertIn(expected, message) + + def test_release_targets_follow_the_operating_system(self): + self.assertEqual( + nvx_rs.release_target(), + "x86_64-pc-windows-msvc" + if os.name == "nt" + else "x86_64-unknown-linux-musl", + ) + self.assertEqual(nvx_rs.FEATURES, ("cli",)) + # BUILD_COMMAND builds the features that releases ship. + features = nvx_rs.BUILD_COMMAND.index("--features") + 1 + self.assertEqual(nvx_rs.BUILD_COMMAND[features].split(","), ["cli"]) + + def test_source_digest_covers_the_build_inputs_in_any_line_endings(self): + relative = [ + path.relative_to(BuildConstants.REPO_ROOT).as_posix() + for path in nvx_rs.source_files() + ] + self.assertEqual(relative, sorted(relative)) + for name in ( + "nvx-rs/Cargo.toml", + "nvx-rs/Cargo.lock", + "nvx-rs/build.rs", + "nvx-rs/src/bin/nvx-rs.rs", + "rust-toolchain.toml", + ): + self.assertIn(name, relative) + # Tests, documentation, and the bundled release pin build no part of + # the executable. + for name in relative: + self.assertNotRegex(name, r"^nvx-rs/(tests/|README\.md$|artifacts\.json$)") + self.assertEqual(nvx_rs.source_sha256(), nvx_rs.source_sha256()) + with tempfile.TemporaryDirectory() as temporary: + checkouts = [Path(temporary) / "lf", Path(temporary) / "crlf"] + for checkout, newline in zip(checkouts, (b"\n", b"\r\n"), strict=True): + for name in ( + "nvx-rs/Cargo.toml", + "nvx-rs/Cargo.lock", + "nvx-rs/build.rs", + "nvx-rs/src/lib.rs", + "nvx-rs/src/bin/nvx-rs.rs", + "rust-toolchain.toml", + ): + path = checkout / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(newline.join((name.encode(), b"line", b""))) + digest = nvx_rs.source_sha256(checkouts[0]) + # Checkouts differ only in line endings, as on Windows. + self.assertEqual(nvx_rs.source_sha256(checkouts[1]), digest) + source = checkouts[0] / "nvx-rs" / "src" / "lib.rs" + source.write_bytes(b"changed\n") + self.assertNotEqual(nvx_rs.source_sha256(checkouts[0]), digest) + # A new source file changes the digest too. + (checkouts[1] / "nvx-rs" / "src" / "new.rs").write_bytes(b"") + self.assertNotEqual(nvx_rs.source_sha256(checkouts[1]), digest) + shutil.rmtree(checkouts[1] / "nvx-rs" / "src") + with self.assertRaisesRegex( + common.ScriptError, "nvx-rs source directory not found" + ): + nvx_rs.source_sha256(checkouts[1]) + + def test_host_target_is_the_host_of_the_pinned_rust(self): + version = ( + "rustc 1.95.0 (abc 2026-01-01)\nbinary: rustc\n" + "host: x86_64-unknown-linux-gnu\nrelease: 1.95.0\n" + ) + with patch( + "subprocess.run", + return_value=subprocess.CompletedProcess(["rustc"], 0, version, ""), + ) as run: + self.assertEqual(nvx_rs.host_target(), "x86_64-unknown-linux-gnu") + self.assertEqual(run.call_args.args[0], ("rustc", "-vV")) + # From the repository root, so that rust-toolchain.toml applies. + self.assertEqual(run.call_args.kwargs["cwd"], BuildConstants.REPO_ROOT) + for result in ( + subprocess.CompletedProcess(["rustc"], 1, "", "no rustc"), + subprocess.CompletedProcess(["rustc"], 0, "release: 1.95.0\n", ""), + ): with ( - patch.object(OpenVMMBuildConstants, "DIRECTORY", openvmm), - patch.object( - ci, - "artifact_path", - side_effect=[kernel, initrd], - ), - patch.object(ci.os, "access", return_value=True), - patch.object(ci.Path, "exists", return_value=False), - patch.object(ci, "require_tool", side_effect=["cargo", "rustup"]), - patch.object( - ci, - "run_capture", - return_value=installed_targets, - ) as run_capture, - patch.object(ci, "run_checked") as run_checked, - patch.dict( - os.environ, - { - "PETRI_CAPABILITIES": "vpci", - "RUNNER_TEMP": os.fspath(root), - }, - ), + self.subTest(result=result), + patch("subprocess.run", return_value=result), + self.assertRaisesRegex(common.ScriptError, "host target"), ): - ci.run_openvmm_tests(backend) + nvx_rs.host_target() + with ( + patch("subprocess.run", side_effect=OSError("missing")), + self.assertRaisesRegex(common.ScriptError, "failed to run rustc"), + ): + nvx_rs.host_target() - run_capture.assert_called_once_with( - [ - "rustup", - "target", - "list", - "--installed", - "--toolchain", - OpenVMMBuildConstants.RUST_TOOLCHAIN, - ] - ) - self.assertEqual(run_checked.call_count, 2) - restore, tests = run_checked.call_args_list - self.assertEqual( - restore.args[0], - ["cargo", "xflowey", "restore-packages", "--no-compat-igvm"], - ) - environment = restore.kwargs["env"] - self.assertEqual( - environment["RUSTUP_TOOLCHAIN"], - OpenVMMBuildConstants.RUST_TOOLCHAIN, - ) - if os.name == "nt": - self.assertNotIn("XDG_CACHE_HOME", environment) - else: + def test_provenance_binds_the_executable_that_nvx_py_runs(self): + linux = nvx_rs.RELEASE_TARGETS["linux"] + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + binary = root / "nvx-rs" + other = root / "other-nvx-rs" + provenance = root / "build" / nvx_rs.PROVENANCE_NAME + with ( + patch.object(nvx_rs, "binary_path", return_value=binary), + patch.object(nvx_rs, "provenance_path", return_value=provenance), + patch.object(nvx_rs, "host_target", return_value="host-triple"), + ): + with ( + _without_nvx_rs_variable(), + self.assertRaisesRegex( + common.ScriptError, + f"nvx-rs is not built at {re.escape(str(binary))}", + ), + ): + nvx_rs.record_provenance() + self.assertFalse(provenance.exists()) + + binary.write_bytes(b"nvx-rs") + with _without_nvx_rs_variable(): + self.assertEqual(nvx_rs.record_provenance(), provenance) self.assertEqual( - environment["XDG_CACHE_HOME"], - os.fspath(root / "openvmm-cache"), + json.loads(provenance.read_text(encoding="utf-8")), + { + "format": 1, + "source_sha256": nvx_rs.source_sha256(), + "toolchain": GuestRustBuildConstants.TOOLCHAIN, + "target": "host-triple", + "features": ["cli"], + "executable_sha256": common.sha256_file(binary), + }, ) - command = tests.args[0] - self.assertEqual(command[:3], ["cargo", "xflowey", "vmm-tests-run"]) - filter_index = command.index("--filter") - self.assertEqual( - command[filter_index + 1], - ci.OPENVMM_TEST_FILTERS[backend], - ) - self.assertEqual(tests.kwargs["cwd"], openvmm) - self.assertIs(tests.kwargs["env"], environment) - self.assertEqual( - environment["OPENVMM_MICROVM_TEST_KERNEL"], - os.fspath(kernel.resolve()), - ) - self.assertEqual( - environment["OPENVMM_MICROVM_TEST_INITRD"], - os.fspath(initrd.resolve()), - ) - self.assertEqual(environment["PETRI_CAPABILITIES"], "vpci") - if os.name == "nt": + + # The executable that NVX_RS names is the one that nvx.py + # runs, and so the one that the record describes. + other.write_bytes(b"other nvx-rs") + with patch.dict(os.environ, {nvx_rs.VARIABLE: str(other)}): + nvx_rs.record_provenance("x86_64-pc-windows-msvc") + record = json.loads(provenance.read_text(encoding="utf-8")) + self.assertEqual(record["target"], "x86_64-pc-windows-msvc") + self.assertEqual(record["executable_sha256"], common.sha256_file(other)) + + # A record for the Linux release target needs the static + # executable that such a build gives. + with ( + _without_nvx_rs_variable(), + self.assertRaisesRegex( + common.ScriptError, f"invalid {linux} nvx-rs executable" + ), + ): + nvx_rs.record_provenance(linux) + binary.write_bytes( + _elf_file(((1, b"code"), (3, b"/lib/ld-musl.so.1\0")), elf_type=3) + ) + with ( + _without_nvx_rs_variable(), + self.assertRaisesRegex(common.ScriptError, "is not static"), + ): + nvx_rs.record_provenance(linux) + binary.write_bytes(_elf_file(elf_type=3)) + with _without_nvx_rs_variable(): + nvx_rs.record_provenance(linux) self.assertEqual( - command[command.index("--dir") + 1], - os.fspath(root / backend), + json.loads(provenance.read_text(encoding="utf-8"))["target"], + linux, ) - def test_openvmm_tests_pin_toolchain_without_runner_temp(self): - installed_targets = common.CommandResult( - args=("rustup", "target", "list"), - returncode=0, - stdout=( - "\n".join(OpenVMMBuildConstants.TEST_RUST_TARGETS["whp"]) + "\n" - ).encode(), - stderr=b"", + def test_record_command_records_the_provenance(self): + args = nvx.parse_args( + ["record-nvx-rs-provenance", "--target", "x86_64-unknown-linux-musl"] ) - + self.assertEqual(args.target, "x86_64-unknown-linux-musl") + self.assertIsNone(nvx.parse_args(["record-nvx-rs-provenance"]).target) + recorded = Path("build") / nvx_rs.PROVENANCE_NAME with ( - patch.object(ci, "run_capture", return_value=installed_targets), - patch.object(ci, "run_checked") as run_checked, - patch.dict(os.environ, {}, clear=True), + patch.object(nvx_rs, "record_provenance", return_value=recorded) as record, + contextlib.redirect_stdout(io.StringIO()) as stdout, ): - environment = ci._prepare_openvmm_test_environment("whp", "rustup") - + args.handler(args) + record.assert_called_once_with("x86_64-unknown-linux-musl") self.assertEqual( - environment, - {"RUSTUP_TOOLCHAIN": OpenVMMBuildConstants.RUST_TOOLCHAIN}, + stdout.getvalue(), f">> recorded the provenance of nvx-rs in {recorded}\n" ) - run_checked.assert_not_called() - def test_openvmm_tests_prepare_job_local_toolchain_for_missing_targets(self): + def test_failed_builds_report_cargo(self): with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - openvmm = root / "openvmm" - openvmm.mkdir() - (openvmm / "Cargo.toml").touch() - kernel = root / "vmlinux" - initrd = root / "initramfs.cpio.gz" - kernel.touch() - initrd.touch() - backend = "whp" if os.name == "nt" else "kvm" - installed_targets = common.CommandResult( - args=("rustup", "target", "list"), - returncode=0, - stdout=f"{OpenVMMBuildConstants.GUEST_RUST_TARGET}\n".encode(), - stderr=b"", - ) - - with ( - patch.object(OpenVMMBuildConstants, "DIRECTORY", openvmm), - patch.object( - ci, - "artifact_path", - side_effect=[kernel, initrd], - ), - patch.object(ci.os, "access", return_value=True), - patch.object(ci.Path, "exists", return_value=False), - patch.object(ci, "require_tool", side_effect=["cargo", "rustup"]), - patch.object(ci, "run_capture", return_value=installed_targets), - patch.object(ci, "run_checked") as run_checked, - patch.dict(os.environ, {"RUNNER_TEMP": os.fspath(root)}), + binary = Path(temporary) / "nvx-rs" + for returncode, message in ( + (101, r"Cargo failed to build nvx-rs \(exit 101\)"), + (0, "Cargo did not build"), ): - ci.run_openvmm_tests(backend) + with ( + self.subTest(returncode=returncode), + _without_nvx_rs_variable(), + patch.object(nvx_rs, "binary_path", return_value=binary), + patch("shutil.which", return_value="cargo"), + patch("subprocess.run", return_value=_completed(returncode)), + patch.object(nvx_rs, "record_provenance") as record, + contextlib.redirect_stderr(io.StringIO()), + self.assertRaisesRegex(common.ScriptError, message), + ): + nvx_rs.require_binary() + record.assert_not_called() - self.assertEqual(run_checked.call_count, 4) - install_toolchain, install_targets, restore, tests = ( - run_checked.call_args_list - ) - environment = install_toolchain.kwargs["env"] - self.assertEqual( - environment["RUSTUP_HOME"], - os.fspath(root / "openvmm-rustup"), - ) - self.assertEqual( - environment["RUSTUP_TOOLCHAIN"], - OpenVMMBuildConstants.RUST_TOOLCHAIN, - ) - if os.name != "nt": - self.assertEqual( - environment["XDG_CACHE_HOME"], - os.fspath(root / "openvmm-cache"), - ) + def test_commands_name_the_program_and_the_repository_root(self): + binary = Path("bin") / "nvx-rs" + with patch.object(nvx_rs, "require_binary", return_value=binary): self.assertEqual( - install_toolchain.args[0], + nvx_rs._command(["sandbox", "exec"], "nvx.py"), [ - "rustup", - "toolchain", - "install", - OpenVMMBuildConstants.RUST_TOOLCHAIN, - "--profile", - "minimal", + os.fspath(binary), + "--prog", + "nvx.py", + "--root", + os.fspath(BuildConstants.REPO_ROOT), + "sandbox", + "exec", ], ) self.assertEqual( - install_targets.args[0], + nvx_rs._command(["artifacts"], None), [ - "rustup", - "target", - "add", - *OpenVMMBuildConstants.TEST_RUST_TARGETS[backend], - "--toolchain", - OpenVMMBuildConstants.RUST_TOOLCHAIN, + os.fspath(binary), + "--root", + os.fspath(BuildConstants.REPO_ROOT), + "artifacts", ], ) - for command in (install_targets, restore, tests): - self.assertIs(command.kwargs["env"], environment) - def test_openvmm_tests_install_missing_pinned_toolchain(self): - missing_toolchain = common.CommandResult( - args=("rustup", "target", "list"), - returncode=1, - stdout=b"", - stderr=( - f"error: toolchain '{OpenVMMBuildConstants.RUST_TOOLCHAIN}' " - "is not installed\n" - ).encode(), - ) - install_toolchain = [ - "rustup", - "toolchain", - "install", - OpenVMMBuildConstants.RUST_TOOLCHAIN, - "--profile", - "minimal", - ] - add_targets = [ - "rustup", - "target", - "add", - *OpenVMMBuildConstants.TEST_RUST_TARGETS["kvm"], - "--toolchain", - OpenVMMBuildConstants.RUST_TOOLCHAIN, - ] + def test_exit_statuses_pass_through(self): + for returncode, status in ((0, 0), (1, 1), (2, 2), (130, 130), (255, 255)): + with self.subTest(returncode=returncode): + self.assertEqual(nvx_rs.exit_status(returncode), status) + # A signal that ends nvx-rs gives the status that a shell reports. + self.assertEqual(nvx_rs.exit_status(-15), 143) + self.assertEqual(nvx_rs.exit_status(-9), 137) + if os.name == "nt": + # Windows statuses are 32-bit; SystemExit takes them signed. + self.assertEqual(nvx_rs.exit_status(0xC000013A), -0x3FFFFEC6) + def test_run_shares_the_standard_streams_and_passes_the_status_through(self): with tempfile.TemporaryDirectory() as temporary: - for runner_temp in (None, temporary): - with ( - self.subTest(runner_temp=runner_temp), - patch.object(ci, "run_capture", return_value=missing_toolchain), - patch.object(ci, "run_checked") as run_checked, - patch.dict( - os.environ, - {} if runner_temp is None else {"RUNNER_TEMP": runner_temp}, - clear=True, - ), - ): - environment = ci._prepare_openvmm_test_environment("kvm", "rustup") - - self.assertEqual( - [command.args[0] for command in run_checked.call_args_list], - [install_toolchain, add_targets], - ) - for command in run_checked.call_args_list: - self.assertIs(command.kwargs["env"], environment) - self.assertEqual( - environment["RUSTUP_TOOLCHAIN"], - OpenVMMBuildConstants.RUST_TOOLCHAIN, + fake = self._fake(Path(temporary)) + for status in (0, 1, 2, 7): + with self.subTest(status=status): + result = subprocess.run( + self._driver(fake, "streams", str(status)), + capture_output=True, + timeout=60, ) - if runner_temp is None: - self.assertNotIn("RUSTUP_HOME", environment) + self.assertEqual(result.returncode, status) + self.assertEqual(result.stdout, b"standard output\n") + self.assertEqual(result.stderr, b"standard error\n") + + @unittest.skipIf(os.name == "nt", "Windows has no POSIX signals") + def test_ctrl_c_ends_the_command_as_nvx_rs_ends_it(self): + # A terminal's Ctrl-C reaches the whole process group, and a SIGINT sent + # to nvx.py alone reaches nvx-rs through nvx.py. Either way nvx-rs + # prints Interrupted, and nvx.py neither prints it again nor changes + # the status. + for group in (True, False): + with ( + self.subTest(group=group), + tempfile.TemporaryDirectory() as temporary, + ): + fake = self._fake(Path(temporary)) + started = Path(temporary) / "started" + process = subprocess.Popen( + self._driver(fake, "interruptible", str(started)), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + start_new_session=True, + ) + try: + deadline = time.monotonic() + 30 + while not started.exists(): + self.assertLess(time.monotonic(), deadline) + self.assertIsNone(process.poll()) + time.sleep(0.02) + if group: + _signal_process_group(process.pid, "SIGINT") else: - self.assertEqual( - environment["RUSTUP_HOME"], - os.fspath( - Path(runner_temp) - / OpenVMMBuildConstants.RUSTUP_DIRECTORY_NAME - ), - ) + process.send_signal(signal.SIGINT) + stdout, stderr = process.communicate(timeout=30) + finally: + if process.poll() is None: + _signal_process_group(process.pid, "SIGKILL") + process.wait() + self.assertEqual(process.returncode, 130) + self.assertEqual(stderr, b"Interrupted\n") + self.assertEqual(stdout, b"") + + @unittest.skipIf(os.name == "nt", "Windows has no POSIX signals") + def test_signals_that_end_nvx_rs_give_shell_statuses(self): + with tempfile.TemporaryDirectory() as temporary: + fake = self._fake(Path(temporary)) + for name, status, stderr in ( + ("SIGTERM", 143, b""), + # SIGINT ended nvx-rs before it could print Interrupted. + ("SIGINT", 130, b"Interrupted\n"), + ): + with self.subTest(signal=name): + result = subprocess.run( + self._driver(fake, "signaled", name), + capture_output=True, + timeout=60, + ) + self.assertEqual(result.returncode, status) + self.assertEqual(result.stderr, stderr) - def test_openvmm_tests_define_each_backend_filter(self): - self.assertEqual( - set(ci.OPENVMM_TEST_FILTERS), - set(ci.OPENVMM_TEST_BACKENDS), - ) - self.assertEqual( - set(OpenVMMBuildConstants.TEST_RUST_TARGETS), - set(ci.OPENVMM_TEST_BACKENDS), - ) - self.assertEqual( - ci.OPENVMM_TEST_FILTERS["kvm"], - ci.OPENVMM_KVM_TEST_FILTER, - ) - self.assertIn( - "!test(no_vmbus_prepped_boot_no_vmbus_windows)", - ci.OPENVMM_KVM_TEST_FILTER, - ) - self.assertIn( - "!test(windows_datacenter_core_2022_x64)", - ci.OPENVMM_KVM_TEST_FILTER, - ) - self.assertIn("!test(virtio_net_windows)", ci.OPENVMM_KVM_TEST_FILTER) + def test_an_interrupted_wait_still_waits_for_nvx_rs(self): + process = MagicMock() + process.wait.side_effect = [KeyboardInterrupt, 130] + stderr = io.StringIO() + with ( + patch.object(nvx_rs, "_command", return_value=["nvx-rs", "sandbox"]), + patch("subprocess.Popen", return_value=process) as popen, + contextlib.redirect_stderr(stderr), + ): + self.assertEqual(nvx_rs.run(["sandbox"], program="nvx.py"), 130) + # The standard streams are shared with nvx-rs. + popen.assert_called_once_with(["nvx-rs", "sandbox"]) + self.assertEqual(process.wait.call_count, 2) + self.assertEqual(stderr.getvalue(), "") + if os.name != "nt": + process.send_signal.assert_called_once_with(signal.SIGINT) + + def test_signal_handlers_change_only_while_nvx_rs_runs(self): + before = signal.getsignal(signal.SIGINT) + during: list[object] = [] + process = MagicMock() + + def wait() -> int: + during.append(signal.getsignal(signal.SIGINT)) + return 0 + + process.wait.side_effect = wait + with ( + patch.object(nvx_rs, "_command", return_value=["nvx-rs"]), + patch("subprocess.Popen", return_value=process), + ): + self.assertEqual(nvx_rs.run([], program="nvx.py"), 0) + self.assertEqual(len(during), 1) + self.assertIsNot(during[0], before) + self.assertIs(signal.getsignal(signal.SIGINT), before) + + def test_unstartable_nvx_rs_is_an_error(self): + with ( + patch.object(nvx_rs, "_command", return_value=["nvx-rs"]), + patch("subprocess.Popen", side_effect=OSError("no such file")), + self.assertRaisesRegex( + common.ScriptError, "failed to run nvx-rs: no such file" + ), + ): + nvx_rs.run([], program="nvx.py") + + def test_queries_report_the_messages_of_nvx_rs(self): + for stderr, message in ( + (b"error: invalid egress policy\r\n", "invalid egress policy"), + (b"error: first\nsecond\n", "first\nsecond"), + (b"usage: nvx-rs\n", "usage: nvx-rs"), + (b"", "nvx-rs exited with status 1"), + ): + with ( + self.subTest(stderr=stderr), + patch.object(nvx_rs, "_command", return_value=["nvx-rs"]), + patch("subprocess.run", return_value=_completed(1, stderr=stderr)), + self.assertRaises(common.ScriptError) as raised, + ): + nvx_rs.query(["egress", "compile", "policy.json"]) + self.assertEqual(str(raised.exception), message) + with ( + patch.object(nvx_rs, "_command", return_value=["nvx-rs"]), + patch("subprocess.run", return_value=_completed(0, stdout=b"{")), + self.assertRaisesRegex(common.ScriptError, "printed invalid JSON"), + ): + nvx_rs.query(["artifacts"]) + + def test_egress_policies_compile_to_the_rules_that_nvx_rs_prints(self): + with patch.object( + nvx_rs, + "query", + return_value={"allow": ["192.0.2.0/24:tcp:80"], "deny": []}, + ) as query: + self.assertEqual( + nvx_rs.compile_egress_policy(Path("policy.json")), + nvx_rs.EgressRules(allow=("192.0.2.0/24:tcp:80",), deny=()), + ) + query.assert_called_once_with(("egress", "compile", "policy.json")) + invalid: tuple[object, ...] = ([], {"allow": []}, {"allow": [1], "deny": []}) + for document in invalid: + with ( + self.subTest(document=document), + patch.object(nvx_rs, "query", return_value=document), + self.assertRaises(common.ScriptError), + ): + nvx_rs.compile_egress_policy(Path("policy.json")) + + def test_artifacts_are_the_paths_that_nvx_rs_prints(self): + document = { + "guests": [ + { + "distribution": "Alpine Linux", + "distro_layer": None, + "initramfs": "/nvx/build/initramfs.cpio.gz", + "name": "alpine", + }, + { + "distribution": "Ubuntu Base", + "distro_layer": "/nvx/build/ubuntu-distro.erofs", + "initramfs": "/nvx/build/initramfs-ubuntu.cpio.gz", + "name": "ubuntu", + }, + ], + "kernel": "/nvx/build/vmlinux", + "layout": "repository", + "openvmm": "/nvx/openvmm/target/release/openvmm", + "root": "/nvx", + } + with patch.object(nvx_rs, "query", return_value=document) as query: + paths = nvx_rs.artifacts() + query.assert_called_once_with(("artifacts",)) + self.assertEqual(paths.openvmm, Path("/nvx/openvmm/target/release/openvmm")) + self.assertEqual(paths.kernel, Path("/nvx/build/vmlinux")) self.assertEqual( - set(ci.OPENVMM_KVM_EXCLUDED_TESTS), - { - "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_boot_no_agent", - "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_iso_boot_no_agent", - "multiarch::openvmm_pcat_x64_ubuntu_2404_server_x64_boot", - "multiarch::openvmm_pcat_x64_ubuntu_2504_server_x64_boot", - "multiarch::openvmm_pcat_x64_ubuntu_2504_server_x64_boot_heavy", - }, + paths.guest("ubuntu"), + nvx_rs.GuestArtifacts( + "ubuntu", "Ubuntu Base", Path("/nvx/build/initramfs-ubuntu.cpio.gz") + ), ) - for excluded_test in ci.OPENVMM_KVM_EXCLUDED_TESTS: - self.assertIn( - f"!{ci._exact_openvmm_test(excluded_test)}", - ci.OPENVMM_KVM_TEST_FILTER, + with self.assertRaisesRegex(common.ScriptError, "no artifacts of the x"): + paths.guest("x") + for broken in ( + {**document, "guests": None}, + {**document, "kernel": 1}, + {**document, "guests": [{"name": "alpine"}]}, + ): + with ( + self.subTest(broken=broken), + patch.object(nvx_rs, "query", return_value=broken), + self.assertRaises(common.ScriptError), + ): + nvx_rs.artifacts() + + @unittest.skipUnless( + os.environ.get("NVX_RS"), "set NVX_RS to an nvx-rs executable to run" + ) + def test_the_real_nvx_rs_prints_what_nvx_py_reads(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + (root / "build").mkdir() + policy = root / "policy.json" + policy.write_text( + json.dumps( + { + "allow": [ + {"cidr": "192.0.2.0/24", "except": ["192.0.2.128/25"]} + ], + "deny": [{"cidr": "198.51.100.0/24", "protocol": "icmp"}], + } + ), + encoding="utf-8", ) - self.assertNotIn("!test(openvmm_pcat_x64)", ci.OPENVMM_KVM_TEST_FILTER) - self.assertNotIn( - "openvmm_linux_x64_apicid_offset", - ci.OPENVMM_KVM_TEST_FILTER, - ) - self.assertNotIn( - "openvmm_linux_x64_legacy_xapic", - ci.OPENVMM_KVM_TEST_FILTER, + with patch.object(nvx_rs, "ROOT", root), patch.dict(os.environ): + os.environ.pop("NVX_OPENVMM", None) + self.assertEqual( + nvx_rs.compile_egress_policy(policy), + nvx_rs.EgressRules( + allow=("192.0.2.0/25",), deny=("198.51.100.0/24:icmp",) + ), + ) + policy.write_text("[]", encoding="utf-8") + with self.assertRaisesRegex( + common.ScriptError, "^egress policy must be an object$" + ): + nvx_rs.compile_egress_policy(policy) + paths = nvx_rs.artifacts() + self.assertEqual(paths.kernel, root / "build" / "vmlinux") + self.assertEqual( + paths.openvmm, + root + / "openvmm" + / "target" + / "release" + / ("openvmm.exe" if os.name == "nt" else "openvmm"), + ) + self.assertEqual( + [ + (guest.name, guest.distribution, guest.initramfs) + for guest in paths.guests + ], + [ + ( + name, + guests.guest_descriptor(name).distribution, + root / "build" / guests.guest_descriptor(name).initramfs_name, + ) + for name in guests.GUEST_NAMES + ], + ) + + +class SandboxDelegationTests(unittest.TestCase): + def test_sandbox_records_canonical_options_in_command_line_order(self): + args = nvx.parse_args( + [ + "sandbox", + "--layer", + "distro,a.erofs,11111111-1111-1111-1111-111111111111", + "--scratch=s.ext4", + "--mount", + "/a,dir-a", + "--mount-deny", + "x", + "--mount=/b,dir-b,rw", + "--mount-write", + "y", + "provision", + "--state", + "state", + "--arg=-c", + "--arg", + "true", + "--environment", + "A=B=C", + "--inherit", + "--dry", + "--hyp", + "kvm", + "--cmdline=", + "--timeout", + "-1", + ] ) + self.assertEqual(args.sandbox_operation, "provision") + # Abbreviations are resolved, the order is kept, and every value is + # attached, so that a value that starts with "-" stays a value. self.assertEqual( - ci.OPENVMM_TEST_FILTERS["mshv"], - ci.OPENVMM_MSHV_TEST_FILTER, + args.sandbox_arguments, + [ + "--layer=distro,a.erofs,11111111-1111-1111-1111-111111111111", + "--scratch=s.ext4", + "--mount=/a,dir-a", + "--mount-deny=x", + "--mount=/b,dir-b,rw", + "--mount-write=y", + "--state-dir=state", + "--arg=-c", + "--arg=true", + "--environment=A=B=C", + "--inherit-default-environment", + "--dry-run", + "--hypervisor=kvm", + "--cmdline=", + "--timeout=-1", + ], ) - self.assertIn( - "!test(windows_datacenter_core_2022_x64)", - ci.OPENVMM_MSHV_TEST_FILTER, - ) - self.assertEqual( - set(ci.OPENVMM_MSHV_EXCLUDED_TESTS), - { - "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_boot_no_agent", - "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_iso_boot_no_agent", - "multiarch::openvmm_pcat_x64_ubuntu_2404_server_x64_boot", - "multiarch::openvmm_pcat_x64_ubuntu_2504_server_x64_boot", - "multiarch::openvmm_pcat_x64_ubuntu_2504_server_x64_boot_heavy", - "multiarch::pcie::openvmm_linux_x64_pcie_save_restore", - }, - ) - self.assertNotIn( - "x86_64::openvmm_linux_x64_virtio_blk_device", - ci.OPENVMM_MSHV_EXCLUDED_TESTS, - ) - for excluded_test in ci.OPENVMM_MSHV_EXCLUDED_TESTS: - self.assertIn( - f"!{ci._exact_openvmm_test(excluded_test)}", - ci.OPENVMM_MSHV_TEST_FILTER, - ) - self.assertNotIn("!test(openvmm_pcat_x64)", ci.OPENVMM_MSHV_TEST_FILTER) - for backend, test_filter in ci.OPENVMM_TEST_FILTERS.items(): - for required_test in ci.OPENVMM_REQUIRED_MICROVM_TESTS: - with self.subTest(backend=backend, required_test=required_test): - self.assertIn( - ci._exact_openvmm_test(required_test), - test_filter, - ) - self.assertEqual(len(ci.OPENVMM_WHP_TESTS), 29) - self.assertEqual( - len(set(ci.OPENVMM_WHP_TESTS)), - len(ci.OPENVMM_WHP_TESTS), - ) - for required_test in ci.OPENVMM_REQUIRED_MICROVM_TESTS: - self.assertIn(required_test, ci.OPENVMM_WHP_TESTS) - # Only Linux hosts can build the Linux pipette this TTRPC test boots. - self.assertNotIn("test_ttrpc_interface", ci.OPENVMM_TEST_FILTERS["whp"]) - for backend in ("kvm", "mshv"): - with self.subTest(backend=backend): - self.assertIn("test(ttrpc)", ci.OPENVMM_TEST_FILTERS[backend]) - self.assertNotIn( - "test_ttrpc_interface", - ci.OPENVMM_TEST_FILTERS[backend], - ) - self.assertEqual(ci.OPENVMM_WHP_EXCLUDED_TESTS, ()) - self.assertNotIn( - "multiarch::openvmm_pcat_x64_windows_datacenter_core_2022_x64_boot_heavy", - ci.OPENVMM_WHP_EXCLUDED_TESTS, - ) - self.assertNotIn( - "multiarch::openvmm_pcat_x64_windows_datacenter_core_2022_x64_boot", - ci.OPENVMM_WHP_EXCLUDED_TESTS, - ) - self.assertNotIn( - "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_iso_boot_no_agent", - ci.OPENVMM_WHP_EXCLUDED_TESTS, - ) - self.assertNotIn( - "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_boot_no_agent", - ci.OPENVMM_WHP_EXCLUDED_TESTS, - ) - self.assertNotIn( - "multiarch::pcie::openvmm_uefi_x64_windows_datacenter_core_2022_x64_pcie_nvme_boot", - ci.OPENVMM_WHP_EXCLUDED_TESTS, - ) - self.assertTrue( - set(ci.OPENVMM_WHP_EXCLUDED_TESTS).issubset(ci.OPENVMM_WHP_TESTS) - ) - self.assertEqual( - ci.OPENVMM_TEST_FILTERS["whp"], - ci._join_openvmm_tests( - ci.OPENVMM_WHP_TESTS, - ci.OPENVMM_WHP_EXCLUDED_TESTS, - ), - ) - self.assertEqual( - ci._join_openvmm_tests( - ("suite::boot", "suite::boot_heavy"), - ("suite::boot_heavy",), - ), - ( - "(test(/^suite::boot$/) | test(/^suite::boot_heavy$/))" - " & !test(/^suite::boot_heavy$/)" - ), + # nvx-rs converts, checks, and defaults every value. + for name in ("layer", "scratch", "workload_user", "timeout", "memory_mib"): + self.assertFalse(hasattr(args, name), name) + default = nvx.parse_args(["sandbox"]) + self.assertEqual(default.sandbox_operation, "run") + self.assertEqual(default.sandbox_arguments, []) + + def test_sandbox_runs_nvx_rs_and_exits_with_its_status(self): + program = argparse.ArgumentParser().prog + with patch.object(nvx_rs, "run", return_value=7) as run: + self.assertEqual( + nvx.main( + [ + "sandbox", + "exec", + "--state-dir", + "state", + "--entrypoint", + "/bin/true", + ] + ), + 7, + ) + run.assert_called_once_with( + ["sandbox", "exec", "--state-dir=state", "--entrypoint=/bin/true"], + program=program, ) - def test_openvmm_tests_reject_unknown_backend(self): - with self.assertRaisesRegex(common.ScriptError, "unsupported.*backend"): - ci.run_openvmm_tests("unknown") + def test_sandbox_usage_errors_stay_with_argparse(self): + for arguments in ( + ["sandbox", "--bogus"], + ["sandbox", "--hypervisor", "hyper-v"], + ["sandbox", "pause"], + ["sandbox", "--layer"], + ): + with ( + self.subTest(arguments=arguments), + patch.object(nvx_rs, "run") as run, + patch("sys.stderr", new_callable=io.StringIO), + self.assertRaises(SystemExit) as raised, + ): + nvx.main(arguments) + self.assertEqual(raised.exception.code, 2) + run.assert_not_called() + def test_sandbox_reports_a_missing_nvx_rs_as_an_error(self): + stderr = io.StringIO() + with ( + patch.object( + nvx_rs, "require_binary", side_effect=common.ScriptError("no nvx-rs") + ), + contextlib.redirect_stderr(stderr), + ): + self.assertEqual(nvx.main(["sandbox", "stop", "--state-dir", "s"]), 1) + self.assertEqual(stderr.getvalue(), "error: no nvx-rs\n") + + def _main_driver(self, fake: Path, mode: list[str], arguments: list[str]): + return [ + sys.executable, + "-c", + NVX_PY_DRIVER, + str(SCRIPTS_DIRECTORY), + str(fake), + *mode, + "--", + *arguments, + ] -class CiConfigurationTests(unittest.TestCase): - def test_ci_needs_reference_declared_jobs(self): - workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" - ).read_text(encoding="utf-8") - jobs = set( - re.findall( - r"^ ([A-Za-z0-9_-]+):$", - workflow.split("jobs:\n", 1)[1], - re.MULTILINE, + def test_sandbox_shares_the_standard_streams_and_status_of_nvx_rs(self): + with tempfile.TemporaryDirectory() as temporary: + fake = Path(temporary) / "fake_nvx_rs.py" + fake.write_text(FAKE_NVX_RS, encoding="utf-8") + result = subprocess.run( + self._main_driver( + fake, + ["streams", "3"], + ["sandbox", "exec", "--state-dir", "state"], + ), + capture_output=True, + timeout=60, ) - ) - for job_name in sorted(jobs): - job = _workflow_job(workflow, job_name) - match = re.search(r"^ needs:(.*)$", job, re.MULTILINE) - if match is None: - continue - inline = match.group(1).strip() - if inline: - dependencies = {name.strip() for name in inline.strip("[]").split(",")} - else: - dependencies = set( - re.findall( - r"^ - ([A-Za-z0-9_-]+)$", - job.split(" if:", 1)[0], - re.MULTILINE, - ) - ) - with self.subTest(job=job_name): - self.assertLessEqual( - dependencies, - jobs, - f"undefined job dependencies: {dependencies - jobs}", + self.assertEqual(result.returncode, 3) + self.assertEqual(result.stdout, b"standard output\n") + self.assertEqual(result.stderr, b"standard error\n") + + @unittest.skipIf(os.name == "nt", "Windows has no POSIX signals") + def test_ctrl_c_ends_a_sandbox_command_once(self): + # nvx-rs reports Ctrl-C; nvx.py prints nothing more and keeps 130. + with tempfile.TemporaryDirectory() as temporary: + fake = Path(temporary) / "fake_nvx_rs.py" + fake.write_text(FAKE_NVX_RS, encoding="utf-8") + started = Path(temporary) / "started" + process = subprocess.Popen( + self._main_driver( + fake, + ["interruptible", str(started)], + ["sandbox", "exec", "--state-dir", "state"], + ), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + start_new_session=True, + ) + try: + deadline = time.monotonic() + 30 + while not started.exists(): + self.assertLess(time.monotonic(), deadline) + self.assertIsNone(process.poll()) + time.sleep(0.02) + _signal_process_group(process.pid, "SIGINT") + stdout, stderr = process.communicate(timeout=30) + finally: + if process.poll() is None: + _signal_process_group(process.pid, "SIGKILL") + process.wait() + self.assertEqual(process.returncode, 130) + self.assertEqual(stderr, b"Interrupted\n") + self.assertEqual(stdout, b"") + + @unittest.skipUnless( + os.environ.get("NVX_RS"), "set NVX_RS to an nvx-rs executable to run" + ) + def test_the_real_nvx_rs_runs_the_options_that_nvx_py_reads(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + openvmm = ( + f"openvmm/target/release/openvmm{'.exe' if os.name == 'nt' else ''}" + ) + for relative in ("build/vmlinux", "build/initramfs.cpio.gz", openvmm): + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"artifact") + (root / "distro.erofs").write_bytes(b"layer") + (root / "scratch.ext4").write_bytes(b"scratch") + (root / "share" / "hidden").mkdir(parents=True) + uuid = "11111111-1111-1111-1111-111111111111" + + def nvx_py(*arguments: str) -> subprocess.CompletedProcess[bytes]: + return subprocess.run( + [ + sys.executable, + "-c", + NVX_PY_REAL_DRIVER, + str(SCRIPTS_DIRECTORY), + str(root), + *arguments, + ], + cwd=root, + capture_output=True, + timeout=60, ) - required = _workflow_job(workflow, "required-status-check") - for job in ("aci-edge-sandboxes", "nvx-rs"): - with self.subTest(job=job): - self.assertIn(f" - {job}\n", required) - self.assertIn(f"${{{{ needs.{job}.result }}}}", required) - def test_development_release_requires_successful_crate_checks(self): - workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" - ).read_text(encoding="utf-8") - release_job = _workflow_job(workflow, "release") - predicate = release_job.split(" if:", 1)[1].split(" runs-on:", 1)[0] - for job in ("aci-edge-sandboxes", "nvx-rs"): - with self.subTest(job=job): - self.assertIn(f" - {job}\n", release_job) - self.assertIn(f"needs.{job}.result == 'success' &&", predicate) - self.assertNotIn(f"needs.{job}.result == 'skipped'", predicate) + result = nvx_py( + "sandbox", + "--dry", + "--layer", + f"distro,distro.erofs,{uuid}", + "--scratch", + "scratch.ext4", + "--arg=-c", + "--arg", + "true", + "--mount", + f"/data,{root / 'share'},rw", + "--mount-deny", + "hidden", + "--hyp", + "kvm", + "--memory-max", + "268435456", + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, b"") + command = result.stdout.decode("utf-8") + self.assertTrue(command.startswith(">> "), command) + for expected in ( + "--microvm-sandbox-block distro:file:distro.erofs,ro", + "--microvm-sandbox-block scratch:file:scratch.ext4", + "--hypervisor kvm", + "nvx_arg=-c nvx_arg=true nvx_memory_max=268435456", + "--mount-deny hidden", + ): + self.assertIn(expected, command) + # A value that nvx-rs refuses is a usage error of nvx.py sandbox. + result = nvx_py("sandbox", "--workload-user", "0:0") + self.assertEqual(result.returncode, 2) + stderr = result.stderr.decode("utf-8").replace("\r\n", "\n") + self.assertTrue(stderr.startswith("usage: nvx.py sandbox [-h] "), stderr) + self.assertTrue( + stderr.endswith( + "nvx.py sandbox: error: argument --workload-user: " + "--workload-user UID must be between 1 and 4294967295\n" + ), + stderr, + ) + result = nvx_py("sandbox", "exec", "--state-dir", "state") + self.assertEqual(result.returncode, 1) + self.assertEqual( + result.stderr.decode("utf-8").replace("\r\n", "\n"), + f"error: sandbox state path is not a plain directory: " + f"{root / 'state'}\n", + ) - def test_crate_checks_run_on_every_os(self): - workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" - ).read_text(encoding="utf-8") - for job, action in ( - ("aci-edge-sandboxes", "check-aci-edge-sandboxes"), - ("nvx-rs", "check-nvx-rs"), - ): - with self.subTest(job=job): - definition = _workflow_job(workflow, job) - self.assertIn("os: [ubuntu-latest, windows-latest]", definition) - self.assertIn(f"uses: ./.github/actions/{action}", definition) - self.assertEqual(ci.REQUIRED_CI_RESULT_ENVIRONMENTS["nvx-rs"], "NVX_RS_RESULT") - def test_nvx_rs_builds_and_checks_every_guest_binary_for_musl(self): - crate = BuildConstants.REPO_ROOT / "nvx-rs" - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "check-nvx-rs" - / "action.yml" - ).read_text(encoding="utf-8") - manifest = (crate / "Cargo.toml").read_text(encoding="utf-8") - guest_binaries = set( - re.findall( - r'^\[\[bin\]\]\nname = "([^"]+)"\npath = "[^"]+"\n' - r'required-features = \["guest"\]$', - manifest, - re.MULTILINE, +class CiTests(unittest.TestCase): + def test_openvmm_unit_tests_run_nextest_and_doctests(self): + with tempfile.TemporaryDirectory() as temporary: + openvmm = Path(temporary) / "openvmm" + openvmm.mkdir() + (openvmm / "Cargo.toml").touch() + fuzz_crates = common.CommandResult( + args=("cargo", "xtask", "fuzz", "list", "--crates"), + returncode=0, + stdout=b"fuzz_alpha\nfuzz_beta\n", + stderr=b"", ) - ) - self.assertIn("nvx-exit", guest_binaries) - lint_and_test = "Lint and test the guest binaries" - build_and_check = "Build and check the guest binaries" - check = _composite_action_step(action, lint_and_test) - build = _composite_action_step(action, build_and_check) - for step in (check, build): - self.assertIn("if: runner.os == 'Linux'", step) - # nvx.py owns the Cargo and rustup commands, which developers reuse. - self.assertNotIn("cargo ", step) - self.assertNotIn("rustup", step) - self.assertIn( - "python3 scripts/nvx.py check-guest-binaries " - '--toolchain "${{ steps.toolchains.outputs.toolchain }}"', - " ".join(check.split()), - ) - # The check adds the musl target that the build needs. - self.assertLess( - action.index(f"- name: {lint_and_test}"), - action.index(f"- name: {build_and_check}"), - ) - self.assertEqual(set(GuestRustBuildConstants.BINARIES), guest_binaries) - for name in guest_binaries: - module = name.removeprefix("nvx-").replace("-", "_") - root = (crate / "src" / "bin" / f"{name}.rs").read_text(encoding="utf-8") - with self.subTest(binary=name): - # Every guest binary runs the main of its nvx::guest module. - self.assertIn(f"nvx::guest::{module}::main()", root) - self.assertIn( - 'python3 ../scripts/nvx.py build-guest-binaries --output "${directory}"', - build, - ) - self.assertIn( - 'python3 ../scripts/nvx.py test-guest-binaries --binaries "${directory}"', - build, - ) - - def test_required_ci_result_policy(self): - always_successful = { - "quality", - "aci-edge-sandboxes", - "nvx-rs", - "openvmm-changes", - } - # Jobs that succeed whenever the repository's jobs run. - repository = always_successful | set(ci.REQUIRED_CI_CLI_JOBS) - builds = set(ci.REQUIRED_CI_BUILD_JOBS) - openvmm_tests = set(ci.REQUIRED_CI_OPENVMM_TEST_JOBS) - openvmm_artifact_tests = set(ci.REQUIRED_CI_OPENVMM_ARTIFACT_TEST_JOBS) - microvm_tests = set(ci.REQUIRED_CI_MICROVM_TEST_JOBS) - debug_kernel = set(ci.REQUIRED_CI_MICROVM_DEBUG_JOBS) - debug_kernel_push = set(ci.REQUIRED_CI_MICROVM_DEBUG_PUSH_JOBS) - artifacts = {ci.REQUIRED_CI_ARTIFACT_JOB, ci.REQUIRED_CI_DEBUG_KERNEL_JOB} - platforms = set(ci.REQUIRED_CI_PLATFORM_JOBS) - workloads = artifacts | builds | platforms - microvm = microvm_tests | debug_kernel - # Event, same repository, the change detector's outputs (tests, - # workloads, OpenVMM unit tests, OpenVMM VMM tests), and the jobs that - # must succeed; every other required job must be skipped. - cases = ( - ("pull_request", True, (False, False, False, False), repository), - ( - "pull_request", - True, - (False, True, False, False), - repository | workloads | {"performance-gate"}, - ), - ( - "pull_request", - True, - (True, False, True, True), - repository | builds | openvmm_tests, - ), - ( - "pull_request", - True, - (True, True, True, True), - repository - | workloads - | openvmm_tests - | openvmm_artifact_tests - | microvm - | {"performance-gate"}, - ), - # A pull request that changes no input of either OpenVMM suite. - ( - "pull_request", - True, - (True, True, False, False), - repository | workloads | microvm | {"performance-gate"}, - ), - # Guest inputs reach the VMM tests but not the unit tests. - ( - "pull_request", - True, - (True, True, False, True), - repository - | workloads - | openvmm_artifact_tests - | microvm - | {"performance-gate"}, - ), - ("pull_request", False, (False, False, False, False), always_successful), - ( - "pull_request", - False, - (False, True, False, False), - always_successful | artifacts, - ), - ("pull_request", False, (True, False, True, True), always_successful), - ( - "pull_request", - False, - (True, True, True, True), - always_successful | artifacts, - ), - ("push", True, (False, False, False, False), repository), - ("push", True, (False, True, False, False), repository | workloads), - ( - "push", - True, - (True, False, True, True), - repository | builds | openvmm_tests, - ), - ( - "push", - True, - (True, True, True, True), - repository - | workloads - | openvmm_tests - | openvmm_artifact_tests - | microvm - | debug_kernel_push, - ), - ) - for event_name, same_repository, flags, successful_jobs in cases: - changes = ci.CiChanges(*flags) - with self.subTest( - event_name=event_name, - same_repository=same_repository, - changes=changes, + with ( + patch.object(OpenVMMBuildConstants, "DIRECTORY", openvmm), + patch.object(ci, "require_tool", return_value="cargo"), + patch.object(ci, "run_capture", return_value=fuzz_crates) as capture, + patch.object(ci, "run_checked") as run_checked, ): - expected = ci.required_ci_expected_results( - event_name, - same_repository=same_repository, - changes=changes, - ) - self.assertEqual(set(expected), set(ci.REQUIRED_CI_RESULT_ENVIRONMENTS)) - self.assertEqual( - {job for job, result in expected.items() if result == "success"}, - successful_jobs, - ) - self.assertEqual( - ci.required_ci_failures( - event_name, - same_repository=same_repository, - changes=changes, - results=expected, - ), - [], - ) + ci.run_openvmm_unit_tests() - for job, expected_result in expected.items(): - unexpected = expected.copy() - unexpected[job] = ( - "skipped" if expected_result == "success" else "success" - ) - with self.subTest(job=job, actual=unexpected[job]): - self.assertEqual( - ci.required_ci_failures( - event_name, - same_repository=same_repository, - changes=changes, - results=unexpected, - ), - [ - f"{job}: expected {expected_result}, " - f"got {unexpected[job]}" - ], - ) + capture.assert_called_once_with( + ["cargo", "xtask", "fuzz", "list", "--crates"], + cwd=openvmm, + ) + self.assertEqual(run_checked.call_count, 3) + self.assertEqual( + run_checked.call_args_list[0], + call( + [ + "cargo", + "xflowey", + "restore-packages", + "--no-compat-igvm", + ], + cwd=openvmm, + ), + ) + command = run_checked.call_args_list[1].args[0] + self.assertEqual( + command[:10], + [ + "cargo", + "nextest", + "run", + "--profile", + "agent", + "--workspace", + "--tests", + "--bins", + "--features", + "ci", + ], + ) + excluded_packages = [ + command[index + 1] + for index, argument in enumerate(command) + if argument == "--exclude" + ] + self.assertEqual( + excluded_packages, + [ + *ci.OPENVMM_UNIT_TEST_EXCLUDED_PACKAGES, + "fuzz_alpha", + "fuzz_beta", + ], + ) + self.assertEqual(run_checked.call_args_list[1].kwargs["cwd"], openvmm) + self.assertEqual( + run_checked.call_args_list[2], + call( + [ + "cargo", + "test", + "--locked", + "--doc", + "--workspace", + "--no-fail-fast", + ], + cwd=openvmm, + ), + ) - for job in expected: - failed = expected.copy() - failed[job] = "failure" - with self.subTest(job=job, actual="failure"): - self.assertEqual( - ci.required_ci_failures( - event_name, - same_repository=same_repository, - changes=changes, - results=failed, - ), - [f"{job}: expected {expected[job]}, got failure"], - ) + def test_openvmm_unit_tests_stop_when_fuzz_crate_query_fails(self): + with tempfile.TemporaryDirectory() as temporary: + openvmm = Path(temporary) / "openvmm" + openvmm.mkdir() + (openvmm / "Cargo.toml").touch() + failed_query = common.CommandResult( + args=("cargo", "xtask", "fuzz", "list", "--crates"), + returncode=2, + stdout=b"", + stderr=b"query failed", + ) - def test_required_ci_job_uses_tested_policy(self): - workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" - ).read_text(encoding="utf-8") - job = _workflow_job(workflow, "required-status-check") + with ( + patch.object(OpenVMMBuildConstants, "DIRECTORY", openvmm), + patch.object(ci, "require_tool", return_value="cargo"), + patch.object(ci, "run_capture", return_value=failed_query), + patch.object(ci, "run_checked") as run_checked, + self.assertRaisesRegex( + common.ScriptError, + "OpenVMM fuzz crate query exited 2", + ), + ): + ci.run_openvmm_unit_tests() - self.assertIn("python3 scripts/nvx.py check-required-ci", job) - self.assertIn('--same-repository "${SAME_REPOSITORY}"', job) - self.assertIn( - "github.event.pull_request.head.repo.full_name == github.repository", - job, - ) - self.assertLess( - job.index("uses: actions/checkout@v5"), - job.index("python3 scripts/nvx.py check-required-ci"), - ) - self.assertIn("persist-credentials: false", job) - for environment in ci.REQUIRED_CI_RESULT_ENVIRONMENTS.values(): - with self.subTest(environment=environment): - self.assertIn(f" {environment}:", job) - # Every suite flag reaches the policy as the detector reported it. - detector = _workflow_job(workflow, "openvmm-changes") - for output in ci.CI_CHANGE_OUTPUTS: - environment = output.upper().replace("-", "_") - with self.subTest(output=output): - self.assertIn( - f" {output}: ${{{{ steps.changes.outputs.{output} }}}}", - detector, + run_checked.assert_not_called() + + def test_openvmm_tests_use_nvx_guest_artifacts(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + openvmm = root / "openvmm" + openvmm.mkdir() + (openvmm / "Cargo.toml").touch() + kernel = root / "vmlinux" + initrd = root / "initramfs.cpio.gz" + kernel.touch() + initrd.touch() + backend = "whp" if os.name == "nt" else "kvm" + installed_targets = common.CommandResult( + args=("rustup", "target", "list"), + returncode=0, + stdout=( + "\n".join(OpenVMMBuildConstants.TEST_RUST_TARGETS[backend]) + "\n" + ).encode(), + stderr=b"", + ) + + with ( + patch.object(OpenVMMBuildConstants, "DIRECTORY", openvmm), + patch.object( + ci, + "artifact_path", + side_effect=[kernel, initrd], + ), + patch.object(ci.os, "access", return_value=True), + patch.object(ci.Path, "exists", return_value=False), + patch.object(ci, "require_tool", side_effect=["cargo", "rustup"]), + patch.object( + ci, + "run_capture", + return_value=installed_targets, + ) as run_capture, + patch.object(ci, "run_checked") as run_checked, + patch.dict( + os.environ, + { + "PETRI_CAPABILITIES": "vpci", + "RUNNER_TEMP": os.fspath(root), + }, + ), + ): + ci.run_openvmm_tests(backend) + + run_capture.assert_called_once_with( + [ + "rustup", + "target", + "list", + "--installed", + "--toolchain", + OpenVMMBuildConstants.RUST_TOOLCHAIN, + ] + ) + self.assertEqual(run_checked.call_count, 2) + restore, tests = run_checked.call_args_list + self.assertEqual( + restore.args[0], + ["cargo", "xflowey", "restore-packages", "--no-compat-igvm"], + ) + environment = restore.kwargs["env"] + self.assertEqual( + environment["RUSTUP_TOOLCHAIN"], + OpenVMMBuildConstants.RUST_TOOLCHAIN, + ) + if os.name == "nt": + self.assertNotIn("XDG_CACHE_HOME", environment) + else: + self.assertEqual( + environment["XDG_CACHE_HOME"], + os.fspath(root / "openvmm-cache"), ) - self.assertIn( - f" {environment}: " - f"${{{{ needs.openvmm-changes.outputs.{output} }}}}", - job, + command = tests.args[0] + self.assertEqual(command[:3], ["cargo", "xflowey", "vmm-tests-run"]) + filter_index = command.index("--filter") + self.assertEqual( + command[filter_index + 1], + ci.OPENVMM_TEST_FILTERS[backend], + ) + self.assertEqual(tests.kwargs["cwd"], openvmm) + self.assertIs(tests.kwargs["env"], environment) + self.assertEqual( + environment["OPENVMM_MICROVM_TEST_KERNEL"], + os.fspath(kernel.resolve()), + ) + self.assertEqual( + environment["OPENVMM_MICROVM_TEST_INITRD"], + os.fspath(initrd.resolve()), + ) + self.assertEqual(environment["PETRI_CAPABILITIES"], "vpci") + if os.name == "nt": + self.assertEqual( + command[command.index("--dir") + 1], + os.fspath(root / backend), ) - self.assertIn(f'--{output} "${{{environment}}}"', job) - def test_ci_change_classification_selects_suites_by_input(self): - # The pull-request outputs (tests, workloads, OpenVMM unit tests, - # OpenVMM VMM tests) for each class of changed path. - cases = ( - ((), (False, True, False, False)), - (("doc/ci.md", "README.md", "DOC/Guide.MD"), (False, False, False, False)), - (("openvmm",), (True, True, True, True)), - (("rust-toolchain.toml",), (True, True, True, True)), - (("scripts/setup/setup-windows-whp.ps1",), (True, True, True, True)), - (("scripts/setup/README.md",), (True, False, True, True)), - ((".github/workflows/ci.yml",), (True, True, True, True)), - ((".github/actions/validate-runner/action.yml",), (True, True, True, True)), - ((".github/actions/sccache/action.yml",), (True, True, True, True)), - ((".github/actions/setup-curl/curl-shim.rs",), (True, True, True, True)), - (("scripts/nvx.py",), (True, True, True, True)), - (("scripts/nvx_tools/ci.py",), (True, True, True, True)), - ((".dockerignore",), (True, True, False, True)), - (("scripts/nvx_tools/common.py",), (True, True, True, True)), - (("scripts/nvx_tools/build.py",), (True, True, False, True)), - (("scripts/nvx_tools/guests.py",), (True, True, False, True)), - (("kernel/config-microvm",), (True, True, False, True)), - (("kernel/patches/0001-example.patch",), (True, True, False, True)), - (("guest/common/init",), (True, True, False, True)), - (("guest/alpine/nvx-container-enter",), (True, True, False, True)), - (("SOURCE-MANIFEST.json",), (True, True, False, True)), - (("docker/Dockerfile",), (True, True, False, True)), - ( - (".github/actions/build-guest-artifacts/action.yml",), - (True, True, False, True), - ), - (("guest/ubuntu/nvx-bashrc",), (True, True, False, False)), - (("ubuntu/packages.lock.json",), (True, True, False, False)), - (("azurelinux/packages.lock.json",), (True, True, False, False)), - (("aci_edge_sandboxes/src/lib.rs",), (True, True, False, False)), - (("nvx-rs/src/lib.rs",), (True, True, False, False)), - (("nvx-rs/Cargo.lock",), (True, True, False, False)), - (("scripts/nvx_tools/microvm_tests.py",), (True, True, False, False)), - ( - (".github/workflows/run-nvx-microvm-tests.yml",), - (True, True, False, False), - ), - ((".github/actions/build-openvmm/action.yml",), (True, True, False, False)), - ((".github/actions/validate-nvx/action.yml",), (False, True, False, False)), - ( - (".github/actions/run-benchmark/action.yml",), - (False, True, False, False), - ), - (("scripts/test_nvx_tools.py",), (False, True, False, False)), - (("data/linux-kvm-virtual-machine.csv",), (False, True, False, False)), - ( - ("scripts/nvx_tools/microvm_tests.py", "kernel/config-microvm"), - (True, True, False, True), - ), + def test_openvmm_tests_pin_toolchain_without_runner_temp(self): + installed_targets = common.CommandResult( + args=("rustup", "target", "list"), + returncode=0, + stdout=( + "\n".join(OpenVMMBuildConstants.TEST_RUST_TARGETS["whp"]) + "\n" + ).encode(), + stderr=b"", ) - for changed_files, flags in cases: - with self.subTest(changed_files=changed_files): - pull_request = ci.classify_ci_changes("pull_request", changed_files) - self.assertEqual(pull_request, ci.CiChanges(*flags)) - # Pushes keep running both OpenVMM suites with the tests. - run_tests, run_workloads = flags[:2] - self.assertEqual( - ci.classify_ci_changes("push", changed_files), - ci.CiChanges(run_tests, run_workloads, run_tests, run_tests), - ) + + with ( + patch.object(ci, "run_capture", return_value=installed_targets), + patch.object(ci, "run_checked") as run_checked, + patch.dict(os.environ, {}, clear=True), + ): + environment = ci._prepare_openvmm_test_environment("whp", "rustup") + self.assertEqual( - ci.classify_ci_changes("pull_request", ["openvmm", ""]), - ci.classify_ci_changes("pull_request", ["openvmm"]), + environment, + {"RUSTUP_TOOLCHAIN": OpenVMMBuildConstants.RUST_TOOLCHAIN}, ) - with self.assertRaisesRegex(ValueError, "unsupported CI event"): - ci.classify_ci_changes("workflow_dispatch", ["openvmm"]) - - def test_ci_change_classification_covers_each_suite_input(self): - github = BuildConstants.REPO_ROOT / ".github" - tools = BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" - workflow = (github / "workflows" / "ci.yml").read_text(encoding="utf-8") + run_checked.assert_not_called() - def local_actions(configuration: str) -> set[str]: - names = set( - re.findall(r"uses: \./\.github/actions/([a-z0-9-]+)", configuration) + def test_openvmm_tests_prepare_job_local_toolchain_for_missing_targets(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + openvmm = root / "openvmm" + openvmm.mkdir() + (openvmm / "Cargo.toml").touch() + kernel = root / "vmlinux" + initrd = root / "initramfs.cpio.gz" + kernel.touch() + initrd.touch() + backend = "whp" if os.name == "nt" else "kvm" + installed_targets = common.CommandResult( + args=("rustup", "target", "list"), + returncode=0, + stdout=f"{OpenVMMBuildConstants.GUEST_RUST_TARGET}\n".encode(), + stderr=b"", ) - pending = list(names) - while pending: - action = (github / "actions" / pending.pop() / "action.yml").read_text( - encoding="utf-8" - ) - for name in local_actions(action) - names: - names.add(name) - pending.append(name) - return names - - def imported_modules(module: str) -> set[str]: - modules = {module} - pending = [module] - while pending: - tree = ast.parse( - (tools / f"{pending.pop()}.py").read_text(encoding="utf-8") - ) - for node in ast.walk(tree): - if not isinstance(node, ast.ImportFrom) or node.level != 1: - continue - names = ( - [node.module.split(".")[0]] - if node.module - else [alias.name for alias in node.names] - ) - for name in set(names) - modules: - modules.add(name) - pending.append(name) - return modules - command_inputs = { - "scripts/nvx.py", - "scripts/nvx_tools/__init__.py", - ".github/workflows/ci.yml", - *(f"scripts/nvx_tools/{name}.py" for name in imported_modules("ci")), - } - self.assertLessEqual( - {"ci", "common", "build_constants"}, imported_modules("ci") - ) - unit_inputs = command_inputs | { - f".github/actions/{name}/action.yml" - for name in local_actions(_workflow_job(workflow, "openvmm-unit-tests")) - } - vmm_inputs = command_inputs | { - f".github/actions/{name}/action.yml" - for job in ("openvmm-vmm-tests", "artifacts") - for name in local_actions(_workflow_job(workflow, job)) - } - # Every cache key input of the kernel and Alpine initramfs that the - # VMM tests boot, and of every guest that the microVM tests boot. - guest_action = ( - github / "actions" / "build-guest-artifacts" / "action.yml" - ).read_text(encoding="utf-8") - guest_inputs = { - match.group(1): { - path.replace("**", "example").replace("*", "example") - for path in re.findall(r"'([^']+)'", match.group(2)) - } - for match in re.finditer( - r"^\s+([A-Z_]+)_INPUT_HASH: \$\{\{ hashFiles\((.+)\) \}\}$", - guest_action, - re.MULTILINE, + with ( + patch.object(OpenVMMBuildConstants, "DIRECTORY", openvmm), + patch.object( + ci, + "artifact_path", + side_effect=[kernel, initrd], + ), + patch.object(ci.os, "access", return_value=True), + patch.object(ci.Path, "exists", return_value=False), + patch.object(ci, "require_tool", side_effect=["cargo", "rustup"]), + patch.object(ci, "run_capture", return_value=installed_targets), + patch.object(ci, "run_checked") as run_checked, + patch.dict(os.environ, {"RUNNER_TEMP": os.fspath(root)}), + ): + ci.run_openvmm_tests(backend) + + self.assertEqual(run_checked.call_count, 4) + install_toolchain, install_targets, restore, tests = ( + run_checked.call_args_list ) - } - self.assertEqual( - set(guest_inputs), - {"KERNEL", "DEBUG_KERNEL", "ALPINE", "UBUNTU", "AZURELINUX"}, - ) - vmm_inputs |= guest_inputs["KERNEL"] | guest_inputs["ALPINE"] - test_inputs = vmm_inputs.union(*guest_inputs.values()) | { - ".github/workflows/run-nvx-microvm-tests.yml", - ".github/workflows/build-openvmm-binary.yml", - *( - f".github/actions/{name}/action.yml" - for configuration in ( - (github / "workflows" / "run-nvx-microvm-tests.yml").read_text( - encoding="utf-8" - ), - (github / "workflows" / "build-openvmm-binary.yml").read_text( - encoding="utf-8" - ), - _workflow_job(workflow, "debug-kernel"), + environment = install_toolchain.kwargs["env"] + self.assertEqual( + environment["RUSTUP_HOME"], + os.fspath(root / "openvmm-rustup"), + ) + self.assertEqual( + environment["RUSTUP_TOOLCHAIN"], + OpenVMMBuildConstants.RUST_TOOLCHAIN, + ) + if os.name != "nt": + self.assertEqual( + environment["XDG_CACHE_HOME"], + os.fspath(root / "openvmm-cache"), ) - for name in local_actions(configuration) - ), - } - self.assertNotIn(".github/actions/validate-nvx/action.yml", test_inputs) - for pattern, inputs in ( - (ci.OPENVMM_UNIT_TEST_INPUT_PATHS, unit_inputs), - (ci.OPENVMM_VMM_TEST_INPUT_PATHS, vmm_inputs), - (ci.CI_TEST_INPUT_PATHS, test_inputs), - ): - for path in sorted(inputs): - with self.subTest(pattern=pattern.pattern, path=path): - self.assertIsNotNone(pattern.match(path)) + self.assertEqual( + install_toolchain.args[0], + [ + "rustup", + "toolchain", + "install", + OpenVMMBuildConstants.RUST_TOOLCHAIN, + "--profile", + "minimal", + ], + ) + self.assertEqual( + install_targets.args[0], + [ + "rustup", + "target", + "add", + *OpenVMMBuildConstants.TEST_RUST_TARGETS[backend], + "--toolchain", + OpenVMMBuildConstants.RUST_TOOLCHAIN, + ], + ) + for command in (install_targets, restore, tests): + self.assertIs(command.kwargs["env"], environment) - def test_guest_cache_keys_hash_the_docker_build_context_filter(self): - # Docker excludes files by .dockerignore before COPY, so a change to that - # file changes the guest build even when no other input changes. - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-guest-artifacts" - / "action.yml" - ) - expressions = re.findall( - r"hashFiles\(([^)]*)\)", action.read_text(encoding="utf-8") + def test_openvmm_tests_install_missing_pinned_toolchain(self): + missing_toolchain = common.CommandResult( + args=("rustup", "target", "list"), + returncode=1, + stdout=b"", + stderr=( + f"error: toolchain '{OpenVMMBuildConstants.RUST_TOOLCHAIN}' " + "is not installed\n" + ).encode(), ) - hashed = [ - expression - for expression in expressions - if "docker/Dockerfile" in expression + install_toolchain = [ + "rustup", + "toolchain", + "install", + OpenVMMBuildConstants.RUST_TOOLCHAIN, + "--profile", + "minimal", ] - self.assertTrue(hashed) - for expression in hashed: - with self.subTest(expression=expression[:60]): - self.assertIn("'.dockerignore'", expression) - - def test_ci_change_summary_explains_each_decision(self): - changed_files = [ - "scripts/nvx_tools/microvm_tests.py", - "guest/common/init", - "guest/common/nvx-hostmount", - "guest/common/nvx-snapshot", - "guest/common/nvx-identity-probe", + add_targets = [ + "rustup", + "target", + "add", + *OpenVMMBuildConstants.TEST_RUST_TARGETS["kvm"], + "--toolchain", + OpenVMMBuildConstants.RUST_TOOLCHAIN, ] - changes = ci.classify_ci_changes("pull_request", changed_files) - summary = ci.describe_ci_changes("pull_request", changed_files, changes) - self.assertIn("### Artifact-backed tests and benchmarks: run\n", summary) - self.assertIn("### NVX microVM tests: run\n", summary) - self.assertIn( - "### OpenVMM unit tests: skip\nNo input of this suite changed.\n", - summary, - ) - self.assertIn( - "### OpenVMM VMM tests: run\nSuite inputs changed: " - "`guest/common/init`, `guest/common/nvx-hostmount`, " - "`guest/common/nvx-snapshot`, and 1 more.\n", - summary, - ) - push = ci.describe_ci_changes( - "push", changed_files, ci.classify_ci_changes("push", changed_files) + + with tempfile.TemporaryDirectory() as temporary: + for runner_temp in (None, temporary): + with ( + self.subTest(runner_temp=runner_temp), + patch.object(ci, "run_capture", return_value=missing_toolchain), + patch.object(ci, "run_checked") as run_checked, + patch.dict( + os.environ, + {} if runner_temp is None else {"RUNNER_TEMP": runner_temp}, + clear=True, + ), + ): + environment = ci._prepare_openvmm_test_environment("kvm", "rustup") + + self.assertEqual( + [command.args[0] for command in run_checked.call_args_list], + [install_toolchain, add_targets], + ) + for command in run_checked.call_args_list: + self.assertIs(command.kwargs["env"], environment) + self.assertEqual( + environment["RUSTUP_TOOLCHAIN"], + OpenVMMBuildConstants.RUST_TOOLCHAIN, + ) + if runner_temp is None: + self.assertNotIn("RUSTUP_HOME", environment) + else: + self.assertEqual( + environment["RUSTUP_HOME"], + os.fspath( + Path(runner_temp) + / OpenVMMBuildConstants.RUSTUP_DIRECTORY_NAME + ), + ) + + def test_openvmm_tests_define_each_backend_filter(self): + self.assertEqual( + set(ci.OPENVMM_TEST_FILTERS), + set(ci.OPENVMM_TEST_BACKENDS), ) - self.assertIn( - "### OpenVMM unit tests: run\n" - "Pushes run every OpenVMM suite whenever the tests run.\n", - push, + self.assertEqual( + set(OpenVMMBuildConstants.TEST_RUST_TARGETS), + set(ci.OPENVMM_TEST_BACKENDS), ) - documentation = ci.describe_ci_changes( - "pull_request", - ["doc/ci.md"], - ci.classify_ci_changes("pull_request", ["doc/ci.md"]), + self.assertEqual( + ci.OPENVMM_TEST_FILTERS["kvm"], + ci.OPENVMM_KVM_TEST_FILTER, ) self.assertIn( - "### Artifact-backed tests and benchmarks: skip\n" - "Only documentation files changed.\n", - documentation, + "!test(no_vmbus_prepped_boot_no_vmbus_windows)", + ci.OPENVMM_KVM_TEST_FILTER, ) self.assertIn( - "### OpenVMM VMM tests: skip\nNo test input changed.\n", documentation + "!test(windows_datacenter_core_2022_x64)", + ci.OPENVMM_KVM_TEST_FILTER, ) - - def test_classify_ci_changes_command_prints_every_output(self): - with tempfile.TemporaryDirectory() as temporary: - changed_files = Path(temporary) / "changed-files" - changed_files.write_text("guest/common/init\n", encoding="utf-8") - summary = Path(temporary) / "summary.md" - summary.write_text("before\n", encoding="utf-8") - args = nvx.parse_args( - [ - "classify-ci-changes", - "--event-name", - "pull_request", - "--changed-files", - os.fspath(changed_files), - "--summary", - os.fspath(summary), - ] + self.assertIn("!test(virtio_net_windows)", ci.OPENVMM_KVM_TEST_FILTER) + self.assertEqual( + set(ci.OPENVMM_KVM_EXCLUDED_TESTS), + { + "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_boot_no_agent", + "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_iso_boot_no_agent", + "multiarch::openvmm_pcat_x64_ubuntu_2404_server_x64_boot", + "multiarch::openvmm_pcat_x64_ubuntu_2504_server_x64_boot", + "multiarch::openvmm_pcat_x64_ubuntu_2504_server_x64_boot_heavy", + }, + ) + for excluded_test in ci.OPENVMM_KVM_EXCLUDED_TESTS: + self.assertIn( + f"!{ci._exact_openvmm_test(excluded_test)}", + ci.OPENVMM_KVM_TEST_FILTER, ) - stdout = io.StringIO() - with contextlib.redirect_stdout(stdout): - args.handler(args) - self.assertEqual( - stdout.getvalue().splitlines(), - [ - "run-tests=true", - "run-workloads=true", - "run-openvmm-unit-tests=false", - "run-openvmm-vmm-tests=true", - ], + self.assertNotIn("!test(openvmm_pcat_x64)", ci.OPENVMM_KVM_TEST_FILTER) + self.assertNotIn( + "openvmm_linux_x64_apicid_offset", + ci.OPENVMM_KVM_TEST_FILTER, + ) + self.assertNotIn( + "openvmm_linux_x64_legacy_xapic", + ci.OPENVMM_KVM_TEST_FILTER, + ) + self.assertEqual( + ci.OPENVMM_TEST_FILTERS["mshv"], + ci.OPENVMM_MSHV_TEST_FILTER, + ) + self.assertIn( + "!test(windows_datacenter_core_2022_x64)", + ci.OPENVMM_MSHV_TEST_FILTER, + ) + self.assertEqual( + set(ci.OPENVMM_MSHV_EXCLUDED_TESTS), + { + "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_boot_no_agent", + "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_iso_boot_no_agent", + "multiarch::openvmm_pcat_x64_ubuntu_2404_server_x64_boot", + "multiarch::openvmm_pcat_x64_ubuntu_2504_server_x64_boot", + "multiarch::openvmm_pcat_x64_ubuntu_2504_server_x64_boot_heavy", + "multiarch::pcie::openvmm_linux_x64_pcie_save_restore", + }, + ) + self.assertNotIn( + "x86_64::openvmm_linux_x64_virtio_blk_device", + ci.OPENVMM_MSHV_EXCLUDED_TESTS, + ) + for excluded_test in ci.OPENVMM_MSHV_EXCLUDED_TESTS: + self.assertIn( + f"!{ci._exact_openvmm_test(excluded_test)}", + ci.OPENVMM_MSHV_TEST_FILTER, ) - text = summary.read_text(encoding="utf-8") - self.assertTrue(text.startswith("before\n### ")) - self.assertIn("### OpenVMM VMM tests: run\n", text) + self.assertNotIn("!test(openvmm_pcat_x64)", ci.OPENVMM_MSHV_TEST_FILTER) + for backend, test_filter in ci.OPENVMM_TEST_FILTERS.items(): + for required_test in ci.OPENVMM_REQUIRED_MICROVM_TESTS: + with self.subTest(backend=backend, required_test=required_test): + self.assertIn( + ci._exact_openvmm_test(required_test), + test_filter, + ) + self.assertEqual(len(ci.OPENVMM_WHP_TESTS), 29) + self.assertEqual( + len(set(ci.OPENVMM_WHP_TESTS)), + len(ci.OPENVMM_WHP_TESTS), + ) + for required_test in ci.OPENVMM_REQUIRED_MICROVM_TESTS: + self.assertIn(required_test, ci.OPENVMM_WHP_TESTS) + # Only Linux hosts can build the Linux pipette this TTRPC test boots. + self.assertNotIn("test_ttrpc_interface", ci.OPENVMM_TEST_FILTERS["whp"]) + for backend in ("kvm", "mshv"): + with self.subTest(backend=backend): + self.assertIn("test(ttrpc)", ci.OPENVMM_TEST_FILTERS[backend]) + self.assertNotIn( + "test_ttrpc_interface", + ci.OPENVMM_TEST_FILTERS[backend], + ) + self.assertEqual(ci.OPENVMM_WHP_EXCLUDED_TESTS, ()) + self.assertNotIn( + "multiarch::openvmm_pcat_x64_windows_datacenter_core_2022_x64_boot_heavy", + ci.OPENVMM_WHP_EXCLUDED_TESTS, + ) + self.assertNotIn( + "multiarch::openvmm_pcat_x64_windows_datacenter_core_2022_x64_boot", + ci.OPENVMM_WHP_EXCLUDED_TESTS, + ) + self.assertNotIn( + "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_iso_boot_no_agent", + ci.OPENVMM_WHP_EXCLUDED_TESTS, + ) + self.assertNotIn( + "multiarch::openvmm_pcat_x64_freebsd_13_2_x64_boot_no_agent", + ci.OPENVMM_WHP_EXCLUDED_TESTS, + ) + self.assertNotIn( + "multiarch::pcie::openvmm_uefi_x64_windows_datacenter_core_2022_x64_pcie_nvme_boot", + ci.OPENVMM_WHP_EXCLUDED_TESTS, + ) + self.assertTrue( + set(ci.OPENVMM_WHP_EXCLUDED_TESTS).issubset(ci.OPENVMM_WHP_TESTS) + ) + self.assertEqual( + ci.OPENVMM_TEST_FILTERS["whp"], + ci._join_openvmm_tests( + ci.OPENVMM_WHP_TESTS, + ci.OPENVMM_WHP_EXCLUDED_TESTS, + ), + ) + self.assertEqual( + ci._join_openvmm_tests( + ("suite::boot", "suite::boot_heavy"), + ("suite::boot_heavy",), + ), + ( + "(test(/^suite::boot$/) | test(/^suite::boot_heavy$/))" + " & !test(/^suite::boot_heavy$/)" + ), + ) - def test_classify_ci_changes_command_reads_null_delimited_paths(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - changed_files = root / "changed-files" - changed_files.write_bytes( - "doc/notes.md\0kernel/patches/é\r\n.patch\0".encode() - ) - args = nvx.parse_args( - [ - "classify-ci-changes", - "--event-name", - "pull_request", - "--changed-files", - os.fspath(changed_files), - "--null", - "--summary", - os.fspath(root / "summary"), - ] - ) - stdout = io.StringIO() - with contextlib.redirect_stdout(stdout): - args.handler(args) - self.assertEqual( - stdout.getvalue().splitlines(), - [ - "run-tests=true", - "run-workloads=true", - "run-openvmm-unit-tests=false", - "run-openvmm-vmm-tests=true", - ], - ) - summary = (root / "summary").read_bytes().decode("utf-8") - self.assertIn("kernel/patches/é\r\n.patch", summary) + def test_openvmm_tests_reject_unknown_backend(self): + with self.assertRaisesRegex(common.ScriptError, "unsupported.*backend"): + ci.run_openvmm_tests("unknown") - @unittest.skipUnless(os.name == "posix", "requires bash and POSIX filenames") - def test_ci_change_detector_classifies_real_git_paths(self): + +class CiConfigurationTests(unittest.TestCase): + def test_ci_needs_reference_declared_jobs(self): workflow = ( BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" ).read_text(encoding="utf-8") - step = _workflow_step(workflow, "openvmm-changes", "Detect relevant changes") - lines = step.splitlines() - start = lines.index(" run: |") + 1 - script = "\n".join(line[10:] for line in lines[start:]) - classifier = BuildConstants.REPO_ROOT / "scripts" / "nvx.py" - shim = ( - f"python3() {{ {shlex.quote(sys.executable)} " - f'{shlex.quote(os.fspath(classifier))} "${{@:2}}"; }}\n' - ) - cases = ( - ("rename", "doc/input.patch", True), - ("delete", None, True), - ("unicode", "kernel/patches/é.patch", True), - ("newline", "kernel/patches/line\nbreak.patch", True), - ("documentation", "doc/notes.md", False), + jobs = set( + re.findall( + r"^ ([A-Za-z0-9_-]+):$", + workflow.split("jobs:\n", 1)[1], + re.MULTILINE, + ) ) - for name, destination, test_input in cases: - with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - - def git(*arguments: str, directory: Path = root) -> str: - result = subprocess.run( - ["git", *arguments], - cwd=directory, - capture_output=True, - text=True, - check=True, + for job_name in sorted(jobs): + job = _workflow_job(workflow, job_name) + match = re.search(r"^ needs:(.*)$", job, re.MULTILINE) + if match is None: + continue + inline = match.group(1).strip() + if inline: + dependencies = {name.strip() for name in inline.strip("[]").split(",")} + else: + dependencies = set( + re.findall( + r"^ - ([A-Za-z0-9_-]+)$", + job.split(" if:", 1)[0], + re.MULTILINE, ) - return result.stdout.strip() + ) + with self.subTest(job=job_name): + self.assertLessEqual( + dependencies, + jobs, + f"undefined job dependencies: {dependencies - jobs}", + ) + required = _workflow_job(workflow, "required-status-check") + for job in ("aci-edge-sandboxes", "nvx-rs"): + with self.subTest(job=job): + self.assertIn(f" - {job}\n", required) + self.assertIn(f"${{{{ needs.{job}.result }}}}", required) - git("init", "--quiet") - git("config", "user.name", "Reproduction") - git("config", "user.email", "repro@example.invalid") - git("config", "commit.gpgsign", "false") - source = root / "kernel" / "patches" / "input.patch" - source.parent.mkdir(parents=True) - source.write_text("suite input\n", encoding="utf-8") - git("add", ".") - git("commit", "--quiet", "-m", "baseline") - base = git("rev-parse", "HEAD") - if name == "delete": - source.unlink() - else: - assert destination is not None - target = root / destination - target.parent.mkdir(parents=True, exist_ok=True) - if name == "rename": - source.rename(target) - else: - target.write_text("new input\n", encoding="utf-8") - git("add", "--all") - git("commit", "--quiet", "-m", "changed paths") - for event in ("pull_request", "push"): - with self.subTest(event=event): - output = root / f"output-{event}" - environment = os.environ.copy() - environment.update( - EVENT_NAME=event, - BASE_SHA=base, - HEAD_SHA=git("rev-parse", "HEAD"), - GITHUB_OUTPUT=os.fspath(output), - GITHUB_STEP_SUMMARY=os.fspath(root / "summary"), - RUNNER_TEMP=temporary, - ) - result = subprocess.run( - ["bash", "-c", shim + script], - cwd=root, - env=environment, - capture_output=True, - text=True, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - actual = dict( - line.split("=", 1) - for line in output.read_text(encoding="utf-8").splitlines() - ) - expected = { - "run-tests": str(test_input).lower(), - "run-workloads": str(test_input).lower(), - "run-openvmm-unit-tests": str( - test_input and event == "push" - ).lower(), - "run-openvmm-vmm-tests": str(test_input).lower(), - } - self.assertEqual(actual, expected) + def test_development_release_requires_successful_crate_checks(self): + workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" + ).read_text(encoding="utf-8") + release_job = _workflow_job(workflow, "release") + predicate = release_job.split(" if:", 1)[1].split(" runs-on:", 1)[0] + for job in ("aci-edge-sandboxes", "nvx-rs"): + with self.subTest(job=job): + self.assertIn(f" - {job}\n", release_job) + self.assertIn(f"needs.{job}.result == 'success' &&", predicate) + self.assertNotIn(f"needs.{job}.result == 'skipped'", predicate) - @unittest.skipUnless(os.name == "posix", "requires bash") - def test_ci_change_detector_runs_the_full_matrix_unless_classified(self): + def test_crate_checks_run_on_every_os(self): workflow = ( BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" ).read_text(encoding="utf-8") - step = _workflow_step(workflow, "openvmm-changes", "Detect relevant changes") - lines = step.splitlines() - start = lines.index(" run: |") + 1 - script = "\n".join(line[10:] for line in lines[start:]) - stub = """ -git() { - case "$1" in - cat-file) return "${GIT_CAT_FILE_STATUS}" ;; - diff) - printf '%b' "${CHANGED_FILES}" - return "${GIT_DIFF_STATUS}" - ;; - esac - echo "unexpected git command: $*" >&2 - return 1 -} -python3() { - if [[ -n "${CLASSIFIER:-}" ]]; then - "${CLASSIFIER}" "$@" - return - fi - printf '%b' "${CLASSIFICATION}" - return "${CLASSIFIER_STATUS}" -} -""" - complete = ( - "run-tests=true\\nrun-workloads=true\\n" - "run-openvmm-unit-tests=false\\nrun-openvmm-vmm-tests=false\\n" - ) - everything = dict.fromkeys(ci.CI_CHANGE_OUTPUTS, "true") - classified = { - "run-tests": "true", - "run-workloads": "true", - "run-openvmm-unit-tests": "false", - "run-openvmm-vmm-tests": "false", - } - cases = ( - ("missing base", {"BASE_SHA": ""}, everything), - ("zero base", {"BASE_SHA": "0" * 40}, everything), - ("missing commit", {"GIT_CAT_FILE_STATUS": "1"}, everything), - ("failed diff", {"GIT_DIFF_STATUS": "128"}, everything), - ("failed classifier", {"CLASSIFIER_STATUS": "1"}, everything), - ( - "missing output", - {"CLASSIFICATION": complete.rsplit("run-openvmm-vmm", 1)[0]}, - everything, - ), - ( - "duplicate output", - {"CLASSIFICATION": complete + "run-tests=false\\n"}, - everything, - ), - ( - "malformed output", - {"CLASSIFICATION": complete.replace("=true", "=yes", 1)}, - everything, - ), - ( - "classified", - {"CLASSIFICATION": "notice: ignored\\n" + complete}, - classified, - ), - ( - "real classifier", - { - "CLASSIFIER": sys.executable, - "CHANGED_FILES": "scripts/nvx_tools/microvm_tests.py\\0", - }, - classified, - ), - ) - for name, overrides, expected in cases: - with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - environment = os.environ.copy() - environment.update( - { - "EVENT_NAME": "pull_request", - "BASE_SHA": "1" * 40, - "HEAD_SHA": "2" * 40, - "GITHUB_OUTPUT": os.fspath(root / "output"), - "GITHUB_STEP_SUMMARY": os.fspath(root / "summary"), - "RUNNER_TEMP": temporary, - "GIT_CAT_FILE_STATUS": "0", - "GIT_DIFF_STATUS": "0", - "CHANGED_FILES": "openvmm\\0", - "CLASSIFICATION": complete, - "CLASSIFIER_STATUS": "0", - } - ) - environment.update(overrides) - result = subprocess.run( - ["bash", "-c", stub + script], - cwd=BuildConstants.REPO_ROOT, - env=environment, - capture_output=True, - text=True, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - output = (root / "output").read_text(encoding="utf-8").splitlines() - self.assertEqual(len(output), len(expected), output) - self.assertEqual(dict(line.split("=", 1) for line in output), expected) - summary = (root / "summary").read_text(encoding="utf-8") - if expected is everything: - self.assertIn("### Full matrix: run\n", summary) - else: - self.assertNotIn("Full matrix", summary) + for job, action in ( + ("aci-edge-sandboxes", "check-aci-edge-sandboxes"), + ("nvx-rs", "check-nvx-rs"), + ): + with self.subTest(job=job): + definition = _workflow_job(workflow, job) + self.assertIn("os: [ubuntu-latest, windows-latest]", definition) + self.assertIn(f"uses: ./.github/actions/{action}", definition) + self.assertEqual(ci.REQUIRED_CI_RESULT_ENVIRONMENTS["nvx-rs"], "NVX_RS_RESULT") - def test_flowey_downloads_use_retrying_curl(self): + def test_nvx_rs_bundles_a_release_in_the_current_layout(self): + # The bundled feature stages what a release holds besides nvx-rs, so + # its synthetic release ships nvx-rs and its provenance, as releases + # do, and the pinned older release, which doesn't, stages as well. action = ( BuildConstants.REPO_ROOT / ".github" / "actions" - / "setup-curl" + / "check-nvx-rs" / "action.yml" ).read_text(encoding="utf-8") - windows_shim = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "setup-curl" - / "curl-shim.rs" - ).read_text(encoding="utf-8") - workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" - ).read_text(encoding="utf-8") - microvm_workflow = ( - BuildConstants.REPO_ROOT - / ".github" - / "workflows" - / "run-nvx-microvm-tests.yml" - ).read_text(encoding="utf-8") - build_action = ( + script = _composite_action_script(action, "Check artifact bundling") + for member in ( + "bin/${executable}", + "bin/${cli}", + "guest/vmlinux", + "guest/initramfs.cpio.gz", + f"provenance/{nvx_rs.PROVENANCE_NAME}", + ): + with self.subTest(member=member): + self.assertIn(f'"${{bundle}}/{member}"', script) + self.assertIn(member, script.split("sha256sum", 1)[1]) + self.assertIn("cli=nvx-rs.exe", script) + self.assertIn("--features bundled artifacts", script) + + def test_nvx_rs_builds_and_checks_every_guest_binary_for_musl(self): + crate = BuildConstants.REPO_ROOT / "nvx-rs" + action = ( BuildConstants.REPO_ROOT / ".github" / "actions" - / "build-openvmm" + / "check-nvx-rs" / "action.yml" ).read_text(encoding="utf-8") - - for option in ( - "--retry 5", - "--retry-all-errors", - "--retry-delay 2", - "--retry-max-time 90", - ): - self.assertEqual(action.count(option), 1) - for argument in ( - '"--retry"', - '"5"', - '"--retry-all-errors"', - '"--retry-delay"', - '"2"', - '"--retry-max-time"', - '"90"', - ): - self.assertIn(argument, windows_shim) - self.assertIn('system_curl="${NVX_SYSTEM_CURL:-$(command -v curl)}"', action) - self.assertIn("Get-Command curl.exe", action) - self.assertIn('Join-Path $ShimDirectory "curl.exe"', action) - self.assertIn("Get-Command rustc.exe", action) - self.assertNotIn("curl.cmd", action) - for job_name in ( - "openvmm-vmm-tests", - "openvmm-unit-tests", - ): - with self.subTest(job_name=job_name): - self.assertEqual( - _workflow_job(workflow, job_name).count( - "uses: ./.github/actions/setup-curl" - ), - 1, - ) - self.assertEqual( - microvm_workflow.count("uses: ./.github/actions/setup-curl"), - 1, + manifest = (crate / "Cargo.toml").read_text(encoding="utf-8") + guest_binaries = set( + re.findall( + r'^\[\[bin\]\]\nname = "([^"]+)"\npath = "[^"]+"\n' + r'required-features = \["guest"\]$', + manifest, + re.MULTILINE, + ) + ) + self.assertIn("nvx-exit", guest_binaries) + lint_and_test = "Lint and test the guest binaries" + build_and_check = "Build and check the guest binaries" + check = _composite_action_step(action, lint_and_test) + build = _composite_action_step(action, build_and_check) + for step in (check, build): + self.assertIn("if: runner.os == 'Linux'", step) + # nvx.py owns the Cargo and rustup commands, which developers reuse. + self.assertNotIn("cargo ", step) + self.assertNotIn("rustup", step) + self.assertIn( + "python3 scripts/nvx.py check-guest-binaries " + '--toolchain "${{ steps.toolchains.outputs.toolchain }}"', + " ".join(check.split()), + ) + # The check adds the musl target that the build needs. + self.assertLess( + action.index(f"- name: {lint_and_test}"), + action.index(f"- name: {build_and_check}"), + ) + self.assertEqual(set(GuestRustBuildConstants.BINARIES), guest_binaries) + for name in guest_binaries: + module = name.removeprefix("nvx-").replace("-", "_") + root = (crate / "src" / "bin" / f"{name}.rs").read_text(encoding="utf-8") + with self.subTest(binary=name): + # Every guest binary runs the main of its nvx::guest module. + self.assertIn(f"nvx::guest::{module}::main()", root) + self.assertIn( + 'python3 ../scripts/nvx.py build-guest-binaries --output "${directory}"', + build, + ) + self.assertIn( + 'python3 ../scripts/nvx.py test-guest-binaries --binaries "${directory}"', + build, ) - self.assertIn("uses: ./.github/actions/setup-curl", build_action) - - @unittest.skipUnless(os.name == "nt", "Windows-specific curl resolution") - def test_windows_curl_shim_retries_http_500(self): - rustc = shutil.which("rustc") - system_curl = shutil.which("curl.exe") - if rustc is None or system_curl is None: - self.fail("Windows curl shim test requires rustc and curl.exe") - - class RetryHandler(http.server.BaseHTTPRequestHandler): - request_count = 0 - - def do_GET(self) -> None: - type(self).request_count += 1 - if type(self).request_count == 1: - self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR) - return - body = b"ok" - self.send_response(http.HTTPStatus.OK) - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - def log_message(self, format: str, *args: object) -> None: - pass + def test_required_ci_result_policy(self): + always_successful = { + "quality", + "aci-edge-sandboxes", + "nvx-rs", + "openvmm-changes", + } + # Jobs that succeed whenever the repository's jobs run. + repository = always_successful | set(ci.REQUIRED_CI_CLI_JOBS) + builds = set(ci.REQUIRED_CI_BUILD_JOBS) + openvmm_tests = set(ci.REQUIRED_CI_OPENVMM_TEST_JOBS) + openvmm_artifact_tests = set(ci.REQUIRED_CI_OPENVMM_ARTIFACT_TEST_JOBS) + microvm_tests = set(ci.REQUIRED_CI_MICROVM_TEST_JOBS) + debug_kernel = set(ci.REQUIRED_CI_MICROVM_DEBUG_JOBS) + debug_kernel_push = set(ci.REQUIRED_CI_MICROVM_DEBUG_PUSH_JOBS) + artifacts = {ci.REQUIRED_CI_ARTIFACT_JOB, ci.REQUIRED_CI_DEBUG_KERNEL_JOB} + platforms = set(ci.REQUIRED_CI_PLATFORM_JOBS) + workloads = artifacts | builds | platforms + microvm = microvm_tests | debug_kernel + # Event, same repository, the change detector's outputs (tests, + # workloads, OpenVMM unit tests, OpenVMM VMM tests), and the jobs that + # must succeed; every other required job must be skipped. + cases = ( + ("pull_request", True, (False, False, False, False), repository), + ( + "pull_request", + True, + (False, True, False, False), + repository | workloads | {"performance-gate"}, + ), + ( + "pull_request", + True, + (True, False, True, True), + repository | builds | openvmm_tests, + ), + ( + "pull_request", + True, + (True, True, True, True), + repository + | workloads + | openvmm_tests + | openvmm_artifact_tests + | microvm + | {"performance-gate"}, + ), + # A pull request that changes no input of either OpenVMM suite. + ( + "pull_request", + True, + (True, True, False, False), + repository | workloads | microvm | {"performance-gate"}, + ), + # Guest inputs reach the VMM tests but not the unit tests. + ( + "pull_request", + True, + (True, True, False, True), + repository + | workloads + | openvmm_artifact_tests + | microvm + | {"performance-gate"}, + ), + ("pull_request", False, (False, False, False, False), always_successful), + ( + "pull_request", + False, + (False, True, False, False), + always_successful | artifacts, + ), + ("pull_request", False, (True, False, True, True), always_successful), + ( + "pull_request", + False, + (True, True, True, True), + always_successful | artifacts, + ), + ("push", True, (False, False, False, False), repository), + ("push", True, (False, True, False, False), repository | workloads), + ( + "push", + True, + (True, False, True, True), + repository | builds | openvmm_tests, + ), + ( + "push", + True, + (True, True, True, True), + repository + | workloads + | openvmm_tests + | openvmm_artifact_tests + | microvm + | debug_kernel_push, + ), + ) - server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), RetryHandler) - server_thread = threading.Thread(target=server.serve_forever, daemon=True) - server_thread.start() - try: - with tempfile.TemporaryDirectory() as directory: - shim = Path(directory) / "curl.exe" - shim_source = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "setup-curl" - / "curl-shim.rs" + for event_name, same_repository, flags, successful_jobs in cases: + changes = ci.CiChanges(*flags) + with self.subTest( + event_name=event_name, + same_repository=same_repository, + changes=changes, + ): + expected = ci.required_ci_expected_results( + event_name, + same_repository=same_repository, + changes=changes, ) - subprocess.run( - [ - rustc, - "--edition=2021", - "-C", - "opt-level=s", - "-o", - os.fspath(shim), - os.fspath(shim_source), - ], - check=True, + self.assertEqual(set(expected), set(ci.REQUIRED_CI_RESULT_ENVIRONMENTS)) + self.assertEqual( + {job for job, result in expected.items() if result == "success"}, + successful_jobs, ) - environment = os.environ.copy() - environment["NVX_SYSTEM_CURL"] = system_curl - environment["PATH"] = directory + os.pathsep + environment["PATH"] - port = server.server_address[1] - result = subprocess.run( - [ - os.fspath(shim), - "--fail", - "-L", - f"http://127.0.0.1:{port}/asset", - ], - check=False, - capture_output=True, - env=environment, + self.assertEqual( + ci.required_ci_failures( + event_name, + same_repository=same_repository, + changes=changes, + results=expected, + ), + [], ) - finally: - server.shutdown() - server.server_close() - server_thread.join(timeout=5) - self.assertFalse( - server_thread.is_alive(), - "HTTP test server did not stop within 5 seconds", - ) - self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace")) - self.assertEqual(result.stdout, b"ok") - self.assertEqual(RetryHandler.request_count, 2) + for job, expected_result in expected.items(): + unexpected = expected.copy() + unexpected[job] = ( + "skipped" if expected_result == "success" else "success" + ) + with self.subTest(job=job, actual=unexpected[job]): + self.assertEqual( + ci.required_ci_failures( + event_name, + same_repository=same_repository, + changes=changes, + results=unexpected, + ), + [ + f"{job}: expected {expected_result}, " + f"got {unexpected[job]}" + ], + ) - def test_ci_shares_openvmm_inputs_and_binary_artifacts(self): + for job in expected: + failed = expected.copy() + failed[job] = "failure" + with self.subTest(job=job, actual="failure"): + self.assertEqual( + ci.required_ci_failures( + event_name, + same_repository=same_repository, + changes=changes, + results=failed, + ), + [f"{job}: expected {expected[job]}, got failure"], + ) + + def test_required_ci_job_uses_tested_policy(self): workflow = ( BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" ).read_text(encoding="utf-8") - build_workflow = ( - BuildConstants.REPO_ROOT - / ".github" - / "workflows" - / "build-openvmm-binary.yml" - ).read_text(encoding="utf-8") - microvm_workflow = ( - BuildConstants.REPO_ROOT - / ".github" - / "workflows" - / "run-nvx-microvm-tests.yml" - ).read_text(encoding="utf-8") - platform_workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / "run-platform.yml" - ).read_text(encoding="utf-8") - build_action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-openvmm" - / "action.yml" - ).read_text(encoding="utf-8") + job = _workflow_job(workflow, "required-status-check") - for configuration in (workflow, build_action): - self.assertIn( - "openvmm/flowey-persist/flowey_lib_common__download_gh_release", - configuration, - ) - self.assertIn( - "openvmm/flowey-persist/flowey_lib_common__cache", - configuration, - ) - self.assertIn( - "openvmm-inputs-v1-${{ runner.os }}-${{ runner.arch }}-", - configuration, - ) - producers = { - "build-openvmm-linux-gnu": ( - "kvm", - "openvmm-linux-gnu", - ), - "build-openvmm-linux-musl": ( - "mshv", - "openvmm-linux-musl", - ), - "build-openvmm-windows-msvc": ( - "whp", - "openvmm-windows-msvc", - ), - } - self.assertEqual( - workflow.count("uses: ./.github/workflows/build-openvmm-binary.yml"), - len(producers), + self.assertIn("python3 scripts/nvx.py check-required-ci", job) + self.assertIn('--same-repository "${SAME_REPOSITORY}"', job) + self.assertIn( + "github.event.pull_request.head.repo.full_name == github.repository", + job, ) - for job_name, (backend, artifact) in producers.items(): - with self.subTest(producer=job_name): - job = _workflow_job(workflow, job_name) - self.assertIn(f"backend: {backend}", job) - self.assertIn(f"artifact: {artifact}", job) - self.assertNotIn("target:", job) - self.assertNotIn("build-mode:", job) - self.assertNotIn("strategy:", job) - - consumers = { - "nvx-microvm-tests-kvm": ( - "build-openvmm-linux-gnu", - "openvmm-linux-gnu", - "run-nvx-microvm-tests.yml", - ), - "nvx-microvm-tests-mshv": ( - "build-openvmm-linux-gnu", - "openvmm-linux-gnu", - "run-nvx-microvm-tests.yml", - ), - "nvx-microvm-tests-whp": ( - "build-openvmm-windows-msvc", - "openvmm-windows-msvc", - "run-nvx-microvm-tests.yml", - ), - "nvx-microvm-debug-kvm": ( - "build-openvmm-linux-gnu", - "openvmm-linux-gnu", - "run-nvx-microvm-tests.yml", - ), - "nvx-microvm-debug-mshv": ( - "build-openvmm-linux-gnu", - "openvmm-linux-gnu", - "run-nvx-microvm-tests.yml", - ), - "nvx-microvm-debug-whp": ( - "build-openvmm-windows-msvc", - "openvmm-windows-msvc", - "run-nvx-microvm-tests.yml", - ), - "platform-kvm": ( - "build-openvmm-linux-gnu", - "openvmm-linux-gnu", - "run-platform.yml", - ), - "platform-mshv": ( - "build-openvmm-linux-musl", - "openvmm-linux-musl", - "run-platform.yml", - ), - "platform-whp": ( - "build-openvmm-windows-msvc", - "openvmm-windows-msvc", - "run-platform.yml", - ), - } - for job_name, (producer, artifact, reusable_workflow) in consumers.items(): - with self.subTest(consumer=job_name): - job = _workflow_job(workflow, job_name) - debug_kernel = ( - "debug-kernel, " - if job_name.startswith("nvx-microvm-debug-") - else "" - ) - cli = ( - "nvx-cli-windows" - if producer == "build-openvmm-windows-msvc" - else "nvx-cli-linux" + self.assertLess( + job.index("uses: actions/checkout@v5"), + job.index("python3 scripts/nvx.py check-required-ci"), + ) + self.assertIn("persist-credentials: false", job) + for environment in ci.REQUIRED_CI_RESULT_ENVIRONMENTS.values(): + with self.subTest(environment=environment): + self.assertIn(f" {environment}:", job) + # Every suite flag reaches the policy as the detector reported it. + detector = _workflow_job(workflow, "openvmm-changes") + for output in ci.CI_CHANGE_OUTPUTS: + environment = output.upper().replace("-", "_") + with self.subTest(output=output): + self.assertIn( + f" {output}: ${{{{ steps.changes.outputs.{output} }}}}", + detector, ) self.assertIn( - f"needs: [artifacts, {debug_kernel}{producer}, {cli}, " - "openvmm-changes]", + f" {environment}: " + f"${{{{ needs.openvmm-changes.outputs.{output} }}}}", job, ) - self.assertIn(f"needs.{producer}.result == 'success'", job) - self.assertIn(f"needs.{cli}.result == 'success'", job) - self.assertIn(f"uses: ./.github/workflows/{reusable_workflow}", job) - self.assertIn(f"openvmm-artifact: {artifact}", job) - for unrelated_producer in producers.keys() - {producer}: - self.assertNotIn(unrelated_producer, job) + self.assertIn(f'--{output} "${{{environment}}}"', job) - self.assertIn("build/openvmm.provenance.json", build_workflow) - self.assertIn( - "name: ${{ inputs.artifact }}-executable", - build_workflow, - ) - self.assertIn( - "name: ${{ inputs.artifact }}-provenance", - build_workflow, + def test_ci_change_classification_selects_suites_by_input(self): + # The pull-request outputs (tests, workloads, OpenVMM unit tests, + # OpenVMM VMM tests) for each class of changed path. + cases = ( + ((), (False, True, False, False)), + (("doc/ci.md", "README.md", "DOC/Guide.MD"), (False, False, False, False)), + (("openvmm",), (True, True, True, True)), + (("rust-toolchain.toml",), (True, True, True, True)), + (("scripts/setup/setup-windows-whp.ps1",), (True, True, True, True)), + (("scripts/setup/README.md",), (True, False, True, True)), + ((".github/workflows/ci.yml",), (True, True, True, True)), + ((".github/actions/validate-runner/action.yml",), (True, True, True, True)), + ((".github/actions/sccache/action.yml",), (True, True, True, True)), + ((".github/actions/setup-curl/curl-shim.rs",), (True, True, True, True)), + (("scripts/nvx.py",), (True, True, True, True)), + (("scripts/nvx_tools/ci.py",), (True, True, True, True)), + ((".dockerignore",), (True, True, False, True)), + (("scripts/nvx_tools/common.py",), (True, True, True, True)), + (("scripts/nvx_tools/build.py",), (True, True, False, True)), + (("scripts/nvx_tools/guests.py",), (True, True, False, True)), + (("kernel/config-microvm",), (True, True, False, True)), + (("kernel/patches/0001-example.patch",), (True, True, False, True)), + (("guest/common/init",), (True, True, False, True)), + (("guest/alpine/nvx-container-enter",), (True, True, False, True)), + (("SOURCE-MANIFEST.json",), (True, True, False, True)), + (("docker/Dockerfile",), (True, True, False, True)), + ( + (".github/actions/build-guest-artifacts/action.yml",), + (True, True, False, True), + ), + (("guest/ubuntu/nvx-bashrc",), (True, True, False, False)), + (("ubuntu/packages.lock.json",), (True, True, False, False)), + (("azurelinux/packages.lock.json",), (True, True, False, False)), + (("aci_edge_sandboxes/src/lib.rs",), (True, True, False, False)), + (("nvx-rs/src/lib.rs",), (True, True, False, True)), + (("nvx-rs/Cargo.lock",), (True, True, False, True)), + (("scripts/nvx_tools/microvm_tests.py",), (True, True, False, False)), + ( + (".github/workflows/run-nvx-microvm-tests.yml",), + (True, True, False, False), + ), + ((".github/actions/build-openvmm/action.yml",), (True, True, False, False)), + ((".github/actions/validate-nvx/action.yml",), (False, True, False, False)), + ( + (".github/actions/run-benchmark/action.yml",), + (False, True, False, False), + ), + (("scripts/test_nvx_tools.py",), (False, True, False, False)), + (("data/linux-kvm-virtual-machine.csv",), (False, True, False, False)), + ( + ("scripts/nvx_tools/microvm_tests.py", "kernel/config-microvm"), + (True, True, False, True), + ), ) + for changed_files, flags in cases: + with self.subTest(changed_files=changed_files): + pull_request = ci.classify_ci_changes("pull_request", changed_files) + self.assertEqual(pull_request, ci.CiChanges(*flags)) + # Pushes keep running both OpenVMM suites with the tests. + run_tests, run_workloads = flags[:2] + self.assertEqual( + ci.classify_ci_changes("push", changed_files), + ci.CiChanges(run_tests, run_workloads, run_tests, run_tests), + ) self.assertEqual( - build_workflow.count("uses: ./.github/actions/build-openvmm"), - 1, + ci.classify_ci_changes("pull_request", ["openvmm", ""]), + ci.classify_ci_changes("pull_request", ["openvmm"]), ) - self.assertIn("backend: ${{ inputs.backend }}", build_workflow) - self.assertNotIn("target: ${{ inputs.target }}", build_workflow) - self.assertNotIn("build-mode: ${{ inputs.build-mode }}", build_workflow) - self.assertEqual(build_workflow.count("overwrite: true"), 2) - self.assertEqual(build_workflow.count("retention-days: 1"), 2) - for consumer_workflow, download_count in ( - (microvm_workflow, 4), - (platform_workflow, 3), - ): - self.assertIn("path: openvmm/target/release", consumer_workflow) - self.assertIn("path: build", consumer_workflow) - self.assertEqual( - consumer_workflow.count("uses: actions/download-artifact@v8"), - download_count, - ) - self.assertIn( - "run: chmod +x openvmm/target/release/openvmm", - consumer_workflow, + with self.assertRaisesRegex(ValueError, "unsupported CI event"): + ci.classify_ci_changes("workflow_dispatch", ["openvmm"]) + + def test_ci_change_classification_covers_each_suite_input(self): + github = BuildConstants.REPO_ROOT / ".github" + tools = BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" + workflow = (github / "workflows" / "ci.yml").read_text(encoding="utf-8") + + def local_actions(configuration: str) -> set[str]: + names = set( + re.findall(r"uses: \./\.github/actions/([a-z0-9-]+)", configuration) ) - for configuration in ( - workflow, - build_workflow, - microvm_workflow, - platform_workflow, - ): - self.assertNotIn("path: .", configuration) - self.assertIn("openvmm-binary-v6-${{ inputs.backend }}-", build_action) - self.assertNotIn("openvmm-binary-v5-", build_action) - self.assertNotIn("inputs.target", build_action) - self.assertNotIn("inputs.build-mode", build_action) - self.assertNotIn("rustup target add", build_action) - self.assertNotIn("X86_64_UNKNOWN_LINUX_MUSL", build_action) - self.assertEqual( - build_action.count( - 'python3 scripts/nvx.py build-openvmm --backend "${{ inputs.backend }}"' - ), - 1, - ) - self.assertEqual( - build_action.count( - 'python scripts\\nvx.py build-openvmm --backend "${{ inputs.backend }}"' - ), - 1, - ) - self.assertIn("Verify OpenVMM provenance on Linux", build_action) - self.assertIn("Verify OpenVMM provenance on Windows", build_action) - self.assertNotIn("nvx-microvm-tests-v1", workflow) - self.assertNotIn("cargo-v2-", build_action) - self.assertNotIn("openvmm-tests-v2-", workflow) - self.assertNotIn("Restore OpenVMM test build", workflow) - self.assertNotIn("Save OpenVMM test build", workflow) - self.assertEqual( - build_workflow.count("uses: ./.github/actions/sccache"), - 2, - ) - for job_name, cache_namespace in ( - ("openvmm-vmm-tests", "vmm-tests"), - ("openvmm-unit-tests", "unit-tests"), - ): - with self.subTest(job_name=job_name): - job = _workflow_job(workflow, job_name) - self.assertEqual( - job.count("uses: ./.github/actions/sccache"), - 2, - ) - self.assertIn( - f"key: openvmm-inputs-v1-${{{{ runner.os }}}}-" - f"${{{{ runner.arch }}}}-{cache_namespace}-" - "${{ steps.openvmm.outputs.sha }}", - job, - ) - self.assertIn( - f"key: openvmm-cargo-v1-${{{{ runner.os }}}}-" - f"${{{{ runner.arch }}}}-{cache_namespace}-" - "${{ hashFiles('openvmm/Cargo.lock') }}", - job, - ) - self.assertIn( - "restore-keys: |\n" - " openvmm-inputs-v1-${{ runner.os }}-" - "${{ runner.arch }}-", - job, - ) - self.assertIn( - "restore-keys: |\n" - " openvmm-cargo-v1-${{ runner.os }}-" - "${{ runner.arch }}-", - job, + pending = list(names) + while pending: + action = (github / "actions" / pending.pop() / "action.yml").read_text( + encoding="utf-8" ) - self.assertNotIn("uses: actions/cache@v5", workflow) - self.assertNotIn("uses: actions/cache@v5", build_workflow) - self.assertNotIn("uses: actions/cache@v5", build_action) + for name in local_actions(action) - names: + names.add(name) + pending.append(name) + return names - release_job = _workflow_job(workflow, "release") - performance_gate_job = _workflow_job(workflow, "performance-gate") - performance_persist_job = _workflow_job(workflow, "performance-persist") - for job_name in consumers: - self.assertIn(f" - {job_name}", release_job) - self.assertIn(f"needs.{job_name}.result", release_job) - self.assertIn(f" - {job_name}", performance_persist_job) - self.assertIn(f"needs.{job_name}.result", performance_persist_job) - for job_name in ("platform-kvm", "platform-mshv", "platform-whp"): - self.assertIn(job_name, performance_gate_job) - self.assertIn(f"needs.{job_name}.result", performance_gate_job) + def imported_modules(module: str) -> set[str]: + modules = {module} + pending = [module] + while pending: + tree = ast.parse( + (tools / f"{pending.pop()}.py").read_text(encoding="utf-8") + ) + for node in ast.walk(tree): + if not isinstance(node, ast.ImportFrom) or node.level != 1: + continue + names = ( + [node.module.split(".")[0]] + if node.module + else [alias.name for alias in node.names] + ) + for name in set(names) - modules: + modules.add(name) + pending.append(name) + return modules - def test_copilot_setup_restores_ci_cache_entries(self): - github = BuildConstants.REPO_ROOT / ".github" - setup = (github / "workflows" / "copilot-setup-steps.yml").read_text( - encoding="utf-8" - ) - workflow = (github / "workflows" / "ci.yml").read_text(encoding="utf-8") - build_workflow = (github / "workflows" / "build-openvmm-binary.yml").read_text( - encoding="utf-8" - ) - build_action = (github / "actions" / "build-openvmm" / "action.yml").read_text( - encoding="utf-8" + command_inputs = { + "scripts/nvx.py", + "scripts/nvx_tools/__init__.py", + ".github/workflows/ci.yml", + *(f"scripts/nvx_tools/{name}.py" for name in imported_modules("ci")), + } + self.assertLessEqual( + {"ci", "common", "build_constants"}, imported_modules("ci") ) - checkout_action = ( - github / "actions" / "checkout-openvmm" / "action.yml" + unit_inputs = command_inputs | { + f".github/actions/{name}/action.yml" + for name in local_actions(_workflow_job(workflow, "openvmm-unit-tests")) + } + vmm_inputs = command_inputs | { + f".github/actions/{name}/action.yml" + for job in ("openvmm-vmm-tests", "artifacts") + for name in local_actions(_workflow_job(workflow, job)) + } + # Every cache key input of the kernel and Alpine initramfs that the + # VMM tests boot, and of every guest that the microVM tests boot. + guest_action = ( + github / "actions" / "build-guest-artifacts" / "action.yml" ).read_text(encoding="utf-8") - - # Guest artifact keys stay in sync because both sides use one action. - guest_artifacts = _workflow_step( - setup, "copilot-setup-steps", "Restore guest artifacts built by CI" - ) + guest_inputs = { + match.group(1): { + path.replace("**", "example").replace("*", "example") + for path in re.findall(r"'([^']+)'", match.group(2)) + if not path.startswith("!") + } + for match in re.finditer( + r"^\s+([A-Z_]+)_INPUT_HASH: \$\{\{ hashFiles\((.+)\) \}\}$", + guest_action, + re.MULTILINE, + ) + } self.assertEqual( - _yaml_field(guest_artifacts, "uses"), - ["./.github/actions/build-guest-artifacts"], - ) - self.assertEqual(_yaml_field(guest_artifacts, "restore-only"), ['"true"']) - - # The OpenVMM binary restore repeats the cache entry that the Linux GNU - # producer saves. A different key, path list, or compression method - # would make every session miss it without failing setup. - producer = _workflow_job(workflow, "build-openvmm-linux-gnu") - [backend] = _yaml_field(producer, "backend") - [binary] = _yaml_field(producer, "binary") - self.assertIn("binary-path: ${{ inputs.binary }}", build_workflow) - saved = _composite_action_step(build_action, "Save OpenVMM binary on Linux") - restored = _workflow_step( - setup, "copilot-setup-steps", "Restore the OpenVMM binary built by CI" + set(guest_inputs), + {"KERNEL", "DEBUG_KERNEL", "ALPINE", "UBUNTU", "AZURELINUX"}, ) - self.assertEqual( - _yaml_field(restored, "key"), - [ - key.replace("${{ inputs.backend }}", backend).replace( - "steps.openvmm.outputs.sha", "steps.openvmm-cache.outputs.sha" + vmm_inputs |= guest_inputs["KERNEL"] | guest_inputs["ALPINE"] + test_inputs = vmm_inputs.union(*guest_inputs.values()) | { + ".github/workflows/run-nvx-microvm-tests.yml", + ".github/workflows/build-openvmm-binary.yml", + *( + f".github/actions/{name}/action.yml" + for configuration in ( + (github / "workflows" / "run-nvx-microvm-tests.yml").read_text( + encoding="utf-8" + ), + (github / "workflows" / "build-openvmm-binary.yml").read_text( + encoding="utf-8" + ), + _workflow_job(workflow, "debug-kernel"), ) - for key in _yaml_field(saved, "key") - ], - ) - self.assertEqual( - _yaml_field(restored, "path"), - [ - path.replace("${{ inputs.binary-path }}", binary) - for path in _yaml_field(saved, "path") - ], - ) - - prepare = _workflow_step( - setup, "copilot-setup-steps", "Prepare OpenVMM cache restore" - ) - for revision_source in (checkout_action, prepare): - self.assertIn("sha=$(git rev-parse :openvmm)", revision_source) - - def tool_links(configuration: str) -> list[str]: - return [ - line.strip() - for line in configuration.splitlines() - if line.strip().startswith("ln -sf ") - ] - - self.assertEqual( - _yaml_field(saved, "PATH"), - ["${{ steps.cache-tools-linux.outputs.path }}"], - ) - self.assertEqual( - _yaml_field(restored, "PATH"), - ["${{ steps.openvmm-cache.outputs.path }}"], - ) - self.assertEqual( - tool_links(prepare), - tool_links( - _composite_action_step(build_action, "Prepare Linux cache tools") + for name in local_actions(configuration) ), - ) + } + self.assertNotIn(".github/actions/validate-nvx/action.yml", test_inputs) + for pattern, inputs in ( + (ci.OPENVMM_UNIT_TEST_INPUT_PATHS, unit_inputs), + (ci.OPENVMM_VMM_TEST_INPUT_PATHS, vmm_inputs), + (ci.CI_TEST_INPUT_PATHS, test_inputs), + ): + for path in sorted(inputs): + with self.subTest(pattern=pattern.pattern, path=path): + self.assertIsNotNone(pattern.match(path)) - def test_guest_artifact_restore_only_guards_builds_and_cache_saves(self): + def test_guest_cache_keys_hash_the_docker_build_context_filter(self): + # Docker excludes files by .dockerignore before COPY, so a change to that + # file changes the guest build even when no other input changes. action = ( BuildConstants.REPO_ROOT / ".github" / "actions" / "build-guest-artifacts" / "action.yml" - ).read_text(encoding="utf-8") - steps = _composite_action_steps(action) - guarded_steps = [ - step - for step in steps - if "docker build" in step or "uses: actions/cache/save@" in step + ) + expressions = re.findall( + r"hashFiles\(([^)]*)\)", action.read_text(encoding="utf-8") + ) + hashed = [ + expression + for expression in expressions + if "docker/Dockerfile" in expression ] + self.assertTrue(hashed) + for expression in hashed: + with self.subTest(expression=expression[:60]): + self.assertIn("'.dockerignore'", expression) - self.assertTrue(guarded_steps) - for step in guarded_steps: - try: - name = _yaml_scalar(step, "name") - except AssertionError: - name = "" - with self.subTest(step=name): - condition = _yaml_scalar(step, "if") - guard = "inputs.restore-only != 'true' && " - self.assertTrue(condition.startswith(guard), condition) - cache_condition = condition.removeprefix(guard) - self.assertIn("cache-hit != 'true'", cache_condition) - if "||" in cache_condition: - self.assertTrue( - _outer_parentheses_enclose(cache_condition), - cache_condition, - ) - - def test_guest_artifact_restore_only_leaves_restore_and_scratch_steps_active(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-guest-artifacts" - / "action.yml" - ).read_text(encoding="utf-8") - steps = _composite_action_steps(action) - restore_steps = [ - step for step in steps if "uses: actions/cache/restore@" in step + def test_ci_change_summary_explains_each_decision(self): + changed_files = [ + "scripts/nvx_tools/microvm_tests.py", + "guest/common/init", + "guest/common/nvx-hostmount", + "guest/common/nvx-snapshot", + "guest/common/nvx-identity-probe", ] - - self.assertTrue(restore_steps) - scratch = _composite_action_step( - action, "Prepare Ubuntu sandbox scratch template" + changes = ci.classify_ci_changes("pull_request", changed_files) + summary = ci.describe_ci_changes("pull_request", changed_files, changes) + self.assertIn("### Artifact-backed tests and benchmarks: run\n", summary) + self.assertIn("### NVX microVM tests: run\n", summary) + self.assertIn( + "### OpenVMM unit tests: skip\nNo input of this suite changed.\n", + summary, + ) + self.assertIn( + "### OpenVMM VMM tests: run\nSuite inputs changed: " + "`guest/common/init`, `guest/common/nvx-hostmount`, " + "`guest/common/nvx-snapshot`, and 1 more.\n", + summary, + ) + push = ci.describe_ci_changes( + "push", changed_files, ci.classify_ci_changes("push", changed_files) + ) + self.assertIn( + "### OpenVMM unit tests: run\n" + "Pushes run every OpenVMM suite whenever the tests run.\n", + push, + ) + documentation = ci.describe_ci_changes( + "pull_request", + ["doc/ci.md"], + ci.classify_ci_changes("pull_request", ["doc/ci.md"]), + ) + self.assertIn( + "### Artifact-backed tests and benchmarks: skip\n" + "Only documentation files changed.\n", + documentation, + ) + self.assertIn( + "### OpenVMM VMM tests: skip\nNo test input changed.\n", documentation ) - for step in (*restore_steps, scratch): - try: - name = _yaml_scalar(step, "name") - except AssertionError: - name = "" - with self.subTest(step=name): - # Other inputs, such as debug-kernel, may select these steps, - # but restore-only must leave them active. - try: - condition = _yaml_scalar(step, "if") - except AssertionError: - continue - self.assertNotIn("restore-only", condition) - def test_ci_runs_the_debug_kernel_on_kvm_per_pull_request_and_all_on_dev(self): + def test_classify_ci_changes_command_prints_every_output(self): + with tempfile.TemporaryDirectory() as temporary: + changed_files = Path(temporary) / "changed-files" + changed_files.write_text("guest/common/init\n", encoding="utf-8") + summary = Path(temporary) / "summary.md" + summary.write_text("before\n", encoding="utf-8") + args = nvx.parse_args( + [ + "classify-ci-changes", + "--event-name", + "pull_request", + "--changed-files", + os.fspath(changed_files), + "--summary", + os.fspath(summary), + ] + ) + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + args.handler(args) + self.assertEqual( + stdout.getvalue().splitlines(), + [ + "run-tests=true", + "run-workloads=true", + "run-openvmm-unit-tests=false", + "run-openvmm-vmm-tests=true", + ], + ) + text = summary.read_text(encoding="utf-8") + self.assertTrue(text.startswith("before\n### ")) + self.assertIn("### OpenVMM VMM tests: run\n", text) + + def test_classify_ci_changes_command_reads_null_delimited_paths(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + changed_files = root / "changed-files" + changed_files.write_bytes( + "doc/notes.md\0kernel/patches/é\r\n.patch\0".encode() + ) + args = nvx.parse_args( + [ + "classify-ci-changes", + "--event-name", + "pull_request", + "--changed-files", + os.fspath(changed_files), + "--null", + "--summary", + os.fspath(root / "summary"), + ] + ) + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + args.handler(args) + self.assertEqual( + stdout.getvalue().splitlines(), + [ + "run-tests=true", + "run-workloads=true", + "run-openvmm-unit-tests=false", + "run-openvmm-vmm-tests=true", + ], + ) + summary = (root / "summary").read_bytes().decode("utf-8") + self.assertIn("kernel/patches/é\r\n.patch", summary) + + @unittest.skipUnless(os.name == "posix", "requires bash and POSIX filenames") + def test_ci_change_detector_classifies_real_git_paths(self): workflow = ( BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" ).read_text(encoding="utf-8") - microvm_workflow = ( - BuildConstants.REPO_ROOT - / ".github" - / "workflows" - / "run-nvx-microvm-tests.yml" - ).read_text(encoding="utf-8") - - artifacts = _workflow_job(workflow, "artifacts") - self.assertNotIn("debug-kernel", artifacts) - self.assertNotIn("vmlinux-debug", artifacts) - # The debug kernel builds on GitHub-hosted capacity beside the shared - # artifacts, under the same condition, and only the debug jobs wait. - debug_build = _workflow_job(workflow, "debug-kernel") - self.assertIn("runs-on: ubuntu-latest", debug_build) - self.assertIn('debug-kernel: "true"', debug_build) - self.assertIn('guest-images: "false"', debug_build) - self.assertIn("name: guest-debug-kernel", debug_build) - for name in ( - KernelBuildConstants.DEBUG_BINARY_NAME, - KernelBuildConstants.DEBUG_CONFIG_NAME, - ): - self.assertIn(f"build/{name}\n", debug_build) - - def condition(job: str) -> str: - return job[job.index(" if: >-\n") : job.index(" runs-on:")] + step = _workflow_step(workflow, "openvmm-changes", "Detect relevant changes") + lines = step.splitlines() + start = lines.index(" run: |") + 1 + script = "\n".join(line[10:] for line in lines[start:]) + classifier = BuildConstants.REPO_ROOT / "scripts" / "nvx.py" + shim = ( + f"python3() {{ {shlex.quote(sys.executable)} " + f'{shlex.quote(os.fspath(classifier))} "${{@:2}}"; }}\n' + ) + cases = ( + ("rename", "doc/input.patch", True), + ("delete", None, True), + ("unicode", "kernel/patches/é.patch", True), + ("newline", "kernel/patches/line\nbreak.patch", True), + ("documentation", "doc/notes.md", False), + ) + for name, destination, test_input in cases: + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) - self.assertEqual(condition(debug_build), condition(artifacts)) - for consumer in ("release", "performance-persist"): - with self.subTest(consumer=consumer): - job = _workflow_job(workflow, consumer) - self.assertIn(" - debug-kernel\n", job) - self.assertIn("needs.debug-kernel.result == 'success' &&", job) - kvm = _workflow_job(workflow, "nvx-microvm-debug-kvm") - self.assertIn("github.event.pull_request.head.repo.full_name", kvm) - for job_name in ci.REQUIRED_CI_MICROVM_DEBUG_PUSH_JOBS: - with self.subTest(job=job_name): - job = _workflow_job(workflow, job_name) - self.assertIn("github.event_name == 'push' &&", job) - self.assertNotIn("pull_request", job) - for job_name in ( - *ci.REQUIRED_CI_MICROVM_DEBUG_JOBS, - *ci.REQUIRED_CI_MICROVM_DEBUG_PUSH_JOBS, - ): - with self.subTest(job=job_name): - job = _workflow_job(workflow, job_name) - self.assertIn("debug-kernel: true", job) - self.assertIn("needs.debug-kernel.result == 'success' &&", job) - self.assertIn(job_name, ci.REQUIRED_CI_RESULT_ENVIRONMENTS) - for consumer in ("release", "performance-persist"): - self.assertIn( - f"needs.{job_name}.result == 'success' ||\n" - f" needs.{job_name}.result == 'skipped'", - _workflow_job(workflow, consumer), + def git(*arguments: str, directory: Path = root) -> str: + result = subprocess.run( + ["git", *arguments], + cwd=directory, + capture_output=True, + text=True, + check=True, ) + return result.stdout.strip() - self.assertIn(" debug-kernel:\n", microvm_workflow) - self.assertIn(" type: boolean\n", microvm_workflow) - download = microvm_workflow.index("name: guest-debug-kernel") - self.assertIn( - "if: inputs.debug-kernel", - microvm_workflow[download - 120 : download], - ) - debug_flag = "${{ inputs.debug-kernel && '--debug-kernel' || '' }}" - self.assertEqual(microvm_workflow.count(debug_flag), 2) - # The debug run replaces every production-kernel workload rather than - # adding to it: each step that drives nvx.py without the debug kernel - # flag boots build/vmlinux, so it must skip the debug jobs (#371). - guard = " && !inputs.debug-kernel" - debug_steps: list[str] = [] - production_steps: list[str] = [] - for step in _workflow_steps(microvm_workflow, "test"): - try: - script = _yaml_scalar(step, "run") - except AssertionError: - continue - if "nvx.py" not in script: - continue - name = _yaml_scalar(step, "name") - try: - step_condition = _yaml_scalar(step, "if") - except AssertionError: - step_condition = "" - with self.subTest(step=name): - if debug_flag in script: - debug_steps.append(name) - self.assertNotIn("debug-kernel", step_condition) - continue - production_steps.append(name) - self.assertTrue(step_condition.endswith(guard), step_condition) - selector = step_condition.removesuffix(guard) - if "||" in selector: - self.assertTrue( - _outer_parentheses_enclose(selector), step_condition - ) - self.assertEqual(len(debug_steps), 2) - self.assertTrue(production_steps) + git("init", "--quiet") + git("config", "user.name", "Reproduction") + git("config", "user.email", "repro@example.invalid") + git("config", "commit.gpgsign", "false") + source = root / "kernel" / "patches" / "input.patch" + source.parent.mkdir(parents=True) + source.write_text("suite input\n", encoding="utf-8") + git("add", ".") + git("commit", "--quiet", "-m", "baseline") + base = git("rev-parse", "HEAD") + if name == "delete": + source.unlink() + else: + assert destination is not None + target = root / destination + target.parent.mkdir(parents=True, exist_ok=True) + if name == "rename": + source.rename(target) + else: + target.write_text("new input\n", encoding="utf-8") + git("add", "--all") + git("commit", "--quiet", "-m", "changed paths") + for event in ("pull_request", "push"): + with self.subTest(event=event): + output = root / f"output-{event}" + environment = os.environ.copy() + environment.update( + EVENT_NAME=event, + BASE_SHA=base, + HEAD_SHA=git("rev-parse", "HEAD"), + GITHUB_OUTPUT=os.fspath(output), + GITHUB_STEP_SUMMARY=os.fspath(root / "summary"), + RUNNER_TEMP=temporary, + ) + result = subprocess.run( + ["bash", "-c", shim + script], + cwd=root, + env=environment, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr) + actual = dict( + line.split("=", 1) + for line in output.read_text(encoding="utf-8").splitlines() + ) + expected = { + "run-tests": str(test_input).lower(), + "run-workloads": str(test_input).lower(), + "run-openvmm-unit-tests": str( + test_input and event == "push" + ).lower(), + "run-openvmm-vmm-tests": str(test_input).lower(), + } + self.assertEqual(actual, expected) - def test_benchmarks_rely_on_the_microvm_correctness_jobs(self): - # #286: the benchmark action ran a second smp-lapic gate before - # acceptance. The microVM correctness jobs already gate the required - # status, the development release, and performance persistence. + @unittest.skipUnless(os.name == "posix", "requires bash") + def test_ci_change_detector_runs_the_full_matrix_unless_classified(self): workflow = ( BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" ).read_text(encoding="utf-8") + step = _workflow_step(workflow, "openvmm-changes", "Detect relevant changes") + lines = step.splitlines() + start = lines.index(" run: |") + 1 + script = "\n".join(line[10:] for line in lines[start:]) + stub = """ +git() { + case "$1" in + cat-file) return "${GIT_CAT_FILE_STATUS}" ;; + diff) + printf '%b' "${CHANGED_FILES}" + return "${GIT_DIFF_STATUS}" + ;; + esac + echo "unexpected git command: $*" >&2 + return 1 +} +python3() { + if [[ -n "${CLASSIFIER:-}" ]]; then + "${CLASSIFIER}" "$@" + return + fi + printf '%b' "${CLASSIFICATION}" + return "${CLASSIFIER_STATUS}" +} +""" + complete = ( + "run-tests=true\\nrun-workloads=true\\n" + "run-openvmm-unit-tests=false\\nrun-openvmm-vmm-tests=false\\n" + ) + everything = dict.fromkeys(ci.CI_CHANGE_OUTPUTS, "true") + classified = { + "run-tests": "true", + "run-workloads": "true", + "run-openvmm-unit-tests": "false", + "run-openvmm-vmm-tests": "false", + } + cases = ( + ("missing base", {"BASE_SHA": ""}, everything), + ("zero base", {"BASE_SHA": "0" * 40}, everything), + ("missing commit", {"GIT_CAT_FILE_STATUS": "1"}, everything), + ("failed diff", {"GIT_DIFF_STATUS": "128"}, everything), + ("failed classifier", {"CLASSIFIER_STATUS": "1"}, everything), + ( + "missing output", + {"CLASSIFICATION": complete.rsplit("run-openvmm-vmm", 1)[0]}, + everything, + ), + ( + "duplicate output", + {"CLASSIFICATION": complete + "run-tests=false\\n"}, + everything, + ), + ( + "malformed output", + {"CLASSIFICATION": complete.replace("=true", "=yes", 1)}, + everything, + ), + ( + "classified", + {"CLASSIFICATION": "notice: ignored\\n" + complete}, + classified, + ), + ( + "real classifier", + { + "CLASSIFIER": sys.executable, + "CHANGED_FILES": "scripts/nvx_tools/microvm_tests.py\\0", + }, + classified, + ), + ) + for name, overrides, expected in cases: + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + environment = os.environ.copy() + environment.update( + { + "EVENT_NAME": "pull_request", + "BASE_SHA": "1" * 40, + "HEAD_SHA": "2" * 40, + "GITHUB_OUTPUT": os.fspath(root / "output"), + "GITHUB_STEP_SUMMARY": os.fspath(root / "summary"), + "RUNNER_TEMP": temporary, + "GIT_CAT_FILE_STATUS": "0", + "GIT_DIFF_STATUS": "0", + "CHANGED_FILES": "openvmm\\0", + "CLASSIFICATION": complete, + "CLASSIFIER_STATUS": "0", + } + ) + environment.update(overrides) + result = subprocess.run( + ["bash", "-c", stub + script], + cwd=BuildConstants.REPO_ROOT, + env=environment, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr) + output = (root / "output").read_text(encoding="utf-8").splitlines() + self.assertEqual(len(output), len(expected), output) + self.assertEqual(dict(line.split("=", 1) for line in output), expected) + summary = (root / "summary").read_text(encoding="utf-8") + if expected is everything: + self.assertIn("### Full matrix: run\n", summary) + else: + self.assertNotIn("Full matrix", summary) + + def test_flowey_downloads_use_retrying_curl(self): action = ( BuildConstants.REPO_ROOT / ".github" / "actions" - / "run-benchmark" + / "setup-curl" / "action.yml" ).read_text(encoding="utf-8") - - self.assertNotIn("test-microvm", action) - self.assertNotIn("lapic-correctness", action) - for job_name in ci.REQUIRED_CI_MICROVM_TEST_JOBS: - with self.subTest(job=job_name): - self.assertIn(job_name, ci.REQUIRED_CI_RESULT_ENVIRONMENTS) - for consumer in ("release", "performance-persist"): - job = _workflow_job(workflow, consumer) - self.assertIn(f" - {job_name}\n", job) - self.assertIn( - f"needs.{job_name}.result == 'success' ||\n" - f" needs.{job_name}.result == 'skipped'", - job, - ) - - def test_ci_runs_openvmm_vmm_tests_per_backend_and_unit_tests_per_os(self): + windows_shim = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "setup-curl" + / "curl-shim.rs" + ).read_text(encoding="utf-8") workflow = ( BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" ).read_text(encoding="utf-8") - vmm_tests_job = _workflow_job(workflow, "openvmm-vmm-tests") - unit_tests_job = _workflow_job(workflow, "openvmm-unit-tests") - - def legs(job: str) -> list[tuple[str, str, str]]: - return re.findall( - r"^ - name: (.+)\n" - r" backend: (\S+)\n" - r" runner: '(.+)'$", - job, - re.MULTILINE, - ) + microvm_workflow = ( + BuildConstants.REPO_ROOT + / ".github" + / "workflows" + / "run-nvx-microvm-tests.yml" + ).read_text(encoding="utf-8") + build_action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "build-openvmm" + / "action.yml" + ).read_text(encoding="utf-8") + for option in ( + "--retry 5", + "--retry-all-errors", + "--retry-delay 2", + "--retry-max-time 90", + ): + self.assertEqual(action.count(option), 1) + for argument in ( + '"--retry"', + '"5"', + '"--retry-all-errors"', + '"--retry-delay"', + '"2"', + '"--retry-max-time"', + '"90"', + ): + self.assertIn(argument, windows_shim) + self.assertIn('system_curl="${NVX_SYSTEM_CURL:-$(command -v curl)}"', action) + self.assertIn("Get-Command curl.exe", action) + self.assertIn('Join-Path $ShimDirectory "curl.exe"', action) + self.assertIn("Get-Command rustc.exe", action) + self.assertNotIn("curl.cmd", action) + for job_name in ( + "openvmm-vmm-tests", + "openvmm-unit-tests", + ): + with self.subTest(job_name=job_name): + self.assertEqual( + _workflow_job(workflow, job_name).count( + "uses: ./.github/actions/setup-curl" + ), + 1, + ) self.assertEqual( - legs(vmm_tests_job), - [ - ( - "Linux / KVM", - "kvm", - '["self-hosted", "linux", "kvm", "virtual-machine"]', - ), - ( - "Linux / MSHV", - "mshv", - '["self-hosted", "linux", "mshv", "virtual-machine"]', - ), - ( - "Windows / WHP", - "whp", - '["self-hosted", "windows", "whp", "virtual-machine"]', - ), - ], - ) - # The unit tests that need /dev/kvm or /dev/mshv are ignored, so one - # Linux runner covers both Linux backends. - self.assertEqual( - legs(unit_tests_job), - [ - ( - "Linux", - "mshv", - '["self-hosted", "linux", "mshv", "virtual-machine"]', - ), - ( - "Windows", - "whp", - '["self-hosted", "windows", "whp", "virtual-machine"]', - ), - ], - ) - self.assertIn( - """ - name: Run OpenVMM unit tests on Linux - if: runner.os != 'Windows' - shell: bash - run: python3 scripts/nvx.py test-openvmm-unit""", - unit_tests_job, - ) - self.assertIn( - """ - name: Run OpenVMM unit tests on Windows - if: runner.os == 'Windows' - shell: powershell - run: python scripts\\nvx.py test-openvmm-unit""", - unit_tests_job, - ) - self.assertEqual( - vmm_tests_job.count( - 'scripts/nvx.py test-openvmm --backend "${{ matrix.backend }}"' - ), - 2, - ) - self.assertEqual( - vmm_tests_job.count( - 'scripts\\nvx.py test-openvmm --backend "${{ matrix.backend }}"' - ), - 1, - ) - self.assertNotIn("test-openvmm-unit", vmm_tests_job) - self.assertEqual(unit_tests_job.count("scripts/nvx.py test-openvmm-unit"), 1) - self.assertEqual( - unit_tests_job.count("scripts\\nvx.py test-openvmm-unit"), + microvm_workflow.count("uses: ./.github/actions/setup-curl"), 1, ) - self.assertNotIn("test-openvmm --backend", unit_tests_job) + self.assertIn("uses: ./.github/actions/setup-curl", build_action) - def test_ci_measures_each_performance_series_on_its_benchmark_runner(self): - # Each backend's pool mixes VMs whose results differ by more than the - # run-to-run noise (#383, #415). The platform jobs, and only they, - # require the label of the one runner per pool that measures a series. - github = BuildConstants.REPO_ROOT / ".github" - workflow = (github / "workflows" / "ci.yml").read_text(encoding="utf-8") - platform_workflow = (github / "workflows" / "run-platform.yml").read_text( - encoding="utf-8" - ) - pools = { - "platform-kvm": ("linux", "kvm"), - "platform-mshv": ("linux", "mshv"), - "platform-whp": ("windows", "whp"), - } - self.assertEqual(set(pools), set(ci.REQUIRED_CI_PLATFORM_JOBS)) - jobs = re.findall( - r"^ ([a-z0-9-]+):$", workflow.split("\njobs:\n", 1)[1], re.MULTILINE - ) - self.assertLessEqual(set(pools), set(jobs)) - for job_name in jobs: - job = _workflow_job(workflow, job_name) - runners = [ - json.loads(labels) - for labels in re.findall(r"^ +runner: '(.+)'$", job, re.MULTILINE) - ] - with self.subTest(job=job_name): - if job_name in pools: - operating_system, backend = pools[job_name] - self.assertIn( - " uses: ./.github/workflows/run-platform.yml\n", job - ) - self.assertEqual( - runners, - [ - [ - "self-hosted", - operating_system, - backend, - "virtual-machine", - "benchmark", - ] - ], - ) - else: - self.assertNotIn("run-platform.yml", job) - for labels in runners: - self.assertNotIn("benchmark", labels) - self.assertIn( - " runs-on: ${{ fromJSON(inputs.runner) }}\n", platform_workflow - ) - self.assertIn( - " - benchmark\n", - (github / "actionlint.yaml").read_text(encoding="utf-8"), - ) + @unittest.skipUnless(os.name == "nt", "Windows-specific curl resolution") + def test_windows_curl_shim_retries_http_500(self): + rustc = shutil.which("rustc") + system_curl = shutil.which("curl.exe") + if rustc is None or system_curl is None: + self.fail("Windows curl shim test requires rustc and curl.exe") - def test_ci_validates_the_cli_once_per_os_after_checking_out_openvmm(self): - # The CLI tests read OpenVMM's pinned CPU profiles from the submodule, - # which a runner's workspace holds at the pinned revision only after - # the job checks it out. The tests do not depend on the backend, so - # one GitHub-hosted job per OS runs them instead of every hypervisor - # job, and the microVM and platform jobs of each OS wait for it. - workflows = BuildConstants.REPO_ROOT / ".github" / "workflows" - workflow = (workflows / "ci.yml").read_text(encoding="utf-8") - self.assertEqual(ci.REQUIRED_CI_CLI_JOBS, ("nvx-cli-linux", "nvx-cli-windows")) - for job_name, runner in ( - ("nvx-cli-linux", "ubuntu-latest"), - ("nvx-cli-windows", "windows-latest"), - ): - with self.subTest(job=job_name): - job = _workflow_job(workflow, job_name) - self.assertIn(f" runs-on: {runner}\n", job) - self.assertNotIn(" needs:", job) - self.assertIn( - "github.event.pull_request.head.repo.full_name " - "== github.repository", - job, - ) - self.assertEqual(job.count("uses: ./.github/actions/validate-nvx"), 1) - for prerequisite in ( - "uses: ./.github/actions/checkout-openvmm", - "uses: actions/setup-python@v6", - ): - self.assertLess( - job.index(prerequisite), - job.index("uses: ./.github/actions/validate-nvx"), - ) - # The release that the self-hosted runners provide. - self.assertIn('python-version: "3.12"', job) - self.assertIn(job_name, ci.REQUIRED_CI_RESULT_ENVIRONMENTS) - windows = _workflow_job(workflow, "nvx-cli-windows") - self.assertIn("rust-toolchain.toml", windows) - self.assertIn("--profile minimal --no-self-update", windows) - self.assertLess( - windows.index("rustup toolchain install"), - windows.index("uses: ./.github/actions/validate-nvx"), - ) + class RetryHandler(http.server.BaseHTTPRequestHandler): + request_count = 0 - configurations = { - name: (workflows / name).read_text(encoding="utf-8") - for name in ( - "ci.yml", - "build-openvmm-binary.yml", - "run-nvx-microvm-tests.yml", - "run-platform.yml", - ) - } - self.assertEqual( - sum( - configuration.count("uses: ./.github/actions/validate-nvx") - for configuration in configurations.values() - ), - len(ci.REQUIRED_CI_CLI_JOBS), - ) - for job_name in ( - *ci.REQUIRED_CI_MICROVM_TEST_JOBS, - *ci.REQUIRED_CI_MICROVM_DEBUG_JOBS, - *ci.REQUIRED_CI_MICROVM_DEBUG_PUSH_JOBS, - *ci.REQUIRED_CI_PLATFORM_JOBS, - ): - cli = "nvx-cli-windows" if job_name.endswith("-whp") else "nvx-cli-linux" - with self.subTest(consumer=job_name): - job = _workflow_job(workflow, job_name) - needs = re.search(r"^ needs: \[(.+)\]$", job, re.MULTILINE) - assert needs is not None - self.assertIn(cli, needs.group(1).split(", ")) - self.assertIn(f" needs.{cli}.result == 'success' &&\n", job) - for other in set(ci.REQUIRED_CI_CLI_JOBS) - {cli}: - self.assertNotIn(other, job) - for job_name in ("openvmm-vmm-tests", "openvmm-unit-tests"): - with self.subTest(job=job_name): - self.assertNotIn("nvx-cli", _workflow_job(workflow, job_name)) - for consumer in ("release", "performance-persist", "required-status-check"): - job = _workflow_job(workflow, consumer) - for cli in ci.REQUIRED_CI_CLI_JOBS: - with self.subTest(consumer=consumer, cli=cli): - self.assertIn(f" - {cli}\n", job) - if consumer != "required-status-check": - self.assertIn( - f" needs.{cli}.result == 'success' &&\n", job - ) + def do_GET(self) -> None: + type(self).request_count += 1 + if type(self).request_count == 1: + self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR) + return + body = b"ok" + self.send_response(http.HTTPStatus.OK) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) - def test_ci_preserves_failed_openvmm_test_diagnostics(self): - workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" - ).read_text(encoding="utf-8") - job = _workflow_job(workflow, "openvmm-vmm-tests") - step_name = " - name: Upload OpenVMM test diagnostics" - self.assertIn(step_name, job) - upload = job.split(step_name, 1)[1].split("\n - name:", 1)[0] + def log_message(self, format: str, *args: object) -> None: + pass - self.assertIn("\n if: failure()", upload) - self.assertIn("uses: actions/upload-artifact@v7", upload) - self.assertIn( - "name: openvmm-vmm-tests-${{ runner.os }}-${{ matrix.backend }}-" - "${{ github.run_id }}-${{ github.run_attempt }}", - upload, - ) - self.assertIn( - "path: ${{ runner.os == 'Windows' && " - "format('{0}/{1}/test_results', runner.temp, matrix.backend) || " - "'openvmm/target/vmm_tests/test_results' }}", - upload, - ) - self.assertIn("if-no-files-found: warn", upload) - self.assertIn("retention-days: 7", upload) - self.assertLess( - job.index(step_name), - job.index(" - name: Report sccache"), - ) - - def test_runner_setup_pins_and_validates_sccache(self): - workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" - ).read_text(encoding="utf-8") - action = ( - BuildConstants.REPO_ROOT / ".github" / "actions" / "sccache" / "action.yml" - ).read_text(encoding="utf-8") - validate_runner = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "validate-runner" - / "action.yml" - ).read_text(encoding="utf-8") - windows_setup = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" - ).read_text(encoding="utf-8") - linux_setup = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" - ).read_text(encoding="utf-8") - manifest = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "tool-versions.conf" - ).read_text(encoding="utf-8") - - # The manifest pins sccache and its checksums, and both runner setups - # install that release; test_tool_versions.py checks the manifest. - self.assertRegex(manifest, r"(?m)^sccache\.version=[0-9]+\.[0-9]+\.[0-9]+$") - for platform in ("linux-x86_64", "windows-x86_64"): - self.assertRegex( - manifest, - rf"(?m)^sccache\.artifacts\.{platform}\.sha256=[0-9a-f]{{64}}$", - ) - self.assertIn("\n tool_manifest_artifact sccache\n", linux_setup) - self.assertIn( - '\n$sccacheRelease = Get-ToolArtifact $ToolManifest "sccache" $ToolPlatform\n', - windows_setup, - ) - for configuration in (windows_setup, linux_setup): - self.assertIn("RUSTC_WRAPPER", configuration) - self.assertIn("CARGO_INCREMENTAL", configuration) - self.assertIn("SCCACHE_DIR", configuration) - self.assertIn("SCCACHE_CACHE_SIZE", configuration) - self.assertIn("sccache --zero-stats", action) - self.assertIn("sccache --show-stats", action) - self.assertIn("sccache --stop-server", action) - self.assertIn("SCCACHE_IDLE_TIMEOUT", action) - self.assertIn("sccache --version", validate_runner) - self.assertNotRegex( - workflow, - r"(?m)^\s+path: openvmm/target\s*$", - ) - self.assertLess( - linux_setup.index('test -f "${runner_directory}/.runner"'), - linux_setup.index('test -w "$runner_sccache_dir"'), - ) - - def test_linux_unnamed_setup_defers_existing_runner_validation(self): - linux_setup = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" - ).read_text(encoding="utf-8") - environment_check = linux_setup.split("check_environment() {\n", 1)[1].split( - "\n}\n", 1 - )[0] - runner_checks = environment_check.split( - " validate_runner_state_paths\n validate_runner_work_paths\n", 1 - )[1] - guard = 'if [ "$configure_runner" = false ] && [ "$check_only" = false ]; then' - self.assertIn(guard, runner_checks) - self.assertLess( - runner_checks.index(guard), - runner_checks.index('test -w "$runner_sccache_dir"'), - ) - if os.name != "posix": - return - - harness = ( - "set -eu\n" - "runner_directory=/missing-runner\n" - "runner_sccache_dir=/missing-runner/_work/_sccache\n" - "validate_runner_state_paths() { echo state-checked; }\n" - "validate_runner_work_paths() { echo work-checked; }\n" - 'run_as_root() { [ "$1" = test ] && [ "$2" = -f ]; }\n' - "check_runner_state() {\n" - " validate_runner_state_paths\n" - " validate_runner_work_paths\n" - f"{runner_checks}\n" - "}\n" - ) - for configure_runner, check_only in ( - ("false", "false"), - ("false", "true"), - ("true", "false"), - ): - with self.subTest(configure_runner=configure_runner, check_only=check_only): + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), RetryHandler) + server_thread = threading.Thread(target=server.serve_forever, daemon=True) + server_thread.start() + try: + with tempfile.TemporaryDirectory() as directory: + shim = Path(directory) / "curl.exe" + shim_source = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "setup-curl" + / "curl-shim.rs" + ) + subprocess.run( + [ + rustc, + "--edition=2021", + "-C", + "opt-level=s", + "-o", + os.fspath(shim), + os.fspath(shim_source), + ], + check=True, + ) + environment = os.environ.copy() + environment["NVX_SYSTEM_CURL"] = system_curl + environment["PATH"] = directory + os.pathsep + environment["PATH"] + port = server.server_address[1] result = subprocess.run( [ - "sh", - "-c", - harness + f"{configure_runner=}\n{check_only=}\n" - "check_runner_state\n", + os.fspath(shim), + "--fail", + "-L", + f"http://127.0.0.1:{port}/asset", ], - capture_output=True, - text=True, - timeout=10, check=False, + capture_output=True, + env=environment, ) - self.assertEqual( - result.returncode == 0, - configure_runner == check_only == "false", - result.stderr, - ) - if result.returncode == 0: - self.assertIn("state-checked", result.stdout) - self.assertIn("work-checked", result.stdout) + finally: + server.shutdown() + server.server_close() + server_thread.join(timeout=5) - def test_linux_setup_installs_openvmm_perl_modules(self): - setup_directory = BuildConstants.REPO_ROOT / "scripts" / "setup" - configurations = ( - (setup_directory / "setup-linux-runner.sh").read_text(encoding="utf-8"), - (setup_directory / "setup-linux-mshv.sh").read_text(encoding="utf-8"), + self.assertFalse( + server_thread.is_alive(), + "HTTP test server did not stop within 5 seconds", ) + self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace")) + self.assertEqual(result.stdout, b"ok") + self.assertEqual(RetryHandler.request_count, 2) - for configuration in configurations: - for package in ( - "perl-FindBin", - "perl-IPC-Cmd", - "perl-Time-Piece", - "perl-lib", - ): - self.assertIn(package, configuration) - - def test_windows_runner_requires_inbox_pcat_firmware(self): - windows_setup = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" + def test_ci_shares_openvmm_inputs_and_binary_artifacts(self): + workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" ).read_text(encoding="utf-8") - validate_runner = ( + build_workflow = ( BuildConstants.REPO_ROOT / ".github" - / "actions" - / "validate-runner" - / "action.yml" + / "workflows" + / "build-openvmm-binary.yml" ).read_text(encoding="utf-8") - - self.assertIn('"Microsoft-Hyper-V"', windows_setup) - for configuration in (windows_setup, validate_runner): - for firmware in ( - "vmfirmwarepcat.dll", - "vmfirmware.dll", - "VmEmulatedDevices.dll", - ): - self.assertIn(firmware, configuration) - - def test_linux_runners_report_the_invariant_tsc_as_evidence(self): - validate_runner = ( + microvm_workflow = ( + BuildConstants.REPO_ROOT + / ".github" + / "workflows" + / "run-nvx-microvm-tests.yml" + ).read_text(encoding="utf-8") + platform_workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / "run-platform.yml" + ).read_text(encoding="utf-8") + build_action = ( BuildConstants.REPO_ROOT / ".github" / "actions" - / "validate-runner" + / "build-openvmm" / "action.yml" ).read_text(encoding="utf-8") - linux_setup = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" - ).read_text(encoding="utf-8") - - # Neither CI nor provisioning rejects a host without an invariant - # TSC: the guest warp probe measures what its guests observe (#265). - step = validate_runner.split(" - name: Report host TSC\n", 1)[1] - step = step.split("\n\n - name: ", 1)[0] - self.assertIn(" if: runner.os != 'Windows'\n", step) - check = "\n".join( - line.removeprefix(" ") - for line in step.split(" run: |\n", 1)[1].splitlines() - ) - function = linux_setup.split("report_invariant_tsc() {\n", 1)[1] - function = "report_invariant_tsc() {\n" + function.split("\n}\n", 1)[0] - function += "\n}\n" - self.assertNotRegex(function, r"\bdie\b") - self.assertLess( - linux_setup.index("report_invariant_tsc /proc/cpuinfo\n"), - linux_setup.index('sudo -n true || die "passwordless sudo is required"'), - ) - if os.name != "posix": - return - with tempfile.TemporaryDirectory() as temporary: - cpuinfo = Path(temporary) / "cpuinfo" - for flags, invariant in ( - ("fpu tsc constant_tsc nonstop_tsc tsc_known_freq", True), - ("fpu tsc constant_tsc tsc_known_freq", False), - ("fpu tsc constant_tsc nonstop_tsc_x", False), - ): - cpuinfo.write_text( - f"model name\t: Test CPU\nflags\t\t: {flags}\n", - encoding="utf-8", - ) - with self.subTest(flags=flags, check="validate-runner"): - result = subprocess.run( - ["bash", "-c", check.replace("/proc/cpuinfo", str(cpuinfo))], - capture_output=True, - text=True, - timeout=10, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("CPU: Test CPU", result.stdout) - self.assertIn( - "TSC flag nonstop_tsc: " - + ("present" if invariant else "absent"), - result.stdout, - ) - self.assertEqual( - "::notice::Runner host does not expose an invariant TSC" - in result.stdout, - not invariant, - ) - self.assertNotIn("::error::", result.stdout + result.stderr) - with self.subTest(flags=flags, check="setup-linux-runner"): - result = subprocess.run( - [ - "sh", - "-c", - f"set -eu\n{function}report_invariant_tsc '{cpuinfo}'\n", - ], - capture_output=True, - text=True, - timeout=10, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual( - "warning: host does not expose an invariant TSC" - in result.stderr, - not invariant, - ) - - def test_runners_qualify_their_host_time_before_every_job(self): - validate_runner = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "validate-runner" - / "action.yml" - ).read_text(encoding="utf-8") - - # The doctor follows the host TSC report on Linux: the backend, the CPU - # fingerprint and generation, and the TSC rate stability. Jobs that - # haven't downloaded OpenVMM skip its CPU profile check (H2) and - # preflight (H3), and only jobs that ask for it run the guest warp - # probe (H6). - self.assertLess( - validate_runner.index(" - name: Report host TSC\n"), - validate_runner.index(" - name: Qualify host time on Linux\n"), + for configuration in (workflow, build_action): + self.assertIn( + "openvmm/flowey-persist/flowey_lib_common__download_gh_release", + configuration, + ) + self.assertIn( + "openvmm/flowey-persist/flowey_lib_common__cache", + configuration, + ) + self.assertIn( + "openvmm-inputs-v1-${{ runner.os }}-${{ runner.arch }}-", + configuration, + ) + producers = { + "build-openvmm-linux-gnu": ( + "kvm", + "openvmm-linux-gnu", + ), + "build-openvmm-linux-musl": ( + "mshv", + "openvmm-linux-musl", + ), + "build-openvmm-windows-msvc": ( + "whp", + "openvmm-windows-msvc", + ), + } + self.assertEqual( + workflow.count("uses: ./.github/workflows/build-openvmm-binary.yml"), + len(producers), ) - for name, shell, command, summary in ( - ( - "Qualify host time on Linux", - "bash", - "python3 scripts/nvx.py doctor", - '--summary "${GITHUB_STEP_SUMMARY}"', + for job_name, (backend, artifact) in producers.items(): + with self.subTest(producer=job_name): + job = _workflow_job(workflow, job_name) + self.assertIn(f"backend: {backend}", job) + self.assertIn(f"artifact: {artifact}", job) + self.assertNotIn("target:", job) + self.assertNotIn("build-mode:", job) + self.assertNotIn("strategy:", job) + + consumers = { + "nvx-microvm-tests-kvm": ( + "build-openvmm-linux-gnu", + "openvmm-linux-gnu", + "run-nvx-microvm-tests.yml", ), - ( - "Qualify host time on Windows", - "powershell", - "python scripts\\nvx.py doctor", - '--summary "$env:GITHUB_STEP_SUMMARY"', + "nvx-microvm-tests-mshv": ( + "build-openvmm-linux-gnu", + "openvmm-linux-gnu", + "run-nvx-microvm-tests.yml", ), - ): - with self.subTest(step=name): - step = validate_runner.split(f" - name: {name}\n", 1)[1] - step = step.split("\n\n", 1)[0] - self.assertIn(f" shell: {shell}\n", step) - self.assertIn(command, step) - self.assertIn('--backend "${{ inputs.backend }}"', step) - self.assertIn("--checks H1 H2 H4\n", step) - self.assertIn( - "${{ inputs.warp-probe == 'true' && 'H6' || '' }}\n", step + "nvx-microvm-tests-whp": ( + "build-openvmm-windows-msvc", + "openvmm-windows-msvc", + "run-nvx-microvm-tests.yml", + ), + "nvx-microvm-debug-kvm": ( + "build-openvmm-linux-gnu", + "openvmm-linux-gnu", + "run-nvx-microvm-tests.yml", + ), + "nvx-microvm-debug-mshv": ( + "build-openvmm-linux-gnu", + "openvmm-linux-gnu", + "run-nvx-microvm-tests.yml", + ), + "nvx-microvm-debug-whp": ( + "build-openvmm-windows-msvc", + "openvmm-windows-msvc", + "run-nvx-microvm-tests.yml", + ), + "platform-kvm": ( + "build-openvmm-linux-gnu", + "openvmm-linux-gnu", + "run-platform.yml", + ), + "platform-mshv": ( + "build-openvmm-linux-musl", + "openvmm-linux-musl", + "run-platform.yml", + ), + "platform-whp": ( + "build-openvmm-windows-msvc", + "openvmm-windows-msvc", + "run-platform.yml", + ), + } + for job_name, (producer, artifact, reusable_workflow) in consumers.items(): + with self.subTest(consumer=job_name): + job = _workflow_job(workflow, job_name) + debug_kernel = ( + "debug-kernel, " + if job_name.startswith("nvx-microvm-debug-") + else "" ) - fingerprint = ( - '"${RUNNER_TEMP}/nvx-cpu-fingerprint.json"' - if shell == "bash" - else '"$env:RUNNER_TEMP\\nvx-cpu-fingerprint.json"' + cli = ( + "nvx-cli-windows" + if producer == "build-openvmm-windows-msvc" + else "nvx-cli-linux" ) self.assertIn( - "${{ inputs.verify-openvmm == 'true' && " - f"'H3 --cpu-fingerprint {fingerprint}' || '--no-openvmm' }}}}\n", - step, + f"needs: [artifacts, {debug_kernel}{producer}, {cli}, " + "openvmm-changes]", + job, ) - self.assertIn("--ci-schedule\n", step) - self.assertIn(summary, step) - args = nvx.parse_args( - ["doctor", "--backend", "kvm", "--checks", "H1", "H2", "H4", "H3"] - + ["--cpu-fingerprint", "fingerprint.json", "--ci-schedule"] + self.assertIn(f"needs.{producer}.result == 'success'", job) + self.assertIn(f"needs.{cli}.result == 'success'", job) + self.assertIn(f"uses: ./.github/workflows/{reusable_workflow}", job) + self.assertIn(f"openvmm-artifact: {artifact}", job) + for unrelated_producer in producers.keys() - {producer}: + self.assertNotIn(unrelated_producer, job) + + self.assertIn("build/openvmm.provenance.json", build_workflow) + self.assertIn( + "name: ${{ inputs.artifact }}-executable", + build_workflow, ) - self.assertEqual(args.checks, ["H1", "H2", "H4", "H3"]) - self.assertEqual(args.cpu_fingerprint, Path("fingerprint.json")) - args = nvx.parse_args( - ["doctor", "--backend", "kvm", "--checks", "H1", "H2", "H4", "H6", "H3"] - + ["--cpu-fingerprint", "fingerprint.json", "--ci-schedule"] + self.assertIn( + "name: ${{ inputs.artifact }}-provenance", + build_workflow, ) - self.assertEqual(args.checks, ["H1", "H2", "H4", "H6", "H3"]) - args = nvx.parse_args( - ["doctor", "--backend", "kvm", "--checks", "H1", "H2", "H4"] - + ["--no-openvmm", "--ci-schedule"] + self.assertEqual( + build_workflow.count("uses: ./.github/actions/build-openvmm"), + 1, ) - self.assertTrue(args.no_openvmm) - self.assertTrue(args.ci_schedule) - self.assertIn(" verify-openvmm:\n", validate_runner) - self.assertIn(' default: "false"\n', validate_runner) - warp_probe = validate_runner.split(" warp-probe:\n", 1)[1] - warp_probe = warp_probe.split("\nruns:\n", 1)[0] - self.assertIn("requires\n verify-openvmm\n", warp_probe) - self.assertIn(' default: "false"\n', warp_probe) - # A failed CPU profile check keeps OpenVMM's fingerprint. - upload = validate_runner.split(" - name: Upload the CPU fingerprint\n")[1] - upload = upload.split("\n\n", 1)[0] - self.assertIn("if: failure() && inputs.verify-openvmm == 'true'\n", upload) - self.assertIn("path: ${{ runner.temp }}/nvx-cpu-fingerprint.json\n", upload) - self.assertIn("if-no-files-found: ignore\n", upload) - # Jobs that run OpenVMM qualify the runner after downloading it. - benchmark = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "run-benchmark" - / "action.yml" - ).read_text(encoding="utf-8") - self.assertNotIn("download-artifact", benchmark) - for workflow_name in ("run-nvx-microvm-tests.yml", "run-platform.yml"): - with self.subTest(workflow=workflow_name): - workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / workflow_name - ).read_text(encoding="utf-8") - validate = workflow.index(" - name: Validate runner\n") - self.assertEqual(workflow.count(" - name: Validate runner\n"), 1) - self.assertIn( - 'verify-openvmm: "true"', workflow[validate : validate + 220] - ) - # The microVM scenarios probe after every boot and restore; - # the benchmarks boot guests without such probes. - self.assertEqual( - 'warp-probe: "true"' in workflow[validate : validate + 260], - workflow_name == "run-platform.yml", - ) - for step in ( - " - name: Download guest artifacts\n", - " - name: Download OpenVMM executable\n", - " - name: Make OpenVMM executable\n", - ): - self.assertLess(workflow.index(step), validate) - self.assertLess( - validate_runner.index("Validate Linux toolchain"), - validate_runner.index("Qualify host time on Linux"), - ) - - def test_runner_setups_install_backend_native_openvmm_targets(self): - linux_setup = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" - ).read_text(encoding="utf-8") - windows_setup = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" - ).read_text(encoding="utf-8") - - for target in ( - "x86_64-unknown-none", - "x86_64-unknown-uefi", + self.assertIn("backend: ${{ inputs.backend }}", build_workflow) + self.assertNotIn("target: ${{ inputs.target }}", build_workflow) + self.assertNotIn("build-mode: ${{ inputs.build-mode }}", build_workflow) + self.assertEqual(build_workflow.count("overwrite: true"), 2) + self.assertEqual(build_workflow.count("retention-days: 1"), 2) + # The microVM workflow also downloads nvx-rs, which the benchmarks + # don't run. + for consumer_workflow, download_count in ( + (microvm_workflow, 5), + (platform_workflow, 3), ): - self.assertIn(target, linux_setup) - self.assertIn(target, windows_setup) - self.assertIn("x86_64-unknown-linux-musl", linux_setup) - for cross_platform_tool in ( - "x86_64-pc-windows-gnu", - "gcc-mingw-w64-x86-64-win32", - "mingw64-gcc", - "x86_64-w64-mingw32-gcc", - "x86_64-w64-mingw32-dlltool", + self.assertIn("path: openvmm/target/release", consumer_workflow) + self.assertIn("path: build", consumer_workflow) + self.assertEqual( + consumer_workflow.count("uses: actions/download-artifact@v8"), + download_count, + ) + self.assertIn( + "run: chmod +x openvmm/target/release/openvmm", + consumer_workflow, + ) + for configuration in ( + workflow, + build_workflow, + microvm_workflow, + platform_workflow, ): - self.assertNotIn(cross_platform_tool, linux_setup) + self.assertNotIn("path: .", configuration) + self.assertIn("openvmm-binary-v6-${{ inputs.backend }}-", build_action) + self.assertNotIn("openvmm-binary-v5-", build_action) + self.assertNotIn("inputs.target", build_action) + self.assertNotIn("inputs.build-mode", build_action) + self.assertNotIn("rustup target add", build_action) + self.assertNotIn("X86_64_UNKNOWN_LINUX_MUSL", build_action) + self.assertEqual( + build_action.count( + 'python3 scripts/nvx.py build-openvmm --backend "${{ inputs.backend }}"' + ), + 1, + ) + self.assertEqual( + build_action.count( + 'python scripts\\nvx.py build-openvmm --backend "${{ inputs.backend }}"' + ), + 1, + ) + self.assertIn("Verify OpenVMM provenance on Linux", build_action) + self.assertIn("Verify OpenVMM provenance on Windows", build_action) + self.assertNotIn("nvx-microvm-tests-v1", workflow) + self.assertNotIn("cargo-v2-", build_action) + self.assertNotIn("openvmm-tests-v2-", workflow) + self.assertNotIn("Restore OpenVMM test build", workflow) + self.assertNotIn("Save OpenVMM test build", workflow) + self.assertEqual( + build_workflow.count("uses: ./.github/actions/sccache"), + 2, + ) + for job_name, cache_namespace in ( + ("openvmm-vmm-tests", "vmm-tests"), + ("openvmm-unit-tests", "unit-tests"), + ): + with self.subTest(job_name=job_name): + job = _workflow_job(workflow, job_name) + self.assertEqual( + job.count("uses: ./.github/actions/sccache"), + 2, + ) + self.assertIn( + f"key: openvmm-inputs-v1-${{{{ runner.os }}}}-" + f"${{{{ runner.arch }}}}-{cache_namespace}-" + "${{ steps.openvmm.outputs.sha }}", + job, + ) + self.assertIn( + f"key: openvmm-cargo-v1-${{{{ runner.os }}}}-" + f"${{{{ runner.arch }}}}-{cache_namespace}-" + "${{ hashFiles('openvmm/Cargo.lock') }}", + job, + ) + self.assertIn( + "restore-keys: |\n" + " openvmm-inputs-v1-${{ runner.os }}-" + "${{ runner.arch }}-", + job, + ) + self.assertIn( + "restore-keys: |\n" + " openvmm-cargo-v1-${{ runner.os }}-" + "${{ runner.arch }}-", + job, + ) + self.assertNotIn("uses: actions/cache@v5", workflow) + self.assertNotIn("uses: actions/cache@v5", build_workflow) + self.assertNotIn("uses: actions/cache@v5", build_action) - def test_linux_runner_setup_adds_the_benchmark_label_on_request(self): - # Provisioning replaces a runner's registration, so the benchmark - # runner keeps the label that CI's platform jobs require only when - # the setup script registers it. - linux_setup = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" - ).read_text(encoding="utf-8") - self.assertIn( - " --benchmark-runner)\n" - " benchmark_runner=true\n" - " shift\n" - " ;;\n", - linux_setup, + release_job = _workflow_job(workflow, "release") + performance_gate_job = _workflow_job(workflow, "performance-gate") + performance_persist_job = _workflow_job(workflow, "performance-persist") + for job_name in consumers: + self.assertIn(f" - {job_name}", release_job) + self.assertIn(f"needs.{job_name}.result", release_job) + self.assertIn(f" - {job_name}", performance_persist_job) + self.assertIn(f"needs.{job_name}.result", performance_persist_job) + for job_name in ("platform-kvm", "platform-mshv", "platform-whp"): + self.assertIn(job_name, performance_gate_job) + self.assertIn(f"needs.{job_name}.result", performance_gate_job) + + def test_copilot_setup_restores_ci_cache_entries(self): + github = BuildConstants.REPO_ROOT / ".github" + setup = (github / "workflows" / "copilot-setup-steps.yml").read_text( + encoding="utf-8" ) - self.assertIn("\n --benchmark-runner ", linux_setup) - # Registration and check mode both use the computed label set. - self.assertIn('--labels "$expected_runner_labels"', linux_setup) - self.assertIn( - '"$expected_runner_labels" ] ||\n' - ' die "GitHub Actions runner labels are not validated"', - linux_setup, + workflow = (github / "workflows" / "ci.yml").read_text(encoding="utf-8") + build_workflow = (github / "workflows" / "build-openvmm-binary.yml").read_text( + encoding="utf-8" ) - start = linux_setup.index('\n[ "$benchmark_runner" = false ]') - end = linux_setup.index("\nfi\n", start) + len("\nfi\n") - labels = linux_setup[start:end] - self.assertIn( - "\nexpected_runner_labels=linux,${backend},virtual-machine\n", labels + build_action = (github / "actions" / "build-openvmm" / "action.yml").read_text( + encoding="utf-8" ) - if os.name != "posix": - return + checkout_action = ( + github / "actions" / "checkout-openvmm" / "action.yml" + ).read_text(encoding="utf-8") - harness = ( - "set -eu\n" - "die() { printf 'error: %s\\n' \"$*\" >&2; exit 1; }\n" - "backend=$1\nrunner_name=$2\nbenchmark_runner=$3\n" - "runner_directory=/opt/nvx-runner\n" - f"{labels}" - 'printf "%s\\n" "$expected_runner_labels"\n' + # Guest artifact keys stay in sync because both sides use one action. + guest_artifacts = _workflow_step( + setup, "copilot-setup-steps", "Restore guest artifacts built by CI" ) - for backend, runner_name, benchmark_runner, expected in ( - ("kvm", "azure-kvm-1", "false", "linux,kvm,virtual-machine"), - ("mshv", "", "false", "linux,mshv,virtual-machine"), - ("kvm", "azure-kvm-3", "true", "linux,kvm,virtual-machine,benchmark"), - ("mshv", "azure-mshv-4", "true", "linux,mshv,virtual-machine,benchmark"), - ("kvm", "", "true", None), - ): - with self.subTest( - backend=backend, - runner_name=runner_name, - benchmark_runner=benchmark_runner, - ): - result = subprocess.run( - ["sh", "-c", harness, "sh", backend, runner_name, benchmark_runner], - capture_output=True, - text=True, - timeout=10, - check=False, + self.assertEqual( + _yaml_field(guest_artifacts, "uses"), + ["./.github/actions/build-guest-artifacts"], + ) + self.assertEqual(_yaml_field(guest_artifacts, "restore-only"), ['"true"']) + + # The OpenVMM binary restore repeats the cache entry that the Linux GNU + # producer saves. A different key, path list, or compression method + # would make every session miss it without failing setup. + producer = _workflow_job(workflow, "build-openvmm-linux-gnu") + [backend] = _yaml_field(producer, "backend") + [binary] = _yaml_field(producer, "binary") + self.assertIn("binary-path: ${{ inputs.binary }}", build_workflow) + saved = _composite_action_step(build_action, "Save OpenVMM binary on Linux") + restored = _workflow_step( + setup, "copilot-setup-steps", "Restore the OpenVMM binary built by CI" + ) + self.assertEqual( + _yaml_field(restored, "key"), + [ + key.replace("${{ inputs.backend }}", backend).replace( + "steps.openvmm.outputs.sha", "steps.openvmm-cache.outputs.sha" ) - if expected is None: - self.assertEqual(result.returncode, 1) - self.assertEqual( - result.stderr, - "error: --benchmark-runner requires --runner-name\n", - ) - else: - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, f"{expected}\n") + for key in _yaml_field(saved, "key") + ], + ) + self.assertEqual( + _yaml_field(restored, "path"), + [ + path.replace("${{ inputs.binary-path }}", binary) + for path in _yaml_field(saved, "path") + ], + ) - def test_windows_runner_setup_adds_the_benchmark_label_on_request(self): - setup = BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" - windows_setup = setup.read_text(encoding="utf-8") - self.assertIn(" [switch]$BenchmarkRunner,\n", windows_setup) - self.assertIn( - "if ($BenchmarkRunner -and\n" - " (-not $RunnerOnly -or [string]::IsNullOrWhiteSpace($RunnerName))) {\n" - ' throw "-BenchmarkRunner requires -RunnerOnly and -RunnerName"\n' - "}\n", - windows_setup, + prepare = _workflow_step( + setup, "copilot-setup-steps", "Prepare OpenVMM cache restore" ) - # Registration and check mode share one label set. - self.assertEqual(windows_setup.count('"windows,whp,virtual-machine"'), 1) - for function_name, assignment in ( - ("Install-ActionsRunner", "\n $labels = Get-ActionsRunnerLabels\n"), - ( - "Assert-ActionsRunner", - "\n $expectedLabels = Get-ActionsRunnerLabels\n", - ), - ): - with self.subTest(function=function_name): - function = windows_setup.split(f"\nfunction {function_name} {{\n", 1)[1] - self.assertIn(assignment, function.split("\n}\n", 1)[0]) - if os.name != "nt": - return + for revision_source in (checkout_action, prepare): + self.assertIn("sha=$(git rev-parse :openvmm)", revision_source) - harness = f""" -Set-StrictMode -Version 2.0 -$ErrorActionPreference = "Stop" -$tokens = $null -$errors = $null -$ast = [System.Management.Automation.Language.Parser]::ParseFile( - '{setup}', - [ref]$tokens, - [ref]$errors -) -if ($errors.Count -ne 0) {{ - throw "setup script has parse errors" -}} -$definition = $ast.Find({{ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq "Get-ActionsRunnerLabels" - }}, $true) -. ([scriptblock]::Create($definition.Extent.Text)) -$BenchmarkRunner = $false -Write-Output (Get-ActionsRunnerLabels) -$BenchmarkRunner = $true -Write-Output (Get-ActionsRunnerLabels) -""" - result = subprocess.run( - ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", harness], - capture_output=True, - text=True, - timeout=30, - check=False, - ) + def tool_links(configuration: str) -> list[str]: + return [ + line.strip() + for line in configuration.splitlines() + if line.strip().startswith("ln -sf ") + ] - self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( - result.stdout.splitlines(), - [ - "windows,whp,virtual-machine", - "windows,whp,virtual-machine,benchmark", - ], - ) - - def test_repository_pins_one_exact_rust_toolchain(self): - root = BuildConstants.REPO_ROOT - [toolchain] = re.findall( - r'^channel = "([^"]*)"$', - (root / "rust-toolchain.toml").read_text(encoding="utf-8"), - re.MULTILINE, + _yaml_field(saved, "PATH"), + ["${{ steps.cache-tools-linux.outputs.path }}"], ) - # A floating channel such as `stable` lets runners drift apart (#402). - self.assertRegex(toolchain, r"^[0-9]+\.[0-9]+\.[0-9]+$") - self.assertEqual(OpenVMMBuildConstants.RUST_TOOLCHAIN, toolchain) - self.assertIn("rust-toolchain.toml", ReleaseBuildConstants.PROJECT_SOURCE_PATHS) - - setup = root / "scripts" / "setup" - # The setup scripts and the Specula runner setup take the release from - # the setup tool manifest, which rustup cannot read. self.assertEqual( - re.findall( - r"^rust\.toolchain=(.*)$", - (setup / "tool-versions.conf").read_text(encoding="utf-8"), - re.MULTILINE, - ), - [toolchain], - ) - for path, reader in ( - (setup / "setup-linux-runner.sh", "\n RUST_TOOLCHAIN=$tool_value\n"), - (setup / "setup-linux-mshv.sh", "\n RUST_TOOLCHAIN=$tool_value\n"), - ( - setup / "setup-windows-whp.ps1", - '\n$RustToolchain = Get-ToolManifestValue $ToolManifest "rust.toolchain"\n', - ), - ( - root / ".github" / "specula" / "setup-runner.sh", - '\nRUST_VERSION="$(tool_value rust.toolchain "$VERSION_PATTERN")"\n', - ), - ): - with self.subTest(path=path.name): - self.assertIn(reader, path.read_text(encoding="utf-8")) - - def test_ci_takes_rust_toolchain_from_repository_pin(self): - github = BuildConstants.REPO_ROOT / ".github" - workflows = github / "workflows" - - validate_runner = ( - github / "actions" / "validate-runner" / "action.yml" - ).read_text(encoding="utf-8") - for step_name in ("Validate Linux toolchain", "Validate Windows toolchain"): - with self.subTest(step=step_name): - step = _composite_action_step(validate_runner, step_name) - self.assertIn("rust-toolchain.toml", step) - # Report a missing toolchain without writing the trusted RUSTUP_HOME. - self.assertEqual(_yaml_field(step, "RUSTUP_AUTO_INSTALL"), ['"0"']) - - for crate in ("aci-edge-sandboxes", "nvx-rs"): - with self.subTest(crate=crate): - crate_action = ( - github / "actions" / f"check-{crate}" / "action.yml" - ).read_text(encoding="utf-8") - self.assertEqual(_yaml_field(crate_action, "default"), ['""']) - install = _composite_action_step( - crate_action, "Install Rust toolchains" - ) - self.assertIn("../rust-toolchain.toml", install) - self.assertIn("REQUESTED_TOOLCHAIN: ${{ inputs.toolchain }}", install) - self.assertEqual(crate_action.count("${{ inputs.toolchain }}"), 1) - - setup = (workflows / "copilot-setup-steps.yml").read_text(encoding="utf-8") - self.assertIn(" - rust-toolchain.toml\n", setup) - self.assertIn( - "rust-toolchain.toml", - _workflow_step( - setup, "copilot-setup-steps", "Resolve pinned tool versions" - ), - ) - rust = _workflow_step(setup, "copilot-setup-steps", "Install Rust toolchains") - self.assertIn('rustup default "${RUST_TOOLCHAIN}"', rust) - self.assertNotIn("stable", rust) - - # A binary built by another compiler must not satisfy the cache. - build_action = (github / "actions" / "build-openvmm" / "action.yml").read_text( - encoding="utf-8" + _yaml_field(restored, "PATH"), + ["${{ steps.openvmm-cache.outputs.path }}"], ) self.assertEqual( - build_action.count( - "${{ hashFiles('rust-toolchain.toml') }}-${{ steps.openvmm.outputs.sha }}" + tool_links(prepare), + tool_links( + _composite_action_step(build_action, "Prepare Linux cache tools") ), - 4, ) - # A new release reruns every suite that it builds. - for pattern in ( - ci.CI_TEST_INPUT_PATHS, - ci.OPENVMM_UNIT_TEST_INPUT_PATHS, - ci.OPENVMM_VMM_TEST_INPUT_PATHS, - ): - self.assertIsNotNone(pattern.match("rust-toolchain.toml")) - - def test_release_actions_use_deterministic_immutable_tooling(self): - package_action = ( + def test_guest_artifact_restore_only_guards_builds_and_cache_saves(self): + action = ( BuildConstants.REPO_ROOT / ".github" / "actions" - / "package-release" + / "build-guest-artifacts" / "action.yml" ).read_text(encoding="utf-8") - publish_action = ( + steps = _composite_action_steps(action) + guarded_steps = [ + step + for step in steps + if "docker build" in step or "uses: actions/cache/save@" in step + ] + + self.assertTrue(guarded_steps) + for step in guarded_steps: + try: + name = _yaml_scalar(step, "name") + except AssertionError: + name = "" + with self.subTest(step=name): + condition = _yaml_scalar(step, "if") + guard = "inputs.restore-only != 'true' && " + self.assertTrue(condition.startswith(guard), condition) + cache_condition = condition.removeprefix(guard) + self.assertIn("cache-hit != 'true'", cache_condition) + if "||" in cache_condition: + self.assertTrue( + _outer_parentheses_enclose(cache_condition), + cache_condition, + ) + + def test_guest_artifact_restore_only_leaves_restore_and_scratch_steps_active(self): + action = ( BuildConstants.REPO_ROOT / ".github" / "actions" - / "publish-development-release" + / "build-guest-artifacts" / "action.yml" ).read_text(encoding="utf-8") + steps = _composite_action_steps(action) + restore_steps = [ + step for step in steps if "uses: actions/cache/restore@" in step + ] - self.assertEqual(package_action.count("archive-release"), 2) - self.assertIn( - "python scripts\\nvx.py materialize-kernel-provenance-inputs", - package_action, - ) - self.assertLess( - package_action.index( - "python scripts\\nvx.py materialize-kernel-provenance-inputs" - ), - package_action.index("- name: Package Windows release"), - ) - self.assertNotIn("git checkout-index", package_action) - self.assertNotIn("tar -czf", package_action) - self.assertNotIn("Compress-Archive", package_action) - self.assertIn( - "python3 -u scripts/publish_development_release.py", - publish_action, - ) - self.assertNotIn("--clobber", publish_action) - - def test_ci_artifact_actions_use_node24(self): - configurations = "\n".join( - path.read_text(encoding="utf-8") - for pattern in ("*.yml", "*.yaml") - for path in (BuildConstants.REPO_ROOT / ".github").rglob(pattern) + self.assertTrue(restore_steps) + scratch = _composite_action_step( + action, "Prepare Ubuntu sandbox scratch template" ) + for step in (*restore_steps, scratch): + try: + name = _yaml_scalar(step, "name") + except AssertionError: + name = "" + with self.subTest(step=name): + # Other inputs, such as debug-kernel, may select these steps, + # but restore-only must leave them active. + try: + condition = _yaml_scalar(step, "if") + except AssertionError: + continue + self.assertNotIn("restore-only", condition) - self.assertNotRegex(configurations, r"actions/upload-artifact@v[1-5]\b") - self.assertNotRegex(configurations, r"actions/download-artifact@v[1-6]\b") - - def test_benchmark_diagnostics_preserve_each_workflow_attempt(self): - action = ( - Path(__file__).parents[1] + def test_ci_runs_the_debug_kernel_on_kvm_per_pull_request_and_all_on_dev(self): + workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" + ).read_text(encoding="utf-8") + microvm_workflow = ( + BuildConstants.REPO_ROOT / ".github" - / "actions" - / "run-benchmark" - / "action.yml" + / "workflows" + / "run-nvx-microvm-tests.yml" ).read_text(encoding="utf-8") - diagnostics = _composite_action_step(action, "Upload benchmark diagnostics") - name = ( - "benchmark-diagnostics-${{ inputs.platform }}-${{ github.run_id }}" - "-attempt-${{ github.run_attempt }}" - ) - self.assertIn(" if: always()", diagnostics) - self.assertIn(" uses: actions/upload-artifact@v7", diagnostics) - self.assertIn(" path: data/runs/${{ inputs.platform }}", diagnostics) - self.assertIn(f" name: {name}", diagnostics) - self.assertIn(" if-no-files-found: error", diagnostics) - self.assertIn(" overwrite: false", diagnostics) - self.assertIn(" retention-days: 1", diagnostics) - self.assertLess( - action.index("- name: Upload benchmark diagnostics"), - action.index("- name: Upload benchmark results"), - ) - - artifacts: dict[str, tuple[str, int]] = {} - for attempt in (1, 2): - for platform in ( - "linux-kvm-virtual-machine", - "linux-mshv-virtual-machine", - "windows-whp-virtual-machine", - ): - artifact = ( - name.replace("${{ inputs.platform }}", platform) - .replace("${{ github.run_id }}", "35771477786") - .replace("${{ github.run_attempt }}", str(attempt)) - ) - self.assertNotIn("${{", artifact) - self.assertNotIn(artifact, artifacts) - artifacts[artifact] = (platform, attempt) - self.assertEqual(len(artifacts), 6) - - def test_benchmark_result_handoff_keeps_run_scoped_names(self): - actions = Path(__file__).parents[1] / ".github" / "actions" - action = (actions / "run-benchmark" / "action.yml").read_text(encoding="utf-8") - prepare = (actions / "prepare-performance-results" / "action.yml").read_text( - encoding="utf-8" - ) - results = _composite_action_step(action, "Upload benchmark results") - name = "benchmark-${{ inputs.platform }}-${{ github.run_id }}" - - self.assertIn(f" name: {name}", results) - self.assertIn(" path: data/runs/${{ inputs.platform }}", results) - self.assertIn(" overwrite: true", results) - self.assertNotIn("github.run_attempt", results) - self.assertNotIn("github.run_attempt", prepare) - for platform in ( - "linux-kvm-virtual-machine", - "linux-mshv-virtual-machine", - "windows-whp-virtual-machine", + artifacts = _workflow_job(workflow, "artifacts") + self.assertNotIn("debug-kernel", artifacts) + self.assertNotIn("vmlinux-debug", artifacts) + # The debug kernel builds on GitHub-hosted capacity beside the shared + # artifacts, under the same condition, and only the debug jobs wait. + debug_build = _workflow_job(workflow, "debug-kernel") + self.assertIn("runs-on: ubuntu-latest", debug_build) + self.assertIn('debug-kernel: "true"', debug_build) + self.assertIn('guest-images: "false"', debug_build) + self.assertIn("name: guest-debug-kernel", debug_build) + for name in ( + KernelBuildConstants.DEBUG_BINARY_NAME, + KernelBuildConstants.DEBUG_CONFIG_NAME, ): - with self.subTest(platform=platform): - artifact = name.replace("${{ inputs.platform }}", platform) - self.assertIn(f" name: {artifact}", prepare) + self.assertIn(f"build/{name}\n", debug_build) - def test_benchmarks_check_the_expected_host_before_measuring(self): - # A series' history comes from the host in data/-host.json, - # so a platform job refuses another host before it measures (#526). - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "run-benchmark" - / "action.yml" - ).read_text(encoding="utf-8") - steps = re.findall(r"^ - name: (.+)$", action, re.MULTILINE) - output = ( - '--output "data/runs/${{ inputs.platform }}/' - '${{ inputs.platform }}-host.json"' - ) + def condition(job: str) -> str: + return job[job.index(" if: >-\n") : job.index(" runs-on:")] - self.assertEqual(steps[0], "Check Linux benchmark host") - self.assertLess( - steps.index("Prepare Windows benchmark scratch"), - steps.index("Check Windows benchmark host"), - ) - for operating_system, condition in ( - ("Linux", "runner.os != 'Windows'"), - ("Windows", "runner.os == 'Windows'"), + self.assertEqual(condition(debug_build), condition(artifacts)) + for consumer in ("release", "performance-persist"): + with self.subTest(consumer=consumer): + job = _workflow_job(workflow, consumer) + self.assertIn(" - debug-kernel\n", job) + self.assertIn("needs.debug-kernel.result == 'success' &&", job) + kvm = _workflow_job(workflow, "nvx-microvm-debug-kvm") + self.assertIn("github.event.pull_request.head.repo.full_name", kvm) + for job_name in ci.REQUIRED_CI_MICROVM_DEBUG_PUSH_JOBS: + with self.subTest(job=job_name): + job = _workflow_job(workflow, job_name) + self.assertIn("github.event_name == 'push' &&", job) + self.assertNotIn("pull_request", job) + for job_name in ( + *ci.REQUIRED_CI_MICROVM_DEBUG_JOBS, + *ci.REQUIRED_CI_MICROVM_DEBUG_PUSH_JOBS, ): - check = f"Check {operating_system} benchmark host" - with self.subTest(step=check): - self.assertIn( - f" if: {condition}", _composite_action_step(action, check) - ) - script = _composite_action_script(action, check) - self.assertEqual(script.count("performance check-host"), 1) - self.assertIn('--platform "${{ inputs.platform }}"', script) - self.assertIn("--history-dir data", script) - self.assertIn(output, script) - self.assertIn("--summary", script) - measuring = [ - step - for step in steps - if step.startswith(f"Run {operating_system} ") - ] - self.assertTrue(measuring) - for step in measuring: - self.assertLess(steps.index(check), steps.index(step)) - windows = _composite_action_script(action, "Check Windows benchmark host") - self.assertIn( - '$ScratchArgs = @("--scratch-dir", $env:NVX_BENCHMARK_SCRATCH_DIR)', - windows, - ) - self.assertIn("@ScratchArgs", windows) - self.assertTrue( - windows.rstrip().endswith("if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }") - ) - self.assertIn( - "set -euo pipefail", - _composite_action_script(action, "Check Linux benchmark host"), - ) - - def test_performance_history_tracks_the_expected_hosts(self): - actions = BuildConstants.REPO_ROOT / ".github" / "actions" - persist = (actions / "persist-performance-results" / "action.yml").read_text( - encoding="utf-8" - ) - gate = (actions / "gate-performance" / "action.yml").read_text(encoding="utf-8") + with self.subTest(job=job_name): + job = _workflow_job(workflow, job_name) + self.assertIn("debug-kernel: true", job) + self.assertIn("needs.debug-kernel.result == 'success' &&", job) + self.assertIn(job_name, ci.REQUIRED_CI_RESULT_ENVIRONMENTS) + for consumer in ("release", "performance-persist"): + self.assertIn( + f"needs.{job_name}.result == 'success' ||\n" + f" needs.{job_name}.result == 'skipped'", + _workflow_job(workflow, consumer), + ) - # dev refuses results whose recorded host differs from data/. - self.assertIn( - " --history-dir data \\\n --require-host\n", - _composite_action_step(persist, "Persist results"), - ) - # The gate compares the pull request's expected hosts with the base - # branch's, so that a new host must reset its series. - load = _composite_action_script(gate, "Load base branch performance history") - self.assertIn("done < <(git ls-tree --name-only FETCH_HEAD data/)", load) - self.assertIn("data/*-host.json)", load) + self.assertIn(" debug-kernel:\n", microvm_workflow) + self.assertIn(" type: boolean\n", microvm_workflow) + download = microvm_workflow.index("name: guest-debug-kernel") self.assertIn( - 'git show "FETCH_HEAD:${path}" > "data/baseline/${path#data/}"', load + "if: inputs.debug-kernel", + microvm_workflow[download - 120 : download], ) - command = _composite_action_step(gate, "Gate on performance regressions") - self.assertIn("--baseline-dir data/baseline", command) - self.assertIn("--history-reset-dir data", command) + debug_flag = "${{ inputs.debug-kernel && '--debug-kernel' || '' }}" + self.assertEqual(microvm_workflow.count(debug_flag), 2) + # The debug run replaces every production-kernel workload rather than + # adding to it: each step that drives nvx.py without the debug kernel + # flag boots build/vmlinux, so it must skip the debug jobs (#371). + guard = " && !inputs.debug-kernel" + debug_steps: list[str] = [] + production_steps: list[str] = [] + for step in _workflow_steps(microvm_workflow, "test"): + try: + script = _yaml_scalar(step, "run") + except AssertionError: + continue + if "nvx.py" not in script: + continue + name = _yaml_scalar(step, "name") + try: + step_condition = _yaml_scalar(step, "if") + except AssertionError: + step_condition = "" + with self.subTest(step=name): + if debug_flag in script: + debug_steps.append(name) + self.assertNotIn("debug-kernel", step_condition) + continue + production_steps.append(name) + self.assertTrue(step_condition.endswith(guard), step_condition) + selector = step_condition.removesuffix(guard) + if "||" in selector: + self.assertTrue( + _outer_parentheses_enclose(selector), step_condition + ) + self.assertEqual(len(debug_steps), 2) + self.assertTrue(production_steps) - def test_ci_remeasures_only_unstable_lifecycle_results(self): + def test_benchmarks_rely_on_the_microvm_correctness_jobs(self): + # #286: the benchmark action ran a second smp-lapic gate before + # acceptance. The microVM correctness jobs already gate the required + # status, the development release, and performance persistence. + workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" + ).read_text(encoding="utf-8") action = ( BuildConstants.REPO_ROOT / ".github" @@ -5548,11494 +4956,7987 @@ def test_ci_remeasures_only_unstable_lifecycle_results(self): / "action.yml" ).read_text(encoding="utf-8") - self.assertEqual(action.count("performance validate-openvmm"), 2) - for step, loop, unstable in ( - ( - "Run Linux acceptance test", - "for attempt in 1 2; do", - '"${validation_status}" -ne 75', - ), - ( - "Run Windows acceptance test", - "foreach ($Attempt in 1, 2)", - "$ValidationStatus -ne 75", - ), - ): - with self.subTest(step=step): - script = _composite_action_script(action, step) - self.assertEqual(script.count("performance validate-openvmm"), 1) - self.assertEqual(script.count(loop), 1) - self.assertEqual(script.count(unstable), 1) - self.assertEqual( - script.count("Lifecycle snapshot generation was unstable"), - 1, - ) - self.assertEqual(script.count("--attempt-log"), 1) - self.assertIn("acceptance-attempts.json", script) - # Publication names the attempt that supplied the lifecycle metrics. - for step, log in ( - ( - "Publish Linux performance results", - '--lifecycle-attempt-log "${run_dir}/acceptance-attempts.json"', - ), - ( - "Publish Windows performance results", - '--lifecycle-attempt-log "$RunDir/acceptance-attempts.json"', - ), - ): - with self.subTest(step=step): - script = _composite_action_script(action, step) - self.assertEqual(script.count(log), 1) - self.assertLess(script.index("--lifecycle-input"), script.index(log)) - - def test_windows_benchmarks_use_provisioned_data_volume_scratch(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "run-benchmark" - / "action.yml" - ).read_text(encoding="utf-8") - setup = ( - BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" + self.assertNotIn("test-microvm", action) + self.assertNotIn("lapic-correctness", action) + for job_name in ci.REQUIRED_CI_MICROVM_TEST_JOBS: + with self.subTest(job=job_name): + self.assertIn(job_name, ci.REQUIRED_CI_RESULT_ENVIRONMENTS) + for consumer in ("release", "performance-persist"): + job = _workflow_job(workflow, consumer) + self.assertIn(f" - {job_name}\n", job) + self.assertIn( + f"needs.{job_name}.result == 'success' ||\n" + f" needs.{job_name}.result == 'skipped'", + job, + ) + + def test_ci_runs_openvmm_vmm_tests_per_backend_and_unit_tests_per_os(self): + workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" ).read_text(encoding="utf-8") - prepare = _composite_action_step(action, "Prepare Windows benchmark scratch") - cleanup = _composite_action_step(action, "Remove Windows benchmark scratch") + vmm_tests_job = _workflow_job(workflow, "openvmm-vmm-tests") + unit_tests_job = _workflow_job(workflow, "openvmm-unit-tests") - self.assertIn('$BenchmarkScratchVariable = "NVX_BENCHMARK_SCRATCH"', setup) + def legs(job: str) -> list[tuple[str, str, str]]: + return re.findall( + r"^ - name: (.+)\n" + r" backend: (\S+)\n" + r" runner: '(.+)'$", + job, + re.MULTILINE, + ) + + self.assertEqual( + legs(vmm_tests_job), + [ + ( + "Linux / KVM", + "kvm", + '["self-hosted", "linux", "kvm", "virtual-machine"]', + ), + ( + "Linux / MSHV", + "mshv", + '["self-hosted", "linux", "mshv", "virtual-machine"]', + ), + ( + "Windows / WHP", + "whp", + '["self-hosted", "windows", "whp", "virtual-machine"]', + ), + ], + ) + # The unit tests that need /dev/kvm or /dev/mshv are ignored, so one + # Linux runner covers both Linux backends. + self.assertEqual( + legs(unit_tests_job), + [ + ( + "Linux", + "mshv", + '["self-hosted", "linux", "mshv", "virtual-machine"]', + ), + ( + "Windows", + "whp", + '["self-hosted", "windows", "whp", "virtual-machine"]', + ), + ], + ) self.assertIn( - '[Environment]::GetEnvironmentVariable("NVX_BENCHMARK_SCRATCH", "Machine")', - prepare, + """ - name: Run OpenVMM unit tests on Linux + if: runner.os != 'Windows' + shell: bash + run: python3 scripts/nvx.py test-openvmm-unit""", + unit_tests_job, ) - self.assertIn('"NVX_BENCHMARK_SCRATCH_DIR=$Scratch"', prepare) - self.assertIn("::warning::NVX_BENCHMARK_SCRATCH is not provisioned", prepare) - self.assertIn(" if: always() && runner.os == 'Windows'", cleanup) - self.assertLess( - action.index("- name: Remove Windows benchmark scratch"), - action.index("- name: Upload benchmark diagnostics"), + self.assertIn( + """ - name: Run OpenVMM unit tests on Windows + if: runner.os == 'Windows' + shell: powershell + run: python scripts\\nvx.py test-openvmm-unit""", + unit_tests_job, ) - for step in ( - "Run Windows acceptance test", - "Run Windows performance suite", - "Run Windows multi-vCPU shell restore", - "Run Windows device operation rates", - ): - with self.subTest(step=step): - script = _composite_action_script(action, step) - self.assertIn( - '$ScratchArgs = @("--scratch-dir", $env:NVX_BENCHMARK_SCRATCH_DIR)', - script, - ) - self.assertIn("@ScratchArgs", script) - self.assertLess( - action.index("- name: Prepare Windows benchmark scratch"), - action.index(f"- name: {step}"), - ) + self.assertEqual( + vmm_tests_job.count( + 'scripts/nvx.py test-openvmm --backend "${{ matrix.backend }}"' + ), + 2, + ) + self.assertEqual( + vmm_tests_job.count( + 'scripts\\nvx.py test-openvmm --backend "${{ matrix.backend }}"' + ), + 1, + ) + self.assertNotIn("test-openvmm-unit", vmm_tests_job) + self.assertEqual(unit_tests_job.count("scripts/nvx.py test-openvmm-unit"), 1) + self.assertEqual( + unit_tests_job.count("scripts\\nvx.py test-openvmm-unit"), + 1, + ) + self.assertNotIn("test-openvmm --backend", unit_tests_job) + + def test_ci_measures_each_performance_series_on_its_benchmark_runner(self): + # Each backend's pool mixes VMs whose results differ by more than the + # run-to-run noise (#383, #415). The platform jobs, and only they, + # require the label of the one runner per pool that measures a series. + github = BuildConstants.REPO_ROOT / ".github" + workflow = (github / "workflows" / "ci.yml").read_text(encoding="utf-8") + platform_workflow = (github / "workflows" / "run-platform.yml").read_text( + encoding="utf-8" + ) + pools = { + "platform-kvm": ("linux", "kvm"), + "platform-mshv": ("linux", "mshv"), + "platform-whp": ("windows", "whp"), + } + self.assertEqual(set(pools), set(ci.REQUIRED_CI_PLATFORM_JOBS)) + jobs = re.findall( + r"^ ([a-z0-9-]+):$", workflow.split("\njobs:\n", 1)[1], re.MULTILINE + ) + self.assertLessEqual(set(pools), set(jobs)) + for job_name in jobs: + job = _workflow_job(workflow, job_name) + runners = [ + json.loads(labels) + for labels in re.findall(r"^ +runner: '(.+)'$", job, re.MULTILINE) + ] + with self.subTest(job=job_name): + if job_name in pools: + operating_system, backend = pools[job_name] + self.assertIn( + " uses: ./.github/workflows/run-platform.yml\n", job + ) + self.assertEqual( + runners, + [ + [ + "self-hosted", + operating_system, + backend, + "virtual-machine", + "benchmark", + ] + ], + ) + else: + self.assertNotIn("run-platform.yml", job) + for labels in runners: + self.assertNotIn("benchmark", labels) self.assertIn( - " Configure-SccacheEnvironment\n Configure-BenchmarkScratch\n", - setup, + " runs-on: ${{ fromJSON(inputs.runner) }}\n", platform_workflow ) self.assertIn( - "Assert-ServiceDirectoryAcl -Path $SccacheDirectory -Writable\n" - " Assert-BenchmarkScratch\n", - setup, + " - benchmark\n", + (github / "actionlint.yaml").read_text(encoding="utf-8"), ) - @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") - def test_windows_acceptance_forwards_benchmark_scratch(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "run-benchmark" - / "action.yml" - ).read_text(encoding="utf-8") - script = _composite_action_script(action, "Run Windows acceptance test") - for name, value in ( - ("backend", "whp"), - ("platform", "windows-whp-virtual-machine"), - ("warmups", "1"), - ("runs", "10"), - ("teardown-mode", "guest-exit"), + def test_ci_validates_the_cli_once_per_os_after_checking_out_openvmm(self): + # The CLI tests read OpenVMM's pinned CPU profiles from the submodule, + # which a runner's workspace holds at the pinned revision only after + # the job checks it out. The tests do not depend on the backend, so + # one GitHub-hosted job per OS runs them instead of every hypervisor + # job, and the microVM and platform jobs of each OS wait for it. + workflows = BuildConstants.REPO_ROOT / ".github" / "workflows" + workflow = (workflows / "ci.yml").read_text(encoding="utf-8") + self.assertEqual(ci.REQUIRED_CI_CLI_JOBS, ("nvx-cli-linux", "nvx-cli-windows")) + for job_name, runner in ( + ("nvx-cli-linux", "ubuntu-latest"), + ("nvx-cli-windows", "windows-latest"), ): - script = script.replace("${{ inputs." + name + " }}", value) - stub = """ -function python { - $global:LASTEXITCODE = 0 - if ($args[1] -ne "benchmark") { - return - } - $output = $args[[Array]::IndexOf($args, "--output") + 1] - $index = [Array]::IndexOf($args, "--scratch-dir") - $scratch = "" - if ($index -ge 0) { - $scratch = $args[$index + 1] - } - New-Item -ItemType Directory -Path (Split-Path $output) -Force | Out-Null - Set-Content -LiteralPath $output -Encoding UTF8 -Value ( - ConvertTo-Json -Compress @{ scratch = $scratch } - ) -} -""" - for scratch in ("", r"F:\nvx-benchmark-scratch\job"): - with ( - self.subTest(scratch=scratch), - tempfile.TemporaryDirectory() as temporary, - ): - root = Path(temporary).resolve() - environment = os.environ.copy() - environment.pop("NVX_BENCHMARK_SCRATCH_DIR", None) - if scratch: - environment["NVX_BENCHMARK_SCRATCH_DIR"] = scratch - result = subprocess.run( - [ - "powershell.exe", - "-NoProfile", - "-NonInteractive", - "-Command", - stub + script, - ], - cwd=root, - env=environment, - capture_output=True, - text=True, - timeout=30, - check=False, + with self.subTest(job=job_name): + job = _workflow_job(workflow, job_name) + self.assertIn(f" runs-on: {runner}\n", job) + self.assertNotIn(" needs:", job) + self.assertIn( + "github.event.pull_request.head.repo.full_name " + "== github.repository", + job, ) + self.assertEqual(job.count("uses: ./.github/actions/validate-nvx"), 1) + for prerequisite in ( + "uses: ./.github/actions/checkout-openvmm", + "uses: actions/setup-python@v6", + ): + self.assertLess( + job.index(prerequisite), + job.index("uses: ./.github/actions/validate-nvx"), + ) + # The release that the self-hosted runners provide. + self.assertIn('python-version: "3.12"', job) + self.assertIn(job_name, ci.REQUIRED_CI_RESULT_ENVIRONMENTS) + windows = _workflow_job(workflow, "nvx-cli-windows") + self.assertIn("rust-toolchain.toml", windows) + self.assertIn("--profile minimal --no-self-update", windows) + self.assertLess( + windows.index("rustup toolchain install"), + windows.index("uses: ./.github/actions/validate-nvx"), + ) - self.assertEqual(result.returncode, 0, result.stderr) - accepted = ( - root - / "data" - / "runs" - / "windows-whp-virtual-machine" - / "microvm-v2" - / "1vcpu" - / "acceptance.json" + configurations = { + name: (workflows / name).read_text(encoding="utf-8") + for name in ( + "ci.yml", + "build-openvmm-binary.yml", + "run-nvx-microvm-tests.yml", + "run-platform.yml", + ) + } + self.assertEqual( + sum( + configuration.count("uses: ./.github/actions/validate-nvx") + for configuration in configurations.values() + ), + len(ci.REQUIRED_CI_CLI_JOBS), + ) + for job_name in ( + *ci.REQUIRED_CI_MICROVM_TEST_JOBS, + *ci.REQUIRED_CI_MICROVM_DEBUG_JOBS, + *ci.REQUIRED_CI_MICROVM_DEBUG_PUSH_JOBS, + *ci.REQUIRED_CI_PLATFORM_JOBS, + ): + cli = "nvx-cli-windows" if job_name.endswith("-whp") else "nvx-cli-linux" + with self.subTest(consumer=job_name): + job = _workflow_job(workflow, job_name) + needs = re.search(r"^ needs: \[(.+)\]$", job, re.MULTILINE) + assert needs is not None + self.assertIn(cli, needs.group(1).split(", ")) + self.assertIn(f" needs.{cli}.result == 'success' &&\n", job) + for other in set(ci.REQUIRED_CI_CLI_JOBS) - {cli}: + self.assertNotIn(other, job) + for job_name in ("openvmm-vmm-tests", "openvmm-unit-tests"): + with self.subTest(job=job_name): + self.assertNotIn("nvx-cli", _workflow_job(workflow, job_name)) + for consumer in ("release", "performance-persist", "required-status-check"): + job = _workflow_job(workflow, consumer) + for cli in ci.REQUIRED_CI_CLI_JOBS: + with self.subTest(consumer=consumer, cli=cli): + self.assertIn(f" - {cli}\n", job) + if consumer != "required-status-check": + self.assertIn( + f" needs.{cli}.result == 'success' &&\n", job + ) + + def test_ci_hands_nvx_rs_to_the_microvm_and_platform_jobs(self): + # nvx.py runs nvx-rs for its sandbox command and for run's egress and + # artifact steps, and finds it where `cargo build --release` puts it. + # Each NVX CLI job builds it for its OS, records its provenance, tests + # with it, and hands both to the microVM jobs, which run it, and to the + # platform jobs, which package it, as the OpenVMM build jobs hand + # over OpenVMM. + github = BuildConstants.REPO_ROOT / ".github" + workflow = (github / "workflows" / "ci.yml").read_text(encoding="utf-8") + microvm = (github / "workflows" / "run-nvx-microvm-tests.yml").read_text( + encoding="utf-8" + ) + platform = (github / "workflows" / "run-platform.yml").read_text( + encoding="utf-8" + ) + package = (github / "actions" / "package-release" / "action.yml").read_text( + encoding="utf-8" + ) + builds = { + "nvx-cli-linux": ( + "nvx-rs-linux-musl", + "nvx-rs/target/release/nvx-rs", + "x86_64-unknown-linux-musl", + ), + "nvx-cli-windows": ( + "nvx-rs-windows-msvc", + "nvx-rs/target/release/nvx-rs.exe", + "x86_64-pc-windows-msvc", + ), + } + current = builds["nvx-cli-windows" if os.name == "nt" else "nvx-cli-linux"] + self.assertEqual( + nvx_rs.binary_path().relative_to(BuildConstants.REPO_ROOT).as_posix(), + current[1], + ) + # Packages ship the build for the release target of their OS. + self.assertEqual(nvx_rs.release_target(), current[2]) + self.assertEqual( + set(nvx_rs.RELEASE_TARGETS.values()), + {target for _, _, target in builds.values()}, + ) + for job_name, (artifact, path, target) in builds.items(): + with self.subTest(job=job_name): + job = _workflow_job(workflow, job_name) + build = _workflow_step(workflow, job_name, "Build nvx-rs") + record = _workflow_step(workflow, job_name, "Record nvx-rs provenance") + upload = _workflow_step(workflow, job_name, "Upload nvx-rs executable") + provenance = _workflow_step( + workflow, job_name, "Upload nvx-rs provenance" ) - self.assertEqual( - json.loads(accepted.read_text(encoding="utf-8-sig")), - {"scratch": scratch}, + for option in ("--release", "--locked", "--features cli"): + self.assertIn(option, build) + self.assertIn("--manifest-path nvx-rs/Cargo.toml", build) + self.assertIn( + f"record-nvx-rs-provenance --target {target}", + _yaml_scalar(record, "run"), ) - - @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") - def test_windows_benchmark_scratch_steps_manage_per_job_directories(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "run-benchmark" - / "action.yml" - ).read_text(encoding="utf-8") - machine_lookup = ( - '[Environment]::GetEnvironmentVariable("NVX_BENCHMARK_SCRATCH", "Machine")' + self.assertIn(f"name: {artifact}-executable\n", upload) + self.assertIn(f"path: {path}\n", upload) + self.assertIn(f"name: {artifact}-provenance\n", provenance) + self.assertIn(f"path: build/{nvx_rs.PROVENANCE_NAME}\n", provenance) + for step in (upload, provenance): + self.assertIn("if-no-files-found: error", step) + # The build is recorded as it is built, the CLI tests run with + # it, and only a validated build leaves the job. + validate = job.index("uses: ./.github/actions/validate-nvx") + self.assertLess( + job.index("- name: Build nvx-rs"), + job.index("- name: Record nvx-rs provenance"), + ) + self.assertLess(job.index("- name: Record nvx-rs provenance"), validate) + self.assertGreater( + job.index("- name: Upload nvx-rs executable"), validate + ) + self.assertGreater( + job.index("- name: Upload nvx-rs provenance"), validate + ) + # Self-contained executables run on every self-hosted runner. + linux = _workflow_step(workflow, "nvx-cli-linux", "Build nvx-rs") + self.assertEqual(linux.count("x86_64-unknown-linux-musl"), 3) + self.assertIn("rust-toolchain.toml", linux) + self.assertIn( + "install -D -m 0755 nvx-rs/target/x86_64-unknown-linux-musl/release/" + "nvx-rs \\\n nvx-rs/target/release/nvx-rs", + linux, + ) + # The tests that run nvx-rs directly need NVX_RS to name the build. + self.assertIn( + 'echo "NVX_RS=${GITHUB_WORKSPACE}/nvx-rs/target/release/nvx-rs" ' + '>> "${GITHUB_ENV}"', + linux, + ) + windows = _workflow_job(workflow, "nvx-cli-windows") + windows_build = _workflow_step(workflow, "nvx-cli-windows", "Build nvx-rs") + self.assertIn("RUSTFLAGS: -C target-feature=+crt-static", windows_build) + self.assertIn( + '"NVX_RS=$env:GITHUB_WORKSPACE\\nvx-rs\\target\\release\\nvx-rs.exe" |\n' + " Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append", + windows_build, + ) + self.assertLess( + windows.index("- name: Install the pinned Rust release"), + windows.index("- name: Build nvx-rs"), ) - prepare = _composite_action_script( - action, "Prepare Windows benchmark scratch" - ).replace("${{ inputs.platform }}", "windows-whp-virtual-machine") - cleanup = _composite_action_script(action, "Remove Windows benchmark scratch") - self.assertIn(machine_lookup, prepare) - def run_step(script: str, environment: dict[str, str]): - return subprocess.run( - ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script], - env=environment, - capture_output=True, - text=True, - timeout=30, - check=False, - ) + for job_name in ( + *ci.REQUIRED_CI_MICROVM_TEST_JOBS, + *ci.REQUIRED_CI_MICROVM_DEBUG_JOBS, + *ci.REQUIRED_CI_MICROVM_DEBUG_PUSH_JOBS, + *ci.REQUIRED_CI_PLATFORM_JOBS, + ): + cli = "nvx-cli-windows" if job_name.endswith("-whp") else "nvx-cli-linux" + with self.subTest(consumer=job_name): + job = _workflow_job(workflow, job_name) + self.assertIn( + "uses: ./.github/workflows/run-platform.yml" + if job_name in ci.REQUIRED_CI_PLATFORM_JOBS + else "uses: ./.github/workflows/run-nvx-microvm-tests.yml", + job, + ) + self.assertEqual( + re.findall(r"^ nvx-rs-artifact: (\S+)$", job, re.MULTILINE), + [builds[cli][0]], + ) - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() / "scratch" - root.mkdir() - stale = root / "windows-whp-virtual-machine-1-1" - fresh = root / "windows-whp-virtual-machine-2-1" - for directory in (stale, fresh): - directory.mkdir() - (directory / "memory.bin").write_bytes(b"ram") - old = time.time() - 7 * 60 * 60 - os.utime(stale, (old, old)) - github_env = Path(temporary) / "github-env.txt" - github_env.write_text("", encoding="utf-8") - environment = os.environ.copy() - environment.update( - { - "GITHUB_ENV": str(github_env), - "GITHUB_RUN_ID": "35805840174", - "GITHUB_RUN_ATTEMPT": "2", - } + for configuration in (microvm, platform): + self.assertIn( + " nvx-rs-artifact:\n" + " description: Base name of the nvx-rs artifacts for the " + "runner's OS\n" + " required: true\n" + " type: string\n", + configuration, ) + download = microvm.index(" - name: Download nvx-rs executable\n") + self.assertIn( + " uses: actions/download-artifact@v8\n" + " with:\n" + " name: ${{ inputs.nvx-rs-artifact }}-executable\n" + " path: nvx-rs/target/release\n", + microvm[download:], + ) + chmod = microvm.index(" - name: Make nvx-rs executable\n") + self.assertIn( + " if: runner.os != 'Windows'\n" + " shell: bash\n" + " run: chmod +x nvx-rs/target/release/nvx-rs\n", + microvm[chmod:], + ) + self.assertLess(download, chmod) + self.assertLess(chmod, microvm.index("scripts/nvx.py")) - result = run_step( - prepare.replace(machine_lookup, f"'{root}'"), - environment, + # The platform jobs package nvx-rs with its provenance: they download + # both where nvx.py package looks for them, before they package. + self.assertIn( + " nvx-rs-artifact: ${{ inputs.nvx-rs-artifact }}", + _workflow_step(platform, "run", "Package release").splitlines(), + ) + steps = _composite_action_steps(package) + names = [_yaml_scalar(step, "name") for step in steps] + executable = steps[names.index("Download nvx-rs executable")] + provenance = steps[names.index("Download nvx-rs provenance")] + self.assertIn("name: ${{ inputs.nvx-rs-artifact }}-executable\n", executable) + self.assertIn("path: nvx-rs/target/release\n", executable) + self.assertIn("name: ${{ inputs.nvx-rs-artifact }}-provenance\n", provenance) + self.assertIn("path: build\n", provenance) + self.assertIn( + "run: chmod +x nvx-rs/target/release/nvx-rs", + steps[names.index("Make nvx-rs executable")], + ) + for name in ("Package Linux release", "Package Windows release"): + self.assertLess( + names.index("Download nvx-rs executable"), names.index(name) ) - - self.assertEqual(result.returncode, 0, result.stderr) - scratch = root / "windows-whp-virtual-machine-35805840174-2" - self.assertTrue(scratch.is_dir()) - self.assertFalse(stale.exists()) - self.assertTrue(fresh.is_dir()) - self.assertEqual( - github_env.read_text(encoding="utf-8-sig").splitlines(), - [f"NVX_BENCHMARK_SCRATCH_DIR={scratch}"], + self.assertLess( + names.index("Download nvx-rs provenance"), names.index(name) ) + self.assertLess(names.index("Make nvx-rs executable"), names.index(name)) - locked = scratch / "locked.bin" - with locked.open("wb"): - result = run_step( - cleanup, - {**environment, "NVX_BENCHMARK_SCRATCH_DIR": str(scratch)}, + def test_pull_requests_package_releases_that_only_dev_uploads(self): + # Pull requests package and archive each platform's release, which + # proves that it builds; only a dev push uploads it for publication. + github = BuildConstants.REPO_ROOT / ".github" + platform = (github / "workflows" / "run-platform.yml").read_text( + encoding="utf-8" + ) + package = (github / "actions" / "package-release" / "action.yml").read_text( + encoding="utf-8" + ) + step = _workflow_step(platform, "run", "Package release") + self.assertIn(" if: inputs.package-release\n", step) + self.assertNotIn("github.event_name", _yaml_scalar(step, "if")) + self.assertIn( + " upload: ${{ github.event_name == 'push' && " + "github.ref == 'refs/heads/dev' }}", + step, + ) + steps = _composite_action_steps(package) + names = [_yaml_scalar(step, "name") for step in steps] + for name, operating_system in ( + ("Upload Linux release", "runner.os != 'Windows'"), + ("Upload Windows release", "runner.os == 'Windows'"), + ): + with self.subTest(step=name): + self.assertEqual( + _yaml_scalar(steps[names.index(name)], "if"), + f"{operating_system} && inputs.upload == 'true'", ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn( - "::warning::Could not remove benchmark scratch", result.stdout - ) - - result = run_step( - cleanup, - {**environment, "NVX_BENCHMARK_SCRATCH_DIR": str(scratch)}, - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertFalse(scratch.exists()) + for name in ("Package Linux release", "Package Windows release"): + self.assertNotIn("upload", _yaml_scalar(steps[names.index(name)], "if")) + self.assertIn("--binary-only", steps[names.index(name)]) + self.assertIn("archive-release", steps[names.index(name)]) - github_env.write_text("", encoding="utf-8") - result = run_step(prepare.replace(machine_lookup, "''"), environment) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn( - "::warning::NVX_BENCHMARK_SCRATCH is not provisioned", result.stdout - ) - self.assertEqual(github_env.read_text(encoding="utf-8"), "") + def test_ci_preserves_failed_openvmm_test_diagnostics(self): + workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" + ).read_text(encoding="utf-8") + job = _workflow_job(workflow, "openvmm-vmm-tests") + step_name = " - name: Upload OpenVMM test diagnostics" + self.assertIn(step_name, job) + upload = job.split(step_name, 1)[1].split("\n - name:", 1)[0] - @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") - def test_windows_setup_selects_largest_data_volume_for_benchmark_scratch(self): - setup = BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" - harness = f""" -Set-StrictMode -Version 2.0 -$ErrorActionPreference = "Stop" -$tokens = $null -$errors = $null -$ast = [System.Management.Automation.Language.Parser]::ParseFile( - '{setup}', - [ref]$tokens, - [ref]$errors -) -if ($errors.Count -ne 0) {{ - throw "setup script has parse errors" -}} -foreach ($name in "Test-SystemVolumePath", "Get-BenchmarkScratchDirectory") {{ - $definition = $ast.Find({{ - param($node) - $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and - $node.Name -eq $name - }}, $true) - . ([scriptblock]::Create($definition.Extent.Text)) -}} -$BenchmarkScratchVariable = "NVX_TEST_UNSET_{uuid.uuid4().hex}" -$BenchmarkScratchName = "nvx-benchmark-scratch" -$BenchmarkScratchDirectory = $null -$env:SystemDrive = "C:" -$Volumes = @( - [pscustomobject]@{{ DriveLetter = [char]"C"; DriveType = "Fixed"; FileSystem = "NTFS"; Size = 900GB }}, - [pscustomobject]@{{ DriveLetter = [char]"D"; DriveType = "Fixed"; FileSystem = "NTFS"; Size = 64GB }}, - [pscustomobject]@{{ DriveLetter = $null; DriveType = "Fixed"; FileSystem = "NTFS"; Size = 2TB }}, - [pscustomobject]@{{ DriveLetter = [char]"E"; DriveType = "CD-ROM"; FileSystem = ""; Size = 1TB }}, - [pscustomobject]@{{ DriveLetter = [char]"F"; DriveType = "Fixed"; FileSystem = "NTFS"; Size = 512GB }}, - [pscustomobject]@{{ DriveLetter = [char]"G"; DriveType = "Fixed"; FileSystem = "FAT32"; Size = 1TB }} -) -function Get-Volume {{ - $script:Volumes -}} -Write-Output (Get-BenchmarkScratchDirectory) -$Volumes = @($Volumes[0]) -Write-Output ("none=" + ($null -eq (Get-BenchmarkScratchDirectory))) -$BenchmarkScratchDirectory = "H:\\explicit\\scratch" -Write-Output (Get-BenchmarkScratchDirectory) -""" - - result = subprocess.run( - ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", harness], - capture_output=True, - text=True, - timeout=30, - check=False, + self.assertIn("\n if: failure()", upload) + self.assertIn("uses: actions/upload-artifact@v7", upload) + self.assertIn( + "name: openvmm-vmm-tests-${{ runner.os }}-${{ matrix.backend }}-" + "${{ github.run_id }}-${{ github.run_attempt }}", + upload, ) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual( - result.stdout.splitlines(), - [ - r"F:\nvx-benchmark-scratch", - "none=True", - r"H:\explicit\scratch", - ], + self.assertIn( + "path: ${{ runner.os == 'Windows' && " + "format('{0}/{1}/test_results', runner.temp, matrix.backend) || " + "'openvmm/target/vmm_tests/test_results' }}", + upload, + ) + self.assertIn("if-no-files-found: warn", upload) + self.assertIn("retention-days: 7", upload) + self.assertLess( + job.index(step_name), + job.index(" - name: Report sccache"), ) - def test_cli_validation_runs_every_script_test_module(self): - # Each scripts/test_*.py module must run in both CLI validation jobs; - # test_control_session.py once ran in neither (#229). + def test_runner_setup_pins_and_validates_sccache(self): + workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" + ).read_text(encoding="utf-8") action = ( + BuildConstants.REPO_ROOT / ".github" / "actions" / "sccache" / "action.yml" + ).read_text(encoding="utf-8") + validate_runner = ( BuildConstants.REPO_ROOT / ".github" / "actions" - / "validate-nvx" + / "validate-runner" / "action.yml" ).read_text(encoding="utf-8") - modules = { - path.name - for path in (BuildConstants.REPO_ROOT / "scripts").glob("test_*.py") - } - self.assertIn("test_control_session.py", modules) - for step in ("Validate NVX CLI on Linux", "Validate NVX CLI on Windows"): - with self.subTest(step=step): - script = _composite_action_script(action, step) - invoked = re.findall(r"(?:^|\s)scripts[/\\](test_\w+\.py)", script) - self.assertEqual(sorted(invoked), sorted(modules)) + windows_setup = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" + ).read_text(encoding="utf-8") + linux_setup = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" + ).read_text(encoding="utf-8") + manifest = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "tool-versions.conf" + ).read_text(encoding="utf-8") - def test_ci_runs_the_public_sandbox_acceptance_on_every_backend(self): - workflows = BuildConstants.REPO_ROOT / ".github" / "workflows" - workflow = (workflows / "ci.yml").read_text(encoding="utf-8") - microvm_workflow = (workflows / "run-nvx-microvm-tests.yml").read_text( - encoding="utf-8" + # The manifest pins sccache and its checksums, and both runner setups + # install that release; test_tool_versions.py checks the manifest. + self.assertRegex(manifest, r"(?m)^sccache\.version=[0-9]+\.[0-9]+\.[0-9]+$") + for platform in ("linux-x86_64", "windows-x86_64"): + self.assertRegex( + manifest, + rf"(?m)^sccache\.artifacts\.{platform}\.sha256=[0-9a-f]{{64}}$", + ) + self.assertIn("\n tool_manifest_artifact sccache\n", linux_setup) + self.assertIn( + '\n$sccacheRelease = Get-ToolArtifact $ToolManifest "sccache" $ToolPlatform\n', + windows_setup, ) - for backend in ("kvm", "mshv", "whp"): - with self.subTest(backend=backend): - job = _workflow_job(workflow, f"nvx-microvm-tests-{backend}") - self.assertIn( - "uses: ./.github/workflows/run-nvx-microvm-tests.yml", job - ) - self.assertIn(f"backend: {backend}", job) - self.assertNotIn("debug-kernel: true", job) - for system in ("Linux", "Windows"): - with self.subTest(system=system): - acceptance = _workflow_step( - microvm_workflow, - "test", - f"Run public managed sandbox acceptance on {system}", - ) - self.assertIn("!inputs.debug-kernel", acceptance) - self.assertIn("--scenario managed-exec-config", acceptance) - self.assertIn("--scenario sandbox-lifecycle", acceptance) - self.assertIn( - '--output-dir "build/test-results/public-exec-${{ inputs.backend }}"', - acceptance, - ) - smoke = _workflow_step( - microvm_workflow, - "test", - f"Run Ubuntu sandbox layer smoke test on {system}", - ) - for option in ( - "--entrypoint /sbin/nvx-sandbox-smoke", - "--arg limits", - "--arg 33554432", - "--arg 8", - "--memory-max 33554432", - "--pids-max 8", - ): - self.assertIn(option, smoke) - upload = _workflow_step( - microvm_workflow, "test", "Upload NVX microVM failure logs" + for configuration in (windows_setup, linux_setup): + self.assertIn("RUSTC_WRAPPER", configuration) + self.assertIn("CARGO_INCREMENTAL", configuration) + self.assertIn("SCCACHE_DIR", configuration) + self.assertIn("SCCACHE_CACHE_SIZE", configuration) + self.assertIn("sccache --zero-stats", action) + self.assertIn("sccache --show-stats", action) + self.assertIn("sccache --stop-server", action) + self.assertIn("SCCACHE_IDLE_TIMEOUT", action) + self.assertIn("sccache --version", validate_runner) + self.assertNotRegex( + workflow, + r"(?m)^\s+path: openvmm/target\s*$", + ) + self.assertLess( + linux_setup.index('test -f "${runner_directory}/.runner"'), + linux_setup.index('test -w "$runner_sccache_dir"'), ) - self.assertIn("build/test-results/public-exec-${{ inputs.backend }}", upload) - @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") - def test_windows_cli_validation_stops_at_each_failed_command(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "validate-nvx" - / "action.yml" + def test_linux_unnamed_setup_defers_existing_runner_validation(self): + linux_setup = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" ).read_text(encoding="utf-8") - script = _composite_action_script(action, "Validate NVX CLI on Windows") - command_count = sum(line.startswith("python ") for line in script.splitlines()) - self.assertGreater(command_count, 0) - stub = """ -$CommandCount = 0 -function python { - $script:CommandCount += 1 - Write-Output "python-$script:CommandCount" - $global:LASTEXITCODE = 0 - if ($script:CommandCount -eq $script:FailureAt) { - $global:LASTEXITCODE = 37 - } -} -""" - for failure_at in range(command_count + 1): - with self.subTest(failure_at=failure_at): + environment_check = linux_setup.split("check_environment() {\n", 1)[1].split( + "\n}\n", 1 + )[0] + runner_checks = environment_check.split( + " validate_runner_state_paths\n validate_runner_work_paths\n", 1 + )[1] + guard = 'if [ "$configure_runner" = false ] && [ "$check_only" = false ]; then' + self.assertIn(guard, runner_checks) + self.assertLess( + runner_checks.index(guard), + runner_checks.index('test -w "$runner_sccache_dir"'), + ) + if os.name != "posix": + return + + harness = ( + "set -eu\n" + "runner_directory=/missing-runner\n" + "runner_sccache_dir=/missing-runner/_work/_sccache\n" + "validate_runner_state_paths() { echo state-checked; }\n" + "validate_runner_work_paths() { echo work-checked; }\n" + 'run_as_root() { [ "$1" = test ] && [ "$2" = -f ]; }\n' + "check_runner_state() {\n" + " validate_runner_state_paths\n" + " validate_runner_work_paths\n" + f"{runner_checks}\n" + "}\n" + ) + for configure_runner, check_only in ( + ("false", "false"), + ("false", "true"), + ("true", "false"), + ): + with self.subTest(configure_runner=configure_runner, check_only=check_only): result = subprocess.run( [ - "powershell.exe", - "-NoProfile", - "-NonInteractive", - "-Command", - f"$FailureAt = {failure_at}\n{stub}\n{script}", + "sh", + "-c", + harness + f"{configure_runner=}\n{check_only=}\n" + "check_runner_state\n", ], capture_output=True, text=True, - timeout=30, + timeout=10, check=False, ) self.assertEqual( - result.returncode, 37 if failure_at else 0, result.stderr - ) - self.assertEqual( - result.stdout.splitlines(), - [ - f"python-{index}" - for index in range(1, (failure_at or command_count) + 1) - ], + result.returncode == 0, + configure_runner == check_only == "false", + result.stderr, ) + if result.returncode == 0: + self.assertIn("state-checked", result.stdout) + self.assertIn("work-checked", result.stdout) - @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") - def test_windows_acceptance_preserves_attempts_and_publishes_only_valid_data(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "run-benchmark" + def test_linux_setup_installs_openvmm_perl_modules(self): + setup_directory = BuildConstants.REPO_ROOT / "scripts" / "setup" + configurations = ( + (setup_directory / "setup-linux-runner.sh").read_text(encoding="utf-8"), + (setup_directory / "setup-linux-mshv.sh").read_text(encoding="utf-8"), + ) + + for configuration in configurations: + for package in ( + "perl-FindBin", + "perl-IPC-Cmd", + "perl-Time-Piece", + "perl-lib", + ): + self.assertIn(package, configuration) + + def test_windows_runner_requires_inbox_pcat_firmware(self): + windows_setup = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" + ).read_text(encoding="utf-8") + validate_runner = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "validate-runner" / "action.yml" ).read_text(encoding="utf-8") - script = _composite_action_script(action, "Run Windows acceptance test") - for name, value in ( - ("backend", "whp"), - ("platform", "windows-whp-virtual-machine"), - ("warmups", "1"), - ("runs", "10"), - ("teardown-mode", "guest-exit"), - ): - script = script.replace("${{ inputs." + name + " }}", value) - stub = """ -$BenchmarkCount = 0 -$ValidationCount = 0 -function python { - if ($args[1] -eq "benchmark") { - $script:BenchmarkCount += 1 - $output = $args[[Array]::IndexOf($args, "--output") + 1] - New-Item -ItemType Directory -Path (Split-Path $output) -Force | Out-Null - Set-Content -LiteralPath $output -Encoding UTF8 -Value ( - '{"attempt":' + $script:BenchmarkCount + '}' + + self.assertIn('"Microsoft-Hyper-V"', windows_setup) + for configuration in (windows_setup, validate_runner): + for firmware in ( + "vmfirmwarepcat.dll", + "vmfirmware.dll", + "VmEmulatedDevices.dll", + ): + self.assertIn(firmware, configuration) + + def test_linux_runners_report_the_invariant_tsc_as_evidence(self): + validate_runner = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "validate-runner" + / "action.yml" + ).read_text(encoding="utf-8") + linux_setup = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" + ).read_text(encoding="utf-8") + + # Neither CI nor provisioning rejects a host without an invariant + # TSC: the guest warp probe measures what its guests observe (#265). + step = validate_runner.split(" - name: Report host TSC\n", 1)[1] + step = step.split("\n\n - name: ", 1)[0] + self.assertIn(" if: runner.os != 'Windows'\n", step) + check = "\n".join( + line.removeprefix(" ") + for line in step.split(" run: |\n", 1)[1].splitlines() ) - $global:LASTEXITCODE = $script:BenchmarkStatus - } - elseif ($args[1] -eq "performance") { - $log = $args[[Array]::IndexOf($args, "--attempt-log") + 1] - Add-Content -LiteralPath "attempt-logs.txt" -Value $log - $global:LASTEXITCODE = $script:ValidationStatuses[$script:ValidationCount] - $script:ValidationCount += 1 - } - else { - throw "unexpected command: $args" - } -} -""" - for name, statuses, benchmark_status in ( - ("first-success", (0,), 0), - ("remeasured-success", (75, 0), 0), - ("both-unstable", (75, 75), 0), - ("invalid-data", (1,), 0), - ("benchmark-failure", (0,), 42), - ): - with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - output = ( - root - / "data" - / "runs" - / "windows-whp-virtual-machine" - / "microvm-v2" - / "1vcpu" - ) - output.mkdir(parents=True) - accepted = output / "acceptance.json" - accepted.write_text('{"attempt":"stale"}', encoding="utf-8") - # A rerun in the same directory must not keep a previous - # run's attempt log or later attempts. - attempt_log = output / "acceptance-attempts.json" - attempt_log.write_text('{"attempts":"stale"}', encoding="utf-8") - (output / "acceptance-attempt-2.json").write_text( - '{"attempt":"stale"}', encoding="utf-8" - ) - setup = ( - "$ValidationStatuses = @(" - + ", ".join(str(status) for status in statuses) - + f")\n$BenchmarkStatus = {benchmark_status}\n" - ) - result = subprocess.run( - [ - "powershell.exe", - "-NoProfile", - "-NonInteractive", - "-Command", - setup + stub + script, - ], - cwd=root, - capture_output=True, - text=True, - timeout=30, - check=False, - ) - expected_status = benchmark_status or statuses[-1] - self.assertEqual(result.returncode, expected_status, result.stderr) - self.assertFalse(attempt_log.exists()) - validations = root / "attempt-logs.txt" - self.assertEqual( - ( - validations.read_text(encoding="utf-8").splitlines() - if validations.exists() - else [] - ), - [ - "data/runs/windows-whp-virtual-machine/microvm-v2/1vcpu/" - "acceptance-attempts.json" - ] - * (0 if benchmark_status else len(statuses)), - ) - self.assertEqual( - sorted(path.name for path in output.glob("acceptance-attempt-*")), - [ - f"acceptance-attempt-{index}.json" - for index in range(1, len(statuses) + 1) - ], + function = linux_setup.split("report_invariant_tsc() {\n", 1)[1] + function = "report_invariant_tsc() {\n" + function.split("\n}\n", 1)[0] + function += "\n}\n" + self.assertNotRegex(function, r"\bdie\b") + self.assertLess( + linux_setup.index("report_invariant_tsc /proc/cpuinfo\n"), + linux_setup.index('sudo -n true || die "passwordless sudo is required"'), + ) + if os.name != "posix": + return + + with tempfile.TemporaryDirectory() as temporary: + cpuinfo = Path(temporary) / "cpuinfo" + for flags, invariant in ( + ("fpu tsc constant_tsc nonstop_tsc tsc_known_freq", True), + ("fpu tsc constant_tsc tsc_known_freq", False), + ("fpu tsc constant_tsc nonstop_tsc_x", False), + ): + cpuinfo.write_text( + f"model name\t: Test CPU\nflags\t\t: {flags}\n", + encoding="utf-8", ) - for index in range(1, len(statuses) + 1): - attempt = output / f"acceptance-attempt-{index}.json" + with self.subTest(flags=flags, check="validate-runner"): + result = subprocess.run( + ["bash", "-c", check.replace("/proc/cpuinfo", str(cpuinfo))], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("CPU: Test CPU", result.stdout) + self.assertIn( + "TSC flag nonstop_tsc: " + + ("present" if invariant else "absent"), + result.stdout, + ) self.assertEqual( - json.loads(attempt.read_text(encoding="utf-8-sig")), - {"attempt": index}, + "::notice::Runner host does not expose an invariant TSC" + in result.stdout, + not invariant, ) - self.assertEqual(accepted.exists(), expected_status == 0) - if expected_status == 0: + self.assertNotIn("::error::", result.stdout + result.stderr) + with self.subTest(flags=flags, check="setup-linux-runner"): + result = subprocess.run( + [ + "sh", + "-c", + f"set -eu\n{function}report_invariant_tsc '{cpuinfo}'\n", + ], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( - json.loads(accepted.read_text(encoding="utf-8-sig")), - {"attempt": len(statuses)}, + "warning: host does not expose an invariant TSC" + in result.stderr, + not invariant, ) - @unittest.skipUnless(os.name == "posix", "requires bash") - def test_linux_acceptance_preserves_attempts_and_publishes_only_valid_data(self): - action = ( + def test_runners_qualify_their_host_time_before_every_job(self): + validate_runner = ( BuildConstants.REPO_ROOT / ".github" / "actions" - / "run-benchmark" + / "validate-runner" / "action.yml" ).read_text(encoding="utf-8") - script = _composite_action_script(action, "Run Linux acceptance test") - platform = "linux-kvm-virtual-machine" - for name, value in ( - ("backend", "kvm"), - ("platform", platform), - ("warmups", "1"), - ("runs", "10"), - ("teardown-mode", "guest-exit"), - ): - script = script.replace("${{ inputs." + name + " }}", value) - stub = """ -benchmark_count=0 -validation_count=0 -python3() { - local argument output="" input="" platform="" attempt_log="" previous="" - for argument in "$@"; do - case "${previous}" in - --output) output="${argument}" ;; - --input) input="${argument}" ;; - --platform) platform="${argument}" ;; - --attempt-log) attempt_log="${argument}" ;; - esac - previous="${argument}" - done - if [[ "${2-}" == benchmark ]]; then - benchmark_count=$((benchmark_count + 1)) - echo "benchmark ${platform} ${output}" - mkdir -p "$(dirname "${output}")" - printf '{"attempt":%d}' "${benchmark_count}" >"${output}" - return "${BENCHMARK_STATUS}" - fi - if [[ "${2-} ${3-}" == "performance validate-openvmm" ]]; then - validation_count=$((validation_count + 1)) - echo "validate ${platform} ${input} ${attempt_log}" - return "${VALIDATION_STATUSES[validation_count - 1]}" - fi - echo "unexpected command: $*" >&2 - return 1 -} -""" - environment = os.environ.copy() - environment.update( - { - "NVX_BENCHMARK_CPUS": "0-7", - "NVX_BENCHMARK_HOST_CPU_RESERVE": "0", - "NVX_BENCHMARK_MARKER_TIMEOUT": "40", - } + + # The doctor follows the host TSC report on Linux: the backend, the CPU + # fingerprint and generation, and the TSC rate stability. Jobs that + # haven't downloaded OpenVMM skip its CPU profile check (H2) and + # preflight (H3), and only jobs that ask for it run the guest warp + # probe (H6). + self.assertLess( + validate_runner.index(" - name: Report host TSC\n"), + validate_runner.index(" - name: Qualify host time on Linux\n"), ) - run_dir = f"data/runs/{platform}/microvm-v2/1vcpu" - for name, statuses, benchmark_status in ( - ("first-success", (0,), 0), - ("remeasured-success", (75, 0), 0), - ("both-unstable", (75, 75), 0), - ("invalid-data", (2,), 0), - ("unstable-then-invalid", (75, 2), 0), - ("benchmark-failure", (0,), 42), + for name, shell, command, summary in ( + ( + "Qualify host time on Linux", + "bash", + "python3 scripts/nvx.py doctor", + '--summary "${GITHUB_STEP_SUMMARY}"', + ), + ( + "Qualify host time on Windows", + "powershell", + "python scripts\\nvx.py doctor", + '--summary "$env:GITHUB_STEP_SUMMARY"', + ), ): - with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - output = root / run_dir - output.mkdir(parents=True) - accepted = output / "acceptance.json" - accepted.write_text('{"attempt":"stale"}', encoding="utf-8") - # A rerun in the same directory must not keep a previous - # run's attempt log or later attempts. - attempt_log = output / "acceptance-attempts.json" - attempt_log.write_text('{"attempts":"stale"}', encoding="utf-8") - (output / "acceptance-attempt-2.json").write_text( - '{"attempt":"stale"}', encoding="utf-8" + with self.subTest(step=name): + step = validate_runner.split(f" - name: {name}\n", 1)[1] + step = step.split("\n\n", 1)[0] + self.assertIn(f" shell: {shell}\n", step) + self.assertIn(command, step) + self.assertIn('--backend "${{ inputs.backend }}"', step) + self.assertIn("--checks H1 H2 H4\n", step) + self.assertIn( + "${{ inputs.warp-probe == 'true' && 'H6' || '' }}\n", step ) - setup = ( - "VALIDATION_STATUSES=(" - + " ".join(str(status) for status in statuses) - + f")\nBENCHMARK_STATUS={benchmark_status}\n" + fingerprint = ( + '"${RUNNER_TEMP}/nvx-cpu-fingerprint.json"' + if shell == "bash" + else '"$env:RUNNER_TEMP\\nvx-cpu-fingerprint.json"' ) - result = subprocess.run( - [ - "bash", - "--noprofile", - "--norc", - "-eo", - "pipefail", - "-c", - setup + stub + script, - ], - cwd=root, - env=environment, - capture_output=True, - text=True, - timeout=30, - check=False, + self.assertIn( + "${{ inputs.verify-openvmm == 'true' && " + f"'H3 --cpu-fingerprint {fingerprint}' || '--no-openvmm' }}}}\n", + step, ) - expected_status = benchmark_status or statuses[-1] - self.assertEqual(result.returncode, expected_status, result.stderr) - self.assertFalse(attempt_log.exists()) - expected_stdout: list[str] = [] - for index in range(1, len(statuses) + 1): - attempt = f"{run_dir}/acceptance-attempt-{index}.json" - if index > 1: - expected_stdout.append( - "::warning::Lifecycle snapshot generation was unstable " - f"on attempt {index - 1}; remeasuring once" - ) - expected_stdout.append(f"benchmark {platform} {attempt}") - if not benchmark_status: - expected_stdout.append( - f"validate {platform} {attempt} " - f"{run_dir}/acceptance-attempts.json" - ) - self.assertEqual(result.stdout.splitlines(), expected_stdout) + self.assertIn("--ci-schedule\n", step) + self.assertIn(summary, step) + args = nvx.parse_args( + ["doctor", "--backend", "kvm", "--checks", "H1", "H2", "H4", "H3"] + + ["--cpu-fingerprint", "fingerprint.json", "--ci-schedule"] + ) + self.assertEqual(args.checks, ["H1", "H2", "H4", "H3"]) + self.assertEqual(args.cpu_fingerprint, Path("fingerprint.json")) + args = nvx.parse_args( + ["doctor", "--backend", "kvm", "--checks", "H1", "H2", "H4", "H6", "H3"] + + ["--cpu-fingerprint", "fingerprint.json", "--ci-schedule"] + ) + self.assertEqual(args.checks, ["H1", "H2", "H4", "H6", "H3"]) + args = nvx.parse_args( + ["doctor", "--backend", "kvm", "--checks", "H1", "H2", "H4"] + + ["--no-openvmm", "--ci-schedule"] + ) + self.assertTrue(args.no_openvmm) + self.assertTrue(args.ci_schedule) + self.assertIn(" verify-openvmm:\n", validate_runner) + self.assertIn(' default: "false"\n', validate_runner) + warp_probe = validate_runner.split(" warp-probe:\n", 1)[1] + warp_probe = warp_probe.split("\nruns:\n", 1)[0] + self.assertIn("requires\n verify-openvmm\n", warp_probe) + self.assertIn(' default: "false"\n', warp_probe) + # A failed CPU profile check keeps OpenVMM's fingerprint. + upload = validate_runner.split(" - name: Upload the CPU fingerprint\n")[1] + upload = upload.split("\n\n", 1)[0] + self.assertIn("if: failure() && inputs.verify-openvmm == 'true'\n", upload) + self.assertIn("path: ${{ runner.temp }}/nvx-cpu-fingerprint.json\n", upload) + self.assertIn("if-no-files-found: ignore\n", upload) + # Jobs that run OpenVMM qualify the runner after downloading it. + benchmark = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "run-benchmark" + / "action.yml" + ).read_text(encoding="utf-8") + self.assertNotIn("download-artifact", benchmark) + for workflow_name in ("run-nvx-microvm-tests.yml", "run-platform.yml"): + with self.subTest(workflow=workflow_name): + workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / workflow_name + ).read_text(encoding="utf-8") + validate = workflow.index(" - name: Validate runner\n") + self.assertEqual(workflow.count(" - name: Validate runner\n"), 1) + self.assertIn( + 'verify-openvmm: "true"', workflow[validate : validate + 220] + ) + # The microVM scenarios probe after every boot and restore; + # the benchmarks boot guests without such probes. self.assertEqual( - sorted(path.name for path in output.glob("acceptance-attempt-*")), - [ - f"acceptance-attempt-{index}.json" - for index in range(1, len(statuses) + 1) - ], + 'warp-probe: "true"' in workflow[validate : validate + 260], + workflow_name == "run-platform.yml", ) - for index in range(1, len(statuses) + 1): - attempt = output / f"acceptance-attempt-{index}.json" - self.assertEqual( - json.loads(attempt.read_text(encoding="utf-8")), - {"attempt": index}, - ) - self.assertEqual(accepted.exists(), expected_status == 0) - if expected_status == 0: - self.assertEqual( - json.loads(accepted.read_text(encoding="utf-8")), - {"attempt": len(statuses)}, - ) + for step in ( + " - name: Download guest artifacts\n", + " - name: Download OpenVMM executable\n", + " - name: Make OpenVMM executable\n", + ): + self.assertLess(workflow.index(step), validate) + self.assertLess( + validate_runner.index("Validate Linux toolchain"), + validate_runner.index("Qualify host time on Linux"), + ) + def test_runner_setups_install_backend_native_openvmm_targets(self): + linux_setup = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" + ).read_text(encoding="utf-8") + windows_setup = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" + ).read_text(encoding="utf-8") -class BuildConstantsTests(unittest.TestCase): - def test_constants_are_grouped_class_namespaces(self): - for namespace in ( - BuildConstants, - KernelBuildConstants, - OpenVMMBuildConstants, - AlpineBuildConstants, - UbuntuBuildConstants, - InitramfsBuildConstants, - DockerBuildConstants, - ZstdBuildConstants, - ReleaseBuildConstants, + for target in ( + "x86_64-unknown-none", + "x86_64-unknown-uefi", ): - with self.subTest(namespace=namespace.__name__): - self.assertEqual(namespace.__module__, "nvx_tools.build_constants") - self.assertTrue( - all( - name.isupper() - for name in vars(namespace) - if not name.startswith("__") - ) - ) + self.assertIn(target, linux_setup) + self.assertIn(target, windows_setup) + self.assertIn("x86_64-unknown-linux-musl", linux_setup) + for cross_platform_tool in ( + "x86_64-pc-windows-gnu", + "gcc-mingw-w64-x86-64-win32", + "mingw64-gcc", + "x86_64-w64-mingw32-gcc", + "x86_64-w64-mingw32-dlltool", + ): + self.assertNotIn(cross_platform_tool, linux_setup) - def test_build_pins_and_artifact_names_have_one_literal_owner(self): - centralized = { - KernelBuildConstants.VERSION, - KernelBuildConstants.URL, - KernelBuildConstants.SHA256, - KernelBuildConstants.BINARY_NAME, - KernelBuildConstants.CONFIG_NAME, - KernelBuildConstants.PROVENANCE_NAME, - KernelBuildConstants.DEBUG_BINARY_NAME, - KernelBuildConstants.DEBUG_CONFIG_NAME, - KernelBuildConstants.DEBUG_PROVENANCE_NAME, - OpenVMMBuildConstants.GNU_RUST_TARGET, - OpenVMMBuildConstants.MUSL_RUST_TARGET, - OpenVMMBuildConstants.WINDOWS_RUST_TARGET, - OpenVMMBuildConstants.PROVENANCE_NAME, - OpenVMMBuildConstants.CONTROL_CONTRACT_REVISION, - AlpineBuildConstants.VERSION, - AlpineBuildConstants.BRANCH, - AlpineBuildConstants.MINIROOTFS_SHA256, - AlpineBuildConstants.INITRAMFS_NAME, - AlpineBuildConstants.PACKAGE_MANIFEST_NAME, - UbuntuBuildConstants.VERSION, - UbuntuBuildConstants.BASE_URL, - UbuntuBuildConstants.BASE_SHA256, - UbuntuBuildConstants.INITRAMFS_NAME, - UbuntuBuildConstants.PACKAGE_MANIFEST_NAME, - UbuntuBuildConstants.DISTRO_NAME, - UbuntuBuildConstants.DISTRO_MANIFEST_NAME, - InitramfsBuildConstants.PROVENANCE_NAME, - ZstdBuildConstants.VERSION, - ZstdBuildConstants.SHA256, - } - duplicates: list[tuple[str, int, str]] = [] - for path in (BuildConstants.REPO_ROOT / "scripts").rglob("*.py"): - if path.name == "build_constants.py" or path.name.startswith("test_"): - continue - tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - duplicates.extend( - (path.name, node.lineno, node.value) - for node in ast.walk(tree) - if isinstance(node, ast.Constant) - and isinstance(node.value, str) - and node.value in centralized - ) - self.assertEqual(duplicates, []) - - def test_old_module_level_constants_are_not_reexported(self): - names = ( - "REPO_ROOT", - "BUILD_DIR", - "SOURCE_DIR", - "OPENVMM_DIR", - "DEFAULT_KERNEL_VERSION", - "DEFAULT_KERNEL_URL", - "DEFAULT_KERNEL_SHA256", - "DEFAULT_ALPINE_VERSION", - "DEFAULT_ALPINE_BRANCH", - "DEFAULT_ALPINE_MINIROOTFS_SHA256", - "DEFAULT_UBUNTU_VERSION", - "DEFAULT_UBUNTU_CODENAME", - "DEFAULT_UBUNTU_ARCHITECTURE", - "DEFAULT_UBUNTU_BASE_URL", - "DEFAULT_UBUNTU_BASE_SHA256", - "UBUNTU_PACKAGE_LOCK", - "UBUNTU_EROFS_FORMAT", - "MICROVM_ABI_VERSION", - "CONTROL_SESSION_PROTOCOL_VERSION", - "CONTROL_CONTRACT_REVISION", - "OPENVMM_PROVENANCE_NAME", - "KERNEL_PROVENANCE_NAME", - "INITRAMFS_PROVENANCE_NAME", - "REQUIRED_VIRTIO_CONSOLE_CONFIG", - "REQUIRED_SHARED_STATUS_KERNEL_CONFIG", - "REQUIRED_SANDBOX_KERNEL_CONFIG", - "ZSTD_VERSION", - "ZSTD_ARCHIVE", - "ZSTD_URL", - "ZSTD_SHA256", - "OPENVMM_RUST_TOOLCHAIN", - "OPENVMM_GUEST_RUST_TARGET", - "OPENVMM_UEFI_RUST_TARGET", - "OPENVMM_LINUX_MUSL_RUST_TARGET", - "OPENVMM_RUST_TARGETS", - "APORTS_URL", - "REPOSITORIES", - "UBUNTU_ARCHIVE_KEYRING_URL", - "UBUNTU_ARCHIVE_KEYRING_SHA256", - "UBUNTU_SNAPSHOT_ARCHIVE_URL", - "UBUNTU_SOURCE_INDEXES", - "_UBUNTU_POCKET_SUITES", - "_UBUNTU_COMPONENTS", - "_LAUNCHPAD_ARCHIVE_API", - "_LAUNCHPAD_SERIES_API", - "_GENERATED_OUTPUT_ENTRIES", - "_KNOWN_PACKAGE_CONTROL_ACTIONS", - "PROJECT_SOURCE_PATHS", - "GUEST_RELEASE_NAMES", - "_ZIP_TIMESTAMP", - ) - for module in ( - build, - build_config, - common, - ubuntu, - ci, - collect_alpine_sources, - collect_ubuntu_sources, - release, - benchmark, - archive, - ): - with self.subTest(module=module.__name__): - self.assertFalse(set(names).intersection(vars(module))) - - def test_guest_descriptors_and_source_manifest_share_build_pins(self): - manifest = json.loads( - (BuildConstants.REPO_ROOT / "SOURCE-MANIFEST.json").read_text( - encoding="utf-8" - ) + def test_linux_runner_setup_adds_the_benchmark_label_on_request(self): + # Provisioning replaces a runner's registration, so the benchmark + # runner keeps the label that CI's platform jobs require only when + # the setup script registers it. + linux_setup = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-linux-runner.sh" + ).read_text(encoding="utf-8") + self.assertIn( + " --benchmark-runner)\n" + " benchmark_runner=true\n" + " shift\n" + " ;;\n", + linux_setup, ) - self.assertEqual(KernelBuildConstants.VERSION, manifest["linux"]["version"]) - self.assertEqual(KernelBuildConstants.URL, manifest["linux"]["upstream_url"]) - self.assertEqual( - KernelBuildConstants.SHA256, - manifest["linux"]["upstream_archive_sha256"], + self.assertIn("\n --benchmark-runner ", linux_setup) + # Registration and check mode both use the computed label set. + self.assertIn('--labels "$expected_runner_labels"', linux_setup) + self.assertIn( + '"$expected_runner_labels" ] ||\n' + ' die "GitHub Actions runner labels are not validated"', + linux_setup, ) - self.assertEqual( - AlpineBuildConstants.MINIROOTFS_URL, manifest["alpine"]["minirootfs_url"] + start = linux_setup.index('\n[ "$benchmark_runner" = false ]') + end = linux_setup.index("\nfi\n", start) + len("\nfi\n") + labels = linux_setup[start:end] + self.assertIn( + "\nexpected_runner_labels=linux,${backend},virtual-machine\n", labels ) - self.assertEqual( - AlpineBuildConstants.MINIROOTFS_SHA256, - manifest["alpine"]["minirootfs_sha256"], + if os.name != "posix": + return + + harness = ( + "set -eu\n" + "die() { printf 'error: %s\\n' \"$*\" >&2; exit 1; }\n" + "backend=$1\nrunner_name=$2\nbenchmark_runner=$3\n" + "runner_directory=/opt/nvx-runner\n" + f"{labels}" + 'printf "%s\\n" "$expected_runner_labels"\n' ) - for descriptor in (guests.ALPINE_GUEST, guests.UBUNTU_GUEST): - with self.subTest(guest=descriptor.name): - pinned = manifest[descriptor.name] - self.assertEqual(descriptor.release, pinned["version"]) - self.assertEqual(descriptor.architecture, pinned["architecture"]) - self.assertEqual( - f"build/{descriptor.package_manifest_name}", - pinned["package_manifests"][0], + for backend, runner_name, benchmark_runner, expected in ( + ("kvm", "azure-kvm-1", "false", "linux,kvm,virtual-machine"), + ("mshv", "", "false", "linux,mshv,virtual-machine"), + ("kvm", "azure-kvm-3", "true", "linux,kvm,virtual-machine,benchmark"), + ("mshv", "azure-mshv-4", "true", "linux,mshv,virtual-machine,benchmark"), + ("kvm", "", "true", None), + ): + with self.subTest( + backend=backend, + runner_name=runner_name, + benchmark_runner=benchmark_runner, + ): + result = subprocess.run( + ["sh", "-c", harness, "sh", backend, runner_name, benchmark_runner], + capture_output=True, + text=True, + timeout=10, + check=False, ) - self.assertEqual( - OpenVMMBuildConstants.MICROVM_ABI_VERSION, - manifest["openvmm"]["microvm_abi_version"], - ) - self.assertEqual( - OpenVMMBuildConstants.CONTROL_SESSION_PROTOCOL_VERSION, - manifest["openvmm"]["control_session_protocol_version"], - ) - self.assertEqual( - OpenVMMBuildConstants.CONTROL_CONTRACT_REVISION, - manifest["openvmm"]["control_contract_revision"], - ) - - def test_cache_defaults_are_resolved_for_each_configuration(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - for configured in ("", str(root / "first"), str(root / "second")): - with ( - self.subTest(configured=configured), - patch.dict(os.environ, {"NVX_CACHE_DIR": configured}), - ): - expected = ( - Path(configured) - if configured - else BuildConstants.REPO_ROOT / ".cache" - ).resolve() - self.assertEqual( - build_config.KernelBuildConfig().cache_directory, expected - ) - self.assertEqual(common.cache_root(), expected) + if expected is None: + self.assertEqual(result.returncode, 1) self.assertEqual( - build_config.KernelBuildConfig( - cache_directory=root / "explicit" - ).cache_directory, - root / "explicit", + result.stderr, + "error: --benchmark-runner requires --runner-name\n", ) + else: + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, f"{expected}\n") - def test_openvmm_executable_default_is_resolved_at_configuration_time(self): - for host, binary in (("nt", "openvmm.exe"), ("posix", "openvmm")): - expected = OpenVMMBuildConstants.DIRECTORY / "target" / "release" / binary - with self.subTest(host=host), patch.object(common.os, "name", host): - self.assertEqual(common.openvmm_binary_path(), expected) - self.assertEqual(build_config.OpenVmmBuildConfig().output, expected) - - def test_initramfs_provenance_tracks_constants_source(self): - relative = "scripts/nvx_tools/build_constants.py" - source = BuildConstants.REPO_ROOT / relative - original = build.initramfs_provenance_inputs() - records = cast(list[dict[str, str]], original["source_files"]) - self.assertEqual( - [record for record in records if record["path"] == relative], - [{"path": relative, "sha256": common.sha256_file(source)}], - ) - - def changed_sha256(path: Path) -> str: - return "f" * 64 if path == source else common.sha256_file(path) - - with patch.object(build, "sha256_file", side_effect=changed_sha256): - changed = build.initramfs_provenance_inputs() - self.assertNotEqual(original, changed) - self.assertEqual(original["alpine"], changed["alpine"]) - - def test_constants_are_in_docker_and_release_source_inputs(self): - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" + def test_windows_runner_setup_adds_the_benchmark_label_on_request(self): + setup = BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" + windows_setup = setup.read_text(encoding="utf-8") + self.assertIn(" [switch]$BenchmarkRunner,\n", windows_setup) + self.assertIn( + "if ($BenchmarkRunner -and\n" + " (-not $RunnerOnly -or [string]::IsNullOrWhiteSpace($RunnerName))) {\n" + ' throw "-BenchmarkRunner requires -RunnerOnly and -RunnerName"\n' + "}\n", + windows_setup, ) - self.assertIn("COPY scripts/ /repo/scripts/", dockerfile) - self.assertIn("scripts", ReleaseBuildConstants.PROJECT_SOURCE_PATHS) - self.assertTrue( + # Registration and check mode share one label set. + self.assertEqual(windows_setup.count('"windows,whp,virtual-machine"'), 1) + for function_name, assignment in ( + ("Install-ActionsRunner", "\n $labels = Get-ActionsRunnerLabels\n"), ( - BuildConstants.REPO_ROOT - / "scripts" - / "nvx_tools" - / "build_constants.py" - ).is_file() - ) + "Assert-ActionsRunner", + "\n $expectedLabels = Get-ActionsRunnerLabels\n", + ), + ): + with self.subTest(function=function_name): + function = windows_setup.split(f"\nfunction {function_name} {{\n", 1)[1] + self.assertIn(assignment, function.split("\n}\n", 1)[0]) + if os.name != "nt": + return - def test_project_source_paths_exist_and_are_tracked(self): - for relative in ReleaseBuildConstants.PROJECT_SOURCE_PATHS: - with self.subTest(path=relative): - self.assertTrue((BuildConstants.REPO_ROOT / relative).exists()) - tracked = subprocess.run( - ["git", "ls-files", "--error-unmatch", "--", relative], - cwd=BuildConstants.REPO_ROOT, - capture_output=True, - text=True, - check=False, - ) - self.assertEqual(tracked.returncode, 0, tracked.stderr) + harness = f""" +Set-StrictMode -Version 2.0 +$ErrorActionPreference = "Stop" +$tokens = $null +$errors = $null +$ast = [System.Management.Automation.Language.Parser]::ParseFile( + '{setup}', + [ref]$tokens, + [ref]$errors +) +if ($errors.Count -ne 0) {{ + throw "setup script has parse errors" +}} +$definition = $ast.Find({{ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq "Get-ActionsRunnerLabels" + }}, $true) +. ([scriptblock]::Create($definition.Extent.Text)) +$BenchmarkRunner = $false +Write-Output (Get-ActionsRunnerLabels) +$BenchmarkRunner = $true +Write-Output (Get-ActionsRunnerLabels) +""" + result = subprocess.run( + ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", harness], + capture_output=True, + text=True, + timeout=30, + check=False, + ) - def test_constants_consumers_import_in_either_order(self): - modules = ( - "nvx_tools.build_constants", - "nvx_tools.guests", - "nvx_tools.common", - "nvx_tools.build_config", - "nvx_tools.build", - "nvx_tools.release", - "nvx", + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + result.stdout.splitlines(), + [ + "windows,whp,virtual-machine", + "windows,whp,virtual-machine,benchmark", + ], ) - for ordered in (modules, tuple(reversed(modules))): - with self.subTest(first=ordered[0]): - result = subprocess.run( - [ - sys.executable, - "-c", - "import importlib, sys; " - "[importlib.import_module(name) for name in sys.argv[1:]]", - *ordered, - ], - cwd=BuildConstants.REPO_ROOT / "scripts", - capture_output=True, - text=True, - timeout=30, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) + def test_repository_pins_one_exact_rust_toolchain(self): + root = BuildConstants.REPO_ROOT + [toolchain] = re.findall( + r'^channel = "([^"]*)"$', + (root / "rust-toolchain.toml").read_text(encoding="utf-8"), + re.MULTILINE, + ) + # A floating channel such as `stable` lets runners drift apart (#402). + self.assertRegex(toolchain, r"^[0-9]+\.[0-9]+\.[0-9]+$") + self.assertEqual(OpenVMMBuildConstants.RUST_TOOLCHAIN, toolchain) + self.assertIn("rust-toolchain.toml", ReleaseBuildConstants.PROJECT_SOURCE_PATHS) -class UbuntuSourceCollectionTests(unittest.TestCase): - def test_malformed_package_manifest_is_reported_as_script_error(self): - with tempfile.TemporaryDirectory() as temporary: - manifest = Path(temporary) / "packages.json" - manifest.write_text("{", encoding="utf-8") + setup = root / "scripts" / "setup" + # The setup scripts and the Specula runner setup take the release from + # the setup tool manifest, which rustup cannot read. + self.assertEqual( + re.findall( + r"^rust\.toolchain=(.*)$", + (setup / "tool-versions.conf").read_text(encoding="utf-8"), + re.MULTILINE, + ), + [toolchain], + ) + for path, reader in ( + (setup / "setup-linux-runner.sh", "\n RUST_TOOLCHAIN=$tool_value\n"), + (setup / "setup-linux-mshv.sh", "\n RUST_TOOLCHAIN=$tool_value\n"), + ( + setup / "setup-windows-whp.ps1", + '\n$RustToolchain = Get-ToolManifestValue $ToolManifest "rust.toolchain"\n', + ), + ( + root / ".github" / "specula" / "setup-runner.sh", + '\nRUST_VERSION="$(tool_value rust.toolchain "$VERSION_PATTERN")"\n', + ), + ): + with self.subTest(path=path.name): + self.assertIn(reader, path.read_text(encoding="utf-8")) - with self.assertRaises(collect_ubuntu_sources.ScriptError) as context: - collect_ubuntu_sources._source_requirements([manifest]) + def test_ci_takes_rust_toolchain_from_repository_pin(self): + github = BuildConstants.REPO_ROOT / ".github" + workflows = github / "workflows" - self.assertIn("cannot read Ubuntu package manifest", str(context.exception)) + validate_runner = ( + github / "actions" / "validate-runner" / "action.yml" + ).read_text(encoding="utf-8") + for step_name in ("Validate Linux toolchain", "Validate Windows toolchain"): + with self.subTest(step=step_name): + step = _composite_action_step(validate_runner, step_name) + self.assertIn("rust-toolchain.toml", step) + # Report a missing toolchain without writing the trusted RUSTUP_HOME. + self.assertEqual(_yaml_field(step, "RUSTUP_AUTO_INSTALL"), ['"0"']) + for crate in ("aci-edge-sandboxes", "nvx-rs"): + with self.subTest(crate=crate): + crate_action = ( + github / "actions" / f"check-{crate}" / "action.yml" + ).read_text(encoding="utf-8") + self.assertEqual(_yaml_field(crate_action, "default"), ['""']) + install = _composite_action_step( + crate_action, "Install Rust toolchains" + ) + self.assertIn("../rust-toolchain.toml", install) + self.assertIn("REQUESTED_TOOLCHAIN: ${{ inputs.toolchain }}", install) + self.assertEqual(crate_action.count("${{ inputs.toolchain }}"), 1) -class AlpineSourceCollectionTests(unittest.TestCase): - INSTALL_SCRIPT = b"#!/bin/sh\nexit 0\n" + setup = (workflows / "copilot-setup-steps.yml").read_text(encoding="utf-8") + self.assertIn(" - rust-toolchain.toml\n", setup) + self.assertIn(" - nvx-rs/**\n", setup) + self.assertIn( + "rust-toolchain.toml", + _workflow_step( + setup, "copilot-setup-steps", "Resolve pinned tool versions" + ), + ) + rust = _workflow_step(setup, "copilot-setup-steps", "Install Rust toolchains") + self.assertIn('rustup default "${RUST_TOOLCHAIN}"', rust) + self.assertIn("x86_64-unknown-linux-musl", rust) + self.assertNotIn("stable", rust) - @staticmethod - def _aports_commit(cache: Path, files: dict[str, tuple[str, bytes]]) -> str: - environment = { - key: value - for key, value in os.environ.items() - if not key.startswith("GIT_CONFIG_") - } - environment.update( - GIT_AUTHOR_NAME="NVX Tests", - GIT_AUTHOR_EMAIL="nvx-tests@example.com", - GIT_COMMITTER_NAME="NVX Tests", - GIT_COMMITTER_EMAIL="nvx-tests@example.com", + # A binary built by another compiler must not satisfy the cache. + build_action = (github / "actions" / "build-openvmm" / "action.yml").read_text( + encoding="utf-8" + ) + self.assertEqual( + build_action.count( + "${{ hashFiles('rust-toolchain.toml') }}-${{ steps.openvmm.outputs.sha }}" + ), + 4, ) - def git(*args: str, data: bytes | None = None) -> str: - result = subprocess.run( - ["git", "-C", str(cache), *args], - input=data, - env=environment, - capture_output=True, - check=True, - ) - return result.stdout.decode("ascii").strip() + # A new release reruns every suite that it builds. + for pattern in ( + ci.CI_TEST_INPUT_PATHS, + ci.OPENVMM_UNIT_TEST_INPUT_PATHS, + ci.OPENVMM_VMM_TEST_INPUT_PATHS, + ): + self.assertIsNotNone(pattern.match("rust-toolchain.toml")) - cache.mkdir(parents=True, exist_ok=True) - git("init", "-q") - git("config", "core.autocrlf", "false") - for path, (mode, contents) in files.items(): - blob = git("hash-object", "-w", "--stdin", data=contents) - git("update-index", "--add", "--cacheinfo", f"{mode},{blob},{path}") - return git("commit-tree", git("write-tree"), "-m", "aports fixture") + def test_release_actions_use_deterministic_immutable_tooling(self): + package_action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "package-release" + / "action.yml" + ).read_text(encoding="utf-8") + publish_action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "publish-development-release" + / "action.yml" + ).read_text(encoding="utf-8") - def _symlinked_recipe_commit(self, cache: Path) -> str: - return self._aports_commit( - cache, - { - "main/foo/APKBUILD": ("100644", b"pkgname=foo\n"), - "main/foo/foo.post-install": ("100755", self.INSTALL_SCRIPT), - "main/foo/foo.post-upgrade": ("120000", b"foo.post-install"), - }, + self.assertEqual(package_action.count("archive-release"), 2) + self.assertIn( + "python scripts\\nvx.py materialize-kernel-provenance-inputs", + package_action, + ) + self.assertLess( + package_action.index( + "python scripts\\nvx.py materialize-kernel-provenance-inputs" + ), + package_action.index("- name: Package Windows release"), + ) + self.assertNotIn("git checkout-index", package_action) + self.assertNotIn("tar -czf", package_action) + self.assertNotIn("Compress-Archive", package_action) + self.assertIn( + "python3 -u scripts/publish_development_release.py", + publish_action, ) + self.assertNotIn("--clobber", publish_action) - @staticmethod - def _package_manifest(root: Path, commit: str) -> Path: - manifest = root / "packages.json" - manifest.write_text( - json.dumps( - { - "guest": AlpineBuildConstants.GUEST_NAME, - "alpine_branch": AlpineBuildConstants.BRANCH, - "architecture": AlpineBuildConstants.ARCHITECTURE, - "packages": [ - { - "name": "foo", - "version": "1.0-r0", - "origin": "foo", - "license": "MIT", - "aports_commit": commit, - } - ], - } - ), - encoding="utf-8", + def test_ci_artifact_actions_use_node24(self): + configurations = "\n".join( + path.read_text(encoding="utf-8") + for pattern in ("*.yml", "*.yaml") + for path in (BuildConstants.REPO_ROOT / ".github").rglob(pattern) ) - return manifest - - @staticmethod - def _member( - name: str, - kind: bytes = tarfile.REGTYPE, - data: bytes = b"", - *, - linkname: str = "", - mode: int = 0o664, - ) -> tuple[tarfile.TarInfo, bytes]: - member = tarfile.TarInfo(name) - member.type = kind - member.linkname = linkname - member.mode = mode - member.size = len(data) if kind == tarfile.REGTYPE else 0 - return member, data - - @staticmethod - def _archive(*members: tuple[tarfile.TarInfo, bytes]) -> bytes: - buffer = io.BytesIO() - with tarfile.open(fileobj=buffer, mode="w") as archive: - for member, data in members: - archive.addfile(member, io.BytesIO(data) if member.isfile() else None) - return buffer.getvalue() - - def test_malformed_package_manifest_is_reported_as_source_error(self): - with tempfile.TemporaryDirectory() as temporary: - for contents in ("{", "[]"): - manifest = Path(temporary) / "packages.json" - manifest.write_text(contents, encoding="utf-8") - with ( - self.subTest(contents=contents), - self.assertRaises(collect_alpine_sources.SourceError) as context, - ): - collect_alpine_sources._load_packages([manifest]) + self.assertNotRegex(configurations, r"actions/upload-artifact@v[1-5]\b") + self.assertNotRegex(configurations, r"actions/download-artifact@v[1-6]\b") - self.assertIn("package manifest", str(context.exception)) + def test_benchmark_diagnostics_preserve_each_workflow_attempt(self): + action = ( + Path(__file__).parents[1] + / ".github" + / "actions" + / "run-benchmark" + / "action.yml" + ).read_text(encoding="utf-8") + diagnostics = _composite_action_step(action, "Upload benchmark diagnostics") + name = ( + "benchmark-diagnostics-${{ inputs.platform }}-${{ github.run_id }}" + "-attempt-${{ github.run_attempt }}" + ) - def test_recipe_collection_preserves_blob_line_endings(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - cache = root / "aports" - apkbuild = b"pkgname=foo\npkgver=1.0\npkgrel=0\n" - commit = self._aports_commit( - cache, - {"main/foo/APKBUILD": ("100644", apkbuild)}, - ) - subprocess.run( - ["git", "-C", str(cache), "config", "core.autocrlf", "true"], - check=True, - ) - output = root / "alpine" + self.assertIn(" if: always()", diagnostics) + self.assertIn(" uses: actions/upload-artifact@v7", diagnostics) + self.assertIn(" path: data/runs/${{ inputs.platform }}", diagnostics) + self.assertIn(f" name: {name}", diagnostics) + self.assertIn(" if-no-files-found: error", diagnostics) + self.assertIn(" overwrite: false", diagnostics) + self.assertIn(" retention-days: 1", diagnostics) + self.assertLess( + action.index("- name: Upload benchmark diagnostics"), + action.index("- name: Upload benchmark results"), + ) - with ( - patch.object(collect_alpine_sources, "_prepare_aports"), - contextlib.redirect_stdout(io.StringIO()), + artifacts: dict[str, tuple[str, int]] = {} + for attempt in (1, 2): + for platform in ( + "linux-kvm-virtual-machine", + "linux-mshv-virtual-machine", + "windows-whp-virtual-machine", ): - collect_alpine_sources.collect_alpine_sources( - [self._package_manifest(root, commit)], - output, - cache, - skip_upstream=True, + artifact = ( + name.replace("${{ inputs.platform }}", platform) + .replace("${{ github.run_id }}", "35771477786") + .replace("${{ github.run_attempt }}", str(attempt)) ) + self.assertNotIn("${{", artifact) + self.assertNotIn(artifact, artifacts) + artifacts[artifact] = (platform, attempt) + self.assertEqual(len(artifacts), 6) - collected = output / "recipes" / commit / "main/foo/APKBUILD" - self.assertEqual(collected.read_bytes(), apkbuild) + def test_benchmark_result_handoff_keeps_run_scoped_names(self): + actions = Path(__file__).parents[1] / ".github" / "actions" + action = (actions / "run-benchmark" / "action.yml").read_text(encoding="utf-8") + prepare = (actions / "prepare-performance-results" / "action.yml").read_text( + encoding="utf-8" + ) + results = _composite_action_step(action, "Upload benchmark results") + name = "benchmark-${{ inputs.platform }}-${{ github.run_id }}" - def test_recipe_symlink_is_collected_as_verified_regular_file(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - cache = root / "aports" - commit = self._symlinked_recipe_commit(cache) - output = root / "alpine" + self.assertIn(f" name: {name}", results) + self.assertIn(" path: data/runs/${{ inputs.platform }}", results) + self.assertIn(" overwrite: true", results) + self.assertNotIn("github.run_attempt", results) + self.assertNotIn("github.run_attempt", prepare) + for platform in ( + "linux-kvm-virtual-machine", + "linux-mshv-virtual-machine", + "windows-whp-virtual-machine", + ): + with self.subTest(platform=platform): + artifact = name.replace("${{ inputs.platform }}", platform) + self.assertIn(f" name: {artifact}", prepare) - with ( - patch.object(collect_alpine_sources, "_prepare_aports"), - contextlib.redirect_stdout(io.StringIO()), - ): - collect_alpine_sources.collect_alpine_sources( - [self._package_manifest(root, commit)], - output, - cache, - skip_upstream=True, + def test_benchmarks_check_the_expected_host_before_measuring(self): + # A series' history comes from the host in data/-host.json, + # so a platform job refuses another host before it measures (#526). + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "run-benchmark" + / "action.yml" + ).read_text(encoding="utf-8") + steps = re.findall(r"^ - name: (.+)$", action, re.MULTILINE) + output = ( + '--output "data/runs/${{ inputs.platform }}/' + '${{ inputs.platform }}-host.json"' + ) + + self.assertEqual(steps[0], "Check Linux benchmark host") + self.assertLess( + steps.index("Prepare Windows benchmark scratch"), + steps.index("Check Windows benchmark host"), + ) + for operating_system, condition in ( + ("Linux", "runner.os != 'Windows'"), + ("Windows", "runner.os == 'Windows'"), + ): + check = f"Check {operating_system} benchmark host" + with self.subTest(step=check): + self.assertIn( + f" if: {condition}", _composite_action_step(action, check) ) + script = _composite_action_script(action, check) + self.assertEqual(script.count("performance check-host"), 1) + self.assertIn('--platform "${{ inputs.platform }}"', script) + self.assertIn("--history-dir data", script) + self.assertIn(output, script) + self.assertIn("--summary", script) + measuring = [ + step + for step in steps + if step.startswith(f"Run {operating_system} ") + ] + self.assertTrue(measuring) + for step in measuring: + self.assertLess(steps.index(check), steps.index(step)) + windows = _composite_action_script(action, "Check Windows benchmark host") + self.assertIn( + '$ScratchArgs = @("--scratch-dir", $env:NVX_BENCHMARK_SCRATCH_DIR)', + windows, + ) + self.assertIn("@ScratchArgs", windows) + self.assertTrue( + windows.rstrip().endswith("if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }") + ) + self.assertIn( + "set -euo pipefail", + _composite_action_script(action, "Check Linux benchmark host"), + ) - upgrade = output / "recipes" / commit / "main/foo/foo.post-upgrade" - self.assertFalse(upgrade.is_symlink()) - self.assertEqual(upgrade.read_bytes(), self.INSTALL_SCRIPT) - source_manifest = json.loads( - (output / "manifest.json").read_text(encoding="utf-8") - ) - self.assertEqual( - source_manifest["format"], AlpineBuildConstants.SOURCE_MANIFEST_FORMAT - ) - self.assertEqual( - source_manifest["executables"], - [ - f"recipes/{commit}/main/foo/foo.post-install", - f"recipes/{commit}/main/foo/foo.post-upgrade", - ], - ) - self.assertIn( - f"recipes/{commit}/main/foo/foo.post-upgrade", - dict(common.verify_sha256_sums(output).files), - ) - if os.name == "posix": - self.assertEqual(upgrade.stat().st_mode & 0o777, 0o755) + def test_performance_history_tracks_the_expected_hosts(self): + actions = BuildConstants.REPO_ROOT / ".github" / "actions" + persist = (actions / "persist-performance-results" / "action.yml").read_text( + encoding="utf-8" + ) + gate = (actions / "gate-performance" / "action.yml").read_text(encoding="utf-8") - def test_collection_replaces_stale_output_and_passes_release_validation(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - cache = root / "aports" - commit = self._symlinked_recipe_commit(cache) - manifest = self._package_manifest(root, commit) - source_dir = root / "sources" - output = source_dir / "alpine" - stale_files = ( - output / "recipes" / ("0" * 40) / "main/stale/APKBUILD", - output / "upstream" / ("0" * 40) / "main/stale/stale.tar.gz", - ) - for stale in stale_files: - stale.parent.mkdir(parents=True) - stale.write_bytes(b"stale") - (output / "manifest.json").write_text("{}", encoding="utf-8") - (output / "SHA256SUMS").write_text("", encoding="ascii") - - def fetch_upstream(bundle: Path, _alpine_version: str) -> None: - upstream = bundle / "upstream" / commit / "main" / "foo" - upstream.mkdir(parents=True) - (upstream / "foo-1.0.tar.gz").write_bytes(b"upstream") - - with ( - patch.object(collect_alpine_sources, "_prepare_aports"), - patch.object( - collect_alpine_sources, - "_fetch_upstream_sources", - side_effect=fetch_upstream, - ), - patch.object(BuildConstants, "SOURCE_DIR", source_dir), - contextlib.redirect_stdout(io.StringIO()), - ): - collect_alpine_sources.collect_alpine_sources([manifest], output, cache) - executables = release._validate_alpine_sources([manifest]) + # dev refuses results whose recorded host differs from data/. + self.assertIn( + " --history-dir data \\\n --require-host\n", + _composite_action_step(persist, "Persist results"), + ) + # The gate compares the pull request's expected hosts with the base + # branch's, so that a new host must reset its series. + load = _composite_action_script(gate, "Load base branch performance history") + self.assertIn("done < <(git ls-tree --name-only FETCH_HEAD data/)", load) + self.assertIn("data/*-host.json)", load) + self.assertIn( + 'git show "FETCH_HEAD:${path}" > "data/baseline/${path#data/}"', load + ) + command = _composite_action_step(gate, "Gate on performance regressions") + self.assertIn("--baseline-dir data/baseline", command) + self.assertIn("--history-reset-dir data", command) - self.assertEqual( - executables, - { - f"recipes/{commit}/main/foo/foo.post-install", - f"recipes/{commit}/main/foo/foo.post-upgrade", - }, - ) - for stale in stale_files: - self.assertFalse(stale.parent.parent.exists(), stale) - self.assertEqual( - sorted(path.name for path in output.iterdir()), - ["SHA256SUMS", "manifest.json", "recipes", "upstream"], - ) + def test_ci_remeasures_only_unstable_lifecycle_results(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "run-benchmark" + / "action.yml" + ).read_text(encoding="utf-8") - def test_recipe_extraction_materializes_symlinks_inside_recipe(self): - member = self._member - archive = self._archive( - member("main/", tarfile.DIRTYPE, mode=0o775), - member("main/foo/", tarfile.DIRTYPE, mode=0o775), - member("main/foo/patches/", tarfile.DIRTYPE, mode=0o775), - member("main/foo/APKBUILD", data=b"pkgname=foo\n"), - member("main/foo/foo.post-install", data=self.INSTALL_SCRIPT, mode=0o775), - member( - "main/foo/foo.post-upgrade", - tarfile.SYMTYPE, - linkname="foo.post-install", + self.assertEqual(action.count("performance validate-openvmm"), 2) + for step, loop, unstable in ( + ( + "Run Linux acceptance test", + "for attempt in 1 2; do", + '"${validation_status}" -ne 75', ), - member( - "main/foo/foo.pre-upgrade", - tarfile.SYMTYPE, - linkname="./foo.post-upgrade", + ( + "Run Windows acceptance test", + "foreach ($Attempt in 1, 2)", + "$ValidationStatus -ne 75", ), - member("main/foo/patches/fix.patch", data=b"patch\n"), - member( - "main/foo/fix.patch", - tarfile.SYMTYPE, - linkname="patches/../patches/fix.patch", + ): + with self.subTest(step=step): + script = _composite_action_script(action, step) + self.assertEqual(script.count("performance validate-openvmm"), 1) + self.assertEqual(script.count(loop), 1) + self.assertEqual(script.count(unstable), 1) + self.assertEqual( + script.count("Lifecycle snapshot generation was unstable"), + 1, + ) + self.assertEqual(script.count("--attempt-log"), 1) + self.assertIn("acceptance-attempts.json", script) + # Publication names the attempt that supplied the lifecycle metrics. + for step, log in ( + ( + "Publish Linux performance results", + '--lifecycle-attempt-log "${run_dir}/acceptance-attempts.json"', ), - member( - "main/foo/patches/APKBUILD", tarfile.SYMTYPE, linkname="../APKBUILD" + ( + "Publish Windows performance results", + '--lifecycle-attempt-log "$RunDir/acceptance-attempts.json"', ), - ) - with tempfile.TemporaryDirectory() as temporary: - destination = Path(temporary) - - executables = collect_alpine_sources._safe_extract( - archive, destination, "main/foo" - ) + ): + with self.subTest(step=step): + script = _composite_action_script(action, step) + self.assertEqual(script.count(log), 1) + self.assertLess(script.index("--lifecycle-input"), script.index(log)) - self.assertEqual( - executables, - [ - "main/foo/foo.post-install", - "main/foo/foo.post-upgrade", - "main/foo/foo.pre-upgrade", - ], - ) - recipe = destination / "main" / "foo" - expected = { - "APKBUILD": (b"pkgname=foo\n", 0o644), - "foo.post-install": (self.INSTALL_SCRIPT, 0o755), - "foo.post-upgrade": (self.INSTALL_SCRIPT, 0o755), - "foo.pre-upgrade": (self.INSTALL_SCRIPT, 0o755), - "fix.patch": (b"patch\n", 0o644), - "patches/fix.patch": (b"patch\n", 0o644), - "patches/APKBUILD": (b"pkgname=foo\n", 0o644), - } - for relative, (contents, mode) in expected.items(): - with self.subTest(path=relative): - path = recipe / relative - self.assertFalse(path.is_symlink()) - self.assertEqual(path.read_bytes(), contents) - if os.name == "posix": - self.assertEqual(path.stat().st_mode & 0o777, mode) + def test_windows_benchmarks_use_provisioned_data_volume_scratch(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "run-benchmark" + / "action.yml" + ).read_text(encoding="utf-8") + setup = ( + BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" + ).read_text(encoding="utf-8") + prepare = _composite_action_step(action, "Prepare Windows benchmark scratch") + cleanup = _composite_action_step(action, "Remove Windows benchmark scratch") - def test_recipe_extraction_accepts_implicit_directories(self): - member = self._member - archive = self._archive( - member("main/foo/APKBUILD", data=b"pkgname=foo\n"), - member("main/foo/scripts/install", data=self.INSTALL_SCRIPT, mode=0o775), - member( - "main/foo/foo.post-install", - tarfile.SYMTYPE, - linkname="scripts/install", - ), - member( - "main/foo/scripts/APKBUILD", tarfile.SYMTYPE, linkname="../APKBUILD" - ), + self.assertIn('$BenchmarkScratchVariable = "NVX_BENCHMARK_SCRATCH"', setup) + self.assertIn( + '[Environment]::GetEnvironmentVariable("NVX_BENCHMARK_SCRATCH", "Machine")', + prepare, ) - with tempfile.TemporaryDirectory() as temporary: - destination = Path(temporary) - - executables = collect_alpine_sources._safe_extract( - archive, destination, "main/foo" - ) - - self.assertEqual( - executables, - ["main/foo/foo.post-install", "main/foo/scripts/install"], - ) - recipe = destination / "main" / "foo" - for relative, contents in ( - ("foo.post-install", self.INSTALL_SCRIPT), - ("scripts/APKBUILD", b"pkgname=foo\n"), - ): - with self.subTest(path=relative): - path = recipe / relative - self.assertFalse(path.is_symlink()) - self.assertEqual(path.read_bytes(), contents) - if os.name == "posix": - for directory in (destination / "main", recipe, recipe / "scripts"): - with self.subTest(directory=directory.name): - self.assertEqual(directory.stat().st_mode & 0o777, 0o755) - - def test_recipe_extraction_rejects_unsafe_members(self): - member = self._member - recipe = ( - member("main/", tarfile.DIRTYPE), - member("main/foo/", tarfile.DIRTYPE), - member("main/foo/sub/", tarfile.DIRTYPE), - member("main/foo/APKBUILD", data=b"pkgname=foo\n"), - member("main/foo/sub/file", data=b"file\n"), + self.assertIn('"NVX_BENCHMARK_SCRATCH_DIR=$Scratch"', prepare) + self.assertIn("::warning::NVX_BENCHMARK_SCRATCH is not provisioned", prepare) + self.assertIn(" if: always() && runner.os == 'Windows'", cleanup) + self.assertLess( + action.index("- name: Remove Windows benchmark scratch"), + action.index("- name: Upload benchmark diagnostics"), + ) + for step in ( + "Run Windows acceptance test", + "Run Windows performance suite", + "Run Windows multi-vCPU shell restore", + "Run Windows device operation rates", + ): + with self.subTest(step=step): + script = _composite_action_script(action, step) + self.assertIn( + '$ScratchArgs = @("--scratch-dir", $env:NVX_BENCHMARK_SCRATCH_DIR)', + script, + ) + self.assertIn("@ScratchArgs", script) + self.assertLess( + action.index("- name: Prepare Windows benchmark scratch"), + action.index(f"- name: {step}"), + ) + self.assertIn( + " Configure-SccacheEnvironment\n Configure-BenchmarkScratch\n", + setup, + ) + self.assertIn( + "Assert-ServiceDirectoryAcl -Path $SccacheDirectory -Writable\n" + " Assert-BenchmarkScratch\n", + setup, ) - cases: dict[str, tuple[tuple[tarfile.TarInfo, bytes], ...]] = { - "absolute path": (member("/main/foo/file"),), - "parent path": (member("main/foo/../../file"),), - "outside recipe": (member("main/bar/file"),), - "duplicate path": (member("main/foo/APKBUILD"),), - "member below a file": (member("main/foo/APKBUILD/file"),), - "member below a symlink": ( - member("main/foo/alias", tarfile.SYMTYPE, linkname="sub"), - member("main/foo/alias/file"), - ), - "hard link": ( - member("main/foo/link", tarfile.LNKTYPE, linkname="main/foo/APKBUILD"), - ), - "fifo": (member("main/foo/pipe", tarfile.FIFOTYPE),), - "character device": (member("main/foo/device", tarfile.CHRTYPE),), - "absolute symlink": ( - member("main/foo/link", tarfile.SYMTYPE, linkname="/etc/passwd"), - ), - "escaping symlink": ( - member("main/foo/link", tarfile.SYMTYPE, linkname="../../../passwd"), - ), - "sibling recipe symlink": ( - member("main/foo/link", tarfile.SYMTYPE, linkname="../bar/APKBUILD"), - ), - "dangling symlink": ( - member("main/foo/link", tarfile.SYMTYPE, linkname="missing"), - ), - "symlink through a missing directory": ( - member( - "main/foo/link", - tarfile.SYMTYPE, - linkname="missing/../APKBUILD", - ), - ), - "directory symlink": ( - member("main/foo/link", tarfile.SYMTYPE, linkname="sub"), - ), - "symlink loop": ( - member("main/foo/first", tarfile.SYMTYPE, linkname="second"), - member("main/foo/second", tarfile.SYMTYPE, linkname="first"), - ), - "symlinked directory traversal": ( - member("main/foo/link", tarfile.SYMTYPE, linkname="alias/file"), - member("main/foo/alias", tarfile.SYMTYPE, linkname="sub"), - ), - } - for name, members in cases.items(): - with self.subTest(case=name), tempfile.TemporaryDirectory() as temporary: - destination = Path(temporary) / "recipes" - - with self.assertRaises(collect_alpine_sources.SourceError): - collect_alpine_sources._safe_extract( - self._archive(*recipe, *members), - destination, - "main/foo", - ) - - self.assertEqual(list(destination.iterdir()), []) - - def test_collection_guards_output_before_replacing_it(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - manifest = self._package_manifest(root, "0" * 40) - output = root / "alpine" - (output / "recipes").mkdir(parents=True) - notes = output / "notes.txt" - notes.write_text("keep", encoding="utf-8") - not_directory = root / "file" - not_directory.write_text("keep", encoding="utf-8") - - for target, message in ( - (output, "unexpected entries: notes.txt"), - (not_directory, "is not a directory"), - ): - with ( - self.subTest(output=target.name), - patch.object(collect_alpine_sources, "_prepare_aports") as prepare, - self.assertRaisesRegex( - collect_alpine_sources.SourceError, - message, - ), - ): - collect_alpine_sources.collect_alpine_sources( - [manifest], - target, - root / "aports", - skip_upstream=True, - ) - prepare.assert_not_called() - self.assertTrue(notes.is_file()) - self.assertTrue((output / "recipes").is_dir()) - notes.unlink() + @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") + def test_windows_acceptance_forwards_benchmark_scratch(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "run-benchmark" + / "action.yml" + ).read_text(encoding="utf-8") + script = _composite_action_script(action, "Run Windows acceptance test") + for name, value in ( + ("backend", "whp"), + ("platform", "windows-whp-virtual-machine"), + ("warmups", "1"), + ("runs", "10"), + ("teardown-mode", "guest-exit"), + ): + script = script.replace("${{ inputs." + name + " }}", value) + stub = """ +function python { + $global:LASTEXITCODE = 0 + if ($args[1] -ne "benchmark") { + return + } + $output = $args[[Array]::IndexOf($args, "--output") + 1] + $index = [Array]::IndexOf($args, "--scratch-dir") + $scratch = "" + if ($index -ge 0) { + $scratch = $args[$index + 1] + } + New-Item -ItemType Directory -Path (Split-Path $output) -Force | Out-Null + Set-Content -LiteralPath $output -Encoding UTF8 -Value ( + ConvertTo-Json -Compress @{ scratch = $scratch } + ) +} +""" + for scratch in ("", r"F:\nvx-benchmark-scratch\job"): with ( - patch.object(collect_alpine_sources, "_prepare_aports"), - patch.object( - collect_alpine_sources.shutil, - "rmtree", - side_effect=PermissionError(13, "Permission denied"), - ), - self.assertRaisesRegex( - collect_alpine_sources.SourceError, - "cannot remove previous Alpine source output.*remove it manually", - ), + self.subTest(scratch=scratch), + tempfile.TemporaryDirectory() as temporary, ): - collect_alpine_sources.collect_alpine_sources( - [manifest], - output, - root / "aports", - skip_upstream=True, + root = Path(temporary).resolve() + environment = os.environ.copy() + environment.pop("NVX_BENCHMARK_SCRATCH_DIR", None) + if scratch: + environment["NVX_BENCHMARK_SCRATCH_DIR"] = scratch + result = subprocess.run( + [ + "powershell.exe", + "-NoProfile", + "-NonInteractive", + "-Command", + stub + script, + ], + cwd=root, + env=environment, + capture_output=True, + text=True, + timeout=30, + check=False, ) - def test_collection_refuses_symlinked_output(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - output = root / "alpine" - (output / "recipes").mkdir(parents=True) - link = root / "alpine-link" - try: - link.symlink_to(output, target_is_directory=True) - except OSError as error: - self.skipTest(f"symlinks are unavailable: {error}") - - with ( - patch.object(collect_alpine_sources, "_prepare_aports") as prepare, - self.assertRaisesRegex( - collect_alpine_sources.SourceError, - "Alpine source output is not a directory", - ), - ): - collect_alpine_sources.collect_alpine_sources( - [self._package_manifest(root, "0" * 40)], - link, - root / "aports", - skip_upstream=True, + self.assertEqual(result.returncode, 0, result.stderr) + accepted = ( + root + / "data" + / "runs" + / "windows-whp-virtual-machine" + / "microvm-v2" + / "1vcpu" + / "acceptance.json" + ) + self.assertEqual( + json.loads(accepted.read_text(encoding="utf-8-sig")), + {"scratch": scratch}, ) - prepare.assert_not_called() - self.assertTrue((output / "recipes").is_dir()) + @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") + def test_windows_benchmark_scratch_steps_manage_per_job_directories(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "run-benchmark" + / "action.yml" + ).read_text(encoding="utf-8") + machine_lookup = ( + '[Environment]::GetEnvironmentVariable("NVX_BENCHMARK_SCRATCH", "Machine")' + ) + prepare = _composite_action_script( + action, "Prepare Windows benchmark scratch" + ).replace("${{ inputs.platform }}", "windows-whp-virtual-machine") + cleanup = _composite_action_script(action, "Remove Windows benchmark scratch") + self.assertIn(machine_lookup, prepare) - def test_collection_refuses_output_junctions(self): - if sys.platform != "win32": - self.skipTest("directory junctions exist only on Windows") - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - manifest = self._package_manifest(root, "0" * 40) - elsewhere = root / "elsewhere" - (elsewhere / "recipes").mkdir(parents=True) - kept = elsewhere / "recipes" / "kept" - kept.write_bytes(b"kept") - output = root / "alpine" - output.mkdir() - cases = ( - ( - root / "junction", - elsewhere, - root / "junction", - "Alpine source output is not a directory", - ), - ( - output / "recipes", - elsewhere / "recipes", - output, - "Alpine source generated path is not a directory", - ), + def run_step(script: str, environment: dict[str, str]): + return subprocess.run( + ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script], + env=environment, + capture_output=True, + text=True, + timeout=30, + check=False, ) - for link, target, collected, message in cases: - subprocess.run( - ["cmd", "/c", "mklink", "/J", str(link), str(target)], - capture_output=True, - check=True, - ) - with ( - self.subTest(link=link.name), - patch.object(collect_alpine_sources, "_prepare_aports"), - self.assertRaisesRegex(collect_alpine_sources.SourceError, message), - ): - collect_alpine_sources.collect_alpine_sources( - [manifest], - collected, - root / "aports", - skip_upstream=True, - ) - self.assertEqual(kept.read_bytes(), b"kept") - def test_upstream_fetch_uses_mirror_and_restores_bundle_ownership(self): - with ( - tempfile.TemporaryDirectory() as temporary, - patch.object(collect_alpine_sources, "_run") as run, - ): - collect_alpine_sources._fetch_upstream_sources(Path(temporary), "3.24") + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() / "scratch" + root.mkdir() + stale = root / "windows-whp-virtual-machine-1-1" + fresh = root / "windows-whp-virtual-machine-2-1" + for directory in (stale, fresh): + directory.mkdir() + (directory / "memory.bin").write_bytes(b"ram") + old = time.time() - 7 * 60 * 60 + os.utime(stale, (old, old)) + github_env = Path(temporary) / "github-env.txt" + github_env.write_text("", encoding="utf-8") + environment = os.environ.copy() + environment.update( + { + "GITHUB_ENV": str(github_env), + "GITHUB_RUN_ID": "35805840174", + "GITHUB_RUN_ATTEMPT": "2", + } + ) - command = run.call_args.args[0] - self.assertEqual(command[:4], ["docker", "run", "--rm", "--volume"]) - self.assertEqual( - command[5:7], - [ - "--env", - "DISTFILES_MIRROR=https://distfiles.alpinelinux.org/distfiles/v3.24", - ], - ) - self.assertEqual(command[7], "alpine:3.24") - script = command[-1] - self.assertIn("owner=$(stat -c '%u:%g' /bundle)", script) - self.assertIn('chown -R "$owner" /bundle/upstream', script) - self.assertLess( - script.index("trap restore_owner EXIT"), - script.index("mkdir -p /bundle/upstream"), - ) + result = run_step( + prepare.replace(machine_lookup, f"'{root}'"), + environment, + ) + self.assertEqual(result.returncode, 0, result.stderr) + scratch = root / "windows-whp-virtual-machine-35805840174-2" + self.assertTrue(scratch.is_dir()) + self.assertFalse(stale.exists()) + self.assertTrue(fresh.is_dir()) + self.assertEqual( + github_env.read_text(encoding="utf-8-sig").splitlines(), + [f"NVX_BENCHMARK_SCRATCH_DIR={scratch}"], + ) -class BuildTests(unittest.TestCase): - def test_build_config_owns_standard_runtime_paths(self): - config = build_config.BuildConfig() - alpine = config.initramfs_config("alpine") - ubuntu_config = config.initramfs_config("ubuntu") - distro = config.distro_layer_config() + locked = scratch / "locked.bin" + with locked.open("wb"): + result = run_step( + cleanup, + {**environment, "NVX_BENCHMARK_SCRATCH_DIR": str(scratch)}, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn( + "::warning::Could not remove benchmark scratch", result.stdout + ) - self.assertIsInstance(config.docker, build_config.DockerBuildConfig) - self.assertIsInstance(config.kernel, build_config.KernelBuildConfig) - self.assertIsInstance(config.openvmm, build_config.OpenVmmBuildConfig) - self.assertIsInstance(alpine, build_config.InitramfsBuildConfig) - self.assertIsInstance(ubuntu_config, build_config.InitramfsBuildConfig) - self.assertIsInstance(distro, build_config.DistroLayerBuildConfig) - self.assertIsNone(config.openvmm.backend) - self.assertEqual(config.selected_guests(), ("alpine",)) - self.assertEqual(config.kernel.work, BuildConstants.BUILD_DIR / "linux") - self.assertEqual(config.kernel.output, BuildConstants.BUILD_DIR / "vmlinux") - self.assertEqual( - alpine.work, - BuildConstants.BUILD_DIR / "initramfs-alpine-work", - ) - self.assertEqual( - alpine.output, - BuildConstants.BUILD_DIR / "initramfs.cpio.gz", + result = run_step( + cleanup, + {**environment, "NVX_BENCHMARK_SCRATCH_DIR": str(scratch)}, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(scratch.exists()) + + github_env.write_text("", encoding="utf-8") + result = run_step(prepare.replace(machine_lookup, "''"), environment) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn( + "::warning::NVX_BENCHMARK_SCRATCH is not provisioned", result.stdout + ) + self.assertEqual(github_env.read_text(encoding="utf-8"), "") + + @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") + def test_windows_setup_selects_largest_data_volume_for_benchmark_scratch(self): + setup = BuildConstants.REPO_ROOT / "scripts" / "setup" / "setup-windows-whp.ps1" + harness = f""" +Set-StrictMode -Version 2.0 +$ErrorActionPreference = "Stop" +$tokens = $null +$errors = $null +$ast = [System.Management.Automation.Language.Parser]::ParseFile( + '{setup}', + [ref]$tokens, + [ref]$errors +) +if ($errors.Count -ne 0) {{ + throw "setup script has parse errors" +}} +foreach ($name in "Test-SystemVolumePath", "Get-BenchmarkScratchDirectory") {{ + $definition = $ast.Find({{ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq $name + }}, $true) + . ([scriptblock]::Create($definition.Extent.Text)) +}} +$BenchmarkScratchVariable = "NVX_TEST_UNSET_{uuid.uuid4().hex}" +$BenchmarkScratchName = "nvx-benchmark-scratch" +$BenchmarkScratchDirectory = $null +$env:SystemDrive = "C:" +$Volumes = @( + [pscustomobject]@{{ DriveLetter = [char]"C"; DriveType = "Fixed"; FileSystem = "NTFS"; Size = 900GB }}, + [pscustomobject]@{{ DriveLetter = [char]"D"; DriveType = "Fixed"; FileSystem = "NTFS"; Size = 64GB }}, + [pscustomobject]@{{ DriveLetter = $null; DriveType = "Fixed"; FileSystem = "NTFS"; Size = 2TB }}, + [pscustomobject]@{{ DriveLetter = [char]"E"; DriveType = "CD-ROM"; FileSystem = ""; Size = 1TB }}, + [pscustomobject]@{{ DriveLetter = [char]"F"; DriveType = "Fixed"; FileSystem = "NTFS"; Size = 512GB }}, + [pscustomobject]@{{ DriveLetter = [char]"G"; DriveType = "Fixed"; FileSystem = "FAT32"; Size = 1TB }} +) +function Get-Volume {{ + $script:Volumes +}} +Write-Output (Get-BenchmarkScratchDirectory) +$Volumes = @($Volumes[0]) +Write-Output ("none=" + ($null -eq (Get-BenchmarkScratchDirectory))) +$BenchmarkScratchDirectory = "H:\\explicit\\scratch" +Write-Output (Get-BenchmarkScratchDirectory) +""" + + result = subprocess.run( + ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", harness], + capture_output=True, + text=True, + timeout=30, + check=False, ) + + self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( - ubuntu_config.output, - BuildConstants.BUILD_DIR / "initramfs-ubuntu.cpio.gz", + result.stdout.splitlines(), + [ + r"F:\nvx-benchmark-scratch", + "none=True", + r"H:\explicit\scratch", + ], ) - self.assertEqual( - distro.output, - BuildConstants.BUILD_DIR / "ubuntu-distro.erofs", + + def test_cli_validation_runs_every_script_test_module(self): + # Each scripts/test_*.py module must run in both CLI validation jobs; + # the retired test_control_session.py once ran in neither (#229). + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "validate-nvx" + / "action.yml" + ).read_text(encoding="utf-8") + modules = { + path.name + for path in (BuildConstants.REPO_ROOT / "scripts").glob("test_*.py") + } + self.assertIn("test_nvx_tools.py", modules) + for step in ("Validate NVX CLI on Linux", "Validate NVX CLI on Windows"): + with self.subTest(step=step): + script = _composite_action_script(action, step) + invoked = re.findall(r"(?:^|\s)scripts[/\\](test_\w+\.py)", script) + self.assertEqual(sorted(invoked), sorted(modules)) + + def test_ci_runs_the_public_sandbox_acceptance_on_every_backend(self): + workflows = BuildConstants.REPO_ROOT / ".github" / "workflows" + workflow = (workflows / "ci.yml").read_text(encoding="utf-8") + microvm_workflow = (workflows / "run-nvx-microvm-tests.yml").read_text( + encoding="utf-8" ) - self.assertEqual(config.docker.artifact_destination, BuildConstants.BUILD_DIR) - self.assertEqual( - config.docker.linux_source_destination, - BuildConstants.SOURCE_DIR / "linux", + for backend in ("kvm", "mshv", "whp"): + with self.subTest(backend=backend): + job = _workflow_job(workflow, f"nvx-microvm-tests-{backend}") + self.assertIn( + "uses: ./.github/workflows/run-nvx-microvm-tests.yml", job + ) + self.assertIn(f"backend: {backend}", job) + self.assertNotIn("debug-kernel: true", job) + for system in ("Linux", "Windows"): + with self.subTest(system=system): + acceptance = _workflow_step( + microvm_workflow, + "test", + f"Run public managed sandbox acceptance on {system}", + ) + self.assertIn("!inputs.debug-kernel", acceptance) + self.assertIn("--scenario managed-exec-config", acceptance) + self.assertIn("--scenario sandbox-lifecycle", acceptance) + self.assertIn( + '--output-dir "build/test-results/public-exec-${{ inputs.backend }}"', + acceptance, + ) + smoke = _workflow_step( + microvm_workflow, + "test", + f"Run Ubuntu sandbox layer smoke test on {system}", + ) + for option in ( + "--entrypoint /sbin/nvx-sandbox-smoke", + "--arg limits", + "--arg 33554432", + "--arg 8", + "--memory-max 33554432", + "--pids-max 8", + ): + self.assertIn(option, smoke) + upload = _workflow_step( + microvm_workflow, "test", "Upload NVX microVM failure logs" ) + self.assertIn("build/test-results/public-exec-${{ inputs.backend }}", upload) - def test_detects_openvmm_build_platform_without_runtime_devices(self): - cases: tuple[ - tuple[ - str, - build_config.OpenVmmBackend | None, - build_config.OpenVmmPlatform, - str, - build_config.OpenVmmBuildMode, - ], - ..., - ] = ( - ( - "win32", - None, - "windows-msvc", - "x86_64-pc-windows-msvc", - "native", - ), - ( - "win32", - "whp", - "windows-msvc", - "x86_64-pc-windows-msvc", - "native", - ), - ( - "linux", - None, - "linux-gnu", - "x86_64-unknown-linux-gnu", - "native", - ), - ( - "linux", - "kvm", - "linux-gnu", - "x86_64-unknown-linux-gnu", - "native", - ), - ( - "linux", - "mshv", - "linux-musl", - "x86_64-unknown-linux-musl", - "musl", - ), - ) - for host, backend, expected_platform, expected_target, expected_mode in cases: - with self.subTest(host=host, backend=backend): - with ( - patch.object(build.sys, "platform", host), - patch.object(build.os, "access") as access, - ): - config = build_config.OpenVmmBuildConfig() - platform = build.detect_openvmm_platform(backend) - - self.assertEqual(platform, expected_platform) - self.assertEqual(config.openvmm_target(platform), expected_target) - self.assertEqual(config.openvmm_build_mode(platform), expected_mode) - access.assert_not_called() + @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") + def test_windows_cli_validation_stops_at_each_failed_command(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "validate-nvx" + / "action.yml" + ).read_text(encoding="utf-8") + script = _composite_action_script(action, "Validate NVX CLI on Windows") + command_count = sum(line.startswith("python ") for line in script.splitlines()) + self.assertGreater(command_count, 0) + stub = """ +$CommandCount = 0 +function python { + $script:CommandCount += 1 + Write-Output "python-$script:CommandCount" + $global:LASTEXITCODE = 0 + if ($script:CommandCount -eq $script:FailureAt) { + $global:LASTEXITCODE = 37 + } +} +""" + for failure_at in range(command_count + 1): + with self.subTest(failure_at=failure_at): + result = subprocess.run( + [ + "powershell.exe", + "-NoProfile", + "-NonInteractive", + "-Command", + f"$FailureAt = {failure_at}\n{stub}\n{script}", + ], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + self.assertEqual( + result.returncode, 37 if failure_at else 0, result.stderr + ) + self.assertEqual( + result.stdout.splitlines(), + [ + f"python-{index}" + for index in range(1, (failure_at or command_count) + 1) + ], + ) - def test_rejects_unsupported_openvmm_build_platforms(self): - cases: tuple[tuple[str, build_config.OpenVmmBackend | None], ...] = ( - ("linux", "whp"), - ("win32", "kvm"), - ("win32", "mshv"), - ("darwin", None), - ("darwin", "kvm"), - ("darwin", "mshv"), - ("darwin", "whp"), + @unittest.skipUnless(os.name == "nt", "requires Windows PowerShell") + def test_windows_acceptance_preserves_attempts_and_publishes_only_valid_data(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "run-benchmark" + / "action.yml" + ).read_text(encoding="utf-8") + script = _composite_action_script(action, "Run Windows acceptance test") + for name, value in ( + ("backend", "whp"), + ("platform", "windows-whp-virtual-machine"), + ("warmups", "1"), + ("runs", "10"), + ("teardown-mode", "guest-exit"), + ): + script = script.replace("${{ inputs." + name + " }}", value) + stub = """ +$BenchmarkCount = 0 +$ValidationCount = 0 +function python { + if ($args[1] -eq "benchmark") { + $script:BenchmarkCount += 1 + $output = $args[[Array]::IndexOf($args, "--output") + 1] + New-Item -ItemType Directory -Path (Split-Path $output) -Force | Out-Null + Set-Content -LiteralPath $output -Encoding UTF8 -Value ( + '{"attempt":' + $script:BenchmarkCount + '}' ) - for host, backend in cases: - with self.subTest(host=host, backend=backend): - with ( - patch.object(build.sys, "platform", host), - patch.object(build.os, "access") as access, - self.assertRaisesRegex( - common.ScriptError, - "unsupported", + $global:LASTEXITCODE = $script:BenchmarkStatus + } + elseif ($args[1] -eq "performance") { + $log = $args[[Array]::IndexOf($args, "--attempt-log") + 1] + Add-Content -LiteralPath "attempt-logs.txt" -Value $log + $global:LASTEXITCODE = $script:ValidationStatuses[$script:ValidationCount] + $script:ValidationCount += 1 + } + else { + throw "unexpected command: $args" + } +} +""" + for name, statuses, benchmark_status in ( + ("first-success", (0,), 0), + ("remeasured-success", (75, 0), 0), + ("both-unstable", (75, 75), 0), + ("invalid-data", (1,), 0), + ("benchmark-failure", (0,), 42), + ): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + output = ( + root + / "data" + / "runs" + / "windows-whp-virtual-machine" + / "microvm-v2" + / "1vcpu" + ) + output.mkdir(parents=True) + accepted = output / "acceptance.json" + accepted.write_text('{"attempt":"stale"}', encoding="utf-8") + # A rerun in the same directory must not keep a previous + # run's attempt log or later attempts. + attempt_log = output / "acceptance-attempts.json" + attempt_log.write_text('{"attempts":"stale"}', encoding="utf-8") + (output / "acceptance-attempt-2.json").write_text( + '{"attempt":"stale"}', encoding="utf-8" + ) + setup = ( + "$ValidationStatuses = @(" + + ", ".join(str(status) for status in statuses) + + f")\n$BenchmarkStatus = {benchmark_status}\n" + ) + result = subprocess.run( + [ + "powershell.exe", + "-NoProfile", + "-NonInteractive", + "-Command", + setup + stub + script, + ], + cwd=root, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + expected_status = benchmark_status or statuses[-1] + self.assertEqual(result.returncode, expected_status, result.stderr) + self.assertFalse(attempt_log.exists()) + validations = root / "attempt-logs.txt" + self.assertEqual( + ( + validations.read_text(encoding="utf-8").splitlines() + if validations.exists() + else [] ), - ): - build.detect_openvmm_platform(backend) - access.assert_not_called() - - def test_linux_openvmm_builds_do_not_require_hypervisor_devices(self): - for devices_usable in (False, True): - for combined in (False, True): - with ( - self.subTest(devices_usable=devices_usable, combined=combined), - tempfile.TemporaryDirectory() as temporary, - ): - openvmm_dir = Path(temporary) / "openvmm" - output = openvmm_dir / "target" / "release" / "openvmm" - output.parent.mkdir(parents=True) - output.write_bytes(b"openvmm") - (openvmm_dir / "Cargo.toml").touch() - config = build_config.OpenVmmBuildConfig( - skip_restore=True, - directory=openvmm_dir, - output=output, + [ + "data/runs/windows-whp-virtual-machine/microvm-v2/1vcpu/" + "acceptance-attempts.json" + ] + * (0 if benchmark_status else len(statuses)), + ) + self.assertEqual( + sorted(path.name for path in output.glob("acceptance-attempt-*")), + [ + f"acceptance-attempt-{index}.json" + for index in range(1, len(statuses) + 1) + ], + ) + for index in range(1, len(statuses) + 1): + attempt = output / f"acceptance-attempt-{index}.json" + self.assertEqual( + json.loads(attempt.read_text(encoding="utf-8-sig")), + {"attempt": index}, + ) + self.assertEqual(accepted.exists(), expected_status == 0) + if expected_status == 0: + self.assertEqual( + json.loads(accepted.read_text(encoding="utf-8-sig")), + {"attempt": len(statuses)}, ) - with ( - patch.object(build.sys, "platform", "linux"), - patch.object( - build.os, "access", return_value=devices_usable - ) as access, - patch.object(build, "run_checked") as run_checked, - patch.object(build, "record_openvmm_provenance"), - patch.object(build, "build_guest") as guest, - ): - if combined: - aggregate = build_config.BuildConfig(openvmm=config) - build.build_all(aggregate) - guest.assert_called_once_with(aggregate) - else: - build.build_openvmm(config) - guest.assert_not_called() - - access.assert_not_called() - run_checked.assert_called_once_with( - [ - "cargo", - "build", - "--release", - "-p", - "openvmm", - "--bin", - "openvmm", - ], - cwd=openvmm_dir, - ) - - def test_openvmm_target_output_matches_build_mode(self): - config = build_config.OpenVmmBuildConfig(directory=Path("checkout")) - cases: tuple[tuple[build_config.OpenVmmPlatform, Path], ...] = ( - ("linux-gnu", Path("target") / "release" / "openvmm"), - ( - "linux-musl", - Path("target") / "x86_64-unknown-linux-musl" / "release" / "openvmm", - ), - ("windows-msvc", Path("target") / "release" / "openvmm.exe"), - ) - for platform, relative_output in cases: - with self.subTest(platform=platform): - self.assertEqual( - config.openvmm_target_output(platform), - config.directory / relative_output, - ) - - def test_native_openvmm_build_normalizes_output_before_recording_provenance(self): - platforms: tuple[tuple[build_config.OpenVmmPlatform, str], ...] = ( - ("linux-gnu", "openvmm"), - ("windows-msvc", "openvmm.exe"), - ) - for platform, executable in platforms: - for existing_output in (False, True): - with ( - self.subTest(platform=platform, existing_output=existing_output), - tempfile.TemporaryDirectory() as temporary, - ): - root = Path(temporary) - openvmm_dir = root / "openvmm" - native_output = openvmm_dir / "target" / "release" / executable - native_output.parent.mkdir(parents=True) - binary = b"newly-built-openvmm" - native_output.write_bytes(binary) - (openvmm_dir / "Cargo.toml").touch() - output = root / "artifacts" / executable - if existing_output: - output.parent.mkdir(parents=True) - output.write_bytes(b"stale-openvmm") - config = build_config.OpenVmmBuildConfig( - skip_restore=True, - build_directory=root / "build", - directory=openvmm_dir, - output=output, - ) - revision = b"0bc357bbcf3a654b63dfb51f1103c5751bf3d31f\n" - results = [ - common.CommandResult(("git",), 0, revision, b""), - common.CommandResult(("git",), 0, revision, b""), - common.CommandResult(("git",), 0, b"", b""), - ] - - with ( - patch.object(build, "run_checked"), - patch.object(common, "run_capture", side_effect=results), - ): - build.build_openvmm(config, platform=platform) - - self.assertEqual(output.read_bytes(), binary) - provenance = json.loads( - ( - config.build_directory - / OpenVMMBuildConstants.PROVENANCE_NAME - ).read_text(encoding="utf-8") - ) - self.assertEqual( - provenance["executable_sha256"], - hashlib.sha256(binary).hexdigest(), - ) - - def test_native_openvmm_build_restores_and_records_provenance(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - openvmm_dir = root / "openvmm" - output = openvmm_dir / "target" / "release" / "openvmm" - output.parent.mkdir(parents=True) - output.write_bytes(b"openvmm") - (openvmm_dir / "Cargo.toml").touch() - config = build_config.OpenVmmBuildConfig( - build_directory=root / "build", - directory=openvmm_dir, - output=output, - ) - - with ( - patch.object(build, "run_checked") as run_checked, - patch.object(build, "record_openvmm_provenance") as provenance, - ): - build.build_openvmm(config, platform="linux-gnu") - - self.assertEqual( - run_checked.call_args_list, - [ - call( - [ - "cargo", - "xflowey", - "restore-packages", - "--no-compat-igvm", - ], - cwd=openvmm_dir, - ), - call( - [ - "cargo", - "build", - "--release", - "-p", - "openvmm", - "--bin", - "openvmm", - ], - cwd=openvmm_dir, - ), - ], - ) - provenance.assert_called_once_with(config) - - def test_openvmm_build_honors_skip_restore(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - openvmm_dir = root / "openvmm" - output = openvmm_dir / "target" / "release" / "openvmm" - output.parent.mkdir(parents=True) - output.write_bytes(b"openvmm") - (openvmm_dir / "Cargo.toml").touch() - config = build_config.OpenVmmBuildConfig( - skip_restore=True, - build_directory=root / "build", - directory=openvmm_dir, - output=output, - ) - - with ( - patch.object(build, "run_checked") as run_checked, - patch.object(build, "record_openvmm_provenance"), - ): - build.build_openvmm(config, platform="linux-gnu") - - self.assertEqual(run_checked.call_count, 1) - self.assertEqual(run_checked.call_args.args[0][:2], ["cargo", "build"]) - - def test_musl_openvmm_build_sets_sysroot_and_normalizes_output(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - openvmm_dir = root / "openvmm" - (openvmm_dir / "Cargo.toml").parent.mkdir(parents=True) - (openvmm_dir / "Cargo.toml").touch() - sysroot = openvmm_dir / ".packages" / "extracted" / "x86_64-sysroot" - (sysroot / "lib").mkdir(parents=True) - (sysroot / "lib" / "libsymcrypt.a").touch() - output = openvmm_dir / "target" / "release" / "openvmm" - config = build_config.OpenVmmBuildConfig( - build_directory=root / "build", - directory=openvmm_dir, - output=output, - backend="mshv", - ) - target_output = config.openvmm_target_output("linux-musl") - target_output.parent.mkdir(parents=True) - target_output.write_bytes(b"musl-openvmm") - - with ( - patch.object(build.sys, "platform", "linux"), - patch.object(build.os, "access", return_value=False) as access, - patch.object(build, "run_checked") as run_checked, - patch.object(build, "record_openvmm_provenance") as provenance, - patch.object(Path, "chmod") as chmod, - ): - build.build_openvmm(config) - - access.assert_not_called() - self.assertEqual(run_checked.call_count, 3) - self.assertEqual( - run_checked.call_args_list[1], - call(["rustup", "target", "add", "x86_64-unknown-linux-musl"]), - ) - cargo_call = run_checked.call_args_list[2] - self.assertIn("x86_64-unknown-linux-musl", cargo_call.args[0]) - environment = cargo_call.kwargs["env"] - resolved_sysroot = ( - openvmm_dir.resolve() / ".packages" / "extracted" / "x86_64-sysroot" - ) - self.assertEqual( - environment["X86_64_UNKNOWN_LINUX_MUSL_OPENSSL_DIR"], - os.fspath(resolved_sysroot), - ) - self.assertEqual( - environment["X86_64_UNKNOWN_LINUX_MUSL_SYMCRYPT_LIB_PATH"], - os.fspath(resolved_sysroot / "lib"), - ) - self.assertEqual(output.read_bytes(), b"musl-openvmm") - chmod.assert_called_once() - provenance.assert_called_once_with(config) - - def test_guest_build_routes_one_config_to_native_or_docker_consumers(self): - with ( - patch.object(build, "build_kernel") as kernel, - patch.object(build, "build_initramfs") as initramfs, - patch.object(build, "build_docker_artifacts") as docker, - ): - native = build_config.BuildConfig(native_guest=True) - build.build_guest(native) - kernel.assert_called_once_with(native.kernel) - initramfs.assert_called_once_with(native.initramfs_config("alpine")) - docker.assert_not_called() - - with ( - patch.object(build, "build_kernel") as kernel, - patch.object(build, "build_initramfs") as initramfs, - patch.object(build, "build_docker_artifacts") as docker, - ): - portable = build_config.BuildConfig() - build.build_guest(portable) - docker.assert_called_once_with(portable.docker, "alpine") - kernel.assert_not_called() - initramfs.assert_not_called() - - with ( - patch.object(build, "build_kernel") as kernel, - patch.object(build, "build_initramfs") as initramfs, - patch.object(build, "build_distro_layer") as distro, - ): - all_guests = build_config.BuildConfig(guest="all", native_guest=True) - with self.assertRaisesRegex( - common.ScriptError, - "Azure Linux initramfs builds require Docker", - ): - build.build_guest(all_guests) - kernel.assert_not_called() - initramfs.assert_not_called() - distro.assert_not_called() - - def test_combined_build_passes_explicit_backend_to_openvmm_build(self): - config = build_config.BuildConfig( - openvmm=build_config.OpenVmmBuildConfig(backend="mshv") - ) - with ( - patch.object(build.sys, "platform", "linux"), - patch.object(build.os, "access", return_value=False) as access, - patch.object(build, "build_guest") as guest, - patch.object(build, "build_openvmm") as openvmm, - ): - build.build_all(config) - - access.assert_not_called() - guest.assert_called_once_with(config) - openvmm.assert_called_once_with(config.openvmm, platform="linux-musl") - - def test_combined_build_validates_platform_before_building_guest(self): - config = build_config.BuildConfig( - openvmm=build_config.OpenVmmBuildConfig(backend="whp") - ) - with ( - patch.object(build.sys, "platform", "linux"), - patch.object(build, "build_guest") as guest, - patch.object(build, "build_openvmm") as openvmm, - self.assertRaisesRegex( - common.ScriptError, "backend 'whp' is unsupported on linux" - ), - ): - build.build_all(config) - - guest.assert_not_called() - openvmm.assert_not_called() - - def test_release_source_collection_passes_docker_build_config(self): - config = build_config.DockerBuildConfig( - linux_source_destination=Path("custom-linux-source") - ) - with ( - patch.object( - release, - "_guest_release_inputs", - return_value=((), (), (), ()), - ) as guest_release_inputs, - patch.object(release, "collect_alpine_sources"), - patch.object(release, "collect_ubuntu_sources"), - patch.object(release, "build_docker_linux_source") as linux_source, - ): - release.collect_release_sources(config) - - guest_release_inputs.assert_called_once_with(include_azurelinux=False) - linux_source.assert_called_once_with(config) - - def test_records_openvmm_revision_cleanliness_and_executable_hash(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - openvmm_dir = root / "openvmm" - executable = openvmm_dir / "target" / "release" / "openvmm" - executable.parent.mkdir(parents=True) - executable.write_bytes(b"openvmm") - revision = b"0bc357bbcf3a654b63dfb51f1103c5751bf3d31f\n" - results = [ - common.CommandResult(("git",), 0, revision, b""), - common.CommandResult(("git",), 0, revision, b""), - common.CommandResult(("git",), 0, b" M src/main.rs\n", b""), - ] - - with ( - patch.object(BuildConstants, "REPO_ROOT", root), - patch.object(common, "run_capture", side_effect=results), - ): - build.record_openvmm_provenance( - build_config.OpenVmmBuildConfig( - build_directory=root / "build", - directory=openvmm_dir, - output=executable, - ) - ) - - provenance = json.loads( - (root / "build" / OpenVMMBuildConstants.PROVENANCE_NAME).read_text( - encoding="utf-8" - ) - ) - self.assertEqual(provenance["source_revision"], revision.decode().strip()) - self.assertFalse(provenance["source_clean"]) - self.assertEqual( - provenance["executable_sha256"], - hashlib.sha256(b"openvmm").hexdigest(), - ) - - def test_kernel_build_rejects_input_config_mutation_during_build(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - input_config = root / "kernel" / "config-microvm" - input_config.parent.mkdir(parents=True) - input_config.write_text( - "\n".join( - ( - *KernelBuildConstants.REQUIRED_DIRECT_BOOT_CONFIG, - *KernelBuildConstants.REQUIRED_VIRTIO_CONSOLE_CONFIG, - *KernelBuildConstants.REQUIRED_SHARED_STATUS_CONFIG, - *KernelBuildConstants.REQUIRED_SANDBOX_CONFIG, - *KernelBuildConstants.REQUIRED_NETWORK_CONFIG, - *KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG, - *KernelBuildConstants.REQUIRED_HARDENING_CONFIG, - ) - ) - + "\n", - encoding="utf-8", - ) - patch_path = root / "kernel" / "patches" / "example.patch" - patch_path.parent.mkdir() - patch_path.write_text("patch", encoding="utf-8") - source = root / "source" - source.mkdir() - work = root / "work" - output = root / "output" / "vmlinux" - prior_provenance = output.with_name(KernelBuildConstants.PROVENANCE_NAME) - prior_provenance.parent.mkdir() - prior_provenance.write_text("stale", encoding="utf-8") - - with patch.object(BuildConstants, "REPO_ROOT", root): - source_fingerprint = build._kernel_source_fingerprint() - - def run_build(command: object, **_kwargs: object) -> None: - if isinstance(command, list) and command[-1] == "vmlinux": - (work / "vmlinux").write_bytes(b"kernel") - input_config.write_text("CONFIG_CHANGED=y\n", encoding="utf-8") - - with ( - patch.object(BuildConstants, "REPO_ROOT", root), - patch.object(build, "_require_linux"), - patch.object(build, "require_tool", return_value="tool"), - patch.object( - build, - "prepare_kernel_source", - return_value=(source, source_fingerprint), - ), - patch.object(build, "run_checked", side_effect=run_build), - self.assertRaisesRegex( - common.ScriptError, - "inputs changed during the build", - ), - ): - build.build_kernel( - build_config.KernelBuildConfig( - work=work, - output=output, - ) - ) - - self.assertFalse(output.exists()) - self.assertFalse(output.with_name("vmlinux.config").exists()) - self.assertFalse(prior_provenance.exists()) - - def test_materialize_kernel_provenance_inputs_bypasses_mutable_index(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - git_environment = { - key: value - for key, value in os.environ.items() - if not key.startswith("GIT_CONFIG_") - } - config = root / "kernel" / "config-microvm" - patch_path = root / "kernel" / "patches" / "example.patch" - stale_patch = root / "kernel" / "patches" / "stale.patch" - config.parent.mkdir(parents=True) - patch_path.parent.mkdir() - config.write_bytes(b"CONFIG_EXAMPLE=y\n") - patch_path.write_bytes(b"patch\n") - (root / ".gitattributes").write_text( - "\n".join( - ( - "kernel/config-microvm text eol=lf", - "kernel/patches/*.patch text eol=lf", - ) - ) - + "\n", - encoding="ascii", - ) - subprocess.run( - ["git", "init", "-q"], - cwd=root, - env=git_environment, - check=True, - ) - subprocess.run( - ["git", "config", "user.name", "NVX Tests"], - cwd=root, - env=git_environment, - check=True, - ) - subprocess.run( - ["git", "config", "user.email", "nvx-tests@example.com"], - cwd=root, - env=git_environment, - check=True, - ) - subprocess.run( - [ - "git", - "add", - ".gitattributes", - "kernel/config-microvm", - "kernel/patches/example.patch", - ], - cwd=root, - env=git_environment, - check=True, - ) - subprocess.run( - ["git", "commit", "-qm", "kernel provenance fixture"], - cwd=root, - env=git_environment, - check=True, - ) - git_dir = Path( - subprocess.run( - ["git", "rev-parse", "--git-dir"], - cwd=root, - env=git_environment, - check=True, - capture_output=True, - text=True, - ).stdout.strip() - ) - (root / git_dir / "info" / "attributes").write_text( - "\n".join( - ( - "kernel/config-microvm -text", - "kernel/patches/*.patch -text", - ) - ) - + "\n", - encoding="ascii", - ) - config.write_bytes(b"CONFIG_EXAMPLE=y\r\n") - patch_path.write_bytes(b"patch\r\n") - stale_patch.write_bytes(b"stale\r\n") - subprocess.run( - [ - "git", - "add", - "kernel/config-microvm", - "kernel/patches/example.patch", - "kernel/patches/stale.patch", - ], - cwd=root, - env=git_environment, - check=True, - ) - subprocess.run( - [ - "git", - "checkout-index", - "--force", - "--", - "kernel/config-microvm", - "kernel/patches/example.patch", - "kernel/patches/stale.patch", - ], - cwd=root, - env=git_environment, - check=True, - ) - self.assertEqual(config.read_bytes(), b"CONFIG_EXAMPLE=y\r\n") - self.assertEqual(patch_path.read_bytes(), b"patch\r\n") - self.assertEqual(stale_patch.read_bytes(), b"stale\r\n") - - with ( - patch.dict(os.environ, git_environment, clear=True), - patch.object(BuildConstants, "REPO_ROOT", root), - ): - build.materialize_kernel_provenance_inputs() - - self.assertEqual(config.read_bytes(), b"CONFIG_EXAMPLE=y\n") - self.assertEqual(patch_path.read_bytes(), b"patch\n") - self.assertFalse(stale_patch.exists()) - - def test_manifest_tracks_every_kernel_patch(self): - manifest = json.loads( - (BuildConstants.REPO_ROOT / "SOURCE-MANIFEST.json").read_text( - encoding="utf-8" - ) - ) - self.assertEqual( - manifest["linux"]["patches"], - [ - path.relative_to(BuildConstants.REPO_ROOT).as_posix() - for path in build._kernel_patch_files() - ], - ) - self.assertNotIn("guest_agent", manifest) - self.assertEqual( - manifest["openvmm"], - { - "microvm_abi_version": 2, - "control_session_protocol_version": 1, - "control_contract_revision": "nvx-microvm-v2-control-v2", - }, - ) - - def test_guest_descriptors_preserve_alpine_and_select_guest_outputs(self): - alpine = guests.guest_descriptor("alpine") - ubuntu_guest = guests.guest_descriptor("ubuntu") - azurelinux_guest = guests.guest_descriptor("azurelinux") - - self.assertEqual(alpine.initramfs_name, "initramfs.cpio.gz") - self.assertEqual(alpine.default_memory_mib, 128) - self.assertTrue(alpine.sandbox_control) - self.assertEqual( - ubuntu_guest.initramfs_name, - "initramfs-ubuntu.cpio.gz", - ) - self.assertEqual(ubuntu_guest.default_memory_mib, 512) - self.assertFalse(ubuntu_guest.sandbox_control) - self.assertEqual( - azurelinux_guest.initramfs_name, - "initramfs-azurelinux.cpio.gz", - ) - self.assertEqual(azurelinux_guest.default_memory_mib, 512) - self.assertFalse(azurelinux_guest.sandbox_control) - - def test_azurelinux_manifest_and_package_lock_match_build_pins(self): - manifest = json.loads( - (BuildConstants.REPO_ROOT / "SOURCE-MANIFEST.json").read_text( - encoding="utf-8" - ) - )["azurelinux"] - self.assertEqual(manifest["image"], AzureLinuxBuildConstants.IMAGE) - self.assertEqual(manifest["package_lock"], "azurelinux/packages.lock.json") - self.assertEqual( - manifest["package_lock_sha256"], - azurelinux.package_lock_sha256(), - ) - packages = azurelinux.load_package_lock() - self.assertEqual( - {"busybox", "util-linux"} - {package["name"] for package in packages}, - set(), - ) - for package in packages: - self.assertTrue( - package["url"].startswith( - f"{AzureLinuxBuildConstants.REPOSITORY_URL}/Packages/" - ) - ) - attributes = (BuildConstants.REPO_ROOT / ".gitattributes").read_text( - encoding="utf-8" - ) - self.assertIn("azurelinux/packages.lock.json text eol=lf", attributes) - - def test_azurelinux_package_lock_rejects_unpinned_records(self): - document = json.loads( - azurelinux.package_lock_path().read_text(encoding="utf-8") - ) - first = document["packages"][0] - cases: tuple[tuple[str, dict[str, object], str], ...] = ( - ("image", {"image": "mcr.microsoft.com/azurelinux/base/core:3.0"}, "image"), - ( - "url", - { - "packages": [ - {**first, "url": first["url"].replace("/prod/", "/preview/")}, - *document["packages"][1:], - ] - }, - "must be fetched from", - ), - ( - "sha256", - { - "packages": [ - {**first, "sha256": "0" * 63}, - *document["packages"][1:], - ] - }, - "invalid SHA-256", - ), - ( - "order", - {"packages": list(reversed(document["packages"]))}, - "sorted by name", - ), - ) - for name, update, message in cases: - with ( - self.subTest(case=name), - tempfile.TemporaryDirectory() as temporary, - ): - path = Path(temporary) / "packages.lock.json" - path.write_text(json.dumps({**document, **update}), encoding="utf-8") - with self.assertRaisesRegex(common.ScriptError, message): - azurelinux.load_package_lock(path) - - def test_azurelinux_download_packages_verifies_each_locked_rpm(self): - destination = Path("rpms") - with patch.object(azurelinux, "download_verified") as download_verified: - downloaded = azurelinux.download_packages(destination) - - packages = azurelinux.load_package_lock() - self.assertEqual( - download_verified.call_args_list, - [ - call( - package["url"], - destination / package["url"].rsplit("/", maxsplit=1)[-1], - package["sha256"], - ) - for package in packages - ], - ) - self.assertEqual(len(downloaded), len(packages)) - - def test_ubuntu_manifest_and_package_lock_match_build_pins(self): - manifest = json.loads( - (BuildConstants.REPO_ROOT / "SOURCE-MANIFEST.json").read_text( - encoding="utf-8" - ) - ) - ubuntu_manifest = manifest["ubuntu"] - self.assertEqual( - ubuntu_manifest["version"], - UbuntuBuildConstants.VERSION, - ) - self.assertEqual( - ubuntu_manifest["base_url"], - UbuntuBuildConstants.BASE_URL, - ) - self.assertEqual( - ubuntu_manifest["base_sha256"], - UbuntuBuildConstants.BASE_SHA256, - ) - self.assertEqual( - ubuntu_manifest["archive_keyring_url"], - UbuntuBuildConstants.ARCHIVE_KEYRING_URL, - ) - self.assertEqual( - ubuntu_manifest["archive_keyring_sha256"], - UbuntuBuildConstants.ARCHIVE_KEYRING_SHA256, - ) - self.assertEqual( - ubuntu_manifest["package_lock_sha256"], - ubuntu.package_lock_sha256(), - ) - self.assertEqual( - ubuntu_manifest["package_lock_sha256"], - "fcdd30223b96fc26e24fcf4b6763a4a2504e9cd25882e68e89fbf6bf45277ef0", - ) - packages = ubuntu.load_package_lock() - self.assertEqual( - [package["name"] for package in packages], - [ - "busybox-static", - "iputils-ping", - "libcap2", - "libidn2-0", - "libunistring5", - "net-tools", - "netcat-openbsd", - ], - ) - - def test_ubuntu_package_lock_read_errors_are_actionable(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "packages.json" - path.write_text("{", encoding="utf-8") - - with self.assertRaisesRegex( - common.ScriptError, "failed to read Ubuntu package lock" - ): - ubuntu.load_package_lock(path) - - def test_ubuntu_safe_extractor_rejects_archive_symlink_escape(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - archive_path = root / "unsafe.tar.gz" - with tarfile.open(archive_path, "w:gz") as archive_file: - symlink = tarfile.TarInfo("etc") - symlink.type = tarfile.SYMTYPE - symlink.linkname = "../../outside" - archive_file.addfile(symlink) - payload = b"nameserver 192.0.2.1\n" - resolver = tarfile.TarInfo("etc/resolv.conf") - resolver.size = len(payload) - archive_file.addfile(resolver, io.BytesIO(payload)) - - with self.assertRaisesRegex(common.ScriptError, "escapes"): - ubuntu.safe_extract_tar( - archive_path, - root / "extracted", - label="test archive", - ) - self.assertFalse((root / "outside").exists()) - - def test_ubuntu_safe_extractor_roots_absolute_symlinks(self): - self.assertEqual( - ubuntu._virtual_symlink_target( - PurePosixPath("var/run"), - "/run", - "test archive", - ), - "../run", - ) - self.assertEqual( - ubuntu._virtual_symlink_target( - PurePosixPath("etc/alternatives/awk"), - "/usr/bin/mawk", - "test archive", - ), - "../../usr/bin/mawk", - ) - - def test_ubuntu_safe_extractor_rejects_absolute_and_parent_paths(self): - for member_name in ("/absolute", "../parent"): - with self.subTest(member_name=member_name): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - archive_path = root / "unsafe.tar" - with tarfile.open(archive_path, "w") as archive_file: - payload = b"x" - member = tarfile.TarInfo(member_name) - member.size = len(payload) - archive_file.addfile(member, io.BytesIO(payload)) - with self.assertRaisesRegex( - common.ScriptError, - "absolute path|path traversal", - ): - ubuntu.safe_extract_tar( - archive_path, - root / "extracted", - label="test archive", - ) - - def test_ubuntu_rootfs_digest_and_erofs_uuid_are_deterministic(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - first = root / "first" - second = root / "second" - for directory in (first, second): - (directory / "etc").mkdir(parents=True) - (directory / "etc" / "os-release").write_text( - "ID=ubuntu\n", - encoding="ascii", - ) - os.utime(second / "etc" / "os-release", (1234, 1234)) - - first_digest = ubuntu.rootfs_sha256(first) - second_digest = ubuntu.rootfs_sha256(second) - self.assertEqual(first_digest, second_digest) - self.assertEqual( - ubuntu.erofs_uuid(first_digest), - ubuntu.erofs_uuid(second_digest), - ) - - def test_ubuntu_prepare_root_normalizes_root_directory_mode(self): - with tempfile.TemporaryDirectory() as temporary: - temporary_root = Path(temporary) - work = temporary_root / "work" - expected_root = work / "root" - - def extract( - archive: Path, - destination: Path, - *, - label: str, - ) -> None: - self.assertEqual( - archive.name, - f"ubuntu-base-{UbuntuBuildConstants.VERSION}-base-amd64.tar.gz", - ) - self.assertEqual(label, "Ubuntu Base archive") - status = destination / "var" / "lib" / "dpkg" / "status" - status.parent.mkdir(parents=True) - status.write_text("", encoding="utf-8") - - with ( - patch.object( - ubuntu, - "cache_root", - return_value=temporary_root / "cache", - ), - patch.object(ubuntu, "download_verified"), - patch.object(ubuntu, "safe_extract_tar", side_effect=extract), - patch.object(ubuntu, "_validate_ubuntu_identity"), - patch.object(ubuntu, "load_package_lock", return_value=()), - patch.object(ubuntu, "_validate_package_closure"), - patch.object(ubuntu, "_customize_root"), - patch.object(ubuntu.Path, "chmod", autospec=True) as chmod, - ): - self.assertEqual(ubuntu.prepare_root(work), expected_root) - - chmod.assert_called_once_with(expected_root, 0o755) - - def test_ubuntu_customization_installs_busybox_wget(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - for relative in ("etc", "usr/bin", "usr/sbin"): - (root / relative).mkdir(parents=True) - with ( - patch.object(ubuntu, "_clear_directory"), - patch.object(ubuntu, "_validate_accounts"), - patch.object(ubuntu, "_validate_usr_merge"), - patch.object(ubuntu, "_ensure_symlink") as ensure_symlink, - ): - ubuntu._customize_root(root) - - ensure_symlink.assert_any_call(root, "usr/bin/wget", "busybox") - - def test_distro_layer_refuses_existing_output_without_replace(self): - with tempfile.TemporaryDirectory() as temporary: - output = Path(temporary) / "ubuntu.erofs" - output.touch() - with ( - patch.object(build.sys, "platform", "linux"), - self.assertRaisesRegex(common.ScriptError, "refusing to replace"), - ): - build.build_distro_layer( - build_config.DistroLayerBuildConfig(output=output) - ) - - def test_ubuntu_initramfs_manifest_binds_artifact_digest(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - output = root / "initramfs-ubuntu.cpio.gz" - output.write_bytes(b"ubuntu initramfs") - with patch.object( - ubuntu, - "package_manifest", - return_value={"format": 1, "guest": "ubuntu"}, - ): - build._write_ubuntu_manifest(root, output, {}, "a" * 64) - - manifest = json.loads( - output.with_name(f"{output.name}.packages.json").read_text( - encoding="utf-8" - ) - ) - self.assertEqual(manifest["artifact"], output.name) - self.assertEqual( - manifest["artifact_sha256"], - common.sha256_file(output), - ) - self.assertEqual(manifest["input_sha256"], "a" * 64) - - def test_ubuntu_source_requirements_deduplicate_binary_manifests(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - manifests: list[Path] = [] - for name in ("initramfs.json", "layer.json"): - path = root / name - path.write_text( - json.dumps( - { - "guest": "ubuntu", - "release": UbuntuBuildConstants.VERSION, - "architecture": UbuntuBuildConstants.ARCHITECTURE, - "packages": [ - { - "source_name": "glibc", - "source_version": "2.43-2ubuntu2.3", - } - ], - } - ), - encoding="utf-8", - ) - manifests.append(path) - - requirements = collect_ubuntu_sources._source_requirements(manifests) - - self.assertEqual( - requirements, - ( - { - "source_name": "glibc", - "source_version": "2.43-2ubuntu2.3", - }, - ), - ) - - def test_ubuntu_dsc_validation_matches_source_index(self): - payload = b"source" - digest = hashlib.sha256(payload).hexdigest() - with tempfile.TemporaryDirectory() as temporary: - dsc = Path(temporary) / "example_1.0.dsc" - dsc.write_text( - ( - "Format: 3.0 (quilt)\n" - "Source: example\n" - "Version: 1.0\n" - "Checksums-Sha256:\n" - f" {digest} {len(payload)} example_1.0.orig.tar.xz\n" - ), - encoding="utf-8", - ) - record: collect_ubuntu_sources.SourceRecord = { - "source_name": "example", - "source_version": "1.0", - "directory": "pool/main/e/example", - "index_url": "https://archive.invalid/Sources.xz", - "index_sha256": "0" * 64, - "index_release_url": "https://archive.invalid/InRelease", - "index_release_sha256": "1" * 64, - "files": [ - { - "name": "example_1.0.dsc", - "size": dsc.stat().st_size, - "sha256": common.sha256_file(dsc), - "url": "https://archive.invalid/example_1.0.dsc", - }, - { - "name": "example_1.0.orig.tar.xz", - "size": len(payload), - "sha256": digest, - "url": "https://archive.invalid/example_1.0.orig.tar.xz", - }, - ], - } - - collect_ubuntu_sources._validate_dsc(dsc, record) - record["files"][1]["sha256"] = "f" * 64 - with self.assertRaisesRegex( - common.ScriptError, - "do not match", - ): - collect_ubuntu_sources._validate_dsc(dsc, record) - - def test_ubuntu_source_index_is_authenticated_by_signed_release(self): - dsc_payload = b"dsc" - source_payload = b"source" - sources = ( - "Package: example\n" - "Version: 1.0\n" - "Directory: pool/main/e/example\n" - "Checksums-Sha256:\n" - f" {hashlib.sha256(dsc_payload).hexdigest()} " - f"{len(dsc_payload)} example_1.0.dsc\n" - f" {hashlib.sha256(source_payload).hexdigest()} " - f"{len(source_payload)} example_1.0.orig.tar.xz\n" - ).encode() - compressed = lzma.compress(sources) - index_sha256 = hashlib.sha256(compressed).hexdigest() - inrelease = ( - "-----BEGIN PGP SIGNED MESSAGE-----\n" - "Hash: SHA256\n" - "\n" - "Origin: Ubuntu\n" - f"Codename: {UbuntuBuildConstants.CODENAME}\n" - f"Suite: {UbuntuBuildConstants.CODENAME}\n" - "SHA256:\n" - f" {index_sha256} {len(compressed)} main/source/Sources.xz\n" - "-----BEGIN PGP SIGNATURE-----\n" - "test\n" - "-----END PGP SIGNATURE-----\n" - ) - - def download_release(_url: str, destination: Path) -> None: - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_text(inrelease, encoding="utf-8") - - def download_index( - _url: str, - destination: Path, - expected_sha256: str, - ) -> None: - self.assertEqual(expected_sha256, index_sha256) - destination.write_bytes(compressed) - - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - keyring = root / "ubuntu-archive-keyring.gpg" - keyring.write_bytes(b"keyring") - with ( - patch.object( - collect_ubuntu_sources, - "download", - side_effect=download_release, - ), - patch.object( - collect_ubuntu_sources, - "download_verified", - side_effect=download_index, - ), - patch.object( - collect_ubuntu_sources, - "require_tool", - return_value="gpgv", - ), - patch.object(collect_ubuntu_sources, "run_checked") as run_checked, - ): - records, metadata = ( - collect_ubuntu_sources._load_authenticated_source_index( - root / "cache", - keyring, - "https://archive.invalid/ubuntu", - UbuntuBuildConstants.CODENAME, - "main", - ) - ) - - run_checked.assert_called_once() - self.assertEqual(run_checked.call_args.args[0][0], "gpgv") - self.assertIn("--homedir", run_checked.call_args.args[0]) - record = records[("example", "1.0")] - self.assertEqual( - record["index_release_url"], - "https://archive.invalid/ubuntu/dists/resolute/InRelease", - ) - self.assertEqual( - [item["role"] for item in metadata], - ["ubuntu-inrelease", "ubuntu-source-index"], - ) - self.assertEqual( - metadata[1]["authenticated_by"], - record["index_release_url"], - ) - - def test_launchpad_source_record_retains_verifiable_raw_metadata(self): - source_payload = b"source archive" - source_sha256 = hashlib.sha256(source_payload).hexdigest() - self_link = f"{UbuntuBuildConstants.LAUNCHPAD_ARCHIVE_API}/+sourcepub/123" - query = { - "entries": [ - { - "source_package_name": "example", - "source_package_version": "1.0", - "status": "Superseded", - "date_published": "2026-01-01T00:00:00Z", - "date_superseded": "2026-01-03T00:00:00Z", - "pocket": "Updates", - "component_name": "main", - "self_link": self_link, - } - ] - } - source_urls = [ - "https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/" - "example/1.0/example_1.0.dsc", - "https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/" - "example/1.0/example_1.0.orig.tar.xz", - ] - - def download_json(url: str, destination: Path) -> object: - document: object = source_urls if "sourceFileUrls" in url else query - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_text( - json.dumps(document, separators=(",", ":")) + "\n", - encoding="utf-8", - ) - return document - - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - snapshot_release = root / "cache" / "snapshot.InRelease" - snapshot_release.parent.mkdir(parents=True) - snapshot_release.write_text("signed release", encoding="utf-8") - snapshot_record: collect_ubuntu_sources.SourceRecord = { - "source_name": "example", - "source_version": "1.0", - "directory": "pool/main/e/example", - "index_url": ( - "https://snapshot.ubuntu.com/ubuntu/20260102T000000Z/" - "dists/resolute-updates/main/source/Sources.xz" - ), - "index_sha256": "2" * 64, - "index_release_url": ( - "https://snapshot.ubuntu.com/ubuntu/20260102T000000Z/" - "dists/resolute-updates/InRelease" - ), - "index_release_sha256": common.sha256_file(snapshot_release), - "files": [ - { - "name": "example_1.0.dsc", - "size": 3, - "sha256": "4" * 64, - "url": ( - "https://snapshot.ubuntu.com/ubuntu/" - "20260102T000000Z/pool/main/e/example/" - "example_1.0.dsc" - ), - }, - { - "name": "example_1.0.orig.tar.xz", - "size": len(source_payload), - "sha256": source_sha256, - "url": ( - "https://snapshot.ubuntu.com/ubuntu/" - "20260102T000000Z/pool/main/e/example/" - "example_1.0.orig.tar.xz" - ), - }, - ], - } - snapshot_metadata: list[collect_ubuntu_sources.SourceMetadata] = [ - { - "role": "ubuntu-inrelease", - "url": snapshot_record["index_release_url"], - "sha256": snapshot_record["index_release_sha256"], - "cache_path": snapshot_release, - "output_name": "snapshot.InRelease", - "authenticated_by": (UbuntuBuildConstants.ARCHIVE_KEYRING_URL), - }, - { - "role": "ubuntu-source-index", - "url": snapshot_record["index_url"], - "sha256": snapshot_record["index_sha256"], - "cache_path": None, - "output_name": None, - "authenticated_by": snapshot_record["index_release_url"], - }, - ] - with ( - patch.object( - collect_ubuntu_sources, - "_download_json", - side_effect=download_json, - ), - patch.object( - collect_ubuntu_sources, - "_load_authenticated_source_index", - return_value=( - {("example", "1.0"): snapshot_record}, - snapshot_metadata, - ), - ), - ): - record, metadata = collect_ubuntu_sources._launchpad_source_record( - root / "cache", - root / "ubuntu-archive-keyring.gpg", - "example", - "1.0", - ) - - self.assertEqual(record, snapshot_record) - self.assertEqual( - [item["role"] for item in metadata], - [ - "ubuntu-inrelease", - "ubuntu-source-index", - "launchpad-publishing-history", - "launchpad-source-file-urls", - ], - ) - for item in metadata: - if item["cache_path"] is None: - continue - self.assertEqual( - item["sha256"], - common.sha256_file(item["cache_path"]), - ) - - output = root / "output" - output.mkdir() - manifest_metadata = collect_ubuntu_sources._materialize_source_metadata( - output, - metadata, - ) - for item, manifest_item in zip(metadata, manifest_metadata, strict=True): - if item["cache_path"] is None: - self.assertNotIn("path", manifest_item) - continue - retained = output / manifest_item["path"] - self.assertEqual( - retained.read_bytes(), - item["cache_path"].read_bytes(), - ) - self.assertEqual( - common.sha256_file(retained), - manifest_item["sha256"], - ) - - def test_ubuntu_source_collection_rejects_unexpected_output_entries(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - output = root / "output" - output.mkdir() - stale = output / "unrelated.txt" - stale.write_text("do not archive", encoding="utf-8") - - with self.assertRaisesRegex( - common.ScriptError, - "unexpected entries: unrelated.txt", - ): - collect_ubuntu_sources.collect_ubuntu_sources( - [], - output, - root / "cache", - ) - - self.assertEqual(stale.read_text(encoding="utf-8"), "do not archive") - - def test_ubuntu_source_collection_validates_output_before_resolving(self): - output = MagicMock(spec=Path) - with patch.object( - collect_ubuntu_sources, - "_validate_source_output", - ) as validate: - - def resolve() -> Path: - validate.assert_called_once_with(output) - raise RuntimeError("stop after ordering assertion") - - output.resolve.side_effect = resolve - with self.assertRaisesRegex(RuntimeError, "ordering assertion"): - collect_ubuntu_sources.collect_ubuntu_sources( - [], - output, - Path("cache"), - ) - - def test_ci_kernel_cache_key_includes_patches(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-guest-artifacts" - / "action.yml" - ).read_text(encoding="utf-8") - for cache_input in ( - "SOURCE-MANIFEST.json", - "kernel/config-microvm", - "kernel/patches/**", - "scripts/nvx_tools/build.py", - "scripts/nvx_tools/common.py", - ): - self.assertIn(cache_input, action) - self.assertIn("linux-kernel-v1-", action) - self.assertEqual(action.count("build/vmlinux.provenance.json"), 2) - self.assertEqual(action.count("build/initramfs.provenance.json"), 2) - - def test_kernel_patch_identity_follows_patch_paths_and_order(self): - # A content-only hash ignores names, so a rename reused a kernel whose - # provenance named the old patch. The identity must see paths and order. - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - (root / "0001-a.patch").write_bytes(b"same bytes\n") - (root / "0002-b.patch").write_bytes(b"other bytes\n") - original = ci.kernel_patch_identity(root) - self.assertEqual(original, ci.kernel_patch_identity(root)) - - (root / "0001-a.patch").rename(root / "0001-renamed.patch") - self.assertNotEqual(original, ci.kernel_patch_identity(root)) - - renamed = ci.kernel_patch_identity(root) - (root / "0001-renamed.patch").rename(root / "0003-renamed.patch") - self.assertNotEqual(renamed, ci.kernel_patch_identity(root)) - - def test_kernel_patch_identity_rejects_missing_or_empty_patch_directories(self): - # An empty digest would stop following patch renames without an error. - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - with self.assertRaisesRegex(common.ScriptError, "no kernel patches"): - ci.kernel_patch_identity(root / "missing") - with self.assertRaisesRegex(common.ScriptError, "no kernel patches"): - ci.kernel_patch_identity(root) - - def test_kernel_patch_identity_ignores_files_the_kernel_build_does_not_read(self): - # Provenance hashes only top-level *.patch files, so the key must match. - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - (root / "0001-a.patch").write_bytes(b"patch\n") - original = ci.kernel_patch_identity(root) - (root / "README.md").write_bytes(b"notes\n") - (root / "nested").mkdir() - (root / "nested" / "0002-b.patch").write_bytes(b"nested\n") - self.assertEqual(original, ci.kernel_patch_identity(root)) - - def test_kernel_patch_identity_changes_when_provenance_would_reject_the_kernel( - self, - ): - # Renaming a patch changes the provenance source, so the identity must - # change with it and the stale provenance must fail validation. - with tempfile.TemporaryDirectory() as temporary: - tree = Path(temporary) - patches = tree / KernelBuildConstants.PATCH_DIRECTORY - patches.mkdir(parents=True) - (tree / KernelBuildConstants.INPUT_CONFIG).parent.mkdir( - parents=True, exist_ok=True - ) - (tree / KernelBuildConstants.INPUT_CONFIG).write_bytes(b"CONFIG_X=y\n") - (patches / "0001-a.patch").write_bytes(b"same bytes\n") - (patches / "0002-b.patch").write_bytes(b"other bytes\n") - kernel = tree / "vmlinux" - kernel.write_bytes(b"kernel\n") - kernel_config = tree / "kernel.config" - kernel_config.write_bytes(b"CONFIG_X=y\n") - provenance_path = tree / "provenance.json" - - with ( - patch.object(BuildConstants, "REPO_ROOT", tree), - patch.object(release, "assert_required_kernel_config"), - ): - stale_identity = ci.kernel_patch_identity(patches) - provenance_path.write_text( - json.dumps( - { - "format": KernelBuildConstants.PROVENANCE_FORMAT, - **build.kernel_provenance_inputs(), - "kernel_sha256": release.sha256_file(kernel), - "config_sha256": release.sha256_file(kernel_config), - } - ), - encoding="utf-8", - ) - release._validate_kernel_provenance( - kernel, kernel_config, provenance_path - ) - - (patches / "0001-a.patch").rename(patches / "0003-a.patch") - - self.assertNotEqual(stale_identity, ci.kernel_patch_identity(patches)) - with self.assertRaisesRegex(common.ScriptError, "does not match"): - release._validate_kernel_provenance( - kernel, kernel_config, provenance_path - ) - - def test_ci_guest_kernel_keys_include_the_patch_identity(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-guest-artifacts" - / "action.yml" - ).read_text(encoding="utf-8") - self.assertIn("kernel-patch-identity", action) - self.assertIn( - "linux-kernel-v1-${KERNEL_VERSION}-${KERNEL_INPUT_HASH}-${patch_identity}", - action, - ) - self.assertIn( - "linux-kernel-debug-v1-${KERNEL_VERSION}-${DEBUG_KERNEL_INPUT_HASH}-${patch_identity}", - action, - ) - - def test_ci_guest_cache_keys_include_build_configuration(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-guest-artifacts" - / "action.yml" - ).read_text(encoding="utf-8") - cache_names = ( - "KERNEL_INPUT_HASH", - "DEBUG_KERNEL_INPUT_HASH", - "ALPINE_INPUT_HASH", - "UBUNTU_INPUT_HASH", - "AZURELINUX_INPUT_HASH", - ) - # Every artifact, both kernels included, builds inside the image that - # docker/Dockerfile pins, so a toolchain change must miss every cache. - self.assertEqual( - action.count("--file docker/Dockerfile"), - action.count("docker build"), - ) - for cache_name in cache_names: - with self.subTest(cache=cache_name): - cache_input = next( - line - for line in action.splitlines() - if line.strip().startswith(f"{cache_name}:") - ) - self.assertIn("'docker/Dockerfile'", cache_input) - self.assertIn("'scripts/nvx_tools/build_config.py'", cache_input) - self.assertIn("'scripts/nvx_tools/build_constants.py'", cache_input) - - def test_ci_azurelinux_cache_key_covers_build_input_digest(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-guest-artifacts" - / "action.yml" - ).read_text(encoding="utf-8") - cache_input = next( - line - for line in action.splitlines() - if line.strip().startswith("AZURELINUX_INPUT_HASH:") - ) - patterns = cache_input.split("'")[1::2] - self.assertIn("scripts/nvx_tools/azurelinux.py", patterns) - for path in azurelinux.input_files(): - relative = path.relative_to(BuildConstants.REPO_ROOT).as_posix() - with self.subTest(path=relative): - self.assertTrue( - any( - relative == pattern - or ( - pattern.endswith("/**") - and relative.startswith(pattern[:-2]) - ) - for pattern in patterns - ) - ) - - def test_kernel_input_config_uses_canonical_lf_line_endings(self): - attributes = (BuildConstants.REPO_ROOT / ".gitattributes").read_text( - encoding="utf-8" - ) - for path in ( - KernelBuildConstants.INPUT_CONFIG, - KernelBuildConstants.DEBUG_CONFIG_FRAGMENT, - ): - with self.subTest(path=path): - self.assertIn( - f"{path.as_posix()} text eol=lf", - attributes.splitlines(), - ) - - def test_ci_debug_kernel_is_opt_in_and_keyed_by_its_fragment(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-guest-artifacts" - / "action.yml" - ).read_text(encoding="utf-8") - cache_input = next( - line - for line in action.splitlines() - if line.strip().startswith("DEBUG_KERNEL_INPUT_HASH:") - ) - for path in ( - "kernel/config-microvm", - "kernel/config-microvm-debug", - "kernel/patches/**", - "scripts/nvx_tools/build.py", - "scripts/nvx_tools/build_constants.py", - ): - with self.subTest(path=path): - self.assertIn(f"'{path}'", cache_input) - kernel_input = next( - line - for line in action.splitlines() - if line.strip().startswith("KERNEL_INPUT_HASH:") - ) - self.assertNotIn("config-microvm-debug", kernel_input) - build_step = _composite_action_step(action, "Build Linux debug kernel (Docker)") - self.assertIn("inputs.debug-kernel == 'true'", build_step) - self.assertIn( - f"--target {DockerBuildConstants.DEBUG_KERNEL_TARGET}", build_step - ) - self.assertIn('default: "false"', action) - for name in DockerBuildConstants.DEBUG_KERNEL_ARTIFACT_NAMES: - with self.subTest(artifact=name): - self.assertEqual( - [line.strip() for line in action.splitlines()].count( - f"build/{name}" - ), - 2, - ) - - def test_ci_guest_images_input_leaves_only_the_debug_kernel(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-guest-artifacts" - / "action.yml" - ).read_text(encoding="utf-8") - guard = "inputs.guest-images == 'true'" - self.assertIn(" guest-images:\n", action) - self.assertIn('default: "true"', action) - steps = [ - line.strip().removeprefix("- name: ") - for line in action.splitlines() - if line.startswith(" - name: ") - ] - debug_steps = {name for name in steps if "debug kernel" in name} - self.assertEqual(len(debug_steps), 3) - always = {"Resolve guest artifact versions", "Resolve guest cache keys"} - for name in steps: - with self.subTest(step=name): - step = _composite_action_step(action, name) - if name in debug_steps or name in always: - self.assertNotIn("guest-images", step) - else: - self.assertIn(guard, step) - - def test_ci_guest_cache_keys_include_shared_build_modules(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "build-guest-artifacts" - / "action.yml" - ).read_text(encoding="utf-8") - for variable in ("ALPINE_INPUT_HASH", "UBUNTU_INPUT_HASH"): - assignment = next(line for line in action.splitlines() if variable in line) - self.assertIn("'scripts/nvx_tools/common.py'", assignment) - self.assertIn("'scripts/nvx_tools/guests.py'", assignment) - - def test_quality_checks_include_shared_identity_probe(self): - action = ( - BuildConstants.REPO_ROOT - / ".github" - / "actions" - / "check-quality" - / "action.yml" - ).read_text(encoding="utf-8") - - self.assertEqual(action.count("guest/common/nvx-identity-probe"), 2) - - def test_docker_guest_builder_pins_erofs_toolchain(self): - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" - ) - self.assertIn( - "ARG DEBIAN_IMAGE=debian:12-slim@sha256:" - "3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251", - dockerfile, - ) - self.assertIn("ARG EROFS_UTILS_VERSION=1.5-1", dockerfile) - self.assertIn("erofs-utils=${EROFS_UTILS_VERSION}", dockerfile) - - def test_azurelinux_initramfs_uses_locked_busybox_applets(self): - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" - ) - base_stage = dockerfile.split("FROM base AS kernel", 1)[0] - self.assertNotIn("busybox", base_stage) - azure_stage = dockerfile.split("FROM base AS azurelinux-initramfs", 1)[1] - self.assertIn("ln -sf ../sbin/busybox /rootfs/usr/bin/busybox", azure_stage) - applets = ( - azure_stage.split("for utility in ", 1)[1] - .split("; do", 1)[0] - .replace("\\\n", " ") - .split() - ) - for applet in ("sh", "arp", "nc", "wget", "mdev", "ifconfig", "route"): - self.assertIn(applet, applets) - # util-linux provides these; the BusyBox applets lack required options. - self.assertNotIn("setpriv", applets) - self.assertNotIn("unshare", applets) - launcher = ( - BuildConstants.REPO_ROOT - / "guest" - / "common" - / "nvx-container-launch-azurelinux" - ).read_text(encoding="utf-8") - self.assertIn("exec unshare --mount --pid --uts --fork --kill-child", launcher) - self.assertIn( - "busybox", [package["name"] for package in azurelinux.load_package_lock()] - ) - - def test_azurelinux_initramfs_gives_nobody_a_home(self): - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" - ) - azure_stage = dockerfile.split("FROM base AS azurelinux-initramfs", 1)[1] - self.assertIn( - "sed -i 's#^\\(nobody:[^:]*:65534:65534:[^:]*:\\)/dev/null:" - "#\\1/nonexistent:#'", - azure_stage, - ) - self.assertIn( - "grep -q '^nobody:[^:]*:65534:65534:[^:]*:/nonexistent:' " - "/rootfs/etc/passwd", - azure_stage, - ) - self.assertIn("install -d -m 0755 /rootfs/nonexistent", azure_stage) - - def test_azurelinux_rootfs_installs_only_locked_rpms(self): - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" - ) - self.assertNotIn("tdnf install", dockerfile) - packages_stage = dockerfile.split("FROM base AS azurelinux-packages", 1)[1] - self.assertIn( - "from nvx_tools.azurelinux import download_packages", - packages_stage.split("\nFROM ", 1)[0], - ) - rootfs_stage = dockerfile.split( - "FROM ${AZURELINUX_IMAGE} AS azurelinux-rootfs", 1 - )[1].split("\nFROM ", 1)[0] - self.assertIn( - "COPY --from=azurelinux-packages /out/rpms/ /tmp/azurelinux-rpms/", - rootfs_stage, - ) - checksig = rootfs_stage.index("rpm --checksig /tmp/azurelinux-rpms/*.rpm") - install = rootfs_stage.index("rpm --upgrade --verbose --hash") - self.assertLess(checksig, install) - - def test_azurelinux_manifest_uses_package_metadata(self): - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" - ) - self.assertIn("rpm -qa --queryformat", dockerfile) - self.assertIn("if name == 'gpg-pubkey':", dockerfile) - self.assertNotIn("'type': 'deb'", dockerfile) - self.assertIn("'packages': json.loads", dockerfile) - - def test_azurelinux_initramfs_packs_normalized_rootfs(self): - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" - ) - azure_stage = dockerfile.split("FROM base AS azurelinux-initramfs", 1)[1] - cleanup = azure_stage.index( - "rm -rf /rootfs/usr/lib/sysimage/tdnf /rootfs/var/cache/tdnf \\\n" - " /rootfs/var/cache/ldconfig /rootfs/var/lib/rpm" - ) - normalize = azure_stage.index( - "find . -exec touch --no-dereference " - f"--date=@{InitramfsBuildConstants.TIMESTAMP} {{}} +" - ) - pack = azure_stage.index("find . -print0 | LC_ALL=C sort -z | cpio") - self.assertLess(cleanup, normalize) - self.assertLess(normalize, pack) - - def test_azurelinux_manifest_records_shared_input_digest(self): - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" - ) - azure_stage = dockerfile.split("FROM base AS azurelinux-initramfs", 1)[1] - self.assertIn("from nvx_tools.azurelinux import input_sha256", azure_stage) - self.assertIn( - "'input_sha256': input_sha256(\n" - " image=os.environ['AZURELINUX_IMAGE'],\n" - " version=os.environ['AZURELINUX_VERSION'],\n" - " ),", - azure_stage, - ) - - def test_azurelinux_input_digest_tracks_guest_sources_and_dockerfile(self): - checkout_digest = azurelinux.input_sha256() - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - for source in azurelinux.input_files(): - destination = root / source.relative_to(BuildConstants.REPO_ROOT) - destination.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(source, destination) - with patch.object(BuildConstants, "REPO_ROOT", root): - baseline = azurelinux.input_sha256() - self.assertEqual(baseline, checkout_digest) - self.assertNotEqual( - azurelinux.input_sha256(image="example.invalid/other@sha256:0"), - baseline, - ) - self.assertNotEqual(azurelinux.input_sha256(version="3.1"), baseline) - for relative in ( - "guest/common/init", - "docker/Dockerfile", - "azurelinux/packages.lock.json", - "scripts/nvx_tools/azurelinux.py", - "scripts/nvx_tools/build_constants.py", - "scripts/nvx_tools/common.py", - ): - with self.subTest(path=relative): - path = root / relative - original = path.read_bytes() - path.write_bytes(original + b"\n# changed\n") - self.assertNotEqual(azurelinux.input_sha256(), baseline) - path.write_bytes(original) - self.assertEqual(azurelinux.input_sha256(), baseline) - unrelated = root / "guest" / "ubuntu" / "nvx-bashrc" - unrelated.parent.mkdir(parents=True, exist_ok=True) - unrelated.write_text("changed\n", encoding="utf-8") - self.assertEqual(azurelinux.input_sha256(), baseline) - - def test_guest_init_runs_virtfs_helper_from_installed_path(self): - installed: list[Path] = [] - - def install(_source: Path, destination: Path) -> dict[str, str]: - installed.append(destination) - return {} - - root = Path("rootfs") - with ( - patch.object(build, "_install", side_effect=install), - patch.object(build, "_build_static_helper", return_value={}), - patch.object(build, "_build_device_io_helper", return_value={}), - ): - build._install_guest_files( - build_config.InitramfsBuildConfig(work=Path("work")), - root, - guests.ALPINE_GUEST, - ) - self.assertIn(root / "sbin" / "nvx-hostmount", installed) - - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" - ) - azure_stage = dockerfile.split("FROM base AS azurelinux-initramfs", 1)[1] - azure_sbin_scripts = azure_stage.split( - "install -m 0755 /repo/guest/common/init /rootfs/init\n", 1 - )[1].split("/rootfs/sbin/", 1)[0] - self.assertIn("/repo/guest/common/nvx-hostmount", azure_sbin_scripts) - init_script = ( - BuildConstants.REPO_ROOT / "guest" / "common" / "init" - ).read_text(encoding="utf-8") - self.assertIn( - '/sbin/nvx-hostmount || fatal "virtfs: failed to mount live host directory"', - init_script, - ) - self.assertNotIn("/usr/sbin/nvx-hostmount", init_script) - - def test_openvmm_ci_downloads_guest_artifacts(self): - workflow = ( - BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" - ).read_text(encoding="utf-8") - openvmm_tests = _workflow_job(workflow, "openvmm-vmm-tests") - self.assertIn("needs: [artifacts, openvmm-changes]", openvmm_tests) - self.assertIn("needs.artifacts.result == 'success'", openvmm_tests) - self.assertIn("- name: Download guest artifacts", openvmm_tests) - self.assertIn("name: guest-artifacts", openvmm_tests) - self.assertIn("path: build", openvmm_tests) - self.assertIn("openvmm-vmm-tests", ci.REQUIRED_CI_OPENVMM_ARTIFACT_TEST_JOBS) - - def test_apk_add_uses_host_ca_bundle_without_overriding_configuration(self): - root = Path("root") - with ( - patch.object(build.ssl, "get_default_verify_paths") as verify_paths, - patch.object(build, "run_checked") as run, - ): - verify_paths.return_value.cafile = "/etc/host-ca.pem" - with patch.dict(os.environ, {}, clear=True): - build._apk_add(root, "example") - - environment = run.call_args.kwargs["env"] - self.assertEqual(environment["SSL_CERT_FILE"], "/etc/host-ca.pem") - self.assertEqual( - environment["LD_LIBRARY_PATH"], - f"{root / 'lib'}:{root / 'usr' / 'lib'}", - ) - - with patch.dict( - os.environ, - {"SSL_CERT_FILE": "/etc/configured-ca.pem"}, - clear=True, - ): - build._apk_add(root, "example") - - environment = run.call_args.kwargs["env"] - self.assertEqual(environment["SSL_CERT_FILE"], "/etc/configured-ca.pem") - - def test_device_io_helper_is_static_with_nonexecutable_stack(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - work = root / "work" - destination = root / "root" / "sbin" / "nvx-device-io" - work.mkdir() - destination.parent.mkdir(parents=True) - - def compile_helper( - command: list[str | os.PathLike[str]], **_kwargs: object - ) -> None: - output = Path(command[command.index("-o") + 1]) - output.write_bytes(b"static-elf") - - with ( - patch.object(build, "require_tool", return_value="cc"), - patch.object(build, "run_checked", side_effect=compile_helper) as run, - ): - provenance = build._build_device_io_helper(work, destination) - - command = run.call_args.args[0] - self.assertIn("-nostdlib", command) - self.assertIn("-static", command) - self.assertIn("-Wl,-z,noexecstack", command) - self.assertEqual(destination.read_bytes(), b"static-elf") - self.assertEqual( - provenance["binary_sha256"], common.sha256_file(destination) - ) - self.assertEqual(len(provenance["source_sha256"]), 64) - - def test_device_io_provenance_uses_shared_helper_source(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - checkout = root / "nvx" - source = checkout / "guest" / "common" / "nvx-device-io.c" - source.parent.mkdir(parents=True) - source.write_bytes(b"shared helper source") - initrd = root / "initramfs.cpio.gz" - manifest = initrd.with_name(f"{initrd.name}.packages.json") - manifest.write_text( - json.dumps( - { - "helpers": { - "nvx-device-io": { - "source_sha256": common.sha256_file(source), - "binary_sha256": "1" * 64, - } - } - } - ), - encoding="utf-8", - ) - - provenance = benchmark._device_io_helper_provenance( - argparse.Namespace(nvx_dir=checkout), - initrd, - ) - - self.assertEqual(provenance["source"], str(source.resolve())) - self.assertEqual( - provenance["source_sha256"], - common.sha256_file(source), - ) - - def test_sandbox_kernel_config_requires_every_feature(self): - with tempfile.TemporaryDirectory() as temporary: - config = Path(temporary) / ".config" - config.write_text( - "\n".join(KernelBuildConstants.REQUIRED_SANDBOX_CONFIG) + "\n", - encoding="utf-8", - ) - build._assert_sandbox_kernel_config(config) - - for missing in KernelBuildConstants.REQUIRED_SANDBOX_CONFIG: - with self.subTest(missing=missing): - config.write_text( - "\n".join( - setting - for setting in KernelBuildConstants.REQUIRED_SANDBOX_CONFIG - if setting != missing - ) - + "\n", - encoding="utf-8", - ) - with self.assertRaisesRegex(common.ScriptError, missing): - build._assert_sandbox_kernel_config(config) - - def test_network_kernel_config_requires_every_feature(self): - with tempfile.TemporaryDirectory() as temporary: - config = Path(temporary) / ".config" - config.write_text( - "\n".join(KernelBuildConstants.REQUIRED_NETWORK_CONFIG) + "\n", - encoding="utf-8", - ) - build._assert_network_kernel_config(config) - - for missing in KernelBuildConstants.REQUIRED_NETWORK_CONFIG: - with self.subTest(missing=missing): - config.write_text( - "\n".join( - setting - for setting in KernelBuildConstants.REQUIRED_NETWORK_CONFIG - if setting != missing - ) - + "\n", - encoding="utf-8", - ) - with self.assertRaisesRegex( - common.ScriptError, "portable network profile" - ) as raised: - build._assert_network_kernel_config(config) - self.assertIn(missing, str(raised.exception)) - - # An enabled SIT device leaves the "is not set" line out. - config.write_text( - "\n".join( - "CONFIG_IPV6_SIT=y" - if setting == "# CONFIG_IPV6_SIT is not set" - else setting - for setting in KernelBuildConstants.REQUIRED_NETWORK_CONFIG - ) - + "\n", - encoding="utf-8", - ) - with self.assertRaisesRegex(common.ScriptError, "CONFIG_IPV6_SIT"): - build._assert_network_kernel_config(config) - - def test_checked_in_config_provides_the_portable_network_profile(self): - config = BuildConstants.REPO_ROOT / KernelBuildConstants.INPUT_CONFIG - build._assert_network_kernel_config(config) - - def test_checked_in_config_preserves_generic_sandbox_capabilities(self): - config = BuildConstants.REPO_ROOT / "kernel" / "config-microvm" - build._assert_sandbox_kernel_config(config) - configured = config.read_text(encoding="utf-8").splitlines() - for setting in ( - "CONFIG_SECCOMP_FILTER=y", - "CONFIG_UNIX=y", - "# CONFIG_OVERLAY_FS_REDIRECT_ALWAYS_FOLLOW is not set", - ): - self.assertIn(setting, configured) - - def test_shared_status_kernel_config_is_required(self): - with tempfile.TemporaryDirectory() as temporary: - config = Path(temporary) / ".config" - config.write_text( - "\n".join(KernelBuildConstants.REQUIRED_SHARED_STATUS_CONFIG) + "\n", - encoding="utf-8", - ) - build._assert_shared_status_kernel_config(config) - - config.write_text("", encoding="utf-8") - with self.assertRaisesRegex( - common.ScriptError, - KernelBuildConstants.REQUIRED_SHARED_STATUS_CONFIG[0], - ): - build._assert_shared_status_kernel_config(config) - - def test_checked_in_config_meets_time_abi_kernel_contract(self): - config = BuildConstants.REPO_ROOT / KernelBuildConstants.INPUT_CONFIG - build._assert_time_abi_kernel_config(config) - build._assert_hardening_kernel_config(config) - build._assert_watchdog_kernel_config(config, debug=False) - configured = config.read_text(encoding="utf-8").splitlines() - for removed in ( - "CONFIG_CPU_FREQ=y", - "CONFIG_X86_INTEL_PSTATE=y", - "CONFIG_SCHED_MC_PRIO=y", - "CONFIG_HYPERV=y", - "CONFIG_KVM_GUEST=y", - "CONFIG_PARAVIRT_CLOCK=y", - "CONFIG_HALTPOLL_CPUIDLE=y", - "CONFIG_ARCH_CPUIDLE_HALTPOLL=y", - ): - with self.subTest(removed=removed): - self.assertNotIn(removed, configured) - self.assertFalse( - any(line.startswith("CONFIG_CPU_FREQ_") for line in configured) - ) - - def test_time_abi_kernel_config_requires_every_setting(self): - with tempfile.TemporaryDirectory() as temporary: - config = Path(temporary) / ".config" - for missing in KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG: - with self.subTest(missing=missing): - config.write_text( - "\n".join( - setting - for setting in KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG - if setting != missing - ) - + "\n", - encoding="utf-8", - ) - with self.assertRaisesRegex(common.ScriptError, missing): - build._assert_time_abi_kernel_config(config) - - def test_hardening_kernel_config_requires_every_setting(self): - with tempfile.TemporaryDirectory() as temporary: - config = Path(temporary) / ".config" - for missing in KernelBuildConstants.REQUIRED_HARDENING_CONFIG: - with self.subTest(missing=missing): - config.write_text( - "\n".join( - setting - for setting in KernelBuildConstants.REQUIRED_HARDENING_CONFIG - if setting != missing - ) - + "\n", - encoding="utf-8", - ) - with self.assertRaisesRegex(common.ScriptError, re.escape(missing)): - build._assert_hardening_kernel_config(config) - - def test_hardening_kernel_config_rejects_loadable_modules(self): - with tempfile.TemporaryDirectory() as temporary: - config = Path(temporary) / ".config" - config.write_text( - "\n".join( - (*KernelBuildConstants.REQUIRED_HARDENING_CONFIG, "CONFIG_FOO=m") - ) - + "\n", - encoding="utf-8", - ) - with self.assertRaisesRegex(common.ScriptError, "CONFIG_FOO=m"): - build._assert_hardening_kernel_config(config) - - def test_watchdog_kernel_config_separates_production_and_debug(self): - with tempfile.TemporaryDirectory() as temporary: - config = Path(temporary) / ".config" - config.write_text( - "\n".join(KernelBuildConstants.REQUIRED_DEBUG_CONFIG) + "\n", - encoding="utf-8", - ) - build._assert_watchdog_kernel_config(config, debug=True) - with self.assertRaisesRegex(common.ScriptError, "debug-only watchdogs"): - build._assert_watchdog_kernel_config(config, debug=False) - - config.write_text("CONFIG_DEBUG_KERNEL=y\n", encoding="utf-8") - build._assert_watchdog_kernel_config(config, debug=False) - with self.assertRaisesRegex( - common.ScriptError, "CONFIG_SOFTLOCKUP_DETECTOR=y" - ): - build._assert_watchdog_kernel_config(config, debug=True) - - def test_kernel_config_fragment_replaces_base_assignments(self): - base = "\n".join( - ( - "#", - "# Kernel hacking", - "#", - "# CONFIG_DEBUG_KERNEL is not set", - "CONFIG_RCU_CPU_STALL_TIMEOUT=21", - "CONFIG_UNRELATED=y", - ) - ) - fragment = "\n".join( - ( - "# Comment lines and blank lines are ignored.", - "", - "CONFIG_DEBUG_KERNEL=y", - "# CONFIG_DEBUG_MISC is not set", - "CONFIG_RCU_CPU_STALL_TIMEOUT=21", - ) - ) - self.assertEqual( - build.merge_kernel_config_fragment(base, fragment).splitlines(), - [ - "#", - "# Kernel hacking", - "#", - "CONFIG_UNRELATED=y", - "CONFIG_DEBUG_KERNEL=y", - "# CONFIG_DEBUG_MISC is not set", - "CONFIG_RCU_CPU_STALL_TIMEOUT=21", - ], - ) - for invalid, message in ( - ("DEBUG_KERNEL=y\n", "line 1 is invalid"), - ("# only a comment\n", "assigns no symbols"), - ( - "CONFIG_DEBUG_KERNEL=y\n# CONFIG_DEBUG_KERNEL is not set\n", - "more than once", - ), + @unittest.skipUnless(os.name == "posix", "requires bash") + def test_linux_acceptance_preserves_attempts_and_publishes_only_valid_data(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "run-benchmark" + / "action.yml" + ).read_text(encoding="utf-8") + script = _composite_action_script(action, "Run Linux acceptance test") + platform = "linux-kvm-virtual-machine" + for name, value in ( + ("backend", "kvm"), + ("platform", platform), + ("warmups", "1"), + ("runs", "10"), + ("teardown-mode", "guest-exit"), ): - with self.subTest(fragment=invalid): - with self.assertRaisesRegex(common.ScriptError, message): - build.merge_kernel_config_fragment(base, invalid) - - def test_checked_in_debug_fragment_only_adds_watchdogs(self): - base = (BuildConstants.REPO_ROOT / KernelBuildConstants.INPUT_CONFIG).read_text( - encoding="utf-8" + script = script.replace("${{ inputs." + name + " }}", value) + stub = """ +benchmark_count=0 +validation_count=0 +python3() { + local argument output="" input="" platform="" attempt_log="" previous="" + for argument in "$@"; do + case "${previous}" in + --output) output="${argument}" ;; + --input) input="${argument}" ;; + --platform) platform="${argument}" ;; + --attempt-log) attempt_log="${argument}" ;; + esac + previous="${argument}" + done + if [[ "${2-}" == benchmark ]]; then + benchmark_count=$((benchmark_count + 1)) + echo "benchmark ${platform} ${output}" + mkdir -p "$(dirname "${output}")" + printf '{"attempt":%d}' "${benchmark_count}" >"${output}" + return "${BENCHMARK_STATUS}" + fi + if [[ "${2-} ${3-}" == "performance validate-openvmm" ]]; then + validation_count=$((validation_count + 1)) + echo "validate ${platform} ${input} ${attempt_log}" + return "${VALIDATION_STATUSES[validation_count - 1]}" + fi + echo "unexpected command: $*" >&2 + return 1 +} +""" + environment = os.environ.copy() + environment.update( + { + "NVX_BENCHMARK_CPUS": "0-7", + "NVX_BENCHMARK_HOST_CPU_RESERVE": "0", + "NVX_BENCHMARK_MARKER_TIMEOUT": "40", + } ) - fragment = ( - BuildConstants.REPO_ROOT / KernelBuildConstants.DEBUG_CONFIG_FRAGMENT - ).read_text(encoding="utf-8") - merged = build.merge_kernel_config_fragment(base, fragment).splitlines() - for setting in KernelBuildConstants.REQUIRED_DEBUG_CONFIG: - with self.subTest(setting=setting): - self.assertIn(setting, merged) - for setting in ( - "# CONFIG_DEBUG_MISC is not set", - "# CONFIG_RCU_TRACE is not set", - "# CONFIG_X86_DEBUG_FPU is not set", - "CONFIG_RCU_CPU_STALL_TIMEOUT=21", + run_dir = f"data/runs/{platform}/microvm-v2/1vcpu" + for name, statuses, benchmark_status in ( + ("first-success", (0,), 0), + ("remeasured-success", (75, 0), 0), + ("both-unstable", (75, 75), 0), + ("invalid-data", (2,), 0), + ("unstable-then-invalid", (75, 2), 0), + ("benchmark-failure", (0,), 42), ): - with self.subTest(setting=setting): - self.assertIn(setting, merged) - self.assertNotIn("# CONFIG_DEBUG_KERNEL is not set", merged) - for setting in KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG: - with self.subTest(time_abi=setting): - self.assertIn(setting, merged) - - def test_debug_kernel_build_applies_fragment_and_records_provenance(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - input_config = root / KernelBuildConstants.INPUT_CONFIG - input_config.parent.mkdir(parents=True) - input_config.write_text( - "\n".join( - ( - *KernelBuildConstants.REQUIRED_DIRECT_BOOT_CONFIG, - *KernelBuildConstants.REQUIRED_VIRTIO_CONSOLE_CONFIG, - *KernelBuildConstants.REQUIRED_SHARED_STATUS_CONFIG, - *KernelBuildConstants.REQUIRED_SANDBOX_CONFIG, - *KernelBuildConstants.REQUIRED_NETWORK_CONFIG, - *KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG, - *KernelBuildConstants.REQUIRED_HARDENING_CONFIG, - "# CONFIG_DEBUG_KERNEL is not set", - ) + with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary).resolve() + output = root / run_dir + output.mkdir(parents=True) + accepted = output / "acceptance.json" + accepted.write_text('{"attempt":"stale"}', encoding="utf-8") + # A rerun in the same directory must not keep a previous + # run's attempt log or later attempts. + attempt_log = output / "acceptance-attempts.json" + attempt_log.write_text('{"attempts":"stale"}', encoding="utf-8") + (output / "acceptance-attempt-2.json").write_text( + '{"attempt":"stale"}', encoding="utf-8" ) - + "\n", - encoding="utf-8", - ) - fragment = root / KernelBuildConstants.DEBUG_CONFIG_FRAGMENT - fragment.write_text( - "# debug fixture\n" - + "\n".join(KernelBuildConstants.REQUIRED_DEBUG_CONFIG) - + "\n", - encoding="utf-8", - ) - patch_path = root / KernelBuildConstants.PATCH_DIRECTORY / "example.patch" - patch_path.parent.mkdir() - patch_path.write_text("patch", encoding="utf-8") - source = root / "source" - source.mkdir() - output_directory = root / "output" - output_directory.mkdir() - production_provenance = ( - output_directory / KernelBuildConstants.PROVENANCE_NAME - ) - production_provenance.write_text("production", encoding="utf-8") - config = build_config.KernelBuildConfig( - work=root / "work-debug", - output=output_directory / KernelBuildConstants.DEBUG_BINARY_NAME, - debug=True, - ) - - with patch.object(BuildConstants, "REPO_ROOT", root): - source_fingerprint = build._kernel_source_fingerprint() - - def run_build(command: object, **_kwargs: object) -> None: - if isinstance(command, list) and command[-1] == "vmlinux": - (config.work / "vmlinux").write_bytes(b"debug kernel") - - with ( - patch.object(BuildConstants, "REPO_ROOT", root), - patch.object(build, "_require_linux"), - patch.object(build, "require_tool", return_value="tool"), - patch.object( - build, - "prepare_kernel_source", - return_value=(source, source_fingerprint), - ), - patch.object(build, "run_checked", side_effect=run_build), - ): - build.build_kernel(config) - - generated = ( - output_directory / KernelBuildConstants.DEBUG_CONFIG_NAME - ).read_text(encoding="utf-8") - self.assertIn("CONFIG_DEBUG_KERNEL=y", generated.splitlines()) - self.assertNotIn("# CONFIG_DEBUG_KERNEL is not set", generated) - provenance = json.loads( - ( - output_directory / KernelBuildConstants.DEBUG_PROVENANCE_NAME - ).read_text(encoding="utf-8") - ) - self.assertEqual( - provenance["debug_config_fragment"], - { - "path": KernelBuildConstants.DEBUG_CONFIG_FRAGMENT.as_posix(), - "sha256": common.sha256_file(fragment), - }, - ) - self.assertEqual( - provenance["input_config"]["sha256"], common.sha256_file(input_config) - ) - self.assertEqual( - provenance["kernel_sha256"], - hashlib.sha256(b"debug kernel").hexdigest(), - ) - self.assertEqual( - production_provenance.read_text(encoding="utf-8"), "production" - ) - self.assertIn( - "debug_config_fragment_sha256", - (config.work / KernelBuildConstants.BUILD_STAMP_NAME).read_text( - encoding="utf-8" - ), - ) - self.assertEqual( - config.provenance_name, KernelBuildConstants.DEBUG_PROVENANCE_NAME - ) - self.assertEqual( - build_config.KernelBuildConfig().provenance_name, - KernelBuildConstants.PROVENANCE_NAME, - ) - self.assertNotIn("debug_config_fragment", build.kernel_provenance_inputs()) + setup = ( + "VALIDATION_STATUSES=(" + + " ".join(str(status) for status in statuses) + + f")\nBENCHMARK_STATUS={benchmark_status}\n" + ) + result = subprocess.run( + [ + "bash", + "--noprofile", + "--norc", + "-eo", + "pipefail", + "-c", + setup + stub + script, + ], + cwd=root, + env=environment, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + expected_status = benchmark_status or statuses[-1] + self.assertEqual(result.returncode, expected_status, result.stderr) + self.assertFalse(attempt_log.exists()) + expected_stdout: list[str] = [] + for index in range(1, len(statuses) + 1): + attempt = f"{run_dir}/acceptance-attempt-{index}.json" + if index > 1: + expected_stdout.append( + "::warning::Lifecycle snapshot generation was unstable " + f"on attempt {index - 1}; remeasuring once" + ) + expected_stdout.append(f"benchmark {platform} {attempt}") + if not benchmark_status: + expected_stdout.append( + f"validate {platform} {attempt} " + f"{run_dir}/acceptance-attempts.json" + ) + self.assertEqual(result.stdout.splitlines(), expected_stdout) + self.assertEqual( + sorted(path.name for path in output.glob("acceptance-attempt-*")), + [ + f"acceptance-attempt-{index}.json" + for index in range(1, len(statuses) + 1) + ], + ) + for index in range(1, len(statuses) + 1): + attempt = output / f"acceptance-attempt-{index}.json" + self.assertEqual( + json.loads(attempt.read_text(encoding="utf-8")), + {"attempt": index}, + ) + self.assertEqual(accepted.exists(), expected_status == 0) + if expected_status == 0: + self.assertEqual( + json.loads(accepted.read_text(encoding="utf-8")), + {"attempt": len(statuses)}, + ) - def test_guest_build_adds_debug_kernel_only_when_requested(self): - with ( - patch.object(build, "build_kernel") as kernel, - patch.object(build, "build_initramfs"), - patch.object(build, "build_docker_debug_kernel") as docker_debug, - ): - native = build_config.BuildConfig(native_guest=True, debug_kernel=True) - build.build_guest(native) - self.assertEqual( - kernel.call_args_list, - [ - call(native.kernel), - call(build_config.KernelBuildConfig.debug_variant()), - ], - ) - docker_debug.assert_not_called() - with ( - patch.object(build, "build_docker_artifacts") as docker, - patch.object(build, "build_docker_debug_kernel") as docker_debug, +class BuildConstantsTests(unittest.TestCase): + def test_constants_are_grouped_class_namespaces(self): + for namespace in ( + BuildConstants, + KernelBuildConstants, + OpenVMMBuildConstants, + AlpineBuildConstants, + UbuntuBuildConstants, + InitramfsBuildConstants, + DockerBuildConstants, + ZstdBuildConstants, + ReleaseBuildConstants, ): - portable = build_config.BuildConfig(debug_kernel=True) - build.build_guest(portable) - docker.assert_called_once_with(portable.docker, "alpine") - docker_debug.assert_called_once_with(portable.docker) + with self.subTest(namespace=namespace.__name__): + self.assertEqual(namespace.__module__, "nvx_tools.build_constants") + self.assertTrue( + all( + name.isupper() + for name in vars(namespace) + if not name.startswith("__") + ) + ) - with ( - patch.object(build, "build_docker_artifacts"), - patch.object(build, "build_docker_debug_kernel") as docker_debug, + def test_build_pins_and_artifact_names_have_one_literal_owner(self): + centralized = { + KernelBuildConstants.VERSION, + KernelBuildConstants.URL, + KernelBuildConstants.SHA256, + KernelBuildConstants.BINARY_NAME, + KernelBuildConstants.CONFIG_NAME, + KernelBuildConstants.PROVENANCE_NAME, + KernelBuildConstants.DEBUG_BINARY_NAME, + KernelBuildConstants.DEBUG_CONFIG_NAME, + KernelBuildConstants.DEBUG_PROVENANCE_NAME, + OpenVMMBuildConstants.GNU_RUST_TARGET, + OpenVMMBuildConstants.MUSL_RUST_TARGET, + OpenVMMBuildConstants.WINDOWS_RUST_TARGET, + OpenVMMBuildConstants.PROVENANCE_NAME, + OpenVMMBuildConstants.CONTROL_CONTRACT_REVISION, + AlpineBuildConstants.VERSION, + AlpineBuildConstants.BRANCH, + AlpineBuildConstants.MINIROOTFS_SHA256, + AlpineBuildConstants.INITRAMFS_NAME, + AlpineBuildConstants.PACKAGE_MANIFEST_NAME, + UbuntuBuildConstants.VERSION, + UbuntuBuildConstants.BASE_URL, + UbuntuBuildConstants.BASE_SHA256, + UbuntuBuildConstants.INITRAMFS_NAME, + UbuntuBuildConstants.PACKAGE_MANIFEST_NAME, + UbuntuBuildConstants.DISTRO_NAME, + UbuntuBuildConstants.DISTRO_MANIFEST_NAME, + InitramfsBuildConstants.PROVENANCE_NAME, + ZstdBuildConstants.VERSION, + ZstdBuildConstants.SHA256, + } + duplicates: list[tuple[str, int, str]] = [] + for path in (BuildConstants.REPO_ROOT / "scripts").rglob("*.py"): + if path.name == "build_constants.py" or path.name.startswith("test_"): + continue + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + duplicates.extend( + (path.name, node.lineno, node.value) + for node in ast.walk(tree) + if isinstance(node, ast.Constant) + and isinstance(node.value, str) + and node.value in centralized + ) + self.assertEqual(duplicates, []) + + def test_old_module_level_constants_are_not_reexported(self): + names = ( + "REPO_ROOT", + "BUILD_DIR", + "SOURCE_DIR", + "OPENVMM_DIR", + "DEFAULT_KERNEL_VERSION", + "DEFAULT_KERNEL_URL", + "DEFAULT_KERNEL_SHA256", + "DEFAULT_ALPINE_VERSION", + "DEFAULT_ALPINE_BRANCH", + "DEFAULT_ALPINE_MINIROOTFS_SHA256", + "DEFAULT_UBUNTU_VERSION", + "DEFAULT_UBUNTU_CODENAME", + "DEFAULT_UBUNTU_ARCHITECTURE", + "DEFAULT_UBUNTU_BASE_URL", + "DEFAULT_UBUNTU_BASE_SHA256", + "UBUNTU_PACKAGE_LOCK", + "UBUNTU_EROFS_FORMAT", + "MICROVM_ABI_VERSION", + "CONTROL_SESSION_PROTOCOL_VERSION", + "CONTROL_CONTRACT_REVISION", + "OPENVMM_PROVENANCE_NAME", + "KERNEL_PROVENANCE_NAME", + "INITRAMFS_PROVENANCE_NAME", + "REQUIRED_VIRTIO_CONSOLE_CONFIG", + "REQUIRED_SHARED_STATUS_KERNEL_CONFIG", + "REQUIRED_SANDBOX_KERNEL_CONFIG", + "ZSTD_VERSION", + "ZSTD_ARCHIVE", + "ZSTD_URL", + "ZSTD_SHA256", + "OPENVMM_RUST_TOOLCHAIN", + "OPENVMM_GUEST_RUST_TARGET", + "OPENVMM_UEFI_RUST_TARGET", + "OPENVMM_LINUX_MUSL_RUST_TARGET", + "OPENVMM_RUST_TARGETS", + "APORTS_URL", + "REPOSITORIES", + "UBUNTU_ARCHIVE_KEYRING_URL", + "UBUNTU_ARCHIVE_KEYRING_SHA256", + "UBUNTU_SNAPSHOT_ARCHIVE_URL", + "UBUNTU_SOURCE_INDEXES", + "_UBUNTU_POCKET_SUITES", + "_UBUNTU_COMPONENTS", + "_LAUNCHPAD_ARCHIVE_API", + "_LAUNCHPAD_SERIES_API", + "_GENERATED_OUTPUT_ENTRIES", + "_KNOWN_PACKAGE_CONTROL_ACTIONS", + "PROJECT_SOURCE_PATHS", + "GUEST_RELEASE_NAMES", + "_ZIP_TIMESTAMP", + ) + for module in ( + build, + build_config, + common, + ubuntu, + ci, + collect_alpine_sources, + collect_ubuntu_sources, + release, + benchmark, + archive, ): - build.build_guest(build_config.BuildConfig()) - docker_debug.assert_not_called() + with self.subTest(module=module.__name__): + self.assertFalse(set(names).intersection(vars(module))) - debug = build_config.KernelBuildConfig.debug_variant() - self.assertTrue(debug.debug) - self.assertEqual(debug.output.name, KernelBuildConstants.DEBUG_BINARY_NAME) + def test_guest_descriptors_and_source_manifest_share_build_pins(self): + manifest = json.loads( + (BuildConstants.REPO_ROOT / "SOURCE-MANIFEST.json").read_text( + encoding="utf-8" + ) + ) + self.assertEqual(KernelBuildConstants.VERSION, manifest["linux"]["version"]) + self.assertEqual(KernelBuildConstants.URL, manifest["linux"]["upstream_url"]) self.assertEqual( - debug.work.name, KernelBuildConstants.DEBUG_WORK_DIRECTORY_NAME + KernelBuildConstants.SHA256, + manifest["linux"]["upstream_archive_sha256"], ) - - def test_guest_files_build_time_probe_with_its_own_flags(self): - source = BuildConstants.REPO_ROOT / "guest" / "common" / "nvx-time-probe.c" - header = source.read_text(encoding="utf-8").splitlines()[:2] self.assertEqual( - header, - [ - "// Copyright (c) Microsoft Corporation.", - "// Licensed under the MIT License.", - ], + AlpineBuildConstants.MINIROOTFS_URL, manifest["alpine"]["minirootfs_url"] ) - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) / "root" - work = Path(temporary) / "work" - for directory in (root / "sbin", root / "etc", work): - directory.mkdir(parents=True) - commands: list[list[str]] = [] - - def compile_helper( - command: list[str | os.PathLike[str]], **_kwargs: object - ) -> None: - commands.append([os.fspath(part) for part in command]) - output = Path(command[command.index("-o") + 1]) - output.write_bytes(f"static-elf {output.name}".encode()) - - def find_tool(name: str) -> str: - return name - - with ( - patch.object(build, "require_tool", side_effect=find_tool), - patch.object(build, "run_checked", side_effect=compile_helper), - ): - helpers = build._install_guest_files( - build_config.InitramfsBuildConfig(work=work), - root, - guests.guest_descriptor("alpine"), - ) - - probe = InitramfsBuildConstants.TIME_PROBE_NAME - flags_by_source = { - Path(command[-1]).stem: command[1 : command.index("-o")] - for command in commands - } - compiler_by_source = { - Path(command[-1]).stem: command[0] for command in commands - } self.assertEqual( - flags_by_source[probe], list(InitramfsBuildConstants.TIME_PROBE_CFLAGS) + AlpineBuildConstants.MINIROOTFS_SHA256, + manifest["alpine"]["minirootfs_sha256"], ) - self.assertIn("-pthread", flags_by_source[probe]) - musl = InitramfsBuildConstants.MUSL_COMPILER - self.assertEqual(compiler_by_source[probe], musl) - for name in InitramfsBuildConstants.STATIC_HELPERS: - with self.subTest(helper=name): - self.assertEqual( - flags_by_source[name], - list(InitramfsBuildConstants.STATIC_HELPER_CFLAGS), - ) - self.assertEqual(compiler_by_source[name], "cc") - for name in InitramfsBuildConstants.MUSL_STATIC_HELPERS: - with self.subTest(helper=name): + for descriptor in (guests.ALPINE_GUEST, guests.UBUNTU_GUEST): + with self.subTest(guest=descriptor.name): + pinned = manifest[descriptor.name] + self.assertEqual(descriptor.release, pinned["version"]) + self.assertEqual(descriptor.architecture, pinned["architecture"]) self.assertEqual( - flags_by_source[name], - list(InitramfsBuildConstants.STATIC_HELPER_CFLAGS), + f"build/{descriptor.package_manifest_name}", + pinned["package_manifests"][0], ) - self.assertEqual(compiler_by_source[name], musl) - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" - ) - self.assertIn(" musl-tools \\\n", dockerfile) - self.assertIn( - " for helper in " - + " ".join(InitramfsBuildConstants.MUSL_STATIC_HELPERS) - + "; do \\\n musl-gcc ", - dockerfile, - ) - self.assertEqual(helpers[probe]["source_sha256"], common.sha256_file(source)) self.assertEqual( - helpers[probe]["binary_sha256"], - hashlib.sha256(f"static-elf {probe}".encode()).hexdigest(), + OpenVMMBuildConstants.MICROVM_ABI_VERSION, + manifest["openvmm"]["microvm_abi_version"], ) - - def test_docker_debug_kernel_target_exports_debug_artifacts(self): - dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( - encoding="utf-8" + self.assertEqual( + OpenVMMBuildConstants.CONTROL_SESSION_PROTOCOL_VERSION, + manifest["openvmm"]["control_session_protocol_version"], ) - self.assertIn( - "FROM base AS kernel-debug\n" - "RUN python3 scripts/nvx.py build-kernel --debug\n", - dockerfile, + self.assertEqual( + OpenVMMBuildConstants.CONTROL_CONTRACT_REVISION, + manifest["openvmm"]["control_contract_revision"], ) - self.assertIn( - f"FROM scratch AS {DockerBuildConstants.DEBUG_KERNEL_TARGET}", dockerfile + self.assertEqual( + manifest["guest_rust"], + { + "toolchain": GuestRustBuildConstants.TOOLCHAIN, + "target": GuestRustBuildConstants.TARGET, + }, ) - for name in DockerBuildConstants.DEBUG_KERNEL_ARTIFACT_NAMES: - with self.subTest(artifact=name): - self.assertIn( - f"COPY --from=kernel-debug /repo/build/{name} /{name}", dockerfile + for guest in ("alpine", "ubuntu", "azurelinux"): + with self.subTest(guest=guest): + self.assertTrue( + set(GuestRustBuildConstants.SOURCE_PATHS).issubset( + manifest[guest]["guest_sources"] + ) ) + def test_cache_defaults_are_resolved_for_each_configuration(self): with tempfile.TemporaryDirectory() as temporary: - destination = Path(temporary) - - def export(_command: object, **_kwargs: object) -> None: - for name in DockerBuildConstants.DEBUG_KERNEL_ARTIFACT_NAMES: - (destination / name).write_bytes(b"artifact") - - config = build_config.DockerBuildConfig(artifact_destination=destination) - with ( - patch.object(build, "require_tool", return_value="docker"), - patch.object(build, "run_checked", side_effect=export) as run, - ): - build.build_docker_debug_kernel(config) - command = run.call_args.args[0] - self.assertEqual( - command[command.index("--target") + 1], - DockerBuildConstants.DEBUG_KERNEL_TARGET, - ) - - for name in DockerBuildConstants.DEBUG_KERNEL_ARTIFACT_NAMES: - (destination / name).unlink() - with ( - patch.object(build, "require_tool", return_value="docker"), - patch.object(build, "run_checked"), - self.assertRaisesRegex(common.ScriptError, "did not produce"), - ): - build.build_docker_debug_kernel(config) - - -def _posix_shell() -> str | None: - shell = shutil.which("sh") - if shell is None: - git = shutil.which("git") - git_shell = Path(git).parent.parent / "bin" / "sh.exe" if git else None - if git_shell is not None and git_shell.is_file(): - shell = str(git_shell) - return shell - - -def _shell_function(source: str, name: str) -> str: - start = source.index(f"\n{name}() {{\n") + 1 - return source[start : source.index("\n}\n", start) + 3] - - -class SandboxShareAgentTests(unittest.TestCase): - AGENT = Path(__file__).parents[1] / "guest" / "common" / "nvx-init-agent" - - def setUp(self): - shell = _posix_shell() - if shell is None: - self.skipTest("POSIX shell is unavailable") - self.shell = shell - self.source = self.AGENT.read_text(encoding="utf-8") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - self.root = Path(temporary.name) - self.rootfs = self.root / "rootfs" - self.rootfs.mkdir() - self.runtime = (self.root / "run").as_posix() - - SHARE_FUNCTIONS = ( - "validate_share_target", - "make_share_mount_point", - "record_live_share", - "claim_share_target", - "mount_live_share", - "parse_share_child", - "bind_live_share", - "mount_live_shares", - ) + root = Path(temporary) + for configured in ("", str(root / "first"), str(root / "second")): + with ( + self.subTest(configured=configured), + patch.dict(os.environ, {"NVX_CACHE_DIR": configured}), + ): + expected = ( + Path(configured) + if configured + else BuildConstants.REPO_ROOT / ".cache" + ).resolve() + self.assertEqual( + build_config.KernelBuildConfig().cache_directory, expected + ) + self.assertEqual(common.cache_root(), expected) + self.assertEqual( + build_config.KernelBuildConfig( + cache_directory=root / "explicit" + ).cache_directory, + root / "explicit", + ) - def _aggregate(self, *children: str, mode: str = "rw") -> str: - """Returns the bootstrap of an aggregate whose children the - CHILD,TARGET,MODE specifications `children` place.""" - tokens = [ - f"virtfs_dir={self.runtime}/shares", - "virtfs_tag=microvm", - f"virtfs_mode={mode}", - "virtfs_aggregate=1", - *(f"nvx_share={child}" for child in children), - ] - return " ".join(tokens) + def test_openvmm_executable_default_is_resolved_at_configuration_time(self): + for host, binary in (("nt", "openvmm.exe"), ("posix", "openvmm")): + expected = OpenVMMBuildConstants.DIRECTORY / "target" / "release" / binary + with self.subTest(host=host), patch.object(common.os, "name", host): + self.assertEqual(common.openvmm_binary_path(), expected) + self.assertEqual(build_config.OpenVmmBuildConfig().output, expected) - def _run(self, cmdline: str) -> tuple[subprocess.CompletedProcess[str], str]: - mount_log = self.root / "mount.log" - mount_log.unlink(missing_ok=True) - functions = "".join( - _shell_function(self.source, name) for name in self.SHARE_FUNCTIONS - ) - script = ( - "set -eu\n" - "rootfs=$1\ncmdline=$2\nmount_log=$3\nruntime=$4\nshare_mountpoints=\n" - 'fatal() { echo "FATAL: $*" >&2; exit 125; }\n' - 'mount() { printf "%s\\n" "$*" >>"$mount_log"; }\n' - f"{functions}" - "mount_live_shares\n" - "echo \"mountpoints=$(printf '%s' \"$share_mountpoints\" | tr '\\n' ' ')\"\n" - ) - result = subprocess.run( - [ - self.shell, - "-s", - "--", - self.rootfs.as_posix(), - cmdline, - mount_log.as_posix(), - self.runtime, - ], - input=script, - text=True, - capture_output=True, - check=False, + def test_initramfs_provenance_tracks_constants_source(self): + relative = "scripts/nvx_tools/build_constants.py" + source = BuildConstants.REPO_ROOT / relative + original = build.initramfs_provenance_inputs() + records = cast(list[dict[str, str]], original["source_files"]) + self.assertEqual( + [record for record in records if record["path"] == relative], + [{"path": relative, "sha256": common.normalized_sha256_file(source)}], ) - log = mount_log.read_text(encoding="utf-8") if mount_log.exists() else "" - return result, log - def _run_teardown( - self, script: str, *, shares: tuple[str, ...], failing: str = "" - ) -> tuple[subprocess.CompletedProcess[str], list[str]]: - runtime = self.root / "run" - runtime.mkdir(exist_ok=True) - for name in ("container.pid", "workload-machine-id"): - (runtime / name).write_text("x\n", encoding="utf-8") - log = self.root / "teardown.log" - log.unlink(missing_ok=True) - functions = "".join( - _shell_function(self.source, name) - for name in ("unmount_live_shares", "fatal", "teardown") - ).replace("/sbin/nvx-exit", "nvx_exit") - # The agent records mounted shares one per line, most recent first. - # Windows drops a newline from a process argument, so pass a separator. - mountpoints = "|".join( - f"{self.rootfs.as_posix()}{target}" for target in reversed(shares) - ) - result = subprocess.run( - [ - self.shell, - "-s", - "--", - runtime.as_posix(), - log.as_posix(), - mountpoints, - failing, - ], - input=( - "set -eu\n" - "runtime=$1\nlog=$2\nfailing=$4\n" - "share_mountpoints=$(printf '%s' \"$3\" | tr '|' '\\n')\n" - "rootfs=$runtime/rootfs\nlayers=$runtime/layers\n" - "scratch=$runtime/scratch\n" - 'umount() { printf "umount %s\\n" "$1" >>"$log"; ' - '[ "$1" != "$failing" ]; }\n' - 'mountpoint() { [ "$2" = "$layers/distro" ]; }\n' - 'nvx_exit() { printf "exit %s\\n" "$1" >>"$log"; exit "$1"; }\n' - f"{functions}{script}\n" - ), - text=True, - capture_output=True, - check=False, - ) - lines = log.read_text(encoding="utf-8").splitlines() if log.exists() else [] - return result, lines + def changed_sha256(path: Path) -> str: + return "f" * 64 if path == source else common.normalized_sha256_file(path) - def test_agent_mounts_share_after_lifecycle_checks_for_both_lifecycles(self): - mount_call = self.source.index("\nmount_live_shares\n") - self.assertLess( - self.source.index('fatal "configured workload home is unavailable"'), - mount_call, + with patch.object(build, "normalized_sha256_file", side_effect=changed_sha256): + changed = build.initramfs_provenance_inputs() + self.assertNotEqual(original, changed) + self.assertEqual(original["alpine"], changed["alpine"]) + + def test_constants_are_in_docker_and_release_source_inputs(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" ) - self.assertLess( - self.source.index('fatal "unsupported workload lifecycle'), mount_call + self.assertIn("COPY scripts/ /repo/scripts/", dockerfile) + self.assertIn("scripts", ReleaseBuildConstants.PROJECT_SOURCE_PATHS) + self.assertIn("nvx-rs", ReleaseBuildConstants.PROJECT_SOURCE_PATHS) + self.assertIn( + Path("nvx-rs") / "target", + ReleaseBuildConstants.PROJECT_SOURCE_EXCLUDES, ) - machine_id = self.source.index('>"$runtime/workload-machine-id"') - managed_agent = self.source.index("\n /sbin/nvx-managed-agent \\\n") - self.assertLess(mount_call, machine_id) - self.assertLess(machine_id, managed_agent) - self.assertLess(managed_agent, self.source.index("/sbin/nvx-container-launch")) - self.assertNotIn("exec /sbin/nvx-managed-agent", self.source) - self.assertEqual(self.source.count('\nteardown "$status"\n'), 1) - self.assertEqual(self.source.count('\n teardown "$status"\n'), 1) - self.assertLess(managed_agent, self.source.index('\n teardown "$status"\n')) - - def test_agent_teardown_unmounts_share_before_overlay_layers_and_scratch(self): - result, log = self._run_teardown("teardown 7", shares=("/workspace",)) - runtime = (self.root / "run").as_posix() - self.assertEqual(result.returncode, 7, result.stderr) - self.assertEqual( - log, - [ - f"umount {self.rootfs.as_posix()}/workspace", - f"umount {runtime}/rootfs", - f"umount {runtime}/layers/distro", - f"umount {runtime}/scratch", - "exit 7", - ], + self.assertTrue( + ( + BuildConstants.REPO_ROOT + / "scripts" + / "nvx_tools" + / "build_constants.py" + ).is_file() ) - self.assertIn("NVX-SANDBOX-EXIT: status=7", result.stdout) - self.assertFalse((self.root / "run" / "workload-machine-id").exists()) - self.assertFalse((self.root / "run" / "container.pid").exists()) - result, log = self._run_teardown("teardown 0", shares=()) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(log[0], f"umount {runtime}/rootfs") + def test_project_source_paths_exist_and_are_tracked(self): + for relative in ReleaseBuildConstants.PROJECT_SOURCE_PATHS: + with self.subTest(path=relative): + self.assertTrue((BuildConstants.REPO_ROOT / relative).exists()) + tracked = subprocess.run( + ["git", "ls-files", "--error-unmatch", "--", relative], + cwd=BuildConstants.REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(tracked.returncode, 0, tracked.stderr) - def test_agent_teardown_unmounts_shares_in_reverse_mount_order(self): - rootfs = self.rootfs.as_posix() - runtime = (self.root / "run").as_posix() - result, log = self._run_teardown( - "teardown 0", shares=("/workspace", "/opt/hostedtoolcache") + def test_no_c_or_assembler_sources_remain_outside_openvmm(self): + tracked = subprocess.run( + ["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], + cwd=BuildConstants.REPO_ROOT, + capture_output=True, + check=True, + ).stdout.split(b"\0") + forbidden = sorted( + path.decode("utf-8") + for path in tracked + if path + and (BuildConstants.REPO_ROOT / path.decode("utf-8")).is_file() + and not path.startswith(b"openvmm/") + and PurePosixPath(path.decode("utf-8")).suffix.lower() in {".c", ".h", ".s"} + ) + self.assertEqual(forbidden, []) + + def test_guest_binary_third_party_notices_cover_the_rust_runtime(self): + notices = (BuildConstants.REPO_ROOT / "THIRD_PARTY_NOTICES.md").read_text( + encoding="utf-8" + ) + musl = next( + line for line in notices.splitlines() if line.startswith("| musl libc |") ) - self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( - log[:3], - [ - f"umount {rootfs}/opt/hostedtoolcache", - f"umount {rootfs}/workspace", - f"umount {runtime}/rootfs", - ], + set(re.findall(r"`/sbin/(nvx-[a-z-]+)`", musl)), + set(GuestRustBuildConstants.BINARIES), + ) + for component in ( + "Rust core and standard libraries", + "Rust `compiler_builtins`", + "Rust `libc` crate", + "Rust `nix` crate 0.31.3", + "Rust `getrandom` crate 0.4.3", + "Rust `bitflags` crate 2.13.2", + "Rust `cfg-if` crate 1.0.5", + "Rust `memoffset` crate 0.9.1", + "Apache-2.0 WITH LLVM-exception", + ): + with self.subTest(component=component): + self.assertIn(component, notices) + + def test_nvx_rs_third_party_notices_cover_every_linked_crate(self): + # The crates of `cargo tree --locked --features cli -e normal` for the + # release targets, x86_64-unknown-linux-musl and x86_64-pc-windows-msvc: + # what bin/nvx-rs[.exe] links, and serde_derive with the crates that it + # runs on while nvx-rs builds. + shipped = { + "bitflags", + "block-buffer", + "cfg-if", + "cpufeatures", + "crypto-common", + "digest", + "generic-array", + "getrandom", + "itoa", + "libc", + "memchr", + "memoffset", + "nix", + "proc-macro2", + "quote", + "serde", + "serde_core", + "serde_derive", + "serde_json", + "sha2", + "syn", + "typenum", + "unicode-ident", + "windows-link", + "windows-sys", + "zmij", + } + # Build scripts, other targets, and the tests. + unshipped = { + "autocfg", + "cfg_aliases", + "errno", + "fastrand", + "linux-raw-sys", + "once_cell", + "r-efi", + "rustix", + "tempfile", + "version_check", + } + lock = (BuildConstants.REPO_ROOT / "nvx-rs" / "Cargo.lock").read_text( + encoding="utf-8" ) - - # A share that fails to unmount does not stop the others. - result, log = self._run_teardown( - "teardown 0", - shares=("/workspace", "/opt/hostedtoolcache"), - failing=f"{rootfs}/opt/hostedtoolcache", + versions = dict( + re.findall( + r'^\[\[package\]\]\nname = "([^"]+)"\nversion = "([^"]+)"$', + lock, + re.MULTILINE, + ) ) - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("failed to unmount the live share", result.stderr) - self.assertEqual( - log[:2], - [f"umount {rootfs}/opt/hostedtoolcache", f"umount {rootfs}/workspace"], + # A new dependency is one or the other, and a shipped one needs notices. + self.assertEqual(set(versions) - {"nvx-rs"}, shipped | unshipped) + self.assertFalse(shipped & unshipped) + notices = (BuildConstants.REPO_ROOT / "THIRD_PARTY_NOTICES.md").read_text( + encoding="utf-8" ) - self.assertEqual(log[-1], "exit 1") + rows = { + line.split(" | ", 1)[0]: line + for line in notices.splitlines() + if line.startswith("| ") + } + for name in sorted(shipped): + with self.subTest(crate=name): + row = rows[f"| Rust `{name}` crate {versions[name]}"] + self.assertIn("bin/nvx-rs", row) + self.assertIn("nvx-rs/Cargo.lock", row) + # The runtimes that the release executables link statically. + for component, location in ( + ("| Rust core and standard libraries", "`bin/nvx-rs`"), + ("| Rust `compiler_builtins`", "`bin/nvx-rs[.exe]`"), + ("| musl libc", "`bin/nvx-rs` of the `linux-kvm` and `linux-mshv`"), + ("| LLVM `libunwind`", "the Linux `bin/nvx-rs`"), + ( + "| Microsoft C runtime: the Visual C++ runtime and the Universal C " + "Runtime", + "`bin/nvx-rs.exe`", + ), + ): + with self.subTest(component=component): + self.assertIn(location, rows[component]) - def test_agent_teardown_reports_share_unmount_failure(self): - share = f"{self.rootfs.as_posix()}/workspace" - result, log = self._run_teardown( - "teardown 0", shares=("/workspace",), failing=share - ) - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("failed to unmount the live share", result.stderr) - self.assertEqual(log[0], f"umount {share}") - self.assertEqual(log[-1], "exit 1") - self.assertIn("NVX-SANDBOX-EXIT: status=1", result.stdout) + def test_project_source_archive_excludes_the_nvx_rs_target(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + crate = root / "nvx-rs" + (crate / "src").mkdir(parents=True) + (crate / "src" / "lib.rs").write_text("source\n", encoding="utf-8") + (crate / "target" / "debug").mkdir(parents=True) + (crate / "target" / "debug" / "nvx-rs").write_text( + "build output\n", encoding="utf-8" + ) + output = root / "source.tar.gz" + archive.create_reproducible_tar_gz( + output, + [(crate, "nvx-project-source/nvx-rs")], + ) + with tarfile.open(output, "r:gz") as source_archive: + names = source_archive.getnames() - def test_agent_fatal_unmounts_share_before_power_off(self): - result, log = self._run_teardown( - 'fatal "synthetic failure"', shares=("/workspace",) + self.assertIn("nvx-project-source/nvx-rs/src/lib.rs", names) + self.assertFalse(any("/target" in name for name in names)) + + def test_constants_consumers_import_in_either_order(self): + modules = ( + "nvx_tools.build_constants", + "nvx_tools.guests", + "nvx_tools.common", + "nvx_tools.build_config", + "nvx_tools.build", + "nvx_tools.release", + "nvx", ) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertIn("NVX-SANDBOX-ERROR: synthetic failure", result.stderr) - self.assertEqual( - log, [f"umount {self.rootfs.as_posix()}/workspace", "exit 125"] + for ordered in (modules, tuple(reversed(modules))): + with self.subTest(first=ordered[0]): + result = subprocess.run( + [ + sys.executable, + "-c", + "import importlib, sys; " + "[importlib.import_module(name) for name in sys.argv[1:]]", + *ordered, + ], + cwd=BuildConstants.REPO_ROOT / "scripts", + capture_output=True, + text=True, + timeout=30, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + +class UbuntuSourceCollectionTests(unittest.TestCase): + def test_malformed_package_manifest_is_reported_as_script_error(self): + with tempfile.TemporaryDirectory() as temporary: + manifest = Path(temporary) / "packages.json" + manifest.write_text("{", encoding="utf-8") + + with self.assertRaises(collect_ubuntu_sources.ScriptError) as context: + collect_ubuntu_sources._source_requirements([manifest]) + + self.assertIn("cannot read Ubuntu package manifest", str(context.exception)) + + +class AlpineSourceCollectionTests(unittest.TestCase): + INSTALL_SCRIPT = b"#!/bin/sh\nexit 0\n" + + @staticmethod + def _aports_commit(cache: Path, files: dict[str, tuple[str, bytes]]) -> str: + environment = { + key: value + for key, value in os.environ.items() + if not key.startswith("GIT_CONFIG_") + } + environment.update( + GIT_AUTHOR_NAME="NVX Tests", + GIT_AUTHOR_EMAIL="nvx-tests@example.com", + GIT_COMMITTER_NAME="NVX Tests", + GIT_COMMITTER_EMAIL="nvx-tests@example.com", ) - result, log = self._run_teardown('fatal "early failure"', shares=()) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertEqual(log, ["exit 125"]) + def git(*args: str, data: bytes | None = None) -> str: + result = subprocess.run( + ["git", "-C", str(cache), *args], + input=data, + env=environment, + capture_output=True, + check=True, + ) + return result.stdout.decode("ascii").strip() - rootfs = self.rootfs.as_posix() - result, log = self._run_teardown( - 'fatal "late failure"', shares=("/workspace", "/opt/hostedtoolcache") + cache.mkdir(parents=True, exist_ok=True) + git("init", "-q") + git("config", "core.autocrlf", "false") + for path, (mode, contents) in files.items(): + blob = git("hash-object", "-w", "--stdin", data=contents) + git("update-index", "--add", "--cacheinfo", f"{mode},{blob},{path}") + return git("commit-tree", git("write-tree"), "-m", "aports fixture") + + def _symlinked_recipe_commit(self, cache: Path) -> str: + return self._aports_commit( + cache, + { + "main/foo/APKBUILD": ("100644", b"pkgname=foo\n"), + "main/foo/foo.post-install": ("100755", self.INSTALL_SCRIPT), + "main/foo/foo.post-upgrade": ("120000", b"foo.post-install"), + }, ) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertEqual( - log, - [ - f"umount {rootfs}/opt/hostedtoolcache", - f"umount {rootfs}/workspace", - "exit 125", - ], + + @staticmethod + def _package_manifest(root: Path, commit: str) -> Path: + manifest = root / "packages.json" + manifest.write_text( + json.dumps( + { + "guest": AlpineBuildConstants.GUEST_NAME, + "alpine_branch": AlpineBuildConstants.BRANCH, + "architecture": AlpineBuildConstants.ARCHITECTURE, + "packages": [ + { + "name": "foo", + "version": "1.0-r0", + "origin": "foo", + "license": "MIT", + "aports_commit": commit, + } + ], + } + ), + encoding="utf-8", ) + return manifest - def test_agent_skips_share_without_bootstrap_tokens(self): - result, log = self._run("console=hvc0 nvx_sandbox=1") - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("mountpoints=\n", result.stdout) - self.assertEqual(log, "") + @staticmethod + def _member( + name: str, + kind: bytes = tarfile.REGTYPE, + data: bytes = b"", + *, + linkname: str = "", + mode: int = 0o664, + ) -> tuple[tarfile.TarInfo, bytes]: + member = tarfile.TarInfo(name) + member.type = kind + member.linkname = linkname + member.mode = mode + member.size = len(data) if kind == tarfile.REGTYPE else 0 + return member, data + + @staticmethod + def _archive(*members: tuple[tarfile.TarInfo, bytes]) -> bytes: + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w") as archive: + for member, data in members: + archive.addfile(member, io.BytesIO(data) if member.isfile() else None) + return buffer.getvalue() + + def test_malformed_package_manifest_is_reported_as_source_error(self): + with tempfile.TemporaryDirectory() as temporary: + for contents in ("{", "[]"): + manifest = Path(temporary) / "packages.json" + manifest.write_text(contents, encoding="utf-8") + + with ( + self.subTest(contents=contents), + self.assertRaises(collect_alpine_sources.SourceError) as context, + ): + collect_alpine_sources._load_packages([manifest]) + + self.assertIn("package manifest", str(context.exception)) + + def test_recipe_collection_preserves_blob_line_endings(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + cache = root / "aports" + apkbuild = b"pkgname=foo\npkgver=1.0\npkgrel=0\n" + commit = self._aports_commit( + cache, + {"main/foo/APKBUILD": ("100644", apkbuild)}, + ) + subprocess.run( + ["git", "-C", str(cache), "config", "core.autocrlf", "true"], + check=True, + ) + output = root / "alpine" - def test_agent_mounts_share_inside_container_rootfs(self): - (self.rootfs / "opt").mkdir() - for mode in ("rw", "ro"): - with self.subTest(mode=mode): - result, log = self._run( - "nvx_sandbox=1 virtfs_dir=/opt/hostedtoolcache " - f"virtfs_tag=microvm virtfs_mode={mode}" - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertTrue((self.rootfs / "opt" / "hostedtoolcache").is_dir()) - self.assertIn( - f"NVX-SANDBOX-SHARE: target=/opt/hostedtoolcache mode={mode}", - result.stdout, - ) - self.assertEqual( - log.split(), - [ - "-t", - "virtiofs", - "-o", - f"{mode},nosuid,nodev", - "microvm", - f"{self.rootfs.as_posix()}/opt/hostedtoolcache", - ], + with ( + patch.object(collect_alpine_sources, "_prepare_aports"), + contextlib.redirect_stdout(io.StringIO()), + ): + collect_alpine_sources.collect_alpine_sources( + [self._package_manifest(root, commit)], + output, + cache, + skip_upstream=True, ) - self.assertIn( - f"mountpoints={self.rootfs.as_posix()}/opt/hostedtoolcache\n", - result.stdout, + + collected = output / "recipes" / commit / "main/foo/APKBUILD" + self.assertEqual(collected.read_bytes(), apkbuild) + + def test_recipe_symlink_is_collected_as_verified_regular_file(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + cache = root / "aports" + commit = self._symlinked_recipe_commit(cache) + output = root / "alpine" + + with ( + patch.object(collect_alpine_sources, "_prepare_aports"), + contextlib.redirect_stdout(io.StringIO()), + ): + collect_alpine_sources.collect_alpine_sources( + [self._package_manifest(root, commit)], + output, + cache, + skip_upstream=True, ) - def test_agent_binds_every_child_of_an_aggregate(self): - rootfs = self.rootfs.as_posix() - result, log = self._run( - "nvx_sandbox=1 " - + self._aggregate("0,/workspace,rw", "1,/opt/hostedtoolcache,ro") - ) - self.assertEqual(result.returncode, 0, result.stderr) - # The aggregate stays outside the container rootfs; each child is bound - # inside it with its own mode. - self.assertEqual( - log.splitlines(), - [ - f"-t virtiofs -o rw,nosuid,nodev microvm {self.runtime}/shares", - f"-o bind {self.runtime}/shares/0 {rootfs}/workspace", - f"-o remount,bind,rw,nosuid,nodev {rootfs}/workspace", - f"-o bind {self.runtime}/shares/1 {rootfs}/opt/hostedtoolcache", - f"-o remount,bind,ro,nosuid,nodev {rootfs}/opt/hostedtoolcache", - ], - ) - self.assertIn( - "NVX-SANDBOX-SHARE: target=/workspace mode=rw child=0\n" - "NVX-SANDBOX-SHARE: target=/opt/hostedtoolcache mode=ro child=1\n", - result.stdout, - ) - # Teardown unmounts the most recent bind first, and the aggregate last. - self.assertIn( - f"mountpoints={rootfs}/opt/hostedtoolcache {rootfs}/workspace " - f"{self.runtime}/shares\n", - result.stdout, - ) + upgrade = output / "recipes" / commit / "main/foo/foo.post-upgrade" + self.assertFalse(upgrade.is_symlink()) + self.assertEqual(upgrade.read_bytes(), self.INSTALL_SCRIPT) + source_manifest = json.loads( + (output / "manifest.json").read_text(encoding="utf-8") + ) + self.assertEqual( + source_manifest["format"], AlpineBuildConstants.SOURCE_MANIFEST_FORMAT + ) + self.assertEqual( + source_manifest["executables"], + [ + f"recipes/{commit}/main/foo/foo.post-install", + f"recipes/{commit}/main/foo/foo.post-upgrade", + ], + ) + self.assertIn( + f"recipes/{commit}/main/foo/foo.post-upgrade", + dict(common.verify_sha256_sums(output).files), + ) + if os.name == "posix": + self.assertEqual(upgrade.stat().st_mode & 0o777, 0o755) - def test_agent_binds_children_by_the_names_that_nvx_gives_them(self): - rootfs = self.rootfs.as_posix() - workspace = sandbox.aggregate_child_name(0, "/workspace") - tools = sandbox.aggregate_child_name(1, "/opt/tools") - result, log = self._run( - self._aggregate( - f"{workspace},/workspace,rw", f"{tools},/opt/tools,ro", "a.b_C-9,/x,ro" + def test_collection_replaces_stale_output_and_passes_release_validation(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + cache = root / "aports" + commit = self._symlinked_recipe_commit(cache) + manifest = self._package_manifest(root, commit) + source_dir = root / "sources" + output = source_dir / "alpine" + stale_files = ( + output / "recipes" / ("0" * 40) / "main/stale/APKBUILD", + output / "upstream" / ("0" * 40) / "main/stale/stale.tar.gz", ) - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn( - f"-o bind {self.runtime}/shares/{workspace} {rootfs}/workspace\n", log - ) - self.assertIn( - f"-o bind {self.runtime}/shares/{tools} {rootfs}/opt/tools\n", log - ) - self.assertIn(f"-o bind {self.runtime}/shares/a.b_C-9 {rootfs}/x\n", log) - self.assertIn( - f"NVX-SANDBOX-SHARE: target=/workspace mode=rw child={workspace}\n", - result.stdout, - ) - # A repeated name is refused, however the child is named. - result, log = self._run( - self._aggregate(f"{workspace},/workspace,rw", f"{workspace},/b,ro") - ) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertIn("child is not unique", result.stderr) - self.assertEqual(log, "") + for stale in stale_files: + stale.parent.mkdir(parents=True) + stale.write_bytes(b"stale") + (output / "manifest.json").write_text("{}", encoding="utf-8") + (output / "SHA256SUMS").write_text("", encoding="ascii") - def test_hostmount_accepts_only_child_names_inside_the_aggregate(self): - hostmount = self.AGENT.with_name("nvx-hostmount").read_text(encoding="utf-8") - script = ( - 'fail() { echo "FAIL: $*" >&2; exit 1; }\n' - f"{_shell_function(hostmount, 'parse_share')}" - 'parse_share "$1"\n' - 'printf "%s|%s|%s|%s\\n" "$share_child" "$share_target" "$share_mode" ' - '"$share_kind"\n' - ) + def fetch_upstream(bundle: Path, _alpine_version: str) -> None: + upstream = bundle / "upstream" / commit / "main" / "foo" + upstream.mkdir(parents=True) + (upstream / "foo-1.0.tar.gz").write_bytes(b"upstream") - def parse(share: str) -> subprocess.CompletedProcess[str]: - return subprocess.run( - [self.shell, "-s", "--", share], - input=script, - text=True, - capture_output=True, - check=False, - ) + with ( + patch.object(collect_alpine_sources, "_prepare_aports"), + patch.object( + collect_alpine_sources, + "_fetch_upstream_sources", + side_effect=fetch_upstream, + ), + patch.object(BuildConstants, "SOURCE_DIR", source_dir), + contextlib.redirect_stdout(io.StringIO()), + ): + collect_alpine_sources.collect_alpine_sources([manifest], output, cache) + executables = release._validate_alpine_sources([manifest]) - name = sandbox.aggregate_child_name(0, "/workspace") - for share, expected in ( - (f"{name},/workspace,rw", f"{name}|/workspace|rw|directory\n"), - ("0,/opt/tools,ro", "0|/opt/tools|ro|directory\n"), - ("a.b_C-9,/x,ro", "a.b_C-9|/x|ro|directory\n"), - # A file child is bound at a file. - ("1,/config/settings.json,ro,file", "1|/config/settings.json|ro|file\n"), - ("1,/results/output.txt,rw,file", "1|/results/output.txt|rw|file\n"), - ): - with self.subTest(share=share): - result = parse(share) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, expected) - for share in ( - ",/workspace,rw", - ".,/workspace,rw", - "..,/workspace,rw", - "a/b,/workspace,rw", - "a:b,/workspace,rw", - "a*,/workspace,rw", - "0,/workspace,file", - "0,/workspace,rw,dir", - ): - with self.subTest(share=share): - result = parse(share) - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("invalid live share", result.stderr) + self.assertEqual( + executables, + { + f"recipes/{commit}/main/foo/foo.post-install", + f"recipes/{commit}/main/foo/foo.post-upgrade", + }, + ) + for stale in stale_files: + self.assertFalse(stale.parent.parent.exists(), stale) + self.assertEqual( + sorted(path.name for path in output.iterdir()), + ["SHA256SUMS", "manifest.json", "recipes", "upstream"], + ) - def test_hostmount_makes_file_mount_points_without_following_links(self): - hostmount = self.AGENT.with_name("nvx-hostmount").read_text(encoding="utf-8") - script = ( - f"{_shell_function(hostmount, 'make_file_mount_point')}" - 'make_file_mount_point "$1"\n' + def test_recipe_extraction_materializes_symlinks_inside_recipe(self): + member = self._member + archive = self._archive( + member("main/", tarfile.DIRTYPE, mode=0o775), + member("main/foo/", tarfile.DIRTYPE, mode=0o775), + member("main/foo/patches/", tarfile.DIRTYPE, mode=0o775), + member("main/foo/APKBUILD", data=b"pkgname=foo\n"), + member("main/foo/foo.post-install", data=self.INSTALL_SCRIPT, mode=0o775), + member( + "main/foo/foo.post-upgrade", + tarfile.SYMTYPE, + linkname="foo.post-install", + ), + member( + "main/foo/foo.pre-upgrade", + tarfile.SYMTYPE, + linkname="./foo.post-upgrade", + ), + member("main/foo/patches/fix.patch", data=b"patch\n"), + member( + "main/foo/fix.patch", + tarfile.SYMTYPE, + linkname="patches/../patches/fix.patch", + ), + member( + "main/foo/patches/APKBUILD", tarfile.SYMTYPE, linkname="../APKBUILD" + ), ) - - def make(target: Path) -> int: - return subprocess.run( - [self.shell, "-s", "--", target.as_posix()], - input=script, - text=True, - capture_output=True, - check=False, - ).returncode - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - # A missing file is created with its directories, and an existing - # one is kept as it is. - created = root / "config" / "settings.json" - self.assertEqual(make(created), 0) - self.assertEqual(created.read_bytes(), b"") - existing = root / "existing.json" - existing.write_bytes(b"{}") - self.assertEqual(make(existing), 0) - self.assertEqual(existing.read_bytes(), b"{}") - (root / "directory").mkdir() - self.assertEqual(make(root / "directory"), 1) - if os.name == "nt": - return - # A link is refused before anything is created through it, whether - # or not its target exists. - dangling = root / "dangling.json" - dangling.symlink_to(root / "elsewhere.json") - self.assertEqual(make(dangling), 1) - self.assertFalse((root / "elsewhere.json").exists()) - linked = root / "linked.json" - linked.symlink_to(existing) - self.assertEqual(make(linked), 1) - self.assertEqual(existing.read_bytes(), b"{}") + destination = Path(temporary) - def test_agent_binds_a_file_child_at_a_file(self): - rootfs = self.rootfs.as_posix() - (self.rootfs / "opt").mkdir() - (self.rootfs / "opt" / "existing.json").write_text("{}", encoding="utf-8") - result, log = self._run( - "nvx_sandbox=1 " - + self._aggregate( - "0,/workspace,rw", - "1,/etc/app/settings.json,ro,file", - "2,/opt/existing.json,rw,file", + executables = collect_alpine_sources._safe_extract( + archive, destination, "main/foo" ) - ) - self.assertEqual(result.returncode, 0, result.stderr) - # Each file child gets a file mount point, created inside the container - # rootfs or reused there, and is bound with its own mode. - settings = self.rootfs / "etc" / "app" / "settings.json" - self.assertTrue(settings.is_file()) - self.assertEqual(settings.read_text(encoding="utf-8"), "") - self.assertEqual( - (self.rootfs / "opt" / "existing.json").read_text(encoding="utf-8"), "{}" - ) - self.assertTrue((self.rootfs / "workspace").is_dir()) - self.assertEqual( - log.splitlines(), - [ - f"-t virtiofs -o rw,nosuid,nodev microvm {self.runtime}/shares", - f"-o bind {self.runtime}/shares/0 {rootfs}/workspace", - f"-o remount,bind,rw,nosuid,nodev {rootfs}/workspace", - f"-o bind {self.runtime}/shares/1 {rootfs}/etc/app/settings.json", - f"-o remount,bind,ro,nosuid,nodev {rootfs}/etc/app/settings.json", - f"-o bind {self.runtime}/shares/2 {rootfs}/opt/existing.json", - f"-o remount,bind,rw,nosuid,nodev {rootfs}/opt/existing.json", - ], - ) - self.assertIn( - f"mountpoints={rootfs}/opt/existing.json {rootfs}/etc/app/settings.json " - f"{rootfs}/workspace {self.runtime}/shares\n", - result.stdout, - ) - # A directory is no file mount point, a file no directory one, and - # the runtime binds the machine ID itself. - (self.rootfs / "dir").mkdir() - (self.rootfs / "file").write_text("x", encoding="utf-8") - for child, message in ( - ("0,/dir,ro,file", "not a regular file"), - ("0,/file/settings.json,ro,file", "not a directory"), - ("0,/file,ro", "not a directory"), - ("0,/etc/machine-id,ro,file", "reserved"), - ("0,/etc/machine-id/child,ro", "reserved"), - ): - with self.subTest(child=child): - result, log = self._run(self._aggregate(child)) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertIn(message, result.stderr) - self.assertEqual(log, "") + self.assertEqual( + executables, + [ + "main/foo/foo.post-install", + "main/foo/foo.post-upgrade", + "main/foo/foo.pre-upgrade", + ], + ) + recipe = destination / "main" / "foo" + expected = { + "APKBUILD": (b"pkgname=foo\n", 0o644), + "foo.post-install": (self.INSTALL_SCRIPT, 0o755), + "foo.post-upgrade": (self.INSTALL_SCRIPT, 0o755), + "foo.pre-upgrade": (self.INSTALL_SCRIPT, 0o755), + "fix.patch": (b"patch\n", 0o644), + "patches/fix.patch": (b"patch\n", 0o644), + "patches/APKBUILD": (b"pkgname=foo\n", 0o644), + } + for relative, (contents, mode) in expected.items(): + with self.subTest(path=relative): + path = recipe / relative + self.assertFalse(path.is_symlink()) + self.assertEqual(path.read_bytes(), contents) + if os.name == "posix": + self.assertEqual(path.stat().st_mode & 0o777, mode) - def test_agent_rejects_repeated_children_and_overlapping_targets(self): - for children, message in ( - (("0,/workspace,rw", "0,/opt/tools,ro"), "child is not unique"), - (("0,/workspace,rw", "1,/workspace,ro"), "targets overlap"), - (("0,/workspace,rw", "1,/workspace/cache,ro"), "targets overlap"), - (("0,/opt/tools/node,ro", "1,/opt,rw"), "targets overlap"), - ): - with self.subTest(children=children): - result, log = self._run(self._aggregate(*children)) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertIn(message, result.stderr) - # Every child is checked before anything is mounted. - self.assertEqual(log, "") + def test_recipe_extraction_accepts_implicit_directories(self): + member = self._member + archive = self._archive( + member("main/foo/APKBUILD", data=b"pkgname=foo\n"), + member("main/foo/scripts/install", data=self.INSTALL_SCRIPT, mode=0o775), + member( + "main/foo/foo.post-install", + tarfile.SYMTYPE, + linkname="scripts/install", + ), + member( + "main/foo/scripts/APKBUILD", tarfile.SYMTYPE, linkname="../APKBUILD" + ), + ) + with tempfile.TemporaryDirectory() as temporary: + destination = Path(temporary) - # A shared prefix that is not a path component is not an overlap. - result, log = self._run(self._aggregate("0,/work,rw", "1,/workspace,ro")) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(len(log.splitlines()), 5) + executables = collect_alpine_sources._safe_extract( + archive, destination, "main/foo" + ) - def test_agent_parses_every_share_before_mounting_any(self): - for cmdline in ( - "virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rw " - "virtfs_dir=/opt/tools", - "virtfs_tag=microvm virtfs_mode=ro", - "virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rw " - "virtfs_dir= virtfs_tag=microvm virtfs_mode=ro", - "virtfs_aggregate=1", - "nvx_share=0,/workspace,rw", - ): - with self.subTest(cmdline=cmdline): - result, log = self._run(cmdline) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertIn("bootstrap is incomplete", result.stderr) - self.assertEqual(log, "") + self.assertEqual( + executables, + ["main/foo/foo.post-install", "main/foo/scripts/install"], + ) + recipe = destination / "main" / "foo" + for relative, contents in ( + ("foo.post-install", self.INSTALL_SCRIPT), + ("scripts/APKBUILD", b"pkgname=foo\n"), + ): + with self.subTest(path=relative): + path = recipe / relative + self.assertFalse(path.is_symlink()) + self.assertEqual(path.read_bytes(), contents) + if os.name == "posix": + for directory in (destination / "main", recipe, recipe / "scripts"): + with self.subTest(directory=directory.name): + self.assertEqual(directory.stat().st_mode & 0o777, 0o755) - def test_agent_fatal_unmounts_mounted_shares(self): - mount_log = self.root / "mount.log" - functions = "".join( - _shell_function(self.source, name) - for name in ("unmount_live_shares", "fatal", *self.SHARE_FUNCTIONS) - ).replace("/sbin/nvx-exit", "nvx_exit") - result = subprocess.run( - [ - self.shell, - "-s", - "--", - self.rootfs.as_posix(), - self._aggregate("0,/workspace,rw", "1,/opt/tools,ro"), - mount_log.as_posix(), - self.runtime, - ], - input=( - "set -eu\n" - "rootfs=$1\ncmdline=$2\nmount_log=$3\nruntime=$4\n" - "share_mountpoints=\n" - 'mount() { printf "mount %s\\n" "$*" >>"$mount_log"; ' - 'case "$*" in *shares/1*) return 1 ;; esac; }\n' - 'umount() { printf "umount %s\\n" "$1" >>"$mount_log"; }\n' - 'nvx_exit() { printf "exit %s\\n" "$1" >>"$mount_log"; exit "$1"; }\n' - f"{functions}mount_live_shares\n" + def test_recipe_extraction_rejects_unsafe_members(self): + member = self._member + recipe = ( + member("main/", tarfile.DIRTYPE), + member("main/foo/", tarfile.DIRTYPE), + member("main/foo/sub/", tarfile.DIRTYPE), + member("main/foo/APKBUILD", data=b"pkgname=foo\n"), + member("main/foo/sub/file", data=b"file\n"), + ) + cases: dict[str, tuple[tuple[tarfile.TarInfo, bytes], ...]] = { + "absolute path": (member("/main/foo/file"),), + "parent path": (member("main/foo/../../file"),), + "outside recipe": (member("main/bar/file"),), + "duplicate path": (member("main/foo/APKBUILD"),), + "member below a file": (member("main/foo/APKBUILD/file"),), + "member below a symlink": ( + member("main/foo/alias", tarfile.SYMTYPE, linkname="sub"), + member("main/foo/alias/file"), + ), + "hard link": ( + member("main/foo/link", tarfile.LNKTYPE, linkname="main/foo/APKBUILD"), + ), + "fifo": (member("main/foo/pipe", tarfile.FIFOTYPE),), + "character device": (member("main/foo/device", tarfile.CHRTYPE),), + "absolute symlink": ( + member("main/foo/link", tarfile.SYMTYPE, linkname="/etc/passwd"), + ), + "escaping symlink": ( + member("main/foo/link", tarfile.SYMTYPE, linkname="../../../passwd"), + ), + "sibling recipe symlink": ( + member("main/foo/link", tarfile.SYMTYPE, linkname="../bar/APKBUILD"), + ), + "dangling symlink": ( + member("main/foo/link", tarfile.SYMTYPE, linkname="missing"), + ), + "symlink through a missing directory": ( + member( + "main/foo/link", + tarfile.SYMTYPE, + linkname="missing/../APKBUILD", + ), + ), + "directory symlink": ( + member("main/foo/link", tarfile.SYMTYPE, linkname="sub"), ), - text=True, - capture_output=True, - check=False, - ) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertIn("failed to bind live share 1", result.stderr) - rootfs = self.rootfs.as_posix() - self.assertEqual( - mount_log.read_text(encoding="utf-8").splitlines(), - [ - f"mount -t virtiofs -o rw,nosuid,nodev microvm {self.runtime}/shares", - f"mount -o bind {self.runtime}/shares/0 {rootfs}/workspace", - f"mount -o remount,bind,rw,nosuid,nodev {rootfs}/workspace", - f"mount -o bind {self.runtime}/shares/1 {rootfs}/opt/tools", - f"umount {rootfs}/workspace", - f"umount {self.runtime}/shares", - "exit 125", - ], - ) - - def test_agent_fails_closed_for_invalid_share_bootstrap(self): - (self.rootfs / "file").write_text("x", encoding="utf-8") - for cmdline, message in ( - ("virtfs_dir=/workspace", "incomplete"), - ("virtfs_dir=/workspace virtfs_tag=microvm", "incomplete"), - ("virtfs_dir=/workspace virtfs_tag=bad/tag virtfs_mode=rw", "tag"), - ("virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rx", "mode"), - ("virtfs_dir=workspace virtfs_tag=microvm virtfs_mode=rw", "absolute"), - ("virtfs_dir=/ virtfs_tag=microvm virtfs_mode=rw", "canonical"), - ("virtfs_dir=/a//b virtfs_tag=microvm virtfs_mode=rw", "canonical"), - ("virtfs_dir=/a/ virtfs_tag=microvm virtfs_mode=rw", "canonical"), - ("virtfs_dir=/a/./b virtfs_tag=microvm virtfs_mode=rw", "canonical"), - ("virtfs_dir=/a/../b virtfs_tag=microvm virtfs_mode=rw", "canonical"), - ("virtfs_dir=/a=b virtfs_tag=microvm virtfs_mode=rw", "reserved char"), - ("virtfs_dir=/proc virtfs_tag=microvm virtfs_mode=rw", "reserved"), - ("virtfs_dir=/sys/fs virtfs_tag=microvm virtfs_mode=rw", "reserved"), - ("virtfs_dir=/dev/shm virtfs_tag=microvm virtfs_mode=rw", "reserved"), - ("virtfs_dir=/.nvx-agent virtfs_tag=microvm virtfs_mode=ro", "reserved"), - ("virtfs_dir=/etc virtfs_tag=microvm virtfs_mode=ro", "reserved"), - ("virtfs_dir=/file virtfs_tag=microvm virtfs_mode=ro", "not a directory"), - ( - "virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rw " - "nvx_share=0,/workspace,rw", - "require an aggregate", + "symlink loop": ( + member("main/foo/first", tarfile.SYMTYPE, linkname="second"), + member("main/foo/second", tarfile.SYMTYPE, linkname="first"), ), - ( - "virtfs_dir=/shares virtfs_tag=microvm virtfs_mode=rw " - "virtfs_aggregate=1 nvx_share=0,/workspace,rw", - "must be mounted at", + "symlinked directory traversal": ( + member("main/foo/link", tarfile.SYMTYPE, linkname="alias/file"), + member("main/foo/alias", tarfile.SYMTYPE, linkname="sub"), ), - (self._aggregate(), "has no children"), - (self._aggregate(",/workspace,rw"), "child is invalid"), - (self._aggregate(".,/workspace,rw"), "child is invalid"), - (self._aggregate("..,/workspace,rw"), "child is invalid"), - (self._aggregate("a/b,/workspace,rw"), "child is invalid"), - (self._aggregate("a:b,/workspace,rw"), "child is invalid"), - (self._aggregate("a*,/workspace,rw"), "child is invalid"), - (self._aggregate("0,/workspace"), "child is invalid"), - (self._aggregate("0,/workspace,rx"), "mode"), - (self._aggregate("0,workspace,rw"), "absolute"), - (self._aggregate("0,/a,b,rw"), "reserved char"), - (self._aggregate("0,/workspace,rw", "1,/proc,ro"), "reserved"), - (self._aggregate("0,/file,ro"), "not a directory"), - ): - with self.subTest(cmdline=cmdline): - result, log = self._run(cmdline) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertIn("live share", result.stderr) - self.assertIn(message, result.stderr) - self.assertEqual(log, "") + } + for name, members in cases.items(): + with self.subTest(case=name), tempfile.TemporaryDirectory() as temporary: + destination = Path(temporary) / "recipes" - def test_agent_rejects_symbolic_link_in_share_target(self): - outside = self.root / "outside" - outside.mkdir() - try: - (self.rootfs / "workspace").symlink_to(outside, target_is_directory=True) - except OSError as error: - self.skipTest(f"symbolic links are unavailable: {error}") - result, log = self._run( - "virtfs_dir=/workspace/project virtfs_tag=microvm virtfs_mode=rw" - ) - self.assertEqual(result.returncode, 125, result.stdout) - self.assertIn("crosses a symbolic link", result.stderr) - self.assertEqual(log, "") - self.assertFalse((outside / "project").exists()) + with self.assertRaises(collect_alpine_sources.SourceError): + collect_alpine_sources._safe_extract( + self._archive(*recipe, *members), + destination, + "main/foo", + ) + self.assertEqual(list(destination.iterdir()), []) -@unittest.skipIf(os.name == "nt", "requires POSIX symbolic links") -class SandboxSmokeShareTests(unittest.TestCase): - SMOKE = Path(__file__).parents[1] / "guest" / "common" / "nvx-sandbox-smoke" + def test_collection_guards_output_before_replacing_it(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + manifest = self._package_manifest(root, "0" * 40) + output = root / "alpine" + (output / "recipes").mkdir(parents=True) + notes = output / "notes.txt" + notes.write_text("keep", encoding="utf-8") + not_directory = root / "file" + not_directory.write_text("keep", encoding="utf-8") - def setUp(self): - shell = _posix_shell() - if shell is None: - self.skipTest("POSIX shell is unavailable") - self.shell = shell - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - self.root = Path(temporary.name) - self.share = self.root / "share" - self.links = self.share / "nvx-links" - self.links.mkdir(parents=True) - (self.share / "nvx-host-marker").write_text("host-to-guest\n", encoding="utf-8") - # The guest resolves nvx-links/outside to this path through ../.. - self.outside = self.root / "nvx-outside-marker" - source = self.SMOKE.read_text(encoding="utf-8") - self.functions = "".join( - _shell_function(source, name) - for name in ( - "check_share", - "check_share_ownership", - "check_share_denied", - "expect_eperm", - "check_share_policy", - "expect_erofs", - "check_share_symlinks", - "check_shares_isolated", + for target, message in ( + (output, "unexpected entries: notes.txt"), + (not_directory, "is not a directory"), + ): + with ( + self.subTest(output=target.name), + patch.object(collect_alpine_sources, "_prepare_aports") as prepare, + self.assertRaisesRegex( + collect_alpine_sources.SourceError, + message, + ), + ): + collect_alpine_sources.collect_alpine_sources( + [manifest], + target, + root / "aports", + skip_upstream=True, + ) + prepare.assert_not_called() + self.assertTrue(notes.is_file()) + self.assertTrue((output / "recipes").is_dir()) + + notes.unlink() + with ( + patch.object(collect_alpine_sources, "_prepare_aports"), + patch.object( + collect_alpine_sources.shutil, + "rmtree", + side_effect=PermissionError(13, "Permission denied"), + ), + self.assertRaisesRegex( + collect_alpine_sources.SourceError, + "cannot remove previous Alpine source output.*remove it manually", + ), + ): + collect_alpine_sources.collect_alpine_sources( + [manifest], + output, + root / "aports", + skip_upstream=True, + ) + + def test_collection_refuses_symlinked_output(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + output = root / "alpine" + (output / "recipes").mkdir(parents=True) + link = root / "alpine-link" + try: + link.symlink_to(output, target_is_directory=True) + except OSError as error: + self.skipTest(f"symlinks are unavailable: {error}") + + with ( + patch.object(collect_alpine_sources, "_prepare_aports") as prepare, + self.assertRaisesRegex( + collect_alpine_sources.SourceError, + "Alpine source output is not a directory", + ), + ): + collect_alpine_sources.collect_alpine_sources( + [self._package_manifest(root, "0" * 40)], + link, + root / "aports", + skip_upstream=True, + ) + + prepare.assert_not_called() + self.assertTrue((output / "recipes").is_dir()) + + def test_collection_refuses_output_junctions(self): + if sys.platform != "win32": + self.skipTest("directory junctions exist only on Windows") + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + manifest = self._package_manifest(root, "0" * 40) + elsewhere = root / "elsewhere" + (elsewhere / "recipes").mkdir(parents=True) + kept = elsewhere / "recipes" / "kept" + kept.write_bytes(b"kept") + output = root / "alpine" + output.mkdir() + cases = ( + ( + root / "junction", + elsewhere, + root / "junction", + "Alpine source output is not a directory", + ), + ( + output / "recipes", + elsewhere / "recipes", + output, + "Alpine source generated path is not a directory", + ), ) - ).replace("/tmp/nvx-tool", '"$scratch/nvx-tool"') + for link, target, collected, message in cases: + subprocess.run( + ["cmd", "/c", "mklink", "/J", str(link), str(target)], + capture_output=True, + check=True, + ) + with ( + self.subTest(link=link.name), + patch.object(collect_alpine_sources, "_prepare_aports"), + self.assertRaisesRegex(collect_alpine_sources.SourceError, message), + ): + collect_alpine_sources.collect_alpine_sources( + [manifest], + collected, + root / "aports", + skip_upstream=True, + ) + self.assertEqual(kept.read_bytes(), b"kept") + + def test_upstream_fetch_uses_mirror_and_restores_bundle_ownership(self): + with ( + tempfile.TemporaryDirectory() as temporary, + patch.object(collect_alpine_sources, "_run") as run, + ): + collect_alpine_sources._fetch_upstream_sources(Path(temporary), "3.24") - def _run(self, call: str) -> subprocess.CompletedProcess[str]: - scratch = self.root / "scratch" - scratch.mkdir(exist_ok=True) - return subprocess.run( - [self.shell, "-s", "--", self.share.as_posix(), scratch.as_posix()], - input=f"set -eu\nshare=$1\nscratch=$2\n{self.functions}{call}\n", - text=True, - capture_output=True, - check=False, + command = run.call_args.args[0] + self.assertEqual(command[:4], ["docker", "run", "--rm", "--volume"]) + self.assertEqual( + command[5:7], + [ + "--env", + "DISTFILES_MIRROR=https://distfiles.alpinelinux.org/distfiles/v3.24", + ], + ) + self.assertEqual(command[7], "alpine:3.24") + script = command[-1] + self.assertIn("owner=$(stat -c '%u:%g' /bundle)", script) + self.assertIn('chown -R "$owner" /bundle/upstream', script) + self.assertLess( + script.index("trap restore_owner EXIT"), + script.index("mkdir -p /bundle/upstream"), ) - def test_rw_share_creates_links_with_exact_targets(self): - result = self._run('check_share "$share" rw') - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn( - f"NVX-UBUNTU-SANDBOX-SYMLINK-OK target={self.share.as_posix()}", - result.stdout, +class BuildTests(unittest.TestCase): + def test_build_config_owns_standard_runtime_paths(self): + config = build_config.BuildConfig() + alpine = config.initramfs_config("alpine") + ubuntu_config = config.initramfs_config("ubuntu") + distro = config.distro_layer_config() + + self.assertIsInstance(config.docker, build_config.DockerBuildConfig) + self.assertIsInstance(config.kernel, build_config.KernelBuildConfig) + self.assertIsInstance(config.openvmm, build_config.OpenVmmBuildConfig) + self.assertIsInstance(alpine, build_config.InitramfsBuildConfig) + self.assertIsInstance(ubuntu_config, build_config.InitramfsBuildConfig) + self.assertIsInstance(distro, build_config.DistroLayerBuildConfig) + self.assertIsNone(config.openvmm.backend) + self.assertEqual(config.selected_guests(), ("alpine",)) + self.assertEqual(config.kernel.work, BuildConstants.BUILD_DIR / "linux") + self.assertEqual(config.kernel.output, BuildConstants.BUILD_DIR / "vmlinux") + self.assertEqual( + alpine.work, + BuildConstants.BUILD_DIR / "initramfs-alpine-work", ) - self.assertIn("NVX-UBUNTU-SANDBOX-SHARE-OK", result.stdout) self.assertEqual( - {name: os.readlink(self.links / name) for name in os.listdir(self.links)}, - { - "nvx-tool": "../nvx-tool", - "absolute": "/nvx/absolute/target", - "outside": "../../nvx-outside-marker", - "denied": "../nvx-denied/secret", - "denied-directory": "../nvx-denied", - }, + alpine.output, + BuildConstants.BUILD_DIR / "initramfs.cpio.gz", ) self.assertEqual( - (self.share / "nvx-guest-marker").read_text(encoding="utf-8"), - "guest-to-host\n", + ubuntu_config.output, + BuildConstants.BUILD_DIR / "initramfs-ubuntu.cpio.gz", + ) + self.assertEqual( + distro.output, + BuildConstants.BUILD_DIR / "ubuntu-distro.erofs", + ) + self.assertEqual(config.docker.artifact_destination, BuildConstants.BUILD_DIR) + self.assertEqual( + config.docker.linux_source_destination, + BuildConstants.SOURCE_DIR / "linux", ) - self.assertTrue((self.share / "nvx-guest-directory").is_dir()) - def test_links_fail_when_they_expose_host_data(self): - (self.share / "nvx-guest-marker").write_text( - "guest-to-host\n", encoding="utf-8" + def test_detects_openvmm_build_platform_without_runtime_devices(self): + cases: tuple[ + tuple[ + str, + build_config.OpenVmmBackend | None, + build_config.OpenVmmPlatform, + str, + build_config.OpenVmmBuildMode, + ], + ..., + ] = ( + ( + "win32", + None, + "windows-msvc", + "x86_64-pc-windows-msvc", + "native", + ), + ( + "win32", + "whp", + "windows-msvc", + "x86_64-pc-windows-msvc", + "native", + ), + ( + "linux", + None, + "linux-gnu", + "x86_64-unknown-linux-gnu", + "native", + ), + ( + "linux", + "kvm", + "linux-gnu", + "x86_64-unknown-linux-gnu", + "native", + ), + ( + "linux", + "mshv", + "linux-musl", + "x86_64-unknown-linux-musl", + "musl", + ), ) - self.outside.write_text("host-outside\n", encoding="utf-8") - result = self._run('check_share_symlinks "$share"') - self.assertNotEqual(result.returncode, 0, result.stdout) - self.assertNotIn("NVX-UBUNTU-SANDBOX-SYMLINK-OK", result.stdout) + for host, backend, expected_platform, expected_target, expected_mode in cases: + with self.subTest(host=host, backend=backend): + with ( + patch.object(build.sys, "platform", host), + patch.object(build.os, "access") as access, + ): + config = build_config.OpenVmmBuildConfig() + platform = build.detect_openvmm_platform(backend) - for path in self.links.iterdir(): - path.unlink() - self.outside.unlink() - denied = self.share / "nvx-denied" - denied.mkdir() - (denied / "secret").write_text("secret\n", encoding="utf-8") - result = self._run('check_share_symlinks "$share"') - self.assertNotEqual(result.returncode, 0, result.stdout) - self.assertNotIn("NVX-UBUNTU-SANDBOX-SYMLINK-OK", result.stdout) + self.assertEqual(platform, expected_platform) + self.assertEqual(config.openvmm_target(platform), expected_target) + self.assertEqual(config.openvmm_build_mode(platform), expected_mode) + access.assert_not_called() - def test_ro_share_rejects_links(self): - geteuid = getattr(os, "geteuid", None) - if geteuid is not None and geteuid() == 0: - self.skipTest("root ignores directory write permissions") - self.share.chmod(0o555) - self.addCleanup(self.share.chmod, 0o755) + def test_rejects_unsupported_openvmm_build_platforms(self): + cases: tuple[tuple[str, build_config.OpenVmmBackend | None], ...] = ( + ("linux", "whp"), + ("win32", "kvm"), + ("win32", "mshv"), + ("darwin", None), + ("darwin", "kvm"), + ("darwin", "mshv"), + ("darwin", "whp"), + ) + for host, backend in cases: + with self.subTest(host=host, backend=backend): + with ( + patch.object(build.sys, "platform", host), + patch.object(build.os, "access") as access, + self.assertRaisesRegex( + common.ScriptError, + "unsupported", + ), + ): + build.detect_openvmm_platform(backend) + access.assert_not_called() - result = self._run('check_share "$share" ro') - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("NVX-UBUNTU-SANDBOX-SHARE-OK", result.stdout) - self.assertFalse(os.path.lexists(self.share / "nvx-guest-link")) + def test_linux_openvmm_builds_do_not_require_hypervisor_devices(self): + for devices_usable in (False, True): + for combined in (False, True): + with ( + self.subTest(devices_usable=devices_usable, combined=combined), + tempfile.TemporaryDirectory() as temporary, + ): + openvmm_dir = Path(temporary) / "openvmm" + output = openvmm_dir / "target" / "release" / "openvmm" + output.parent.mkdir(parents=True) + output.write_bytes(b"openvmm") + (openvmm_dir / "Cargo.toml").touch() + config = build_config.OpenVmmBuildConfig( + skip_restore=True, + directory=openvmm_dir, + output=output, + ) - self.share.chmod(0o755) - result = self._run('check_share "$share" ro') - self.assertEqual(result.returncode, 1, result.stdout) + with ( + patch.object(build.sys, "platform", "linux"), + patch.object( + build.os, "access", return_value=devices_usable + ) as access, + patch.object(build, "run_checked") as run_checked, + patch.object(build, "record_openvmm_provenance"), + patch.object(build, "build_guest") as guest, + ): + if combined: + aggregate = build_config.BuildConfig(openvmm=config) + build.build_all(aggregate) + guest.assert_called_once_with(aggregate) + else: + build.build_openvmm(config) + guest.assert_not_called() + + access.assert_not_called() + run_checked.assert_called_once_with( + [ + "cargo", + "build", + "--release", + "-p", + "openvmm", + "--bin", + "openvmm", + ], + cwd=openvmm_dir, + ) + + def test_openvmm_target_output_matches_build_mode(self): + config = build_config.OpenVmmBuildConfig(directory=Path("checkout")) + cases: tuple[tuple[build_config.OpenVmmPlatform, Path], ...] = ( + ("linux-gnu", Path("target") / "release" / "openvmm"), + ( + "linux-musl", + Path("target") / "x86_64-unknown-linux-musl" / "release" / "openvmm", + ), + ("windows-msvc", Path("target") / "release" / "openvmm.exe"), + ) + for platform, relative_output in cases: + with self.subTest(platform=platform): + self.assertEqual( + config.openvmm_target_output(platform), + config.directory / relative_output, + ) + + def test_native_openvmm_build_normalizes_output_before_recording_provenance(self): + platforms: tuple[tuple[build_config.OpenVmmPlatform, str], ...] = ( + ("linux-gnu", "openvmm"), + ("windows-msvc", "openvmm.exe"), + ) + for platform, executable in platforms: + for existing_output in (False, True): + with ( + self.subTest(platform=platform, existing_output=existing_output), + tempfile.TemporaryDirectory() as temporary, + ): + root = Path(temporary) + openvmm_dir = root / "openvmm" + native_output = openvmm_dir / "target" / "release" / executable + native_output.parent.mkdir(parents=True) + binary = b"newly-built-openvmm" + native_output.write_bytes(binary) + (openvmm_dir / "Cargo.toml").touch() + output = root / "artifacts" / executable + if existing_output: + output.parent.mkdir(parents=True) + output.write_bytes(b"stale-openvmm") + config = build_config.OpenVmmBuildConfig( + skip_restore=True, + build_directory=root / "build", + directory=openvmm_dir, + output=output, + ) + revision = b"0bc357bbcf3a654b63dfb51f1103c5751bf3d31f\n" + results = [ + common.CommandResult(("git",), 0, revision, b""), + common.CommandResult(("git",), 0, revision, b""), + common.CommandResult(("git",), 0, b"", b""), + ] - def test_policy_share_requires_openvmm_to_refuse_writes(self): - (self.share / "nvx-writable").mkdir() - allowed = self.share / "nvx-denied" / "nvx-allowed" - allowed.mkdir(parents=True) - (allowed / "payload").write_text("payload\n", encoding="utf-8") + with ( + patch.object(build, "run_checked"), + patch.object(common, "run_capture", side_effect=results), + ): + build.build_openvmm(config, platform=platform) - # Only OpenVMM's access policy can make the share refuse writes - # outside nvx-writable, so a plain directory fails the checks. - result = self._run('check_share "$share" policy') - self.assertEqual(result.returncode, 1, result.stdout) - self.assertNotIn("NVX-UBUNTU-SANDBOX-POLICY-OK", result.stdout) + self.assertEqual(output.read_bytes(), binary) + provenance = json.loads( + ( + config.build_directory + / OpenVMMBuildConstants.PROVENANCE_NAME + ).read_text(encoding="utf-8") + ) + self.assertEqual( + provenance["executable_sha256"], + hashlib.sha256(binary).hexdigest(), + ) - # A refusal other than EROFS, such as a permission check, fails too. - geteuid = getattr(os, "geteuid", None) - if geteuid is not None and geteuid() == 0: - return - (self.share / "nvx-guest-marker").unlink(missing_ok=True) - self.share.chmod(0o555) - self.addCleanup(self.share.chmod, 0o755) - result = self._run('check_share "$share" policy') - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("unexpected share error", result.stderr) - self.assertIn("Permission denied", result.stderr) + def test_native_openvmm_build_restores_and_records_provenance(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + openvmm_dir = root / "openvmm" + output = openvmm_dir / "target" / "release" / "openvmm" + output.parent.mkdir(parents=True) + output.write_bytes(b"openvmm") + (openvmm_dir / "Cargo.toml").touch() + config = build_config.OpenVmmBuildConfig( + build_directory=root / "build", + directory=openvmm_dir, + output=output, + ) - def test_rw_share_link_cannot_write_into_ro_share(self): - geteuid = getattr(os, "geteuid", None) - if geteuid is not None and geteuid() == 0: - self.skipTest("root ignores file write permissions") - toolcache = self.root / "toolcache" - toolcache.mkdir() - marker = toolcache / "nvx-host-marker" - marker.write_text("host-to-guest\n", encoding="utf-8") - marker.chmod(0o444) - self.addCleanup(marker.chmod, 0o644) + with ( + patch.object(build, "run_checked") as run_checked, + patch.object(build, "record_openvmm_provenance") as provenance, + ): + build.build_openvmm(config, platform="linux-gnu") - call = f'check_shares_isolated "$share" "{toolcache.as_posix()}"' - result = self._run(call) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn( - f"NVX-UBUNTU-SANDBOX-SHARES-ISOLATED-OK rw={self.share.as_posix()} " - f"ro={toolcache.as_posix()}", - result.stdout, - ) - self.assertFalse(os.path.lexists(self.share / "nvx-cross-share-link")) - self.assertEqual(marker.read_text(encoding="utf-8"), "host-to-guest\n") + self.assertEqual( + run_checked.call_args_list, + [ + call( + [ + "cargo", + "xflowey", + "restore-packages", + "--no-compat-igvm", + ], + cwd=openvmm_dir, + ), + call( + [ + "cargo", + "build", + "--release", + "-p", + "openvmm", + "--bin", + "openvmm", + ], + cwd=openvmm_dir, + ), + ], + ) + provenance.assert_called_once_with(config) - # A write that reaches the read-only share fails the check. - marker.chmod(0o644) - result = self._run(call) - self.assertEqual(result.returncode, 1, result.stdout) - self.assertNotIn("NVX-UBUNTU-SANDBOX-SHARES-ISOLATED-OK", result.stdout) + def test_openvmm_build_honors_skip_restore(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + openvmm_dir = root / "openvmm" + output = openvmm_dir / "target" / "release" / "openvmm" + output.parent.mkdir(parents=True) + output.write_bytes(b"openvmm") + (openvmm_dir / "Cargo.toml").touch() + config = build_config.OpenVmmBuildConfig( + skip_restore=True, + build_directory=root / "build", + directory=openvmm_dir, + output=output, + ) - def test_caller_share_owns_and_populates_its_own_directories(self): - if sys.platform != "linux": - self.skipTest("ownership checks use GNU stat and Linux identities") - result = self._run('check_share "$share" caller') + with ( + patch.object(build, "run_checked") as run_checked, + patch.object(build, "record_openvmm_provenance"), + ): + build.build_openvmm(config, platform="linux-gnu") - self.assertEqual(result.returncode, 0, result.stderr) - share = self.share.as_posix() - self.assertIn( - f"NVX-UBUNTU-SANDBOX-OWNER-OK target={share} " - f"identity={os.getuid()}:{os.getgid()}", - result.stdout, - ) - self.assertIn(f"NVX-UBUNTU-SANDBOX-SYMLINK-OK target={share}", result.stdout) - self.assertIn( - f"NVX-UBUNTU-SANDBOX-SHARE-OK target={share} mode=caller", result.stdout - ) - links = self.share / "nvx-caller-links" - self.assertEqual( - {name: os.readlink(links / name) for name in os.listdir(links)}, - { - "nvx-tool": "../nvx-tool", - "absolute": "/nvx/absolute/target", - "outside": "../../nvx-outside-marker", - "denied": "../nvx-denied/secret", - "denied-directory": "../nvx-denied", - }, - ) - self.assertEqual(os.listdir(self.links), []) - renamed = self.share / "nvx-guest-directory" / "nested" / "renamed" - self.assertEqual(renamed.read_text(encoding="utf-8"), "nested\n") + self.assertEqual(run_checked.call_count, 1) + self.assertEqual(run_checked.call_args.args[0][:2], ["cargo", "build"]) - def test_eperm_share_requires_reads_to_fail_with_eperm_and_writes_to_fail(self): - result = self._run('check_share "$share" eperm') - self.assertNotEqual(result.returncode, 0, result.stdout) - self.assertNotIn("NVX-UBUNTU-SANDBOX-SHARE-OK", result.stdout) + def test_musl_openvmm_build_sets_sysroot_and_normalizes_output(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + openvmm_dir = root / "openvmm" + (openvmm_dir / "Cargo.toml").parent.mkdir(parents=True) + (openvmm_dir / "Cargo.toml").touch() + sysroot = openvmm_dir / ".packages" / "extracted" / "x86_64-sysroot" + (sysroot / "lib").mkdir(parents=True) + (sysroot / "lib" / "libsymcrypt.a").touch() + output = openvmm_dir / "target" / "release" / "openvmm" + config = build_config.OpenVmmBuildConfig( + build_directory=root / "build", + directory=openvmm_dir, + output=output, + backend="mshv", + ) + target_output = config.openvmm_target_output("linux-musl") + target_output.parent.mkdir(parents=True) + target_output.write_bytes(b"musl-openvmm") - geteuid = getattr(os, "geteuid", None) - if geteuid is not None and geteuid() == 0: - self.skipTest("root ignores directory write permissions") - # Writes need only fail, so a read-only host directory stands in for - # the host's refusal. - self.share.chmod(0o555) - self.addCleanup(self.share.chmod, 0o755) - denied = "".join( - f'{command}() {{ echo "{command}: $1: Operation not permitted" >&2; ' - "return 1; }\n" - for command in ("cat", "ls") - ) - result = self._run(denied + 'check_share "$share" eperm') - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn( - f"NVX-UBUNTU-SANDBOX-SHARE-OK target={self.share.as_posix()} mode=eperm", - result.stdout, - ) - self.assertFalse((self.share / "nvx-guest-marker").exists()) + with ( + patch.object(build.sys, "platform", "linux"), + patch.object(build.os, "access", return_value=False) as access, + patch.object(build, "run_checked") as run_checked, + patch.object(build, "record_openvmm_provenance") as provenance, + patch.object(Path, "chmod") as chmod, + ): + build.build_openvmm(config) - other_error = denied.replace("Operation not permitted", "Permission denied") - result = self._run(other_error + 'check_share "$share" eperm') - self.assertNotEqual(result.returncode, 0, result.stdout) - self.assertIn("unexpected share error", result.stderr) + access.assert_not_called() + self.assertEqual(run_checked.call_count, 3) + self.assertEqual( + run_checked.call_args_list[1], + call(["rustup", "target", "add", "x86_64-unknown-linux-musl"]), + ) + cargo_call = run_checked.call_args_list[2] + self.assertIn("x86_64-unknown-linux-musl", cargo_call.args[0]) + environment = cargo_call.kwargs["env"] + resolved_sysroot = ( + openvmm_dir.resolve() / ".packages" / "extracted" / "x86_64-sysroot" + ) + self.assertEqual( + environment["X86_64_UNKNOWN_LINUX_MUSL_OPENSSL_DIR"], + os.fspath(resolved_sysroot), + ) + self.assertEqual( + environment["X86_64_UNKNOWN_LINUX_MUSL_SYMCRYPT_LIB_PATH"], + os.fspath(resolved_sysroot / "lib"), + ) + self.assertEqual(output.read_bytes(), b"musl-openvmm") + chmod.assert_called_once() + provenance.assert_called_once_with(config) - self.share.chmod(0o755) - result = self._run(denied + 'check_share "$share" eperm') - self.assertNotEqual(result.returncode, 0, result.stdout) - self.assertNotIn("NVX-UBUNTU-SANDBOX-SHARE-OK", result.stdout) + def test_guest_build_routes_one_config_to_native_or_docker_consumers(self): + with ( + patch.object(build, "build_kernel") as kernel, + patch.object(build, "build_initramfs") as initramfs, + patch.object(build, "build_docker_artifacts") as docker, + ): + native = build_config.BuildConfig(native_guest=True) + build.build_guest(native) + kernel.assert_called_once_with(native.kernel) + initramfs.assert_called_once_with(native.initramfs_config("alpine")) + docker.assert_not_called() + with ( + patch.object(build, "build_kernel") as kernel, + patch.object(build, "build_initramfs") as initramfs, + patch.object(build, "build_docker_artifacts") as docker, + ): + portable = build_config.BuildConfig() + build.build_guest(portable) + docker.assert_called_once_with(portable.docker, "alpine") + kernel.assert_not_called() + initramfs.assert_not_called() -_SANDBOX_MOUNTINFO = ( - "20 1 0:20 / / rw,relatime - overlay overlay rw," - "lowerdir=/run/nvx/layers/distro,upperdir=/run/nvx/scratch/upper," - "workdir=/run/nvx/scratch/work,metacopy=on,xino=on\n" - "21 20 0:21 / /.nvx-agent ro,nosuid,nodev,relatime - tmpfs tmpfs ro,mode=755\n" - "22 20 0:22 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw\n" - "23 20 0:23 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro\n" - "24 20 0:24 / /dev rw,nosuid,relatime - tmpfs tmpfs rw,mode=755\n" - "25 24 0:25 / /dev/pts rw,relatime - devpts devpts rw,mode=620,ptmxmode=666\n" - "26 24 0:26 / /dev/shm rw,nosuid,nodev,relatime - tmpfs tmpfs rw\n" -) -_SANDBOX_STATUS = ( - "Name:\tsh\nUid:\t65534\t65534\t65534\t65534\n" - "Gid:\t65534\t65534\t65534\t65534\nGroups:\t\nNoNewPrivs:\t1\n" - + "".join( - f"{name}:\t0000000000000000\n" - for name in ("CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb") - ) -) + with ( + patch.object(build, "build_kernel") as kernel, + patch.object(build, "build_initramfs") as initramfs, + patch.object(build, "build_distro_layer") as distro, + ): + all_guests = build_config.BuildConfig(guest="all", native_guest=True) + with self.assertRaisesRegex( + common.ScriptError, + "Azure Linux initramfs builds require Docker", + ): + build.build_guest(all_guests) + kernel.assert_not_called() + initramfs.assert_not_called() + distro.assert_not_called() + def test_combined_build_passes_explicit_backend_to_openvmm_build(self): + config = build_config.BuildConfig( + openvmm=build_config.OpenVmmBuildConfig(backend="mshv") + ) + with ( + patch.object(build.sys, "platform", "linux"), + patch.object(build.os, "access", return_value=False) as access, + patch.object(build, "build_guest") as guest, + patch.object(build, "build_openvmm") as openvmm, + ): + build.build_all(config) -@unittest.skipIf(os.name == "nt", "requires a POSIX shell and process tools") -class SandboxSmokeProfileTests(unittest.TestCase): - SMOKE = Path(__file__).parents[1] / "guest" / "common" / "nvx-sandbox-smoke" + access.assert_not_called() + guest.assert_called_once_with(config) + openvmm.assert_called_once_with(config.openvmm, platform="linux-musl") - def setUp(self): - shell = _posix_shell() - if shell is None: - self.skipTest("POSIX shell is unavailable") - self.shell = shell - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - self.root = Path(temporary.name) - self.source = self.SMOKE.read_text(encoding="utf-8") + def test_combined_build_validates_platform_before_building_guest(self): + config = build_config.BuildConfig( + openvmm=build_config.OpenVmmBuildConfig(backend="whp") + ) + with ( + patch.object(build.sys, "platform", "linux"), + patch.object(build, "build_guest") as guest, + patch.object(build, "build_openvmm") as openvmm, + self.assertRaisesRegex( + common.ScriptError, "backend 'whp' is unsupported on linux" + ), + ): + build.build_all(config) - def _fixture(self, name: str, content: str) -> str: - path = self.root / name - path.write_text(content, encoding="utf-8") - return path.as_posix() + guest.assert_not_called() + openvmm.assert_not_called() - def _run( - self, - names: tuple[str, ...], - call: str, - replacements: dict[str, str] | None = None, - ) -> subprocess.CompletedProcess[str]: - # The globals before fail() configure the checks. - script = self.source[: self.source.index("\nfail() {\n") + 1] + "".join( - _shell_function(self.source, name) for name in ("fail", *names) - ) - for old, new in (replacements or {}).items(): - script = script.replace(old, new) - return subprocess.run( - [self.shell, "-s"], - input=f"{script}{call}\n", - text=True, - capture_output=True, - check=False, + def test_release_source_collection_passes_docker_build_config(self): + config = build_config.DockerBuildConfig( + linux_source_destination=Path("custom-linux-source") ) + with ( + patch.object( + release, + "_guest_release_inputs", + return_value=((), (), (), ()), + ) as guest_release_inputs, + patch.object(release, "collect_alpine_sources"), + patch.object(release, "collect_ubuntu_sources"), + patch.object(release, "build_docker_linux_source") as linux_source, + ): + release.collect_release_sources(config) - def test_status_fields_join_their_values(self): - status = self._fixture("status", _SANDBOX_STATUS) - result = self._run( - ("status_field",), - f"status_field {status} Uid\nstatus_field {status} Groups\n" - f"status_field {status} Missing", - ) + guest_release_inputs.assert_called_once_with(include_azurelinux=False) + linux_source.assert_called_once_with(config) - self.assertEqual(result.returncode, 1, result.stderr) - self.assertEqual(result.stdout, "65534 65534 65534 65534\n\n") - self.assertIn(f"{status} has no Missing field", result.stderr) + def test_records_openvmm_revision_cleanliness_and_executable_hash(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + openvmm_dir = root / "openvmm" + executable = openvmm_dir / "target" / "release" / "openvmm" + executable.parent.mkdir(parents=True) + executable.write_bytes(b"openvmm") + revision = b"0bc357bbcf3a654b63dfb51f1103c5751bf3d31f\n" + results = [ + common.CommandResult(("git",), 0, revision, b""), + common.CommandResult(("git",), 0, revision, b""), + common.CommandResult(("git",), 0, b" M src/main.rs\n", b""), + ] - def test_identity_and_capability_checks_read_the_process_status(self): - identity = ( - 'id() { case "$1" in -u | -g) echo 65534 ;; esac; }\nHOME=/nonexistent\n' - ) - mutations = { - "valid": ("", ""), - "saved-uid": ( - "Uid:\t65534\t65534\t65534\t65534", - "Uid:\t65534\t65534\t0\t65534", - ), - "groups": ("Groups:\t", "Groups:\t27 "), - "bounding": ( - "CapBnd:\t0000000000000000", - "CapBnd:\t000001ffffffffff", - ), - "no-new-privs": ("NoNewPrivs:\t1", "NoNewPrivs:\t0"), - } - for mutation, (old, new) in mutations.items(): - with self.subTest(mutation=mutation): - status = self._fixture("status", _SANDBOX_STATUS.replace(old, new)) - result = self._run( - ("status_field", "check_identity", "check_capabilities"), - f"{identity}check_identity\ncheck_capabilities\necho checked", - {"/proc/self/status": status}, + with ( + patch.object(BuildConstants, "REPO_ROOT", root), + patch.object(common, "run_capture", side_effect=results), + ): + build.record_openvmm_provenance( + build_config.OpenVmmBuildConfig( + build_directory=root / "build", + directory=openvmm_dir, + output=executable, + ) ) - if mutation == "valid": - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, "checked\n") - else: - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("NVX-UBUNTU-SANDBOX-FAIL", result.stderr) - def test_namespace_check_rejects_the_initial_namespaces(self): - def run( - links: dict[str, str], - *, - init_status: str = _SANDBOX_STATUS, - command_line: str = "console=hvc0 nvx_hostname=nvx-lifecycle", - hostname: str = "nvx-lifecycle", - ) -> subprocess.CompletedProcess[str]: - readlink = ( - 'readlink() { case "$1" in ' - + " ".join( - f"*/{name}) echo '{link}' ;;" for name, link in links.items() + provenance = json.loads( + (root / "build" / OpenVMMBuildConstants.PROVENANCE_NAME).read_text( + encoding="utf-8" ) - + " esac; }\n" ) - return self._run( - ("status_field", "check_namespaces"), - f"{readlink}check_namespaces\necho checked", - { - "/proc/1/status": self._fixture("init-status", init_status), - "/proc/cmdline": self._fixture("cmdline", command_line + "\n"), - "/proc/sys/kernel/hostname": self._fixture( - "hostname", hostname + "\n" - ), - }, + self.assertEqual(provenance["source_revision"], revision.decode().strip()) + self.assertFalse(provenance["source_clean"]) + self.assertEqual( + provenance["executable_sha256"], + hashlib.sha256(b"openvmm").hexdigest(), + ) + + def test_kernel_build_rejects_input_config_mutation_during_build(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + input_config = root / "kernel" / "config-microvm" + input_config.parent.mkdir(parents=True) + input_config.write_text( + "\n".join( + ( + *KernelBuildConstants.REQUIRED_DIRECT_BOOT_CONFIG, + *KernelBuildConstants.REQUIRED_VIRTIO_CONSOLE_CONFIG, + *KernelBuildConstants.REQUIRED_SHARED_STATUS_CONFIG, + *KernelBuildConstants.REQUIRED_SANDBOX_CONFIG, + *KernelBuildConstants.REQUIRED_NETWORK_CONFIG, + *KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG, + *KernelBuildConstants.REQUIRED_HARDENING_CONFIG, + ) + ) + + "\n", + encoding="utf-8", ) + patch_path = root / "kernel" / "patches" / "example.patch" + patch_path.parent.mkdir() + patch_path.write_text("patch", encoding="utf-8") + source = root / "source" + source.mkdir() + work = root / "work" + output = root / "output" / "vmlinux" + prior_provenance = output.with_name(KernelBuildConstants.PROVENANCE_NAME) + prior_provenance.parent.mkdir() + prior_provenance.write_text("stale", encoding="utf-8") - private = { - "mnt": "mnt:[4026532200]", - "pid": "pid:[4026532201]", - "uts": "uts:[4026532202]", - } - result = run(private) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, "checked\n") - result = run(private, command_line="console=hvc0", hostname="nvx-sandbox") - self.assertEqual(result.returncode, 0, result.stderr) - - for name, initial in ( - ("mnt", "mnt:[4026531832]"), - ("pid", "pid:[4026531836]"), - ("uts", "uts:[4026531838]"), - ): - with self.subTest(namespace=name): - result = run({**private, name: initial}) - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn(f"shares the initial {name} namespace", result.stderr) - for link in ("pid:[]", "pid:[42a]", "mnt:[42]", "pid:42"): - with self.subTest(link=link): - result = run({**private, "pid": link}) - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("unexpected pid namespace link", result.stderr) - result = run( - private, - init_status=_SANDBOX_STATUS.replace( - "Uid:\t65534\t65534\t65534\t65534", "Uid:\t0\t0\t0\t0" - ), - ) - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("PID 1 of the workload PID namespace runs as 0", result.stderr) - result = run(private, hostname="nvx-sandbox") - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("expected nvx-lifecycle", result.stderr) + with patch.object(BuildConstants, "REPO_ROOT", root): + source_fingerprint = build._kernel_source_fingerprint() - def test_mount_check_requires_private_runtime_mounts_once(self): - functions = ("mount_records", "require_mount", "check_mounts") - mountinfo = self._fixture("mountinfo", _SANDBOX_MOUNTINFO) - result = self._run( - functions, - "check_mounts\necho checked", - {"/proc/self/mountinfo": mountinfo}, - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout, "checked\n") + def run_build(command: object, **_kwargs: object) -> None: + if isinstance(command, list) and command[-1] == "vmlinux": + (work / "vmlinux").write_bytes(b"kernel") + input_config.write_text("CONFIG_CHANGED=y\n", encoding="utf-8") - proc = "22 20 0:22 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw\n" - mutations = { - "stacked": ( - proc, - proc + proc.replace("22 20 0:22", "27 22 0:27"), - "several mounts are stacked at /proc", - ), - "propagation": ( - proc, - proc.replace("relatime - proc", "relatime shared:5 - proc"), - "the mount at /proc propagates: shared:5", - ), - "writable-sysfs": ( - "/sys ro,nosuid", - "/sys rw,nosuid", - "/sys is mounted rw,nosuid,nodev,noexec,relatime, without ro", - ), - "upper": ( - "upperdir=/run/nvx/scratch/upper", - "upperdir=/run/nvx/upper", - "upper layer is not on scratch", - ), - "lower": ( - "lowerdir=/run/nvx/layers/distro", - "lowerdir=/srv/distro", - "does not use the image layers", - ), - "missing-shm": ( - "26 24 0:26 / /dev/shm rw,nosuid,nodev,relatime - tmpfs tmpfs rw\n", - "", - "nothing is mounted at /dev/shm", - ), - "host-devices": ( - "- tmpfs tmpfs rw,mode=755\n", - "- devtmpfs devtmpfs rw,mode=755\n", - "/dev is devtmpfs, expected tmpfs", - ), - } - for mutation, (old, new, message) in mutations.items(): - with self.subTest(mutation=mutation): - self.assertIn(old, _SANDBOX_MOUNTINFO) - mountinfo = self._fixture( - "mountinfo", _SANDBOX_MOUNTINFO.replace(old, new, 1) - ) - result = self._run( - functions, "check_mounts", {"/proc/self/mountinfo": mountinfo} + with ( + patch.object(BuildConstants, "REPO_ROOT", root), + patch.object(build, "_require_linux"), + patch.object(build, "require_tool", return_value="tool"), + patch.object( + build, + "prepare_kernel_source", + return_value=(source, source_fingerprint), + ), + patch.object(build, "run_checked", side_effect=run_build), + self.assertRaisesRegex( + common.ScriptError, + "inputs changed during the build", + ), + ): + build.build_kernel( + build_config.KernelBuildConfig( + work=work, + output=output, + ) ) - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn(message, result.stderr) - def test_cgroup_check_requires_the_container_cgroup(self): - for content, returncode in ( - ("0::/container\n", 0), - ("0::/agent\n", 1), - ("12:pids:/container\n", 1), - ): - with self.subTest(content=content): - cgroup = self._fixture("cgroup", content) - result = self._run( - ("check_cgroup",), "check_cgroup", {"/proc/self/cgroup": cgroup} - ) - self.assertEqual(result.returncode, returncode, result.stderr) + self.assertFalse(output.exists()) + self.assertFalse(output.with_name("vmlinux.config").exists()) + self.assertFalse(prior_provenance.exists()) - def test_limits_check_validates_its_arguments(self): - for arguments, message in ( - ("abc 8", "limits requires numeric MEMORY_MAX and PIDS_MAX"), - ("33554432 ''", "limits requires numeric MEMORY_MAX and PIDS_MAX"), - ("33554432 2", "requires PIDS_MAX of at least 3"), - ): - with self.subTest(arguments=arguments): - result = self._run( - ("start_sleepers", "check_limits"), f"check_limits {arguments}" + def test_materialize_kernel_provenance_inputs_bypasses_mutable_index(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + git_environment = { + key: value + for key, value in os.environ.items() + if not key.startswith("GIT_CONFIG_") + } + config = root / "kernel" / "config-microvm" + patch_path = root / "kernel" / "patches" / "example.patch" + stale_patch = root / "kernel" / "patches" / "stale.patch" + config.parent.mkdir(parents=True) + patch_path.parent.mkdir() + config.write_bytes(b"CONFIG_EXAMPLE=y\n") + patch_path.write_bytes(b"patch\n") + (root / ".gitattributes").write_text( + "\n".join( + ( + "kernel/config-microvm text eol=lf", + "kernel/patches/*.patch text eol=lf", + ) ) - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn(message, result.stderr) + + "\n", + encoding="ascii", + ) + subprocess.run( + ["git", "init", "-q"], + cwd=root, + env=git_environment, + check=True, + ) + subprocess.run( + ["git", "config", "user.name", "NVX Tests"], + cwd=root, + env=git_environment, + check=True, + ) + subprocess.run( + ["git", "config", "user.email", "nvx-tests@example.com"], + cwd=root, + env=git_environment, + check=True, + ) + subprocess.run( + [ + "git", + "add", + ".gitattributes", + "kernel/config-microvm", + "kernel/patches/example.patch", + ], + cwd=root, + env=git_environment, + check=True, + ) + subprocess.run( + ["git", "commit", "-qm", "kernel provenance fixture"], + cwd=root, + env=git_environment, + check=True, + ) + git_dir = Path( + subprocess.run( + ["git", "rev-parse", "--git-dir"], + cwd=root, + env=git_environment, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + ) + (root / git_dir / "info" / "attributes").write_text( + "\n".join( + ( + "kernel/config-microvm -text", + "kernel/patches/*.patch -text", + ) + ) + + "\n", + encoding="ascii", + ) + config.write_bytes(b"CONFIG_EXAMPLE=y\r\n") + patch_path.write_bytes(b"patch\r\n") + stale_patch.write_bytes(b"stale\r\n") + subprocess.run( + [ + "git", + "add", + "kernel/config-microvm", + "kernel/patches/example.patch", + "kernel/patches/stale.patch", + ], + cwd=root, + env=git_environment, + check=True, + ) + subprocess.run( + [ + "git", + "checkout-index", + "--force", + "--", + "kernel/config-microvm", + "kernel/patches/example.patch", + "kernel/patches/stale.patch", + ], + cwd=root, + env=git_environment, + check=True, + ) + self.assertEqual(config.read_bytes(), b"CONFIG_EXAMPLE=y\r\n") + self.assertEqual(patch_path.read_bytes(), b"patch\r\n") + self.assertEqual(stale_patch.read_bytes(), b"stale\r\n") - @unittest.skipUnless(sys.platform == "linux", "inspects the Linux process table") - def test_sleeper_subshell_records_and_reaps_its_sleepers(self): - record = self.root / "sleepers" - result = self._run( - ("start_sleepers",), - f'(start_sleepers "{record.as_posix()}" 3)\necho done', - # A distinct duration identifies this test's sleepers. - {"sleep 600": "sleep 611.25"}, - ) + with ( + patch.dict(os.environ, git_environment, clear=True), + patch.object(BuildConstants, "REPO_ROOT", root), + ): + build.materialize_kernel_provenance_inputs() - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(record.read_text(encoding="utf-8"), "3\n") - for process in Path("/proc").iterdir(): - if process.name.isdigit(): - try: - command = (process / "cmdline").read_bytes() - except OSError: - continue - self.assertNotEqual(command, b"sleep\x00611.25\x00") + self.assertEqual(config.read_bytes(), b"CONFIG_EXAMPLE=y\n") + self.assertEqual(patch_path.read_bytes(), b"patch\n") + self.assertFalse(stale_patch.exists()) - def test_main_body_runs_the_profile_then_limits_and_shares(self): - body = self.source[self.source.index("\ncheck_identity\n") + 1 :] - stubs = "".join( - f'{name}() {{ echo "{name} $*"; }}\n' - for name in ( - "check_identity", - "check_capabilities", - "check_namespaces", - "check_mounts", - "check_devices", - "check_cgroup", - "check_image", - "check_limits", - "check_share", - "check_shares_isolated", + def test_manifest_tracks_every_kernel_patch(self): + manifest = json.loads( + (BuildConstants.REPO_ROOT / "SOURCE-MANIFEST.json").read_text( + encoding="utf-8" ) ) - script = ( - self.source[: self.source.index("\nfail() {\n") + 1] - + _shell_function(self.source, "fail") - + stubs - + body - ) - - def run(*arguments: str) -> subprocess.CompletedProcess[str]: - return subprocess.run( - [self.shell, "-s", "--", *arguments], - input=script, - text=True, - capture_output=True, - check=False, - ) - - result = run("limits", "33554432", "8", "/workspace", "rw", "/tools") - self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( - result.stdout.splitlines(), + manifest["linux"]["patches"], [ - "check_identity ", - "check_capabilities ", - "check_namespaces ", - "check_mounts ", - "check_devices ", - "check_cgroup ", - "check_image ", - "NVX-UBUNTU-SANDBOX-PROFILE-OK uid=65534 gid=65534", - "check_limits 33554432 8", - "check_share /workspace rw", - "check_share /tools ro", - "check_shares_isolated /workspace /tools", - "NVX-UBUNTU-SANDBOX-OK uid=65534 gid=65534", + path.relative_to(BuildConstants.REPO_ROOT).as_posix() + for path in build._kernel_patch_files() ], ) - result = run("limits", "33554432") - self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("limits requires MEMORY_MAX and PIDS_MAX", result.stderr) - self.assertNotIn("NVX-UBUNTU-SANDBOX-OK", result.stdout) - - -class ManagedAgentCgroupTests(unittest.TestCase): - SOURCE = Path(__file__).parents[1] / "guest" / "common" / "nvx-managed-agent.c" - - def setUp(self): - if sys.platform != "linux": - self.skipTest("the managed agent requires Linux") - compiler = shutil.which("cc") or shutil.which("gcc") - if compiler is None: - self.skipTest("C compiler is unavailable") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - root = Path(temporary.name) - self.cgroup = root / "cgroup" - self.cgroup.mkdir() - (self.cgroup / "cgroup.procs").touch() - self.subtree_control = self.cgroup / "cgroup.subtree_control" - self.subtree_control.touch() - self.execution = self.cgroup / "nvx-exec" - self.execution.mkdir() - self.tasks = self.execution / "pids.current" - self.tasks.write_bytes(b"0\n") - (self.execution / "cgroup.kill").touch() - source = root / "cgroup-test.c" - source.write_text( - f"""#define _GNU_SOURCE -#include -#include -static pid_t harness_waitpid(pid_t pid, int *status, int options); -#define waitpid(pid, status, options) harness_waitpid(pid, status, options) -#define CGROUP_ROOT {json.dumps(str(self.cgroup))} -#define main managed_agent_main -#include {json.dumps(str(self.SOURCE))} -#undef main -#undef waitpid - -static pid_t harness_process = -1; - -/* Like the pids controller, stops charging a process once it is reaped. */ -static pid_t harness_waitpid(pid_t pid, int *status, int options) -{{ - pid_t result = waitpid(pid, status, options); - - if (result > 0 && result == harness_process) {{ - int fd = open(EXEC_CGROUP "/pids.current", O_WRONLY | O_TRUNC | O_CLOEXEC); - - if (fd < 0 || write(fd, "0\\n", 2) != 2 || close(fd) != 0) {{ - _exit(3); - }} - harness_process = -1; - }} - return result; -}} - -/* - * Calls `check` while a workload process that the cgroup charges exits, or - * once it has exited if `exited` is set, and requires `check` to reap it. - */ -static int with_exiting_process(int (*check)(void), int exited) -{{ - siginfo_t information = {{0}}; - int result; - int status; - pid_t child = fork(); - - if (child < 0) {{ - _exit(2); - }} - if (child == 0) {{ - const struct timespec delay = {{.tv_nsec = 50L * 1000L * 1000L}}; - - nanosleep(&delay, NULL); - _exit(0); - }} - harness_process = child; - if (exited && waitid(P_PID, (id_t)child, &information, WEXITED | WNOWAIT) != 0) {{ - _exit(2); - }} - result = check(); - status = errno; - if (harness_process >= 0 || waitpid(child, NULL, WNOHANG) >= 0 || errno != ECHILD) {{ - _exit(4); - }} - errno = status; - return result; -}} - -int main(int argc, char **argv) -{{ - int result; - if (argc != 2) {{ - return 2; - }} - errno = 0; - if (strcmp(argv[1], "retry") == 0) {{ - struct control_session session = {{.fd = STDOUT_FILENO}}; - struct agent_config config = {{.direct = 1}}; - struct exec_config exec_config = {{0}}; - char *command[] = {{"/bin/true", NULL}}; - result = run_exec(&session, &config, 42, 0, command, &exec_config); - return result == 0 - ? run_exec(&session, &config, 43, 0, command, &exec_config) - : result; - }} - if (strcmp(argv[1], "tasks") == 0) {{ - result = exec_cgroup_has_tasks(); - }} else if (strcmp(argv[1], "prepare") == 0) {{ - result = prepare_exec_cgroup(); - }} else if (strcmp(argv[1], "settle") == 0) {{ - result = settle_exec_cgroup(); - }} else if (strcmp(argv[1], "settle-reap") == 0) {{ - result = with_exiting_process(settle_exec_cgroup, 0); - }} else if (strcmp(argv[1], "verify-reap") == 0) {{ - result = with_exiting_process(reap_exec_cgroup, 1); - }} else {{ - return 2; - }} - printf("%d %d\\n", result, errno); - return 0; -}} -""", - encoding="utf-8", + self.assertNotIn("guest_agent", manifest) + self.assertEqual( + manifest["openvmm"], + { + "microvm_abi_version": 2, + "control_session_protocol_version": 1, + "control_contract_revision": "nvx-microvm-v2-control-v2", + }, ) - self.helper = root / "cgroup-test" - flags = [ - flag - for flag in InitramfsBuildConstants.STATIC_HELPER_CFLAGS - if flag != "-static" - ] - result = subprocess.run( - [compiler, *flags, "-o", str(self.helper), str(source)], - capture_output=True, - text=True, - timeout=30, - check=False, + self.assertEqual( + manifest["guest_rust"], + { + "toolchain": GuestRustBuildConstants.TOOLCHAIN, + "target": GuestRustBuildConstants.TARGET, + }, ) - self.assertEqual(result.returncode, 0, result.stderr) - def _check(self, mode: str) -> tuple[int, int]: - result = subprocess.run( - [str(self.helper), mode], - capture_output=True, - text=True, - timeout=5, - check=False, + def test_guest_artifact_inputs_cover_every_rust_binary_source(self): + rust_sources = set(guest_binaries.guest_binary_source_files()) + inputs = { + "alpine provenance": set(build._initramfs_source_files()), + "alpine customization": set( + build._guest_customization_files(guests.ALPINE_GUEST) + ), + "ubuntu customization": set(ubuntu.customization_files()), + "azure linux": set(azurelinux.input_files()), + } + for name, files in inputs.items(): + with self.subTest(inputs=name): + self.assertTrue(rust_sources.issubset(files)) + self.assertIn( + BuildConstants.REPO_ROOT / "scripts" / "nvx_tools" / "guest_binaries.py", + inputs["alpine provenance"], ) - self.assertEqual(result.returncode, 0, result.stderr) - value, status = result.stdout.split() - return int(value), int(status) - def _assert_further_exec_refused(self): - result = subprocess.run( - [str(self.helper), "retry"], - capture_output=True, - timeout=5, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - frames = result.stdout - for expected_id in (42, 43): - outer = ManagedAgentStopTests.OUTER - app = ManagedAgentStopTests.APP - *_, length = outer.unpack(frames[: outer.size]) - frame = frames[outer.size : outer.size + length] - _, _, kind, _, request_id, status, payload_len = app.unpack( - frame[: app.size] - ) - payload = frame[app.size :] - self.assertEqual((kind, request_id, status), (0xFF, expected_id, 125)) - self.assertEqual(payload_len, len(payload)) - self.assertEqual(payload, b"containment-failed") - frames = frames[outer.size + length :] - self.assertEqual(frames, b"") - - def test_task_count_requires_a_canonical_decimal(self): - for contents, expected in ( - (b"0\n", 0), - (b"1\n", 1), - (b"10\n", 1), - (b"4194304\n", 1), - ): - with self.subTest(contents=contents): - self.tasks.write_bytes(contents) - self.assertEqual(self._check("tasks")[0], expected) - for contents in ( - b"", - b"\n", - b"0", - b"00\n", - b"01\n", - b"-1\n", - b"+1\n", - b" 1\n", - b"1 \n", - b"0\n0\n", - b"1\x00\n", - b"max\n", - ): - with self.subTest(contents=contents): - self.tasks.write_bytes(contents) - self.assertEqual(self._check("tasks"), (-1, errno.EPROTO)) - self.tasks.write_bytes(b"1" * 40 + b"\n") - self.assertEqual(self._check("tasks"), (-1, errno.EOVERFLOW)) - - def test_missing_and_unreadable_task_counts_are_errors(self): - self.tasks.unlink() - self.assertEqual(self._check("tasks"), (-1, errno.ENOENT)) - self._assert_further_exec_refused() - self.tasks.mkdir() - self.assertEqual(self._check("tasks"), (-1, errno.EISDIR)) - self._assert_further_exec_refused() - - def test_preparation_enables_the_pids_controller_once(self): - self.assertEqual(self._check("prepare")[0], 0) - self.assertEqual(self.subtree_control.read_bytes(), b"") - self.tasks.unlink() - self.assertEqual(self._check("prepare")[0], 0) - self.assertEqual(self.subtree_control.read_bytes(), b"+pids") - self.subtree_control.unlink() - self.assertEqual(self._check("prepare"), (-1, errno.ENOENT)) - - def test_settlement_verifies_emptiness(self): - self.assertEqual(self._check("settle")[0], 0) - self.assertEqual((self.execution / "cgroup.kill").read_bytes(), b"1") - - def test_settlement_and_verification_reap_what_the_workload_left(self): - for mode in ("settle-reap", "verify-reap"): - with self.subTest(mode=mode): - self.tasks.write_bytes(b"1\n") - self.assertEqual(self._check(mode)[0], 0) - self.assertEqual(self.tasks.read_bytes(), b"0\n") - - def test_settlement_does_not_hide_kill_or_verification_failures(self): - self.tasks.write_bytes(b"1\n") - (self.execution / "cgroup.kill").unlink() - self.assertEqual(self._check("settle"), (-1, errno.ENOENT)) - self._assert_further_exec_refused() - (self.execution / "cgroup.kill").touch() - self.tasks.unlink() - self.assertEqual(self._check("settle"), (-1, errno.ENOENT)) - self._assert_further_exec_refused() - - def test_unreaped_tasks_at_deadline_are_not_settled(self): - # cgroup.events no longer counts a killed process that awaits its - # reaper, but the pids controller still charges it. - (self.execution / "cgroup.events").write_bytes(b"populated 0\nfrozen 0\n") - self.tasks.write_bytes(b"1\n") - started = time.monotonic() - self.assertEqual(self._check("settle"), (-1, errno.ETIMEDOUT)) - self.assertGreaterEqual(time.monotonic() - started, 1.9) - self._assert_further_exec_refused() - - def test_malformed_task_count_refuses_further_exec_until_verified_empty(self): - self.tasks.write_bytes(b"max\n") - self.assertEqual(self._check("settle"), (-1, errno.EPROTO)) - self._assert_further_exec_refused() - self.tasks.write_bytes(b"0\n") - self.assertEqual(self._check("tasks")[0], 0) - self.assertEqual(self._check("settle")[0], 0) + def test_guest_artifact_identities_normalize_source_line_endings(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = root / "nvx-rs" / "src" / "lib.rs" + source.parent.mkdir(parents=True) + source.write_bytes(b"pub fn fixture() {}\n") + with ( + patch.object(BuildConstants, "REPO_ROOT", root), + patch.object(build, "_initramfs_source_files", return_value=(source,)), + patch.object(ubuntu, "load_package_lock", return_value=()), + patch.object(azurelinux, "input_files", return_value=(source,)), + ): + lf = { + "alpine": build.initramfs_provenance_inputs(), + "ubuntu": ubuntu.converter_input_sha256((source,)), + "azurelinux": azurelinux.input_sha256(), + } + source.write_bytes(b"pub fn fixture() {}\r\n") + crlf = { + "alpine": build.initramfs_provenance_inputs(), + "ubuntu": ubuntu.converter_input_sha256((source,)), + "azurelinux": azurelinux.input_sha256(), + } + self.assertEqual(crlf, lf) -def _elf_file( - segments: tuple[tuple[int, bytes], ...] = ((1, b"code"),), - *, - elf_type: int = 2, - elf_class: int = 2, -) -> bytes: - """Returns a minimal ELF file with one program header for each segment.""" - header_size, entry_size = 64, 56 - payload_offset = header_size + entry_size * len(segments) - program_headers = b"" - payloads = b"" - for segment_type, payload in segments: - program_headers += struct.pack( - " bytes: - return b"".join(struct.pack(" Path: - path = self.root / name - path.write_bytes(data) - return path + self.assertEqual(alpine.initramfs_name, "initramfs.cpio.gz") + self.assertEqual(alpine.default_memory_mib, 128) + self.assertTrue(alpine.sandbox_control) + self.assertEqual( + ubuntu_guest.initramfs_name, + "initramfs-ubuntu.cpio.gz", + ) + self.assertEqual(ubuntu_guest.default_memory_mib, 512) + self.assertFalse(ubuntu_guest.sandbox_control) + self.assertEqual( + azurelinux_guest.initramfs_name, + "initramfs-azurelinux.cpio.gz", + ) + self.assertEqual(azurelinux_guest.default_memory_mib, 512) + self.assertFalse(azurelinux_guest.sandbox_control) - def test_static_and_static_pie_executables_pass(self): - executable = elf.read_elf_executable(self._write("exit", _elf_file())) - self.assertTrue(executable.static) + def test_azurelinux_manifest_and_package_lock_match_build_pins(self): + manifest = json.loads( + (BuildConstants.REPO_ROOT / "SOURCE-MANIFEST.json").read_text( + encoding="utf-8" + ) + )["azurelinux"] + self.assertEqual(manifest["image"], AzureLinuxBuildConstants.IMAGE) + self.assertEqual(manifest["package_lock"], "azurelinux/packages.lock.json") + self.assertEqual( + manifest["package_lock_sha256"], + azurelinux.package_lock_sha256(), + ) + packages = azurelinux.load_package_lock() self.assertEqual( + {"busybox", "util-linux"} - {package["name"] for package in packages}, + set(), + ) + for package in packages: + self.assertTrue( + package["url"].startswith( + f"{AzureLinuxBuildConstants.REPOSITORY_URL}/Packages/" + ) + ) + attributes = (BuildConstants.REPO_ROOT / ".gitattributes").read_text( + encoding="utf-8" + ) + self.assertIn("azurelinux/packages.lock.json text eol=lf", attributes) + + def test_azurelinux_package_lock_rejects_unpinned_records(self): + document = json.loads( + azurelinux.package_lock_path().read_text(encoding="utf-8") + ) + first = document["packages"][0] + cases: tuple[tuple[str, dict[str, object], str], ...] = ( + ("image", {"image": "mcr.microsoft.com/azurelinux/base/core:3.0"}, "image"), ( - executable.elf_type, - executable.interpreter, - executable.needed, - executable.dynamic, + "url", + { + "packages": [ + {**first, "url": first["url"].replace("/prod/", "/preview/")}, + *document["packages"][1:], + ] + }, + "must be fetched from", + ), + ( + "sha256", + { + "packages": [ + {**first, "sha256": "0" * 63}, + *document["packages"][1:], + ] + }, + "invalid SHA-256", + ), + ( + "order", + {"packages": list(reversed(document["packages"]))}, + "sorted by name", ), - ("EXEC", None, 0, False), ) + for name, update, message in cases: + with ( + self.subTest(case=name), + tempfile.TemporaryDirectory() as temporary, + ): + path = Path(temporary) / "packages.lock.json" + path.write_text(json.dumps({**document, **update}), encoding="utf-8") + with self.assertRaisesRegex(common.ScriptError, message): + azurelinux.load_package_lock(path) + + def test_azurelinux_download_packages_verifies_each_locked_rpm(self): + destination = Path("rpms") + with patch.object(azurelinux, "download_verified") as download_verified: + downloaded = azurelinux.download_packages(destination) + + packages = azurelinux.load_package_lock() self.assertEqual( - executable.describe(), - f"{executable.path}: ELF EXEC, {executable.size} bytes, static", - ) - # A static PIE relocates itself through its dynamic section; entries - # after DT_NULL do not count. - dynamic = _dynamic_entries((7, 0x1000), (0, 0), (1, 1)) - pie = elf.read_elf_executable( - self._write("pie", _elf_file(((1, b"code"), (2, dynamic)), elf_type=3)) + download_verified.call_args_list, + [ + call( + package["url"], + destination / package["url"].rsplit("/", maxsplit=1)[-1], + package["sha256"], + ) + for package in packages + ], ) - self.assertTrue(pie.static) - self.assertEqual((pie.elf_type, pie.dynamic), ("DYN", True)) - self.assertTrue(pie.describe().endswith(" bytes, static-pie")) + self.assertEqual(len(downloaded), len(packages)) - def test_an_interpreter_or_a_needed_library_fails(self): - dynamic = elf.read_elf_executable( - self._write( - "dynamic", - _elf_file(((1, b"code"), self.INTERPRETER, self.NEEDED), elf_type=3), + def test_ubuntu_manifest_and_package_lock_match_build_pins(self): + manifest = json.loads( + (BuildConstants.REPO_ROOT / "SOURCE-MANIFEST.json").read_text( + encoding="utf-8" ) ) - self.assertFalse(dynamic.static) + ubuntu_manifest = manifest["ubuntu"] self.assertEqual( - (dynamic.interpreter, dynamic.needed), ("/lib/ld-musl-x86_64.so.1", 1) + ubuntu_manifest["version"], + UbuntuBuildConstants.VERSION, ) - self.assertIn( - "dynamic: interpreter /lib/ld-musl-x86_64.so.1, 1 needed libraries", - dynamic.describe(), + self.assertEqual( + ubuntu_manifest["base_url"], + UbuntuBuildConstants.BASE_URL, ) - needed = elf.read_elf_executable( - self._write("needed", _elf_file(((1, b"code"), self.NEEDED), elf_type=3)) + self.assertEqual( + ubuntu_manifest["base_sha256"], + UbuntuBuildConstants.BASE_SHA256, ) - self.assertFalse(needed.static) - - def test_rejects_files_that_are_not_64_bit_executables(self): - truncated_segment = _elf_file(((1, b"code"), self.INTERPRETER))[:-4] - for name, data in ( - ("script", b"#!/bin/sh\nexit 0\n"), - ("elf32", _elf_file(elf_class=1)), - ("object", _elf_file(elf_type=1)), - ("header", _elf_file()[:40]), - ("segment", truncated_segment), - ): - with self.subTest(name=name), self.assertRaises(ValueError): - elf.read_elf_executable(self._write(name, data)) - - def test_check_lists_every_executable_and_fails_on_dynamic_ones(self): - static = self._write("static", _elf_file()) - dynamic = self._write("dynamic", _elf_file(((1, b"code"), self.INTERPRETER))) - output = io.StringIO() - with ( - contextlib.redirect_stdout(output), - self.assertRaisesRegex( - common.ScriptError, "not static executables: .*dynamic" - ), - ): - elf.check_static_executables([static, dynamic]) - self.assertIn(f"{static}: ELF EXEC", output.getvalue()) - self.assertIn(f"{dynamic}: ELF EXEC", output.getvalue()) - - output = io.StringIO() - with contextlib.redirect_stdout(output): - self.assertEqual(nvx.main(["check-static-elf", str(static)]), 0) self.assertEqual( - output.getvalue(), - f"{static}: ELF EXEC, {static.stat().st_size} bytes, static\n", + ubuntu_manifest["archive_keyring_url"], + UbuntuBuildConstants.ARCHIVE_KEYRING_URL, ) - with ( - contextlib.redirect_stdout(io.StringIO()), - contextlib.redirect_stderr(io.StringIO()) as error, - ): - self.assertEqual( - nvx.main(["check-static-elf", str(static), str(dynamic)]), 1 - ) - self.assertIn("not static executables", error.getvalue()) - - -class GuestExitTests(unittest.TestCase): - """nvx-exit's arguments, and its single one-byte write at port 0x604.""" - - SOURCE = Path(__file__).parents[1] / "guest" / "common" / "nvx-exit.c" - PORT = 0x604 - - def setUp(self): - if sys.platform != "linux": - self.skipTest("nvx-exit builds on Linux") - compiler = shutil.which("cc") or shutil.which("gcc") - if compiler is None: - self.skipTest("C compiler is unavailable") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - root = Path(temporary.name) - # A regular file stands in for /dev/port, so no test writes a port. - self.device = root / "port" - flags = [ - flag - for flag in InitramfsBuildConstants.STATIC_HELPER_CFLAGS - if flag != "-static" - ] - self.binaries: dict[str, Path] = {} - for name, device in (("ok", self.device), ("missing", root / "no" / "port")): - binary = root / f"nvx-exit-{name}" - result = subprocess.run( - [ - compiler, - *flags, - f'-DNVX_EXIT_DEVICE="{device}"', - "-o", - str(binary), - str(self.SOURCE), - ], - capture_output=True, - text=True, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.binaries[name] = binary - - def _run(self, name: str, *arguments: str) -> subprocess.CompletedProcess[bytes]: - return subprocess.run( - [str(self.binaries[name]), *arguments], - capture_output=True, - check=False, - timeout=10, + self.assertEqual( + ubuntu_manifest["archive_keyring_sha256"], + UbuntuBuildConstants.ARCHIVE_KEYRING_SHA256, ) - - def test_writes_the_exit_code_as_one_byte_at_the_control_port(self): - cases = ( - ((), 1), - (("",), 1), - (("0",), 0), - (("37",), 37), - (("255",), 255), - (("256",), 1), - (("-1",), 1), - (("+5",), 1), - ((" 7",), 1), - (("12a",), 1), - (("99999999999999999999",), 1), - # Out of range after any digit, so the value never wraps. - (("4294967296",), 1), - (("4294967551",), 1), - (("00000000000000000255",), 255), - # Decimal: the old script's printf read a leading zero as octal. - (("010",), 10), - (("7", "9"), 7), - ) - for arguments, expected in cases: - with self.subTest(arguments=arguments): - self.device.write_bytes(b"") - result = self._run("ok", *arguments) - self.assertEqual( - (result.returncode, result.stdout, result.stderr), (0, b"", b"") - ) - self.assertEqual( - self.device.read_bytes(), bytes(self.PORT) + bytes([expected]) - ) - - def test_fails_silently_when_the_port_cannot_be_opened(self): - result = self._run("missing", "0") self.assertEqual( - (result.returncode, result.stdout, result.stderr), (1, b"", b"") + ubuntu_manifest["package_lock_sha256"], + ubuntu.package_lock_sha256(), + ) + self.assertEqual( + ubuntu_manifest["package_lock_sha256"], + "fcdd30223b96fc26e24fcf4b6763a4a2504e9cd25882e68e89fbf6bf45277ef0", + ) + packages = ubuntu.load_package_lock() + self.assertEqual( + [package["name"] for package in packages], + [ + "busybox-static", + "iputils-ping", + "libcap2", + "libidn2-0", + "libunistring5", + "net-tools", + "netcat-openbsd", + ], ) + def test_ubuntu_package_lock_read_errors_are_actionable(self): + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "packages.json" + path.write_text("{", encoding="utf-8") -class ManagedAgentStopTests(unittest.TestCase): - SOURCE = Path(__file__).parents[1] / "guest" / "common" / "nvx-managed-agent.c" - OUTER = struct.Struct("<4sHBB16sQQI") - APP = struct.Struct("<4sBBHQiI") - - def setUp(self): - if sys.platform != "linux": - self.skipTest("the managed agent requires a Linux pseudo-terminal") - compiler = shutil.which("cc") or shutil.which("gcc") - if compiler is None: - self.skipTest("C compiler is unavailable") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - self.agent = Path(temporary.name) / "nvx-managed-agent" - flags = [ - flag - for flag in InitramfsBuildConstants.STATIC_HELPER_CFLAGS - if flag != "-static" - ] - result = subprocess.run( - [compiler, *flags, "-o", str(self.agent), str(self.SOURCE)], - capture_output=True, - text=True, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) + with self.assertRaisesRegex( + common.ScriptError, "failed to read Ubuntu package lock" + ): + ubuntu.load_package_lock(path) - def _record( - self, record_type: int, instance: bytes, sequence: int, payload: bytes = b"" - ) -> bytes: - header = self.OUTER.pack( - b"NVXS", 1, record_type, 0, instance, 1, sequence, len(payload) - ) - return header + payload - - def _read_exact(self, fd: int, length: int, deadline: float) -> bytes: - data = b"" - while len(data) < length: - remaining = deadline - time.monotonic() - self.assertGreater(remaining, 0, "managed agent did not respond") - readable, _, _ = select.select([fd], [], [], remaining) - if readable: - chunk = os.read(fd, length - len(data)) - self.assertTrue(chunk, "managed agent closed the control tty") - data += chunk - return data - - def _read_record(self, fd: int, deadline: float) -> tuple[int, int, bytes]: - header = self._read_exact(fd, self.OUTER.size, deadline) - magic, version, record_type, flags, _, _, sequence, length = self.OUTER.unpack( - header - ) - self.assertEqual((magic, version, flags), (b"NVXS", 1, 0)) - return record_type, sequence, self._read_exact(fd, length, deadline) + def test_ubuntu_safe_extractor_rejects_archive_symlink_escape(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + archive_path = root / "unsafe.tar.gz" + with tarfile.open(archive_path, "w:gz") as archive_file: + symlink = tarfile.TarInfo("etc") + symlink.type = tarfile.SYMTYPE + symlink.linkname = "../../outside" + archive_file.addfile(symlink) + payload = b"nameserver 192.0.2.1\n" + resolver = tarfile.TarInfo("etc/resolv.conf") + resolver.size = len(payload) + archive_file.addfile(resolver, io.BytesIO(payload)) - @staticmethod - def _reap(process: subprocess.Popen[bytes]) -> None: - if process.poll() is None: - process.kill() - process.wait(timeout=5) - if process.stderr is not None: - process.stderr.close() - - def _open_session( - self, rootfs: str - ) -> tuple[subprocess.Popen[bytes], int, bytes, float]: - """Starts an agent and completes the attach and reset handshake.""" - if sys.platform != "linux": - self.skipTest("the managed agent requires a Linux pseudo-terminal") - import pty + with self.assertRaisesRegex(common.ScriptError, "escapes"): + ubuntu.safe_extract_tar( + archive_path, + root / "extracted", + label="test archive", + ) + self.assertFalse((root / "outside").exists()) - master, slave = pty.openpty() - self.addCleanup(os.close, master) - self.addCleanup(os.close, slave) - process = subprocess.Popen( - [ - str(self.agent), - os.ttyname(slave), - rootfs, - "nvx-sandbox", - "65534", - "65534", - "nobody", - "/nonexistent", - ], - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - ) - self.addCleanup(self._reap, process) - deadline = time.monotonic() + 10 - instance = bytes(range(1, 17)) - - self.assertEqual(self._read_record(master, deadline)[0], 1) - os.write(master, self._record(3, instance, 7)) - record_type, sequence, credit = self._read_record(master, deadline) - self.assertEqual((record_type, sequence, len(credit)), (4, 0, 4)) - return process, master, instance, deadline - - def _request( - self, rootfs: str, kind: int, payload: bytes = b"" - ) -> tuple[int, int, int, bytes]: - """Sends one request to a fresh agent; returns the kind, request ID, status, and payload of its answer.""" - _, master, instance, deadline = self._open_session(rootfs) - frame = self.APP.pack(b"NVXC", 1, kind, 0, 42, 0, len(payload)) + payload - os.write(master, self._record(5, instance, 8, frame)) - self.assertEqual(self._read_record(master, deadline)[0], 9) - record_type, _, answer = self._read_record(master, deadline) - self.assertEqual(record_type, 5) - _, _, answered, _, request_id, status, length = self.APP.unpack( - answer[: self.APP.size] - ) - body = answer[self.APP.size :] - self.assertEqual(length, len(body)) - return answered, request_id, status, body - - def test_sandbox_agent_returns_to_init_agent_after_stop(self): - if sys.platform != "linux": - self.skipTest("the managed agent requires a Linux pseudo-terminal") - process, master, instance, deadline = self._open_session("/run/nvx/rootfs") - stop = self.APP.pack(b"NVXC", 1, 3, 0, 42, 0, 0) - os.write(master, self._record(5, instance, 8, stop)) - self.assertEqual(self._read_record(master, deadline)[0], 9) - record_type, _, payload = self._read_record(master, deadline) - self.assertEqual(record_type, 5) - _, _, kind, _, request_id, status, length = self.APP.unpack(payload) - self.assertEqual((kind, request_id, status, length), (0x85, 42, 0, 0)) - - _, stderr = process.communicate(timeout=10) - self.assertEqual(process.returncode, 0, stderr) - - def test_agent_advertises_the_control_features_of_its_mode(self): - cancel, workload_account, exec_cgroup, environment = (1, 4, 8, 16) - exec_cwd, host_mapping_table = (32, 64) - for rootfs, expected in ( - # Only the direct agent maps host paths, gives each workload a cgroup, and - # refuses a working directory that the workload cannot enter. Bit 1, which - # announced nvx_map= kernel tokens, is retired. - ( - "-", - cancel - | workload_account - | exec_cgroup - | environment - | exec_cwd - | host_mapping_table, - ), - ("/run/nvx/rootfs", cancel | workload_account | environment), - ): - with self.subTest(rootfs=rootfs): - kind, request_id, status, body = self._request(rootfs, 5) - self.assertEqual((kind, request_id, status), (0x81, 42, 0)) - self.assertEqual(struct.unpack(" bytes: - """Encodes an extended EXEC payload that carries `cwd` after the arguments.""" - arguments = b"".join(struct.pack(" -int harness_setenv(const char *name, const char *value, int overwrite); -#define setenv(name, value, overwrite) harness_setenv(name, value, overwrite) -#define CGROUP_ROOT {json.dumps(str(cgroup))} -#define setgroups(size, list) harness_setgroups(size, list) -#define setegid(gid) harness_setegid(gid) -#define seteuid(uid) harness_seteuid(uid) -#define main managed_agent_main -#include {json.dumps(str(self.SOURCE))} -#undef main - -int harness_setenv(const char *name, const char *value, int overwrite) -{{ - if (strcmp(name, "PWD") == 0 && getenv("HARNESS_FAIL_PWD") != NULL) {{ - errno = ENOMEM; - return -1; - }} - return (setenv)(name, value, overwrite); -}} + def extract( + archive: Path, + destination: Path, + *, + label: str, + ) -> None: + self.assertEqual( + archive.name, + f"ubuntu-base-{UbuntuBuildConstants.VERSION}-base-amd64.tar.gz", + ) + self.assertEqual(label, "Ubuntu Base archive") + status = destination / "var" / "lib" / "dpkg" / "status" + status.parent.mkdir(parents=True) + status.write_text("", encoding="utf-8") -int harness_setgroups(size_t size, const gid_t *list) -{{ - (void)size; - (void)list; - if (getenv("HARNESS_FAIL_SETGROUPS") != NULL) {{ - errno = EPERM; - return -1; - }} - return 0; -}} + with ( + patch.object( + ubuntu, + "cache_root", + return_value=temporary_root / "cache", + ), + patch.object(ubuntu, "download_verified"), + patch.object(ubuntu, "safe_extract_tar", side_effect=extract), + patch.object(ubuntu, "_validate_ubuntu_identity"), + patch.object(ubuntu, "load_package_lock", return_value=()), + patch.object(ubuntu, "_validate_package_closure"), + patch.object(ubuntu, "_customize_root"), + patch.object(ubuntu.Path, "chmod", autospec=True) as chmod, + ): + self.assertEqual(ubuntu.prepare_root(work), expected_root) -int harness_setegid(gid_t gid) -{{ - (void)gid; - return 0; -}} + chmod.assert_called_once_with(expected_root, 0o755) -int harness_seteuid(uid_t uid) -{{ - (void)uid; - return 0; -}} + def test_ubuntu_customization_installs_busybox_wget(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for relative in ("etc", "usr/bin", "usr/sbin"): + (root / relative).mkdir(parents=True) + with ( + patch.object(ubuntu, "_clear_directory"), + patch.object(ubuntu, "_validate_accounts"), + patch.object(ubuntu, "_validate_usr_merge"), + patch.object(ubuntu, "_ensure_symlink") as ensure_symlink, + ): + ubuntu._customize_root(root) -int main(int argc, char **argv) -{{ - if (argc >= 2 && strcmp(argv[1], "--exec-config-fd") == 0) {{ - return launch_workload(argc, argv); - }} - - struct control_session session = {{.fd = STDOUT_FILENO}}; - struct agent_config config = {{ - .direct = 1, - .uid = "65534", - .gid = "65534", - .user = "nobody", - .home = "/", - }}; - char *environment[] = {{getenv("HARNESS_ENVIRONMENT"), NULL}}; - struct exec_config exec_config = {{ - .cwd = argc == 3 ? argv[2] : NULL, - .environment = environment[0] == NULL ? NULL : environment, - .environment_count = environment[0] == NULL ? 0 : 1, - .environment_present = environment[0] != NULL, - .inherit_default_env = - environment[0] != NULL && getenv("HARNESS_LAYER_ENVIRONMENT") != NULL, - }}; - char *command[] = {{"/bin/sh", "-c", argv[1], NULL}}; - - if (argc != 2 && argc != 3) {{ - return 2; - }} - return run_exec(&session, &config, 42, 0, command, &exec_config) == 0 ? 0 : 1; -}} -""", - encoding="utf-8", - ) - flags = [ - flag - for flag in InitramfsBuildConstants.STATIC_HELPER_CFLAGS - if flag != "-static" - ] - result = subprocess.run( - [compiler, *flags, "-o", str(self.harness), str(source)], - capture_output=True, - text=True, - timeout=60, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - self.environment = { - **os.environ, - "PATH": f"{tools}{os.pathsep}{os.environ.get('PATH', '/usr/bin:/bin')}", - } - for name in ( - "HARNESS_BEFORE_LAUNCH", - "HARNESS_ENVIRONMENT", - "HARNESS_FAIL_PWD", - "HARNESS_FAIL_SETGROUPS", - "HARNESS_LAYER_ENVIRONMENT", - ): - self.environment.pop(name, None) + ensure_symlink.assert_any_call(root, "usr/bin/wget", "busybox") - def _exec( - self, - script: str, - cwd: str | None = None, - environment: dict[str, str] | None = None, - ) -> list[tuple[int, int, int, bytes]]: - """Runs one workload; returns the kind, request ID, status, and payload of each frame.""" - arguments = [str(self.harness), script] - if cwd is not None: - arguments.append(cwd) - result = subprocess.run( - arguments, - capture_output=True, - timeout=30, - env=environment or self.environment, - check=False, - ) - self.assertEqual(result.returncode, 0, result.stderr) - frames: list[tuple[int, int, int, bytes]] = [] - rest = result.stdout - while rest: - _, _, record_type, _, _, _, _, length = self.OUTER.unpack( - rest[: self.OUTER.size] - ) - payload = rest[self.OUTER.size : self.OUTER.size + length] - rest = rest[self.OUTER.size + length :] - self.assertEqual(record_type, 5) - _, _, kind, _, request_id, status, payload_len = self.APP.unpack( - payload[: self.APP.size] - ) - body = payload[self.APP.size :] - self.assertEqual(payload_len, len(body)) - frames.append((kind, request_id, status, body)) - return frames - - def _completed(self, frames: list[tuple[int, int, int, bytes]]) -> bytes: - """Returns the standard output of a workload that exited with status zero.""" - self.assertEqual(frames[-1], (0x84, 42, 0, b"exit"), frames) - self.assertTrue(all(kind == 0x82 for kind, *_ in frames[:-1]), frames) - return b"".join(body for *_, body in frames[:-1]) - - def _refused( - self, frames: list[tuple[int, int, int, bytes]], cwd: str, error: int - ) -> None: - diagnostic = ( - f"nvx-managed-agent: cannot enter working directory {cwd}: " - f"{os.strerror(error)}\n" - ).encode() - self.assertEqual( - frames, - [(0x83, 42, 0, diagnostic), (0xFF, 42, error, b"cwd-failed")], - ) + def test_distro_layer_refuses_existing_output_without_replace(self): + with tempfile.TemporaryDirectory() as temporary: + output = Path(temporary) / "ubuntu.erofs" + output.touch() + with ( + patch.object(build.sys, "platform", "linux"), + self.assertRaisesRegex(common.ScriptError, "refusing to replace"), + ): + build.build_distro_layer( + build_config.DistroLayerBuildConfig(output=output) + ) - def test_workloads_start_in_the_root_directory_by_default(self): - output = self._completed(self._exec('pwd; printf "%s\\n" "$PWD"')) - self.assertEqual(output, b"/\n/\n") + def test_ubuntu_initramfs_manifest_binds_artifact_digest(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + output = root / "initramfs-ubuntu.cpio.gz" + output.write_bytes(b"ubuntu initramfs") + with patch.object( + ubuntu, + "package_manifest", + return_value={"format": 1, "guest": "ubuntu"}, + ): + build._write_ubuntu_manifest(root, output, {}, "a" * 64) - def test_workloads_start_in_the_requested_directory(self): - for name in ("a", "b"): - directory = self.root / "work" / name - directory.mkdir(parents=True) - with self.subTest(directory=directory): - output = self._completed( - self._exec('pwd; printf "%s\\n" "$PWD"', str(directory)) + manifest = json.loads( + output.with_name(f"{output.name}.packages.json").read_text( + encoding="utf-8" ) - self.assertEqual(output, f"{directory}\n{directory}\n".encode()) - - def test_pwd_keeps_the_requested_name_of_a_canonical_path(self): - target = self.root / "target" - target.mkdir() - link = self.root / "link" - link.symlink_to(target) - output = self._completed(self._exec("pwd; /bin/pwd -P", str(link))) - self.assertEqual(output, f"{link}\n{target}\n".encode()) - # Without a canonical name, PWD names the directory itself. - output = self._completed(self._exec('printf "%s\\n" "$PWD"', f"{link}/.")) - self.assertEqual(output, f"{target}\n".encode()) - - def test_unusable_directories_refuse_the_launch(self): - marker = self.root / "ran" - regular = self.root / "file" - regular.touch() - cases = [ - (self.root / "missing", errno.ENOENT), - (regular, errno.ENOTDIR), - ] - # Root may search any directory, so only an unprivileged user observes EACCES. - if sys.platform == "linux" and os.geteuid() != 0: - locked = self.root / "locked" - locked.mkdir(mode=0) - self.addCleanup(locked.chmod, 0o755) - cases.append((locked, errno.EACCES)) - for directory, error in cases: - with self.subTest(directory=directory): - frames = self._exec(f"touch {marker}", str(directory)) - self._refused(frames, str(directory), error) - self.assertFalse(marker.exists()) - - def test_identity_failures_refuse_the_launch(self): - environment = {**self.environment, "HARNESS_FAIL_SETGROUPS": "1"} - frames = self._exec(f"touch {self.root / 'ran'}", None, environment) - self.assertEqual(frames, [(0xFF, 42, 125, b"launch-failed")]) - self.assertFalse((self.root / "ran").exists()) - - def test_workloads_start_in_the_directory_that_was_checked(self): - checked = self.root / "checked" - retarget = self.root / "retarget" - renamed = self.root / "renamed" - checked.mkdir() - retarget.mkdir() - link = self.root / "link" - link.symlink_to(checked) - # Neither a retargeted link nor a renamed directory may fail the - # launch or move the workload to a directory that was not checked. - quote = shlex.quote - for cwd, change, expected in ( - (link, f"ln -sfn {quote(str(retarget))} {quote(str(link))}", checked), - (retarget, f"mv {quote(str(retarget))} {quote(str(renamed))}", renamed), - ): - with self.subTest(cwd=cwd): - environment = {**self.environment, "HARNESS_BEFORE_LAUNCH": change} - frames = self._exec("/bin/pwd -P", str(cwd), environment) - self.assertEqual(self._completed(frames), f"{expected}\n".encode()) - - def test_pwd_failures_refuse_the_launch(self): - marker = self.root / "ran" - environment = {**self.environment, "HARNESS_FAIL_PWD": "1"} - for cwd in (None, str(self.root)): - with self.subTest(cwd=cwd): - frames = self._exec(f"touch {marker}", cwd, environment) - self.assertEqual(frames, [(0xFF, 42, 125, b"launch-failed")]) - self.assertFalse(marker.exists()) - - def test_exact_environments_control_pwd(self): - # The agent leaves PWD to a request that replaces the environment, so - # it does not try to set PWD, and cannot fail to. - environment = { - **self.environment, - "HARNESS_ENVIRONMENT": "MODE=exact", - "HARNESS_FAIL_PWD": "1", - } - output = self._completed( - self._exec( - 'printf "%s\\n" "$MODE"; /bin/pwd -P', str(self.root), environment - ) - ) - self.assertEqual(output, f"exact\n{self.root}\n".encode()) - - def test_layered_environments_keep_the_agents_pwd(self): - # Entries layered over the default environment keep the PWD that the - # agent points at the directory, which, unlike a PWD that the shell - # computes, names a link as the request did. - target = self.root / "target" - target.mkdir() - link = self.root / "link" - link.symlink_to(target) - layered = { - **self.environment, - "HARNESS_ENVIRONMENT": "MODE=layered", - "HARNESS_LAYER_ENVIRONMENT": "1", - } - output = self._completed( - self._exec('printf "%s %s\\n" "$MODE" "$PWD"', str(link), layered) - ) - self.assertEqual(output, f"layered {link}\n".encode()) - frames = self._exec("true", str(link), {**layered, "HARNESS_FAIL_PWD": "1"}) - self.assertEqual(frames, [(0xFF, 42, 125, b"launch-failed")]) - + ) + self.assertEqual(manifest["artifact"], output.name) + self.assertEqual( + manifest["artifact_sha256"], + common.sha256_file(output), + ) + self.assertEqual(manifest["input_sha256"], "a" * 64) -class AciSandboxRunnerTests(unittest.TestCase): - def _artifacts(self, root: Path) -> dict[str, Path]: - names = { - KernelBuildConstants.BINARY_NAME: b"kernel", - AlpineBuildConstants.INITRAMFS_NAME: b"initrd", - "openvmm": b"vmm", - } - paths: dict[str, Path] = {} - for name, contents in names.items(): - paths[name] = root / name - paths[name].write_bytes(contents) - return paths + def test_ubuntu_source_requirements_deduplicate_binary_manifests(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + manifests: list[Path] = [] + for name in ("initramfs.json", "layer.json"): + path = root / name + path.write_text( + json.dumps( + { + "guest": "ubuntu", + "release": UbuntuBuildConstants.VERSION, + "architecture": UbuntuBuildConstants.ARCHITECTURE, + "packages": [ + { + "source_name": "glibc", + "source_version": "2.43-2ubuntu2.3", + } + ], + } + ), + encoding="utf-8", + ) + manifests.append(path) - def _environment(self, root: Path, paths: dict[str, Path]) -> dict[str, str]: - def artifact(name: str) -> Path: - return root / name + requirements = collect_ubuntu_sources._source_requirements(manifests) - def openvmm() -> Path: - return paths["openvmm"] + self.assertEqual( + requirements, + ( + { + "source_name": "glibc", + "source_version": "2.43-2ubuntu2.3", + }, + ), + ) - with ( - patch.object(aci_edge_sandboxes_tests, "artifact_path", artifact), - patch.object(aci_edge_sandboxes_tests, "openvmm_binary_path", openvmm), - ): - return aci_edge_sandboxes_tests.e2e_environment( - "kvm", root / "state", root / "out" + def test_ubuntu_dsc_validation_matches_source_index(self): + payload = b"source" + digest = hashlib.sha256(payload).hexdigest() + with tempfile.TemporaryDirectory() as temporary: + dsc = Path(temporary) / "example_1.0.dsc" + dsc.write_text( + ( + "Format: 3.0 (quilt)\n" + "Source: example\n" + "Version: 1.0\n" + "Checksums-Sha256:\n" + f" {digest} {len(payload)} example_1.0.orig.tar.xz\n" + ), + encoding="utf-8", ) + record: collect_ubuntu_sources.SourceRecord = { + "source_name": "example", + "source_version": "1.0", + "directory": "pool/main/e/example", + "index_url": "https://archive.invalid/Sources.xz", + "index_sha256": "0" * 64, + "index_release_url": "https://archive.invalid/InRelease", + "index_release_sha256": "1" * 64, + "files": [ + { + "name": "example_1.0.dsc", + "size": dsc.stat().st_size, + "sha256": common.sha256_file(dsc), + "url": "https://archive.invalid/example_1.0.dsc", + }, + { + "name": "example_1.0.orig.tar.xz", + "size": len(payload), + "sha256": digest, + "url": "https://archive.invalid/example_1.0.orig.tar.xz", + }, + ], + } - def test_cli_registers_the_lifecycle_test(self): - args = nvx.parse_args(["test-aci-edge-sandboxes", "--backend", "mshv"]) + collect_ubuntu_sources._validate_dsc(dsc, record) + record["files"][1]["sha256"] = "f" * 64 + with self.assertRaisesRegex( + common.ScriptError, + "do not match", + ): + collect_ubuntu_sources._validate_dsc(dsc, record) - self.assertEqual(args.backend, "mshv") - self.assertEqual(args.cargo, "cargo") - self.assertFalse(hasattr(args, "scratch_template")) - self.assertIs( - args.handler, aci_edge_sandboxes_tests.command_test_aci_edge_sandboxes + def test_ubuntu_source_index_is_authenticated_by_signed_release(self): + dsc_payload = b"dsc" + source_payload = b"source" + sources = ( + "Package: example\n" + "Version: 1.0\n" + "Directory: pool/main/e/example\n" + "Checksums-Sha256:\n" + f" {hashlib.sha256(dsc_payload).hexdigest()} " + f"{len(dsc_payload)} example_1.0.dsc\n" + f" {hashlib.sha256(source_payload).hexdigest()} " + f"{len(source_payload)} example_1.0.orig.tar.xz\n" + ).encode() + compressed = lzma.compress(sources) + index_sha256 = hashlib.sha256(compressed).hexdigest() + inrelease = ( + "-----BEGIN PGP SIGNED MESSAGE-----\n" + "Hash: SHA256\n" + "\n" + "Origin: Ubuntu\n" + f"Codename: {UbuntuBuildConstants.CODENAME}\n" + f"Suite: {UbuntuBuildConstants.CODENAME}\n" + "SHA256:\n" + f" {index_sha256} {len(compressed)} main/source/Sources.xz\n" + "-----BEGIN PGP SIGNATURE-----\n" + "test\n" + "-----END PGP SIGNATURE-----\n" ) - def test_environment_resolves_repository_artifacts(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - paths = self._artifacts(root) - environment = self._environment(root, paths) + def download_release(_url: str, destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(inrelease, encoding="utf-8") - self.assertEqual(environment["ACI_EDGE_SANDBOXES_E2E_HYPERVISOR"], "kvm") - self.assertEqual( - Path(environment["ACI_EDGE_SANDBOXES_E2E_OPENVMM"]), - paths["openvmm"].resolve(), - ) - self.assertEqual( - Path(environment["ACI_EDGE_SANDBOXES_E2E_INITRD"]), - paths[AlpineBuildConstants.INITRAMFS_NAME].resolve(), - ) - self.assertEqual( - Path(environment["ACI_EDGE_SANDBOXES_E2E_STATE_ROOT"]), - (root / "state").resolve(), - ) - self.assertFalse( - any( - name.startswith("ACI_EDGE_SANDBOXES_E2E_DISTRO") - for name in environment - ) - ) - self.assertNotIn("ACI_EDGE_SANDBOXES_E2E_SCRATCH", environment) + def download_index( + _url: str, + destination: Path, + expected_sha256: str, + ) -> None: + self.assertEqual(expected_sha256, index_sha256) + destination.write_bytes(compressed) - def test_environment_requires_the_alpine_initramfs(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) - paths = self._artifacts(root) - paths[AlpineBuildConstants.INITRAMFS_NAME].unlink() - with self.assertRaisesRegex(common.ScriptError, "Alpine initramfs"): - self._environment(root, paths) - - def test_command_runs_only_the_ignored_lifecycle_test(self): - command = aci_edge_sandboxes_tests.e2e_command("cargo") + keyring = root / "ubuntu-archive-keyring.gpg" + keyring.write_bytes(b"keyring") + with ( + patch.object( + collect_ubuntu_sources, + "download", + side_effect=download_release, + ), + patch.object( + collect_ubuntu_sources, + "download_verified", + side_effect=download_index, + ), + patch.object( + collect_ubuntu_sources, + "require_tool", + return_value="gpgv", + ), + patch.object(collect_ubuntu_sources, "run_checked") as run_checked, + ): + records, metadata = ( + collect_ubuntu_sources._load_authenticated_source_index( + root / "cache", + keyring, + "https://archive.invalid/ubuntu", + UbuntuBuildConstants.CODENAME, + "main", + ) + ) - self.assertEqual(command[:2], ["cargo", "test"]) + run_checked.assert_called_once() + self.assertEqual(run_checked.call_args.args[0][0], "gpgv") + self.assertIn("--homedir", run_checked.call_args.args[0]) + record = records[("example", "1.0")] self.assertEqual( - Path(command[command.index("--manifest-path") + 1]), - BuildConstants.REPO_ROOT / "aci_edge_sandboxes" / "Cargo.toml", + record["index_release_url"], + "https://archive.invalid/ubuntu/dists/resolute/InRelease", + ) + self.assertEqual( + [item["role"] for item in metadata], + ["ubuntu-inrelease", "ubuntu-source-index"], + ) + self.assertEqual( + metadata[1]["authenticated_by"], + record["index_release_url"], ) - self.assertIn("--locked", command) - self.assertEqual(command[command.index("--test") + 1], "openvmm_e2e") - self.assertEqual(command[-2:], ["--ignored", "--nocapture"]) - - def test_handler_returns_the_test_status(self): - observed: list[str] = [] - def fake_run( - command: list[str], *, env: dict[str, str], check: bool - ) -> subprocess.CompletedProcess[bytes]: - self.assertFalse(check) - self.assertEqual(command, aci_edge_sandboxes_tests.e2e_command("cargo")) - observed.append(env["ACI_EDGE_SANDBOXES_E2E_STATE_ROOT"]) - return subprocess.CompletedProcess(command, 3) + def test_launchpad_source_record_retains_verifiable_raw_metadata(self): + source_payload = b"source archive" + source_sha256 = hashlib.sha256(source_payload).hexdigest() + self_link = f"{UbuntuBuildConstants.LAUNCHPAD_ARCHIVE_API}/+sourcepub/123" + query = { + "entries": [ + { + "source_package_name": "example", + "source_package_version": "1.0", + "status": "Superseded", + "date_published": "2026-01-01T00:00:00Z", + "date_superseded": "2026-01-03T00:00:00Z", + "pocket": "Updates", + "component_name": "main", + "self_link": self_link, + } + ] + } + source_urls = [ + "https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/" + "example/1.0/example_1.0.dsc", + "https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/" + "example/1.0/example_1.0.orig.tar.xz", + ] - def fake_environment( - backend: str, state_root: Path, output_dir: Path - ) -> dict[str, str]: - self.assertEqual(backend, "whp") - return {"ACI_EDGE_SANDBOXES_E2E_STATE_ROOT": os.fspath(state_root)} + def download_json(url: str, destination: Path) -> object: + document: object = source_urls if "sourceFileUrls" in url else query + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text( + json.dumps(document, separators=(",", ":")) + "\n", + encoding="utf-8", + ) + return document with tempfile.TemporaryDirectory() as temporary: - args = argparse.Namespace( - backend="whp", - output_dir=Path(temporary) / "results", - cargo="cargo", - ) + root = Path(temporary) + snapshot_release = root / "cache" / "snapshot.InRelease" + snapshot_release.parent.mkdir(parents=True) + snapshot_release.write_text("signed release", encoding="utf-8") + snapshot_record: collect_ubuntu_sources.SourceRecord = { + "source_name": "example", + "source_version": "1.0", + "directory": "pool/main/e/example", + "index_url": ( + "https://snapshot.ubuntu.com/ubuntu/20260102T000000Z/" + "dists/resolute-updates/main/source/Sources.xz" + ), + "index_sha256": "2" * 64, + "index_release_url": ( + "https://snapshot.ubuntu.com/ubuntu/20260102T000000Z/" + "dists/resolute-updates/InRelease" + ), + "index_release_sha256": common.sha256_file(snapshot_release), + "files": [ + { + "name": "example_1.0.dsc", + "size": 3, + "sha256": "4" * 64, + "url": ( + "https://snapshot.ubuntu.com/ubuntu/" + "20260102T000000Z/pool/main/e/example/" + "example_1.0.dsc" + ), + }, + { + "name": "example_1.0.orig.tar.xz", + "size": len(source_payload), + "sha256": source_sha256, + "url": ( + "https://snapshot.ubuntu.com/ubuntu/" + "20260102T000000Z/pool/main/e/example/" + "example_1.0.orig.tar.xz" + ), + }, + ], + } + snapshot_metadata: list[collect_ubuntu_sources.SourceMetadata] = [ + { + "role": "ubuntu-inrelease", + "url": snapshot_record["index_release_url"], + "sha256": snapshot_record["index_release_sha256"], + "cache_path": snapshot_release, + "output_name": "snapshot.InRelease", + "authenticated_by": (UbuntuBuildConstants.ARCHIVE_KEYRING_URL), + }, + { + "role": "ubuntu-source-index", + "url": snapshot_record["index_url"], + "sha256": snapshot_record["index_sha256"], + "cache_path": None, + "output_name": None, + "authenticated_by": snapshot_record["index_release_url"], + }, + ] with ( - patch.object(aci_edge_sandboxes_tests, "validate_openvmm_test_backend"), patch.object( - aci_edge_sandboxes_tests, "e2e_environment", fake_environment + collect_ubuntu_sources, + "_download_json", + side_effect=download_json, ), - patch.object(aci_edge_sandboxes_tests.subprocess, "run", fake_run), - ): - self.assertEqual( - aci_edge_sandboxes_tests.command_test_aci_edge_sandboxes(args), 3 - ) - self.assertTrue((Path(temporary) / "results").is_dir()) - - self.assertEqual(len(observed), 1) - self.assertEqual(Path(observed[0]).name, "state") - - def _run_leaving( - self, records: dict[str, object] | None, *, interrupt: bool = False - ) -> tuple[Path, str]: - """Runs the handler with a test command that leaves one sandbox provisioned. - - The sandbox gets `records` as extra state files; `None` leaves only the lock - directories that deprovisioning keeps. Returns the state root that the command - used and the handler's standard error. - """ - state_roots: list[Path] = [] - - def fake_environment( - backend: str, state_root: Path, output_dir: Path - ) -> dict[str, str]: - state_roots.append(state_root) - return {} - - def fake_run( - command: list[str], *, env: dict[str, str], check: bool - ) -> subprocess.CompletedProcess[bytes]: - (state_roots[0] / "filesystem" / ".locks").mkdir(parents=True) - if records is not None: - sandbox = state_roots[0] / "network" / ("a" * 32) - sandbox.mkdir(parents=True) - (sandbox / "sandbox.json").write_text("{}", encoding="utf-8") - for name, record in records.items(): - (sandbox / name).write_text(json.dumps(record), encoding="utf-8") - if interrupt: - raise KeyboardInterrupt - return subprocess.CompletedProcess(command, 0) - - with tempfile.TemporaryDirectory() as temporary: - args = argparse.Namespace( - backend="kvm", output_dir=Path(temporary) / "results", cargo="cargo" - ) - with ( - patch.object(aci_edge_sandboxes_tests, "validate_openvmm_test_backend"), patch.object( - aci_edge_sandboxes_tests, "e2e_environment", fake_environment + collect_ubuntu_sources, + "_load_authenticated_source_index", + return_value=( + {("example", "1.0"): snapshot_record}, + snapshot_metadata, + ), ), - patch.object(aci_edge_sandboxes_tests.subprocess, "run", fake_run), - patch("sys.stderr", io.StringIO()) as stderr, ): - if interrupt: - with self.assertRaises(KeyboardInterrupt): - aci_edge_sandboxes_tests.command_test_aci_edge_sandboxes(args) - else: - self.assertEqual( - aci_edge_sandboxes_tests.command_test_aci_edge_sandboxes(args), - 0, - ) - state_root = state_roots[0] - if state_root.parent.exists(): - self.addCleanup(shutil.rmtree, state_root.parent, True) - return state_root, stderr.getvalue() - - def test_handler_deletes_state_once_every_sandbox_is_deprovisioned(self): - state_root, stderr = self._run_leaving(None) - - self.assertFalse(state_root.parent.exists()) - self.assertEqual(stderr, "") - - def test_handler_keeps_state_of_sandboxes_left_provisioned(self): - state_root, stderr = self._run_leaving({"runtime.json": {"pid": 4242}}) - - sandbox = state_root / "network" / ("a" * 32) - self.assertTrue((sandbox / "runtime.json").is_file()) - self.assertIn(f"kept {state_root}", stderr) - self.assertIn(f"{sandbox} (OpenVMM pid 4242)", stderr) - - def test_handler_keeps_state_when_the_test_is_interrupted(self): - state_root, stderr = self._run_leaving( - {"launch.json": {"process": {"pid": 77}}}, interrupt=True - ) - - sandbox = state_root / "network" / ("a" * 32) - self.assertTrue((sandbox / "launch.json").is_file()) - self.assertIn(f"{sandbox} (OpenVMM pid 77)", stderr) - - -def _write_managed_runtime(state: Path, pid: int, start_time: int | None) -> None: - runtime: dict[str, object] = { - "format": sandbox_lifecycle.STATE_FORMAT, - "pid": pid, - "control_endpoint": os.fspath(state / sandbox_lifecycle.CONTROL_SOCKET_NAME), - } - if start_time is not None: - runtime["start_time"] = start_time - sandbox_lifecycle._write_json(state / sandbox_lifecycle.RUNTIME_NAME, runtime) - (state / sandbox_lifecycle.CAPABILITY_NAME).write_bytes(b"x" * 32) - - -def _provision_managed_sandbox(root: Path) -> Path: - layer_path = root / "distro.erofs" - scratch_path = root / "scratch.ext4" - layer_path.write_bytes(b"layer") - scratch_path.write_bytes(b"scratch") - state = root / "state" - sandbox_lifecycle.provision( - state, - sandbox.SandboxLaunch( - layers=( - sandbox.SandboxLayer( - role="distro", - path=layer_path, - uuid="11111111-1111-1111-1111-111111111111", - ), - ), - scratch=scratch_path, - ), - hypervisor="whp", - memory_mib=256, - net=None, - network_profile=None, - network_egress=None, - network_ingress=None, - network_egress_allow=(), - network_egress_deny=(), - host_loopback=None, - network_proxy=None, - host_loopback_forward=(), - cmdline="quiet", - timeout=10, - ) - return state - - -def _stdin_bound_child() -> subprocess.Popen[bytes]: - return subprocess.Popen( - [sys.executable, "-c", "import sys; sys.stdin.read()"], - stdin=subprocess.PIPE, - ) - - -def _finish_child(child: subprocess.Popen[bytes]) -> None: - if child.stdin is not None: - child.stdin.close() - child.wait(timeout=30) - - -def _exit_unreaped(child: subprocess.Popen[bytes]) -> None: - if sys.platform != "linux": - raise unittest.SkipTest("zombie processes require a Linux host") - assert child.stdin is not None - child.stdin.close() - # WNOWAIT leaves the exited child unreaped, so it stays a zombie. - os.waitid(os.P_PID, child.pid, os.WEXITED | os.WNOWAIT) - - -def _end_child(child: subprocess.Popen[bytes]) -> None: - if child.poll() is None: - child.kill() - child.wait(timeout=30) + record, metadata = collect_ubuntu_sources._launchpad_source_record( + root / "cache", + root / "ubuntu-archive-keyring.gpg", + "example", + "1.0", + ) + self.assertEqual(record, snapshot_record) + self.assertEqual( + [item["role"] for item in metadata], + [ + "ubuntu-inrelease", + "ubuntu-source-index", + "launchpad-publishing-history", + "launchpad-source-file-urls", + ], + ) + for item in metadata: + if item["cache_path"] is None: + continue + self.assertEqual( + item["sha256"], + common.sha256_file(item["cache_path"]), + ) -def _openvmm_mock(status: int | None) -> MagicMock: - """Mocks an OpenVMM process that exits on its own with `status`. + output = root / "output" + output.mkdir() + manifest_metadata = collect_ubuntu_sources._materialize_source_metadata( + output, + metadata, + ) + for item, manifest_item in zip(metadata, manifest_metadata, strict=True): + if item["cache_path"] is None: + self.assertNotIn("path", manifest_item) + continue + retained = output / manifest_item["path"] + self.assertEqual( + retained.read_bytes(), + item["cache_path"].read_bytes(), + ) + self.assertEqual( + common.sha256_file(retained), + manifest_item["sha256"], + ) - With None, it runs until it is terminated and then exits with status 1. - """ - process = MagicMock() - process.pid = 123 - process.stdin = io.BytesIO() - process.poll.return_value = status + def test_ubuntu_source_collection_rejects_unexpected_output_entries(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + output = root / "output" + output.mkdir() + stale = output / "unrelated.txt" + stale.write_text("do not archive", encoding="utf-8") - def wait(timeout: float) -> int: - if process.terminate.called: - return 1 - if status is None: - raise subprocess.TimeoutExpired("openvmm", timeout) - return status + with self.assertRaisesRegex( + common.ScriptError, + "unexpected entries: unrelated.txt", + ): + collect_ubuntu_sources.collect_ubuntu_sources( + [], + output, + root / "cache", + ) - process.wait.side_effect = wait - return process + self.assertEqual(stale.read_text(encoding="utf-8"), "do not archive") + def test_ubuntu_source_collection_validates_output_before_resolving(self): + output = MagicMock(spec=Path) + with patch.object( + collect_ubuntu_sources, + "_validate_source_output", + ) as validate: -# Stands in for OpenVMM when the managed guest exits during startup. Like OpenVMM, -# it closes the control endpoint before it publishes its outcome report through a -# staging file, and a slow flush keeps that file in place for a second. It takes -# the size of the attach request and then OpenVMM's arguments. -_EXITING_OPENVMM = r""" -import json -import os -import socket -import sys -import tempfile -import time + def resolve() -> Path: + validate.assert_called_once_with(output) + raise RuntimeError("stop after ordering assertion") -attach_size = int(sys.argv[1]) -arguments = sys.argv[2:] -endpoint = arguments[arguments.index("--microvm-control-console") + 1] -endpoint = endpoint.removeprefix("listen=") -report = arguments[arguments.index("--microvm-report") + 1] -sys.stdin.buffer.read() -received = 0 -if os.name == "nt": - import _winapi - - pipe = _winapi.CreateNamedPipe( - endpoint.replace("/", "\\"), - _winapi.PIPE_ACCESS_DUPLEX, - _winapi.PIPE_WAIT, - 1, - 65536, - 65536, - 0, - _winapi.NULL, - ) - try: - _winapi.ConnectNamedPipe(pipe, False) - except OSError as error: - if error.winerror != _winapi.ERROR_PIPE_CONNECTED: - raise - while received < attach_size: - data, _ = _winapi.ReadFile(pipe, attach_size - received) - received += len(data) - _winapi.CloseHandle(pipe) -else: - listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - listener.bind(endpoint) - listener.listen(1) - connection, _ = listener.accept() - while received < attach_size: - chunk = connection.recv(attach_size - received) - if not chunk: - sys.exit("the control client closed before it attached") - received += len(chunk) - connection.close() - listener.close() -report_document = json.dumps( - { - "schema_version": 1, - "outcome": { - "operation": "managed", - "category": "guest-exit", - "status_code": 125, - }, - "network_policy": {"status": "not-requested"}, - "teardown": {"vm_stopped": True}, - } -).encode() -staging = tempfile.NamedTemporaryFile( - dir=os.path.dirname(report), prefix=".tmp", delete=False -) -staging.write(report_document[: len(report_document) // 2]) -staging.flush() -time.sleep(1) -staging.write(report_document[len(report_document) // 2 :]) -staging.close() -os.rename(staging.name, report) -sys.exit(125) -""" + output.resolve.side_effect = resolve + with self.assertRaisesRegex(RuntimeError, "ordering assertion"): + collect_ubuntu_sources.collect_ubuntu_sources( + [], + output, + Path("cache"), + ) -# Stands in for the OpenVMM of a managed sandbox that runs until it is stopped. -# Like OpenVMM, it reads its capability from standard input, serves the control -# endpoint one client at a time, rejects an attach with another capability, -# acknowledges a ping, and on a stop publishes its outcome report and exits. -_SERVING_OPENVMM = r""" -import json -import os -import secrets -import socket -import struct -import sys + def test_ci_kernel_cache_key_includes_patches(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "build-guest-artifacts" + / "action.yml" + ).read_text(encoding="utf-8") + for cache_input in ( + "SOURCE-MANIFEST.json", + "kernel/config-microvm", + "kernel/patches/**", + "scripts/nvx_tools/build.py", + "scripts/nvx_tools/common.py", + ): + self.assertIn(cache_input, action) + self.assertIn("linux-kernel-v1-", action) + self.assertEqual(action.count("build/vmlinux.provenance.json"), 2) + self.assertEqual(action.count("build/initramfs.provenance.json"), 2) -OUTER = struct.Struct("<4sHBB16sQQI") -APP = struct.Struct("<4sBBHQiI") -arguments = sys.argv[1:] -endpoint = arguments[arguments.index("--microvm-control-console") + 1] -endpoint = endpoint.removeprefix("listen=") -report = arguments[arguments.index("--microvm-report") + 1] -capability = sys.stdin.buffer.read() -instance = secrets.token_bytes(16) -if os.name == "nt": - import _winapi - - def accept(): - pipe = _winapi.CreateNamedPipe( - endpoint.replace("/", "\\"), - _winapi.PIPE_ACCESS_DUPLEX, - _winapi.PIPE_WAIT, - 1, - 65536, - 65536, - 0, - _winapi.NULL, - ) - try: - _winapi.ConnectNamedPipe(pipe, False) - except OSError as error: - if error.winerror != _winapi.ERROR_PIPE_CONNECTED: - raise - return pipe + def test_kernel_patch_identity_follows_patch_paths_and_order(self): + # A content-only hash ignores names, so a rename reused a kernel whose + # provenance named the old patch. The identity must see paths and order. + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / "0001-a.patch").write_bytes(b"same bytes\n") + (root / "0002-b.patch").write_bytes(b"other bytes\n") + original = ci.kernel_patch_identity(root) + self.assertEqual(original, ci.kernel_patch_identity(root)) - def receive(pipe, length): - return _winapi.ReadFile(pipe, length)[0] + (root / "0001-a.patch").rename(root / "0001-renamed.patch") + self.assertNotEqual(original, ci.kernel_patch_identity(root)) - def send(pipe, data): - _winapi.WriteFile(pipe, data) + renamed = ci.kernel_patch_identity(root) + (root / "0001-renamed.patch").rename(root / "0003-renamed.patch") + self.assertNotEqual(renamed, ci.kernel_patch_identity(root)) - def close(pipe): - _winapi.CloseHandle(pipe) + def test_kernel_patch_identity_rejects_missing_or_empty_patch_directories(self): + # An empty digest would stop following patch renames without an error. + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + with self.assertRaisesRegex(common.ScriptError, "no kernel patches"): + ci.kernel_patch_identity(root / "missing") + with self.assertRaisesRegex(common.ScriptError, "no kernel patches"): + ci.kernel_patch_identity(root) -else: - listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - listener.bind(endpoint) - listener.listen(1) + def test_kernel_patch_identity_ignores_files_the_kernel_build_does_not_read(self): + # Provenance hashes only top-level *.patch files, so the key must match. + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / "0001-a.patch").write_bytes(b"patch\n") + original = ci.kernel_patch_identity(root) + (root / "README.md").write_bytes(b"notes\n") + (root / "nested").mkdir() + (root / "nested" / "0002-b.patch").write_bytes(b"nested\n") + self.assertEqual(original, ci.kernel_patch_identity(root)) - def accept(): - return listener.accept()[0] + def test_kernel_patch_identity_changes_when_provenance_would_reject_the_kernel( + self, + ): + # Renaming a patch changes the provenance source, so the identity must + # change with it and the stale provenance must fail validation. + with tempfile.TemporaryDirectory() as temporary: + tree = Path(temporary) + patches = tree / KernelBuildConstants.PATCH_DIRECTORY + patches.mkdir(parents=True) + (tree / KernelBuildConstants.INPUT_CONFIG).parent.mkdir( + parents=True, exist_ok=True + ) + (tree / KernelBuildConstants.INPUT_CONFIG).write_bytes(b"CONFIG_X=y\n") + (patches / "0001-a.patch").write_bytes(b"same bytes\n") + (patches / "0002-b.patch").write_bytes(b"other bytes\n") + kernel = tree / "vmlinux" + kernel.write_bytes(b"kernel\n") + kernel_config = tree / "kernel.config" + kernel_config.write_bytes(b"CONFIG_X=y\n") + provenance_path = tree / "provenance.json" - def receive(connection, length): - return connection.recv(length) + with ( + patch.object(BuildConstants, "REPO_ROOT", tree), + patch.object(release, "assert_required_kernel_config"), + ): + stale_identity = ci.kernel_patch_identity(patches) + provenance_path.write_text( + json.dumps( + { + "format": KernelBuildConstants.PROVENANCE_FORMAT, + **build.kernel_provenance_inputs(), + "kernel_sha256": release.sha256_file(kernel), + "config_sha256": release.sha256_file(kernel_config), + } + ), + encoding="utf-8", + ) + release._validate_kernel_provenance( + kernel, kernel_config, provenance_path + ) - def send(connection, data): - connection.sendall(data) + (patches / "0001-a.patch").rename(patches / "0003-a.patch") - def close(connection): - connection.close() + self.assertNotEqual(stale_identity, ci.kernel_patch_identity(patches)) + with self.assertRaisesRegex(common.ScriptError, "does not match"): + release._validate_kernel_provenance( + kernel, kernel_config, provenance_path + ) + def test_ci_guest_kernel_keys_include_the_patch_identity(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "build-guest-artifacts" + / "action.yml" + ).read_text(encoding="utf-8") + self.assertIn("kernel-patch-identity", action) + self.assertIn( + "linux-kernel-v1-${KERNEL_VERSION}-${KERNEL_INPUT_HASH}-${patch_identity}", + action, + ) + self.assertIn( + "linux-kernel-debug-v1-${KERNEL_VERSION}-${DEBUG_KERNEL_INPUT_HASH}-${patch_identity}", + action, + ) -def read_exact(connection, length): - data = b"" - while len(data) < length: - chunk = receive(connection, length - len(data)) - if not chunk: - raise EOFError - data += chunk - return data + def test_ci_guest_image_keys_include_the_rust_source_identity(self): + # hashFiles hashes contents only, so a module move that kept every + # file's bytes reused images whose provenance named the old paths. + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "build-guest-artifacts" + / "action.yml" + ).read_text(encoding="utf-8") + self.assertIn( + "rust_source_identity=$(python3 scripts/nvx.py guest-binary-source-identity)", + action, + ) + for key in ( + "alpine=alpine-image-v2-${ALPINE_VERSION}-${ALPINE_INPUT_HASH}", + "ubuntu=ubuntu-initramfs-v2-${UBUNTU_VERSION}-${UBUNTU_INPUT_HASH}", + "ubuntu-distro=ubuntu-distro-v2-${UBUNTU_VERSION}-${UBUNTU_INPUT_HASH}", + "azurelinux=azurelinux-initramfs-v2-${AZURELINUX_VERSION}-${AZURELINUX_INPUT_HASH}", + ): + with self.subTest(key=key.split("=", 1)[0]): + self.assertIn(f'echo "{key}-${{rust_source_identity}}"', action) + def test_ci_guest_cache_keys_include_build_configuration(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "build-guest-artifacts" + / "action.yml" + ).read_text(encoding="utf-8") + cache_names = ( + "KERNEL_INPUT_HASH", + "DEBUG_KERNEL_INPUT_HASH", + "ALPINE_INPUT_HASH", + "UBUNTU_INPUT_HASH", + "AZURELINUX_INPUT_HASH", + ) + # Every artifact, both kernels included, builds inside the image that + # docker/Dockerfile pins, so a toolchain change must miss every cache. + self.assertEqual( + action.count("--file docker/Dockerfile"), + action.count("docker build"), + ) + for cache_name in cache_names: + with self.subTest(cache=cache_name): + cache_input = next( + line + for line in action.splitlines() + if line.strip().startswith(f"{cache_name}:") + ) + self.assertIn("'docker/Dockerfile'", cache_input) + self.assertIn("'scripts/nvx_tools/build_config.py'", cache_input) + self.assertIn("'scripts/nvx_tools/build_constants.py'", cache_input) -def read_record(connection): - header = OUTER.unpack(read_exact(connection, OUTER.size)) - return header[2], read_exact(connection, header[7]) if header[7] else b"" + def test_ci_azurelinux_cache_key_covers_build_input_digest(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "build-guest-artifacts" + / "action.yml" + ).read_text(encoding="utf-8") + cache_input = next( + line + for line in action.splitlines() + if line.strip().startswith("AZURELINUX_INPUT_HASH:") + ) + patterns = cache_input.split("'")[1::2] + self.assertIn("scripts/nvx_tools/azurelinux.py", patterns) + for path in azurelinux.input_files(): + relative = path.relative_to(BuildConstants.REPO_ROOT).as_posix() + with self.subTest(path=relative): + self.assertTrue( + any( + relative == pattern + or ( + pattern.endswith("/**") + and relative.startswith(pattern[:-2]) + ) + for pattern in patterns + ) + ) + def test_kernel_input_config_uses_canonical_lf_line_endings(self): + attributes = (BuildConstants.REPO_ROOT / ".gitattributes").read_text( + encoding="utf-8" + ) + for path in ( + KernelBuildConstants.INPUT_CONFIG, + KernelBuildConstants.DEBUG_CONFIG_FRAGMENT, + ): + with self.subTest(path=path): + self.assertIn( + f"{path.as_posix()} text eol=lf", + attributes.splitlines(), + ) -def write_record(connection, kind, sequence, payload=b""): - header = OUTER.pack(b"NVXS", 1, kind, 0, instance, 1, sequence, len(payload)) - send(connection, header + payload) + def test_ci_debug_kernel_is_opt_in_and_keyed_by_its_fragment(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "build-guest-artifacts" + / "action.yml" + ).read_text(encoding="utf-8") + cache_input = next( + line + for line in action.splitlines() + if line.strip().startswith("DEBUG_KERNEL_INPUT_HASH:") + ) + for path in ( + "kernel/config-microvm", + "kernel/config-microvm-debug", + "kernel/patches/**", + "scripts/nvx_tools/build.py", + "scripts/nvx_tools/build_constants.py", + ): + with self.subTest(path=path): + self.assertIn(f"'{path}'", cache_input) + kernel_input = next( + line + for line in action.splitlines() + if line.strip().startswith("KERNEL_INPUT_HASH:") + ) + self.assertNotIn("config-microvm-debug", kernel_input) + build_step = _composite_action_step(action, "Build Linux debug kernel (Docker)") + self.assertIn("inputs.debug-kernel == 'true'", build_step) + self.assertIn( + f"--target {DockerBuildConstants.DEBUG_KERNEL_TARGET}", build_step + ) + self.assertIn('default: "false"', action) + for name in DockerBuildConstants.DEBUG_KERNEL_ARTIFACT_NAMES: + with self.subTest(artifact=name): + self.assertEqual( + [line.strip() for line in action.splitlines()].count( + f"build/{name}" + ), + 2, + ) + def test_ci_guest_images_input_leaves_only_the_debug_kernel(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "build-guest-artifacts" + / "action.yml" + ).read_text(encoding="utf-8") + guard = "inputs.guest-images == 'true'" + self.assertIn(" guest-images:\n", action) + self.assertIn('default: "true"', action) + steps = [ + line.strip().removeprefix("- name: ") + for line in action.splitlines() + if line.startswith(" - name: ") + ] + debug_steps = {name for name in steps if "debug kernel" in name} + self.assertEqual(len(debug_steps), 3) + always = {"Resolve guest artifact versions", "Resolve guest cache keys"} + for name in steps: + with self.subTest(step=name): + step = _composite_action_step(action, name) + if name in debug_steps or name in always: + self.assertNotIn("guest-images", step) + else: + self.assertIn(guard, step) -while True: - connection = accept() - try: - kind, payload = read_record(connection) - if kind != 2 or payload != capability: - write_record(connection, 8, 0) - continue - write_record(connection, 7, 0) - sequence = 1 - while True: - request = APP.unpack(read_record(connection)[1][: APP.size]) - stopping = request[2] == 3 - if stopping: - with open(report, "w", encoding="utf-8") as output: - json.dump( - { - "schema_version": 1, - "outcome": { - "operation": "managed", - "category": "success", - "status_code": 0, - }, - "network_policy": {"status": "not-requested"}, - "teardown": {"vm_stopped": True}, - }, - output, - ) - reply = APP.pack(b"NVXC", 1, 0x85 if stopping else 0x81, 0, request[4], 0, 0) - write_record(connection, 5, sequence, reply) - sequence += 1 - if stopping: - sys.exit(0) - except (EOFError, OSError): - pass - finally: - close(connection) -""" + def test_ci_guest_cache_keys_include_shared_build_modules(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "build-guest-artifacts" + / "action.yml" + ).read_text(encoding="utf-8") + for variable in ( + "ALPINE_INPUT_HASH", + "UBUNTU_INPUT_HASH", + "AZURELINUX_INPUT_HASH", + ): + assignment = next(line for line in action.splitlines() if variable in line) + self.assertIn("'scripts/nvx_tools/common.py'", assignment) + self.assertIn("'scripts/nvx_tools/guests.py'", assignment) + self.assertIn("'scripts/nvx_tools/elf.py'", assignment) + self.assertIn("'scripts/nvx_tools/guest_binaries.py'", assignment) + self.assertIn("'nvx-rs/**'", assignment) + self.assertIn("'!nvx-rs/target/**'", assignment) + self.assertIn("'rust-toolchain.toml'", assignment) + for prefix in ( + "alpine-image", + "ubuntu-initramfs", + "ubuntu-distro", + "azurelinux-initramfs", + ): + with self.subTest(cache=prefix): + self.assertIn(f"{prefix}-v2-", action) + self.assertNotIn(f"{prefix}-v1-", action) + def test_quality_checks_include_shared_identity_probe(self): + action = ( + BuildConstants.REPO_ROOT + / ".github" + / "actions" + / "check-quality" + / "action.yml" + ).read_text(encoding="utf-8") -class AgentBundlingTests(unittest.TestCase): - def test_cli_registers_the_bundling_check(self): - args = nvx.parse_args(["test-agent-bundling", "--toolchain", "1.95.0"]) + self.assertEqual(action.count("guest/common/nvx-identity-probe"), 2) - self.assertEqual(args.toolchain, "1.95.0") - self.assertEqual(args.cargo, "cargo") - self.assertIs(args.handler, agent_bundling_tests.command_test_agent_bundling) + def test_docker_guest_builder_pins_erofs_toolchain(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" + ) + self.assertIn( + "ARG DEBIAN_IMAGE=debian:12-slim@sha256:" + "3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251", + dockerfile, + ) + self.assertIn("ARG EROFS_UTILS_VERSION=1.5-1", dockerfile) + self.assertIn("erofs-utils=${EROFS_UTILS_VERSION}", dockerfile) - def test_names_and_the_sandbox_abi_come_from_the_crate(self): - names = agent_bundling_tests.contract_names() + def test_docker_builds_one_shared_rust_guest_binary_set(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" + ) + self.assertIn( + f"ARG RUST_BUILDER_IMAGE={DockerBuildConstants.RUST_BUILDER_IMAGE}", + dockerfile, + ) + builder = dockerfile.split("FROM ${RUST_BUILDER_IMAGE} AS guest-binaries", 1)[ + 1 + ].split("\nFROM ", 1)[0] + self.assertIn("COPY rust-toolchain.toml /repo/", builder) + self.assertIn("COPY nvx-rs/ /repo/nvx-rs/", builder) + self.assertIn( + 'rustup target add --toolchain "$toolchain" x86_64-unknown-linux-musl', + builder, + ) + self.assertIn( + "python3 scripts/nvx.py build-guest-binaries --output /out/guest-binaries", + builder, + ) + self.assertEqual(dockerfile.count("build-guest-binaries"), 1) - self.assertEqual(names["BUNDLE_DIR_ENV"], "ACI_EDGE_SANDBOXES_AGENT_BUNDLE_DIR") - self.assertEqual(names["SOURCE_MANIFEST"], "SOURCE-MANIFEST.json") - for name in agent_bundling_tests.OTHER_SOURCES: - self.assertIn(name, names) - self.assertGreaterEqual(agent_bundling_tests.sandbox_abi_version(), 1) + base = dockerfile.split("FROM ${DEBIAN_IMAGE} AS base", 1)[1].split( + "\nFROM ", 1 + )[0] + self.assertIn(" build-essential \\\n", base) + self.assertNotIn("musl-tools", base) + self.assertNotIn("nvx-rs", base) + self.assertNotIn("rust-toolchain.toml", base) + self.assertNotIn("guest-binaries", base) + + guest_base = dockerfile.split("FROM base AS guest-base", 1)[1].split( + "\nFROM ", 1 + )[0] + self.assertIn("COPY nvx-rs/ /repo/nvx-rs/", guest_base) + self.assertIn("COPY rust-toolchain.toml /repo/", guest_base) + self.assertIn( + "COPY --from=guest-binaries /out/guest-binaries/ " + "/repo/build/guest-binaries/", + guest_base, + ) + self.assertIn( + "ENV NVX_GUEST_BINARIES=/repo/build/guest-binaries", + guest_base, + ) - def test_sources_without_the_constants_are_rejected(self): - with tempfile.TemporaryDirectory() as temporary: - source = Path(temporary) / "empty.rs" - source.write_text("pub const OTHER: u32 = 1;\n", encoding="utf-8") + determinism = dockerfile.split("FROM guest-base AS ubuntu-determinism", 1)[ + 1 + ].split("\nFROM ", 1)[0] + self.assertIn( + "COPY --from=guest-binaries /usr/local/cargo/ /usr/local/cargo/", + determinism, + ) + self.assertIn( + "COPY --from=guest-binaries /usr/local/rustup/ /usr/local/rustup/", + determinism, + ) + self.assertIn( + "RUN NVX_GUEST_BINARIES= python3 scripts/nvx.py verify-guest-determinism", + determinism, + ) - with self.assertRaisesRegex(common.ScriptError, "SANDBOX_ABI_VERSION"): - agent_bundling_tests.sandbox_abi_version(source) - with self.assertRaisesRegex(common.ScriptError, "declares no names"): - agent_bundling_tests.contract_names(source) + azure = dockerfile.split("FROM guest-base AS azurelinux-initramfs", 1)[1].split( + "\nFROM ", 1 + )[0] + self.assertIn( + "source = Path('/repo/build/guest-binaries')", + azure, + ) + self.assertIn("inspect_guest_binaries", azure) + self.assertIn("for name in GuestRustBuildConstants.BINARIES:", azure) + self.assertIn("'helpers': helpers", azure) + self.assertNotIn("azurelinux-helpers", dockerfile) + self.assertNotIn("musl-gcc", dockerfile) + self.assertNotIn("cc -static", dockerfile) + + def test_docker_kernel_targets_do_not_build_the_guest_binaries(self): + # BuildKit builds every stage that a target inherits or copies from, so + # a kernel-only build must not compile the Rust guest binaries. + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" + ) + dependencies: dict[str, set[str]] = {} + stage = "" + for line in dockerfile.splitlines(): + if match := re.fullmatch(r"FROM (\S+) AS (\S+)", line): + stage = match.group(2) + dependencies[stage] = {match.group(1)} + elif match := re.match(r"COPY --from=(\S+) ", line): + dependencies[stage].add(match.group(1)) + + def stages_built_for(target: str) -> set[str]: + built: set[str] = set() + pending = [target] + while pending: + for dependency in dependencies.get(pending.pop(), set()): + if dependency not in built: + built.add(dependency) + pending.append(dependency) + return built - def test_bundles_list_and_pin_every_file(self): - for platform, library in ( - ("win32", "bin/aci_edge_agent.dll"), - ("linux", "bin/libaci_edge_agent.so"), + for target in ( + "kernel-artifacts", + DockerBuildConstants.DEBUG_KERNEL_TARGET, + DockerBuildConstants.LINUX_SOURCE_TARGET, ): - layout = agent_bundling_tests.BundleLayout.for_platform(platform) - with self.subTest(platform=platform), tempfile.TemporaryDirectory() as temp: - bundle = Path(temp) - agent_bundling_tests.write_bundle( - bundle, layout, "SOURCE-MANIFEST.json", 7 - ) + with self.subTest(target=target): + self.assertIn("base", stages_built_for(target)) + self.assertNotIn("guest-binaries", stages_built_for(target)) + for target in ( + DockerBuildConstants.ALPINE_TARGET, + DockerBuildConstants.UBUNTU_TARGET, + DockerBuildConstants.AZURELINUX_TARGET, + "ubuntu-determinism", + ): + with self.subTest(target=target): + self.assertIn("guest-binaries", stages_built_for(target)) - listed: dict[str, str] = {} - for line in (bundle / "SHA256SUMS").read_text().splitlines(): - digest, name = line.split(" ", 1) - listed[name] = digest - self.assertEqual( - set(listed), - {*layout.files(), "SOURCE-MANIFEST.json", "bundle-manifest.json"}, - ) - for name, digest in listed.items(): - self.assertEqual(common.sha256_file(bundle / name), digest, name) - source = json.loads((bundle / "SOURCE-MANIFEST.json").read_text()) - self.assertEqual(source["format"], 2) - self.assertEqual( - source["linux"]["kernel_sha256"], listed["guest/vmlinux"] - ) - self.assertEqual(source["agent"]["library_sha256"], listed[library]) - manifest = json.loads((bundle / "bundle-manifest.json").read_text()) - consumer = manifest["consumer"] - self.assertEqual(consumer["backendLibrary"], library) - self.assertEqual( - consumer["runtimeBundleManifestSha256"], - listed["SOURCE-MANIFEST.json"], - ) - self.assertEqual(consumer["sandboxAbiVersion"], 7) + def test_azurelinux_initramfs_uses_locked_busybox_applets(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" + ) + base_stage = dockerfile.split("FROM base AS kernel", 1)[0] + self.assertNotIn("busybox", base_stage) + azure_stage = dockerfile.split("FROM guest-base AS azurelinux-initramfs", 1)[1] + self.assertIn("ln -sf ../sbin/busybox /rootfs/usr/bin/busybox", azure_stage) + applets = ( + azure_stage.split("for utility in ", 1)[1] + .split("; do", 1)[0] + .replace("\\\n", " ") + .split() + ) + for applet in ("sh", "arp", "nc", "wget", "mdev", "ifconfig", "route"): + self.assertIn(applet, applets) + # util-linux provides these; the BusyBox applets lack required options. + self.assertNotIn("setpriv", applets) + self.assertNotIn("unshare", applets) + launcher = ( + BuildConstants.REPO_ROOT + / "guest" + / "common" + / "nvx-container-launch-azurelinux" + ).read_text(encoding="utf-8") + self.assertIn("exec unshare --mount --pid --uts --fork --kill-child", launcher) + self.assertIn( + "busybox", [package["name"] for package in azurelinux.load_package_lock()] + ) - def test_a_bundle_can_pin_another_file_as_its_kernel(self): - layout = agent_bundling_tests.BundleLayout.for_platform("linux") - initramfs = agent_bundling_tests.INITRAMFS - with tempfile.TemporaryDirectory() as temporary: - bundle = Path(temporary) - agent_bundling_tests.write_bundle( - bundle, layout, "SOURCE-MANIFEST.json", 1, kernel=initramfs - ) + def test_azurelinux_initramfs_gives_nobody_a_home(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" + ) + azure_stage = dockerfile.split("FROM guest-base AS azurelinux-initramfs", 1)[1] + self.assertIn( + "sed -i 's#^\\(nobody:[^:]*:65534:65534:[^:]*:\\)/dev/null:" + "#\\1/nonexistent:#'", + azure_stage, + ) + self.assertIn( + "grep -q '^nobody:[^:]*:65534:65534:[^:]*:/nonexistent:' " + "/rootfs/etc/passwd", + azure_stage, + ) + self.assertIn("install -d -m 0755 /rootfs/nonexistent", azure_stage) - source = json.loads((bundle / "SOURCE-MANIFEST.json").read_text()) - self.assertEqual( - source["linux"]["kernel_sha256"], common.sha256_file(bundle / initramfs) - ) + def test_azurelinux_rootfs_installs_only_locked_rpms(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" + ) + self.assertNotIn("tdnf install", dockerfile) + packages_stage = dockerfile.split("FROM base AS azurelinux-packages", 1)[1] + self.assertIn( + "from nvx_tools.azurelinux import download_packages", + packages_stage.split("\nFROM ", 1)[0], + ) + rootfs_stage = dockerfile.split( + "FROM ${AZURELINUX_IMAGE} AS azurelinux-rootfs", 1 + )[1].split("\nFROM ", 1)[0] + self.assertIn( + "COPY --from=azurelinux-packages /out/rpms/ /tmp/azurelinux-rpms/", + rootfs_stage, + ) + checksig = rootfs_stage.index("rpm --checksig /tmp/azurelinux-rpms/*.rpm") + install = rootfs_stage.index("rpm --upgrade --verbose --hash") + self.assertLess(checksig, install) - def test_cargo_commands_select_the_crate_feature_and_toolchain(self): - command = agent_bundling_tests.cargo_command("cargo", "1.95.0", "test", "x") + def test_azurelinux_manifest_uses_package_metadata(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" + ) + self.assertIn("rpm -qa --queryformat", dockerfile) + self.assertIn("if name == 'gpg-pubkey':", dockerfile) + self.assertNotIn("'type': 'deb'", dockerfile) + self.assertIn("'packages': json.loads", dockerfile) - self.assertEqual(command[:3], ["cargo", "+1.95.0", "test"]) - self.assertEqual( - Path(command[command.index("--manifest-path") + 1]), - BuildConstants.REPO_ROOT / "aci_edge_sandboxes" / "Cargo.toml", + def test_azurelinux_initramfs_packs_normalized_rootfs(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" ) - self.assertIn("--locked", command) - self.assertEqual(command[-4:], ["--lib", "--features", "agent-bundled", "x"]) - self.assertEqual( - agent_bundling_tests.cargo_command("cargo", None, "check")[:2], - ["cargo", "check"], + azure_stage = dockerfile.split("FROM guest-base AS azurelinux-initramfs", 1)[1] + cleanup = azure_stage.index( + "rm -rf /rootfs/usr/lib/sysimage/tdnf /rootfs/var/cache/tdnf \\\n" + " /rootfs/var/cache/ldconfig /rootfs/var/lib/rpm" + ) + normalize = azure_stage.index( + "find . -exec touch --no-dereference " + f"--date=@{InitramfsBuildConstants.TIMESTAMP} {{}} +" ) + pack = azure_stage.index("find . -print0 | LC_ALL=C sort -z | cpio") + self.assertLess(cleanup, normalize) + self.assertLess(normalize, pack) - def test_the_synthetic_bundle_is_the_only_bundle_source(self): - names = agent_bundling_tests.contract_names() - inherited = { - names[name]: "inherited" for name in agent_bundling_tests.OTHER_SOURCES - } - inherited[names["BUNDLE_DIR_ENV"]] = "elsewhere" - with patch.dict(os.environ, inherited): - environment = agent_bundling_tests.bundle_environment(names, Path("x")) + def test_azurelinux_manifest_records_shared_input_digest(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" + ) + azure_stage = dockerfile.split("FROM guest-base AS azurelinux-initramfs", 1)[1] + self.assertIn("from nvx_tools.azurelinux import input_sha256", azure_stage) + self.assertIn( + "'input_sha256': input_sha256(\n" + " image=os.environ['AZURELINUX_IMAGE'],\n" + " version=os.environ['AZURELINUX_VERSION'],\n" + " ),", + azure_stage, + ) + + def test_azurelinux_input_digest_tracks_guest_sources_and_dockerfile(self): + checkout_digest = azurelinux.input_sha256() + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for source in azurelinux.input_files(): + destination = root / source.relative_to(BuildConstants.REPO_ROOT) + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, destination) + with patch.object(BuildConstants, "REPO_ROOT", root): + baseline = azurelinux.input_sha256() + self.assertEqual(baseline, checkout_digest) + self.assertNotEqual( + azurelinux.input_sha256(image="example.invalid/other@sha256:0"), + baseline, + ) + self.assertNotEqual(azurelinux.input_sha256(version="3.1"), baseline) + for relative in ( + "guest/common/init", + "docker/Dockerfile", + "azurelinux/packages.lock.json", + "scripts/nvx_tools/azurelinux.py", + "scripts/nvx_tools/build_constants.py", + "scripts/nvx_tools/common.py", + "scripts/nvx_tools/guest_binaries.py", + "nvx-rs/src/lib.rs", + "rust-toolchain.toml", + ): + with self.subTest(path=relative): + path = root / relative + original = path.read_bytes() + path.write_bytes(original + b"\n# changed\n") + self.assertNotEqual(azurelinux.input_sha256(), baseline) + path.write_bytes(original) + self.assertEqual(azurelinux.input_sha256(), baseline) + unrelated = root / "guest" / "ubuntu" / "nvx-bashrc" + unrelated.parent.mkdir(parents=True, exist_ok=True) + unrelated.write_text("changed\n", encoding="utf-8") + self.assertEqual(azurelinux.input_sha256(), baseline) - self.assertEqual(environment[names["BUNDLE_DIR_ENV"]], "x") - for name in agent_bundling_tests.OTHER_SOURCES: - self.assertNotIn(names[name], environment) + def test_guest_init_runs_virtfs_helper_from_installed_path(self): + installed: list[Path] = [] - def test_directory_links_need_no_privilege(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - (root / "target").mkdir() - (root / "target" / "file").write_text("content", encoding="utf-8") + def install(_source: Path, destination: Path) -> dict[str, str]: + installed.append(destination) + return {} - agent_bundling_tests.link_directory(root / "link", root / "target") + def stage_binaries(output: Path) -> guest_binaries.GuestBinaryBuildResult: + output.mkdir() + binaries: dict[str, str] = {} + for name in GuestRustBuildConstants.BINARIES: + path = output / name + path.write_bytes(name.encode("ascii")) + binaries[name] = common.sha256_file(path) + return guest_binaries.GuestBinaryBuildResult( + source_sha256="a" * 64, + binaries=binaries, + ) - self.assertEqual((root / "link" / "file").read_text(), "content") - # A link, not a copy: only links have a target to read. - self.assertTrue(os.readlink(root / "link")) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / "rootfs" + work = Path(temporary) / "work" + (root / "sbin").mkdir(parents=True) + (root / "etc").mkdir() + work.mkdir() + with ( + patch.object(build, "_install", side_effect=install), + patch.object(build, "build_guest_binaries", side_effect=stage_binaries), + ): + build._install_guest_files( + build_config.InitramfsBuildConfig(work=work), + root, + guests.ALPINE_GUEST, + ) + self.assertIn(root / "sbin" / "nvx-hostmount", installed) - def _outcomes(self, **overrides: tuple[int, str]) -> dict[str, tuple[int, str]]: - """Returns the status and output of cargo for each bundle that the check builds, - as the build script reports them, with `overrides`.""" - abi = agent_bundling_tests.sandbox_abi_version() - outcomes = { - "good": (0, ""), - "tampered": (101, "SHA-256 mismatch for guest/vmlinux: SHA256SUMS lists"), - "pinned": (101, "pins /linux/kernel_sha256 to another digest than"), - "other-abi": (101, f"library speaks sandbox ABI {abi + 1}, but this"), - "linked": (101, "guest is a link; a bundle holds only files"), - } - outcomes.update( - {name.replace("_", "-"): outcome for name, outcome in overrides.items()} + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" ) - return outcomes - - def _check(self, outcomes: dict[str, tuple[int, str]]) -> tuple[int, list[str]]: - """Runs the check with cargo replaced by `outcomes`, and returns its status and - the cargo subcommand and bundle of each command that it ran.""" - names = agent_bundling_tests.contract_names() - calls: list[str] = [] + azure_stage = dockerfile.split("FROM guest-base AS azurelinux-initramfs", 1)[1] + azure_sbin_scripts = azure_stage.split( + "install -m 0755 /repo/guest/common/init /rootfs/init\n", 1 + )[1].split("/rootfs/sbin/", 1)[0] + self.assertIn("/repo/guest/common/nvx-hostmount", azure_sbin_scripts) + init_script = ( + BuildConstants.REPO_ROOT / "guest" / "common" / "init" + ).read_text(encoding="utf-8") + self.assertIn( + '/sbin/nvx-hostmount || fatal "virtfs: failed to mount live host directory"', + init_script, + ) + self.assertNotIn("/usr/sbin/nvx-hostmount", init_script) - def fake_run( - command: list[str], *, env: dict[str, str], check: bool, **_: object - ) -> subprocess.CompletedProcess[str]: - self.assertFalse(check) - subcommand = command[command.index("--manifest-path") - 1] - bundle = Path(env[names["BUNDLE_DIR_ENV"]]).name - calls.append(f"{subcommand} {bundle}") - status, output = outcomes[bundle] - return subprocess.CompletedProcess(command, status, output, "") + def test_openvmm_ci_downloads_guest_artifacts(self): + workflow = ( + BuildConstants.REPO_ROOT / ".github" / "workflows" / "ci.yml" + ).read_text(encoding="utf-8") + openvmm_tests = _workflow_job(workflow, "openvmm-vmm-tests") + self.assertIn("needs: [artifacts, openvmm-changes]", openvmm_tests) + self.assertIn("needs.artifacts.result == 'success'", openvmm_tests) + self.assertIn("- name: Download guest artifacts", openvmm_tests) + self.assertIn("name: guest-artifacts", openvmm_tests) + self.assertIn("path: build", openvmm_tests) + self.assertIn("openvmm-vmm-tests", ci.REQUIRED_CI_OPENVMM_ARTIFACT_TEST_JOBS) - args = argparse.Namespace(cargo="cargo", toolchain="1.95.0") + def test_apk_add_uses_host_ca_bundle_without_overriding_configuration(self): + root = Path("root") with ( - patch.object(agent_bundling_tests.subprocess, "run", fake_run), - patch.object(agent_bundling_tests, "link_directory"), - patch("sys.stdout", io.StringIO()), - patch("sys.stderr", io.StringIO()), + patch.object(build.ssl, "get_default_verify_paths") as verify_paths, + patch.object(build, "run_checked") as run, ): - return agent_bundling_tests.command_test_agent_bundling(args), calls - - def test_check_requires_the_tests_and_every_rejection(self): - status, calls = self._check(self._outcomes()) + verify_paths.return_value.cafile = "/etc/host-ca.pem" + with patch.dict(os.environ, {}, clear=True): + build._apk_add(root, "example") - self.assertEqual(status, 0) - self.assertEqual( - calls, - [ - "test good", - "check tampered", - "check pinned", - "check other-abi", - "check linked", - ], - ) + environment = run.call_args.kwargs["env"] + self.assertEqual(environment["SSL_CERT_FILE"], "/etc/host-ca.pem") + self.assertEqual( + environment["LD_LIBRARY_PATH"], + f"{root / 'lib'}:{root / 'usr' / 'lib'}", + ) - def test_check_returns_the_status_of_failing_tests(self): - status, calls = self._check(self._outcomes(good=(3, ""))) + with patch.dict( + os.environ, + {"SSL_CERT_FILE": "/etc/configured-ca.pem"}, + clear=True, + ): + build._apk_add(root, "example") - self.assertEqual(status, 3) - self.assertEqual(calls, ["test good"]) + environment = run.call_args.kwargs["env"] + self.assertEqual(environment["SSL_CERT_FILE"], "/etc/configured-ca.pem") - def test_check_fails_when_the_build_accepts_a_broken_bundle(self): - with self.assertRaisesRegex(common.ScriptError, "accepted the tampered bundle"): - self._check(self._outcomes(tampered=(0, ""))) + def test_device_io_provenance_uses_shared_rust_source_digest(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + checkout = root / "nvx" + crate = checkout / "nvx-rs" + (crate / "src").mkdir(parents=True) + sources = { + crate / "Cargo.toml": "[package]\nname = 'fixture'\n", + crate / "Cargo.lock": "# lock\n", + crate / "build.rs": "fn main() {}\n", + crate / "src" / "lib.rs": "pub fn fixture() {}\n", + checkout / "rust-toolchain.toml": ( + f'[toolchain]\nchannel = "{GuestRustBuildConstants.TOOLCHAIN}"\n' + ), + checkout / "scripts" / "nvx_tools" / "build_constants.py": ( + "PROFILE = 'guest'\n" + ), + checkout / "scripts" / "nvx_tools" / "guest_binaries.py": ( + "def build(): pass\n" + ), + } + for path, contents in sources.items(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(contents, encoding="utf-8") + source_sha256 = guest_binaries.guest_binary_source_sha256(checkout) + initrd = root / "initramfs.cpio.gz" + manifest = initrd.with_name(f"{initrd.name}.packages.json") + manifest.write_text( + json.dumps( + { + "helpers": { + "nvx-device-io": { + "source_sha256": source_sha256, + "binary_sha256": "1" * 64, + } + } + } + ), + encoding="utf-8", + ) - def test_check_fails_when_a_rejection_reports_another_cause(self): - with self.assertRaisesRegex(common.ScriptError, "/linux/kernel_sha256"): - self._check(self._outcomes(pinned=(101, "cannot read the bundle"))) + provenance = benchmark._device_io_helper_provenance( + argparse.Namespace(nvx_dir=checkout), + initrd, + ) + self.assertEqual(provenance["source"], str(crate.resolve())) + self.assertEqual(provenance["source_sha256"], source_sha256) + self.assertEqual(provenance["toolchain"], GuestRustBuildConstants.TOOLCHAIN) + self.assertEqual(provenance["target"], GuestRustBuildConstants.TARGET) -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" - ) - distro = sandbox.SandboxLayer.parse( - "distro,distro.erofs,11111111-1111-1111-1111-111111111111" - ) - launch = sandbox.SandboxLaunch( - layers=(custom, distro), - scratch=Path("scratch.ext4"), - entrypoint="/bin/workload", - args=("--serve",), - hostname="example", - workload_identity=(1000, 1001), - memory_max=268435456, - pids_max=64, - ) + with ( + patch.object(GuestRustBuildConstants, "TOOLCHAIN", "9.9.9"), + self.assertRaisesRegex( + common.ScriptError, + "Rust guest build toolchain mismatch", + ), + ): + benchmark._device_io_helper_provenance( + argparse.Namespace(nvx_dir=checkout), + initrd, + ) - 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" - ), - ) - self.assertEqual( - launch.openvmm_arguments(), - [ - "--machine", - "microvm", - "--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", - ], - ) + (crate / "src" / "lib.rs").write_text("changed\n", encoding="utf-8") + with self.assertRaisesRegex( + ValueError, + "Rust guest binary sources do not match", + ): + benchmark._device_io_helper_provenance( + argparse.Namespace(nvx_dir=checkout), + initrd, + ) - 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"): - sandbox.SandboxLaunch( - layers=(distro, distro), - scratch=Path("scratch.ext4"), + def test_sandbox_kernel_config_requires_every_feature(self): + with tempfile.TemporaryDirectory() as temporary: + config = Path(temporary) / ".config" + config.write_text( + "\n".join(KernelBuildConstants.REQUIRED_SANDBOX_CONFIG) + "\n", + encoding="utf-8", ) + build._assert_sandbox_kernel_config(config) - launch = sandbox.SandboxLaunch( - layers=(distro,), - scratch=Path("scratch.ext4"), - ) - with self.assertRaisesRegex(common.ScriptError, "owned"): - launch.kernel_command_line("nvx_sandbox=0") - with self.assertRaisesRegex(common.ScriptError, "owned"): - launch.kernel_command_line(r"foo=bar\ nvx_memory_max=max") - with self.assertRaisesRegex(common.ScriptError, "1024-byte"): - launch.kernel_command_line("x" * sandbox.SANDBOX_COMMAND_LINE_MAX_SIZE) - with self.assertRaisesRegex(common.ScriptError, "between 1"): - sandbox.parse_workload_identity("0:0") + for missing in KernelBuildConstants.REQUIRED_SANDBOX_CONFIG: + with self.subTest(missing=missing): + config.write_text( + "\n".join( + setting + for setting in KernelBuildConstants.REQUIRED_SANDBOX_CONFIG + if setting != missing + ) + + "\n", + encoding="utf-8", + ) + with self.assertRaisesRegex(common.ScriptError, missing): + build._assert_sandbox_kernel_config(config) - def test_layer_parser_rejects_invalid_role_and_uuid(self): - with self.assertRaisesRegex(common.ScriptError, "unsupported layer role"): - sandbox.SandboxLayer.parse( - "unknown,layer.erofs,11111111-1111-1111-1111-111111111111" + def test_network_kernel_config_requires_every_feature(self): + with tempfile.TemporaryDirectory() as temporary: + config = Path(temporary) / ".config" + config.write_text( + "\n".join(KernelBuildConstants.REQUIRED_NETWORK_CONFIG) + "\n", + encoding="utf-8", ) - with self.assertRaisesRegex(common.ScriptError, "UUID is invalid"): - sandbox.SandboxLayer.parse("distro,layer.erofs,not-a-uuid") + build._assert_network_kernel_config(config) - def test_mount_parser_defaults_to_read_only_and_accepts_rw(self): - default = sandbox.SandboxMount.parse("/workspace,host-dir") - self.assertEqual(default.guest_target, "/workspace") - self.assertEqual(default.host_path, Path("host-dir")) - self.assertEqual(default.access, "ro") - self.assertEqual(default.denied_paths, ()) + for missing in KernelBuildConstants.REQUIRED_NETWORK_CONFIG: + with self.subTest(missing=missing): + config.write_text( + "\n".join( + setting + for setting in KernelBuildConstants.REQUIRED_NETWORK_CONFIG + if setting != missing + ) + + "\n", + encoding="utf-8", + ) + with self.assertRaisesRegex( + common.ScriptError, "portable network profile" + ) as raised: + build._assert_network_kernel_config(config) + self.assertIn(missing, str(raised.exception)) - writable = sandbox.SandboxMount.parse( - "/opt/hostedtoolcache,host-dir,rw", ("logs", "secrets") - ) - self.assertEqual(writable.access, "rw") - self.assertEqual(writable.denied_paths, ("logs", "secrets")) - self.assertEqual( - sandbox.mounts_openvmm_arguments((writable,)), - [ - "--mount", - f"/opt/hostedtoolcache,{os.fspath(Path('host-dir'))},rw", - "--mount-deny", - "logs", - "--mount-deny", - "secrets", - ], - ) - self.assertEqual( - writable.command_line_fragment(), - " virtfs_dir=/opt/hostedtoolcache virtfs_tag=microvm virtfs_mode=rw", - ) + # An enabled SIT device leaves the "is not set" line out. + config.write_text( + "\n".join( + "CONFIG_IPV6_SIT=y" + if setting == "# CONFIG_IPV6_SIT is not set" + else setting + for setting in KernelBuildConstants.REQUIRED_NETWORK_CONFIG + ) + + "\n", + encoding="utf-8", + ) + with self.assertRaisesRegex(common.ScriptError, "CONFIG_IPV6_SIT"): + build._assert_network_kernel_config(config) - def test_mount_parser_rejects_malformed_specifications(self): - for value, message in ( - ("/workspace", "GUEST_TARGET,HOST_PATH"), - ("/workspace,", "host path is empty"), - ("/workspace,,rw", "host path is empty"), - ("/workspace,host,rx", "unsupported --mount mode 'rx'"), - # With more than one comma, the value must end in the mode, which - # the error suggests for a host path that contains commas. - ("/workspace,host,rw,extra", "mode 'extra'.*if HOST_PATH contains commas"), - ("/workspace,alpha,beta", "mode 'beta'.*if HOST_PATH contains commas"), - ): - with self.subTest(value=value): - with self.assertRaisesRegex(common.ScriptError, message): - sandbox.SandboxMount.parse(value) - with self.assertRaisesRegex(common.ScriptError, "NUL"): - sandbox.SandboxMount(guest_target="/workspace", host_path=Path("a\0b")) - with self.assertRaisesRegex(common.ScriptError, "parent component"): - sandbox.SandboxMount.parse("/workspace,link/../share") + def test_checked_in_config_provides_the_portable_network_profile(self): + config = BuildConstants.REPO_ROOT / KernelBuildConstants.INPUT_CONFIG + build._assert_network_kernel_config(config) - def test_mount_parser_accepts_host_paths_with_commas(self): - alpha_path = r"C:\projects\alpha,beta" - reports_path = r"C:\reports\2026, final" - for value, target, host_path, access in ( - (f"/work,{alpha_path},rw", "/work", alpha_path, "rw"), - (f"/reports,{reports_path},ro", "/reports", reports_path, "ro"), - # A host path that ends in a comma, or in what looks like a mode, - # keeps it before the mode. - ("/data,/srv/data,rw,ro", "/data", "/srv/data,rw", "ro"), - ("/data,/srv/data,,rw", "/data", "/srv/data,", "rw"), - # Values without commas in the host path keep their meaning. - ("/data,/srv/data,rw", "/data", "/srv/data", "rw"), - ("/data,/srv/data", "/data", "/srv/data", "ro"), + def test_checked_in_config_preserves_generic_sandbox_capabilities(self): + config = BuildConstants.REPO_ROOT / "kernel" / "config-microvm" + build._assert_sandbox_kernel_config(config) + configured = config.read_text(encoding="utf-8").splitlines() + for setting in ( + "CONFIG_SECCOMP_FILTER=y", + "CONFIG_UNIX=y", + "# CONFIG_OVERLAY_FS_REDIRECT_ALWAYS_FOLLOW is not set", ): - with self.subTest(value=value): - self.assertEqual( - sandbox.parse_mount_value(value), (target, host_path, access) - ) - mount = sandbox.SandboxMount.parse(value, ("secret",)) - self.assertEqual(mount.host_path, Path(host_path)) - self.assertEqual(mount.access, access) - # OpenVMM finds the end of the host path before the mode, which - # NVX therefore always gives. - self.assertEqual( - sandbox.mounts_openvmm_arguments((mount,)), - [ - "--mount", - f"{target},{os.fspath(Path(host_path))},{access}", - "--mount-deny", - "secret", - ], - ) + self.assertIn(setting, configured) - alpha = sandbox.SandboxMount.parse(f"/work,{alpha_path},rw") - reports = sandbox.SandboxMount.parse(f"/reports,{reports_path},ro") - arguments = sandbox.mounts_openvmm_arguments((alpha, reports)) - self.assertEqual( - [ - arguments[index + 1] - for index, value in enumerate(arguments) - if value == "--mount-child" - ], - [ - f"{sandbox.aggregate_child_name(0, '/work')}," - f"{os.fspath(Path(alpha_path))},rw", - f"{sandbox.aggregate_child_name(1, '/reports')}," - f"{os.fspath(Path(reports_path))},ro", - ], - ) + def test_shared_status_kernel_config_is_required(self): + with tempfile.TemporaryDirectory() as temporary: + config = Path(temporary) / ".config" + config.write_text( + "\n".join(KernelBuildConstants.REQUIRED_SHARED_STATUS_CONFIG) + "\n", + encoding="utf-8", + ) + build._assert_shared_status_kernel_config(config) - def test_mount_owner_defaults_to_vmm_and_forwards_caller(self): - default = sandbox.SandboxMount.parse("/workspace,host-dir,rw") - self.assertEqual(default.owner, "vmm") - self.assertNotIn("--mount-owner", sandbox.mounts_openvmm_arguments((default,))) + config.write_text("", encoding="utf-8") + with self.assertRaisesRegex( + common.ScriptError, + KernelBuildConstants.REQUIRED_SHARED_STATUS_CONFIG[0], + ): + build._assert_shared_status_kernel_config(config) - caller = sandbox.SandboxMount.parse( - "/workspace,host-dir,rw", ("secrets",), "caller" - ) - self.assertEqual( - sandbox.mounts_openvmm_arguments((caller,)), - [ - "--mount", - f"/workspace,{os.fspath(Path('host-dir'))},rw", - "--mount-deny", - "secrets", - "--mount-owner", - "caller", - ], - ) - self.assertEqual(caller.absolute().owner, "caller") - self.assertEqual( - caller.command_line_fragment(), default.command_line_fragment() - ) - with self.assertRaisesRegex( - common.ScriptError, "unsupported sandbox mount owner" + def test_checked_in_config_meets_time_abi_kernel_contract(self): + config = BuildConstants.REPO_ROOT / KernelBuildConstants.INPUT_CONFIG + build._assert_time_abi_kernel_config(config) + build._assert_hardening_kernel_config(config) + build._assert_watchdog_kernel_config(config, debug=False) + configured = config.read_text(encoding="utf-8").splitlines() + for removed in ( + "CONFIG_CPU_FREQ=y", + "CONFIG_X86_INTEL_PSTATE=y", + "CONFIG_SCHED_MC_PRIO=y", + "CONFIG_HYPERV=y", + "CONFIG_KVM_GUEST=y", + "CONFIG_PARAVIRT_CLOCK=y", + "CONFIG_HALTPOLL_CPUIDLE=y", + "CONFIG_ARCH_CPUIDLE_HALTPOLL=y", ): - sandbox.SandboxMount.parse("/workspace,host-dir", (), "root") - - def test_mount_access_policy_is_validated_and_forwarded(self): - mount = sandbox.SandboxMount.parse( - "/tmp/mcp-runtime,host-dir,rw", - ("mcp-logs",), - allowed_paths=("mcp-logs/payloads",), - writable_paths=("agent", "cache"), - ) - self.assertEqual( - sandbox.mounts_openvmm_arguments((mount,)), - [ - "--mount", - f"/tmp/mcp-runtime,{os.fspath(Path('host-dir'))},rw", - "--mount-deny", - "mcp-logs", - "--mount-allow", - "mcp-logs/payloads", - "--mount-write", - "agent", - "--mount-write", - "cache", - ], - ) - absolute = mount.absolute() - self.assertEqual(absolute.allowed_paths, ("mcp-logs/payloads",)) - self.assertEqual(absolute.writable_paths, ("agent", "cache")) - # OpenVMM enforces the policy on the host, so the guest's bootstrap - # tokens are those of the plain share. - self.assertEqual( - mount.command_line_fragment(), - " virtfs_dir=/tmp/mcp-runtime virtfs_tag=microvm virtfs_mode=rw", + with self.subTest(removed=removed): + self.assertNotIn(removed, configured) + self.assertFalse( + any(line.startswith("CONFIG_CPU_FREQ_") for line in configured) ) - for create_mount, message in ( - ( - lambda: sandbox.SandboxMount.parse( - "/w,host,ro", writable_paths=("out",) - ), - "/w is read-only", - ), - ( - lambda: sandbox.SandboxMount.parse( - "/w,host,rw", allowed_paths=("logs/payloads",) - ), - "which has none", - ), - ( - lambda: sandbox.SandboxMount.parse( - "/w,host,rw", ("logs",), allowed_paths=("logs/a", "logs/a") - ), - "allowed paths must be unique", - ), - ( - lambda: sandbox.SandboxMount.parse("/w,host,rw", writable_paths=("",)), - "writable paths must be nonempty", - ), - ( - lambda: sandbox.SandboxMount.parse( - "/w,host,rw", - writable_paths=tuple(f"path-{index}" for index in range(129)), - ), - "at most 128 writable paths", - ), - ): - with self.subTest(message=message): - with self.assertRaisesRegex(common.ScriptError, message): - create_mount() - def test_caller_owner_requires_linux_and_a_directory_that_root_does_not_own(self): - with patch.object(sandbox.os, "name", "nt"): - with self.assertRaisesRegex(common.ScriptError, "requires a Linux host"): - sandbox.require_mount_owner_supported("caller") - sandbox.require_mount_owner_supported("vmm") - if sys.platform == "win32": - with tempfile.TemporaryDirectory() as temporary: - mount = sandbox.SandboxMount( - guest_target="/workspace", host_path=Path(temporary), owner="caller" - ) - with self.assertRaisesRegex( - common.ScriptError, "requires a Linux host" - ): - mount.validated() - return + def test_time_abi_kernel_config_requires_every_setting(self): + with tempfile.TemporaryDirectory() as temporary: + config = Path(temporary) / ".config" + for missing in KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG: + with self.subTest(missing=missing): + config.write_text( + "\n".join( + setting + for setting in KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG + if setting != missing + ) + + "\n", + encoding="utf-8", + ) + with self.assertRaisesRegex(common.ScriptError, missing): + build._assert_time_abi_kernel_config(config) + def test_hardening_kernel_config_requires_every_setting(self): with tempfile.TemporaryDirectory() as temporary: - mount = sandbox.SandboxMount( - guest_target="/workspace", host_path=Path(temporary), owner="caller" - ) - owner = Path(temporary).stat() - if owner.st_uid != 0 and owner.st_gid != 0: - self.assertIs(mount.validated(), mount) - filesystem_root = Path("/") - if filesystem_root.stat().st_uid == 0: - root_owned = sandbox.SandboxMount( - guest_target="/workspace", host_path=filesystem_root, owner="caller" - ) - with self.assertRaisesRegex(common.ScriptError, "UID 0 or GID 0"): - root_owned.validated() - sandbox.SandboxMount( - guest_target="/workspace", host_path=filesystem_root - ).validated() - - def test_mount_absolute_path_keeps_symbolic_link_components(self): + config = Path(temporary) / ".config" + for missing in KernelBuildConstants.REQUIRED_HARDENING_CONFIG: + with self.subTest(missing=missing): + config.write_text( + "\n".join( + setting + for setting in KernelBuildConstants.REQUIRED_HARDENING_CONFIG + if setting != missing + ) + + "\n", + encoding="utf-8", + ) + with self.assertRaisesRegex(common.ScriptError, re.escape(missing)): + build._assert_hardening_kernel_config(config) + + def test_hardening_kernel_config_rejects_loadable_modules(self): with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - (root / "link-target" / "share").mkdir(parents=True) - try: - (root / "link").symlink_to( - root / "link-target", target_is_directory=True + config = Path(temporary) / ".config" + config.write_text( + "\n".join( + (*KernelBuildConstants.REQUIRED_HARDENING_CONFIG, "CONFIG_FOO=m") ) - except OSError as error: - self.skipTest(f"symbolic links are unavailable: {error}") - previous = Path.cwd() - os.chdir(root) - try: - mount = sandbox.SandboxMount.parse("/workspace,link/share,rw") - absolute = mount.absolute() - expected = Path.cwd() / "link" / "share" - finally: - os.chdir(previous) - - self.assertEqual(absolute.host_path, expected) - self.assertNotIn("link-target", absolute.host_path.parts) - self.assertEqual(absolute.absolute(), absolute) - with self.assertRaisesRegex(common.ScriptError, "unique"): - sandbox.SandboxMount.parse("/workspace,host", ("logs", "logs")) - with self.assertRaisesRegex(common.ScriptError, "nonempty"): - sandbox.SandboxMount.parse("/workspace,host", ("",)) - with self.assertRaisesRegex(common.ScriptError, "at most 128"): - sandbox.SandboxMount.parse( - "/workspace,host", tuple(f"path-{index}" for index in range(129)) + + "\n", + encoding="utf-8", ) + with self.assertRaisesRegex(common.ScriptError, "CONFIG_FOO=m"): + build._assert_hardening_kernel_config(config) - def test_mount_target_validation_rejects_unsafe_and_reserved_targets(self): - for target in ( - "", - "/", - "workspace", - "/workspace/", - "/work//space", - "/work/./space", - "/work/../space", - "/work space", - "/work=space", - "/work\\space", - "/" + "x" * 4096, - ): - with self.subTest(target=target): - with self.assertRaisesRegex(common.ScriptError, "invalid sandbox"): - sandbox.validate_mount_target(target) - for target in ( - "/proc", - "/proc/self", - "/sys", - "/sys/fs", - "/dev", - "/dev/shm", - "/.nvx-agent", - "/.nvx-agent/tools", - "/etc", - ): - with self.subTest(target=target): - with self.assertRaisesRegex(common.ScriptError, "reserved"): - sandbox.validate_mount_target(target) - for target in ("/workspace", "/procfs", "/etc/app", "/opt/hostedtoolcache"): - with self.subTest(target=target): - self.assertEqual(sandbox.validate_mount_target(target), target) - - def test_launch_contract_attaches_mount_and_reserves_bootstrap_tokens(self): - distro = sandbox.SandboxLayer.parse( - "distro,distro.erofs,11111111-1111-1111-1111-111111111111" - ) - launch = sandbox.SandboxLaunch( - layers=(distro,), - scratch=Path("scratch.ext4"), - mounts=(sandbox.SandboxMount.parse("/workspace,share,rw", ("secrets",)),), - ) - - arguments = launch.openvmm_arguments() - self.assertEqual( - arguments[arguments.index("--mount") :], - [ - "--mount", - f"/workspace,{os.fspath(Path('share'))},rw", - "--mount-deny", - "secrets", - ], - ) - self.assertNotIn("virtfs_", launch.kernel_command_line("quiet")) - for token in ("virtfs_dir=/other", "virtfs_tag=other", "virtfs_mode=rw"): - with self.subTest(token=token): - with self.assertRaisesRegex(common.ScriptError, "--mount option"): - launch.kernel_command_line(token) + def test_watchdog_kernel_config_separates_production_and_debug(self): + with tempfile.TemporaryDirectory() as temporary: + config = Path(temporary) / ".config" + config.write_text( + "\n".join(KernelBuildConstants.REQUIRED_DEBUG_CONFIG) + "\n", + encoding="utf-8", + ) + build._assert_watchdog_kernel_config(config, debug=True) + with self.assertRaisesRegex(common.ScriptError, "debug-only watchdogs"): + build._assert_watchdog_kernel_config(config, debug=False) - unmounted = sandbox.SandboxLaunch(layers=(distro,), scratch=Path("s.ext4")) - base = unmounted.kernel_command_line() - fragment = sandbox.mounts_command_line_fragment(launch.mounts) - self.assertEqual( - fragment, " virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rw" - ) - fitting = "x" * ( - sandbox.SANDBOX_COMMAND_LINE_MAX_SIZE - len(base) - len(fragment) - 2 - ) - launch.kernel_command_line(fitting) - with self.assertRaisesRegex(common.ScriptError, "1024-byte"): - launch.kernel_command_line(fitting + "x") - - def test_launch_contract_attaches_several_shares_as_an_aggregate(self): - distro = sandbox.SandboxLayer.parse( - "distro,distro.erofs,11111111-1111-1111-1111-111111111111" - ) - launch = sandbox.SandboxLaunch( - layers=(distro,), - scratch=Path("scratch.ext4"), - mounts=( - sandbox.SandboxMount.parse("/workspace,work,rw", ("secrets",)), - sandbox.SandboxMount.parse( - "/opt/hostedtoolcache,tools,ro", ("credentials",) - ), - sandbox.SandboxMount.parse("/srv/cache,cache,ro"), - ), - ) + config.write_text("CONFIG_DEBUG_KERNEL=y\n", encoding="utf-8") + build._assert_watchdog_kernel_config(config, debug=False) + with self.assertRaisesRegex( + common.ScriptError, "CONFIG_SOFTLOCKUP_DETECTOR=y" + ): + build._assert_watchdog_kernel_config(config, debug=True) - arguments = launch.openvmm_arguments() - workspace, toolcache, cache = ( - sandbox.aggregate_child_name(index, mount.guest_target) - for index, mount in enumerate(launch.mounts) - ) - # Several shares are the children of one aggregate, and OpenVMM - # attributes each denied path to the child that contains it, so - # relative paths are joined to their share. - self.assertNotIn("--mount", arguments) - self.assertEqual( - arguments[arguments.index("--mount-aggregate") :], - [ - "--mount-aggregate", - "/run/nvx/shares", - "--mount-child", - f"{workspace},{os.fspath(Path('work'))},rw", - "--mount-child", - f"{toolcache},{os.fspath(Path('tools'))},ro", - "--mount-child", - f"{cache},{os.fspath(Path('cache'))},ro", - "--mount-deny", - os.fspath(Path.cwd() / "work" / "secrets"), - "--mount-deny", - os.fspath(Path.cwd() / "tools" / "credentials"), - ], - ) - fragment = sandbox.mounts_command_line_fragment(launch.mounts) - self.assertEqual( - fragment, - " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=rw" - " virtfs_aggregate=1", - ) - # The guest binds each child at its target, in order. - command_line = launch.kernel_command_line() - self.assertNotIn("virtfs_", command_line) - self.assertTrue( - command_line.endswith( - f" nvx_share={workspace},/workspace,rw" - f" nvx_share={toolcache},/opt/hostedtoolcache,ro" - f" nvx_share={cache},/srv/cache,ro" - ), - command_line, + def test_kernel_config_fragment_replaces_base_assignments(self): + base = "\n".join( + ( + "#", + "# Kernel hacking", + "#", + "# CONFIG_DEBUG_KERNEL is not set", + "CONFIG_RCU_CPU_STALL_TIMEOUT=21", + "CONFIG_UNRELATED=y", + ) ) - self.assertEqual( - sandbox.mounts_command_line_fragment(launch.mounts[1:]), - " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=ro" - " virtfs_aggregate=1", - ) - with self.assertRaisesRegex(common.ScriptError, "owned by the sandbox"): - launch.kernel_command_line("nvx_share=0,/etc,rw") - - # Every share's tokens count toward the x86 budget. - base = sandbox.SandboxLaunch( - layers=(distro,), scratch=Path("s.ext4") - ).kernel_command_line() - shares = len(command_line) - len(base) - fitting = "x" * ( - sandbox.SANDBOX_COMMAND_LINE_MAX_SIZE - - len(base) - - shares - - len(fragment) - - 2 - ) - launch.kernel_command_line(fitting) - with self.assertRaisesRegex(common.ScriptError, "1024-byte"): - launch.kernel_command_line(fitting + "x") - - caller = tuple( - sandbox.SandboxMount( - guest_target=target, host_path=Path(path), owner="caller" - ) - for target, path in (("/workspace", "work"), ("/tools", "tools")) - ) - arguments = sandbox.mounts_openvmm_arguments(caller) - self.assertEqual(arguments.count("--mount-owner"), 1) - self.assertEqual(arguments[-2:], ["--mount-owner", "caller"]) - - def test_aggregate_child_names_pin_their_targets(self): - name = sandbox.aggregate_child_name(0, "/workspace") - self.assertEqual(name, f"0-{hashlib.sha256(b'/workspace').hexdigest()[:32]}") - # Equal targets give equal names, so a restore names its children as - # the boot did, and another target or position gives another name. - self.assertEqual(sandbox.aggregate_child_name(0, "/workspace"), name) - self.assertNotEqual(sandbox.aggregate_child_name(0, "/workspace2"), name) - self.assertNotEqual(sandbox.aggregate_child_name(1, "/workspace"), name) - # Every name follows OpenVMM's rule for the name of a child. - for index, target in ((0, "/w"), (999, "/" + "x" * 1000)): - self.assertRegex( - sandbox.aggregate_child_name(index, target), - r"\A[0-9]+-[0-9a-f]{32}\Z", + fragment = "\n".join( + ( + "# Comment lines and blank lines are ignored.", + "", + "CONFIG_DEBUG_KERNEL=y", + "# CONFIG_DEBUG_MISC is not set", + "CONFIG_RCU_CPU_STALL_TIMEOUT=21", ) - self.assertLessEqual(len(sandbox.aggregate_child_name(999, "/w")), 64) - - def test_launch_bounds_shares_by_the_kernel_command_line(self): - distro = sandbox.SandboxLayer.parse( - "distro,distro.erofs,11111111-1111-1111-1111-111111111111" ) - - def launch(count: int) -> sandbox.SandboxLaunch: - return sandbox.SandboxLaunch( - layers=(distro,), - scratch=Path("s.ext4"), - mounts=tuple( - sandbox.SandboxMount.parse(f"/share/{index:02},host{index}") - for index in range(count) - ), - ) - - # No fixed number of shares is the limit, only the command line. - launch(10).kernel_command_line() - with self.assertRaisesRegex(common.ScriptError, "fewer --mount shares"): - launch(20).kernel_command_line() - - def test_launch_rejects_invalid_share_sets(self): - distro = sandbox.SandboxLayer.parse( - "distro,distro.erofs,11111111-1111-1111-1111-111111111111" + self.assertEqual( + build.merge_kernel_config_fragment(base, fragment).splitlines(), + [ + "#", + "# Kernel hacking", + "#", + "CONFIG_UNRELATED=y", + "CONFIG_DEBUG_KERNEL=y", + "# CONFIG_DEBUG_MISC is not set", + "CONFIG_RCU_CPU_STALL_TIMEOUT=21", + ], ) - - def mount(value: str, owner: str = "vmm") -> sandbox.SandboxMount: - return sandbox.SandboxMount.parse(value, (), owner) - - for mounts, message in ( - ((mount("/workspace,a,rw"), mount("/workspace,b")), "overlap"), - ((mount("/workspace,a,rw"), mount("/workspace/cache,b")), "overlap"), - ((mount("/opt/tools/node,a"), mount("/opt,b,rw")), "overlap"), + for invalid, message in ( + ("DEBUG_KERNEL=y\n", "line 1 is invalid"), + ("# only a comment\n", "assigns no symbols"), ( - (mount("/workspace,a,rw"), mount("/tools,b", "caller")), - "same --mount-owner", + "CONFIG_DEBUG_KERNEL=y\n# CONFIG_DEBUG_KERNEL is not set\n", + "more than once", ), ): - with self.subTest(mounts=[item.guest_target for item in mounts]): + with self.subTest(fragment=invalid): with self.assertRaisesRegex(common.ScriptError, message): - sandbox.SandboxLaunch( - layers=(distro,), scratch=Path("s.ext4"), mounts=mounts - ) - # A shared prefix that is not a path component is not an overlap. - sandbox.SandboxLaunch( - layers=(distro,), - scratch=Path("s.ext4"), - mounts=(mount("/work,a,rw"), mount("/workspace,b")), - ) + build.merge_kernel_config_fragment(base, invalid) - def test_launch_validation_rejects_overlapping_or_foreign_host_paths(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - work = root / "work" - tools = root / "tools" - layer.write_bytes(b"layer") - scratch.write_bytes(b"scratch") - (work / "nested").mkdir(parents=True) - (tools / "credentials").mkdir(parents=True) - - def launch(*mounts: sandbox.SandboxMount) -> sandbox.SandboxLaunch: - return sandbox.SandboxLaunch( - layers=( - sandbox.SandboxLayer( - role="distro", - path=layer, - uuid="11111111-1111-1111-1111-111111111111", - ), - ), - scratch=scratch, - mounts=mounts, - ) + def test_checked_in_debug_fragment_only_adds_watchdogs(self): + base = (BuildConstants.REPO_ROOT / KernelBuildConstants.INPUT_CONFIG).read_text( + encoding="utf-8" + ) + fragment = ( + BuildConstants.REPO_ROOT / KernelBuildConstants.DEBUG_CONFIG_FRAGMENT + ).read_text(encoding="utf-8") + merged = build.merge_kernel_config_fragment(base, fragment).splitlines() + for setting in KernelBuildConstants.REQUIRED_DEBUG_CONFIG: + with self.subTest(setting=setting): + self.assertIn(setting, merged) + for setting in ( + "# CONFIG_DEBUG_MISC is not set", + "# CONFIG_RCU_TRACE is not set", + "# CONFIG_X86_DEBUG_FPU is not set", + "CONFIG_RCU_CPU_STALL_TIMEOUT=21", + ): + with self.subTest(setting=setting): + self.assertIn(setting, merged) + self.assertNotIn("# CONFIG_DEBUG_KERNEL is not set", merged) + for setting in KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG: + with self.subTest(time_abi=setting): + self.assertIn(setting, merged) - workspace = sandbox.SandboxMount.parse(f"/workspace,{work},rw") - for host_path in (work / "nested", work, root): - with self.subTest(host_path=host_path): - with self.assertRaisesRegex(common.ScriptError, "overlap"): - launch( - workspace, - sandbox.SandboxMount.parse( - f"/opt/hostedtoolcache,{host_path}" - ), - ).validated() - - # Each denied path must be inside its own share. - foreign = sandbox.SandboxMount.parse( - f"/workspace,{work},rw", (os.fspath(tools / "credentials"),) - ) - toolcache = sandbox.SandboxMount.parse(f"/opt/hostedtoolcache,{tools}") - with self.assertRaisesRegex(common.ScriptError, "not inside"): - launch(foreign, toolcache).validated() - # A denied path may name a share's root, which OpenVMM then hides - # behind its allowed paths, but no allowed or writable path may. - launch( - sandbox.SandboxMount.parse( - f"/workspace,{work},rw", - (".",), - allowed_paths=(os.fspath(work / "nested"),), - ), - toolcache, - ).validated() - for allowed_paths, writable_paths in (((".",), ()), ((), (".",))): - with self.subTest(allowed=allowed_paths, writable=writable_paths): - with self.assertRaisesRegex(common.ScriptError, "not inside"): - launch( - sandbox.SandboxMount.parse( - f"/workspace,{work},rw", - ("nested",), - allowed_paths=allowed_paths, - writable_paths=writable_paths, - ), - toolcache, - ).validated() - - validated = launch( - sandbox.SandboxMount.parse(f"/workspace,{work},rw", ("nested",)), - sandbox.SandboxMount.parse( - f"/opt/hostedtoolcache,{tools}", - (os.fspath(tools / "credentials"),), - ), - ).validated() - self.assertEqual(len(validated.mounts), 2) - - # Resolving a path doesn't see through a bind mount, so one directory - # reached through two paths, or a share inside a bind mount of the - # other share's directory, is rejected by its file identities. - sandbox.validate_mount_host_paths((workspace, toolcache)) - with patch.object(sandbox, "_path_identity", return_value=(1, 1)): - with self.assertRaisesRegex(common.ScriptError, "same directory"): - sandbox.validate_mount_host_paths((workspace, toolcache)) - alias = root / "alias" - (alias / "cache").mkdir(parents=True) - - def identity(path: Path) -> tuple[int, int]: - # `alias` is a bind mount of the workspace's directory. - if path in (work.resolve(), alias.resolve()): - return 1, 1 - return 0, hash(path) - - cache = sandbox.SandboxMount.parse( - f"/opt/hostedtoolcache,{alias / 'cache'}" - ) - with patch.object(sandbox, "_path_identity", side_effect=identity): - sandbox.validate_mount_host_paths((workspace, toolcache)) - for mounts in ((workspace, cache), (cache, workspace)): - with self.assertRaisesRegex(common.ScriptError, "inside the other"): - sandbox.validate_mount_host_paths(mounts) - - def test_launch_validation_requires_plain_mount_directory(self): + def test_debug_kernel_build_applies_fragment_and_records_provenance(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - share = root / "share" - layer.write_bytes(b"layer") - scratch.write_bytes(b"scratch") - - def launch(host_path: Path) -> sandbox.SandboxLaunch: - return sandbox.SandboxLaunch( - layers=( - sandbox.SandboxLayer( - role="distro", - path=layer, - uuid="11111111-1111-1111-1111-111111111111", - ), - ), - scratch=scratch, - mounts=( - sandbox.SandboxMount( - guest_target="/workspace", host_path=host_path - ), - ), + input_config = root / KernelBuildConstants.INPUT_CONFIG + input_config.parent.mkdir(parents=True) + input_config.write_text( + "\n".join( + ( + *KernelBuildConstants.REQUIRED_DIRECT_BOOT_CONFIG, + *KernelBuildConstants.REQUIRED_VIRTIO_CONSOLE_CONFIG, + *KernelBuildConstants.REQUIRED_SHARED_STATUS_CONFIG, + *KernelBuildConstants.REQUIRED_SANDBOX_CONFIG, + *KernelBuildConstants.REQUIRED_NETWORK_CONFIG, + *KernelBuildConstants.REQUIRED_TIME_ABI_CONFIG, + *KernelBuildConstants.REQUIRED_HARDENING_CONFIG, + "# CONFIG_DEBUG_KERNEL is not set", + ) ) - - with self.assertRaisesRegex(common.ScriptError, "plain directory"): - launch(share).validated() - share.write_bytes(b"file") - with self.assertRaisesRegex(common.ScriptError, "plain directory"): - launch(share).validated() - share.unlink() - share.mkdir() - validated = launch(share).validated() - self.assertEqual(validated.mounts[0].host_path, share) - - def test_sandbox_command_forwards_mount_to_openvmm(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - share = root / "share" - layer.write_bytes(b"distro") - scratch.write_bytes(b"scratch") - share.mkdir() - args = nvx.parse_args( - [ - "sandbox", - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - "--mount", - f"/workspace,{share},rw", - "--mount-deny", - "secrets", - "--dry-run", - ] + + "\n", + encoding="utf-8", + ) + fragment = root / KernelBuildConstants.DEBUG_CONFIG_FRAGMENT + fragment.write_text( + "# debug fixture\n" + + "\n".join(KernelBuildConstants.REQUIRED_DEBUG_CONFIG) + + "\n", + encoding="utf-8", + ) + patch_path = root / KernelBuildConstants.PATCH_DIRECTORY / "example.patch" + patch_path.parent.mkdir() + patch_path.write_text("patch", encoding="utf-8") + source = root / "source" + source.mkdir() + output_directory = root / "output" + output_directory.mkdir() + production_provenance = ( + output_directory / KernelBuildConstants.PROVENANCE_NAME + ) + production_provenance.write_text("production", encoding="utf-8") + config = build_config.KernelBuildConfig( + work=root / "work-debug", + output=output_directory / KernelBuildConstants.DEBUG_BINARY_NAME, + debug=True, ) - def require(path: Path, _description: str) -> Path: - return path + with patch.object(BuildConstants, "REPO_ROOT", root): + source_fingerprint = build._kernel_source_fingerprint() + + def run_build(command: object, **_kwargs: object) -> None: + if isinstance(command, list) and command[-1] == "vmlinux": + (config.work / "vmlinux").write_bytes(b"debug kernel") with ( - patch.object(nvx, "require_file", side_effect=require), + patch.object(BuildConstants, "REPO_ROOT", root), + patch.object(build, "_require_linux"), + patch.object(build, "require_tool", return_value="tool"), patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, + build, + "prepare_kernel_source", + return_value=(source, source_fingerprint), + ), + patch.object(build, "run_checked", side_effect=run_build), ): - nvx.command_sandbox(args) + build.build_kernel(config) - command = format_command.call_args.args[0] + generated = ( + output_directory / KernelBuildConstants.DEBUG_CONFIG_NAME + ).read_text(encoding="utf-8") + self.assertIn("CONFIG_DEBUG_KERNEL=y", generated.splitlines()) + self.assertNotIn("# CONFIG_DEBUG_KERNEL is not set", generated) + provenance = json.loads( + ( + output_directory / KernelBuildConstants.DEBUG_PROVENANCE_NAME + ).read_text(encoding="utf-8") + ) + self.assertEqual( + provenance["debug_config_fragment"], + { + "path": KernelBuildConstants.DEBUG_CONFIG_FRAGMENT.as_posix(), + "sha256": common.sha256_file(fragment), + }, + ) self.assertEqual( - command[command.index("--mount") + 1], f"/workspace,{share},rw" + provenance["input_config"]["sha256"], common.sha256_file(input_config) + ) + self.assertEqual( + provenance["kernel_sha256"], + hashlib.sha256(b"debug kernel").hexdigest(), + ) + self.assertEqual( + production_provenance.read_text(encoding="utf-8"), "production" + ) + self.assertIn( + "debug_config_fragment_sha256", + (config.work / KernelBuildConstants.BUILD_STAMP_NAME).read_text( + encoding="utf-8" + ), + ) + self.assertEqual( + config.provenance_name, KernelBuildConstants.DEBUG_PROVENANCE_NAME + ) + self.assertEqual( + build_config.KernelBuildConfig().provenance_name, + KernelBuildConstants.PROVENANCE_NAME, ) - self.assertEqual(command[command.index("--mount-deny") + 1], "secrets") - self.assertNotIn("--mount-owner", command) + self.assertNotIn("debug_config_fragment", build.kernel_provenance_inputs()) - def test_sandbox_command_shares_a_file(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - settings = root / "settings.json" - layer.write_bytes(b"distro") - scratch.write_bytes(b"scratch") - settings.write_bytes(b"{}") + def test_guest_build_adds_debug_kernel_only_when_requested(self): + with ( + patch.object(build, "build_kernel") as kernel, + patch.object(build, "build_initramfs"), + patch.object(build, "build_docker_debug_kernel") as docker_debug, + ): + native = build_config.BuildConfig(native_guest=True, debug_kernel=True) + build.build_guest(native) + self.assertEqual( + kernel.call_args_list, + [ + call(native.kernel), + call(build_config.KernelBuildConfig.debug_variant()), + ], + ) + docker_debug.assert_not_called() - def sandbox_args(*mount: str) -> argparse.Namespace: - return nvx.parse_args( - [ - "sandbox", - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - *mount, - "--dry-run", - ] - ) + with ( + patch.object(build, "build_docker_artifacts") as docker, + patch.object(build, "build_docker_debug_kernel") as docker_debug, + ): + portable = build_config.BuildConfig(debug_kernel=True) + build.build_guest(portable) + docker.assert_called_once_with(portable.docker, "alpine") + docker_debug.assert_called_once_with(portable.docker) - def require(path: Path, _description: str) -> Path: - return path + with ( + patch.object(build, "build_docker_artifacts"), + patch.object(build, "build_docker_debug_kernel") as docker_debug, + ): + build.build_guest(build_config.BuildConfig()) + docker_debug.assert_not_called() - with ( - patch.object(nvx, "require_file", side_effect=require), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, - ): - nvx.command_sandbox( - sandbox_args("--mount", f"/etc/app/settings.json,{settings},ro") - ) - command = format_command.call_args.args[0] - name = sandbox.aggregate_child_name(0, "/etc/app/settings.json") - # The regular file is a child of its own, without its directory, - # which OpenVMM must find to be a regular file. - self.assertNotIn("--mount", command) - self.assertEqual( - command[command.index("--mount-child") + 1], - f"{name},{settings},ro,file", - ) - self.assertIn( - f"nvx_share={name},/etc/app/settings.json,ro,file", - command[command.index("--cmdline") + 1], - ) - with self.assertRaisesRegex(common.ScriptError, "shares a file"): - nvx.command_sandbox( - sandbox_args( - "--mount", - f"/etc/app/settings.json,{settings},rw", - "--mount-write", - str(settings), - ) - ) + debug = build_config.KernelBuildConfig.debug_variant() + self.assertTrue(debug.debug) + self.assertEqual(debug.output.name, KernelBuildConstants.DEBUG_BINARY_NAME) + self.assertEqual( + debug.work.name, KernelBuildConstants.DEBUG_WORK_DIRECTORY_NAME + ) - def test_sandbox_command_shares_host_paths_with_commas(self): + def test_guest_files_install_one_checked_rust_build(self): with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - alpha = root / "alpha,beta" - reports = root / "2026, final" - layer.write_bytes(b"distro") - scratch.write_bytes(b"scratch") - for share in (alpha, reports): - (share / "secret").mkdir(parents=True) + root = Path(temporary) / "root" + work = Path(temporary) / "work" + for directory in (root / "sbin", root / "etc", work): + directory.mkdir(parents=True) + payloads = { + name: f"rust-guest-binary {name}".encode() + for name in GuestRustBuildConstants.BINARIES + } + result = guest_binaries.GuestBinaryBuildResult( + source_sha256="a" * 64, + binaries={ + name: hashlib.sha256(payload).hexdigest() + for name, payload in payloads.items() + }, + ) - def sandbox_args(*mounts: str) -> argparse.Namespace: - return nvx.parse_args( - [ - "sandbox", - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - *mounts, - "--dry-run", - ] + def stage_binaries(output: Path) -> guest_binaries.GuestBinaryBuildResult: + output.mkdir() + for name, payload in payloads.items(): + (output / name).write_bytes(payload) + return result + + with patch.object( + build, "build_guest_binaries", side_effect=stage_binaries + ) as rust_build: + helpers = build._install_guest_files( + build_config.InitramfsBuildConfig(work=work), + root, + guests.guest_descriptor("alpine"), ) - def require(path: Path, _description: str) -> Path: - return path + rust_build.assert_called_once_with( + work / GuestRustBuildConstants.OUTPUT_DIRECTORY_NAME + ) + for name, payload in payloads.items(): + with self.subTest(helper=name): + installed = root / "sbin" / name + self.assertEqual(installed.read_bytes(), payload) + if os.name != "nt": + self.assertEqual(installed.stat().st_mode & 0o777, 0o755) + self.assertEqual( + helpers[name], + { + "source_sha256": result.source_sha256, + "binary_sha256": result.binaries[name], + }, + ) + for name in ( + "STATIC_HELPER_CFLAGS", + "DEVICE_IO_CFLAGS", + "STATIC_HELPERS", + "MUSL_COMPILER", + "MUSL_STATIC_HELPERS", + "TIME_PROBE_CFLAGS", + ): + with self.subTest(helper=name): + self.assertFalse(hasattr(InitramfsBuildConstants, name)) + def test_guest_files_reuse_the_configured_prebuilt_rust_binaries(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / "root" + work = Path(temporary) / "work" + prebuilt = Path(temporary) / "prebuilt" + for directory in (root / "sbin", root / "etc", work, prebuilt): + directory.mkdir(parents=True) + binaries: dict[str, str] = {} + for name in GuestRustBuildConstants.BINARIES: + path = prebuilt / name + path.write_bytes(name.encode("ascii")) + binaries[name] = common.sha256_file(path) + result = guest_binaries.GuestBinaryBuildResult( + source_sha256="a" * 64, + binaries=binaries, + ) + environment = GuestRustBuildConstants.BINARIES_VARIABLE with ( - patch.object(nvx, "require_file", side_effect=require), + patch.dict(os.environ, {environment: str(prebuilt)}), patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, + build, "inspect_guest_binaries", return_value=result + ) as inspect, + patch.object(build, "build_guest_binaries") as rust_build, ): - nvx.command_sandbox( - sandbox_args( - "--mount", f"/work,{alpha},rw", "--mount-deny", "secret" - ) - ) - command = format_command.call_args.args[0] - self.assertEqual( - command[command.index("--mount") + 1], f"/work,{alpha},rw" + helpers = build._install_guest_files( + build_config.InitramfsBuildConfig(work=work), + root, + guests.guest_descriptor("alpine"), ) - self.assertEqual(command[command.index("--mount-deny") + 1], "secret") - - nvx.command_sandbox( - sandbox_args( - "--mount", - f"/work,{alpha},rw", - "--mount-deny", - "secret", - "--mount", - f"/reports,{reports},ro", - "--mount-deny", - "secret", + + inspect.assert_called_once_with(prebuilt) + rust_build.assert_not_called() + for name in GuestRustBuildConstants.BINARIES: + with self.subTest(helper=name): + self.assertEqual( + (root / "sbin" / name).read_bytes(), name.encode("ascii") ) + self.assertEqual(helpers[name]["source_sha256"], "a" * 64) + self.assertEqual(helpers[name]["binary_sha256"], binaries[name]) + + def test_docker_debug_kernel_target_exports_debug_artifacts(self): + dockerfile = (BuildConstants.REPO_ROOT / "docker" / "Dockerfile").read_text( + encoding="utf-8" + ) + self.assertIn( + "FROM base AS kernel-debug\n" + "RUN python3 scripts/nvx.py build-kernel --debug\n", + dockerfile, + ) + self.assertIn( + f"FROM scratch AS {DockerBuildConstants.DEBUG_KERNEL_TARGET}", dockerfile + ) + for name in DockerBuildConstants.DEBUG_KERNEL_ARTIFACT_NAMES: + with self.subTest(artifact=name): + self.assertIn( + f"COPY --from=kernel-debug /repo/build/{name} /{name}", dockerfile ) - command = format_command.call_args.args[0] - work, report = ( - sandbox.aggregate_child_name(index, target) - for index, target in enumerate(("/work", "/reports")) - ) - self.assertEqual( - [ - (value, command[index + 1]) - for index, value in enumerate(command) - if value in ("--mount-child", "--mount-deny") - ], - [ - ("--mount-child", f"{work},{alpha},rw"), - ("--mount-child", f"{report},{reports},ro"), - ("--mount-deny", os.fspath(alpha / "secret")), - ("--mount-deny", os.fspath(reports / "secret")), - ], - ) - with self.assertRaisesRegex( - common.ScriptError, "end the value with ,ro or ,rw" - ): - nvx.command_sandbox(sandbox_args("--mount", f"/work,{alpha}")) - - # A managed sandbox records the host path as given and attaches it - # on start. - state = root / "state" - sandbox_lifecycle.provision( - state, - sandbox.SandboxLaunch( - layers=( - sandbox.SandboxLayer( - role="distro", - path=layer, - uuid="11111111-1111-1111-1111-111111111111", - ), - ), - scratch=scratch, - mounts=( - sandbox.SandboxMount.parse( - f"/reports,{reports},ro", ("secret",) - ), - ), - ), - hypervisor="whp", - memory_mib=256, - net=None, - network_profile=None, - network_egress=None, - network_ingress=None, - network_egress_allow=(), - network_egress_deny=(), - host_loopback=None, - network_proxy=None, - host_loopback_forward=(), - cmdline="quiet", - timeout=10, - ) - config = json.loads( - (state / sandbox_lifecycle.CONFIG_NAME).read_text(encoding="utf-8") - ) - self.assertEqual(config["mount"]["host_path"], os.fspath(reports)) - process = MagicMock() - process.pid = 123 - process.stdin = io.BytesIO() + + with tempfile.TemporaryDirectory() as temporary: + destination = Path(temporary) + + def export(_command: object, **_kwargs: object) -> None: + for name in DockerBuildConstants.DEBUG_KERNEL_ARTIFACT_NAMES: + (destination / name).write_bytes(b"artifact") + + config = build_config.DockerBuildConfig(artifact_destination=destination) with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object( - sandbox_lifecycle.subprocess, "Popen", return_value=process - ) as popen, - patch.object( - sandbox_lifecycle, "_process_start_time", return_value=456 - ), - patch.object( - sandbox_lifecycle.ControlSession, - "connect", - return_value=MagicMock(), - ), + patch.object(build, "require_tool", return_value="docker"), + patch.object(build, "run_checked", side_effect=export) as run, ): - sandbox_lifecycle.start(state, 10) - command = popen.call_args.args[0] + build.build_docker_debug_kernel(config) + command = run.call_args.args[0] self.assertEqual( - command[command.index("--mount") + 1], f"/reports,{reports},ro" + command[command.index("--target") + 1], + DockerBuildConstants.DEBUG_KERNEL_TARGET, ) - self.assertEqual(command[command.index("--mount-deny") + 1], "secret") - def test_run_attaches_a_file_as_an_aggregate_child(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - settings = root / "settings.json" - settings.write_bytes(b"{}") - work = root / "work" - work.mkdir() + for name in DockerBuildConstants.DEBUG_KERNEL_ARTIFACT_NAMES: + (destination / name).unlink() with ( - patch.object(nvx, "require_file", return_value=Path("artifact")), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, + patch.object(build, "require_tool", return_value="docker"), + patch.object(build, "run_checked"), + self.assertRaisesRegex(common.ScriptError, "did not produce"), ): - nvx.command_run( - nvx.parse_args( - [ - "run", - "--mount", - f"/config/settings.json,{settings},ro", - "--dry-run", - ] - ) - ) - command = format_command.call_args.args[0] - name = sandbox.aggregate_child_name(0, "/config/settings.json") - # Even alone, a file is the child of an aggregate. - self.assertNotIn("--mount", command) - self.assertEqual( - command[command.index("--mount-aggregate") + 1], "/run/nvx/shares" - ) - self.assertEqual( - command[command.index("--mount-child") + 1], - f"{name},{settings},ro,file", - ) - self.assertEqual( - command[command.index("--cmdline") + 1], - f"nvx_share={name},/config/settings.json,ro,file", - ) + build.build_docker_debug_kernel(config) - # A directory and a file share the aggregate, and only the - # file's token is marked. - nvx.command_run( - nvx.parse_args( - [ - "run", - "--mount", - f"/work,{work},rw", - "--mount", - f"/config/settings.json,{settings},ro", - "--dry-run", - ] - ) - ) - command = format_command.call_args.args[0] - work_name, settings_name = ( - sandbox.aggregate_child_name(index, target) - for index, target in enumerate(("/work", "/config/settings.json")) - ) - self.assertEqual( - command[command.index("--cmdline") + 1], - f"nvx_share={work_name},/work,rw " - f"nvx_share={settings_name},/config/settings.json,ro,file", - ) - for arguments, message in ( - (["--mount-deny", "secrets"], "absolute host path"), - (["--cmdline", "x" * 1024], "1024-byte"), - ): - with self.subTest(arguments=arguments): - invalid = nvx.parse_args( - [ - "run", - "--mount", - f"/config/settings.json,{settings},ro", - *arguments, - "--dry-run", - ] - ) - with self.assertRaisesRegex(common.ScriptError, message): - nvx.command_run(invalid) - invalid = nvx.parse_args( - ["run", f"--mount=/a b,{settings}", "--dry-run"] - ) - with self.assertRaisesRegex( - common.ScriptError, "invalid --mount target" - ): - nvx.command_run(invalid) +def _posix_shell() -> str | None: + shell = shutil.which("sh") + if shell is None: + git = shutil.which("git") + git_shell = Path(git).parent.parent / "bin" / "sh.exe" if git else None + if git_shell is not None and git_shell.is_file(): + shell = str(git_shell) + return shell + + +def _shell_function(source: str, name: str) -> str: + start = source.index(f"\n{name}() {{\n") + 1 + return source[start : source.index("\n}\n", start) + 3] + + +class SandboxShareAgentTests(unittest.TestCase): + AGENT = Path(__file__).parents[1] / "guest" / "common" / "nvx-init-agent" + + def setUp(self): + shell = _posix_shell() + if shell is None: + self.skipTest("POSIX shell is unavailable") + self.shell = shell + self.source = self.AGENT.read_text(encoding="utf-8") + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.rootfs = self.root / "rootfs" + self.rootfs.mkdir() + self.runtime = (self.root / "run").as_posix() + + SHARE_FUNCTIONS = ( + "validate_share_target", + "make_share_mount_point", + "record_live_share", + "claim_share_target", + "mount_live_share", + "parse_share_child", + "bind_live_share", + "mount_live_shares", + ) + + def _aggregate(self, *children: str, mode: str = "rw") -> str: + """Returns the bootstrap of an aggregate whose children the + CHILD,TARGET,MODE specifications `children` place.""" + tokens = [ + f"virtfs_dir={self.runtime}/shares", + "virtfs_tag=microvm", + f"virtfs_mode={mode}", + "virtfs_aggregate=1", + *(f"nvx_share={child}" for child in children), + ] + return " ".join(tokens) + + def _run(self, cmdline: str) -> tuple[subprocess.CompletedProcess[str], str]: + mount_log = self.root / "mount.log" + mount_log.unlink(missing_ok=True) + functions = "".join( + _shell_function(self.source, name) for name in self.SHARE_FUNCTIONS + ) + script = ( + "set -eu\n" + "rootfs=$1\ncmdline=$2\nmount_log=$3\nruntime=$4\nshare_mountpoints=\n" + 'fatal() { echo "FATAL: $*" >&2; exit 125; }\n' + 'mount() { printf "%s\\n" "$*" >>"$mount_log"; }\n' + f"{functions}" + "mount_live_shares\n" + "echo \"mountpoints=$(printf '%s' \"$share_mountpoints\" | tr '\\n' ' ')\"\n" + ) + result = subprocess.run( + [ + self.shell, + "-s", + "--", + self.rootfs.as_posix(), + cmdline, + mount_log.as_posix(), + self.runtime, + ], + input=script, + text=True, + capture_output=True, + check=False, + ) + log = mount_log.read_text(encoding="utf-8") if mount_log.exists() else "" + return result, log - def test_sandbox_command_forwards_mount_owner(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - share = root / "share" - layer.write_bytes(b"distro") - scratch.write_bytes(b"scratch") - share.mkdir() - args = nvx.parse_args( - [ - "sandbox", - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - "--mount", - f"/workspace,{share},rw", - "--mount-owner", - "caller", - "--dry-run", - ] - ) + def _run_teardown( + self, script: str, *, shares: tuple[str, ...], failing: str = "" + ) -> tuple[subprocess.CompletedProcess[str], list[str]]: + runtime = self.root / "run" + runtime.mkdir(exist_ok=True) + for name in ("container.pid", "workload-machine-id"): + (runtime / name).write_text("x\n", encoding="utf-8") + log = self.root / "teardown.log" + log.unlink(missing_ok=True) + functions = "".join( + _shell_function(self.source, name) + for name in ("unmount_live_shares", "fatal", "teardown") + ).replace("/sbin/nvx-exit", "nvx_exit") + # The agent records mounted shares one per line, most recent first. + # Windows drops a newline from a process argument, so pass a separator. + mountpoints = "|".join( + f"{self.rootfs.as_posix()}{target}" for target in reversed(shares) + ) + result = subprocess.run( + [ + self.shell, + "-s", + "--", + runtime.as_posix(), + log.as_posix(), + mountpoints, + failing, + ], + input=( + "set -eu\n" + "runtime=$1\nlog=$2\nfailing=$4\n" + "share_mountpoints=$(printf '%s' \"$3\" | tr '|' '\\n')\n" + "rootfs=$runtime/rootfs\nlayers=$runtime/layers\n" + "scratch=$runtime/scratch\n" + 'umount() { printf "umount %s\\n" "$1" >>"$log"; ' + '[ "$1" != "$failing" ]; }\n' + 'mountpoint() { [ "$2" = "$layers/distro" ]; }\n' + 'nvx_exit() { printf "exit %s\\n" "$1" >>"$log"; exit "$1"; }\n' + f"{functions}{script}\n" + ), + text=True, + capture_output=True, + check=False, + ) + lines = log.read_text(encoding="utf-8").splitlines() if log.exists() else [] + return result, lines - def require(path: Path, _description: str) -> Path: - return path + def test_agent_mounts_share_after_lifecycle_checks_for_both_lifecycles(self): + mount_call = self.source.index("\nmount_live_shares\n") + self.assertLess( + self.source.index('fatal "configured workload home is unavailable"'), + mount_call, + ) + self.assertLess( + self.source.index('fatal "unsupported workload lifecycle'), mount_call + ) + machine_id = self.source.index('>"$runtime/workload-machine-id"') + managed_agent = self.source.index("\n /sbin/nvx-managed-agent \\\n") + self.assertLess(mount_call, machine_id) + self.assertLess(machine_id, managed_agent) + self.assertLess(managed_agent, self.source.index("/sbin/nvx-container-launch")) + self.assertNotIn("exec /sbin/nvx-managed-agent", self.source) + self.assertEqual(self.source.count('\nteardown "$status"\n'), 1) + self.assertEqual(self.source.count('\n teardown "$status"\n'), 1) + self.assertLess(managed_agent, self.source.index('\n teardown "$status"\n')) - with ( - patch.object(nvx, "require_file", side_effect=require), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, - ): - if sys.platform == "win32": - with self.assertRaisesRegex(common.ScriptError, "Linux host"): - nvx.command_sandbox(args) - format_command.assert_not_called() - return - if share.stat().st_uid == 0: - self.skipTest("caller ownership refuses a root-owned share") - nvx.command_sandbox(args) + def test_agent_teardown_unmounts_share_before_overlay_layers_and_scratch(self): + result, log = self._run_teardown("teardown 7", shares=("/workspace",)) + runtime = (self.root / "run").as_posix() + self.assertEqual(result.returncode, 7, result.stderr) + self.assertEqual( + log, + [ + f"umount {self.rootfs.as_posix()}/workspace", + f"umount {runtime}/rootfs", + f"umount {runtime}/layers/distro", + f"umount {runtime}/scratch", + "exit 7", + ], + ) + self.assertIn("NVX-SANDBOX-EXIT: status=7", result.stdout) + self.assertFalse((self.root / "run" / "workload-machine-id").exists()) + self.assertFalse((self.root / "run" / "container.pid").exists()) - command = format_command.call_args.args[0] - self.assertEqual(command[command.index("--mount-owner") + 1], "caller") - self.assertLess(command.index("--mount"), command.index("--mount-owner")) + result, log = self._run_teardown("teardown 0", shares=()) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(log[0], f"umount {runtime}/rootfs") - def test_sandbox_command_rejects_misplaced_mount_options(self): - deny_only = nvx.parse_args( - [ - "sandbox", - "--layer", - "distro,distro.erofs,11111111-1111-1111-1111-111111111111", - "--scratch", - "scratch.ext4", - "--mount-deny", - "secrets", - ] + def test_agent_teardown_unmounts_shares_in_reverse_mount_order(self): + rootfs = self.rootfs.as_posix() + runtime = (self.root / "run").as_posix() + result, log = self._run_teardown( + "teardown 0", shares=("/workspace", "/opt/hostedtoolcache") ) - with self.assertRaisesRegex(common.ScriptError, "requires --mount"): - nvx.command_sandbox(deny_only) - owner_only = nvx.parse_args( + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + log[:3], [ - "sandbox", - "--layer", - "distro,distro.erofs,11111111-1111-1111-1111-111111111111", - "--scratch", - "scratch.ext4", - "--mount-owner", - "caller", - ] + f"umount {rootfs}/opt/hostedtoolcache", + f"umount {rootfs}/workspace", + f"umount {runtime}/rootfs", + ], ) - with self.assertRaisesRegex(common.ScriptError, "--mount-owner requires"): - nvx.command_sandbox(owner_only) - for operation in ("start", "exec", "stop", "deprovision"): - with self.subTest(operation=operation): - args = nvx.parse_args( - [ - "sandbox", - operation, - "--state-dir", - "state", - "--mount", - "/workspace,share,rw", - ] - ) - with self.assertRaisesRegex(common.ScriptError, "only valid"): - nvx.command_sandbox(args) - - def test_sandbox_command_attributes_policy_paths_to_the_preceding_mount(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - work = root / "work" - tools = root / "tools" - layer.write_bytes(b"distro") - scratch.write_bytes(b"scratch") - work.mkdir() - tools.mkdir() - common_args = [ - "sandbox", - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - ] - args = nvx.parse_args( - [ - *common_args, - "--mount", - f"/workspace,{work},rw", - "--mount-deny", - "logs", - "--mount-allow", - "logs/payloads", - "--mount-write", - "out", - "--mount", - f"/opt/hostedtoolcache,{tools},ro", - "--mount-deny", - "credentials", - "--dry-run", - ] - ) - workspace, toolcache = nvx._sandbox_mounts(args) - self.assertEqual( - ( - workspace.denied_paths, - workspace.allowed_paths, - workspace.writable_paths, - ), - (("logs",), ("logs/payloads",), ("out",)), - ) - self.assertEqual( - ( - toolcache.denied_paths, - toolcache.allowed_paths, - toolcache.writable_paths, - ), - (("credentials",), (), ()), - ) - def require(path: Path, _description: str) -> Path: - return path + # A share that fails to unmount does not stop the others. + result, log = self._run_teardown( + "teardown 0", + shares=("/workspace", "/opt/hostedtoolcache"), + failing=f"{rootfs}/opt/hostedtoolcache", + ) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("failed to unmount the live share", result.stderr) + self.assertEqual( + log[:2], + [f"umount {rootfs}/opt/hostedtoolcache", f"umount {rootfs}/workspace"], + ) + self.assertEqual(log[-1], "exit 1") - with ( - patch.object(nvx, "require_file", side_effect=require), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, - ): - nvx.command_sandbox(args) + def test_agent_teardown_reports_share_unmount_failure(self): + share = f"{self.rootfs.as_posix()}/workspace" + result, log = self._run_teardown( + "teardown 0", shares=("/workspace",), failing=share + ) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("failed to unmount the live share", result.stderr) + self.assertEqual(log[0], f"umount {share}") + self.assertEqual(log[-1], "exit 1") + self.assertIn("NVX-SANDBOX-EXIT: status=1", result.stdout) - command = format_command.call_args.args[0] + def test_agent_fatal_unmounts_share_before_power_off(self): + result, log = self._run_teardown( + 'fatal "synthetic failure"', shares=("/workspace",) + ) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn("NVX-SANDBOX-ERROR: synthetic failure", result.stderr) + self.assertEqual( + log, [f"umount {self.rootfs.as_posix()}/workspace", "exit 125"] + ) - def values(option: str) -> list[str]: - return [ - command[index + 1] - for index, value in enumerate(command) - if value == option - ] + result, log = self._run_teardown('fatal "early failure"', shares=()) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertEqual(log, ["exit 125"]) - # With several shares, OpenVMM attributes absolute paths. - self.assertEqual( - values("--mount-allow"), [os.fspath(work / "logs" / "payloads")] - ) - self.assertEqual(values("--mount-write"), [os.fspath(work / "out")]) + rootfs = self.rootfs.as_posix() + result, log = self._run_teardown( + 'fatal "late failure"', shares=("/workspace", "/opt/hostedtoolcache") + ) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertEqual( + log, + [ + f"umount {rootfs}/opt/hostedtoolcache", + f"umount {rootfs}/workspace", + "exit 125", + ], + ) - for arguments, message in ( - (["--mount-write", "out"], "--mount-write requires --mount"), - # A --mount-write after the read-only share would narrow it. - ( - [ - "--mount", - f"/workspace,{work},rw", - "--mount", - f"/opt/hostedtoolcache,{tools},ro", - "--mount-write", - "cache", - ], - "is read-only", - ), - ( - [ - "--mount-allow", - "logs", - "--mount", - f"/workspace,{work},rw", - "--mount", - f"/opt/hostedtoolcache,{tools}", - ], - "each --mount-allow must follow the --mount", - ), - # A path in one share cannot be attributed to the other. - ( - [ - "--mount", - f"/workspace,{work},rw", - "--mount-write", - os.fspath(tools / "cache"), - "--mount", - f"/opt/hostedtoolcache,{tools}", - ], - "--mount-write .* is not inside the host directory", - ), - # Only a denied path may name a share's root. - ( - [ - "--mount", - f"/workspace,{work},rw", - "--mount-deny", - "logs", - "--mount-allow", - os.fspath(work), - "--mount", - f"/opt/hostedtoolcache,{tools}", - ], - "--mount-allow .* is not inside the host directory", - ), - ( + def test_agent_skips_share_without_bootstrap_tokens(self): + result, log = self._run("console=hvc0 nvx_sandbox=1") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("mountpoints=\n", result.stdout) + self.assertEqual(log, "") + + def test_agent_mounts_share_inside_container_rootfs(self): + (self.rootfs / "opt").mkdir() + for mode in ("rw", "ro"): + with self.subTest(mode=mode): + result, log = self._run( + "nvx_sandbox=1 virtfs_dir=/opt/hostedtoolcache " + f"virtfs_tag=microvm virtfs_mode={mode}" + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue((self.rootfs / "opt" / "hostedtoolcache").is_dir()) + self.assertIn( + f"NVX-SANDBOX-SHARE: target=/opt/hostedtoolcache mode={mode}", + result.stdout, + ) + self.assertEqual( + log.split(), [ - "--mount", - f"/workspace,{work},rw", - "--mount-write", - os.fspath(work), - "--mount", - f"/opt/hostedtoolcache,{tools}", + "-t", + "virtiofs", + "-o", + f"{mode},nosuid,nodev", + "microvm", + f"{self.rootfs.as_posix()}/opt/hostedtoolcache", ], - "--mount-write .* is not inside the host directory", - ), - ): - with self.subTest(message=message): - with self.assertRaisesRegex(common.ScriptError, message): - nvx.command_sandbox(nvx.parse_args([*common_args, *arguments])) + ) + self.assertIn( + f"mountpoints={self.rootfs.as_posix()}/opt/hostedtoolcache\n", + result.stdout, + ) - def test_sandbox_command_hides_a_share_root_behind_its_allowed_paths(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - work = root / "work" - tools = root / "tools" - layer.write_bytes(b"distro") - scratch.write_bytes(b"scratch") - (work / "src").mkdir(parents=True) - tools.mkdir() - args = nvx.parse_args( - [ - "sandbox", - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - "--mount", - f"/workspace,{work},rw", - "--mount-deny", - os.fspath(work), - "--mount-allow", - os.fspath(work / "src"), - "--mount", - f"/opt/hostedtoolcache,{tools}", - "--dry-run", - ] + def test_agent_binds_every_child_of_an_aggregate(self): + rootfs = self.rootfs.as_posix() + result, log = self._run( + "nvx_sandbox=1 " + + self._aggregate("0,/workspace,rw", "1,/opt/hostedtoolcache,ro") + ) + self.assertEqual(result.returncode, 0, result.stderr) + # The aggregate stays outside the container rootfs; each child is bound + # inside it with its own mode. + self.assertEqual( + log.splitlines(), + [ + f"-t virtiofs -o rw,nosuid,nodev microvm {self.runtime}/shares", + f"-o bind {self.runtime}/shares/0 {rootfs}/workspace", + f"-o remount,bind,rw,nosuid,nodev {rootfs}/workspace", + f"-o bind {self.runtime}/shares/1 {rootfs}/opt/hostedtoolcache", + f"-o remount,bind,ro,nosuid,nodev {rootfs}/opt/hostedtoolcache", + ], + ) + self.assertIn( + "NVX-SANDBOX-SHARE: target=/workspace mode=rw child=0\n" + "NVX-SANDBOX-SHARE: target=/opt/hostedtoolcache mode=ro child=1\n", + result.stdout, + ) + # Teardown unmounts the most recent bind first, and the aggregate last. + self.assertIn( + f"mountpoints={rootfs}/opt/hostedtoolcache {rootfs}/workspace " + f"{self.runtime}/shares\n", + result.stdout, + ) + + def test_agent_binds_children_by_the_names_that_nvx_gives_them(self): + rootfs = self.rootfs.as_posix() + workspace = sandbox.aggregate_child_name(0, "/workspace") + tools = sandbox.aggregate_child_name(1, "/opt/tools") + result, log = self._run( + self._aggregate( + f"{workspace},/workspace,rw", f"{tools},/opt/tools,ro", "a.b_C-9,/x,ro" ) + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn( + f"-o bind {self.runtime}/shares/{workspace} {rootfs}/workspace\n", log + ) + self.assertIn( + f"-o bind {self.runtime}/shares/{tools} {rootfs}/opt/tools\n", log + ) + self.assertIn(f"-o bind {self.runtime}/shares/a.b_C-9 {rootfs}/x\n", log) + self.assertIn( + f"NVX-SANDBOX-SHARE: target=/workspace mode=rw child={workspace}\n", + result.stdout, + ) + # A repeated name is refused, however the child is named. + result, log = self._run( + self._aggregate(f"{workspace},/workspace,rw", f"{workspace},/b,ro") + ) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn("child is not unique", result.stderr) + self.assertEqual(log, "") - def require(path: Path, _description: str) -> Path: - return path + def test_hostmount_accepts_only_child_names_inside_the_aggregate(self): + hostmount = self.AGENT.with_name("nvx-hostmount").read_text(encoding="utf-8") + script = ( + 'fail() { echo "FAIL: $*" >&2; exit 1; }\n' + f"{_shell_function(hostmount, 'parse_share')}" + 'parse_share "$1"\n' + 'printf "%s|%s|%s|%s\\n" "$share_child" "$share_target" "$share_mode" ' + '"$share_kind"\n' + ) - with ( - patch.object(nvx, "require_file", side_effect=require), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, - ): - nvx.command_sandbox(args) + def parse(share: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [self.shell, "-s", "--", share], + input=script, + text=True, + capture_output=True, + check=False, + ) - command = format_command.call_args.args[0] + name = sandbox.aggregate_child_name(0, "/workspace") + for share, expected in ( + (f"{name},/workspace,rw", f"{name}|/workspace|rw|directory\n"), + ("0,/opt/tools,ro", "0|/opt/tools|ro|directory\n"), + ("a.b_C-9,/x,ro", "a.b_C-9|/x|ro|directory\n"), + # A file child is bound at a file. + ("1,/config/settings.json,ro,file", "1|/config/settings.json|ro|file\n"), + ("1,/results/output.txt,rw,file", "1|/results/output.txt|rw|file\n"), + ): + with self.subTest(share=share): + result = parse(share) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, expected) + for share in ( + ",/workspace,rw", + ".,/workspace,rw", + "..,/workspace,rw", + "a/b,/workspace,rw", + "a:b,/workspace,rw", + "a*,/workspace,rw", + "0,/workspace,file", + "0,/workspace,rw,dir", + ): + with self.subTest(share=share): + result = parse(share) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("invalid live share", result.stderr) - def values(option: str) -> list[str]: - return [ - command[index + 1] - for index, value in enumerate(command) - if value == option - ] + def test_hostmount_makes_file_mount_points_without_following_links(self): + hostmount = self.AGENT.with_name("nvx-hostmount").read_text(encoding="utf-8") + script = ( + f"{_shell_function(hostmount, 'make_file_mount_point')}" + 'make_file_mount_point "$1"\n' + ) - # OpenVMM hides the workspace's root behind its one allowed path. - workspace = sandbox.aggregate_child_name(0, "/workspace") - toolcache = sandbox.aggregate_child_name(1, "/opt/hostedtoolcache") - self.assertEqual( - values("--mount-child"), - [f"{workspace},{work},rw", f"{toolcache},{tools},ro"], - ) - self.assertEqual(values("--mount-deny"), [os.fspath(work)]) - self.assertEqual(values("--mount-allow"), [os.fspath(work / "src")]) + def make(target: Path) -> int: + return subprocess.run( + [self.shell, "-s", "--", target.as_posix()], + input=script, + text=True, + capture_output=True, + check=False, + ).returncode - def test_sandbox_command_attributes_mount_deny_to_the_preceding_mount(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) - layer = root / "distro.erofs" - scratch = root / "scratch.ext4" - work = root / "work" - tools = root / "tools" - layer.write_bytes(b"distro") - scratch.write_bytes(b"scratch") - work.mkdir() - tools.mkdir() - common_args = [ - "sandbox", - "--layer", - f"distro,{layer},11111111-1111-1111-1111-111111111111", - "--scratch", - str(scratch), - ] - args = nvx.parse_args( - [ - *common_args, - "--mount", - f"/workspace,{work},rw", - "--mount-deny", - "secrets", - "--mount", - f"/opt/hostedtoolcache,{tools},ro", - "--mount-deny", - "credentials", - "--mount-deny", - "logs", - "--dry-run", - ] - ) - mounts = nvx._sandbox_mounts(args) - self.assertEqual( - [(mount.guest_target, mount.denied_paths) for mount in mounts], - [ - ("/workspace", ("secrets",)), - ("/opt/hostedtoolcache", ("credentials", "logs")), - ], - ) + # A missing file is created with its directories, and an existing + # one is kept as it is. + created = root / "config" / "settings.json" + self.assertEqual(make(created), 0) + self.assertEqual(created.read_bytes(), b"") + existing = root / "existing.json" + existing.write_bytes(b"{}") + self.assertEqual(make(existing), 0) + self.assertEqual(existing.read_bytes(), b"{}") + (root / "directory").mkdir() + self.assertEqual(make(root / "directory"), 1) + if os.name == "nt": + return + # A link is refused before anything is created through it, whether + # or not its target exists. + dangling = root / "dangling.json" + dangling.symlink_to(root / "elsewhere.json") + self.assertEqual(make(dangling), 1) + self.assertFalse((root / "elsewhere.json").exists()) + linked = root / "linked.json" + linked.symlink_to(existing) + self.assertEqual(make(linked), 1) + self.assertEqual(existing.read_bytes(), b"{}") - def require(path: Path, _description: str) -> Path: - return path + def test_agent_binds_a_file_child_at_a_file(self): + rootfs = self.rootfs.as_posix() + (self.rootfs / "opt").mkdir() + (self.rootfs / "opt" / "existing.json").write_text("{}", encoding="utf-8") + result, log = self._run( + "nvx_sandbox=1 " + + self._aggregate( + "0,/workspace,rw", + "1,/etc/app/settings.json,ro,file", + "2,/opt/existing.json,rw,file", + ) + ) + self.assertEqual(result.returncode, 0, result.stderr) + # Each file child gets a file mount point, created inside the container + # rootfs or reused there, and is bound with its own mode. + settings = self.rootfs / "etc" / "app" / "settings.json" + self.assertTrue(settings.is_file()) + self.assertEqual(settings.read_text(encoding="utf-8"), "") + self.assertEqual( + (self.rootfs / "opt" / "existing.json").read_text(encoding="utf-8"), "{}" + ) + self.assertTrue((self.rootfs / "workspace").is_dir()) + self.assertEqual( + log.splitlines(), + [ + f"-t virtiofs -o rw,nosuid,nodev microvm {self.runtime}/shares", + f"-o bind {self.runtime}/shares/0 {rootfs}/workspace", + f"-o remount,bind,rw,nosuid,nodev {rootfs}/workspace", + f"-o bind {self.runtime}/shares/1 {rootfs}/etc/app/settings.json", + f"-o remount,bind,ro,nosuid,nodev {rootfs}/etc/app/settings.json", + f"-o bind {self.runtime}/shares/2 {rootfs}/opt/existing.json", + f"-o remount,bind,rw,nosuid,nodev {rootfs}/opt/existing.json", + ], + ) + self.assertIn( + f"mountpoints={rootfs}/opt/existing.json {rootfs}/etc/app/settings.json " + f"{rootfs}/workspace {self.runtime}/shares\n", + result.stdout, + ) - with ( - patch.object(nvx, "require_file", side_effect=require), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, - ): - nvx.command_sandbox(args) + # A directory is no file mount point, a file no directory one, and + # the runtime binds the machine ID itself. + (self.rootfs / "dir").mkdir() + (self.rootfs / "file").write_text("x", encoding="utf-8") + for child, message in ( + ("0,/dir,ro,file", "not a regular file"), + ("0,/file/settings.json,ro,file", "not a directory"), + ("0,/file,ro", "not a directory"), + ("0,/etc/machine-id,ro,file", "reserved"), + ("0,/etc/machine-id/child,ro", "reserved"), + ("0,/run,ro", "reserved"), + ("0,/run/nvx,ro", "reserved"), + ("0,/run/nvx/time/state,ro", "reserved"), + ): + with self.subTest(child=child): + result, log = self._run(self._aggregate(child)) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn(message, result.stderr) + self.assertEqual(log, "") - command = format_command.call_args.args[0] - workspace = sandbox.aggregate_child_name(0, "/workspace") - toolcache = sandbox.aggregate_child_name(1, "/opt/hostedtoolcache") - self.assertNotIn("--mount", command) - self.assertEqual( - [ - command[index + 1] - for index, value in enumerate(command) - if value in ("--mount-aggregate", "--mount-child") - ], - [ - "/run/nvx/shares", - f"{workspace},{work},rw", - f"{toolcache},{tools},ro", - ], - ) - self.assertEqual( - [ - command[index + 1] - for index, value in enumerate(command) - if value == "--mount-deny" - ], - [ - os.fspath(work / "secrets"), - os.fspath(tools / "credentials"), - os.fspath(tools / "logs"), - ], - ) - cmdline = command[command.index("--cmdline") + 1] - self.assertIn( - f"nvx_share={workspace},/workspace,rw " - f"nvx_share={toolcache},/opt/hostedtoolcache,ro", - cmdline, - ) + def test_agent_rejects_repeated_children_and_overlapping_targets(self): + for children, message in ( + (("0,/workspace,rw", "0,/opt/tools,ro"), "child is not unique"), + (("0,/workspace,rw", "1,/workspace,ro"), "targets overlap"), + (("0,/workspace,rw", "1,/workspace/cache,ro"), "targets overlap"), + (("0,/opt/tools/node,ro", "1,/opt,rw"), "targets overlap"), + ): + with self.subTest(children=children): + result, log = self._run(self._aggregate(*children)) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn(message, result.stderr) + # Every child is checked before anything is mounted. + self.assertEqual(log, "") - # With one share, a --mount-deny may come first; with several, it - # must follow the share whose directory it hides. - single = nvx.parse_args( - [*common_args, "--mount-deny", "secrets", "--mount", f"/w,{work},rw"] - ) - self.assertEqual(nvx._sandbox_mounts(single)[0].denied_paths, ("secrets",)) - leading = nvx.parse_args( - [ - *common_args, - "--mount-deny", - "secrets", - "--mount", - f"/workspace,{work},rw", - "--mount", - f"/opt/hostedtoolcache,{tools}", - ] - ) - with self.assertRaisesRegex(common.ScriptError, "must follow the --mount"): - nvx.command_sandbox(leading) - # More shares are children of the same aggregate. - (root / "cache").mkdir() - three = nvx.parse_args( - [ - *common_args, - "--mount", - f"/a,{work}", - "--mount", - f"/b,{tools}", - "--mount", - f"/c,{root / 'cache'}", - "--dry-run", - ] - ) - with ( - patch.object(nvx, "require_file", side_effect=require), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, - ): - nvx.command_sandbox(three) - command = format_command.call_args.args[0] - self.assertEqual(command.count("--mount-child"), 3) + # A shared prefix that is not a path component is not an overlap. + result, log = self._run(self._aggregate("0,/work,rw", "1,/workspace,ro")) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(len(log.splitlines()), 5) - def test_run_shares_host_paths_with_commas(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - alpha = root / "alpha,beta" - reports = root / "2026, final" - settings = root / "settings,v2.json" - alpha.mkdir() - reports.mkdir() - settings.write_bytes(b"{}") - with ( - patch.object(nvx, "require_file", return_value=Path("artifact")), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, - ): - # The mode follows the last comma, so a lone directory stays a - # plain --mount of its exact path, and OpenVMM gets the mode - # even when the value omits it. - for value, expected in ( - (f"/work,{alpha},rw", f"/work,{alpha},rw"), - (f"/reports,{reports},ro", f"/reports,{reports},ro"), - (f"/plain,{root}", f"/plain,{root},ro"), - ): - with self.subTest(value=value): - nvx.command_run( - nvx.parse_args(["run", "--mount", value, "--dry-run"]) - ) - command = format_command.call_args.args[0] - self.assertNotIn("--mount-aggregate", command) - self.assertEqual( - command[command.index("--mount") + 1], expected - ) + def test_agent_parses_every_share_before_mounting_any(self): + for cmdline in ( + "virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rw " + "virtfs_dir=/opt/tools", + "virtfs_tag=microvm virtfs_mode=ro", + "virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rw " + "virtfs_dir= virtfs_tag=microvm virtfs_mode=ro", + "virtfs_aggregate=1", + "nvx_share=0,/workspace,rw", + ): + with self.subTest(cmdline=cmdline): + result, log = self._run(cmdline) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn("bootstrap is incomplete", result.stderr) + self.assertEqual(log, "") - # Several shares are the children of an aggregate, whose values - # end in the mode and, for a file, the `file` flag. - nvx.command_run( - nvx.parse_args( - [ - "run", - "--mount", - f"/work,{alpha},rw", - "--mount", - f"/reports,{reports},ro", - "--mount", - f"/config/settings.json,{settings},ro", - "--mount-deny", - str(alpha / "secret"), - "--dry-run", - ] - ) - ) - command = format_command.call_args.args[0] - work, report, config = ( - sandbox.aggregate_child_name(index, target) - for index, target in enumerate( - ("/work", "/reports", "/config/settings.json") - ) - ) - self.assertEqual( - [ - command[index + 1] - for index, value in enumerate(command) - if value == "--mount-child" - ], - [ - f"{work},{alpha},rw", - f"{report},{reports},ro", - f"{config},{settings},ro,file", - ], - ) - self.assertEqual( - command[command.index("--mount-deny") + 1], str(alpha / "secret") - ) + def test_agent_fatal_unmounts_mounted_shares(self): + mount_log = self.root / "mount.log" + functions = "".join( + _shell_function(self.source, name) + for name in ("unmount_live_shares", "fatal", *self.SHARE_FUNCTIONS) + ).replace("/sbin/nvx-exit", "nvx_exit") + result = subprocess.run( + [ + self.shell, + "-s", + "--", + self.rootfs.as_posix(), + self._aggregate("0,/workspace,rw", "1,/opt/tools,ro"), + mount_log.as_posix(), + self.runtime, + ], + input=( + "set -eu\n" + "rootfs=$1\ncmdline=$2\nmount_log=$3\nruntime=$4\n" + "share_mountpoints=\n" + 'mount() { printf "mount %s\\n" "$*" >>"$mount_log"; ' + 'case "$*" in *shares/1*) return 1 ;; esac; }\n' + 'umount() { printf "umount %s\\n" "$1" >>"$mount_log"; }\n' + 'nvx_exit() { printf "exit %s\\n" "$1" >>"$mount_log"; exit "$1"; }\n' + f"{functions}mount_live_shares\n" + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn("failed to bind live share 1", result.stderr) + rootfs = self.rootfs.as_posix() + self.assertEqual( + mount_log.read_text(encoding="utf-8").splitlines(), + [ + f"mount -t virtiofs -o rw,nosuid,nodev microvm {self.runtime}/shares", + f"mount -o bind {self.runtime}/shares/0 {rootfs}/workspace", + f"mount -o remount,bind,rw,nosuid,nodev {rootfs}/workspace", + f"mount -o bind {self.runtime}/shares/1 {rootfs}/opt/tools", + f"umount {rootfs}/workspace", + f"umount {self.runtime}/shares", + "exit 125", + ], + ) - # Without the mode, the text after the last comma is no mode. - for value in (f"/work,{alpha}", f"/reports,{reports}"): - with ( - self.subTest(value=value), - self.assertRaisesRegex( - common.ScriptError, "end the value with ,ro or ,rw" - ), - ): - nvx.command_run( - nvx.parse_args(["run", "--mount", value, "--dry-run"]) - ) + def test_agent_fails_closed_for_invalid_share_bootstrap(self): + (self.rootfs / "file").write_text("x", encoding="utf-8") + for cmdline, message in ( + ("virtfs_dir=/workspace", "incomplete"), + ("virtfs_dir=/workspace virtfs_tag=microvm", "incomplete"), + ("virtfs_dir=/workspace virtfs_tag=bad/tag virtfs_mode=rw", "tag"), + ("virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rx", "mode"), + ("virtfs_dir=workspace virtfs_tag=microvm virtfs_mode=rw", "absolute"), + ("virtfs_dir=/ virtfs_tag=microvm virtfs_mode=rw", "canonical"), + ("virtfs_dir=/a//b virtfs_tag=microvm virtfs_mode=rw", "canonical"), + ("virtfs_dir=/a/ virtfs_tag=microvm virtfs_mode=rw", "canonical"), + ("virtfs_dir=/a/./b virtfs_tag=microvm virtfs_mode=rw", "canonical"), + ("virtfs_dir=/a/../b virtfs_tag=microvm virtfs_mode=rw", "canonical"), + ("virtfs_dir=/a=b virtfs_tag=microvm virtfs_mode=rw", "reserved char"), + ("virtfs_dir=/proc virtfs_tag=microvm virtfs_mode=rw", "reserved"), + ("virtfs_dir=/sys/fs virtfs_tag=microvm virtfs_mode=rw", "reserved"), + ("virtfs_dir=/dev/shm virtfs_tag=microvm virtfs_mode=rw", "reserved"), + ("virtfs_dir=/.nvx-agent virtfs_tag=microvm virtfs_mode=ro", "reserved"), + ("virtfs_dir=/etc virtfs_tag=microvm virtfs_mode=ro", "reserved"), + ("virtfs_dir=/file virtfs_tag=microvm virtfs_mode=ro", "not a directory"), + ( + "virtfs_dir=/workspace virtfs_tag=microvm virtfs_mode=rw " + "nvx_share=0,/workspace,rw", + "require an aggregate", + ), + ( + "virtfs_dir=/shares virtfs_tag=microvm virtfs_mode=rw " + "virtfs_aggregate=1 nvx_share=0,/workspace,rw", + "must be mounted at", + ), + (self._aggregate(), "has no children"), + (self._aggregate(",/workspace,rw"), "child is invalid"), + (self._aggregate(".,/workspace,rw"), "child is invalid"), + (self._aggregate("..,/workspace,rw"), "child is invalid"), + (self._aggregate("a/b,/workspace,rw"), "child is invalid"), + (self._aggregate("a:b,/workspace,rw"), "child is invalid"), + (self._aggregate("a*,/workspace,rw"), "child is invalid"), + (self._aggregate("0,/workspace"), "child is invalid"), + (self._aggregate("0,/workspace,rx"), "mode"), + (self._aggregate("0,workspace,rw"), "absolute"), + (self._aggregate("0,/a,b,rw"), "reserved char"), + (self._aggregate("0,/workspace,rw", "1,/proc,ro"), "reserved"), + (self._aggregate("0,/file,ro"), "not a directory"), + ): + with self.subTest(cmdline=cmdline): + result, log = self._run(cmdline) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn("live share", result.stderr) + self.assertIn(message, result.stderr) + self.assertEqual(log, "") - def test_run_attaches_several_mounts_as_an_aggregate(self): - args = nvx.parse_args( - [ - "run", - "--mount", - "/workspace,work,rw", - "--mount", - "/opt/hostedtoolcache,tools", - "--mount", - "/srv/cache,cache,ro", - "--cmdline", - "quiet", - "--dry-run", - ] + def test_agent_rejects_symbolic_link_in_share_target(self): + outside = self.root / "outside" + outside.mkdir() + try: + (self.rootfs / "workspace").symlink_to(outside, target_is_directory=True) + except OSError as error: + self.skipTest(f"symbolic links are unavailable: {error}") + result, log = self._run( + "virtfs_dir=/workspace/project virtfs_tag=microvm virtfs_mode=rw" ) - with ( - patch.object(nvx, "require_file", return_value=Path("artifact")), - patch.object( - nvx, "_format_command", return_value="formatted" - ) as format_command, - ): - nvx.command_run(args) - command = format_command.call_args.args[0] - workspace, toolcache, cache = ( - sandbox.aggregate_child_name(index, target) - for index, target in enumerate( - ("/workspace", "/opt/hostedtoolcache", "/srv/cache") - ) - ) - booted = [ - "/run/nvx/shares", - f"{workspace},work,rw", - f"{toolcache},tools,ro", - f"{cache},cache,ro", - ] - self.assertNotIn("--mount", command) - self.assertEqual( - [ - command[index + 1] - for index, value in enumerate(command) - if value in ("--mount-aggregate", "--mount-child") - ], - booted, - ) - self.assertEqual( - command[command.index("--cmdline") + 1], - f"quiet nvx_share={workspace},/workspace,rw " - f"nvx_share={toolcache},/opt/hostedtoolcache,ro " - f"nvx_share={cache},/srv/cache,ro", - ) + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn("crosses a symbolic link", result.stderr) + self.assertEqual(log, "") + self.assertFalse((outside / "project").exists()) - # One share stays a plain --mount without bind tokens. - single = nvx.parse_args( - ["run", "--mount", "/workspace,work,rw", "--dry-run"] - ) - nvx.command_run(single) - command = format_command.call_args.args[0] - self.assertEqual( - command[command.index("--mount") :], ["--mount", "/workspace,work,rw"] - ) - self.assertNotIn("--cmdline", command) - # A restored guest keeps its binds, so only the devices are named, - # with the children's names of the boot: they pin the targets that - # the guest bound, and another target names another child. - restored = nvx.parse_args( - [ - "run", - "--restore-snapshot", - "snapshot", - "--mount", - "/workspace,work,rw", - "--mount", - "/opt/hostedtoolcache,tools", - "--mount", - "/srv/cache,cache,ro", - "--dry-run", - ] - ) - nvx.command_run(restored) - command = format_command.call_args.args[0] - self.assertEqual( - [ - command[index + 1] - for index, value in enumerate(command) - if value in ("--mount-aggregate", "--mount-child") - ], - booted, - ) - self.assertNotIn("--cmdline", command) - retargeted = nvx.parse_args( - [ - "run", - "--restore-snapshot", - "snapshot", - "--mount", - "/workspace,work,rw", - "--mount", - "/opt/tools,tools", - "--mount", - "/srv/cache,cache,ro", - "--dry-run", - ] - ) - nvx.command_run(retargeted) - command = format_command.call_args.args[0] - children = [ - command[index + 1] - for index, value in enumerate(command) - if value == "--mount-child" - ] - self.assertEqual(children[0::2], booted[1::2]) - self.assertNotEqual(children[1], booted[2]) - self.assertEqual(children[1].split(",", 1)[1], "tools,ro") +@unittest.skipIf(os.name == "nt", "requires POSIX symbolic links") +class SandboxSmokeShareTests(unittest.TestCase): + SMOKE = Path(__file__).parents[1] / "guest" / "common" / "nvx-sandbox-smoke" - for mounts, message in ( - (["/a,a,rx"], "unsupported --mount mode"), - (["/workspace,a", "/workspace/cache,b"], "overlap"), - (["/run/nvx,a", "/b,b"], "overlap"), - (["/a b,a", "/b,b"], "invalid --mount target"), - (["relative,a", "/b,b"], "invalid --mount target"), - ([f"/share/{index:02},host" for index in range(60)], "1024-byte"), - ): - with self.subTest(mounts=mounts[:2]): - invalid = nvx.parse_args( - ["run", *[f"--mount={mount}" for mount in mounts], "--dry-run"] - ) - with self.assertRaisesRegex(common.ScriptError, message): - nvx.command_run(invalid) - relative = nvx.parse_args( - [ - "run", - "--mount", - "/a,a", - "--mount", - "/b,b", - "--mount-deny", - "secrets", - "--dry-run", - ] + def setUp(self): + shell = _posix_shell() + if shell is None: + self.skipTest("POSIX shell is unavailable") + self.shell = shell + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.share = self.root / "share" + self.links = self.share / "nvx-links" + self.links.mkdir(parents=True) + (self.share / "nvx-host-marker").write_text("host-to-guest\n", encoding="utf-8") + # The guest resolves nvx-links/outside to this path through ../.. + self.outside = self.root / "nvx-outside-marker" + source = self.SMOKE.read_text(encoding="utf-8") + self.functions = "".join( + _shell_function(source, name) + for name in ( + "check_share", + "check_share_ownership", + "check_share_denied", + "expect_eperm", + "check_share_policy", + "expect_erofs", + "check_share_symlinks", + "check_shares_isolated", ) - with self.assertRaisesRegex(common.ScriptError, "absolute host path"): - nvx.command_run(relative) + ).replace("/tmp/nvx-tool", '"$scratch/nvx-tool"') - def test_managed_lifecycle_persists_and_replays_two_shares(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - layer_path = root / "distro.erofs" - scratch_path = root / "scratch.ext4" - work = root / "work" - tools = root / "tools" - layer_path.write_bytes(b"layer") - scratch_path.write_bytes(b"scratch") - work.mkdir() - tools.mkdir() - state = root / "state" - sandbox_lifecycle.provision( - state, - sandbox.SandboxLaunch( - layers=( - sandbox.SandboxLayer( - role="distro", - path=layer_path, - uuid="11111111-1111-1111-1111-111111111111", - ), - ), - scratch=scratch_path, - mounts=( - sandbox.SandboxMount.parse( - f"/workspace,{work},rw", ("secrets",) - ), - sandbox.SandboxMount.parse(f"/opt/hostedtoolcache,{tools}"), - ), - ), - hypervisor="whp", - memory_mib=256, - net=None, - network_profile=None, - network_egress=None, - network_ingress=None, - network_egress_allow=(), - network_egress_deny=(), - host_loopback=None, - network_proxy=None, - host_loopback_forward=(), - cmdline="quiet", - timeout=10, - ) + def _run(self, call: str) -> subprocess.CompletedProcess[str]: + scratch = self.root / "scratch" + scratch.mkdir(exist_ok=True) + return subprocess.run( + [self.shell, "-s", "--", self.share.as_posix(), scratch.as_posix()], + input=f"set -eu\nshare=$1\nscratch=$2\n{self.functions}{call}\n", + text=True, + capture_output=True, + check=False, + ) - config = json.loads( - (state / sandbox_lifecycle.CONFIG_NAME).read_text(encoding="utf-8") - ) - self.assertEqual( - config["format"], sandbox_lifecycle.MULTI_MOUNT_CONFIG_FORMAT - ) - # Older releases know only the single `mount`, and reject format 4. - self.assertNotIn("mount", config) - with self.assertRaisesRegex(common.ScriptError, "unsupported format"): - sandbox_lifecycle._read_json( - state / sandbox_lifecycle.CONFIG_NAME, - "sandbox configuration", - version=(1, 2, 3), - ) - self.assertEqual( - config["mounts"], - [ - { - "guest_target": "/workspace", - "host_path": os.fspath(work), - "access": "rw", - "denied_paths": ["secrets"], - "owner": "vmm", - }, - { - "guest_target": "/opt/hostedtoolcache", - "host_path": os.fspath(tools), - "access": "ro", - "denied_paths": [], - "owner": "vmm", - }, - ], - ) + def test_rw_share_creates_links_with_exact_targets(self): + result = self._run('check_share "$share" rw') + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn( + f"NVX-UBUNTU-SANDBOX-SYMLINK-OK target={self.share.as_posix()}", + result.stdout, + ) + self.assertIn("NVX-UBUNTU-SANDBOX-SHARE-OK", result.stdout) + self.assertEqual( + {name: os.readlink(self.links / name) for name in os.listdir(self.links)}, + { + "nvx-tool": "../nvx-tool", + "absolute": "/nvx/absolute/target", + "outside": "../../nvx-outside-marker", + "denied": "../nvx-denied/secret", + "denied-directory": "../nvx-denied", + }, + ) + self.assertEqual( + (self.share / "nvx-guest-marker").read_text(encoding="utf-8"), + "guest-to-host\n", + ) + self.assertTrue((self.share / "nvx-guest-directory").is_dir()) - process = MagicMock() - process.pid = 123 - process.stdin = io.BytesIO() + def test_links_fail_when_they_expose_host_data(self): + (self.share / "nvx-guest-marker").write_text( + "guest-to-host\n", encoding="utf-8" + ) + self.outside.write_text("host-outside\n", encoding="utf-8") + result = self._run('check_share_symlinks "$share"') + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertNotIn("NVX-UBUNTU-SANDBOX-SYMLINK-OK", result.stdout) - def require(path: Path, _description: str) -> Path: - return path + for path in self.links.iterdir(): + path.unlink() + self.outside.unlink() + denied = self.share / "nvx-denied" + denied.mkdir() + (denied / "secret").write_text("secret\n", encoding="utf-8") + result = self._run('check_share_symlinks "$share"') + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertNotIn("NVX-UBUNTU-SANDBOX-SYMLINK-OK", result.stdout) - with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object( - sandbox_lifecycle.subprocess, "Popen", return_value=process - ) as popen, - patch.object( - sandbox_lifecycle, "_process_start_time", return_value=456 - ), - patch.object( - sandbox_lifecycle.ControlSession, - "connect", - return_value=MagicMock(), - ), - ): - sandbox_lifecycle.start(state, 10) + def test_ro_share_rejects_links(self): + geteuid = getattr(os, "geteuid", None) + if geteuid is not None and geteuid() == 0: + self.skipTest("root ignores directory write permissions") + self.share.chmod(0o555) + self.addCleanup(self.share.chmod, 0o755) - command = popen.call_args.args[0] - self.assertEqual( - [ - command[index + 1] - for index, value in enumerate(command) - if value in ("--mount-aggregate", "--mount-child", "--mount-deny") - ], - [ - "/run/nvx/shares", - f"{sandbox.aggregate_child_name(0, '/workspace')},{work},rw", - f"{sandbox.aggregate_child_name(1, '/opt/hostedtoolcache')}," - f"{tools},ro", - os.fspath(work / "secrets"), - ], - ) + result = self._run('check_share "$share" ro') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("NVX-UBUNTU-SANDBOX-SHARE-OK", result.stdout) + self.assertFalse(os.path.lexists(self.share / "nvx-guest-link")) - def test_managed_lifecycle_persists_and_replays_the_access_policy(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - layer_path = root / "distro.erofs" - scratch_path = root / "scratch.ext4" - work = root / "work" - layer_path.write_bytes(b"layer") - scratch_path.write_bytes(b"scratch") - work.mkdir() - state = root / "state" - sandbox_lifecycle.provision( - state, - sandbox.SandboxLaunch( - layers=( - sandbox.SandboxLayer( - role="distro", - path=layer_path, - uuid="11111111-1111-1111-1111-111111111111", - ), - ), - scratch=scratch_path, - mounts=( - sandbox.SandboxMount.parse( - f"/workspace,{work},rw", - ("logs",), - allowed_paths=("logs/payloads",), - writable_paths=("out",), - ), - ), - ), - hypervisor="whp", - memory_mib=256, - net=None, - network_profile=None, - network_egress=None, - network_ingress=None, - network_egress_allow=(), - network_egress_deny=(), - host_loopback=None, - network_proxy=None, - host_loopback_forward=(), - cmdline="quiet", - timeout=10, - ) + self.share.chmod(0o755) + result = self._run('check_share "$share" ro') + self.assertEqual(result.returncode, 1, result.stdout) - config = json.loads( - (state / sandbox_lifecycle.CONFIG_NAME).read_text(encoding="utf-8") - ) - self.assertEqual(config["format"], sandbox_lifecycle.POLICY_CONFIG_FORMAT) - # Earlier releases read at most format 4, which would start the - # share without its policy, so they must reject format 5. - self.assertNotIn("mount", config) - with self.assertRaisesRegex(common.ScriptError, "unsupported format"): - sandbox_lifecycle._read_json( - state / sandbox_lifecycle.CONFIG_NAME, - "sandbox configuration", - version=(1, 2, 3, 4), - ) - self.assertEqual( - config["mounts"], - [ - { - "guest_target": "/workspace", - "host_path": os.fspath(work), - "access": "rw", - "denied_paths": ["logs"], - "allowed_paths": ["logs/payloads"], - "writable_paths": ["out"], - "owner": "vmm", - }, - ], - ) + def test_policy_share_requires_openvmm_to_refuse_writes(self): + (self.share / "nvx-writable").mkdir() + allowed = self.share / "nvx-denied" / "nvx-allowed" + allowed.mkdir(parents=True) + (allowed / "payload").write_text("payload\n", encoding="utf-8") - process = MagicMock() - process.pid = 123 - process.stdin = io.BytesIO() + # Only OpenVMM's access policy can make the share refuse writes + # outside nvx-writable, so a plain directory fails the checks. + result = self._run('check_share "$share" policy') + self.assertEqual(result.returncode, 1, result.stdout) + self.assertNotIn("NVX-UBUNTU-SANDBOX-POLICY-OK", result.stdout) - def require(path: Path, _description: str) -> Path: - return path + # A refusal other than EROFS, such as a permission check, fails too. + geteuid = getattr(os, "geteuid", None) + if geteuid is not None and geteuid() == 0: + return + (self.share / "nvx-guest-marker").unlink(missing_ok=True) + self.share.chmod(0o555) + self.addCleanup(self.share.chmod, 0o755) + result = self._run('check_share "$share" policy') + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("unexpected share error", result.stderr) + self.assertIn("Permission denied", result.stderr) - with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object( - sandbox_lifecycle.subprocess, "Popen", return_value=process - ) as popen, - patch.object( - sandbox_lifecycle, "_process_start_time", return_value=456 - ), - patch.object( - sandbox_lifecycle.ControlSession, - "connect", - return_value=MagicMock(), - ), - ): - sandbox_lifecycle.start(state, 10) + def test_rw_share_link_cannot_write_into_ro_share(self): + geteuid = getattr(os, "geteuid", None) + if geteuid is not None and geteuid() == 0: + self.skipTest("root ignores file write permissions") + toolcache = self.root / "toolcache" + toolcache.mkdir() + marker = toolcache / "nvx-host-marker" + marker.write_text("host-to-guest\n", encoding="utf-8") + marker.chmod(0o444) + self.addCleanup(marker.chmod, 0o644) - command = popen.call_args.args[0] - self.assertEqual( - [ - (value, command[index + 1]) - for index, value in enumerate(command) - if value.startswith("--mount") - ], - [ - ("--mount", f"/workspace,{work},rw"), - ("--mount-deny", "logs"), - ("--mount-allow", "logs/payloads"), - ("--mount-write", "out"), - ], - ) + call = f'check_shares_isolated "$share" "{toolcache.as_posix()}"' + result = self._run(call) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn( + f"NVX-UBUNTU-SANDBOX-SHARES-ISOLATED-OK rw={self.share.as_posix()} " + f"ro={toolcache.as_posix()}", + result.stdout, + ) + self.assertFalse(os.path.lexists(self.share / "nvx-cross-share-link")) + self.assertEqual(marker.read_text(encoding="utf-8"), "host-to-guest\n") - # Format 5 holds an access policy, and only format 5 does. - config["mounts"][0]["allowed_paths"] = [] - config["mounts"][0]["writable_paths"] = [] - with ( - patch.object(sandbox, "require_file", side_effect=require), - self.assertRaisesRegex(common.ScriptError, "does not match"), - ): - sandbox_lifecycle._deserialize_launch(config) + # A write that reaches the read-only share fails the check. + marker.chmod(0o644) + result = self._run(call) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertNotIn("NVX-UBUNTU-SANDBOX-SHARES-ISOLATED-OK", result.stdout) - def test_launch_contract_attaches_a_file_as_an_aggregate_child(self): - distro = sandbox.SandboxLayer.parse( - "distro,distro.erofs,11111111-1111-1111-1111-111111111111" - ) - settings = sandbox.SandboxMount.parse( - "/etc/app/settings.json,settings.json,ro", kind="file" - ) - launch = sandbox.SandboxLaunch( - layers=(distro,), scratch=Path("scratch.ext4"), mounts=(settings,) + def test_caller_share_owns_and_populates_its_own_directories(self): + if sys.platform != "linux": + self.skipTest("ownership checks use GNU stat and Linux identities") + result = self._run('check_share "$share" caller') + + self.assertEqual(result.returncode, 0, result.stderr) + share = self.share.as_posix() + self.assertIn( + f"NVX-UBUNTU-SANDBOX-OWNER-OK target={share} " + f"identity={os.getuid()}:{os.getgid()}", + result.stdout, ) - name = sandbox.aggregate_child_name(0, "/etc/app/settings.json") - # Even alone, a file is the child of an aggregate, because the guest - # mounts the device's root, which is a directory. - self.assertTrue(sandbox.mounts_use_aggregate(launch.mounts)) - arguments = launch.openvmm_arguments() - self.assertNotIn("--mount", arguments) - self.assertEqual( - arguments[arguments.index("--mount-aggregate") :], - [ - "--mount-aggregate", - "/run/nvx/shares", - "--mount-child", - f"{name},{os.fspath(Path('settings.json'))},ro,file", - ], + self.assertIn(f"NVX-UBUNTU-SANDBOX-SYMLINK-OK target={share}", result.stdout) + self.assertIn( + f"NVX-UBUNTU-SANDBOX-SHARE-OK target={share} mode=caller", result.stdout ) + links = self.share / "nvx-caller-links" self.assertEqual( - sandbox.mounts_command_line_fragment(launch.mounts), - " virtfs_dir=/run/nvx/shares virtfs_tag=microvm virtfs_mode=ro" - " virtfs_aggregate=1", - ) - self.assertTrue( - launch.kernel_command_line().endswith( - f" nvx_share={name},/etc/app/settings.json,ro,file" - ) + {name: os.readlink(links / name) for name in os.listdir(links)}, + { + "nvx-tool": "../nvx-tool", + "absolute": "/nvx/absolute/target", + "outside": "../../nvx-outside-marker", + "denied": "../nvx-denied/secret", + "denied-directory": "../nvx-denied", + }, ) + self.assertEqual(os.listdir(self.links), []) + renamed = self.share / "nvx-guest-directory" / "nested" / "renamed" + self.assertEqual(renamed.read_text(encoding="utf-8"), "nested\n") - # Files and directories share one aggregate, and only a file's token - # is marked. - workspace = sandbox.SandboxMount.parse("/workspace,work,rw", ("secrets",)) - output = sandbox.SandboxMount.parse( - "/results/output.txt,output.txt,rw", kind="file" - ) - mixed = sandbox.SandboxLaunch( - layers=(distro,), - scratch=Path("scratch.ext4"), - mounts=(workspace, settings, output), + def test_eperm_share_requires_reads_to_fail_with_eperm_and_writes_to_fail(self): + result = self._run('check_share "$share" eperm') + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertNotIn("NVX-UBUNTU-SANDBOX-SHARE-OK", result.stdout) + + geteuid = getattr(os, "geteuid", None) + if geteuid is not None and geteuid() == 0: + self.skipTest("root ignores directory write permissions") + # Writes need only fail, so a read-only host directory stands in for + # the host's refusal. + self.share.chmod(0o555) + self.addCleanup(self.share.chmod, 0o755) + denied = "".join( + f'{command}() {{ echo "{command}: $1: Operation not permitted" >&2; ' + "return 1; }\n" + for command in ("cat", "ls") ) - names = [ - sandbox.aggregate_child_name(index, mount.guest_target) - for index, mount in enumerate(mixed.mounts) - ] - self.assertTrue( - mixed.kernel_command_line().endswith( - f" nvx_share={names[0]},/workspace,rw" - f" nvx_share={names[1]},/etc/app/settings.json,ro,file" - f" nvx_share={names[2]},/results/output.txt,rw,file" - ) + result = self._run(denied + 'check_share "$share" eperm') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn( + f"NVX-UBUNTU-SANDBOX-SHARE-OK target={self.share.as_posix()} mode=eperm", + result.stdout, ) - arguments = mixed.openvmm_arguments() - self.assertEqual( - arguments[arguments.index("--mount-deny") + 1], - os.fspath(Path.cwd() / "work" / "secrets"), - ) - - # A file's mode applies to the whole file, so no policy path names - # one, and the runtime owns the machine ID's path. - for option in ("denied_paths", "allowed_paths", "writable_paths"): - policy: dict[str, Any] = {option: ("x",)} - with self.subTest(option=option): - with self.assertRaisesRegex(common.ScriptError, "shares a file"): - sandbox.SandboxMount( - guest_target="/results/output.txt", - host_path=Path("output.txt"), - access="rw", - kind="file", - **policy, - ) - with self.assertRaisesRegex( - common.ScriptError, "unsupported sandbox mount kind" - ): - sandbox.SandboxMount.parse("/a,a", kind="socket") - # The runtime replaces /etc/machine-id with a file, so nothing at or - # below it may be shared. - for target in ("/etc/machine-id", "/etc/machine-id/child"): - with self.subTest(target=target): - with self.assertRaisesRegex(common.ScriptError, "reserved"): - sandbox.SandboxMount.parse(f"{target},id", kind="file") - # Only /etc itself is reserved, not what lies below it. - sandbox.SandboxMount.parse("/etc/app,app") + self.assertFalse((self.share / "nvx-guest-marker").exists()) - def test_launch_validation_requires_the_kind_of_each_share(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - config = root / "config" - config.mkdir() - settings = config / "settings.json" - settings.write_bytes(b"{}") - self.assertEqual(sandbox.mount_kind(settings), "file") - self.assertEqual(sandbox.mount_kind(config), "directory") - self.assertEqual(sandbox.mount_kind(root / "missing"), "directory") - share = sandbox.SandboxMount( - guest_target="/etc/app/settings.json", host_path=settings, kind="file" - ) - self.assertIs(share.validated(), share) - # A directory at the file's path is not shared in its place. - settings.unlink() - settings.mkdir() - with self.assertRaisesRegex(common.ScriptError, "not a regular file"): - share.validated() - settings.rmdir() - with self.assertRaisesRegex(common.ScriptError, "not a regular file"): - share.validated() - - # A file overlaps a shared directory that contains it, and another - # share of the same file, such as through a hard link, but not a - # file beside it. - settings.write_bytes(b"{}") - other = config / "other.json" - other.write_bytes(b"{}") - link = root / "link.json" - os.link(settings, link) - - def file_share(target: str, path: Path) -> sandbox.SandboxMount: - return sandbox.SandboxMount( - guest_target=target, host_path=path, kind="file" - ) + other_error = denied.replace("Operation not permitted", "Permission denied") + result = self._run(other_error + 'check_share "$share" eperm') + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertIn("unexpected share error", result.stderr) - sandbox.validate_mount_host_paths( - (file_share("/a.json", settings), file_share("/b.json", other)) - ) - for mounts, message in ( - ( - ( - sandbox.SandboxMount(guest_target="/config", host_path=config), - file_share("/a.json", settings), - ), - "overlap", - ), - ( - (file_share("/a.json", settings), file_share("/b.json", link)), - "same directory or file", - ), - ): - with self.subTest(message=message): - with self.assertRaisesRegex(common.ScriptError, message): - sandbox.validate_mount_host_paths(mounts) + self.share.chmod(0o755) + result = self._run(denied + 'check_share "$share" eperm') + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertNotIn("NVX-UBUNTU-SANDBOX-SHARE-OK", result.stdout) - def test_managed_configuration_format_records_file_shares(self): - def entry(target: str, path: str, kind: str) -> dict[str, object]: - return { - "guest_target": target, - "host_path": path, - "access": "ro", - "denied_paths": [], - "owner": "vmm", - "allowed_paths": [], - "writable_paths": [], - "kind": kind, - } - config: dict[str, object] = { - "format": sandbox_lifecycle.FILE_MOUNT_CONFIG_FORMAT, - "layers": [ - { - "role": "distro", - "path": "distro.erofs", - "uuid": "11111111-1111-1111-1111-111111111111", - } - ], - "scratch": "scratch.ext4", - "hostname": "nvx-sandbox", - "workload_uid": 65534, - "workload_gid": 65534, - "memory_max": None, - "pids_max": None, - "mounts": [entry("/etc/app/settings.json", "settings.json", "file")], - } +_SANDBOX_MOUNTINFO = ( + "20 1 0:20 / / rw,relatime - overlay overlay rw," + "lowerdir=/run/nvx/layers/distro,upperdir=/run/nvx/scratch/upper," + "workdir=/run/nvx/scratch/work,metacopy=on,xino=on\n" + "21 20 0:21 / /.nvx-agent ro,nosuid,nodev,relatime - tmpfs tmpfs ro,mode=755\n" + "22 20 0:22 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw\n" + "23 20 0:23 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro\n" + "24 20 0:24 / /dev rw,nosuid,relatime - tmpfs tmpfs rw,mode=755\n" + "25 24 0:25 / /dev/pts rw,relatime - devpts devpts rw,mode=620,ptmxmode=666\n" + "26 24 0:26 / /dev/shm rw,nosuid,nodev,relatime - tmpfs tmpfs rw\n" +) +_SANDBOX_STATUS = ( + "Name:\tsh\nUid:\t65534\t65534\t65534\t65534\n" + "Gid:\t65534\t65534\t65534\t65534\nGroups:\t\nNoNewPrivs:\t1\n" + + "".join( + f"{name}:\t0000000000000000\n" + for name in ("CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb") + ) +) - def require(path: Path, _description: str) -> Path: - return path - def unchanged(mount: sandbox.SandboxMount) -> sandbox.SandboxMount: - return mount +@unittest.skipIf(os.name == "nt", "requires a POSIX shell and process tools") +class SandboxSmokeProfileTests(unittest.TestCase): + SMOKE = Path(__file__).parents[1] / "guest" / "common" / "nvx-sandbox-smoke" - with ( - patch.object(sandbox, "require_file", side_effect=require), - patch.object(sandbox.SandboxMount, "validated", unchanged), - ): - launch = sandbox_lifecycle._deserialize_launch(config) - self.assertEqual([mount.kind for mount in launch.mounts], ["file"]) - serialized = sandbox_lifecycle._serialize_launch( - launch, - hypervisor="kvm", - memory_mib=256, - net=None, - network_profile=None, - network_egress=None, - network_ingress=None, - network_egress_allow=(), - network_egress_deny=(), - host_loopback=None, - network_proxy=None, - host_loopback_forward=(), - cmdline="", - ) - self.assertEqual( - serialized["format"], sandbox_lifecycle.FILE_MOUNT_CONFIG_FORMAT - ) - self.assertEqual(serialized["mounts"][0]["kind"], "file") - # Only format 6 holds a file share, and every format-6 share - # records its kind. - config["mounts"] = [entry("/workspace", "work", "directory")] - with self.assertRaisesRegex(common.ScriptError, "does not match"): - sandbox_lifecycle._deserialize_launch(config) - config["mounts"] = [{**entry("/a.json", "a.json", "file"), "kind": None}] - with self.assertRaisesRegex(common.ScriptError, "kind"): - sandbox_lifecycle._deserialize_launch(config) - unmarked = entry("/a.json", "a.json", "file") - del unmarked["kind"] - config["mounts"] = [unmarked] - with self.assertRaisesRegex(common.ScriptError, "malformed"): - sandbox_lifecycle._deserialize_launch(config) - config["format"] = sandbox_lifecycle.POLICY_CONFIG_FORMAT - config["mounts"] = [entry("/a.json", "a.json", "file")] - with self.assertRaisesRegex(common.ScriptError, "does not match"): - sandbox_lifecycle._deserialize_launch(config) - - def test_managed_configuration_format_binds_several_mounts(self): - def entry(target: str, path: str, owner: str = "vmm") -> dict[str, object]: - return { - "guest_target": target, - "host_path": path, - "access": "rw", - "denied_paths": [], - "owner": owner, - } + def setUp(self): + shell = _posix_shell() + if shell is None: + self.skipTest("POSIX shell is unavailable") + self.shell = shell + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.source = self.SMOKE.read_text(encoding="utf-8") - config: dict[str, object] = { - "format": sandbox_lifecycle.MULTI_MOUNT_CONFIG_FORMAT, - "layers": [ - { - "role": "distro", - "path": "distro.erofs", - "uuid": "11111111-1111-1111-1111-111111111111", - } - ], - "scratch": "scratch.ext4", - "hostname": "nvx-sandbox", - "workload_uid": 65534, - "workload_gid": 65534, - "memory_max": None, - "pids_max": None, - "mounts": [entry("/workspace", "work"), entry("/tools", "tools")], - } + def _fixture(self, name: str, content: str) -> str: + path = self.root / name + path.write_text(content, encoding="utf-8") + return path.as_posix() - def require(path: Path, _description: str) -> Path: - return path + def _run( + self, + names: tuple[str, ...], + call: str, + replacements: dict[str, str] | None = None, + ) -> subprocess.CompletedProcess[str]: + # The globals before fail() configure the checks. + script = self.source[: self.source.index("\nfail() {\n") + 1] + "".join( + _shell_function(self.source, name) for name in ("fail", *names) + ) + for old, new in (replacements or {}).items(): + script = script.replace(old, new) + return subprocess.run( + [self.shell, "-s"], + input=f"{script}{call}\n", + text=True, + capture_output=True, + check=False, + ) - def unchanged(mount: sandbox.SandboxMount) -> sandbox.SandboxMount: - return mount + def test_status_fields_join_their_values(self): + status = self._fixture("status", _SANDBOX_STATUS) + result = self._run( + ("status_field",), + f"status_field {status} Uid\nstatus_field {status} Groups\n" + f"status_field {status} Missing", + ) - def identity(path: Path) -> tuple[int, int]: - return 0, hash(path) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertEqual(result.stdout, "65534 65534 65534 65534\n\n") + self.assertIn(f"{status} has no Missing field", result.stderr) - with ( - patch.object(sandbox, "require_file", side_effect=require), - patch.object(sandbox.SandboxMount, "validated", unchanged), - patch.object(sandbox, "_path_identity", side_effect=identity), - ): - launch = sandbox_lifecycle._deserialize_launch(config) - self.assertEqual( - [mount.guest_target for mount in launch.mounts], - ["/workspace", "/tools"], - ) - # Format 4 always holds several shares, and only format 4 does. - config["mounts"] = [entry("/workspace", "work")] - with self.assertRaisesRegex(common.ScriptError, "does not match"): - sandbox_lifecycle._deserialize_launch(config) - config["mounts"] = {"guest_target": "/workspace"} - with self.assertRaisesRegex(common.ScriptError, "malformed"): - sandbox_lifecycle._deserialize_launch(config) - config["mounts"] = [entry("/workspace", "work"), entry("/a", "a", "caller")] - with self.assertRaisesRegex(common.ScriptError, "same --mount-owner"): - sandbox_lifecycle._deserialize_launch(config) - config["mounts"] = [entry("/workspace", "work"), entry("/workspace", "b")] - with self.assertRaisesRegex(common.ScriptError, "overlap"): - sandbox_lifecycle._deserialize_launch(config) - config["format"] = sandbox_lifecycle.MOUNT_CONFIG_FORMAT - config["mounts"] = [entry("/workspace", "work"), entry("/tools", "tools")] - with self.assertRaisesRegex(common.ScriptError, "does not match"): - sandbox_lifecycle._deserialize_launch(config) - - def test_managed_lifecycle_persists_and_replays_mount(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - layer_path = root / "distro.erofs" - scratch_path = root / "scratch.ext4" - share = root / "share" - layer_path.write_bytes(b"layer") - scratch_path.write_bytes(b"scratch") - share.mkdir() - state = root / "state" - previous = Path.cwd() - os.chdir(root) - try: - sandbox_lifecycle.provision( - state, - sandbox.SandboxLaunch( - layers=( - sandbox.SandboxLayer( - role="distro", - path=layer_path, - uuid="11111111-1111-1111-1111-111111111111", - ), - ), - scratch=scratch_path, - mounts=( - sandbox.SandboxMount.parse( - "/workspace,share,rw", ("secrets",) - ), - ), - ), - hypervisor="whp", - memory_mib=256, - net=None, - network_profile=None, - network_egress=None, - network_ingress=None, - network_egress_allow=(), - network_egress_deny=(), - host_loopback=None, - network_proxy=None, - host_loopback_forward=(), - cmdline="quiet", - timeout=10, + def test_identity_and_capability_checks_read_the_process_status(self): + identity = ( + 'id() { case "$1" in -u | -g) echo 65534 ;; esac; }\nHOME=/nonexistent\n' + ) + mutations = { + "valid": ("", ""), + "saved-uid": ( + "Uid:\t65534\t65534\t65534\t65534", + "Uid:\t65534\t65534\t0\t65534", + ), + "groups": ("Groups:\t", "Groups:\t27 "), + "bounding": ( + "CapBnd:\t0000000000000000", + "CapBnd:\t000001ffffffffff", + ), + "no-new-privs": ("NoNewPrivs:\t1", "NoNewPrivs:\t0"), + } + for mutation, (old, new) in mutations.items(): + with self.subTest(mutation=mutation): + status = self._fixture("status", _SANDBOX_STATUS.replace(old, new)) + result = self._run( + ("status_field", "check_identity", "check_capabilities"), + f"{identity}check_identity\ncheck_capabilities\necho checked", + {"/proc/self/status": status}, ) - finally: - os.chdir(previous) + if mutation == "valid": + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "checked\n") + else: + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("NVX-UBUNTU-SANDBOX-FAIL", result.stderr) - config = json.loads( - (state / sandbox_lifecycle.CONFIG_NAME).read_text(encoding="utf-8") - ) - self.assertEqual(config["format"], sandbox_lifecycle.MOUNT_CONFIG_FORMAT) - with self.assertRaisesRegex(common.ScriptError, "unsupported format"): - sandbox_lifecycle._read_json( - state / sandbox_lifecycle.CONFIG_NAME, - "sandbox configuration", - version=1, + def test_namespace_check_rejects_the_initial_namespaces(self): + def run( + links: dict[str, str], + *, + init_status: str = _SANDBOX_STATUS, + command_line: str = "console=hvc0 nvx_hostname=nvx-lifecycle", + hostname: str = "nvx-lifecycle", + ) -> subprocess.CompletedProcess[str]: + readlink = ( + 'readlink() { case "$1" in ' + + " ".join( + f"*/{name}) echo '{link}' ;;" for name, link in links.items() ) - self.assertEqual( - config["mount"], + + " esac; }\n" + ) + return self._run( + ("status_field", "check_namespaces"), + f"{readlink}check_namespaces\necho checked", { - "guest_target": "/workspace", - "host_path": os.fspath(share), - "access": "rw", - "denied_paths": ["secrets"], - "owner": "vmm", + "/proc/1/status": self._fixture("init-status", init_status), + "/proc/cmdline": self._fixture("cmdline", command_line + "\n"), + "/proc/sys/kernel/hostname": self._fixture( + "hostname", hostname + "\n" + ), }, ) - process = MagicMock() - process.pid = 123 - process.stdin = io.BytesIO() - context = MagicMock() - - def require(path: Path, _description: str) -> Path: - return path + private = { + "mnt": "mnt:[4026532200]", + "pid": "pid:[4026532201]", + "uts": "uts:[4026532202]", + } + result = run(private) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "checked\n") + result = run(private, command_line="console=hvc0", hostname="nvx-sandbox") + self.assertEqual(result.returncode, 0, result.stderr) - with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object( - sandbox_lifecycle.subprocess, "Popen", return_value=process - ) as popen, - patch.object( - sandbox_lifecycle, "_process_start_time", return_value=456 - ), - patch.object( - sandbox_lifecycle.ControlSession, "connect", return_value=context - ), - ): - sandbox_lifecycle.start(state, 10) + for name, initial in ( + ("mnt", "mnt:[4026531832]"), + ("pid", "pid:[4026531836]"), + ("uts", "uts:[4026531838]"), + ): + with self.subTest(namespace=name): + result = run({**private, name: initial}) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn(f"shares the initial {name} namespace", result.stderr) + for link in ("pid:[]", "pid:[42a]", "mnt:[42]", "pid:42"): + with self.subTest(link=link): + result = run({**private, "pid": link}) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("unexpected pid namespace link", result.stderr) + result = run( + private, + init_status=_SANDBOX_STATUS.replace( + "Uid:\t65534\t65534\t65534\t65534", "Uid:\t0\t0\t0\t0" + ), + ) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("PID 1 of the workload PID namespace runs as 0", result.stderr) + result = run(private, hostname="nvx-sandbox") + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("expected nvx-lifecycle", result.stderr) - command = popen.call_args.args[0] - self.assertEqual( - command[command.index("--mount") + 1], f"/workspace,{share},rw" - ) - self.assertEqual(command[command.index("--mount-deny") + 1], "secrets") + def test_mount_check_requires_private_runtime_mounts_once(self): + functions = ("mount_records", "require_mount", "check_mounts") + mountinfo = self._fixture("mountinfo", _SANDBOX_MOUNTINFO) + result = self._run( + functions, + "check_mounts\necho checked", + {"/proc/self/mountinfo": mountinfo}, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "checked\n") - def test_managed_lifecycle_accepts_configuration_without_mount(self): - config: dict[str, object] = { - "format": sandbox_lifecycle.CONFIG_FORMAT, - "layers": [ - { - "role": "distro", - "path": "distro.erofs", - "uuid": "11111111-1111-1111-1111-111111111111", - } - ], - "scratch": "scratch.ext4", - "hostname": "nvx-sandbox", - "workload_uid": 65534, - "workload_gid": 65534, - "memory_max": None, - "pids_max": None, + proc = "22 20 0:22 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw\n" + mutations = { + "stacked": ( + proc, + proc + proc.replace("22 20 0:22", "27 22 0:27"), + "several mounts are stacked at /proc", + ), + "propagation": ( + proc, + proc.replace("relatime - proc", "relatime shared:5 - proc"), + "the mount at /proc propagates: shared:5", + ), + "writable-sysfs": ( + "/sys ro,nosuid", + "/sys rw,nosuid", + "/sys is mounted rw,nosuid,nodev,noexec,relatime, without ro", + ), + "upper": ( + "upperdir=/run/nvx/scratch/upper", + "upperdir=/run/nvx/upper", + "upper layer is not on scratch", + ), + "lower": ( + "lowerdir=/run/nvx/layers/distro", + "lowerdir=/srv/distro", + "does not use the image layers", + ), + "missing-shm": ( + "26 24 0:26 / /dev/shm rw,nosuid,nodev,relatime - tmpfs tmpfs rw\n", + "", + "nothing is mounted at /dev/shm", + ), + "host-devices": ( + "- tmpfs tmpfs rw,mode=755\n", + "- devtmpfs devtmpfs rw,mode=755\n", + "/dev is devtmpfs, expected tmpfs", + ), } + for mutation, (old, new, message) in mutations.items(): + with self.subTest(mutation=mutation): + self.assertIn(old, _SANDBOX_MOUNTINFO) + mountinfo = self._fixture( + "mountinfo", _SANDBOX_MOUNTINFO.replace(old, new, 1) + ) + result = self._run( + functions, "check_mounts", {"/proc/self/mountinfo": mountinfo} + ) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn(message, result.stderr) - def require(path: Path, _description: str) -> Path: - return path - - with ( - patch.object(sandbox, "require_file", side_effect=require), + def test_cgroup_check_requires_the_container_cgroup(self): + for content, returncode in ( + ("0::/container\n", 0), + ("0::/agent\n", 1), + ("12:pids:/container\n", 1), ): - self.assertEqual(sandbox_lifecycle._deserialize_launch(config).mounts, ()) - config["format"] = sandbox_lifecycle.MOUNT_CONFIG_FORMAT - with self.assertRaisesRegex(common.ScriptError, "does not match"): - sandbox_lifecycle._deserialize_launch(config) - config["mount"] = {"guest_target": "/workspace"} - with self.assertRaisesRegex(common.ScriptError, "malformed"): - sandbox_lifecycle._deserialize_launch(config) - config["mount"] = { - "guest_target": "/proc", - "host_path": "share", - "access": "rw", - "denied_paths": [], - } - with self.assertRaisesRegex(common.ScriptError, "reserved"): - sandbox_lifecycle._deserialize_launch(config) - config["mount"] = { - "guest_target": "/workspace", - "host_path": "share", - "access": "rw", - "denied_paths": [], - } - config["format"] = sandbox_lifecycle.CONFIG_FORMAT - with self.assertRaisesRegex(common.ScriptError, "does not match"): - sandbox_lifecycle._deserialize_launch(config) - - def test_managed_configuration_format_binds_the_mount_owner(self): - mount: dict[str, object] = { - "guest_target": "/workspace", - "host_path": "share", - "access": "rw", - "denied_paths": [], - } - config: dict[str, object] = { - "format": sandbox_lifecycle.MOUNT_CONFIG_FORMAT, - "layers": [ - { - "role": "distro", - "path": "distro.erofs", - "uuid": "11111111-1111-1111-1111-111111111111", - } - ], - "scratch": "scratch.ext4", - "hostname": "nvx-sandbox", - "workload_uid": 65534, - "workload_gid": 65534, - "memory_max": None, - "pids_max": None, - "mount": mount, - } + with self.subTest(content=content): + cgroup = self._fixture("cgroup", content) + result = self._run( + ("check_cgroup",), "check_cgroup", {"/proc/self/cgroup": cgroup} + ) + self.assertEqual(result.returncode, returncode, result.stderr) - def require(path: Path, _description: str) -> Path: - return path + def test_limits_check_validates_its_arguments(self): + for arguments, message in ( + ("abc 8", "limits requires numeric MEMORY_MAX and PIDS_MAX"), + ("33554432 ''", "limits requires numeric MEMORY_MAX and PIDS_MAX"), + ("33554432 2", "requires PIDS_MAX of at least 3"), + ): + with self.subTest(arguments=arguments): + result = self._run( + ("start_sleepers", "check_limits"), f"check_limits {arguments}" + ) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn(message, result.stderr) - def unchanged(mount: sandbox.SandboxMount) -> sandbox.SandboxMount: - return mount + @unittest.skipUnless(sys.platform == "linux", "inspects the Linux process table") + def test_sleeper_subshell_records_and_reaps_its_sleepers(self): + record = self.root / "sleepers" + result = self._run( + ("start_sleepers",), + f'(start_sleepers "{record.as_posix()}" 3)\necho done', + # A distinct duration identifies this test's sleepers. + {"sleep 600": "sleep 611.25"}, + ) - with ( - patch.object(sandbox, "require_file", side_effect=require), - patch.object(sandbox.SandboxMount, "validated", unchanged), - ): - # A share configured before ownership modes runs as the VMM. - launch = sandbox_lifecycle._deserialize_launch(config) - self.assertEqual(launch.mounts[0].owner, "vmm") - mount["owner"] = "vmm" - launch = sandbox_lifecycle._deserialize_launch(config) - self.assertEqual(launch.mounts[0].owner, "vmm") - # Only format 3, which older releases reject, carries caller ownership. - mount["owner"] = "caller" - with self.assertRaisesRegex(common.ScriptError, "does not match"): - sandbox_lifecycle._deserialize_launch(config) - config["format"] = sandbox_lifecycle.OWNER_CONFIG_FORMAT - launch = sandbox_lifecycle._deserialize_launch(config) - self.assertEqual(launch.mounts[0].owner, "caller") - mount["owner"] = "vmm" - with self.assertRaisesRegex(common.ScriptError, "does not match"): - sandbox_lifecycle._deserialize_launch(config) - mount["owner"] = "root" - with self.assertRaisesRegex( - common.ScriptError, "unsupported sandbox mount owner" - ): - sandbox_lifecycle._deserialize_launch(config) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(record.read_text(encoding="utf-8"), "3\n") + for process in Path("/proc").iterdir(): + if process.name.isdigit(): + try: + command = (process / "cmdline").read_bytes() + except OSError: + continue + self.assertNotEqual(command, b"sleep\x00611.25\x00") - def test_managed_lifecycle_persists_and_replays_caller_owner(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - layer_path = root / "distro.erofs" - scratch_path = root / "scratch.ext4" - share = root / "share" - layer_path.write_bytes(b"layer") - scratch_path.write_bytes(b"scratch") - share.mkdir() - state = root / "state" - launch = sandbox.SandboxLaunch( - layers=( - sandbox.SandboxLayer( - role="distro", - path=layer_path, - uuid="11111111-1111-1111-1111-111111111111", - ), - ), - scratch=scratch_path, - mounts=( - sandbox.SandboxMount.parse(f"/workspace,{share},rw", (), "caller"), - ), + def test_main_body_runs_the_profile_then_limits_and_shares(self): + body = self.source[self.source.index("\ncheck_identity\n") + 1 :] + stubs = "".join( + f'{name}() {{ echo "{name} $*"; }}\n' + for name in ( + "check_identity", + "check_capabilities", + "check_namespaces", + "check_mounts", + "check_devices", + "check_cgroup", + "check_image", + "check_limits", + "check_share", + "check_shares_isolated", ) + ) + script = ( + self.source[: self.source.index("\nfail() {\n") + 1] + + _shell_function(self.source, "fail") + + stubs + + body + ) - def provision() -> None: - sandbox_lifecycle.provision( - state, - launch, - hypervisor="whp" if sys.platform == "win32" else "kvm", - memory_mib=256, - net=None, - network_profile=None, - network_egress=None, - network_ingress=None, - network_egress_allow=(), - network_egress_deny=(), - host_loopback=None, - network_proxy=None, - host_loopback_forward=(), - cmdline="quiet", - timeout=10, - ) - - if sys.platform == "win32": - with self.assertRaisesRegex(common.ScriptError, "Linux host"): - provision() - self.assertFalse((state / sandbox_lifecycle.CONFIG_NAME).exists()) - return - if share.stat().st_uid == 0: - self.skipTest("caller ownership refuses a root-owned share") - provision() - - config = json.loads( - (state / sandbox_lifecycle.CONFIG_NAME).read_text(encoding="utf-8") - ) - self.assertEqual(config["format"], sandbox_lifecycle.OWNER_CONFIG_FORMAT) - self.assertEqual(config["mount"]["owner"], "caller") - # NVX releases that know only formats 1 and 2 refuse to start it. - with self.assertRaisesRegex(common.ScriptError, "unsupported format"): - sandbox_lifecycle._read_json( - state / sandbox_lifecycle.CONFIG_NAME, - "sandbox configuration", - version=( - sandbox_lifecycle.CONFIG_FORMAT, - sandbox_lifecycle.MOUNT_CONFIG_FORMAT, - ), - ) + def run(*arguments: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [self.shell, "-s", "--", *arguments], + input=script, + text=True, + capture_output=True, + check=False, + ) - process = MagicMock() - process.pid = 123 - process.stdin = io.BytesIO() + result = run("limits", "33554432", "8", "/workspace", "rw", "/tools") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + result.stdout.splitlines(), + [ + "check_identity ", + "check_capabilities ", + "check_namespaces ", + "check_mounts ", + "check_devices ", + "check_cgroup ", + "check_image ", + "NVX-UBUNTU-SANDBOX-PROFILE-OK uid=65534 gid=65534", + "check_limits 33554432 8", + "check_share /workspace rw", + "check_share /tools ro", + "check_shares_isolated /workspace /tools", + "NVX-UBUNTU-SANDBOX-OK uid=65534 gid=65534", + ], + ) + result = run("limits", "33554432") + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("limits requires MEMORY_MAX and PIDS_MAX", result.stderr) + self.assertNotIn("NVX-UBUNTU-SANDBOX-OK", result.stdout) - def require(path: Path, _description: str) -> Path: - return path - with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object( - sandbox_lifecycle.subprocess, "Popen", return_value=process - ) as popen, - patch.object( - sandbox_lifecycle, "_process_start_time", return_value=456 - ), - patch.object( - sandbox_lifecycle.ControlSession, - "connect", - return_value=MagicMock(), - ), - ): - sandbox_lifecycle.start(state, 10) +def _elf_file( + segments: tuple[tuple[int, bytes], ...] = ((1, b"code"),), + *, + elf_type: int = 2, + elf_class: int = 2, +) -> bytes: + """Returns a minimal ELF file with one program header for each segment.""" + header_size, entry_size = 64, 56 + payload_offset = header_size + entry_size * len(segments) + program_headers = b"" + payloads = b"" + for segment_type, payload in segments: + program_headers += struct.pack( + " bytes: + return b"".join(struct.pack(" Path: - return path + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) - with ( - patch.object( - sandbox_lifecycle, - "require_file", - side_effect=require, - ), - self.assertRaisesRegex( - common.ScriptError, "sandbox configuration is malformed" - ), - ): - sandbox_lifecycle.start(state, 10) - - with self.assertRaisesRegex(common.ScriptError, "already provisioned"): - sandbox_lifecycle.provision( - state, - launch, - hypervisor="whp", - memory_mib=256, - net=None, - network_profile=None, - network_egress=None, - network_ingress=None, - network_egress_allow=(), - network_egress_deny=(), - host_loopback=None, - network_proxy=None, - host_loopback_forward=(), - cmdline="quiet", - timeout=10, - ) + def _write(self, name: str, data: bytes) -> Path: + path = self.root / name + path.write_bytes(data) + return path - sandbox_lifecycle.deprovision(state, 10) - self.assertFalse(state.exists()) + def test_static_and_static_pie_executables_pass(self): + executable = elf.read_elf_executable(self._write("exit", _elf_file())) + self.assertTrue(executable.static) + self.assertEqual( + ( + executable.elf_type, + executable.interpreter, + executable.needed, + executable.dynamic, + ), + ("EXEC", None, 0, False), + ) + self.assertEqual( + executable.describe(), + f"{executable.path}: ELF EXEC, {executable.size} bytes, static", + ) + # A static PIE relocates itself through its dynamic section; entries + # after DT_NULL do not count. + dynamic = _dynamic_entries((7, 0x1000), (0, 0), (1, 1)) + pie = elf.read_elf_executable( + self._write("pie", _elf_file(((1, b"code"), (2, dynamic)), elf_type=3)) + ) + self.assertTrue(pie.static) + self.assertEqual((pie.elf_type, pie.dynamic), ("DYN", True)) + self.assertTrue(pie.describe().endswith(" bytes, static-pie")) - def test_managed_lifecycle_start_requests_openvmm_outcome(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - layer_path = root / "distro.erofs" - scratch_path = root / "scratch.ext4" - layer_path.write_bytes(b"layer") - scratch_path.write_bytes(b"scratch") - state = root / "state" - sandbox_lifecycle.provision( - state, - sandbox.SandboxLaunch( - layers=( - sandbox.SandboxLayer( - role="distro", - path=layer_path, - uuid="11111111-1111-1111-1111-111111111111", - ), - ), - scratch=scratch_path, - ), - hypervisor="whp", - memory_mib=256, - net=None, - network_profile=None, - network_egress=None, - network_ingress=None, - network_egress_allow=(), - network_egress_deny=(), - host_loopback=None, - network_proxy=None, - host_loopback_forward=(), - cmdline="quiet", - timeout=10, + def test_an_interpreter_or_a_needed_library_fails(self): + dynamic = elf.read_elf_executable( + self._write( + "dynamic", + _elf_file(((1, b"code"), self.INTERPRETER, self.NEEDED), elf_type=3), ) - process = MagicMock() - process.pid = 123 - process.stdin = io.BytesIO() - session = MagicMock() - context = MagicMock() - context.__enter__.return_value = session + ) + self.assertFalse(dynamic.static) + self.assertEqual( + (dynamic.interpreter, dynamic.needed), ("/lib/ld-musl-x86_64.so.1", 1) + ) + self.assertIn( + "dynamic: interpreter /lib/ld-musl-x86_64.so.1, 1 needed libraries", + dynamic.describe(), + ) + needed = elf.read_elf_executable( + self._write("needed", _elf_file(((1, b"code"), self.NEEDED), elf_type=3)) + ) + self.assertFalse(needed.static) - def require(path: Path, _description: str) -> Path: - return path + def test_rejects_files_that_are_not_64_bit_executables(self): + truncated_segment = _elf_file(((1, b"code"), self.INTERPRETER))[:-4] + for name, data in ( + ("script", b"#!/bin/sh\nexit 0\n"), + ("elf32", _elf_file(elf_class=1)), + ("object", _elf_file(elf_type=1)), + ("header", _elf_file()[:40]), + ("segment", truncated_segment), + ): + with self.subTest(name=name), self.assertRaises(ValueError): + elf.read_elf_executable(self._write(name, data)) - with ( - patch.object( - sandbox_lifecycle, - "require_file", - side_effect=require, - ), - patch.object( - sandbox_lifecycle.subprocess, - "Popen", - return_value=process, - ) as popen, - patch.object( - sandbox_lifecycle, - "_process_start_time", - return_value=456, - ) as process_start_time, - patch.object( - sandbox_lifecycle.ControlSession, - "connect", - return_value=context, - ), - ): - sandbox_lifecycle.start(state, 10) + def test_check_lists_every_executable_and_fails_on_dynamic_ones(self): + static = self._write("static", _elf_file()) + dynamic = self._write("dynamic", _elf_file(((1, b"code"), self.INTERPRETER))) + output = io.StringIO() + with ( + contextlib.redirect_stdout(output), + self.assertRaisesRegex( + common.ScriptError, "not static executables: .*dynamic" + ), + ): + elf.check_static_executables([static, dynamic]) + self.assertIn(f"{static}: ELF EXEC", output.getvalue()) + self.assertIn(f"{dynamic}: ELF EXEC", output.getvalue()) - command = popen.call_args.args[0] + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(nvx.main(["check-static-elf", str(static)]), 0) + self.assertEqual( + output.getvalue(), + f"{static}: ELF EXEC, {static.stat().st_size} bytes, static\n", + ) + with ( + contextlib.redirect_stdout(io.StringIO()), + contextlib.redirect_stderr(io.StringIO()) as error, + ): self.assertEqual( - Path(command[command.index("--microvm-report") + 1]), - state / sandbox_lifecycle.OUTCOME_NAME, - ) - session.ping.assert_called_once_with(10) - process_start_time.assert_called_once_with(123) - runtime = json.loads( - (state / sandbox_lifecycle.RUNTIME_NAME).read_text(encoding="utf-8") + nvx.main(["check-static-elf", str(static), str(dynamic)]), 1 ) - self.assertEqual((runtime["pid"], runtime["start_time"]), (123, 456)) + self.assertIn("not static executables", error.getvalue()) - def test_versioned_json_reader_supports_custom_version_field(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "outcome.json" - payload = {"schema_version": sandbox_lifecycle.OUTCOME_SCHEMA_VERSION} - path.write_text(json.dumps(payload), encoding="utf-8") - self.assertEqual( - sandbox_lifecycle._read_json( - path, - "OpenVMM outcome report", - version_field="schema_version", - version=sandbox_lifecycle.OUTCOME_SCHEMA_VERSION, - ), - payload, - ) - with self.assertRaisesRegex(common.ScriptError, "unsupported format"): - sandbox_lifecycle._read_json( - path, - "OpenVMM outcome report", - version_field="schema_version", - version=sandbox_lifecycle.OUTCOME_SCHEMA_VERSION + 1, - ) +class AciSandboxRunnerTests(unittest.TestCase): + def _artifacts(self, root: Path) -> dict[str, Path]: + names = { + KernelBuildConstants.BINARY_NAME: b"kernel", + AlpineBuildConstants.INITRAMFS_NAME: b"initrd", + "openvmm": b"vmm", + } + paths: dict[str, Path] = {} + for name, contents in names.items(): + paths[name] = root / name + paths[name].write_bytes(contents) + return paths - def test_versioned_json_reader_rejects_invalid_utf8(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "config.json" - path.write_bytes(b"\xff") + def _environment(self, root: Path, paths: dict[str, Path]) -> dict[str, str]: + def artifact(name: str) -> Path: + return root / name - with self.assertRaisesRegex(common.ScriptError, "failed to read"): - sandbox_lifecycle._read_json(path, "sandbox configuration") + def openvmm() -> Path: + return paths["openvmm"] - def test_managed_exec_outcome_excludes_workload_data(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "outcome.json" - sandbox_lifecycle.write_exec_outcome( - path, - sandbox_lifecycle.ManagedExecResult( - 124, - "timeout", - b"sensitive stdout", - b"sensitive stderr", - ), + with ( + patch.object(aci_edge_sandboxes_tests, "artifact_path", artifact), + patch.object(aci_edge_sandboxes_tests, "openvmm_binary_path", openvmm), + ): + return aci_edge_sandboxes_tests.e2e_environment( + "kvm", root / "state", root / "out" ) - outcome = json.loads(path.read_text(encoding="utf-8")) - self.assertEqual(outcome["schema_version"], 1) - self.assertEqual( - outcome["outcome"], - { - "operation": "exec", - "category": "timeout", - "status_code": 124, - }, - ) - self.assertEqual(len(outcome["operation_id"]), 32) - encoded = json.dumps(outcome) - self.assertNotIn("stdout", encoded) - self.assertNotIn("stderr", encoded) - self.assertNotIn("sensitive", encoded) + def test_cli_registers_the_lifecycle_test(self): + args = nvx.parse_args(["test-aci-edge-sandboxes", "--backend", "mshv"]) - def test_managed_exec_outcome_does_not_overwrite(self): - with tempfile.TemporaryDirectory() as temporary: - path = Path(temporary) / "outcome.json" - path.write_text("existing", encoding="utf-8") - with self.assertRaisesRegex(common.ScriptError, "already exists"): - sandbox_lifecycle.write_exec_outcome( - path, - sandbox_lifecycle.ManagedExecResult(0, "exit", b"", b""), - ) + self.assertEqual(args.backend, "mshv") + self.assertEqual(args.cargo, "cargo") + self.assertFalse(hasattr(args, "scratch_template")) + self.assertIs( + args.handler, aci_edge_sandboxes_tests.command_test_aci_edge_sandboxes + ) - def test_managed_exec_outcome_rejects_existing_symlink(self): + def test_environment_resolves_repository_artifacts(self): with tempfile.TemporaryDirectory() as temporary: - directory = Path(temporary) - target = directory / "missing-target" - path = directory / "outcome.json" - try: - path.symlink_to(target) - except OSError as error: - self.skipTest(f"symlinks are unavailable: {error}") - with self.assertRaisesRegex(common.ScriptError, "already exists"): - sandbox_lifecycle.write_exec_outcome( - path, - sandbox_lifecycle.ManagedExecResult(0, "exit", b"", b""), - ) + root = Path(temporary) + paths = self._artifacts(root) + environment = self._environment(root, paths) - def test_managed_stop_cleans_runtime_state_if_report_is_invalid(self): - with tempfile.TemporaryDirectory() as temporary: - state = Path(temporary) - sandbox_lifecycle._write_json( - state / sandbox_lifecycle.RUNTIME_NAME, - { - "format": sandbox_lifecycle.STATE_FORMAT, - "pid": 123, - "control_endpoint": "control.sock", - }, - ) - (state / sandbox_lifecycle.CAPABILITY_NAME).write_bytes(b"x" * 32) - (state / sandbox_lifecycle.CONTROL_SOCKET_NAME).touch() - (state / sandbox_lifecycle.OUTCOME_NAME).write_text( - "invalid", - encoding="utf-8", + self.assertEqual(environment["ACI_EDGE_SANDBOXES_E2E_HYPERVISOR"], "kvm") + self.assertEqual( + Path(environment["ACI_EDGE_SANDBOXES_E2E_OPENVMM"]), + paths["openvmm"].resolve(), ) - context = MagicMock() - with ( - patch.object( - sandbox_lifecycle, - "_process_running", - side_effect=(True, False), - ), - patch.object( - sandbox_lifecycle.ControlSession, - "connect", - return_value=context, - ), - self.assertRaisesRegex(common.ScriptError, "outcome report"), - ): - sandbox_lifecycle.stop(state, 10) - - self.assertFalse((state / sandbox_lifecycle.RUNTIME_NAME).exists()) - self.assertFalse((state / sandbox_lifecycle.CAPABILITY_NAME).exists()) - self.assertFalse((state / sandbox_lifecycle.CONTROL_SOCKET_NAME).exists()) - self.assertTrue((state / sandbox_lifecycle.OUTCOME_NAME).exists()) - - def test_process_identity_parses_linux_process_status(self): - def status(state: bytes, start_time: bytes = b"98765") -> bytes: - fields = b" ".join([state, *(b"0" for _ in range(18)), start_time, b"0"]) - # The command name contains a space and parentheses. - return b"42 (sh) x (y) " + fields + b"\n" - - self.assertEqual(sandbox_lifecycle._linux_start_time(status(b"S")), 98765) - self.assertEqual(sandbox_lifecycle._linux_start_time(status(b"D")), 98765) - for state in (b"Z", b"X"): - self.assertIsNone(sandbox_lifecycle._linux_start_time(status(state))) - for malformed in ( - b"42 (sh", - b"42 (sh) S 1 2\n", - status(b"S", b"-1"), - status(b"S", b"tick"), - ): - with self.assertRaisesRegex(ValueError, "unexpected format"): - sandbox_lifecycle._linux_start_time(malformed) - - def test_process_identity_requires_the_recorded_start_time(self): - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - pid = os.getpid() - start_time = sandbox_lifecycle._process_start_time(pid) - assert isinstance(start_time, int) - - self.assertTrue(sandbox_lifecycle._process_running(pid, start_time)) - self.assertFalse(sandbox_lifecycle._process_running(pid, start_time + 1)) - # Records from earlier NVX versions identify OpenVMM by process ID alone. - self.assertTrue(sandbox_lifecycle._process_running(pid, None)) - - def test_runtime_records_report_whether_their_process_runs(self): - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - pid = os.getpid() - start_time = sandbox_lifecycle._process_start_time(pid) - assert isinstance(start_time, int) - - self.assertTrue( - sandbox_lifecycle.runtime_process_running( - {"pid": pid, "start_time": start_time} + self.assertEqual( + Path(environment["ACI_EDGE_SANDBOXES_E2E_INITRD"]), + paths[AlpineBuildConstants.INITRAMFS_NAME].resolve(), ) - ) - self.assertFalse( - sandbox_lifecycle.runtime_process_running( - {"pid": pid, "start_time": start_time + 1} + self.assertEqual( + Path(environment["ACI_EDGE_SANDBOXES_E2E_STATE_ROOT"]), + (root / "state").resolve(), ) - ) - with self.assertRaisesRegex(common.ScriptError, "invalid process ID"): - sandbox_lifecycle.runtime_process_running({"pid": "openvmm"}) - with self.assertRaisesRegex(common.ScriptError, "invalid process start"): - sandbox_lifecycle.runtime_process_running({"pid": pid, "start_time": -1}) - - def test_process_identity_counts_an_exited_process_as_stopped(self): - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - child = _stdin_bound_child() - try: - start_time = sandbox_lifecycle._process_start_time(child.pid) - assert isinstance(start_time, int) - self.assertTrue(sandbox_lifecycle._process_running(child.pid, start_time)) - finally: - _finish_child(child) - - # On Windows, the handle that Popen holds keeps the exited process's ID. - self.assertFalse(sandbox_lifecycle._process_running(child.pid, start_time)) - self.assertFalse(sandbox_lifecycle._process_running(child.pid, None)) - - def test_process_identity_counts_a_zombie_as_stopped(self): - if sys.platform != "linux": - self.skipTest("zombie processes require a Linux host") - child = _stdin_bound_child() - try: - start_time = sandbox_lifecycle._process_start_time(child.pid) - assert isinstance(start_time, int) - _exit_unreaped(child) - stat = Path(f"/proc/{child.pid}/stat").read_bytes() - self.assertEqual(stat[stat.rfind(b")") + 2 :][:1], b"Z") - - self.assertIsNone(sandbox_lifecycle._process_start_time(child.pid)) - self.assertFalse(sandbox_lifecycle._process_running(child.pid, start_time)) - self.assertFalse(sandbox_lifecycle._process_running(child.pid, None)) - finally: - _finish_child(child) - - def test_managed_stop_completes_while_openvmm_remains_unreaped(self): - if sys.platform != "linux": - self.skipTest("zombie processes require a Linux host") - child = _stdin_bound_child() - try: - with tempfile.TemporaryDirectory() as temporary: - state = Path(temporary) - _write_managed_runtime( - state, - child.pid, - sandbox_lifecycle._process_start_time(child.pid), - ) - outcome = { - "schema_version": sandbox_lifecycle.OUTCOME_SCHEMA_VERSION, - "outcome": {"category": "stopped"}, - "network_policy": {}, - "teardown": {}, - } - (state / sandbox_lifecycle.OUTCOME_NAME).write_text( - json.dumps(outcome), encoding="utf-8" + self.assertFalse( + any( + name.startswith("ACI_EDGE_SANDBOXES_E2E_DISTRO") + for name in environment ) - session = MagicMock() - context = MagicMock() - context.__enter__.return_value = session - - def acknowledge_stop(_timeout: float) -> None: - # OpenVMM exits after the guest acknowledges STOP, but its - # parent, such as a container init without --init, never - # reaps it. - _exit_unreaped(child) - - session.stop.side_effect = acknowledge_stop - with patch.object( - sandbox_lifecycle.ControlSession, - "connect", - return_value=context, - ): - self.assertEqual(sandbox_lifecycle.stop(state, 10), outcome) - - session.stop.assert_called_once_with(10) - self.assertFalse((state / sandbox_lifecycle.RUNTIME_NAME).exists()) - self.assertFalse((state / sandbox_lifecycle.CAPABILITY_NAME).exists()) - finally: - _finish_child(child) + ) + self.assertNotIn("ACI_EDGE_SANDBOXES_E2E_SCRATCH", environment) - def test_managed_lifecycle_treats_a_reused_process_id_as_stopped(self): - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") + def test_environment_requires_the_alpine_initramfs(self): with tempfile.TemporaryDirectory() as temporary: - state = Path(temporary) / "state" - state.mkdir() - start_time = sandbox_lifecycle._process_start_time(os.getpid()) - assert isinstance(start_time, int) - # OpenVMM is gone, and this test process now has its process ID. - _write_managed_runtime(state, os.getpid(), start_time + 1) - (state / sandbox_lifecycle.CONFIG_NAME).write_text("{}", encoding="utf-8") - - with patch.object(sandbox_lifecycle.ControlSession, "connect") as connect: - with self.assertRaisesRegex( - common.ScriptError, "runtime state is stale" - ): - sandbox_lifecycle.stop(state, 10) - with self.assertRaisesRegex( - common.ScriptError, "runtime state is stale" - ): - sandbox_lifecycle.exec_workload( - state, ("/bin/true",), timeout_ms=0, response_timeout=10 - ) - connect.assert_not_called() + root = Path(temporary) + paths = self._artifacts(root) + paths[AlpineBuildConstants.INITRAMFS_NAME].unlink() + with self.assertRaisesRegex(common.ScriptError, "Alpine initramfs"): + self._environment(root, paths) - sandbox_lifecycle.deprovision(state, 10) - self.assertFalse(state.exists()) + def test_command_runs_only_the_ignored_lifecycle_test(self): + command = aci_edge_sandboxes_tests.e2e_command("cargo") - def test_managed_lifecycle_identifies_legacy_records_by_process_id(self): - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - with tempfile.TemporaryDirectory() as temporary: - state = Path(temporary) - _write_managed_runtime(state, os.getpid(), None) + self.assertEqual(command[:2], ["cargo", "test"]) + self.assertEqual( + Path(command[command.index("--manifest-path") + 1]), + BuildConstants.REPO_ROOT / "aci_edge_sandboxes" / "Cargo.toml", + ) + self.assertIn("--locked", command) + self.assertEqual(command[command.index("--test") + 1], "openvmm_e2e") + self.assertEqual(command[-2:], ["--ignored", "--nocapture"]) - with self.assertRaisesRegex(common.ScriptError, "must be stopped"): - sandbox_lifecycle.deprovision(state, 10) - self.assertTrue((state / sandbox_lifecycle.RUNTIME_NAME).exists()) + def test_handler_returns_the_test_status(self): + observed: list[str] = [] - def test_managed_lifecycle_rejects_invalid_process_start_times(self): - with tempfile.TemporaryDirectory() as temporary: - state = Path(temporary) - for start_time in ("1", True, -1, 1.5): - with self.subTest(start_time=start_time): - sandbox_lifecycle._write_json( - state / sandbox_lifecycle.RUNTIME_NAME, - { - "format": sandbox_lifecycle.STATE_FORMAT, - "pid": os.getpid(), - "start_time": start_time, - "control_endpoint": "control.sock", - }, - ) - for operation in ( - lambda: sandbox_lifecycle.stop(state, 10), - lambda: sandbox_lifecycle.deprovision(state, 10), - ): - with self.assertRaisesRegex( - common.ScriptError, "invalid process start time" - ): - operation() + def fake_run( + command: list[str], *, env: dict[str, str], check: bool + ) -> subprocess.CompletedProcess[bytes]: + self.assertFalse(check) + self.assertEqual(command, aci_edge_sandboxes_tests.e2e_command("cargo")) + observed.append(env["ACI_EDGE_SANDBOXES_E2E_STATE_ROOT"]) + return subprocess.CompletedProcess(command, 3) - def test_managed_start_fails_closed_without_openvmm_identity(self): - def require(path: Path, _description: str) -> Path: - return path + def fake_environment( + backend: str, state_root: Path, output_dir: Path + ) -> dict[str, str]: + self.assertEqual(backend, "whp") + return {"ACI_EDGE_SANDBOXES_E2E_STATE_ROOT": os.fspath(state_root)} - # A side effect that yields None reports that no live process has the ID. - for identity, exit_status, message in ( - ((None,), 1, "OpenVMM exited during startup"), - ((None,), None, "cannot identify the OpenVMM process$"), - (OSError("denied"), None, "cannot identify the OpenVMM process: denied"), - ): - with ( - self.subTest(message=message), - tempfile.TemporaryDirectory() as temporary, - ): - state = _provision_managed_sandbox(Path(temporary).resolve()) - process = MagicMock() - process.pid = 123 - process.stdin = io.BytesIO() - process.poll.return_value = exit_status - with ( - patch.object( - sandbox_lifecycle, "require_file", side_effect=require - ), - patch.object( - sandbox_lifecycle.subprocess, "Popen", return_value=process - ), - patch.object( - sandbox_lifecycle, "_process_start_time", side_effect=identity - ), - patch.object( - sandbox_lifecycle.ControlSession, "connect" - ) as connect, - self.assertRaisesRegex(common.ScriptError, message), - ): - sandbox_lifecycle.start(state, 10) - - connect.assert_not_called() - self.assertEqual(process.terminate.called, exit_status is None) - self.assertFalse((state / sandbox_lifecycle.RUNTIME_NAME).exists()) - self.assertFalse((state / sandbox_lifecycle.CAPABILITY_NAME).exists()) - self.assertTrue((state / sandbox_lifecycle.CONFIG_NAME).exists()) - - def test_managed_start_lets_exiting_openvmm_publish_its_outcome(self): - # A guest that exits during startup, such as one that refuses the workload - # identity, makes OpenVMM close the control endpoint before it publishes - # its outcome report. Terminating OpenVMM then left the report's staging - # file, which deprovision refuses to remove (issue #438). - if os.name != "nt" and sys.platform != "linux": - self.skipTest("managed sandboxes require a Linux or Windows host") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - state = _provision_managed_sandbox(Path(temporary.name).resolve()) - popen = subprocess.Popen - launched: list[subprocess.Popen[bytes]] = [] - - def launch(command: list[str], **options: Any) -> subprocess.Popen[bytes]: - attach_size = control_session.OUTER_HEADER.size + 32 - # With unknown options, the type checker cannot tell that start - # requests binary streams. - process = cast( - "subprocess.Popen[bytes]", - popen( - [ - sys.executable, - "-c", - _EXITING_OPENVMM, - str(attach_size), - *command[1:], - ], - **options, - ), + with tempfile.TemporaryDirectory() as temporary: + args = argparse.Namespace( + backend="whp", + output_dir=Path(temporary) / "results", + cargo="cargo", ) - self.addCleanup(_end_child, process) - launched.append(process) - return process - - def require(path: Path, _description: str) -> Path: - return path - - with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object(sandbox_lifecycle.subprocess, "Popen", side_effect=launch), - self.assertRaisesRegex( - common.ScriptError, - "^OpenVMM exited during startup: guest-exit status 125$", - ), - ): - sandbox_lifecycle.start(state, 30) - - log = (state / sandbox_lifecycle.LOG_NAME).read_text( - encoding="utf-8", errors="replace" - ) - # OpenVMM exited on its own, after it published the report. - self.assertEqual([process.returncode for process in launched], [125], log) - self.assertEqual( - sorted(path.name for path in state.iterdir()), - [ - sandbox_lifecycle.CONFIG_NAME, - sandbox_lifecycle.LOCK_NAME, - sandbox_lifecycle.LOG_NAME, - sandbox_lifecycle.OUTCOME_NAME, - ], - log, - ) - sandbox_lifecycle.deprovision(state, 10) - self.assertFalse(state.exists()) - - def test_managed_start_waits_only_for_openvmm_that_closed_its_endpoint(self): - def require(path: Path, _description: str) -> Path: - return path - - closed = control_session.ControlEndpointClosed() - # Each case holds the control failure, the status with which OpenVMM exits - # on its own or None if it runs until terminated, the waits for it, and the - # error that start raises. - for failure, status, waits, raised, message in ( - # OpenVMM closed the endpoint and exits within the grace period. - ( - closed, - 7, - [call(timeout=10)], - common.ScriptError, - "^OpenVMM exited during startup with status 7$", - ), - # It closed the endpoint but outlives the grace period. - ( - closed, - None, - [call(timeout=10), call(timeout=5)], - control_session.ControlEndpointClosed, - "^managed control endpoint closed$", - ), - # It still serves the endpoint, so it gets no grace period. - ( - TimeoutError("managed control response timed out"), - None, - [call(timeout=5)], - TimeoutError, - "^managed control response timed out$", - ), - # A session reset also comes from an OpenVMM that serves the endpoint. - ( - ConnectionError("managed control session was reset"), - None, - [call(timeout=5)], - ConnectionError, - "^managed control session was reset$", - ), - # An interrupt propagates even if OpenVMM exited. - (KeyboardInterrupt(), 0, [], KeyboardInterrupt, ""), - ): with ( - self.subTest(failure=failure, status=status), - tempfile.TemporaryDirectory() as temporary, - ): - state = _provision_managed_sandbox(Path(temporary).resolve()) - process = _openvmm_mock(status) - session = MagicMock() - session.ping.side_effect = failure - context = MagicMock() - context.__enter__.return_value = session - context.__exit__.return_value = False - with ( - patch.object( - sandbox_lifecycle, "require_file", side_effect=require - ), - patch.object( - sandbox_lifecycle.subprocess, "Popen", return_value=process - ), - patch.object( - sandbox_lifecycle, "_process_start_time", return_value=456 - ), - patch.object( - sandbox_lifecycle.ControlSession, - "connect", - return_value=context, - ), - self.assertRaisesRegex(raised, message), - ): - sandbox_lifecycle.start(state, 10) - - self.assertEqual(process.wait.call_args_list, waits) - self.assertEqual(process.terminate.called, status is None) + patch.object(aci_edge_sandboxes_tests, "validate_openvmm_test_backend"), + patch.object( + aci_edge_sandboxes_tests, "e2e_environment", fake_environment + ), + patch.object(aci_edge_sandboxes_tests.subprocess, "run", fake_run), + ): self.assertEqual( - sorted(path.name for path in state.iterdir()), - [ - sandbox_lifecycle.CONFIG_NAME, - sandbox_lifecycle.LOCK_NAME, - sandbox_lifecycle.LOG_NAME, - ], + aci_edge_sandboxes_tests.command_test_aci_edge_sandboxes(args), 3 ) + self.assertTrue((Path(temporary) / "results").is_dir()) - def test_managed_start_spawns_openvmm_with_a_prepared_capability_pipe(self): - # OpenVMM reads its capability as soon as it starts, so start hands it a - # pipe that already holds the capability and the end of file (#440). - def require(path: Path, _description: str) -> Path: - return path + self.assertEqual(len(observed), 1) + self.assertEqual(Path(observed[0]).name, "state") + + def _run_leaving( + self, records: dict[str, object] | None, *, interrupt: bool = False + ) -> tuple[Path, str]: + """Runs the handler with a test command that leaves one sandbox provisioned. - capability = bytes(range(1, 33)) - prepared: list[int] = [] - spawned: list[object] = [] + The sandbox gets `records` as extra state files; `None` leaves only the lock + directories that deprovisioning keeps. Returns the state root that the command + used and the handler's standard error. + """ + state_roots: list[Path] = [] - def prepare(value: bytes) -> int: - self.assertEqual(value, capability) - descriptor = control_session.capability_pipe(value) - prepared.append(descriptor) - return descriptor + def fake_environment( + backend: str, state_root: Path, output_dir: Path + ) -> dict[str, str]: + state_roots.append(state_root) + return {} + + def fake_run( + command: list[str], *, env: dict[str, str], check: bool + ) -> subprocess.CompletedProcess[bytes]: + (state_roots[0] / "filesystem" / ".locks").mkdir(parents=True) + if records is not None: + sandbox = state_roots[0] / "network" / ("a" * 32) + sandbox.mkdir(parents=True) + (sandbox / "sandbox.json").write_text("{}", encoding="utf-8") + for name, record in records.items(): + (sandbox / name).write_text(json.dumps(record), encoding="utf-8") + if interrupt: + raise KeyboardInterrupt + return subprocess.CompletedProcess(command, 0) - process = MagicMock() - process.pid = 123 - process.stdin = None - - def launch(_command: list[str], *, stdin: object, **_options: object) -> Any: - # The pipe stays open until OpenVMM has started with it. - self.assertIsInstance(stdin, int) - os.fstat(cast(int, stdin)) - spawned.append(stdin) - return process - - session = MagicMock() - context = MagicMock() - context.__enter__.return_value = session - context.__exit__.return_value = False with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) + args = argparse.Namespace( + backend="kvm", output_dir=Path(temporary) / "results", cargo="cargo" + ) with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object( - sandbox_lifecycle.secrets, "token_bytes", return_value=capability - ), - patch.object(sandbox_lifecycle, "capability_pipe", side_effect=prepare), - patch.object(sandbox_lifecycle.subprocess, "Popen", side_effect=launch), + patch.object(aci_edge_sandboxes_tests, "validate_openvmm_test_backend"), patch.object( - sandbox_lifecycle, "_process_start_time", return_value=456 + aci_edge_sandboxes_tests, "e2e_environment", fake_environment ), - patch.object( - sandbox_lifecycle.ControlSession, "connect", return_value=context - ) as connect, + patch.object(aci_edge_sandboxes_tests.subprocess, "run", fake_run), + patch("sys.stderr", io.StringIO()) as stderr, ): - sandbox_lifecycle.start(state, 10) - - self.assertEqual( - (state / sandbox_lifecycle.CAPABILITY_NAME).read_bytes(), capability - ) - self.assertEqual(len(prepared), 1) - self.assertEqual(spawned, prepared) - # start closes its copy of the pipe once OpenVMM has it. - with self.assertRaises(OSError): - os.fstat(prepared[0]) - self.assertEqual(connect.call_args.args[1], capability) - session.ping.assert_called_once_with(10) - - def test_managed_start_launches_one_openvmm_for_overlapping_starts(self): - # Overlapping starts of one sandbox each launched OpenVMM. The runtime - # record and capability could then name different VMs, so that stop - # ended one VM or neither, and a failed start removed the other's files - # (#464). - if os.name != "nt" and sys.platform != "linux": - self.skipTest("managed sandboxes require a Linux or Windows host") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - state = _provision_managed_sandbox(Path(temporary.name).resolve()) - popen = subprocess.Popen - launched: list[subprocess.Popen[bytes]] = [] - attempts: list[None] = [] - attempts_lock = threading.Lock() - overtaken = threading.Event() - - def launch(command: list[str], **options: Any) -> subprocess.Popen[bytes]: - with attempts_lock: - first = not attempts - attempts.append(None) - if first: - # Gives the other start time to launch its own OpenVMM before - # this one records its own, as it could without the lock. - overtaken.wait(0.5) - else: - overtaken.set() - # With unknown options, the type checker cannot tell that start - # requests binary streams. - process = cast( - "subprocess.Popen[bytes]", - popen( - [sys.executable, "-c", _SERVING_OPENVMM, *command[1:]], **options - ), - ) - self.addCleanup(_end_child, process) - launched.append(process) - return process + if interrupt: + with self.assertRaises(KeyboardInterrupt): + aci_edge_sandboxes_tests.command_test_aci_edge_sandboxes(args) + else: + self.assertEqual( + aci_edge_sandboxes_tests.command_test_aci_edge_sandboxes(args), + 0, + ) + state_root = state_roots[0] + if state_root.parent.exists(): + self.addCleanup(shutil.rmtree, state_root.parent, True) + return state_root, stderr.getvalue() - def require(path: Path, _description: str) -> Path: - return path + def test_handler_deletes_state_once_every_sandbox_is_deprovisioned(self): + state_root, stderr = self._run_leaving(None) - barrier = threading.Barrier(2) - results: list[str] = [] + self.assertFalse(state_root.parent.exists()) + self.assertEqual(stderr, "") - def contend() -> None: - barrier.wait() - try: - sandbox_lifecycle.start(state, 30) - results.append("started") - except common.ScriptError as error: - results.append(str(error)) + def test_handler_keeps_state_of_sandboxes_left_provisioned(self): + state_root, stderr = self._run_leaving({"runtime.json": {"pid": 4242}}) - with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object(sandbox_lifecycle.subprocess, "Popen", side_effect=launch), - ): - contenders = [ - threading.Thread(target=contend, daemon=True) for _ in range(2) - ] - for contender in contenders: - contender.start() - for contender in contenders: - contender.join(120) - self.assertFalse(any(contender.is_alive() for contender in contenders)) + sandbox = state_root / "network" / ("a" * 32) + self.assertTrue((sandbox / "runtime.json").is_file()) + self.assertIn(f"kept {state_root}", stderr) + self.assertIn(f"{sandbox} (OpenVMM pid 4242)", stderr) - self.assertEqual(len(launched), 1) - self.assertEqual( - sorted(results), - ["sandbox is already running or has stale runtime state", "started"], - ) - runtime = json.loads( - (state / sandbox_lifecycle.RUNTIME_NAME).read_text(encoding="utf-8") + def test_handler_keeps_state_when_the_test_is_interrupted(self): + state_root, stderr = self._run_leaving( + {"launch.json": {"process": {"pid": 77}}}, interrupt=True ) - self.assertEqual(runtime["pid"], launched[0].pid) - # OpenVMM does not inherit the lock, so the next transition can take it - # while the VM runs. - relocked = threading.Event() + sandbox = state_root / "network" / ("a" * 32) + self.assertTrue((sandbox / "launch.json").is_file()) + self.assertIn(f"{sandbox} (OpenVMM pid 77)", stderr) - def relock() -> None: - with sandbox_lifecycle._LifecycleLock(state, 30): - relocked.set() - threading.Thread(target=relock, daemon=True).start() - self.assertTrue(relocked.wait(30)) +class AgentBundlingTests(unittest.TestCase): + def test_cli_registers_the_bundling_check(self): + args = nvx.parse_args(["test-agent-bundling", "--toolchain", "1.95.0"]) - # The capability on record authenticates stop to the only VM, which ends. - outcome = sandbox_lifecycle.stop(state, 30) - self.assertEqual(outcome["outcome"]["category"], "success") - self.assertEqual(launched[0].wait(timeout=30), 0) - sandbox_lifecycle.deprovision(state, 10) - self.assertFalse(state.exists()) + self.assertEqual(args.toolchain, "1.95.0") + self.assertEqual(args.cargo, "cargo") + self.assertIs(args.handler, agent_bundling_tests.command_test_agent_bundling) - def test_managed_start_refuses_runtime_files_without_a_record(self): - # A start that was killed can leave its capability or control socket, but - # no runtime record, while its OpenVMM still runs. Start and deprovision - # refuse them rather than launch a second VM for the sandbox or remove - # the files of a VM that may still run. - def require(path: Path, _description: str) -> Path: - return path + def test_names_and_the_sandbox_abi_come_from_the_crate(self): + names = agent_bundling_tests.contract_names() - for name in ( - sandbox_lifecycle.CAPABILITY_NAME, - sandbox_lifecycle.CONTROL_SOCKET_NAME, - ): - with ( - self.subTest(name=name), - tempfile.TemporaryDirectory() as temporary, - ): - state = _provision_managed_sandbox(Path(temporary).resolve()) - leftover = state / name - leftover.write_bytes(b"leftover") - with ( - patch.object( - sandbox_lifecycle, "require_file", side_effect=require - ), - patch.object(sandbox_lifecycle.subprocess, "Popen") as popen, - self.assertRaisesRegex( - common.ScriptError, - "^sandbox is already running or has stale runtime state$", - ), - ): - sandbox_lifecycle.start(state, 10) + self.assertEqual(names["BUNDLE_DIR_ENV"], "ACI_EDGE_SANDBOXES_AGENT_BUNDLE_DIR") + self.assertEqual(names["SOURCE_MANIFEST"], "SOURCE-MANIFEST.json") + for name in agent_bundling_tests.OTHER_SOURCES: + self.assertIn(name, names) + self.assertGreaterEqual(agent_bundling_tests.sandbox_abi_version(), 1) - popen.assert_not_called() - self.assertEqual(leftover.read_bytes(), b"leftover") - self.assertFalse((state / sandbox_lifecycle.RUNTIME_NAME).exists()) + def test_sources_without_the_constants_are_rejected(self): + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / "empty.rs" + source.write_text("pub const OTHER: u32 = 1;\n", encoding="utf-8") - with self.assertRaisesRegex( - common.ScriptError, - "^sandbox has runtime files but no runtime record, so its " - "OpenVMM process may still run", - ): - sandbox_lifecycle.deprovision(state, 10) - self.assertEqual(leftover.read_bytes(), b"leftover") + with self.assertRaisesRegex(common.ScriptError, "SANDBOX_ABI_VERSION"): + agent_bundling_tests.sandbox_abi_version(source) + with self.assertRaisesRegex(common.ScriptError, "declares no names"): + agent_bundling_tests.contract_names(source) + + def test_bundles_list_and_pin_every_file(self): + for platform, library in ( + ("win32", "bin/aci_edge_agent.dll"), + ("linux", "bin/libaci_edge_agent.so"), + ): + layout = agent_bundling_tests.BundleLayout.for_platform(platform) + with self.subTest(platform=platform), tempfile.TemporaryDirectory() as temp: + bundle = Path(temp) + agent_bundling_tests.write_bundle( + bundle, layout, "SOURCE-MANIFEST.json", 7 + ) + + listed: dict[str, str] = {} + for line in (bundle / "SHA256SUMS").read_text().splitlines(): + digest, name = line.split(" ", 1) + listed[name] = digest self.assertEqual( - sorted(path.name for path in state.iterdir()), - sorted( - ( - sandbox_lifecycle.CONFIG_NAME, - sandbox_lifecycle.LOCK_NAME, - name, - ) - ), + set(listed), + {*layout.files(), "SOURCE-MANIFEST.json", "bundle-manifest.json"}, + ) + for name, digest in listed.items(): + self.assertEqual(common.sha256_file(bundle / name), digest, name) + source = json.loads((bundle / "SOURCE-MANIFEST.json").read_text()) + self.assertEqual(source["format"], 2) + self.assertEqual( + source["linux"]["kernel_sha256"], listed["guest/vmlinux"] + ) + self.assertEqual(source["agent"]["library_sha256"], listed[library]) + manifest = json.loads((bundle / "bundle-manifest.json").read_text()) + consumer = manifest["consumer"] + self.assertEqual(consumer["backendLibrary"], library) + self.assertEqual( + consumer["runtimeBundleManifestSha256"], + listed["SOURCE-MANIFEST.json"], ) + self.assertEqual(consumer["sandboxAbiVersion"], 7) - # A start creates its capability only if no other holds one. + def test_a_bundle_can_pin_another_file_as_its_kernel(self): + layout = agent_bundling_tests.BundleLayout.for_platform("linux") + initramfs = agent_bundling_tests.INITRAMFS with tempfile.TemporaryDirectory() as temporary: - capability = Path(temporary) / sandbox_lifecycle.CAPABILITY_NAME - capability.write_bytes(b"leftover") - with self.assertRaisesRegex( - common.ScriptError, - "^sandbox is already running or has stale runtime state$", - ): - sandbox_lifecycle._create_capability(capability, bytes(range(1, 33))) - self.assertEqual(capability.read_bytes(), b"leftover") - - def test_managed_cleanups_remove_the_runtime_record_last(self): - # An interrupted cleanup then leaves a record that shows OpenVMM gone, - # which deprovision accepts, rather than runtime files that it refuses. - unlink = Path.unlink - removed: list[str] = [] - expected = [ - sandbox_lifecycle.CONTROL_SOCKET_NAME, - sandbox_lifecycle.CAPABILITY_NAME, - sandbox_lifecycle.RUNTIME_NAME, - ] - - def tracked_unlink(path: Path, missing_ok: bool = False) -> None: - if path.name in sandbox_lifecycle.RUNTIME_FILE_NAMES: - removed.append(path.name) - unlink(path, missing_ok=missing_ok) + bundle = Path(temporary) + agent_bundling_tests.write_bundle( + bundle, layout, "SOURCE-MANIFEST.json", 1, kernel=initramfs + ) - with tempfile.TemporaryDirectory() as temporary: - state = Path(temporary) - _write_managed_runtime(state, 123, 456) - (state / sandbox_lifecycle.OUTCOME_NAME).write_text( - json.dumps( - { - "schema_version": sandbox_lifecycle.OUTCOME_SCHEMA_VERSION, - "outcome": {"category": "success"}, - "network_policy": {}, - "teardown": {}, - } - ), - encoding="utf-8", + source = json.loads((bundle / "SOURCE-MANIFEST.json").read_text()) + self.assertEqual( + source["linux"]["kernel_sha256"], common.sha256_file(bundle / initramfs) ) - with ( - patch.object( - sandbox_lifecycle, "_process_running", side_effect=(True, False) - ), - patch.object( - sandbox_lifecycle.ControlSession, - "connect", - return_value=MagicMock(), - ), - patch.object(Path, "unlink", autospec=True, side_effect=tracked_unlink), - ): - sandbox_lifecycle.stop(state, 10) - self.assertEqual(removed, expected) - def require(path: Path, _description: str) -> Path: - return path + def test_cargo_commands_select_the_crate_feature_and_toolchain(self): + command = agent_bundling_tests.cargo_command("cargo", "1.95.0", "test", "x") - removed.clear() - session = MagicMock() - session.ping.side_effect = TimeoutError("managed control response timed out") - context = MagicMock() - context.__enter__.return_value = session - context.__exit__.return_value = False - with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) - with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object( - sandbox_lifecycle.subprocess, - "Popen", - return_value=_openvmm_mock(None), - ), - patch.object( - sandbox_lifecycle, "_process_start_time", return_value=456 - ), - patch.object( - sandbox_lifecycle.ControlSession, "connect", return_value=context - ), - patch.object(Path, "unlink", autospec=True, side_effect=tracked_unlink), - self.assertRaisesRegex(TimeoutError, "response timed out"), - ): - sandbox_lifecycle.start(state, 10) - self.assertEqual(removed, expected) + self.assertEqual(command[:3], ["cargo", "+1.95.0", "test"]) + self.assertEqual( + Path(command[command.index("--manifest-path") + 1]), + BuildConstants.REPO_ROOT / "aci_edge_sandboxes" / "Cargo.toml", + ) + self.assertIn("--locked", command) + self.assertEqual(command[-4:], ["--lib", "--features", "agent-bundled", "x"]) + self.assertEqual( + agent_bundling_tests.cargo_command("cargo", None, "check")[:2], + ["cargo", "check"], + ) - def test_managed_start_keeps_runtime_files_while_openvmm_may_run(self): - # A failed start that cannot end its OpenVMM process leaves the runtime - # files in place rather than make the sandbox look stopped, so start and - # deprovision refuse it while OpenVMM may still run. - def require(path: Path, _description: str) -> Path: - return path + def test_the_synthetic_bundle_is_the_only_bundle_source(self): + names = agent_bundling_tests.contract_names() + inherited = { + names[name]: "inherited" for name in agent_bundling_tests.OTHER_SOURCES + } + inherited[names["BUNDLE_DIR_ENV"]] = "elsewhere" + with patch.dict(os.environ, inherited): + environment = agent_bundling_tests.bundle_environment(names, Path("x")) - process = _openvmm_mock(None) - process.wait.side_effect = subprocess.TimeoutExpired("openvmm", 5) - session = MagicMock() - session.ping.side_effect = TimeoutError("managed control response timed out") - context = MagicMock() - context.__enter__.return_value = session - context.__exit__.return_value = False - with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) - with ( - patch.object(sandbox_lifecycle, "require_file", side_effect=require), - patch.object( - sandbox_lifecycle.subprocess, "Popen", return_value=process - ), - patch.object( - sandbox_lifecycle, "_process_start_time", return_value=456 - ), - patch.object( - sandbox_lifecycle.ControlSession, "connect", return_value=context - ), - self.assertRaises(subprocess.TimeoutExpired), - ): - sandbox_lifecycle.start(state, 10) + self.assertEqual(environment[names["BUNDLE_DIR_ENV"]], "x") + for name in agent_bundling_tests.OTHER_SOURCES: + self.assertNotIn(names[name], environment) - process.kill.assert_called_once_with() - self.assertTrue((state / sandbox_lifecycle.RUNTIME_NAME).is_file()) - self.assertTrue((state / sandbox_lifecycle.CAPABILITY_NAME).is_file()) - with self.assertRaisesRegex( - common.ScriptError, - "^sandbox is already running or has stale runtime state$", - ): - sandbox_lifecycle.start(state, 10) - with ( - patch.object(sandbox_lifecycle, "_process_running", return_value=True), - self.assertRaisesRegex( - common.ScriptError, "^sandbox must be stopped before deprovision$" - ), - ): - sandbox_lifecycle.deprovision(state, 10) + def test_directory_links_need_no_privilege(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / "target").mkdir() + (root / "target" / "file").write_text("content", encoding="utf-8") - def test_lifecycle_lock_excludes_other_holders(self): - with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) - acquired = threading.Event() - - def wait_for_lock() -> None: - with sandbox_lifecycle._LifecycleLock(state, 30): - acquired.set() - - waiter = threading.Thread(target=wait_for_lock, daemon=True) - with sandbox_lifecycle._LifecycleLock(state, 30): - waiter.start() - self.assertFalse(acquired.wait(0.5)) - self.assertTrue(acquired.wait(30)) - waiter.join(30) - self.assertFalse(waiter.is_alive()) - self.assertTrue((state / sandbox_lifecycle.LOCK_NAME).is_file()) - - def test_lifecycle_lock_waits_at_most_the_timeout(self): - # A transition queued behind another that does not end fails once its - # own timeout passes, and changes nothing. - with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) - config = (state / sandbox_lifecycle.CONFIG_NAME).read_bytes() - with ( - sandbox_lifecycle._LifecycleLock(state, 30), - patch.object(sandbox_lifecycle.subprocess, "Popen") as popen, - ): - for operation in ( - lambda: sandbox_lifecycle.start(state, 0.2), - lambda: sandbox_lifecycle.stop(state, 0.2), - lambda: sandbox_lifecycle.deprovision(state, 0.2), - ): - began = time.monotonic() - with self.assertRaisesRegex( - TimeoutError, - "^another lifecycle transition of the sandbox is still " - "in progress$", - ): - operation() - self.assertLess(time.monotonic() - began, 10) - popen.assert_not_called() - self.assertEqual( - sorted(path.name for path in state.iterdir()), - [sandbox_lifecycle.CONFIG_NAME, sandbox_lifecycle.LOCK_NAME], - ) - self.assertEqual( - (state / sandbox_lifecycle.CONFIG_NAME).read_bytes(), config - ) + agent_bundling_tests.link_directory(root / "link", root / "target") - def test_lifecycle_lock_waiter_fails_after_deprovision(self): - # A transition that waits while deprovision removes the sandbox fails as - # for an unprovisioned sandbox, without opening the lock file again. - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - state = _provision_managed_sandbox(Path(temporary.name).resolve()) - # A stale record makes deprovision check its process while it holds the - # lock. - _write_managed_runtime(state, os.getpid(), None) - try_lock_file = sandbox_lifecycle._try_lock_file - open_lock_file = sandbox_lifecycle._open_lock_file - waiting = threading.Event() - waiter_opens: list[Path] = [] - errors: list[BaseException] = [] - - def waiter_thread() -> bool: - return threading.current_thread() is not threading.main_thread() - - def try_lock(descriptor: int) -> bool: - if waiter_thread(): - waiting.set() - return try_lock_file(descriptor) - - def open_lock(path: Path) -> int: - if waiter_thread(): - waiter_opens.append(path) - return open_lock_file(path) - - def stop() -> None: - try: - sandbox_lifecycle.stop(state, 10) - except BaseException as error: # noqa: BLE001 - errors.append(error) + self.assertEqual((root / "link" / "file").read_text(), "content") + # A link, not a copy: only links have a target to read. + self.assertTrue(os.readlink(root / "link")) + + def _outcomes(self, **overrides: tuple[int, str]) -> dict[str, tuple[int, str]]: + """Returns the status and output of cargo for each bundle that the check builds, + as the build script reports them, with `overrides`.""" + abi = agent_bundling_tests.sandbox_abi_version() + outcomes = { + "good": (0, ""), + "tampered": (101, "SHA-256 mismatch for guest/vmlinux: SHA256SUMS lists"), + "pinned": (101, "pins /linux/kernel_sha256 to another digest than"), + "other-abi": (101, f"library speaks sandbox ABI {abi + 1}, but this"), + "linked": (101, "guest is a link; a bundle holds only files"), + } + outcomes.update( + {name.replace("_", "-"): outcome for name, outcome in overrides.items()} + ) + return outcomes - waiter = threading.Thread(target=stop, daemon=True) + def _check(self, outcomes: dict[str, tuple[int, str]]) -> tuple[int, list[str]]: + """Runs the check with cargo replaced by `outcomes`, and returns its status and + the cargo subcommand and bundle of each command that it ran.""" + names = agent_bundling_tests.contract_names() + calls: list[str] = [] - def process_running(_pid: int, _start_time: int | None) -> bool: - waiter.start() - self.assertTrue(waiting.wait(30)) - return False + def fake_run( + command: list[str], *, env: dict[str, str], check: bool, **_: object + ) -> subprocess.CompletedProcess[str]: + self.assertFalse(check) + subcommand = command[command.index("--manifest-path") - 1] + bundle = Path(env[names["BUNDLE_DIR_ENV"]]).name + calls.append(f"{subcommand} {bundle}") + status, output = outcomes[bundle] + return subprocess.CompletedProcess(command, status, output, "") + args = argparse.Namespace(cargo="cargo", toolchain="1.95.0") with ( - patch.object(sandbox_lifecycle, "_try_lock_file", side_effect=try_lock), - patch.object(sandbox_lifecycle, "_open_lock_file", side_effect=open_lock), - patch.object( - sandbox_lifecycle, "_process_running", side_effect=process_running - ), + patch.object(agent_bundling_tests.subprocess, "run", fake_run), + patch.object(agent_bundling_tests, "link_directory"), + patch("sys.stdout", io.StringIO()), + patch("sys.stderr", io.StringIO()), ): - sandbox_lifecycle.deprovision(state, 10) - waiter.join(30) - - self.assertFalse(waiter.is_alive()) - self.assertEqual(len(errors), 1) - self.assertIsInstance(errors[0], common.ScriptError) - self.assertEqual(str(errors[0]), "sandbox is not provisioned") - self.assertEqual(waiter_opens, [state / sandbox_lifecycle.LOCK_NAME]) - self.assertFalse(state.exists()) - - def test_managed_deprovision_releases_its_lock_if_the_lock_file_lingers(self): - # NFS, and Windows file systems without POSIX deletion such as ReFS, keep - # a removed file in its directory while it is open, so the directory - # empties only once deprovision releases the lock. - with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) - rmdir = Path.rmdir - release = sandbox_lifecycle._LifecycleLock.release - events: list[str] = [] - - def lingering_rmdir(path: Path) -> None: - events.append("rmdir") - if events.count("rmdir") == 1: - raise OSError(errno.ENOTEMPTY, "Directory not empty", str(path)) - rmdir(path) - - def tracked_release(lock: sandbox_lifecycle._LifecycleLock) -> None: - events.append("release") - release(lock) + return agent_bundling_tests.command_test_agent_bundling(args), calls - with ( - patch.object(Path, "rmdir", autospec=True, side_effect=lingering_rmdir), - patch.object( - sandbox_lifecycle._LifecycleLock, - "release", - autospec=True, - side_effect=tracked_release, - ), - ): - sandbox_lifecycle.deprovision(state, 10) - - self.assertEqual(events, ["rmdir", "release", "rmdir", "release"]) - self.assertFalse(state.exists()) - - def test_managed_deprovision_retries_only_while_lock_files_linger(self): - # Deprovision retries removing the emptied directory only while it is not - # empty, at once if the last lingering lock file is gone by then. Any - # other failure, such as a parent that denies the removal, it reports at - # once rather than after its timeout. - def check( - failure: int, failures: int | None, attempts: int, pauses: int - ) -> None: - with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) - rmdir = Path.rmdir - removals: list[Path] = [] - - def failing_rmdir(path: Path) -> None: - if path == state: - removals.append(path) - if failures is None or len(removals) <= failures: - raise OSError(failure, os.strerror(failure), str(path)) - rmdir(path) + def test_check_requires_the_tests_and_every_rejection(self): + status, calls = self._check(self._outcomes()) - with ( - patch.object( - Path, "rmdir", autospec=True, side_effect=failing_rmdir - ), - patch.object( - sandbox_lifecycle, "_pause", wraps=sandbox_lifecycle._pause - ) as pause, - ): - if failures is None: - with self.assertRaises(PermissionError): - sandbox_lifecycle.deprovision(state, 1) - else: - sandbox_lifecycle.deprovision(state, 30) + self.assertEqual(status, 0) + self.assertEqual( + calls, + [ + "test good", + "check tampered", + "check pinned", + "check other-abi", + "check linked", + ], + ) - self.assertEqual(len(removals), attempts) - self.assertEqual(pause.call_count, pauses) - if failures is None: - self.assertEqual(list(state.iterdir()), []) - else: - self.assertFalse(state.exists()) + def test_check_returns_the_status_of_failing_tests(self): + status, calls = self._check(self._outcomes(good=(3, ""))) - for name, failure, failures, attempts, pauses in ( - ("last lock file gone", errno.ENOTEMPTY, 2, 3, 0), - ("hidden lock file", errno.ENOTEMPTY, 4, 5, 1), - ("removal denied", errno.EACCES, None, 1, 0), - ): - with self.subTest(name): - check(failure, failures, attempts, pauses) - - def test_managed_deprovision_outlasts_waiters_that_keep_the_lock_file(self): - # On NFS and on Windows file systems without POSIX deletion, each waiter's - # handle keeps the removed lock file in the directory until the waiter - # finds the sandbox gone. Deprovision then retries until they have - # closed it, and no waiter creates a new lock file. - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - state = _provision_managed_sandbox(Path(temporary.name).resolve()) - _write_managed_runtime(state, os.getpid(), None) - rmdir = Path.rmdir - try_lock_file = sandbox_lifecycle._try_lock_file - polling: set[int] = set() - polling_lock = threading.Lock() - both_polling = threading.Event() - errors: list[BaseException] = [] - - def try_lock(descriptor: int) -> bool: - if threading.current_thread() is not threading.main_thread(): - with polling_lock: - polling.add(threading.get_ident()) - if len(polling) == 2: - both_polling.set() - return try_lock_file(descriptor) - - def stop() -> None: - try: - sandbox_lifecycle.stop(state, 30) - except BaseException as error: # noqa: BLE001 - errors.append(error) + self.assertEqual(status, 3) + self.assertEqual(calls, ["test good"]) - waiters = [threading.Thread(target=stop, daemon=True) for _ in range(2)] + def test_check_fails_when_the_build_accepts_a_broken_bundle(self): + with self.assertRaisesRegex(common.ScriptError, "accepted the tampered bundle"): + self._check(self._outcomes(tampered=(0, ""))) - def process_running(_pid: int, _start_time: int | None) -> bool: - for waiter in waiters: - waiter.start() - self.assertTrue(both_polling.wait(30)) - return False + def test_check_fails_when_a_rejection_reports_another_cause(self): + with self.assertRaisesRegex(common.ScriptError, "/linux/kernel_sha256"): + self._check(self._outcomes(pinned=(101, "cannot read the bundle"))) - def lingering_rmdir(path: Path) -> None: - if path == state and any(waiter.is_alive() for waiter in waiters): - raise OSError(errno.ENOTEMPTY, "Directory not empty", str(path)) - rmdir(path) - with ( - patch.object(sandbox_lifecycle, "_try_lock_file", side_effect=try_lock), - patch.object( - sandbox_lifecycle, "_process_running", side_effect=process_running - ), - patch.object(Path, "rmdir", autospec=True, side_effect=lingering_rmdir), - ): - sandbox_lifecycle.deprovision(state, 30) - for waiter in waiters: - waiter.join(30) +class RunShareTests(_CheckoutArtifacts): + """The share helpers of sandbox.py, which `nvx.py run` uses; nvx-rs owns + those of `nvx.py sandbox`.""" - self.assertFalse(any(waiter.is_alive() for waiter in waiters)) + def test_mount_value_defaults_to_read_only_and_accepts_rw(self): self.assertEqual( - [str(error) for error in errors], ["sandbox is not provisioned"] * 2 + sandbox.parse_mount_value("/workspace,host-dir"), + ("/workspace", "host-dir", "ro"), + ) + self.assertEqual( + sandbox.parse_mount_value("/opt/hostedtoolcache,host-dir,rw"), + ("/opt/hostedtoolcache", "host-dir", "rw"), ) - self.assertFalse(state.exists()) - - def test_managed_deprovision_outlasts_a_transition_that_starts_meanwhile(self): - # A transition that starts after deprovision removed the lock file holds - # a new one in the directory until it finds the sandbox gone, which must - # not make deprovision fail. - with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) - rmdir = Path.rmdir - arrived = threading.Event() - leave = threading.Event() - attempts: list[bool] = [] - - def arrive() -> None: - with sandbox_lifecycle._LifecycleLock(state, 30): - arrived.set() - leave.wait(30) - - arrival = threading.Thread(target=arrive, daemon=True) - - def contended_rmdir(path: Path) -> None: - if path != state: - rmdir(path) - return - if not attempts: - attempts.append(False) - arrival.start() - self.assertTrue(arrived.wait(30)) - raise OSError(errno.ENOTEMPTY, "Directory not empty", str(path)) - try: - rmdir(path) - except OSError: - attempts.append(False) - leave.set() - raise - attempts.append(True) - - with patch.object( - Path, "rmdir", autospec=True, side_effect=contended_rmdir - ): - sandbox_lifecycle.deprovision(state, 30) - arrival.join(30) - - self.assertFalse(arrival.is_alive()) - self.assertEqual(attempts[0], False) - self.assertIn(False, attempts[1:]) - self.assertEqual(attempts[-1], True) - self.assertFalse(state.exists()) - - def test_provision_waiting_on_deprovision_provisions_a_new_directory(self): - # A provision that waits while deprovision removes the sandbox creates - # nothing in the directory being removed. Once deprovision has removed - # it, the provision creates the directory again, and both succeed. - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - root = Path(temporary.name).resolve() - state = _provision_managed_sandbox(root) - _write_managed_runtime(state, os.getpid(), None) - rmdir = Path.rmdir - try_lock_file = sandbox_lifecycle._try_lock_file - polling = threading.Event() - removals: list[bool] = [] - errors: list[BaseException] = [] - - def try_lock(descriptor: int) -> bool: - if threading.current_thread() is not threading.main_thread(): - polling.set() - return try_lock_file(descriptor) - - def provision() -> None: - try: - _provision_managed_sandbox(root) - except BaseException as error: # noqa: BLE001 - errors.append(error) - - waiter = threading.Thread(target=provision, daemon=True) - - def process_running(_pid: int, _start_time: int | None) -> bool: - waiter.start() - self.assertTrue(polling.wait(30)) - return False - - def lingering_rmdir(path: Path) -> None: - if path == state and not removals: - # As on NFS, the waiter's handle keeps the removed lock file. - removals.append(False) - raise OSError(errno.ENOTEMPTY, "Directory not empty", str(path)) - rmdir(path) - if path == state: - removals.append(True) - with ( - patch.object(sandbox_lifecycle, "_try_lock_file", side_effect=try_lock), - patch.object( - sandbox_lifecycle, "_process_running", side_effect=process_running - ), - patch.object(Path, "rmdir", autospec=True, side_effect=lingering_rmdir), + def test_mount_value_rejects_malformed_specifications(self): + for value, message in ( + ("/workspace", "GUEST_TARGET,HOST_PATH"), + ("/workspace,", "host path is empty"), + ("/workspace,,rw", "host path is empty"), + ("/workspace,host,rx", "unsupported --mount mode 'rx'"), + # With more than one comma, the value must end in the mode, which + # the error suggests for a host path that contains commas. + ("/workspace,host,rw,extra", "mode 'extra'.*if HOST_PATH contains commas"), + ("/workspace,alpha,beta", "mode 'beta'.*if HOST_PATH contains commas"), ): - sandbox_lifecycle.deprovision(state, 30) - waiter.join(30) + with self.subTest(value=value): + with self.assertRaisesRegex(common.ScriptError, message): + sandbox.parse_mount_value(value) - self.assertFalse(waiter.is_alive()) - self.assertEqual(errors, []) - self.assertEqual(removals[0], False) - self.assertIn(True, removals[1:]) - self.assertEqual( - sorted(path.name for path in state.iterdir()), - [sandbox_lifecycle.CONFIG_NAME, sandbox_lifecycle.LOCK_NAME], - ) - - def test_provision_waiting_on_deprovision_retries_once_deprovision_ends(self): - # Deprovision clears the mark of the removed lock file once it has removed - # the directory, or failed to remove it or the lock file, so a provision - # that waited for it retries at once. It then finds the sandbox that - # another provision created in between, or provisions the directory that - # deprovision left. - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - - def check(ending: str) -> None: - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - root = Path(temporary.name).resolve() - state = _provision_managed_sandbox(root) - _write_managed_runtime(state, os.getpid(), None) - rmdir = Path.rmdir - unlink = Path.unlink - try_lock_file = sandbox_lifecycle._try_lock_file - polling = threading.Event() - denials: list[Path] = [] - errors: list[BaseException] = [] - - def deny(path: Path) -> None: - denials.append(path) - raise PermissionError( - errno.EACCES, os.strerror(errno.EACCES), str(path) + def test_mount_value_accepts_host_paths_with_commas(self): + alpha_path = r"C:\projects\alpha,beta" + reports_path = r"C:\reports\2026, final" + for value, target, host_path, access in ( + (f"/work,{alpha_path},rw", "/work", alpha_path, "rw"), + (f"/reports,{reports_path},ro", "/reports", reports_path, "ro"), + # A host path that ends in a comma, or in what looks like a mode, + # keeps it before the mode. + ("/data,/srv/data,rw,ro", "/data", "/srv/data,rw", "ro"), + ("/data,/srv/data,,rw", "/data", "/srv/data,", "rw"), + # Values without commas in the host path keep their meaning. + ("/data,/srv/data,rw", "/data", "/srv/data", "rw"), + ("/data,/srv/data", "/data", "/srv/data", "ro"), + ): + with self.subTest(value=value): + self.assertEqual( + sandbox.parse_mount_value(value), (target, host_path, access) ) - def try_lock(descriptor: int) -> bool: - if threading.current_thread() is not threading.main_thread(): - polling.set() - return try_lock_file(descriptor) + def test_caller_owner_requires_a_linux_host(self): + with patch.object(sandbox.os, "name", "nt"): + with self.assertRaisesRegex(common.ScriptError, "requires a Linux host"): + sandbox.require_mount_owner_supported("caller") + sandbox.require_mount_owner_supported("vmm") + with patch.object(sandbox.os, "name", "posix"): + sandbox.require_mount_owner_supported("caller") - def provision() -> None: - try: - _provision_managed_sandbox(root) - except BaseException as error: # noqa: BLE001 - errors.append(error) + def test_aggregate_child_names_pin_their_targets(self): + name = sandbox.aggregate_child_name(0, "/workspace") + self.assertEqual(name, f"0-{hashlib.sha256(b'/workspace').hexdigest()[:32]}") + # Equal targets give equal names, so a restore names its children as + # the boot did, and another target or position gives another name. + self.assertEqual(sandbox.aggregate_child_name(0, "/workspace"), name) + self.assertNotEqual(sandbox.aggregate_child_name(0, "/workspace2"), name) + self.assertNotEqual(sandbox.aggregate_child_name(1, "/workspace"), name) + # Every name follows OpenVMM's rule for the name of a child. + for index, target in ((0, "/w"), (999, "/" + "x" * 1000)): + self.assertRegex( + sandbox.aggregate_child_name(index, target), + r"\A[0-9]+-[0-9a-f]{32}\Z", + ) + self.assertLessEqual(len(sandbox.aggregate_child_name(999, "/w")), 64) - waiter = threading.Thread(target=provision, daemon=True) + def test_run_attaches_a_file_as_an_aggregate_child(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + settings = root / "settings.json" + settings.write_bytes(b"{}") + work = root / "work" + work.mkdir() + with ( + patch.object(nvx, "require_file", return_value=Path("artifact")), + patch.object( + nvx, "_format_command", return_value="formatted" + ) as format_command, + ): + nvx.command_run( + nvx.parse_args( + [ + "run", + "--mount", + f"/config/settings.json,{settings},ro", + "--dry-run", + ] + ) + ) + command = format_command.call_args.args[0] + name = sandbox.aggregate_child_name(0, "/config/settings.json") + # Even alone, a file is the child of an aggregate. + self.assertNotIn("--mount", command) + self.assertEqual( + command[command.index("--mount-aggregate") + 1], "/run/nvx/shares" + ) + self.assertEqual( + command[command.index("--mount-child") + 1], + f"{name},{settings},ro,file", + ) + self.assertEqual( + command[command.index("--cmdline") + 1], + f"nvx_share={name},/config/settings.json,ro,file", + ) - def process_running(_pid: int, _start_time: int | None) -> bool: - waiter.start() - self.assertTrue(polling.wait(30)) - return False + # A directory and a file share the aggregate, and only the + # file's token is marked. + nvx.command_run( + nvx.parse_args( + [ + "run", + "--mount", + f"/work,{work},rw", + "--mount", + f"/config/settings.json,{settings},ro", + "--dry-run", + ] + ) + ) + command = format_command.call_args.args[0] + work_name, settings_name = ( + sandbox.aggregate_child_name(index, target) + for index, target in enumerate(("/work", "/config/settings.json")) + ) + self.assertEqual( + command[command.index("--cmdline") + 1], + f"nvx_share={work_name},/work,rw " + f"nvx_share={settings_name},/config/settings.json,ro,file", + ) - def ending_rmdir(path: Path) -> None: - if path == state and ending == "removal denied": - deny(path) - rmdir(path) - if path == state and ending == "replaced": - # Another provision recreates the directory before the waiter - # could find it gone. - _provision_managed_sandbox(root) - - def ending_unlink(path: Path, missing_ok: bool = False) -> None: - # Only the first removal of the lock file fails, so the release - # that follows removes it. - lock = path == state / sandbox_lifecycle.LOCK_NAME - if lock and ending == "unlink denied" and not denials: - deny(path) - unlink(path, missing_ok=missing_ok) + for arguments, message in ( + (["--mount-deny", "secrets"], "absolute host path"), + (["--cmdline", "x" * 1024], "1024-byte"), + ): + with self.subTest(arguments=arguments): + invalid = nvx.parse_args( + [ + "run", + "--mount", + f"/config/settings.json,{settings},ro", + *arguments, + "--dry-run", + ] + ) + with self.assertRaisesRegex(common.ScriptError, message): + nvx.command_run(invalid) + invalid = nvx.parse_args( + ["run", f"--mount=/a b,{settings}", "--dry-run"] + ) + with self.assertRaisesRegex( + common.ScriptError, "invalid --mount target" + ): + nvx.command_run(invalid) + def test_run_shares_host_paths_with_commas(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + alpha = root / "alpha,beta" + reports = root / "2026, final" + settings = root / "settings,v2.json" + alpha.mkdir() + reports.mkdir() + settings.write_bytes(b"{}") with ( - patch.object(sandbox_lifecycle, "_try_lock_file", side_effect=try_lock), + patch.object(nvx, "require_file", return_value=Path("artifact")), patch.object( - sandbox_lifecycle, "_process_running", side_effect=process_running - ), - patch.object(Path, "rmdir", autospec=True, side_effect=ending_rmdir), - patch.object(Path, "unlink", autospec=True, side_effect=ending_unlink), + nvx, "_format_command", return_value="formatted" + ) as format_command, ): - if ending == "replaced": - sandbox_lifecycle.deprovision(state, 30) - else: - with self.assertRaises(PermissionError): - sandbox_lifecycle.deprovision(state, 30) - waiter.join(30) - - self.assertFalse(waiter.is_alive()) - self.assertEqual(len(denials), 0 if ending == "replaced" else 1) - self.assertEqual( - [str(error) for error in errors], - ["sandbox is already provisioned"] if ending == "replaced" else [], - ) - self.assertEqual( - sorted(path.name for path in state.iterdir()), - [sandbox_lifecycle.CONFIG_NAME, sandbox_lifecycle.LOCK_NAME], - ) - - for ending in ("replaced", "removal denied", "unlink denied"): - with self.subTest(ending): - check(ending) + # The mode follows the last comma, so a lone directory stays a + # plain --mount of its exact path, and OpenVMM gets the mode + # even when the value omits it. + for value, expected in ( + (f"/work,{alpha},rw", f"/work,{alpha},rw"), + (f"/reports,{reports},ro", f"/reports,{reports},ro"), + (f"/plain,{root}", f"/plain,{root},ro"), + ): + with self.subTest(value=value): + nvx.command_run( + nvx.parse_args(["run", "--mount", value, "--dry-run"]) + ) + command = format_command.call_args.args[0] + self.assertNotIn("--mount-aggregate", command) + self.assertEqual( + command[command.index("--mount") + 1], expected + ) - def test_provision_handoff_completes_or_ends_the_removal(self): - # While only lingering lock files keep the directory that deprovision - # removes, a provision waits. It stops once the path names no directory or - # another one, or the directory holds anything else, and once nothing - # keeps the directory, it removes it itself, in case deprovision has given - # up. A removal that fails for another reason leaves it to provision the - # directory in place. - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - state = root / "state" - state.mkdir() - lingering = state / sandbox_lifecycle.LOCK_NAME - lingering.write_bytes(b"") - removing = os.lstat(state) - with self.assertRaisesRegex( - TimeoutError, - "^another lifecycle transition of the sandbox is still in progress$", - ): - sandbox_lifecycle._wait_for_handoff( - state, removing, time.monotonic() + 0.1 + # Several shares are the children of an aggregate, whose values + # end in the mode and, for a file, the `file` flag. + nvx.command_run( + nvx.parse_args( + [ + "run", + "--mount", + f"/work,{alpha},rw", + "--mount", + f"/reports,{reports},ro", + "--mount", + f"/config/settings.json,{settings},ro", + "--mount-deny", + str(alpha / "secret"), + "--dry-run", + ] + ) ) - - def wait() -> None: - sandbox_lifecycle._wait_for_handoff( - state, removing, time.monotonic() + 30 + command = format_command.call_args.args[0] + work, report, config = ( + sandbox.aggregate_child_name(index, target) + for index, target in enumerate( + ("/work", "/reports", "/config/settings.json") + ) + ) + self.assertEqual( + [ + command[index + 1] + for index, value in enumerate(command) + if value == "--mount-child" + ], + [ + f"{work},{alpha},rw", + f"{report},{reports},ro", + f"{config},{settings},ro,file", + ], + ) + self.assertEqual( + command[command.index("--mount-deny") + 1], str(alpha / "secret") ) - with patch.object(sandbox_lifecycle, "_pause", return_value=False) as pause: - for name in (sandbox_lifecycle.CONFIG_NAME, "foreign"): - (state / name).write_text("{}", encoding="utf-8") - wait() - (state / name).unlink() - # The old directory keeps its identity under another name, so the - # new one cannot reuse it. - state.rename(root / "old") - state.mkdir() - wait() - self.assertTrue(state.is_dir()) - state.rmdir() - (root / "old").rename(state) - lingering.unlink() - with patch.object( - Path, - "rmdir", - autospec=True, - side_effect=PermissionError(errno.EACCES, "denied"), - ): - wait() - self.assertTrue(state.is_dir()) - wait() - self.assertFalse(state.exists()) - wait() - pause.assert_not_called() - - def test_provision_completes_a_removal_that_deprovision_gave_up(self): - # Once a lingering lock file makes deprovision release the lock before the - # directory is gone, a provision that waits for the removal completes it - # itself if deprovision gives up, and then provisions the path again. - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - root = Path(temporary.name).resolve() - state = _provision_managed_sandbox(root) - _write_managed_runtime(state, os.getpid(), None) - rmdir = Path.rmdir - try_lock_file = sandbox_lifecycle._try_lock_file - wait_for_handoff = sandbox_lifecycle._wait_for_handoff - polling = threading.Event() - deprovisioned = threading.Event() - attempts: list[Path] = [] - handoffs: list[Path] = [] - errors: list[BaseException] = [] - - def try_lock(descriptor: int) -> bool: - if threading.current_thread() is not threading.main_thread(): - polling.set() - return try_lock_file(descriptor) - - def provision() -> None: - try: - _provision_managed_sandbox(root) - except BaseException as error: # noqa: BLE001 - errors.append(error) - - waiter = threading.Thread(target=provision, daemon=True) - - def process_running(_pid: int, _start_time: int | None) -> bool: - waiter.start() - self.assertTrue(polling.wait(30)) - return False - - def abandoned_rmdir(path: Path) -> None: - # A lock file lingers at deprovision's first attempt, and its retry is - # denied, so that it gives up. - if path == state and threading.current_thread() is threading.main_thread(): - attempts.append(path) - if len(attempts) == 1: - raise OSError(errno.ENOTEMPTY, os.strerror(errno.ENOTEMPTY)) - raise PermissionError(errno.EACCES, os.strerror(errno.EACCES)) - rmdir(path) - - def handoff(state_dir: Path, removing: os.stat_result, deadline: float) -> None: - handoffs.append(state_dir) - if not deprovisioned.wait(30): - raise AssertionError("deprovision did not give up") - wait_for_handoff(state_dir, removing, deadline) + # Without the mode, the text after the last comma is no mode. + for value in (f"/work,{alpha}", f"/reports,{reports}"): + with ( + self.subTest(value=value), + self.assertRaisesRegex( + common.ScriptError, "end the value with ,ro or ,rw" + ), + ): + nvx.command_run( + nvx.parse_args(["run", "--mount", value, "--dry-run"]) + ) - with ( - patch.object(sandbox_lifecycle, "_try_lock_file", side_effect=try_lock), - patch.object( - sandbox_lifecycle, "_process_running", side_effect=process_running - ), - patch.object(Path, "rmdir", autospec=True, side_effect=abandoned_rmdir), - patch.object(sandbox_lifecycle, "_wait_for_handoff", side_effect=handoff), - ): - with self.assertRaises(PermissionError): - sandbox_lifecycle.deprovision(state, 30) - deprovisioned.set() - waiter.join(30) - - self.assertFalse(waiter.is_alive()) - self.assertEqual(errors, []) - self.assertEqual(attempts, [state, state]) - self.assertEqual(handoffs, [state]) - self.assertEqual( - sorted(path.name for path in state.iterdir()), - [sandbox_lifecycle.CONFIG_NAME, sandbox_lifecycle.LOCK_NAME], + def test_run_attaches_several_mounts_as_an_aggregate(self): + args = nvx.parse_args( + [ + "run", + "--mount", + "/workspace,work,rw", + "--mount", + "/opt/hostedtoolcache,tools", + "--mount", + "/srv/cache,cache,ro", + "--cmdline", + "quiet", + "--dry-run", + ] ) - - def test_managed_deprovision_accepts_a_removal_that_a_provision_completed(self): - # A provision that waits for deprovision to remove the directory may - # remove it first and provision the path again. Deprovision then succeeds - # rather than report the new sandbox as provisioned again, which it tells - # apart even where a new directory could reuse the old inode number. - if os.name != "nt" and sys.platform != "linux": - self.skipTest("process identity requires a Linux or Windows host") - temporary = tempfile.TemporaryDirectory() - self.addCleanup(temporary.cleanup) - root = Path(temporary.name).resolve() - state = _provision_managed_sandbox(root) - _write_managed_runtime(state, os.getpid(), None) - rmdir = Path.rmdir - try_lock_file = sandbox_lifecycle._try_lock_file - polling = threading.Event() - provisioned = threading.Event() - attempts: list[Path] = [] - errors: list[BaseException] = [] - - def try_lock(descriptor: int) -> bool: - if threading.current_thread() is not threading.main_thread(): - polling.set() - return try_lock_file(descriptor) - - def provision() -> None: - try: - _provision_managed_sandbox(root) - except BaseException as error: # noqa: BLE001 - errors.append(error) - finally: - provisioned.set() - - waiter = threading.Thread(target=provision, daemon=True) - - def process_running(_pid: int, _start_time: int | None) -> bool: - waiter.start() - self.assertTrue(polling.wait(30)) - return False - - def contended_rmdir(path: Path) -> None: - if path == state and threading.current_thread() is threading.main_thread(): - attempts.append(path) - if len(attempts) == 1: - raise OSError(errno.ENOTEMPTY, os.strerror(errno.ENOTEMPTY)) - # Deprovision retries only once the waiting provision has removed - # the directory and provisioned the path again. - self.assertTrue(provisioned.wait(30)) - rmdir(path) - with ( - patch.object(sandbox_lifecycle, "_try_lock_file", side_effect=try_lock), + patch.object(nvx, "require_file", return_value=Path("artifact")), patch.object( - sandbox_lifecycle, "_process_running", side_effect=process_running - ), - patch.object(Path, "rmdir", autospec=True, side_effect=contended_rmdir), + nvx, "_format_command", return_value="formatted" + ) as format_command, ): - sandbox_lifecycle.deprovision(state, 30) - waiter.join(30) - - self.assertFalse(waiter.is_alive()) - self.assertEqual(errors, []) - self.assertEqual(attempts, [state, state]) - self.assertEqual( - sorted(path.name for path in state.iterdir()), - [sandbox_lifecycle.CONFIG_NAME, sandbox_lifecycle.LOCK_NAME], - ) - - def test_lifecycle_lock_file_transfers_complete_despite_short_counts(self): - # The tombstone must be complete before deprovision removes the lock file, - # and a waiter must read all of it, even if os.write() or os.read() - # transfers only part of it at a time. - with tempfile.TemporaryDirectory() as temporary: - descriptor = sandbox_lifecycle._open_lock_file( - Path(temporary) / sandbox_lifecycle.LOCK_NAME + nvx.command_run(args) + command = format_command.call_args.args[0] + workspace, toolcache, cache = ( + sandbox.aggregate_child_name(index, target) + for index, target in enumerate( + ("/workspace", "/opt/hostedtoolcache", "/srv/cache") + ) ) - try: - self._check_lock_file_transfers(descriptor) - finally: - os.close(descriptor) - - def _check_lock_file_transfers(self, descriptor: int) -> None: - write = os.write - read = os.read - tombstone = sandbox_lifecycle.LOCK_TOMBSTONE - - def short_write(fd: int, data: memoryview) -> int: - return write(fd, bytes(data[:3])) - - def short_read(fd: int, size: int) -> bytes: - return read(fd, min(size, 3)) - - with ( - patch.object( - sandbox_lifecycle.os, "write", side_effect=short_write - ) as writes, - patch.object(sandbox_lifecycle.os, "read", side_effect=short_read), - ): - sandbox_lifecycle._write_lock_file(descriptor, tombstone) + booted = [ + "/run/nvx/shares", + f"{workspace},work,rw", + f"{toolcache},tools,ro", + f"{cache},cache,ro", + ] + self.assertNotIn("--mount", command) self.assertEqual( - sandbox_lifecycle._read_lock_file(descriptor, len(tombstone)), - tombstone, + [ + command[index + 1] + for index, value in enumerate(command) + if value in ("--mount-aggregate", "--mount-child") + ], + booted, ) - self.assertEqual(writes.call_count, -(-len(tombstone) // 3)) - self.assertEqual(os.lseek(descriptor, 0, os.SEEK_CUR), 0) - with ( - patch.object(sandbox_lifecycle.os, "write", return_value=0), - self.assertRaisesRegex(OSError, "made no progress"), - ): - sandbox_lifecycle._write_lock_file(descriptor, tombstone) - - def test_managed_deprovision_reports_a_provision_that_starts_meanwhile(self): - # A provision that starts after deprovision removed the lock file, and - # before it removed the directory, provisions the directory again first. - # Deprovision then reports that rather than remove the new sandbox. - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - state = _provision_managed_sandbox(root) - rmdir = Path.rmdir - provisioned: list[Path] = [] - - def contended_rmdir(path: Path) -> None: - if path == state and not provisioned: - arrival = threading.Thread( - target=lambda: provisioned.append( - _provision_managed_sandbox(root) - ), - daemon=True, - ) - arrival.start() - arrival.join(30) - rmdir(path) - - with ( - patch.object(Path, "rmdir", autospec=True, side_effect=contended_rmdir), - self.assertRaisesRegex( - common.ScriptError, - "^sandbox was provisioned again while deprovision removed it$", - ), - ): - sandbox_lifecycle.deprovision(state, 10) - - self.assertEqual(provisioned, [state]) self.assertEqual( - sorted(path.name for path in state.iterdir()), - [sandbox_lifecycle.CONFIG_NAME, sandbox_lifecycle.LOCK_NAME], + command[command.index("--cmdline") + 1], + f"quiet nvx_share={workspace},/workspace,rw " + f"nvx_share={toolcache},/opt/hostedtoolcache,ro " + f"nvx_share={cache},/srv/cache,ro", ) - def test_lifecycle_deadlines_reject_retries_after_they_pass(self): - # Each wait sleeps at most until its deadline, and no retry starts after - # it, so neither a directory that deprovision removes only then nor a lock - # file that another transition replaces only then counts. - self.assertFalse(sandbox_lifecycle._pause(time.monotonic() - 1)) - with tempfile.TemporaryDirectory() as temporary: - directory = Path(temporary) / "going" - directory.mkdir() - # A lock file lingers in the directory until the deadline. - lingering = directory / sandbox_lifecycle.LOCK_NAME - lingering.write_bytes(b"") - deadline = 3 / 64 - clock = [0.0] - sleeps: list[float] = [] - - def sleep(seconds: float) -> None: - sleeps.append(seconds) - clock[0] += seconds - if clock[0] >= deadline and directory.exists(): - lingering.unlink() - directory.rmdir() - - with ( - patch.object(sandbox_lifecycle, "LOCK_RETRY_INTERVAL", 1 / 32), - patch.object( - sandbox_lifecycle.time, "monotonic", side_effect=lambda: clock[0] - ), - patch.object(sandbox_lifecycle.time, "sleep", side_effect=sleep), - self.assertRaisesRegex( - TimeoutError, - "^another lifecycle transition of the sandbox is still in " - "progress$", - ), - ): - sandbox_lifecycle._wait_for_handoff( - directory, os.lstat(directory), deadline - ) - - self.assertEqual(sleeps, [1 / 32, 1 / 64]) - self.assertFalse(directory.exists()) - - with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) - names_open_file = sandbox_lifecycle._names_open_file - clock = [0.0] - checks: list[Path] = [] - - def replaced_late(path: Path, descriptor: int) -> bool: - checks.append(path) - if len(checks) > 1: - return names_open_file(path, descriptor) - clock[0] = 1.0 - return False - - with ( - patch.object( - sandbox_lifecycle.time, "monotonic", side_effect=lambda: clock[0] - ), - patch.object( - sandbox_lifecycle, "_names_open_file", side_effect=replaced_late - ), - self.assertRaisesRegex( - TimeoutError, - "^another lifecycle transition of the sandbox is still in " - "progress$", - ), - sandbox_lifecycle._LifecycleLock(state, 1), - ): - self.fail("the lock was acquired after its deadline") - - self.assertEqual(checks, [state / sandbox_lifecycle.LOCK_NAME]) + # One share stays a plain --mount without bind tokens. + single = nvx.parse_args( + ["run", "--mount", "/workspace,work,rw", "--dry-run"] + ) + nvx.command_run(single) + command = format_command.call_args.args[0] self.assertEqual( - sorted(path.name for path in state.iterdir()), - [sandbox_lifecycle.CONFIG_NAME, sandbox_lifecycle.LOCK_NAME], + command[command.index("--mount") :], ["--mount", "/workspace,work,rw"] ) + self.assertNotIn("--cmdline", command) - def test_provision_recreates_a_state_directory_that_deprovision_removed(self): - # A deprovision can remove the state directory after provision prepared - # it and before provision opens the lock file in it. The provision then - # creates the directory again instead of failing. - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - state = root / "state" - open_lock_file = sandbox_lifecycle._open_lock_file - opened: list[Path] = [] - - def open_after_deprovision(path: Path) -> int: - if not opened: - shutil.rmtree(state) - opened.append(path) - return open_lock_file(path) - - with patch.object( - sandbox_lifecycle, "_open_lock_file", side_effect=open_after_deprovision - ): - self.assertEqual(_provision_managed_sandbox(root), state) - - self.assertEqual(len(opened), 2) + # A restored guest keeps its binds, so only the devices are named, + # with the children's names of the boot: they pin the targets that + # the guest bound, and another target names another child. + restored = nvx.parse_args( + [ + "run", + "--restore-snapshot", + "snapshot", + "--mount", + "/workspace,work,rw", + "--mount", + "/opt/hostedtoolcache,tools", + "--mount", + "/srv/cache,cache,ro", + "--dry-run", + ] + ) + nvx.command_run(restored) + command = format_command.call_args.args[0] self.assertEqual( - sorted(path.name for path in state.iterdir()), - [sandbox_lifecycle.CONFIG_NAME, sandbox_lifecycle.LOCK_NAME], + [ + command[index + 1] + for index, value in enumerate(command) + if value in ("--mount-aggregate", "--mount-child") + ], + booted, ) + self.assertNotIn("--cmdline", command) + retargeted = nvx.parse_args( + [ + "run", + "--restore-snapshot", + "snapshot", + "--mount", + "/workspace,work,rw", + "--mount", + "/opt/tools,tools", + "--mount", + "/srv/cache,cache,ro", + "--dry-run", + ] + ) + nvx.command_run(retargeted) + command = format_command.call_args.args[0] + children = [ + command[index + 1] + for index, value in enumerate(command) + if value == "--mount-child" + ] + self.assertEqual(children[0::2], booted[1::2]) + self.assertNotEqual(children[1], booted[2]) + self.assertEqual(children[1].split(",", 1)[1], "tools,ro") - def test_provision_waits_out_a_lock_file_pending_deletion(self): - # Without POSIX deletion semantics, Windows denies opening a lock file - # that deprovision removed until its last handle closes, so a provision - # retries. Elsewhere, such a denial is a real error. - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - open_lock_path = sandbox_lifecycle._open_lock_path - attempts: list[Path] = [] - - def pending_deletion(path: Path) -> int: - attempts.append(path) - if len(attempts) == 1: - raise PermissionError(errno.EACCES, "Access is denied", str(path)) - return open_lock_path(path) - - with patch.object( - sandbox_lifecycle, "_open_lock_path", side_effect=pending_deletion + for mounts, message in ( + (["/a,a,rx"], "unsupported --mount mode"), + (["/workspace,a", "/workspace/cache,b"], "overlap"), + (["/run/nvx,a", "/b,b"], "overlap"), + (["/a b,a", "/b,b"], "invalid --mount target"), + (["relative,a", "/b,b"], "invalid --mount target"), + ([f"/share/{index:02},host" for index in range(60)], "1024-byte"), ): - if os.name == "nt": - _provision_managed_sandbox(root) - self.assertEqual(len(attempts), 2) - self.assertTrue( - (root / "state" / sandbox_lifecycle.CONFIG_NAME).is_file() + with self.subTest(mounts=mounts[:2]): + invalid = nvx.parse_args( + ["run", *[f"--mount={mount}" for mount in mounts], "--dry-run"] ) - else: - with self.assertRaises(PermissionError): - _provision_managed_sandbox(root) - self.assertEqual(len(attempts), 1) - - def test_lifecycle_lock_refuses_a_linked_lock_file(self): - # Deprovision writes to the lock file, so a link to a file elsewhere must - # not stand in for it. - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - state = _provision_managed_sandbox(root) - target = root / "precious" - target.write_bytes(b"precious data") - lock = state / sandbox_lifecycle.LOCK_NAME - links: dict[str, Callable[[], None]] = { - "hard link": lambda: os.link(target, lock), - "symbolic link": lambda: os.symlink(target, lock), - } - for kind, link in links.items(): - with self.subTest(kind=kind): - lock.unlink(missing_ok=True) - try: - link() - except OSError as error: - self.skipTest(f"cannot create a {kind}: {error}") - with self.assertRaisesRegex( - common.ScriptError, - "^sandbox lifecycle lock is not a standalone regular file", - ): - sandbox_lifecycle.deprovision(state, 10) - self.assertEqual(target.read_bytes(), b"precious data") - self.assertTrue((state / sandbox_lifecycle.CONFIG_NAME).is_file()) - - def test_lifecycle_lock_rejects_an_unbounded_timeout(self): - with tempfile.TemporaryDirectory() as temporary: - state = _provision_managed_sandbox(Path(temporary).resolve()) - for timeout in (0.0, -1.0, float("nan"), float("inf")): - with ( - self.subTest(timeout=timeout), - self.assertRaisesRegex( - ValueError, - "^lifecycle timeout must be finite and greater than 0$", - ), - ): - sandbox_lifecycle.stop(state, timeout) + with self.assertRaisesRegex(common.ScriptError, message): + nvx.command_run(invalid) + relative = nvx.parse_args( + [ + "run", + "--mount", + "/a,a", + "--mount", + "/b,b", + "--mount-deny", + "secrets", + "--dry-run", + ] + ) + with self.assertRaisesRegex(common.ScriptError, "absolute host path"): + nvx.command_run(relative) - def test_lifecycle_lock_file_stays_only_in_a_provisioned_sandbox(self): - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary).resolve() - unprovisioned = root / "unprovisioned" - unprovisioned.mkdir() - with self.assertRaisesRegex(common.ScriptError, "sandbox configuration"): - sandbox_lifecycle.start(unprovisioned, 10) - with self.assertRaisesRegex(common.ScriptError, "sandbox is not running"): - sandbox_lifecycle.stop(unprovisioned, 10) - self.assertEqual(list(unprovisioned.iterdir()), []) - - state = _provision_managed_sandbox(root) - self.assertEqual( - sorted(path.name for path in state.iterdir()), - [sandbox_lifecycle.CONFIG_NAME, sandbox_lifecycle.LOCK_NAME], - ) - # A sandbox that an earlier NVX version provisioned gains a lock file. - (state / sandbox_lifecycle.LOCK_NAME).unlink() - with self.assertRaisesRegex(common.ScriptError, "sandbox is not running"): - sandbox_lifecycle.stop(state, 10) - self.assertTrue((state / sandbox_lifecycle.LOCK_NAME).is_file()) - - # Deprovision removes every NVX file, the lock file included, even when - # a file of another owner keeps the directory. - foreign = state / "operator-note.txt" - foreign.write_text("operator data\n", encoding="utf-8") - with self.assertRaisesRegex(common.ScriptError, "operator-note.txt"): - sandbox_lifecycle.deprovision(state, 10) - self.assertEqual(list(state.iterdir()), [foreign]) - foreign.unlink() - sandbox_lifecycle.deprovision(state, 10) - self.assertFalse(state.exists()) - - def test_launch_contract_rejects_disk_option_delimiters(self): + def test_mount_kind_follows_the_host_path(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) - layer_path = root / "layer;create=1G" - scratch = root / "scratch.ext4" - layer_path.touch() - scratch.touch() - layer = sandbox.SandboxLayer( - role="distro", - path=layer_path, - uuid="11111111-1111-1111-1111-111111111111", - ) - - with self.assertRaisesRegex(common.ScriptError, "semicolons"): - sandbox.SandboxLaunch( - layers=(layer,), - scratch=scratch, - ).validated() + settings = root / "settings.json" + settings.write_bytes(b"{}") + self.assertEqual(sandbox.mount_kind(settings), "file") + self.assertEqual(sandbox.mount_kind(root), "directory") + self.assertEqual(sandbox.mount_kind(root / "missing"), "directory") class BenchmarkTests(unittest.TestCase): @@ -20578,6 +16479,17 @@ def _missing_release() -> urllib.error.HTTPError: class ReleaseTests(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + # Installing a release replaces or removes nvx-rs; no test may do + # either to the nvx-rs of this checkout. + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.nvx_rs_path = Path(temporary.name) / nvx_rs.binary_path().name + patcher = patch.object(nvx_rs, "binary_path", return_value=self.nvx_rs_path) + patcher.start() + self.addCleanup(patcher.stop) + def test_alpine_source_validation_rejects_malformed_manifest(self): with tempfile.TemporaryDirectory() as temporary: source_dir = Path(temporary) @@ -21298,6 +17210,7 @@ def artifact_path(name: str) -> Path: side_effect=artifact_path, ), patch.object(release, "openvmm_binary_path", return_value=binary), + patch.object(nvx_rs, "binary_path", return_value=paths["nvx_rs"]), patch.object( release, "kernel_provenance_inputs", @@ -21318,6 +17231,14 @@ def artifact_path(name: str) -> Path: ) self.assertTrue((destination / "bin" / binary.name).is_file()) + # The release build of nvx-rs ships beside OpenVMM, under the name + # that nvx.py download installs on this OS. + packaged_nvx_rs = destination / "bin" / nvx_rs.binary_path().name + self.assertEqual(packaged_nvx_rs.read_bytes(), paths["nvx_rs"].read_bytes()) + self.assertEqual( + sorted(path.name for path in (destination / "bin").iterdir()), + sorted((binary.name, packaged_nvx_rs.name)), + ) for name in ReleaseBuildConstants.GUEST_ARTIFACT_NAMES: self.assertTrue((destination / "guest" / name).is_file()) self.assertTrue( @@ -21379,7 +17300,44 @@ def artifact_path(name: str) -> Path: / AzureLinuxBuildConstants.PACKAGE_MANIFEST_NAME ), ) - common.verify_sha256_sums(destination) + # The package ships the provenance of nvx-rs, which binds it to the + # sources that build it, the Rust release, the target, and the + # features, and the manifest records them with its digest. + packaged_provenance = json.loads( + (destination / "provenance" / nvx_rs.PROVENANCE_NAME).read_text( + encoding="utf-8" + ) + ) + self.assertEqual( + packaged_provenance, + { + "format": 1, + "source_sha256": nvx_rs.source_sha256(root), + "toolchain": GuestRustBuildConstants.TOOLCHAIN, + "target": nvx_rs.release_target(), + "features": ["cli"], + "executable_sha256": common.sha256_file(packaged_nvx_rs), + }, + ) + self.assertEqual( + packaged_provenance["source_sha256"], nvx_rs.source_sha256() + ) + self.assertEqual( + manifest["nvx_rs"], + { + "executable_sha256": common.sha256_file(packaged_nvx_rs), + "features": ["cli"], + "source_sha256": nvx_rs.source_sha256(root), + "target": nvx_rs.release_target(), + "toolchain": GuestRustBuildConstants.TOOLCHAIN, + }, + ) + inventory = dict(common.verify_sha256_sums(destination).files) + self.assertEqual( + inventory[f"bin/{packaged_nvx_rs.name}"], + common.sha256_file(paths["nvx_rs"]), + ) + self.assertIn(f"provenance/{nvx_rs.PROVENANCE_NAME}", inventory) self.assertIn("binary-only package", stderr.getvalue()) def test_source_package_omits_azure_linux_artifacts(self): @@ -21441,6 +17399,7 @@ def write_source_archive(output: Path, *_args: object) -> None: side_effect=artifact_path, ), patch.object(release, "openvmm_binary_path", return_value=binary), + patch.object(nvx_rs, "binary_path", return_value=paths["nvx_rs"]), patch.object( release, "kernel_provenance_inputs", @@ -21492,12 +17451,98 @@ def write_source_archive(output: Path, *_args: object) -> None: (destination / "SOURCE-MANIFEST.json").read_text(encoding="utf-8") ) self.assertNotIn("azurelinux", manifest) + # Source-inclusive packages ship nvx-rs too; the project source + # archive holds the sources that build it. + packaged_nvx_rs = destination / "bin" / paths["nvx_rs"].name + self.assertEqual(packaged_nvx_rs.read_bytes(), paths["nvx_rs"].read_bytes()) + self.assertEqual( + manifest["nvx_rs"]["executable_sha256"], + common.sha256_file(packaged_nvx_rs), + ) common.verify_sha256_sums(destination) self.assertIn("omits the Azure Linux guest", stderr.getvalue()) + def test_packages_require_the_release_build_of_nvx_rs(self): + # A package ships the nvx-rs build for the release target of its + # operating system, with the provenance that binds it to the current + # sources. On Linux, that build is the static musl executable that + # runs on every host, which a build for the host's own target is not. + linux = nvx_rs.RELEASE_TARGETS["linux"] + windows = nvx_rs.RELEASE_TARGETS["windows"] + dynamic = _elf_file( + ((1, b"code"), (3, b"/lib64/ld-linux-x86-64.so.2\0")), elf_type=3 + ) + mismatch = "from the current nvx-rs sources" + with tempfile.TemporaryDirectory() as temporary: + build_dir = Path(temporary) / "build" + build_dir.mkdir() + executable = Path(temporary) / "nvx-rs" + provenance = build_dir / nvx_rs.PROVENANCE_NAME + cases: tuple[ + tuple[str, bytes | None, dict[str, object] | None, str | None], ... + ] = ( + (linux, _elf_file(), {}, None), + (linux, _elf_file(elf_type=3), {}, None), + (windows, b"MZ", {}, None), + (linux, dynamic, {}, "is not static"), + (linux, b"MZ", {}, f"invalid {linux} nvx-rs executable"), + (linux, _elf_file(), {"target": windows}, f"as a {linux} build"), + (windows, b"MZ", {"target": linux}, f"as a {windows} build"), + (linux, _elf_file(), {"source_sha256": "0" * 64}, mismatch), + (linux, _elf_file(), {"executable_sha256": "0" * 64}, mismatch), + (linux, _elf_file(), {"toolchain": "1.0.0"}, mismatch), + (linux, _elf_file(), {"features": []}, mismatch), + (linux, _elf_file(), {"format": 2}, mismatch), + (linux, _elf_file(), None, "nvx-rs build provenance not found"), + (linux, None, {}, "nvx-rs release executable not found"), + ) + for target, contents, changes, error in cases: + with self.subTest(target=target, changes=changes, error=error): + executable.unlink(missing_ok=True) + provenance.unlink(missing_ok=True) + if contents is not None: + executable.write_bytes(contents) + if contents is not None and changes is not None: + record = nvx_rs.provenance_record(executable, target) + record.update(changes) + provenance.write_text(json.dumps(record), encoding="utf-8") + with ( + patch.object(nvx_rs, "binary_path", return_value=executable), + patch.object(nvx_rs, "release_target", return_value=target), + patch.object( + release, "artifact_path", side_effect=build_dir.joinpath + ), + ): + if error is None: + self.assertEqual( + release._require_release_nvx_rs(), + ( + executable, + provenance, + json.loads(provenance.read_text(encoding="utf-8")), + ), + ) + continue + with self.assertRaisesRegex( + common.ScriptError, re.escape(error) + ) as raised: + release._require_release_nvx_rs() + if error == "is not static": + # The message names the build that packages need. + self.assertIn( + f"--features cli --manifest-path nvx-rs/Cargo.toml " + f"--target {linux}", + str(raised.exception), + ) + if error == mismatch: + self.assertIn( + "nvx.py record-nvx-rs-provenance", str(raised.exception) + ) + def test_runtime_provenance_validation_uses_required_paths(self): build_dir = Path("build") binary = Path("openvmm") + nvx_rs_binary = Path("nvx-rs") def artifact_path(name: str) -> Path: return build_dir / name @@ -21508,6 +17553,7 @@ def require_file(path: Path, _description: str) -> Path: with ( patch.object(release, "artifact_path", side_effect=artifact_path), patch.object(release, "openvmm_binary_path", return_value=binary), + patch.object(nvx_rs, "find_binary", return_value=nvx_rs_binary), patch.object( release, "require_file", @@ -21516,11 +17562,12 @@ def require_file(path: Path, _description: str) -> Path: patch.object(release, "_validate_openvmm_provenance") as openvmm, patch.object(release, "_validate_kernel_provenance"), patch.object(release, "_validate_initramfs_provenance"), + patch.object(release, "_validate_nvx_rs_provenance") as nvx_rs_check, ): release.validate_runtime_artifact_provenance() self.assertEqual( - require.call_args_list[-3:], + require.call_args_list[-4:], [ call( build_dir / OpenVMMBuildConstants.PROVENANCE_NAME, @@ -21534,11 +17581,83 @@ def require_file(path: Path, _description: str) -> Path: build_dir / InitramfsBuildConstants.PROVENANCE_NAME, "initramfs build provenance", ), + call(build_dir / nvx_rs.PROVENANCE_NAME, "nvx-rs build provenance"), ], ) openvmm.assert_called_once_with( binary, build_dir / OpenVMMBuildConstants.PROVENANCE_NAME ) + # The nvx-rs that nvx.py runs, of any target. + nvx_rs_check.assert_called_once_with( + nvx_rs_binary, build_dir / nvx_rs.PROVENANCE_NAME, target=None + ) + + def test_runtime_provenance_validation_rejects_a_stale_nvx_rs(self): + # nvx.py never rebuilds an nvx-rs that exists, so one left from + # before a change of its sources, or that its provenance does not + # describe, would run. The adversarial executor validates it first. + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + build_dir = root / "build" + build_dir.mkdir() + provenance = build_dir / nvx_rs.PROVENANCE_NAME + other = root / "other-nvx-rs" + other.write_bytes(b"another nvx-rs") + + def require_file(path: Path, _description: str) -> Path: + return path + + def validate() -> None: + with ( + patch.object( + release, "artifact_path", side_effect=build_dir.joinpath + ), + patch.object(release, "openvmm_binary_path"), + patch.object(release, "require_file", side_effect=require_file), + patch.object(release, "_validate_openvmm_provenance"), + patch.object(release, "_validate_kernel_provenance"), + patch.object(release, "_validate_initramfs_provenance"), + ): + release.validate_runtime_artifact_provenance() + + self.nvx_rs_path.write_bytes(b"nvx-rs") + provenance.write_text( + json.dumps( + nvx_rs.provenance_record( + self.nvx_rs_path, "x86_64-unknown-linux-gnu" + ) + ), + encoding="utf-8", + ) + with _without_nvx_rs_variable(): + validate() + # A rebuild after the record. + self.nvx_rs_path.write_bytes(b"rebuilt nvx-rs") + with self.assertRaisesRegex( + common.ScriptError, "does not describe .* as a build from the" + ): + validate() + # The nvx-rs that NVX_RS names is the one that nvx.py runs. + with ( + patch.dict(os.environ, {nvx_rs.VARIABLE: str(other)}), + self.assertRaisesRegex(common.ScriptError, re.escape(str(other))), + ): + validate() + provenance.write_text( + json.dumps(nvx_rs.provenance_record(other, "x86_64-unknown-linux-gnu")), + encoding="utf-8", + ) + with patch.dict(os.environ, {nvx_rs.VARIABLE: str(other)}): + validate() + # A record of other sources. + record = nvx_rs.provenance_record(other, "x86_64-unknown-linux-gnu") + record["source_sha256"] = "0" * 64 + provenance.write_text(json.dumps(record), encoding="utf-8") + with ( + patch.dict(os.environ, {nvx_rs.VARIABLE: str(other)}), + self.assertRaisesRegex(common.ScriptError, "current nvx-rs sources"), + ): + validate() def test_package_rejects_dirty_openvmm_provenance(self): with tempfile.TemporaryDirectory() as temporary: @@ -21805,6 +17924,18 @@ def test_package_rejects_tampered_source_metadata_without_replacing_output(self) "0" * 64, "Alpine minirootfs_sha256", ), + ( + "alpine", + "guest_sources", + ["guest/common", "guest/alpine"], + "Alpine guest_sources", + ), + ( + "guest_rust", + "toolchain", + "stable", + "Rust guest toolchain", + ), ) for section, field, invalid_value, error in cases: with ( @@ -21852,6 +17983,7 @@ def artifact_path( "openvmm_binary_path", return_value=paths["binary"], ), + patch.object(nvx_rs, "binary_path", return_value=paths["nvx_rs"]), patch.object( release, "kernel_provenance_inputs", @@ -21881,6 +18013,14 @@ def artifact_path( ) def test_failed_staged_verification_preserves_existing_release(self): + for corrupted, error in ( + ("binary", "packaged OpenVMM executable"), + ("nvx_rs", "packaged nvx-rs executable does not match its build"), + ): + with self.subTest(corrupted=corrupted): + self._check_failed_staged_verification(corrupted, error) + + def _check_failed_staged_verification(self, corrupted: str, error: str) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) paths, kernel_inputs, revision = _write_release_fixture(root) @@ -21897,7 +18037,7 @@ def artifact_path(name: str) -> Path: def corrupt_packaged_binary(source: Path, target: Path) -> None: original_copy(source, target) - if target.name == paths["binary"].name: + if target.name == paths[corrupted].name: target.write_bytes(b"corrupt") with ( @@ -21915,6 +18055,7 @@ def corrupt_packaged_binary(source: Path, target: Path) -> None: "openvmm_binary_path", return_value=paths["binary"], ), + patch.object(nvx_rs, "binary_path", return_value=paths["nvx_rs"]), patch.object( release, "kernel_provenance_inputs", @@ -21931,10 +18072,7 @@ def corrupt_packaged_binary(source: Path, target: Path) -> None: side_effect=corrupt_packaged_binary, ), patch("sys.stderr", io.StringIO()), - self.assertRaisesRegex( - common.ScriptError, - "packaged OpenVMM executable", - ), + self.assertRaisesRegex(common.ScriptError, error), ): release.package_release( version="1.0.0", @@ -22080,59 +18218,98 @@ def test_publication_rechecks_force_destination_scope(self): ) self.assertFalse(staging.exists()) + def _runtime_release_archive( + self, + root: Path, + *, + nvx_rs_contents: bytes | None, + nvx_rs_provenance: Mapping[str, object] | None, + ) -> Path: + """Write the archive of a binary package for this OS, with or without + nvx-rs, and with or without its provenance, which `nvx_rs_provenance` + changes from the record of a build of this checkout's sources.""" + package_root = root / "package" / "nvx-1.2.3-test" + binary_name = "openvmm.exe" if os.name == "nt" else "openvmm" + binary = package_root / "bin" / binary_name + binary.parent.mkdir(parents=True) + binary.write_bytes(b"openvmm") + for name in ReleaseBuildConstants.GUEST_ARTIFACT_NAMES: + guest = package_root / "guest" / name + guest.parent.mkdir(parents=True, exist_ok=True) + guest.write_bytes(name.encode("ascii")) + for name in ( + OpenVMMBuildConstants.PROVENANCE_NAME, + KernelBuildConstants.PROVENANCE_NAME, + InitramfsBuildConstants.PROVENANCE_NAME, + ): + provenance = package_root / "provenance" / name + provenance.parent.mkdir(parents=True, exist_ok=True) + provenance.write_bytes(name.encode("ascii")) + if nvx_rs_contents is not None: + packaged_nvx_rs = package_root / "bin" / self.nvx_rs_path.name + packaged_nvx_rs.write_bytes(nvx_rs_contents) + # The installed nvx-rs is executable, whatever mode the archive + # records. + packaged_nvx_rs.chmod(0o644) + if nvx_rs_provenance is not None: + record = nvx_rs.provenance_record( + packaged_nvx_rs, nvx_rs.release_target() + ) + record.update(nvx_rs_provenance) + (package_root / "provenance" / nvx_rs.PROVENANCE_NAME).write_text( + json.dumps(record), encoding="utf-8" + ) + (package_root / "SOURCE-MANIFEST.json").write_text( + json.dumps({"linux": {}, "alpine": {}, "ubuntu": {}, "azurelinux": {}}), + encoding="utf-8", + ) + common.write_sha256_sums(package_root) + if os.name == "nt": + archive_path = root / "nvx-1.2.3-windows-whp.zip" + with zipfile.ZipFile(archive_path, "w") as package: + for path in package_root.rglob("*"): + package.write(path, path.relative_to(package_root.parent)) + else: + archive_path = root / "nvx-1.2.3-linux-kvm.tar.gz" + with tarfile.open(archive_path, "w:gz") as package: + package.add(package_root, arcname=package_root.name) + return archive_path + + def _install_runtime_archive(self, root: Path, archive_path: Path) -> str: + """Install the archive into the runtime directory below `root`, and + return what the installer printed.""" + binary_name = "openvmm.exe" if os.name == "nt" else "openvmm" + with ( + patch.object( + release, + "artifact_path", + side_effect=(root / "runtime" / "build").joinpath, + ), + patch.object( + release, + "openvmm_binary_path", + return_value=root / "runtime" / "openvmm" / binary_name, + ), + contextlib.redirect_stdout(io.StringIO()) as stdout, + ): + release._install_release_archive(archive_path) + return stdout.getvalue() + def test_release_archive_installs_runtime_layout(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) - package_root = root / "package" / "nvx-1.2.3-test" - binary_name = "openvmm.exe" if os.name == "nt" else "openvmm" - binary = package_root / "bin" / binary_name - binary.parent.mkdir(parents=True) - binary.write_bytes(b"openvmm") - for name in ReleaseBuildConstants.GUEST_ARTIFACT_NAMES: - guest = package_root / "guest" / name - guest.parent.mkdir(parents=True, exist_ok=True) - guest.write_bytes(name.encode("ascii")) - for name in ( - OpenVMMBuildConstants.PROVENANCE_NAME, - KernelBuildConstants.PROVENANCE_NAME, - InitramfsBuildConstants.PROVENANCE_NAME, - ): - provenance = package_root / "provenance" / name - provenance.parent.mkdir(parents=True, exist_ok=True) - provenance.write_bytes(name.encode("ascii")) - common.write_sha256_sums(package_root) - - if os.name == "nt": - archive_path = root / "nvx-1.2.3-windows-whp.zip" - with zipfile.ZipFile(archive_path, "w") as package: - for path in package_root.rglob("*"): - package.write(path, path.relative_to(package_root.parent)) - else: - archive_path = root / "nvx-1.2.3-linux-kvm.tar.gz" - with tarfile.open(archive_path, "w:gz") as package: - package.add(package_root, arcname=package_root.name) - + archive_path = self._runtime_release_archive( + root, nvx_rs_contents=b"nvx-rs", nvx_rs_provenance={} + ) build_dir = root / "runtime" / "build" binary_destination = ( - root / "runtime" / "openvmm" / "target" / "release" / binary_name + root + / "runtime" + / "openvmm" + / ("openvmm.exe" if os.name == "nt" else "openvmm") ) - def artifact_path(name: str) -> Path: - return build_dir / name - - with ( - patch.object( - release, - "artifact_path", - side_effect=artifact_path, - ), - patch.object( - release, - "openvmm_binary_path", - return_value=binary_destination, - ), - ): - release._install_release_archive(archive_path) + output = self._install_runtime_archive(root, archive_path) self.assertEqual(binary_destination.read_bytes(), b"openvmm") for name in ReleaseBuildConstants.GUEST_ARTIFACT_NAMES: @@ -22149,6 +18326,123 @@ def artifact_path(name: str) -> Path: (build_dir / name).read_bytes(), name.encode("ascii"), ) + # nvx-rs, which this checkout's sources built, goes where nvx.py + # finds it, executable, with its provenance beside OpenVMM's. + self.assertEqual(self.nvx_rs_path.read_bytes(), b"nvx-rs") + self.assertEqual(output, "") + if os.name != "nt": + self.assertEqual(self.nvx_rs_path.stat().st_mode & 0o777, 0o755) + self.assertEqual(binary_destination.stat().st_mode & 0o777, 0o755) + installed_provenance = build_dir / nvx_rs.PROVENANCE_NAME + self.assertEqual( + json.loads(installed_provenance.read_text(encoding="utf-8")), + nvx_rs.provenance_record(self.nvx_rs_path, nvx_rs.release_target()), + ) + release._validate_nvx_rs_provenance( + self.nvx_rs_path, installed_provenance, target=None + ) + # nvx.py then runs it without Cargo. + with ( + _without_nvx_rs_variable(), + patch("shutil.which", return_value=None), + patch("subprocess.run") as run, + ): + self.assertEqual(nvx_rs.require_binary(), self.nvx_rs_path) + run.assert_not_called() + + def test_release_archive_leaves_nvx_rs_of_other_sources_to_cargo(self): + # nvx.py runs the nvx-rs that download installs without rebuilding it, + # so download installs only the build of the checkout's nvx-rs sources + # that nvx.py runs, and removes an earlier one and its provenance, so + # that nvx.py builds nvx-rs from the checkout instead. Releases that + # predate nvx-rs install too. + name = self.nvx_rs_path.name + archive_name = ( + "nvx-1.2.3-windows-whp.zip" + if os.name == "nt" + else "nvx-1.2.3-linux-kvm.tar.gz" + ) + target = nvx_rs.release_target() + other_target = ( + "x86_64-unknown-linux-gnu" + if target == nvx_rs.RELEASE_TARGETS["linux"] + else nvx_rs.RELEASE_TARGETS["linux"] + ) + other_build = ( + f"the bin/{name} of {archive_name} is not the {target} build of this " + "checkout's nvx-rs sources that nvx.py runs" + ) + cases: tuple[tuple[str, bytes | None, dict[str, object] | None, str], ...] = ( + ("older release", None, None, f"{archive_name} ships no bin/{name}"), + ( + "no provenance", + b"nvx-rs", + None, + f"{archive_name} ships no provenance for its bin/{name}", + ), + ("other sources", b"nvx-rs", {"source_sha256": "0" * 64}, other_build), + ("other Rust release", b"nvx-rs", {"toolchain": "1.0.0"}, other_build), + ("other features", b"nvx-rs", {"features": ["cli", "x"]}, other_build), + ("other target", b"nvx-rs", {"target": other_target}, other_build), + ("other format", b"nvx-rs", {"format": 2}, other_build), + ) + for case, contents, provenance, reason in cases: + with ( + self.subTest(case=case), + tempfile.TemporaryDirectory() as temporary, + ): + root = Path(temporary) + archive_path = self._runtime_release_archive( + root, nvx_rs_contents=contents, nvx_rs_provenance=provenance + ) + build_dir = root / "runtime" / "build" + build_dir.mkdir(parents=True) + stale_provenance = build_dir / nvx_rs.PROVENANCE_NAME + stale_provenance.write_text("{}", encoding="utf-8") + self.nvx_rs_path.write_bytes(b"stale nvx-rs") + + output = self._install_runtime_archive(root, archive_path) + + self.assertTrue( + (build_dir / KernelBuildConstants.BINARY_NAME).is_file() + ) + self.assertFalse(self.nvx_rs_path.exists()) + self.assertFalse(stale_provenance.exists()) + self.assertIn(reason, output) + self.assertIn( + "so nvx.py builds nvx-rs from this checkout with Cargo when a " + "command needs it, unless NVX_RS names one", + output, + ) + # Without Cargo, nvx.py names the ways to get nvx-rs. + with ( + _without_nvx_rs_variable(), + patch("shutil.which", return_value=None), + self.assertRaisesRegex(common.ScriptError, "nvx.py download"), + ): + nvx_rs.require_binary() + + def test_release_archive_with_inconsistent_nvx_rs_installs_nothing(self): + # A package whose nvx-rs does not match its own provenance is broken, + # so download fails before it replaces anything. + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + archive_path = self._runtime_release_archive( + root, + nvx_rs_contents=b"nvx-rs", + nvx_rs_provenance={"executable_sha256": "0" * 64}, + ) + self.nvx_rs_path.write_bytes(b"earlier nvx-rs") + with self.assertRaisesRegex( + common.ScriptError, + re.escape( + f"the bin/{self.nvx_rs_path.name} of {archive_path.name} does " + "not match its build provenance" + ), + ): + self._install_runtime_archive(root, archive_path) + self.assertFalse((root / "runtime").exists()) + self.assertEqual(self.nvx_rs_path.read_bytes(), b"earlier nvx-rs") def test_source_release_archive_removes_stale_azure_artifacts(self): with tempfile.TemporaryDirectory() as temporary: @@ -22210,6 +18504,7 @@ def artifact_path(name: str) -> Path: "openvmm_binary_path", return_value=binary_destination, ), + contextlib.redirect_stdout(io.StringIO()), ): release._install_release_archive(archive_path) @@ -22543,6 +18838,45 @@ def test_release_archives_are_reproducible_with_fixed_layout_and_modes(self): ) ) + def test_release_archives_ship_both_executables_executable(self): + # Every package ships OpenVMM and nvx-rs in bin/, executable whatever + # mode the staging host stored, and publication finds both. + for suffix, platform, extension in ( + (".tar.gz", "linux-kvm", ""), + (".zip", "windows-whp", ".exe"), + ): + with ( + self.subTest(suffix=suffix), + tempfile.TemporaryDirectory() as temporary, + ): + root = Path(temporary) + source = root / f"nvx-1.0.0-{platform}" + for name in (f"openvmm{extension}", f"nvx-rs{extension}"): + executable = source / "bin" / name + executable.parent.mkdir(parents=True, exist_ok=True) + executable.write_bytes(name.encode("ascii")) + executable.chmod(0o644) + common.write_sha256_sums(source) + package = root / f"nvx-1.0.0-{platform}{suffix}" + release.create_release_archive(source, package) + + if suffix == ".tar.gz": + with tarfile.open(package, "r:gz") as archive_file: + modes = { + member.name: member.mode + for member in archive_file.getmembers() + } + else: + with zipfile.ZipFile(package) as archive_file: + modes = { + member.filename: (member.external_attr >> 16) & 0o777 + for member in archive_file.infolist() + } + for path in release.release_package_executables(platform): + self.assertEqual(modes[f"{source.name}/{path}"], 0o755) + self.assertEqual(modes[f"{source.name}/SHA256SUMS"], 0o644) + release.require_release_package_executables(package, platform) + def test_release_archive_rejects_internally_inconsistent_staging(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) diff --git a/scripts/test_tool_versions.py b/scripts/test_tool_versions.py index 1456605a3..0fe40bde5 100644 --- a/scripts/test_tool_versions.py +++ b/scripts/test_tool_versions.py @@ -691,6 +691,13 @@ def test_linux_setups_pin_their_tools_to_the_manifest(self): [f"{name}={value}" for name, value in expected.items()], ) + def test_linux_setups_install_the_pinned_musl_target(self): + for script in (LINUX_RUNNER, LINUX_MSHV): + with self.subTest(script=script.name): + source = script.read_text(encoding="utf-8") + self.assertIn('target add --toolchain "$RUST_TOOLCHAIN"', source) + self.assertIn("x86_64-unknown-linux-musl", source) + def test_windows_setup_pins_its_tools_to_the_manifest(self): powershells = _powershells() if not powershells: