mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Matches latest Linux behavior. The native pidfd test has also been deflaked. PiperOrigin-RevId: 911141375
450 lines
13 KiB
Go
450 lines
13 KiB
Go
// Copyright 2026 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 kernel
|
|
|
|
import (
|
|
"sync/atomic"
|
|
|
|
"gvisor.dev/gvisor/pkg/abi/linux"
|
|
"gvisor.dev/gvisor/pkg/atomicbitops"
|
|
"gvisor.dev/gvisor/pkg/context"
|
|
"gvisor.dev/gvisor/pkg/errors/linuxerr"
|
|
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
|
|
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
|
"gvisor.dev/gvisor/pkg/waiter"
|
|
)
|
|
|
|
// pidFD implements vfs.FileDescriptionImpl for pidfds.
|
|
//
|
|
// +stateify savable
|
|
type pidFD struct {
|
|
vfsFD vfs.FileDescription
|
|
vfs.FileDescriptionDefaultImpl
|
|
vfs.DentryMetadataFileDescriptionImpl
|
|
vfs.NoLockFD
|
|
|
|
// isThread is true if the pidfd represents a task.
|
|
// It is set during creation and is immutable thereafter.
|
|
isThread bool
|
|
|
|
// All pidfds pointing to the same task point to the one pid struct.
|
|
// It is set during creation and is immutable thereafter.
|
|
pid *pid
|
|
}
|
|
|
|
// Release implements vfs.FileDescriptionImpl.Release.
|
|
func (f *pidFD) Release(ctx context.Context) {
|
|
}
|
|
|
|
// Readiness implements waiter.Waitable.Readiness.
|
|
func (f *pidFD) Readiness(mask waiter.EventMask) waiter.EventMask {
|
|
// man pidfd_open(2) says:
|
|
// When the task that it refers to terminates and becomes a
|
|
// zombie, these interfaces indicate the file descriptor as
|
|
// readable (EPOLLIN). When the task is reaped, these interfaces
|
|
// produce a hangup event (EPOLLHUP).
|
|
// ...
|
|
// • With PIDFD_THREAD, the file descriptor becomes readable when
|
|
// the task exits and becomes a zombie, even if the thread-
|
|
// group is not empty.
|
|
// • Without PIDFD_THREAD, the file descriptor becomes readable
|
|
// only when the last thread in the thread group exits.
|
|
if f.pid.t.Load() == nil {
|
|
return waiter.EventHUp | waiter.ReadableEvents
|
|
}
|
|
if f.pid.ready.Load() {
|
|
return waiter.ReadableEvents
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// EventRegister implements waiter.Waitable.EventRegister.
|
|
func (f *pidFD) EventRegister(e *waiter.Entry) error {
|
|
f.pid.notifier.EventRegister(e)
|
|
return nil
|
|
}
|
|
|
|
// EventUnregister implements waiter.Waitable.EventUnregister.
|
|
func (f *pidFD) EventUnregister(e *waiter.Entry) {
|
|
f.pid.notifier.EventUnregister(e)
|
|
}
|
|
|
|
// Epollable implements vfs.FileDescriptionImpl.Epollable.
|
|
func (f *pidFD) Epollable() bool {
|
|
return true
|
|
}
|
|
|
|
// RegisterFileAsyncHandler implements vfs.FileDescriptionImpl.RegisterFileAsyncHandler.
|
|
func (f *pidFD) RegisterFileAsyncHandler(fd *vfs.FileDescription) error {
|
|
return linuxerr.EINVAL
|
|
}
|
|
|
|
// UnregisterFileAsyncHandler implements vfs.FileDescriptionImpl.UnregisterFileAsyncHandler.
|
|
func (f *pidFD) UnregisterFileAsyncHandler(fd *vfs.FileDescription) {}
|
|
|
|
// PIDFDOpen helps implement the linux syscall pidfd_open(2).
|
|
// It returns an fd with an extra reference.
|
|
func (t *Task) PIDFDOpen(tid ThreadID, isThread bool, nonBlock bool) (*vfs.FileDescription, error) {
|
|
t.tg.pidns.owner.mu.Lock()
|
|
pid, err := t.pidStructFromTIDLocked(tid, isThread)
|
|
t.tg.pidns.owner.mu.Unlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return t.pidFDOpen(pid, isThread, nonBlock)
|
|
}
|
|
|
|
func (t *Task) pidFDOpen(pid *pid, isThread bool, nonBlock bool) (*vfs.FileDescription, error) {
|
|
f := &pidFD{
|
|
isThread: isThread,
|
|
pid: pid,
|
|
}
|
|
|
|
vd := t.Kernel().VFS().NewAnonVirtualDentry("[pidfd]")
|
|
defer vd.DecRef(t)
|
|
var flags uint32
|
|
if nonBlock {
|
|
flags |= linux.O_NONBLOCK
|
|
}
|
|
|
|
err := f.vfsFD.Init(f, flags, t.Credentials(), vd.Mount(), vd.Dentry(), &vfs.FileDescriptionOptions{
|
|
UseDentryMetadata: true,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &f.vfsFD, nil
|
|
}
|
|
|
|
// PIDFDGetFD helps implement the Linux syscall pidfd_getfd(2).
|
|
func (t *Task) PIDFDGetFD(pidfd int32, targetfd int32, flags uint32) (uintptr, error) {
|
|
file := t.GetFile(pidfd)
|
|
if file == nil {
|
|
return 0, linuxerr.EBADF
|
|
}
|
|
defer file.DecRef(t)
|
|
pfd, ok := file.Impl().(*pidFD)
|
|
if !ok {
|
|
return 0, linuxerr.EBADF
|
|
}
|
|
|
|
target, err := pfd.liveTask()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !t.CanTrace(target, true) {
|
|
return 0, linuxerr.EPERM
|
|
}
|
|
|
|
targetFile := target.GetFile(targetfd)
|
|
if targetFile == nil {
|
|
return 0, linuxerr.EBADF
|
|
}
|
|
defer targetFile.DecRef(t)
|
|
|
|
// man pidfd_getfd(2) says:
|
|
// "The close-on-exec flag (FD_CLOEXEC; see fcntl(2)) is set on the
|
|
// file descriptor returned by pidfd_getfd()."
|
|
fd, err := t.NewFDFrom(0, targetFile, FDFlags{
|
|
CloseOnExec: true,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uintptr(fd), nil
|
|
}
|
|
|
|
func (f *pidFD) liveTask() (*Task, error) {
|
|
if f.pid.ready.Load() {
|
|
return nil, linuxerr.ESRCH
|
|
}
|
|
t := f.pid.t.Load()
|
|
if t == nil {
|
|
return nil, linuxerr.ESRCH
|
|
}
|
|
// Not necessary on account of the "ready" check above, but extra defense feels good.
|
|
if t.ExitState() >= TaskExitZombie {
|
|
return nil, linuxerr.ESRCH
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// ThreadGroupFromPIDFD returns the thread group associated with the integral pidfd
|
|
// and a bool indicating if the pidfd has O_NONBLOCK set.
|
|
// Note: Returns EINVAL if the pidfd is a PIDFD_THREAD pidfd.
|
|
func (t *Task) ThreadGroupFromPIDFD(pfdNum int32) (*ThreadGroup, bool, error) {
|
|
pfd, err := t.pidFDFromFDNum(pfdNum)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if pfd.isThread {
|
|
return nil, false, linuxerr.EINVAL
|
|
}
|
|
|
|
target := pfd.pid.t.Load()
|
|
if target == nil {
|
|
return nil, false, linuxerr.ESRCH
|
|
}
|
|
tg := target.ThreadGroup()
|
|
|
|
nonBlock := pfd.vfsFD.StatusFlags()&linux.O_NONBLOCK != 0
|
|
return tg, nonBlock, nil
|
|
}
|
|
|
|
func (t *Task) pidFDFromFDNum(pfdNum int32) (*pidFD, error) {
|
|
file := t.GetFile(pfdNum)
|
|
if file == nil {
|
|
return nil, linuxerr.EBADF
|
|
}
|
|
defer file.DecRef(t)
|
|
pfd, ok := file.Impl().(*pidFD)
|
|
if !ok {
|
|
return nil, linuxerr.EBADF
|
|
}
|
|
return pfd, nil
|
|
}
|
|
|
|
// pid represents a task for the purposes of pidfds.
|
|
// There is at most one pid struct per task which all pidfds refer to.
|
|
//
|
|
// +stateify savable
|
|
type pid struct {
|
|
// The task that this pid struct refers to.
|
|
// It is set during creation and is not mutated until it's nilled when the task is reaped.
|
|
t atomic.Pointer[Task] `state:".(*Task)"`
|
|
|
|
// For a non-leader subthread, a set ready means that it has become a zombie.
|
|
// For a leader thread, a set ready means that it has become a zombie and that the
|
|
// thread group has no other tasks.
|
|
ready atomicbitops.Bool
|
|
|
|
notifier waiter.Queue
|
|
}
|
|
|
|
func (t *Task) pidStructFromTIDLocked(tid ThreadID, isThread bool) (*pid, error) {
|
|
t2 := t.tg.pidns.tasks[tid]
|
|
if t2 == nil {
|
|
return nil, linuxerr.ESRCH
|
|
}
|
|
isLeader := t2 == t2.tg.leader
|
|
if !isThread && !isLeader {
|
|
return nil, linuxerr.ENOENT
|
|
}
|
|
return t2.pidStructLocked()
|
|
}
|
|
|
|
func (t *Task) pidStructLocked() (*pid, error) {
|
|
if t.ExitState() == TaskExitDead {
|
|
return nil, linuxerr.ESRCH
|
|
}
|
|
if t.pid != nil {
|
|
return t.pid, nil
|
|
}
|
|
t.pid = &pid{}
|
|
t.pid.t.Store(t)
|
|
|
|
isLeader := t == t.tg.leader
|
|
isZombie := t.exitStateLocked() == TaskExitZombie
|
|
if isLeader {
|
|
if isZombie && t.tg.tasksCount == 1 {
|
|
t.pid.ready.Store(true)
|
|
}
|
|
} else if isZombie {
|
|
t.pid.ready.Store(true)
|
|
}
|
|
|
|
return t.pid, nil
|
|
}
|
|
|
|
// notifyPIDFDsOnDeathLocked() is invoked by Task.exitNotifyLocked() when a task is dying.
|
|
// Preconditions: The TaskSet mutex must be held.
|
|
func (t *Task) notifyPIDFDsOnDeathLocked() {
|
|
if pid := t.pid; pid != nil {
|
|
// Stop referring to t from the pid struct, so that the presence of pidfds does not
|
|
// prevent the GC from reclaiming (the sizeable) Task t.
|
|
if pid.t.Swap(nil) == nil {
|
|
return
|
|
}
|
|
pid.notifier.Notify(waiter.ReadableEvents | waiter.EventHUp)
|
|
t.pid = nil // Inconsequential but hygienic.
|
|
}
|
|
}
|
|
|
|
// notifyPIDFDsLocked() is invoked by runExitNotify.execute() in the following cases:
|
|
// - If t is a non leader, as soon as it becomes a zombie.
|
|
// - If t is a leader, when it becomes a zombie and it is the last task in the thread group.
|
|
// Preconditions: The TaskSet mutex must be held.
|
|
func (t *Task) notifyPIDFDsLocked() {
|
|
if pid := t.pid; pid != nil {
|
|
if pid.ready.Swap(true) {
|
|
return
|
|
}
|
|
pid.notifier.Notify(waiter.ReadableEvents)
|
|
}
|
|
}
|
|
|
|
// TaskOwnedInode is an interface for /proc/$pid inodes so that pidfd syscalls can
|
|
// also work on "classic" pidfds from procfs.
|
|
type TaskOwnedInode interface {
|
|
TaskFromProcPIDInode() *Task
|
|
}
|
|
|
|
// pidfd syscalls work on not just pidfds, but also /proc/$pid procfs fds.
|
|
// Man pidfd_send_signal(2) says:
|
|
//
|
|
// "The pidfd argument is a PID file descriptor, a file descriptor
|
|
// that refers to process. Such a file descriptor can be obtained in
|
|
// any of the following ways:
|
|
// - by opening a /proc/pid directory;
|
|
// - using pidfd_open(2); or
|
|
// - via the PID file descriptor that is returned by a call to
|
|
// clone(2) or clone3(2) that specifies the CLONE_PIDFD flag."
|
|
//
|
|
// taskFromProcPIDFD returns the task associated with the /proc/$pid node represented by vfsfd.
|
|
// Returns an error if the fd is not a /proc/$pid node.
|
|
func taskFromProcPIDFD(vfsfd *vfs.FileDescription) (*Task, error) {
|
|
d, ok := vfsfd.Dentry().Impl().(*kernfs.Dentry)
|
|
if !ok {
|
|
return nil, linuxerr.EBADF
|
|
}
|
|
i, ok := d.Inode().(TaskOwnedInode)
|
|
if !ok {
|
|
return nil, linuxerr.EBADF
|
|
}
|
|
t := i.TaskFromProcPIDInode()
|
|
if t == nil {
|
|
return nil, linuxerr.ESRCH
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// PIDFDSendSignal sends a signal to the task or thread group associated with the pidfd represented
|
|
// by vfsfd. It could either be a true pidfd or a /proc/pid node.
|
|
func (t *Task) PIDFDSendSignal(vfsfd *vfs.FileDescription, sig linux.Signal, info *linux.SignalInfo, flags uint32) error {
|
|
pfd, isPidFD := vfsfd.Impl().(*pidFD)
|
|
var target *Task
|
|
var err error
|
|
|
|
if isPidFD {
|
|
target = pfd.pid.t.Load()
|
|
if target == nil {
|
|
err = linuxerr.ESRCH
|
|
}
|
|
} else {
|
|
target, err = taskFromProcPIDFD(vfsfd)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
effectiveFlags := flags
|
|
if isPidFD && effectiveFlags == 0 {
|
|
if pfd.isThread {
|
|
effectiveFlags = linux.PIDFD_SIGNAL_THREAD
|
|
} else {
|
|
effectiveFlags = linux.PIDFD_SIGNAL_THREAD_GROUP
|
|
}
|
|
}
|
|
if effectiveFlags == 0 {
|
|
effectiveFlags = linux.PIDFD_SIGNAL_THREAD_GROUP
|
|
}
|
|
|
|
if info == nil {
|
|
info = &linux.SignalInfo{
|
|
Signo: int32(sig),
|
|
Code: linux.SI_USER,
|
|
}
|
|
info.SetPID(int32(target.PIDNamespace().IDOfTask(t)))
|
|
info.SetUID(int32(t.Credentials().RealKUID.In(target.UserNamespace()).OrOverflow()))
|
|
} else {
|
|
if info.Signo != int32(sig) {
|
|
return linuxerr.EINVAL
|
|
}
|
|
// Do not allow spoofing unless to self.
|
|
if info.Code >= 0 || info.Code == linux.SI_TKILL {
|
|
if target != t || effectiveFlags == linux.PIDFD_SIGNAL_PROCESS_GROUP {
|
|
return linuxerr.EPERM
|
|
}
|
|
}
|
|
}
|
|
if !t.MayKill(target, sig) {
|
|
return linuxerr.EPERM
|
|
}
|
|
|
|
switch effectiveFlags {
|
|
case linux.PIDFD_SIGNAL_THREAD:
|
|
return target.SendSignal(info)
|
|
case linux.PIDFD_SIGNAL_THREAD_GROUP:
|
|
return target.SendGroupSignal(info)
|
|
case linux.PIDFD_SIGNAL_PROCESS_GROUP:
|
|
tg := target.ThreadGroup()
|
|
if tg.ProcessGroup().Originator() != tg {
|
|
return linuxerr.EINVAL
|
|
}
|
|
return tg.ProcessGroup().SendSignal(info)
|
|
default: // Unreachable.
|
|
t.Warningf("Unexpected error in PIDFDSendSignal with flags %d", flags)
|
|
return linuxerr.EINVAL
|
|
}
|
|
}
|
|
|
|
// PIDFDTask returns the task associated with the pidfd represented by vfsfd.
|
|
// If the vfsfd is neither a true pidfd nor a /proc/pid node, an error is returned.
|
|
func PIDFDTask(vfsfd *vfs.FileDescription) (*Task, error) {
|
|
pfd, ok := vfsfd.Impl().(*pidFD)
|
|
if ok { // vfsfd is a true pidfd (a product of pidfd_open() or clone3(CLONE_PIDFD)).
|
|
return pfd.liveTask()
|
|
}
|
|
t, err := taskFromProcPIDFD(vfsfd) // Maybe it's a classic pidfd from /proc/$pid.
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if t.ExitState() >= TaskExitZombie {
|
|
return nil, linuxerr.ESRCH
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// ObservedTIDsForPIDFD returns a slice of Thread IDs of the target task represented by
|
|
// vfsfd in all its ancestral pidns's ending at the pidns of the observing task t, with
|
|
// deeper pidns tids earlier in the slice.
|
|
// Returns an error if vfsfd is not a true pidfd.
|
|
func ObservedTIDsForPIDFD(vfsfd *vfs.FileDescription, t *Task) ([]int32, error) {
|
|
pfd, ok := vfsfd.Impl().(*pidFD)
|
|
if !ok {
|
|
return nil, linuxerr.EBADF
|
|
}
|
|
target := pfd.pid.t.Load()
|
|
if target == nil {
|
|
return []int32{-1}, nil // target is reaped.
|
|
}
|
|
|
|
tNS := t.PIDNamespace()
|
|
targetNS := target.PIDNamespace()
|
|
tidAtT := tNS.IDOfTask(target)
|
|
if tidAtT == 0 {
|
|
return []int32{0}, nil
|
|
}
|
|
|
|
var nspids []int32
|
|
for cur := targetNS; cur != nil; cur = cur.parent {
|
|
nspids = append(nspids, int32(cur.IDOfTask(target)))
|
|
if cur == tNS {
|
|
break // Stop at the observing task's pidns.
|
|
}
|
|
}
|
|
return nspids, nil
|
|
}
|