A by-value string param the callee writes changes the caller's string
Found 2026-09-24 by frankS while chasing frankb-12's dynarray leak census
(a by-value dynarray param rebound by SetLength leaked 0.98 blocks per call).
Measured
function UpperStr(s: string): string; ... s[i] := UpCase(s[i]); Result := s;left the caller'snameupper-cased. fpc -Mobjfpc printshello world | HELLO WORLD; pxx HEAD and pin v423 printedHELLO WORLD | HELLO WORLD. Same on i386, arm32, aarch64 and riscv32.- Rebinding a param (
s := s + 'x',SetLength(a, 9)): allocs 9755 / frees 2 over 10000 calls. After the fix, frees 9752 / live 3. - Dynarray ELEMENT writes through a value param still reach the caller, as in fpc: dynarrays share, strings copy on write.
Fix
The store side already treated the param as owned. The entry retain and the exit release were missing. Both halves now key on SymOwnedValueParam, and all seven copies of the sweep loop ask SymReleasedAtScopeExit.
Cost: +2.0% on a self-compile of compiler.pas (22395 -> 22832 ms, min of 5, interleaved A/B, same tree with and without the change). fpc pays the same. Possible follow-up: skip the retain for a param that the body never writes.
Verified: fixture output byte-identical to fpc on x86-64, i386, arm32, aarch64, riscv32, esp32c3 and esp32s3. Census flat on x86-64, i386 and aarch64. The pin fails both halves (corrupted output; live=23395).