← board

i386 target: try...except segfaults (layout-sensitive, not universal)

Symptom

program i386_one_block;
uses sysutils;
begin
  try raise Exception.Create('a'); except on E: Exception do writeln('c1'); end;
  writeln('done');
end.

Compiled --target=i386 and run under tools/run_target.sh i386, this exits 139 (SIGSEGV) — no output at all, not even c1. Same result for:

type EMy = class(Exception) end;
begin
  try raise EMy.Create('a'); except on E: EMy do writeln('c1'); end;
  writeln('done');
end.

and for two sequential (non-nested) try...except blocks in a row (using Exception directly, no subclassing).

But it is not universal — this superficially-similar program does NOT crash and prints correctly (msg: then done, though E.Message itself comes back empty — a separate, also-pre-existing issue, see below):

type EMy = class(Exception) end;
begin
  try raise EMy.Create('hello');
  except on E: EMy do writeln('msg:', E.Message); end;
  writeln('done');
end.

The only difference between the crashing and non-crashing minimal repros above is the handler body's statement (writeln('c1') vs. writeln('msg:', E.Message)) — i.e. this looks layout/code-shape-sensitive (consistent with a stack-frame or exception-frame corruption bug whose symptom depends on what happens to sit in the corrupted memory), not a clean "any try/except crashes" defect. Each individual repro is deterministic (same binary, same crash, every run) — it's cross-program variation, not run-to-run flakiness.

Confirmed pre-existing, not a new regression

Reproduced against the compiler binary from immediately before [[bug-except-base-handler-misses-derived]]'s fix landed (pin v127) — this bug predates that work and is unrelated to it. Filing separately since it's a distinct defect (crash/memory-corruption vs. that ticket's handler-selection logic bug).

Independent of the crash: even in the non-crashing repro above, E.Message prints as empty on i386/arm32/aarch64 (msg: with nothing after the colon), while the identical program on x86-64 (native) correctly prints msg:hello. This reproduces on the pre-fix v127 baseline too — a real, separate, pre-existing cross-target defect (the exception object's Message field, or its accessor, isn't reaching the raised value correctly on non-x64 targets). Filing as a candidate follow-up in this ticket's scope note since both were found together, but it may deserve its own ticket if picked up separately — distinct symptom (wrong/empty data, not a crash), possibly a distinct root cause (field layout vs. String/AnsiString ABI differences across targets).

Scope

Acceptance

Fixed (v140)

Root cause: a shared, target-independent IR-lowering gap, not a per-backend codegen bug — which is why it hit i386/arm32/aarch64 alike but not x86-64 (whose call-arg path happens to tolerate the bad value; the "layout-sensitive" framing was a red herring caused by chasing per-backend codegen when the real bug was upstream in ir.inc).

ir.inc's AN_CALL lowering materializes a non-lvalue managed-string argument (a literal, concat, or coercion) into a hidden owning local before passing it by value — this is what gives a const s: string parameter a real managed AnsiString heap handle instead of a raw frozen-string representation. That materialization was gated on cpi >= 0. Class instantiation (TFoo.Create(...)) lowers through a distinct negative cpi sentinel (-Ord(tkGetMem)), so every constructor call was silently excluded from this path. A string-literal argument to any constructor's const s: string parameter therefore reached codegen still tagged as a raw frozen string; the by-value call-arg push in each backend then pushed that raw value as if it already were a real heap pointer.

This explains both observations filed in this ticket as one root cause:

  1. E.Message emptyException.Create('literal') stored the bogus non-managed "handle" into the Message field. Reading it back doesn't crash, it just isn't a valid AnsiString, so printing it gives nothing.
  2. The SIGSEGV — the same bogus handle blows up only when something later in the unwind/ARC-release path actually dereferences or frees it (e.g. AnsiString release on scope exit). Whether that dereference is reached depends on subtle codegen differences between handler bodies — exactly the "layout-sensitive" behavior originally observed (the E.Message handler body happened to avoid the release path that the writeln('c1')-only handler body hit).

Fix (compiler/ir.inc, argIsManagedTemp in the AN_CALL lowering loop): added a second disjunct covering the constructor case, reusing slot (the constructor's already-resolved proc index, computed earlier in the same loop iteration by the existing isRefArg block) to look up the target parameter's TypeKind, mirroring the existing cpi >= 0 case:

argIsManagedTemp :=
  (not isRefArg) and
  (((cpi >= 0) and (pathIdx < Procs[cpi].ParamCount) and
    (Procs[cpi].Params[pathIdx].TypeKind = tyAnsiString)) or
   ((cpi < 0) and (-cpi = Ord(tkGetMem)) and (pathIdx > 0) and
    (slot >= 0) and (pathIdx < Procs[slot].ParamCount) and
    (Procs[slot].Params[pathIdx].TypeKind = tyAnsiString))) and
  (ASTKind[ASTLeft[item]] <> AN_IDENT) and
  (ASTKind[ASTLeft[item]] <> AN_FIELD) and
  (ASTKind[ASTLeft[item]] <> AN_INDEX) and
  (ASTKind[ASTLeft[item]] <> AN_DEREF);

One shared fix resolved all four targets (x86-64 unaffected/no regression; i386/arm32/aarch64 all fixed) with a single change, no per-backend patch needed.

Verification:

Committed as dcc98ca1 (pin v140).

Log