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
32 changes: 30 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2401,6 +2401,28 @@ jobs:
python scripts/oracle_run.py scripts/repro/imported_memory_size_1584_differential.py | tee impsize1584.txt
grep -Eq '^#1584 compiled=28 refused=14 compared=[1-9][0-9]{2,}$' impsize1584.txt
grep -q '^PASS' impsize1584.txt
- name: "Run RQ-91-COMPEXPORTS component-input refusal oracle (#1191 — every backend x mode, controls EXECUTED vs wasmtime)"
# A WebAssembly COMPONENT was decoded as ONE flattened core module
# (`Parser::parse_all` descends into every nested module): rc=0 on
# every backend, exporting internal interfaces, omitting declared
# ones, leaving internal wiring undefined, and binding `g` to the
# WRONG body (2 under unicorn vs 101 from wasmtime's component
# runtime). Five component shapes x six modes (+ the .wat input) must
# now REFUSE naming #1191 and `meld fuse`, writing no output; the
# declared interface is derived from wasmtime's component type. The
# CORE-module controls (shape 4's twin and its frozen meld 0.9.0
# fused module) still compile and EXECUTE to wasmtime's values.
# Red-first: `--expect-red` passes on 6b697bcf (35 red cases, 5
# symbol-table mismatches, g() = 2). Lives here because this job
# installs wasmtime + unicorn + pyelftools and builds the default
# (riscv-featured) synth. `set -euo pipefail` per #890.
run: |
set -euo pipefail
SYNTH=./target/debug/synth \
python scripts/oracle_run.py scripts/repro/component_input_1191_differential.py | tee compin1191.txt
grep -Eq '^refusals: 35$' compin1191.txt
grep -Eq '^executions: [4-9]$' compin1191.txt
grep -q '^RESULT: PASS$' compin1191.txt
- name: Run #851 v0.53 op-surface oracle (select x4 types + wrap/extends + drop/nop + memory.size/grow)
# The VCR-SEL-005 third-backend closes: select (CSEL/FCSEL) incl.
# NaN/-0 carry, wrap/extends with POISONED upper argument bits (the
Expand Down Expand Up @@ -4997,7 +5019,13 @@ jobs:
# one exposed a real D2 shape (an i64 local.set pops one slot, so an
# i32 result under it returned the pair's low half), now a pinned
# case of stack_top_return_rq91_differential.py. `hits: 0`.
grep -Eq '^functions audited: 7739$' haa1189.txt
# RQ-91-COMPEXPORTS (#1191, v0.91), re-derived on the tree rebased onto
# 5dc4e286: 7739 -> 7745 and (below) 244173 -> 244183; homes 9312 unchanged.
# CORPUS GROWTH ONLY: with the one new CORE fixture component_1191_twin.wat
# held aside, this branch's binary prints main's own 7739 / 9312 / 244173.
# The five new COMPONENT fixtures contribute 0: they are refused before
# selection. `hits: 0`.
grep -Eq '^functions audited: 7745$' haa1189.txt
grep -Eq '^homes watched: 9312$' haa1189.txt
# RQ-71-STACKDEPTH: this figure is now ASSERTED, not narrated. It sat in
# the #1321 comment above as "217081 attributed instructions ...
Expand All @@ -5013,7 +5041,7 @@ jobs:
# `break-inner` +4 and loop.wast `break-inner` +5 per leg, nothing
# else. Both still return 0xf / 0x1f executed. functions audited
# 7307, homes watched 8370 and hits 0 are UNCHANGED.
grep -Eq '^attributed instructions: 244173 ' haa1189.txt
grep -Eq '^attributed instructions: 244183 ' haa1189.txt
# POTENCY: the detector still fires, on this binary, through the
# same decline / needle / parse path — per (fixture, leg) EXACTLY
# 58/7/58/7 (i32 params r0-r3), 11/9/11/9 (promoted locals r4-r8),
Expand Down
38 changes: 37 additions & 1 deletion artifacts/release-v0.91/RQ-91-COMPEXPORTS.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ artifacts:
- id: RQ-91-COMPEXPORTS
type: system-req
release: v0.91
status: proposed
status: implemented
title: "A relocatable compile of a component emits the exports it declares and only its real imports, or refuses"
description: >
gale's ask, ranked after #1145. On a component, --relocatable
Expand Down Expand Up @@ -59,6 +59,42 @@ artifacts:
issue: "#1191"
verification-track: manual
issue-scope: closes
landed: "PR #1687 - #1191 - DELIVERED as a LOUD REFUSAL, not a lowering. The refuting command was RUN FIRST on main 6b697bcf and the defect was LIVE (export half: defines t:c/sched#state, nothing for the declared t:c/time#now; rc 0, no warning). Root cause, not specific to --relocatable: both decode functions in crates/synth-core/src/wasm_decoder.rs walked the input with Parser::parse_all, which descends into every nested core module of a component, and had no Payload::Version arm, so a component was decoded as ONE module whose function/import/export index spaces were the concatenation of its nested modules'. Module ORDER decides which symptom shows, so the import half the planning toy missed DOES reproduce on other shapes (an internal `U now`; `U __cabi_arena_realloc` for a component declaring no imports), and on a two-module shape the export name binds to the WRONG BODY (silent wrong code). Fix: `synth_core::refuse_component_encoding` (wasmparser's Parser::is_component), called first in decode_wasm_module and decode_wasm_functions and in the CLI ahead of loom/arena_bind on all three input paths; the diagnostic names the hazard, cites #1191 and gives the `meld fuse` route. NEWLY REFUSED: every component-encoded input (binary layer 1 or component text) on every backend and mode; 0 PRE-EXISTING in-repo inputs are components (measured on 6b697bcf). This change adds 5 component fixtures, refused by design: they are listed in arm_corpus_sweep_973.py REFUSED_BY_DESIGN, the self-contained boot sweep counts them as declines (+10), and the home-alias audit corpus does not see them (its pinned counts move only by the new CORE twin fixture, +6 functions / +10 attributed, decomposed by isolation). The acceptance ladder was NOT re-measured: /Volumes/Work/corpora/wasm-243 was not mounted, so v0.62's 6/5/5-of-112 component accepts (arm/riscv/aarch64) are EXPECTED to become declines and re-measurement is pending until the corpus is mounted (noted beside the ladder header)."
verified-by: >
DELIVERED, execution-verified red and refusal-verified green, by
scripts/repro/component_input_1191_differential.py (wired in the
aarch64-oracle job). The declared interface of each fixture is
DERIVED from wasmtime 48's component type, and each declared
export's value comes from wasmtime's component runtime. RED on
6b697bcf (`--expect-red` PASS): 35 of 35 shape x mode cases
accepted or wrong (5 shapes x 6 modes + the .wat input) - every
one rc 0 except the single-function path, which cannot find a
declared export (shapes 1, 2), and shape 5's self-contained path,
which fails with a misleading "#418 internal" arena error; 5 of 5
symbol tables disagree with the declared interface; shape 4's
`g`, EXECUTED under unicorn, returns 2 where wasmtime's component
runtime returns 101. GREEN on the branch: 35 of 35 refused, rc 1,
naming #1191 and `meld fuse`, no output written. Paired controls,
both trees: shape 4's core-module twin and its frozen meld 0.9.0
fused module (sha256 de1ed50a..., its attestation's input hash
equal to shape 4's assembled bytes) compile and EXECUTE f/g to
wasmtime's 1/101, and both objects are byte-identical between
6b697bcf and the branch. Crossed: the green assertions FAIL on
6b697bcf (0 refusals) and `--expect-red` FAILS on the branch (0
red cases). Mutants, each restored after: the helper made
always-Ok reds the oracle (0 of 35) and 2 of the 3 new
synth-core unit tests; the check removed from
decode_wasm_functions only reds
`component_1191_refused_by_decode_wasm_functions` (the oracle
cannot reach that function in the default build); the three CLI
calls removed reds the oracle on shape 5 arm-self-contained (34
of 35). The oracle declares `ci-checks: emulations >= 4` (4
measured via oracle_run.py: f/g of the twin and of the fused
module); under RQ-91-EMUFLOOR's per-oracle protocol it is held
against the base tree by `oracle_wiring_check.py --floor-base`,
and no summed pin moves. The home-alias audit corpus's exact
pins move by the twin fixture alone (+6 functions, +10
attributed), decomposed by isolation.
done-when: >
manual:2027-03-31 a relocatable compile of a component emits a
defined symbol for every function of every interface the
Expand Down
9 changes: 9 additions & 0 deletions crates/synth-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2210,6 +2210,10 @@ fn compile_command(
file_bytes
};

// RQ-91-COMPEXPORTS (#1191): refuse a component BEFORE loom and
// arena_bind read the bytes, so the refusal is what the user sees.
synth_core::refuse_component_encoding(&wasm_bytes)?;

// Run Loom WASM optimizer if --loom is enabled
let wasm_bytes = maybe_run_loom(loom, wasm_bytes)?;

Expand Down Expand Up @@ -4622,6 +4626,7 @@ fn compile_all_exports(
let mut merged_type_arg_counts: Vec<u32> = Vec::new();

for (idx, wasm_bytes) in module_binaries.iter().enumerate() {
synth_core::refuse_component_encoding(wasm_bytes)?; // #1191
// Run Loom optimizer on each module if --loom is enabled
let wasm_bytes = maybe_run_loom(loom, wasm_bytes.clone())?;
// First decoded module is the SBOM default; refined below to
Expand Down Expand Up @@ -4855,6 +4860,10 @@ fn compile_all_exports(
file_bytes
};

// RQ-91-COMPEXPORTS (#1191): refuse a component BEFORE loom and
// arena_bind read the bytes, so the refusal is what the user sees.
synth_core::refuse_component_encoding(&wasm_bytes)?;

// Run Loom optimizer if --loom is enabled
let wasm_bytes = maybe_run_loom(loom, wasm_bytes)?;

Expand Down
2 changes: 1 addition & 1 deletion crates/synth-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub use sbom::{CycloneDxSbom, SbomInputs};
pub use target::*;
pub use wasm_decoder::{
CallIndirectGuards, DecodedModule, ElemSegmentInfo, FunctionOps, ImportEntry, ImportKind,
TableGuards, WasmMemory, decode_wasm_functions, decode_wasm_module,
TableGuards, WasmMemory, decode_wasm_functions, decode_wasm_module, refuse_component_encoding,
};
pub use wasm_op::{
ParamBlockLowering, WasmOp, arm_param_block_lowering, count_params_heuristic,
Expand Down
84 changes: 84 additions & 0 deletions crates/synth-core/src/wasm_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,37 @@ pub fn canonicalize_types(
}

/// Decode a WASM binary and extract functions, memory, and data segments
/// RQ-91-COMPEXPORTS (#1191): refuse a WebAssembly COMPONENT (binary
/// layer one) loudly. synth lowers core modules only, and both decode functions walk
/// the input with `Parser::parse_all`, which descends into every nested core
/// module of a component and concatenates them into ONE function/import/
/// export index space. The result was accepted with exit 0 on every backend
/// and mode: an object exporting a component's INTERNAL interfaces, omitting
/// the ones it declares, leaving internal wiring as undefined symbols, and —
/// once two modules' indices collide — binding an export name to the WRONG
/// function body (measured: `g` returns 2 where wasmtime's component runtime
/// returns 101). Canonical-ABI options (string encoding, realloc,
/// post-return) are never honoured either, so no component is refused
/// selectively: the supported route is to fuse it into a core module first.
pub fn refuse_component_encoding(wasm_bytes: &[u8]) -> Result<()> {
if Parser::is_component(wasm_bytes) {
anyhow::bail!(
"input is a WebAssembly COMPONENT (component-model binary, layer 1), \
not a core module: synth lowers core modules only. Decoding a \
component as one module merges its nested core modules' index \
spaces — the object would export the component's internal \
interfaces instead of the ones it declares, leave internal wiring \
as undefined symbols, and can bind an export to the wrong function \
body; refusing (#1191). Fuse the component into a single core \
module first (`meld fuse <component.wasm> -o fused.wasm`) and \
compile that."
);
}
Ok(())
}

pub fn decode_wasm_module(wasm_bytes: &[u8]) -> Result<DecodedModule> {
refuse_component_encoding(wasm_bytes)?;
let mut functions = Vec::new();
let mut memories = Vec::new();
let mut imported_memories: Vec<WasmMemory> = Vec::new();
Expand Down Expand Up @@ -1869,6 +1899,7 @@ fn apply_name_section(functions: &mut [FunctionOps], names: &HashMap<u32, String

/// Decode a WASM binary and extract all function bodies as WasmOp sequences
pub fn decode_wasm_functions(wasm_bytes: &[u8]) -> Result<Vec<FunctionOps>> {
refuse_component_encoding(wasm_bytes)?;
let mut functions = Vec::new();
let mut func_index = 0u32;
let mut num_imported_funcs = 0u32;
Expand Down Expand Up @@ -3046,6 +3077,59 @@ fn convert_operator(op: &wasmparser::Operator) -> Option<WasmOp> {

#[cfg(test)]
mod tests {

// RQ-91-COMPEXPORTS (#1191): shape 4 of the lane's oracle — two core
// modules in one component. Before the refusal, the flattened decode
// named the body `i32.const 2` as `g` (wasmtime: g() = 101).
const COMPONENT_1191: &str = r#"(component
(core module $A (func (export "f") (result i32) i32.const 1))
(core module $B
(import "x" "f" (func $f (result i32)))
(func (result i32) i32.const 2)
(func (export "g") (result i32) call $f i32.const 100 i32.add))
(core instance $a (instantiate $A))
(core instance $b (instantiate $B (with "x" (instance $a))))
(func $g (result u32) (canon lift (core func $b "g")))
(export "g" (func $g)))"#;

#[test]
fn component_1191_refused_by_decode_wasm_module() {
let wasm = wat::parse_str(COMPONENT_1191).expect("component parses");
let err = decode_wasm_module(&wasm).expect_err("a component must be refused");
let msg = format!("{err:#}");
assert!(msg.contains("WebAssembly COMPONENT"), "{msg}");
assert!(msg.contains("#1191"), "{msg}");
assert!(msg.contains("meld fuse"), "{msg}");
}

#[test]
fn component_1191_refused_by_decode_wasm_functions() {
let wasm = wat::parse_str(COMPONENT_1191).expect("component parses");
let err = decode_wasm_functions(&wasm).expect_err("a component must be refused");
assert!(format!("{err:#}").contains("#1191"));
}

#[test]
fn component_1191_core_module_twin_still_decodes() {
// Paired control: the same bodies as ONE core module are accepted,
// and `g` is the body that calls `f` and adds 100.
let wasm = wat::parse_str(
r#"(module
(func $f (export "f") (result i32) i32.const 1)
(func (result i32) i32.const 2)
(func (export "g") (result i32) call $f i32.const 100 i32.add))"#,
)
.expect("core module parses");
assert!(refuse_component_encoding(&wasm).is_ok());
let m = decode_wasm_module(&wasm).expect("core module decodes");
let g = m
.functions
.iter()
.find(|f| f.export_name.as_deref() == Some("g"))
.expect("g exported");
assert_eq!(g.index, 2);
assert!(g.ops.contains(&WasmOp::Call(0)), "{:?}", g.ops);
}
use super::*;

// ---- #1441: the declared page size, and the TWIN (initial/max) ----------
Expand Down
14 changes: 14 additions & 0 deletions docs/status/ACCEPTANCE_LADDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ integrity was re-verified in the same session, with the paths relative to
shasum -a 256 -c ../MANIFEST.sha256` exited 0 with **243 `: OK` lines** (the
positive control) and 0 failures. Raw outputs: RQ-88-LADDER10.

> **COMPONENT INPUTS NOW DECLINE — RE-MEASUREMENT PENDING (noted 2026-11-08,
> RQ-91-COMPEXPORTS, #1191).** As of this change, every component-encoded input
> (binary layer 1, or component text) is refused at MODULE LEVEL, on every
> backend and mode, before decode: synth had been decoding a component as one
> flattened core module, which exported internal interfaces, omitted declared
> ones and, on a measured shape, bound an export to the wrong function body.
> The last per-stratum figures on record, v0.62's (RQ-62-REACH, `ae92a35f`),
> accepted **6 / 5 / 5 of the 112 components** on arm / riscv / aarch64; those
> are EXPECTED to become declines, which lowers the rungs above by up to that
> many. This is NOT a measured figure: the rungs in this table were NOT
> re-measured for this change, because `/Volumes/Work/corpora/wasm-243` was not
> mounted. Re-measurement is pending until it is; until then, cite the table
> above WITH this note.

> **THE CORPUS IS EXTERNAL TO THIS REPO, AND IT IS REACHABLE.** `corpora/` is not
> tracked here; the corpus lives at `/Volumes/Work/corpora/wasm-243`, on a work
> volume of the release machine that is not always mounted (mounted 2026-10-20;
Expand Down
Loading
Loading