Skip to content

GC: the two lazy intrinsic-tower builders need #7217's no-move window, and need a gate that can fail first #7251

Description

@proggeramlug

Summary

#7217 established the invariant and fixed the one site that had a runtime witness:

A bootstrap that builds an IMMORTAL object graph through raw pointers held across its own allocations must run in a NO-MOVE WINDOW.

populate_global_this_builtins now runs in a GcSuppressScope, gated by a cargo-test-visible unit test that arms a collection, shows the bootstrap does not service it, and shows the same armed request IS serviced by ordinary allocation once the window closes.

Two sibling builders have the identical shape and are NOT covered, because I could not build a gate for them that can fail. Filing rather than shipping ungated, per CLAUDE.md's knob-kill policy ("shipping a GC-trigger change with no test that can fail without it is the thing [it] exists to stop", quoted verbatim at crates/perry-runtime/src/string/alloc.rs:49).

The two sites

builder the raw locals also reachable ahead of the bootstrap from
object/global_this/generator.rs::ensure_generator_intrinsicsbuild_generator_tower *mut ObjectHeader / *mut ClosureHeader ctor+proto locals threaded across every allocating install in the tower generator_function_proto_of, generator_function_prototype_of, generator_prototype_ptr — i.e. any program that reflects on a generator without touching globalThis
object/global_this/typed_array.rs::ensure_typed_array_intrinsic ctor and proto, held across ~a dozen set_builtin_property_attrs / set_field installs typedarray_props.rs:812

Both build an immortal graph (the towers hang off process-global intrinsic slots), so the same argument applies verbatim: a collection inside them frees nothing and can only move the holders out from under them.

When they are reached from populate_global_this_builtins they inherit its window and are safe today. The exposure is the lazy-ahead-of-bootstrap path only.

Why #7217 could not gate them, measured

populate_global_this_builtins is gateable behaviourally — arm a pending collection, run it, assert nothing collected — because it allocates ~1.15 MB and therefore crosses several arena block boundaries, and arena_alloc_gc calls gc_check_trigger() on each. A tower is three orders of magnitude smaller and fits inside one block's tail, so it can reach no trigger at all:

  • a first version of the gate arming a collection around ensure_generator_intrinsics() passed with the GcSuppressScope deleted;
  • a second version pre-filling to a block boundary, and re-execing the test binary so the child process is the first to touch a tower (the slots are process-global AtomicI64s, so libtest's name ordering meant array::tests's js_get_global_this() built them first and the test was measuring nothing) — also passed with the scope deleted;
  • a third version recording gc_is_suppressed() from inside the builder under #[cfg(test)] reported suppressed even with the scope removed, which is unexplained and is itself a reason not to trust it as a gate. (Worth a look: something else may already be suppressing across part of the tower build — symbol/get.rs calls the manual gc_suppress()/gc_unsuppress() pair at three sites, and well_known_symbol is on this path.)

What would close this

Either of:

  1. A workload witness. A gap test that reflects on a generator (or touches a typed array) under enough allocation pressure to put a collection inside the tower build, run on the allocation-point arm (PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off) with PERRY_GC_PROTECT_FROMSPACE=1. If the quarantine faults inside build_generator_tower, the window is justified and the witness is its gate.
  2. A trigger the tower's size can reach. GcTriggerThresholdTestGuard::make_malloc_sweep_due() arms the malloc-count trigger, which gc_check_trigger sees on every gc_malloc rather than once per arena block. If the tower's installs go through gc_malloc at all, that gives a lever a few-hundred-object build can actually trip.

Resolve the third bullet's anomaly first — if something already suppresses across these builds, the windows may be redundant rather than missing, and that is a better answer than adding them.

Refs #7217, #7196.

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