From c5162fe735b86617162a9d6dfd3b2c40d087eac4 Mon Sep 17 00:00:00 2001 From: Joshua Scarsbrook Date: Sun, 13 Sep 2026 20:00:55 +1000 Subject: [PATCH 1/3] Expose x86 KVM state and execution control for PC guests --- hypervisor/x86.go | 51 ++++++ hypervisor/x86_linux_amd64.go | 137 ++++++++++++++++ hypervisor/x86_linux_amd64_test.go | 159 +++++++++++++++++++ hypervisor/x86_other.go | 12 ++ hypervisor/x86state/state.go | 59 +++++++ internal/hv/kvm/abi_linux_amd64.go | 62 +------- internal/hv/kvm/architectural_linux_amd64.go | 60 +++++++ internal/hv/kvm/bindings_linux_amd64.go | 5 + internal/hv/kvm/debug_linux_amd64.go | 21 +++ 9 files changed, 510 insertions(+), 56 deletions(-) create mode 100644 hypervisor/x86.go create mode 100644 hypervisor/x86_linux_amd64.go create mode 100644 hypervisor/x86_linux_amd64_test.go create mode 100644 hypervisor/x86_other.go create mode 100644 hypervisor/x86state/state.go create mode 100644 internal/hv/kvm/architectural_linux_amd64.go create mode 100644 internal/hv/kvm/debug_linux_amd64.go diff --git a/hypervisor/x86.go b/hypervisor/x86.go new file mode 100644 index 00000000..ea6cce55 --- /dev/null +++ b/hypervisor/x86.go @@ -0,0 +1,51 @@ +package hypervisor + +import ( + "context" + "j5.nz/cc/hypervisor/x86state" +) + +// X86Exit exposes architectural exits. IO.Data is borrowed until the next Run; +// fill it for input instructions. CompleteMMIORead completes MMIO reads. +type X86Exit struct { + Reason uint32 + Port uint16 + Size uint8 + Count uint32 + Write bool + Data []byte + Address uint64 +} + +// RAMRegion maps a page-aligned range of a shared backing allocation. +// Unmapped ranges remain device apertures and generate MMIO exits. +type RAMRegion struct{ Address, Offset, Size uint64 } + +const ( + X86ExitIO uint32 = 2 + X86ExitDebug uint32 = 4 + X86ExitHLT uint32 = 5 + X86ExitMMIO uint32 = 6 + X86ExitShutdown uint32 = 8 +) + +// X86 owns its RAM and one CPU. All methods except Cancel must be serialized. +// The PC interrupt controllers and interval timer are provided by the backend. +// Firmware, disk and display device semantics belong to the caller. +type X86 interface { + MapRAM(base, size uint64) ([]byte, error) + MapRAMRegions(size uint64, regions []RAMRegion) ([]byte, error) + Registers() (x86state.Registers, error) + SystemRegisters() (x86state.SystemRegisters, error) + SetRegisters(x86state.Registers) error + SetSystemRegisters(x86state.SystemRegisters) error + Run(context.Context) (X86Exit, error) + // CompleteIO commits a pending IO/MMIO operation without executing the + // next instruction. Call before inspecting or changing CPU state after IO. + CompleteIO() error + CompleteMMIORead(value uint64, size uint32) + SetIRQ(line uint32, level bool) error + InterruptState() (map[string]uint64, error) + Cancel() error + Close() error +} diff --git a/hypervisor/x86_linux_amd64.go b/hypervisor/x86_linux_amd64.go new file mode 100644 index 00000000..1aaa4ef2 --- /dev/null +++ b/hypervisor/x86_linux_amd64.go @@ -0,0 +1,137 @@ +//go:build linux && amd64 + +package hypervisor + +import ( + "context" + "errors" + "fmt" + "runtime" + + "golang.org/x/sys/unix" + "j5.nz/cc/internal/hv/kvm" +) + +type nativeX86 struct { + *kvm.VM + slot uint32 + allocations [][]byte + mappings []RAMRegion +} + +func (n *nativeX86) MapRAMRegions(size uint64, regions []RAMRegion) ([]byte, error) { + if size == 0 || size > uint64(^uint(0)>>1) || size%4096 != 0 || len(regions) == 0 || n.slot != 0 || len(n.allocations) != 0 { + return nil, fmt.Errorf("invalid RAM layout or RAM already mapped") + } + for i, r := range regions { + if r.Size == 0 || r.Offset > size || r.Size > size-r.Offset || r.Address > ^uint64(0)-r.Size || (r.Address|r.Offset|r.Size)%4096 != 0 { + return nil, fmt.Errorf("invalid RAM region %d", i) + } + for _, prior := range regions[:i] { + if r.Address < prior.Address+prior.Size && prior.Address < r.Address+r.Size { + return nil, fmt.Errorf("overlapping RAM regions") + } + } + } + ram, err := unix.Mmap(-1, 0, int(size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANONYMOUS|unix.MAP_PRIVATE) + if err != nil { + return nil, err + } + n.allocations = append(n.allocations, ram) + for _, r := range regions { + if err = n.MapSharedMemory(ram[r.Offset:r.Offset+r.Size], r.Address); err != nil { + return nil, err + } + } + return ram, nil +} + +func (n *nativeX86) Close() error { + err := n.VM.Close() + for _, ram := range n.allocations { + err = errors.Join(err, unix.Munmap(ram)) + } + n.allocations = nil + return err +} + +func NewX86(ctx context.Context) (X86, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + vm, err := kvm.NewVM() + if err != nil { + return nil, err + } + return &nativeX86{VM: vm}, nil +} +func (n *nativeX86) MapRAM(base, size uint64) ([]byte, error) { + if len(n.allocations) != 0 { + return nil, fmt.Errorf("RAM layout already mapped") + } + if size == 0 || size > uint64(^uint(0)>>1) || base > ^uint64(0)-size || (base|size)%4096 != 0 { + return nil, fmt.Errorf("invalid page-aligned RAM mapping") + } + for _, r := range n.mappings { + if base < r.Address+r.Size && r.Address < base+size { + return nil, fmt.Errorf("RAM mapping overlaps existing RAM") + } + } + ram, err := n.MapAnonymousMemorySlot(n.slot, size, base) + if err == nil { + n.slot++ + n.mappings = append(n.mappings, RAMRegion{Address: base, Size: size}) + } + return ram, err +} +func (n *nativeX86) Cancel() error { n.RequestImmediateExit(); return nil } +func (n *nativeX86) Run(ctx context.Context) (X86Exit, error) { + if err := ctx.Err(); err != nil { + return X86Exit{}, err + } + runtime.LockOSThread() + defer runtime.UnlockOSThread() + n.SetVCPUTID(0, unix.Gettid()) + defer n.SetVCPUTID(0, 0) + // Most runs finish at an IO/MMIO exit long before cancellation. Starting + // and joining a goroutine for every VGA access forces scheduler handoffs + // in the hottest device path. AfterFunc runs a goroutine only if cancelled. + joined := make(chan struct{}) + stop := context.AfterFunc(ctx, func() { + n.RequestImmediateExit() + close(joined) + }) + defer func() { + if !stop() { + // A late callback must not interrupt a subsequent run or race Close. + <-joined + } + }() + for { + if err := ctx.Err(); err != nil { + return X86Exit{}, err + } + var ex kvm.Exit + err := n.RunVCPUInterruptible(0, &ex) + if errors.Is(err, unix.EINTR) { + return X86Exit{}, ctx.Err() + } + if err != nil { + return X86Exit{}, err + } + ret := X86Exit{Reason: uint32(ex.Reason)} + if ex.Reason == kvm.ExitUnknown { + return X86Exit{}, fmt.Errorf("unknown KVM hardware exit") + } + if ex.Reason == kvm.ExitIO { + ret.Port, ret.Size, ret.Count, ret.Write, ret.Data = ex.IO.Port, ex.IO.Size, ex.IO.Count, ex.IO.Write, ex.IO.Data + } else if ex.Reason == kvm.ExitMMIO { + if ex.MMIO.Len == 0 || ex.MMIO.Len > 8 { + return X86Exit{}, fmt.Errorf("invalid KVM MMIO width %d", ex.MMIO.Len) + } + ret.Address, ret.Size, ret.Write = ex.MMIO.Addr, uint8(ex.MMIO.Len), ex.MMIO.Write + ret.Data = append([]byte(nil), ex.MMIO.Data[:ex.MMIO.Len]...) + } + return ret, nil + } +} diff --git a/hypervisor/x86_linux_amd64_test.go b/hypervisor/x86_linux_amd64_test.go new file mode 100644 index 00000000..6377a406 --- /dev/null +++ b/hypervisor/x86_linux_amd64_test.go @@ -0,0 +1,159 @@ +//go:build linux && amd64 + +package hypervisor + +import ( + "context" + "errors" + "os" + "testing" + "time" + + "j5.nz/cc/hypervisor/x86state" +) + +func TestX86RealModeInterrupt(t *testing.T) { + if _, err := os.Stat("/dev/kvm"); err != nil { + t.Skip("KVM unavailable") + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + cpu, err := NewX86(ctx) + if err != nil { + t.Fatal(err) + } + defer cpu.Close() + ram, err := cpu.MapRAM(0, 2<<20) + if err != nil { + t.Fatal(err) + } + // INT 13h calls a ROM stub which traps to Go, then IRET returns to + // the boot sector. The sector emits a second OUT to prove the return. + copy(ram[0x7c00:], []byte{0xcd, 0x13, 0xba, 0xf1, 0, 0xee, 0xeb, 0xfe}) + copy(ram[0xf0100:], []byte{0x50, 0xba, 0xf0, 0, 0xee, 0x58, 0xcf}) + ram[0x4c], ram[0x4d], ram[0x4e], ram[0x4f] = 0, 1, 0, 0xf0 + s, err := cpu.SystemRegisters() + if err != nil { + t.Fatal(err) + } + seg := x86state.Segment{Limit: 0xffff, Present: 1, S: 1, Type: 3} + s.Cs, s.Ds, s.Es, s.Ss, s.Fs, s.Gs = seg, seg, seg, seg, seg, seg + s.Cs.Type = 11 + s.Cr0 = 0x10 + s.Cr3 = 0 + s.Cr4 = 0 + s.Efer = 0 + s.Idt = x86state.DescriptorTable{Limit: 0x3ff} + if err = cpu.SetSystemRegisters(s); err != nil { + t.Fatal(err) + } + if err = cpu.SetRegisters(x86state.Registers{Rip: 0x7c00, Rsp: 0x7c00, Rflags: 2, Rax: 0x1234}); err != nil { + t.Fatal(err) + } + for _, port := range []uint16{0xf0, 0xf1} { + for { + ex, err := cpu.Run(ctx) + if err != nil { + t.Fatal(err) + } + if ex.Reason == 0 { + continue + } + if ex.Reason != X86ExitIO || ex.Port != port || !ex.Write || len(ex.Data) != 1 || ex.Data[0] != 0x34 { + t.Fatalf("unexpected exit: %+v", ex) + } + if err := cpu.CompleteIO(); err != nil { + t.Fatal(err) + } + break + } + } + r, err := cpu.Registers() + if err != nil { + t.Fatal(err) + } + if r.Rsp != 0x7c00 { + t.Fatalf("IRET did not restore stack: %#x", r.Rsp) + } + // An execution breakpoint must stop before the instruction, without + // changing guest code, and disabling it must allow normal execution. + debug := cpu.(interface{ SetBreakpoint(uint64) error }) + r.Rip = 0x7c02 + if err := cpu.SetRegisters(r); err != nil { + t.Fatal(err) + } + if err := debug.SetBreakpoint(r.Rip); err != nil { + t.Fatal(err) + } + for { + ex, err := cpu.Run(ctx) + if err != nil { + t.Fatal(err) + } + if ex.Reason == 0 { + continue + } + if ex.Reason != X86ExitDebug { + t.Fatalf("breakpoint: %+v", ex) + } + break + } + stopped, err := cpu.Registers() + if err != nil || stopped.Rip != r.Rip || ram[0x7c02] != 0xba { + t.Fatalf("breakpoint changed execution/code: %+v %v", stopped, err) + } + if err := debug.SetBreakpoint(0); err != nil { + t.Fatal(err) + } + for { + ex, err := cpu.Run(ctx) + if err != nil { + t.Fatal(err) + } + if ex.Reason == 0 { + continue + } + if ex.Reason != X86ExitIO || ex.Port != 0xf1 { + t.Fatalf("resume breakpoint: %+v", ex) + } + if err := cpu.CompleteIO(); err != nil { + t.Fatal(err) + } + break + } + // The next instruction spins. Cancellation must interrupt KVM, and its + // callback must finish before a subsequent run starts using the same CPU. + for i := 0; i < 20; i++ { + deadline, cancel := context.WithTimeout(ctx, 2*time.Millisecond) + for { + _, err = cpu.Run(deadline) + if err != nil { + break + } + } + cancel() + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("cancel run: %v", err) + } + r.Rip = 0x7c02 // mov dx,f1; out dx,al; spin + if err = cpu.SetRegisters(r); err != nil { + t.Fatal(err) + } + for { + ex, err := cpu.Run(ctx) + if err != nil { + t.Fatal(err) + } + if ex.Reason == 0 { + continue + } + if ex.Reason != X86ExitIO || ex.Port != 0xf1 { + t.Fatalf("exit after cancellation: %+v", ex) + } + if err := cpu.CompleteIO(); err != nil { + t.Fatal(err) + } + break + } + } +} diff --git a/hypervisor/x86_other.go b/hypervisor/x86_other.go new file mode 100644 index 00000000..fee3818b --- /dev/null +++ b/hypervisor/x86_other.go @@ -0,0 +1,12 @@ +//go:build !linux || !amd64 + +package hypervisor + +import ( + "context" + "fmt" +) + +func NewX86(context.Context) (X86, error) { + return nil, fmt.Errorf("cc x86 native execution requires Linux/amd64 KVM") +} diff --git a/hypervisor/x86state/state.go b/hypervisor/x86state/state.go new file mode 100644 index 00000000..3cfab456 --- /dev/null +++ b/hypervisor/x86state/state.go @@ -0,0 +1,59 @@ +// Package x86state describes architectural x86 CPU state without host resources. +package x86state + +type Registers struct { + Rax uint64 + Rbx uint64 + Rcx uint64 + Rdx uint64 + Rsi uint64 + Rdi uint64 + Rsp uint64 + Rbp uint64 + R8 uint64 + R9 uint64 + R10 uint64 + R11 uint64 + R12 uint64 + R13 uint64 + R14 uint64 + R15 uint64 + Rip uint64 + Rflags uint64 +} + +type Segment struct { + Base uint64 + Limit uint32 + Selector uint16 + Type uint8 + Present uint8 + Dpl uint8 + Db uint8 + S uint8 + L uint8 + G uint8 + Avl uint8 + Unusable uint8 + Padding uint8 +} + +type DescriptorTable struct { + Base uint64 + Limit uint16 + Padding [3]uint16 +} + +type SystemRegisters struct { + Cs, Ds, Es, Fs, Gs, Ss Segment + Tr, Ldt Segment + Gdt, Idt DescriptorTable + Cr0 uint64 + Cr2 uint64 + Cr3 uint64 + Cr4 uint64 + Cr8 uint64 + Efer uint64 + ApicBase uint64 + InterruptBitmap [4]uint64 +} diff --git a/internal/hv/kvm/abi_linux_amd64.go b/internal/hv/kvm/abi_linux_amd64.go index 55156fd7..fa32cc5a 100644 --- a/internal/hv/kvm/abi_linux_amd64.go +++ b/internal/hv/kvm/abi_linux_amd64.go @@ -2,6 +2,8 @@ package kvm +import "j5.nz/cc/hypervisor/x86state" + const ( kvmNrInterrupts = 256 ) @@ -205,62 +207,10 @@ type kvmClockData struct { Pad [4]uint32 } -type kvmRegs struct { - Rax uint64 - Rbx uint64 - Rcx uint64 - Rdx uint64 - Rsi uint64 - Rdi uint64 - Rsp uint64 - Rbp uint64 - R8 uint64 - R9 uint64 - R10 uint64 - R11 uint64 - R12 uint64 - R13 uint64 - R14 uint64 - R15 uint64 - Rip uint64 - Rflags uint64 -} - -type kvmSegment struct { - Base uint64 - Limit uint32 - Selector uint16 - Type uint8 - Present uint8 - Dpl uint8 - Db uint8 - S uint8 - L uint8 - G uint8 - Avl uint8 - Unusable uint8 - Padding uint8 -} - -type kvmDTable struct { - Base uint64 - Limit uint16 - Padding [3]uint16 -} - -type kvmSRegs struct { - Cs, Ds, Es, Fs, Gs, Ss kvmSegment - Tr, Ldt kvmSegment - Gdt, Idt kvmDTable - Cr0 uint64 - Cr2 uint64 - Cr3 uint64 - Cr4 uint64 - Cr8 uint64 - Efer uint64 - ApicBase uint64 - InterruptBitmap [(kvmNrInterrupts + 63) / 64]uint64 -} +type kvmRegs = x86state.Registers +type kvmSegment = x86state.Segment +type kvmDTable = x86state.DescriptorTable +type kvmSRegs = x86state.SystemRegisters type kvmCPUIDEntry2 struct { Function uint32 diff --git a/internal/hv/kvm/architectural_linux_amd64.go b/internal/hv/kvm/architectural_linux_amd64.go new file mode 100644 index 00000000..9fcdc296 --- /dev/null +++ b/internal/hv/kvm/architectural_linux_amd64.go @@ -0,0 +1,60 @@ +//go:build linux && amd64 + +package kvm + +import ( + "errors" + "fmt" + "unsafe" + + "golang.org/x/sys/unix" + "j5.nz/cc/hypervisor/x86state" +) + +// CompleteIO follows the KVM API's immediate_exit completion protocol. Guest +// registers at an IO exit are otherwise allowed to describe an instruction +// which is still pending inside KVM's emulator. +func (v *VM) CompleteIO() error { + if len(v.vcpus) == 0 || v.vcpus[0] == nil || len(v.vcpus[0].run) == 0 { + return fmt.Errorf("VCPU is closed") + } + cpu := v.vcpus[0] + run := (*kvmRunData)(unsafe.Pointer(&cpu.run[0])) + run.immediateExit = 1 + _, err := ioctlRunVCPUInterruptible(uintptr(cpu.fd)) + if errors.Is(err, unix.EINTR) { + return nil + } + if err != nil { + return err + } + return fmt.Errorf("KVM did not stop after IO completion") +} + +func (v *VM) InterruptState() (map[string]uint64, error) { + result := map[string]uint64{} + for id := uint32(0); id < 2; id++ { + chip, err := getIRQChip(v.vmfd, id) + if err != nil { + return nil, err + } + for i, name := range []string{"last_irr", "irr", "imr", "isr", "priority_add", "irq_base", "read_reg_select", "poll", "special_mask", "init_state", "auto_eoi", "rotate_on_auto_eoi", "special_fully_nested_mode", "init4", "elcr", "elcr_mask"} { + result[fmt.Sprintf("pic%d.%s", id, name)] = uint64(chip.Chip[i]) + } + } + pit, err := getPIT2(v.vmfd) + if err != nil { + return nil, err + } + for i, c := range pit.Channels { + result[fmt.Sprintf("pit%d.count", i)] = uint64(c.Count) + result[fmt.Sprintf("pit%d.mode", i)] = uint64(c.Mode) + result[fmt.Sprintf("pit%d.gate", i)] = uint64(c.Gate) + } + return result, nil +} + +func (v *VM) Registers() (x86state.Registers, error) { return getRegs(v.vcpufd) } +func (v *VM) SystemRegisters() (x86state.SystemRegisters, error) { return getSRegs(v.vcpufd) } +func (v *VM) SetRegisters(r x86state.Registers) error { return setRegs(v.vcpufd, &r) } +func (v *VM) SetSystemRegisters(r x86state.SystemRegisters) error { return setSRegs(v.vcpufd, &r) } diff --git a/internal/hv/kvm/bindings_linux_amd64.go b/internal/hv/kvm/bindings_linux_amd64.go index 5ad479fe..e446311a 100644 --- a/internal/hv/kvm/bindings_linux_amd64.go +++ b/internal/hv/kvm/bindings_linux_amd64.go @@ -69,6 +69,10 @@ const ( const ia32MiscEnableDefault = (1 << 0) | (1 << 3) | (1 << 11) | (1 << 12) | (1 << 16) | (1 << 18) | (1 << 23) +// ioctl arguments may point into Go objects. Preserve those objects across +// stack growth and the blocking system call, including through the retry layer. +// +//go:uintptrescapes func ioctl(fd uintptr, request uint64, arg uintptr) (uintptr, error) { v1, _, err := unix.Syscall(unix.SYS_IOCTL, fd, uintptr(request), arg) if err != 0 { @@ -77,6 +81,7 @@ func ioctl(fd uintptr, request uint64, arg uintptr) (uintptr, error) { return v1, nil } +//go:uintptrescapes func ioctlWithRetry(fd uintptr, request uint64, arg uintptr) (uintptr, error) { for { v1, err := ioctl(fd, request, arg) diff --git a/internal/hv/kvm/debug_linux_amd64.go b/internal/hv/kvm/debug_linux_amd64.go new file mode 100644 index 00000000..5bbba1ef --- /dev/null +++ b/internal/hv/kvm/debug_linux_amd64.go @@ -0,0 +1,21 @@ +//go:build linux && amd64 + +package kvm + +import "unsafe" + +// SetBreakpoint installs one debugger-owned execution breakpoint. A zero +// address disables it. It does not modify guest code or guest debug registers. +func (v *VM) SetBreakpoint(address uint64) error { + var debug struct { + Control, Padding uint32 + Registers [8]uint64 + } + if address != 0 { + debug.Control = 0x20001 // ENABLE | USE_HW_BP + debug.Registers[0] = address + debug.Registers[7] = 0x601 // fixed DR7 bits and local breakpoint 0 + } + _, err := ioctlWithRetry(uintptr(v.vcpus[0].fd), 0x4048ae9b, uintptr(unsafe.Pointer(&debug))) + return err +} From baea0b95eb812ee4fc89d318280a1437aeaa515f Mon Sep 17 00:00:00 2001 From: Joshua Scarsbrook Date: Sun, 13 Sep 2026 20:44:08 +1000 Subject: [PATCH 2/3] Remove redundant loop around a single x86 exit --- hypervisor/x86_linux_amd64.go | 48 +++++++++++++++++------------------ 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/hypervisor/x86_linux_amd64.go b/hypervisor/x86_linux_amd64.go index 1aaa4ef2..31bf86cc 100644 --- a/hypervisor/x86_linux_amd64.go +++ b/hypervisor/x86_linux_amd64.go @@ -107,31 +107,29 @@ func (n *nativeX86) Run(ctx context.Context) (X86Exit, error) { <-joined } }() - for { - if err := ctx.Err(); err != nil { - return X86Exit{}, err - } - var ex kvm.Exit - err := n.RunVCPUInterruptible(0, &ex) - if errors.Is(err, unix.EINTR) { - return X86Exit{}, ctx.Err() - } - if err != nil { - return X86Exit{}, err - } - ret := X86Exit{Reason: uint32(ex.Reason)} - if ex.Reason == kvm.ExitUnknown { - return X86Exit{}, fmt.Errorf("unknown KVM hardware exit") - } - if ex.Reason == kvm.ExitIO { - ret.Port, ret.Size, ret.Count, ret.Write, ret.Data = ex.IO.Port, ex.IO.Size, ex.IO.Count, ex.IO.Write, ex.IO.Data - } else if ex.Reason == kvm.ExitMMIO { - if ex.MMIO.Len == 0 || ex.MMIO.Len > 8 { - return X86Exit{}, fmt.Errorf("invalid KVM MMIO width %d", ex.MMIO.Len) - } - ret.Address, ret.Size, ret.Write = ex.MMIO.Addr, uint8(ex.MMIO.Len), ex.MMIO.Write - ret.Data = append([]byte(nil), ex.MMIO.Data[:ex.MMIO.Len]...) + if err := ctx.Err(); err != nil { + return X86Exit{}, err + } + var ex kvm.Exit + err := n.RunVCPUInterruptible(0, &ex) + if errors.Is(err, unix.EINTR) { + return X86Exit{}, ctx.Err() + } + if err != nil { + return X86Exit{}, err + } + ret := X86Exit{Reason: uint32(ex.Reason)} + if ex.Reason == kvm.ExitUnknown { + return X86Exit{}, fmt.Errorf("unknown KVM hardware exit") + } + if ex.Reason == kvm.ExitIO { + ret.Port, ret.Size, ret.Count, ret.Write, ret.Data = ex.IO.Port, ex.IO.Size, ex.IO.Count, ex.IO.Write, ex.IO.Data + } else if ex.Reason == kvm.ExitMMIO { + if ex.MMIO.Len == 0 || ex.MMIO.Len > 8 { + return X86Exit{}, fmt.Errorf("invalid KVM MMIO width %d", ex.MMIO.Len) } - return ret, nil + ret.Address, ret.Size, ret.Write = ex.MMIO.Addr, uint8(ex.MMIO.Len), ex.MMIO.Write + ret.Data = append([]byte(nil), ex.MMIO.Data[:ex.MMIO.Len]...) } + return ret, nil } From b42f4a01872d07e4df7eb8a8d9c08b41ea677643 Mon Sep 17 00:00:00 2001 From: Joshua Scarsbrook Date: Sun, 13 Sep 2026 20:46:46 +1000 Subject: [PATCH 3/3] Enable KVM for native x86 CI tests --- .github/workflows/test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e0a3842..f6c2ef05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -89,6 +89,13 @@ jobs: # an allocation advisory. Neither is a correctness failure. run: staticcheck -checks='SA*,-SA1019,-SA6002' ./... + - name: Enable KVM for native x86 tests + if: matrix.name == 'linux-amd64' + run: | + sudo chown "$(id -u):$(id -g)" /dev/kvm + sudo chmod 660 /dev/kvm + test -r /dev/kvm && test -w /dev/kvm + - name: Run tests run: go test -short ./... -count=1 -timeout 30m