Skip to content

Clear immer transience stamps during GC migration#1220

Open
anvacaru wants to merge 2 commits into
developfrom
anvacaru/mem-corruption-fix
Open

Clear immer transience stamps during GC migration#1220
anvacaru wants to merge 2 commits into
developfrom
anvacaru/mem-corruption-fix

Conversation

@anvacaru

@anvacaru anvacaru commented Jul 7, 2026

Copy link
Copy Markdown

This directly affects the evm-semantics, causing conformance tests to fail in runtimeverification/evm-semantics#2833.

The backend instantiates immer collections with no_refcount_policy, which selects gc_transience_policy.
Under that policy, every transient operation (used by the List/Map/Set hooks) creates an ownership token and stamps the nodes it creates with it; node::can_mutate(edit) is a raw pointer comparison against that stamp (refs.unique() is always false under no_refcount_policy).

Three facts combine into memory corruption:

  1. A token is the address of a small kore-heap allocation (gc_transience_policy.hpp: make_token_()).
  2. The young-generation bump allocator restarts at its semispace based on every collection, so token addresses are recycled once their owning transient is gone.
  3. GC migration copied collection nodes verbatim (migrate_collection_nodememcpy), so stale stamps survived collections, including promotion into the old generation.

When a later hook's freshly allocated token lands on a recycled address equal to a surviving node's stale stamp, can_mutate returns true on a shared node, and the hook mutates it in place, corrupting every collection value that aliases that node.

Observed in KEVM : a ListItem(X) REST match compiles to hook_LIST_range_long(L, 1, 0), whose drop(1) took the in-place branch of slice_left_mut_visitor::visit_leaf and shifted a shared 6-element leaf from [e1..e6] to [e2..e6, e6].
The collision requires an exact allocation-address coincidence, so the symptom is layout-sensitive and can affect any K semantics using collection hooks.

Related prior fix in the same family: #1219.

Fix

runtime/collect/migrate_collection.cpp: clear the transience stamp of every node visited during collection — rbts inner/leaf nodes, rbts relaxed structs, champ nodes, and champ values buffers — using the node types' own ownee/edit_t API.

  • Safe: no transient is live during a collection (hooks complete within a rewrite step; GC runs between steps), so stamps carry no information the collector needs to preserve. Clearing only restores copy-on-write the next time the node is touched.
  • Complete: every stamp assignment in immer is either on a freshly allocated node or guarded by can_mutate, so a node cleared at its first migration can never be re-stamped; old-generation nodes were cleared when migrated there. Within one GC epoch tokens are distinct bump allocations, so no collision is possible before first migration.
  • No slowdown in execution: legitimate can_mutate hits only occur within a single hook invocation, on nodes allocated after the last collection; those stamps are untouched. GC overhead is one 8-byte store per visited node.

Regression test

