Skip to content

gc: per-object layout global latch can publish false zero and leaks armed thread exits #7873

Description

@proggeramlug

Summary

#7834 introduced a process-global zero/nonzero mirror for the per-thread layout side tables. It currently uses two independent atomics:

  • PER_OBJECT_LAYOUT_ARMED_THREADS: AtomicIsize
  • exported PERRY_PER_OBJECT_LAYOUTS_ANY: AtomicU8, read directly by generated allocation sites

The disarm path reads the count and then separately stores 0 to the byte. The comment says a race can only leave a conservative stale 1, but this interleaving leaves the authoritative fast-path byte at false zero while a thread is armed:

initial: count=1, any=1
A: count.fetch_sub(1) -> count=0
A: count.load() == 0
B: count.fetch_add(1) -> count=1
B: any.store(1)
A: any.store(0)
final: count=1, any=0

Even SeqCst on the existing two atomics would permit this interleaving; the state needs one atomic source of truth or a synchronization protocol that cannot publish a false zero.

Separately, the source explicitly documents that a thread exiting while armed leaks its count permanently, leaving all future allocations in the process on the pre-#7834 call path.

Impact

0 is documented as a proof that no per-object record exists, and generated code uses it to skip js_gc_forget_object_layout entirely. A false zero can therefore skip removal of an address-keyed record at object death/reuse, allowing a recycled object address to inherit stale layout metadata. That is a GC scanning/representation correctness risk, not just a performance miss.

The TLS-exit leak is conservative for correctness but process-wide for performance: one short-lived perry/thread agent that exits with a record can make every subsequent inline construction pay the runtime branch/call path forever.

Relevant source:

This is a focused successor to the broader layout-performance umbrella #5094.

Acceptance criteria

  • Make the generated-code zero test read a single authoritative atomic state (for example an exported atomic count with a safe zero/nonzero transition), so no interleaving can publish 0 while any thread is armed.
  • Disarm the process-global state from the owning TLS value's destructor when a thread exits armed; do not depend on destructor order between separate TLS keys.
  • Add a deterministic synchronization-hook test for the interleaving above. A probabilistic thread race is not enough.
  • Add a thread-exit test that arms a side table, exits without emptying it, and proves the process-global state returns to zero.
  • Add/retain a recycled-address correctness witness proving the gate never hides a live/stale per-object layout record.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugConfirmed defect or regressionperformanceRuntime, compile-time, build-size, or memory performance

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions