Skip to content

Fix two miscompiles of values of two to four longs passed and returned in registers - #519

Open
j-modernc-org wants to merge 2 commits into
totalspectrum:masterfrom
j-modernc-org:multi-long-values
Open

j-modernc-org wants to merge 2 commits into
totalspectrum:masterfrom
j-modernc-org:multi-long-values

Conversation

@j-modernc-org

Copy link
Copy Markdown

Two silent miscompiles of values of two to four longs, which the asm backends pass and return in registers. They are independent: one commit each, each with its own test.

An argument evaluated once per long. An argument of two to four longs whose expression has a side effect was evaluated once per long: take2(*get2()) called get2 twice for a struct of two longs, three times for three and four times for four. take2(arr[idx()]), take2(getq()->q), a long long element indexed by a call and a call through a function pointer did the same. CheckFunctionCalls passes such an argument through _parm_ temporaries with a multiple assignment, and BuildExprlistFromObject splits an expression that is neither an identifier nor a call into one member reference per long, each of which evaluates the whole expression again. An assignment of the same value is right, because hltransform.c extracts its side effects first.

The fix evaluates the side effects of what the argument reads through, an index and a pointer, into temporaries of one long ahead of the call, so the split reads storage that is already there. It does not reuse ExtractSideEffects: that copies an array whose base has a side effect into a temporary of the array's type, and with it an element of a 64-bit array read as (*f()->words)[g()] came out wrong in one of our test programs.

A member of a returned struct read from the wrong register. A member past the first long of a struct that a call returns in registers was read from a register nothing wrote: mkP().b and mkS().len printed garbage, while mkP().a was right. outasm.c compiles the member (AST_METHODREF) as the call's first result, copied to a temporary, at the member's offset. A member of such a call is now the call's result of that index.

Tests. Test/cexec08.c and Test/cexec09.c fail on master and pass with this change, as P2 asm (on a P2-EDGE) and as P1 asm (spinsim). The bytecode backends pass and return these values their own way, and this change leaves them alone. P1 bytecode (spinsim) gets them wrong on master too: it evaluates such an argument once per long, reads arr[idx()] and the long long element wrong, reads every member of a returned struct wrong, and cannot build cexec08 as written ("Cannot handle indices with large values"). nucode refuses both tests as written. So under __OUTPUT_BYTECODE__ the tests only print their expected text.

Checked with flexspin built from this branch (master b5993a8 plus these two commits):

  • make test_offline: the same tests pass as on master.
  • make test_spinsim: all pass, cexec08 and cexec09 included.
  • make "OPT=-O2 -Werror" builds.
  • The tests Test/runtests_p2.sh runs, on a P2-EDGE: all 30 pass, cexec08 and cexec09 included.
  • 1425 C programs (our compiler's test corpus and fuzzer output, plus our reproducers): 194 build differently from master. The two reproducers of these faults now print gcc's values, and each of the other 192 prints on a P2-EDGE what it printed with master. Most of those pass a value read through a pointer that our compiler checks for null first, and the check now runs once instead of once per long.

Jan Mercl added 2 commits October 14, 2026 15:01
CheckFunctionCalls passes an argument of n > 1 longs through n _parm_
temporaries, set by the multiple assignment (_parm_0, _parm_1) = expr, and
BuildExprlistFromObject splits an expression that is neither an identifier
nor a call into one member reference per long, each of which evaluates the
expression again. So take2(*get2()) called get2 twice for a struct of two
longs, three times for three and four times for four, and an element
indexed by a call, a member read through a call, a long long element
indexed by a call and a call through a function pointer did the same. An
assignment of such a value is right: hltransform.c extracts its side
effects first.

The side effects of what such an argument reads through, an index and a
pointer, are now evaluated once into temporaries of one long ahead of the
call, the base before the index, so the split reads storage that is
already there.

Test/cexec08.c prints each value and the number of calls computing it
made: 2 to 4 calls on master, 1 with this change, as gcc prints. The
bytecode backends pass such an argument their own way, which this does not
change, and the test prints its expected text there.
A struct of two to four longs comes back from a call in registers.
outasm.c compiles a member of it (AST_METHODREF) as the call's first
result, copied to a temporary, at the member's offset, so a member past
the first long was read from a register nothing wrote: mkP().b and
mkS().len were garbage, mkP().a right.

A member of a call whose struct comes back in registers is now the call's
result of that index.

Test/cexec09.c prints garbage for every member past the first on master
and gcc's values with this change. The bytecode backends return such a
struct their own way, which this does not change, and the test prints its
expected text there.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant