From 46115504ec3221fb9d9863ad4999a1244baea6fc Mon Sep 17 00:00:00 2001 From: Andrei Vagin Date: Mon, 24 Jul 2023 15:42:22 -0700 Subject: [PATCH] Implement the setns syscall This change introduces the nsfs file system. Each new namespace allocates a new nsfs inode. Here are reasons why we need these inodes: * each namespace has to have an unique id. * proc/pid/ns/ contains one entry for each namespace. Bind mounting one of the files in this directory to somewhere else in the filesystem keeps the corresponding namespace alive even if all processes currently in the namespace terminate. * setns() allows the calling process to join an existing namespace specified by a file descriptor. PiperOrigin-RevId: 550694515 --- pkg/abi/linux/fs.go | 1 + pkg/sentry/fsimpl/nsfs/BUILD | 39 +++++ pkg/sentry/fsimpl/nsfs/nsfs.go | 205 +++++++++++++++++++++++ pkg/sentry/fsimpl/proc/BUILD | 1 + pkg/sentry/fsimpl/proc/task.go | 2 +- pkg/sentry/fsimpl/proc/task_files.go | 40 ++++- pkg/sentry/fsutil/BUILD | 1 - pkg/sentry/inet/BUILD | 2 + pkg/sentry/inet/namespace.go | 58 +++++-- pkg/sentry/kernel/BUILD | 1 + pkg/sentry/kernel/kernel.go | 23 ++- pkg/sentry/kernel/task.go | 4 +- pkg/sentry/kernel/task_clone.go | 59 +++++-- pkg/sentry/kernel/task_exit.go | 4 +- pkg/sentry/kernel/task_net.go | 14 ++ pkg/sentry/kernel/task_start.go | 2 +- pkg/sentry/socket/netstack/netstack.go | 2 +- pkg/sentry/socket/unix/transport/BUILD | 1 - pkg/sentry/syscalls/linux/linux64.go | 4 +- pkg/sentry/syscalls/linux/sys_mount.go | 2 +- pkg/sentry/syscalls/linux/sys_thread.go | 14 ++ runsc/boot/loader.go | 18 +- test/syscalls/linux/BUILD | 2 + test/syscalls/linux/network_namespace.cc | 54 ++++++ 24 files changed, 509 insertions(+), 44 deletions(-) create mode 100644 pkg/sentry/fsimpl/nsfs/BUILD create mode 100644 pkg/sentry/fsimpl/nsfs/nsfs.go diff --git a/pkg/abi/linux/fs.go b/pkg/abi/linux/fs.go index edc90e54c..28942326d 100644 --- a/pkg/abi/linux/fs.go +++ b/pkg/abi/linux/fs.go @@ -24,6 +24,7 @@ const ( EXT_SUPER_MAGIC = 0xef53 FUSE_SUPER_MAGIC = 0x65735546 MQUEUE_MAGIC = 0x19800202 + NSFS_MAGIC = 0x6e736673 OVERLAYFS_SUPER_MAGIC = 0x794c7630 PIPEFS_MAGIC = 0x50495045 PROC_SUPER_MAGIC = 0x9fa0 diff --git a/pkg/sentry/fsimpl/nsfs/BUILD b/pkg/sentry/fsimpl/nsfs/BUILD new file mode 100644 index 000000000..07f20d601 --- /dev/null +++ b/pkg/sentry/fsimpl/nsfs/BUILD @@ -0,0 +1,39 @@ +load("//tools:defs.bzl", "go_library") +load("//tools/go_generics:defs.bzl", "go_template_instance") + +package(default_applicable_licenses = ["//:license"]) + +licenses(["notice"]) + +go_template_instance( + name = "inode_refs", + out = "inode_refs.go", + package = "nsfs", + prefix = "inode", + template = "//pkg/refs:refs_template", + types = { + "T": "Inode", + }, +) + +go_library( + name = "nsfs", + srcs = [ + "inode_refs.go", + "nsfs.go", + ], + visibility = ["//pkg/sentry:internal"], + deps = [ + "//pkg/abi/linux", + "//pkg/atomicbitops", + "//pkg/context", + "//pkg/errors/linuxerr", + "//pkg/hostarch", + "//pkg/refs", + "//pkg/sentry/fsimpl/kernfs", + "//pkg/sentry/kernel/auth", + "//pkg/sentry/kernel/time", + "//pkg/sentry/vfs", + "//pkg/sync", + ], +) diff --git a/pkg/sentry/fsimpl/nsfs/nsfs.go b/pkg/sentry/fsimpl/nsfs/nsfs.go new file mode 100644 index 000000000..eb2466db8 --- /dev/null +++ b/pkg/sentry/fsimpl/nsfs/nsfs.go @@ -0,0 +1,205 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package nsfs provides the filesystem implementation backing +// Kernel.NsfsMount. +package nsfs + +import ( + "fmt" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" + "gvisor.dev/gvisor/pkg/sentry/vfs" +) + +// +stateify savable +type filesystemType struct{} + +// Name implements vfs.FilesystemType.Name. +func (filesystemType) Name() string { + return "nsfs" +} + +// Release implements vfs.FilesystemType.Release. +func (filesystemType) Release(ctx context.Context) {} + +// GetFilesystem implements vfs.FilesystemType.GetFilesystem. +func (filesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, source string, opts vfs.GetFilesystemOptions) (*vfs.Filesystem, *vfs.Dentry, error) { + panic("nsfs.filesystemType.GetFilesystem should never be called") +} + +// +stateify savable +type filesystem struct { + kernfs.Filesystem + + devMinor uint32 +} + +// NewFilesystem sets up and returns a new vfs.Filesystem implemented by nsfs. +func NewFilesystem(vfsObj *vfs.VirtualFilesystem) (*vfs.Filesystem, error) { + devMinor, err := vfsObj.GetAnonBlockDevMinor() + if err != nil { + return nil, err + } + fs := &filesystem{ + devMinor: devMinor, + } + fs.Filesystem.VFSFilesystem().Init(vfsObj, filesystemType{}, fs) + return fs.Filesystem.VFSFilesystem(), nil +} + +// Release implements vfs.FilesystemImpl.Release. +func (fs *filesystem) Release(ctx context.Context) { + fs.Filesystem.VFSFilesystem().VirtualFilesystem().PutAnonBlockDevMinor(fs.devMinor) + fs.Filesystem.Release(ctx) +} + +// MountOptions implements vfs.FilesystemImpl.MountOptions. +func (fs *filesystem) MountOptions() string { + return "" +} + +// Inode implements kernfs.Inode. +// +// +stateify savable +type Inode struct { + kernfs.InodeAttrs + kernfs.InodeNotAnonymous + kernfs.InodeNotDirectory + kernfs.InodeNotSymlink + kernfs.InodeWatches + inodeRefs + + locks vfs.FileLocks + namespace Namespace + + mnt *vfs.Mount +} + +// DecRef implements kernfs.Inode.DecRef. +func (i *Inode) DecRef(ctx context.Context) { + i.inodeRefs.DecRef(func() { i.namespace.Destroy(ctx) }) +} + +// Keep implements kernfs.Inode.Keep. +func (i *Inode) Keep() bool { + return false +} + +// Namespace is the namespace interface. +type Namespace interface { + Type() string + Destroy(ctx context.Context) +} + +// NewInode creates a new nsfs inode. +func NewInode(ctx context.Context, mnt *vfs.Mount, namespace Namespace) *Inode { + fs := mnt.Filesystem().Impl().(*filesystem) + creds := auth.CredentialsFromContext(ctx) + i := &Inode{ + namespace: namespace, + mnt: mnt, + } + i.InodeAttrs.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.Filesystem.NextIno(), nsfsMode) + i.InitRefs() + return i +} + +const nsfsMode = linux.S_IFREG | linux.ModeUserRead | linux.ModeGroupRead | linux.ModeOtherRead + +// Namespace returns the namespace associated with the inode. +func (i *Inode) Namespace() Namespace { + return i.namespace +} + +// Name returns the inode name that is used to implement readlink() of +// /proc/pid/ns/ files. +func (i *Inode) Name() string { + return fmt.Sprintf("%s:[%d]", i.namespace.Type(), i.Ino()) +} + +// VirtualDentry returns VirtualDentry for the inode. +func (i *Inode) VirtualDentry() vfs.VirtualDentry { + dentry := &kernfs.Dentry{} + mnt := i.mnt + fs := mnt.Filesystem().Impl().(*filesystem) + i.IncRef() + mnt.IncRef() + dentry.Init(&fs.Filesystem, i) + vd := vfs.MakeVirtualDentry(mnt, dentry.VFSDentry()) + return vd +} + +// Mode implements kernfs.Inode.Mode. +func (i *Inode) Mode() linux.FileMode { + return nsfsMode +} + +// SetStat implements kernfs.Inode.SetStat. +// +// Linux sets S_IMMUTABLE to nsfs inodes that prevents any attribute changes on +// them. +func (i *Inode) SetStat(ctx context.Context, vfsfs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions) error { + return linuxerr.EPERM +} + +// namespace FD is a synthetic file that represents a namespace in +// /proc/[pid]/ns/*. +// +// +stateify savable +type namespaceFD struct { + vfs.FileDescriptionDefaultImpl + vfs.LockFD + + vfsfd vfs.FileDescription + inode *Inode +} + +// Stat implements vfs.FileDescriptionImpl.Stat. +func (fd *namespaceFD) Stat(ctx context.Context, opts vfs.StatOptions) (linux.Statx, error) { + vfs := fd.vfsfd.VirtualDentry().Mount().Filesystem() + return fd.inode.Stat(ctx, vfs, opts) +} + +// SetStat implements vfs.FileDescriptionImpl.SetStat. +func (fd *namespaceFD) SetStat(ctx context.Context, opts vfs.SetStatOptions) error { + vfs := fd.vfsfd.VirtualDentry().Mount().Filesystem() + creds := auth.CredentialsFromContext(ctx) + return fd.inode.SetStat(ctx, vfs, creds, opts) +} + +// Release implements vfs.FileDescriptionImpl.Release. +func (fd *namespaceFD) Release(ctx context.Context) { + fd.inode.DecRef(ctx) +} + +// Open implements kernfs.Inode.Open. +func (i *Inode) Open(ctx context.Context, rp *vfs.ResolvingPath, d *kernfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) { + fd := &namespaceFD{inode: i} + i.IncRef() + fd.LockFD.Init(&i.locks) + if err := fd.vfsfd.Init(fd, opts.Flags, rp.Mount(), d.VFSDentry(), &vfs.FileDescriptionOptions{}); err != nil { + return nil, err + } + return &fd.vfsfd, nil +} + +// StatFS implements kernfs.Inode.StatFS. +func (i *Inode) StatFS(ctx context.Context, fs *vfs.Filesystem) (linux.Statfs, error) { + return vfs.GenericStatFS(linux.NSFS_MAGIC), nil +} diff --git a/pkg/sentry/fsimpl/proc/BUILD b/pkg/sentry/fsimpl/proc/BUILD index d966dacac..e21a1ad6a 100644 --- a/pkg/sentry/fsimpl/proc/BUILD +++ b/pkg/sentry/fsimpl/proc/BUILD @@ -91,6 +91,7 @@ go_library( "//pkg/safemem", "//pkg/sentry/fsimpl/kernfs", "//pkg/sentry/fsimpl/lock", + "//pkg/sentry/fsimpl/nsfs", "//pkg/sentry/inet", "//pkg/sentry/kernel", "//pkg/sentry/kernel/auth", diff --git a/pkg/sentry/fsimpl/proc/task.go b/pkg/sentry/fsimpl/proc/task.go index 4ab06eb1d..0a2a280ab 100644 --- a/pkg/sentry/fsimpl/proc/task.go +++ b/pkg/sentry/fsimpl/proc/task.go @@ -72,7 +72,7 @@ func (fs *filesystem) newTaskInode(ctx context.Context, task *kernel.Task, pidns "mounts": fs.newTaskOwnedInode(ctx, task, fs.NextIno(), 0444, &mountsData{fs: fs, task: task}), "net": fs.newTaskNetDir(ctx, task), "ns": fs.newTaskOwnedDir(ctx, task, fs.NextIno(), 0511, map[string]kernfs.Inode{ - "net": fs.newFakeNamespaceSymlink(ctx, task, fs.NextIno(), "net"), + "net": fs.newNamespaceSymlink(ctx, task, fs.NextIno(), linux.CLONE_NEWNET), "pid": fs.newPIDNamespaceSymlink(ctx, task, fs.NextIno()), "user": fs.newFakeNamespaceSymlink(ctx, task, fs.NextIno(), "user"), }), diff --git a/pkg/sentry/fsimpl/proc/task_files.go b/pkg/sentry/fsimpl/proc/task_files.go index 7b7803ddf..a4c7cc986 100644 --- a/pkg/sentry/fsimpl/proc/task_files.go +++ b/pkg/sentry/fsimpl/proc/task_files.go @@ -27,6 +27,7 @@ import ( "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/safemem" "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/nsfs" "gvisor.dev/gvisor/pkg/sentry/kernel" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/pkg/sentry/limits" @@ -1230,7 +1231,18 @@ func (i *mountsData) Generate(ctx context.Context, buf *bytes.Buffer) error { type namespaceSymlink struct { kernfs.StaticSymlink - task *kernel.Task + task *kernel.Task + nsType int +} + +func (fs *filesystem) newNamespaceSymlink(ctx context.Context, task *kernel.Task, ino uint64, nsType int) kernfs.Inode { + inode := &namespaceSymlink{task: task, nsType: nsType} + + // Note: credentials are overridden by taskOwnedInode. + inode.Init(ctx, task.Credentials(), linux.UNNAMED_MAJOR, fs.devMinor, ino, "") + + taskInode := &taskOwnedInode{Inode: inode, owner: task} + return taskInode } func (fs *filesystem) newPIDNamespaceSymlink(ctx context.Context, task *kernel.Task, ino uint64) kernfs.Inode { @@ -1258,11 +1270,29 @@ func (fs *filesystem) newFakeNamespaceSymlink(ctx context.Context, task *kernel. return taskInode } +func (s *namespaceSymlink) getInode(t *kernel.Task) *nsfs.Inode { + switch s.nsType { + case linux.CLONE_NEWNET: + return t.GetNetworkNamespace().GetInode() + default: + panic("unknown namespace") + } +} + // Readlink implements kernfs.Inode.Readlink. func (s *namespaceSymlink) Readlink(ctx context.Context, mnt *vfs.Mount) (string, error) { if err := checkTaskState(s.task); err != nil { return "", err } + if s.nsType != 0 { + inode := s.getInode(s.task) + if inode == nil { + return "", linuxerr.ENOENT + } + target := inode.Name() + inode.DecRef(ctx) + return target, nil + } return s.StaticSymlink.Readlink(ctx, mnt) } @@ -1272,6 +1302,14 @@ func (s *namespaceSymlink) Getlink(ctx context.Context, mnt *vfs.Mount) (vfs.Vir return vfs.VirtualDentry{}, "", err } + if s.nsType != 0 { + inode := s.getInode(s.task) + if inode == nil { + return vfs.VirtualDentry{}, "", linuxerr.ENOENT + } + defer inode.DecRef(ctx) + return inode.VirtualDentry(), "", nil + } // Create a synthetic inode to represent the namespace. fs := mnt.Filesystem().Impl().(*filesystem) nsInode := &namespaceInode{} diff --git a/pkg/sentry/fsutil/BUILD b/pkg/sentry/fsutil/BUILD index 3591067d0..b7ddc3fe2 100644 --- a/pkg/sentry/fsutil/BUILD +++ b/pkg/sentry/fsutil/BUILD @@ -100,7 +100,6 @@ go_library( "//pkg/sentry/kernel/time", "//pkg/sentry/memmap", "//pkg/sentry/pgalloc", - "//pkg/sentry/socket/unix/transport", "//pkg/sentry/usage", "//pkg/state", "//pkg/sync", diff --git a/pkg/sentry/inet/BUILD b/pkg/sentry/inet/BUILD index 089b18db8..ee5dfe6ca 100644 --- a/pkg/sentry/inet/BUILD +++ b/pkg/sentry/inet/BUILD @@ -44,6 +44,8 @@ go_library( "//pkg/atomicbitops", "//pkg/context", "//pkg/refs", + "//pkg/sentry/fsimpl/nsfs", + "//pkg/sentry/kernel/auth", "//pkg/tcpip", "//pkg/tcpip/stack", ], diff --git a/pkg/sentry/inet/namespace.go b/pkg/sentry/inet/namespace.go index e66dbaa9f..5e006c6ae 100644 --- a/pkg/sentry/inet/namespace.go +++ b/pkg/sentry/inet/namespace.go @@ -14,11 +14,17 @@ package inet +import ( + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/nsfs" + "gvisor.dev/gvisor/pkg/sentry/kernel/auth" +) + // Namespace represents a network namespace. See network_namespaces(7). // // +stateify savable type Namespace struct { - namespaceRefs + inode *nsfs.Inode // stack is the network stack implementation of this network namespace. stack Stack `state:"nosave"` @@ -32,38 +38,68 @@ type Namespace struct { // isRoot indicates whether this is the root network namespace. isRoot bool + + userNS *auth.UserNamespace } // NewRootNamespace creates the root network namespace, with creator // allowing new network namespaces to be created. If creator is nil, no // networking will function if the network is namespaced. -func NewRootNamespace(stack Stack, creator NetworkStackCreator) *Namespace { +func NewRootNamespace(stack Stack, creator NetworkStackCreator, userNS *auth.UserNamespace) *Namespace { n := &Namespace{ stack: stack, creator: creator, isRoot: true, + userNS: userNS, } - n.InitRefs() return n } +// UserNamespace returns the user namespace associated with this namespace. +func (n *Namespace) UserNamespace() *auth.UserNamespace { + return n.userNS +} + +// SetInode sets the nsfs `inode` to the namespace. +func (n *Namespace) SetInode(inode *nsfs.Inode) { + n.inode = inode +} + +// GetInode returns the nsfs inode associated with this namespace. +func (n *Namespace) GetInode() *nsfs.Inode { + return n.inode +} + // NewNamespace creates a new network namespace from the root. -func NewNamespace(root *Namespace) *Namespace { +func NewNamespace(root *Namespace, userNS *auth.UserNamespace) *Namespace { n := &Namespace{ creator: root.creator, + userNS: userNS, } n.init() - n.InitRefs() return n } +// Destroy implements nsfs.Namespace.Destroy. +func (n *Namespace) Destroy(ctx context.Context) { + if s := n.Stack(); s != nil { + s.Destroy() + } +} + +// Type implements nsfs.Namespace.Type. +func (n *Namespace) Type() string { + return "net" +} + +// IncRef increments the Namespace's refcount. +func (n *Namespace) IncRef() { + n.inode.IncRef() +} + // DecRef decrements the Namespace's refcount. -func (n *Namespace) DecRef() { - n.namespaceRefs.DecRef(func() { - if s := n.Stack(); s != nil { - s.Destroy() - } - }) +func (n *Namespace) DecRef(ctx context.Context) { + n.inode.DecRef(ctx) } // Stack returns the network stack of n. Stack may return nil if no network diff --git a/pkg/sentry/kernel/BUILD b/pkg/sentry/kernel/BUILD index 43110a895..c4394cdf5 100644 --- a/pkg/sentry/kernel/BUILD +++ b/pkg/sentry/kernel/BUILD @@ -354,6 +354,7 @@ go_library( "//pkg/sentry/fsimpl/kernfs", "//pkg/sentry/fsimpl/lock", "//pkg/sentry/fsimpl/mqfs", + "//pkg/sentry/fsimpl/nsfs", "//pkg/sentry/fsimpl/pipefs", "//pkg/sentry/fsimpl/sockfs", "//pkg/sentry/fsimpl/timerfd", diff --git a/pkg/sentry/kernel/kernel.go b/pkg/sentry/kernel/kernel.go index e97793692..9a7b4b568 100644 --- a/pkg/sentry/kernel/kernel.go +++ b/pkg/sentry/kernel/kernel.go @@ -47,6 +47,7 @@ import ( "gvisor.dev/gvisor/pkg/fspath" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/nsfs" "gvisor.dev/gvisor/pkg/sentry/fsimpl/pipefs" "gvisor.dev/gvisor/pkg/sentry/fsimpl/sockfs" "gvisor.dev/gvisor/pkg/sentry/fsimpl/timerfd" @@ -274,6 +275,9 @@ type Kernel struct { // syscalls (as opposed to named pipes created by mknod()). pipeMount *vfs.Mount + // nsfsMount is the Mount used for namespaces. + nsfsMount *vfs.Mount + // shmMount is the Mount used for anonymous files created by the // memfd_create() syscalls. It is analogous to Linux's shm_mnt. shmMount *vfs.Mount @@ -395,7 +399,7 @@ func (k *Kernel) Init(args InitKernelArgs) error { k.rootAbstractSocketNamespace = args.RootAbstractSocketNamespace k.rootNetworkNamespace = args.RootNetworkNamespace if k.rootNetworkNamespace == nil { - k.rootNetworkNamespace = inet.NewRootNamespace(nil, nil) + k.rootNetworkNamespace = inet.NewRootNamespace(nil, nil, args.RootUserNamespace) } k.runningTasksCond.L = &k.runningTasksMu k.cpuClockTickerWakeCh = make(chan struct{}, 1) @@ -439,6 +443,15 @@ func (k *Kernel) Init(args InitKernelArgs) error { pipeMount := k.vfs.NewDisconnectedMount(pipeFilesystem, nil, &vfs.MountOptions{}) k.pipeMount = pipeMount + nsfsFilesystem, err := nsfs.NewFilesystem(&k.vfs) + if err != nil { + return fmt.Errorf("failed to create nsfs filesystem: %v", err) + } + defer nsfsFilesystem.DecRef(ctx) + nsfsMount := k.vfs.NewDisconnectedMount(nsfsFilesystem, nil, &vfs.MountOptions{}) + k.nsfsMount = nsfsMount + k.rootNetworkNamespace.SetInode(nsfs.NewInode(ctx, nsfsMount, k.rootNetworkNamespace)) + tmpfsOpts := vfs.GetFilesystemOptions{ InternalData: tmpfs.FilesystemOpts{ // See mm/shmem.c:shmem_init() => vfs_kern_mount(flags=SB_KERNMOUNT). @@ -1607,6 +1620,11 @@ func (k *Kernel) PipeMount() *vfs.Mount { return k.pipeMount } +// NsfsMount returns the nsfs mount. +func (k *Kernel) NsfsMount() *vfs.Mount { + return k.nsfsMount +} + // ShmMount returns the tmpfs mount. func (k *Kernel) ShmMount() *vfs.Mount { return k.shmMount @@ -1630,12 +1648,13 @@ func (k *Kernel) Release() { ctx := k.SupervisorContext() k.hostMount.DecRef(ctx) k.pipeMount.DecRef(ctx) + k.nsfsMount.DecRef(ctx) k.shmMount.DecRef(ctx) k.socketMount.DecRef(ctx) k.vfs.Release(ctx) k.timekeeper.Destroy() k.vdso.Release(ctx) - k.RootNetworkNamespace().DecRef() + k.RootNetworkNamespace().DecRef(ctx) } // PopulateNewCgroupHierarchy moves all tasks into a newly created cgroup diff --git a/pkg/sentry/kernel/task.go b/pkg/sentry/kernel/task.go index c29b76123..200dc1f9c 100644 --- a/pkg/sentry/kernel/task.go +++ b/pkg/sentry/kernel/task.go @@ -506,7 +506,9 @@ type Task struct { numaPolicy linux.NumaPolicy numaNodeMask uint64 - // netns is the task's network namespace. netns is never nil. + // netns is the task's network namespace. It has to be changed under mu + // so that GetNetworkNamespace can take a reference before it is + // released. netns inet.NamespaceAtomicPtr // If rseqPreempted is true, before the next call to p.Switch(), diff --git a/pkg/sentry/kernel/task_clone.go b/pkg/sentry/kernel/task_clone.go index b9ede9006..5a2fa27cc 100644 --- a/pkg/sentry/kernel/task_clone.go +++ b/pkg/sentry/kernel/task_clone.go @@ -21,9 +21,12 @@ import ( "gvisor.dev/gvisor/pkg/cleanup" "gvisor.dev/gvisor/pkg/errors/linuxerr" "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/nsfs" "gvisor.dev/gvisor/pkg/sentry/inet" "gvisor.dev/gvisor/pkg/sentry/seccheck" pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto" + "gvisor.dev/gvisor/pkg/sentry/vfs" "gvisor.dev/gvisor/pkg/usermem" ) @@ -117,14 +120,16 @@ func (t *Task) Clone(args *linux.CloneArgs) (ThreadID, *SyscallControl, error) { }) defer cu.Clean() - netns := t.NetworkNamespace() + netns := t.netns.Load() if args.Flags&linux.CLONE_NEWNET != 0 { - netns = inet.NewNamespace(netns) + netns = inet.NewNamespace(netns, userns) + inode := nsfs.NewInode(t, t.k.nsfsMount, netns) + netns.SetInode(inode) } else { netns.IncRef() } cu.Add(func() { - netns.DecRef() + netns.DecRef(t) }) // TODO(b/63601033): Implement CLONE_NEWNS. @@ -405,6 +410,38 @@ func (r *runSyscallAfterVforkStop) execute(t *Task) taskRunState { return (*runSyscallExit)(nil) } +// Setns reassociates thread with the specified namespace. +func (t *Task) Setns(fd *vfs.FileDescription, flags int32) error { + d, ok := fd.Dentry().Impl().(*kernfs.Dentry) + if !ok { + return linuxerr.EINVAL + } + i, ok := d.Inode().(*nsfs.Inode) + if !ok { + return linuxerr.EINVAL + } + + switch ns := i.Namespace().(type) { + case *inet.Namespace: + if flags != 0 && flags != linux.CLONE_NEWNET { + return linuxerr.EINVAL + } + if !t.HasCapabilityIn(linux.CAP_SYS_ADMIN, ns.UserNamespace()) || + !t.Credentials().HasCapability(linux.CAP_SYS_ADMIN) { + return linuxerr.EPERM + } + oldNS := t.NetworkNamespace() + ns.IncRef() + t.mu.Lock() + t.netns.Store(ns) + t.mu.Unlock() + oldNS.DecRef(t) + return nil + default: + return linuxerr.EINVAL + } +} + // Unshare changes the set of resources t shares with other tasks, as specified // by flags. // @@ -456,7 +493,7 @@ func (t *Task) Unshare(flags int32) error { if err != nil { return err } - // Need to reload creds, becaue t.SetUserNamespace() changed task credentials. + // Need to reload creds, because t.SetUserNamespace() changed task credentials. creds = t.Credentials() } haveCapSysAdmin := t.HasCapability(linux.CAP_SYS_ADMIN) @@ -466,13 +503,18 @@ func (t *Task) Unshare(flags int32) error { } t.childPIDNamespace = t.tg.pidns.NewChild(t.UserNamespace()) } - var oldNETNS *inet.Namespace if flags&linux.CLONE_NEWNET != 0 { if !haveCapSysAdmin { return linuxerr.EPERM } - oldNETNS = t.netns.Load() - t.netns.Store(inet.NewNamespace(t.netns.Load())) + netns := t.NetworkNamespace() + netns = inet.NewNamespace(netns, t.UserNamespace()) + netnsInode := nsfs.NewInode(t, t.k.nsfsMount, netns) + netns.SetInode(netnsInode) + t.mu.Lock() + netns = t.netns.Swap(netns) + t.mu.Unlock() + netns.DecRef(t) } t.mu.Lock() // Can't defer unlock: DecRefs must occur without holding t.mu. @@ -511,9 +553,6 @@ func (t *Task) Unshare(flags int32) error { if oldIPCNS != nil { oldIPCNS.DecRef(t) } - if oldNETNS != nil { - oldNETNS.DecRef() - } if oldFDTable != nil { oldFDTable.DecRef(t) } diff --git a/pkg/sentry/kernel/task_exit.go b/pkg/sentry/kernel/task_exit.go index 30d299bd8..21f02b82e 100644 --- a/pkg/sentry/kernel/task_exit.go +++ b/pkg/sentry/kernel/task_exit.go @@ -288,13 +288,13 @@ func (*runExitMain) execute(t *Task) taskRunState { mntns := t.mountNamespace t.mountNamespace = nil ipcns := t.ipcns - netns := t.NetworkNamespace() + netns := t.netns.Swap(nil) t.mu.Unlock() if mntns != nil { mntns.DecRef(t) } ipcns.DecRef(t) - netns.DecRef() + netns.DecRef(t) // If this is the last task to exit from the thread group, release the // thread group's resources. diff --git a/pkg/sentry/kernel/task_net.go b/pkg/sentry/kernel/task_net.go index e31e2b2e8..c6698fb9f 100644 --- a/pkg/sentry/kernel/task_net.go +++ b/pkg/sentry/kernel/task_net.go @@ -36,3 +36,17 @@ func (t *Task) NetworkContext() inet.Stack { func (t *Task) NetworkNamespace() *inet.Namespace { return t.netns.Load() } + +// GetNetworkNamespace takes a reference on the task network namespace and +// returns it. It can return nil if the task isn't alive. +func (t *Task) GetNetworkNamespace() *inet.Namespace { + // t.mu is required to be sure that the network namespace will not be + // released. + t.mu.Lock() + netns := t.netns.Load() + if netns != nil { + netns.IncRef() + } + t.mu.Unlock() + return netns +} diff --git a/pkg/sentry/kernel/task_start.go b/pkg/sentry/kernel/task_start.go index 0a8c8c04a..d9dbde37e 100644 --- a/pkg/sentry/kernel/task_start.go +++ b/pkg/sentry/kernel/task_start.go @@ -117,7 +117,7 @@ func (ts *TaskSet) NewTask(ctx context.Context, cfg *TaskConfig) (*Task, error) cfg.FSContext.DecRef(ctx) cfg.FDTable.DecRef(ctx) cfg.IPCNamespace.DecRef(ctx) - cfg.NetworkNamespace.DecRef() + cfg.NetworkNamespace.DecRef(ctx) if cfg.MountNamespace != nil { cfg.MountNamespace.DecRef(ctx) } diff --git a/pkg/sentry/socket/netstack/netstack.go b/pkg/sentry/socket/netstack/netstack.go index 5f3e1819f..31c17af42 100644 --- a/pkg/sentry/socket/netstack/netstack.go +++ b/pkg/sentry/socket/netstack/netstack.go @@ -442,7 +442,7 @@ func (s *sock) Release(ctx context.Context) { _ = t.BlockWithDeadline(ch, true, deadline) } } - s.namespace.DecRef() + s.namespace.DecRef(ctx) } // Epollable implements FileDescriptionImpl.Epollable. diff --git a/pkg/sentry/socket/unix/transport/BUILD b/pkg/sentry/socket/unix/transport/BUILD index 019bd04ac..975de8102 100644 --- a/pkg/sentry/socket/unix/transport/BUILD +++ b/pkg/sentry/socket/unix/transport/BUILD @@ -97,7 +97,6 @@ go_library( "//pkg/log", "//pkg/refs", "//pkg/sentry/hostfd", - "//pkg/sentry/inet", "//pkg/sentry/uniqueid", "//pkg/sync", "//pkg/sync/locking", diff --git a/pkg/sentry/syscalls/linux/linux64.go b/pkg/sentry/syscalls/linux/linux64.go index d6159f28c..17bb0e1fd 100644 --- a/pkg/sentry/syscalls/linux/linux64.go +++ b/pkg/sentry/syscalls/linux/linux64.go @@ -360,7 +360,7 @@ var AMD64 = &kernel.SyscallTable{ 305: syscalls.CapError("clock_adjtime", linux.CAP_SYS_TIME, "", nil), 306: syscalls.Supported("syncfs", Syncfs), 307: syscalls.Supported("sendmmsg", SendMMsg), - 308: syscalls.ErrorWithEvent("setns", linuxerr.EOPNOTSUPP, "Needs filesystem support", []string{"gvisor.dev/issue/140"}), // TODO(b/29354995) + 308: syscalls.Supported("setns", Setns), 309: syscalls.Supported("getcpu", Getcpu), 310: syscalls.ErrorWithEvent("process_vm_readv", linuxerr.ENOSYS, "", []string{"gvisor.dev/issue/158"}), // TODO(b/260724654) 311: syscalls.ErrorWithEvent("process_vm_writev", linuxerr.ENOSYS, "", []string{"gvisor.dev/issue/158"}), // TODO(b/260724654) @@ -683,7 +683,7 @@ var ARM64 = &kernel.SyscallTable{ 265: syscalls.Error("open_by_handle_at", linuxerr.EOPNOTSUPP, "Not supported by gVisor filesystems", nil), 266: syscalls.CapError("clock_adjtime", linux.CAP_SYS_TIME, "", nil), 267: syscalls.Supported("syncfs", Syncfs), - 268: syscalls.ErrorWithEvent("setns", linuxerr.EOPNOTSUPP, "Needs filesystem support", []string{"gvisor.dev/issue/140"}), // TODO(b/29354995) + 268: syscalls.Supported("setns", Setns), 269: syscalls.Supported("sendmmsg", SendMMsg), 270: syscalls.ErrorWithEvent("process_vm_readv", linuxerr.ENOSYS, "", []string{"gvisor.dev/issue/158"}), // TODO(b/260724654) 271: syscalls.ErrorWithEvent("process_vm_writev", linuxerr.ENOSYS, "", []string{"gvisor.dev/issue/158"}), // TODO(b/260724654) diff --git a/pkg/sentry/syscalls/linux/sys_mount.go b/pkg/sentry/syscalls/linux/sys_mount.go index 7b5a6a71d..eef4ae60f 100644 --- a/pkg/sentry/syscalls/linux/sys_mount.go +++ b/pkg/sentry/syscalls/linux/sys_mount.go @@ -76,7 +76,7 @@ func Mount(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, return 0, nil, err } var sourceTpop taskPathOperation - sourceTpop, err = getTaskPathOperation(t, linux.AT_FDCWD, sourcePath, disallowEmptyPath, nofollowFinalSymlink) + sourceTpop, err = getTaskPathOperation(t, linux.AT_FDCWD, sourcePath, disallowEmptyPath, followFinalSymlink) if err != nil { return 0, nil, err } diff --git a/pkg/sentry/syscalls/linux/sys_thread.go b/pkg/sentry/syscalls/linux/sys_thread.go index 9b448821f..ddae15cd3 100644 --- a/pkg/sentry/syscalls/linux/sys_thread.go +++ b/pkg/sentry/syscalls/linux/sys_thread.go @@ -443,6 +443,20 @@ func SetTidAddress(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (u return uintptr(t.ThreadID()), nil, nil } +// Setns implements linux syscall setns(2). +func Setns(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { + fd := args[0].Int() + + file := t.GetFile(fd) + if file == nil { + return 0, nil, linuxerr.EBADF + } + defer file.DecRef(t) + + flags := args[1].Int() + return 0, nil, t.Setns(file, flags) +} + // Unshare implements linux syscall unshare(2). func Unshare(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { flags := args[0].Int() diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 3ebe7f9f2..8384bd682 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -383,12 +383,6 @@ func New(args Args) (*Loader, error) { return nil, fmt.Errorf("enabling strace: %w", err) } - // Create root network namespace/stack. - netns, err := newRootNetworkNamespace(args.Conf, tk, k) - if err != nil { - return nil, fmt.Errorf("creating network: %w", err) - } - // Create capabilities. caps, err := specutils.Capabilities(args.Conf.EnableRaw, args.Spec.Process.Capabilities) if err != nil { @@ -409,6 +403,12 @@ func New(args Args) (*Loader, error) { caps, auth.NewRootUserNamespace()) + // Create root network namespace/stack. + netns, err := newRootNetworkNamespace(args.Conf, tk, k, creds.UserNamespace) + if err != nil { + return nil, fmt.Errorf("creating network: %w", err) + } + if args.NumCPU == 0 { args.NumCPU = runtime.NumCPU() } @@ -1212,7 +1212,7 @@ func (l *Loader) WaitExit() linux.WaitStatus { return l.k.GlobalInit().ExitStatus() } -func newRootNetworkNamespace(conf *config.Config, clock tcpip.Clock, uniqueID stack.UniqueID) (*inet.Namespace, error) { +func newRootNetworkNamespace(conf *config.Config, clock tcpip.Clock, uniqueID stack.UniqueID, userns *auth.UserNamespace) (*inet.Namespace, error) { // Create an empty network stack because the network namespace may be empty at // this point. Netns is configured before Run() is called. Netstack is // configured using a control uRPC message. Host network is configured inside @@ -1226,7 +1226,7 @@ func newRootNetworkNamespace(conf *config.Config, clock tcpip.Clock, uniqueID st return nil, fmt.Errorf("configuring network=host with raw sockets requires CAP_NET_RAW capability") } // No network namespacing support for hostinet yet, hence creator is nil. - return inet.NewRootNamespace(hostinet.NewStack(), nil), nil + return inet.NewRootNamespace(hostinet.NewStack(), nil, userns), nil case config.NetworkNone, config.NetworkSandbox: s, err := newEmptySandboxNetworkStack(clock, uniqueID, conf.AllowPacketEndpointWrite) @@ -1238,7 +1238,7 @@ func newRootNetworkNamespace(conf *config.Config, clock tcpip.Clock, uniqueID st uniqueID: uniqueID, allowPacketEndpointWrite: conf.AllowPacketEndpointWrite, } - return inet.NewRootNamespace(s, creator), nil + return inet.NewRootNamespace(s, creator, userns), nil default: panic(fmt.Sprintf("invalid network configuration: %v", conf.Network)) diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 597c403b2..f11cd874f 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -4281,6 +4281,8 @@ cc_binary( gtest, ":ip_socket_test_util", "//test/util:capability_util", + "//test/util:file_descriptor", + "//test/util:temp_path", "//test/util:test_main", "//test/util:test_util", "//test/util:thread_util", diff --git a/test/syscalls/linux/network_namespace.cc b/test/syscalls/linux/network_namespace.cc index 61809062a..5bff89ffa 100644 --- a/test/syscalls/linux/network_namespace.cc +++ b/test/syscalls/linux/network_namespace.cc @@ -12,9 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include + #include "gtest/gtest.h" #include "test/syscalls/linux/ip_socket_test_util.h" #include "test/util/capability_util.h" +#include "test/util/file_descriptor.h" +#include "test/util/temp_path.h" #include "test/util/test_util.h" #include "test/util/thread_util.h" @@ -36,6 +40,56 @@ TEST(NetworkNamespaceTest, LoopbackExists) { }); } +TEST(NetworkNamespaceTest, Setns) { + // TODO(b/267210840): Fix this tests for hostinet. + SKIP_IF(IsRunningWithHostinet()); + + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_ADMIN))); + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + struct stat st; + uint64_t netns1, netns2, netns3; + const FileDescriptor nsfd = + ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/thread-self/ns/net", O_RDONLY)); + + ASSERT_THAT(stat("/proc/thread-self/ns/net", &st), SyscallSucceeds()); + netns1 = st.st_ino; + + ASSERT_THAT(unshare(CLONE_NEWNET), SyscallSucceedsWithValue(0)); + ASSERT_THAT(stat("/proc/thread-self/ns/net", &st), SyscallSucceeds()); + netns2 = st.st_ino; + EXPECT_NE(netns1, netns2); + + ASSERT_THAT(setns(nsfd.get(), CLONE_NEWNET), SyscallSucceedsWithValue(0)); + ASSERT_THAT(stat("/proc/thread-self/ns/net", &st), SyscallSucceeds()); + netns3 = st.st_ino; + EXPECT_EQ(netns1, netns3); + + ASSERT_NE(ASSERT_NO_ERRNO_AND_VALUE(GetLoopbackIndex()), 0); +} + +TEST(NetworkNamespaceTest, BindMount) { + // TODO(b/267210840): Fix this tests for hostinet. + SKIP_IF(IsRunningWithHostinet()); + + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_ADMIN))); + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + + auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + ASSERT_THAT( + mount("/proc/self/ns/net", file.path().c_str(), NULL, MS_BIND, NULL), + SyscallSucceedsWithValue(0)); + + const FileDescriptor nsfd = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path().c_str(), O_RDONLY)); + ASSERT_THAT(umount2(file.path().c_str(), MNT_DETACH), + SyscallSucceedsWithValue(0)); + ASSERT_THAT(unshare(CLONE_NEWNET), SyscallSucceedsWithValue(0)); + ASSERT_THAT(setns(nsfd.get(), CLONE_NEWNET), SyscallSucceedsWithValue(0)); + + ASSERT_NE(ASSERT_NO_ERRNO_AND_VALUE(GetLoopbackIndex()), 0); +} + } // namespace } // namespace testing } // namespace gvisor