Summary
The untraced whole-block promotion path charges retained garbage from the previous cycle's survival ratio, not from the liveness of the bytes it is currently promoting. After a live-to-dead phase change, a prior 1000‰ measurement makes implied_dead = 0, so the advertised 32 MiB PROMOTED_DEAD_BUDGET_BYTES is not charged while the collector blindly promotes the new dead cohort.
The separate stale-estimate budget allows max(128 MiB, old-gen-at-last-measurement) of such promotion before forcing a traced cycle. On a large live heap that can be hundreds of MiB or GiB. The eventual traced cycle measures only its current young cohort; it cannot recover or retroactively charge garbage already promoted by the preceding untraced cycles. Those bytes remain in old-gen until a full collection.
This is a bounded-policy design, not an immediate memory-safety bug, but the bound is much larger than the comments/changelog's 32 MiB footprint claim and grows with old-gen size.
Phase-change sequence
- A traced copying minor observes 1000‰ young survival.
- The application changes phase and begins allocating/droping short-lived cohorts.
- The next promotion(s) satisfy the stale 1000‰ predictor and skip tracing.
- Every parseable young object is promoted and accounted as live, including objects that are now unreachable.
note_untraced_promotion() computes zero implied dead bytes from the stale 1000‰ value, so PROMOTED_DEAD_BYTES remains unchanged.
- Only after
UNTRACED_PROMOTED_BYTES reaches max(128 MiB, old-gen-at-last-measurement) does a traced promoting cycle run. It updates the future predictor but cannot identify/reclaim the earlier promoted garbage.
credit_promoted_bytes_to_old_baseline() and live-census publication treat all untraced promoted bytes as live, which can defer the full collection needed to reclaim them.
Evidence
Impact
- A phase-changing server can park up to the old generation's prior live size again as dead-but-accounted-live memory.
- Major pacing and old-reclaim pacing are biased toward retaining it because the promoted delta is credited as clean/live.
- On device-constrained heaps, the fixed 128 MiB floor alone is larger than some intended heap budgets.
- Telemetry reports the assumed-live promoted bytes as
heapUsed until a full census/reclaim, so the policy cannot distinguish confidence from measured liveness.
Acceptance criteria
Audit context
Found while re-auditing the v0.5.1473→v0.5.1481 GC delta, specifically #7888 and #7895. No heavy benchmark was run for this audit; the finding follows from the policy/state transitions and existing source tests.
Summary
The untraced whole-block promotion path charges retained garbage from the previous cycle's survival ratio, not from the liveness of the bytes it is currently promoting. After a live-to-dead phase change, a prior 1000‰ measurement makes
implied_dead = 0, so the advertised 32 MiBPROMOTED_DEAD_BUDGET_BYTESis not charged while the collector blindly promotes the new dead cohort.The separate stale-estimate budget allows
max(128 MiB, old-gen-at-last-measurement)of such promotion before forcing a traced cycle. On a large live heap that can be hundreds of MiB or GiB. The eventual traced cycle measures only its current young cohort; it cannot recover or retroactively charge garbage already promoted by the preceding untraced cycles. Those bytes remain in old-gen until a full collection.This is a bounded-policy design, not an immediate memory-safety bug, but the bound is much larger than the comments/changelog's 32 MiB footprint claim and grows with old-gen size.
Phase-change sequence
note_untraced_promotion()computes zero implied dead bytes from the stale 1000‰ value, soPROMOTED_DEAD_BYTESremains unchanged.UNTRACED_PROMOTED_BYTESreachesmax(128 MiB, old-gen-at-last-measurement)does a traced promoting cycle run. It updates the future predictor but cannot identify/reclaim the earlier promoted garbage.credit_promoted_bytes_to_old_baseline()and live-census publication treat all untraced promoted bytes as live, which can defer the full collection needed to reclaim them.Evidence
max(128 MiB, old-gen-at-last-measurement).note_untraced_promotion()charges dead bytes by extrapolating the previous ratio. A prior 1000‰ value charges zero regardless of current liveness.PromotionLiveness::AssumeAllLive.AssumeAllLivepromotions.Impact
heapUseduntil a full census/reclaim, so the policy cannot distinguish confidence from measured liveness.Acceptance criteria
AssumeAllLivebytes to the clean old-reclaim baseline as equivalent to marked-live promotions, or track them in a separate uncertain/debt class.1000‰ live phase -> dead churn phaseratchet that asserts old-gen growth, full-GC timing, RSS, andheapUsedremain within the documented bound.PERRY_GC_HEAP_LIMIT.Audit context
Found while re-auditing the v0.5.1473→v0.5.1481 GC delta, specifically #7888 and #7895. No heavy benchmark was run for this audit; the finding follows from the policy/state transitions and existing source tests.