Skip to content

GC/threads: process-global address-keyed caches outlive thread arenas (3rd instance: #11319, #11462, #11470) — audit + gate #11471

Closed

Description

@proggeramlug

One bug class, three instances in one day. Process-global state keyed by (or holding) a heap address outlives the thread whose arena owned that address. After the thread exits, Arena::drop frees its blocks, and another thread's arena can reuse the same addresses. The stale entry then describes a different object.

  1. perry-stdlib lib tests: GC young-log rule-1 assertion fires in readline listeners_provider test on main #11319 / fix: perry-stdlib lib tests pass on clean main (#11417, #11319) #11422: closure side tables (process-global) kept a dead thread's entries, so a new closure at a reused address inherited them (young-log rule-1 abort). Fixed by release_closure_side_table_owners_in_ranges in Arena::drop.
  2. fix(runtime): cache node:tls helper arrays per thread (fetch::lifecycle test flake) #11462: tls.rs cached tls.rootCertificates and sigalgs arrays in process-global AtomicU64 GC roots, which pinned whatever object later occupied the address (fetch::lifecycle flake; a dangling pointer in real perry/thread programs). Fixed by making them per-thread.
  3. fix(runtime): a thread exit forgets its typed arrays in the global kind cache (#11463) #11470: PERRY_TA_KIND_CACHE / INLINE_OWNING_U32_CACHE (process-global, keyed by address) kept "this address is a typed array" after the owning thread exited, so a Promise at the reused address was misread as a Uint8Array (streams::pipe flake). Fixed by invalidate_kind_caches_in_ranges in Arena::drop.

Gap. scripts/gc_runtime_root_holders.py enumerates statics that can hold heap pointers as GC roots. It does not cover process-global lookup caches keyed by heap addresses, which aren't roots but are just as stale after arena reuse. Each instance so far was found by chasing a flake.

Ask.

  • Audit every process-global cache or table in perry-runtime and perry-stdlib keyed by or storing a heap address, and make each one per-thread or invalidated in Arena::drop.
  • Add a gate, like the root-holder inventory, that requires every such static to declare its thread-exit story, so the next one fails CI instead of flaking.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions