Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 90 additions & 1 deletion internal/gcs-sidecar/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package bridge

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
Comment on lines +172 to +177

Copy link
Copy Markdown
Member

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 that processParams.Environment come from a stored OCI spec instead?)

Copy link
Copy Markdown
Contributor

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:

  • createContaiener enforces and drops env vars and save the result in memory (resulting env vars are not passed to inbox gcs)
  • executeProcess gets a request including env vars and check they match the env vars in memory (currently this part is missing)

cwcowHostedSystemConfig.Spec = spec
}

Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down
158 changes: 158 additions & 0 deletions internal/gcs-sidecar/stage_dll_test.go
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")
}
})
}
}
2 changes: 1 addition & 1 deletion internal/guest/runtime/hcsv2/uvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
21 changes: 13 additions & 8 deletions pkg/securitypolicy/securitypolicy_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,46 +191,51 @@ 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()
Comment thread
anmaxvl marked this conversation as resolved.
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)
}
}

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

return securityContextDir, nil
}
return nil
return "", nil
}
Loading