diff --git a/apparmor.go b/apparmor.go new file mode 100644 index 000000000..117b52bb1 --- /dev/null +++ b/apparmor.go @@ -0,0 +1,13 @@ +// +build linux + +package containerd + +import specs "github.com/opencontainers/runtime-spec/specs-go" + +// WithApparmor sets the provided apparmor profile to the spec +func WithApparmorProfile(profile string) SpecOpts { + return func(s *specs.Spec) error { + s.Process.ApparmorProfile = profile + return nil + } +} diff --git a/archive/path.go b/archive/path.go index 32374459c..0f6cfa32e 100644 --- a/archive/path.go +++ b/archive/path.go @@ -1,107 +1 @@ package archive - -import ( - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" -) - -var ( - errTooManyLinks = errors.New("too many links") -) - -// rootPath joins a path with a root, evaluating and bounding any -// symlink to the root directory. -// TODO(dmcgowan): Expose and move to fs package or continuity path driver -func rootPath(root, path string) (string, error) { - if path == "" { - return root, nil - } - var linksWalked int // to protect against cycles - for { - i := linksWalked - newpath, err := walkLinks(root, path, &linksWalked) - if err != nil { - return "", err - } - path = newpath - if i == linksWalked { - newpath = filepath.Join("/", newpath) - if path == newpath { - return filepath.Join(root, newpath), nil - } - path = newpath - } - } -} - -func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) { - if *linksWalked > 255 { - return "", false, errTooManyLinks - } - - path = filepath.Join("/", path) - if path == "/" { - return path, false, nil - } - realPath := filepath.Join(root, path) - - fi, err := os.Lstat(realPath) - if err != nil { - // If path does not yet exist, treat as non-symlink - if os.IsNotExist(err) { - return path, false, nil - } - return "", false, err - } - if fi.Mode()&os.ModeSymlink == 0 { - return path, false, nil - } - newpath, err = os.Readlink(realPath) - if err != nil { - return "", false, err - } - if filepath.IsAbs(newpath) && strings.HasPrefix(newpath, root) { - newpath = newpath[:len(root)] - if !strings.HasPrefix(newpath, "/") { - newpath = "/" + newpath - } - } - *linksWalked++ - return newpath, true, nil -} - -func walkLinks(root, path string, linksWalked *int) (string, error) { - switch dir, file := filepath.Split(path); { - case dir == "": - newpath, _, err := walkLink(root, file, linksWalked) - return newpath, err - case file == "": - if os.IsPathSeparator(dir[len(dir)-1]) { - if dir == "/" { - return dir, nil - } - return walkLinks(root, dir[:len(dir)-1], linksWalked) - } - newpath, _, err := walkLink(root, dir, linksWalked) - return newpath, err - default: - newdir, err := walkLinks(root, dir, linksWalked) - if err != nil { - return "", err - } - newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked) - if err != nil { - return "", err - } - if !islink { - return newpath, nil - } - if filepath.IsAbs(newpath) { - return newpath, nil - } - return filepath.Join(newdir, newpath), nil - } -} diff --git a/archive/tar.go b/archive/tar.go index b29699e10..843234c0a 100644 --- a/archive/tar.go +++ b/archive/tar.go @@ -128,7 +128,7 @@ func Apply(ctx context.Context, root string, r io.Reader) (int64, error) { // Split name and resolve symlinks for root directory. ppath, base := filepath.Split(hdr.Name) - ppath, err = rootPath(root, ppath) + ppath, err = fs.RootPath(root, ppath) if err != nil { return 0, errors.Wrap(err, "failed to get root path") } @@ -170,7 +170,7 @@ func Apply(ctx context.Context, root string, r io.Reader) (int64, error) { } defer os.RemoveAll(aufsTempdir) } - p, err := rootPath(aufsTempdir, basename) + p, err := fs.RootPath(aufsTempdir, basename) if err != nil { return 0, err } @@ -243,7 +243,7 @@ func Apply(ctx context.Context, root string, r io.Reader) (int64, error) { if srcHdr == nil { return 0, fmt.Errorf("Invalid aufs hardlink") } - p, err := rootPath(aufsTempdir, linkBasename) + p, err := fs.RootPath(aufsTempdir, linkBasename) if err != nil { return 0, err } @@ -268,7 +268,7 @@ func Apply(ctx context.Context, root string, r io.Reader) (int64, error) { } for _, hdr := range dirs { - path, err := rootPath(root, hdr.Name) + path, err := fs.RootPath(root, hdr.Name) if err != nil { return 0, err } @@ -478,7 +478,7 @@ func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header } case tar.TypeLink: - targetPath, err := rootPath(extractDir, hdr.Linkname) + targetPath, err := fs.RootPath(extractDir, hdr.Linkname) if err != nil { return err } diff --git a/archive/tar_test.go b/archive/tar_test.go index 68cfeebee..67dc8a02a 100644 --- a/archive/tar_test.go +++ b/archive/tar_test.go @@ -182,11 +182,11 @@ func TestBreakouts(t *testing.T) { errFileDiff := errors.New("files differ") sameFile := func(f1, f2 string) func(string) error { return func(root string) error { - p1, err := rootPath(root, f1) + p1, err := fs.RootPath(root, f1) if err != nil { return err } - p2, err := rootPath(root, f2) + p2, err := fs.RootPath(root, f2) if err != nil { return err } @@ -484,7 +484,7 @@ func TestApplyTar(t *testing.T) { directoriesExist := func(dirs ...string) func(string) error { return func(root string) error { for _, d := range dirs { - p, err := rootPath(root, d) + p, err := fs.RootPath(root, d) if err != nil { return err } diff --git a/container_test.go b/container_test.go index 38e8b699f..2b423285f 100644 --- a/container_test.go +++ b/container_test.go @@ -889,3 +889,81 @@ func TestContainerExecNoBinaryExists(t *testing.T) { } <-finished } + +func TestUserNamespaces(t *testing.T) { + client, err := newClient(t, address) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + var ( + image Image + ctx, cancel = testContext() + id = t.Name() + ) + defer cancel() + + if runtime.GOOS != "windows" { + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Error(err) + return + } + } + + spec, err := generateSpec( + withImageConfig(ctx, image), + withExitStatus(7), + withUserNamespace(0, 1000, 10000), + ) + if err != nil { + t.Error(err) + return + } + container, err := client.NewContainer(ctx, id, + WithSpec(spec), + withRemappedSnapshot(id, image, 1000, 1000), + ) + if err != nil { + t.Error(err) + return + } + defer container.Delete(ctx, WithSnapshotCleanup) + + task, err := container.NewTask(ctx, Stdio) + if err != nil { + t.Error(err) + return + } + defer task.Delete(ctx) + + statusC := make(chan uint32, 1) + go func() { + status, err := task.Wait(ctx) + if err != nil { + t.Error(err) + } + statusC <- status + }() + + if pid := task.Pid(); pid <= 0 { + t.Errorf("invalid task pid %d", pid) + } + if err := task.Start(ctx); err != nil { + t.Error(err) + task.Delete(ctx) + return + } + status := <-statusC + if status != 7 { + t.Errorf("expected status 7 from wait but received %d", status) + } + if status, err = task.Delete(ctx); err != nil { + t.Error(err) + return + } + if status != 7 { + t.Errorf("expected status 7 from delete but received %d", status) + } +} diff --git a/fs/path.go b/fs/path.go index a46d0fcbd..644b1ee2e 100644 --- a/fs/path.go +++ b/fs/path.go @@ -7,6 +7,12 @@ import ( "os" "path/filepath" "strings" + + "github.com/pkg/errors" +) + +var ( + errTooManyLinks = errors.New("too many links") ) type currentPath struct { @@ -160,3 +166,96 @@ func nextPath(ctx context.Context, pathC <-chan *currentPath) (*currentPath, err return p, nil } } + +// RootPath joins a path with a root, evaluating and bounding any +// symlink to the root directory. +func RootPath(root, path string) (string, error) { + if path == "" { + return root, nil + } + var linksWalked int // to protect against cycles + for { + i := linksWalked + newpath, err := walkLinks(root, path, &linksWalked) + if err != nil { + return "", err + } + path = newpath + if i == linksWalked { + newpath = filepath.Join("/", newpath) + if path == newpath { + return filepath.Join(root, newpath), nil + } + path = newpath + } + } +} + +func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) { + if *linksWalked > 255 { + return "", false, errTooManyLinks + } + + path = filepath.Join("/", path) + if path == "/" { + return path, false, nil + } + realPath := filepath.Join(root, path) + + fi, err := os.Lstat(realPath) + if err != nil { + // If path does not yet exist, treat as non-symlink + if os.IsNotExist(err) { + return path, false, nil + } + return "", false, err + } + if fi.Mode()&os.ModeSymlink == 0 { + return path, false, nil + } + newpath, err = os.Readlink(realPath) + if err != nil { + return "", false, err + } + if filepath.IsAbs(newpath) && strings.HasPrefix(newpath, root) { + newpath = newpath[:len(root)] + if !strings.HasPrefix(newpath, "/") { + newpath = "/" + newpath + } + } + *linksWalked++ + return newpath, true, nil +} + +func walkLinks(root, path string, linksWalked *int) (string, error) { + switch dir, file := filepath.Split(path); { + case dir == "": + newpath, _, err := walkLink(root, file, linksWalked) + return newpath, err + case file == "": + if os.IsPathSeparator(dir[len(dir)-1]) { + if dir == "/" { + return dir, nil + } + return walkLinks(root, dir[:len(dir)-1], linksWalked) + } + newpath, _, err := walkLink(root, dir, linksWalked) + return newpath, err + default: + newdir, err := walkLinks(root, dir, linksWalked) + if err != nil { + return "", err + } + newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked) + if err != nil { + return "", err + } + if !islink { + return newpath, nil + } + if filepath.IsAbs(newpath) { + return newpath, nil + } + return filepath.Join(newdir, newpath), nil + } +} diff --git a/archive/path_test.go b/fs/path_test.go similarity index 98% rename from archive/path_test.go rename to fs/path_test.go index 6cb83ef62..b09090800 100644 --- a/archive/path_test.go +++ b/fs/path_test.go @@ -1,6 +1,6 @@ // +build !windows -package archive +package fs import ( "io/ioutil" @@ -179,7 +179,7 @@ func testRootPathSymlinkRootScope(t *testing.T) { if err != nil { t.Fatal(err) } - rewrite, err := rootPath("/", tmpdir) + rewrite, err := RootPath("/", tmpdir) if err != nil { t.Fatal(err) } @@ -192,7 +192,7 @@ func testRootPathSymlinkEmpty(t *testing.T) { if err != nil { t.Fatal(err) } - res, err := rootPath(wd, "") + res, err := RootPath(wd, "") if err != nil { t.Fatal(err) } @@ -221,7 +221,7 @@ func makeRootPathTest(t *testing.T, apply fstest.Applier, checks []rootCheck) fu root = check.scope(root) } - actual, err := rootPath(root, check.unresolved) + actual, err := RootPath(root, check.unresolved) if check.cause != nil { if err == nil { t.Errorf("(Check %d) Expected error %q, %q evaluated as %q", i+1, check.cause.Error(), check.unresolved, actual) diff --git a/helpers_unix_test.go b/helpers_unix_test.go index 7186d70fe..ea93953ef 100644 --- a/helpers_unix_test.go +++ b/helpers_unix_test.go @@ -49,3 +49,7 @@ func withImageConfig(ctx context.Context, i Image) SpecOpts { func withNewSnapshot(id string, i Image) NewContainerOpts { return WithNewSnapshot(id, i) } + +var withUserNamespace = WithUserNamespace + +var withRemappedSnapshot = WithRemappedSnapshot diff --git a/helpers_windows_test.go b/helpers_windows_test.go index 9a6e632a2..7a148d90c 100644 --- a/helpers_windows_test.go +++ b/helpers_windows_test.go @@ -63,3 +63,15 @@ func withNewSnapshot(id string, i Image) NewContainerOpts { return nil } } + +func withUserNamespace(u, g, s uint32) SpecOpts { + return func(s *specs.Spec) error { + return nil + } +} + +func withRemappedSnapshot(id string, i Image, u, g uint32) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + return nil + } +} diff --git a/linux/bundle.go b/linux/bundle.go index b4b672342..42465c3d3 100644 --- a/linux/bundle.go +++ b/linux/bundle.go @@ -21,7 +21,7 @@ func loadBundle(path, namespace string) *bundle { // newBundle creates a new bundle on disk at the provided path for the given id func newBundle(path, namespace, id string, spec []byte) (b *bundle, err error) { - if err := os.MkdirAll(path, 0700); err != nil { + if err := os.MkdirAll(path, 0711); err != nil { return nil, err } path = filepath.Join(path, id) @@ -30,10 +30,10 @@ func newBundle(path, namespace, id string, spec []byte) (b *bundle, err error) { os.RemoveAll(path) } }() - if err := os.Mkdir(path, 0700); err != nil { + if err := os.Mkdir(path, 0711); err != nil { return nil, err } - if err := os.Mkdir(filepath.Join(path, "rootfs"), 0700); err != nil { + if err := os.Mkdir(filepath.Join(path, "rootfs"), 0711); err != nil { return nil, err } f, err := os.Create(filepath.Join(path, configFilename)) diff --git a/linux/runtime.go b/linux/runtime.go index b32e3f24c..7179c2929 100644 --- a/linux/runtime.go +++ b/linux/runtime.go @@ -68,7 +68,7 @@ type Config struct { } func New(ic *plugin.InitContext) (interface{}, error) { - if err := os.MkdirAll(ic.Root, 0700); err != nil { + if err := os.MkdirAll(ic.Root, 0711); err != nil { return nil, err } monitor, err := ic.Get(plugin.TaskMonitorPlugin) diff --git a/linux/shim/init.go b/linux/shim/init.go index f34defe70..d1eb0a86e 100644 --- a/linux/shim/init.go +++ b/linux/shim/init.go @@ -120,7 +120,6 @@ func newInitProcess(context context.Context, path, namespace string, r *shimapi. } defer os.Remove(socket.Path()) } else { - // TODO: get uid/gid if io, err = runc.NewPipeIO(0, 0); err != nil { return nil, errors.Wrap(err, "failed to create OCI runtime io pipes") } diff --git a/server/server.go b/server/server.go index 1d205261f..940f40eaf 100644 --- a/server/server.go +++ b/server/server.go @@ -36,7 +36,7 @@ func New(ctx context.Context, config *Config) (*Server, error) { if config.Root == "" { return nil, errors.New("root must be specified") } - if err := os.MkdirAll(config.Root, 0700); err != nil { + if err := os.MkdirAll(config.Root, 0711); err != nil { return nil, err } if err := apply(ctx, config); err != nil { @@ -168,7 +168,7 @@ func loadPlugins(config *Config) ([]*plugin.Registration, error) { Type: plugin.MetadataPlugin, ID: "bolt", Init: func(ic *plugin.InitContext) (interface{}, error) { - if err := os.MkdirAll(ic.Root, 0700); err != nil { + if err := os.MkdirAll(ic.Root, 0711); err != nil { return nil, err } return bolt.Open(filepath.Join(ic.Root, "meta.db"), 0644, nil) diff --git a/snapshot/overlay/overlay.go b/snapshot/overlay/overlay.go index c759829d5..7d1616bc6 100644 --- a/snapshot/overlay/overlay.go +++ b/snapshot/overlay/overlay.go @@ -255,12 +255,12 @@ func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshot.Kind, ke } }() - if err = os.MkdirAll(filepath.Join(td, "fs"), 0711); err != nil { + if err = os.MkdirAll(filepath.Join(td, "fs"), 0755); err != nil { return nil, err } if kind == snapshot.KindActive { - if err = os.MkdirAll(filepath.Join(td, "work"), 0700); err != nil { + if err = os.MkdirAll(filepath.Join(td, "work"), 0711); err != nil { return nil, err } } diff --git a/spec.go b/spec.go index 53e1fd5f5..3d52b9cc3 100644 --- a/spec.go +++ b/spec.go @@ -2,8 +2,10 @@ package containerd import specs "github.com/opencontainers/runtime-spec/specs-go" +// SpecOpts sets spec specific information to a newly generated OCI spec type SpecOpts func(s *specs.Spec) error +// WithProcessArgs replaces the args on the generated spec func WithProcessArgs(args ...string) SpecOpts { return func(s *specs.Spec) error { s.Process.Args = args diff --git a/spec_unix.go b/spec_unix.go index b8e1171b6..0addded1b 100644 --- a/spec_unix.go +++ b/spec_unix.go @@ -6,12 +6,21 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" + "os" + "path/filepath" "strconv" "strings" + "syscall" + + "golang.org/x/sys/unix" "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/fs" "github.com/containerd/containerd/images" + "github.com/containerd/containerd/mount" "github.com/containerd/containerd/typeurl" + "github.com/opencontainers/image-spec/identity" "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -78,7 +87,6 @@ func createDefaultSpec() (*specs.Spec, error) { Permitted: defaltCaps(), Inheritable: defaltCaps(), Effective: defaltCaps(), - Ambient: defaltCaps(), }, Rlimits: []specs.POSIXRlimit{ { @@ -130,24 +138,6 @@ func createDefaultSpec() (*specs.Spec, error) { Source: "tmpfs", Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, }, - { - Destination: "/etc/resolv.conf", - Type: "bind", - Source: "/etc/resolv.conf", - Options: []string{"rbind", "ro"}, - }, - { - Destination: "/etc/hosts", - Type: "bind", - Source: "/etc/hosts", - Options: []string{"rbind", "ro"}, - }, - { - Destination: "/etc/localtime", - Type: "bind", - Source: "/etc/localtime", - Options: []string{"rbind", "ro"}, - }, }, Linux: &specs.Linux{ // TODO (@crosbymichael) make sure we don't have have two containers in the same cgroup @@ -272,6 +262,7 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts { } } +// WithSpec sets the provided spec for a new container func WithSpec(spec *specs.Spec) NewContainerOpts { return func(ctx context.Context, client *Client, c *containers.Container) error { any, err := typeurl.MarshalAny(spec) @@ -283,9 +274,153 @@ func WithSpec(spec *specs.Spec) NewContainerOpts { } } +// WithResources sets the provided resources on the spec for task updates func WithResources(resources *specs.LinuxResources) UpdateTaskOpts { return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error { r.Resources = resources return nil } } + +// WithNoNewPrivileges sets no_new_privileges on the process for the container +func WithNoNewPrivileges(s *specs.Spec) error { + s.Process.NoNewPrivileges = true + return nil +} + +func WithHostHosts(s *specs.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/hosts", + Type: "bind", + Source: "/etc/hosts", + Options: []string{"rbind", "ro"}, + }) + return nil +} + +func WithHostResoveconf(s *specs.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/resolv.conf", + Type: "bind", + Source: "/etc/resolv.conf", + Options: []string{"rbind", "ro"}, + }) + return nil +} + +func WithHostLocaltime(s *specs.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/localtime", + Type: "bind", + Source: "/etc/localtime", + Options: []string{"rbind", "ro"}, + }) + return nil +} + +// WithUserNamespace sets the uid and gid mappings for the task +// this can be called multiple times to add more mappings to the generated spec +func WithUserNamespace(container, host, size uint32) SpecOpts { + return func(s *specs.Spec) error { + var hasUserns bool + for _, ns := range s.Linux.Namespaces { + if ns.Type == specs.UserNamespace { + hasUserns = true + break + } + } + if !hasUserns { + s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{ + Type: specs.UserNamespace, + }) + } + mapping := specs.LinuxIDMapping{ + ContainerID: container, + HostID: host, + Size: size, + } + s.Linux.UIDMappings = append(s.Linux.UIDMappings, mapping) + s.Linux.GIDMappings = append(s.Linux.GIDMappings, mapping) + return nil + } +} + +// WithRemappedSnapshot creates a new snapshot and remaps the uid/gid for the +// filesystem to be used by a container with user namespaces +func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore()) + if err != nil { + return err + } + var ( + snapshotter = client.SnapshotService(c.Snapshotter) + parent = identity.ChainID(diffIDs).String() + usernsID = fmt.Sprintf("%s-%d-%d", parent, uid, gid) + ) + if _, err := snapshotter.Stat(ctx, usernsID); err == nil { + if _, err := snapshotter.Prepare(ctx, id, usernsID); err != nil { + return err + } + c.RootFS = id + c.Image = i.Name() + return nil + } + mounts, err := snapshotter.Prepare(ctx, usernsID+"-remap", parent) + if err != nil { + return err + } + if err := remapRootFS(mounts, uid, gid); err != nil { + snapshotter.Remove(ctx, usernsID) + return err + } + if err := snapshotter.Commit(ctx, usernsID, usernsID+"-remap"); err != nil { + return err + } + if _, err := snapshotter.Prepare(ctx, id, usernsID); err != nil { + return err + } + c.RootFS = id + c.Image = i.Name() + return nil + } +} + +func remapRootFS(mounts []mount.Mount, uid, gid uint32) error { + root, err := ioutil.TempDir("", "ctd-remap") + if err != nil { + return err + } + defer os.RemoveAll(root) + for _, m := range mounts { + if err := m.Mount(root); err != nil { + return err + } + } + defer unix.Unmount(root, 0) + return filepath.Walk(root, incrementFS(root, uid, gid)) +} + +func incrementFS(root string, uidInc, gidInc uint32) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if root == path { + return nil + } + var ( + stat = info.Sys().(*syscall.Stat_t) + u, g = int(stat.Uid + uidInc), int(stat.Gid + gidInc) + symlink = info.Mode()&os.ModeSymlink != 0 + ) + // make sure we resolve links inside the root for symlinks + if path, err = fs.RootPath(root, strings.TrimPrefix(path, root)); err != nil { + return err + } + if err := os.Chown(path, u, g); err != nil && !symlink { + return err + } + return nil + } +} diff --git a/spec_unix_test.go b/spec_unix_test.go index 394771ea3..bd23b77ea 100644 --- a/spec_unix_test.go +++ b/spec_unix_test.go @@ -20,7 +20,6 @@ func TestGenerateSpec(t *testing.T) { // check for matching caps defaults := defaltCaps() for _, cl := range [][]string{ - s.Process.Capabilities.Ambient, s.Process.Capabilities.Bounding, s.Process.Capabilities.Permitted, s.Process.Capabilities.Inheritable,