Skip to content

Three aarch64 defects in the new dynarec - #826

Merged
LibretroAdmin merged 3 commits into
libretro:masterfrom
Pitchoune:fix-arm-dynarec-defects
Aug 22, 2026
Merged

Three aarch64 defects in the new dynarec#826
LibretroAdmin merged 3 commits into
libretro:masterfrom
Pitchoune:fix-arm-dynarec-defects

Conversation

@Pitchoune

Copy link
Copy Markdown
Contributor

Three independent defects on aarch64, found while running this core on a
Raspberry Pi 5.

1. The jump table scan runs off the end

genjmp() bounds its loop with sizeof(jump_table_symbols)/4. The elements
are uintptr_t, eight bytes here, so it iterates twice as many times as the
table has entries.

2. The XKPHYS fold emits an immediate aarch64 cannot encode

emit_xkphys_fold() calls emit_orimm(HOST_TEMPREG, 0xA0000000, HOST_TEMPREG). Bits 31 and 29 are not a contiguous run, so this is not a
valid logical immediate; genimm() rejects it and emit_orimm takes its
fallback, which asserts rs != HOST_TEMPREG and imm < 65536 — both false
here — then materialises the value into HOST_TEMPREG, destroying the source
before reading it. Release builds define NDEBUG, so the asserts vanish and
the fold silently emits wrong code for every memory access with a 64-bit base
register.

Setting the two bits separately encodes directly and never reaches the
fallback. x86 pays one extra instruction on a cold path.

3. Exception paths skip registers clean_registers() cleared

clean_registers() deliberately understates wasdirty: it clears the bit of
any register the block will rewrite before the next block reads it. That
holds for the fall-through path, not for an exception, which leaves the block
at an arbitrary point. The six exception paths wrote back wasdirty verbatim
and handed the interpreter a stale register file.

Note on verification: defects 1 and 3 were found from misbehaviour and
confirmed by fixing them. Defect 2 is silent — I found it by reading
emit_orimm after the fold landed, and have no reproduction case to offer.

genjmp() walks jump_table_symbols with sizeof(table)/4.  The elements
are uintptr_t, eight bytes here, so the loop ran twice as many
iterations as the table has entries and read past its end.
emit_xkphys_fold folds the base onto the segment with

    emit_orimm(HOST_TEMPREG, 0xA0000000, HOST_TEMPREG);

0xA0000000 has bits 31 and 29 set, which is not a contiguous run and so
not a valid aarch64 logical immediate.  genimm() rejects it and
emit_orimm takes its fallback path, which asserts rs != HOST_TEMPREG and
imm < 65536 - both false here - and then materialises the value into
HOST_TEMPREG, destroying the source register before reading it.  Release
builds define NDEBUG, so the asserts vanish and the fold silently emits
wrong code for every memory access whose base register is 64-bit wide.

Set the two bits with one instruction each.  A single set bit is a
contiguous run, so both encode directly and the fallback is never
reached.  x86 pays one extra instruction on a cold path.
clean_registers() deliberately understates wasdirty: it clears the bit
of any register the block will rewrite before the next block reads it,
because that write makes the earlier value irrelevant.  That reasoning
holds for the fall-through path and not for an exception, which leaves
the block at an arbitrary point - possibly before the rewrite.

The six exception paths wrote back i_regs->wasdirty verbatim and so
skipped exactly those registers, handing the interpreter a stale
register file.

Add wb_exception_dirtys(), which restores the cleared bits before
calling wb_dirtys(), and use it on all six.
@LibretroAdmin
LibretroAdmin merged commit 6b28d6c into libretro:master Aug 22, 2026
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.

2 participants