Skip to content

Phase 4: Thumb-2 LDR/STR with negative offsets - #5

Merged
NevynUK merged 1 commit into
MergeManagedCodefrom
Phase4b
Oct 14, 2026
Merged

NevynUK merged 1 commit into
MergeManagedCodefrom
Phase4b

Conversation

@NevynUK

@NevynUK NevynUK commented Oct 14, 2026

Copy link
Copy Markdown

arm_ldr_imm and arm_str_imm in thumb-codegen.h chose the 16-bit LDR/STR (immediate) encoding for low registers whenever imm < 128 and imm % 4 == 0, but never checked imm >= 0. That encoding takes only an unsigned word offset (0-124), so negative offsets were emitted as large positive ones. The byte and halfword variants already have the check; this adds it to the word variants.

How it showed up

Under --interp, a GC stress app on a Meadow F7 (CCM) locked the core in a double fault: the Mono main thread's SP had become 0. I dumped the interp-to-native trampoline from SDRAM and disassembled it:

str   r1, [r7, #124]     <- ARM_STR_IMM (code, ARMREG_R1, fp_reg, -4)
str   r0, [r7, #120]     <- ARM_STR_IMM (code, ARMREG_R0, fp_reg, -8)
ldr.w ip, [r7, #-4]      (high destination register, so already 32-bit and correct)

On every P/Invoke and icall from interpreted code, the trampoline overwrote its caller's frame (saved registers and frame pointers) and called through uninitialised slots. JIT-compiled code doesn't use this trampoline, but any negative-offset low-register LDR/STR the JIT emitted was affected in the same way.

Testing (CCM, Meadow.OS Phase4 / WildernessLabs/Meadow.OS#697)

  • GC stress under --interp: 5 passes in 229 s, no failures. Before the fix, the first test never completed.
  • GC stress under JIT (--debug), as a regression check: 30/30 tests pass in 121 s.

🤖 Generated with Claude Code

arm_ldr_imm and arm_str_imm picked the 16-bit LDR/STR (immediate) encoding
for low registers whenever imm < 128 and imm was a multiple of 4, without
checking imm >= 0.  That encoding has an unsigned word offset (0-124), so a
negative offset was emitted as a large positive one:
ARM_STR_IMM (code, r1, r7, -4) became "str r1, [r7, dotnet#124]".

The interp-to-native trampoline saves its CallContext pointer and target
address that way, so every P/Invoke or icall from interpreted code wrote
into its caller's stack frame and read back uninitialised slots.  Under the
interpreter the Mono main thread ended up with SP = 0 and the core locked up
in a double fault.  Require imm >= 0 for the 16-bit form, as the byte and
halfword variants already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NevynUK added a commit to WildernessLabs/Meadow.OS that referenced this pull request Oct 14, 2026
WildernessLabs/runtime#5: the 32-bit Thumb-2 LDR/STR encoding for negative
offsets, which fixes the interpreter (--interp) crash.  To be moved to the
MergeManagedCode merge commit once that pull request is merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NevynUK
NevynUK merged commit fafd81e into MergeManagedCode Oct 14, 2026
NevynUK added a commit to WildernessLabs/Meadow.OS that referenced this pull request Oct 14, 2026
WildernessLabs/runtime#5 is merged; the tree is the same as the Phase4b
commit pinned before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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