Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions internal/controller/device/vpci/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//go:build windows

// Package vpci provides a controller for managing virtual PCI (vPCI) device
// assignments on a Utility VM (UVM). It handles assigning and removing
// PCI devices from the UVM via HCS modify calls.
//
// # Lifecycle
//
// [Controller] tracks active device assignments by VMBus GUID (device identifier
// within UVM) in an internal map. Each assignment is reference-counted to
// support shared access by multiple callers.
//
// A device follows the state machine below.
//
// ┌─────────────────┐
// │ StateReserved │
// └────────┬────────┘
// │ AddToVM host ok
// ▼
// ┌─────────────────┐ AddToVM host fails ┌─────────────────┐
// │ StateAssigned │──────────────────────────►│ StateRemoved │
// └────────┬────────┘ └────────┬────────┘
// ┌───────────┤ │ RemoveFromVM
// │ │ waitGuest ok ▼
// │ ▼ (untracked)
// │ ┌─────────────────┐
// │ │ StateReady │◄── AddToVM (refCount++)
// │ └────────┬────────┘
// │ │ RemoveFromVM ok
// │ ▼
// │ (untracked)
// │
// │ ┌──────────────────────┐
// └──waitGuest fail───────────────►│ StateAssignedInvalid │◄── RemoveFromVM host fails
// └──────────┬───────────┘
// │ RemoveFromVM ok
// ▼
// (untracked)
//
// - [Controller.Reserve] generates a unique VMBus GUID for a device and
// records the reservation. If the same device is already reserved, the
// existing GUID is returned.
// - [Controller.AddToVM] assigns a previously reserved device to the VM
// using the VMBus GUID returned by Reserve. If the device is already
// ready for use in the VM, the reference count is incremented.
// - [Controller.RemoveFromVM] decrements the reference count for the device
// identified by VMBus GUID. When it reaches zero, the device is removed
// from the VM. It also handles cleanup for devices that were reserved
// but never assigned, and for devices in an invalid state.
//
// # Invalid Devices
//
// The device is marked invalid if the host-side assignment succeeds but the
// guest-side notification fails or if the host-side remove call fails.
// The device remains tracked as Invalid so that the caller can call
// [Controller.RemoveFromVM] to perform host-side cleanup.
//
// # Virtual Functions
//
// Each Virtual Function is assigned as an independent guest device with its own
// VMBus GUID. Multiple Virtual Functions on the same physical device are treated
// as separate devices in the guest.
//
// # Guest Requests
//
// On LCOW, assigning a vPCI device requires a guest-side notification so the
// GCS can wait for the required device paths to become available.
// WCOW does not require a guest request as part of device assignment.
package vpci
110 changes: 110 additions & 0 deletions internal/controller/device/vpci/mocks/mock_vpci.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 97 additions & 0 deletions internal/controller/device/vpci/state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//go:build windows

package vpci

// State represents the current state of a vPCI device assignment lifecycle.
//
// The normal progression is:
//
// StateReserved → StateAssigned → StateReady → StateRemoved+untracked
//
// [StateAssigned] is a transient state set within a single [Controller.AddToVM] call
// after the host-side HCS modify succeeds. [waitGuestDeviceReady] is then called; on
// success the device moves to [StateReady], on failure to [StateAssignedInvalid].
//
// A device transitions to [StateAssignedInvalid] when an operation partially succeeds
// and leaves the VM in an inconsistent state. This can happen in two ways:
// - [Controller.AddToVM]: host-side assignment succeeds but guest-side notification fails.
// - [Controller.RemoveFromVM]: the host-side remove call fails.
//
// A device in [StateAssignedInvalid] can only be cleaned up via [Controller.RemoveFromVM].
//
// Full state-transition table:
//
// Current State │ Trigger │ Next State
// ─────────────────────┼────────────────────────────────────────────────────┼──────────────────────
// StateReserved │ AddToVM host-side succeeds │ StateAssigned
// StateReserved │ AddToVM host-side fails │ StateRemoved
// StateReserved │ RemoveFromVM called │ (untracked)
// StateAssigned │ waitGuestDeviceReady succeeds │ StateReady
// StateAssigned │ waitGuestDeviceReady fails │ StateAssignedInvalid
// StateReady │ AddToVM called (refCount++) │ StateReady
// StateReady │ RemoveFromVM refCount drops to 0, succeeds │ (untracked)
// StateReady │ RemoveFromVM refCount drops to 0, host-side fails │ StateAssignedInvalid
// StateAssignedInvalid │ RemoveFromVM succeeds │ (untracked)
// StateAssignedInvalid │ RemoveFromVM host-side fails │ StateAssignedInvalid
// StateRemoved │ AddToVM called │ error (call RemoveFromVM)
// StateRemoved │ RemoveFromVM called │ (untracked)
type State int32

