Skip to content

[cDAC] Fix AMD64 epilogue unwinder reading register number from wrong byte offset#124845

Merged
max-charlamb merged 2 commits intomainfrom
copilot/fix-pop-instruction-reading
Feb 25, 2026
Merged

[cDAC] Fix AMD64 epilogue unwinder reading register number from wrong byte offset#124845
max-charlamb merged 2 commits intomainfrom
copilot/fix-pop-instruction-reading

Conversation

Copy link
Contributor

Copilot AI commented Feb 25, 2026

Description

During epilogue emulation in the managed cDAC AMD64 unwinder, the register number for single-byte pop r64 instructions (registers 0–7) was extracted from nextByte + 2 instead of nextByte. Since pop r64 is a single-byte opcode (0x58 + r), the register is encoded in the low 3 bits of the opcode byte itself — reading two bytes ahead returns an arbitrary instruction stream byte, silently corrupting the unwound register context and potentially cascading into wrong instruction pointers in subsequent frames.

Fix

// Before (wrong — reads 2 bytes past the opcode)
byte registerNumber = (byte)(ReadByteAt(nextByte + 2) & 0x7);

// After (correct — matches native dbs_stack_x64.cpp: RegisterNumber = NextByte[0] & 0x7)
byte registerNumber = (byte)(ReadByteAt(nextByte) & 0x7);

The REX-prefix pop case (registers 8–15) and the non-epilogue epilogue-detection scan are both correct and unaffected.

Changes

  • AMD64Unwinder.cs line 479: Change ReadByteAt(nextByte + 2)ReadByteAt(nextByte) to correctly extract the register number from the pop r64 opcode byte, matching the native unwinder in dbs_stack_x64.cpp.

Testing

  • Existing cDAC test suite (836 tests) passes.
Original prompt

Bug

In src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64/AMD64Unwinder.cs, the epilogue emulation code for single-byte pop r64 instructions (registers 0–7) reads the register number from the wrong byte offset.

The Problem

On x64, a pop r64 instruction for registers 0–7 is a single byte: 0x58 + register (e.g., pop rbx = 0x5b). The register number is encoded in the low 3 bits of that opcode byte itself.

At line 479, during epilogue emulation (the block starting around line 470 where inEpilogue is true), the code reads:

byte registerNumber = (byte)(ReadByteAt(nextByte + 2) & 0x7);

This reads 2 bytes past the pop instruction, which is some unrelated instruction byte. It should read from nextByte itself:

byte registerNumber = (byte)(ReadByteAt(nextByte) & 0x7);

Reference: Native Implementation

The native C++ unwinder in src/coreclr/unwinder/amd64/dbs_stack_x64.cpp correctly does:

RegisterNumber = NextByte[0] & 0x7;

Impact

This causes the wrong register to be restored during epilogue unwinding on x64. For example, if the instruction is pop rbx (0x5b), instead of restoring register 3 (rbx), the code reads whatever byte is at controlPC + 2 and uses its low 3 bits as the register index. This corrupts the unwound context, which can cascade into incorrect instruction pointers in subsequent frames, potentially causing divergence between the cDAC and native DAC stack walks.

Fix

In the epilogue emulation section of AMD64Unwinder.Unwind() (around line 479), change:

byte registerNumber = (byte)(ReadByteAt(nextByte + 2) & 0x7);

to:

byte registerNumber = (byte)(ReadByteAt(nextByte) & 0x7);

This matches the native unwinder behavior and correctly extracts the register number from the pop opcode byte.

This pull request was created from Copilot chat.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Fix register number reading for pop r64 instructions Fix AMD64 epilogue unwinder reading register number from wrong byte offset Feb 25, 2026
Copilot AI requested a review from max-charlamb February 25, 2026 05:20
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@max-charlamb max-charlamb marked this pull request as ready for review February 25, 2026 14:25
Copilot AI review requested due to automatic review settings February 25, 2026 14:25
@max-charlamb max-charlamb changed the title Fix AMD64 epilogue unwinder reading register number from wrong byte offset [cDAC] Fix AMD64 epilogue unwinder reading register number from wrong byte offset Feb 25, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a bug in the managed cDAC AMD64 unwinder’s epilogue emulation where the destination register for single-byte pop r64 opcodes (regs 0–7) was decoded from the wrong instruction byte, which could corrupt the unwound register context.

Changes:

  • Correctly decode the register number for pop r64 (0–7) from the opcode byte at nextByte instead of nextByte + 2.

Copy link
Member

@max-charlamb max-charlamb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot found this bug in my C# amd64 unwinder implementation.

@max-charlamb max-charlamb merged commit 69e4ae5 into main Feb 25, 2026
57 of 59 checks passed
@max-charlamb max-charlamb deleted the copilot/fix-pop-instruction-reading branch February 25, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants