Skip to content
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

[v0.11] exec: add extra validation for submount sources #4640

Open
wants to merge 1 commit into
base: v0.11
Choose a base branch
from
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
30 changes: 18 additions & 12 deletions executor/oci/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/oci"
"github.com/containerd/containerd/pkg/userns"
"github.com/containerd/continuity/fs"
"github.com/docker/docker/pkg/idtools"
"github.com/mitchellh/hashstructure/v2"
"github.com/moby/buildkit/executor"
Expand Down Expand Up @@ -208,6 +207,7 @@ func GenerateSpec(ctx context.Context, meta executor.Meta, mounts []executor.Mou
type mountRef struct {
mount mount.Mount
unmount func() error
subRefs map[string]mountRef
}

type submounts struct {
Expand All @@ -226,10 +226,17 @@ func (s *submounts) subMount(m mount.Mount, subPath string) (mount.Mount, error)
return mount.Mount{}, nil
}
if mr, ok := s.m[h]; ok {
sm, err := sub(mr.mount, subPath)
if sm, ok := mr.subRefs[subPath]; ok {
return sm.mount, nil
}
sm, unmount, err := sub(mr.mount, subPath)
if err != nil {
return mount.Mount{}, nil
}
mr.subRefs[subPath] = mountRef{
mount: sm,
unmount: unmount,
}
return sm, nil
}

Expand All @@ -254,12 +261,17 @@ func (s *submounts) subMount(m mount.Mount, subPath string) (mount.Mount, error)
Options: opts,
},
unmount: lm.Unmount,
subRefs: map[string]mountRef{},
}

sm, err := sub(s.m[h].mount, subPath)
sm, unmount, err := sub(s.m[h].mount, subPath)
if err != nil {
return mount.Mount{}, err
}
s.m[h].subRefs[subPath] = mountRef{
mount: sm,
unmount: unmount,
}
return sm, nil
}

