Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions host/test/kernel-blocking-retry-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -675,7 +675,7 @@ describe("blocking retry snapshot contract", () => {
]);

const fromSyscall = BLOCKED_RETRY_SOURCE.match(
/pub\(crate\) fn from_syscall\(syscall: u32\) -> Result<Self, Errno> \{([\s\S]*?)\n \}/,
/pub fn from_syscall\(syscall: u32\) -> Result<Self, Errno> \{([\s\S]*?)\n \}/,
)?.[1];
expect(fromSyscall, "BlockingRetryOperation::from_syscall").toBeDefined();
const targetedFamilies = Array.from(
Expand Down
2 changes: 1 addition & 1 deletion host/test/kernel-scratch-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
19 changes: 7 additions & 12 deletions host/test/privileged-projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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");
Expand All @@ -221,16 +218,15 @@ describe("privileged product publication", () => {
sources: source.sources,
writableBottleFileSystems: [sharedAlias],
})).resolves.toBeUndefined();
expect(() => new VirtualPlatformIO([{
mountPoint: "/",
expect(() => resolveMountSetIdCapability({
backend: candidate,
readonly: true,
setIdCapability: {
kind: "trusted-root-product",
guestWritable: false,
stableExecutableIdentity: true,
},
}], new NodeTimeProvider())).toThrow(/immutable product backend/i);
})).toThrow(/immutable product backend/i);
expect(Reflect.has(
privilegedProjectionModule,
"admitPrivilegedProgramProductCandidate",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -494,16 +490,15 @@ 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: {
kind: "trusted-root-product",
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 () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/registry/vim/build-vim.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down
4 changes: 4 additions & 0 deletions packages/registry/vim/build.toml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions scripts/ci-vitest-evidence-classes.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
114 changes: 94 additions & 20 deletions tools/xtask/src/build_deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -21890,6 +21922,12 @@ wasm = "second.wasm"
}

fn wasm_exporting_names(names: &[&str]) -> Vec<u8> {
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<u8> {
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]));
Expand All @@ -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
}

Expand Down Expand Up @@ -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] };
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand All @@ -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")],
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
22 changes: 15 additions & 7 deletions tools/xtask/src/local_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()])
);
}

Expand Down