unittests/runtime-gc (new target runtime-gc-tests) links the real allocator, collector, and list hooks and arms the collision deterministically:
build a 6-element list (final concat stamps the tail leaf), migrate it through two semispace flips (tail promoted to old generation, young allocator back at the stamp's semispace base), pad the young space so the next token lands exactly on the recycled stamp address, then run hook_LIST_range_long(l, 1, 0).
An assertion on the result's tail stamp proves the collision armed.
Without the fix, the kept list reads [2,3,4,5,6,6]; with the fix, it is intact.

Testing

  • runtime-gc-tests fails on the unfixed runtime, passes with the fix.
  • ctest: all 7 unit-test targets pass.
  • lit test/defn: 88/88 pass.
  • KEVM: an interpreter rebuilt from the failing kdist definition against the fixed backend runs the investigation's failing-3.kore repro to completion with no stuck authorizationList check.

🤖 Generated with Claude Code

anvacaru and others added 2 commits July 7, 2026 16:11
…ing GC migration

Immer's gc_transience_policy stamps nodes created by a transient with an
ownership token so later operations by the same transient may mutate them
in place. Tokens are addresses of small kore-heap allocations, and the
young-generation bump allocator restarts at its semispace base on every
collection, so token addresses are recycled once their owner is gone.
Migration copied nodes verbatim, keeping stale stamps alive indefinitely;
when a later hook's fresh token landed on a recycled address matching a
stale stamp, can_mutate passed on a shared node and the hook mutated it
in place, corrupting every collection value aliasing that node.

Clear the stamp of every rbts node, relaxed struct, champ node, and champ
values buffer visited during collection. No transient is live during GC
(hooks complete within a rewrite step; GC runs between steps), so this
only restores copy-on-write the next time a surviving node is touched.
All immer stamp writes are creation-time or can_mutate-guarded, so a node
cleared at its first migration can never be re-stamped, and within one GC
epoch tokens are distinct bump allocations - stamps can no longer collide.

Root-cause investigation: evm-semantics
docs/2026-06-12-llvm-backend-immer-transience-bug.md (corrupted
<txAuthList> tuple in an eip7702 conformance test). Related prior fix in
the same family: #1219.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…amp recycling

Links the real allocator, collector, and list hooks and arms the
token-address collision by construction: build a 6-element list (the
final concat's transient stamps the tail leaf), migrate it through two
semispace flips so the young allocator returns to the stamp's semispace
with the tail promoted to the old generation, pad the young space so the
next token allocation lands exactly on the recycled stamp address, then
run hook_LIST_range_long(l, 1, 0) - the compiled form of a
'ListItem(X) REST' match. An assertion on the result's tail stamp proves
the collision armed.

Without the stamp-clearing fix the kept list reads [2,3,4,5,6,6] (head
dropped, last element duplicated - byte-for-byte the corruption observed
in KEVM); with it the list is intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@anvacaru
anvacaru changed the base branch from master to develop July 7, 2026 13:20
@anvacaru

anvacaru commented Jul 7, 2026

Copy link
Copy Markdown
Author

Performance check: KEVM conformance suite, with vs without this fix

Setup: two interpreters kompiled from the same KEVM semantics, differing only in the llvm-backend: a32cc6a2 (develop, no fix) vs fa27dd75 (this PR). Both nix-built via kup --override llvm-backend <commit>, K v7.1.337. Suite: ethereum-tests conformance (2774 fixtures, 2764 run), pytest -n8, two runs per backend, interleaved.

run backend pass sum-of-per-test (s) wall-clock (s)
baseline-r1 no fix 2764/2764 837.1 204
fix-r1 PR 2764/2764 826.8 203
baseline-r2 no fix 2764/2764 825.3 201
fix-r2 PR 2764/2764 825.2 202
  • Pass/fail outcomes identical in all runs. (The corruption bug is not armed by any test in this suite — it lives in an execution-spec-tests fixture, see below — so the timing comparison is not skewed by divergent execution paths.)
  • Run-to-run noise (same backend): up to −1.4% (first run carries cache warmup).
  • Warm like-for-like (r2 vs r2): −0.1 s on 825 s (−0.0%) — no measurable slowdown, as expected (the fix adds one 8-byte store per collection node per GC visit and disables no legitimate in-place path).

Functional side-check with the same two binaries, outside the benchmark suite: on the standalone repro extracted from the failing execution-spec-tests fixture (eip7702_set_code_tx/test_gas_cost.json, fork_Osaka-…-many_valid_authorizations_multiple_signers), the no-fix interpreter still reproduces the <txAuthList> corruption (stuck on the authorizationList check, byte-identical to the archived failure); the PR interpreter runs it to completion.

@anvacaru
anvacaru requested a review from gtrepta July 7, 2026 16:24

@gtrepta gtrepta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

While looking into it, I found this "gc_transience_policy" in the immer lib: https://github.com/arximboldi/immer/blob/master/immer/transience/gc_transience_policy.hpp

Looks like it may be intended for our GC use case, but I didn't investigate it any further. The fix we have now is probably good enough.

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.

2 participants