Files
Ryan El Kochta efb3bcd85f Implement openat2(2) in gVisor
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).
2026-08-21 10:34:56 -07:00

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