A shortstring write on xtensa corrupts a NEIGHBOURING variable
THE PREMISE IS WRONG. Nothing is corrupted. (frankS, 2026-08-30)
This is not memory corruption. It is frozen-string EQUALITY comparing addresses. I wrote the "memory corruption" framing myself, into the handback table, a commit message and a message to the coordinator, and I got it from the test's own failure message without checking it. The slug and title stay so the links resolve; everything below the fold is the real defect.
test_shortstring_truncprintsb-CLOBBEREDbecause it testsif b = 'BBBB'. Printbinstead and it isBBBB,Length(b)is4, and the neighbour is intact. The write truncates correctly. The comparison is what is broken, and it is broken for every frozen string, not just after a truncating write.How the wrong reading survived one level of scepticism: my first minimal repro assigned an oversized literal to
a, printedaand printedb, and passed. I had reproduced the write and dropped the comparison — removing the actual defect from my own repro while believing I had bounded it. The test row that saysCLOBBEREDnames a mechanism it never verifies, and a repro built from the message rather than from the code inherits the same error.
The defect
b = 'BBBB' for b: string[4] answers false.
xtensa print=[BBBB] len=4 | eq-lit WRONG | ne-lit ok | eq-sh WRONG | eq-ansi ok | gt-lit ok
x86-64 print=[BBBB] len=4 | eq-lit ok | ne-lit ok | eq-sh ok | eq-ansi ok | gt-lit ok
The pattern names the cause exactly: = between two frozen strings is wrong;
everything else is right. <> is "right" only because two distinct addresses
really are unequal. > is right because the ordered arm handles frozen. = ms
is right because an AnsiString on either side reaches the string arm.
ir_codegen_xtensa.inc had two guards in front of one compare arm:
{ equality } (op in [tkEq,tkNeq]) and (left=tyAnsiString or right=tyAnsiString)
{ ordered } (op in [tkLt,tkLe,tkGt,tkGe]) and (… or TypeIsFrozenString(left) or TypeIsFrozenString(right))
Both sides frozen and the operator =: neither guard fires, so it falls through
to the integer compare and compares the two buffer addresses.
The comment that made it invisible, and why it was written
Directly above the guard:
The ordered guard also accepts a FROZEN string on either side, which the equality guard does not need: frozen equality already works.
That is false, and it is not carelessness — it is a measurement that passes for the wrong reason. Measured here both ways:
'BBBB' = 'BBBB' -> ok (two identical literals INTERN TO ONE ADDRESS)
b = 'BBBB' -> WRONG
Check frozen equality with two literals and address equality and string equality agree, so a broken compiler answers correctly. Check it with a variable and it never worked. The claim was written by someone who tested the reachable-looking case; the case that distinguishes the two mechanisms is the one with a variable in it.
Sixth backend skipped, again — and riscv32 already had the fix
grepping the sibling before writing this: x86-64, i386, arm32, aarch64 and
riscv32 all pass every check. Only xtensa failed. riscv32 carries the fix with
the identical root cause in its own comment:
Was gated on tyAnsiString only, so frozen = frozen (e.g.
ParamStr(1) = '--selftest') compared ADDRESSES.
Same shape as ABIParamSlotHoldsValueAddr and as PXXStrCmp3's own miscount
three lines above this guard — "the FOUR cross backends had no ordered-string
arm at all; there were five, and that miscount is why xtensa was never
visited." The arm that says that was itself written with a two-guard split,
which is the same defect committed a second time inside its own fix.
Fix
Merge the two guards into one covering all six operators with the frozen
terms on both sides — rather than adding the missing terms beside the existing
guard. The operand decompose already handles frozen (len at [buf], src at
buf+8); only the guard excluded it. One guard cannot diverge from itself.
Measured
129-source differential, compiler 147123b1bb41 (verified self-host fixedpoint):
before MATCH 99 DIFF 8 CFAIL 21
after MATCH 100 DIFF 7 CFAIL 21
exactly one row moved, DIFF -> MATCH; regressions: NONE
test_shortstring_trunc wired into test-xtensa (now 101). The row uses the
variable form deliberately, with a comment saying why a literal-vs-literal
check would pass on the broken compiler — otherwise the next person simplifying
that row reintroduces the blind spot that hid this for the life of the backend.
Log
- 2026-08-30 — resolved, commit 6369a8d63.