diff --git a/cmd/containerd-shim-lcow-v2/service/mocks/mock_service.go b/cmd/containerd-shim-lcow-v2/service/mocks/mock_service.go index 597f391143..af7d4a6a6a 100644 --- a/cmd/containerd-shim-lcow-v2/service/mocks/mock_service.go +++ b/cmd/containerd-shim-lcow-v2/service/mocks/mock_service.go @@ -13,6 +13,7 @@ package mocks import ( context "context" + json "encoding/json" reflect "reflect" time "time" @@ -248,6 +249,21 @@ func (mr *MockvmControllerMockRecorder) Plan9Controller() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Plan9Controller", reflect.TypeOf((*MockvmController)(nil).Plan9Controller)) } +// ProcessorRequirements mocks base method. +func (m *MockvmController) ProcessorRequirements(ctx context.Context) (json.RawMessage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProcessorRequirements", ctx) + ret0, _ := ret[0].(json.RawMessage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProcessorRequirements indicates an expected call of ProcessorRequirements. +func (mr *MockvmControllerMockRecorder) ProcessorRequirements(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessorRequirements", reflect.TypeOf((*MockvmController)(nil).ProcessorRequirements), ctx) +} + // Resume mocks base method. func (m *MockvmController) Resume(ctx context.Context, rebuildBridge bool) error { m.ctrl.T.Helper() diff --git a/cmd/containerd-shim-lcow-v2/service/service_sandbox_internal.go b/cmd/containerd-shim-lcow-v2/service/service_sandbox_internal.go index 41cd60d6b1..774a59bc91 100644 --- a/cmd/containerd-shim-lcow-v2/service/service_sandbox_internal.go +++ b/cmd/containerd-shim-lcow-v2/service/service_sandbox_internal.go @@ -33,6 +33,10 @@ const ( SandboxStateReady = "SANDBOX_READY" // SandboxStateNotReady indicates the sandbox is not ready. SandboxStateNotReady = "SANDBOX_NOTREADY" + + // vmProcessorRequirementsInfoKey is the verbose SandboxStatus info key under + // which the UVM's processor feature/compatibility set (raw HCS JSON) is reported. + vmProcessorRequirementsInfoKey = "vmProcessorRequirements" ) // createSandboxInternal is the implementation for CreateSandbox. @@ -194,7 +198,7 @@ func (s *Service) waitSandboxInternal(ctx context.Context, request *sandbox.Wait // It synthesizes a status response from the current vmController state. // When verbose is true, the response may be extended with additional // diagnostic information. -func (s *Service) sandboxStatusInternal(_ context.Context, request *sandbox.SandboxStatusRequest) (*sandbox.SandboxStatusResponse, error) { +func (s *Service) sandboxStatusInternal(ctx context.Context, request *sandbox.SandboxStatusRequest) (*sandbox.SandboxStatusResponse, error) { if s.sandboxID != request.SandboxID { return nil, fmt.Errorf("sandbox ID mismatch, expected %s, got %s", s.sandboxID, request.SandboxID) } @@ -223,8 +227,21 @@ func (s *Service) sandboxStatusInternal(_ context.Context, request *sandbox.Sand resp.ExitedAt = timestamppb.New(stoppedStatus.StoppedTime) } - if request.Verbose { //nolint:staticcheck - // TODO: Add compat info and any other details. + if request.Verbose { + // Surface the UVM's processor feature/compatibility set for diagnostics. + // Best-effort: a status query must not fail because this optional property + // could not be read. + procReqs, err := s.vmController.ProcessorRequirements(ctx) + if err != nil { + log.G(ctx).WithError(err).Debug("failed to query VM processor requirements for verbose sandbox status") + } + + if len(procReqs) > 0 { + if resp.Info == nil { + resp.Info = make(map[string]string) + } + resp.Info[vmProcessorRequirementsInfoKey] = string(procReqs) + } } return resp, nil diff --git a/cmd/containerd-shim-lcow-v2/service/service_sandbox_internal_test.go b/cmd/containerd-shim-lcow-v2/service/service_sandbox_internal_test.go index 29aecbf06b..13f6628817 100644 --- a/cmd/containerd-shim-lcow-v2/service/service_sandbox_internal_test.go +++ b/cmd/containerd-shim-lcow-v2/service/service_sandbox_internal_test.go @@ -4,6 +4,7 @@ package service import ( "context" + "encoding/json" "errors" "os" "path/filepath" @@ -35,6 +36,7 @@ var ( errVMWait = errors.New("vm wait failed") errVMExitStat = errors.New("vm exit status unavailable") errVMStats = errors.New("vm stats unavailable") + errVMProcReqs = errors.New("vm processor requirements unavailable") ) // newTestService builds a [Service] wired to a mock vm controller. @@ -599,6 +601,59 @@ func TestSandboxStatus_TerminatedExitStatusFailure(t *testing.T) { } } +// TestSandboxStatus_VerboseIncludesProcessorRequirements verifies that a verbose +// status request on a running VM surfaces the processor requirements JSON under +// the vmProcessorRequirementsInfoKey info key. +func TestSandboxStatus_VerboseIncludesProcessorRequirements(t *testing.T) { + t.Parallel() + startedAt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + procReqs := json.RawMessage(`{"ProcessorFeatures":["Sse3"],"CacheLineFlushSize":8}`) + + svc, mockCtrl := newTestService(t) + svc.sandboxID = "test-sandbox" + + mockCtrl.EXPECT().State().Return(vm.StateRunning) + mockCtrl.EXPECT().StartTime().Return(startedAt) + mockCtrl.EXPECT().ProcessorRequirements(gomock.Any()).Return(procReqs, nil) + + resp, err := svc.sandboxStatusInternal(context.Background(), &sandboxsvc.SandboxStatusRequest{ + SandboxID: "test-sandbox", + Verbose: true, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := resp.Info[vmProcessorRequirementsInfoKey]; got != string(procReqs) { + t.Errorf("Info[%q] = %q, want %q", vmProcessorRequirementsInfoKey, got, string(procReqs)) + } +} + +// TestSandboxStatus_VerboseProcessorRequirementsBestEffort verifies that a +// failure to query processor requirements does not fail the status call; the +// info key is simply omitted. +func TestSandboxStatus_VerboseProcessorRequirementsBestEffort(t *testing.T) { + t.Parallel() + startedAt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + + svc, mockCtrl := newTestService(t) + svc.sandboxID = "test-sandbox" + + mockCtrl.EXPECT().State().Return(vm.StateRunning) + mockCtrl.EXPECT().StartTime().Return(startedAt) + mockCtrl.EXPECT().ProcessorRequirements(gomock.Any()).Return(nil, errVMProcReqs) + + resp, err := svc.sandboxStatusInternal(context.Background(), &sandboxsvc.SandboxStatusRequest{ + SandboxID: "test-sandbox", + Verbose: true, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := resp.Info[vmProcessorRequirementsInfoKey]; ok { + t.Errorf("expected %q to be absent from Info on query failure", vmProcessorRequirementsInfoKey) + } +} + // ─── pingSandboxInternal tests ──────────────────────────────────────────── // TestPingSandbox_NotImplemented verifies that pingSandboxInternal returns diff --git a/cmd/containerd-shim-lcow-v2/service/types.go b/cmd/containerd-shim-lcow-v2/service/types.go index 16bad2bafe..07e776e7b4 100644 --- a/cmd/containerd-shim-lcow-v2/service/types.go +++ b/cmd/containerd-shim-lcow-v2/service/types.go @@ -4,6 +4,7 @@ package service import ( "context" + "encoding/json" "time" "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats" @@ -58,6 +59,10 @@ type vmController interface { // Stats returns runtime statistics for the VM. Stats(ctx context.Context) (*stats.VirtualMachineStatistics, error) + // ProcessorRequirements returns the UVM's processor feature/compatibility + // set as raw JSON. + ProcessorRequirements(ctx context.Context) (json.RawMessage, error) + // UpdatePolicyFragment injects a security policy fragment into the running guest. UpdatePolicyFragment(ctx context.Context, fragment guestresource.SecurityPolicyFragment) error diff --git a/internal/controller/vm/vm.go b/internal/controller/vm/vm.go index 09a9d4d04d..de8c2e751d 100644 --- a/internal/controller/vm/vm.go +++ b/internal/controller/vm/vm.go @@ -581,6 +581,32 @@ func (c *Controller) Stats(ctx context.Context) (*stats.VirtualMachineStatistics return s, nil } +// ProcessorRequirements returns the UVM's processor feature and compatibility +// set (processor features, xsave features, synthetic features, cache line flush +// size, etc.) as the raw JSON emitted by HCS. The VM must be in [StateRunning]. +func (c *Controller) ProcessorRequirements(ctx context.Context) (json.RawMessage, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + if c.vmState != StateRunning { + return nil, fmt.Errorf("cannot query processor requirements: VM is in state %s", c.vmState) + } + + props, err := c.uvm.PropertiesV3(ctx, &hcsschema.PropertyQuery{ + Queries: map[string]interface{}{hcsschema.VMProcessorRequirementsProperty: nil}, + }) + if err != nil { + return nil, fmt.Errorf("query processor requirements: %w", err) + } + + resp, ok := props.PropertyResponses[hcsschema.VMProcessorRequirementsProperty] + if !ok || len(resp.Response) == 0 { + return nil, fmt.Errorf("processor requirements not present in property response") + } + + return resp.Response, nil +} + // TerminateVM forcefully terminates a running VM, closes the guest connection, // and releases HCS resources. // diff --git a/internal/controller/vm/vm_migration.go b/internal/controller/vm/vm_migration.go index f4a3a86060..f727411153 100644 --- a/internal/controller/vm/vm_migration.go +++ b/internal/controller/vm/vm_migration.go @@ -17,10 +17,6 @@ import ( "github.com/Microsoft/hcsshim/internal/vm/vmutils" ) -// compatibilityInfoProperty is the HCS property name used to retrieve the -// VM's opaque migration-compatibility blob via PropertiesV3. -const compatibilityInfoProperty = "CompatibilityInfo" - // InitializeLiveMigrationOnSource prepares the running source VM for an // outgoing live migration. Once it succeeds the VM accepts only live-migration // calls until the migration completes or is rolled back. @@ -76,14 +72,14 @@ func (c *Controller) CompatibilityInfo(ctx context.Context) ([]byte, error) { // Ask the HCS for the compatibility property. props, err := c.uvm.PropertiesV3(ctx, &hcsschema.PropertyQuery{ - Queries: map[string]interface{}{compatibilityInfoProperty: nil}, + Queries: map[string]interface{}{hcsschema.CompatibilityInfoProperty: nil}, }) if err != nil { return nil, fmt.Errorf("query compatibility info: %w", err) } // Pull the raw blob out of the property response. - resp, ok := props.PropertyResponses[compatibilityInfoProperty] + resp, ok := props.PropertyResponses[hcsschema.CompatibilityInfoProperty] if !ok || len(resp.Response) == 0 { return nil, fmt.Errorf("compatibility info not present in property response") } diff --git a/internal/hcs/schema2/property_type.go b/internal/hcs/schema2/property_type.go index 934f777fcf..be63d60a17 100644 --- a/internal/hcs/schema2/property_type.go +++ b/internal/hcs/schema2/property_type.go @@ -25,3 +25,13 @@ const ( PTCPUGroup PropertyType = "CpuGroup" PTSystemGUID PropertyType = "SystemGUID" ) + +const ( + // CompatibilityInfoProperty is the HCS property name used to retrieve the + // VM's opaque migration-compatibility blob via PropertiesV3. + CompatibilityInfoProperty = "CompatibilityInfo" + + // VMProcessorRequirementsProperty is the HCS property name used to retrieve the + // VM's processor feature/compatibility. + VMProcessorRequirementsProperty = "VmProcessorRequirements" +)