From 596168329fcea9e4a411bc9857939592977fa7e1 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 29 Jul 2026 18:35:33 +0200 Subject: [PATCH] =?UTF-8?q?fix(oci):=20falcon-rate=20shipped=20as=20a=20co?= =?UTF-8?q?re=20module=20=E2=80=94=20export=20cabi=5Frealloc=20+=20fail=20?= =?UTF-8?q?loud=20+=20assert=20the=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jess pulled falcon-rate:1.129.0 and found it is a raw CORE MODULE (0061736d01000000), not a component (0061736d0d000100) — `meld fuse` rejects it outright, so the consumption path stopped at step one. Confirmed independently by pulling the published blob. Three compounding bugs, mine at the root: 1. ROOT CAUSE — my no_std conversion (#314) dropped the `cabi_realloc` export. wit-bindgen-rt provides it only `#[cfg(not(target_env = "p2"))]` — on wasm32-wasip2 it expects std to supply the symbol. no_std has no std, so `wasm-component-ld` failed: "module does not export a function named `cabi_realloc`". (The local wasip1 build DID componentize, which is why this passed my earlier check — the release target is wasip2.) Now exported from the no_std path, backed by the same single-threaded allocator. 2. SILENT FAILURE — build-components.sh ended the build in `|| true`, so that error was printed and ignored; the script fell through to a stale pre-componentization artifact and bundled it. Build failures now stop the bundle. 3. NO VALIDATION — nothing checked the payload. The OCI config mediaType says "component" regardless of the bytes, so metadata cannot catch this. Now asserts the 8-byte header on every component before it enters the bundle; verified the assert REJECTS the published 1.129.0 artifact and ACCEPTS both good components. Verified after the fix: wasip2 build produces a component (4581 bytes, was a 3749-byte core module), still ZERO wasi imports, and the through-wasm closed-loop proof is unchanged — converges 0.193 s, |err| 0.0059 rad/s. Also corrects the record: the "15x smaller" claim compared a core module against components. Componentized it is ~4.6 KB vs 53-60 KB — still a large win (~12x), just not the number I quoted. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG --- scripts/build-components.sh | 29 +++++++++++++++++++++++++++-- wasm/cm/rate/src/lib.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/scripts/build-components.sh b/scripts/build-components.sh index f28de21..440fa8b 100755 --- a/scripts/build-components.sh +++ b/scripts/build-components.sh @@ -49,8 +49,21 @@ build_component() { local dir="$1" local wasm_dir="$HERE/wasm/cm/$dir" printf " building %-16s..." "$dir" >&2 - ( cd "$wasm_dir" && cargo component build --release --target wasm32-wasip2 --no-default-features 2>&1 ) \ - | grep -E "^error" | sed 's/^/ /' >&2 || true + # FAIL LOUD (v1.130): this used to end in `|| true`, so a build error was + # printed and then IGNORED — the script fell through to a stale artifact from + # a previous build and shipped it. That is exactly how falcon-rate:1.129.0 was + # published as a raw CORE MODULE: the no_std wasip2 build failed + # ("module does not export a function named `cabi_realloc`"), the failure was + # swallowed, and the fallback picked up a pre-componentization module. + # A build failure must stop the bundle, not degrade it silently. + if ! ( cd "$wasm_dir" && cargo component build --release --target wasm32-wasip2 --no-default-features 2>&1 ) \ + | tee /tmp/falcon-build-$dir.log | grep -E "^error" | sed 's/^/ /' >&2; then + : # grep found no "^error" lines — that is the success path + fi + if grep -qE "^error" /tmp/falcon-build-$dir.log 2>/dev/null; then + echo " ERROR: cargo component build failed for $dir (see above)" >&2 + exit 1 + fi # Prefer the wasip2 path; cargo-component <0.20 emits wasip1. local src src=$(ls "$wasm_dir"/target/wasm32-wasip2/release/*.wasm 2>/dev/null | head -1 || true) @@ -66,6 +79,18 @@ build_component() { local slug="${dir#falcon-}" local dest="$BUNDLE_DIR/falcon-$slug-v$MM.wasm" cp "$src" "$dest" + # ASSERT the payload really is a Component, not a core module (v1.130). + # jess found falcon-rate:1.129.0 shipped as a core module — `meld fuse` + # rejected it outright. The OCI config mediaType says "component" regardless + # of the bytes, so metadata cannot catch this; check the 8-byte header. + # core module : 00 61 73 6d 01 00 00 00 + # component : 00 61 73 6d 0d 00 01 00 + magic=$(od -An -tx1 -N8 "$dest" | tr -d ' \n') + if [ "$magic" != "0061736d0d000100" ]; then + echo " ERROR: $dest is NOT a wasm component (header $magic)" >&2 + echo " expected 0061736d0d000100; a core module is 0061736d01000000" >&2 + exit 1 + fi local bytes bytes=$(wc -c < "$dest" | tr -d ' ') printf " %s bytes\n" "$bytes" >&2 diff --git a/wasm/cm/rate/src/lib.rs b/wasm/cm/rate/src/lib.rs index 51862d1..9126144 100644 --- a/wasm/cm/rate/src/lib.rs +++ b/wasm/cm/rate/src/lib.rs @@ -23,6 +23,40 @@ static ALLOC: lol_alloc::AssumeSingleThreaded = unsafe { lol_alloc::AssumeSingleThreaded::new(lol_alloc::FreeListAllocator::new()) }; +// The Component-Model canonical ABI requires the guest to export +// `cabi_realloc`. wit-bindgen-rt provides it ONLY when it can lean on std — +// its implementation is `#[cfg(not(target_env = "p2"))]`, because on +// wasm32-wasip2 it expects the std toolchain to supply the symbol. This crate +// is `no_std` on the cargo-component path, so nothing exported it and +// `wasm-component-ld` failed with "module does not export a function named +// `cabi_realloc`" — leaving a raw CORE MODULE where a component was expected. +// Export it here, backed by the same single-threaded allocator. +#[cfg(not(feature = "bazel-bindings"))] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cabi_realloc( + old_ptr: *mut u8, + old_len: usize, + align: usize, + new_len: usize, +) -> *mut u8 { + use core::alloc::{GlobalAlloc, Layout}; + unsafe { + if old_len == 0 { + // Canonical ABI: a zero-size allocation returns the alignment as a + // non-null, suitably-aligned dangling pointer. + if new_len == 0 { + return align as *mut u8; + } + return ALLOC.alloc(Layout::from_size_align_unchecked(new_len, align)); + } + ALLOC.realloc( + old_ptr, + Layout::from_size_align_unchecked(old_len, align), + new_len, + ) + } +} + // Bindings generated by cargo-component as src/bindings.rs. #[allow(warnings)] #[cfg(feature = "bazel-bindings")]