From 0931a512a1b3ae27c7afd7a817b772617e85e524 Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:37:26 +0200 Subject: [PATCH 1/4] mason: verify pinned mutation runner on Windows --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74291e5..a246d11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -390,6 +390,12 @@ jobs: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('Cargo.lock') }}-${{ steps.rust.outputs.version }}-windows-baseline-${{ matrix.os }}- - name: Prepare the log directory run: mkdir -p "$RUNNER_TEMP/windows-baseline" + - name: Verify the pinned mutation runner on Windows + if: matrix.os == 'windows-2022' + run: | + cargo install --locked --git https://github.com/cortexkit/commons --rev "$CK_MUTATE_REV" cortexkit-mutate 2>&1 | tee "$RUNNER_TEMP/windows-baseline/install-mutate.log" + ckdev-mutate --version 2>&1 | tee "$RUNNER_TEMP/windows-baseline/mutate-version.log" + ckdev-mutate check 2>&1 | tee "$RUNNER_TEMP/windows-baseline/mutate-check.log" # The Windows launcher's own tests start real confined children, so # they can only run here. The deviations feature adds the test-only # launch variants, one per confinement check. They run first and print From bae5a5dab6b252bd89790f9733e07b7b2d3ea24f Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:32:21 +0200 Subject: [PATCH 2/4] mason: require Windows CI and cover confinement guards Promote both native Windows jobs without dropping existing Linux or macOS gates. Replay Windows controls on windows-2022 and retain indexed mutation witnesses alongside runner reports. Expand manifest tilde roots using the process token's loaded profile, resolving Userenv dynamically from System32 in the parent. Test hostile USERPROFILE in a subprocess. Upgrade only cortexkit-log to 0.3.5, adding its already-resolved cortexkit-lease edge. The old sandbox-page test required the obsolete claim that Windows confinement was unavailable. Replace that claim with assertions for the implemented guarantees, residual inventory, non-claims and placement requirements; retain every Linux/macOS limit assertion. Move the Windows wire format into a variant-specific module without changing its portable encoding so the tag control can run on Windows. --- .github/workflows/check_mutation_coverage.py | 22 +- .github/workflows/ci.yml | 94 +- .github/workflows/test_mutation_coverage.py | 46 +- Cargo.lock | 5 +- Cargo.toml | 2 +- crates/basal-host/src/builtins/fs/windows.rs | 59 +- crates/basal-launch/src/windows/mod.rs | 5 +- crates/basal-launch/src/windows/profile.rs | 72 +- crates/basal-launch/tests/windows_launch.rs | 15 + crates/basal-module/src/pool.rs | 18 +- crates/basal-module/src/windows.rs | 11 + crates/basal-proto/src/wire.rs | 24 +- crates/basal-proto/src/wire/windows.rs | 33 + crates/basal-worker/tests/sandbox_page.rs | 62 +- docs/sandbox.md | 149 ++- mutations.toml | 1149 +++++++++++++++++- script/tests/__init__.py | 1 + script/tests/witness_mutations.py | 85 ++ script/witness-mutations.py | 113 ++ 19 files changed, 1858 insertions(+), 107 deletions(-) create mode 100644 crates/basal-proto/src/wire/windows.rs create mode 100644 script/tests/witness_mutations.py create mode 100644 script/witness-mutations.py diff --git a/.github/workflows/check_mutation_coverage.py b/.github/workflows/check_mutation_coverage.py index db942e9..a450f05 100644 --- a/.github/workflows/check_mutation_coverage.py +++ b/.github/workflows/check_mutation_coverage.py @@ -11,9 +11,10 @@ import tomllib -PLATFORMS = ("linux", "macos") +PLATFORMS = ("linux", "macos", "windows") PORTABLE_PACKAGES = {"basal-proto", "basal-core", "basal-host"} SUCCESS = {"CAUGHT", "CAUGHT_BROADLY", "HUB", "EQUIVALENT", "UNREACHABLE"} +ARTIFACT_PATTERN = re.compile(r"mutations-(linux|macos|windows)-[1-9][0-9]*") def partition(rows): @@ -21,13 +22,18 @@ def partition(rows): selected = {platform: set() for platform in PLATFORMS} seen = set() for row in rows: - hosts = set(row.get("platforms", PLATFORMS)) & set(PLATFORMS) - if len(hosts) != 1: + platforms = row.get("platforms", PLATFORMS) + hosts = set(platforms) & set(PLATFORMS) + if len(platforms) != 1 or len(hosts) != 1: raise ValueError(f"{row['id']}: expected exactly one CI platform, got {sorted(hosts)}") host = hosts.pop() package = row.get("package") - if package in PORTABLE_PACKAGES and host != "linux": - raise ValueError(f"{row['id']}: {package} proofs belong on Linux") + file = Path(row.get("file", "")) + windows_module = "windows" in file.parts or file.name == "windows.rs" + if package in PORTABLE_PACKAGES and host != "linux" and not ( + host == "windows" and windows_module + ): + raise ValueError(f"{row['id']}: {package} proofs belong on Linux unless targeting a Windows-only module on Windows") if row["id"] in seen: raise ValueError(f"duplicate catalogue ID: {row['id']}") seen.add(row["id"]) @@ -55,7 +61,7 @@ def verify_reports(selected, reports): executed[platform][identity] += 1 for platform in PLATFORMS: # Each host shards the entire catalogue; the runner reports the other - # host's rows as skips. Checking both views also detects a missing shard. + # hosts' rows as skips. Checking every view also detects a missing shard. if reported[platform] != Counter({identity: 1 for identity in expected}): raise ValueError(f"{platform}: reports must contain every catalogue ID exactly once") if executed[platform] != Counter({identity: 1 for identity in selected[platform]}): @@ -74,12 +80,12 @@ def main(): if args.reports: reports = [] for path in sorted(args.reports.glob("mutations-*/all.json")): - match = re.fullmatch(r"mutations-(linux|macos)-[1-9][0-9]*", path.parent.name) + match = ARTIFACT_PATTERN.fullmatch(path.parent.name) if not match: raise ValueError(f"unrecognized mutation artifact: {path.parent.name}") reports.append((match[1], json.loads(path.read_text()))) counts = verify_reports(selected, reports) - print(f"Mutation coverage: linux={counts['linux']}, macos={counts['macos']}, " + print(f"Mutation coverage: linux={counts['linux']}, macos={counts['macos']}, windows={counts['windows']}, " f"total={sum(counts.values())} catalogue rows; exactly one platform per row") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a246d11..fef1c92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: if: matrix.platform != 'linux-arm64' id: mutate-cache with: - path: ~/.cargo/bin/ckdev-mutate + path: ~/.cargo/bin/ckdev-mutate${{ runner.os == 'Windows' && '.exe' || '' }} key: ${{ runner.os }}-${{ runner.arch }}-ckdev-mutate-${{ hashFiles('Cargo.lock') }}-${{ steps.rust.outputs.version }}-${{ env.CK_MUTATE_REV }} - name: Install pinned mutation runner if: matrix.platform != 'linux-arm64' && steps.mutate-cache.outputs.cache-hit != 'true' @@ -177,9 +177,12 @@ jobs: mutations-pr: if: github.event_name == 'pull_request' needs: checks + defaults: + run: + shell: bash # Replays the mutation controls whose files this pull request changes. # Every catalogue row is marked for exactly one OS, so running the replay - # on both hosts covers every changed control without running any twice. + # on all three platforms covers every changed control without running any twice. strategy: fail-fast: false matrix: @@ -188,11 +191,17 @@ jobs: os: ubuntu-latest - platform: macos os: macos-latest + - platform: windows + os: windows-2022 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + if: matrix.platform == 'windows' + with: + python-version: '3.12' - name: Install stable Rust run: | rustup toolchain install stable --profile minimal @@ -216,7 +225,7 @@ jobs: - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: mutate-cache with: - path: ~/.cargo/bin/ckdev-mutate + path: ~/.cargo/bin/ckdev-mutate${{ runner.os == 'Windows' && '.exe' || '' }} key: ${{ runner.os }}-${{ runner.arch }}-ckdev-mutate-${{ hashFiles('Cargo.lock') }}-${{ steps.rust.outputs.version }}-${{ env.CK_MUTATE_REV }} - name: Install pinned mutation runner if: steps.mutate-cache.outputs.cache-hit != 'true' @@ -226,10 +235,18 @@ jobs: mkdir -p target/mutations rm -f target/mutations/diff.json env -u BASAL_WORKER_BIN -u BASAL_CUT_EXHAUSTIVE ckdev-mutate run --diff origin/main --report target/mutations/diff.json + - name: Capture exact Windows mutation application and restoration witnesses + if: matrix.platform == 'windows' + run: python script/witness-mutations.py --platform windows --output "$RUNNER_TEMP/windows-mutation-evidence" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() && matrix.platform == 'windows' + with: + name: windows-mutation-evidence-pr + path: ${{ runner.temp }}/windows-mutation-evidence - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: - name: mutations-pr-${{ matrix.platform }} + name: ${{ matrix.platform == 'windows' && 'mutations-windows-1' || format('mutations-pr-{0}', matrix.platform) }} path: target/mutations/diff.json # On a push to main: the full catalogue, each row against its own target. @@ -237,6 +254,9 @@ jobs: mutations-main: if: always() && ((github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.checks.result == 'success') || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && needs.checks.result == 'success')) needs: checks + defaults: + run: + shell: bash # Scheduled breadth runs deliberately skip the redundant checks job. # A push still requires checks to succeed before replaying mutations. # always() lets the scheduled run proceed past that skipped dependency. @@ -245,11 +265,10 @@ jobs: strategy: fail-fast: false matrix: - # Each catalogue row is one mutation control. The 125 rows marked for - # Linux use two shards, about 63 each. The 431 marked for macOS use five, - # about 86 each: five is the most macOS jobs this GitHub account runs at - # once. Each host shards the sorted catalogue on its own, and rows marked - # for the other OS are reported SKIPPED_PLATFORM without being built. + # Each catalogue row is one mutation control. Linux and Windows use + # two shards each; macOS uses five, the most macOS jobs this account + # runs at once. Each host shards the sorted catalogue on its own; + # rows marked for the other platforms are skipped without being built. include: - { platform: linux, os: ubuntu-latest, shard: 1, shards: 2 } - { platform: linux, os: ubuntu-latest, shard: 2, shards: 2 } @@ -258,8 +277,14 @@ jobs: - { platform: macos, os: macos-latest, shard: 3, shards: 5 } - { platform: macos, os: macos-latest, shard: 4, shards: 5 } - { platform: macos, os: macos-latest, shard: 5, shards: 5 } + - { platform: windows, os: windows-2022, shard: 1, shards: 2 } + - { platform: windows, os: windows-2022, shard: 2, shards: 2 } steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + if: matrix.platform == 'windows' + with: + python-version: '3.12' - name: Install stable Rust run: | rustup toolchain install stable --profile minimal @@ -280,7 +305,7 @@ jobs: - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: mutate-cache with: - path: ~/.cargo/bin/ckdev-mutate + path: ~/.cargo/bin/ckdev-mutate${{ runner.os == 'Windows' && '.exe' || '' }} key: ${{ runner.os }}-${{ runner.arch }}-ckdev-mutate-${{ hashFiles('Cargo.lock') }}-${{ steps.rust.outputs.version }}-${{ env.CK_MUTATE_REV }} - name: Install pinned mutation runner if: steps.mutate-cache.outputs.cache-hit != 'true' @@ -292,13 +317,21 @@ jobs: env -u BASAL_WORKER_BIN -u BASAL_CUT_EXHAUSTIVE ckdev-mutate run --all --shard ${{ matrix.shard }}/${{ matrix.shards }} ${BROAD:+--broad} --report target/mutations/all.json env: BROAD: ${{ github.event_name != 'push' && 'true' || '' }} + - name: Capture exact Windows mutation application and restoration witnesses + if: matrix.platform == 'windows' + run: python script/witness-mutations.py --platform windows --shard ${{ matrix.shard }}/${{ matrix.shards }} --output "$RUNNER_TEMP/windows-mutation-evidence" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() && matrix.platform == 'windows' + with: + name: windows-mutation-evidence-${{ matrix.shard }} + path: ${{ runner.temp }}/windows-mutation-evidence - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: mutations-${{ matrix.platform }}-${{ matrix.shard }} path: target/mutations/all.json - # Reads every shard's replay report (target/mutations/all.json) after both + # Reads every shard's replay report (target/mutations/all.json) after all # hosts finish, and requires exactly one successful replay of each # catalogue row across all of them. A row skipped everywhere, missing from # every report or replayed twice fails here, even if each shard passed. @@ -348,14 +381,10 @@ jobs: cargo +1.88.0 check --workspace --all-targets --locked cargo +1.88.0 check --workspace --all-targets --locked --features basal-module/rig-kill-hook - # Windows build baseline. basal does not build on Windows yet, and the causes - # are measured here rather than guessed. The job runs the workspace check and - # tests on windows-latest and records what fails without failing the - # workflow: every build and test step continues on error, the job itself - # continues on error, and no other job depends on it. When a step fails, a - # second run collects the complete list (`--keep-going` for the check, - # `--no-fail-fast` for the tests), since the plain commands stop at the - # first failure. Every log is uploaded as the windows-baseline-logs artifact. + # Each Windows image leaves a different set of loader and thread-pool handles + # in the worker, so both must exercise the startup allowlist. Native suites + # also exercise the launcher, worker probes and filesystem/Git guards before + # the workspace gate. Logs are retained even when a gate fails. windows-baseline: if: github.event_name != 'schedule' strategy: @@ -363,7 +392,6 @@ jobs: matrix: os: [windows-latest, windows-2022] runs-on: ${{ matrix.os }} - continue-on-error: true timeout-minutes: 120 defaults: run: @@ -403,7 +431,6 @@ jobs: # workspace steps below fail. - name: Test the Windows launcher with its test variants id: test-launch - continue-on-error: true timeout-minutes: 20 run: | cargo test -p basal-launch --locked -- --show-output 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-launch-production.log" @@ -416,7 +443,6 @@ jobs: # cannot break from outside. - name: Test the worker on its own id: test-worker - continue-on-error: true timeout-minutes: 30 run: cargo test -p basal-worker --locked --no-fail-fast -- --show-output 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-worker.log" # The creators and counts of production startup handles are measured @@ -424,12 +450,10 @@ jobs: # live handle table with the fixed per-image count limits; the image test # rejects DLL imports absent from the worker's windows-imports.txt file. - name: Witness the startup-handle and import-inventory mutants - continue-on-error: true timeout-minutes: 10 run: python crates/basal-worker/tests/windows_mutation_witness.py "$RUNNER_TEMP/windows-baseline" - name: Test the worker with its test variants id: test-worker-deviations - continue-on-error: true timeout-minutes: 30 run: cargo test -p basal-worker --features deviations --locked --no-fail-fast -- --show-output 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-worker-deviations.log" # The module's tests: workers started, supervised and killed through @@ -440,7 +464,6 @@ jobs: # are basal-core's. - name: Test the module and the core id: test-module - continue-on-error: true timeout-minutes: 40 run: cargo test -p basal-module -p basal-core --locked --no-fail-fast -- --show-output 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-module.log" # Every Windows spawn takes one process-wide lock while its child's @@ -449,56 +472,47 @@ jobs: # stdin, stdout or stderr. - name: Test the shared spawn lock across the launcher and git id: test-spawn-lock - continue-on-error: true timeout-minutes: 20 run: cargo test -p basal-host --lib --locked -- spawn_lock_tests --show-output 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-spawn-lock.log" - name: Test the shared test kit on its own id: test-testkit - continue-on-error: true timeout-minutes: 30 run: cargo test -p basal-testkit --locked --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-testkit.log" - name: Test the integration rig on its own id: test-rig - continue-on-error: true timeout-minutes: 30 run: cargo test -p basal-rig --locked --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-rig.log" - name: Check every workspace target id: check - continue-on-error: true timeout-minutes: 40 run: cargo check --workspace --all-targets --locked 2>&1 | tee "$RUNNER_TEMP/windows-baseline/check.log" - name: Check every workspace target, continuing past failed crates - if: steps.check.outcome == 'failure' - continue-on-error: true + if: failure() && steps.check.outcome == 'failure' timeout-minutes: 40 run: cargo check --workspace --all-targets --locked --keep-going 2>&1 | tee "$RUNNER_TEMP/windows-baseline/check-keep-going.log" - name: Test the workspace id: test - continue-on-error: true timeout-minutes: 40 run: cargo test --workspace --locked 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test.log" - name: Test the workspace, continuing past failed tests - if: steps.test.outcome == 'failure' - continue-on-error: true + if: failure() && steps.test.outcome == 'failure' timeout-minutes: 40 run: cargo test --workspace --locked --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-no-fail-fast.log" - # The workspace test can't build until every crate compiles on Windows, - # so basal-host's own tests run separately to report its built-ins now. + # Separate host suites retain filesystem and Git failure logs so a failing + # built-in can be diagnosed without searching the full workspace log. - name: Test basal-host on its own id: test-host - continue-on-error: true timeout-minutes: 30 run: cargo test -p basal-host --lib --locked --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-host.log" - name: Test the host integration regressions on their own id: test-host-regressions - continue-on-error: true timeout-minutes: 20 run: cargo test -p basal-host --test builtin_host_regressions --locked --no-fail-fast 2>&1 | tee "$RUNNER_TEMP/windows-baseline/test-basal-host-regressions.log" - - name: Summarize the baseline + - name: Summarize the Windows gates if: always() run: | { - echo "## Windows baseline ($(rustc --version))" + echo "## Windows gates ($(rustc --version))" echo echo "- cargo check --workspace --all-targets --locked: ${{ steps.check.outcome }}" echo "- cargo test --workspace --locked: ${{ steps.test.outcome }}" diff --git a/.github/workflows/test_mutation_coverage.py b/.github/workflows/test_mutation_coverage.py index 227d96f..3623da8 100644 --- a/.github/workflows/test_mutation_coverage.py +++ b/.github/workflows/test_mutation_coverage.py @@ -1,10 +1,10 @@ """Tests for check_mutation_coverage.py: every catalogue row is assigned to -exactly one CI host (Linux or macOS), with portable packages kept on Linux, + exactly one CI host, with portable packages on Linux or Windows-only modules on Windows, and the shards' replay reports contain one successful replay of every row.""" import unittest -from check_mutation_coverage import partition, verify_reports +from check_mutation_coverage import ARTIFACT_PATTERN, PLATFORMS, partition, verify_reports class MutationCoverageChecks(unittest.TestCase): @@ -12,20 +12,28 @@ def setUp(self): self.rows = [ {"id": "portable", "package": "basal-host", "platforms": ["linux"]}, {"id": "worker", "package": "basal-worker", "platforms": ["macos"]}, + {"id": "native", "package": "basal-host", "platforms": ["windows"], + "file": "crates/basal-host/src/builtins/fs/windows.rs"}, ] self.reports = [ ("linux", [{"id": "portable", "outcome": "CAUGHT"}, - {"id": "worker", "outcome": "SKIPPED_PLATFORM"}]), + {"id": "worker", "outcome": "SKIPPED_PLATFORM"}, + {"id": "native", "outcome": "SKIPPED_PLATFORM"}]), ("macos", [{"id": "portable", "outcome": "SKIPPED_PLATFORM"}, - {"id": "worker", "outcome": "CAUGHT"}]), + {"id": "worker", "outcome": "CAUGHT"}, + {"id": "native", "outcome": "SKIPPED_PLATFORM"}]), + ("windows", [{"id": "portable", "outcome": "SKIPPED_PLATFORM"}, + {"id": "worker", "outcome": "SKIPPED_PLATFORM"}, + {"id": "native", "outcome": "CAUGHT"}]), ] def test_partition_accepts_one_host_per_row(self): - self.assertEqual(partition(self.rows), {"linux": {"portable"}, "macos": {"worker"}}) + self.assertEqual(partition(self.rows), {"linux": {"portable"}, "macos": {"worker"}, "windows": {"native"}}) def test_partition_rejects_rows_selected_by_no_job_or_both(self): for package in (None, "basal-worker", "basal-module", "basal-testkit", "basal-rig"): - for platforms in ([], ["windows"], ["linux", "macos"], None): + for platforms in ([], ["unknown"], ["linux", "macos"], ["linux", "windows"], + ["macos", "windows"], ["windows", "unknown"], ["windows", "windows"], None): with self.subTest(package=package, platforms=platforms): row = {"id": "unassigned", "runner": "command", "package": package} if platforms is not None: @@ -35,10 +43,10 @@ def test_partition_rejects_rows_selected_by_no_job_or_both(self): def test_partition_accepts_worker_packages_on_either_host(self): for package in ("basal-worker", "basal-module", "basal-testkit", "basal-rig"): - for host in ("linux", "macos"): + for host in PLATFORMS: with self.subTest(package=package, host=host): row = {"id": "worker", "package": package, "platforms": [host]} - expected = {"linux": set(), "macos": set()} + expected = {platform: set() for platform in PLATFORMS} expected[host] = {"worker"} self.assertEqual(partition([row]), expected) @@ -46,11 +54,29 @@ def test_partition_keeps_portable_packages_on_linux(self): for package in ("basal-proto", "basal-core", "basal-host"): with self.subTest(package=package): row = {"id": "portable", "package": package, "platforms": ["linux"]} - self.assertEqual(partition([row]), {"linux": {"portable"}, "macos": set()}) + self.assertEqual(partition([row]), {"linux": {"portable"}, "macos": set(), "windows": set()}) row["platforms"] = ["macos"] with self.assertRaisesRegex(ValueError, "belong on Linux"): partition([row]) + def test_windows_portable_rows_require_a_windows_only_module(self): + for package in ("basal-proto", "basal-core", "basal-host"): + for file in ("src/windows.rs", "src/windows/guard.rs", "src/nested/windows/check.rs"): + with self.subTest(package=package, file=file): + row = {"id": "native", "package": package, "file": file, "platforms": ["windows"]} + self.assertEqual(partition([row]), {"linux": set(), "macos": set(), "windows": {"native"}}) + for file in ("", "src/lib.rs", "src/notwindows/guard.rs", "src/windows_guard.rs", "src/windows.rs.bak"): + with self.subTest(package=package, file=file): + row = {"id": "shared", "package": package, "file": file, "platforms": ["windows"]} + with self.assertRaisesRegex(ValueError, "Windows-only module"): + partition([row]) + + def test_artifact_names_accept_windows_shards_but_not_pr_or_unknown_hosts(self): + for name in ("mutations-linux-1", "mutations-macos-5", "mutations-windows-1", "mutations-windows-12"): + self.assertIsNotNone(ARTIFACT_PATTERN.fullmatch(name), name) + for name in ("mutations-pr-windows", "mutations-windows-0", "mutations-other-1", "mutations-windows-one"): + self.assertIsNone(ARTIFACT_PATTERN.fullmatch(name), name) + def test_partition_rejects_duplicate_ids(self): for duplicate in (self.rows[0], {"id": "portable", "platforms": ["macos"]}): with self.subTest(duplicate=duplicate): @@ -59,7 +85,7 @@ def test_partition_rejects_duplicate_ids(self): def test_reports_count_replayed_rows_not_platform_skips(self): self.assertEqual(verify_reports(partition(self.rows), self.reports), - {"linux": 1, "macos": 1}) + {"linux": 1, "macos": 1, "windows": 1}) def test_reports_reject_missing_shard(self): with self.assertRaisesRegex(ValueError, "every catalogue ID exactly once"): diff --git a/Cargo.lock b/Cargo.lock index dabab69..e6ff0d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,11 +309,12 @@ dependencies = [ [[package]] name = "cortexkit-log" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceae091889f52b64ef218ca24346fd18ed14d24053396d7a8cad031a78c430e7" +checksum = "0c237e3dbd3b9bc5b7bd77e4899c660d8f5773192a0b98a35bab9f68307feffe" dependencies = [ "chrono", + "cortexkit-lease", "cortexkit-store-types", "regex", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 9f8e72c..1c2addd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ async-trait = "0.1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } # The fleet's log format and retention; the module logs through it as # entorhinal does. -cortexkit-log = "0.3.3" +cortexkit-log = "0.3.5" tracing = "0.1" # net.fetch, the network built-in. basal writes its own small HTTP/1.1 # client over rustls so it controls every step the safety rules name: diff --git a/crates/basal-host/src/builtins/fs/windows.rs b/crates/basal-host/src/builtins/fs/windows.rs index bf43b82..c1549b1 100644 --- a/crates/basal-host/src/builtins/fs/windows.rs +++ b/crates/basal-host/src/builtins/fs/windows.rs @@ -999,6 +999,35 @@ fn failure_rank(d: &Denial) -> u8 { } } +/// Resolve the profile of the process token, not a caller-controlled +/// environment variable. A missing or unreadable profile grants no root. +#[cfg(windows)] +fn process_profile_directory() -> Result { + basal_launch::process_profile_directory().map_err(|error| Denial::new(codes::IO, error)) +} + +#[cfg(windows)] +fn expand_root(root: &str) -> Result, Denial> { + let suffix = if root == "~" { + "" + } else if let Some(suffix) = root.strip_prefix("~/") { + suffix + } else { + return Ok(std::borrow::Cow::Borrowed(root)); + }; + let mut profile = process_profile_directory()?; + validate_raw_spelling(&profile)?; + if !suffix.is_empty() { + if !profile.ends_with('\\') { + profile.push('\\'); + } + // Preserve components, including refused spellings, until validation. + profile.push_str(&suffix.replace('/', "\\")); + } + validate_raw_spelling(&profile)?; + Ok(std::borrow::Cow::Owned(profile)) +} + /// Finds a manifest root that `path` lies under and walks it /// ([`walk_from_volume_root`]). A file root grants only its own path. When /// every root `path` lies under fails to walk, the most telling failure is @@ -1011,10 +1040,11 @@ fn select_root(path: &str, roots: &[String], share: u32) -> Result = None; for r in roots { - if validate_raw_spelling(r).is_err() || !path_under(path, r) { + let r = expand_root(r)?; + if validate_raw_spelling(&r).is_err() || !path_under(path, &r) { continue; } - match walk_from_volume_root(r, share) { + match walk_from_volume_root(&r, share) { Ok(vr) if vr.is_directory || path == vr.manifest => return Ok(vr), Ok(_) => {} Err(d) => { @@ -2218,8 +2248,12 @@ mod tests { impl WinTree { fn new(tag: &str) -> Self { + Self::new_under(tag, &std::env::temp_dir()) + } + + fn new_under(tag: &str, parent: &Path) -> Self { static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); - let base = std::env::temp_dir().join(format!( + let base = parent.join(format!( "basal-win-tree-{tag}-{}-{}", std::process::id(), SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed) @@ -2262,6 +2296,25 @@ mod tests { } } + #[test] + fn tilde_root_reads_below_the_process_tokens_profile() { + let profile = process_profile_directory().expect("process token profile"); + let tree = WinTree::new_under("profile", Path::new(&profile)); + let name = tree.base.file_name().unwrap().to_str().unwrap(); + let roots = vec![format!("~/{name}/root")]; + assert_eq!( + read(&tree.p("sub\\b.txt"), &roots, 100).unwrap()["text"], + "nested" + ); + for root in [ + format!("~/{name}/root/.."), + format!("~/{name}//root"), + format!("~/{name}/root:stream"), + ] { + assert!(read(&tree.p("sub\\b.txt"), &[root], 100).is_err()); + } + } + fn wide(s: &str) -> Vec { s.encode_utf16().chain(Some(0)).collect() } diff --git a/crates/basal-launch/src/windows/mod.rs b/crates/basal-launch/src/windows/mod.rs index bd2e223..5ff915e 100644 --- a/crates/basal-launch/src/windows/mod.rs +++ b/crates/basal-launch/src/windows/mod.rs @@ -23,7 +23,10 @@ pub use launch::{ }; pub use plain::{PlainCommand, PlainStdio, spawn_plain}; pub use process::{ConfinedProcess, KILL_EXIT_CODE, OwnedProcess}; -pub use profile::{PROFILE_NAME, PackageSid, create_or_open_profile, grant_test_binary_directory}; +pub use profile::{ + PROFILE_NAME, PackageSid, create_or_open_profile, grant_test_binary_directory, + process_profile_directory, +}; pub use spawn_lock::{SpawnLock, make_inheritable, spawn_lock, spawn_lock_held}; pub use token::{ IMPERSONATION_LEVEL, LOW_INTEGRITY, NULL_SID, TOKEN_IMPERSONATION, TOKEN_PRIMARY, TokenFacts, diff --git a/crates/basal-launch/src/windows/profile.rs b/crates/basal-launch/src/windows/profile.rs index 1f6261c..fbbf98b 100644 --- a/crates/basal-launch/src/windows/profile.rs +++ b/crates/basal-launch/src/windows/profile.rs @@ -1,5 +1,5 @@ -//! The AppContainer profile every worker shares, and the test-only grant on -//! the directory of a worker binary. +//! The AppContainer profile every worker shares, the parent's loaded user +//! profile, and the test-only grant on the directory of a worker binary. use super::error::{LaunchError, Refusal}; use super::native::{Result, SidBuf, check, last, owned, wide}; @@ -9,7 +9,7 @@ use std::path::Path; use std::ptr::{null, null_mut}; use std::sync::Mutex; use windows_sys::Win32::Foundation::{ - FreeLibrary, HMODULE, LocalFree, WAIT_ABANDONED, WAIT_OBJECT_0, + FreeLibrary, HANDLE, HMODULE, LocalFree, WAIT_ABANDONED, WAIT_OBJECT_0, }; use windows_sys::Win32::Security::Authorization::{ EXPLICIT_ACCESS_W, GRANT_ACCESS, GetNamedSecurityInfoW, NO_MULTIPLE_TRUSTEE, SE_FILE_OBJECT, @@ -17,14 +17,14 @@ use windows_sys::Win32::Security::Authorization::{ }; use windows_sys::Win32::Security::{ DACL_SECURITY_INFORMATION, FreeSid, PSID, SID_AND_ATTRIBUTES, - SUB_CONTAINERS_AND_OBJECTS_INHERIT, + SUB_CONTAINERS_AND_OBJECTS_INHERIT, TOKEN_QUERY, }; use windows_sys::Win32::Storage::FileSystem::{FILE_GENERIC_EXECUTE, FILE_GENERIC_READ}; use windows_sys::Win32::System::LibraryLoader::{ GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExW, }; use windows_sys::Win32::System::Threading::{ - CreateMutexW, INFINITE, ReleaseMutex, WaitForSingleObject, + CreateMutexW, GetCurrentProcess, INFINITE, OpenProcessToken, ReleaseMutex, WaitForSingleObject, }; /// The name of the one AppContainer profile all basal workers run under. @@ -47,6 +47,40 @@ type CreateProfile = unsafe extern "system" fn( *mut PSID, ) -> i32; type DeriveSid = unsafe extern "system" fn(*const u16, *mut PSID) -> i32; +type UserProfileDirectory = unsafe extern "system" fn(HANDLE, *mut u16, *mut u32) -> i32; + +/// Reads the loaded profile folder from the process's primary token, never +/// from environment variables or a thread's impersonation token. Userenv is +/// resolved from System32 only in the parent, not imported into worker images. +pub fn process_profile_directory() -> Result { + let mut token = null_mut(); + // SAFETY: the process pseudo-handle is valid and token is writable. + check( + unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) }, + "OpenProcessToken(profile)", + )?; + let token = owned(token, "OpenProcessToken(profile)")?; + let userenv = Library::system32("userenv.dll")?; + // SAFETY: the function type matches the documented Userenv export. + let directory: UserProfileDirectory = unsafe { userenv.symbol(b"GetUserProfileDirectoryW\0")? }; + let mut size = 0; + // SAFETY: a null buffer asks for the size; the owned token stays open. + unsafe { directory(token.as_raw_handle(), null_mut(), &mut size) }; + if std::io::Error::last_os_error().raw_os_error() != Some(122) || size <= 1 || size > 32768 { + return Err(last("GetUserProfileDirectoryW(size)")); + } + let mut buffer = vec![0u16; size as usize]; + // SAFETY: the buffer has the requested capacity and the token is live. + check( + unsafe { directory(token.as_raw_handle(), buffer.as_mut_ptr(), &mut size) }, + "GetUserProfileDirectoryW", + )?; + let end = buffer + .iter() + .position(|unit| *unit == 0) + .ok_or_else(|| "user profile path is not terminated".to_owned())?; + String::from_utf16(&buffer[..end]).map_err(|_| "user profile path is not Unicode".to_owned()) +} /// The package SID of an AppContainer profile. #[derive(Clone, PartialEq, Eq)] @@ -306,6 +340,34 @@ impl Drop for SessionMutex { #[cfg(test)] mod tests { use super::*; + + #[test] + fn the_process_token_profile_ignores_userprofile_environment() { + const MARKER: &str = "BASAL_PROFILE_ENV_CONTROL"; + if let Some(fake) = std::env::var_os(MARKER) { + let fake = fake.to_str().expect("fixture profile is Unicode"); + let actual = process_profile_directory().expect("loaded process token profile"); + assert_ne!(actual.to_lowercase(), fake.to_lowercase()); + return; + } + // A subprocess changes only its own environment, so concurrent profile + // and launcher tests never see the hostile USERPROFILE value. + let fake = std::env::temp_dir().join(format!("basal-fake-profile-{}", std::process::id())); + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "windows::profile::tests::the_process_token_profile_ignores_userprofile_environment", "--nocapture"]) + .env(MARKER, &fake) + .env("USERPROFILE", &fake) + .output().expect("run profile environment control"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stdout) + ); + assert!( + String::from_utf8_lossy(&output.stdout).contains("1 passed"), + "the profile control did not execute" + ); + } use std::sync::{Arc, Barrier}; /// A profile used only by the race test below, so deleting it cannot diff --git a/crates/basal-launch/tests/windows_launch.rs b/crates/basal-launch/tests/windows_launch.rs index 5bed4af..640ac4f 100644 --- a/crates/basal-launch/tests/windows_launch.rs +++ b/crates/basal-launch/tests/windows_launch.rs @@ -195,6 +195,21 @@ mod deviations { use super::*; use basal_launch::Refusal; + #[test] + fn a_confined_child_is_created_under_the_shared_spawn_lock() { + let mut options = LaunchOptions::new(placed_child(), COMMIT_BYTES); + options.before_resume = Some(|_, _| { + assert!( + basal_launch::spawn_lock_held(), + "the child's pipe ends are still inheritable before resume" + ); + Ok(()) + }); + let child = launch(&options).expect("launch under the shared spawn lock"); + assert!(!basal_launch::spawn_lock_held()); + kill_and_reap(&child); + } + fn assert_refused(deviation: Deviation, refusal: Refusal) { assert_eq!(deviation.parent_refusal(), Some(refusal)); let options = LaunchOptions::new(placed_child(), COMMIT_BYTES).deviation(deviation); diff --git a/crates/basal-module/src/pool.rs b/crates/basal-module/src/pool.rs index e1f2f92..b948880 100644 --- a/crates/basal-module/src/pool.rs +++ b/crates/basal-module/src/pool.rs @@ -259,7 +259,23 @@ fn accepts_welcome(welcome: &Welcome, landlock: LandlockPolicy) -> bool { no_thread_token, mitigations, handle_table, - } => cfg!(windows) && lpac && untrusted && no_thread_token && mitigations && handle_table, + } => { + #[cfg(windows)] + { + crate::windows::accepts_report( + lpac, + untrusted, + no_thread_token, + mitigations, + handle_table, + ) + } + #[cfg(not(windows))] + { + let _ = (lpac, untrusted, no_thread_token, mitigations, handle_table); + false + } + } Confinement::None => false, } } diff --git a/crates/basal-module/src/windows.rs b/crates/basal-module/src/windows.rs index ff5e290..060111c 100644 --- a/crates/basal-module/src/windows.rs +++ b/crates/basal-module/src/windows.rs @@ -17,6 +17,17 @@ use basal_proto::{CODEMODE_JOB_COMMIT_BYTES, FLOW_JOB_COMMIT_BYTES}; use crate::process::SpawnError; +/// A Windows worker must report every startup check, not just its token kind. +pub(crate) fn accepts_report( + lpac: bool, + untrusted: bool, + no_thread_token: bool, + mitigations: bool, + handle_table: bool, +) -> bool { + lpac && untrusted && no_thread_token && mitigations && handle_table +} + /// `STATUS_ACCESS_VIOLATION`: the worker touched memory it may not, for /// example code the dynamic-code policy refused to make executable. pub const STATUS_ACCESS_VIOLATION: u32 = 0xC000_0005; diff --git a/crates/basal-proto/src/wire.rs b/crates/basal-proto/src/wire.rs index 78c45a5..16636e3 100644 --- a/crates/basal-proto/src/wire.rs +++ b/crates/basal-proto/src/wire.rs @@ -6,6 +6,8 @@ use crate::limits::{ }; use crate::types::*; +mod windows; + // Parent and worker tags live in disjoint ranges, so a frame sent in the // wrong direction is refused as an unknown tag instead of being misread. const TAG_HELLO: u8 = 1; @@ -663,12 +665,14 @@ pub(crate) fn encode_worker(enc: &mut Encoder, m: &WorkerMessage) { mitigations, handle_table, } => { - enc.u8(3); - enc.u8(u8::from(lpac)); - enc.u8(u8::from(untrusted)); - enc.u8(u8::from(no_thread_token)); - enc.u8(u8::from(mitigations)); - enc.u8(u8::from(handle_table)); + windows::encode( + enc, + lpac, + untrusted, + no_thread_token, + mitigations, + handle_table, + ); } } } @@ -724,13 +728,7 @@ pub(crate) fn decode_worker(dec: &mut Decoder<'_>) -> Result Confinement::Windows { - lpac: dec.bool("lpac")?, - untrusted: dec.bool("untrusted")?, - no_thread_token: dec.bool("no_thread_token")?, - mitigations: dec.bool("mitigations")?, - handle_table: dec.bool("handle_table")?, - }, + windows::TAG => windows::decode(dec)?, tag => { return Err(DecodeError::UnknownTag { field: "confinement", diff --git a/crates/basal-proto/src/wire/windows.rs b/crates/basal-proto/src/wire/windows.rs new file mode 100644 index 0000000..050b196 --- /dev/null +++ b/crates/basal-proto/src/wire/windows.rs @@ -0,0 +1,33 @@ +//! The Windows confinement report has its own tag and five ordered booleans. +//! It is encoded on every platform so journals and protocol fixtures stay portable. + +use crate::codec::{DecodeError, Decoder, Encoder}; +use crate::types::Confinement; + +pub(super) const TAG: u8 = 3; + +pub(super) fn encode( + enc: &mut Encoder, + lpac: bool, + untrusted: bool, + no_thread_token: bool, + mitigations: bool, + handle_table: bool, +) { + enc.u8(TAG); + enc.u8(u8::from(lpac)); + enc.u8(u8::from(untrusted)); + enc.u8(u8::from(no_thread_token)); + enc.u8(u8::from(mitigations)); + enc.u8(u8::from(handle_table)); +} + +pub(super) fn decode(dec: &mut Decoder<'_>) -> Result { + Ok(Confinement::Windows { + lpac: dec.bool("lpac")?, + untrusted: dec.bool("untrusted")?, + no_thread_token: dec.bool("no_thread_token")?, + mitigations: dec.bool("mitigations")?, + handle_table: dec.bool("handle_table")?, + }) +} diff --git a/crates/basal-worker/tests/sandbox_page.rs b/crates/basal-worker/tests/sandbox_page.rs index 8a07472..5f5540a 100644 --- a/crates/basal-worker/tests/sandbox_page.rs +++ b/crates/basal-worker/tests/sandbox_page.rs @@ -96,9 +96,61 @@ fn a_caught_memory_or_stack_failure_does_not_retire_the_worker() { } #[test] -fn windows_section_says_confinement_is_not_provided_yet() { - assert_mentions( - §ion("Windows"), - "Windows confinement is not provided yet", - ); +fn windows_section_states_the_layers_residual_and_limits() { + let windows = section("Windows"); + for topic in [ + "LPAC", + "Untrusted", + "zero capabilities", + "deny-only", + "Before resume", + "committed memory", + "handle list", + "all five confinement fields true", + "26 handles", + "windows-latest", + "windows-2022", + "**25**", + "windows-handle-provenance.md", + "one image profile", + "self-escalation", + "File stdin", + "File stdout/stderr", + "Directory", + "Event", + "IoCompletion", + "TpWorkerFactory", + "IRTimer", + "WaitCompletionPacket", + "Semaphore", + "SchedulerSharedData", + "0x000f0001", + "private, unnamed", + "AFD", + "KnownDlls-relative section opens", + "Native API probing", + "weaker token", + "profile", + "scheduler-tick resolution", + "wall deadline", + "idle retirement", + "loaded user profile", + "install-directory", + "AppContainer SID", + "SUBC", + "ck setup", + "ck upgrade", + ] { + assert_mentions(&windows, topic); + } + assert!(!windows.contains("not provided yet")); + let differences = section("Differences between the systems"); + for topic in [ + "| Windows |", + "allowed inside the worker", + "mounts and in-root links not traversed", + "fatal rather than an error", + ] { + assert_mentions(&differences, topic); + } } diff --git a/docs/sandbox.md b/docs/sandbox.md index 1745143..ff1f63b 100644 --- a/docs/sandbox.md +++ b/docs/sandbox.md @@ -106,17 +106,20 @@ published yet. ## Differences between the systems -| | Linux | macOS | -| --- | --- | --- | -| Boundary | seccomp allowlist, fatal SIGSYS | Seatbelt profile, denied calls fail | -| Second layer | Landlock | none | -| Threads | blocked by seccomp | not blocked | -| Ptrace and memory reads | blocked by non-dumpable | blocked by the hardened runtime | -| Privacy grants | not applicable | launched detached from the module's privacy grants | -| Health counter | SIGSYS deaths | none | +| | Linux | macOS | Windows | +| --- | --- | --- | --- | +| Boundary | seccomp allowlist, fatal SIGSYS | Seatbelt profile, denied calls fail | Untrusted LPAC restricted token, job and process mitigations | +| Second layer | Landlock | none | startup token, mitigation and handle attestation | +| Threads | blocked by seccomp | not blocked | allowed inside the worker | +| Ptrace and memory reads | blocked by non-dumpable | blocked by the hardened runtime | worker cannot open another process for query or memory reads | +| Privacy grants | not applicable | launched detached from the module's privacy grants | no capabilities; environment cleared except SystemRoot | +| Health counter | SIGSYS deaths | none | `fatal_status_deaths` for invalid-handle and access-violation exits | +| Host fs mounts and links | mounts traversed; in-root links allowed | mounts traversed; in-root links allowed | mounts and in-root links not traversed; every reparse point refused, including stat | +| Strict-handle misuse | not applicable | not applicable | fatal `0xc0000008`, not a returned error | A denied operation on Linux kills the worker; on macOS the call fails and the -engine keeps running. +engine keeps running. On Windows most denied operations return an error, but +strict-handle misuse is fatal rather than an error the engine can catch. ### CPU bound of a pooled worker @@ -127,7 +130,7 @@ flow and reused for that flow's later activations until it is retired after 256 activations or after sitting idle for 10 minutes. On Linux the engine thread is the worker's only thread, so the CPU budget -covers all of the worker's CPU use. On macOS a subverted engine could start +covers all of the worker's CPU use. On macOS and Windows a subverted engine could start another thread whose CPU the engine thread's clock does not count, and that thread could keep running after its activation returns. The CPU such a pooled worker can use is therefore bounded only by time: the wall-clock deadline @@ -146,6 +149,126 @@ normally, the worker is reused for that flow. ## Windows -Windows confinement is not provided yet. The worker has no Windows sandbox, -and the module refuses any worker whose `Welcome` reports no confinement, so -no flow runs there. +### Layers and guarantees + +The native launcher creates a GUI-subsystem worker suspended, with a +Less Privileged AppContainer (LPAC) primary token for the fixed profile +`cortexkit.basal.worker`, zero capabilities, no privileges, NULL-only +restricting SIDs, and every access group deny-only. The primary is Low at birth +so the loader can initialize. A matching Low impersonation token on the initial +thread is used only during startup. The environment is cleared except +`SystemRoot`; an explicit handle list passes only the three stdio pipes. + +Before resume, the parent checks the actual suspended primary and initial +thread tokens, the owned job's limits and the child's membership in that job. +The job has kill-on-close, no breakaway, an active-process limit of one, and all +UI restrictions. Its process memory limit bounds **committed memory**, not +address space as Linux's `RLIMIT_AS` does. The flow and codemode profiles have +separate commit limits with headroom above their JavaScript heap budgets. +Dropping or killing a worker terminates its job and every thread in the process. + +Before reading its first frame, the worker reverts the thread token, checks that +none remains, lowers its actual primary to Untrusted, and closes all Advanced +Local Procedure Call (ALPC) ports +and non-inheritable non-stdio File handles (including KsecDD on Server 2022). +It attests the actual token, required process mitigations and the handle table. +A failed check exits 70 with a named reason, never a weaker sandbox. The parent +accepts only a Windows `Welcome` with all five confinement fields true. Its +`fatal_status_deaths` health counter counts worker exits `0xc0000008` (invalid +handle) and `0xc0000005` (access violation), not refusal exit 70 or an intentional +parent kill. + +The mitigations prohibit dynamic code, child-process creation, Win32k system +calls and extension points; require Microsoft-signed images; reject remote and +low-label images; prefer System32 when loading images; and permanently enable +strict handle checks. +Thread opt-out and remote downgrade of dynamic-code policy are forbidden. + +A confined worker can read stdin, write stdout and stderr, allocate private +memory, read clocks, create threads inside itself and exit. The measured probes +deny file data and metadata opens, package-folder creates and native package-hive +writes, new IPC, process creation, access to another process, and network access +through Winsock, including loopback. Host calls remain the only authorized way +for a flow to affect the outside world: the parent checks their manifest grants. + +### Residual handles and production measurements + +Windows needs runtime handles beyond stdio. The startup allowlist fits the +entire inventory to **one image profile**: either the windows-latest ceilings +or the windows-2022 ceilings in the table below, never their union. +Stdio and `\KnownDlls` must be present at exactly their counts; all other rows +are ceilings. Unknown types, access masks, extra inheritable handles and excess +counts are refused. Each profile permits at most **26 handles**: + +| Type | Access | windows-latest ceiling | windows-2022 ceiling | windows-latest production | windows-2022 production | +| --- | --- | ---: | ---: | ---: | ---: | +| File stdin | `0x120189` | 1 (exact) | 1 (exact) | 1 | 1 | +| File stdout/stderr | `0x120196` | 2 (exact) | 2 (exact) | 2 | 2 | +| Directory `\KnownDlls` | `0x3` | 1 (exact) | 1 (exact) | 1 | 1 | +| Event | `0x1f0003` | ≤7 | ≤7 | 6 | 6 | +| IoCompletion | `0x1f0003` | ≤2 | ≤2 | 2 | 2 | +| TpWorkerFactory | `0xf00ff` | ≤2 | ≤2 | 2 | 2 | +| IRTimer | `0x100002` | ≤4 | ≤4 | 4 | 4 | +| WaitCompletionPacket | `0x1` | ≤6 | ≤5 | 6 | 5 | +| Semaphore | `0x100003` | 0 | ≤2 | 0 | 2 | +| SchedulerSharedData | `0x1` | ≤1 | 0 | 1 | 0 | +| **Total** | | **≤26** | **≤26** | **25** | **25** | + +The production counts and creator chains are committed in +[`windows-handle-provenance.md`](../crates/basal-worker/tests/data/windows-handle-provenance.md). +CI measurements counted 25 pre-input handles per production release worker +and traced their creators in a production build without test-only options that +can weaken the worker's startup checks. The images were `win25-vs2026` version +`20260925.250.1` and `win22` version `20261004.326.1`. An image change requires +remeasurement, not an automatic widening of the allowlist. Production used six +Event handles per image, one below each seven-Event ceiling. + +`\KnownDlls` is shared and read-only (query and traverse). Every other residual +object beyond stdio is unnamed and was private to the worker in the measured +snapshots. Their access masks are **not all read-only**: events, queues, timers, +wait packets and semaphores carry process-local runtime authority. Closing the +pool's completion and synchronization handles crashes real pool work, so they +must remain open. + +On images with SchedulerSharedData, the worker can duplicate its `0x1` handle +with `GENERIC_ALL` and obtain `0x000f0001`, the type's full rights. This +**self-escalation** affects its own private, unnamed scheduler object; no other +process held that object in the measurement. It is not a file, network, peer or +persistent-state capability. Closing it crashes the worker when the pool runs. + +### Non-claims and profile reuse + +These measurements do not establish denial of direct native access to the AFD +network device bypassing Winsock, or of KnownDlls-relative section opens. +Native API probing covered a finite set of operations, not every operation +against every residual handle type. +The CSR ALPC port is closed before input; whether it would accept messages is +not part of the residual claim. An unnamed handle is not, by itself, proof of +harmlessness; the inventory, creator chains and finite probes delimit the claim. + +All workers reuse the `cortexkit.basal.worker` AppContainer profile. Profile +reuse is a state channel for a **weaker token** that can create package files or +write the package hive. The full token denies both operations; there is no +per-worker profile and no claim that LPAC alone gives the full boundary. + +### Clock and thread bounds + +The engine thread's CPU clock uses `GetThreadTimes`, kernel plus user time, with +scheduler-tick resolution. A sub-tick CPU budget is imprecise; the wall deadline +is the hard bound. Small budgets are not refused. A subverted engine can use +additional threads whose CPU is not counted by that clock. For a pooled worker, +those threads are bounded by the activation wall deadline plus idle retirement, +not by the engine-thread CPU budget. Killing or retiring the worker ends every +thread. Pooled workers are retired after 256 activations or 10 minutes idle; +extra threads cannot outlive that retirement or a kill at the wall deadline. + +### Production placement requirements + +Windows confinement is implemented, but production placement also needs SUBC, +the module-hosting daemon, to support Windows placement, an install-directory +read/execute ACE for basal's +AppContainer SID, and a deployment account with a loaded user profile. The +launcher does not add that install ACE in production; test harnesses grant it +only on their worker binary's directory. Store and log files must retain their +owner-only Windows DACLs. Users install with `ck setup` and update with +`ck upgrade`; `script/stage.sh` is not a Windows or Linux installer. diff --git a/mutations.toml b/mutations.toml index 4510d0a..293d50b 100644 --- a/mutations.toml +++ b/mutations.toml @@ -12,11 +12,10 @@ # Build and test deadlines are separate: 3600 s for loaded-host builds, # 600 s for test binaries unless a row extends the bound for many stalled tests. # Reports belong in target/mutations/. -# Each proof has one CI host. Worker-using packages run on macOS (Seatbelt) -# or Linux (seccomp and Landlock); rows editing Linux-only confinement code -# run on Linux. Portable crates and offline command fixtures run only on Linux -# to avoid replaying them twice. Offline signing/debugger probes replace macOS -# tools with fixtures rather than invoking codesign or lldb. +# Each proof has one CI host. Worker-using packages run on macOS (Seatbelt), +# Linux (seccomp and Landlock), or Windows (the native launcher). Portable +# code runs on Linux; Windows-only modules run on Windows. Offline command +# fixtures run on Linux rather than invoking macOS signing/debugger tools. # worker: 32 controls [[control]] @@ -14233,3 +14232,1143 @@ only = true file = "crates/basal-core/src/codemode/supervisor.rs" old = 'if let Some(description) = run.description { value["description"] = description.into(); }' new = 'value["description"] = run.description.into(); // NON-VACUITY BREAK: null when absent' + +# Drive-path grammar, executable suffix handling and raw Unix name encoding +# manipulate strings/bytes only, so their controls need no Windows APIs. +[[control]] +id = "windows-manifest-root-skips-raw-spelling" +guards = "Windows manifest roots refuse unsafe raw spellings before installation" +test_file = "crates/basal-core/src/manifest.rs" +runner = "cargo" +package = "basal-core" +platforms = ["linux"] +target = "--lib" +expect_red = ["manifest::tests::windows_roots_are_the_fs_built_ins_drive_paths_or_under_the_profile"] +only = true +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-core/src/manifest.rs" +old = "basal_host::builtins::fs::windows::validate_raw_spelling(&spelling)\n .map_err(|denial| invalid(field, format!(\"{root:?}: {}\", denial.message)))" +new = "let _ = (field, spelling); Ok(()) // NON-VACUITY BREAK" + +[[control]] +id = "windows-sibling-worker-loses-exe-suffix" +guards = "Worker executable naming preserves a trailing case-insensitive exe suffix" +test_file = "crates/basal-module/src/pool.rs" +runner = "cargo" +package = "basal-module" +platforms = ["linux"] +target = "--lib" +expect_red = ["pool::names::worker_file_name_follows_parent_in_production_and_development"] +only = true +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-module/src/pool.rs" +old = "name.push(\".exe\");" +new = "let _ = &name; // NON-VACUITY BREAK" + +[[control]] +id = "retention-unix-names-become-lossy" +guards = "Retention preserves raw Unix names already stored in temporary-file records" +test_file = "crates/basal-core/src/retention.rs" +runner = "cargo" +package = "basal-core" +platforms = ["linux"] +target = "--lib" +expect_red = ["retention::os_bytes_tests::unix_names_are_stored_as_their_raw_bytes"] +only = true +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-core/src/retention.rs" +old = "value.as_bytes().to_vec()" +new = "value.to_string_lossy().as_bytes().to_vec() // NON-VACUITY BREAK" + +# Windows startup refuses unsafe tokens and checks the loader/thread-pool +# handles that must remain open after its pre-input cleanup. + +[[control]] +id = "windows-worker-skips-thread-token-present" +guards = "Worker refuses startup with thread-token-present before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +features = ["deviations"] +expect_red = ["a_kept_start_up_token_is_refused_as_thread_token_present"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/startup.rs" +old = "drop_thread_token(arguments.keeps_thread_token())?;" +new = "let _ = drop_thread_token(arguments.keeps_thread_token()); // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-skips-integrity-lower-failed" +guards = "Worker refuses startup with integrity-lower-failed before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +features = ["deviations"] +expect_red = ["a_failed_lowering_is_refused_as_integrity_lower_failed"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/startup.rs" +old = "native::set_untrusted(token.0).map_err(refuse)?;" +new = "let _ = native::set_untrusted(token.0); // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-skips-not-lpac" +guards = "Worker refuses startup with not-lpac before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +expect_red = ["a_primary_without_the_lpac_claim_is_refused_as_not_lpac"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/attest.rs" +old = "if !matches!(claim, Claim::Unsigned(values) if values.as_slice() == [1]) {" +new = "if false && !matches!(claim, Claim::Unsigned(values) if values.as_slice() == [1]) { // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-skips-capabilities-present" +guards = "Worker refuses startup with capabilities-present before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +expect_red = ["a_primary_with_a_capability_is_refused_as_capabilities_present"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/attest.rs" +old = "if !capabilities.is_empty() {" +new = "if false && !capabilities.is_empty() { // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-skips-restricting-sid-mismatch" +guards = "Worker refuses startup with restricting-sid-mismatch before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +expect_red = ["a_second_restricting_sid_is_refused_as_restricting_sid_mismatch"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/attest.rs" +old = "if sids.as_slice() != [NULL_SID] {" +new = "if false && sids.as_slice() != [NULL_SID] { // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-skips-group-not-deny-only" +guards = "Worker refuses startup with group-not-deny-only before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +expect_red = ["an_enabled_logon_group_is_refused_as_group_not_deny_only"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/attest.rs" +old = "if !enabled.is_empty() {" +new = "if false && !enabled.is_empty() { // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-skips-privileges-present" +guards = "Worker refuses startup with privileges-present before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +expect_red = ["a_primary_with_privileges_is_refused_as_privileges_present"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/attest.rs" +old = "if privileges != 0 {" +new = "if false && privileges != 0 { // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-skips-integrity-not-untrusted" +guards = "Worker refuses startup with integrity-not-untrusted before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +features = ["deviations"] +expect_red = ["a_skipped_lowering_is_refused_as_integrity_not_untrusted"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/attest.rs" +old = "if integrity != UNTRUSTED_INTEGRITY {" +new = "if false && integrity != UNTRUSTED_INTEGRITY { // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-skips-mitigation-mismatch" +guards = "Worker refuses startup with mitigation-mismatch before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +expect_red = ["allowed_dynamic_code_is_refused_as_mitigation_mismatch"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/startup.rs" +old = "mitigation::validate(&words)\n .map_err(|error| Refusal::new(reason::MITIGATION_MISMATCH, error))?;" +new = "let _ = mitigation::validate(&words); // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-skips-handle-not-allowed" +guards = "Worker refuses startup with handle-not-allowed before reading its first frame" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +expect_red = ["an_inherited_handle_beyond_the_pipes_is_refused_as_handle_not_allowed"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/startup.rs" +old = "allowlist::check(&table).map_err(|error| Refusal::new(reason::HANDLE_NOT_ALLOWED, error))?;" +new = "let _ = allowlist::check(&table); // NON-VACUITY BREAK" + +[[control]] +id = "windows-allowlist-skips-alpc-close" +guards = "windows allowlist skips alpc close" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::startup_closes_ports_and_private_files_only"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "\"ALPC Port\" => true," +new = "\"ALPC Port\" => false, // NON-VACUITY BREAK" + +[[control]] +id = "windows-allowlist-skips-private-file-close" +guards = "windows allowlist skips private file close" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::startup_closes_ports_and_private_files_only"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "\"File\" => !stdio && !inheritable," +new = "\"File\" => false, // NON-VACUITY BREAK" + +[[control]] +id = "windows-allowlist-skips-unknown-row" +guards = "windows allowlist skips unknown row" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::an_unknown_row_is_refused"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "None => refused.push(describe(entry))," +new = "None => {} // NON-VACUITY BREAK" + +[[control]] +id = "windows-allowlist-skips-excess-count" +guards = "windows allowlist skips excess count" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::an_excess_count_is_refused"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "if count > row.count {" +new = "if false && count > row.count { // NON-VACUITY BREAK" + +[[control]] +id = "windows-allowlist-skips-mixed-inventory" +guards = "windows allowlist skips mixed inventory" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::the_mixed_table_fits_no_profile"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "pub const PROFILES: [&Profile; 2] = [&WINDOWS_LATEST, &WINDOWS_2022];" +new = "pub const UNION_PROFILE: Profile = Profile { // NON-VACUITY BREAK\n name: \"union\",\n ceilings: [\n ceiling(\"Event\", 0x001f_0003, 7),\n ceiling(\"IoCompletion\", 0x001f_0003, 2),\n ceiling(\"TpWorkerFactory\", 0x000f_00ff, 2),\n ceiling(\"IRTimer\", 0x0010_0002, 4),\n ceiling(\"WaitCompletionPacket\", 0x1, 6),\n ceiling(\"Semaphore\", 0x0010_0003, 2),\n ceiling(\"SchedulerSharedData\", 0x1, 1),\n ],\n total: 28,\n};\npub const PROFILES: [&Profile; 2] = [&UNION_PROFILE, &UNION_PROFILE];" + +[[control]] +id = "windows-allowlist-skips-exact-pipes" +guards = "windows allowlist skips exact pipes" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::a_missing_pipe_or_known_dlls_is_refused"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "if stdio[index] != 1 {" +new = "if false && stdio[index] != 1 { // NON-VACUITY BREAK" + +[[control]] +id = "windows-allowlist-skips-known-dlls-count" +guards = "windows allowlist skips known dlls count" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::a_missing_pipe_or_known_dlls_is_refused"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "if known_dlls != 1 {" +new = "if false && known_dlls != 1 { // NON-VACUITY BREAK" + +[[control]] +id = "windows-allowlist-skips-inheritable-non-stdio" +guards = "windows allowlist skips inheritable non stdio" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::an_inheritable_handle_that_is_not_stdio_is_refused"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "if entry.inheritable {\n refused.push(format!(\"inheritable {}\", describe(entry)));" +new = "if false && entry.inheritable { // NON-VACUITY BREAK\n refused.push(format!(\"inheritable {}\", describe(entry)));" + +[[control]] +id = "windows-allowlist-skips-known-dlls-identity" +guards = "windows allowlist skips known dlls identity" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::only_the_known_dlls_directory_is_allowed"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "if entry.name.as_deref() == Some(KNOWN_DLLS) && entry.access == KNOWN_DLLS_ACCESS {" +new = "if entry.access == KNOWN_DLLS_ACCESS { // NON-VACUITY BREAK" + +[[control]] +id = "windows-allowlist-skips-stdio-identity" +guards = "windows allowlist skips stdio identity" +test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::allowlist::tests::a_standard_handle_must_be_its_own_pipe"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/allowlist.rs" +old = "if entry.type_name == \"File\" && entry.access == access {" +new = "if true { // NON-VACUITY BREAK" + +[[control]] +id = "windows-confinement-wire-tag" +guards = "The Windows report uses protocol tag 3 independently of its decoder" +test_file = "crates/basal-proto/src/lib.rs" +runner = "cargo" +package = "basal-proto" +platforms = ["windows"] +target = "--lib" +expect_red = ["tests::round_trip_confinement_reports"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-proto/src/wire/windows.rs" +old = "enc.u8(TAG);" +new = "enc.u8(99); // NON-VACUITY BREAK" + +[[control]] +id = "windows-fault-status-is-not-counted" +guards = "windows fault status is not counted" +test_file = "crates/basal-module/src/windows.rs" +runner = "cargo" +package = "basal-module" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::tests::fault_statuses_count_and_the_refusal_and_kill_codes_do_not"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-module/src/windows.rs" +old = "matches!(code, STATUS_ACCESS_VIOLATION | STATUS_INVALID_HANDLE)" +new = "false // NON-VACUITY BREAK" + +# Before resume the parent checks the suspended child's job and tokens. +# Process creation fixes its mitigation bits; all spawns share one lock while +# their child's standard handles are inheritable. + +[[control]] +id = "windows-parent-skips-job-limits-mismatch" +guards = "windows parent skips job limits mismatch" +test_file = "crates/basal-launch/tests/windows_launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--test windows_launch" +features = ["deviations"] +expect_red = ["deviations::a_job_with_other_limits_is_refused_as_job_limits_mismatch"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "if limits != required {" +new = "if false && limits != required { // NON-VACUITY BREAK" + +[[control]] +id = "windows-parent-skips-not-in-owned-job" +guards = "windows parent skips not in owned job" +test_file = "crates/basal-launch/tests/windows_launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--test windows_launch" +features = ["deviations"] +expect_red = ["deviations::a_child_outside_the_owned_job_is_refused_as_not_in_owned_job"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "if !member {" +new = "if false && !member { // NON-VACUITY BREAK" + +[[control]] +id = "windows-parent-skips-birth-token-mismatch" +guards = "windows parent skips birth token mismatch" +test_file = "crates/basal-launch/tests/windows_launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--test windows_launch" +features = ["deviations"] +expect_red = ["deviations::a_primary_with_privileges_is_refused_as_birth_token_mismatch"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "token::check_birth(&birth, expected)\n .map_err(|error| LaunchError::refused(Refusal::BirthTokenMismatch, error))?;" +new = "let _ = token::check_birth(&birth, expected); // NON-VACUITY BREAK" + +[[control]] +id = "windows-parent-skips-initial-token-open" +guards = "windows parent skips initial token open" +test_file = "crates/basal-launch/tests/windows_launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--test windows_launch" +features = ["deviations"] +expect_red = ["deviations::a_missing_start_up_token_is_refused_as_initial_token_open"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "let assigned = thread_token(child.thread.as_raw_handle())\n .and_then(|token| token::read_facts(token.as_raw_handle()))\n .map_err(|error| LaunchError::refused(Refusal::InitialTokenOpen, error))?;\n token::check_initial(&assigned, &expected.package)\n .map_err(|error| LaunchError::refused(Refusal::InitialTokenOpen, error))?;" +new = "let _ = child; // NON-VACUITY BREAK" + +[[control]] +id = "windows-creation-drops-strict-handle-checks" +guards = "windows creation drops strict handle checks" +test_file = "crates/basal-launch/src/windows/launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::launch::tests::the_mitigation_policy_is_the_eight_always_on_bits"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "pub const MITIGATION_STRICT_HANDLE_CHECKS: u64 = 1 << 24;" +new = "pub const MITIGATION_STRICT_HANDLE_CHECKS: u64 = 0; // NON-VACUITY BREAK" + +[[control]] +id = "windows-creation-drops-win32k-system-call-disable" +guards = "windows creation drops win32k system call disable" +test_file = "crates/basal-launch/src/windows/launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::launch::tests::the_mitigation_policy_is_the_eight_always_on_bits"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "pub const MITIGATION_WIN32K_SYSTEM_CALL_DISABLE: u64 = 1 << 28;" +new = "pub const MITIGATION_WIN32K_SYSTEM_CALL_DISABLE: u64 = 0; // NON-VACUITY BREAK" + +[[control]] +id = "windows-creation-drops-extension-point-disable" +guards = "windows creation drops extension point disable" +test_file = "crates/basal-launch/src/windows/launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::launch::tests::the_mitigation_policy_is_the_eight_always_on_bits"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "pub const MITIGATION_EXTENSION_POINT_DISABLE: u64 = 1 << 32;" +new = "pub const MITIGATION_EXTENSION_POINT_DISABLE: u64 = 0; // NON-VACUITY BREAK" + +[[control]] +id = "windows-creation-drops-prohibit-dynamic-code" +guards = "windows creation drops prohibit dynamic code" +test_file = "crates/basal-launch/src/windows/launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::launch::tests::the_mitigation_policy_is_the_eight_always_on_bits"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "pub const MITIGATION_PROHIBIT_DYNAMIC_CODE: u64 = 1 << 36;" +new = "pub const MITIGATION_PROHIBIT_DYNAMIC_CODE: u64 = 0; // NON-VACUITY BREAK" + +[[control]] +id = "windows-creation-drops-microsoft-signed-only" +guards = "windows creation drops microsoft signed only" +test_file = "crates/basal-launch/src/windows/launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::launch::tests::the_mitigation_policy_is_the_eight_always_on_bits"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "pub const MITIGATION_MICROSOFT_SIGNED_ONLY: u64 = 1 << 44;" +new = "pub const MITIGATION_MICROSOFT_SIGNED_ONLY: u64 = 0; // NON-VACUITY BREAK" + +[[control]] +id = "windows-creation-drops-no-remote-images" +guards = "windows creation drops no remote images" +test_file = "crates/basal-launch/src/windows/launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::launch::tests::the_mitigation_policy_is_the_eight_always_on_bits"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "pub const MITIGATION_NO_REMOTE_IMAGES: u64 = 1 << 52;" +new = "pub const MITIGATION_NO_REMOTE_IMAGES: u64 = 0; // NON-VACUITY BREAK" + +[[control]] +id = "windows-creation-drops-no-low-label-images" +guards = "windows creation drops no low label images" +test_file = "crates/basal-launch/src/windows/launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::launch::tests::the_mitigation_policy_is_the_eight_always_on_bits"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "pub const MITIGATION_NO_LOW_LABEL_IMAGES: u64 = 1 << 56;" +new = "pub const MITIGATION_NO_LOW_LABEL_IMAGES: u64 = 0; // NON-VACUITY BREAK" + +[[control]] +id = "windows-creation-drops-prefer-system32-images" +guards = "windows creation drops prefer system32 images" +test_file = "crates/basal-launch/src/windows/launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::launch::tests::the_mitigation_policy_is_the_eight_always_on_bits"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "pub const MITIGATION_PREFER_SYSTEM32_IMAGES: u64 = 1 << 60;" +new = "pub const MITIGATION_PREFER_SYSTEM32_IMAGES: u64 = 0; // NON-VACUITY BREAK" + +[[control]] +id = "windows-plain-spawn-releases-lock-before-creation" +guards = "windows plain spawn releases lock before creation" +test_file = "crates/basal-launch/src/windows/plain/tests.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::plain::tests::the_child_is_created_suspended_under_the_lock_inheriting_only_its_stdio"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/plain.rs" +old = " let mut info: PROCESS_INFORMATION = unsafe { zeroed() };\n check(\n unsafe {\n CreateProcessW(\n application.as_ptr(),\n command_line.as_mut_ptr(),\n null(),\n null(),\n 1,\n CREATION_FLAGS,\n environment.as_ptr().cast::(),\n cwd.as_ref().map_or(null(), |cwd| cwd.as_ptr()),\n &startup.StartupInfo,\n &mut info,\n )\n },\n \"CreateProcessW\",\n )?;\n let created = Created {\n process: unsafe { OwnedHandle::from_raw_handle(info.hProcess) },\n thread: unsafe { OwnedHandle::from_raw_handle(info.hThread) },\n };\n #[cfg(test)]\n tests::observe(&created, &inherited);\n // The child holds its own copies now. Closing the parent's before the\n // lock is released ends the window in which another spawn could copy\n // them, and lets the pipes report end of file when the child exits.\n drop(attributes);\n let [stdin, stdout, stderr] = [stdin, stdout, stderr].map(Stdio::close_child);\n drop(spawn_lock);" +new = " drop(spawn_lock); // NON-VACUITY BREAK\n let mut info: PROCESS_INFORMATION = unsafe { zeroed() };\n check(\n unsafe {\n CreateProcessW(\n application.as_ptr(),\n command_line.as_mut_ptr(),\n null(),\n null(),\n 1,\n CREATION_FLAGS,\n environment.as_ptr().cast::(),\n cwd.as_ref().map_or(null(), |cwd| cwd.as_ptr()),\n &startup.StartupInfo,\n &mut info,\n )\n },\n \"CreateProcessW\",\n )?;\n let created = Created {\n process: unsafe { OwnedHandle::from_raw_handle(info.hProcess) },\n thread: unsafe { OwnedHandle::from_raw_handle(info.hThread) },\n };\n #[cfg(test)]\n tests::observe(&created, &inherited);\n // The child holds its own copies now. Closing the parent's before the\n // lock is released ends the window in which another spawn could copy\n // them, and lets the pipes report end of file when the child exits.\n drop(attributes);\n let [stdin, stdout, stderr] = [stdin, stdout, stderr].map(Stdio::close_child);\n" + +[[control]] +id = "windows-plain-spawn-drops-handle-list" +guards = "windows plain spawn drops handle list" +test_file = "crates/basal-launch/src/windows/plain/tests.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::plain::tests::the_child_is_created_suspended_under_the_lock_inheriting_only_its_stdio"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/plain.rs" +old = "attributes.add(PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &inherited)?;" +new = "// NON-VACUITY BREAK: no inherited-handle list" + +[[control]] +id = "windows-plain-spawn-drops-job-list" +guards = "windows plain spawn drops job list" +test_file = "crates/basal-launch/src/windows/plain/tests.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::plain::tests::the_child_is_in_a_fresh_kill_on_close_job_without_breakaway"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/plain.rs" +old = "attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &jobs)?;" +new = "// NON-VACUITY BREAK: no creation job list" + +[[control]] +id = "windows-launcher-context-names-are-predictable" +guards = "windows launcher context names are predictable" +test_file = "crates/basal-launch/src/windows/context.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::context::tests::context_names_are_random_and_never_repeat"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/context.rs" +old = "Ok(format!(\"cortexkit_basal_{hex}\"))" +new = "static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);\n Ok(format!(\"cortexkit_basal_{}_{}\", std::process::id(), NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed))) // NON-VACUITY BREAK" + +[[control]] +id = "windows-git-spawn-releases-lock-before-creation" +guards = "windows git spawn releases lock before creation" +test_file = "crates/basal-host/src/builtins/git/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::git::windows::spawn_lock_tests::windows_git_children_are_created_under_the_spawn_lock"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/git/windows.rs" +old = " let mut process: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };\n // SAFETY: buffers, owned attribute payloads, handles and startup structure\n // remain live through creation; lpApplicationName is the canonical image.\n if unsafe {\n CreateProcessW(\n application.as_ptr(),\n command_line.as_mut_ptr(),\n std::ptr::null(),\n std::ptr::null(),\n 1,\n EXTENDED_STARTUPINFO_PRESENT\n | CREATE_NO_WINDOW\n | CREATE_UNICODE_ENVIRONMENT\n | CREATE_SUSPENDED,\n env.as_ptr().cast(),\n cwd_pointer,\n &startup.StartupInfo,\n &mut process,\n )\n } == 0\n {\n return Err(native_error(\"start git\"));\n }\n #[cfg(test)]\n spawn_lock_tests::created();\n // The child now holds its own copies of its three standard handles, so\n // the parent closes its copies before releasing the spawn lock. That\n // also lets the pipes report end of file once git exits.\n drop(attributes);\n drop((stdin, stdout_writer, stderr_writer));\n drop(spawn_lock);" +new = " drop(spawn_lock); // NON-VACUITY BREAK\n let mut process: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };\n // SAFETY: buffers, owned attribute payloads, handles and startup structure\n // remain live through creation; lpApplicationName is the canonical image.\n if unsafe {\n CreateProcessW(\n application.as_ptr(),\n command_line.as_mut_ptr(),\n std::ptr::null(),\n std::ptr::null(),\n 1,\n EXTENDED_STARTUPINFO_PRESENT\n | CREATE_NO_WINDOW\n | CREATE_UNICODE_ENVIRONMENT\n | CREATE_SUSPENDED,\n env.as_ptr().cast(),\n cwd_pointer,\n &startup.StartupInfo,\n &mut process,\n )\n } == 0\n {\n return Err(native_error(\"start git\"));\n }\n #[cfg(test)]\n spawn_lock_tests::created();\n // The child now holds its own copies of its three standard handles, so\n // the parent closes its copies before releasing the spawn lock. That\n // also lets the pipes report end of file once git exits.\n drop(attributes);\n drop((stdin, stdout_writer, stderr_writer));\n" + +# Windows filesystem path spelling, containment and replacement permissions; +# Git config/hook isolation, output bounds and child-tree termination. + +[[control]] +id = "windows-fs-skips-raw-spelling" +guards = "windows fs skips raw spelling" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::spelling_refuses_unc_and_device_namespaces"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "pub fn validate_raw_spelling(path: &str) -> Result<(), Denial> {" +new = "pub fn validate_raw_spelling(path: &str) -> Result<(), Denial> {\n return Ok(()); // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-ads" +guards = "windows fs skips ads" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::spelling_refuses_alternate_data_streams"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "if comp.contains(':') {" +new = "if false && comp.contains(':') { // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-device-names" +guards = "windows fs skips device names" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::spelling_refuses_reserved_device_names"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "if is_reserved_device_name(base) {" +new = "if false && is_reserved_device_name(base) { // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-trailing-dot-space" +guards = "windows fs skips trailing dot space" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::spelling_refuses_trailing_dots_and_spaces"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "if comp.ends_with('.') || comp.ends_with(' ') {" +new = "if false && (comp.ends_with('.') || comp.ends_with(' ')) { // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-empty-component" +guards = "windows fs skips empty component" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::spelling_refuses_relative_and_empty_components"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "if comp.is_empty() {" +new = "if false && comp.is_empty() { // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-component-containment" +guards = "windows fs skips component containment" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::guid_path_inside_component_wise_matches"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "if r != t {" +new = "if false && r != t { // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-file-root-descendants" +guards = "windows fs skips file root descendants" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::guid_path_inside_component_wise_matches"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "if !is_directory_root && target_parts.len() != root_parts.len() {" +new = "if false && !is_directory_root && target_parts.len() != root_parts.len() { // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-reparse-refusal" +guards = "windows fs skips reparse refusal" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::win_tests::windows_reparse_points_at_root_middle_leaf"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "if basic.FileAttributes & ffi::FILE_ATTRIBUTE_REPARSE_POINT != 0 {" +new = "if false && basic.FileAttributes & ffi::FILE_ATTRIBUTE_REPARSE_POINT != 0 { // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-write-dacl" +guards = "windows fs skips write dacl" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::win_tests::windows_replace_keeps_the_protected_dacl"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "security_descriptor: target_dacl.as_ref().map_or(std::ptr::null_mut(), |d| d.sd)," +new = "security_descriptor: std::ptr::null_mut(), // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-write-parent-pinning" +guards = "windows fs skips write parent pinning" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::win_tests::windows_write_parent_cannot_be_moved_out_during_the_write"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "const SHARE_NO_DELETE: u32 = ffi::FILE_SHARE_READ | ffi::FILE_SHARE_WRITE;" +new = "const SHARE_NO_DELETE: u32 = ffi::FILE_SHARE_READ | ffi::FILE_SHARE_WRITE | ffi::FILE_SHARE_DELETE; // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-flush-refusal" +guards = "windows fs skips flush refusal" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::win_tests::windows_failed_flush_stops_the_replace"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "if flush_status < 0 {" +new = "if false && flush_status < 0 { // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-rename-refusal" +guards = "windows fs skips rename refusal" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::win_tests::windows_posix_rename_refusal_simulated"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "if rename_status < 0 {" +new = "if false && rename_status < 0 { // NON-VACUITY BREAK" + +[[control]] +id = "windows-fs-skips-tilde-expansion" +guards = "windows fs skips tilde expansion" +test_file = "crates/basal-host/src/builtins/fs/windows.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::fs::windows::tests::win_tests::tilde_root_reads_below_the_process_tokens_profile"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/fs/windows.rs" +old = "let r = expand_root(r)?;" +new = "let r = std::borrow::Cow::Borrowed(r); // NON-VACUITY BREAK" + +[[control]] +id = "windows-git-skips-global-config-isolation" +guards = "windows git skips global config isolation" +test_file = "crates/basal-host/src/builtins/git/windows/tests.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::git::windows::tests::windows_contaminated_global_config_positive_control_and_isolation"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/git/windows.rs" +old = " .env(\n \"GIT_CONFIG_GLOBAL\",\n isolation.directory.join(\"global.config\"),\n )" +new = " // NON-VACUITY BREAK: use the user-global configuration" + +[[control]] +id = "windows-git-skips-hooks-suppression" +guards = "windows git skips hooks suppression" +test_file = "crates/basal-host/src/builtins/git/windows/tests.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::git::windows::tests::windows_hook_positive_control_and_hardened_suppression"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/git/windows.rs" +old = "hooks.push(isolation.directory.join(\"hooks\"));" +new = "hooks.push(\".git/hooks\"); // NON-VACUITY BREAK" + +[[control]] +id = "windows-git-skips-fsmonitor-suppression" +guards = "windows git skips fsmonitor suppression" +test_file = "crates/basal-host/src/builtins/git/windows/tests.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::git::windows::tests::windows_fsmonitor_positive_control_and_hardened_suppression"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/git/windows.rs" +old = "\"core.fsmonitor=false\"," +new = "\"core.fsmonitor=true\", // NON-VACUITY BREAK" + +[[control]] +id = "windows-git-skips-environment-isolation" +guards = "windows git skips environment isolation" +test_file = "crates/basal-host/src/builtins/git/windows/tests.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::git::windows::tests::windows_inherited_git_environment_is_not_imported"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/git/windows.rs" +old = "if !hardened {" +new = "if true { // NON-VACUITY BREAK" + +[[control]] +id = "windows-git-skips-byte-cap" +guards = "windows git skips byte cap" +test_file = "crates/basal-host/src/builtins/git/windows/tests.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::git::windows::tests::windows_tags_stop_while_reading_and_enforce_byte_cap"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/git/windows.rs" +old = "if cap > STDERR_BYTES && output.bytes.len() > MAX_OUTPUT_BYTES {" +new = "if false && cap > STDERR_BYTES && output.bytes.len() > MAX_OUTPUT_BYTES { // NON-VACUITY BREAK" + +[[control]] +id = "windows-git-skips-kill-on-close" +guards = "windows git skips kill on close" +test_file = "crates/basal-host/src/builtins/git/windows/tests.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::git::windows::tests::windows_attribute_payloads_and_job_membership_before_work"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/git/windows.rs" +old = "limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;" +new = "limits.BasicLimitInformation.LimitFlags = 0; // NON-VACUITY BREAK" + +[[control]] +id = "windows-git-timeout-leaves-descendant-live" +guards = "windows git timeout leaves descendant live" +test_file = "crates/basal-host/src/builtins/git/windows/tests.rs" +runner = "cargo" +package = "basal-host" +platforms = ["windows"] +target = "--lib" +expect_red = ["builtins::git::windows::tests::windows_timeout_proves_live_descendant_died"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-host/src/builtins/git/windows.rs" +old = " if self.terminate().is_err() {\n let _ = self.terminate();\n }\n // Last-handle close is the kill-on-close fallback, before any joins.\n self.job.take();" +new = " if let Some(job) = self.job.take() {\n std::mem::forget(job); // NON-VACUITY BREAK\n }" + +[[control]] +id = "windows-mitigations-allow-thread-opt-out" +guards = "windows mitigations allow thread opt out" +test_file = "crates/basal-worker/src/confinement/windows/mitigation.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--bin ck-basal-worker" +expect_red = ["confinement::windows::mitigation::tests::every_required_bit_with_a_forbidden_opt_out_is_refused"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows/mitigation.rs" +old = "if forbidden.policy.word(words) & forbidden.mask != 0 {" +new = "if false && forbidden.policy.word(words) & forbidden.mask != 0 { // NON-VACUITY BREAK" + +[[control]] +id = "windows-welcome-ignores-false-confinement-fields" +guards = "The parent refuses every false field in a Windows confinement report" +test_file = "crates/basal-module/src/acceptance_tests.rs" +runner = "cargo" +package = "basal-module" +platforms = ["windows"] +target = "--lib" +expect_red = ["pool::acceptance_tests::windows_confinement_acceptance"] +only = true +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-module/src/windows.rs" +old = "lpac && untrusted && no_thread_token && mitigations && handle_table" +new = "true // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-missing-package-sid-is-not-refused" +guards = "A missing package SID is refused before worker confinement starts" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +expect_red = ["a_missing_package_sid_exits_70_package_sid_argument_missing"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/src/confinement/windows.rs" +old = "let package_sid = package_sid.ok_or(ArgumentError::Missing)?;" +new = "let package_sid = package_sid.unwrap_or(\"S-1-15-2-1\"); // NON-VACUITY BREAK" + +[[control]] +id = "windows-parent-loses-profile-unavailable-reason" +guards = "An unavailable AppContainer profile has its named parent refusal before any launch" +test_file = "crates/basal-launch/src/windows/mod.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::tests::an_unusable_profile_is_refused_by_name"] +only = true +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/profile.rs" +old = "LaunchError::refused(Refusal::AppContainerProfileUnavailable, detail)" +new = "LaunchError::Failed(detail) // NON-VACUITY BREAK" + +[[control]] +id = "windows-profile-root-uses-userprofile-environment" +guards = "Profile root expansion uses the process token rather than a caller-controlled environment variable" +test_file = "crates/basal-launch/src/windows/profile.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--lib" +expect_red = ["windows::profile::tests::the_process_token_profile_ignores_userprofile_environment"] +only = true +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/profile.rs" +old = "pub fn process_profile_directory() -> Result {" +new = "pub fn process_profile_directory() -> Result {\n return std::env::var(\"USERPROFILE\").map_err(|error| error.to_string()); // NON-VACUITY BREAK" + +[[control]] +id = "windows-worker-import-inventory-omits-kernel32" +guards = "The production worker's actual imports must be present in the committed inventory" +test_file = "crates/basal-worker/tests/windows_image.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_image" +expect_red = ["the_worker_is_a_gui_image_without_gui_com_or_c_runtime_imports"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-worker/tests/data/windows-imports.txt" +old = "kernel32.dll\n" +new = "# NON-VACUITY BREAK\n" + +[[control]] +id = "windows-confined-spawn-releases-lock-before-creation" +guards = "The full launcher holds the shared spawn lock until its inheritable pipe ends close" +test_file = "crates/basal-launch/tests/windows_launch.rs" +runner = "cargo" +package = "basal-launch" +platforms = ["windows"] +target = "--test windows_launch" +features = ["deviations"] +expect_red = ["deviations::a_confined_child_is_created_under_the_shared_spawn_lock"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = " let spawned = match deviation.shape() {\n Shape::Confined => spawn_confined(\n options,\n commit_bytes,\n &package,\n parent_token.as_raw_handle(),\n &context,\n &inherited,\n &mut command,\n )?,\n #[cfg(feature = \"deviations\")]\n Shape::LpacOnly | Shape::Plain => spawn_control(\n options,\n &package,\n &context,\n &inherited,\n &mut command,\n deviation.shape() == Shape::LpacOnly,\n )?,\n };\n\n // The child has inherited its own copies of its three pipe ends; the\n // parent's copies would keep the pipes open after the child exits.\n drop((child_stdin, child_stdout, child_stderr));\n drop(spawn_lock);" +new = " drop(spawn_lock); // NON-VACUITY BREAK\n let spawned = match deviation.shape() {\n Shape::Confined => spawn_confined(\n options,\n commit_bytes,\n &package,\n parent_token.as_raw_handle(),\n &context,\n &inherited,\n &mut command,\n )?,\n #[cfg(feature = \"deviations\")]\n Shape::LpacOnly | Shape::Plain => spawn_control(\n options,\n &package,\n &context,\n &inherited,\n &mut command,\n deviation.shape() == Shape::LpacOnly,\n )?,\n };\n\n // The child has inherited its own copies of its three pipe ends; the\n // parent's copies would keep the pipes open after the child exits.\n drop((child_stdin, child_stdout, child_stderr));\n" + +[[control]] +id = "windows-creation-drops-child-process-policy" +guards = "The worker attests the child-process creation restriction before any activation" +test_file = "crates/basal-worker/tests/windows_startup.rs" +runner = "cargo" +package = "basal-worker" +platforms = ["windows"] +target = "--test windows_startup" +expect_red = ["an_activation_completes_under_the_full_confinement"] +only = false +build_timeout_s = 3600 +timeout_s = 600 +file = "crates/basal-launch/src/windows/launch.rs" +old = "attributes.add(PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY, &child_policy)?;" +new = "// NON-VACUITY BREAK: no child-process creation policy" diff --git a/script/tests/__init__.py b/script/tests/__init__.py index 2a2ceb9..69884d8 100644 --- a/script/tests/__init__.py +++ b/script/tests/__init__.py @@ -10,4 +10,5 @@ def load_tests(loader, tests, pattern): "script.tests.flows_rig", "script.tests.sign_worker", "script.tests.stage_card", + "script.tests.witness_mutations", ]) diff --git a/script/tests/witness_mutations.py b/script/tests/witness_mutations.py new file mode 100644 index 0000000..1a3e4ad --- /dev/null +++ b/script/tests/witness_mutations.py @@ -0,0 +1,85 @@ +"""Mutation witnesses must observe the indexed mutant and a named test failure.""" +import importlib.util +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest import mock + +PATH = Path(__file__).resolve().parents[1] / "witness-mutations.py" +SPEC = importlib.util.spec_from_file_location("witness_mutations", PATH) +assert SPEC is not None and SPEC.loader is not None +WITNESS = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(WITNESS) + + +class MutationWitnessChecks(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + subprocess.run(["git", "init", "--quiet", str(self.root)], check=True) + self.source = self.root / "guard.rs" + self.source.write_text("guard();\n") + self.output = self.root / "proofs" + self.output.mkdir() + self.row = {"id": "guard", "runner": "cargo", "package": "fixture", "target": "--lib", + "file": "guard.rs", "old": "guard();", "new": "// NON-VACUITY BREAK", + "expect_red": ["tests::guard"], "features": ["deviations"]} + self.real_run = subprocess.run + self.root_patch = mock.patch.object(WITNESS, "ROOT", self.root) + self.root_patch.start() + self.addCleanup(self.root_patch.stop) + + def cargo(self, args, **kwargs): + if args[0] != "cargo": + return self.real_run(args, **kwargs) + if "NON-VACUITY BREAK" in self.source.read_text(): + return subprocess.CompletedProcess(args, 101, "test tests::guard ... FAILED\n0 passed; 1 failed; 9 filtered out\n") + return subprocess.CompletedProcess(args, 0, "test tests::guard ... ok\n1 passed; 0 failed\n") + + def test_feature_flags_apply_to_the_named_cargo_oracle(self): + self.assertEqual(WITNESS.command(self.row), ["cargo", "test", "--locked", "-p", "fixture", + "--lib", "--features", "deviations", "--", "tests::guard", "--exact", "--nocapture"]) + + def test_witness_stages_live_source_and_restores_it_after_a_named_failure(self): + self.source.write_text("guard();\n// live implementation\n") + with mock.patch.object(WITNESS.subprocess, "run", side_effect=self.cargo): + proof = WITNESS.witness(self.row, self.output) + self.assertEqual(proof["outcome"], "reddened") + self.assertIn("guard.rs", proof["applied_evidence"]) + self.assertIn("after: (empty)", proof["applied_evidence"]) + self.assertEqual(self.source.read_text(), "guard();\n// live implementation\n") + self.assertEqual(WITNESS.git("diff", "--stat"), "") + self.assertIn("1 file changed", proof["applied_evidence"]) + + def test_a_build_failure_is_not_a_named_mutation_catch(self): + def fail_build(args, **kwargs): + if args[0] == "cargo" and "NON-VACUITY BREAK" in self.source.read_text(): + return subprocess.CompletedProcess(args, 101, "error: could not compile fixture\n") + return self.cargo(args, **kwargs) + with mock.patch.object(WITNESS.subprocess, "run", side_effect=fail_build): + proof = WITNESS.witness(self.row, self.output) + self.assertEqual(proof["outcome"], "not_reached") + self.assertEqual(self.source.read_text(), "guard();\n") + self.assertEqual(WITNESS.git("diff", "--stat"), "") + + def test_a_zero_test_baseline_is_refused(self): + def absent(args, **kwargs): + if args[0] == "cargo": + return subprocess.CompletedProcess(args, 0, "0 passed; 0 failed; 10 filtered out\n") + return self.real_run(args, **kwargs) + with mock.patch.object(WITNESS.subprocess, "run", side_effect=absent): + with self.assertRaisesRegex(SystemExit, "named baseline did not execute"): + WITNESS.witness(self.row, self.output) + self.assertEqual(self.source.read_text(), "guard();\n") + + def test_a_nonunique_anchor_is_refused_before_mutating(self): + self.source.write_text("guard();\nguard();\n") + with self.assertRaisesRegex(SystemExit, "exactly one anchor"): + WITNESS.witness(self.row, self.output) + self.assertEqual(self.source.read_text(), "guard();\nguard();\n") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/script/witness-mutations.py b/script/witness-mutations.py new file mode 100644 index 0000000..a4e8176 --- /dev/null +++ b/script/witness-mutations.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Capture indexed source diffs and exact named-test failures for Cargo controls. + +The pinned ckdev-mutate runner grades whether catalogue mutations are caught by +their declared test targets. This script adds the applied/restored Git diff and +an exact named-test failure; it does not replace that replay or coverage grading. +""" +import argparse +import json +from pathlib import Path +import shlex +import subprocess +import tomllib + +ROOT = Path(__file__).resolve().parents[1] + + +def git(*args): + return subprocess.check_output(["git", *args], cwd=ROOT, text=True).strip() + + +def command(row): + args = ["cargo", "test", "--locked", "-p", row["package"], *shlex.split(row["target"])] + if row.get("features"): + args.extend(["--features", ",".join(row["features"])]) + if row.get("no_default_features"): + args.append("--no-default-features") + if row.get("all_features"): + args.append("--all-features") + return args + ["--", row["expect_red"][0], "--exact", "--nocapture"] + + +def witness(row, output): + if row["runner"] != "cargo" or len(row["expect_red"]) != 1 or "edits" in row: + raise SystemExit(f"{row['id']}: witness requires a single-file Cargo control with one oracle") + path = ROOT / row["file"] + subprocess.run(["git", "add", "--", row["file"]], cwd=ROOT, check=True) + if git("diff", "--stat"): + raise SystemExit("mutation safety: stage the live working state before running witnesses") + original = path.read_text() + if original.count(row["old"]) != 1: + raise SystemExit(f"{row['id']}: expected exactly one anchor") + args = command(row) + baseline = subprocess.run(args, cwd=ROOT, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=row.get("build_timeout_s", 3600)) + test = row["expect_red"][0] + if baseline.returncode or f"test {test} ... ok" not in baseline.stdout: + (output / f"{row['id']}-baseline.log").write_text(baseline.stdout) + raise SystemExit(f"{row['id']}: named baseline did not execute and pass") + result = None + try: + if "NON-VACUITY BREAK" not in row["new"]: + raise SystemExit(f"{row['id']}: mutation lacks its safety marker") + path.write_text(original.replace(row["old"], row["new"])) + during = git("diff", "--stat") + if not during: + raise SystemExit("mutation safety: empty applied diff") + try: + result = subprocess.run(args, cwd=ROOT, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=row.get("build_timeout_s", 3600)) + text = result.stdout + except subprocess.TimeoutExpired as error: + text = (error.stdout or b"").decode(errors="replace") + finally: + subprocess.run(["git", "checkout", "--", row["file"]], cwd=ROOT, check=True) + path.touch() + after = git("diff", "--stat") + if after: + raise SystemExit("mutation safety: restored diff is not empty") + (output / f"{row['id']}-mutant.log").write_text(text) + expected = f"test {test} ... FAILED" + red = result is not None and result.returncode != 0 and expected in text and "0 passed; 1 failed" in text + reached = f"test {test} ..." in text + proof = { + "control": row["id"], + "expected_red": test, + "captured_output": expected + "; 0 passed; 1 failed; all other tests filtered" if red else text[-400:], + "applied_evidence": f"{row['file']}; during: {during}; after: {after or '(empty)'}", + "outcome": "reddened" if red else "hung" if result is None else "not_reached" if not reached else "undefended", + } + print(json.dumps(proof), flush=True) + return proof + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--platform", required=True, choices=["linux", "macos", "windows"]) + parser.add_argument("--only", action="append", default=[]) + parser.add_argument("--shard", default="1/1") + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + shard, total = map(int, args.shard.split("/")) + if not 1 <= shard <= total: + parser.error("shard must be in 1..total") + args.output.mkdir(parents=True, exist_ok=True) + rows = tomllib.loads((ROOT / "mutations.toml").read_text())["control"] + selected = sorted((row for row in rows if row["platforms"] == [args.platform] + and (not args.only or row["id"] in args.only)), key=lambda row: row["id"]) + if not selected or set(args.only) - {row["id"] for row in selected}: + raise SystemExit("no controls selected or an unknown control requested") + proofs = [] + for index, row in enumerate(selected): + if index % total != shard - 1: + continue + proofs.append(witness(row, args.output)) + (args.output / "mutation-evidence.json").write_text(json.dumps(proofs, indent=2)) + if proofs[-1]["outcome"] != "reddened": + raise SystemExit(f"{row['id']}: exact named oracle did not redden") + print(f"Mutation witnesses: {len(proofs)} exact named failures, all sources restored", flush=True) + + +if __name__ == "__main__": + main() From 115629d307223907475a49c9003f74bc1c666b3e Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Sun, 11 Oct 2026 04:22:12 +0200 Subject: [PATCH 3/4] mason: fix Windows mutation targets and isolate fixtures Use the worker library harness for its confinement unit oracles and the existing fixed-tag protocol test. Capture mklink output so native fixture text cannot corrupt mutation runner libtest parsing. Keep plain Git positive controls free of hardened arguments while giving them owned empty configuration files and isolated environments. This removes the NUL config device rejected by the Windows-2022 Git build without weakening the controls. --- crates/basal-host/src/builtins/fs/windows.rs | 14 ++++++++--- .../src/builtins/git/windows/tests.rs | 21 +++++++++++++--- mutations.toml | 24 +++++++++---------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/crates/basal-host/src/builtins/fs/windows.rs b/crates/basal-host/src/builtins/fs/windows.rs index c1549b1..c24c7cb 100644 --- a/crates/basal-host/src/builtins/fs/windows.rs +++ b/crates/basal-host/src/builtins/fs/windows.rs @@ -2344,7 +2344,9 @@ mod tests { /// Creates a directory junction; needs no privilege. fn junction(link: &Path, target: &Path) { - let status = std::process::Command::new("cmd") + // Subprocess text must not interrupt libtest's result lines. + // Keep it for diagnostics only when fixture creation fails. + let output = std::process::Command::new("cmd") .args([ "/c", "mklink", @@ -2352,9 +2354,15 @@ mod tests { link.to_str().unwrap(), target.to_str().unwrap(), ]) - .status() + .output() .expect("run mklink /J"); - assert!(status.success(), "mklink /J {} failed", link.display()); + assert!( + output.status.success(), + "mklink /J {} failed: {} {}", + link.display(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); assert_link(link); } diff --git a/crates/basal-host/src/builtins/git/windows/tests.rs b/crates/basal-host/src/builtins/git/windows/tests.rs index 0f6d6b3..7256ed0 100644 --- a/crates/basal-host/src/builtins/git/windows/tests.rs +++ b/crates/basal-host/src/builtins/git/windows/tests.rs @@ -670,16 +670,31 @@ fn repository(tree: &Tree) -> PathBuf { ); repo } -fn plain_git(repo: &Path) -> Command { +fn plain_git(repo: &Path) -> HardenedCommand { + let isolation = Isolation::new().expect("owned empty global configuration"); let mut command = Command::new( resolve_git().expect("native git.exe is required for Windows confinement tests"), ); + // Positive controls omit the hardened arguments, not fixture isolation. + // A real empty file works across Git builds; NUL is a device, not a config. + command.env_clear(); + for key in ["PATH", "HOME", "SystemRoot", "USERPROFILE"] { + if let Some(value) = std::env::var_os(key) { + command.env(key, value); + } + } command .arg("-C") .arg(repo) .env("GIT_CONFIG_NOSYSTEM", "1") - .env("GIT_CONFIG_GLOBAL", "NUL"); - command + .env( + "GIT_CONFIG_GLOBAL", + isolation.directory.join("global.config"), + ); + HardenedCommand { + command, + _isolation: isolation, + } } fn setup_git(repo: &Path, args: &[&str]) { let output = plain_git(repo).args(args).output().unwrap(); diff --git a/mutations.toml b/mutations.toml index 293d50b..d9ff115 100644 --- a/mutations.toml +++ b/mutations.toml @@ -14456,7 +14456,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::startup_closes_ports_and_private_files_only"] only = false build_timeout_s = 3600 @@ -14472,7 +14472,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::startup_closes_ports_and_private_files_only"] only = false build_timeout_s = 3600 @@ -14488,7 +14488,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::an_unknown_row_is_refused"] only = false build_timeout_s = 3600 @@ -14504,7 +14504,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::an_excess_count_is_refused"] only = false build_timeout_s = 3600 @@ -14520,7 +14520,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::the_mixed_table_fits_no_profile"] only = false build_timeout_s = 3600 @@ -14536,7 +14536,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::a_missing_pipe_or_known_dlls_is_refused"] only = false build_timeout_s = 3600 @@ -14552,7 +14552,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::a_missing_pipe_or_known_dlls_is_refused"] only = false build_timeout_s = 3600 @@ -14568,7 +14568,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::an_inheritable_handle_that_is_not_stdio_is_refused"] only = false build_timeout_s = 3600 @@ -14584,7 +14584,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::only_the_known_dlls_directory_is_allowed"] only = false build_timeout_s = 3600 @@ -14600,7 +14600,7 @@ test_file = "crates/basal-worker/src/confinement/windows/allowlist.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::allowlist::tests::a_standard_handle_must_be_its_own_pipe"] only = false build_timeout_s = 3600 @@ -14617,7 +14617,7 @@ runner = "cargo" package = "basal-proto" platforms = ["windows"] target = "--lib" -expect_red = ["tests::round_trip_confinement_reports"] +expect_red = ["tests::confinement_wire_tags_and_linux_fields_are_stable"] only = false build_timeout_s = 3600 timeout_s = 600 @@ -15251,7 +15251,7 @@ test_file = "crates/basal-worker/src/confinement/windows/mitigation.rs" runner = "cargo" package = "basal-worker" platforms = ["windows"] -target = "--bin ck-basal-worker" +target = "--lib" expect_red = ["confinement::windows::mitigation::tests::every_required_bit_with_a_forbidden_opt_out_is_refused"] only = false build_timeout_s = 3600 From 8317c468002a9273c758cbf0bc263f242198c1e4 Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Sun, 11 Oct 2026 05:25:01 +0200 Subject: [PATCH 4/4] mason: provide Git fixture temp space and remove fsmonitor override Plain Git controls need temp files for external diff and text conversion. Give them the owned fixture directory as TMP/TEMP/TMPDIR without broadening the production environment. Setting core.fsmonitor=true switches to Git's built-in daemon and still suppresses the repository hook, so it is not a removal of the guard. Remove the -c false override itself so the positive-control hook becomes reachable under the mutant. --- crates/basal-host/src/builtins/git/windows/tests.rs | 3 +++ mutations.toml | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/basal-host/src/builtins/git/windows/tests.rs b/crates/basal-host/src/builtins/git/windows/tests.rs index 7256ed0..90009b9 100644 --- a/crates/basal-host/src/builtins/git/windows/tests.rs +++ b/crates/basal-host/src/builtins/git/windows/tests.rs @@ -687,6 +687,9 @@ fn plain_git(repo: &Path) -> HardenedCommand { .arg("-C") .arg(repo) .env("GIT_CONFIG_NOSYSTEM", "1") + .env("TMP", &isolation.directory) + .env("TEMP", &isolation.directory) + .env("TMPDIR", &isolation.directory) .env( "GIT_CONFIG_GLOBAL", isolation.directory.join("global.config"), diff --git a/mutations.toml b/mutations.toml index d9ff115..38b4824 100644 --- a/mutations.toml +++ b/mutations.toml @@ -15177,8 +15177,8 @@ only = false build_timeout_s = 3600 timeout_s = 600 file = "crates/basal-host/src/builtins/git/windows.rs" -old = "\"core.fsmonitor=false\"," -new = "\"core.fsmonitor=true\", // NON-VACUITY BREAK" +old = " \"-c\",\n \"core.fsmonitor=false\"," +new = " // NON-VACUITY BREAK: use the repository's fsmonitor hook" [[control]] id = "windows-git-skips-environment-isolation"