mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Seems that GNU tar extraction has been broken in gVisor since 75b03fd (11/15/2025), which started using openat2(2) to address CVE-2025-45582. tar uses openat2(2) for the RESOLVE_BENEATH flag which allows scoping the path resolution underneath the specified dirfd. This commit implements openat2(2) in gVisor, with support for all flags supported by Linux: RESOLVE_BENEATH, RESOLVE_IN_ROOT, RESOLVE_CACHED, RESOLVE_NO_MAGICLINKS, RESOLVE_NO_SYMLINKS, and RESOLVE_NO_XDEV. This commit also introduces VirtualFilesystem.renameSeq, a global sequence counter that along with mountSeq detects mount or rename operations that race with scoped traversals. A final "resolved-path-is-under-dirfd" check is performed after the traversal. This is for two reasons: (1) in case of a bug in any of the above logic, or (2) in case of an (extremely unlikely) attack where mountSeq overflows by exactly 2^32 during a path resolution. Unfortunately, due to the way path traversal in gVisor is tightly coupled to the operation being performed by a VFS method, using scoped traversals in an operation that *mutates* anything will be difficult, since without major refactoring, the check must happen after the side effect has taken place. Currently, Linux only exposes scoped traversals through openat2(2), so destroying the created fd is all we need to do if the final check fails. (As a result of this limitation, most VFS functions have been modified to reject scoped traversals "just-in-case".) The RESOLVE_NO_XDEV implementation has a slight divergence from Linux. It is a bit looser, permitting absolute symlinks in any case where the root dentry's mount matches the mount the traversal is locked to. Linux's behavior here is likely a bug, and since we can safely be *more* permissive, there's not much reason to match it. This commit also adds syscall tests and an strace formatter for openat2(2).
856 lines
26 KiB
Go
856 lines
26 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 strace implements the logic to print out the input and the return value
|
|
// of each traced syscall.
|
|
package strace
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gvisor.dev/gvisor/pkg/abi"
|
|
"gvisor.dev/gvisor/pkg/abi/linux"
|
|
"gvisor.dev/gvisor/pkg/bits"
|
|
"gvisor.dev/gvisor/pkg/eventchannel"
|
|
"gvisor.dev/gvisor/pkg/marshal/primitive"
|
|
"gvisor.dev/gvisor/pkg/seccomp"
|
|
"gvisor.dev/gvisor/pkg/sentry/arch"
|
|
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
|
pb "gvisor.dev/gvisor/pkg/sentry/strace/strace_go_proto"
|
|
slinux "gvisor.dev/gvisor/pkg/sentry/syscalls/linux"
|
|
|
|
"gvisor.dev/gvisor/pkg/hostarch"
|
|
)
|
|
|
|
// DefaultLogMaximumSize is the default LogMaximumSize.
|
|
const DefaultLogMaximumSize = 1024
|
|
|
|
// LogMaximumSize determines the maximum display size for data blobs (read,
|
|
// write, etc.).
|
|
var LogMaximumSize uint = DefaultLogMaximumSize
|
|
|
|
// EventMaximumSize determines the maximum size for data blobs (read, write,
|
|
// etc.) sent over the event channel. Default is 0 because most clients cannot
|
|
// do anything useful with binary text dump of byte array arguments.
|
|
var EventMaximumSize uint
|
|
|
|
// LogAppDataAllowed is set to true when printing application data in strace
|
|
// logs is allowed.
|
|
var LogAppDataAllowed = true
|
|
|
|
// ItimerTypes are the possible itimer types.
|
|
var ItimerTypes = abi.ValueSet{
|
|
linux.ITIMER_REAL: "ITIMER_REAL",
|
|
linux.ITIMER_VIRTUAL: "ITIMER_VIRTUAL",
|
|
linux.ITIMER_PROF: "ITIMER_PROF",
|
|
}
|
|
|
|
func hexNum(num uint64) string {
|
|
return "0x" + strconv.FormatUint(num, 16)
|
|
}
|
|
|
|
func hexArg(arg arch.SyscallArgument) string {
|
|
return hexNum(arg.Uint64())
|
|
}
|
|
|
|
func iovecs(t *kernel.Task, addr hostarch.Addr, iovcnt int, printContent bool, maxBytes uint64) string {
|
|
if iovcnt < 0 || iovcnt > linux.UIO_MAXIOV {
|
|
return fmt.Sprintf("%#x (error decoding iovecs: invalid iovcnt)", addr)
|
|
}
|
|
ars, err := t.CopyInIovecs(addr, iovcnt)
|
|
if err != nil {
|
|
return fmt.Sprintf("%#x (error decoding iovecs: %v)", addr, err)
|
|
}
|
|
|
|
var totalBytes uint64
|
|
var truncated bool
|
|
iovs := make([]string, iovcnt)
|
|
for i := 0; !ars.IsEmpty(); i, ars = i+1, ars.Tail() {
|
|
ar := ars.Head()
|
|
if ar.Length() == 0 || !printContent {
|
|
iovs[i] = fmt.Sprintf("{base=%#x, len=%d}", ar.Start, ar.Length())
|
|
continue
|
|
}
|
|
|
|
size := uint64(ar.Length())
|
|
if truncated || totalBytes+size > maxBytes {
|
|
truncated = true
|
|
size = maxBytes - totalBytes
|
|
} else {
|
|
totalBytes += uint64(ar.Length())
|
|
}
|
|
|
|
b := make([]byte, size)
|
|
amt, err := t.CopyInBytes(ar.Start, b)
|
|
if err != nil {
|
|
iovs[i] = fmt.Sprintf("{base=%#x, len=%d, %q..., error decoding string: %v}", ar.Start, ar.Length(), b[:amt], err)
|
|
continue
|
|
}
|
|
|
|
dot := ""
|
|
if truncated {
|
|
// Indicate truncation.
|
|
dot = "..."
|
|
}
|
|
iovs[i] = fmt.Sprintf("{base=%#x, len=%d, %q%s}", ar.Start, ar.Length(), b[:amt], dot)
|
|
}
|
|
|
|
return fmt.Sprintf("%#x %s", addr, strings.Join(iovs, ", "))
|
|
}
|
|
|
|
func dump(t *kernel.Task, addr hostarch.Addr, size uint, maximumBlobSize uint, printContent bool) string {
|
|
if !printContent {
|
|
return fmt.Sprintf("{base=%#x, len=%d}", addr, size)
|
|
}
|
|
origSize := size
|
|
if size > maximumBlobSize {
|
|
size = maximumBlobSize
|
|
}
|
|
if size == 0 {
|
|
return ""
|
|
}
|
|
|
|
b := make([]byte, size)
|
|
amt, err := t.CopyInBytes(addr, b)
|
|
if err != nil {
|
|
return fmt.Sprintf("%#x (error decoding string: %s)", addr, err)
|
|
}
|
|
|
|
dot := ""
|
|
if uint(amt) < origSize {
|
|
// ... if we truncated the dump.
|
|
dot = "..."
|
|
}
|
|
|
|
return fmt.Sprintf("%#x %q%s", addr, b[:amt], dot)
|
|
}
|
|
|
|
func path(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "<null>"
|
|
}
|
|
path, err := t.CopyInString(addr, linux.PATH_MAX)
|
|
if err != nil {
|
|
return fmt.Sprintf("%#x (error decoding path: %s)", addr, err)
|
|
}
|
|
return fmt.Sprintf("%#x %s", addr, path)
|
|
}
|
|
|
|
func fd(t *kernel.Task, fd int32) string {
|
|
root := t.FSContext().RootDirectory()
|
|
defer root.DecRef(t)
|
|
|
|
vfsObj := root.Mount().Filesystem().VirtualFilesystem()
|
|
if fd == linux.AT_FDCWD {
|
|
wd := t.FSContext().WorkingDirectory()
|
|
defer wd.DecRef(t)
|
|
|
|
name, _ := vfsObj.PathnameWithDeleted(t, root, wd)
|
|
return fmt.Sprintf("AT_FDCWD %s", name)
|
|
}
|
|
|
|
file := t.GetFile(fd)
|
|
if file == nil {
|
|
// Cast FD to uint64 to avoid printing negative hex.
|
|
return fmt.Sprintf("%#x (bad FD)", uint64(fd))
|
|
}
|
|
defer file.DecRef(t)
|
|
|
|
name, _ := vfsObj.PathnameWithDeleted(t, root, file.VirtualDentry())
|
|
return fmt.Sprintf("%#x %s", fd, name)
|
|
}
|
|
|
|
func fdpair(t *kernel.Task, addr hostarch.Addr) string {
|
|
var fds [2]int32
|
|
_, err := primitive.CopyInt32SliceIn(t, addr, fds[:])
|
|
if err != nil {
|
|
return fmt.Sprintf("%#x (error decoding fds: %s)", addr, err)
|
|
}
|
|
|
|
return fmt.Sprintf("%#x [%d %d]", addr, fds[0], fds[1])
|
|
}
|
|
|
|
func uname(t *kernel.Task, addr hostarch.Addr) string {
|
|
var u linux.UtsName
|
|
if _, err := u.CopyIn(t, addr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding utsname: %s)", addr, err)
|
|
}
|
|
|
|
return fmt.Sprintf("%#x %s", addr, u)
|
|
}
|
|
|
|
func utimensTimespec(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
var tim linux.Timespec
|
|
if _, err := tim.CopyIn(t, addr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding timespec: %s)", addr, err)
|
|
}
|
|
|
|
var ns string
|
|
switch tim.Nsec {
|
|
case linux.UTIME_NOW:
|
|
ns = "UTIME_NOW"
|
|
case linux.UTIME_OMIT:
|
|
ns = "UTIME_OMIT"
|
|
default:
|
|
ns = fmt.Sprintf("%v", tim.Nsec)
|
|
}
|
|
return fmt.Sprintf("%#x {sec=%v nsec=%s}", addr, tim.Sec, ns)
|
|
}
|
|
|
|
func timespec(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
var tim linux.Timespec
|
|
if _, err := tim.CopyIn(t, addr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding timespec: %s)", addr, err)
|
|
}
|
|
return fmt.Sprintf("%#x {sec=%v nsec=%v}", addr, tim.Sec, tim.Nsec)
|
|
}
|
|
|
|
func timeval(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
var tim linux.Timeval
|
|
if _, err := tim.CopyIn(t, addr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding timeval: %s)", addr, err)
|
|
}
|
|
|
|
return fmt.Sprintf("%#x {sec=%v usec=%v}", addr, tim.Sec, tim.Usec)
|
|
}
|
|
|
|
func utimbuf(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
var utim linux.Utime
|
|
if _, err := utim.CopyIn(t, addr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding utimbuf: %s)", addr, err)
|
|
}
|
|
|
|
return fmt.Sprintf("%#x {actime=%v, modtime=%v}", addr, utim.Actime, utim.Modtime)
|
|
}
|
|
|
|
func stat(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
var stat linux.Stat
|
|
if _, err := stat.CopyIn(t, addr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding stat: %s)", addr, err)
|
|
}
|
|
return fmt.Sprintf("%#x {dev=%d, ino=%d, mode=%s, nlink=%d, uid=%d, gid=%d, rdev=%d, size=%d, blksize=%d, blocks=%d, atime=%s, mtime=%s, ctime=%s}", addr, stat.Dev, stat.Ino, linux.FileMode(stat.Mode), stat.Nlink, stat.UID, stat.GID, stat.Rdev, stat.Size, stat.Blksize, stat.Blocks, time.Unix(stat.ATime.Sec, stat.ATime.Nsec), time.Unix(stat.MTime.Sec, stat.MTime.Nsec), time.Unix(stat.CTime.Sec, stat.CTime.Nsec))
|
|
}
|
|
|
|
func itimerval(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
interval := timeval(t, addr)
|
|
value := timeval(t, addr+hostarch.Addr((*linux.Timeval)(nil).SizeBytes()))
|
|
return fmt.Sprintf("%#x {interval=%s, value=%s}", addr, interval, value)
|
|
}
|
|
|
|
func itimerspec(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
interval := timespec(t, addr)
|
|
value := timespec(t, addr+hostarch.Addr((*linux.Timespec)(nil).SizeBytes()))
|
|
return fmt.Sprintf("%#x {interval=%s, value=%s}", addr, interval, value)
|
|
}
|
|
|
|
func stringVector(t *kernel.Task, addr hostarch.Addr) string {
|
|
vec, err := t.CopyInVector(addr, slinux.ExecMaxElemSize, slinux.ExecMaxTotalSize)
|
|
if err != nil {
|
|
return fmt.Sprintf("%#x {error copying vector: %v}", addr, err)
|
|
}
|
|
s := fmt.Sprintf("%#x [", addr)
|
|
for i, v := range vec {
|
|
if i != 0 {
|
|
s += ", "
|
|
}
|
|
s += fmt.Sprintf("%q", v)
|
|
}
|
|
s += "]"
|
|
return s
|
|
}
|
|
|
|
func rusage(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
var ru linux.Rusage
|
|
if _, err := ru.CopyIn(t, addr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding rusage: %s)", addr, err)
|
|
}
|
|
return fmt.Sprintf("%#x %+v", addr, ru)
|
|
}
|
|
|
|
func capHeader(t *kernel.Task, addr hostarch.Addr) string {
|
|
if addr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
var hdr linux.CapUserHeader
|
|
if _, err := hdr.CopyIn(t, addr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding header: %s)", addr, err)
|
|
}
|
|
|
|
var version string
|
|
switch hdr.Version {
|
|
case linux.LINUX_CAPABILITY_VERSION_1:
|
|
version = "1"
|
|
case linux.LINUX_CAPABILITY_VERSION_2:
|
|
version = "2"
|
|
case linux.LINUX_CAPABILITY_VERSION_3:
|
|
version = "3"
|
|
default:
|
|
version = strconv.FormatUint(uint64(hdr.Version), 16)
|
|
}
|
|
|
|
return fmt.Sprintf("%#x {Version: %s, Pid: %d}", addr, version, hdr.Pid)
|
|
}
|
|
|
|
func capData(t *kernel.Task, hdrAddr, dataAddr hostarch.Addr) string {
|
|
if dataAddr == 0 {
|
|
return "null"
|
|
}
|
|
|
|
var hdr linux.CapUserHeader
|
|
if _, err := hdr.CopyIn(t, hdrAddr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding header: %v)", dataAddr, err)
|
|
}
|
|
|
|
var p, i, e uint64
|
|
|
|
switch hdr.Version {
|
|
case linux.LINUX_CAPABILITY_VERSION_1:
|
|
var data linux.CapUserData
|
|
if _, err := data.CopyIn(t, dataAddr); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding data: %v)", dataAddr, err)
|
|
}
|
|
p = uint64(data.Permitted)
|
|
i = uint64(data.Inheritable)
|
|
e = uint64(data.Effective)
|
|
case linux.LINUX_CAPABILITY_VERSION_2, linux.LINUX_CAPABILITY_VERSION_3:
|
|
var data [2]linux.CapUserData
|
|
if _, err := linux.CopyCapUserDataSliceIn(t, dataAddr, data[:]); err != nil {
|
|
return fmt.Sprintf("%#x (error decoding data: %v)", dataAddr, err)
|
|
}
|
|
p = uint64(data[0].Permitted) | (uint64(data[1].Permitted) << 32)
|
|
i = uint64(data[0].Inheritable) | (uint64(data[1].Inheritable) << 32)
|
|
e = uint64(data[0].Effective) | (uint64(data[1].Effective) << 32)
|
|
default:
|
|
return fmt.Sprintf("%#x (unknown version %d)", dataAddr, hdr.Version)
|
|
}
|
|
|
|
return fmt.Sprintf("%#x {Permitted: %s, Inheritable: %s, Effective: %s}", dataAddr, CapabilityBitset.Parse(p), CapabilityBitset.Parse(i), CapabilityBitset.Parse(e))
|
|
}
|
|
|
|
// pre fills in the pre-execution arguments for a system call. If an argument
|
|
// cannot be interpreted before the system call is executed, then a hex value
|
|
// will be used. Note that a full output slice will always be provided, that is
|
|
// len(return) == len(args).
|
|
func (i *SyscallInfo) pre(t *kernel.Task, args arch.SyscallArguments, maximumBlobSize uint) []string {
|
|
var output []string
|
|
|
|
for arg := range args {
|
|
if arg >= len(i.format) {
|
|
break
|
|
}
|
|
switch i.format[arg] {
|
|
case FD:
|
|
output = append(output, fd(t, args[arg].Int()))
|
|
case WriteBuffer:
|
|
output = append(output, dump(t, args[arg].Pointer(), args[arg+1].SizeT(), maximumBlobSize, LogAppDataAllowed /* content */))
|
|
case WriteIOVec:
|
|
output = append(output, iovecs(t, args[arg].Pointer(), int(args[arg+1].Int()), LogAppDataAllowed /* content */, uint64(maximumBlobSize)))
|
|
case ReadIOVec, IOVec:
|
|
output = append(output, iovecs(t, args[arg].Pointer(), int(args[arg+1].Int()), false /* content */, uint64(maximumBlobSize)))
|
|
case SendMsgHdr:
|
|
output = append(output, msghdr(t, args[arg].Pointer(), LogAppDataAllowed /* content */, uint64(maximumBlobSize)))
|
|
case RecvMsgHdr:
|
|
output = append(output, msghdr(t, args[arg].Pointer(), false /* content */, uint64(maximumBlobSize)))
|
|
case Path:
|
|
output = append(output, path(t, args[arg].Pointer()))
|
|
case ExecveStringVector:
|
|
output = append(output, stringVector(t, args[arg].Pointer()))
|
|
case SetSockOptVal:
|
|
output = append(output, sockOptVal(t, args[arg-2].Uint64() /* level */, args[arg-1].Uint64() /* optName */, args[arg].Pointer() /* optVal */, args[arg+1].Uint64() /* optLen */, maximumBlobSize))
|
|
case SockOptLevel:
|
|
output = append(output, sockOptLevels.Parse(args[arg].Uint64()))
|
|
case SockOptName:
|
|
output = append(output, sockOptNames[args[arg-1].Uint64() /* level */].Parse(args[arg].Uint64()))
|
|
case SockAddr:
|
|
output = append(output, sockAddr(t, args[arg].Pointer(), uint32(args[arg+1].Uint64())))
|
|
case SockLen:
|
|
output = append(output, sockLenPointer(t, args[arg].Pointer()))
|
|
case SockFamily:
|
|
output = append(output, SocketFamily.Parse(uint64(args[arg].Int())))
|
|
case SockType:
|
|
output = append(output, sockType(args[arg].Int()))
|
|
case SockProtocol:
|
|
output = append(output, sockProtocol(args[arg-2].Int(), args[arg].Int()))
|
|
case SockFlags:
|
|
output = append(output, sockFlags(args[arg].Int()))
|
|
case Timespec:
|
|
output = append(output, timespec(t, args[arg].Pointer()))
|
|
case UTimeTimespec:
|
|
output = append(output, utimensTimespec(t, args[arg].Pointer()))
|
|
case ItimerVal:
|
|
output = append(output, itimerval(t, args[arg].Pointer()))
|
|
case ItimerSpec:
|
|
output = append(output, itimerspec(t, args[arg].Pointer()))
|
|
case Timeval:
|
|
output = append(output, timeval(t, args[arg].Pointer()))
|
|
case Utimbuf:
|
|
output = append(output, utimbuf(t, args[arg].Pointer()))
|
|
case CloneFlags:
|
|
output = append(output, CloneFlagSet.Parse(uint64(args[arg].Uint())))
|
|
case OpenFlags:
|
|
output = append(output, open(uint64(args[arg].Uint())))
|
|
case OpenHow:
|
|
output = append(output, openHow(t, args[arg].Pointer(), args[arg+1].SizeT()))
|
|
case Mode:
|
|
output = append(output, linux.FileMode(args[arg].ModeT()).String())
|
|
case FutexOp:
|
|
output = append(output, futex(uint64(args[arg].Uint())))
|
|
case PtraceRequest:
|
|
output = append(output, PtraceRequestSet.Parse(args[arg].Uint64()))
|
|
case ItimerType:
|
|
output = append(output, ItimerTypes.Parse(uint64(args[arg].Int())))
|
|
case Signal:
|
|
output = append(output, signalNames.ParseDecimal(args[arg].Uint64()))
|
|
case SignalMaskAction:
|
|
output = append(output, signalMaskActions.Parse(uint64(args[arg].Int())))
|
|
case SigSet:
|
|
output = append(output, sigSet(t, args[arg].Pointer()))
|
|
case SigAction:
|
|
output = append(output, sigAction(t, args[arg].Pointer()))
|
|
case CapHeader:
|
|
output = append(output, capHeader(t, args[arg].Pointer()))
|
|
case CapData:
|
|
output = append(output, capData(t, args[arg-1].Pointer(), args[arg].Pointer()))
|
|
case PollFDs:
|
|
output = append(output, pollFDs(t, args[arg].Pointer(), uint(args[arg+1].Uint()), false))
|
|
case EpollCtlOp:
|
|
output = append(output, epollCtlOps.Parse(uint64(args[arg].Int())))
|
|
case EpollEvent:
|
|
output = append(output, epollEvent(t, args[arg].Pointer()))
|
|
case EpollEvents:
|
|
output = append(output, epollEvents(t, args[arg].Pointer(), 0 /* numEvents */, uint64(maximumBlobSize)))
|
|
case SelectFDSet:
|
|
output = append(output, fdSet(t, int(args[0].Int()), args[arg].Pointer()))
|
|
case MmapProt:
|
|
output = append(output, ProtectionFlagSet.Parse(uint64(args[arg].Uint())))
|
|
case MmapFlags:
|
|
output = append(output, MmapFlagSet.Parse(uint64(args[arg].Uint())))
|
|
case CloseRangeFlags:
|
|
output = append(output, CloseRangeFlagSet.Parse(uint64(args[arg].Uint())))
|
|
case Oct:
|
|
output = append(output, "0o"+strconv.FormatUint(args[arg].Uint64(), 8))
|
|
case Hex:
|
|
fallthrough
|
|
default:
|
|
output = append(output, hexArg(args[arg]))
|
|
}
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
// post fills in the post-execution arguments for a system call. This modifies
|
|
// the given output slice in place with arguments that may only be interpreted
|
|
// after the system call has been executed.
|
|
func (i *SyscallInfo) post(t *kernel.Task, args arch.SyscallArguments, rval uintptr, output []string, maximumBlobSize uint) {
|
|
for arg := range output {
|
|
if arg >= len(i.format) {
|
|
break
|
|
}
|
|
switch i.format[arg] {
|
|
case ReadBuffer:
|
|
output[arg] = dump(t, args[arg].Pointer(), uint(rval), maximumBlobSize, LogAppDataAllowed /* content */)
|
|
case ReadIOVec:
|
|
printLength := uint64(rval)
|
|
if printLength > uint64(maximumBlobSize) {
|
|
printLength = uint64(maximumBlobSize)
|
|
}
|
|
output[arg] = iovecs(t, args[arg].Pointer(), int(args[arg+1].Int()), LogAppDataAllowed /* content */, printLength)
|
|
case WriteIOVec, IOVec, WriteBuffer:
|
|
// We already have a big blast from write.
|
|
output[arg] = "..."
|
|
case SendMsgHdr:
|
|
output[arg] = msghdr(t, args[arg].Pointer(), false /* content */, uint64(maximumBlobSize))
|
|
case RecvMsgHdr:
|
|
output[arg] = msghdr(t, args[arg].Pointer(), LogAppDataAllowed /* content */, uint64(maximumBlobSize))
|
|
case PostPath:
|
|
output[arg] = path(t, args[arg].Pointer())
|
|
case PipeFDs:
|
|
output[arg] = fdpair(t, args[arg].Pointer())
|
|
case Uname:
|
|
output[arg] = uname(t, args[arg].Pointer())
|
|
case Stat:
|
|
output[arg] = stat(t, args[arg].Pointer())
|
|
case PostSockAddr:
|
|
output[arg] = postSockAddr(t, args[arg].Pointer(), args[arg+1].Pointer())
|
|
case SockLen:
|
|
output[arg] = sockLenPointer(t, args[arg].Pointer())
|
|
case PostTimespec:
|
|
output[arg] = timespec(t, args[arg].Pointer())
|
|
case PostItimerVal:
|
|
output[arg] = itimerval(t, args[arg].Pointer())
|
|
case PostItimerSpec:
|
|
output[arg] = itimerspec(t, args[arg].Pointer())
|
|
case Timeval:
|
|
output[arg] = timeval(t, args[arg].Pointer())
|
|
case Rusage:
|
|
output[arg] = rusage(t, args[arg].Pointer())
|
|
case PostSigSet:
|
|
output[arg] = sigSet(t, args[arg].Pointer())
|
|
case PostSigAction:
|
|
output[arg] = sigAction(t, args[arg].Pointer())
|
|
case PostCapData:
|
|
output[arg] = capData(t, args[arg-1].Pointer(), args[arg].Pointer())
|
|
case PollFDs:
|
|
output[arg] = pollFDs(t, args[arg].Pointer(), uint(args[arg+1].Uint()), true)
|
|
case EpollEvents:
|
|
output[arg] = epollEvents(t, args[arg].Pointer(), uint64(rval), uint64(maximumBlobSize))
|
|
case GetSockOptVal:
|
|
output[arg] = getSockOptVal(t, args[arg-2].Uint64() /* level */, args[arg-1].Uint64() /* optName */, args[arg].Pointer() /* optVal */, args[arg+1].Pointer() /* optLen */, maximumBlobSize, rval)
|
|
case SetSockOptVal:
|
|
// No need to print the value again. While it usually
|
|
// isn't, the string version of this arg can be long.
|
|
output[arg] = hexArg(args[arg])
|
|
}
|
|
}
|
|
}
|
|
|
|
// printEntry prints the given system call entry.
|
|
func (i *SyscallInfo) printEnter(t *kernel.Task, args arch.SyscallArguments) []string {
|
|
output := i.pre(t, args, LogMaximumSize)
|
|
|
|
switch len(output) {
|
|
case 0:
|
|
t.Infof("%s E %s()", t.Name(), i.name)
|
|
case 1:
|
|
t.Infof("%s E %s(%s)", t.Name(), i.name,
|
|
output[0])
|
|
case 2:
|
|
t.Infof("%s E %s(%s, %s)", t.Name(), i.name,
|
|
output[0], output[1])
|
|
case 3:
|
|
t.Infof("%s E %s(%s, %s, %s)", t.Name(), i.name,
|
|
output[0], output[1], output[2])
|
|
case 4:
|
|
t.Infof("%s E %s(%s, %s, %s, %s)", t.Name(), i.name,
|
|
output[0], output[1], output[2], output[3])
|
|
case 5:
|
|
t.Infof("%s E %s(%s, %s, %s, %s, %s)", t.Name(), i.name,
|
|
output[0], output[1], output[2], output[3], output[4])
|
|
case 6:
|
|
t.Infof("%s E %s(%s, %s, %s, %s, %s, %s)", t.Name(), i.name,
|
|
output[0], output[1], output[2], output[3], output[4], output[5])
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
// printExit prints the given system call exit.
|
|
func (i *SyscallInfo) printExit(t *kernel.Task, elapsed time.Duration, output []string, args arch.SyscallArguments, retval uintptr, err error, errno int) {
|
|
var rval string
|
|
if err == nil {
|
|
// Fill in the output after successful execution.
|
|
i.post(t, args, retval, output, LogMaximumSize)
|
|
rval = fmt.Sprintf("%d (%#x) (%v)", retval, retval, elapsed)
|
|
} else {
|
|
rval = fmt.Sprintf("%d errno=%d (%s) (%v)", -1, errno, err, elapsed)
|
|
}
|
|
|
|
switch len(output) {
|
|
case 0:
|
|
t.Infof("%s X %s() = %s", t.Name(), i.name,
|
|
rval)
|
|
case 1:
|
|
t.Infof("%s X %s(%s) = %s", t.Name(), i.name,
|
|
output[0], rval)
|
|
case 2:
|
|
t.Infof("%s X %s(%s, %s) = %s", t.Name(), i.name,
|
|
output[0], output[1], rval)
|
|
case 3:
|
|
t.Infof("%s X %s(%s, %s, %s) = %s", t.Name(), i.name,
|
|
output[0], output[1], output[2], rval)
|
|
case 4:
|
|
t.Infof("%s X %s(%s, %s, %s, %s) = %s", t.Name(), i.name,
|
|
output[0], output[1], output[2], output[3], rval)
|
|
case 5:
|
|
t.Infof("%s X %s(%s, %s, %s, %s, %s) = %s", t.Name(), i.name,
|
|
output[0], output[1], output[2], output[3], output[4], rval)
|
|
case 6:
|
|
t.Infof("%s X %s(%s, %s, %s, %s, %s, %s) = %s", t.Name(), i.name,
|
|
output[0], output[1], output[2], output[3], output[4], output[5], rval)
|
|
}
|
|
}
|
|
|
|
// sendEnter sends the syscall enter to event log.
|
|
func (i *SyscallInfo) sendEnter(t *kernel.Task, args arch.SyscallArguments) []string {
|
|
output := i.pre(t, args, EventMaximumSize)
|
|
|
|
event := pb.Strace{
|
|
Process: t.Name(),
|
|
Function: i.name,
|
|
Info: &pb.Strace_Enter{
|
|
Enter: &pb.StraceEnter{},
|
|
},
|
|
}
|
|
event.Args = append(event.Args, output...)
|
|
eventchannel.Emit(&event)
|
|
|
|
return output
|
|
}
|
|
|
|
// sendExit sends the syscall exit to event log.
|
|
func (i *SyscallInfo) sendExit(t *kernel.Task, elapsed time.Duration, output []string, args arch.SyscallArguments, rval uintptr, err error, errno int) {
|
|
if err == nil {
|
|
// Fill in the output after successful execution.
|
|
i.post(t, args, rval, output, EventMaximumSize)
|
|
}
|
|
|
|
exit := &pb.StraceExit{
|
|
Return: fmt.Sprintf("%#x", rval),
|
|
ElapsedNs: elapsed.Nanoseconds(),
|
|
}
|
|
if err != nil {
|
|
exit.Error = err.Error()
|
|
exit.ErrNo = int64(errno)
|
|
}
|
|
event := pb.Strace{
|
|
Process: t.Name(),
|
|
Function: i.name,
|
|
Info: &pb.Strace_Exit{Exit: exit},
|
|
}
|
|
event.Args = append(event.Args, output...)
|
|
eventchannel.Emit(&event)
|
|
}
|
|
|
|
type syscallContext struct {
|
|
info SyscallInfo
|
|
args arch.SyscallArguments
|
|
start time.Time
|
|
logOutput []string
|
|
eventOutput []string
|
|
flags uint32
|
|
}
|
|
|
|
// SyscallEnter implements kernel.Stracer.SyscallEnter. It logs the syscall
|
|
// entry trace.
|
|
func (s SyscallMap) SyscallEnter(t *kernel.Task, sysno uintptr, args arch.SyscallArguments, flags uint32) any {
|
|
info, ok := s[sysno]
|
|
if !ok {
|
|
info = SyscallInfo{
|
|
name: fmt.Sprintf("sys_%d", sysno),
|
|
format: defaultFormat,
|
|
}
|
|
}
|
|
|
|
var output, eventOutput []string
|
|
if bits.IsOn(flags, kernel.StraceEnableLog) {
|
|
output = info.printEnter(t, args)
|
|
}
|
|
if bits.IsOn(flags, kernel.StraceEnableEvent) {
|
|
eventOutput = info.sendEnter(t, args)
|
|
}
|
|
|
|
return &syscallContext{
|
|
info: info,
|
|
args: args,
|
|
start: time.Now(),
|
|
logOutput: output,
|
|
eventOutput: eventOutput,
|
|
flags: flags,
|
|
}
|
|
}
|
|
|
|
// SyscallExit implements kernel.Stracer.SyscallExit. It logs the syscall
|
|
// exit trace.
|
|
func (s SyscallMap) SyscallExit(context any, t *kernel.Task, sysno, rval uintptr, err error) {
|
|
errno := kernel.ExtractErrno(err, int(sysno))
|
|
c := context.(*syscallContext)
|
|
|
|
elapsed := time.Since(c.start)
|
|
if bits.IsOn(c.flags, kernel.StraceEnableLog) {
|
|
c.info.printExit(t, elapsed, c.logOutput, c.args, rval, err, errno)
|
|
}
|
|
if bits.IsOn(c.flags, kernel.StraceEnableEvent) {
|
|
c.info.sendExit(t, elapsed, c.eventOutput, c.args, rval, err, errno)
|
|
}
|
|
}
|
|
|
|
// ConvertToSysnoMap converts the names to a map keyed on the syscall number
|
|
// and value set to true.
|
|
//
|
|
// The map is in a convenient format to pass to SyscallFlagsTable.Enable().
|
|
func (s SyscallMap) ConvertToSysnoMap(syscalls []string) (map[uintptr]bool, error) {
|
|
if syscalls == nil {
|
|
// Sentinel: no list.
|
|
return nil, nil
|
|
}
|
|
|
|
l := make(map[uintptr]bool)
|
|
for _, sc := range syscalls {
|
|
// Try to match this system call.
|
|
sysno, ok := s.ConvertToSysno(sc)
|
|
if !ok {
|
|
return nil, fmt.Errorf("syscall %q not found", sc)
|
|
}
|
|
l[sysno] = true
|
|
}
|
|
|
|
// Success.
|
|
return l, nil
|
|
}
|
|
|
|
// ConvertToSysno converts the name to system call number. Returns false
|
|
// if syscall with same name is not found.
|
|
func (s SyscallMap) ConvertToSysno(syscall string) (uintptr, bool) {
|
|
for sysno, info := range s {
|
|
if info.name != "" && info.name == syscall {
|
|
return sysno, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// Name returns the syscall name.
|
|
func (s SyscallMap) Name(sysno uintptr) string {
|
|
if info, ok := s[sysno]; ok {
|
|
return info.name
|
|
}
|
|
return fmt.Sprintf("sys_%d", sysno)
|
|
}
|
|
|
|
// Initialize prepares all syscall tables for use by this package.
|
|
//
|
|
// N.B. This is not in an init function because we can't be sure all syscall
|
|
// tables are registered with the kernel when init runs.
|
|
func Initialize() {
|
|
for _, table := range kernel.SyscallTables() {
|
|
// Is this known?
|
|
sys, ok := Lookup(table.OS, table.Arch)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
table.Stracer = sys
|
|
}
|
|
}
|
|
|
|
// SinkType defines where to send straces to.
|
|
type SinkType uint32
|
|
|
|
const (
|
|
// SinkTypeLog sends straces to text log
|
|
SinkTypeLog SinkType = 1 << iota
|
|
|
|
// SinkTypeEvent sends strace to event log
|
|
SinkTypeEvent
|
|
)
|
|
|
|
func convertToSyscallFlag(sinks SinkType) uint32 {
|
|
ret := uint32(0)
|
|
if bits.IsOn(uint32(sinks), uint32(SinkTypeLog)) {
|
|
ret |= kernel.StraceEnableLog
|
|
}
|
|
if bits.IsOn(uint32(sinks), uint32(SinkTypeEvent)) {
|
|
ret |= kernel.StraceEnableEvent
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Enable enables the syscalls in allowlist in all syscall tables.
|
|
//
|
|
// Preconditions: Initialize has been called.
|
|
func Enable(allowlist []string, sinks SinkType) error {
|
|
flags := convertToSyscallFlag(sinks)
|
|
for _, table := range kernel.SyscallTables() {
|
|
// Is this known?
|
|
sys, ok := Lookup(table.OS, table.Arch)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Convert to a set of system calls numbers.
|
|
wl, err := sys.ConvertToSysnoMap(allowlist)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
table.FeatureEnable.Enable(flags, wl, true)
|
|
}
|
|
|
|
// Done.
|
|
return nil
|
|
}
|
|
|
|
// Disable will disable Strace for all system calls and missing syscalls.
|
|
//
|
|
// Preconditions: Initialize has been called.
|
|
func Disable(sinks SinkType) {
|
|
flags := convertToSyscallFlag(sinks)
|
|
for _, table := range kernel.SyscallTables() {
|
|
// Strace will be disabled for all syscalls including missing.
|
|
table.FeatureEnable.Enable(flags, nil, false)
|
|
}
|
|
}
|
|
|
|
// EnableAll enables all syscalls in all syscall tables.
|
|
//
|
|
// Preconditions: Initialize has been called.
|
|
func EnableAll(sinks SinkType) {
|
|
flags := convertToSyscallFlag(sinks)
|
|
for _, table := range kernel.SyscallTables() {
|
|
// Is this known?
|
|
if _, ok := Lookup(table.OS, table.Arch); !ok {
|
|
continue
|
|
}
|
|
|
|
table.FeatureEnable.EnableAll(flags)
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
t, ok := Lookup(abi.Host, arch.Host)
|
|
if ok {
|
|
// Provide the native table as the lookup for seccomp
|
|
// debugging. This is best-effort. This is provided this way to
|
|
// avoid dependencies from seccomp to this package.
|
|
seccomp.SyscallName = t.Name
|
|
}
|
|
}
|