A static array of promo ints releases only element 0
Split out of
[[bug-a-managedlocalzerobytes-answers-per-kind-and-has-been-wrong-twice]],
which fixed the init half of one missing IsArray and measured this as the
other half of the same omission.
The asymmetry
compiler/symtab.inc:10792:
else if TypeIsPromoInt(Syms[i].TypeKind) then
begin
procIdx := FindProc('PXXPromoClear');
...
x64_lea_reg_mem(8, rRDI, rRBP, Syms[i].Offset, True); { rdi = slot addr }
EmitCallProc(procIdx);
end
No IsArray test, and the address passed is the array's base — so for
a: array[0..3] of promoint64 exactly one element is cleared. Elements
1..N keep a heap-tier payload that nothing ever releases.
Why it is a leak now and was a use-after-free an hour ago
The init side had the same missing IsArray, so the array was zeroed not at
all and element 0 was cleared from stack garbage — PXXPromoClear releases
the payload as a managed string whenever the tag reads PROMO_TAG_HEAP, and
its own header says it "cannot be used on uninitialised memory".
test/test_promoint_local_array_zero_init.pas segfaults on the pre-fix
compiler for that reason.
With init fixed, every element starts {0, 0}, so clearing element 0 is
harmless and the remaining defect is bounded: elements 1..N leak whatever
heap-tier payload they were given. Safety-critical half closed, correctness
half open — which is why this is filed at 45 rather than inheriting 55.
The precedent names the remedy
This is the same defect as
bug-a-local-static-array-of-string-never-released-at-scope-exit, one type
over. That fix added the "STATIC array with MANAGED ELEMENTS: release EVERY
element" arm at symtab.inc:10745, which calls PXXArrayReleaseImmediate with
an explicit (addr, count, baseKind, ref) — a header-free element walk, since a
static array has no [refcount][length] prefix. Its comment describes this
ticket's symptom verbatim: "fell into the scalar tyAnsiString arm below and
released element 0 ONLY — the other N leaked, silently and linearly."
So the shape of the fix is known: give ManagedElemKind a promo-int kind and
let that existing arm claim promo arrays (it is earlier in the chain than
the promo arm, so it will), and teach PXXArrayReleaseImmediate to dispatch
that kind to PXXPromoClear per element. That is a runtime change in
compiler/builtin/promocore.pas plus a new base-kind number, which is why it
is not folded into the init fix.
Reachability, measured
promoint64 / promoint32 / promoint are spellable Pascal type names
(pasparser_decl.inc:543), so this is reachable from ordinary Pascal. It is
not reachable from NilPy user code, which has no static-array syntax — a
NilPy list is a dyn array (ArrLen = -1) and is claimed by the dyn-array-handle
arm long before either promo arm. No in-repo source declares a promo-int array
today, which is why neither half was noticed.
Gate
make compiler/pascal26 + a leak probe in the shape of
test_open_array_no_leak (a hot loop writing a heap-tier value into
a[1..N], RSS asserted flat) + tools/gate.sh quick.
Log
- 2026-09-01 — resolved, commit 25d4756e4.
Resolved
Fixed in the commit below. Four things worth carrying forward, because the ticket had three of them wrong or absent:
The remedy the ticket predicted was right, and incomplete. It said: give
ManagedElemKind a promo kind and let the existing static-array arm claim it.
That is what landed. But it also said the runtime change belongs in
promocore.pas; it does not, and cannot -- a builtin unit cannot uses
another one, so the element walks live in builtinheap.pas and release the
heap-tier payload as what it is, a managed AnsiString. That is the same
representation PXXVarClear already uses for a variant carrying a promo tag,
so builtinheap was already the file that knew this.
A base-kind number is not enough for these two. Every kind above them
implies its element size; promoint64 is a 16-byte slot and promoint32 an
8-byte one, so kinds 5 and 6 carry the STRIDE in ManagedElemRef, which is the
designated second half of the (kind, ref) pair. Sending the compiler's own
TypeSlotSize is what stops the runtime and the compiler disagreeing about the
layout on any target.
Two faults the ticket did not contain. The unwind path: SymNeedsManagedCleanup
asks ManagedElemKind as well, so a proc whose only managed local was such an
array got no cleanup landing pad, and an exception past it leaked every element
including 0. And b := a on two static promo arrays reached the promo STORE
arm and emitted one PXXPromoCopy on the base address -- element 0 copied,
1..N left holding the destination's old values, silently. Both are fixed by the
same change, which is the argument for fixing it at ManagedElemKind rather
than in the scope-exit arm as the ticket framed it.
"No in-repo source declares a promo-int array today" was true when filed and
is no longer: test_promoint_array_cleanup.pas and test_promoint_lvalue_shapes.pas
both do, and the first runs on all five targets qemu can host.
The ticket's Gate line asked for a hot-loop RSS probe. That is the Makefile row's second half, with the threshold placed between two executed numbers (3.6 MB fixed, 54 MB unfixed) rather than beside one.