Files
2026-08-14 10:39:17 -07:00

802 lines
29 KiB
Go

// Copyright 2018 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
// This file implements the machinery behind the execve() syscall. In brief, a
// thread executes an execve() by killing all other threads in its thread
// group, assuming the leader's identity, and then switching process images.
//
// This design is effectively mandated by Linux. From ptrace(2):
//
// """
// execve(2) under ptrace
// When one thread in a multithreaded process calls execve(2), the
// kernel destroys all other threads in the process, and resets the
// thread ID of the execing thread to the thread group ID (process ID).
// (Or, to put things another way, when a multithreaded process does an
// execve(2), at completion of the call, it appears as though the
// execve(2) occurred in the thread group leader, regardless of which
// thread did the execve(2).) This resetting of the thread ID looks
// very confusing to tracers:
//
// * All other threads stop in PTRACE_EVENT_EXIT stop, if the
// PTRACE_O_TRACEEXIT option was turned on. Then all other threads
// except the thread group leader report death as if they exited via
// _exit(2) with exit code 0.
//
// * The execing tracee changes its thread ID while it is in the
// execve(2). (Remember, under ptrace, the "pid" returned from
// waitpid(2), or fed into ptrace calls, is the tracee's thread ID.)
// That is, the tracee's thread ID is reset to be the same as its
// process ID, which is the same as the thread group leader's thread
// ID.
//
// * Then a PTRACE_EVENT_EXEC stop happens, if the PTRACE_O_TRACEEXEC
// option was turned on.
//
// * If the thread group leader has reported its PTRACE_EVENT_EXIT stop
// by this time, it appears to the tracer that the dead thread leader
// "reappears from nowhere". (Note: the thread group leader does not
// report death via WIFEXITED(status) until there is at least one
// other live thread. This eliminates the possibility that the
// tracer will see it dying and then reappearing.) If the thread
// group leader was still alive, for the tracer this may look as if
// thread group leader returns from a different system call than it
// entered, or even "returned from a system call even though it was
// not in any system call". If the thread group leader was not
// traced (or was traced by a different tracer), then during
// execve(2) it will appear as if it has become a tracee of the
// tracer of the execing tracee.
//
// All of the above effects are the artifacts of the thread ID change in
// the tracee.
// """
import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/cleanup"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/overlay"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/kernel/pipe"
"gvisor.dev/gvisor/pkg/sentry/loader"
"gvisor.dev/gvisor/pkg/sentry/mm"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/syserr"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
)
// Execve implements the execve(2) syscall by first doing these two potentially stop-requiring tasks:
//
// 1. Acquiring a lock that serializes execve with a concurrent PTRACE_ATTACH or seccomp tsync for
// correct creds computation.
// * The next taskRunState is runExecveAfterExecveCredsLock
// * The task will wait in the execveCredsMutexStop stop till it can acquire the lock.
// 2. Killing all other tasks in its thread group and switching to the new TaskImage.
// * The next run state is runExecveAfterSiblingExitStop
// * The task will wait in the siblingExitStop stop till it its siblings have exited.
//
// Execve takes ownership of `executable`.
//
// Preconditions: The caller must be running Task.doSyscallInvoke on the task goroutine.
func (t *Task) Execve(argv, envv []string, flags int32, pathname string, executable *vfs.FileDescription, closeOnExec bool) *SyscallControl {
// Serialize with PTRACE_ATTACH to ensure correct creds computation,
// see Task.shouldStopPrivGainDueToPtracer().
t.execveCredsMutexStartLock(t.tg)
// We cannot load the TaskImage before we serialize with PTRACE_ATTACH because loading the image
// involves setting up aux vectors that reflect the task's new creds, which can only be correctly
// computed after we freeze the identity (or the absence of) a ptracer.
//
// So we save the execve args for the next run state "runSyscallAfterPtraceExecveMutexLocked" that
// will load the new task image.
r := runExecveAfterExecveCredsLock{
argv: argv,
envv: envv,
pathname: pathname, // may be modified during resolution
execfn: pathname, // remains unmodified
flags: flags,
executable: executable,
closeOnExec: closeOnExec,
}
return &SyscallControl{next: &r, ignoreReturn: true}
}
// The runExecveAfterExecveCredsLock state continues execve(2) after the lock serializing
// PTRACE_ATTACH and execve has been acquired.
//
// +stateify savable
type runExecveAfterExecveCredsLock struct {
argv, envv []string
pathname string // the mapped VFS path of the executable after resolution
execfn string // the immutable original pathname passed to execve(2)
flags int32
executable *vfs.FileDescription
closeOnExec bool
}
func (r *runExecveAfterExecveCredsLock) execute(t *Task) taskRunState {
defer func() {
if r.executable != nil {
r.executable.DecRef(t)
}
}()
if t.killed() {
// execveCredsMutexUnlock() will not pass us the lock after we were killed, so there is no
// race in loading the execveCredsMutexOwner value without a lock.
if t.execveCredsMutexOwner != nil {
t.execveCredsMutexUnlock()
}
return (*runInterrupt)(nil)
}
fsContext := t.FSContext()
root := fsContext.RootDirectory()
defer root.DecRef(t)
wd := fsContext.WorkingDirectory()
defer wd.DecRef(t)
// Cannot gain privileges if the ptracer's capabilities prevent it.
stopPrivGain := t.shouldStopPrivGainDueToPtracerLocked()
// Cannot gain privileges if the FSContext is shared outside of our thread group.
if fsContext.checkAndPreventSharingOutsideTG(t.tg) {
stopPrivGain = true
}
remainingTraversals := uint(linux.MaxSymlinkTraversals)
loadArgs := loader.LoadArgs{
Root: root,
WorkingDir: wd,
RemainingTraversals: &remainingTraversals,
ResolveFinal: r.flags&linux.AT_SYMLINK_NOFOLLOW == 0,
Filename: r.pathname,
File: r.executable,
CloseOnExec: r.closeOnExec,
Argv: r.argv,
Envv: r.envv,
Features: t.Kernel().FeatureSet(),
NoNewPrivs: t.GetNoNewPrivs(),
StopPrivGain: stopPrivGain,
AllowSUID: t.Kernel().AllowSUID,
}
if seccheck.Global.Enabled(seccheck.PointExecve) {
// Retain the first executable file that is opened (which may open
// multiple executable files while resolving interpreter scripts).
if r.executable == nil {
loadArgs.AfterOpen = func(f *vfs.FileDescription) {
if r.executable == nil {
f.IncRef()
r.executable = f
r.pathname = r.executable.MappedName(t)
}
}
}
}
image, newCreds, secureExec, se := t.Kernel().LoadTaskImage(t, loadArgs)
if se != nil {
t.releaseExecveCredsLocks()
return t.doSyscallError(se.ToError())
}
return r.execveWithImage(t, image, newCreds, secureExec)
}
// siblingExitStop is a TaskStop that a task sets on itself when it wants to execve
// and is waiting for the other tasks in its thread group to exit first.
//
// +stateify savable
type siblingExitStop struct{}
// Killable implements TaskStop.Killable.
func (*siblingExitStop) Killable() bool { return true }
// execveWithImage continues the implementation of execve(2) now that the PTRACE_ATTACH lock has been
// acquired and the new TaskImage has been loaded.
//
// Enters the execStop state to wait for other tasks in the thread group to exit.
func (r *runExecveAfterExecveCredsLock) execveWithImage(t *Task, newImage *TaskImage, newCreds *auth.Credentials, secureExec bool) taskRunState {
cu := cleanup.Make(func() {
newImage.release(t)
})
cu.Add(func() {
t.releaseExecveCredsLocks()
})
defer cu.Clean()
// We can't clearly hold kernel package locks while stat'ing executable.
if seccheck.Global.Enabled(seccheck.PointExecve) {
mask, info := execveSeccheckInfo(t, r.argv, r.envv, r.executable, r.pathname, r.execfn)
if err := seccheck.Global.SentToSinks(func(c seccheck.Sink) error {
return c.Execve(t, mask, info)
}); err != nil {
return t.doSyscallError(err)
}
}
t.tg.pidns.owner.mu.Lock()
defer t.tg.pidns.owner.mu.Unlock()
t.tg.signalHandlers.mu.Lock()
defer t.tg.signalHandlers.mu.Unlock()
if t.tg.exiting || t.tg.execing != nil {
// We lost to a racing group-exit, kill, or exec from another thread
// and should just exit.
return t.doSyscallError(linuxerr.EINTR)
}
// Cancel any racing group stops.
t.tg.endGroupStopLocked(false)
// If the task has any siblings, they have to exit before the exec can
// continue.
t.tg.execing = t
if t.tg.tasks.Front() != t.tg.tasks.Back() {
// "[All] other threads except the thread group leader report death as
// if they exited via _exit(2) with exit code 0." - ptrace(2)
for sibling := t.tg.tasks.Front(); sibling != nil; sibling = sibling.Next() {
if t != sibling {
sibling.killLocked()
}
}
// The last sibling to exit will wake t.
t.beginInternalStopLocked((*siblingExitStop)(nil))
}
cu.Release()
return &runExecveAfterSiblingExitStop{newImage, newCreds, secureExec}
}
// The runExecveAfterSiblingExitStop state continues execve(2) after all siblings of
// a thread in the execve syscall have exited.
//
// +stateify savable
type runExecveAfterSiblingExitStop struct {
image *TaskImage
newCreds *auth.Credentials
secureExec bool
}
func (r *runExecveAfterSiblingExitStop) execute(t *Task) taskRunState {
defer t.releaseExecveCredsLocks()
t.traceExecEvent(r.image)
t.tg.pidns.owner.mu.Lock()
t.tg.execing = nil
if t.killed() {
t.tg.pidns.owner.mu.Unlock()
r.image.release(t)
return (*runInterrupt)(nil)
}
// We are the thread group leader now. Save our old thread ID for
// PTRACE_EVENT_EXEC. This is racy in that if a tracer attaches after this
// point it will get a PID of 0, but this is consistent with Linux.
oldTID := ThreadID(0)
if tracer := t.Tracer(); tracer != nil {
oldTID = tracer.tg.pidns.tids[t]
}
t.promoteLocked()
// "POSIX timers are not preserved (timer_create(2))." - execve(2). Handle
// this first since POSIX timers are protected by the signal mutex, which
// we're about to change. Note that we have to stop and destroy timers
// without holding any mutexes to avoid circular lock ordering.
var its []*IntervalTimer
oldSignalHandlers := t.tg.signalHandlers
oldSignalHandlers.mu.Lock()
for _, it := range t.tg.timers {
its = append(its, it)
}
clear(t.tg.timers)
oldSignalHandlers.mu.Unlock()
t.tg.pidns.owner.mu.Unlock()
for _, it := range its {
it.DestroyTimer()
}
t.tg.pidns.owner.mu.Lock()
// "During an execve(2), the dispositions of handled signals are reset to
// the default; the dispositions of ignored signals are left unchanged. ...
// [The] signal mask is preserved across execve(2). ... [The] pending
// signal set is preserved across an execve(2)." - signal(7)
//
// Details:
//
// - If the thread group is sharing its signal handlers with another thread
// group via CLONE_SIGHAND, execve forces the signal handlers to be copied
// (see Linux's fs/exec.c:de_thread). We're not reference-counting signal
// handlers, so we always make a copy.
//
// - "Disposition" only means sigaction::sa_handler/sa_sigaction; flags,
// restorer (if present), and mask are always reset. (See Linux's
// fs/exec.c:setup_new_exec => kernel/signal.c:flush_signal_handlers.)
oldSignalHandlers.mu.Lock() // to ensure ThreadGroup.signalLock()'s correctness
t.tg.setSignalHandlersLocked(oldSignalHandlers.copyForExecLocked())
t.endStopCond.L = &t.tg.signalHandlers.mu
oldSignalHandlers.mu.Unlock()
// "Any alternate signal stack is not preserved (sigaltstack(2))." - execve(2)
t.signalStack = linux.SignalStack{Flags: linux.SS_DISABLE}
// "The termination signal is reset to SIGCHLD (see clone(2))."
t.tg.terminationSignal = linux.SIGCHLD
// execed indicates that the process's pgid cannot be changed
// in some scenarios (namely, the parent call setpgid(2) on the child).
// See the Setpgid function in sys_thread.go for more context.
t.tg.execed = true
// Maximum RSS is preserved across execve(2).
t.updateRSSLocked()
// Restartable sequence state is discarded.
t.rseqPreempted = false
t.rseqCPU = -1
t.rseqAddr = 0
t.rseqSignature = 0
t.oldRSeqCPUAddr = 0
t.tg.oldRSeqCritical.Store(&OldRSeqCriticalRegion{})
t.tg.pidns.owner.mu.Unlock()
oldFDTable := t.fdTable
t.fdTable = t.fdTable.Fork(t, int32(t.fdTable.CurrentMaxFDs()))
oldFDTable.DecRef(t)
// Remove FDs with the CloseOnExec flag set.
t.fdTable.RemoveIf(t, func(_ *vfs.FileDescription, flags FDFlags) bool {
return flags.CloseOnExec
})
// Handle the robust futex list.
t.exitRobustList()
// Parent should not signal the now privileged task, undocumented Linux behavior.
if r.secureExec {
t.parentDeathSignal = 0
}
// Update dumpability using just the old creds, see fs/exec.c:begin_new_exec().
// FIXME: Account for executables that may not be read when setting dumpability below.
oldCreds := t.creds.Load()
if oldCreds.EffectiveKUID != oldCreds.RealKUID ||
oldCreds.EffectiveKGID != oldCreds.RealKGID {
r.image.MemoryManager.SetDumpability(mm.NotDumpable) // suid_dumpable is not implemented
} else {
r.image.MemoryManager.SetDumpability(mm.UserDumpable)
}
// Lower dumpability based on cred delta, see kernel/cred.c:commit_creds().
if r.newCreds.EffectiveKUID != oldCreds.EffectiveKUID ||
r.newCreds.EffectiveKGID != oldCreds.EffectiveKGID ||
!r.newCreds.PermittedCaps.IsSubsetOf(oldCreds.PermittedCaps) {
r.image.MemoryManager.SetDumpability(mm.NotDumpable) // suid_dumpable is not implemented
t.parentDeathSignal = 0
}
// Switch to the new process.
t.p.PrepareExecve()
// Update credentials to reflect the execve. This should precede switching
// MMs to ensure that dumpability has been reset first, if needed.
t.creds.Store(r.newCreds)
t.mu.Lock()
oldImage := t.image
t.image = *r.image
t.mu.Unlock()
// Don't hold t.mu while calling t.image.release(), that may
// attempt to acquire TaskImage.MemoryManager.mappingMu, a lock order
// violation.
oldImage.release(t)
t.unstopVforkParent()
t.p.FullStateChanged()
t.ptraceExec(oldTID)
return (*runSyscallExit)(nil)
}
// promoteLocked makes t the leader of its thread group. If t is already the
// thread group leader, promoteLocked is a no-op.
//
// Preconditions:
// - All other tasks in t's thread group, including the existing leader (if it
// is not t), have reached TaskExitZombie.
// - The TaskSet mutex must be locked for writing.
func (t *Task) promoteLocked() {
oldLeader := t.tg.leader
if t == oldLeader {
return
}
// Swap the leader's TIDs with the execing task's. The latter will be
// released when the old leader is reaped below.
for ns := t.tg.pidns; ns != nil; ns = ns.parent {
oldTID, leaderTID := ns.tids[t], ns.tids[oldLeader]
ns.tids[oldLeader] = oldTID
ns.tids[t] = leaderTID
ns.tasks[oldTID] = oldLeader
ns.tasks[leaderTID] = t
// Neither the ThreadGroup nor TGID change, so no need to
// update ns.tgids.
}
// Inherit the old leader's start time.
oldStartTime := oldLeader.StartTime()
t.mu.Lock()
t.startTime = oldStartTime
t.mu.Unlock()
t.tg.leader = t
t.Infof("Becoming TID %d (in root PID namespace)", t.tg.pidns.owner.Root.tids[t])
t.updateInfoLocked()
// Reap the original leader. If it has a tracer, detach it instead of
// waiting for it to acknowledge the original leader's death.
oldLeader.exitParentNotified = true
oldLeader.exitParentAcked = true
if tracer := oldLeader.Tracer(); tracer != nil {
delete(tracer.ptraceTracees, oldLeader)
oldLeader.forgetTracerLocked()
// Notify the tracer that it will no longer be receiving these events
// from the tracee.
tracer.tg.eventQueue.Notify(EventExit | EventTraceeStop | EventGroupContinue)
}
t.notifyPIDFDsOnDeathLocked()
// In assuming the old leader's identity, t must reorient any pidfds
// associated with the old leader towards itself. Note that the old
// struct pid cannot have had "ready" set thus far: on account of the
// very existence of the (so far) non-leader subthread t. This implies
// that it is not possible for userspace to see a regression in the
// readiness of those pidfds.
if oldLeader.pid != nil {
oldLeader.pid.t.Store(t)
t.pid, oldLeader.pid = oldLeader.pid, nil
}
oldLeader.exitNotifyLocked(false)
}
func execveSeccheckInfo(t *Task, argv, env []string, executable *vfs.FileDescription, pathname, execfn string) (seccheck.FieldSet, *pb.ExecveInfo) {
fields := seccheck.Global.GetFieldSet(seccheck.PointExecve)
info := &pb.ExecveInfo{
Argv: argv,
Env: env,
Execfn: execfn,
}
// executable is nil if opening the binary failed early or if checked before opening.
// Only populate file-backed metadata (such as BinaryPath, mode, or SHA-256) when an open
// FileDescription is available.
if executable != nil {
info.BinaryPath = pathname
if fields.Local.Contains(seccheck.FieldSentryExecveBinaryInfo) {
// Note that despite the method name IsCopiedUp, this returns true for any file located on the
// upper layer, including files created or downloaded directly on the upper layer that were
// never copied up from a lower layer.
info.BinaryOverlayfsUpper = overlay.IsCopiedUp(executable.Dentry())
info.BinaryOverlayfsLower = overlay.IsOnLower(executable.Dentry())
info.BinaryInMemfd = tmpfs.IsMemfd(executable)
statOpts := vfs.StatOptions{
Mask: linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_UID | linux.STATX_GID | linux.STATX_INO | linux.STATX_CTIME | linux.STATX_SIZE | linux.STATX_NLINK,
}
if stat, err := executable.Stat(t, statOpts); err == nil {
if stat.Mask&(linux.STATX_TYPE|linux.STATX_MODE) == (linux.STATX_TYPE | linux.STATX_MODE) {
info.BinaryMode = uint32(stat.Mode)
}
if stat.Mask&linux.STATX_UID != 0 {
info.BinaryUid = stat.UID
}
if stat.Mask&linux.STATX_GID != 0 {
info.BinaryGid = stat.GID
}
if stat.Mask&linux.STATX_INO != 0 {
info.BinaryIno = stat.Ino
}
if stat.Mask&linux.STATX_CTIME != 0 {
info.BinaryCtime = &pb.Timespec{
Sec: stat.Ctime.Sec,
Nsec: int64(stat.Ctime.Nsec),
}
}
if stat.Mask&linux.STATX_SIZE != 0 {
info.BinarySize = int64(stat.Size)
}
if stat.Mask&linux.STATX_NLINK != 0 {
info.BinaryNlink = stat.Nlink
}
}
}
if fields.Local.Contains(seccheck.FieldSentryExecveBinarySHA256) || fields.Local.Contains(seccheck.FieldSentryExecveBinarySHA1) {
// Note: The current implementation only supports a single global ("Default")
// seccheck session. This is why we pull the cache directly from seccheck.Global.
if cache := seccheck.Global.ExecveHashCache(); cache != nil {
opts := cache.Opts()
if opts.SHA256 || opts.SHA1 {
hashes := resolveBinaryHashes(t, executable, cache)
if opts.SHA256 {
info.BinarySha256 = hashes.SHA256
}
if opts.SHA1 {
info.BinarySha1 = hashes.SHA1
}
}
}
}
}
if fields.Local.Contains(seccheck.FieldSentryExecveFdInfo) {
info.Stdin = execveFdInfo(t, 0)
info.Stdout = execveFdInfo(t, 1)
info.Stderr = execveFdInfo(t, 2)
}
if fields.Local.Contains(seccheck.FieldSentryExecvePipeProcInfo) {
// Check whether standard input (fd 0) or standard output (fd 1)
// is connected to a pipe by extracting the underlying Pipe object.
var pipeIn, pipeOut *pipe.Pipe
if fileIn := t.GetFile(0); fileIn != nil {
if pfd, ok := fileIn.Impl().(*pipe.VFSPipeFD); ok {
pipeIn = pfd.Pipe()
}
fileIn.DecRef(t)
}
if fileOut := t.GetFile(1); fileOut != nil {
if pfd, ok := fileOut.Impl().(*pipe.VFSPipeFD); ok {
pipeOut = pfd.Pipe()
}
fileOut.DecRef(t)
}
// Inspect sibling tasks under the same parent to find processes connected to the
// opposite end of stdin (pipeIn) or stdout (pipeOut).
if pipeIn != nil || pipeOut != nil {
// Only inspect the parent if pipeIn or pipeOut are present. Calling
// t.Parent() grabs a read lock on the PID namespace owner, which we want to avoid
// on the fast path if no pipes are connected.
if parent := t.Parent(); parent != nil {
for child := range parent.Children() {
if child == t {
continue
}
// Early termination: stop scanning siblings as soon as all applicable
// pipe peers (stdin and/or stdout) have been found or are not pipes.
if (pipeIn == nil || info.PipeInputProc != nil) && (pipeOut == nil || info.PipeOutputProc != nil) {
break
}
fdt := child.FDTable()
if fdt == nil {
continue
}
// Check if the sibling writes to t's stdin pipe via stdout (fd 1).
if pipeIn != nil && info.PipeInputProc == nil {
if childOut, _ := fdt.Get(1); childOut != nil {
pfd, ok := childOut.Impl().(*pipe.VFSPipeFD)
if ok && pfd.Pipe() == pipeIn {
info.PipeInputProc = pipeProcInfo(child, fields.Context)
}
childOut.DecRef(t)
}
}
// Check if the sibling reads from t's stdout pipe via stdin (fd 0).
if pipeOut != nil && info.PipeOutputProc == nil {
if childIn, _ := fdt.Get(0); childIn != nil {
pfd, ok := childIn.Impl().(*pipe.VFSPipeFD)
if ok && pfd.Pipe() == pipeOut {
info.PipeOutputProc = pipeProcInfo(child, fields.Context)
}
childIn.DecRef(t)
}
}
}
}
}
}
if !fields.Context.Empty() {
info.ContextData = &pb.ContextData{}
LoadSeccheckData(t, fields.Context, info.ContextData)
}
return fields, info
}
type socket interface {
GetSockName(t *Task) (linux.SockAddr, uint32, *syserr.Error)
GetPeerName(t *Task) (linux.SockAddr, uint32, *syserr.Error)
}
func sockAddrToProto(sa linux.SockAddr) (*pb.SocketIp, uint32) {
if sa == nil {
return nil, 0
}
switch a := sa.(type) {
case *linux.SockAddrInet:
return &pb.SocketIp{
Family: uint32(a.Family),
Ip: a.Addr[:],
Port: uint32(a.Port),
}, uint32(a.Family)
case *linux.SockAddrInet6:
return &pb.SocketIp{
Family: uint32(a.Family),
Ip: a.Addr[:],
Port: uint32(a.Port),
}, uint32(a.Family)
case *linux.SockAddrUnix:
return &pb.SocketIp{
Family: uint32(a.Family),
}, uint32(a.Family)
}
return nil, 0
}
func execveFdInfo(t *Task, fd int32) *pb.FdInfo {
file := t.GetFile(fd)
if file == nil {
return nil
}
defer file.DecRef(t)
pbfd := &pb.FdInfo{
Path: file.MappedName(t),
}
statOpts := vfs.StatOptions{
Mask: linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_INO,
}
if stat, err := file.Stat(t, statOpts); err == nil {
if stat.Mask&(linux.STATX_TYPE|linux.STATX_MODE) == (linux.STATX_TYPE | linux.STATX_MODE) {
pbfd.Mode = uint32(stat.Mode)
}
if stat.Mask&linux.STATX_INO != 0 {
pbfd.Ino = stat.Ino
}
}
if sops, ok := file.Impl().(socket); ok {
pbfd.Socket = &pb.SocketInfo{}
if local, _, err := sops.GetSockName(t); err == nil {
pbfd.Socket.Local, pbfd.Socket.Family = sockAddrToProto(local)
}
if remote, _, err := sops.GetPeerName(t); err == nil {
pbfd.Socket.Remote, pbfd.Socket.Family = sockAddrToProto(remote)
}
}
return pbfd
}
// execveCredsMutexStop is a TaskStop that a task enters if it fails to acquire the execveCredsMutex
// lock immediately. The task exits the stop when the lock owner eventually calls endInternalStop on
// it in Task.execveCredsMutexUnlock, or if it is killed before that happens.
//
// +stateify savable
type execveCredsMutexStop struct{}
// Killable implements TaskStop.Killable.
func (*execveCredsMutexStop) Killable() bool { return true }
// execveCredsMutexStartLock attempts to acquire the lock that serializes an execve(2) with a
// concurrent PTRACE_ATTACH or seccomp tsync. Such an exclusion is required to ensure correct creds
// computation in execve(2).
//
// Will push the calling Task into the execveCredsMutexStop stop if the lock is busy.
// The owner of the lock will then call endInternalStop on the waiting Task to pass on the lock.
// Since the caller cannot determine if the lock was acquired or the stop entered, it must
// have a distinct taskRunState where it would perform the work protected by the lock.
//
// Preconditions: Must be called from the task goroutine.
func (t *Task) execveCredsMutexStartLock(tg *ThreadGroup) {
tg.execveCredsMutexMu.Lock()
defer tg.execveCredsMutexMu.Unlock()
t.tg.signalHandlers.mu.Lock()
defer t.tg.signalHandlers.mu.Unlock()
if t.execveCredsMutexOwner != nil {
panic("Task already holds a execveCredsMutex lock")
}
if t.killedLocked() {
return // No point in taking the lock when we'd give it up first thing come the next runState.
}
if !tg.execveCredsMutexLocked {
tg.execveCredsMutexLocked = true
t.execveCredsMutexOwner = tg
return
}
t.beginInternalStopLocked((*execveCredsMutexStop)(nil))
tg.execveCredsMutexWaiters[t] = struct{}{}
}
// execveCredsMutexUnlock releases the mutex that serializes an execve(2) with a PTRACE_ATTACH or
// seccomp tsync. If there are other tasks waiting on the lock, one will be pulled out of the
// execveCredsMutexStop stop it is in and handed the lock.
//
// Preconditions: Must be called from the task goroutine. The caller must have called
// execveCredsMutexStartLock() earlier.
func (t *Task) execveCredsMutexUnlock() {
if t.execveCredsMutexOwner == nil {
panic("calling Task does not hold hold any execveCredsMutex lock")
}
tg := t.execveCredsMutexOwner
tg.execveCredsMutexMu.Lock()
defer tg.execveCredsMutexMu.Unlock()
if !tg.execveCredsMutexLocked {
panic("unlock of unlocked ThreadGroup execveCredsMutex")
}
tg.execveCredsMutexLocked = false
t.execveCredsMutexOwner = nil
// Find a waiting task whose execveCredsMutexStop hasn't been killed and pass on the lock.
for t2 := range tg.execveCredsMutexWaiters {
delete(tg.execveCredsMutexWaiters, t2)
if t.execveCredsMutexStartLocked(tg, t2) {
break
}
}
}
// Tries to pass the execveCredsMutex lock in tg to t2. Returns true if the lock was passed.
//
// preconditions: tg.execveCredsMutexMu must be locked.
func (t *Task) execveCredsMutexStartLocked(tg *ThreadGroup, t2 *Task) bool {
t2.tg.signalHandlers.mu.Lock()
defer t2.tg.signalHandlers.mu.Unlock()
// This check achieves the invariant "A Task cannot be passed the lock after being killed",
// allowing the post-kill taskRunState::execute() to access Task.execveCredsMutexOwner
// without anything racing with it.
if _, ok := t2.stop.(*execveCredsMutexStop); ok {
tg.execveCredsMutexLocked = true // tg.execveCredsMutexMu being locked is a precondition
if t2.execveCredsMutexOwner != nil {
panic("Task already holds a execveCredsMutex lock")
}
// The write to t2.execveCredsMutexOwner is safe because we know t2 is in a stop and we hold
// the signalHandlers.mu lock, so it cannot exit the stop from under us.
t2.execveCredsMutexOwner = tg
t2.endInternalStopLocked()
return true
}
return false
}
// shouldStopPrivGainDueToPtracerLocked returns true if the task has a tracer and the tracer's
// capabilities prevent us from elevating the current task's (the tracee's) privileges.
// Preconditions: The caller must have called execveCredsMutexStartLock() earlier.
func (t *Task) shouldStopPrivGainDueToPtracerLocked() bool {
if !t.hasTracer() {
return false
}
tracerCreds := t.Tracer().Credentials()
return !tracerCreds.HasCapabilityIn(linux.CAP_SYS_PTRACE, t.Credentials().UserNamespace)
}
// Releases the mutex that serializes an execve(2) with a PTRACE_ATTACH and allows t.fsContext to
// be shared again via clone(2). The caller must have called execveCredsMutexStartLock() and
// fsContext.checkAndPreventSharingOutsideTG() earlier.
func (t *Task) releaseExecveCredsLocks() {
t.execveCredsMutexUnlock()
t.FSContext().allowSharing()
}
// pipeProcInfo collects Seccheck process information for a sibling task connected via a pipe.
//
// To avoid returning heavy strings (like argv or binary path) on every execve trace point, we only
// populate the lightweight ContextData. A stateful seccheck client library can
// correlate the sibling's thread_group_id and thread_group_start_time_ns with prior Execve/Clone
// events to enrich the trace with full binary_path and argv without kernel overhead.
func pipeProcInfo(child *Task, contextMask seccheck.FieldMask) *pb.PipeProcInfo {
res := &pb.PipeProcInfo{}
if !contextMask.Empty() {
res.ContextData = &pb.ContextData{}
LoadSeccheckData(child, contextMask, res.ContextData)
}
return res
}