From dd3ec21d8909271b53617b095be8be2501db6fae Mon Sep 17 00:00:00 2001 From: Maksim An Date: Tue, 30 Jun 2026 16:13:15 -0700 Subject: [PATCH] gcs-sidecar: stage amdsnppspapi.dll into CWCOW container security-context dir Confidential WCOW workloads need the AMD SNP PSP API DLL (amdsnppspapi.dll) to fetch SNP attestation reports, but it only exists in the UVM's System32 and cannot be bind-mounted as a single file. Copy the DLL from the UVM's System32 into each confidential container's existing security-context directory. Staging happens after security policy enforcement, consistent with the existing UVM_SECURITY_CONTEXT_DIR injection, and is a no-op when the DLL is absent (e.g. non-SNP UVMs). The workload locates the staged DLL via the UVM_SECURITY_CONTEXT_DIR environment variable. WriteSecurityContextDir now returns the created directory path so the sidecar can stage additional files into it; the Linux GCS call site is updated accordingly. Adds unit tests for stageDLL. Signed-off-by: Maksim An Assisted-by: Claude Opus 4.8 --- internal/gcs-sidecar/handlers.go | 63 +++++++++++++++++++- internal/gcs-sidecar/stage_dll_test.go | 61 +++++++++++++++++++ internal/guest/runtime/hcsv2/uvm.go | 2 +- pkg/securitypolicy/securitypolicy_options.go | 21 ++++--- 4 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 internal/gcs-sidecar/stage_dll_test.go diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index f5a7c48d5e..a40c2d72ea 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -4,6 +4,7 @@ package bridge import ( + "context" "encoding/hex" "encoding/json" "fmt" @@ -15,6 +16,7 @@ import ( "github.com/Microsoft/go-winio/pkg/guid" "github.com/Microsoft/hcsshim/hcn" "github.com/Microsoft/hcsshim/internal/bridgeutils/commonutils" + "github.com/Microsoft/hcsshim/internal/copyfile" "github.com/Microsoft/hcsshim/internal/fsformatter" "github.com/Microsoft/hcsshim/internal/gcs/prot" hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" @@ -29,6 +31,7 @@ import ( "github.com/Microsoft/hcsshim/pkg/cimfs" "github.com/Microsoft/hcsshim/pkg/securitypolicy" "github.com/pkg/errors" + "golang.org/x/sys/windows" ) const ( @@ -36,6 +39,10 @@ const ( hivesDirName = "Hives" devPathFormat = "\\\\.\\PHYSICALDRIVE%d" UVMContainerID = "00000000-0000-0000-0000-000000000000" + // amdSnpPspDLLName is the AMD SNP PSP API DLL used to fetch SNP attestation + // reports. It is staged from the UVM's System32 into each confidential + // container's security-context directory so workloads can load it. + amdSnpPspDLLName = "amdsnppspapi.dll" ) // - Handler functions handle the incoming message requests. It @@ -153,9 +160,20 @@ func (b *Bridge) createContainer(req *request) (err error) { }() if oci.ParseAnnotationsBool(ctx, spec.Annotations, annotations.WCOWSecurityPolicyEnv, true) { - if err := b.hostState.securityOptions.WriteSecurityContextDir(&spec); err != nil { + securityContextDir, err := b.hostState.securityOptions.WriteSecurityContextDir(&spec) + if err != nil { return fmt.Errorf("failed to write security context dir: %w", err) } + + // Stage the AMD SNP PSP API DLL into the container's security-context + // directory so the workload can fetch SNP attestation reports. This + // happens after security policy enforcement, consistent with the + // UVM_SECURITY_CONTEXT_DIR env injection done by WriteSecurityContextDir. + if securityContextDir != "" { + if err := stageSnpPspDLL(ctx, securityContextDir); err != nil { + return fmt.Errorf("failed to stage %s: %w", amdSnpPspDLLName, err) + } + } cwcowHostedSystemConfig.Spec = spec } @@ -201,6 +219,49 @@ func (b *Bridge) createContainer(req *request) (err error) { return nil } +// stageSnpPspDLL copies the AMD SNP PSP API DLL from the UVM's System32 into the +// container's security-context directory so the workload can fetch SNP +// attestation reports. The directory is exposed to the container via the +// UVM_SECURITY_CONTEXT_DIR environment variable. If the DLL is not present in +// the UVM (e.g. a non-SNP UVM), staging is skipped without error. +func stageSnpPspDLL(ctx context.Context, securityContextDir string) error { + sysDir, err := windows.GetSystemDirectory() + if err != nil { + return fmt.Errorf("failed to get system directory: %w", err) + } + + srcPath := filepath.Join(sysDir, amdSnpPspDLLName) + staged, err := stageDLL(ctx, srcPath, securityContextDir) + if err != nil { + return err + } + if staged { + log.G(ctx).Debugf("staged %s into %s", amdSnpPspDLLName, securityContextDir) + } else { + log.G(ctx).Debugf("%s not found in %s; skipping staging", amdSnpPspDLLName, sysDir) + } + return nil +} + +// stageDLL copies the DLL at srcPath into dstDir. If the source DLL does not +// exist it is a no-op and returns false without error, so callers can tolerate +// environments where the DLL is not present. +func stageDLL(ctx context.Context, srcPath, dstDir string) (bool, error) { + if _, err := os.Stat(srcPath); err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("failed to stat %s: %w", srcPath, err) + } + + dstPath := filepath.Join(dstDir, filepath.Base(srcPath)) + if err := copyfile.CopyFile(ctx, srcPath, dstPath, true); err != nil { + return false, fmt.Errorf("failed to copy %s to %s: %w", srcPath, dstPath, err) + } + + return true, nil +} + // processParamEnvToOCIEnv converts an Environment field from ProcessParameters // (a map from environment variable to value) into an array of environment // variable assignments (where each is in the form "=") which diff --git a/internal/gcs-sidecar/stage_dll_test.go b/internal/gcs-sidecar/stage_dll_test.go new file mode 100644 index 0000000000..5a381ce3fd --- /dev/null +++ b/internal/gcs-sidecar/stage_dll_test.go @@ -0,0 +1,61 @@ +//go:build windows +// +build windows + +package bridge + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" +) + +func TestStageDLL_Copies(t *testing.T) { + srcDir := t.TempDir() + dstDir := t.TempDir() + + contents := []byte("fake-dll-bytes") + srcPath := filepath.Join(srcDir, amdSnpPspDLLName) + if err := os.WriteFile(srcPath, contents, 0644); err != nil { + t.Fatalf("failed to write source dll: %v", err) + } + + staged, err := stageDLL(context.Background(), srcPath, dstDir) + if err != nil { + t.Fatalf("stageDLL returned error: %v", err) + } + if !staged { + t.Fatal("expected staged to be true") + } + + // The DLL should be copied into dstDir with identical contents. + dstPath := filepath.Join(dstDir, amdSnpPspDLLName) + got, err := os.ReadFile(dstPath) + if err != nil { + t.Fatalf("failed to read staged dll: %v", err) + } + if !bytes.Equal(got, contents) { + t.Errorf("staged dll contents = %q, want %q", got, contents) + } +} + +func TestStageDLL_MissingSourceIsNoOp(t *testing.T) { + dstDir := t.TempDir() + srcPath := filepath.Join(t.TempDir(), amdSnpPspDLLName) // does not exist + + staged, err := stageDLL(context.Background(), srcPath, dstDir) + if err != nil { + t.Fatalf("stageDLL returned error: %v", err) + } + if staged { + t.Fatal("expected staged to be false when source is missing") + } + + // No file should have been written to dstDir. + if entries, err := os.ReadDir(dstDir); err != nil { + t.Fatalf("failed to read dstDir: %v", err) + } else if len(entries) != 0 { + t.Errorf("expected dstDir to be empty, found %d entries", len(entries)) + } +} diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index 513fe00acf..4eb6c4c54d 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -722,7 +722,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM } if oci.ParseAnnotationsBool(ctx, settings.OCISpecification.Annotations, annotations.LCOWSecurityPolicyEnv, true) { - if err := h.securityOptions.WriteSecurityContextDir(settings.OCISpecification); err != nil { + if _, err := h.securityOptions.WriteSecurityContextDir(settings.OCISpecification); err != nil { return nil, fmt.Errorf("failed to write security context dir: %w", err) } } diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index cf993780cd..88537e8a03 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -191,39 +191,43 @@ func writeFileInDir(dir string, filename string, data []byte, perm os.FileMode) // containing the files is exposed via UVM_SECURITY_CONTEXT_DIR env var. // It may be an error to have a security policy but not expose it to the // container as in that case it can never be checked as correct by a verifier. -func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) error { +// +// On success it returns the path (in the UVM/guest namespace) of the created +// security context directory, or an empty string if no directory was created +// because there was nothing to write. +func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) (string, error) { encodedPolicy := s.PolicyEnforcer.EncodedSecurityPolicy() hostAMDCert := spec.Annotations[annotations.WCOWHostAMDCertificate] if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(s.UvmReferenceInfo) > 0 || len(s.UvmHashEnvelopeReferenceInfo) > 0 { // Use os.MkdirTemp to make sure that the directory is unique. securityContextDir, err := os.MkdirTemp(spec.Root.Path, SecurityContextDirTemplate) if err != nil { - return fmt.Errorf("failed to create security context directory: %w", err) + return "", fmt.Errorf("failed to create security context directory: %w", err) } // Make sure that files inside directory are readable if err := os.Chmod(securityContextDir, 0755); err != nil { - return fmt.Errorf("failed to chmod security context directory: %w", err) + return "", fmt.Errorf("failed to chmod security context directory: %w", err) } if len(encodedPolicy) > 0 { if err := writeFileInDir(securityContextDir, PolicyFilename, []byte(encodedPolicy), 0777); err != nil { - return fmt.Errorf("failed to write security policy: %w", err) + return "", fmt.Errorf("failed to write security policy: %w", err) } } if len(s.UvmReferenceInfo) > 0 { if err := writeFileInDir(securityContextDir, ReferenceInfoFilename, []byte(s.UvmReferenceInfo), 0777); err != nil { - return fmt.Errorf("failed to write UVM reference info: %w", err) + return "", fmt.Errorf("failed to write UVM reference info: %w", err) } } if len(s.UvmHashEnvelopeReferenceInfo) > 0 { if err := writeFileInDir(securityContextDir, HashEnvelopeReferenceInfoFilename, []byte(s.UvmHashEnvelopeReferenceInfo), 0777); err != nil { - return fmt.Errorf("failed to write UVM hash envelope reference info: %w", err) + return "", fmt.Errorf("failed to write UVM hash envelope reference info: %w", err) } } if len(hostAMDCert) > 0 { if err := writeFileInDir(securityContextDir, HostAMDCertFilename, []byte(hostAMDCert), 0777); err != nil { - return fmt.Errorf("failed to write host AMD certificate: %w", err) + return "", fmt.Errorf("failed to write host AMD certificate: %w", err) } } @@ -231,6 +235,7 @@ func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) error { secCtxEnv := fmt.Sprintf("UVM_SECURITY_CONTEXT_DIR=%s", containerCtxDir) spec.Process.Env = append(spec.Process.Env, secCtxEnv) + return securityContextDir, nil } - return nil + return "", nil }