mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
Merge pull request #3985 from cyphar/idmap-generic
libcontainer: remove all mount logic from nsexec
This commit is contained in:
@@ -10,7 +10,7 @@ require (
|
||||
github.com/cyphar/filepath-securejoin v0.2.4
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
github.com/moby/sys/mountinfo v0.6.2
|
||||
github.com/moby/sys/mountinfo v0.7.1
|
||||
github.com/moby/sys/user v0.1.0
|
||||
github.com/mrunalp/fileutils v0.5.1
|
||||
github.com/opencontainers/runtime-spec v1.1.1-0.20230823135140-4fec88fd00a4
|
||||
|
||||
@@ -26,8 +26,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||
github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g=
|
||||
github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
|
||||
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
|
||||
github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm/Q=
|
||||
|
||||
@@ -26,14 +26,19 @@ func isEnabled() bool {
|
||||
}
|
||||
|
||||
func setProcAttr(attr, value string) error {
|
||||
// Under AppArmor you can only change your own attr, so use /proc/self/
|
||||
// instead of /proc/<tid>/ like libapparmor does
|
||||
attrPath := "/proc/self/attr/apparmor/" + attr
|
||||
if _, err := os.Stat(attrPath); errors.Is(err, os.ErrNotExist) {
|
||||
attr = utils.CleanPath(attr)
|
||||
attrSubPath := "attr/apparmor/" + attr
|
||||
if _, err := os.Stat("/proc/self/" + attrSubPath); errors.Is(err, os.ErrNotExist) {
|
||||
// fall back to the old convention
|
||||
attrPath = "/proc/self/attr/" + attr
|
||||
attrSubPath = "attr/" + attr
|
||||
}
|
||||
|
||||
// Under AppArmor you can only change your own attr, so there's no reason
|
||||
// to not use /proc/thread-self/ (instead of /proc/<tid>/, like libapparmor
|
||||
// does).
|
||||
attrPath, closer := utils.ProcThreadSelf(attrSubPath)
|
||||
defer closer()
|
||||
|
||||
f, err := os.OpenFile(attrPath, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
)
|
||||
|
||||
func TestParseCgroups(t *testing.T) {
|
||||
// We don't need to use /proc/thread-self here because runc always runs
|
||||
// with every thread in the same cgroup. This lets us avoid having to do
|
||||
// runtime.LockOSThread.
|
||||
cgroups, err := ParseCgroupFile("/proc/self/cgroup")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -136,13 +136,14 @@ func openFile(dir, file string, flags int) (*os.File, error) {
|
||||
//
|
||||
// TODO: if such usage will ever be common, amend this
|
||||
// to reopen cgroupFd and retry openat2.
|
||||
fdStr := strconv.Itoa(cgroupFd)
|
||||
fdDest, _ := os.Readlink("/proc/self/fd/" + fdStr)
|
||||
fdPath, closer := utils.ProcThreadSelf("fd/" + strconv.Itoa(cgroupFd))
|
||||
defer closer()
|
||||
fdDest, _ := os.Readlink(fdPath)
|
||||
if fdDest != cgroupfsDir {
|
||||
// Wrap the error so it is clear that cgroupFd
|
||||
// is opened to an unexpected/wrong directory.
|
||||
err = fmt.Errorf("cgroupFd %s unexpectedly opened to %s != %s: %w",
|
||||
fdStr, fdDest, cgroupfsDir, err)
|
||||
err = fmt.Errorf("cgroupFd %d unexpectedly opened to %s != %s: %w",
|
||||
cgroupFd, fdDest, cgroupfsDir, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@ func _defaultDirPath(root, cgPath, cgParent, cgName string) (string, error) {
|
||||
return filepath.Join(root, innerPath), nil
|
||||
}
|
||||
|
||||
// we don't need to use /proc/thread-self here because runc always runs
|
||||
// with every thread in the same cgroup. This lets us avoid having to do
|
||||
// runtime.LockOSThread.
|
||||
ownCgroup, err := parseCgroupFile("/proc/self/cgroup")
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -99,11 +99,12 @@ func tryDefaultPath(cgroupPath, subsystem string) string {
|
||||
// expensive), so it is assumed that cgroup mounts are not being changed.
|
||||
func readCgroupMountinfo() ([]*mountinfo.Info, error) {
|
||||
readMountinfoOnce.Do(func() {
|
||||
// mountinfo.GetMounts uses /proc/thread-self, so we can use it without
|
||||
// issues.
|
||||
cgroupMountinfo, readMountinfoErr = mountinfo.GetMounts(
|
||||
mountinfo.FSTypeFilter("cgroup"),
|
||||
)
|
||||
})
|
||||
|
||||
return cgroupMountinfo, readMountinfoErr
|
||||
}
|
||||
|
||||
@@ -196,6 +197,9 @@ func getCgroupMountsV1(all bool) ([]Mount, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We don't need to use /proc/thread-self here because runc always runs
|
||||
// with every thread in the same cgroup. This lets us avoid having to do
|
||||
// runtime.LockOSThread.
|
||||
allSubsystems, err := ParseCgroupFile("/proc/self/cgroup")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -214,6 +218,10 @@ func GetOwnCgroup(subsystem string) (string, error) {
|
||||
if IsCgroup2UnifiedMode() {
|
||||
return "", errUnified
|
||||
}
|
||||
|
||||
// We don't need to use /proc/thread-self here because runc always runs
|
||||
// with every thread in the same cgroup. This lets us avoid having to do
|
||||
// runtime.LockOSThread.
|
||||
cgroups, err := ParseCgroupFile("/proc/self/cgroup")
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -3,5 +3,5 @@ package configs
|
||||
const (
|
||||
// EXT_COPYUP is a directive to copy up the contents of a directory when
|
||||
// a tmpfs is mounted over it.
|
||||
EXT_COPYUP = 1 << iota //nolint:golint // ignore "don't use ALL_CAPS" warning
|
||||
EXT_COPYUP = 1 << iota //nolint:golint,revive // ignore "don't use ALL_CAPS" warning
|
||||
)
|
||||
|
||||
@@ -2,6 +2,24 @@ package configs
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
type MountIDMapping struct {
|
||||
// Recursive indicates if the mapping needs to be recursive.
|
||||
Recursive bool `json:"recursive"`
|
||||
|
||||
// UserNSPath is a path to a user namespace that indicates the necessary
|
||||
// id-mappings for MOUNT_ATTR_IDMAP. If set to non-"", UIDMappings and
|
||||
// GIDMappings must be set to nil.
|
||||
UserNSPath string `json:"userns_path,omitempty"`
|
||||
|
||||
// UIDMappings is the uid mapping set for this mount, to be used with
|
||||
// MOUNT_ATTR_IDMAP.
|
||||
UIDMappings []IDMap `json:"uid_mappings,omitempty"`
|
||||
|
||||
// GIDMappings is the gid mapping set for this mount, to be used with
|
||||
// MOUNT_ATTR_IDMAP.
|
||||
GIDMappings []IDMap `json:"gid_mappings,omitempty"`
|
||||
}
|
||||
|
||||
type Mount struct {
|
||||
// Source path for the mount.
|
||||
Source string `json:"source"`
|
||||
@@ -34,17 +52,9 @@ type Mount struct {
|
||||
// Extensions are additional flags that are specific to runc.
|
||||
Extensions int `json:"extensions"`
|
||||
|
||||
// UIDMappings is used to changing file user owners w/o calling chown.
|
||||
// Note that, the underlying filesystem should support this feature to be
|
||||
// used.
|
||||
// Every mount point could have its own mapping.
|
||||
UIDMappings []IDMap `json:"uid_mappings,omitempty"`
|
||||
|
||||
// GIDMappings is used to changing file group owners w/o calling chown.
|
||||
// Note that, the underlying filesystem should support this feature to be
|
||||
// used.
|
||||
// Every mount point could have its own mapping.
|
||||
GIDMappings []IDMap `json:"gid_mappings,omitempty"`
|
||||
// Mapping is the MOUNT_ATTR_IDMAP configuration for the mount. If non-nil,
|
||||
// the mount is configured to use MOUNT_ATTR_IDMAP-style id mappings.
|
||||
IDMapping *MountIDMapping `json:"id_mapping,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Mount) IsBind() bool {
|
||||
@@ -52,5 +62,5 @@ func (m *Mount) IsBind() bool {
|
||||
}
|
||||
|
||||
func (m *Mount) IsIDMapped() bool {
|
||||
return len(m.UIDMappings) > 0 || len(m.GIDMappings) > 0
|
||||
return m.IDMapping != nil
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ func IsNamespaceSupported(ns NamespaceType) bool {
|
||||
if nsFile == "" {
|
||||
return false
|
||||
}
|
||||
// We don't need to use /proc/thread-self here because the list of
|
||||
// namespace types is unrelated to the thread. This lets us avoid having to
|
||||
// do runtime.LockOSThread.
|
||||
_, err := os.Stat("/proc/self/ns/" + nsFile)
|
||||
// a namespace is supported if it exists and we have permissions to read it
|
||||
supported = err == nil
|
||||
|
||||
@@ -309,29 +309,32 @@ func checkBindOptions(m *configs.Mount) error {
|
||||
}
|
||||
|
||||
func checkIDMapMounts(config *configs.Config, m *configs.Mount) error {
|
||||
// Make sure MOUNT_ATTR_IDMAP is not set on any of our mounts. This
|
||||
// attribute is handled differently to all other attributes (through
|
||||
// m.IDMapping), so make sure we never store it in the actual config. This
|
||||
// really shouldn't ever happen.
|
||||
if m.RecAttr != nil && (m.RecAttr.Attr_set|m.RecAttr.Attr_clr)&unix.MOUNT_ATTR_IDMAP != 0 {
|
||||
return errors.New("mount configuration cannot contain recAttr for MOUNT_ATTR_IDMAP")
|
||||
}
|
||||
if !m.IsIDMapped() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !m.IsBind() {
|
||||
return fmt.Errorf("gidMappings/uidMappings is supported only for mounts with the option 'bind'")
|
||||
return errors.New("id-mapped mounts are only supported for bind-mounts")
|
||||
}
|
||||
if config.RootlessEUID {
|
||||
return fmt.Errorf("gidMappings/uidMappings is not supported when runc is being launched with EUID != 0, needs CAP_SYS_ADMIN on the runc parent's user namespace")
|
||||
return errors.New("id-mapped mounts are not supported for rootless containers")
|
||||
}
|
||||
if len(config.UIDMappings) == 0 || len(config.GIDMappings) == 0 {
|
||||
return fmt.Errorf("not yet supported to use gidMappings/uidMappings in a mount without also using a user namespace")
|
||||
if m.IDMapping.UserNSPath == "" {
|
||||
if len(m.IDMapping.UIDMappings) == 0 || len(m.IDMapping.GIDMappings) == 0 {
|
||||
return errors.New("id-mapped mounts must have both uid and gid mappings specified")
|
||||
}
|
||||
} else {
|
||||
if m.IDMapping.UIDMappings != nil || m.IDMapping.GIDMappings != nil {
|
||||
// should never happen
|
||||
return errors.New("[internal error] id-mapped mounts cannot have both userns_path and uid and gid mappings specified")
|
||||
}
|
||||
}
|
||||
if !sameMapping(config.UIDMappings, m.UIDMappings) {
|
||||
return fmt.Errorf("not yet supported for the mount uidMappings to be different than user namespace uidMapping")
|
||||
}
|
||||
if !sameMapping(config.GIDMappings, m.GIDMappings) {
|
||||
return fmt.Errorf("not yet supported for the mount gidMappings to be different than user namespace gidMapping")
|
||||
}
|
||||
if !filepath.IsAbs(m.Source) {
|
||||
return fmt.Errorf("mount source not absolute")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -356,27 +359,6 @@ func mountsStrict(config *configs.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sameMapping checks if the mappings are the same. If the mappings are the same
|
||||
// but in different order, it returns false.
|
||||
func sameMapping(a, b []configs.IDMap) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a {
|
||||
if a[i].ContainerID != b[i].ContainerID {
|
||||
return false
|
||||
}
|
||||
if a[i].HostID != b[i].HostID {
|
||||
return false
|
||||
}
|
||||
if a[i].Size != b[i].Size {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isHostNetNS(path string) (bool, error) {
|
||||
const currentProcessNetns = "/proc/self/ns/net"
|
||||
|
||||
|
||||
@@ -504,17 +504,82 @@ func TestValidateIDMapMounts(t *testing.T) {
|
||||
config *configs.Config
|
||||
}{
|
||||
{
|
||||
name: "idmap mount without bind opt specified",
|
||||
name: "idmap non-bind mount",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/dev/sda1",
|
||||
Destination: "/abs/path/",
|
||||
Device: "ext4",
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap option non-bind mount",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/dev/sda1",
|
||||
Destination: "/abs/path/",
|
||||
Device: "ext4",
|
||||
IDMapping: &configs.MountIDMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ridmap option non-bind mount",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/dev/sda1",
|
||||
Destination: "/abs/path/",
|
||||
Device: "ext4",
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
Recursive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap mount no uid mapping",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
Flags: unix.MS_BIND,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap mount no gid mapping",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -531,76 +596,16 @@ func TestValidateIDMapMounts(t *testing.T) {
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap mount without userns mappings",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap mounts with different userns and mount mappings",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
UIDMappings: []configs.IDMap{
|
||||
{
|
||||
ContainerID: 10,
|
||||
HostID: 10,
|
||||
Size: 1,
|
||||
},
|
||||
},
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap mounts with different userns and mount mappings",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: []configs.IDMap{
|
||||
{
|
||||
ContainerID: 10,
|
||||
HostID: 10,
|
||||
Size: 1,
|
||||
},
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap mounts without abs source path",
|
||||
isErr: true,
|
||||
name: "idmap mounts without abs source path",
|
||||
config: &configs.Config{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
@@ -609,8 +614,10 @@ func TestValidateIDMapMounts(t *testing.T) {
|
||||
Source: "./rel/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -625,8 +632,10 @@ func TestValidateIDMapMounts(t *testing.T) {
|
||||
Source: "/abs/path/",
|
||||
Destination: "./rel/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -641,8 +650,10 @@ func TestValidateIDMapMounts(t *testing.T) {
|
||||
Source: "/another-abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -657,8 +668,104 @@ func TestValidateIDMapMounts(t *testing.T) {
|
||||
Source: "/another-abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND | unix.MS_RDONLY,
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap mount without userns mappings",
|
||||
config: &configs.Config{
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap mounts with different userns and mount mappings",
|
||||
config: &configs.Config{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: []configs.IDMap{
|
||||
{
|
||||
ContainerID: 10,
|
||||
HostID: 10,
|
||||
Size: 1,
|
||||
},
|
||||
},
|
||||
GIDMappings: mapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idmap mounts with different userns and mount mappings",
|
||||
config: &configs.Config{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: mapping,
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
UIDMappings: mapping,
|
||||
GIDMappings: []configs.IDMap{
|
||||
{
|
||||
ContainerID: 10,
|
||||
HostID: 10,
|
||||
Size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mount with 'idmap' option but no mappings",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
IDMapping: &configs.MountIDMapping{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mount with 'ridmap' option but no mappings",
|
||||
isErr: true,
|
||||
config: &configs.Config{
|
||||
Mounts: []*configs.Mount{
|
||||
{
|
||||
Source: "/abs/path/",
|
||||
Destination: "/abs/path/",
|
||||
Flags: unix.MS_BIND,
|
||||
IDMapping: &configs.MountIDMapping{
|
||||
Recursive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+11
-160
@@ -2,7 +2,6 @@ package libcontainer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -510,14 +509,20 @@ func (c *Container) newParentProcess(p *Process) (parentProcess, error) {
|
||||
if dmz.IsSelfExeCloned() {
|
||||
// /proc/self/exe is already a cloned binary -- no need to do anything
|
||||
logrus.Debug("skipping binary cloning -- /proc/self/exe is already cloned!")
|
||||
// We don't need to use /proc/thread-self here because the exe mm of a
|
||||
// thread-group is guaranteed to be the same for all threads by
|
||||
// definition. This lets us avoid having to do runtime.LockOSThread.
|
||||
exePath = "/proc/self/exe"
|
||||
} else {
|
||||
var err error
|
||||
if isDmzBinarySafe(c.config) {
|
||||
dmzExe, err = dmz.Binary(c.stateDir)
|
||||
if err == nil {
|
||||
// We can use our own executable without cloning if we are using
|
||||
// runc-dmz.
|
||||
// We can use our own executable without cloning if we are
|
||||
// using runc-dmz. We don't need to use /proc/thread-self here
|
||||
// because the exe mm of a thread-group is guaranteed to be the
|
||||
// same for all threads by definition. This lets us avoid
|
||||
// having to do runtime.LockOSThread.
|
||||
exePath = "/proc/self/exe"
|
||||
p.clonedExes = append(p.clonedExes, dmzExe)
|
||||
logrus.Debug("runc-dmz: using runc-dmz") // used for tests
|
||||
@@ -629,112 +634,6 @@ func (c *Container) newParentProcess(p *Process) (parentProcess, error) {
|
||||
return c.newSetnsProcess(p, cmd, comm)
|
||||
}
|
||||
|
||||
// shouldSendMountSources says whether the child process must setup bind mounts with
|
||||
// the source pre-opened (O_PATH) in the host user namespace.
|
||||
// See https://github.com/opencontainers/runc/issues/2484
|
||||
func (c *Container) shouldSendMountSources() bool {
|
||||
// Passing the mount sources via SCM_RIGHTS is only necessary when
|
||||
// both userns and mntns are active.
|
||||
if !c.config.Namespaces.Contains(configs.NEWUSER) ||
|
||||
!c.config.Namespaces.Contains(configs.NEWNS) {
|
||||
return false
|
||||
}
|
||||
|
||||
// nsexec.c send_mountsources() requires setns(mntns) capabilities
|
||||
// CAP_SYS_CHROOT and CAP_SYS_ADMIN.
|
||||
if c.config.RootlessEUID {
|
||||
return false
|
||||
}
|
||||
|
||||
// We need to send sources if there are non-idmap bind-mounts.
|
||||
for _, m := range c.config.Mounts {
|
||||
if m.IsBind() && !m.IsIDMapped() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// shouldSendIdmapSources says whether the child process must setup idmap mounts with
|
||||
// the mount_setattr already done in the host user namespace.
|
||||
func (c *Container) shouldSendIdmapSources() bool {
|
||||
// nsexec.c mount_setattr() requires CAP_SYS_ADMIN in:
|
||||
// * the user namespace the filesystem was mounted in;
|
||||
// * the user namespace we're trying to idmap the mount to;
|
||||
// * the owning user namespace of the mount namespace you're currently located in.
|
||||
//
|
||||
// See the comment from Christian Brauner:
|
||||
// https://github.com/opencontainers/runc/pull/3717#discussion_r1103607972
|
||||
//
|
||||
// Let's just rule out rootless, we don't have those permission in the
|
||||
// rootless case.
|
||||
if c.config.RootlessEUID {
|
||||
return false
|
||||
}
|
||||
|
||||
// For the time being we require userns to be in use.
|
||||
if !c.config.Namespaces.Contains(configs.NEWUSER) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We need to send sources if there are idmap bind-mounts.
|
||||
for _, m := range c.config.Mounts {
|
||||
if m.IsBind() && m.IsIDMapped() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Container) sendMountSources(cmd *exec.Cmd, comm *processComm) error {
|
||||
if !c.shouldSendMountSources() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.sendFdsSources(cmd, comm, "_LIBCONTAINER_MOUNT_FDS", func(m *configs.Mount) bool {
|
||||
return m.IsBind() && !m.IsIDMapped()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) sendIdmapSources(cmd *exec.Cmd, comm *processComm) error {
|
||||
if !c.shouldSendIdmapSources() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.sendFdsSources(cmd, comm, "_LIBCONTAINER_IDMAP_FDS", func(m *configs.Mount) bool {
|
||||
return m.IsBind() && m.IsIDMapped()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) sendFdsSources(cmd *exec.Cmd, comm *processComm, envVar string, condition func(*configs.Mount) bool) error {
|
||||
// Elements on these slices will be paired with mounts (see StartInitialization() and
|
||||
// prepareRootfs()). These slices MUST have the same size as c.config.Mounts.
|
||||
fds := make([]int, len(c.config.Mounts))
|
||||
for i, m := range c.config.Mounts {
|
||||
if !condition(m) {
|
||||
// The -1 fd is ignored later.
|
||||
fds[i] = -1
|
||||
continue
|
||||
}
|
||||
|
||||
// The fd passed here will not be used: nsexec.c will overwrite it with
|
||||
// dup3(). We just need to allocate a fd so that we know the number to
|
||||
// pass in the environment variable. The fd must not be closed before
|
||||
// cmd.Start(), so we reuse initSockChild because the lifecycle of that
|
||||
// fd is already taken care of.
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, comm.initSockChild)
|
||||
fds[i] = stdioFdCount + len(cmd.ExtraFiles) - 1
|
||||
}
|
||||
fdsJSON, err := json.Marshal(fds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating %v: %w", envVar, err)
|
||||
}
|
||||
cmd.Env = append(cmd.Env, envVar+"="+string(fdsJSON))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Container) newInitProcess(p *Process, cmd *exec.Cmd, comm *processComm) (*initProcess, error) {
|
||||
cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initStandard))
|
||||
nsMaps := make(map[configs.NamespaceType]string)
|
||||
@@ -743,16 +642,10 @@ func (c *Container) newInitProcess(p *Process, cmd *exec.Cmd, comm *processComm)
|
||||
nsMaps[ns.Type] = ns.Path
|
||||
}
|
||||
}
|
||||
data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps, initStandard)
|
||||
data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.sendMountSources(cmd, comm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.sendIdmapSources(cmd, comm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
init := &initProcess{
|
||||
cmd: cmd,
|
||||
@@ -776,7 +669,7 @@ func (c *Container) newSetnsProcess(p *Process, cmd *exec.Cmd, comm *processComm
|
||||
}
|
||||
// for setns process, we don't have to set cloneflags as the process namespaces
|
||||
// will only be set via setns syscall
|
||||
data, err := c.bootstrapData(0, state.NamespacePaths, initSetns)
|
||||
data, err := c.bootstrapData(0, state.NamespacePaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1165,7 +1058,7 @@ type netlinkError struct{ error }
|
||||
// such as one that uses nsenter package to bootstrap the container's
|
||||
// init process correctly, i.e. with correct namespaces, uid/gid
|
||||
// mapping etc.
|
||||
func (c *Container) bootstrapData(cloneFlags uintptr, nsMaps map[configs.NamespaceType]string, it initType) (_ io.Reader, Err error) {
|
||||
func (c *Container) bootstrapData(cloneFlags uintptr, nsMaps map[configs.NamespaceType]string) (_ io.Reader, Err error) {
|
||||
// create the netlink message
|
||||
r := nl.NewNetlinkRequest(int(InitMsg), 0)
|
||||
|
||||
@@ -1267,48 +1160,6 @@ func (c *Container) bootstrapData(cloneFlags uintptr, nsMaps map[configs.Namespa
|
||||
Value: c.config.RootlessEUID,
|
||||
})
|
||||
|
||||
// Bind mount source to open.
|
||||
if it == initStandard && c.shouldSendMountSources() {
|
||||
var mounts []byte
|
||||
for _, m := range c.config.Mounts {
|
||||
if m.IsBind() && !m.IsIDMapped() {
|
||||
if strings.IndexByte(m.Source, 0) >= 0 {
|
||||
return nil, fmt.Errorf("mount source string contains null byte: %q", m.Source)
|
||||
}
|
||||
mounts = append(mounts, []byte(m.Source)...)
|
||||
}
|
||||
mounts = append(mounts, byte(0))
|
||||
}
|
||||
|
||||
r.AddData(&Bytemsg{
|
||||
Type: MountSourcesAttr,
|
||||
Value: mounts,
|
||||
})
|
||||
}
|
||||
|
||||
// Idmap mount sources to open.
|
||||
if it == initStandard && c.shouldSendIdmapSources() {
|
||||
var mounts []byte
|
||||
for _, m := range c.config.Mounts {
|
||||
if m.IsBind() && m.IsIDMapped() {
|
||||
// While other parts of the code check this too (like
|
||||
// libcontainer/specconv/spec_linux.go) we do it here also because some libcontainer
|
||||
// users don't use those functions.
|
||||
if strings.IndexByte(m.Source, 0) >= 0 {
|
||||
return nil, fmt.Errorf("mount source string contains null byte: %q", m.Source)
|
||||
}
|
||||
|
||||
mounts = append(mounts, []byte(m.Source)...)
|
||||
}
|
||||
mounts = append(mounts, byte(0))
|
||||
}
|
||||
|
||||
r.AddData(&Bytemsg{
|
||||
Type: IdmapSourcesAttr,
|
||||
Value: mounts,
|
||||
})
|
||||
}
|
||||
|
||||
// write boottime and monotonic time ns offsets.
|
||||
if c.config.TimeOffsets != nil {
|
||||
var offsetSpec bytes.Buffer
|
||||
|
||||
+20
-13
@@ -526,6 +526,15 @@ func (c *Container) restoreNetwork(req *criurpc.CriuReq, criuOpts *CriuOpts) {
|
||||
// restore using CRIU. This function is inspired from the code in
|
||||
// rootfs_linux.go.
|
||||
func (c *Container) makeCriuRestoreMountpoints(m *configs.Mount) error {
|
||||
me := mountEntry{Mount: m}
|
||||
dest, err := securejoin.SecureJoin(c.config.Rootfs, m.Destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: pass srcFD? Not sure if criu is impacted by issue #2484.
|
||||
if err := checkProcMount(c.config.Rootfs, dest, me); err != nil {
|
||||
return err
|
||||
}
|
||||
switch m.Device {
|
||||
case "cgroup":
|
||||
// No mount point(s) need to be created:
|
||||
@@ -537,21 +546,19 @@ func (c *Container) makeCriuRestoreMountpoints(m *configs.Mount) error {
|
||||
// the mountpoint appears as soon as /sys is mounted
|
||||
return nil
|
||||
case "bind":
|
||||
// The prepareBindMount() function checks if source
|
||||
// exists. So it cannot be used for other filesystem types.
|
||||
// TODO: pass srcFD? Not sure if criu is impacted by issue #2484.
|
||||
if err := prepareBindMount(mountEntry{Mount: m}, c.config.Rootfs); err != nil {
|
||||
// For bind-mounts (unlike other filesystem types), we need to check if
|
||||
// the source exists.
|
||||
fi, _, err := me.srcStat()
|
||||
if err != nil {
|
||||
// error out if the source of a bind mount does not exist as we
|
||||
// will be unable to bind anything to it.
|
||||
return err
|
||||
}
|
||||
if err := createIfNotExists(dest, fi.IsDir()); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// for all other filesystems just create the mountpoints
|
||||
dest, err := securejoin.SecureJoin(c.config.Rootfs, m.Destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkProcMount(c.config.Rootfs, dest, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(dest, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -618,8 +625,8 @@ func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error {
|
||||
// because during initial container creation mounts are
|
||||
// set up in the order they are configured.
|
||||
if m.Device == "bind" {
|
||||
if err := utils.WithProcfd(c.config.Rootfs, m.Destination, func(dstFD string) error {
|
||||
return mountViaFDs(m.Source, nil, m.Destination, dstFD, "", unix.MS_BIND|unix.MS_REC, "")
|
||||
if err := utils.WithProcfd(c.config.Rootfs, m.Destination, func(dstFd string) error {
|
||||
return mountViaFds(m.Source, nil, m.Destination, dstFd, "", unix.MS_BIND|unix.MS_REC, "")
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -214,18 +214,3 @@ func validateID(id string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFdsFromEnv(envVar string) ([]int, error) {
|
||||
fdsJSON := os.Getenv(envVar)
|
||||
if fdsJSON == "" {
|
||||
// Always return the nil slice if no fd is present.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var fds []int
|
||||
if err := json.Unmarshal([]byte(fdsJSON), &fds); err != nil {
|
||||
return nil, fmt.Errorf("Error unmarshalling %v: %w", envVar, err)
|
||||
}
|
||||
|
||||
return fds, nil
|
||||
}
|
||||
|
||||
@@ -47,18 +47,6 @@ type network struct {
|
||||
TempVethPeerName string `json:"temp_veth_peer_name"`
|
||||
}
|
||||
|
||||
type mountFds struct {
|
||||
// sourceFds are the fds to use as source when mounting.
|
||||
// The slice size should be the same as container mounts, as it will be
|
||||
// paired with them.
|
||||
// The value -1 is used when no fd is needed for the mount.
|
||||
// Can't have a valid fd in the same position that other slices in this struct.
|
||||
// We need to use only one of these fds on any single mount.
|
||||
sourceFds []int
|
||||
// Idem sourceFds, but fds of already created idmap mounts, to use with unix.MoveMount().
|
||||
idmapFds []int
|
||||
}
|
||||
|
||||
// initConfig is used for transferring parameters from Exec() to Init()
|
||||
type initConfig struct {
|
||||
Args []string `json:"args"`
|
||||
@@ -189,18 +177,6 @@ func startInitialization() (retErr error) {
|
||||
defer pidfdSocket.Close()
|
||||
}
|
||||
|
||||
// Get mount files (O_PATH).
|
||||
mountSrcFds, err := parseFdsFromEnv("_LIBCONTAINER_MOUNT_FDS")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get idmap fds.
|
||||
idmapFds, err := parseFdsFromEnv("_LIBCONTAINER_IDMAP_FDS")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get runc-dmz fds.
|
||||
var dmzExe *os.File
|
||||
if dmzFdStr := os.Getenv("_LIBCONTAINER_DMZEXEFD"); dmzFdStr != "" {
|
||||
@@ -232,21 +208,16 @@ func startInitialization() (retErr error) {
|
||||
}
|
||||
|
||||
// If init succeeds, it will not return, hence none of the defers will be called.
|
||||
return containerInit(it, &config, syncPipe, consoleSocket, pidfdSocket, fifofd, logFD, dmzExe, mountFds{sourceFds: mountSrcFds, idmapFds: idmapFds})
|
||||
return containerInit(it, &config, syncPipe, consoleSocket, pidfdSocket, fifofd, logFD, dmzExe)
|
||||
}
|
||||
|
||||
func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSocket, pidfdSocket *os.File, fifoFd, logFd int, dmzExe *os.File, mountFds mountFds) error {
|
||||
func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSocket, pidfdSocket *os.File, fifoFd, logFd int, dmzExe *os.File) error {
|
||||
if err := populateProcessEnvironment(config.Env); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch t {
|
||||
case initSetns:
|
||||
// mount and idmap fds must be nil in this case. We don't mount while doing runc exec.
|
||||
if mountFds.sourceFds != nil || mountFds.idmapFds != nil {
|
||||
return errors.New("mount and idmap fds must be nil; can't mount from exec")
|
||||
}
|
||||
|
||||
i := &linuxSetnsInit{
|
||||
pipe: pipe,
|
||||
consoleSocket: consoleSocket,
|
||||
@@ -266,7 +237,6 @@ func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSock
|
||||
fifoFd: fifoFd,
|
||||
logFd: logFd,
|
||||
dmzExe: dmzExe,
|
||||
mountFds: mountFds,
|
||||
}
|
||||
return i.Init()
|
||||
}
|
||||
@@ -510,6 +480,9 @@ func setupUser(config *initConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// We don't need to use /proc/thread-self here because setgroups is a
|
||||
// per-userns file and thus is global to all threads in a thread-group.
|
||||
// This lets us avoid having to do runtime.LockOSThread.
|
||||
setgroups, err := os.ReadFile("/proc/self/setgroups")
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/userns"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -1691,6 +1692,20 @@ func TestFdLeaksSystemd(t *testing.T) {
|
||||
testFdLeaks(t, true)
|
||||
}
|
||||
|
||||
func fdList(t *testing.T) []string {
|
||||
procSelfFd, closer := utils.ProcThreadSelf("fd")
|
||||
defer closer()
|
||||
|
||||
fdDir, err := os.Open(procSelfFd)
|
||||
ok(t, err)
|
||||
defer fdDir.Close()
|
||||
|
||||
fds, err := fdDir.Readdirnames(-1)
|
||||
ok(t, err)
|
||||
|
||||
return fds
|
||||
}
|
||||
|
||||
func testFdLeaks(t *testing.T, systemd bool) {
|
||||
if testing.Short() {
|
||||
return
|
||||
@@ -1705,21 +1720,12 @@ func testFdLeaks(t *testing.T, systemd bool) {
|
||||
// - /sys/fs/cgroup dirfd opened by prepareOpenat2 in libct/cgroups;
|
||||
// - dbus connection opened by getConnection in libct/cgroups/systemd.
|
||||
_ = runContainerOk(t, config, "true")
|
||||
|
||||
pfd, err := os.Open("/proc/self/fd")
|
||||
ok(t, err)
|
||||
defer pfd.Close()
|
||||
fds0, err := pfd.Readdirnames(0)
|
||||
ok(t, err)
|
||||
_, err = pfd.Seek(0, 0)
|
||||
ok(t, err)
|
||||
fds0 := fdList(t)
|
||||
|
||||
_ = runContainerOk(t, config, "true")
|
||||
fds1 := fdList(t)
|
||||
|
||||
fds1, err := pfd.Readdirnames(0)
|
||||
ok(t, err)
|
||||
|
||||
if len(fds1) == len(fds0) {
|
||||
if reflect.DeepEqual(fds0, fds1) {
|
||||
return
|
||||
}
|
||||
// Show the extra opened files.
|
||||
@@ -1729,6 +1735,10 @@ func testFdLeaks(t *testing.T, systemd bool) {
|
||||
}
|
||||
|
||||
count := 0
|
||||
|
||||
procSelfFd, closer := utils.ProcThreadSelf("fd/")
|
||||
defer closer()
|
||||
|
||||
next_fd:
|
||||
for _, fd1 := range fds1 {
|
||||
for _, fd0 := range fds0 {
|
||||
@@ -1736,7 +1746,7 @@ next_fd:
|
||||
continue next_fd
|
||||
}
|
||||
}
|
||||
dst, _ := os.Readlink("/proc/self/fd/" + fd1)
|
||||
dst, _ := os.Readlink(filepath.Join(procSelfFd, fd1))
|
||||
for _, ex := range excludedPaths {
|
||||
if ex == dst {
|
||||
continue next_fd
|
||||
|
||||
@@ -21,9 +21,7 @@ const (
|
||||
RootlessEUIDAttr uint16 = 27287
|
||||
UidmapPathAttr uint16 = 27288
|
||||
GidmapPathAttr uint16 = 27289
|
||||
MountSourcesAttr uint16 = 27290
|
||||
IdmapSourcesAttr uint16 = 27291
|
||||
TimeOffsetsAttr uint16 = 27292
|
||||
TimeOffsetsAttr uint16 = 27290
|
||||
)
|
||||
|
||||
type Int32msg struct {
|
||||
|
||||
+189
-37
@@ -1,22 +1,48 @@
|
||||
package libcontainer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/userns"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
)
|
||||
|
||||
// mountSourceType indicates what type of file descriptor is being returned. It
|
||||
// is used to tell rootfs_linux.go whether or not to use move_mount(2) to
|
||||
// install the mount.
|
||||
type mountSourceType string
|
||||
|
||||
const (
|
||||
// An open_tree(2)-style file descriptor that needs to be installed using
|
||||
// move_mount(2) to install.
|
||||
mountSourceOpenTree mountSourceType = "open_tree"
|
||||
// A plain file descriptor that can be mounted through /proc/thread-self/fd.
|
||||
mountSourcePlain mountSourceType = "plain-open"
|
||||
)
|
||||
|
||||
type mountSource struct {
|
||||
Type mountSourceType `json:"type"`
|
||||
file *os.File `json:"-"`
|
||||
}
|
||||
|
||||
// mountError holds an error from a failed mount or unmount operation.
|
||||
type mountError struct {
|
||||
op string
|
||||
source string
|
||||
srcFD *int
|
||||
target string
|
||||
dstFD string
|
||||
flags uintptr
|
||||
data string
|
||||
err error
|
||||
op string
|
||||
source string
|
||||
srcFile *mountSource
|
||||
target string
|
||||
dstFd string
|
||||
flags uintptr
|
||||
data string
|
||||
err error
|
||||
}
|
||||
|
||||
// Error provides a string error representation.
|
||||
@@ -25,13 +51,14 @@ func (e *mountError) Error() string {
|
||||
|
||||
if e.source != "" {
|
||||
out += "src=" + e.source + ", "
|
||||
if e.srcFD != nil {
|
||||
out += "srcFD=" + strconv.Itoa(*e.srcFD) + ", "
|
||||
if e.srcFile != nil {
|
||||
out += "srcType=" + string(e.srcFile.Type) + ", "
|
||||
out += "srcFd=" + strconv.Itoa(int(e.srcFile.file.Fd())) + ", "
|
||||
}
|
||||
}
|
||||
out += "dst=" + e.target
|
||||
if e.dstFD != "" {
|
||||
out += ", dstFD=" + e.dstFD
|
||||
if e.dstFd != "" {
|
||||
out += ", dstFd=" + e.dstFd
|
||||
}
|
||||
|
||||
if e.flags != uintptr(0) {
|
||||
@@ -54,38 +81,69 @@ func (e *mountError) Unwrap() error {
|
||||
// mount is a simple unix.Mount wrapper, returning an error with more context
|
||||
// in case it failed.
|
||||
func mount(source, target, fstype string, flags uintptr, data string) error {
|
||||
return mountViaFDs(source, nil, target, "", fstype, flags, data)
|
||||
return mountViaFds(source, nil, target, "", fstype, flags, data)
|
||||
}
|
||||
|
||||
// mountViaFDs is a unix.Mount wrapper which uses srcFD instead of source,
|
||||
// and dstFD instead of target, unless those are empty.
|
||||
// If srcFD is different than nil, its path (i.e. "/proc/self/fd/NN") will be
|
||||
// constructed by this function.
|
||||
// dstFD argument, if non-empty, is expected to be in the form of a path to an
|
||||
// opened file descriptor on procfs (i.e. "/proc/self/fd/NN").
|
||||
// mountViaFds is a unix.Mount wrapper which uses srcFile instead of source,
|
||||
// and dstFd instead of target, unless those are empty.
|
||||
//
|
||||
// If case an FD is used instead of a source or a target path, the
|
||||
// corresponding path is only used to add context to an error in case
|
||||
// the mount operation has failed.
|
||||
func mountViaFDs(source string, srcFD *int, target, dstFD, fstype string, flags uintptr, data string) error {
|
||||
src := source
|
||||
if srcFD != nil {
|
||||
src = "/proc/self/fd/" + strconv.Itoa(*srcFD)
|
||||
// If srcFile is non-nil and flags does not contain MS_REMOUNT, mountViaFds
|
||||
// will mount it according to the mountSourceType of the file descriptor.
|
||||
//
|
||||
// The dstFd argument, if non-empty, is expected to be in the form of a path to
|
||||
// an opened file descriptor on procfs (i.e. "/proc/thread-self/fd/NN").
|
||||
//
|
||||
// If a file descriptor is used instead of a source or a target path, the
|
||||
// corresponding path is only used to add context to an error in case the mount
|
||||
// operation has failed.
|
||||
func mountViaFds(source string, srcFile *mountSource, target, dstFd, fstype string, flags uintptr, data string) error {
|
||||
// MS_REMOUNT and srcFile don't make sense together.
|
||||
if srcFile != nil && flags&unix.MS_REMOUNT != 0 {
|
||||
logrus.Debugf("mount source passed along with MS_REMOUNT -- ignoring srcFile")
|
||||
srcFile = nil
|
||||
}
|
||||
dst := target
|
||||
if dstFD != "" {
|
||||
dst = dstFD
|
||||
if dstFd != "" {
|
||||
dst = dstFd
|
||||
}
|
||||
if err := unix.Mount(src, dst, fstype, flags, data); err != nil {
|
||||
src := source
|
||||
isMoveMount := srcFile != nil && srcFile.Type == mountSourceOpenTree
|
||||
if srcFile != nil {
|
||||
// If we're going to use the /proc/thread-self/... path for classic
|
||||
// mount(2), we need to get a safe handle to /proc/thread-self. This
|
||||
// isn't needed for move_mount(2) because in that case the path is just
|
||||
// a dummy string used for error info.
|
||||
fdStr := strconv.Itoa(int(srcFile.file.Fd()))
|
||||
if isMoveMount {
|
||||
src = "/proc/self/fd/" + fdStr
|
||||
} else {
|
||||
var closer utils.ProcThreadSelfCloser
|
||||
src, closer = utils.ProcThreadSelf("fd/" + fdStr)
|
||||
defer closer()
|
||||
}
|
||||
}
|
||||
|
||||
var op string
|
||||
var err error
|
||||
if isMoveMount {
|
||||
op = "move_mount"
|
||||
err = unix.MoveMount(int(srcFile.file.Fd()), "",
|
||||
unix.AT_FDCWD, dstFd,
|
||||
unix.MOVE_MOUNT_F_EMPTY_PATH|unix.MOVE_MOUNT_T_SYMLINKS)
|
||||
} else {
|
||||
op = "mount"
|
||||
err = unix.Mount(src, dst, fstype, flags, data)
|
||||
}
|
||||
if err != nil {
|
||||
return &mountError{
|
||||
op: "mount",
|
||||
source: source,
|
||||
srcFD: srcFD,
|
||||
target: target,
|
||||
dstFD: dstFD,
|
||||
flags: flags,
|
||||
data: data,
|
||||
err: err,
|
||||
op: op,
|
||||
source: source,
|
||||
srcFile: srcFile,
|
||||
target: target,
|
||||
dstFd: dstFd,
|
||||
flags: flags,
|
||||
data: data,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -121,3 +179,97 @@ func syscallMode(i fs.FileMode) (o uint32) {
|
||||
// No mapping for Go's ModeTemporary (plan9 only).
|
||||
return
|
||||
}
|
||||
|
||||
// mountFd creates a "mount source fd" (either through open_tree(2) or just
|
||||
// open(O_PATH)) based on the provided configuration. This function must be
|
||||
// called from within the container's mount namespace.
|
||||
//
|
||||
// In the case of idmapped mount configurations, the returned mount source will
|
||||
// be an open_tree(2) file with MOUNT_ATTR_IDMAP applied. For other
|
||||
// bind-mounts, it will be an O_PATH. If the type of mount cannot be handled,
|
||||
// the returned mountSource will be nil, indicating that the container init
|
||||
// process will need to do an old-fashioned mount(2) themselves.
|
||||
//
|
||||
// This helper is only intended to be used by goCreateMountSources.
|
||||
func mountFd(nsHandles *userns.Handles, m *configs.Mount) (*mountSource, error) {
|
||||
if !m.IsBind() {
|
||||
return nil, errors.New("new mount api: only bind-mounts are supported")
|
||||
}
|
||||
if nsHandles == nil {
|
||||
nsHandles = new(userns.Handles)
|
||||
defer nsHandles.Release()
|
||||
}
|
||||
|
||||
var mountFile *os.File
|
||||
var sourceType mountSourceType
|
||||
|
||||
// Ideally, we would use OPEN_TREE_CLONE for everything, because we can
|
||||
// be sure that the file descriptor cannot be used to escape outside of
|
||||
// the mount root. Unfortunately, OPEN_TREE_CLONE is far more expensive
|
||||
// than open(2) because it requires doing mounts inside a new anonymous
|
||||
// mount namespace. So we use open(2) for standard bind-mounts, and
|
||||
// OPEN_TREE_CLONE when we need to set mount attributes here.
|
||||
//
|
||||
// While passing open(2)'d paths from the host rootfs isn't exactly the
|
||||
// safest thing in the world, the files will not survive across
|
||||
// execve(2) and "runc init" is non-dumpable so it should not be
|
||||
// possible for a malicious container process to gain access to the
|
||||
// file descriptors. We also don't do any of this for "runc exec",
|
||||
// lessening the risk even further.
|
||||
if m.IsIDMapped() {
|
||||
flags := uint(unix.OPEN_TREE_CLONE | unix.OPEN_TREE_CLOEXEC)
|
||||
if m.Flags&unix.MS_REC == unix.MS_REC {
|
||||
flags |= unix.AT_RECURSIVE
|
||||
}
|
||||
fd, err := unix.OpenTree(unix.AT_FDCWD, m.Source, flags)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "open_tree(OPEN_TREE_CLONE)", Path: m.Source, Err: err}
|
||||
}
|
||||
mountFile = os.NewFile(uintptr(fd), m.Source)
|
||||
sourceType = mountSourceOpenTree
|
||||
|
||||
// Configure the id mapping.
|
||||
var usernsFile *os.File
|
||||
if m.IDMapping.UserNSPath == "" {
|
||||
usernsFile, err = nsHandles.Get(userns.Mapping{
|
||||
UIDMappings: m.IDMapping.UIDMappings,
|
||||
GIDMappings: m.IDMapping.GIDMappings,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create userns for %s id-mapping: %w", m.Source, err)
|
||||
}
|
||||
} else {
|
||||
usernsFile, err = os.Open(m.IDMapping.UserNSPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open existing userns for %s id-mapping: %w", m.Source, err)
|
||||
}
|
||||
}
|
||||
defer usernsFile.Close()
|
||||
|
||||
setAttrFlags := uint(unix.AT_EMPTY_PATH)
|
||||
// If the mount has "ridmap" set, we apply the configuration
|
||||
// recursively. This allows you to create "rbind" mounts where only
|
||||
// the top-level mount has an idmapping. I'm not sure why you'd
|
||||
// want that, but still...
|
||||
if m.IDMapping.Recursive {
|
||||
setAttrFlags |= unix.AT_RECURSIVE
|
||||
}
|
||||
if err := unix.MountSetattr(int(mountFile.Fd()), "", setAttrFlags, &unix.MountAttr{
|
||||
Attr_set: unix.MOUNT_ATTR_IDMAP,
|
||||
Userns_fd: uint64(usernsFile.Fd()),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to set MOUNT_ATTR_IDMAP on %s: %w", m.Source, err)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
mountFile, err = os.OpenFile(m.Source, unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sourceType = mountSourcePlain
|
||||
}
|
||||
return &mountSource{
|
||||
Type: sourceType,
|
||||
file: mountFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#ifndef IDMAP_H
|
||||
#define IDMAP_H
|
||||
|
||||
#include <sys/mount.h>
|
||||
// Centos-7 doesn't have this file nor the __has_include() directive, so let's
|
||||
// just leave this commented out and we can uncomment when it hits EOL (2024-06-30).
|
||||
//#include <linux/mount.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* mount_setattr() */
|
||||
#ifndef MOUNT_ATTR_IDMAP
|
||||
#define MOUNT_ATTR_IDMAP 0x00100000
|
||||
#endif
|
||||
|
||||
#ifndef __NR_mount_setattr
|
||||
#if defined _MIPS_SIM
|
||||
#if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */
|
||||
#define __NR_mount_setattr (442 + 4000)
|
||||
#endif
|
||||
#if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */
|
||||
#define __NR_mount_setattr (442 + 6000)
|
||||
#endif
|
||||
#if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */
|
||||
#define __NR_mount_setattr (442 + 5000)
|
||||
#endif
|
||||
#else
|
||||
#define __NR_mount_setattr 442
|
||||
#endif
|
||||
#endif
|
||||
#ifndef MOUNT_ATTR_SIZE_VER0
|
||||
struct mount_attr {
|
||||
__u64 attr_set;
|
||||
__u64 attr_clr;
|
||||
__u64 propagation;
|
||||
__u64 userns_fd;
|
||||
};
|
||||
#endif
|
||||
|
||||
/* open_tree() */
|
||||
#ifndef OPEN_TREE_CLONE
|
||||
#define OPEN_TREE_CLONE 1
|
||||
#endif
|
||||
|
||||
#ifndef OPEN_TREE_CLOEXEC
|
||||
#define OPEN_TREE_CLOEXEC O_CLOEXEC
|
||||
#endif
|
||||
|
||||
#ifndef __NR_open_tree
|
||||
#if defined _MIPS_SIM
|
||||
#if _MIPS_SIM == _MIPS_SIM_ABI32 /* o32 */
|
||||
#define __NR_open_tree 4428
|
||||
#endif
|
||||
#if _MIPS_SIM == _MIPS_SIM_NABI32 /* n32 */
|
||||
#define __NR_open_tree 6428
|
||||
#endif
|
||||
#if _MIPS_SIM == _MIPS_SIM_ABI64 /* n64 */
|
||||
#define __NR_open_tree 5428
|
||||
#endif
|
||||
#else
|
||||
#define __NR_open_tree 428
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static inline int sys_mount_setattr(int dfd, const char *path, unsigned int flags, struct mount_attr *attr, size_t size)
|
||||
{
|
||||
return syscall(__NR_mount_setattr, dfd, path, flags, attr, size);
|
||||
}
|
||||
|
||||
static inline int sys_open_tree(int dfd, const char *filename, unsigned int flags)
|
||||
{
|
||||
return syscall(__NR_open_tree, dfd, filename, flags);
|
||||
}
|
||||
|
||||
|
||||
#endif /* IDMAP_H */
|
||||
@@ -33,9 +33,6 @@
|
||||
/* Get all of the CLONE_NEW* flags. */
|
||||
#include "namespace.h"
|
||||
|
||||
/* Get definitions for idmap sources */
|
||||
#include "idmap.h"
|
||||
|
||||
/* Synchronisation values. */
|
||||
enum sync_t {
|
||||
SYNC_USERMAP_PLS = 0x40, /* Request parent to map our users. */
|
||||
@@ -44,12 +41,8 @@ enum sync_t {
|
||||
SYNC_RECVPID_ACK = 0x43, /* PID was correctly received by parent. */
|
||||
SYNC_GRANDCHILD = 0x44, /* The grandchild is ready to run. */
|
||||
SYNC_CHILD_FINISH = 0x45, /* The child or grandchild has finished. */
|
||||
SYNC_MOUNTSOURCES_PLS = 0x46, /* Tell parent to send mount sources by SCM_RIGHTS. */
|
||||
SYNC_MOUNTSOURCES_ACK = 0x47, /* All mount sources have been sent. */
|
||||
SYNC_MOUNT_IDMAP_PLS = 0x48, /* Tell parent to mount idmap sources. */
|
||||
SYNC_MOUNT_IDMAP_ACK = 0x49, /* All idmap mounts have been done. */
|
||||
SYNC_TIMEOFFSETS_PLS = 0x50, /* Request parent to write timens offsets. */
|
||||
SYNC_TIMEOFFSETS_ACK = 0x51, /* Timens offsets were written. */
|
||||
SYNC_TIMEOFFSETS_PLS = 0x46, /* Request parent to write timens offsets. */
|
||||
SYNC_TIMEOFFSETS_ACK = 0x47, /* Timens offsets were written. */
|
||||
};
|
||||
|
||||
#define STAGE_SETUP -1
|
||||
@@ -99,14 +92,6 @@ struct nlconfig_t {
|
||||
char *gidmappath;
|
||||
size_t gidmappath_len;
|
||||
|
||||
/* Mount sources opened outside the container userns. */
|
||||
char *mountsources;
|
||||
size_t mountsources_len;
|
||||
|
||||
/* Idmap sources opened outside the container userns which will be id mapped. */
|
||||
char *idmapsources;
|
||||
size_t idmapsources_len;
|
||||
|
||||
/* Time NS offsets. */
|
||||
char *timensoffset;
|
||||
size_t timensoffset_len;
|
||||
@@ -126,9 +111,7 @@ struct nlconfig_t {
|
||||
#define ROOTLESS_EUID_ATTR 27287
|
||||
#define UIDMAPPATH_ATTR 27288
|
||||
#define GIDMAPPATH_ATTR 27289
|
||||
#define MOUNT_SOURCES_ATTR 27290
|
||||
#define IDMAP_SOURCES_ATTR 27291
|
||||
#define TIMENSOFFSET_ATTR 27292
|
||||
#define TIMENSOFFSET_ATTR 27290
|
||||
|
||||
/*
|
||||
* Use the raw syscall for versions of glibc which don't include a function for
|
||||
@@ -446,14 +429,6 @@ static void nl_parse(int fd, struct nlconfig_t *config)
|
||||
case SETGROUP_ATTR:
|
||||
config->is_setgroup = readint8(current);
|
||||
break;
|
||||
case MOUNT_SOURCES_ATTR:
|
||||
config->mountsources = current;
|
||||
config->mountsources_len = payload_len;
|
||||
break;
|
||||
case IDMAP_SOURCES_ATTR:
|
||||
config->idmapsources = current;
|
||||
config->idmapsources_len = payload_len;
|
||||
break;
|
||||
case TIMENSOFFSET_ATTR:
|
||||
config->timensoffset = current;
|
||||
config->timensoffset_len = payload_len;
|
||||
@@ -546,115 +521,6 @@ static inline int sane_kill(pid_t pid, int signum)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* receive_fd_sources parses env_var as an array of fd numbers and, for each element that is
|
||||
* not -1, it receives an fd via SCM_RIGHTS and dup3 it to the fd requested in
|
||||
* the element of the env var.
|
||||
*/
|
||||
void receive_fd_sources(int sockfd, const char *env_var)
|
||||
{
|
||||
char *fds, *endp;
|
||||
long new_fd;
|
||||
|
||||
// This env var must be a json array of ints.
|
||||
fds = getenv(env_var);
|
||||
|
||||
if (fds[0] != '[') {
|
||||
bail("malformed %s env var: missing '['", env_var);
|
||||
}
|
||||
fds++;
|
||||
|
||||
for (endp = fds; *endp != ']'; fds = endp + 1) {
|
||||
new_fd = strtol(fds, &endp, 10);
|
||||
if (endp == fds) {
|
||||
bail("malformed %s env var: not a number", env_var);
|
||||
}
|
||||
if (*endp == '\0') {
|
||||
bail("malformed %s env var: missing ]", env_var);
|
||||
}
|
||||
// The list contains -1 when no fd is needed. Ignore them.
|
||||
if (new_fd == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (new_fd == LONG_MAX || new_fd < 0 || new_fd > INT_MAX) {
|
||||
bail("malformed %s env var: fds out of range", env_var);
|
||||
}
|
||||
|
||||
int recv_fd = receive_fd(sockfd);
|
||||
if (dup3(recv_fd, new_fd, O_CLOEXEC) < 0) {
|
||||
bail("cannot dup3 fd %d to %ld", recv_fd, new_fd);
|
||||
}
|
||||
if (close(recv_fd) < 0) {
|
||||
bail("cannot close fd %d", recv_fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void receive_mountsources(int sockfd)
|
||||
{
|
||||
receive_fd_sources(sockfd, "_LIBCONTAINER_MOUNT_FDS");
|
||||
}
|
||||
|
||||
void send_mountsources(int sockfd, pid_t child, char *mountsources, size_t mountsources_len)
|
||||
{
|
||||
char proc_path[PATH_MAX];
|
||||
int host_mntns_fd;
|
||||
int container_mntns_fd;
|
||||
int fd;
|
||||
int ret;
|
||||
|
||||
// container_linux.go shouldSendMountSources() decides if mount sources
|
||||
// should be pre-opened (O_PATH) and passed via SCM_RIGHTS
|
||||
if (mountsources == NULL)
|
||||
return;
|
||||
|
||||
host_mntns_fd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
|
||||
if (host_mntns_fd == -1)
|
||||
bail("failed to get current mount namespace");
|
||||
|
||||
if (snprintf(proc_path, PATH_MAX, "/proc/%d/ns/mnt", child) < 0)
|
||||
bail("failed to get mount namespace path");
|
||||
|
||||
container_mntns_fd = open(proc_path, O_RDONLY | O_CLOEXEC);
|
||||
if (container_mntns_fd == -1)
|
||||
bail("failed to get container mount namespace");
|
||||
|
||||
if (setns(container_mntns_fd, CLONE_NEWNS) < 0)
|
||||
bail("failed to setns to container mntns");
|
||||
|
||||
char *mountsources_end = mountsources + mountsources_len;
|
||||
while (mountsources < mountsources_end) {
|
||||
if (mountsources[0] == '\0') {
|
||||
mountsources++;
|
||||
continue;
|
||||
}
|
||||
|
||||
fd = open(mountsources, O_PATH | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
bail("failed to open mount source %s", mountsources);
|
||||
|
||||
write_log(DEBUG, "~> sending fd for: %s", mountsources);
|
||||
if (send_fd(sockfd, fd) < 0)
|
||||
bail("failed to send fd %d via unix socket %d", fd, sockfd);
|
||||
|
||||
ret = close(fd);
|
||||
if (ret != 0)
|
||||
bail("failed to close mount source fd %d", fd);
|
||||
|
||||
mountsources += strlen(mountsources) + 1;
|
||||
}
|
||||
|
||||
if (setns(host_mntns_fd, CLONE_NEWNS) < 0)
|
||||
bail("failed to setns to host mntns");
|
||||
|
||||
ret = close(host_mntns_fd);
|
||||
if (ret != 0)
|
||||
bail("failed to close host mount namespace fd %d", host_mntns_fd);
|
||||
ret = close(container_mntns_fd);
|
||||
if (ret != 0)
|
||||
bail("failed to close container mount namespace fd %d", container_mntns_fd);
|
||||
}
|
||||
|
||||
void try_unshare(int flags, const char *msg)
|
||||
{
|
||||
write_log(DEBUG, "unshare %s", msg);
|
||||
@@ -674,89 +540,6 @@ void try_unshare(int flags, const char *msg)
|
||||
bail("failed to unshare %s", msg);
|
||||
}
|
||||
|
||||
void send_idmapsources(int sockfd, pid_t pid, char *idmap_src, int idmap_src_len)
|
||||
{
|
||||
char proc_user_path[PATH_MAX];
|
||||
|
||||
/* Open the userns fd only once.
|
||||
* Currently we only support idmap mounts that use the same mapping than
|
||||
* the userns. This is validated in libcontainer/configs/validate/validator.go,
|
||||
* so if we reached here, we know the mapping for the idmap is the same
|
||||
* as the userns. This is why we just open the userns_fd once from the
|
||||
* PID of the child process that has the userns already applied.
|
||||
*/
|
||||
int ret = snprintf(proc_user_path, sizeof(proc_user_path), "/proc/%d/ns/user", pid);
|
||||
if (ret < 0 || (size_t)ret >= sizeof(proc_user_path)) {
|
||||
sane_kill(pid, SIGKILL);
|
||||
bail("failed to create userns path string");
|
||||
}
|
||||
|
||||
int userns_fd = open(proc_user_path, O_RDONLY | O_CLOEXEC | O_NOCTTY);
|
||||
if (userns_fd < 0) {
|
||||
sane_kill(pid, SIGKILL);
|
||||
bail("failed to get user namespace fd");
|
||||
}
|
||||
|
||||
char *idmap_end = idmap_src + idmap_src_len;
|
||||
while (idmap_src < idmap_end) {
|
||||
if (idmap_src[0] == '\0') {
|
||||
idmap_src++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int fd_tree = sys_open_tree(-EBADF, idmap_src,
|
||||
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC |
|
||||
AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT);
|
||||
if (fd_tree < 0) {
|
||||
sane_kill(pid, SIGKILL);
|
||||
if (errno == ENOSYS) {
|
||||
bail("open_tree(2) failed, the kernel doesn't support ID-mapped mounts");
|
||||
} else if (errno == EINVAL) {
|
||||
bail("open_tree(2) failed with path: %s, the kernel doesn't support ID-mapped mounts",
|
||||
idmap_src);
|
||||
} else {
|
||||
bail("open_tree(2) failed with path: %s", idmap_src);
|
||||
}
|
||||
}
|
||||
|
||||
struct mount_attr attr = {
|
||||
.attr_set = MOUNT_ATTR_IDMAP,
|
||||
.userns_fd = userns_fd,
|
||||
};
|
||||
|
||||
ret = sys_mount_setattr(fd_tree, "", AT_EMPTY_PATH, &attr, sizeof(attr));
|
||||
if (ret < 0) {
|
||||
sane_kill(pid, SIGKILL);
|
||||
if (errno == ENOSYS)
|
||||
bail("mount_setattr(2) failed, the kernel doesn't support ID-mapped mounts");
|
||||
else if (errno == EINVAL)
|
||||
bail("mount_setattr(2) failed with path: %s, maybe the filesystem doesn't support ID-mapped mounts", idmap_src);
|
||||
else
|
||||
bail("mount_setattr(2) failed with path: %s", idmap_src);
|
||||
}
|
||||
|
||||
write_log(DEBUG, "~> sending idmap source: %s with mapping from: %s", idmap_src, proc_user_path);
|
||||
send_fd(sockfd, fd_tree);
|
||||
|
||||
if (close(fd_tree) < 0) {
|
||||
sane_kill(pid, SIGKILL);
|
||||
bail("error closing fd_tree");
|
||||
}
|
||||
|
||||
idmap_src += strlen(idmap_src) + 1;
|
||||
}
|
||||
|
||||
if (close(userns_fd) < 0) {
|
||||
sane_kill(pid, SIGKILL);
|
||||
bail("error closing userns fd");
|
||||
}
|
||||
}
|
||||
|
||||
void receive_idmapsources(int sockfd)
|
||||
{
|
||||
receive_fd_sources(sockfd, "_LIBCONTAINER_IDMAP_FDS");
|
||||
}
|
||||
|
||||
static void update_timens_offsets(pid_t pid, char *map, size_t map_len)
|
||||
{
|
||||
if (map == NULL || map_len == 0)
|
||||
@@ -988,28 +771,6 @@ void nsexec(void)
|
||||
sane_kill(stage2_pid, SIGKILL);
|
||||
bail("failed to sync with runc: write(pid-JSON)");
|
||||
}
|
||||
break;
|
||||
case SYNC_MOUNTSOURCES_PLS:
|
||||
write_log(DEBUG, "stage-1 requested to open mount sources");
|
||||
send_mountsources(syncfd, stage1_pid, config.mountsources,
|
||||
config.mountsources_len);
|
||||
|
||||
s = SYNC_MOUNTSOURCES_ACK;
|
||||
if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
|
||||
sane_kill(stage1_pid, SIGKILL);
|
||||
bail("failed to sync with child: write(SYNC_MOUNTSOURCES_ACK)");
|
||||
}
|
||||
break;
|
||||
case SYNC_MOUNT_IDMAP_PLS:
|
||||
write_log(DEBUG, "stage-1 requested to open idmap sources");
|
||||
send_idmapsources(syncfd, stage1_pid, config.idmapsources,
|
||||
config.idmapsources_len);
|
||||
s = SYNC_MOUNT_IDMAP_ACK;
|
||||
if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
|
||||
sane_kill(stage1_pid, SIGKILL);
|
||||
bail("failed to sync with child: write(SYNC_MOUNT_IDMAP_ACK)");
|
||||
}
|
||||
|
||||
break;
|
||||
case SYNC_TIMEOFFSETS_PLS:
|
||||
write_log(DEBUG, "stage-1 requested timens offsets to be configured");
|
||||
@@ -1186,38 +947,6 @@ void nsexec(void)
|
||||
bail("failed to sync with parent: SYNC_TIMEOFFSETS_ACK: got %u", s);
|
||||
}
|
||||
|
||||
/* Ask our parent to send the mount sources fds. */
|
||||
if (config.mountsources) {
|
||||
write_log(DEBUG, "request stage-0 to send mount sources");
|
||||
s = SYNC_MOUNTSOURCES_PLS;
|
||||
if (write(syncfd, &s, sizeof(s)) != sizeof(s))
|
||||
bail("failed to sync with parent: write(SYNC_MOUNTSOURCES_PLS)");
|
||||
|
||||
/* Receive and install all mount sources fds. */
|
||||
receive_mountsources(syncfd);
|
||||
|
||||
/* Parent finished to send the mount sources fds. */
|
||||
if (read(syncfd, &s, sizeof(s)) != sizeof(s))
|
||||
bail("failed to sync with parent: read(SYNC_MOUNTSOURCES_ACK)");
|
||||
if (s != SYNC_MOUNTSOURCES_ACK)
|
||||
bail("failed to sync with parent: SYNC_MOUNTSOURCES_ACK: got %u", s);
|
||||
}
|
||||
|
||||
if (config.idmapsources) {
|
||||
write_log(DEBUG, "request stage-0 to send idmap sources");
|
||||
s = SYNC_MOUNT_IDMAP_PLS;
|
||||
if (write(syncfd, &s, sizeof(s)) != sizeof(s))
|
||||
bail("failed to sync with parent: write(SYNC_MOUNT_IDMAP_PLS)");
|
||||
|
||||
/* Receive and install all idmap fds. */
|
||||
receive_idmapsources(syncfd);
|
||||
|
||||
if (read(syncfd, &s, sizeof(s)) != sizeof(s))
|
||||
bail("failed to sync with parent: read(SYNC_MOUNT_IDMAP_ACK)");
|
||||
if (s != SYNC_MOUNT_IDMAP_ACK)
|
||||
bail("failed to sync with parent: SYNC_MOUNT_IDMAP_ACK: got %u", s);
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: What about non-namespace clone flags that we're dropping here?
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package libcontainer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -11,18 +12,21 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups/fs2"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/intelrdt"
|
||||
"github.com/opencontainers/runc/libcontainer/logs"
|
||||
"github.com/opencontainers/runc/libcontainer/system"
|
||||
"github.com/opencontainers/runc/libcontainer/userns"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type parentProcess interface {
|
||||
@@ -208,6 +212,9 @@ func (p *setnsProcess) start() (retErr error) {
|
||||
case procHooks:
|
||||
// This shouldn't happen.
|
||||
panic("unexpected procHooks in setns")
|
||||
case procMountPlease:
|
||||
// This shouldn't happen.
|
||||
panic("unexpected procMountPlease in setns")
|
||||
case procSeccomp:
|
||||
if p.config.Config.Seccomp.ListenerPath == "" {
|
||||
return errors.New("seccomp listenerPath is not set")
|
||||
@@ -398,6 +405,110 @@ func (p *initProcess) waitForChildExit(childPid int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mountSourceRequestFn func(*configs.Mount) (*mountSource, error)
|
||||
|
||||
// goCreateMountSources spawns a goroutine which creates open_tree(2)-style
|
||||
// mountfds based on the requested configs.Mount configuration. The returned
|
||||
// requestFn and cancelFn are used to interact with the goroutine.
|
||||
//
|
||||
// The caller of the returned mountSourceRequestFn is responsible for closing
|
||||
// the returned file.
|
||||
func (p *initProcess) goCreateMountSources(ctx context.Context) (mountSourceRequestFn, context.CancelFunc, error) {
|
||||
type response struct {
|
||||
src *mountSource
|
||||
err error
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
requestCh := make(chan *configs.Mount)
|
||||
responseCh := make(chan response)
|
||||
|
||||
ctx, cancelFn := context.WithTimeout(ctx, 1*time.Minute)
|
||||
go func() {
|
||||
// We lock this thread because we need to setns(2) here. There is no
|
||||
// UnlockOSThread() here, to ensure that the Go runtime will kill this
|
||||
// thread once this goroutine returns (ensuring no other goroutines run
|
||||
// in this context).
|
||||
runtime.LockOSThread()
|
||||
|
||||
// Detach from the shared fs of the rest of the Go process in order to
|
||||
// be able to CLONE_NEWNS.
|
||||
if err := unix.Unshare(unix.CLONE_FS); err != nil {
|
||||
err = os.NewSyscallError("unshare(CLONE_FS)", err)
|
||||
errCh <- fmt.Errorf("mount source thread: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Attach to the container's mount namespace.
|
||||
nsFd, err := os.Open(fmt.Sprintf("/proc/%d/ns/mnt", p.pid()))
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("mount source thread: open container mntns: %w", err)
|
||||
return
|
||||
}
|
||||
defer nsFd.Close()
|
||||
if err := unix.Setns(int(nsFd.Fd()), unix.CLONE_NEWNS); err != nil {
|
||||
err = os.NewSyscallError("setns", err)
|
||||
errCh <- fmt.Errorf("mount source thread: join container mntns: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// No errors during setup!
|
||||
close(errCh)
|
||||
logrus.Debugf("mount source thread: successfully running in container mntns")
|
||||
|
||||
nsHandles := new(userns.Handles)
|
||||
defer nsHandles.Release()
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case m, ok := <-requestCh:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
src, err := mountFd(nsHandles, m)
|
||||
logrus.Debugf("mount source thread: handling request for %q: %v %v", m.Source, src, err)
|
||||
responseCh <- response{
|
||||
src: src,
|
||||
err: err,
|
||||
}
|
||||
case <-ctx.Done():
|
||||
break loop
|
||||
}
|
||||
}
|
||||
logrus.Debugf("mount source thread: closing thread: %v", ctx.Err())
|
||||
close(responseCh)
|
||||
}()
|
||||
|
||||
// Check for setup errors.
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
cancelFn()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO: Switch to context.AfterFunc when we switch to Go 1.21.
|
||||
var requestChCloseOnce sync.Once
|
||||
requestFn := func(m *configs.Mount) (*mountSource, error) {
|
||||
var err error
|
||||
select {
|
||||
case requestCh <- m:
|
||||
select {
|
||||
case resp, ok := <-responseCh:
|
||||
if ok {
|
||||
return resp.src, resp.err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
err = fmt.Errorf("receive mount source context cancelled: %w", ctx.Err())
|
||||
}
|
||||
case <-ctx.Done():
|
||||
err = fmt.Errorf("send mount request cancelled: %w", ctx.Err())
|
||||
}
|
||||
requestChCloseOnce.Do(func() { close(requestCh) })
|
||||
return nil, err
|
||||
}
|
||||
return requestFn, cancelFn, nil
|
||||
}
|
||||
|
||||
func (p *initProcess) start() (retErr error) {
|
||||
defer p.comm.closeParent()
|
||||
err := p.cmd.Start()
|
||||
@@ -487,6 +598,22 @@ func (p *initProcess) start() (retErr error) {
|
||||
return fmt.Errorf("error waiting for our first child to exit: %w", err)
|
||||
}
|
||||
|
||||
// Spin up a goroutine to handle remapping mount requests by runc init.
|
||||
// There is no point doing this for rootless containers because they cannot
|
||||
// configure MOUNT_ATTR_IDMAP, nor do OPEN_TREE_CLONE. We could just
|
||||
// service plain-open requests for plain bind-mounts but there's no need
|
||||
// (rootless containers will never have permission issues on a source mount
|
||||
// that the parent process can help with -- they are the same user).
|
||||
var mountRequest mountSourceRequestFn
|
||||
if !p.container.config.RootlessEUID {
|
||||
request, cancel, err := p.goCreateMountSources(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error spawning mount remapping thread: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
mountRequest = request
|
||||
}
|
||||
|
||||
if err := p.createNetworkInterfaces(); err != nil {
|
||||
return fmt.Errorf("error creating network interfaces: %w", err)
|
||||
}
|
||||
@@ -500,6 +627,35 @@ func (p *initProcess) start() (retErr error) {
|
||||
var seenProcReady bool
|
||||
ierr := parseSync(p.comm.syncSockParent, func(sync *syncT) error {
|
||||
switch sync.Type {
|
||||
case procMountPlease:
|
||||
if mountRequest == nil {
|
||||
return fmt.Errorf("cannot fulfil mount requests as a rootless user")
|
||||
}
|
||||
var m *configs.Mount
|
||||
if sync.Arg == nil {
|
||||
return fmt.Errorf("sync %q is missing an argument", sync.Type)
|
||||
}
|
||||
if err := json.Unmarshal(*sync.Arg, &m); err != nil {
|
||||
return fmt.Errorf("sync %q passed invalid mount arg: %w", sync.Type, err)
|
||||
}
|
||||
mnt, err := mountRequest(m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fulfil mount request: %w", err)
|
||||
}
|
||||
defer mnt.file.Close()
|
||||
|
||||
arg, err := json.Marshal(mnt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync %q failed to marshal mountSource: %w", sync.Type, err)
|
||||
}
|
||||
argMsg := json.RawMessage(arg)
|
||||
if err := doWriteSync(p.comm.syncSockParent, syncT{
|
||||
Type: procMountFd,
|
||||
Arg: &argMsg,
|
||||
File: mnt.file,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case procSeccomp:
|
||||
if p.config.Config.Seccomp.ListenerPath == "" {
|
||||
return errors.New("seccomp listenerPath is not set")
|
||||
|
||||
+159
-133
@@ -1,6 +1,7 @@
|
||||
package libcontainer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -8,21 +9,23 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/moby/sys/mountinfo"
|
||||
"github.com/mrunalp/fileutils"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/opencontainers/selinux/go-selinux/label"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups/fs2"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/devices"
|
||||
"github.com/opencontainers/runc/libcontainer/userns"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/opencontainers/selinux/go-selinux/label"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const defaultMountFlags = unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV
|
||||
@@ -39,16 +42,47 @@ type mountConfig struct {
|
||||
// mountEntry contains mount data specific to a mount point.
|
||||
type mountEntry struct {
|
||||
*configs.Mount
|
||||
srcFD *int
|
||||
srcFile *mountSource
|
||||
}
|
||||
|
||||
func (m *mountEntry) src() string {
|
||||
if m.srcFD != nil {
|
||||
return "/proc/self/fd/" + strconv.Itoa(*m.srcFD)
|
||||
// srcName is only meant for error messages, it returns a "friendly" name.
|
||||
func (m mountEntry) srcName() string {
|
||||
if m.srcFile != nil {
|
||||
return m.srcFile.file.Name()
|
||||
}
|
||||
return m.Source
|
||||
}
|
||||
|
||||
func (m mountEntry) srcStat() (os.FileInfo, *syscall.Stat_t, error) {
|
||||
var (
|
||||
st os.FileInfo
|
||||
err error
|
||||
)
|
||||
if m.srcFile != nil {
|
||||
st, err = m.srcFile.file.Stat()
|
||||
} else {
|
||||
st, err = os.Stat(m.Source)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return st, st.Sys().(*syscall.Stat_t), nil
|
||||
}
|
||||
|
||||
func (m mountEntry) srcStatfs() (*unix.Statfs_t, error) {
|
||||
var st unix.Statfs_t
|
||||
if m.srcFile != nil {
|
||||
if err := unix.Fstatfs(int(m.srcFile.file.Fd()), &st); err != nil {
|
||||
return nil, os.NewSyscallError("fstatfs", err)
|
||||
}
|
||||
} else {
|
||||
if err := unix.Statfs(m.Source, &st); err != nil {
|
||||
return nil, &os.PathError{Op: "statfs", Path: m.Source, Err: err}
|
||||
}
|
||||
}
|
||||
return &st, nil
|
||||
}
|
||||
|
||||
// needsSetupDev returns true if /dev needs to be set up.
|
||||
func needsSetupDev(config *configs.Config) bool {
|
||||
for _, m := range config.Mounts {
|
||||
@@ -62,20 +96,12 @@ func needsSetupDev(config *configs.Config) bool {
|
||||
// prepareRootfs sets up the devices, mount points, and filesystems for use
|
||||
// inside a new mount namespace. It doesn't set anything as ro. You must call
|
||||
// finalizeRootfs after this function to finish setting up the rootfs.
|
||||
func prepareRootfs(pipe *syncSocket, iConfig *initConfig, mountFds mountFds) (err error) {
|
||||
func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) {
|
||||
config := iConfig.Config
|
||||
if err := prepareRoot(config); err != nil {
|
||||
return fmt.Errorf("error preparing rootfs: %w", err)
|
||||
}
|
||||
|
||||
if mountFds.sourceFds != nil && len(mountFds.sourceFds) != len(config.Mounts) {
|
||||
return fmt.Errorf("malformed mountFds slice. Expected size: %v, got: %v", len(config.Mounts), len(mountFds.sourceFds))
|
||||
}
|
||||
|
||||
if mountFds.idmapFds != nil && len(mountFds.idmapFds) != len(config.Mounts) {
|
||||
return fmt.Errorf("malformed idmapFds slice: expected size: %v, got: %v", len(config.Mounts), len(mountFds.idmapFds))
|
||||
}
|
||||
|
||||
mountConfig := &mountConfig{
|
||||
root: config.Rootfs,
|
||||
label: config.MountLabel,
|
||||
@@ -83,22 +109,53 @@ func prepareRootfs(pipe *syncSocket, iConfig *initConfig, mountFds mountFds) (er
|
||||
rootlessCgroups: iConfig.RootlessCgroups,
|
||||
cgroupns: config.Namespaces.Contains(configs.NEWCGROUP),
|
||||
}
|
||||
for i, m := range config.Mounts {
|
||||
for _, m := range config.Mounts {
|
||||
entry := mountEntry{Mount: m}
|
||||
// Just before the loop we checked that if not empty, len(mountFds.sourceFds) == len(config.Mounts).
|
||||
// Therefore, we can access mountFds.sourceFds[i] without any concerns.
|
||||
if mountFds.sourceFds != nil && mountFds.sourceFds[i] != -1 {
|
||||
entry.srcFD = &mountFds.sourceFds[i]
|
||||
}
|
||||
|
||||
// We validated before we can access mountFds.idmapFds[i].
|
||||
if mountFds.idmapFds != nil && mountFds.idmapFds[i] != -1 {
|
||||
if entry.srcFD != nil {
|
||||
return fmt.Errorf("malformed mountFds and idmapFds slice, entry: %v has fds in both slices", i)
|
||||
// Figure out whether we need to request runc to give us an
|
||||
// open_tree(2)-style mountfd. For idmapped mounts, this is always
|
||||
// necessary. For bind-mounts, this is only necessary if we cannot
|
||||
// resolve the parent mount (this is only hit if you are running in a
|
||||
// userns -- but for rootless the host-side thread can't help).
|
||||
wantSourceFile := m.IsIDMapped()
|
||||
if m.IsBind() && !config.RootlessEUID {
|
||||
if _, err := os.Stat(m.Source); err != nil {
|
||||
wantSourceFile = true
|
||||
}
|
||||
entry.srcFD = &mountFds.idmapFds[i]
|
||||
}
|
||||
|
||||
if wantSourceFile {
|
||||
// Request a source file from the host.
|
||||
if err := writeSyncArg(pipe, procMountPlease, m); err != nil {
|
||||
return fmt.Errorf("failed to request mountfd for %q: %w", m.Source, err)
|
||||
}
|
||||
sync, err := readSyncFull(pipe, procMountFd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mountfd request for %q failed: %w", m.Source, err)
|
||||
}
|
||||
if sync.File == nil {
|
||||
return fmt.Errorf("mountfd request for %q: response missing attached fd", m.Source)
|
||||
}
|
||||
defer sync.File.Close()
|
||||
// Sanity-check to make sure we didn't get the wrong fd back. Note
|
||||
// that while m.Source might contain symlinks, the (*os.File).Name
|
||||
// is based on the path provided to os.OpenFile, not what it
|
||||
// resolves to. So this should never happen.
|
||||
if sync.File.Name() != m.Source {
|
||||
return fmt.Errorf("returned mountfd for %q doesn't match requested mount configuration: mountfd path is %q", m.Source, sync.File.Name())
|
||||
}
|
||||
// Unmarshal the procMountFd argument (the file is sync.File).
|
||||
var src *mountSource
|
||||
if sync.Arg == nil {
|
||||
return fmt.Errorf("sync %q is missing an argument", sync.Type)
|
||||
}
|
||||
if err := json.Unmarshal(*sync.Arg, &src); err != nil {
|
||||
return fmt.Errorf("invalid mount fd response argument %q: %w", string(*sync.Arg), err)
|
||||
}
|
||||
if src == nil {
|
||||
return fmt.Errorf("mountfd request for %q: no mount source info received", m.Source)
|
||||
}
|
||||
src.file = sync.File
|
||||
entry.srcFile = src
|
||||
}
|
||||
if err := mountToRootfs(mountConfig, entry); err != nil {
|
||||
return fmt.Errorf("error mounting %q to rootfs at %q: %w", m.Source, m.Destination, err)
|
||||
}
|
||||
@@ -224,32 +281,6 @@ func cleanupTmp(tmpdir string) {
|
||||
_ = os.RemoveAll(tmpdir)
|
||||
}
|
||||
|
||||
func prepareBindMount(m mountEntry, rootfs string) error {
|
||||
source := m.src()
|
||||
stat, err := os.Stat(source)
|
||||
if err != nil {
|
||||
// error out if the source of a bind mount does not exist as we will be
|
||||
// unable to bind anything to it.
|
||||
return err
|
||||
}
|
||||
// ensure that the destination of the bind mount is resolved of symlinks at mount time because
|
||||
// any previous mounts can invalidate the next mount's destination.
|
||||
// this can happen when a user specifies mounts within other mounts to cause breakouts or other
|
||||
// evil stuff to try to escape the container's rootfs.
|
||||
var dest string
|
||||
if dest, err = securejoin.SecureJoin(rootfs, m.Destination); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkProcMount(rootfs, dest, source); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := createIfNotExists(dest, stat.IsDir()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountCgroupV1(m *configs.Mount, c *mountConfig) error {
|
||||
binds, err := getCgroupMounts(m)
|
||||
if err != nil {
|
||||
@@ -281,7 +312,7 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error {
|
||||
if err := os.MkdirAll(subsystemPath, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := utils.WithProcfd(c.root, b.Destination, func(dstFD string) error {
|
||||
if err := utils.WithProcfd(c.root, b.Destination, func(dstFd string) error {
|
||||
flags := defaultMountFlags
|
||||
if m.Flags&unix.MS_RDONLY != 0 {
|
||||
flags = flags | unix.MS_RDONLY
|
||||
@@ -294,7 +325,7 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error {
|
||||
data = cgroups.CgroupNamePrefix + data
|
||||
source = "systemd"
|
||||
}
|
||||
return mountViaFDs(source, nil, b.Destination, dstFD, "cgroup", uintptr(flags), data)
|
||||
return mountViaFds(source, nil, b.Destination, dstFd, "cgroup", uintptr(flags), data)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -325,8 +356,8 @@ func mountCgroupV2(m *configs.Mount, c *mountConfig) error {
|
||||
if err := os.MkdirAll(dest, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
err = utils.WithProcfd(c.root, m.Destination, func(dstFD string) error {
|
||||
return mountViaFDs(m.Source, nil, m.Destination, dstFD, "cgroup2", uintptr(m.Flags), m.Data)
|
||||
err = utils.WithProcfd(c.root, m.Destination, func(dstFd string) error {
|
||||
return mountViaFds(m.Source, nil, m.Destination, dstFd, "cgroup2", uintptr(m.Flags), m.Data)
|
||||
})
|
||||
if err == nil || !(errors.Is(err, unix.EPERM) || errors.Is(err, unix.EBUSY)) {
|
||||
return err
|
||||
@@ -347,7 +378,6 @@ func mountCgroupV2(m *configs.Mount, c *mountConfig) error {
|
||||
bindM.Source = c.cgroup2Path
|
||||
}
|
||||
// mountToRootfs() handles remounting for MS_RDONLY.
|
||||
// No need to set mountEntry.srcFD here, because mountToRootfs() calls utils.WithProcfd() by itself in mountPropagate().
|
||||
err = mountToRootfs(c, mountEntry{Mount: bindM})
|
||||
if c.rootlessCgroups && errors.Is(err, unix.ENOENT) {
|
||||
// ENOENT (for `src = c.cgroup2Path`) happens when rootless runc is being executed
|
||||
@@ -392,15 +422,15 @@ func doTmpfsCopyUp(m mountEntry, rootfs, mountLabel string) (Err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
return utils.WithProcfd(rootfs, m.Destination, func(dstFD string) (Err error) {
|
||||
return utils.WithProcfd(rootfs, m.Destination, func(dstFd string) (Err error) {
|
||||
// Copy the container data to the host tmpdir. We append "/" to force
|
||||
// CopyDirectory to resolve the symlink rather than trying to copy the
|
||||
// symlink itself.
|
||||
if err := fileutils.CopyDirectory(dstFD+"/", tmpDir); err != nil {
|
||||
return fmt.Errorf("tmpcopyup: failed to copy %s to %s (%s): %w", m.Destination, dstFD, tmpDir, err)
|
||||
if err := fileutils.CopyDirectory(dstFd+"/", tmpDir); err != nil {
|
||||
return fmt.Errorf("tmpcopyup: failed to copy %s to %s (%s): %w", m.Destination, dstFd, tmpDir, err)
|
||||
}
|
||||
// Now move the mount into the container.
|
||||
if err := mountViaFDs(tmpDir, nil, m.Destination, dstFD, "", unix.MS_MOVE, ""); err != nil {
|
||||
if err := mountViaFds(tmpDir, nil, m.Destination, dstFd, "", unix.MS_MOVE, ""); err != nil {
|
||||
return fmt.Errorf("tmpcopyup: failed to move mount: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -469,6 +499,9 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
// Do not use securejoin as it resolves symlinks.
|
||||
dest = filepath.Join(rootfs, dest)
|
||||
}
|
||||
if err := checkProcMount(rootfs, dest, m); err != nil {
|
||||
return err
|
||||
}
|
||||
if fi, err := os.Lstat(dest); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
@@ -488,6 +521,9 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkProcMount(rootfs, dest, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch m.Device {
|
||||
case "mqueue":
|
||||
@@ -519,38 +555,18 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
|
||||
return err
|
||||
case "bind":
|
||||
if err := prepareBindMount(m, rootfs); err != nil {
|
||||
fi, _, err := m.srcStat()
|
||||
if err != nil {
|
||||
// error out if the source of a bind mount does not exist as we will be
|
||||
// unable to bind anything to it.
|
||||
return err
|
||||
}
|
||||
|
||||
if m.IsBind() && m.IsIDMapped() {
|
||||
if m.srcFD == nil {
|
||||
return fmt.Errorf("error creating mount %+v: idmapFD is invalid, should point to a valid fd", m)
|
||||
}
|
||||
if err := unix.MoveMount(*m.srcFD, "", unix.AT_FDCWD, dest, unix.MOVE_MOUNT_F_EMPTY_PATH); err != nil {
|
||||
return fmt.Errorf("error on unix.MoveMount %+v: %w", m, err)
|
||||
}
|
||||
|
||||
// In nsexec.c, we did not set the propagation field of mount_attr struct.
|
||||
// So, let's deal with these flags right now!
|
||||
if err := utils.WithProcfd(rootfs, dest, func(dstFD string) error {
|
||||
for _, pflag := range m.PropagationFlags {
|
||||
// When using mount for setting propagations flags, the source, file
|
||||
// system type and data arguments are ignored:
|
||||
// https://man7.org/linux/man-pages/man2/mount.2.html
|
||||
// We also ignore procfd because we want to act on dest.
|
||||
if err := mountViaFDs("", nil, dest, dstFD, "", uintptr(pflag), ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("change mount propagation through procfd: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := mountPropagate(m, rootfs, mountLabel); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := createIfNotExists(dest, fi.IsDir()); err != nil {
|
||||
return err
|
||||
}
|
||||
// open_tree()-related shenanigans are all handled in mountViaFds.
|
||||
if err := mountPropagate(m, rootfs, mountLabel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The initial MS_BIND won't change the mount options, we need to do a
|
||||
@@ -563,7 +579,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
// contrast to mount(8)'s current behaviour, but is what users probably
|
||||
// expect. See <https://github.com/util-linux/util-linux/issues/2433>.
|
||||
if m.Flags & ^(unix.MS_BIND|unix.MS_REC|unix.MS_REMOUNT) != 0 || m.ClearedFlags != 0 {
|
||||
if err := utils.WithProcfd(rootfs, m.Destination, func(dstFD string) error {
|
||||
if err := utils.WithProcfd(rootfs, m.Destination, func(dstFd string) error {
|
||||
flags := m.Flags | unix.MS_BIND | unix.MS_REMOUNT
|
||||
// The runtime-spec says we SHOULD map to the relevant mount(8)
|
||||
// behaviour. However, it's not clear whether we want the
|
||||
@@ -590,7 +606,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
// different set of flags. This also has the mount(8) bug where
|
||||
// "nodiratime,norelatime" will result in a
|
||||
// "nodiratime,relatime" mount.
|
||||
mountErr := mountViaFDs("", nil, m.Destination, dstFD, "", uintptr(flags), "")
|
||||
mountErr := mountViaFds("", nil, m.Destination, dstFd, "", uintptr(flags), "")
|
||||
if mountErr == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -603,11 +619,11 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
// that we handle atimes correctly to make sure we error out if
|
||||
// we cannot fulfil the requested mount flags.
|
||||
|
||||
var st unix.Statfs_t
|
||||
if err := unix.Statfs(m.src(), &st); err != nil {
|
||||
return &os.PathError{Op: "statfs", Path: m.src(), Err: err}
|
||||
st, err := m.srcStatfs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcFlags := statfsToMountFlags(st)
|
||||
srcFlags := statfsToMountFlags(*st)
|
||||
// If the user explicitly request one of the locked flags *not*
|
||||
// be set, we need to return an error to avoid producing mounts
|
||||
// that don't match the user's request.
|
||||
@@ -639,7 +655,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
// Retry the mount with the existing lockable mount flags
|
||||
// applied.
|
||||
flags |= srcFlags & mntLockFlags
|
||||
mountErr = mountViaFDs("", nil, m.Destination, dstFD, "", uintptr(flags), "")
|
||||
mountErr = mountViaFds("", nil, m.Destination, dstFd, "", uintptr(flags), "")
|
||||
logrus.Debugf("remount retry: srcFlags=0x%x flagsSet=0x%x flagsClr=0x%x: %v", srcFlags, m.Flags, m.ClearedFlags, mountErr)
|
||||
return mountErr
|
||||
}); err != nil {
|
||||
@@ -663,9 +679,6 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
}
|
||||
return mountCgroupV1(m.Mount, c)
|
||||
default:
|
||||
if err := checkProcMount(rootfs, dest, m.Source); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(dest, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -679,6 +692,9 @@ func getCgroupMounts(m *configs.Mount) ([]*configs.Mount, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We don't need to use /proc/thread-self here because runc always runs
|
||||
// with every thread in the same cgroup. This lets us avoid having to do
|
||||
// runtime.LockOSThread.
|
||||
cgroupPaths, err := cgroups.ParseCgroupFile("/proc/self/cgroup")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -707,11 +723,16 @@ func getCgroupMounts(m *configs.Mount) ([]*configs.Mount, error) {
|
||||
return binds, nil
|
||||
}
|
||||
|
||||
// Taken from <include/linux/proc_ns.h>. If a file is on a filesystem of type
|
||||
// PROC_SUPER_MAGIC, we're guaranteed that only the root of the superblock will
|
||||
// have this inode number.
|
||||
const procRootIno = 1
|
||||
|
||||
// checkProcMount checks to ensure that the mount destination is not over the top of /proc.
|
||||
// dest is required to be an abs path and have any symlinks resolved before calling this function.
|
||||
//
|
||||
// if source is nil, don't stat the filesystem. This is used for restore of a checkpoint.
|
||||
func checkProcMount(rootfs, dest, source string) error {
|
||||
// If m is nil, don't stat the filesystem. This is used for restore of a checkpoint.
|
||||
func checkProcMount(rootfs, dest string, m mountEntry) error {
|
||||
const procPath = "/proc"
|
||||
path, err := filepath.Rel(filepath.Join(rootfs, procPath), dest)
|
||||
if err != nil {
|
||||
@@ -722,18 +743,28 @@ func checkProcMount(rootfs, dest, source string) error {
|
||||
return nil
|
||||
}
|
||||
if path == "." {
|
||||
// an empty source is pasted on restore
|
||||
if source == "" {
|
||||
return nil
|
||||
}
|
||||
// only allow a mount on-top of proc if it's source is "proc"
|
||||
isproc, err := isProc(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// pass if the mount is happening on top of /proc and the source of
|
||||
// the mount is a proc filesystem
|
||||
if isproc {
|
||||
// Only allow bind-mounts on top of /proc, and only if the source is a
|
||||
// procfs mount.
|
||||
if m.IsBind() {
|
||||
fsSt, err := m.srcStatfs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fsSt.Type == unix.PROC_SUPER_MAGIC {
|
||||
if _, uSt, err := m.srcStat(); err != nil {
|
||||
return err
|
||||
} else if uSt.Ino != procRootIno {
|
||||
// We cannot error out in this case, because we've
|
||||
// supported these kinds of mounts for a long time.
|
||||
// However, we would expect users to bind-mount the root of
|
||||
// a real procfs on top of /proc in the container. We might
|
||||
// want to block this in the future.
|
||||
logrus.Warnf("bind-mount %v (source %v) is of type procfs but is not the root of a procfs (inode %d). Future versions of runc might block this configuration -- please report an issue to <https://github.com/opencontainers/runc> if you see this warning.", dest, m.srcName(), uSt.Ino)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else if m.Device == "proc" {
|
||||
// Fresh procfs-type mounts are always safe to mount on top of /proc.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%q cannot be mounted because it is not of type proc", dest)
|
||||
@@ -766,15 +797,10 @@ func checkProcMount(rootfs, dest, source string) error {
|
||||
return fmt.Errorf("%q cannot be mounted because it is inside /proc", dest)
|
||||
}
|
||||
|
||||
func isProc(path string) (bool, error) {
|
||||
var s unix.Statfs_t
|
||||
if err := unix.Statfs(path, &s); err != nil {
|
||||
return false, &os.PathError{Op: "statfs", Path: path, Err: err}
|
||||
}
|
||||
return s.Type == unix.PROC_SUPER_MAGIC, nil
|
||||
}
|
||||
|
||||
func setupDevSymlinks(rootfs string) error {
|
||||
// In theory, these should be links to /proc/thread-self, but systems
|
||||
// expect these to be /proc/self and this matches how most distributions
|
||||
// work.
|
||||
links := [][2]string{
|
||||
{"/proc/self/fd", "/dev/fd"},
|
||||
{"/proc/self/fd/0", "/dev/stdin"},
|
||||
@@ -857,8 +883,8 @@ func bindMountDeviceNode(rootfs, dest string, node *devices.Device) error {
|
||||
if f != nil {
|
||||
_ = f.Close()
|
||||
}
|
||||
return utils.WithProcfd(rootfs, dest, func(dstFD string) error {
|
||||
return mountViaFDs(node.Path, nil, dest, dstFD, "bind", unix.MS_BIND, "")
|
||||
return utils.WithProcfd(rootfs, dest, func(dstFd string) error {
|
||||
return mountViaFds(node.Path, nil, dest, dstFd, "bind", unix.MS_BIND, "")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1251,17 +1277,17 @@ func mountPropagate(m mountEntry, rootfs string, mountLabel string) error {
|
||||
// mutating underneath us, we verify that we are actually going to mount
|
||||
// inside the container with WithProcfd() -- mounting through a procfd
|
||||
// mounts on the target.
|
||||
if err := utils.WithProcfd(rootfs, m.Destination, func(dstFD string) error {
|
||||
return mountViaFDs(m.Source, m.srcFD, m.Destination, dstFD, m.Device, uintptr(flags), data)
|
||||
if err := utils.WithProcfd(rootfs, m.Destination, func(dstFd string) error {
|
||||
return mountViaFds(m.Source, m.srcFile, m.Destination, dstFd, m.Device, uintptr(flags), data)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
// We have to apply mount propagation flags in a separate WithProcfd() call
|
||||
// because the previous call invalidates the passed procfd -- the mount
|
||||
// target needs to be re-opened.
|
||||
if err := utils.WithProcfd(rootfs, m.Destination, func(dstFD string) error {
|
||||
if err := utils.WithProcfd(rootfs, m.Destination, func(dstFd string) error {
|
||||
for _, pflag := range m.PropagationFlags {
|
||||
if err := mountViaFDs("", nil, m.Destination, dstFD, "", uintptr(pflag), ""); err != nil {
|
||||
if err := mountViaFds("", nil, m.Destination, dstFd, "", uintptr(pflag), ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,45 +4,133 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestCheckMountDestOnProc(t *testing.T) {
|
||||
func TestCheckMountDestInProc(t *testing.T) {
|
||||
m := mountEntry{
|
||||
Mount: &configs.Mount{
|
||||
Destination: "/proc/sys",
|
||||
Source: "/proc/sys",
|
||||
Device: "bind",
|
||||
Flags: unix.MS_BIND,
|
||||
},
|
||||
}
|
||||
dest := "/rootfs/proc/sys"
|
||||
err := checkProcMount("/rootfs", dest, "")
|
||||
err := checkProcMount("/rootfs", dest, m)
|
||||
if err == nil {
|
||||
t.Fatal("destination inside proc should return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMountDestOnProcChroot(t *testing.T) {
|
||||
func TestCheckProcMountOnProc(t *testing.T) {
|
||||
m := mountEntry{
|
||||
Mount: &configs.Mount{
|
||||
Destination: "/proc",
|
||||
Source: "foo",
|
||||
Device: "proc",
|
||||
},
|
||||
}
|
||||
dest := "/rootfs/proc/"
|
||||
err := checkProcMount("/rootfs", dest, "/proc")
|
||||
err := checkProcMount("/rootfs", dest, m)
|
||||
if err != nil {
|
||||
t.Fatal("destination inside proc when using chroot should not return an error")
|
||||
t.Fatalf("procfs type mount on /proc should not return an error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckBindMountOnProc(t *testing.T) {
|
||||
m := mountEntry{
|
||||
Mount: &configs.Mount{
|
||||
Destination: "/proc",
|
||||
Source: "/proc/self",
|
||||
Device: "bind",
|
||||
Flags: unix.MS_BIND,
|
||||
},
|
||||
}
|
||||
dest := "/rootfs/proc/"
|
||||
err := checkProcMount("/rootfs", dest, m)
|
||||
if err != nil {
|
||||
t.Fatalf("bind-mount of procfs on top of /proc should not return an error (for now): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckTrickyMountOnProc(t *testing.T) {
|
||||
// Make a non-bind mount that looks like a bit like a bind-mount.
|
||||
m := mountEntry{
|
||||
Mount: &configs.Mount{
|
||||
Destination: "/proc",
|
||||
Source: "/proc",
|
||||
Device: "overlay",
|
||||
Data: "lowerdir=/tmp/fakeproc,upperdir=/tmp/fakeproc2,workdir=/tmp/work",
|
||||
},
|
||||
}
|
||||
dest := "/rootfs/proc/"
|
||||
err := checkProcMount("/rootfs", dest, m)
|
||||
if err == nil {
|
||||
t.Fatalf("dodgy overlayfs mount on top of /proc should return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckTrickyBindMountOnProc(t *testing.T) {
|
||||
// Make a bind mount that looks like it might be a procfs mount.
|
||||
m := mountEntry{
|
||||
Mount: &configs.Mount{
|
||||
Destination: "/proc",
|
||||
Source: "/sys",
|
||||
Device: "proc",
|
||||
Flags: unix.MS_BIND,
|
||||
},
|
||||
}
|
||||
dest := "/rootfs/proc/"
|
||||
err := checkProcMount("/rootfs", dest, m)
|
||||
if err == nil {
|
||||
t.Fatalf("dodgy bind-mount on top of /proc should return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMountDestInSys(t *testing.T) {
|
||||
m := mountEntry{
|
||||
Mount: &configs.Mount{
|
||||
Destination: "/sys/fs/cgroup",
|
||||
Source: "tmpfs",
|
||||
Device: "tmpfs",
|
||||
},
|
||||
}
|
||||
dest := "/rootfs//sys/fs/cgroup"
|
||||
err := checkProcMount("/rootfs", dest, "")
|
||||
err := checkProcMount("/rootfs", dest, m)
|
||||
if err != nil {
|
||||
t.Fatal("destination inside /sys should not return an error")
|
||||
t.Fatalf("destination inside /sys should not return an error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMountDestFalsePositive(t *testing.T) {
|
||||
m := mountEntry{
|
||||
Mount: &configs.Mount{
|
||||
Destination: "/sysfiles/fs/cgroup",
|
||||
Source: "tmpfs",
|
||||
Device: "tmpfs",
|
||||
},
|
||||
}
|
||||
dest := "/rootfs/sysfiles/fs/cgroup"
|
||||
err := checkProcMount("/rootfs", dest, "")
|
||||
err := checkProcMount("/rootfs", dest, m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMountDestNsLastPid(t *testing.T) {
|
||||
m := mountEntry{
|
||||
Mount: &configs.Mount{
|
||||
Destination: "/proc/sys/kernel/ns_last_pid",
|
||||
Source: "lxcfs",
|
||||
Device: "fuse.lxcfs",
|
||||
},
|
||||
}
|
||||
dest := "/rootfs/proc/sys/kernel/ns_last_pid"
|
||||
err := checkProcMount("/rootfs", dest, "/proc")
|
||||
err := checkProcMount("/rootfs", dest, m)
|
||||
if err != nil {
|
||||
t.Fatal("/proc/sys/kernel/ns_last_pid should not return an error")
|
||||
t.Fatalf("/proc/sys/kernel/ns_last_pid should not return an error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ var (
|
||||
clear bool
|
||||
flag int
|
||||
}
|
||||
complexFlags map[string]func(*configs.Mount)
|
||||
)
|
||||
|
||||
func initMaps() {
|
||||
@@ -126,7 +127,6 @@ func initMaps() {
|
||||
"rnostrictatime": {true, unix.MOUNT_ATTR_STRICTATIME},
|
||||
"rnosymfollow": {false, unix.MOUNT_ATTR_NOSYMFOLLOW}, // since kernel 5.14
|
||||
"rsymfollow": {true, unix.MOUNT_ATTR_NOSYMFOLLOW}, // since kernel 5.14
|
||||
// No support for MOUNT_ATTR_IDMAP yet (needs UserNS FD)
|
||||
}
|
||||
|
||||
extensionFlags = map[string]struct {
|
||||
@@ -135,6 +135,17 @@ func initMaps() {
|
||||
}{
|
||||
"tmpcopyup": {false, configs.EXT_COPYUP},
|
||||
}
|
||||
|
||||
complexFlags = map[string]func(*configs.Mount){
|
||||
"idmap": func(m *configs.Mount) {
|
||||
m.IDMapping = new(configs.MountIDMapping)
|
||||
m.IDMapping.Recursive = false // noop
|
||||
},
|
||||
"ridmap": func(m *configs.Mount) {
|
||||
m.IDMapping = new(configs.MountIDMapping)
|
||||
m.IDMapping.Recursive = true
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -415,6 +426,19 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
|
||||
if err := setupUserNamespace(spec, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// For idmap and ridmap mounts without explicit mappings, use the
|
||||
// ones from the container's userns. If we are joining another
|
||||
// userns, stash the path.
|
||||
for _, m := range config.Mounts {
|
||||
if m.IDMapping != nil && m.IDMapping.UIDMappings == nil && m.IDMapping.GIDMappings == nil {
|
||||
if path := config.Namespaces.PathOf(configs.NEWUSER); path != "" {
|
||||
m.IDMapping.UserNSPath = path
|
||||
} else {
|
||||
m.IDMapping.UIDMappings = config.UIDMappings
|
||||
m.IDMapping.GIDMappings = config.GIDMappings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
config.MaskPaths = spec.Linux.MaskedPaths
|
||||
config.ReadonlyPaths = spec.Linux.ReadonlyPaths
|
||||
@@ -447,6 +471,7 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
|
||||
Domain: domain,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Set the host UID that should own the container's cgroup.
|
||||
@@ -552,8 +577,14 @@ func createLibcontainerMount(cwd string, m specs.Mount) (*configs.Mount, error)
|
||||
}
|
||||
}
|
||||
|
||||
mnt.UIDMappings = toConfigIDMap(m.UIDMappings)
|
||||
mnt.GIDMappings = toConfigIDMap(m.GIDMappings)
|
||||
if m.UIDMappings != nil || m.GIDMappings != nil {
|
||||
if mnt.IDMapping == nil {
|
||||
// Neither "idmap" nor "ridmap" were specified.
|
||||
mnt.IDMapping = new(configs.MountIDMapping)
|
||||
}
|
||||
mnt.IDMapping.UIDMappings = toConfigIDMap(m.UIDMappings)
|
||||
mnt.IDMapping.GIDMappings = toConfigIDMap(m.GIDMappings)
|
||||
}
|
||||
|
||||
// None of the mount arguments can contain a null byte. Normally such
|
||||
// strings would either cause some other failure or would just be truncated
|
||||
@@ -1050,20 +1081,24 @@ func parseMountOptions(options []string) *configs.Mount {
|
||||
} else if f, exists := recAttrFlags[o]; exists {
|
||||
if f.clear {
|
||||
recAttrClr |= f.flag
|
||||
recAttrSet &= ^f.flag
|
||||
} else {
|
||||
recAttrSet |= f.flag
|
||||
recAttrClr &= ^f.flag
|
||||
if f.flag&unix.MOUNT_ATTR__ATIME == f.flag {
|
||||
// https://man7.org/linux/man-pages/man2/mount_setattr.2.html
|
||||
// "cannot simply specify the access-time setting in attr_set, but must also include MOUNT_ATTR__ATIME in the attr_clr field."
|
||||
recAttrClr |= unix.MOUNT_ATTR__ATIME
|
||||
}
|
||||
}
|
||||
} else if f, exists := extensionFlags[o]; exists && f.flag != 0 {
|
||||
} else if f, exists := extensionFlags[o]; exists {
|
||||
if f.clear {
|
||||
m.Extensions &= ^f.flag
|
||||
} else {
|
||||
m.Extensions |= f.flag
|
||||
}
|
||||
} else if fn, exists := complexFlags[o]; exists {
|
||||
fn(&m)
|
||||
} else {
|
||||
data = append(data, o)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/opencontainers/runc/libcontainer/keys"
|
||||
"github.com/opencontainers/runc/libcontainer/seccomp"
|
||||
"github.com/opencontainers/runc/libcontainer/system"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
)
|
||||
|
||||
type linuxStandardInit struct {
|
||||
@@ -27,7 +28,6 @@ type linuxStandardInit struct {
|
||||
fifoFd int
|
||||
logFd int
|
||||
dmzExe *os.File
|
||||
mountFds mountFds
|
||||
config *initConfig
|
||||
}
|
||||
|
||||
@@ -88,17 +88,7 @@ func (l *linuxStandardInit) Init() error {
|
||||
// initialises the labeling system
|
||||
selinux.GetEnabled()
|
||||
|
||||
// We don't need the mount nor idmap fds after prepareRootfs() nor if it fails.
|
||||
err := prepareRootfs(l.pipe, l.config, l.mountFds)
|
||||
for _, m := range append(l.mountFds.sourceFds, l.mountFds.idmapFds...) {
|
||||
if m == -1 {
|
||||
continue
|
||||
}
|
||||
if err := unix.Close(m); err != nil {
|
||||
return fmt.Errorf("unable to close mountFds fds: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err := prepareRootfs(l.pipe, l.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -258,11 +248,13 @@ func (l *linuxStandardInit) Init() error {
|
||||
return &os.PathError{Op: "close log pipe", Path: "fd " + strconv.Itoa(l.logFd), Err: err}
|
||||
}
|
||||
|
||||
fifoPath, closer := utils.ProcThreadSelf("fd/" + strconv.Itoa(l.fifoFd))
|
||||
defer closer()
|
||||
|
||||
// Wait for the FIFO to be opened on the other side before exec-ing the
|
||||
// user process. We open it through /proc/self/fd/$fd, because the fd that
|
||||
// was given to us was an O_PATH fd to the fifo itself. Linux allows us to
|
||||
// re-open an O_PATH fd through /proc.
|
||||
fifoPath := "/proc/self/fd/" + strconv.Itoa(l.fifoFd)
|
||||
fd, err := unix.Open(fifoPath, unix.O_WRONLY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "open exec fifo", Path: fifoPath, Err: err}
|
||||
|
||||
@@ -21,6 +21,11 @@ type syncType string
|
||||
//
|
||||
// [ child ] <-> [ parent ]
|
||||
//
|
||||
// procMountPlease --> [open(2) or open_tree(2) and configure mount]
|
||||
// Arg: configs.Mount
|
||||
// <-- procMountFd
|
||||
// file: mountfd
|
||||
//
|
||||
// procSeccomp --> [forward fd to listenerPath]
|
||||
// file: seccomp fd
|
||||
// --- no return synchronisation
|
||||
@@ -39,6 +44,8 @@ const (
|
||||
procRun syncType = "procRun"
|
||||
procHooks syncType = "procHooks"
|
||||
procHooksDone syncType = "procHooksDone"
|
||||
procMountPlease syncType = "procMountPlease"
|
||||
procMountFd syncType = "procMountFd"
|
||||
procSeccomp syncType = "procSeccomp"
|
||||
procSeccompDone syncType = "procSeccompDone"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package userns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
)
|
||||
|
||||
type Mapping struct {
|
||||
UIDMappings []configs.IDMap
|
||||
GIDMappings []configs.IDMap
|
||||
}
|
||||
|
||||
func (m Mapping) toSys() (uids, gids []syscall.SysProcIDMap) {
|
||||
for _, uid := range m.UIDMappings {
|
||||
uids = append(uids, syscall.SysProcIDMap{
|
||||
ContainerID: uid.ContainerID,
|
||||
HostID: uid.HostID,
|
||||
Size: uid.Size,
|
||||
})
|
||||
}
|
||||
for _, gid := range m.GIDMappings {
|
||||
gids = append(gids, syscall.SysProcIDMap{
|
||||
ContainerID: gid.ContainerID,
|
||||
HostID: gid.HostID,
|
||||
Size: gid.Size,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// id returns a unique identifier for this mapping, agnostic of the order of
|
||||
// the uid and gid mappings (because the order doesn't matter to the kernel).
|
||||
// The set of userns handles is indexed using this ID.
|
||||
func (m Mapping) id() string {
|
||||
var uids, gids []string
|
||||
for _, idmap := range m.UIDMappings {
|
||||
uids = append(uids, fmt.Sprintf("%d:%d:%d", idmap.ContainerID, idmap.HostID, idmap.Size))
|
||||
}
|
||||
for _, idmap := range m.GIDMappings {
|
||||
gids = append(gids, fmt.Sprintf("%d:%d:%d", idmap.ContainerID, idmap.HostID, idmap.Size))
|
||||
}
|
||||
// We don't care about the sort order -- just sort them.
|
||||
sort.Strings(uids)
|
||||
sort.Strings(gids)
|
||||
return "uid=" + strings.Join(uids, ",") + ";gid=" + strings.Join(gids, ",")
|
||||
}
|
||||
|
||||
type Handles struct {
|
||||
m sync.Mutex
|
||||
maps map[string]*os.File
|
||||
}
|
||||
|
||||
// Release all resources associated with this Handle. All existing files
|
||||
// returned from Get() will continue to work even after calling Release(). The
|
||||
// same Handles can be re-used after calling Release().
|
||||
func (hs *Handles) Release() {
|
||||
hs.m.Lock()
|
||||
defer hs.m.Unlock()
|
||||
|
||||
// Close the files for good measure, though GC will do that for us anyway.
|
||||
for _, file := range hs.maps {
|
||||
_ = file.Close()
|
||||
}
|
||||
hs.maps = nil
|
||||
}
|
||||
|
||||
func spawnProc(req Mapping) (*os.Process, error) {
|
||||
// We need to spawn a subprocess with the requested mappings, which is
|
||||
// unfortunately quite expensive. The "safe" way of doing this is natively
|
||||
// with Go (and then spawning something like "sleep infinity"), but
|
||||
// execve() is a waste of cycles because we just need some process to have
|
||||
// the right mapping, we don't care what it's executing. The "unsafe"
|
||||
// option of doing a clone() behind the back of Go is probably okay in
|
||||
// theory as long as we just do kill(getpid(), SIGSTOP). However, if we
|
||||
// tell Go to put the new process into PTRACE_TRACEME mode, we can avoid
|
||||
// the exec and not have to faff around with the mappings.
|
||||
//
|
||||
// Note that Go's stdlib does not support newuidmap, but in the case of
|
||||
// id-mapped mounts, it seems incredibly unlikely that the user will be
|
||||
// requesting us to do a remapping as an unprivileged user with mappings
|
||||
// they have privileges over.
|
||||
logrus.Debugf("spawning dummy process for id-mapping %s", req.id())
|
||||
uidMappings, gidMappings := req.toSys()
|
||||
// We don't need to use /proc/thread-self here because the exe mm of a
|
||||
// thread-group is guaranteed to be the same for all threads by definition.
|
||||
// This lets us avoid having to do runtime.LockOSThread.
|
||||
return os.StartProcess("/proc/self/exe", []string{"runc", "--help"}, &os.ProcAttr{
|
||||
Sys: &syscall.SysProcAttr{
|
||||
Cloneflags: unix.CLONE_NEWUSER,
|
||||
UidMappings: uidMappings,
|
||||
GidMappings: gidMappings,
|
||||
GidMappingsEnableSetgroups: false,
|
||||
// Put the process into PTRACE_TRACEME mode to allow us to get the
|
||||
// userns without having a proper execve() target.
|
||||
Ptrace: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func dupFile(f *os.File) (*os.File, error) {
|
||||
newFd, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err)
|
||||
}
|
||||
return os.NewFile(uintptr(newFd), f.Name()), nil
|
||||
}
|
||||
|
||||
// Get returns a handle to a /proc/$pid/ns/user nsfs file with the requested
|
||||
// mapping. The processes spawned to produce userns nsfds are cached, so if
|
||||
// equivalent user namespace mappings are requested, the same user namespace
|
||||
// will be returned. The caller is responsible for closing the returned file
|
||||
// descriptor.
|
||||
func (hs *Handles) Get(req Mapping) (file *os.File, err error) {
|
||||
hs.m.Lock()
|
||||
defer hs.m.Unlock()
|
||||
|
||||
if hs.maps == nil {
|
||||
hs.maps = make(map[string]*os.File)
|
||||
}
|
||||
|
||||
file, ok := hs.maps[req.id()]
|
||||
if !ok {
|
||||
proc, err := spawnProc(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to spawn dummy process for map %s: %w", req.id(), err)
|
||||
}
|
||||
// Make sure we kill the helper process. We ignore errors because
|
||||
// there's not much we can do about them anyway, and ultimately
|
||||
defer func() {
|
||||
_ = proc.Kill()
|
||||
_, _ = proc.Wait()
|
||||
}()
|
||||
|
||||
// Stash away a handle to the userns file. This is neater than keeping
|
||||
// the process alive, because Go's GC can handle files much better than
|
||||
// leaked processes, and having long-living useless processes seems
|
||||
// less than ideal.
|
||||
file, err = os.Open(fmt.Sprintf("/proc/%d/ns/user", proc.Pid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hs.maps[req.id()] = file
|
||||
}
|
||||
// Duplicate the file, to make sure the lifecycle of each *os.File we
|
||||
// return is independent.
|
||||
return dupFile(file)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package userns
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
)
|
||||
|
||||
func BenchmarkSpawnProc(b *testing.B) {
|
||||
if os.Geteuid() != 0 {
|
||||
b.Skip("spawning user namespaced processes requires root")
|
||||
}
|
||||
|
||||
// We can reuse the mapping as we call spawnProc() directly.
|
||||
mapping := Mapping{
|
||||
UIDMappings: []configs.IDMap{
|
||||
{ContainerID: 0, HostID: 1337, Size: 142},
|
||||
{ContainerID: 150, HostID: 0, Size: 1},
|
||||
{ContainerID: 442, HostID: 1111, Size: 12},
|
||||
{ContainerID: 1000, HostID: 9999, Size: 92},
|
||||
{ContainerID: 9999, HostID: 1000000, Size: 4},
|
||||
// Newer kernels support more than 5 entries, but stick to 5 here.
|
||||
},
|
||||
GIDMappings: []configs.IDMap{
|
||||
{ContainerID: 1, HostID: 2337, Size: 142},
|
||||
{ContainerID: 145, HostID: 6, Size: 1},
|
||||
{ContainerID: 200, HostID: 1000, Size: 12},
|
||||
{ContainerID: 1000, HostID: 9888, Size: 92},
|
||||
{ContainerID: 8999, HostID: 1000000, Size: 4},
|
||||
// Newer kernels support more than 5 entries, but stick to 5 here.
|
||||
},
|
||||
}
|
||||
|
||||
procs := make([]*os.Process, 0, b.N)
|
||||
b.Cleanup(func() {
|
||||
for _, proc := range procs {
|
||||
if proc != nil {
|
||||
_ = proc.Kill()
|
||||
_, _ = proc.Wait()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
proc, err := spawnProc(mapping)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
procs = append(procs, proc)
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,12 @@ package utils
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
@@ -102,39 +99,6 @@ func stripRoot(root, path string) string {
|
||||
return CleanPath("/" + path)
|
||||
}
|
||||
|
||||
// WithProcfd runs the passed closure with a procfd path (/proc/self/fd/...)
|
||||
// corresponding to the unsafePath resolved within the root. Before passing the
|
||||
// fd, this path is verified to have been inside the root -- so operating on it
|
||||
// through the passed fdpath should be safe. Do not access this path through
|
||||
// the original path strings, and do not attempt to use the pathname outside of
|
||||
// the passed closure (the file handle will be freed once the closure returns).
|
||||
func WithProcfd(root, unsafePath string, fn func(procfd string) error) error {
|
||||
// Remove the root then forcefully resolve inside the root.
|
||||
unsafePath = stripRoot(root, unsafePath)
|
||||
path, err := securejoin.SecureJoin(root, unsafePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving path inside rootfs failed: %w", err)
|
||||
}
|
||||
|
||||
// Open the target path.
|
||||
fh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open o_path procfd: %w", err)
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
// Double-check the path is the one we expected.
|
||||
procfd := "/proc/self/fd/" + strconv.Itoa(int(fh.Fd()))
|
||||
if realpath, err := os.Readlink(procfd); err != nil {
|
||||
return fmt.Errorf("procfd verification failed: %w", err)
|
||||
} else if realpath != path {
|
||||
return fmt.Errorf("possibly malicious path detected -- refusing to operate on %s", realpath)
|
||||
}
|
||||
|
||||
// Run the closure.
|
||||
return fn(procfd)
|
||||
}
|
||||
|
||||
// SearchLabels searches through a list of key=value pairs for a given key,
|
||||
// returning its value, and the binary flag telling whether the key exist.
|
||||
func SearchLabels(labels []string, key string) (string, bool) {
|
||||
|
||||
@@ -7,9 +7,13 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
@@ -57,7 +61,10 @@ func CloseExecFrom(minFd int) error {
|
||||
return os.NewSyscallError("close_range", err)
|
||||
}
|
||||
|
||||
fdDir, err := os.Open("/proc/self/fd")
|
||||
procSelfFd, closer := ProcThreadSelf("fd")
|
||||
defer closer()
|
||||
|
||||
fdDir, err := os.Open(procSelfFd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -98,3 +105,100 @@ func NewSockPair(name string) (parent, child *os.File, err error) {
|
||||
}
|
||||
return os.NewFile(uintptr(fds[1]), name+"-p"), os.NewFile(uintptr(fds[0]), name+"-c"), nil
|
||||
}
|
||||
|
||||
// WithProcfd runs the passed closure with a procfd path (/proc/self/fd/...)
|
||||
// corresponding to the unsafePath resolved within the root. Before passing the
|
||||
// fd, this path is verified to have been inside the root -- so operating on it
|
||||
// through the passed fdpath should be safe. Do not access this path through
|
||||
// the original path strings, and do not attempt to use the pathname outside of
|
||||
// the passed closure (the file handle will be freed once the closure returns).
|
||||
func WithProcfd(root, unsafePath string, fn func(procfd string) error) error {
|
||||
// Remove the root then forcefully resolve inside the root.
|
||||
unsafePath = stripRoot(root, unsafePath)
|
||||
path, err := securejoin.SecureJoin(root, unsafePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving path inside rootfs failed: %w", err)
|
||||
}
|
||||
|
||||
procSelfFd, closer := ProcThreadSelf("fd/")
|
||||
defer closer()
|
||||
|
||||
// Open the target path.
|
||||
fh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open o_path procfd: %w", err)
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
procfd := filepath.Join(procSelfFd, strconv.Itoa(int(fh.Fd())))
|
||||
// Double-check the path is the one we expected.
|
||||
if realpath, err := os.Readlink(procfd); err != nil {
|
||||
return fmt.Errorf("procfd verification failed: %w", err)
|
||||
} else if realpath != path {
|
||||
return fmt.Errorf("possibly malicious path detected -- refusing to operate on %s", realpath)
|
||||
}
|
||||
|
||||
return fn(procfd)
|
||||
}
|
||||
|
||||
type ProcThreadSelfCloser func()
|
||||
|
||||
var (
|
||||
haveProcThreadSelf bool
|
||||
haveProcThreadSelfOnce sync.Once
|
||||
)
|
||||
|
||||
// ProcThreadSelf returns a string that is equivalent to
|
||||
// /proc/thread-self/<subpath>, with a graceful fallback on older kernels where
|
||||
// /proc/thread-self doesn't exist. This method DOES NOT use SecureJoin,
|
||||
// meaning that the passed string needs to be trusted. The caller _must_ call
|
||||
// the returned procThreadSelfCloser function (which is runtime.UnlockOSThread)
|
||||
// *only once* after it has finished using the returned path string.
|
||||
func ProcThreadSelf(subpath string) (string, ProcThreadSelfCloser) {
|
||||
haveProcThreadSelfOnce.Do(func() {
|
||||
if _, err := os.Stat("/proc/thread-self/"); err == nil {
|
||||
haveProcThreadSelf = true
|
||||
} else {
|
||||
logrus.Debugf("cannot stat /proc/thread-self (%v), falling back to /proc/self/task/<tid>", err)
|
||||
}
|
||||
})
|
||||
|
||||
// We need to lock our thread until the caller is done with the path string
|
||||
// because any non-atomic operation on the path (such as opening a file,
|
||||
// then reading it) could be interrupted by the Go runtime where the
|
||||
// underlying thread is swapped out and the original thread is killed,
|
||||
// resulting in pull-your-hair-out-hard-to-debug issues in the caller. In
|
||||
// addition, the pre-3.17 fallback makes everything non-atomic because the
|
||||
// same thing could happen between unix.Gettid() and the path operations.
|
||||
//
|
||||
// In theory, we don't need to lock in the atomic user case when using
|
||||
// /proc/thread-self/, but it's better to be safe than sorry (and there are
|
||||
// only one or two truly atomic users of /proc/thread-self/).
|
||||
runtime.LockOSThread()
|
||||
|
||||
threadSelf := "/proc/thread-self/"
|
||||
if !haveProcThreadSelf {
|
||||
// Pre-3.17 kernels did not have /proc/thread-self, so do it manually.
|
||||
threadSelf = "/proc/self/task/" + strconv.Itoa(unix.Gettid()) + "/"
|
||||
if _, err := os.Stat(threadSelf); err != nil {
|
||||
// Unfortunately, this code is called from rootfs_linux.go where we
|
||||
// are running inside the pid namespace of the container but /proc
|
||||
// is the host's procfs. Unfortunately there is no real way to get
|
||||
// the correct tid to use here (the kernel age means we cannot do
|
||||
// things like set up a private fsopen("proc") -- even scanning
|
||||
// NSpid in all of the tasks in /proc/self/task/*/status requires
|
||||
// Linux 4.1).
|
||||
//
|
||||
// So, we just have to assume that /proc/self is acceptable in this
|
||||
// one specific case.
|
||||
if os.Getpid() == 1 {
|
||||
logrus.Debugf("/proc/thread-self (tid=%d) cannot be emulated inside the initial container setup -- using /proc/self instead: %v", unix.Gettid(), err)
|
||||
} else {
|
||||
// This should never happen, but the fallback should work in most cases...
|
||||
logrus.Warnf("/proc/thread-self could not be emulated for pid=%d (tid=%d) -- using more buggy /proc/self fallback instead: %v", os.Getpid(), unix.Gettid(), err)
|
||||
}
|
||||
threadSelf = "/proc/self/"
|
||||
}
|
||||
}
|
||||
return threadSelf + subpath, runtime.UnlockOSThread
|
||||
}
|
||||
|
||||
+624
-101
@@ -3,167 +3,690 @@
|
||||
load helpers
|
||||
|
||||
function setup() {
|
||||
OVERFLOW_UID="$(cat /proc/sys/kernel/overflowuid)"
|
||||
OVERFLOW_GID="$(cat /proc/sys/kernel/overflowgid)"
|
||||
requires root
|
||||
requires_kernel 5.12
|
||||
requires_idmap_fs /tmp
|
||||
|
||||
setup_debian
|
||||
requires_idmap_fs .
|
||||
|
||||
# Prepare source folders for bind mount
|
||||
mkdir -p source-{1,2}/
|
||||
touch source-{1,2}/foo.txt
|
||||
# Prepare source folders for mounts.
|
||||
mkdir -p source-{1,2,multi{1,2,3}}/
|
||||
touch source-{1,2,multi{1,2,3}}/foo.txt
|
||||
touch source-multi{1,2,3}/{bar,baz}.txt
|
||||
|
||||
# Use other owner for source-2
|
||||
# Change the owners for everything other than source-1.
|
||||
chown 1:1 source-2/foo.txt
|
||||
|
||||
mkdir -p rootfs/tmp/mount-{1,2}
|
||||
mkdir -p rootfs/mnt/bind-mount-{1,2}
|
||||
# A source with multiple users owning files.
|
||||
chown 100:211 source-multi1/foo.txt
|
||||
chown 101:222 source-multi1/bar.txt
|
||||
chown 102:233 source-multi1/baz.txt
|
||||
|
||||
update_config ' .linux.namespaces += [{"type": "user"}]
|
||||
| .linux.uidMappings += [{"hostID": 100000, "containerID": 0, "size": 65536}]
|
||||
| .linux.gidMappings += [{"hostID": 100000, "containerID": 0, "size": 65536}]
|
||||
| .process.args += ["-c", "stat -c =%u=%g= /tmp/mount-1/foo.txt"]
|
||||
| .mounts += [
|
||||
{
|
||||
"source": "source-1/",
|
||||
"destination": "/tmp/mount-1",
|
||||
"options": ["bind"],
|
||||
"uidMappings": [ {
|
||||
"containerID": 0,
|
||||
"hostID": 100000,
|
||||
"size": 65536
|
||||
}
|
||||
],
|
||||
"gidMappings": [ {
|
||||
"containerID": 0,
|
||||
"hostID": 100000,
|
||||
"size": 65536
|
||||
}
|
||||
]
|
||||
}
|
||||
] '
|
||||
# Same gids as multi1, different uids.
|
||||
chown 200:211 source-multi2/foo.txt
|
||||
chown 201:222 source-multi2/bar.txt
|
||||
chown 202:233 source-multi2/baz.txt
|
||||
|
||||
remap_rootfs
|
||||
# Even more users -- 1000 uids, 500 gids.
|
||||
chown 5000528:6000491 source-multi3/foo.txt
|
||||
chown 5000133:6000337 source-multi3/bar.txt
|
||||
chown 5000999:6000444 source-multi3/baz.txt
|
||||
|
||||
# Add a symlink-containing source.
|
||||
ln -s source-multi1 source-multi1-symlink
|
||||
|
||||
# Add some top-level files in the mount tree.
|
||||
mkdir -p mnt-subtree/multi{1,2}
|
||||
touch mnt-subtree/{foo,bar,baz}.txt
|
||||
chown 100:211 mnt-subtree/foo.txt
|
||||
chown 200:222 mnt-subtree/bar.txt
|
||||
chown 300:233 mnt-subtree/baz.txt
|
||||
|
||||
mounts_file="$PWD/.all-mounts"
|
||||
echo -n >"$mounts_file"
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
if [ -v mounts_file ]; then
|
||||
xargs -n 1 -a "$mounts_file" -- umount -l
|
||||
rm -f "$mounts_file"
|
||||
fi
|
||||
teardown_bundle
|
||||
}
|
||||
|
||||
@test "simple idmap mount" {
|
||||
function setup_host_bind_mount() {
|
||||
src="$1"
|
||||
dst="$2"
|
||||
|
||||
mount --bind "$src" "$dst"
|
||||
echo "$dst" >>"$mounts_file"
|
||||
}
|
||||
|
||||
function setup_idmap_userns() {
|
||||
update_config '.linux.namespaces += [{"type": "user"}]
|
||||
| .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}]
|
||||
| .linux.gidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}]'
|
||||
remap_rootfs
|
||||
}
|
||||
|
||||
function setup_bind_mount() {
|
||||
mountname="${1:-1}"
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "source-'"$mountname"'/",
|
||||
"destination": "/tmp/bind-mount-'"$mountname"'",
|
||||
"options": ["bind"]
|
||||
}
|
||||
]'
|
||||
}
|
||||
|
||||
function setup_idmap_single_mount() {
|
||||
uidmap="$1" # ctr:host:size
|
||||
gidmap="$2" # ctr:host:size
|
||||
mountname="$3"
|
||||
destname="${4:-$mountname}"
|
||||
|
||||
read -r uid_containerID uid_hostID uid_size <<<"$(tr : ' ' <<<"$uidmap")"
|
||||
read -r gid_containerID gid_hostID gid_size <<<"$(tr : ' ' <<<"$gidmap")"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "source-'"$mountname"'/",
|
||||
"destination": "/tmp/mount-'"$destname"'",
|
||||
"options": ["bind"],
|
||||
"uidMappings": [{"containerID": '"$uid_containerID"', "hostID": '"$uid_hostID"', "size": '"$uid_size"'}],
|
||||
"gidMappings": [{"containerID": '"$gid_containerID"', "hostID": '"$gid_hostID"', "size": '"$gid_size"'}]
|
||||
}
|
||||
]'
|
||||
}
|
||||
|
||||
function setup_idmap_basic_mount() {
|
||||
mountname="${1:-1}"
|
||||
setup_idmap_single_mount 0:100000:65536 0:100000:65536 "$mountname"
|
||||
}
|
||||
|
||||
@test "simple idmap mount [userns]" {
|
||||
setup_idmap_userns
|
||||
setup_idmap_basic_mount
|
||||
|
||||
update_config '.process.args = ["sh", "-c", "stat -c =%u=%g= /tmp/mount-1/foo.txt"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=0=0="* ]]
|
||||
}
|
||||
|
||||
@test "write to an idmap mount" {
|
||||
update_config ' .process.args = ["sh", "-c", "touch /tmp/mount-1/bar && stat -c =%u=%g= /tmp/mount-1/bar"]'
|
||||
@test "simple idmap mount [no userns]" {
|
||||
setup_idmap_basic_mount
|
||||
|
||||
update_config '.process.args = ["sh", "-c", "stat -c =%u=%g= /tmp/mount-1/foo.txt"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=100000=100000="* ]]
|
||||
}
|
||||
|
||||
@test "write to an idmap mount [userns]" {
|
||||
setup_idmap_userns
|
||||
setup_idmap_basic_mount
|
||||
|
||||
update_config '.process.args = ["sh", "-c", "touch /tmp/mount-1/bar && stat -c =%u=%g= /tmp/mount-1/bar"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=0=0="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount with propagation flag" {
|
||||
update_config ' .process.args = ["sh", "-c", "findmnt -o PROPAGATION /tmp/mount-1"]'
|
||||
# Add the shared option to the idmap mount
|
||||
update_config ' .mounts |= map((select(.source == "source-1/") | .options += ["shared"]) // .)'
|
||||
@test "write to an idmap mount [no userns]" {
|
||||
setup_idmap_basic_mount
|
||||
|
||||
update_config '.process.args = ["sh", "-c", "touch /tmp/mount-1/bar && stat -c =%u=%g= /tmp/mount-1/bar"]'
|
||||
|
||||
runc run test_debian
|
||||
# The write must fail because the user is unmapped.
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"Value too large for defined data type"* ]] # ERANGE
|
||||
}
|
||||
|
||||
@test "idmap mount with propagation flag [userns]" {
|
||||
setup_idmap_userns
|
||||
setup_idmap_basic_mount
|
||||
|
||||
update_config '.process.args = ["sh", "-c", "findmnt -o PROPAGATION /tmp/mount-1"]'
|
||||
# Add the shared option to the idmap mount.
|
||||
update_config '.mounts |= map((select(.source == "source-1/") | .options += ["shared"]) // .)'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"shared"* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount with relative path" {
|
||||
update_config ' .mounts |= map((select(.source == "source-1/") | .destination = "tmp/mount-1") // .)'
|
||||
@test "idmap mount with relative path [userns]" {
|
||||
setup_idmap_userns
|
||||
setup_idmap_basic_mount
|
||||
|
||||
update_config '.process.args = ["sh", "-c", "stat -c =%u=%g= /tmp/mount-1/foo.txt"]'
|
||||
# Switch the mount to have a relative mount destination.
|
||||
update_config '.mounts |= map((select(.source == "source-1/") | .destination = "tmp/mount-1") // .)'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=0=0="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount with bind mount" {
|
||||
update_config ' .mounts += [
|
||||
{
|
||||
"source": "source-2/",
|
||||
"destination": "/tmp/mount-2",
|
||||
"options": ["bind"]
|
||||
}
|
||||
] '
|
||||
@test "idmap mount with bind mount [userns]" {
|
||||
setup_idmap_userns
|
||||
setup_idmap_basic_mount
|
||||
setup_bind_mount
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/{,bind-}mount-1/foo.txt"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=0=0="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-1/foo.txt:0=0="* ]]
|
||||
[[ "$output" == *"=/tmp/bind-mount-1/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
}
|
||||
|
||||
@test "two idmap mounts with two bind mounts" {
|
||||
update_config ' .process.args = ["sh", "-c", "stat -c =%u=%g= /tmp/mount-1/foo.txt /tmp/mount-2/foo.txt"]
|
||||
| .mounts += [
|
||||
{
|
||||
"source": "source-1/",
|
||||
"destination": "/mnt/bind-mount-1",
|
||||
"options": ["bind"]
|
||||
},
|
||||
{
|
||||
"source": "source-2/",
|
||||
"destination": "/mnt/bind-mount-2",
|
||||
"options": ["bind"]
|
||||
},
|
||||
{
|
||||
"source": "source-2/",
|
||||
"destination": "/tmp/mount-2",
|
||||
"options": ["bind"],
|
||||
"uidMappings": [ {
|
||||
"containerID": 0,
|
||||
"hostID": 100000,
|
||||
"size": 65536
|
||||
}
|
||||
],
|
||||
"gidMappings": [ {
|
||||
"containerID": 0,
|
||||
"hostID": 100000,
|
||||
"size": 65536
|
||||
}
|
||||
]
|
||||
}
|
||||
] '
|
||||
@test "idmap mount with bind mount [no userns]" {
|
||||
setup_idmap_basic_mount
|
||||
setup_bind_mount
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/{,bind-}mount-1/foo.txt"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=0=0="* ]]
|
||||
# source-2/ is owned by 1:1, so we expect this with the idmap mount too.
|
||||
[[ "$output" == *"=1=1="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-1/foo.txt:100000=100000="* ]]
|
||||
[[ "$output" == *"=/tmp/bind-mount-1/foo.txt:0=0="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount without gidMappings fails" {
|
||||
update_config ' .mounts |= map((select(.source == "source-1/") | del(.gidMappings) ) // .)'
|
||||
@test "two idmap mounts (same mapping) with two bind mounts [userns]" {
|
||||
setup_idmap_userns
|
||||
|
||||
setup_idmap_basic_mount 1
|
||||
setup_bind_mount 1
|
||||
setup_bind_mount 2
|
||||
setup_idmap_basic_mount 2
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-[12]/foo.txt"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "${output}" == *"invalid mount"* ]]
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-1/foo.txt:0=0="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-2/foo.txt:1=1="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount without uidMappings fails" {
|
||||
update_config ' .mounts |= map((select(.source == "source-1/") | del(.uidMappings) ) // .)'
|
||||
@test "same idmap mount (different mappings) [userns]" {
|
||||
setup_idmap_userns
|
||||
|
||||
# Mount the same directory with different mappings. Make sure we also use
|
||||
# different mappings for uids and gids.
|
||||
setup_idmap_single_mount 100:100000:100 200:100000:100 multi1
|
||||
setup_idmap_single_mount 100:101000:100 200:102000:100 multi1 multi1-alt
|
||||
setup_idmap_single_mount 100:102000:100 200:103000:100 multi1-symlink multi1-alt-sym
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-multi1{,-alt{,-sym}}/{foo,bar,baz}.txt"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "${output}" == *"invalid mount"* ]]
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/foo.txt:0=11="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/bar.txt:1=22="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/baz.txt:2=33="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt/foo.txt:1000=2011="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt/bar.txt:1001=2022="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt/baz.txt:1002=2033="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt-sym/foo.txt:2000=3011="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt-sym/bar.txt:2001=3022="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt-sym/baz.txt:2002=3033="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount without bind fails" {
|
||||
update_config ' .mounts |= map((select(.source == "source-1/") | .options = [""]) // .)'
|
||||
@test "same idmap mount (different mappings) [no userns]" {
|
||||
# Mount the same directory with different mappings. Make sure we also use
|
||||
# different mappings for uids and gids.
|
||||
setup_idmap_single_mount 100:100000:100 200:100000:100 multi1
|
||||
setup_idmap_single_mount 100:101000:100 200:102000:100 multi1 multi1-alt
|
||||
setup_idmap_single_mount 100:102000:100 200:103000:100 multi1-symlink multi1-alt-sym
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-multi1{,-alt{,-sym}}/{foo,bar,baz}.txt"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "${output}" == *"invalid mount"* ]]
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/foo.txt:100000=100011="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/bar.txt:100001=100022="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/baz.txt:100002=100033="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt/foo.txt:101000=102011="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt/bar.txt:101001=102022="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt/baz.txt:101002=102033="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt-sym/foo.txt:102000=103011="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt-sym/bar.txt:102001=103022="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1-alt-sym/baz.txt:102002=103033="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount with different mapping than userns fails" {
|
||||
# Let's modify the containerID to be 1, instead of 0 as it is in the
|
||||
# userns config.
|
||||
update_config ' .mounts |= map((select(.source == "source-1/") | .uidMappings[0]["containerID"] = 1 ) // .)'
|
||||
@test "multiple idmap mounts (different mappings) [userns]" {
|
||||
setup_idmap_userns
|
||||
|
||||
# Make sure we use different mappings for uids and gids.
|
||||
setup_idmap_single_mount 100:101100:3 200:101900:50 multi1
|
||||
setup_idmap_single_mount 200:102200:3 200:102900:100 multi2
|
||||
setup_idmap_single_mount 5000000:103000:1000 6000000:103000:500 multi3
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-multi[123]/{foo,bar,baz}.txt"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "${output}" == *"invalid mount"* ]]
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/foo.txt:1100=1911="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/bar.txt:1101=1922="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/baz.txt:1102=1933="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi2/foo.txt:2200=2911="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi2/bar.txt:2201=2922="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi2/baz.txt:2202=2933="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi3/foo.txt:3528=3491="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi3/bar.txt:3133=3337="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi3/baz.txt:3999=3444="* ]]
|
||||
}
|
||||
|
||||
@test "multiple idmap mounts (different mappings) [no userns]" {
|
||||
# Make sure we use different mappings for uids and gids.
|
||||
setup_idmap_single_mount 100:1100:3 200:1900:50 multi1
|
||||
setup_idmap_single_mount 200:2200:3 200:2900:100 multi2
|
||||
setup_idmap_single_mount 5000000:3000:1000 6000000:3000:500 multi3
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-multi[123]/{foo,bar,baz}.txt"]'
|
||||
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/foo.txt:1100=1911="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/bar.txt:1101=1922="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/baz.txt:1102=1933="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi2/foo.txt:2200=2911="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi2/bar.txt:2201=2922="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi2/baz.txt:2202=2933="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi3/foo.txt:3528=3491="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi3/bar.txt:3133=3337="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi3/baz.txt:3999=3444="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (complicated mapping) [userns]" {
|
||||
setup_idmap_userns
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "source-multi1/",
|
||||
"destination": "/tmp/mount-multi1",
|
||||
"options": ["bind"],
|
||||
"uidMappings": [
|
||||
{"containerID": 100, "hostID": 101000, "size": 1},
|
||||
{"containerID": 101, "hostID": 102000, "size": 1},
|
||||
{"containerID": 102, "hostID": 103000, "size": 1}
|
||||
],
|
||||
"gidMappings": [
|
||||
{"containerID": 210, "hostID": 101100, "size": 10},
|
||||
{"containerID": 220, "hostID": 102200, "size": 10},
|
||||
{"containerID": 230, "hostID": 103300, "size": 10}
|
||||
]
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-multi1/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/foo.txt:1000=1101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/bar.txt:2000=2202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/baz.txt:3000=3303="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (complicated mapping) [no userns]" {
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "source-multi1/",
|
||||
"destination": "/tmp/mount-multi1",
|
||||
"options": ["bind"],
|
||||
"uidMappings": [
|
||||
{"containerID": 100, "hostID": 1000, "size": 1},
|
||||
{"containerID": 101, "hostID": 2000, "size": 1},
|
||||
{"containerID": 102, "hostID": 3000, "size": 1}
|
||||
],
|
||||
"gidMappings": [
|
||||
{"containerID": 210, "hostID": 1100, "size": 10},
|
||||
{"containerID": 220, "hostID": 2200, "size": 10},
|
||||
{"containerID": 230, "hostID": 3300, "size": 10}
|
||||
]
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-multi1/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/foo.txt:1000=1101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/bar.txt:2000=2202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-multi1/baz.txt:3000=3303="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (non-recursive idmap) [userns]" {
|
||||
setup_idmap_userns
|
||||
|
||||
setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1"
|
||||
setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "mnt-subtree/",
|
||||
"destination": "/tmp/mount-tree",
|
||||
"options": ["rbind"],
|
||||
"uidMappings": [
|
||||
{"containerID": 100, "hostID": 101000, "size": 3},
|
||||
{"containerID": 200, "hostID": 102000, "size": 3},
|
||||
{"containerID": 300, "hostID": 103000, "size": 3}
|
||||
],
|
||||
"gidMappings": [
|
||||
{"containerID": 210, "hostID": 101100, "size": 10},
|
||||
{"containerID": 220, "hostID": 102200, "size": 10},
|
||||
{"containerID": 230, "hostID": 103300, "size": 10}
|
||||
]
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-tree/foo.txt:1000=1101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/bar.txt:2000=2202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/baz.txt:3000=3303="* ]]
|
||||
# Because we used "idmap", the child mounts were not remapped recursively.
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (non-recursive idmap) [no userns]" {
|
||||
setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1"
|
||||
setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "mnt-subtree/",
|
||||
"destination": "/tmp/mount-tree",
|
||||
"options": ["rbind"],
|
||||
"uidMappings": [
|
||||
{"containerID": 100, "hostID": 101000, "size": 3},
|
||||
{"containerID": 200, "hostID": 102000, "size": 3},
|
||||
{"containerID": 300, "hostID": 103000, "size": 3}
|
||||
],
|
||||
"gidMappings": [
|
||||
{"containerID": 210, "hostID": 101100, "size": 10},
|
||||
{"containerID": 220, "hostID": 102200, "size": 10},
|
||||
{"containerID": 230, "hostID": 103300, "size": 10}
|
||||
]
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-tree/foo.txt:101000=101101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/bar.txt:102000=102202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/baz.txt:103000=103303="* ]]
|
||||
# Because we used "idmap", the child mounts were not remapped recursively.
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:100=211="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:101=222="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:102=233="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:200=211="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:201=222="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:202=233="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (idmap flag) [userns]" {
|
||||
setup_idmap_userns
|
||||
|
||||
setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1"
|
||||
setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "mnt-subtree/",
|
||||
"destination": "/tmp/mount-tree",
|
||||
"options": ["rbind", "idmap"],
|
||||
"uidMappings": [
|
||||
{"containerID": 100, "hostID": 101000, "size": 3},
|
||||
{"containerID": 200, "hostID": 102000, "size": 3},
|
||||
{"containerID": 300, "hostID": 103000, "size": 3}
|
||||
],
|
||||
"gidMappings": [
|
||||
{"containerID": 210, "hostID": 101100, "size": 10},
|
||||
{"containerID": 220, "hostID": 102200, "size": 10},
|
||||
{"containerID": 230, "hostID": 103300, "size": 10}
|
||||
]
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-tree/foo.txt:1000=1101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/bar.txt:2000=2202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/baz.txt:3000=3303="* ]]
|
||||
# Because we used "idmap", the child mounts were not remapped recursively.
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (idmap flag) [no userns]" {
|
||||
setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1"
|
||||
setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "mnt-subtree/",
|
||||
"destination": "/tmp/mount-tree",
|
||||
"options": ["rbind", "idmap"],
|
||||
"uidMappings": [
|
||||
{"containerID": 100, "hostID": 101000, "size": 3},
|
||||
{"containerID": 200, "hostID": 102000, "size": 3},
|
||||
{"containerID": 300, "hostID": 103000, "size": 3}
|
||||
],
|
||||
"gidMappings": [
|
||||
{"containerID": 210, "hostID": 101100, "size": 10},
|
||||
{"containerID": 220, "hostID": 102200, "size": 10},
|
||||
{"containerID": 230, "hostID": 103300, "size": 10}
|
||||
]
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-tree/foo.txt:101000=101101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/bar.txt:102000=102202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/baz.txt:103000=103303="* ]]
|
||||
# Because we used "idmap", the child mounts were not remapped recursively.
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:100=211="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:101=222="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:102=233="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:200=211="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:201=222="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:202=233="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (ridmap flag) [userns]" {
|
||||
setup_idmap_userns
|
||||
|
||||
setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1"
|
||||
setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "mnt-subtree/",
|
||||
"destination": "/tmp/mount-tree",
|
||||
"options": ["rbind", "ridmap"],
|
||||
"uidMappings": [
|
||||
{"containerID": 100, "hostID": 101000, "size": 3},
|
||||
{"containerID": 200, "hostID": 102000, "size": 3},
|
||||
{"containerID": 300, "hostID": 103000, "size": 3}
|
||||
],
|
||||
"gidMappings": [
|
||||
{"containerID": 210, "hostID": 101100, "size": 10},
|
||||
{"containerID": 220, "hostID": 102200, "size": 10},
|
||||
{"containerID": 230, "hostID": 103300, "size": 10}
|
||||
]
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-tree/foo.txt:1000=1101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/bar.txt:2000=2202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/baz.txt:3000=3303="* ]]
|
||||
# The child mounts have the same mapping applied.
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:1000=1101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:1001=2202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:1002=3303="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:2000=1101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:2001=2202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:2002=3303="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (ridmap flag) [no userns]" {
|
||||
setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1"
|
||||
setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "mnt-subtree/",
|
||||
"destination": "/tmp/mount-tree",
|
||||
"options": ["rbind", "ridmap"],
|
||||
"uidMappings": [
|
||||
{"containerID": 100, "hostID": 101000, "size": 3},
|
||||
{"containerID": 200, "hostID": 102000, "size": 3},
|
||||
{"containerID": 300, "hostID": 103000, "size": 3}
|
||||
],
|
||||
"gidMappings": [
|
||||
{"containerID": 210, "hostID": 101100, "size": 10},
|
||||
{"containerID": 220, "hostID": 102200, "size": 10},
|
||||
{"containerID": 230, "hostID": 103300, "size": 10}
|
||||
]
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-tree/foo.txt:101000=101101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/bar.txt:102000=102202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/baz.txt:103000=103303="* ]]
|
||||
# The child mounts have the same mapping applied.
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:101000=101101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:101001=102202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:101002=103303="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:102000=101101="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:102001=102202="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:102002=103303="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (idmap flag, implied mapping) [userns]" {
|
||||
setup_idmap_userns
|
||||
|
||||
setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1"
|
||||
setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "mnt-subtree/",
|
||||
"destination": "/tmp/mount-tree",
|
||||
"options": ["rbind", "idmap"],
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-tree/foo.txt:100=211="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/bar.txt:200=222="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/baz.txt:300=233="* ]]
|
||||
# Because we used "idmap", the child mounts were not remapped recursively.
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (ridmap flag, implied mapping) [userns]" {
|
||||
setup_idmap_userns
|
||||
|
||||
setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1"
|
||||
setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "mnt-subtree/",
|
||||
"destination": "/tmp/mount-tree",
|
||||
"options": ["rbind", "ridmap"],
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-tree/foo.txt:100=211="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/bar.txt:200=222="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/baz.txt:300=233="* ]]
|
||||
# The child mounts have the same mapping applied.
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:100=211="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:101=222="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:102=233="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:200=211="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:201=222="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:202=233="* ]]
|
||||
}
|
||||
|
||||
@test "idmap mount (idmap flag, implied mapping, userns join) [userns]" {
|
||||
# Create a detached container with the id-mapping we want.
|
||||
cp config.json config.json.bak
|
||||
update_config '.linux.namespaces += [{"type": "user"}]
|
||||
| .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}]
|
||||
| .linux.gidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}]'
|
||||
update_config '.process.args = ["sleep", "infinity"]'
|
||||
|
||||
runc run -d --console-socket "$CONSOLE_SOCKET" target_userns
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Configure our container to attach to the first container's userns.
|
||||
target_pid="$(__runc state target_userns | jq .pid)"
|
||||
update_config '.linux.namespaces |= map(if .type == "user" then (.path = "/proc/'"$target_pid"'/ns/" + .type) else . end)'
|
||||
update_config 'del(.linux.uidMappings) | del(.linux.gidMappings)'
|
||||
|
||||
setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1"
|
||||
setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2"
|
||||
|
||||
update_config '.mounts += [
|
||||
{
|
||||
"source": "mnt-subtree/",
|
||||
"destination": "/tmp/mount-tree",
|
||||
"options": ["rbind", "idmap"],
|
||||
}
|
||||
]'
|
||||
|
||||
update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]'
|
||||
runc run test_debian
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"=/tmp/mount-tree/foo.txt:100=211="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/bar.txt:200=222="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/baz.txt:300=233="* ]]
|
||||
# Because we used "idmap", the child mounts were not remapped recursively.
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
[[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]]
|
||||
}
|
||||
|
||||
@@ -24,6 +24,109 @@ function test_ro_cgroup_mount() {
|
||||
for line in "${lines[@]}"; do [[ "${line}" == *'ro,'* ]]; done
|
||||
}
|
||||
|
||||
# Parse an "optstring" of the form foo,bar into $is_foo and $is_bar variables.
|
||||
# Usage: parse_optstring foo,bar foo bar baz
|
||||
function parse_optstring() {
|
||||
optstring="$1"
|
||||
shift
|
||||
|
||||
for flag in "$@"; do
|
||||
is_set=
|
||||
if grep -wq "$flag" <<<"$optstring"; then
|
||||
is_set=1
|
||||
fi
|
||||
eval "is_$flag=$is_set"
|
||||
done
|
||||
}
|
||||
|
||||
function config_add_bind_mount() {
|
||||
src="$1"
|
||||
dst="$2"
|
||||
parse_optstring "${3:-}" rbind idmap
|
||||
|
||||
bindtype=bind
|
||||
if [ -n "$is_rbind" ]; then
|
||||
bindtype=rbind
|
||||
fi
|
||||
|
||||
mappings=""
|
||||
if [ -n "$is_idmap" ]; then
|
||||
mappings='
|
||||
"uidMappings": [{"containerID": 0, "hostID": 100000, "size": 65536}],
|
||||
"gidMappings": [{"containerID": 0, "hostID": 100000, "size": 65536}],
|
||||
'
|
||||
fi
|
||||
|
||||
update_config '.mounts += [{
|
||||
"source": "'"$src"'",
|
||||
"destination": "'"$dst"'",
|
||||
"type": "bind",
|
||||
'"$mappings"'
|
||||
"options": [ "'"$bindtype"'", "rprivate" ]
|
||||
}]'
|
||||
}
|
||||
|
||||
# This needs to be placed at the top of the bats file to work around
|
||||
# a shellcheck bug. See <https://github.com/koalaman/shellcheck/issues/2873>.
|
||||
function test_mount_order() {
|
||||
parse_optstring "${1:-}" userns idmap
|
||||
|
||||
if [ -n "$is_userns" ]; then
|
||||
requires root
|
||||
|
||||
update_config '.linux.namespaces += [{"type": "user"}]
|
||||
| .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}]
|
||||
| .linux.gidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}]'
|
||||
remap_rootfs
|
||||
fi
|
||||
|
||||
ctr_src_opts="rbind"
|
||||
if [ -n "$is_idmap" ]; then
|
||||
requires root
|
||||
requires_kernel 5.12
|
||||
requires_idmap_fs .
|
||||
|
||||
ctr_src_opts+=",idmap"
|
||||
fi
|
||||
|
||||
mkdir -p rootfs/{mnt,final}
|
||||
# Create a set of directories we can create a mount tree with.
|
||||
for subdir in a/x b/y c/z; do
|
||||
dir="bind-src/$subdir"
|
||||
mkdir -p "$dir"
|
||||
echo "$subdir" >"$dir/file"
|
||||
# Add a symlink to make sure
|
||||
topdir="$(dirname "$subdir")"
|
||||
ln -s "$topdir" "bind-src/sym-$topdir"
|
||||
done
|
||||
# In userns tests, make sure that the source directory cannot be accessed,
|
||||
# to make sure we're exercising the bind-mount source fd logic.
|
||||
chmod o-rwx bind-src
|
||||
|
||||
rootfs="$(pwd)/rootfs"
|
||||
rm -rf rootfs/mnt
|
||||
mkdir rootfs/mnt
|
||||
|
||||
# Create a bind-mount tree.
|
||||
config_add_bind_mount "$PWD/bind-src/a" "/mnt"
|
||||
config_add_bind_mount "$PWD/bind-src/sym-b" "/mnt/x"
|
||||
config_add_bind_mount "$PWD/bind-src/c" "/mnt/x/y"
|
||||
config_add_bind_mount "$PWD/bind-src/sym-a" "/mnt/x/y/z"
|
||||
# Create a recursive bind-mount that uses part of the current tree in the
|
||||
# container.
|
||||
config_add_bind_mount "$rootfs/mnt/x" "$rootfs/mnt/x/y/z/x" "$ctr_src_opts"
|
||||
config_add_bind_mount "$rootfs/mnt/x/y" "$rootfs/mnt/x/y/z" "$ctr_src_opts"
|
||||
# Finally, bind-mount the whole thing on top of /final.
|
||||
config_add_bind_mount "$rootfs/mnt" "$rootfs/final" "$ctr_src_opts"
|
||||
|
||||
# Check that the entire tree was copied and the mounts were done in the
|
||||
# expected order.
|
||||
update_config '.process.args = ["cat", "/final/x/y/z/z/x/y/z/x/file"]'
|
||||
runc run test_busybox
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"a/x"* ]] # the final "file" was from a/x.
|
||||
}
|
||||
|
||||
# https://github.com/opencontainers/runc/issues/3991
|
||||
@test "runc run [tmpcopyup]" {
|
||||
mkdir -p rootfs/dir1/dir2
|
||||
@@ -108,3 +211,19 @@ function test_ro_cgroup_mount() {
|
||||
update_config '.linux.namespaces |= if index({"type": "cgroup"}) then . else . + [{"type": "cgroup"}] end'
|
||||
test_ro_cgroup_mount
|
||||
}
|
||||
|
||||
@test "runc run [mount order, container bind-mount source]" {
|
||||
test_mount_order
|
||||
}
|
||||
|
||||
@test "runc run [mount order, container bind-mount source] (userns)" {
|
||||
test_mount_order userns
|
||||
}
|
||||
|
||||
@test "runc run [mount order, container idmap source]" {
|
||||
test_mount_order idmap
|
||||
}
|
||||
|
||||
@test "runc run [mount order, container idmap source] (userns)" {
|
||||
test_mount_order userns,idmap
|
||||
}
|
||||
|
||||
+3
-1
@@ -224,8 +224,10 @@ func (r *runner) run(config *specs.Process) (int, error) {
|
||||
process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...)
|
||||
}
|
||||
baseFd := 3 + len(process.ExtraFiles)
|
||||
procSelfFd, closer := utils.ProcThreadSelf("fd/")
|
||||
defer closer()
|
||||
for i := baseFd; i < baseFd+r.preserveFDs; i++ {
|
||||
_, err = os.Stat("/proc/self/fd/" + strconv.Itoa(i))
|
||||
_, err = os.Stat(filepath.Join(procSelfFd, strconv.Itoa(i)))
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("unable to stat preserved-fd %d (of %d): %w", i-baseFd, r.preserveFDs, err)
|
||||
}
|
||||
|
||||
+43
-7
@@ -5,15 +5,19 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// GetMountsFromReader retrieves a list of mounts from the
|
||||
// reader provided, with an optional filter applied (use nil
|
||||
// for no filter). This can be useful in tests or benchmarks
|
||||
// that provide fake mountinfo data, or when a source other
|
||||
// than /proc/self/mountinfo needs to be read from.
|
||||
// than /proc/thread-self/mountinfo needs to be read from.
|
||||
//
|
||||
// This function is Linux-specific.
|
||||
func GetMountsFromReader(r io.Reader, filter FilterFunc) ([]*Info, error) {
|
||||
@@ -127,8 +131,40 @@ func GetMountsFromReader(r io.Reader, filter FilterFunc) ([]*Info, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseMountTable(filter FilterFunc) ([]*Info, error) {
|
||||
f, err := os.Open("/proc/self/mountinfo")
|
||||
var (
|
||||
haveProcThreadSelf bool
|
||||
haveProcThreadSelfOnce sync.Once
|
||||
)
|
||||
|
||||
func parseMountTable(filter FilterFunc) (_ []*Info, err error) {
|
||||
haveProcThreadSelfOnce.Do(func() {
|
||||
_, err := os.Stat("/proc/thread-self/mountinfo")
|
||||
haveProcThreadSelf = err == nil
|
||||
})
|
||||
|
||||
// We need to lock ourselves to the current OS thread in order to make sure
|
||||
// that the thread referenced by /proc/thread-self stays alive until we
|
||||
// finish parsing the file.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
var f *os.File
|
||||
if haveProcThreadSelf {
|
||||
f, err = os.Open("/proc/thread-self/mountinfo")
|
||||
} else {
|
||||
// On pre-3.17 kernels (such as CentOS 7), we don't have
|
||||
// /proc/thread-self/ so we need to manually construct
|
||||
// /proc/self/task/<tid>/ as a fallback.
|
||||
f, err = os.Open("/proc/self/task/" + strconv.Itoa(unix.Gettid()) + "/mountinfo")
|
||||
if os.IsNotExist(err) {
|
||||
// If /proc/self/task/... failed, it means that our active pid
|
||||
// namespace doesn't match the pid namespace of the /proc mount. In
|
||||
// this case we just have to make do with /proc/self, since there
|
||||
// is no other way of figuring out our tid in a parent pid
|
||||
// namespace on pre-3.17 kernels.
|
||||
f, err = os.Open("/proc/self/mountinfo")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -158,10 +194,10 @@ func PidMountInfo(pid int) ([]*Info, error) {
|
||||
// A few specific characters in mountinfo path entries (root and mountpoint)
|
||||
// are escaped using a backslash followed by a character's ascii code in octal.
|
||||
//
|
||||
// space -- as \040
|
||||
// tab (aka \t) -- as \011
|
||||
// newline (aka \n) -- as \012
|
||||
// backslash (aka \\) -- as \134
|
||||
// space -- as \040
|
||||
// tab (aka \t) -- as \011
|
||||
// newline (aka \n) -- as \012
|
||||
// backslash (aka \\) -- as \134
|
||||
//
|
||||
// This function converts path from mountinfo back, i.e. it unescapes the above sequences.
|
||||
func unescape(path string) (string, error) {
|
||||
|
||||
Vendored
+1
-1
@@ -33,7 +33,7 @@ github.com/docker/go-units
|
||||
# github.com/godbus/dbus/v5 v5.1.0
|
||||
## explicit; go 1.12
|
||||
github.com/godbus/dbus/v5
|
||||
# github.com/moby/sys/mountinfo v0.6.2
|
||||
# github.com/moby/sys/mountinfo v0.7.1
|
||||
## explicit; go 1.16
|
||||
github.com/moby/sys/mountinfo
|
||||
# github.com/moby/sys/user v0.1.0
|
||||
|
||||
Reference in New Issue
Block a user