Skip to content

wip: sidecar: sync RAX from intercept message in handle_io_port_exit#3360

Open
emirceski wants to merge 4 commits intomicrosoft:mainfrom
emirceski:sidecar-interrupt-reentry-fix
Open

wip: sidecar: sync RAX from intercept message in handle_io_port_exit#3360
emirceski wants to merge 4 commits intomicrosoft:mainfrom
emirceski:sidecar-interrupt-reentry-fix

Conversation

@emirceski
Copy link
Copy Markdown
Contributor

@emirceski emirceski commented Apr 22, 2026

sidecar: sync RAX from intercept message in IO port exit handler

Fixes ICM 781515258 — assert_eq!(message.rax, cpu_context.gps[RAX]) crash in handle_io_port_exit for sidecar VPs.

Some or all of the below is wrong. Ignore it until we get a better understanding of the crash and fix.

Problem
On sidecar VPs, the cpu_context.gps[] array contains VTL2's own register values, not the guest's. The sidecar's run_vp_once() inline asm captures VTL2's registers on VTL re-entry — the hypervisor restores VTL2's saved state, not the guest's. When an interrupt re-entry precedes an intercept exit, the guest may change RAX between exits, causing the assert_eq! against message.rax to fire.

Fix

In handle_io_port_exit, replace the assert_eq! with an unconditional sync of RAX from the intercept message into cpu_context. The intercept message always carries the correct guest RAX value, regardless of whether the VP is a sidecar VP or not. This is safe for non-sidecar VPs too, since the intercept message RAX matches cpu_context RAX in that case.

Other intercept handlers (CPUID, MSR) already write their result registers to cpu_context from the intercept message fields, so they are not affected.

Call flow

VTL2 (sidecar)                    Hypervisor                     VTL0 (guest)
─────────────                     ──────────                     ────────────
1. Write RAX/RCX to               
   VtlControl.registers[]
2. "call rax" (VTL return) ──────► 3. Save VTL2 regs to shadow
                                   4. Restore VTL0 regs ────────► 5. Guest runs
                                                                   6. Guest hits IO port
                                   7. Save VTL0 regs ◄────────── 
                                   8. Restore VTL2 regs from shadow
◄────── RE-ENTRY ────────────────  9. Return to VTL2
10. lateout("rax") captures
    physical RAX
    = VTL2's saved RAX from step 3
    ≠ guest's RAX from step 6
    
    message.rax = correct guest RAX from step 7
```_

Fixes crash at `assert_eq!(message.rax, cpu_context.gps[RAX])` crash in
`handle_io_port_exit` for sidecar VPs.

The sidecar's `run_vp_once()` uses inline asm `inout`/`lateout` operands
to preserve VTL2's own registers across the VTL return hypercall. On
VTL2 re-entry, the hypervisor restores VTL2's saved register state —
not the guest's. The resulting `cpu_context.gps[]` values are VTL2
round-tripped values, not the guest's actual registers at intercept time.

When the `run_vp_once()` loop handles an interrupt re-entry (loops back)
followed by an intercept, the guest may have changed registers between
the two exits. The VMM then reads stale `cpu_context.gps[RAX]` and the
existing `assert_eq!` against `message.rax` fires.

After copying the intercept message, sync GP and XMM0–5 registers from
the hypervisor's register page into `cpu_context`. The register page
contains the authoritative guest register state, populated by the
hypervisor on each intercept.

All three conditions must be true simultaneously:
1. VP is running in sidecar (non-sidecar VPs use the kernel driver)
2. An interrupt re-entry precedes the intercept (guest runs more code)
3. The VMM reads `cpu_context.gps[]` for the intercept type (IO port)

Sidecar VPs are removed after their first intercept, so the window is
one exit sequence per VP lifetime.

```
VTL2 (sidecar)                    Hypervisor                     VTL0 (guest)
─────────────                     ──────────                     ────────────
1. Write RAX/RCX to
   VtlControl.registers[]
2. "call rax" (VTL return) ──────► 3. Save VTL2 regs to shadow
                                   4. Restore VTL0 regs ────────► 5. Guest runs
                                                                   6. Guest hits
                                                                      interrupt/intercept
                                   7. Save VTL0 regs ◄──────────
                                   8. Restore VTL2 regs from shadow
◄────── RE-ENTRY ────────────────  9. Return to VTL2
                                      (execution resumes after "call rax")
