← board

ArgStr reads past argv into the environment on riscv32 and xtensa

ArgStr(i, s) / ParamStr(i) with i >= ParamCount+1 returns an ENVIRONMENT STRING instead of an empty one on both 32-bit generic backends. x86-64 defends against exactly this and says so:

{ Bounds check: out-of-range index (>= argc, or negative) publishes nil (empty
  string) instead of dereferencing a bad argv slot. argc is at [initial_rsp]. }
  'mov rcx, [rcx]',      { rcx = argc }
  'cmp rax, rcx'

riscv32 has no such check, and its comment records the gap as known:

rv32_lw(reg_a0, reg_a0, 0); { argv[index] (or junk past envp for a huge index — Pascal callers pass 0..ParamCount) }

Measured

test_arm32_arg_runtime run with NO arguments (it does ArgStr(2, fixed)):

target output
x86-64 (oracle) 0\n\n\n
riscv32 0\n\nLESSOPEN=| /usr/bin/lesspipe %s\n
xtensa (both ABIs) 0\n\nLESSOPEN=| /usr/bin/lesspipe %s\n

The stack at entry is [argc][argv0..][NULL][envp0..], so with argc = 1, argv[2] is envp[0]. Both backends compute the slot correctly and simply do not stop at the end of the array.

Severity: it is a silent wrong value, and it reads memory the program did not ask for

Not a crash. A user variable quietly receives an environment string, which is the "plausible wrong value far from the cause" shape — and the value is attacker-influenceable process state rather than garbage, so a program that prints or logs a ParamStr past the end discloses its environment. That is why this is p45 and not p20 despite Pascal callers pass 0..ParamCount being true of careful code.

Provenance — NOT introduced by the xtensa -55 work

xtensa reached this only by gaining the tkArgCount/tkArgStr arms at all (feature-a-xtensa-the-last-five-builtins-and-the-entry-stub-that-blocks-one); before that the program did not compile. riscv32 has behaved this way for as long as it has had the arm, with the identical byte-for-byte output, and was already a DIFF in the cross differential. The xtensa port is faithful to riscv32; both are unfaithful to x86-64.

Filed rather than folded into the -55 change under the standing rule that a grant covers every arm of that defect and not an adjacent different one.

Fix

Port x86-64's check to both backends: load argc from [BSS_INITIAL_RSP], compare the index unsigned against it, and on out-of-range publish an empty result rather than dereferencing — nil handle for a managed destination, a zero-length buffer for a frozen one. Both arms live in ir_codegen_riscv32.inc and ir_codegen_xtensa.inc; nothing shared is involved.

Do BOTH backends in one change. This defect is already the "fixed on one target, left on the others" pattern that this repo keeps paying for — see bug-a-riscv32-pc-relative-encoders-silently-truncate-xtensa-already-guards filed the same night for the identical shape one layer down.

Gate

make compiler/pascal26 to fixedpoint, then test_arm32_arg_runtime run with NO arguments against the x86-64 oracle on riscv32 and xtensa (both ABIs) — that is the case the existing Makefile rows miss, because they all pass alpha beta and stay in range. Add a no-argument row while fixing it.


Resolved 2026-08-31 as a DUPLICATE — verified by running

The fix landed under the sibling ticket [[bug-a-argv-to-frozen-string-is-unchecked-on-four-untested-targets]] as 48c89fab5 "bound the argv index against argc on all five cross backends" (confirmed an ancestor of origin/master, not a ghost). It touched ir_codegen386.inc, _aarch64, _arm32, _riscv32 and _xtensa, and wired test/test_paramstr_out_of_range.pas. This slug was simply never closed.

Checked by executing it, since "the code has a check" is not the same claim as "the check fires". Binary 73396b86f09a:

                riscv32          xtensa           x86-64 (oracle)
no args         1..4 -> len=0    1..4 -> len=0    1..4 -> len=0
AA BB           1=AA 2=BB        1=AA 2=BB        1=AA 2=BB
                3,4 -> len=0     3,4 -> len=0     3,4 -> len=0

The second row is the positive control: with arguments present the in-range indices return real values while the out-of-range ones return '', so the bound is discriminating rather than an unconditional empty string. All three targets agree.

Note for whoever reads the group: the neighbouring bug-a-q-plus-overflow-checking-... is NOT also fixed, though a careless probe says it is — test/test_a64_leafsym_binops.pas, the source its repro names, no longer triggers {$Q+}, so it compiles clean on arm32 and riscv32 and looks green. A real overflow still fails to compile there. Verified 2026-08-31.

Log