From 85cd781ca2a728cf404ba4b1d0640345d44c44ae Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Mon, 14 Aug 2023 13:58:58 +0300 Subject: [PATCH] Implement executor on Windows This change splits the containerdexecutor.Run() function into smaller pieces and enables it to run on Windows. Signed-off-by: Gabriel Adrian Samfira --- executor/containerdexecutor/executor.go | 149 +++++----------- executor/containerdexecutor/executor_unix.go | 167 ++++++++++++++++++ .../containerdexecutor/executor_windows.go | 99 +++++++++++ executor/oci/spec.go | 25 ++- executor/oci/spec_freebsd.go | 4 + executor/oci/spec_linux.go | 4 + executor/oci/spec_windows.go | 35 +++- util/windows/util_windows.go | 156 ++++++++++++++++ 8 files changed, 527 insertions(+), 112 deletions(-) create mode 100644 executor/containerdexecutor/executor_unix.go create mode 100644 executor/containerdexecutor/executor_windows.go create mode 100644 util/windows/util_windows.go diff --git a/executor/containerdexecutor/executor.go b/executor/containerdexecutor/executor.go index 8266fda5f4f52..b5e797e3b1b82 100644 --- a/executor/containerdexecutor/executor.go +++ b/executor/containerdexecutor/executor.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "sync" "syscall" "time" @@ -17,19 +18,13 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/cio" "github.com/containerd/containerd/mount" - containerdoci "github.com/containerd/containerd/oci" - "github.com/containerd/continuity/fs" - "github.com/docker/docker/pkg/idtools" "github.com/moby/buildkit/executor" "github.com/moby/buildkit/executor/oci" resourcestypes "github.com/moby/buildkit/executor/resources/types" gatewayapi "github.com/moby/buildkit/frontend/gateway/pb" "github.com/moby/buildkit/identity" - "github.com/moby/buildkit/snapshot" "github.com/moby/buildkit/solver/pb" "github.com/moby/buildkit/util/network" - rootlessspecconv "github.com/moby/buildkit/util/rootless/specconv" - "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) @@ -39,7 +34,7 @@ type containerdExecutor struct { networkProviders map[pb.NetMode]network.Provider cgroupParent string dnsConfig *oci.DNSConfig - running map[string]chan error + running map[string]*jobDetails mu sync.Mutex apparmorProfile string selinux bool @@ -72,7 +67,7 @@ func New(client *containerd.Client, root, cgroup string, networkProviders map[pb networkProviders: networkProviders, cgroupParent: cgroup, dnsConfig: dnsConfig, - running: make(map[string]chan error), + running: make(map[string]*jobDetails), apparmorProfile: apparmorProfile, selinux: selinux, traceSocket: traceSocket, @@ -80,6 +75,16 @@ func New(client *containerd.Client, root, cgroup string, networkProviders map[pb } } +type jobDetails struct { + done chan error + // On linux the rootfsPath is used to ensure the CWD exists, to fetch user information + // and as a bind mount for the root FS of the container. + rootfsPath string + // On Windows we need to use the root mounts to achieve the same thing that Linux does + // with rootfsPath. So we save both in details. + rootMounts []mount.Mount +} + func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.Mount, mounts []executor.Mount, process executor.ProcessInfo, started chan<- struct{}) (rec resourcestypes.Recorder, err error) { if id == "" { id = identity.NewID() @@ -87,8 +92,11 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M startedOnce := sync.Once{} done := make(chan error, 1) + details := &jobDetails{ + done: done, + } w.mu.Lock() - w.running[id] = done + w.running[id] = details w.mu.Unlock() defer func() { w.mu.Lock() @@ -104,60 +112,16 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M }() meta := process.Meta - - resolvConf, err := oci.GetResolvConf(ctx, w.root, nil, w.dnsConfig) - if err != nil { - return nil, err - } - - hostsFile, clean, err := oci.GetHostsFile(ctx, w.root, meta.ExtraHosts, nil, meta.Hostname) - if err != nil { - return nil, err - } - if clean != nil { - defer clean() - } - - mountable, err := root.Src.Mount(ctx, false) + releasers, resolvConf, hostsFile, err := w.prepareExecutionEnv(ctx, root, mounts, meta, details) if err != nil { + releasers() return nil, err } + defer releasers() - rootMounts, release, err := mountable.Mount() - if err != nil { + if err := w.ensureCWD(ctx, details, meta); err != nil { return nil, err } - if release != nil { - defer release() - } - - lm := snapshot.LocalMounterWithMounts(rootMounts) - rootfsPath, err := lm.Mount() - if err != nil { - return nil, err - } - defer lm.Unmount() - defer executor.MountStubsCleaner(ctx, rootfsPath, mounts, meta.RemoveMountStubsRecursive)() - - uid, gid, sgids, err := oci.GetUser(rootfsPath, meta.User) - if err != nil { - return nil, err - } - - identity := idtools.Identity{ - UID: int(uid), - GID: int(gid), - } - - newp, err := fs.RootPath(rootfsPath, meta.Cwd) - if err != nil { - return nil, errors.Wrapf(err, "working dir %s points to invalid target", newp) - } - if _, err := os.Stat(newp); err != nil { - if err := idtools.MkdirAllAndChown(newp, 0755, identity); err != nil { - return nil, errors.Wrapf(err, "failed to create working directory %s", newp) - } - } provider, ok := w.networkProviders[meta.NetMode] if !ok { @@ -173,23 +137,12 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M bklog.G(ctx).Info("enabling HostNetworking") } - opts := []containerdoci.SpecOpts{oci.WithUIDGID(uid, gid, sgids)} - if meta.ReadonlyRootFS { - opts = append(opts, containerdoci.WithRootFSReadonly()) - } - - processMode := oci.ProcessSandbox // FIXME(AkihiroSuda) - spec, cleanup, err := oci.GenerateSpec(ctx, meta, mounts, id, resolvConf, hostsFile, namespace, w.cgroupParent, processMode, nil, w.apparmorProfile, w.selinux, w.traceSocket, opts...) + spec, specReleasers, err := w.getOCISpec(ctx, id, resolvConf, hostsFile, namespace, mounts, meta, details) if err != nil { + specReleasers() return nil, err } - defer cleanup() - spec.Process.Terminal = meta.Tty - if w.rootless { - if err := rootlessspecconv.ToRootless(spec); err != nil { - return nil, err - } - } + defer specReleasers() container, err := w.client.NewContainer(ctx, id, containerd.WithSpec(spec), @@ -210,20 +163,12 @@ func (w *containerdExecutor) Run(ctx context.Context, id string, root executor.M cioOpts = append(cioOpts, cio.WithTerminal) } - rootfs := containerd.WithRootFS([]mount.Mount{{ - Source: rootfsPath, - Type: "bind", - Options: []string{"rbind"}, - }}) - if runtime.GOOS == "freebsd" { - rootfs = containerd.WithRootFS([]mount.Mount{{ - Source: rootfsPath, - Type: "nullfs", - Options: []string{}, - }}) + taskOpts, err := w.getTaskOpts(ctx, details) + if err != nil { + return nil, err } - task, err := container.NewTask(ctx, cio.NewCreator(cioOpts...), rootfs) + task, err := container.NewTask(ctx, cio.NewCreator(cioOpts...), taskOpts) if err != nil { return nil, err } @@ -259,17 +204,16 @@ func (w *containerdExecutor) Exec(ctx context.Context, id string, process execut // is in the process of being created and check again every 100ms or until // context is canceled. + w.mu.Lock() + details, ok := w.running[id] + w.mu.Unlock() + + if !ok { + return errors.Errorf("container %s not found", id) + } var container containerd.Container var task containerd.Task for { - w.mu.Lock() - done, ok := w.running[id] - w.mu.Unlock() - - if !ok { - return errors.Errorf("container %s not found", id) - } - if container == nil { container, _ = w.client.LoadContainer(ctx, id) } @@ -285,7 +229,7 @@ func (w *containerdExecutor) Exec(ctx context.Context, id string, process execut select { case <-ctx.Done(): return ctx.Err() - case err, ok := <-done: + case err, ok := <-details.done: if !ok || err == nil { return errors.Errorf("container %s has stopped", id) } @@ -301,23 +245,24 @@ func (w *containerdExecutor) Exec(ctx context.Context, id string, process execut } proc := spec.Process - - // TODO how do we get rootfsPath for oci.GetUser in case user passed in username rather than uid:gid? - // For now only support uid:gid if meta.User != "" { - uid, gid, err := oci.ParseUIDGID(meta.User) + userSpec, err := getUserSpec(meta.User, details.rootfsPath) if err != nil { return errors.WithStack(err) } - proc.User = specs.User{ - UID: uid, - GID: gid, - AdditionalGids: []uint32{}, - } + proc.User = userSpec } proc.Terminal = meta.Tty - proc.Args = meta.Args + + if runtime.GOOS == "windows" { + // On Windows passing in Args will lead to double escaping by hcsshim, which leads to errors. + // The recommendation is to use CommandLine. + proc.CommandLine = strings.Join(meta.Args, " ") + } else { + proc.Args = meta.Args + } + if meta.Cwd != "" { spec.Process.Cwd = meta.Cwd } diff --git a/executor/containerdexecutor/executor_unix.go b/executor/containerdexecutor/executor_unix.go new file mode 100644 index 0000000000000..1a609cf6de61d --- /dev/null +++ b/executor/containerdexecutor/executor_unix.go @@ -0,0 +1,167 @@ +//go:build !windows +// +build !windows + +package containerdexecutor + +import ( + "context" + "os" + "runtime" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/mount" + containerdoci "github.com/containerd/containerd/oci" + "github.com/containerd/continuity/fs" + "github.com/docker/docker/pkg/idtools" + "github.com/moby/buildkit/executor" + "github.com/moby/buildkit/executor/oci" + "github.com/moby/buildkit/snapshot" + "github.com/moby/buildkit/util/bklog" + "github.com/moby/buildkit/util/network" + rootlessspecconv "github.com/moby/buildkit/util/rootless/specconv" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +func getUserSpec(user, rootfsPath string) (specs.User, error) { + var err error + var uid, gid uint32 + var sgids []uint32 + if rootfsPath != "" { + uid, gid, sgids, err = oci.GetUser(rootfsPath, user) + } else { + uid, gid, err = oci.ParseUIDGID(user) + } + if err != nil { + return specs.User{}, errors.WithStack(err) + } + return specs.User{ + UID: uid, + GID: gid, + AdditionalGids: sgids, + }, nil +} + +func (w *containerdExecutor) prepareExecutionEnv(ctx context.Context, rootMount executor.Mount, mounts []executor.Mount, meta executor.Meta, details *jobDetails) (func(), string, string, error) { + var releasers []func() + releaseAll := func() { + for i := len(releasers) - 1; i >= 0; i-- { + releasers[i]() + } + } + + resolvConf, err := oci.GetResolvConf(ctx, w.root, nil, w.dnsConfig) + if err != nil { + return releaseAll, "", "", err + } + + hostsFile, clean, err := oci.GetHostsFile(ctx, w.root, meta.ExtraHosts, nil, meta.Hostname) + if err != nil { + return releaseAll, "", "", err + } + releasers = append(releasers, clean) + + mountable, err := rootMount.Src.Mount(ctx, false) + if err != nil { + return releaseAll, "", "", err + } + + rootMounts, release, err := mountable.Mount() + if err != nil { + return releaseAll, "", "", err + } + details.rootMounts = rootMounts + + releasers = append(releasers, func() { + if err := release(); err != nil { + bklog.G(ctx).WithError(err).Error("failed to release root mount") + } + }) + lm := snapshot.LocalMounterWithMounts(rootMounts) + rootfsPath, err := lm.Mount() + if err != nil { + return releaseAll, "", "", err + } + details.rootfsPath = rootfsPath + releasers = append(releasers, func() { + if err := lm.Unmount(); err != nil { + bklog.G(ctx).WithError(err).Error("failed to unmount rootfs") + } + }) + releasers = append(releasers, executor.MountStubsCleaner(ctx, details.rootfsPath, mounts, meta.RemoveMountStubsRecursive)) + + return releaseAll, resolvConf, hostsFile, nil +} + +func (w *containerdExecutor) getTaskOpts(ctx context.Context, details *jobDetails) (containerd.NewTaskOpts, error) { + rootfs := containerd.WithRootFS([]mount.Mount{{ + Source: details.rootfsPath, + Type: "bind", + Options: []string{"rbind"}, + }}) + if runtime.GOOS == "freebsd" { + rootfs = containerd.WithRootFS([]mount.Mount{{ + Source: details.rootfsPath, + Type: "nullfs", + Options: []string{}, + }}) + } + return rootfs, nil +} + +func (w *containerdExecutor) ensureCWD(ctx context.Context, details *jobDetails, meta executor.Meta) error { + newp, err := fs.RootPath(details.rootfsPath, meta.Cwd) + if err != nil { + return errors.Wrapf(err, "working dir %s points to invalid target", newp) + } + + uid, gid, _, err := oci.GetUser(details.rootfsPath, meta.User) + if err != nil { + return err + } + + identity := idtools.Identity{ + UID: int(uid), + GID: int(gid), + } + + if _, err := os.Stat(newp); err != nil { + if err := idtools.MkdirAllAndChown(newp, 0755, identity); err != nil { + return errors.Wrapf(err, "failed to create working directory %s", newp) + } + } + return nil +} + +func (w *containerdExecutor) getOCISpec(ctx context.Context, id, resolvConf, hostsFile string, namespace network.Namespace, mounts []executor.Mount, meta executor.Meta, details *jobDetails) (*specs.Spec, func(), error) { + var releasers []func() + releaseAll := func() { + for _, release := range releasers { + release() + } + } + + uid, gid, sgids, err := oci.GetUser(details.rootfsPath, meta.User) + if err != nil { + return nil, releaseAll, err + } + + opts := []containerdoci.SpecOpts{oci.WithUIDGID(uid, gid, sgids)} + if meta.ReadonlyRootFS { + opts = append(opts, containerdoci.WithRootFSReadonly()) + } + + processMode := oci.ProcessSandbox // FIXME(AkihiroSuda) + spec, cleanup, err := oci.GenerateSpec(ctx, meta, mounts, id, resolvConf, hostsFile, namespace, w.cgroupParent, processMode, nil, w.apparmorProfile, w.selinux, w.traceSocket, opts...) + if err != nil { + return nil, releaseAll, err + } + releasers = append(releasers, cleanup) + spec.Process.Terminal = meta.Tty + if w.rootless { + if err := rootlessspecconv.ToRootless(spec); err != nil { + return nil, releaseAll, err + } + } + return spec, releaseAll, nil +} diff --git a/executor/containerdexecutor/executor_windows.go b/executor/containerdexecutor/executor_windows.go new file mode 100644 index 0000000000000..bb02dd798bfb2 --- /dev/null +++ b/executor/containerdexecutor/executor_windows.go @@ -0,0 +1,99 @@ +package containerdexecutor + +import ( + "context" + "os" + + "github.com/containerd/containerd" + containerdoci "github.com/containerd/containerd/oci" + "github.com/containerd/continuity/fs" + "github.com/docker/docker/pkg/idtools" + "github.com/moby/buildkit/executor" + "github.com/moby/buildkit/executor/oci" + "github.com/moby/buildkit/snapshot" + "github.com/moby/buildkit/util/network" + "github.com/moby/buildkit/util/windows" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +func getUserSpec(user, rootfsPath string) (specs.User, error) { + return specs.User{ + Username: user, + }, nil +} + +func (w *containerdExecutor) prepareExecutionEnv(ctx context.Context, rootMount executor.Mount, mounts []executor.Mount, meta executor.Meta, details *jobDetails) (func(), string, string, error) { + var releasers []func() error + releaseAll := func() { + for _, release := range releasers { + release() + } + } + + mountable, err := rootMount.Src.Mount(ctx, false) + if err != nil { + return releaseAll, "", "", err + } + + rootMounts, release, err := mountable.Mount() + if err != nil { + return releaseAll, "", "", err + } + details.rootMounts = rootMounts + releasers = append(releasers, release) + + return releaseAll, "", "", nil +} + +func (w *containerdExecutor) getTaskOpts(ctx context.Context, details *jobDetails) (containerd.NewTaskOpts, error) { + return containerd.WithRootFS(details.rootMounts), nil +} + +func (w *containerdExecutor) ensureCWD(ctx context.Context, details *jobDetails, meta executor.Meta) (err error) { + // TODO(gabriel-samfira): Use a snapshot? + identity, err := windows.ResolveUsernameToSID(ctx, w, details.rootMounts, meta.User) + if err != nil { + return errors.Wrap(err, "getting user SID") + } + + lm := snapshot.LocalMounterWithMounts(details.rootMounts) + rootfsPath, err := lm.Mount() + if err != nil { + return err + } + defer lm.Unmount() + + newp, err := fs.RootPath(rootfsPath, meta.Cwd) + if err != nil { + return errors.Wrapf(err, "working dir %s points to invalid target", newp) + } + + if _, err := os.Stat(newp); err != nil { + if err := idtools.MkdirAllAndChown(newp, 0755, identity); err != nil { + return errors.Wrapf(err, "failed to create working directory %s", newp) + } + } + return nil +} + +func (w *containerdExecutor) getOCISpec(ctx context.Context, id, resolvConf, hostsFile string, namespace network.Namespace, mounts []executor.Mount, meta executor.Meta, details *jobDetails) (*specs.Spec, func(), error) { + var releasers []func() + releaseAll := func() { + for _, release := range releasers { + release() + } + } + + opts := []containerdoci.SpecOpts{ + containerdoci.WithUser(meta.User), + } + + processMode := oci.ProcessSandbox // FIXME(AkihiroSuda) + spec, cleanup, err := oci.GenerateSpec(ctx, meta, mounts, id, "", "", namespace, "", processMode, nil, "", false, w.traceSocket, opts...) + if err != nil { + return nil, releaseAll, err + } + releasers = append(releasers, cleanup) + return spec, releaseAll, nil +} diff --git a/executor/oci/spec.go b/executor/oci/spec.go index 2a30ad060ef20..17f5adf523ef4 100644 --- a/executor/oci/spec.go +++ b/executor/oci/spec.go @@ -2,8 +2,8 @@ package oci import ( "context" - "path" "path/filepath" + "runtime" "strings" "sync" @@ -125,7 +125,7 @@ func GenerateSpec(ctx context.Context, meta executor.Meta, mounts []executor.Mou } opts = append(opts, - oci.WithProcessArgs(meta.Args...), + withProcessArgs(meta.Args...), oci.WithEnv(meta.Env), oci.WithProcessCwd(meta.Cwd), oci.WithNewPrivileges, @@ -224,7 +224,7 @@ type submounts struct { } func (s *submounts) subMount(m mount.Mount, subPath string) (mount.Mount, error) { - if path.Join("/", subPath) == "/" { + if filepath.ToSlash(filepath.Join("/", subPath)) == "/" { return m, nil } if s.m == nil { @@ -249,17 +249,24 @@ func (s *submounts) subMount(m mount.Mount, subPath string) (mount.Mount, error) return mount.Mount{}, err } - opts := []string{"rbind"} - for _, opt := range m.Options { - if opt == "ro" { - opts = append(opts, opt) - } + var mntType string + opts := []string{} + if m.ReadOnly() { + opts = append(opts, "ro") + } + + if runtime.GOOS != "windows" { + // Windows uses a mechanism similar to bind mounts, but will err out if we request + // a mount type it does not understand. Leaving the mount type empty on Windows will + // yield the same result. + mntType = "bind" + opts = append(opts, "rbind") } s.m[h] = mountRef{ mount: mount.Mount{ Source: mp, - Type: "bind", + Type: mntType, Options: opts, }, unmount: lm.Unmount, diff --git a/executor/oci/spec_freebsd.go b/executor/oci/spec_freebsd.go index 1dacc5581a085..2707c6b4d8873 100644 --- a/executor/oci/spec_freebsd.go +++ b/executor/oci/spec_freebsd.go @@ -8,6 +8,10 @@ import ( "github.com/pkg/errors" ) +func withProcessArgs(args ...string) oci.SpecOpts { + return oci.WithProcessArgs(args...) +} + func generateMountOpts(resolvConf, hostsFile string) ([]oci.SpecOpts, error) { return nil, nil } diff --git a/executor/oci/spec_linux.go b/executor/oci/spec_linux.go index 4f5839cb69f41..cd9912b906bec 100644 --- a/executor/oci/spec_linux.go +++ b/executor/oci/spec_linux.go @@ -29,6 +29,10 @@ const ( tracingSocketPath = "/dev/otel-grpc.sock" ) +func withProcessArgs(args ...string) oci.SpecOpts { + return oci.WithProcessArgs(args...) +} + func generateMountOpts(resolvConf, hostsFile string) ([]oci.SpecOpts, error) { return []oci.SpecOpts{ // https://github.com/moby/buildkit/issues/429 diff --git a/executor/oci/spec_windows.go b/executor/oci/spec_windows.go index 5033d905a475c..0201bcd7689dc 100644 --- a/executor/oci/spec_windows.go +++ b/executor/oci/spec_windows.go @@ -4,9 +4,13 @@ package oci import ( + "context" "fmt" + "os" "path/filepath" + "strings" + "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" "github.com/docker/docker/pkg/idtools" "github.com/moby/buildkit/solver/pb" @@ -18,8 +22,37 @@ const ( tracingSocketPath = "//./pipe/otel-grpc" ) +func withProcessArgs(args ...string) oci.SpecOpts { + cmdLine := strings.Join(args, " ") + // This will set Args to nil and properly set the CommandLine option + // in the spec. On Windows we need to use CommandLine instead of Args. + return oci.WithProcessCommandLine(cmdLine) +} + +func withGetUserInfoMount() oci.SpecOpts { + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error { + execPath, err := os.Executable() + if err != nil { + return errors.Wrap(err, "getting executable path") + } + // The buildkit binary registers a re-exec function that is invoked when called with + // get-user-info as the name. We mount the binary as read-only inside the container. This + // spares us from having to ship a separate binary just for this purpose. The container does + // not share any state with the running buildkit daemon. In this scenario, we use the re-exec + // functionality to simulate a multi-call binary. + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "C:\\Windows\\System32\\get-user-info.exe", + Source: execPath, + Options: []string{"ro"}, + }) + return nil + } +} + func generateMountOpts(resolvConf, hostsFile string) ([]oci.SpecOpts, error) { - return nil, nil + return []oci.SpecOpts{ + withGetUserInfoMount(), + }, nil } // generateSecurityOpts may affect mounts, so must be called after generateMountOpts diff --git a/util/windows/util_windows.go b/util/windows/util_windows.go new file mode 100644 index 0000000000000..87b1136e0f958 --- /dev/null +++ b/util/windows/util_windows.go @@ -0,0 +1,156 @@ +package windows + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + "syscall" + + "github.com/containerd/containerd/mount" + "github.com/docker/docker/pkg/idtools" + "github.com/moby/buildkit/executor" + "github.com/moby/buildkit/snapshot" + "github.com/pkg/errors" +) + +func ResolveUsernameToSID(ctx context.Context, exec executor.Executor, rootMount []mount.Mount, userName string) (idtools.Identity, error) { + // This is a shortcut in case the user is one of the builtin users that should exist + // in any WCOW container. While these users do exist in containers, they don't exist on the + // host. We check them before trying to look them up using LookupSID(). + if strings.EqualFold(userName, "ContainerAdministrator") || userName == "" { + return idtools.Identity{SID: idtools.ContainerAdministratorSidString}, nil + } else if strings.EqualFold(userName, "ContainerUser") { + return idtools.Identity{SID: idtools.ContainerUserSidString}, nil + } + + // We might have a SID set as username. There is no guarantee that this SID will exist + // inside the container, even if we can successfully parse it. If a SID was used, we trust + // that the user has made sure it does map to an identity inside the container. + if strings.HasPrefix(strings.ToLower(userName), "s-") { + if _, err := syscall.StringToSid(userName); err == nil { + return idtools.Identity{SID: userName}, nil + } + } + + // We test for well known accounts that should exist inside any system. This has the potential + // to fail if the usernames/group names differ in the container, as is the case on internationalized + // versions where the builtin account and group names may have names in the local language. + // If the user specified an internationalized version of the account name, but the host is in English, + // this lookup will most likely fail and we will fall back to running get-account-info inside the container. + // This should however catch most of the cases when well known accounts/groups are used. + sid, _, accountType, err := syscall.LookupSID("", userName) + if err == nil { + if accountType == syscall.SidTypeAlias || accountType == syscall.SidTypeWellKnownGroup { + sidAsString, err := sid.String() + if err == nil { + return idtools.Identity{SID: sidAsString}, nil + } + } + } + + // Last resort. + // The equivalent in Windows of /etc/passwd and /etc/group is a registry hive called SAM which can be found + // on any windows system in: C:\Windows\System32\config\SAM. + // + // This hive holds all user information on a particular system, including the SID of the user we care + // about. The bad news is that the data structures in this hive are completely undocumented and there + // is no API we can call to load the security info inside an offline SAM hive. We can load it as a + // registry hive, but parsing the data structures it holds is not documented. It's not impossible to do, + // but in the absence of a supported API to do this for us, we risk that sometime in the future our parser + // will break. + // + // That being said, we have no choice but to execute a command inside the rootMount and attempt to get the + // SID of the user we care about using officially supported Windows APIs. This obviously adds some overhead. + // + // TODO(gsamfira): Should we use a snapshot of the rootMount? + ident, err := GetUserIdentFromContainer(ctx, exec, rootMount, userName) + if err != nil { + return idtools.Identity{}, errors.Wrap(err, "getting account SID from container") + } + return ident, nil +} + +func GetUserIdentFromContainer(ctx context.Context, exec executor.Executor, rootMounts []mount.Mount, userName string) (idtools.Identity, error) { + var ident idtools.Identity + + if len(rootMounts) > 1 { + return ident, fmt.Errorf("unexpected number of root mounts: %d", len(rootMounts)) + } + + stdout := &bytesReadWriteCloser{ + bw: &bytes.Buffer{}, + } + stderr := &bytesReadWriteCloser{ + bw: &bytes.Buffer{}, + } + + defer stdout.Close() + defer stderr.Close() + + procInfo := executor.ProcessInfo{ + Meta: executor.Meta{ + Args: []string{"get-user-info", userName}, + User: "ContainerAdministrator", + Cwd: "/", + }, + Stdin: nil, + Stdout: stdout, + Stderr: stderr, + } + + if _, err := exec.Run(ctx, "", newStubMountable(rootMounts), nil, procInfo, nil); err != nil { + return ident, errors.Wrap(err, "executing command") + } + + data := stdout.bw.Bytes() + if err := json.Unmarshal(data, &ident); err != nil { + return ident, errors.Wrap(err, "reading user info") + } + + return ident, nil +} + +type bytesReadWriteCloser struct { + bw *bytes.Buffer +} + +func (b *bytesReadWriteCloser) Write(p []byte) (int, error) { + if b.bw == nil { + return 0, fmt.Errorf("invalid bytes buffer") + } + return b.bw.Write(p) +} + +func (b *bytesReadWriteCloser) Close() error { + if b.bw == nil { + return nil + } + b.bw.Reset() + return nil +} + +type snapshotMountable struct { + m []mount.Mount +} + +func (m *snapshotMountable) Mount() ([]mount.Mount, func() error, error) { + cleanup := func() error { return nil } + return m.m, cleanup, nil +} +func (m *snapshotMountable) IdentityMapping() *idtools.IdentityMapping { + return nil +} + +type executorMountable struct { + m snapshot.Mountable +} + +func (m *executorMountable) Mount(ctx context.Context, readonly bool) (snapshot.Mountable, error) { + return m.m, nil +} + +func newStubMountable(m []mount.Mount) executor.Mount { + return executor.Mount{Src: &executorMountable{m: &snapshotMountable{m: m}}} +}