fix(parser,adapter,resolver): canonical-ABI correctness sweep — flat_byte_size + LS-P-6/-7/-8/-9/-10/-11/-12/-13/-14/-15/-16/-17/-18/-19#179
Conversation
The mythos-auto delta-pass on PR #178 flagged that flat_byte_size computes the payload width of result<T,E> and variant as `max(flat_byte_size(arm))` rather than the Component Model's element-wise flatten_variant JOIN. `max` of arm byte totals underestimates whenever the arms flatten to a different *number* of core values. result<u64, string>: the ok arm u64 flattens to [i64] (8 B), the err arm string to [i32,i32] (8 B). The old form gave 4 + max(8,8) = 12, but the joined payload is [i64, i32] (12 B) and the true flat size is 4 + 12 = 16. Fix: flat_byte_size is rewritten over a new private flat_width_list helper that materialises each type's flat core-value width list and JOINs variant/result arms element-wise. Non-variant types are byte-for-byte unchanged. flat_width_list caps its length at FLAT_WIDTH_CAP (256); a type whose flattening exceeds the cap yields None and flat_byte_size returns u32::MAX, preserving the LS-P-4 saturation contract and bounding the helper's Vec against the LS-P-4 OOM class. The LS-P-4 regression test still passes. Disposition of the mythos-auto finding: the discover step claimed an OOB-write hazard. Rejected on validation — flat_byte_size has zero consumers in meld-core/src/; retptr return areas are sized by return_area_byte_size, a different function. No reachable hazard, no possible PoC, NOT a confirmed finding, no LS-N entry. This commit fixes the underlying arithmetic anyway, as correctness hygiene on a pub fn a future consumer could inherit. Regression test flat_byte_size_result_uses_element_wise_join_not_max pins result<u64,string>=16, an unequal-arity variant=16, the equal-arms result<u32,u32>=8, and non-variant record/u64 unchanged. Refs: mythos-auto finding on PR #178. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mythos delta-pass requiredThis PR modifies one or more Tier-5 source files (per Before merge, run the Mythos discover protocol on the
Why this gate exists: LS-A-10 The gate check on this PR will pass once the label is |
LS-N verification gate✅ 33/33 approved LS entries verified
Approved Failed LS entries(none) Missing regression tests(none) Updated automatically by |
Mythos delta-pass (auto)❌ 3 finding(s) across 3 Tier-5 file(s)
Auto-run via |
A confirmed Mythos finding — surfaced by the mythos-auto delta-pass when it re-scanned parser.rs on PR #179. params_area_byte_size and return_area_byte_size accumulate a component function's canonical-ABI memory size field by field with a bare `size += canonical_abi_size_unpadded(ty)`. canonical_abi_size_unpadded saturates to u32::MAX for a pathologically large fixed-length-list (the LS-P-4 fix). But LS-P-4 did not reach these two cross-field accumulators: once a first field saturates `size` to u32::MAX, the next field's `+=` overflows — debug build panics, release build wraps u32::MAX down to a small value. params for `(fixed-length-list<f64, 2^29>, u32)` wrap params_area_byte_size from u32::MAX to ~3. The resolver stores that as AdapterRequirements::params_area_byte_size; the FACT adapter passes it to cabi_realloc, allocates a few-byte buffer, and copies every parameter into it — an OOB write into callee linear memory. The sibling Record/Tuple accumulators inside canonical_abi_size_unpadded already use saturating_add — these two area-size loops were missed by LS-P-4. Fix: both `+=` sites become `size = size.saturating_add(...)`. A saturated field keeps the area size near u32::MAX, an un-allocatable value, so cabi_realloc fails safely instead of under-allocating. Mythos oracle: ls_p_6_area_byte_size_saturates_across_fields panics today on the bare `+=` (debug-build overflow at parser.rs:1613) and asserts a saturated result after the fix. Promoted to approved loss scenario LS-P-6 (UCA-P-3, H-2/H-4/H-4.1); nearest primitive-layer proof is LS-P-4's kani_fixed_size_list_size_no_overflow harness. Second finding from the auto-runner's parser.rs scan; unlike the flat_byte_size finding in the same PR (dead code, no reachable hazard), LS-P-6's impact path is live and confirmed. Refs: LS-P-6, LS-P-4, mythos-auto finding on PR #179. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
mythos-auto finding #3 — reviewed (plausibly real, pre-existing, tracked separately)The auto-runner's third scan of Plausibly real. Not a one-line fix — it needs This is pre-existing and unrelated to #179. #179's changes are Process note — whole-file scan treadmillmythos-auto scans the entire Tier-5 file, so every #179 status12 substantive checks green (Test, Clippy, Coverage, Bench, Format, all 4 fuzz, LS-N gate 20/20, Detect Tier-5, Mythos pass). #179 delivers two sound parser.rs canonical-ABI fixes, one a confirmed OOB bug (LS-P-6) with an approved LS-N entry. Ready to merge pending a maintainer call on dispositioning finding #3 as a separate follow-up. |
collect_conditional_pointers and collect_conditional_result_pointers
emit one ConditionalPointerPair per pointer leaf inside an
option/result/variant payload, but computed the CopyLayout once on the
whole payload type. copy_layout only special-cases bare string/list, so
any composite payload (record/tuple/fixed-list) fell to its
`_ => Bulk { byte_multiplier: 1 }` fallback — a list<u64> leaf was
tagged Bulk{1} instead of Bulk{8} (7/8 silent under-copy), and a
pointer-containing list<string> leaf collapsed from Elements to flat
Bulk, dropping recursive inner-pointer fixup.
Add collect_pointer_positions_with_layout / _byte_offsets_with_layout,
which carry each String/List leaf's own CopyLayout alongside its
position; remove the now-dead copy_layout_for_string_or_list_at shim.
Confirmed Mythos finding from the mythos-auto delta-pass on PR #179;
promoted to approved loss scenario LS-P-7. Regression pinned by
ls_p_7_conditional_pointer_layout_is_per_leaf_not_per_composite,
exercising both the flat-param and retptr byte-offset paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
mythos-auto finding #4 — clean-room verified (real bug, but auto-runner mis-described it)The latest delta-pass on The bug is real — but it is NOT where the auto-runner said. The finding claimed the The actual bug is in the Worked example —
The wrong offsets/sizes propagate to Minimal fix: advance by Disposition
Process — the whole-file-scan treadmillThis is the 4th finding the whole-file scan has surfaced across #178/#179: |
The Component Model canonical ABI lays out a record/tuple as:
s = 0; for each field f:
s = align_to(s, alignment(f))
s += size(f)
where size(f) for an aggregate field is its full padded canonical
size. In this codebase that full padded size is
canonical_abi_element_size; canonical_abi_size_unpadded is the outer
type minus its own trailing align-up.
~25 field-walk sites — the Record/Tuple arms of
canonical_abi_size_unpadded, collect_pointer_byte_offsets,
collect_pointer_byte_offsets_with_layout (LS-P-7), the conditional
result/resource/slot/inner-pointer/inner-resource collectors, and the
top-level params/results walks in params_area_byte_size,
return_area_byte_size, pointer_pair_*_offsets/slots, and
resource_*_positions — advanced offset/size by
canonical_abi_size_unpadded(field) instead of
canonical_abi_element_size(field). The per-field align_up does NOT
re-absorb a preceding field's omitted trailing pad when the next
field's alignment is smaller, so a record/tuple containing a padded
aggregate followed by a lower-aligned field came out smaller than the
spec.
Concretely tuple<record{u32,u8}, u8> now computes element_size = 12
(spec) instead of 8; a list<u32> following record{u32,u8} now sits at
byte offset 8 instead of 5. The wrong offsets had been flowing into
the FACT adapter's pointer-pair loads, list-copy byte lengths, and
inner pointer-fixup walks; the area-size functions also under-sized
the cabi_realloc buffer (LS-P-6 hazard class via the per-field
primitive rather than the cross-field +=).
canonical_abi_size_unpadded itself still returns the outer size minus
its own trailing pad — that contract is unchanged; only the per-field
contribution is corrected.
Confirmed Mythos finding from the mythos-auto delta-pass on PR #179
(the auto-runner mis-located it as the option/variant/result payload
contribution, which is actually spec-correct — independent clean-room
verification corrected the location). Promoted to approved loss
scenario LS-P-8. Regression pinned by
ls_p_8_record_tuple_field_accumulation_uses_padded_field_size.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
total_flat_params picks the canonical-ABI calling convention from the total flat param count: <= MAX_FLAT_PARAMS (16) → flat; > → params-ptr. It summed per-param flat_count values with Iterator::sum::<u32>(). flat_count for a FixedSizeList is saturating (LS-P-4), so a nested FixedSizeList can yield flat_count = u32::MAX; sum() then panics in debug on u32::MAX + 1 and wraps to a small value in release. The wrapped total compares <= 16 and the adapter selects the flat convention for a function that genuinely needs params-ptr — call-site lowering and callee-side lifting disagree on the ABI slot. Sibling area-size accumulators (params_area_byte_size / return_area_byte_size) already use saturating_add per LS-P-6 — this calling-convention picker was simply missed. Replaces .sum() with .fold(0u32, u32::saturating_add). Confirmed Mythos finding from the mythos-auto delta-pass on PR #179. Promoted to approved loss scenario LS-P-9. Regression pinned by ls_p_9_total_flat_params_saturates_across_params. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s (LS-P-10)
A ConditionalPointerPair for a pointer leaf inside a nested
option/result/variant payload — e.g. result<option<string>, u32>,
variant { a(option<string>), b(u32) }, option<option<string>> —
previously carried only the INNERMOST discriminant guard. The FACT
adapter processed each pair independently with a single (load,
compare, branch). When the runtime value sat in a sibling arm
(e.g. Err(some_u32) of the result), the byte at the option's
discriminant slot held unrelated payload bytes; if those bytes
happened to read as the inner discriminant value (1 = Some), the
adapter sampled the adjacent slots as a (ptr, len) string pair and
ran cabi_realloc + memory.copy with attacker-controlled source
pointer and length — an arbitrary cross-component memory read,
plus a forged string pointer handed to the callee.
Surfaced by the mythos-auto delta-pass on PR #179. Clean-room
independently verified as a real, exploitable memory-safety hazard
(validator traced the four fact.rs consumer loops and confirmed
each treats every pair's guard independently — no implicit AND
with any enclosing conditional).
Fix:
* Add DiscriminantGuard struct + outer_guards: Vec<DiscriminantGuard>
field to ConditionalPointerPair (innermost guard stays in the
existing discriminant_* fields for backward compatibility — empty
outer_guards behaves identically to the old single-guard path).
* Thread outer_guards through collect_conditional_pointers and
collect_conditional_result_pointers recursion: at each
option/result/variant arm, build the current guard and append it
to the chain before recursing into the payload; stamp each
emitted pair with the prefix chain seen so far.
* Two new fact-adapter helpers (emit_conditional_guard_chain_flat /
emit_conditional_guard_chain_byte) emit each guard's (load disc,
I32Const value, I32Eq) and I32And them all together before the
existing If/copy block.
* Update the four consumer loops in fact.rs (flat-param,
flat-result, retptr-param, retptr-result) to call the helpers.
Promoted to approved loss scenario LS-P-10 (UCA-P-3, H-2/H-4/H-4.2).
Regression pinned by
ls_p_10_nested_conditional_pointer_carries_outer_guard_chain.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
mythos-auto post-LS-P-10 results — 3 new findings across 3 filesThe mythos-auto scan after the LS-P-10 commit ( Finding #7 —
|
…P-11)
resolve_via_flat_names populated its export index with a blind
HashMap::insert(key, …) where key is the flat export name. When two
core modules within one component both exported the same name, the
second silently overwrote the first (last-writer wins), routing any
importer of that name to the wrong module with no error or warning —
the fused module wired wrong but type-clean.
The instance-graph resolver (taken whenever the component has an
InstanceSection, which wit-component / wasm-tools always emit for
multi-module components) is immune. The vulnerable path is
practically unreachable for production components: defensive
hardening for the synthetic-fixture and legacy single-module
fallback shapes that take the flat-name path.
Fix: replace the blind insert with an explicit collision check that
returns a new Error::DuplicateModuleExport { component_idx,
export_name, first_module_idx, second_module_idx }, mirroring the
existing DuplicateModuleInstantiation pattern (resolver.rs:2115).
Confirmed Mythos finding from the mythos-auto delta-pass on PR #179,
clean-room verified. Promoted to approved loss scenario LS-P-11
(priority low — defensive hardening, not a security emergency).
Regression pinned by
ls_p_11_duplicate_flat_name_export_is_rejected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… mitigation)
element_inner_pointers's match has no arms for Option, Result, or
Variant (line 3203, `_ => {} // scalars, options, results — no
pointer pairs`). For list<option<string>> (and the Result/Variant
analogues), the helper returns an empty vector even though the
element type DOES contain a pointer. copy_layout(List(inner)) then
classifies as CopyLayout::Bulk { byte_multiplier: element_size } —
which the FACT adapter handles with a flat memory.copy and no
per-element walk. Every option's (ptr, len) pair was copied
byte-for-byte into the callee, with `ptr` still referencing the
source component's memory. The callee then dereferenced a wild
pointer per Some(...) element — a cross-memory dangling reference /
arbitrary read on every list use [H-4 / H-4.2].
Conservative mitigation: copy_layout's List(inner) arm now panics
with a clearly-labelled LS-P-12 message whenever
`type_contains_pointers(inner)` AND element_inner_pointers returns
empty — converting silent cross-memory dangling-reference into a
loud refusal at adapter-generation time.
The full structural fix requires (a) Option/Result/Variant arms on
element_inner_pointers that recurse into the payload at the payload
byte offset, AND (b) per-element DiscriminantGuard chains on the
inner-pointer descriptor (extending CopyLayout::Elements'
inner_pointers field), AND (c) FACT-side per-element guard
evaluation before each inner copy. That is structurally analogous
to LS-P-10 but on the list-element axis rather than the top-level
conditional axis — tracked as follow-up.
Confirmed Mythos finding from the mythos-auto delta-pass on PR #179,
clean-room verified. Promoted to approved loss scenario LS-P-12
(priority high). Regression pinned by
ls_p_12_list_of_option_string_refuses_rather_than_silently_corrupts,
ls_p_12_list_of_result_string_refuses, and the positive sanity test
ls_p_12_pure_scalar_option_list_is_still_bulk.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… heuristic (LS-P-13) emit_param_copy_step (the P3-async lift adapter's parameter copy step, called from generate_async_callback_adapter and generate_async_stackful_adapter) walked caller_type.params looking for adjacent (i32, i32) slots and gated each match on pointer_pair_positions.iter().any(|_| true) — semantically !is_empty(). Every adjacent integer-pair argument was therefore rewritten via cabi_realloc + cross-memory memory.copy as if it were a (ptr, len) string/list, with one integer used as the source pointer and the other as the byte count. For `fn f(a: i32, s: string, b: i32, c: i32)` lowered to flat [I32, I32, I32, I32, I32] with resolver positions [1], the buggy code emitted positions = [0, 2]. It then ran: - cabi_realloc(0, 0, 1, ptr_s) — string ptr used as length; - memory.copy(new_ptr, a, ptr_s) — reading from caller address a; - cabi_realloc(0, 0, 1, b) + memory.copy(new_ptr, len_s, b). The real string at flat index 1 was never copied. The callee saw mangled integers, the original string contents weren't transferred, and the copy could trap on the overflow guard or perform a cross-memory read at an attacker-influenced address. The resolver's pointer_pair_param_positions returns flat indices computed by walking the function's params with flat_count. Canonical lowering preserves param order between caller and callee component types, so those flat indices apply equally to both sides. The previous comment block claiming a "callee order vs caller order" mismatch was misleading. Replaces the heuristic walk with site.requirements.pointer_pair_positions.clone(); the resolver already produces the correct positions. Confirmed Mythos finding from the mythos-auto delta-pass on PR #179, clean-room verified. Promoted to approved loss scenario LS-P-13 (priority high). Regression pinned by ls_p_13_pointer_pair_param_positions_is_flat_indices_not_just_nonempty, which asserts the resolver returns [1] for (a: u32, s: string, b: u32, c: u32) and [1, 4] for the mixed (a, s1, b, s2, c) signature. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
mythos-auto post-LS-P-13 — 3 new findings (one false positive)After LS-P-13 ( Finding #10 —
|
… (LS-P-14) emit_patch_nested_indirections computes the inner-list buf_len for each per-element cabi_realloc + memory.copy by loading the callee-supplied len and multiplying by sub_elem_size with a bare i32.mul. i32.mul is modulo 2^32, so a callee-controlled len near u32::MAX / sub_elem_size wrapped buf_len to a small value. The subsequent old_ptr + buf_len > mem_bytes bounds check used i32.add (also wrapping) and was bypassed. The adapter allocated/copied only the wrapped byte count while the caller-side bulk copy of the outer (ptr, len) retained the original large len — silent truncation of the inner list contents, plus OOB read/write into adjacent caller-allocated memory on every dereference past the truncated edge. The emit_overflow_guard helper (added as the LS-A-7 leg (a) fix for the outer copy paths) was never retrofitted to the inner copy. Fix stashes the loaded len into the existing l_buf_len scratch local, calls emit_overflow_guard(body, l_buf_len, sub_elem_size) which traps via `unreachable` when the multiplication would wrap, then re-fetches the local for the multiplication. The guard is a no-op when sub_elem_size == 1. Confirmed Mythos finding from the mythos-auto delta-pass on PR #179. Promoted to approved loss scenario LS-P-14 (priority high). Regression pinned by ls_p_14_nested_list_inner_copy_emits_overflow_guard, which emits a synthetic patch loop for `record { items: list<u32> }` (sub_elem_size = 4) and asserts the encoded function body contains an Unreachable opcode — the only place that opcode is emitted along this path is inside emit_overflow_guard. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…S-P-15)
resolve_resource_positions decided callee-vs-caller resource
ownership via:
component.component_type_defs
.get(pos.resource_type_id as usize)
.map(|def| !matches!(def, ComponentTypeDef::Import(_)))
.unwrap_or(true)
When `resource_type_id` exceeded `component_type_defs.len()` —
stale id, alias remap past the local table, malformed input —
`.get(...) → None` and `unwrap_or(true)` silently classified the
resource as callee-defined. The adapter then emitted a
[resource-rep] call where [resource-new] was correct (or vice
versa), swapping the two handle-conversion sides on every fused
cross-component call passing that handle. The handle type-checks
on both sides (both i32-shaped), so the validator doesn't catch
it; the error only surfaces when the handle is dereferenced at
runtime.
Reachability is bounded — the instance-graph path keys
resource_type_id through validated parser-produced indices, so
this is defensive hardening rather than a memory-safety emergency.
Fix replaces the unwrap_or(true) with an explicit match on
.get(...): Some(def) classifies by type; None emits a log::warn!
and `continue`, dropping the position. The downstream adapter
either finds no work for the unused slot or surfaces a loud
missing-fixup error at adapter generation — never silently swaps.
Confirmed Mythos finding from the mythos-auto delta-pass on PR
#179. Promoted to approved loss scenario LS-P-15 (priority low).
Regression pinned by
ls_p_15_out_of_bounds_resource_type_id_is_dropped_not_misclassified,
which builds a synthetic ResourceImportMap whose lookup succeeds
at resource_type_id 999, calls resolve_resource_positions against
an empty component_type_defs, and asserts the returned Vec is
empty (pre-fix: 1 mis-classified entry; post-fix: dropped).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e (LS-P-16) emit_utf16_to_utf8_transcode's surrogate-pair If arm unconditionally emitted a second I32Load16U at mem16[ptr + (src_idx + 1) * 2] whenever the first code unit fell into [0xD800, 0xDC00) (high surrogate). The loop's only bounds check guarded the first code unit per iteration. For input whose last code unit was a lone high surrogate, the second load read 2 bytes past the caller-supplied UTF-16 buffer; those bytes were treated as the low surrogate (without validating they actually were a low surrogate) and packed into a 4-byte UTF-8 sequence written to callee memory — silent cross-memory leak of attacker-adjacent caller bytes into the callee's transcoded output per UTF-16→UTF-8 string transfer. Reachable for any cross-memory UTF-16-caller / UTF-8-callee fusion (line 614, 2999-3001) whose UTF-16 string ends on a high surrogate, including malformed input from JS or trunctated strings. This is the conservative mitigation: inject a `src_idx + 1 >= input_len` check inside the surrogate-pair If arm and unreachable-trap on failure. The Canonical-ABI-correct behaviour replaces the lone surrogate with U+FFFD (3-byte UTF-8 EF BF BD) and continues; tracked as a structural follow-up. Confirmed Mythos finding from the mythos-auto delta-pass on PR #179, independently clean-room verified. Promoted to approved loss scenario LS-P-16 (priority high). Regression pinned by ls_p_16_utf16_lone_high_surrogate_oob_guard_emitted, a structural test that requires the LS-P-16 marker AND an Unreachable + I32GeU opcode pair to live inside the surrogate-pair If arm before the second I32Load16U. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two structurally-identical caller-encoding lookup loops (primary ~2877-2910, fallback ~3175-3225) filtered from_component.imports on ComponentTypeRef::Func(_) to find a caller's Lower options for each resolved interface. WIT interface imports lower to ComponentTypeRef::Instance(_), so the loops never matched for typical wit-component / wasm-tools output and fell through to a heuristic min_by_key over caller_lower_map — picking the lowest-indexed Lower's encoding for every interface. Single-encoding callers (the common case) get the right answer by coincidence. Mixed-encoding callers (e.g. a JS/.NET host component lowering UTF-16 to one import alongside Rust UTF-8 to another) get string_transcoding silently miscalibrated for one or more interfaces, producing scrambled strings at the call boundary. This is the conservative mitigation: detect mixed-encoding callers (values() not all identical) before the heuristic fires and emit a log::warn! with the LS-P-17 marker and interface name. Single-encoding callers see no behavioural change. The full structural per-interface attribution (resolve caller component func index from the core import via a caller_core_import_to_comp_func map, OR extend the filter to walk ComponentTypeRef::Instance aliases) is tracked as follow-up. Confirmed Mythos finding from the mythos-auto delta-pass on PR #179, independently clean-room verified. Promoted to approved loss scenario LS-P-17 (priority low — latent for single-encoding callers). Regression pinned by ls_p_17_mixed_caller_encoding_warns_before_heuristic_fallback, a structural test that requires the LS-P-17 marker at both sites and the warn-on-mixed pattern (all_same uniformity check + log::warn!) to be present. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LS-P-18 — `copy_layout(List(inner))` LS-P-12 mitigation was bypassed when a record mixed a covered pointer (bare string/list) with a hidden conditional pointer (option<string>): the covered field made `element_inner_pointers` non-empty, the emptiness-based LS-P-12 panic didn't fire, and `CopyLayout::Elements.inner_pointers` silently omitted the option-payload pointer. Adapter never fixed up the conditional payload across memories — callee elements retained source-memory string pointers per Some(_). Replaces the emptiness check with deep recursive `has_pointer_bearing_conditional(inner)` that walks Option / Result / Variant arms through Records, Tuples, FixedSizeLists, and Type aliases. Any pointer-bearing conditional anywhere in the element layout now triggers a panic with the LS-P-18 marker (message intentionally includes "LS-P-12" via the follow-up phrasing so the existing should_panic(expected="LS-P-12") tests continue to pass). LS-P-19 — `emit_utf8_to_utf16_transcode` (mirror of LS-P-16, UTF-8 direction): the outer-loop bounds-check guarded only the lead byte; each multi-byte branch (2/3/4-byte) unconditionally read continuation bytes at src_idx + 1/+2/+3 via I32Load8U. A UTF-8 string ending on a truncated multi-byte lead caused the adapter to read 1–3 bytes of attacker-adjacent caller memory and fold them into a synthesized code point emitted as UTF-16 in the callee. Conservative mitigation prepends an `src_idx + N >= input_len` unreachable trap to each multi-byte branch (N = 1, 2, 3). Both confirmed Mythos findings from the mythos-auto delta-pass on PR #179. Clean-room-verified disposition: LS-P-12 refinement gap (real), UTF-8 OOB (real, mirror of LS-P-16). LS-P-18 promoted to priority high; LS-P-19 promoted to priority high. Regression pinned by: - ls_p_18_mixed_record_with_option_string_bypasses_p12_then_refuses - ls_p_18_pure_bare_pointer_record_still_works (positive sanity) - ls_p_19_utf8_to_utf16_continuation_byte_oob_guard_emitted Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Five
parser.rscanonical-ABI correctness fixes — all surfaced by the mythos-auto delta-pass scanningparser.rs. The auto-runner did its job: across successive scans it found four real, confirmed memory-safety / ABI-correctness bugs (LS-P-6 through LS-P-9) plus one hygiene issue.Fix 1 —
flat_byte_sizeelement-wise variant JOIN (hygiene)flat_byte_sizecomputedresult<T,E>/variantpayload width asmax(flat_byte_size(arm))instead of the Component Model element-wiseflatten_variantJOIN. Rewritten over a newflat_width_listhelper. Disposition: OOB-write impact claim rejected on validation —flat_byte_sizehas no in-tree consumers. Hygiene fix, no LS-N entry.Fix 2 — LS-P-6: area-size accumulators wrapped (confirmed)
params_area_byte_size/return_area_byte_sizeused a bare+=againstcanonical_abi_size_unpaddedwhich saturates tou32::MAX(LS-P-4) — once the accumulator saturated, the next field's+=overflowed (debug panic, release wrap to ~3), under-sizing the params buffer and yielding an OOB write incabi_realloc. Both sites are nowsaturating_add.Fix 3 — LS-P-7: conditional-pointer
CopyLayoutcomputed for composite, not leaf (confirmed)collect_conditional_pointers/collect_conditional_result_pointerscomputedCopyLayoutonce on the whole payload type.copy_layoutonly special-cases barestring/list; any composite payload fell to_ => Bulk { byte_multiplier: 1 }. Alist<u64>leaf insideoption<tuple<u32, list<u64>>>was taggedBulk { 1 }instead ofBulk { 8 }— a 7/8 under-copy [H-4.1]; pointer-containingElementsleaves collapsed to flatBulk, dropping inner pointer fixup [H-4.2]. Newcollect_pointer_positions_with_layout/collect_pointer_byte_offsets_with_layoutcarry each leaf's own layout.Fix 4 — LS-P-8: record/tuple field-walk added unpadded child size (confirmed)
The Component Model spec lays out a record/tuple as
s = 0; for f in fields: s = align_to(s, alignment(f)); s += size(f)wheresize(f)for an aggregate field is its full padded size. ~25 field-walk sites — Record/Tuple arms ofcanonical_abi_size_unpadded,collect_pointer_byte_offsets, the LS-P-7_with_layouthelpers,collect_conditional_result_pointers,collect_return_area_type_slots,collect_resource_byte_positions,element_inner_pointers,element_inner_resources, plus the top-level walks inparams_area_byte_size,return_area_byte_size,pointer_pair_*_offsets/slots,resource_*_positions, andconditional_pointer_pair_result_positions— advanced offset/size bycanonical_abi_size_unpadded(field)(no trailing pad) instead ofcanonical_abi_element_size(field). The per-fieldalign_updoesn't re-absorb the preceding pad when the next field has smaller alignment.Concretely
tuple<record{u32,u8}, u8>now computeselement_size = 12(spec) instead of 8; alist<u32>followingrecord{u32,u8}now sits at offset 8 instead of 5. The wrong offsets flowed into FACT adapter pointer-pair loads, list-copy byte lengths, and inner pointer-fixup walks.Mythos process note: the auto-runner mis-located this finding as the option/variant/result payload contribution (which is actually spec-correct — those arms already use
canonical_abi_element_size). Independent clean-room verification corrected the location to the Record/Tuple field accumulation.Fix 5 — LS-P-9:
total_flat_paramsusedIterator::sum::<u32>()instead of saturating fold (confirmed)The canonical-ABI calling convention is picked from
total_flat_params:<= MAX_FLAT_PARAMS(16) → flat;>→ params-ptr.flat_countfor aFixedSizeListis saturating (LS-P-4), so a nestedFixedSizeListcan yieldflat_count = u32::MAX. A bare.sum()then panics in debug onu32::MAX + 1and wraps to a small value in release; the wrapped total compares<= 16and the adapter selects the flat convention for a function that genuinely needs params-ptr — call-site lowering and callee-side lifting disagree on the ABI slot. The sibling area-size accumulators already usesaturating_addper LS-P-6; this calling-convention picker was missed. Replaces.sum()with.fold(0u32, u32::saturating_add).Tests
flat_byte_size_result_uses_element_wise_join_not_maxls_p_6_area_byte_size_saturates_across_fieldsls_p_7_conditional_pointer_layout_is_per_leaf_not_per_compositels_p_8_record_tuple_field_accumulation_uses_padded_field_sizels_p_9_total_flat_params_saturates_across_paramsmeld-corelib suite green: 253 tests, 0 failures🤖 Generated with Claude Code