// 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 }