Skip to content

gc_runtime_root_holders: raw thread_local! blocks escape classification, and no verdict fits census.rs's PASS1_MARKED #9740

Description

@proggeramlug

Two findings, one blocked conversion

While clearing the raw thread_local! blocks that keep tls-budget red on main (four converted in the linked PR), gc/census.rs could not be converted honestly. It surfaced both a gate blind spot and a gap in the verdict vocabulary.

1. A raw thread_local! is invisible to gc_runtime_root_holders.py

The holder inventory enumerates perry_thread_local! declarations (rule T). A raw thread_local! is therefore audited by neither gate: check_thread_locals.py flags it as a TLS-perf violation, but nothing asks whether it holds a heap pointer.

That is the wrong way round — the declarations that skipped the convention are exactly the ones least likely to have had their GC contract considered. Converting four files immediately exposed six previously-unclassified holders, five trivial and one not.

Suggestion: have the holder inventory enumerate raw thread_local! blocks too, so skipping the convention cannot also skip classification.

2. PASS1_MARKED holds heap addresses that no verdict describes truthfully

/// Pass-1 snapshot: sorted header addresses that were marked when mark
/// propagation finished (see the module docs).
static PASS1_MARKED: RefCell<Option<Vec<usize>>> = const { RefCell::new(None) };

Those usizes are real GC header addresses. Against the vocabulary:

  • not_a_gc_pointer — defined as "an id, a counter, an epoch, a code address, a .rodata object, or Rust-owned state". A heap address is none of these. Using it would be false.
  • covered_elsewhere — requires a named registered scanner. None visits this, and none should: tracing the marked set would retain exactly the garbage the census exists to observe.
  • open_gap — asserts a real unrooted GC pointer, and fails the gate. It is not a gap.
  • unverified — fails the gate, and the contract is verified (below).
  • test_only — it is env-gated diagnostic code, not #[cfg(test)].

The actual contract, from the module's own docs:

Inside a SYNCHRONOUS full cycle the mark bits are authoritative and nothing moves.

The addresses are written in census_pass1_if_armed at the end of mark propagation and take()n at sweep entry in the same synchronous full cycle, with no relocation and no mutator resumption in that window. They are safe by construction, and deliberately untraced. Off by default — ARMED is false unless PERRY_GC_CENSUS=<path> is set, leaving the cell None.

So the holder is fine and the vocabulary cannot say so. Rather than force a wrong label to make a gate green, census.rs is left as a raw thread_local! and tls-budget stays red on that one file.

Suggestion: add a verdict for "holds addresses, deliberately untraced, valid only within a single non-moving window", carrying the window's boundaries so a future change that widens it fails the gate. That is a real contract worth pinning — it is the same shape as the retained-array-growth-stub class in #9717.

Happy to implement whichever direction the maintainers prefer.

Activity

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

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