-
Notifications
You must be signed in to change notification settings - Fork 293
gcs-sidecar: stage amdsnppspapi.dll into CWCOW container security-context dir #2802
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
|
@@ -28,14 +30,20 @@ import ( | |
| "github.com/Microsoft/hcsshim/pkg/annotations" | ||
| "github.com/Microsoft/hcsshim/pkg/cimfs" | ||
| "github.com/Microsoft/hcsshim/pkg/securitypolicy" | ||
| "github.com/opencontainers/runtime-spec/specs-go" | ||
| "github.com/pkg/errors" | ||
| "golang.org/x/sys/windows" | ||
| ) | ||
|
|
||
| const ( | ||
| sandboxStateDirName = "WcSandboxState" | ||
| 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 +161,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, &spec, securityContextDir); err != nil { | ||
| return fmt.Errorf("failed to stage %s: %w", amdSnpPspDLLName, err) | ||
| } | ||
| } | ||
| cwcowHostedSystemConfig.Spec = spec | ||
| } | ||
|
|
||
|
|
@@ -201,6 +220,76 @@ 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 and adds that directory (as seen from | ||
| // inside the container) to the container's PATH so the DLL is discoverable via | ||
| // LoadLibrary by name. 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, spec *specs.Spec, 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) | ||
| // The security-context directory is created under the container's root | ||
| // volume, which is surfaced as C:\ inside the container. | ||
| containerCtxDir := filepath.Join(`C:\`, filepath.Base(securityContextDir)) | ||
|
|
||
| staged, err := stageDLL(ctx, spec, srcPath, securityContextDir, containerCtxDir) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if staged { | ||
| log.G(ctx).Debugf("staged %s and added %s to container PATH", amdSnpPspDLLName, containerCtxDir) | ||
| } else { | ||
| log.G(ctx).Debugf("%s not found in %s; skipping staging", amdSnpPspDLLName, sysDir) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // stageDLL copies the DLL at srcPath into dstDir and, when copied, appends | ||
| // containerDir (the directory as seen from inside the container) to the | ||
| // container's PATH. 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, spec *specs.Spec, srcPath, dstDir, containerDir string) (bool, error) { | ||
| if spec == nil || spec.Process == nil { | ||
| return false, fmt.Errorf("cannot stage %s: spec.Process is nil", filepath.Base(srcPath)) | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
|
|
||
| spec.Process.Env = appendToPathEnv(spec.Process.Env, containerDir) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since any client of the driver will need to be picking up the certs, uvm reference info etc it will need to be explicitly using the security context directory so I think it can also construct the path to the dll. We can add that into the go code in the SKR and the cpp PspUtils tool as examples. Thus I am not certain we should do this path manipulation here. Open to debate I guess.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not strictly necessary, really. we could just copy the dll to rootfs and let the container handle setting PATH when loading it. the change is more of a convenience, so lmk what you think. |
||
| return true, nil | ||
| } | ||
|
|
||
| // appendToPathEnv appends dir to the existing PATH entry in env, preserving the | ||
| // original key casing. If no PATH entry exists, one is created. | ||
| func appendToPathEnv(env []string, dir string) []string { | ||
| for i, e := range env { | ||
| if k, v, ok := strings.Cut(e, "="); ok && strings.EqualFold(k, "PATH") { | ||
| if v == "" { | ||
| env[i] = k + "=" + dir | ||
| } else { | ||
| env[i] = k + "=" + v + ";" + dir | ||
| } | ||
| return env | ||
| } | ||
| } | ||
| return append(env, "PATH="+dir) | ||
| } | ||
|
|
||
| // 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 "<variable>=<value>") which | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| //go:build windows | ||
| // +build windows | ||
|
|
||
| package bridge | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| specs "github.com/opencontainers/runtime-spec/specs-go" | ||
| ) | ||
|
|
||
| func TestAppendToPathEnv(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| name string | ||
| env []string | ||
| dir string | ||
| want []string | ||
| }{ | ||
| { | ||
| name: "appends to existing PATH", | ||
| env: []string{"FOO=bar", `PATH=C:\Windows;C:\Windows\System32`}, | ||
| dir: `C:\sec`, | ||
| want: []string{"FOO=bar", `PATH=C:\Windows;C:\Windows\System32;C:\sec`}, | ||
| }, | ||
| { | ||
| name: "creates PATH when absent", | ||
| env: []string{"FOO=bar"}, | ||
| dir: `C:\sec`, | ||
| want: []string{"FOO=bar", `PATH=C:\sec`}, | ||
| }, | ||
| { | ||
| name: "handles empty PATH value", | ||
| env: []string{"PATH="}, | ||
| dir: `C:\sec`, | ||
| want: []string{`PATH=C:\sec`}, | ||
| }, | ||
| { | ||
| name: "is case insensitive and preserves original key casing", | ||
| env: []string{`Path=C:\Windows`}, | ||
| dir: `C:\sec`, | ||
| want: []string{`Path=C:\Windows;C:\sec`}, | ||
| }, | ||
| { | ||
| name: "creates PATH on empty env", | ||
| env: nil, | ||
| dir: `C:\sec`, | ||
| want: []string{`PATH=C:\sec`}, | ||
| }, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| got := appendToPathEnv(tc.env, tc.dir) | ||
| if len(got) != len(tc.want) { | ||
| t.Fatalf("length mismatch: got %v, want %v", got, tc.want) | ||
| } | ||
| for i := range got { | ||
| if got[i] != tc.want[i] { | ||
| t.Errorf("env[%d] = %q, want %q", i, got[i], tc.want[i]) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestStageDLL_CopiesAndUpdatesPath(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) | ||
| } | ||
|
|
||
| spec := &specs.Spec{Process: &specs.Process{Env: []string{`PATH=C:\Windows`}}} | ||
| containerDir := `C:\security-context-abc` | ||
|
|
||
| staged, err := stageDLL(context.Background(), spec, srcPath, dstDir, containerDir) | ||
| 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) | ||
| } | ||
|
|
||
| // The container-visible directory should be appended to PATH. | ||
| want := `PATH=C:\Windows;` + containerDir | ||
| if spec.Process.Env[0] != want { | ||
| t.Errorf("PATH = %q, want %q", spec.Process.Env[0], want) | ||
| } | ||
| } | ||
|
|
||
| func TestStageDLL_MissingSourceIsNoOp(t *testing.T) { | ||
| dstDir := t.TempDir() | ||
| srcPath := filepath.Join(t.TempDir(), amdSnpPspDLLName) // does not exist | ||
|
|
||
| originalEnv := []string{`PATH=C:\Windows`} | ||
| spec := &specs.Spec{Process: &specs.Process{Env: append([]string(nil), originalEnv...)}} | ||
|
|
||
| staged, err := stageDLL(context.Background(), spec, srcPath, dstDir, `C:\security-context-abc`) | ||
| 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)) | ||
| } | ||
|
|
||
| // PATH should be unchanged. | ||
| if len(spec.Process.Env) != len(originalEnv) || spec.Process.Env[0] != originalEnv[0] { | ||
| t.Errorf("env = %v, want unchanged %v", spec.Process.Env, originalEnv) | ||
| } | ||
| } | ||
|
|
||
| func TestStageDLL_NilSpecOrProcess(t *testing.T) { | ||
| srcDir := t.TempDir() | ||
| srcPath := filepath.Join(srcDir, amdSnpPspDLLName) | ||
| if err := os.WriteFile(srcPath, []byte("fake-dll-bytes"), 0644); err != nil { | ||
| t.Fatalf("failed to write source dll: %v", err) | ||
| } | ||
|
|
||
| for _, tc := range []struct { | ||
| name string | ||
| spec *specs.Spec | ||
| }{ | ||
| {name: "nil spec", spec: nil}, | ||
| {name: "nil process", spec: &specs.Spec{}}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| staged, err := stageDLL(context.Background(), tc.spec, srcPath, t.TempDir(), `C:\security-context-abc`) | ||
| if err == nil { | ||
| t.Fatal("expected an error, got nil") | ||
| } | ||
| if staged { | ||
| t.Error("expected staged to be false") | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Untested, but I'm not sure if this will work, we might need to add the environment variables in executeProcess's
processParams.Environment(but maybe @takuro-sato is making thatprocessParams.Environmentcome from a stored OCI spec instead?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding this env should happen in executeProcess at least for now as env vars are not passed to inbox inbox in createContainer.
After I finish my change it can be in between createContainer enforcement and
b.hostState.AddContainer(req.ctx, containerID, c);(, where the enforcement result is stored) too.Env var enforcement will be like: