Skip to content

Commit 3336195

Browse files
committed
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 <maksiman@microsoft.com> Assisted-by: Claude Opus 4.8
1 parent 18fd5c5 commit 3336195

4 files changed

Lines changed: 137 additions & 10 deletions

File tree

internal/gcs-sidecar/handlers.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package bridge
55

66
import (
7+
"context"
78
"encoding/hex"
89
"encoding/json"
910
"fmt"
@@ -15,6 +16,7 @@ import (
1516
"github.com/Microsoft/go-winio/pkg/guid"
1617
"github.com/Microsoft/hcsshim/hcn"
1718
"github.com/Microsoft/hcsshim/internal/bridgeutils/commonutils"
19+
"github.com/Microsoft/hcsshim/internal/copyfile"
1820
"github.com/Microsoft/hcsshim/internal/fsformatter"
1921
"github.com/Microsoft/hcsshim/internal/gcs/prot"
2022
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
@@ -29,13 +31,18 @@ import (
2931
"github.com/Microsoft/hcsshim/pkg/cimfs"
3032
"github.com/Microsoft/hcsshim/pkg/securitypolicy"
3133
"github.com/pkg/errors"
34+
"golang.org/x/sys/windows"
3235
)
3336

3437
const (
3538
sandboxStateDirName = "WcSandboxState"
3639
hivesDirName = "Hives"
3740
devPathFormat = "\\\\.\\PHYSICALDRIVE%d"
3841
UVMContainerID = "00000000-0000-0000-0000-000000000000"
42+
// amdSnpPspDLLName is the AMD SNP PSP API DLL used to fetch SNP attestation
43+
// reports. It is staged from the UVM's System32 into each confidential
44+
// container's security-context directory so workloads can load it.
45+
amdSnpPspDLLName = "amdsnppspapi.dll"
3946
)
4047

4148
// - Handler functions handle the incoming message requests. It
@@ -153,9 +160,20 @@ func (b *Bridge) createContainer(req *request) (err error) {
153160
}()
154161

155162
if oci.ParseAnnotationsBool(ctx, spec.Annotations, annotations.WCOWSecurityPolicyEnv, true) {
156-
if err := b.hostState.securityOptions.WriteSecurityContextDir(&spec); err != nil {
163+
securityContextDir, err := b.hostState.securityOptions.WriteSecurityContextDir(&spec)
164+
if err != nil {
157165
return fmt.Errorf("failed to write security context dir: %w", err)
158166
}
167+
168+
// Stage the AMD SNP PSP API DLL into the container's security-context
169+
// directory so the workload can fetch SNP attestation reports. This
170+
// happens after security policy enforcement, consistent with the
171+
// UVM_SECURITY_CONTEXT_DIR env injection done by WriteSecurityContextDir.
172+
if securityContextDir != "" {
173+
if err := stageSnpPspDLL(ctx, securityContextDir); err != nil {
174+
return fmt.Errorf("failed to stage %s: %w", amdSnpPspDLLName, err)
175+
}
176+
}
159177
cwcowHostedSystemConfig.Spec = spec
160178
}
161179

@@ -201,6 +219,49 @@ func (b *Bridge) createContainer(req *request) (err error) {
201219
return nil
202220
}
203221

222+
// stageSnpPspDLL copies the AMD SNP PSP API DLL from the UVM's System32 into the
223+
// container's security-context directory so the workload can fetch SNP
224+
// attestation reports. The directory is exposed to the container via the
225+
// UVM_SECURITY_CONTEXT_DIR environment variable. If the DLL is not present in
226+
// the UVM (e.g. a non-SNP UVM), staging is skipped without error.
227+
func stageSnpPspDLL(ctx context.Context, securityContextDir string) error {
228+
sysDir, err := windows.GetSystemDirectory()
229+
if err != nil {
230+
return fmt.Errorf("failed to get system directory: %w", err)
231+
}
232+
233+
srcPath := filepath.Join(sysDir, amdSnpPspDLLName)
234+
staged, err := stageDLL(ctx, srcPath, securityContextDir)
235+
if err != nil {
236+
return err
237+
}
238+
if staged {
239+
log.G(ctx).Debugf("staged %s into %s", amdSnpPspDLLName, securityContextDir)
240+
} else {
241+
log.G(ctx).Debugf("%s not found in %s; skipping staging", amdSnpPspDLLName, sysDir)
242+
}
243+
return nil
244+
}
245+
246+
// stageDLL copies the DLL at srcPath into dstDir. If the source DLL does not
247+
// exist it is a no-op and returns false without error, so callers can tolerate
248+
// environments where the DLL is not present.
249+
func stageDLL(ctx context.Context, srcPath, dstDir string) (bool, error) {
250+
if _, err := os.Stat(srcPath); err != nil {
251+
if os.IsNotExist(err) {
252+
return false, nil
253+
}
254+
return false, fmt.Errorf("failed to stat %s: %w", srcPath, err)
255+
}
256+
257+
dstPath := filepath.Join(dstDir, filepath.Base(srcPath))
258+
if err := copyfile.CopyFile(ctx, srcPath, dstPath, true); err != nil {
259+
return false, fmt.Errorf("failed to copy %s to %s: %w", srcPath, dstPath, err)
260+
}
261+
262+
return true, nil
263+
}
264+
204265
// processParamEnvToOCIEnv converts an Environment field from ProcessParameters
205266
// (a map from environment variable to value) into an array of environment
206267
// variable assignments (where each is in the form "<variable>=<value>") which
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//go:build windows
2+
// +build windows
3+
4+
package bridge
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
)
13+
14+
func TestStageDLL_Copies(t *testing.T) {
15+
srcDir := t.TempDir()
16+
dstDir := t.TempDir()
17+
18+
contents := []byte("fake-dll-bytes")
19+
srcPath := filepath.Join(srcDir, amdSnpPspDLLName)
20+
if err := os.WriteFile(srcPath, contents, 0644); err != nil {
21+
t.Fatalf("failed to write source dll: %v", err)
22+
}
23+
24+
staged, err := stageDLL(context.Background(), srcPath, dstDir)
25+
if err != nil {
26+
t.Fatalf("stageDLL returned error: %v", err)
27+
}
28+
if !staged {
29+
t.Fatal("expected staged to be true")
30+
}
31+
32+
// The DLL should be copied into dstDir with identical contents.
33+
dstPath := filepath.Join(dstDir, amdSnpPspDLLName)
34+
got, err := os.ReadFile(dstPath)
35+
if err != nil {
36+
t.Fatalf("failed to read staged dll: %v", err)
37+
}
38+
if !bytes.Equal(got, contents) {
39+
t.Errorf("staged dll contents = %q, want %q", got, contents)
40+
}
41+
}
42+
43+
func TestStageDLL_MissingSourceIsNoOp(t *testing.T) {
44+
dstDir := t.TempDir()
45+
srcPath := filepath.Join(t.TempDir(), amdSnpPspDLLName) // does not exist
46+
47+
staged, err := stageDLL(context.Background(), srcPath, dstDir)
48+
if err != nil {
49+
t.Fatalf("stageDLL returned error: %v", err)
50+
}
51+
if staged {
52+
t.Fatal("expected staged to be false when source is missing")
53+
}
54+
55+
// No file should have been written to dstDir.
56+
if entries, err := os.ReadDir(dstDir); err != nil {
57+
t.Fatalf("failed to read dstDir: %v", err)
58+
} else if len(entries) != 0 {
59+
t.Errorf("expected dstDir to be empty, found %d entries", len(entries))
60+
}
61+
}

internal/guest/runtime/hcsv2/uvm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ func (h *Host) CreateContainer(ctx context.Context, id string, settings *prot.VM
739739
}
740740

741741
if oci.ParseAnnotationsBool(ctx, settings.OCISpecification.Annotations, annotations.LCOWSecurityPolicyEnv, true) {
742-
if err := h.securityOptions.WriteSecurityContextDir(settings.OCISpecification); err != nil {
742+
if _, err := h.securityOptions.WriteSecurityContextDir(settings.OCISpecification); err != nil {
743743
return nil, fmt.Errorf("failed to write security context dir: %w", err)
744744
}
745745
}

pkg/securitypolicy/securitypolicy_options.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,46 +332,51 @@ func writeFileInDir(dir string, filename string, data []byte, perm os.FileMode)
332332
// containing the files is exposed via UVM_SECURITY_CONTEXT_DIR env var.
333333
// It may be an error to have a security policy but not expose it to the
334334
// container as in that case it can never be checked as correct by a verifier.
335-
func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) error {
335+
//
336+
// On success it returns the path (in the UVM/guest namespace) of the created
337+
// security context directory, or an empty string if no directory was created
338+
// because there was nothing to write.
339+
func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) (string, error) {
336340
encodedPolicy := s.PolicyEnforcer.EncodedSecurityPolicy()
337341
hostAMDCert := spec.Annotations[annotations.WCOWHostAMDCertificate]
338342
if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(s.UvmReferenceInfo) > 0 || len(s.UvmHashEnvelopeReferenceInfo) > 0 {
339343
// Use os.MkdirTemp to make sure that the directory is unique.
340344
securityContextDir, err := os.MkdirTemp(spec.Root.Path, SecurityContextDirTemplate)
341345
if err != nil {
342-
return fmt.Errorf("failed to create security context directory: %w", err)
346+
return "", fmt.Errorf("failed to create security context directory: %w", err)
343347
}
344348
// Make sure that files inside directory are readable
345349
if err := os.Chmod(securityContextDir, 0755); err != nil {
346-
return fmt.Errorf("failed to chmod security context directory: %w", err)
350+
return "", fmt.Errorf("failed to chmod security context directory: %w", err)
347351
}
348352

349353
if len(encodedPolicy) > 0 {
350354
if err := writeFileInDir(securityContextDir, PolicyFilename, []byte(encodedPolicy), 0777); err != nil {
351-
return fmt.Errorf("failed to write security policy: %w", err)
355+
return "", fmt.Errorf("failed to write security policy: %w", err)
352356
}
353357
}
354358
if len(s.UvmReferenceInfo) > 0 {
355359
if err := writeFileInDir(securityContextDir, ReferenceInfoFilename, []byte(s.UvmReferenceInfo), 0777); err != nil {
356-
return fmt.Errorf("failed to write UVM reference info: %w", err)
360+
return "", fmt.Errorf("failed to write UVM reference info: %w", err)
357361
}
358362
}
359363
if len(s.UvmHashEnvelopeReferenceInfo) > 0 {
360364
if err := writeFileInDir(securityContextDir, HashEnvelopeReferenceInfoFilename, []byte(s.UvmHashEnvelopeReferenceInfo), 0777); err != nil {
361-
return fmt.Errorf("failed to write UVM hash envelope reference info: %w", err)
365+
return "", fmt.Errorf("failed to write UVM hash envelope reference info: %w", err)
362366
}
363367
}
364368

365369
if len(hostAMDCert) > 0 {
366370
if err := writeFileInDir(securityContextDir, HostAMDCertFilename, []byte(hostAMDCert), 0777); err != nil {
367-
return fmt.Errorf("failed to write host AMD certificate: %w", err)
371+
return "", fmt.Errorf("failed to write host AMD certificate: %w", err)
368372
}
369373
}
370374

371375
containerCtxDir := fmt.Sprintf("/%s", filepath.Base(securityContextDir))
372376
secCtxEnv := fmt.Sprintf("UVM_SECURITY_CONTEXT_DIR=%s", containerCtxDir)
373377
spec.Process.Env = append(spec.Process.Env, secCtxEnv)
374378

379+
return securityContextDir, nil
375380
}
376-
return nil
381+
return "", nil
377382
}

0 commit comments

Comments
 (0)