const (
// StateReserved indicates that a VMBus GUID has been generated and the
// device has been recorded in the Controller, but it has not yet been
// assigned to the VM via a host-side HCS modify call.
// This is the initial state set by [Controller.Reserve].
StateReserved State = iota

// StateAssigned is a transient state that indicates the host-side HCS modify
// has succeeded but [waitGuestDeviceReady] has not yet been called/completed
// within a single [Controller.AddToVM] invocation.
// External callers should never observe this state.
StateAssigned

// StateReady indicates the device has been fully assigned to the VM:
// the host-side HCS modify succeeded and the guest-side device is ready.
// The reference count may be greater than one when multiple callers share
// the same device.
StateReady

// StateAssignedInvalid indicates the device is in an inconsistent state due to a
// partial failure. This state is reached in two ways:
// - [Controller.AddToVM]: the host-side assignment succeeded but the
// guest-side notification failed; the host-side assignment still exists
// but the guest-side device is not in a usable state.
// - [Controller.RemoveFromVM]: the host-side remove call failed; the
// host-side assignment still exists but the reference count has been
// decremented to zero.
// In either case the device must be cleaned up by calling [Controller.RemoveFromVM].
StateAssignedInvalid

// StateRemoved indicates that no host-side VM assignment exists for this device.
// This state is reached in two ways:
// - [Controller.AddToVM]: the host-side add call failed. The device is still
// tracked in the Controller and the caller must call [Controller.RemoveFromVM]
// to clean up the reservation. No further [Controller.AddToVM] calls are allowed.
// - [Controller.untrack]: set as a safety marker immediately before the device
// is deleted from the tracking maps. In this case the state is never externally
// observable — the device is gone from the map by the time the lock is released.
StateRemoved
)

// String returns a human-readable string representation of the device State.
func (s State) String() string {
switch s {
case StateReserved:
return "Reserved"
case StateAssigned:
return "Assigned"
case StateReady:
return "Ready"
case StateAssignedInvalid:
return "AssignedInvalid"
case StateRemoved:
return "Removed"
default:
return "Unknown"
}
}
61 changes: 61 additions & 0 deletions internal/controller/device/vpci/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build windows

package vpci

import (
"context"

"github.com/Microsoft/go-winio/pkg/guid"

hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
)

// Device holds the configuration required to assign a vPCI device to the VM.
type Device struct {
// DeviceInstanceID is the host device instance path of the vPCI device.
DeviceInstanceID string

// VirtualFunctionIndex is the SR-IOV virtual function index to assign.
VirtualFunctionIndex uint16
}

// vmVPCI manages adding and removing vPCI devices for a Utility VM.
// Implemented by [vmmanager.UtilityVM].
type vmVPCI interface {
// AddDevice adds a vPCI device identified by `vmBusGUID` to the Utility VM with the provided settings.
AddDevice(ctx context.Context, vmbusGUID guid.GUID, settings hcsschema.VirtualPciDevice) error

// RemoveDevice removes the vPCI device identified by `vmBusGUID` from the Utility VM.
RemoveDevice(ctx context.Context, vmbusGUID guid.GUID) error
}

// linuxGuestVPCI exposes vPCI device operations in the LCOW guest.
// Implemented by [guestmanager.Guest].
type linuxGuestVPCI interface {
// AddVPCIDevice adds a vPCI device to the guest.
AddVPCIDevice(ctx context.Context, settings guestresource.LCOWMappedVPCIDevice) error
}

// ==============================================================================
// INTERNAL DATA STRUCTURES
// ==============================================================================

// deviceInfo records one vPCI device's assignment state and reference count.
type deviceInfo struct {
// device is the immutable host device identifier used to detect duplicate
// assignment requests.
device Device

// vmBusGUID identifies the vPCI device (backed by a VMBus channel)
// inside the UVM.
vmBusGUID guid.GUID

// state is the current lifecycle state of this device assignment.
// Access must be guarded by [Controller.mu].
state State

// refCount is the number of active callers sharing this device.
// Access must be guarded by [Controller.mu].
refCount uint32
}
34 changes: 32 additions & 2 deletions internal/controller/device/vpci/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package vpci

import (
"fmt"
"path/filepath"
"strconv"
)
Expand All @@ -17,6 +18,16 @@ const (
DeviceIDType = "vpci-instance-id"
)

const (
// vmBusChannelTypeGUIDFormatted is the well-known channel type GUID defined by
// VMBus for all assigned devices.
vmBusChannelTypeGUIDFormatted = "{44c4f61d-4444-4400-9d52-802e27ede19f}"

// assignedDeviceEnumerator is the VMBus enumerator prefix used in device
// instance IDs for assigned devices.
assignedDeviceEnumerator = "VMBUS"
)

// IsValidDeviceType returns true if the device type is valid i.e. supported by the runtime.
func IsValidDeviceType(deviceType string) bool {
return (deviceType == DeviceIDType) ||
Expand All @@ -30,9 +41,28 @@ func GetDeviceInfoFromPath(rawDevicePath string) (string, uint16) {
indexString := filepath.Base(rawDevicePath)
index, err := strconv.ParseUint(indexString, 10, 16)
if err == nil {
// we have a vf index
// We have a VF index.
return filepath.Dir(rawDevicePath), uint16(index)
}
// otherwise, just use default index and full device ID given
// Otherwise, just use default index and the full device ID as given.
return rawDevicePath, 0
}

// GetAssignedDeviceVMBUSInstanceID returns the instance ID of the VMBus channel
// device node created when a device is assigned to a UVM via vPCI.
//
// When a device is assigned to a UVM via vPCI support in HCS, a new VMBus channel device node is
// created in the UVM. The actual device that was assigned in is exposed as a child on this VMBus
// channel device node.
//
// A device node's instance ID is an identifier that distinguishes that device from other devices
// on the system. The GUID of a VMBus channel device node refers to that channel's unique
// identifier used internally by VMBus and can be used to determine the VMBus channel
// device node's instance ID.
//
// A VMBus channel device node's instance ID is in the form:
//
// "VMBUS\{channelTypeGUID}\{vmBusChannelGUID}"
func GetAssignedDeviceVMBUSInstanceID(vmBusChannelGUID string) string {
return fmt.Sprintf("%s\\%s\\{%s}", assignedDeviceEnumerator, vmBusChannelTypeGUIDFormatted, vmBusChannelGUID)
}
Loading
Loading