From f6291f832f4b23ec30b43f09da35d9cb00477f68 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Fri, 3 Jul 2026 21:00:33 +0000 Subject: [PATCH] gcs-sidecar, resource_wcow: Don't use UVM exec to create sandbox mount sources In confidential WCOW this will not work due to the policy not allowing exec, and we don't want to add an exception for this. Instead, we make the gcs-sidecar handle this job, and make the host skip this for confidential containers. To test, use a container json that has a mount: { "metadata": { "name": "wcow_emptydir_info_cwcow_primary" }, "image": { "image": "mcr.microsoft.com/windows/servercore@sha256:f5f92ece3c213cec075d4abbcf6263eb69c1b94daf17b777da239b909d2862a6" }, "command": [ "ping", "-t", "127.0.0.1" ], "mounts": [ { "container_path": "/volumemountscratch", "host_path": "sandbox:///tmp/atlas/emptydir/vol1", "propagation": 2 } ] } along with a policy that denies arbitrary execs: package policy import future.keywords.every import future.keywords.in api_version := "0.12.0" framework_version := "0.5.0" transparency_trust_lists := [ ] fragments := [ ] containers := [ { "allow_stdio_access": true, "command": [ "ping", "-t", "127.0.0.1" ], "env_rules": [ { "pattern": ".+", "required": false, "strategy": "re2" } ], "exec_processes": [ { "command": [ "cmd" ], "signals": [] } ], "id": "mcr.microsoft.com/windows/servercore@sha256:f5f92ece3c213cec075d4abbcf6263eb69c1b94daf17b777da239b909d2862a6", "layers": [ "1aba71bb6a4b4c6df1dee7c37607c8e1da4eb6b93de103b6509daf8efd663a7d", "8f05ecc2618a5410c5f5360d1bfe5f4b413dd18e3d7ba0bbc724d77e8d6e4628" ], "mounted_cim": [ "0b1bdf3036da10b119eb0f656d67f59a87e54bdaa932c3f9dbd1ace1a1738743" ], "mounts": [ { "destination": "/volumemountscratch", "options": [ "rbind", "rshared", "rw" ], "source": "sandbox:///tmp/atlas/emptydir/.+", "type": "bind" }, ], "name": "mcr.microsoft.com/windows/servercore@sha256:f5f92ece3c213cec075d4abbcf6263eb69c1b94daf17b777da239b909d2862a6", "signals": [], "user": "", "working_dir": "C:\\" } ] external_processes := [ { "command": [ "cmd" ], "env_rules": [ { "pattern": ".+", "required": false, "strategy": "re2" } ], "working_dir": "C:\\", "allow_stdio_access": true } ] allow_properties_access := true allow_dump_stacks := true allow_runtime_logging := true allow_environment_variable_dropping := true mount_device := data.framework.mount_device mount_overlay := data.framework.mount_overlay create_container := data.framework.create_container mount_cims := data.framework.mount_cims unmount_device := data.framework.unmount_device unmount_overlay := data.framework.unmount_overlay # exec_in_container := {"allowed": true, "env_list": null} exec_in_container := data.framework.exec_in_container # exec_external := {"allowed": true, "env_list": null, "allow_stdio_access": true} exec_external := data.framework.exec_external shutdown_container := data.framework.shutdown_container signal_container_process := data.framework.signal_container_process plan9_mount := data.framework.plan9_mount plan9_unmount := data.framework.plan9_unmount get_properties := data.framework.get_properties dump_stacks := data.framework.dump_stacks runtime_logging := data.framework.runtime_logging load_fragment := data.framework.load_fragment scratch_mount := data.framework.scratch_mount scratch_unmount := data.framework.scratch_unmount registry_changes := data.framework.registry_changes log_provider := data.framework.log_provider load_transparency_trust_list := data.framework.load_transparency_trust_list reason := {"errors": data.framework.errors, "candidate_containers": data.framework.candidate_containers} Reported-by: Adrian Cotolan @ManalahBT Assisted-by: GitHub-Copilot copilot-review Signed-off-by: Tingmao Wang --- internal/gcs-sidecar/handlers.go | 32 ++++++++++++++++++++++++++++ internal/hcsoci/resources_wcow.go | 35 ++++++++++++++++++------------- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index f5a7c48d5e..6406ef7029 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" @@ -130,6 +131,15 @@ func (b *Bridge) createContainer(req *request) (err error) { return fmt.Errorf("CreateContainer operation is denied by policy: %w", err) } + // Create the source directory for each mapped directory if it does not + // already exist. In non-confidential WCOW the host does this for + // sandbox:// mounts by exec'ing `cmd /c mkdir ... & dir ...` inside the + // UVM (see resources_wcow.go:setupMounts), but for confidential, we + // handle this here in the sidecar GCS. + if err := createMappedDirectorySourceDirs(ctx, container.MappedDirectories); err != nil { + return fmt.Errorf("failed to create mapped directory source directories: %w", err) + } + commandLine := len(spec.Process.Args) > 0 c := &Container{ id: containerID, @@ -201,6 +211,28 @@ func (b *Bridge) createContainer(req *request) (err error) { return nil } +// createMappedDirectorySourceDirs creates the host-side source directory for +// every mapped directory (i.e. "mounts") in the container document, if it does +// not already exist. +func createMappedDirectorySourceDirs(ctx context.Context, mappedDirectories []hcsschema.MappedDirectory) error { + for _, md := range mappedDirectories { + source := md.HostPath + + if _, err := os.Stat(source); err == nil { + // exists + continue + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to stat mapped directory source %q: %w", source, err) + } + + if err := os.MkdirAll(source, 0755); err != nil { + return fmt.Errorf("failed to create mapped directory source %q: %w", source, err) + } + log.G(ctx).WithField("source", source).Debug("created mapped directory source directory") + } + return 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/hcsoci/resources_wcow.go b/internal/hcsoci/resources_wcow.go index baa5d7fb7d..31b35f4246 100644 --- a/internal/hcsoci/resources_wcow.go +++ b/internal/hcsoci/resources_wcow.go @@ -206,20 +206,27 @@ func setupMounts(ctx context.Context, coi *createOptionsInternal, r *resources.R // Now we need to exec a process in the vm that will make these directories as theres // no functionality in the Windows gcs to create an arbitrary directory. // - // Create the directory, but also run dir afterwards regardless of if mkdir succeeded to handle the case where the directory already exists - // e.g. from a previous container specifying the same mount (and thus creating the same directory). - b := &bytes.Buffer{} - stderr, err := cmd.CreatePipeAndListen(b, false) - if err != nil { - return err - } - req := &cmd.CmdProcessRequest{ - Args: []string{"cmd", "/c", "mkdir", sandboxPath, "&", "dir", sandboxPath}, - Stderr: stderr, - } - exitCode, err := coi.HostingSystem.ExecInUVM(ctx, req) - if err != nil { - return errors.Wrapf(err, "failed to create sandbox mount directory in utility VM with exit code %d %q", exitCode, b.String()) + // We do not need to do this for Confidential WCOW, because in that case the gcs-sidecar + // handles the create container request and will do this for us. This way the policy + // does not have to have exceptions for allowing such mkdir commands. + // + // Create the directory, but also run dir afterwards regardless of if mkdir succeeded to + // handle the case where the directory already exists e.g. from a previous container + // specifying the same mount (and thus creating the same directory). + if !coi.HostingSystem.HasConfidentialPolicy() { + b := &bytes.Buffer{} + stderr, err := cmd.CreatePipeAndListen(b, false) + if err != nil { + return err + } + req := &cmd.CmdProcessRequest{ + Args: []string{"cmd", "/c", "mkdir", sandboxPath, "&", "dir", sandboxPath}, + Stderr: stderr, + } + exitCode, err := coi.HostingSystem.ExecInUVM(ctx, req) + if err != nil { + return errors.Wrapf(err, "failed to create sandbox mount directory in utility VM with exit code %d %q", exitCode, b.String()) + } } } else if np, ok := uvm.ParseNamedPipe(coi.HostingSystem, mount); ok { if !np.UVMPipe {