diff --git a/host/test/kernel-blocking-retry-snapshot.test.ts b/host/test/kernel-blocking-retry-snapshot.test.ts index 8ba7de269b..5a318b7cd3 100644 --- a/host/test/kernel-blocking-retry-snapshot.test.ts +++ b/host/test/kernel-blocking-retry-snapshot.test.ts @@ -71,7 +71,7 @@ const KERNEL_WORKER_SOURCE = readFileSync( "utf8", ); const BLOCKED_RETRY_SOURCE = readFileSync( - new URL("../../crates/kernel/src/blocked_retry.rs", import.meta.url), + new URL("../../crates/runtime-core/src/blocked_retry.rs", import.meta.url), "utf8", ); const CHANNEL_SYSCALL_SOURCE = readFileSync( @@ -675,7 +675,7 @@ describe("blocking retry snapshot contract", () => { ]); const fromSyscall = BLOCKED_RETRY_SOURCE.match( - /pub\(crate\) fn from_syscall\(syscall: u32\) -> Result \{([\s\S]*?)\n \}/, + /pub fn from_syscall\(syscall: u32\) -> Result \{([\s\S]*?)\n \}/, )?.[1]; expect(fromSyscall, "BlockingRetryOperation::from_syscall").toBeDefined(); const targetedFamilies = Array.from( diff --git a/host/test/kernel-scratch-contract.test.ts b/host/test/kernel-scratch-contract.test.ts index 67dfac64d8..7f5b5aaaac 100644 --- a/host/test/kernel-scratch-contract.test.ts +++ b/host/test/kernel-scratch-contract.test.ts @@ -108,7 +108,7 @@ const muslSpawnSource = readFileSync( "utf8", ); const kernelSpawnSource = readFileSync( - new URL("../../crates/kernel/src/spawn.rs", import.meta.url), + new URL("../../crates/runtime-core/src/spawn.rs", import.meta.url), "utf8", ); const kernelWasmApiSource = readFileSync( diff --git a/host/test/privileged-projection.test.ts b/host/test/privileged-projection.test.ts index 1aa67c21ce..57a4877a71 100644 --- a/host/test/privileged-projection.test.ts +++ b/host/test/privileged-projection.test.ts @@ -14,7 +14,7 @@ import { import * as privilegedProjectionModule from "../src/vfs/privileged-projection"; import { ensureDirRecursive, writeVfsBinary } from "../src/vfs/image-helpers"; -import { MemoryFileSystem } from "../src/vfs/memory-fs"; +import { MemoryFileSystem, resolveMountSetIdCapability } from "../src/vfs/memory-fs"; import { VirtualPlatformIO } from "../src/vfs/vfs"; import { NodeTimeProvider } from "../src/vfs/time"; @@ -208,10 +208,7 @@ describe("privileged product publication", () => { sources: source.sources, writableBottleFileSystems: [sharedAlias], }); - grantedCapability = new VirtualPlatformIO( - [product.mount], - new NodeTimeProvider(), - ).getMountSetIdCapability("/usr/bin/login").kind; + grantedCapability = resolveMountSetIdCapability(product.mount).kind; } expect(grantedCapability).not.toBe("trusted-root-product"); @@ -221,8 +218,7 @@ describe("privileged product publication", () => { sources: source.sources, writableBottleFileSystems: [sharedAlias], })).resolves.toBeUndefined(); - expect(() => new VirtualPlatformIO([{ - mountPoint: "/", + expect(() => resolveMountSetIdCapability({ backend: candidate, readonly: true, setIdCapability: { @@ -230,7 +226,7 @@ describe("privileged product publication", () => { guestWritable: false, stableExecutableIdentity: true, }, - }], new NodeTimeProvider())).toThrow(/immutable product backend/i); + })).toThrow(/immutable product backend/i); expect(Reflect.has( privilegedProjectionModule, "admitPrivilegedProgramProductCandidate", @@ -273,7 +269,7 @@ describe("privileged product publication", () => { expect(productIdentities.size).toBe(3); expect(product.evidence.every((entry) => entry.collidesWithWritableBottle === false)) .toBe(true); - expect(io.getMountSetIdCapability("/usr/bin/login")).toEqual({ + expect(resolveMountSetIdCapability(product.mount)).toEqual({ kind: "trusted-root-product", guestWritable: false, stableExecutableIdentity: true, @@ -494,8 +490,7 @@ describe("privileged product publication", () => { })).rejects.toThrow(/parent.*writable/i); candidate.chmod("/usr/bin", 0o755); - expect(() => new VirtualPlatformIO([{ - mountPoint: "/", + expect(() => resolveMountSetIdCapability({ backend: candidate, readonly: true, setIdCapability: { @@ -503,7 +498,7 @@ describe("privileged product publication", () => { guestWritable: false, stableExecutableIdentity: true, }, - }], new NodeTimeProvider())).toThrow(/immutable product backend/i); + })).toThrow(/immutable product backend/i); }); it("rolls back the entire group when the final projection fails", async () => { diff --git a/packages/registry/vim/build-vim.sh b/packages/registry/vim/build-vim.sh index ffb30d52de..6af529b696 100755 --- a/packages/registry/vim/build-vim.sh +++ b/packages/registry/vim/build-vim.sh @@ -209,6 +209,12 @@ if grep -qE '^extern (int|char)[[:space:]]+\**[[:space:]]*(tgetent|tgetnum|tgetf fi echo "==> Building vim..." +# Drop the link target so `make` always relinks. The glue +# (channel_syscall.c, which carries __abi_version) is compiled by +# wasm32posix-cc at link time and is not a make dependency, so an ABI +# bump leaves an up-to-date src/vim holding the previous ABI marker. +# Matches build-netcat.sh:171. +rm -f "$SRC_DIR/src/vim" make -j"$(sysctl -n hw.ncpu 2>/dev/null || nproc)" 2>&1 | tail -30 echo "==> Collecting binary..." diff --git a/packages/registry/vim/build.toml b/packages/registry/vim/build.toml index 11a45c369d..5d3d8f4633 100644 --- a/packages/registry/vim/build.toml +++ b/packages/registry/vim/build.toml @@ -1,4 +1,8 @@ script_path = "packages/registry/vim/build-vim.sh" +inputs = [ + "packages/registry/vim/build-vim.sh", + "packages/registry/vim/bundle-runtime.sh", +] repo_url = "https://github.com/brandonpayton/kandelo.git" commit = "8c53383229fab78f97b098c3207a655159c03041" revision = 4 diff --git a/scripts/ci-vitest-evidence-classes.tsv b/scripts/ci-vitest-evidence-classes.tsv index f10dc94431..5c21cfcdd9 100644 --- a/scripts/ci-vitest-evidence-classes.tsv +++ b/scripts/ci-vitest-evidence-classes.tsv @@ -9,12 +9,14 @@ host/test/append-contract.test.ts source-only host/test/audio-integration.test.ts prepared-product host/test/audio-signal-interruption.test.ts prepared-product host/test/binary-resolver.test.ts prepared-product +host/test/browser-cors-proxy-service-worker-parity.test.ts source-only host/test/browser-cors-proxy.test.ts source-only host/test/browser-demo-cors-proxy.test.ts source-only host/test/browser-engine-detection.test.ts source-only host/test/browser-immediate-polyfill.test.ts source-only host/test/browser-kernel.test.ts source-only host/test/browser-lazy-fetcher.test.ts source-only +host/test/browser-mitm-ca-env.test.ts source-only host/test/browser-pcm-driver.test.ts source-only host/test/browser-wasm-fetch.test.ts source-only host/test/browser-worker-adapter.test.ts source-only @@ -361,13 +363,12 @@ packages/registry/zstd/test/zstd.test.ts prepared-product tests/package-system/bash-package.test.ts prepared-product tests/package-system/binary-dev-access.test.ts prepared-product tests/package-system/browser-binary-dependencies.test.ts prepared-product +tests/package-system/build-input-crate-closure.test.ts source-only tests/package-system/build-input-import-closure.test.ts prepared-product -tests/package-system/fetch-binaries-allow-stale.test.ts prepared-product tests/package-system/host-package-projection-contract.test.ts prepared-product tests/package-system/installed-host-package.test.ts prepared-product tests/package-system/kernel-test-fixtures.test.ts prepared-product tests/package-system/local-build-wrapper.test.ts source-only -tests/package-system/package-source-publish-contract.test.ts prepared-product tests/package-system/playwright-server-policy.test.ts prepared-product tests/package-system/program-resolver-literals.test.ts prepared-product tests/package-system/resolve-binary.test.ts prepared-product diff --git a/tools/xtask/src/build_deps.rs b/tools/xtask/src/build_deps.rs index 4923f569b9..affec12d18 100644 --- a/tools/xtask/src/build_deps.rs +++ b/tools/xtask/src/build_deps.rs @@ -14323,6 +14323,38 @@ fn wasm_artifact_policy_failures_for( )); } + // An artifact that must export __abi_version must export the *current* + // one. Presence alone lets a stale marker through: a package whose build + // tree relinks nothing after an ABI bump re-collects the previous + // binary, and the resolver then caches it under the current ABI's key — + // where the host rejects it at exec. Side modules are checked further + // down, against the same identity helper. + // An artifact that must export __abi_version must export the *current* + // one when the marker is extractable. Presence alone lets a stale marker + // through: a package whose build tree relinks nothing after an ABI bump + // re-collects the previous binary, and the resolver then caches it under + // the current ABI's key -- where the host rejects it at exec. + // + // Only `Present(mismatch)` is failed here, not Missing/Invalid/Err. Unlike + // side modules (checked below), executable __abi_version markers are not + // guaranteed to be a constant thunk `artifact_identity` can extract, so a + // legitimate executable can classify as Missing/Invalid; genuine absence + // of the export is already reported by the required-exports check above. + if required_exports.contains(&"__abi_version") && facts.dylink_section_count == 0 { + use fork_instrument::contract_inventory::ArtifactAbiVersion; + + if let Ok(identity) = fork_instrument::contract_inventory::artifact_identity(bytes) { + if let ArtifactAbiVersion::Present(version) = identity.abi_version { + if version != wasm_posix_shared::ABI_VERSION { + failures.push(format!( + "declares __abi_version {version}, expected current ABI {}", + wasm_posix_shared::ABI_VERSION, + )); + } + } + } + } + let fork_exports = wasm_posix_shared::abi::WPK_FORK_REQUIRED_EXPORTS; let fork_imports = wasm_posix_shared::abi::WPK_FORK_REQUIRED_IMPORTS; let present_fork_exports = fork_exports @@ -21890,6 +21922,12 @@ wasm = "second.wasm" } fn wasm_exporting_names(names: &[&str]) -> Vec { + wasm_exporting_names_declaring_abi(names, wasm_posix_shared::ABI_VERSION) + } + + // The single shared body returns `abi_version`, so a fixture exporting + // __abi_version declares that marker. + fn wasm_exporting_names_declaring_abi(names: &[&str], abi_version: u32) -> Vec { let mut bytes = b"\0asm\x01\0\0\0".to_vec(); bytes.extend(wasm_section(1, vec![0x01, 0x60, 0x00, 0x01, 0x7f])); bytes.extend(wasm_section(3, vec![0x01, 0x00])); @@ -21901,7 +21939,15 @@ wasm = "second.wasm" exports.push(0x00); // func index } bytes.extend(wasm_section(7, exports)); - bytes.extend(wasm_section(10, vec![0x01, 0x04, 0x00, 0x41, 0x00, 0x0b])); + + let mut body = vec![0x00]; // no local declarations + body.push(0x41); // i32.const + body.extend(sleb_i32(abi_version as i32)); + body.push(0x0b); // end + let mut code_section = uleb(1); + code_section.extend(uleb(body.len() as u32)); + code_section.extend(body); + bytes.extend(wasm_section(10, code_section)); bytes } @@ -25665,7 +25711,7 @@ wasm = "vim.wasm" "0.1.0", &[], // Build script writes the declared wasm. - r#"mkdir -p "$WASM_POSIX_DEP_OUT_DIR" && printf '\x00asm\x01\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x1a\x02\x0d__abi_version\x00\x00\x06_start\x00\x00\x0a\x06\x01\x04\x00\x41\x00\x0b' > "$WASM_POSIX_DEP_OUT_DIR/tinyprog.wasm""#, + &emit_wasm_build_script("tinyprog.wasm", &minimal_executable_wasm()), &[("tinyprog", "tinyprog.wasm")], ); let reg = Registry { roots: vec![root] }; @@ -25702,9 +25748,11 @@ wasm = "vim.wasm" "runtimeprog", "0.1.0", &[], - r#"mkdir -p "$WASM_POSIX_DEP_OUT_DIR" -printf '\x00asm\x01\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x1a\x02\x0d__abi_version\x00\x00\x06_start\x00\x00\x0a\x06\x01\x04\x00\x41\x00\x0b' > "$WASM_POSIX_DEP_OUT_DIR/runtimeprog.wasm" + &format!( + r#"{} printf runtime-data > "$WASM_POSIX_DEP_OUT_DIR/icu.dat""#, + emit_wasm_build_script("runtimeprog.wasm", &minimal_executable_wasm()), + ), &[("runtimeprog", "runtimeprog.wasm")], ); append_program_runtime_file(&root, "runtimeprog", "icu.dat", "/usr/lib/php/icu.dat"); @@ -27955,8 +28003,7 @@ wasm = "scalar.zip" "runtimemissing", "0.1.0", &[], - r#"mkdir -p "$WASM_POSIX_DEP_OUT_DIR" -printf '\x00asm\x01\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x1a\x02\x0d__abi_version\x00\x00\x06_start\x00\x00\x0a\x06\x01\x04\x00\x41\x00\x0b' > "$WASM_POSIX_DEP_OUT_DIR/runtimemissing.wasm""#, + &emit_wasm_build_script("runtimemissing.wasm", &minimal_executable_wasm()), &[("runtimemissing", "runtimemissing.wasm")], ); append_program_runtime_file(&root, "runtimemissing", "icu.dat", "/usr/lib/php/icu.dat"); @@ -27983,9 +28030,9 @@ printf '\x00asm\x01\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\ "0.1.0", &[], &format!( - r#"mkdir -p "$WASM_POSIX_DEP_OUT_DIR" -printf '\x00asm\x01\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x1a\x02\x0d__abi_version\x00\x00\x06_start\x00\x00\x0a\x06\x01\x04\x00\x41\x00\x0b' > "$WASM_POSIX_DEP_OUT_DIR/runtimesymlink.wasm" + r#"{} ln -s {:?} "$WASM_POSIX_DEP_OUT_DIR/icu.dat""#, + emit_wasm_build_script("runtimesymlink.wasm", &minimal_executable_wasm()), outside ), &[("runtimesymlink", "runtimesymlink.wasm")], @@ -28105,6 +28152,33 @@ wasm = "bad.wasm" ); } + #[test] + fn wasm_artifact_policy_rejects_an_executable_declaring_a_stale_abi() { + // A package whose build tree relinks nothing after an ABI bump + // re-collects the previous binary. It still exports __abi_version, so + // the presence check above passes and the resolver caches it under the + // current ABI's key — where the host rejects it at exec. That is how + // an ABI 43 vim.wasm reached an ABI 44 desktop. Check the value. + let stale = wasm_exporting_names_declaring_abi( + &EXECUTABLE_PROGRAM_REQUIRED_EXPORTS, + wasm_posix_shared::ABI_VERSION - 1, + ); + let failures = wasm_artifact_policy_failures_for( + &stale, + ForkInstrumentationPolicy::Auto, + &EXECUTABLE_PROGRAM_REQUIRED_EXPORTS, + ); + assert_eq!(failures.len(), 1, "got: {failures:?}"); + assert!( + failures[0].contains(&format!( + "declares __abi_version {}, expected current ABI {}", + wasm_posix_shared::ABI_VERSION - 1, + wasm_posix_shared::ABI_VERSION, + )), + "got: {failures:?}", + ); + } + #[test] fn program_output_validation_rejects_fork_without_wpk_exports() { let out = tempdir("prog-out-fork-policy"); @@ -33670,9 +33744,11 @@ printf '%s\n' "{consumer}" > "$WASM_POSIX_DEP_OUT_DIR/lib/out.a" "runtimebin", "0.1.0", &[], - r#"mkdir -p "$WASM_POSIX_DEP_OUT_DIR" -printf '\x00asm\x01\x00\x00\x00\x01\x05\x01\x60\x00\x01\x7f\x03\x02\x01\x00\x07\x1a\x02\x0d__abi_version\x00\x00\x06_start\x00\x00\x0a\x06\x01\x04\x00\x41\x00\x0b' > "$WASM_POSIX_DEP_OUT_DIR/runtimebin.wasm" + &format!( + r#"{} printf canonical-runtime > "$WASM_POSIX_DEP_OUT_DIR/icu.dat""#, + emit_wasm_build_script("runtimebin.wasm", &minimal_executable_wasm()), + ), &[("runtimebin", "runtimebin.wasm")], ); append_program_runtime_file(&root, "runtimebin", "icu.dat", "/usr/lib/php/icu.dat"); @@ -36065,12 +36141,11 @@ commit = "1111111111111111111111111111111111111111" let canonical = first.canonical.unwrap(); let wasm = canonical.join("trustskip.wasm"); let original = fs::read(&wasm).unwrap(); + // Different bytes, same policy-valid shape: a custom section the + // artifact policy ignores. Rewriting the __abi_version body would + // fail validation before the generation check runs. let mut alternate = original.clone(); - let body = alternate - .windows(3) - .position(|window| window == [0x41, 0x00, 0x0b]) - .expect("minimal executable has an i32.const 0 body"); - alternate[body + 1] = 1; + alternate.extend([0x00, 0x05, 0x04, b'd', b'i', b'f', b'f']); // verify_cache = true: mutating the canonical entry after the first // authority capture is re-checked before projection and rejected. @@ -36165,12 +36240,11 @@ commit = "1111111111111111111111111111111111111111" let canonical = first.canonical.unwrap(); let wasm = canonical.join("coherent.wasm"); let original_wasm = fs::read(&wasm).unwrap(); + // Different bytes, same policy-valid shape: a custom section the + // artifact policy ignores. Rewriting the __abi_version body would + // fail validation before the generation check runs. let mut alternate_wasm = original_wasm.clone(); - let body_constant = alternate_wasm - .windows(3) - .position(|window| window == [0x41, 0x00, 0x0b]) - .expect("minimal executable has an i32.const 0 body"); - alternate_wasm[body_constant + 1] = 1; + alternate_wasm.extend([0x00, 0x05, 0x04, b'd', b'i', b'f', b'f']); let changed_output = tempdir("local-rebuild-coherent-output-changed-cache"); let error = resolve_local_build_package_node( diff --git a/tools/xtask/src/local_build.rs b/tools/xtask/src/local_build.rs index d0e5e500cc..037eedc254 100644 --- a/tools/xtask/src/local_build.rs +++ b/tools/xtask/src/local_build.rs @@ -4694,14 +4694,22 @@ mod tests { ); // A leaf nothing else depends on (directly or via a product) removes - // only itself. `ruby` is not in any package's `depends_on`, any - // product's `package_dependencies`/`root_mirror_packages`, or any - // product manifest's composition — verified against the checked-in - // registry and `local-supported.toml` when this test was written. - let leaf_only = clean_removal_set(&graph, &PlanNodeV1::package("ruby", "wasm32")); + // only itself. Read the leaf out of the graph's real node set instead + // of naming one: a named package stops being a leaf as soon as an image + // embeds it. Iterating the actual package nodes (rather than + // reconstructing them from names under a hardcoded arch) keeps the + // candidate honest for any planned target, so the leaf we assert on is + // one the graph truly contains. + let leaf = graph + .dependencies + .keys() + .filter(|node| matches!(node, PlanNodeV1::Package { .. })) + .find(|node| !graph.dependencies.values().any(|deps| deps.contains(node))) + .cloned() + .expect("the checked-in graph has a package nothing else depends on"); assert_eq!( - leaf_only, - BTreeSet::from([PlanNodeV1::package("ruby", "wasm32")]) + clean_removal_set(&graph, &leaf), + BTreeSet::from([leaf.clone()]) ); }