Expand All @@ -269,6 +281,9 @@ func (s *submounts) cleanup() {
for _, m := range s.m {
func(m mountRef) {
go func() {
for _, sm := range m.subRefs {
sm.unmount()
}
m.unmount()
wg.Done()
}()
Expand All @@ -277,15 +292,6 @@ func (s *submounts) cleanup() {
wg.Wait()
}

func sub(m mount.Mount, subPath string) (mount.Mount, error) {
src, err := fs.RootPath(m.Source, subPath)
if err != nil {
return mount.Mount{}, err
}
m.Source = src
return m, nil
}

func specMapping(s []idtools.IDMap) []specs.LinuxIDMapping {
var ids []specs.LinuxIDMapping
for _, item := range s {
Expand Down
57 changes: 57 additions & 0 deletions executor/oci/spec_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//go:build linux
// +build linux

package oci

import (
"os"
"strconv"

"github.com/containerd/containerd/mount"
"github.com/containerd/continuity/fs"
"github.com/moby/buildkit/snapshot"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)

func sub(m mount.Mount, subPath string) (mount.Mount, func() error, error) {
var retries = 10
root := m.Source
for {
src, err := fs.RootPath(root, subPath)
if err != nil {
return mount.Mount{}, nil, err
}
// similar to runc.WithProcfd
fh, err := os.OpenFile(src, unix.O_PATH|unix.O_CLOEXEC, 0)
if err != nil {
return mount.Mount{}, nil, err
}

fdPath := "/proc/self/fd/" + strconv.Itoa(int(fh.Fd()))
if resolved, err := os.Readlink(fdPath); err != nil {
fh.Close()
return mount.Mount{}, nil, err
} else if resolved != src {
retries--
if retries <= 0 {
fh.Close()
return mount.Mount{}, nil, errors.Errorf("unable to safely resolve subpath %s", subPath)
}
fh.Close()
continue
}

m.Source = fdPath
lm := snapshot.LocalMounterWithMounts([]mount.Mount{m}, snapshot.ForceRemount())
mp, err := lm.Mount()
if err != nil {
fh.Close()
return mount.Mount{}, nil, err
}
m.Source = mp
fh.Close() // release the fd, we don't need it anymore

return m, lm.Unmount, nil
}
}
18 changes: 18 additions & 0 deletions executor/oci/spec_non_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build !windows && !linux
// +build !windows,!linux

package oci

import (
"github.com/containerd/containerd/mount"
"github.com/containerd/continuity/fs"
)

func sub(m mount.Mount, subPath string) (mount.Mount, func() error, error) {
src, err := fs.RootPath(m.Source, subPath)
if err != nil {
return mount.Mount{}, nil, err
}
m.Source = src
return m, func() error { return nil }, nil
}
11 changes: 11 additions & 0 deletions executor/oci/spec_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
package oci

import (
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/oci"
"github.com/containerd/continuity/fs"
"github.com/docker/docker/pkg/idtools"
"github.com/moby/buildkit/solver/pb"
"github.com/pkg/errors"
Expand Down Expand Up @@ -43,3 +45,12 @@ func generateRlimitOpts(ulimits []*pb.Ulimit) ([]oci.SpecOpts, error) {
}
return nil, errors.New("no support for POSIXRlimit on Windows")
}

func sub(m mount.Mount, subPath string) (mount.Mount, func() error, error) {
src, err := fs.RootPath(m.Source, subPath)
if err != nil {
return mount.Mount{}, nil, err
}
m.Source = src
return m, func() error { return nil }, nil
}
35 changes: 26 additions & 9 deletions snapshot/localmounter.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,39 @@ type Mounter interface {
Unmount() error
}

type LocalMounterOpt func(*localMounter)

// LocalMounter is a helper for mounting mountfactory to temporary path. In
// addition it can mount binds without privileges
func LocalMounter(mountable Mountable) Mounter {
return &localMounter{mountable: mountable}
func LocalMounter(mountable Mountable, opts ...LocalMounterOpt) Mounter {
lm := &localMounter{mountable: mountable}
for _, opt := range opts {
opt(lm)
}
return lm
}

// LocalMounterWithMounts is a helper for mounting to temporary path. In
// addition it can mount binds without privileges
func LocalMounterWithMounts(mounts []mount.Mount) Mounter {
return &localMounter{mounts: mounts}
func LocalMounterWithMounts(mounts []mount.Mount, opts ...LocalMounterOpt) Mounter {
lm := &localMounter{mounts: mounts}
for _, opt := range opts {
opt(lm)
}
return lm
}

type localMounter struct {
mu sync.Mutex
mounts []mount.Mount
mountable Mountable
target string
release func() error
mu sync.Mutex
mounts []mount.Mount
mountable Mountable
target string
release func() error
forceRemount bool
}

func ForceRemount() LocalMounterOpt {
return func(lm *localMounter) {
lm.forceRemount = true
}
}
45 changes: 32 additions & 13 deletions snapshot/localmounter_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package snapshot

import (
"os"
"path/filepath"
"syscall"

"github.com/containerd/containerd/mount"
Expand Down Expand Up @@ -34,30 +35,48 @@ func (lm *localMounter) Mount() (string, error) {
}
}

var isFile bool
if len(lm.mounts) == 1 && (lm.mounts[0].Type == "bind" || lm.mounts[0].Type == "rbind") {
ro := false
for _, opt := range lm.mounts[0].Options {
if opt == "ro" {
ro = true
break
if !lm.forceRemount {
ro := false
for _, opt := range lm.mounts[0].Options {
if opt == "ro" {
ro = true
break
}
}
if !ro {
return lm.mounts[0].Source, nil
}
}
fi, err := os.Stat(lm.mounts[0].Source)
if err != nil {
return "", err
}
if !ro {
return lm.mounts[0].Source, nil
if !fi.IsDir() {
isFile = true
}
}

dir, err := os.MkdirTemp("", "buildkit-mount")
dest, err := os.MkdirTemp("", "buildkit-mount")
if err != nil {
return "", errors.Wrap(err, "failed to create temp dir")
}

if err := mount.All(lm.mounts, dir); err != nil {
os.RemoveAll(dir)
return "", errors.Wrapf(err, "failed to mount %s: %+v", dir, lm.mounts)
if isFile {
dest = filepath.Join(dest, "file")
if err := os.WriteFile(dest, []byte{}, 0644); err != nil {
os.RemoveAll(dest)
return "", errors.Wrap(err, "failed to create temp file")
}
}

if err := mount.All(lm.mounts, dest); err != nil {
os.RemoveAll(dest)
return "", errors.Wrapf(err, "failed to mount %s: %+v", dest, lm.mounts)
}
lm.target = dir
return dir, nil
lm.target = dest
return dest, nil
}

func (lm *localMounter) Unmount() error {
Expand Down