← board

A memory fault dies silently, where FPC reports runtime error 216

Measured

Same source, fpc -O- -Mobjfpc vs pxx at 27232bed4:

program FPC pxx
p := nil; writeln(p^) Runtime error 216, exit 216 (nothing), exit 139
p := nil; p^ := 1 216 139
nil procedure variable, called 216 139
virtual method on a nil object 216 139
a[1000000] := 5 on array[0..3] 216 139
Int64 div 0 Runtime error 200, exit 200 Runtime error 200 (division by zero), exit 200

Division by zero is exactly right — pxx even prints a better message. Every MEMORY fault is the gap, and it is the most common runtime fault there is.

try..except cannot help either: FPC catches these as EAccessViolation, pxx takes the signal and the process is gone, so the except block and everything after it never run.

Cause

The signal runtime is present and default-on for the PC targets (EmitSignalRuntimeForTarget, --no-signals opts out). It installs the dispatch handler for SIGINT(2) and SIGTERM(15) only; SIGSEGV and SIGBUS are never installed, so the kernel default disposition applies and the process dies with no message.

Everything the fix needs already exists:

So tier 1 is a fault stub modelled on those two, plus SIGSEGV/SIGBUS in the entry stub's install list.

Two tiers, and the second is much bigger

  1. Report and dieRuntime error 216 to the output, exit_group(216). Matches FPC's exit code and first line; leaves try..except blind. Small, and it is what turns "Segmentation fault" into something a user can act on.
  2. Raise a catchable EAccessViolation — FPC does this. It means unwinding out of signal context into the exception machinery, and every trap-to-raise path has to agree with PXXDivZero's existing raise-hook upgrade. Its own sitting.

Do tier 1 first; it is most of the user-visible value.

Open question — DEFAULT-ON or a flag? (Track U)

The signal runtime is default-on ("predictable process behavior beats size sniffing"), which argues for default-on here. But the closest precedent argues the other way: FPC-style float runtime errors are opt-in behind --fpc-float-errors. Installing SIGSEGV by default changes the death of every pxx binary — exit 216 instead of 139, and no core dump unless the handler is declined — so it is a decision, not a detail. Filed alongside as decide-segv-runtime-error-default.

Per-arch scope

x86-64 first. The other signal runtimes (aarch64, arm32, i386, riscv32) have their own emitters and their own frame-shape constraints — the SA_SIGINFO note in EmitSignalRuntime explains why i386/arm32 cannot simply copy the x86-64 install flags. Stage them, and say in the ticket which are done, rather than half-implementing all five.

Gate

make compiler/pascal26 + self-host fixedpoint, tools/gate.sh quick, and a test asserting exit code 216 and the message for each of the five fault shapes above (a .expected cannot carry an exit code, so assert it in the Makefile recipe like the other !-guarded rows do).

Resolution — tier 1, opt-in (2026-08-21)

All five fault shapes in the table now report FPC's number and exit code, behind the new --fpc-mem-errors:

program before after (--fpc-mem-errors)
p := nil; writeln(p^) (nothing), exit 139 Runtime error 216 (access violation: address not mapped), exit 216
p := nil; p^ := 1 exit 139 216
nil procedure variable, called exit 139 216
virtual method on a nil object exit 139 216
a[wild] := 5 on array[0..3] exit 139 216

Without the flag every one of them still dies on 139, unchanged.

Opt-in, because the default is not mine to pick

The ticket's own open question — default-on or a flag — is [[decide-segv-runtime-error-default]] and is unresolved. So this ships as --fpc-mem-errors, the exact shape of the closest precedent (--fpc-float-errors), and the decision stays a one-line change to the default whenever the user makes it. Nothing about the flag's existence pre-empts it.

The Makefile row asserts both directions — 216 with the flag, 139 without — precisely so that flipping the default later is a deliberate act that shows up as a failing row rather than a silent behaviour change.

What it is built from

Nothing new, as the ticket predicted. EmitFpcMemErrStub is EmitFpcFloatErrStub's shape: a parameterless signal hook reading the parked BSS_SIG_NUM and BSS_SIG_CODE, writing the message with sys_write and leaving via exit_group(216). Pure syscalls, no unit dependency, never returns (returning resumes the faulting instruction, forever).

Install is two SigSetHook calls — one for SIGSEGV(11), one for SIGBUS(7). SigSetHookAddr falls through into SigInstallAddr, so each call both records the hook and registers the SA_SIGINFO dispatch handler, which is what parks the si_code the decoder reads.

SIGBUS gets its own message rather than being folded into "access violation": it is a different fault (misaligned or invalid mapping, not an unmapped address), and printing the plausible-but-wrong one is what this dialect refuses. FPC numbers both 216.

Known gaps, stated rather than hidden

Gate

tools/gate.sh quick GREEN (self-host fixedpoint 104s). New test/test_fpc_mem_errors.pas — one source, five fault shapes selected by argv[1] — wired into the core list asserting exit code, message and that the fault happened after before printed, in both flag states.

Log