mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
802 lines
29 KiB
Go
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
|
|
}
|