A local dynamic array of string is released as if it were a string handle
- Track A (
compiler/symtab.inc,EmitManagedLocalCleanup, x86-64 arm). - Found 2026-08-21 while fixing the interface-container family, by running the
cross-target build of that family's repro and noticing
dyn: 0on aarch64 — which led to measuring the string case on every target, including x86-64.
Measured
procedure PArr;
var d: array of string; i: Integer;
begin
SetLength(d, 8);
for i := 0 to 7 do d[i] := 'element-padding-padding-padding-x';
end;
begin for k := 1 to 200000 do PArr; end.
| peak RSS | |
|---|---|
array of string (above) |
112 512 kB |
same loop, array of Integer |
392 kB |
same loop, a scalar string local |
392 kB |
Linear in the iteration count (20k → 11 560 kB, 200k → 112 512 kB), so it is a
leak and not a pool. Identical on pinned (112 512 kB) and on HEAD before the
fix, so it is not a recent regression — it has been there as long as the arm
ordering has.
Cause
EmitManagedLocalCleanup's arms are tried in order, and the SCALAR string arm
sat ABOVE the dynamic-array arm:
else if Syms[i].TypeKind = tyAnsiString then { <-- claimed array of string }
... mov rax,[rbp+off]; AnsiStrRelease
...
else if Syms[i].IsArray and (Syms[i].ArrLen = -1) then
... EmitDynArrayReleaseForSym(i) { <-- never reached }
An array's TypeKind is its element kind, so array of string has
TypeKind = tyAnsiString, IsArray = True, ArrLen = -1 and matched the first
arm. The array's DATA POINTER was then passed to the string releaser as though
it were a string handle. Nothing frees the elements, and nothing frees the block.
This is the same trap that
bug-a-local-static-array-of-string-never-released-at-scope-exit fixed for the
STATIC case — that fix added an arm above the scalar one, and the dynamic case
was left below it, so exactly half the bug was repaired.
Fix
One guard: (Syms[i].TypeKind = tyAnsiString) and not Syms[i].IsArray.
Correction to the first reading of this, recorded because the wrong version
was written into this ticket before the other four epilogues were actually read:
it is NOT true that "the other four backends already carry the guard".
aarch64, riscv32 and xtensa do. i386 and arm32 do not — they had the same
bug, and got the same guard in the same change. Three of five, not four of five;
the claim came from checking one backend and generalising, which is the exact
habit devdocs/dev/debugging-playbook.md says to distrust.
Resolution 2026-08-21
Fixed as above, in the same change as the interface-container family
([[bug-a-a-local-array-of-interfaces-is-never-released-at-scope-exit]]), because
it is the same function and the same "an array's TypeKind is its element kind"
trap. Measured after: 392 kB, flat — the same baseline as the non-leaking
shapes. Regression coverage: the strarr: round-trip line in
test/test_interface_containers.pas proves the elements still read back after
many calls (the leak itself is an RSS measurement, not something a test asserts).
Fixed on x86-64, i386 and arm32 (the three that lacked the guard).
Gate: make compiler/pascal26 (fixedpoint) + tools/gate.sh quick GREEN, and
the interface-container regression run under i386 / arm32 / aarch64 / riscv32.
Log
- 2026-08-21 — resolved, commit bcd8546f9.