← board

Stack frame corruption on inline string concatenation assignments

Problem

Inline string concatenations passed directly to methods or assigned to properties (e.g. Memo.Text := 'Line 1' + #10 + 'Line 2') can corrupt the stack layout in the compiled binary, leading to incorrect values being popped from the stack (such as popping string metadata lengths into parameter registers like rdi or rsi), resulting in segmentation faults or incorrect function calls.

Reproduction

var
  Memo: TMemo;
begin
  Memo := TMemo.Create;
  Memo.Text := 'Line 1' + #10 + 'Line 2'; // stack layout corruption during setter call
end.

Root cause

When string concatenation occurs inline inside a call argument list or property setter assignment, the compiler generates temporary string values on the stack but fails to correctly manage the stack pointer offsets before making the function or method call. This causes caller and callee register/stack mismatches when parameters are popped.

Workaround

Assign the concatenated string to a local variable s first, and then assign that variable to the property or pass it to the method.

var
  s: string;
begin
  s := 'Line 1' + #10 + 'Line 2';
  Memo.Text := s; // Safe, no stack corruption
end;

Fix direction

Correct the stack management and temporary cleanup logic in compiler/ir_codegen.inc when compiling inline binary operations (like string concatenation) that serve as arguments or property setters.

Log

Root-cause analysis 2026-06-20 (precise; fix still open)

Confirmed and narrowed on x86-64:

Attempted fix (reverted): auto-spill a tyString-concat call arg to a hidden local (the automatic s:=a+b; f(s)) in IRLowerCallArg. It compiles + self-hosts byte-identical but the produced programs still crash (return address -> 0), even for a single-arg call inside a proc, for a reason not yet isolated (the manual s:=a+b; f(s) works, so it is something about the IR-lowering-time temp / emission order, not the frame slot per se). Needs a gdb-step of the emitted sequence. STORE_SYM tyString (ir_codegen.inc ~1597) also does not restore rsp.

Fix directions (pick one):

  1. Make frozen-string concat write into a caller-provided destination (the spill temp) and restore rsp — eliminates the stack buffer entirely as an rvalue.
  2. In the internal-call arg loop, copy a concat (rsp-dirtying) arg result to a stable slot before pushing the next arg. Either way the multi-arg hole must go.

DONE 2026-06-20

Fixed in IRLowerCallArg (ir.inc): a frozen-tyString concatenation argument is spilled to a hidden local and passed by ADDRESS (IR_LEA), not value. The extra twist beyond the spill: a frozen-string LOCAL loaded via IR_LOAD_SYM yields the inline length word (TypeSize(tyString)=8), so the argument must be the slot address — IR_LEA — like any frozen-string argument. Eliminates the stack hole. Validated: method + plain calls, single/multi concat, embedded #10. Reseed (make bootstrap), make test green. test/test_inline_concat_arg.pas added.