Skip to content

Commit 074769b

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 and append that directory (as seen from inside the container) to the container PATH so it is discoverable via LoadLibrary. 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). 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 and appendToPathEnv. Signed-off-by: Maksim An <maksiman@microsoft.com> Assisted-by: Claude Opus 4.8
1 parent 9ec809b commit 074769b

4 files changed

Lines changed: 231 additions & 10 deletions

File tree

internal/gcs-sidecar/handlers.go

Lines changed: 86 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"
@@ -28,14 +29,20 @@ import (
2829
"github.com/Microsoft/hcsshim/pkg/annotations"
2930
"github.com/Microsoft/hcsshim/pkg/cimfs"
3031
"github.com/Microsoft/hcsshim/pkg/securitypolicy"
32+
"github.com/opencontainers/runtime-spec/specs-go"
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, &spec, 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,73 @@ 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 and adds that directory (as seen from
224+
// inside the container) to the container's PATH so the DLL is discoverable via
225+
// LoadLibrary by name. If the DLL is not present in the UVM (e.g. a non-SNP
226+
// UVM), staging is skipped without error.
227+
func stageSnpPspDLL(ctx context.Context, spec *specs.Spec, 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+
// The security-context directory is created under the container's root
235+
// volume, which is surfaced as C:\ inside the container.
236+
containerCtxDir := filepath.Join(`C:\`, filepath.Base(securityContextDir))
237+
238+
staged, err := stageDLL(spec, srcPath, securityContextDir, containerCtxDir)
239+
if err != nil {
240+
return err
241+
}
242+
if staged {
243+
log.G(ctx).Debugf("staged %s and added %s to container PATH", amdSnpPspDLLName, containerCtxDir)
244+
} else {
245+
log.G(ctx).Debugf("%s not found in %s; skipping staging", amdSnpPspDLLName, sysDir)
246+
}
247+
return nil
248+
}
249+
250+
// stageDLL copies the DLL at srcPath into dstDir and, when copied, appends
251+
// containerDir (the directory as seen from inside the container) to the
252+
// container's PATH. If the source DLL does not exist it is a no-op and returns
253+
// false without error, so callers can tolerate environments where the DLL is
254+
// not present.
255+
func stageDLL(spec *specs.Spec, srcPath, dstDir, containerDir string) (bool, error) {
256+
data, err := os.ReadFile(srcPath)
257+
if err != nil {
258+
if os.IsNotExist(err) {
259+
return false, nil
260+
}
261+
return false, fmt.Errorf("failed to read %s: %w", srcPath, err)
262+
}
263+
264+
dstPath := filepath.Join(dstDir, filepath.Base(srcPath))
265+
if err := os.WriteFile(dstPath, data, 0644); err != nil {
266+
return false, fmt.Errorf("failed to write %s: %w", dstPath, err)
267+
}
268+
269+
spec.Process.Env = appendToPathEnv(spec.Process.Env, containerDir)
270+
return true, nil
271+
}
272+
273+
// appendToPathEnv appends dir to the existing PATH entry in env, preserving the
274+
// original key casing. If no PATH entry exists, one is created.
275+
func appendToPathEnv(env []string, dir string) []string {
276+
for i, e := range env {
277+
if k, v, ok := strings.Cut(e, "="); ok && strings.EqualFold(k, "PATH") {
278+
if v == "" {
279+
env[i] = k + "=" + dir
280+
} else {
281+
env[i] = k + "=" + v + ";" + dir
282+
}
283+
return env
284+
}
285+
}
286+
return append(env, "PATH="+dir)
287+
}
288+
204289
// processParamEnvToOCIEnv converts an Environment field from ProcessParameters
205290
// (a map from environment variable to value) into an array of environment
206291
// variable assignments (where each is in the form "<variable>=<value>") which
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//go:build windows
2+
// +build windows
3+
4+
package bridge
5+
6+
import (
7+
"bytes"
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
12+
specs "github.com/opencontainers/runtime-spec/specs-go"
13+
)
14+
15+
func TestAppendToPathEnv(t *testing.T) {
16+
for _, tc := range []struct {
17+
name string
18+
env []string
19+
dir string
20+
want []string
21+
}{
22+
{
23+
name: "appends to existing PATH",
24+
env: []string{"FOO=bar", `PATH=C:\Windows;C:\Windows\System32`},
25+
dir: `C:\sec`,
26+
want: []string{"FOO=bar", `PATH=C:\Windows;C:\Windows\System32;C:\sec`},
27+
},
28+
{
29+
name: "creates PATH when absent",
30+
env: []string{"FOO=bar"},
31+
dir: `C:\sec`,
32+
want: []string{"FOO=bar", `PATH=C:\sec`},
33+
},
34+
{
35+
name: "handles empty PATH value",
36+
env: []string{"PATH="},
37+
dir: `C:\sec`,
38+
want: []string{`PATH=C:\sec`},
39+
},
40+
{
41+
name: "is case insensitive and preserves original key casing",
42+
env: []string{`Path=C:\Windows`},
43+
dir: `C:\sec`,
44+
want: []string{`Path=C:\Windows;C:\sec`},
45+
},
46+
{
47+
name: "creates PATH on empty env",
48+
env: nil,
49+
dir: `C:\sec`,
50+
want: []string{`PATH=C:\sec`},
51+
},
52+
} {
53+
t.Run(tc.name, func(t *testing.T) {
54+
got := appendToPathEnv(tc.env, tc.dir)
55+
if len(got) != len(tc.want) {
56+
t.Fatalf("length mismatch: got %v, want %v", got, tc.want)
57+
}
58+
for i := range got {
59+
if got[i] != tc.want[i] {
60+
t.Errorf("env[%d] = %q, want %q", i, got[i], tc.want[i])
61+
}
62+
}
63+
})
64+
}
65+
}
66+
67+
func TestStageDLL_CopiesAndUpdatesPath(t *testing.T) {
68+
srcDir := t.TempDir()
69+
dstDir := t.TempDir()
70+
71+
contents := []byte("fake-dll-bytes")
72+
srcPath := filepath.Join(srcDir, amdSnpPspDLLName)
73+
if err := os.WriteFile(srcPath, contents, 0644); err != nil {
74+
t.Fatalf("failed to write source dll: %v", err)
75+
}
76+
77+
spec := &specs.Spec{Process: &specs.Process{Env: []string{`PATH=C:\Windows`}}}
78+
containerDir := `C:\security-context-abc`
79+
80+
staged, err := stageDLL(spec, srcPath, dstDir, containerDir)
81+
if err != nil {
82+
t.Fatalf("stageDLL returned error: %v", err)
83+
}
84+
if !staged {
85+
t.Fatal("expected staged to be true")
86+
}
87+
88+
// The DLL should be copied into dstDir with identical contents.
89+
dstPath := filepath.Join(dstDir, amdSnpPspDLLName)
90+
got, err := os.ReadFile(dstPath)
91+
if err != nil {
92+
t.Fatalf("failed to read staged dll: %v", err)
93+
}
94+
if !bytes.Equal(got, contents) {
95+
t.Errorf("staged dll contents = %q, want %q", got, contents)
96+
}
97+
98+
// The container-visible directory should be appended to PATH.
99+
want := `PATH=C:\Windows;` + containerDir
100+
if spec.Process.Env[0] != want {
101+
t.Errorf("PATH = %q, want %q", spec.Process.Env[0], want)
102+
}
103+
}
104+
105+
func TestStageDLL_MissingSourceIsNoOp(t *testing.T) {
106+
dstDir := t.TempDir()
107+
srcPath := filepath.Join(t.TempDir(), amdSnpPspDLLName) // does not exist
108+
109+
originalEnv := []string{`PATH=C:\Windows`}
110+
spec := &specs.Spec{Process: &specs.Process{Env: append([]string(nil), originalEnv...)}}
111+
112+
staged, err := stageDLL(spec, srcPath, dstDir, `C:\security-context-abc`)
113+
if err != nil {
114+
t.Fatalf("stageDLL returned error: %v", err)
115+
}
116+
if staged {
117+
t.Fatal("expected staged to be false when source is missing")
118+
}
119+
120+
// No file should have been written to dstDir.
121+
if entries, err := os.ReadDir(dstDir); err != nil {
122+
t.Fatalf("failed to read dstDir: %v", err)
123+
} else if len(entries) != 0 {
124+
t.Errorf("expected dstDir to be empty, found %d entries", len(entries))
125+
}
126+
127+
// PATH should be unchanged.
128+
if len(spec.Process.Env) != len(originalEnv) || spec.Process.Env[0] != originalEnv[0] {
129+
t.Errorf("env = %v, want unchanged %v", spec.Process.Env, originalEnv)
130+
}
131+
}

internal/guest/runtime/hcsv2/uvm.go

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

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

pkg/securitypolicy/securitypolicy_options.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,46 +191,51 @@ func writeFileInDir(dir string, filename string, data []byte, perm os.FileMode)
191191
// containing the files is exposed via UVM_SECURITY_CONTEXT_DIR env var.
192192
// It may be an error to have a security policy but not expose it to the
193193
// container as in that case it can never be checked as correct by a verifier.
194-
func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) error {
194+
//
195+
// On success it returns the path (in the UVM/guest namespace) of the created
196+
// security context directory, or an empty string if no directory was created
197+
// because there was nothing to write.
198+
func (s *SecurityOptions) WriteSecurityContextDir(spec *specs.Spec) (string, error) {
195199
encodedPolicy := s.PolicyEnforcer.EncodedSecurityPolicy()
196200
hostAMDCert := spec.Annotations[annotations.WCOWHostAMDCertificate]
197201
if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(s.UvmReferenceInfo) > 0 || len(s.UvmHashEnvelopeReferenceInfo) > 0 {
198202
// Use os.MkdirTemp to make sure that the directory is unique.
199203
securityContextDir, err := os.MkdirTemp(spec.Root.Path, SecurityContextDirTemplate)
200204
if err != nil {
201-
return fmt.Errorf("failed to create security context directory: %w", err)
205+
return "", fmt.Errorf("failed to create security context directory: %w", err)
202206
}
203207
// Make sure that files inside directory are readable
204208
if err := os.Chmod(securityContextDir, 0755); err != nil {
205-
return fmt.Errorf("failed to chmod security context directory: %w", err)
209+
return "", fmt.Errorf("failed to chmod security context directory: %w", err)
206210
}
207211

208212
if len(encodedPolicy) > 0 {
209213
if err := writeFileInDir(securityContextDir, PolicyFilename, []byte(encodedPolicy), 0777); err != nil {
210-
return fmt.Errorf("failed to write security policy: %w", err)
214+
return "", fmt.Errorf("failed to write security policy: %w", err)
211215
}
212216
}
213217
if len(s.UvmReferenceInfo) > 0 {
214218
if err := writeFileInDir(securityContextDir, ReferenceInfoFilename, []byte(s.UvmReferenceInfo), 0777); err != nil {
215-
return fmt.Errorf("failed to write UVM reference info: %w", err)
219+
return "", fmt.Errorf("failed to write UVM reference info: %w", err)
216220
}
217221
}
218222
if len(s.UvmHashEnvelopeReferenceInfo) > 0 {
219223
if err := writeFileInDir(securityContextDir, HashEnvelopeReferenceInfoFilename, []byte(s.UvmHashEnvelopeReferenceInfo), 0777); err != nil {
220-
return fmt.Errorf("failed to write UVM hash envelope reference info: %w", err)
224+
return "", fmt.Errorf("failed to write UVM hash envelope reference info: %w", err)
221225
}
222226
}
223227

224228
if len(hostAMDCert) > 0 {
225229
if err := writeFileInDir(securityContextDir, HostAMDCertFilename, []byte(hostAMDCert), 0777); err != nil {
226-
return fmt.Errorf("failed to write host AMD certificate: %w", err)
230+
return "", fmt.Errorf("failed to write host AMD certificate: %w", err)
227231
}
228232
}
229233

230234
containerCtxDir := fmt.Sprintf("/%s", filepath.Base(securityContextDir))
231235
secCtxEnv := fmt.Sprintf("UVM_SECURITY_CONTEXT_DIR=%s", containerCtxDir)
232236
spec.Process.Env = append(spec.Process.Env, secCtxEnv)
233237

238+
return securityContextDir, nil
234239
}
235-
return nil
240+
return "", nil
236241
}

0 commit comments

Comments
 (0)