← board

A local dynamic array of string is released as if it were a string handle

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