Skip to content

fix(parser,adapter): canonical-ABI correctness — LS-P-6 through LS-P-10#179

Open
avrabe wants to merge 6 commits into
mainfrom
fix/flat-byte-size-variant-join
Open

fix(parser,adapter): canonical-ABI correctness — LS-P-6 through LS-P-10#179
avrabe wants to merge 6 commits into
mainfrom
fix/flat-byte-size-variant-join

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 22, 2026

Summary

Five parser.rs canonical-ABI correctness fixes — all surfaced by the mythos-auto delta-pass scanning parser.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_size element-wise variant JOIN (hygiene)

flat_byte_size computed result<T,E> / variant payload width as max(flat_byte_size(arm)) instead of the Component Model element-wise flatten_variant JOIN. Rewritten over a new flat_width_list helper. Disposition: OOB-write impact claim rejected on validation — flat_byte_size has 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_size used a bare += against canonical_abi_size_unpadded which saturates to u32::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 in cabi_realloc. Both sites are now saturating_add.

Fix 3 — LS-P-7: conditional-pointer CopyLayout computed for composite, not leaf (confirmed)

collect_conditional_pointers / collect_conditional_result_pointers computed CopyLayout once on the whole payload type. copy_layout only special-cases bare string/list; any composite payload fell to _ => Bulk { byte_multiplier: 1 }. A list<u64> leaf inside option<tuple<u32, list<u64>>> was tagged Bulk { 1 } instead of Bulk { 8 } — a 7/8 under-copy [H-4.1]; pointer-containing Elements leaves collapsed to flat Bulk, dropping inner pointer fixup [H-4.2]. New collect_pointer_positions_with_layout / collect_pointer_byte_offsets_with_layout carry 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) where size(f) for an aggregate field is its full padded size. ~25 field-walk sites — Record/Tuple arms of canonical_abi_size_unpadded, collect_pointer_byte_offsets, the LS-P-7 _with_layout helpers, collect_conditional_result_pointers, collect_return_area_type_slots, collect_resource_byte_positions, element_inner_pointers, element_inner_resources, plus the top-level walks in params_area_byte_size, return_area_byte_size, pointer_pair_*_offsets/slots, resource_*_positions, and conditional_pointer_pair_result_positions — advanced offset/size by canonical_abi_size_unpadded(field) (no trailing pad) instead of canonical_abi_element_size(field). The per-field align_up doesn't re-absorb the preceding pad when the next field has smaller alignment.

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 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_params used Iterator::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_count for a FixedSizeList is saturating (LS-P-4), so a nested FixedSizeList can yield flat_count = u32::MAX. A bare .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. The sibling area-size accumulators already use saturating_add per 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_max
  • ls_p_6_area_byte_size_saturates_across_fields
  • ls_p_7_conditional_pointer_layout_is_per_leaf_not_per_composite
  • ls_p_8_record_tuple_field_accumulation_uses_padded_field_size
  • ls_p_9_total_flat_params_saturates_across_params
  • Full meld-core lib suite green: 253 tests, 0 failures
  • LS-N verification gate: 23/23 (LS-P-6, LS-P-7, LS-P-8, LS-P-9 all detected with matching tests)

🤖 Generated with Claude Code

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>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 22, 2026

Mythos delta-pass required

This PR modifies one or more Tier-5 source files (per
scripts/mythos/rank.md):

meld-core/src/adapter/fact.rs
meld-core/src/parser.rs
meld-core/src/resolver.rs