10. lateout("rax") captures
    physical RAX
    = VTL2's saved RAX from step 3
    ≠ guest's RAX from step 6
```

- 20 crashes, all on TIP build `0.8.277.0`, April 16–18 2026
- Consistent `+0xB0` offset: `cpu_context = (message.rax + 0xB0) & 0xFFFFFFFF`
- `0xB0` = `offsetof(HvVpAssistPage, intercept_message.payload.rax)`
- Not a servicing path (`keepalive=false`, no per-CPU override logs)
- HV team confirmed no hypervisor-side RAX changes

- XMM6–15 remain stale (register page only carries XMM0–5)
- FP control state (MXCSR, FCW) remains stale (`fxsave` captures VTL2's)
- If register page is not mapped (older HV), stale values remain
@emirceski emirceski requested a review from a team as a code owner April 22, 2026 22:18
Copilot AI review requested due to automatic review settings April 22, 2026 22:18
@github-actions github-actions Bot added the unsafe Related to unsafe code label Apr 22, 2026
@github-actions
Copy link
Copy Markdown

⚠️ Unsafe Code Detected

This PR modifies files containing unsafe Rust code. Extra scrutiny is required during review.

For more on why we check whole files, instead of just diffs, check out the Rustonomicon

Copy link
Copy Markdown
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

This PR fixes a crash in sidecar VP intercept handling by ensuring the VMM-visible cpu_context reflects the guest’s authoritative register state at intercept time, rather than VTL2 round-tripped register values after interrupt re-entry.

Changes:

  • Pass register_page_mapped into run_vp_once() to conditionally sync registers on intercept.
  • On intercept, copy GP registers and XMM0–5 from the hypervisor-provided register page into command_page.cpu_context.

Comment thread openhcl/sidecar/src/arch/x86_64/vp.rs Outdated
Comment thread openhcl/sidecar/src/arch/x86_64/vp.rs Outdated
…sters

- Skip index 4 (CR2 in CpuContextX64, RSP in register page) during
  GP register sync to preserve the captured CR2 value.
- Gate register page sync on is_valid != 0 to avoid copying
  stale/zero data when the page is mapped but not populated.
@github-actions
Copy link
Copy Markdown

Copilot AI review requested due to automatic review settings April 23, 2026 16:49
@github-actions github-actions Bot removed the unsafe Related to unsafe code label Apr 23, 2026
Copy link
Copy Markdown
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

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment thread openhcl/virt_mshv_vtl/src/processor/mshv/x64.rs
Comment thread openhcl/virt_mshv_vtl/src/processor/mshv/x64.rs
let interruption_pending = message.header.execution_state.interruption_pending();

if message.access_info.string_op() || message.access_info.rep_prefix() {
self.vp.runner.cpu_context_mut().gps_no_rsp[protocol::RAX] = rax;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

RAX is written into cpu_context in both branches, which makes it easy to accidentally diverge later. Since the assignment is unconditional, consider moving it before the if and/or taking a single mutable reference to the CPU context once and reusing it to avoid repeated cpu_context_mut() calls. This reduces duplication and makes the intent clearer.

Copilot uses AI. Check for mistakes.
let access_size = message.access_info.access_size();
let is_write = message.header.intercept_access_type == HvInterceptAccessType::WRITE;
let port = message.port_number;
self.vp.runner.cpu_context_mut().gps_no_rsp[protocol::RAX] = rax;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

RAX is written into cpu_context in both branches, which makes it easy to accidentally diverge later. Since the assignment is unconditional, consider moving it before the if and/or taking a single mutable reference to the CPU context once and reusing it to avoid repeated cpu_context_mut() calls. This reduces duplication and makes the intent clearer.

Copilot uses AI. Check for mistakes.
@emirceski emirceski changed the title wip: sidecar: sync guest GP/XMM registers from register page on intercept to fix stale cpu_context wip: sidecar: sync RAX from intercept message in handle_io_port_exit Apr 23, 2026
@emirceski emirceski changed the title wip: sidecar: sync RAX from intercept message in handle_io_port_exit sidecar: sync RAX from intercept message in handle_io_port_exit Apr 23, 2026
@emirceski emirceski changed the title sidecar: sync RAX from intercept message in handle_io_port_exit wip: sidecar: sync RAX from intercept message in handle_io_port_exit Apr 23, 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