fix(parser,adapter): canonical-ABI correctness — LS-P-6 through LS-P-10#179
fix(parser,adapter): canonical-ABI correctness — LS-P-6 through LS-P-10#179avrabe wants to merge 6 commits into
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✅ 24/24 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>
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