Files
visor/pkg/sentry/kernel/task_syscall_benchmark_test.go
2026-08-27 10:25:43 -07:00

242 lines
6.1 KiB
Go

// Copyright 2026 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
import (
"testing"
_ "unsafe"
"google.golang.org/protobuf/proto"
"gvisor.dev/gvisor/pkg/sentry/arch"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
pointspb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
)
type myNullSink struct {
seccheck.SinkDefaults
}
func (myNullSink) Name() string { return "null" }
// BenchmarkExecuteSyscall measures the internal overhead of seccheck data
// collection during syscall execution.
//
// Unlike end-to-end benchmarks, this benchmark runs the executeSyscall loop with a dummy task and
// a null sink. This isolates the CPU and memory allocation costs of payload generation
// in LoadSeccheckData (e.g., CWD, Time, Credentials) from all IPC and context switch noise.
//
// To run the benchmark:
//
// make test TARGETS="//pkg/sentry/kernel:kernel_test" \
// OPTIONS="--test_arg=-test.bench=BenchmarkExecuteSyscall --test_arg=-test.benchmem --test_output=all --nocache_test_results"
func BenchmarkExecuteSyscall(b *testing.B) {
testCases := []struct {
name string
fm func() seccheck.FieldMask
}{
{
name: "Empty",
fm: func() seccheck.FieldMask { return seccheck.FieldMask{} },
},
{
// Tests a trivial sub-nanosecond struct property read that doesn't allocate memory.
name: "ThreadStartTime",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtThreadStartTime)
return fm
},
},
{
// Tests the interface overhead of reaching into timekeeper and invoking clock functions.
name: "Time",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtTime)
return fm
},
},
{
// Tests locking overhead (task.k.tasks.mu.RLock()) and sets the groundwork for tracking the most
// expensive path in gVisor: VFS traversal and string-building.
name: "Cwd",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtCwd)
return fm
},
},
{
// Tests protobuf nesting. Calling task.Credentials().LoadSeccheckData(...)
// dynamically builds out nested structs (euid, suid, fsuid, groups, etc.)
// and performs heavier allocations.
name: "Credentials",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtCredentials)
return fm
},
},
{
name: "ThreadID",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtThreadID)
return fm
},
},
{
name: "ThreadGroupID",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtThreadGroupID)
return fm
},
},
{
name: "ThreadGroupStartTime",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtThreadGroupStartTime)
return fm
},
},
{
name: "ContainerID",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtContainerID)
return fm
},
},
{
name: "ProcessName",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtProcessName)
return fm
},
},
{
name: "ParentThreadGroupID",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtParentThreadGroupID)
return fm
},
},
{
name: "IsExecSession",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtIsExecSession)
return fm
},
},
{
name: "All",
fm: func() seccheck.FieldMask {
fm := seccheck.FieldMask{}
fm.Add(seccheck.FieldCtxtTime)
fm.Add(seccheck.FieldCtxtThreadID)
fm.Add(seccheck.FieldCtxtThreadStartTime)
fm.Add(seccheck.FieldCtxtThreadGroupID)
fm.Add(seccheck.FieldCtxtThreadGroupStartTime)
fm.Add(seccheck.FieldCtxtContainerID)
fm.Add(seccheck.FieldCtxtCredentials)
fm.Add(seccheck.FieldCtxtCwd)
fm.Add(seccheck.FieldCtxtProcessName)
fm.Add(seccheck.FieldCtxtParentThreadGroupID)
fm.Add(seccheck.FieldCtxtIsExecSession)
return fm
},
},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
seccheck.Global = seccheck.State{}
seccheck.Initialize()
pt := seccheck.GetPointForSyscall(seccheck.SyscallEnter, 0)
reqs := []seccheck.PointReq{
{
Pt: pt,
Fields: seccheck.FieldSet{
Context: tc.fm(),
},
},
}
seccheck.Global.AppendSink(myNullSink{}, reqs)
_, params := stateTestClocklessTimekeeper(b)
tk := NewTimekeeper()
tk.SetClocks(&mockClocks{}, params)
k := &Kernel{
timekeeper: tk,
tasks: &TaskSet{},
}
creds := auth.NewRootCredentials(auth.NewRootUserNamespace())
task := &Task{
k: k,
}
st := &SyscallTable{
Missing: func(task *Task, sysno uintptr, args arch.SyscallArguments) (uintptr, error) {
return 0, nil
},
}
st.pointCallbacks[0] = func(task *Task, fields seccheck.FieldSet, ctxData *pointspb.ContextData, info SyscallInfo) (proto.Message, pointspb.MessageType) {
return nil, pointspb.MessageType(0)
}
tg := &ThreadGroup{
threadGroupNode: threadGroupNode{
leader: task,
},
}
task.tg = tg
pidns := &PIDNamespace{
tids: map[*Task]ThreadID{task: 1},
tgids: map[*ThreadGroup]ThreadID{tg: 1},
}
k.tasks.Root = pidns
ti := TaskImage{
st: st,
}
task.image = ti
task.creds.Store(creds)
cwd := &FSContext{}
task.fsContext.Store(cwd)
task.SyscallTable().FeatureEnable.EnableAll(SecCheckEnter)
var args arch.SyscallArguments
for b.Loop() {
task.executeSyscall(0, args)
}
})
}
}