IxVM soundness audit: three kernel holes closed, pinned as regressions - #586
Merged
Conversation
arthurpaulino
enabled auto-merge
August 21, 2026 16:40
Adversarial Ixon fixtures for three reference divergences the audit found, each paired with a control. All assert REJECT and currently fail (the holes are live); they turn green when fixed. - sizeof-unit-ignores-instance: try_reduce_size_of_unit folds SizeOf.sizeOf Unit _ () to 1 without reading the prover's instance, so <fun _ => 5> () = 1 typechecks by rfl though it is 5 in Lean. - recursor-motives-minors-split-forged: the recursor motives/minors split is never checked against the reconstruction; forging it (sum preserved) retargets minor_var so R mo m0 m1 c0 reduces to mo, an impredicative (I -> Type) : Type. - struct-eta-fires-on-semantic-prop: is_prop_type tests structural Zero where every reference tests semantic zero, so struct-eta fires on a Sort (max 0 0) major the references leave stuck (lean4#14613).
Fixes the holes pinned in the previous commit; the exploit suite is green under the bytecode interpreter. Codegen regen + FFT repin deferred to a later pass, so the full ixvm suite's pins/parity are stale until then. - is_prop_type (DefEq.lean): test SEMANTIC zero via level_equal, matching is_inductive_prop and all three references, so struct-eta no longer fires on a Sort (max 0 0) major (lean4#14613). - SizeOf.sizeOf Unit (NatPrim.lean): drop the native fold that returned 1 ignoring the instance; reduce honestly via the instance projection, as lean4lean does. Removes the now-dead unit/punit/size-of addr helpers. - recursor arity (Check.lean): assert the declared motives/minors counts equal the canonical reconstruction, since the type check pins only their sum while minor_var reads n_min alone.
klimbs_from_g(0) returned the denormalized single-limb [[0;8]] instead of the canonical empty list. klimbs_eq (behind Nat.beq/decEq/literal_eq) compares limbs without normalizing, so any reducer emitting that zero disagrees with Lean. Two callers normalized individually; String.back (try_str_back) and build_char_list did not, so a NUL codepoint recovered through Char.toNat compared unequal to 0. Normalize inside klimbs_from_g so every caller is covered, and drop the now-redundant wrap at the utf8ByteSize caller. Not directly fixturable: the only reach-path forces Char.ofNat's validity dite and cascades rather than settling to a false accept.
Regenerate aiur_ixvm.rs and repin the 76 shifted kernel FFT costs plus the shard pipeline cost after the three soundness fixes. Values captured from a full `lake test -- --ignored ixvm` run whose only failures were those pins: every real recursor still checks (the new arity assert rejects only the forged fixture), the codegen parity gate passes, and the exploit suite is green.
Add IxVMPrim.sizeof_unit (`sizeOf () = 1`), whose check requires the honest reduction of the SizeOf.sizeOf class projection through the canonical Const instance — the path the removed native fold used to short-circuit. Measured FFT 158_539_438: the constant checks (the projection reduction fires through the real instance), and if that path regresses it goes stuck and this pin fails.
arthurpaulino
force-pushed
the
ap/ixvm-exploits
branch
from
August 21, 2026 17:17
a2f2ba0 to
0aeb7a2
Compare
samuelburnham
approved these changes
Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A deep soundness audit of the IxVM kernel (Codex lead + a four-agent
review against the Rust,
Ix/Tcand lean4lean references) surfaced threegenuinely exploitable soundness holes and one non-weaponizable bug. Each
exploitable hole is first reproduced as an adversarial Ixon fixture that
the kernel wrongly accepts, then fixed so the fixture flips to a correct
rejection. Codegen is regenerated and the FFT pins repinned. A dedicated
regression constant guards the one fix whose reduction path the test
corpus did not otherwise exercise.
Holes found and fixed
try_reduce_size_of_unitfolded@SizeOf.sizeOf Unit _ () → 1reading only the type argument, so@SizeOf.sizeOf Unit ⟨fun _ => 5⟩ () = 1typechecked byrfl(it is5in Lean).NatPrim.lean).motives/minorssplit was never checked against the reconstruction. The type check pins only their sum, butminor_varreadsminorsalone, so a forged split (sum preserved) retargets every iota rule —R mo m0 m1 c0 ↝ mo, i.e.(I → Type) : Type→ Girard.motives/minorscounts equal the canonical reconstruction, matchinginductive.rs(Check.lean).is_prop_typestructural vs semantic zeroKLevelNode.Zero; all three references test semantic zero. A prover-authoredSort (max 0 0)then fired struct-eta on a Prop-typed major (the lean4#14613 guard).level_equal(l, Zero), matchingis_inductive_propand every reference (DefEq.lean).klimbs_from_g(0)returned the denormalized[[0;8]]rather than the canonical empty list;klimbs_eq(behindNat.beq/decEq) compares limbs without normalizing, so a codepoint0fromString.back/build_char_listcompared unequal to0.klimbs_from_gso all callers are covered (NatPrim.lean). Not fixturable — the only reach-path forcesChar.ofNat's validityditeand cascades rather than settling to a false accept.The audit also cleared as faithful to the references: large elimination
of Prop, level construction/comparison, quotient reduction, proof
irrelevance, the projection def-eq, function/struct eta, lazy delta,
Nat-literal ingress, and the Int primitives.
Fixtures (
Tests/Ix/IxVM/Exploits.lean)Each exploit is paired with a control that differs minimally, so a green
suite cannot mean the fixture stopped reaching its target.
sizeof-unit-ignores-instance+control-sizeof-unit-canonical-valuerecursor-motives-minors-split-forged+control-swapped-recursor-rules,recursor-rule-rhs-doctored-binder-domainsstruct-eta-fires-on-semantic-prop+control-struct-eta-syntactic-prop-declinesRegression guard (
Tests/Ix/IxVM.lean)IxVMPrim.sizeof_unit(sizeOf () = 1) forces the honest reduction ofthe
SizeOf.sizeOfclass projection through the canonicalConstinstance — the path the removed fold used to short-circuit, and the one
the exploit control (a literal
SizeOf.mk) did not cover. If thatprojection path regresses, the constant goes stuck and the pin fails.
Validation
control behaves as specified.
ixvmkernel-check suite: the fixes shifted 76 FFT costs and theshard-pipeline cost; codegen (
aiur_ixvm.rs) regenerated and all pinsrepinned. The codegen ↔ bytecode parity gate passes, and every real
recursor still checks — the new arity assert rejects only the forged
fixture, not
HEq.rec/Eq.rec/theIxVMInd.*.recs (main, mutual,aux, nested and indexed).
errors, across all six environments.
Total FFT cost: 339,118,996,450,290
Safety of the fixes
Each fix was analyzed for the two non-soundness failure modes and the
sweep is consistent with the analysis:
level_equaland the recursor recompute are bounded; the honest
SizeOfpath is thesame reduction Lean performs (stuck stays stuck, no loop);
klimbsadds one bounded
normalize.is_prop_typeis byte-identical on canonicallevels (real constants), diverging only on adversarial non-canonical
ones, where it matches Lean. The recursor assert is implied by the
existing type-check invariant, so it cannot reject a recursor the type
check accepts.
klimbs_from_gis strictly more canonical. TheSizeOfremoval was the one path needing empirical confirmation that honest
reduction fires through the real
Constinstance — now pinned byIxVMPrim.sizeof_unitand confirmed by the full sweep.Commits
test(ixvm): pin three soundness holes as exploit fixturesfix(ixvm): close three kernel soundness holesfix(ixvm): normalize klimbs_from_g output at the sourcechore(ixvm): regen codegen and repin FFT after kernel fixestest(ixvm): pin SizeOf.sizeOf Unit reduction as a regression fixture