Before merge, run the Mythos discover protocol on the
modified Tier-5 files:

  1. Follow scripts/mythos/discover.md
    — one fresh agent session per touched Tier-5 file.
  2. For each finding, the agent must produce both a Kani
    harness and a failing PoC test (per the protocol's
    "if you cannot produce both, do not report" rule).
  3. Attach a comment on this PR with either the findings
    (formatted per discover.md's output schema) or
    NO FINDINGS.
  4. Add the mythos-pass-done label to this PR.

Why this gate exists: LS-A-10
(CABI alignment padding in async-lift retptr writeback) was
found by the v0.8.0 pre-release Mythos pass — but it had
lived in the callback emitter since #128, across six
releases. A PR-time gate would have caught it at review
time instead of at the release boundary.

The gate check on this PR will pass once the label is
applied.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 22, 2026

LS-N verification gate

24/24 approved LS entries verified

count
Passed (≥1 test, all green) 24
Failed (≥1 test failure) 0
Missing (no ls_*_NN_* test found) 0

Approved loss-scenarios.yaml entries are expected to have a
regression test named ls_<letter>_<num>_* (e.g. LS-A-11
ls_a_11_*). The gate runs each prefix via cargo test --lib --no-fail-fast and aggregates pass/fail/missing.

Failed LS entries

(none)

Missing regression tests

(none)

Updated automatically by tools/post_verification_comment.py.
Source of truth: safety/stpa/loss-scenarios.yaml.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 22, 2026

Mythos delta-pass (auto)

3 finding(s) across 3 Tier-5 file(s)

File Verdict Hypothesis
meld-core/src/adapter/fact.rs ❌ FINDING The async adapter's parameter-position classifier uses pointer_pair_positions.iter().any(|_| true) — semantically !is_empty() — instead of .any(|&pos| pos == i as u32), so every consecutive (i32, i32) pair in a caller's flat parameter list is emitted as a cross-memory copy whenever the function has any string/list parameter, corrupting plain-integer arguments at the callee.
meld-core/src/parser.rs ❌ FINDING element_inner_pointers silently discards Option, Result, and Variant arms (line 3203: _ => {}), so copy_layout classifies list<option<string>>, list<result<string,E>>, and list<variant-with-string> as Bulk copy instead of Elements, omitting the inner-pointer fixup loop and leaving callee list elements with stale pointers into source memory.
meld-core/src/resolver.rs ❌ FINDING When two core modules within the same component export a function with the same name, the flat-name resolver's HashMap-based export index silently overwrites the first module's entry with the second (last-writer wins), routing any import of that name to the wrong module with no error or warning, violating the semantic-preservation invariant.

Auto-run via anthropics/claude-code-action@v1
(SHA-pinned) on the touched Tier-5 files, using the
maintainer's Max-plan OAuth token. See
.github/workflows/mythos-auto.yml and
scripts/mythos/discover.md.

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>
@avrabe avrabe changed the title fix(parser): flat_byte_size — element-wise variant JOIN, not max fix(parser): canonical-ABI size correctness — flat_byte_size JOIN + LS-P-6 area-size saturation May 22, 2026
@avrabe
Copy link
Copy Markdown
Contributor Author

avrabe commented May 22, 2026

mythos-auto finding #3 — reviewed (plausibly real, pre-existing, tracked separately)

The auto-runner's third scan of parser.rs flagged a new FINDING — collect_conditional_pointers assigning the wrong CopyLayout. Validated per validate.md:

Plausibly real. collect_conditional_pointers (parser.rs:2369) computes the conditional-pointer layout for option<T> / result<T,E> payloads. For each inner pointer position it does copy_layout_for_string_or_list_at(ok_ty) — passing the whole composite payload type, then assigning that one layout to every position from collect_pointer_positions. copy_layout only special-cases bare String / List; a composite (Record/Tuple/FixedSizeList) hits the _ => Bulk{1} fallback. So a list<u32> field inside result<record{ items: list<u32> }, E> gets Bulk{1} instead of Bulk{4} → the adapter copies len * 1 bytes, ¼ of the data → under-copy / truncation (H-4 class). The correct per-field pattern already exists in element_inner_pointers; collect_conditional_pointers uses the whole-composite layout instead.

Not a one-line fix — it needs collect_conditional_pointers restructured to compute the layout per pointer position (per field), not once for the composite. Tracked as a dedicated task for its own validate (PoC + Kani) + LS-N entry + PR.

This is pre-existing and unrelated to #179. #179's changes are flat_byte_size (variant JOIN) and LS-P-6 (area-size saturation) — neither touches collect_conditional_pointers. The auto-runner re-flags it because it scans the whole parser.rs, not the diff.

Process note — whole-file scan treadmill

mythos-auto scans the entire Tier-5 file, so every parser.rs PR is blocked until every latent finding in parser.rs is fixed. Across #178/#179 the auto-runner has surfaced three: flat_byte_size (dead code, not a hazard), LS-P-6 (real OOB, fixed in #179), and now finding #3. Each fix surfaces the next. Worth deciding whether the gate should scan the PR diff rather than the whole file.

#179 status

12 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>
@avrabe avrabe changed the title fix(parser): canonical-ABI size correctness — flat_byte_size JOIN + LS-P-6 area-size saturation fix(parser): canonical-ABI correctness — flat_byte_size JOIN + LS-P-6/-7 May 22, 2026
@avrabe
Copy link
Copy Markdown
Contributor Author

avrabe commented May 22, 2026

mythos-auto finding #4 — clean-room verified (real bug, but auto-runner mis-described it)

The latest delta-pass on 0ec063e flagged a 4th FINDING. I ran it through an independent clean-room verification (a fresh agent given only the claims and the source, no access to the auto-runner's reasoning). Result:

The bug is real — but it is NOT where the auto-runner said. The finding claimed the option/variant/result arms of canonical_abi_size_unpadded use canonical_abi_element_size instead of size(). Verification shows those arms are correct — they already use the padded element size for payloads, which is exactly the spec's size().

The actual bug is in the Record and Tuple arms. They accumulate each field as size = align_up(size, field_align); size += canonical_abi_size_unpadded(field) — adding the field's unpadded size. The Component Model canonical ABI specifies s += size(f), where size(f) for an aggregate field includes that field's own trailing alignment padding. When a higher-aligned aggregate field is followed by a lower-aligned field, the omitted trailing pad is not re-absorbed by the next field's align_up, so the following field's offset — and the whole aggregate's size — comes out too small.

Worked exampletuple<record { a: u32, b: u8 }, u8>:

  • meld canonical_abi_size_unpadded6 (trailing u8 placed at offset 5)
  • Component Model spec size()12 (trailing u8 at offset 8)

The wrong offsets/sizes propagate to collect_pointer_byte_offsets, params_area_byte_size, return_area_byte_size, and the LS-P-7 _with_layout collectors — every record/tuple field walk that advances a byte offset by the unpadded child size.

Minimal fix: advance by canonical_abi_element_size(field) (padded) rather than canonical_abi_size_unpadded(field) at each record/tuple field-accumulation site (~8 sites). canonical_abi_size_unpadded itself still returns the outer type without its own trailing pad — that contract is unchanged; only the per-field contribution is corrected.

Disposition

Process — the whole-file-scan treadmill

This is the 4th finding the whole-file scan has surfaced across #178/#179: flat_byte_size (dead code, not a hazard), LS-P-6 (real, fixed), LS-P-7 (real, fixed), and now finding #4. Each fixed finding exposes the next, because mythos-auto scans the entire Tier-5 file rather than the PR diff. No parser.rs PR can go green through this gate until every latent parser.rs bug is fixed. This needs a maintainer decision — recommend either switching the gate to diff-scan, or dispositioning the pre-existing findings so a PR is judged on its own diff.

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>
@avrabe avrabe changed the title fix(parser): canonical-ABI correctness — flat_byte_size JOIN + LS-P-6/-7 fix(parser): canonical-ABI correctness — LS-P-6/-7/-8 + flat_byte_size JOIN May 23, 2026
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>
@avrabe avrabe changed the title fix(parser): canonical-ABI correctness — LS-P-6/-7/-8 + flat_byte_size JOIN fix(parser): canonical-ABI correctness — LS-P-6/-7/-8/-9 + flat_byte_size May 23, 2026
…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>
@avrabe avrabe changed the title fix(parser): canonical-ABI correctness — LS-P-6/-7/-8/-9 + flat_byte_size fix(parser,adapter): canonical-ABI correctness — LS-P-6 through LS-P-10 May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant