From 575d76def24e884609890b6d0fce0ed64c5b1bb4 Mon Sep 17 00:00:00 2001 From: Fabricio Voznika Date: Fri, 29 Apr 2022 14:47:17 -0700 Subject: [PATCH] Add support for syscall points Each syscall provides 4 different points. There is a raw syscall point that contains the syscall number and all 6 arguments, nothing else. Some syscalls can provide a schematized version of the syscall by defining a function that converts the syscall into a proto representing the syscall. Each of these flavors have a point for enter and another for exit. In both cases, the exit event adds return value and errno (if any). Updates #4805 PiperOrigin-RevId: 445510907 --- pkg/sentry/kernel/BUILD | 1 + pkg/sentry/kernel/syscalls.go | 41 ++++++++- pkg/sentry/kernel/task_syscall.go | 84 +++++++++++++++++++ pkg/sentry/seccheck/BUILD | 1 + pkg/sentry/seccheck/checkers/remote/remote.go | 12 +++ pkg/sentry/seccheck/metadata.go | 25 ++++++ pkg/sentry/seccheck/points/BUILD | 1 + pkg/sentry/seccheck/points/syscall.proto | 36 ++++++++ pkg/sentry/seccheck/seccheck.go | 42 +++++----- pkg/sentry/seccheck/syscall.go | 52 ++++++++++++ 10 files changed, 272 insertions(+), 23 deletions(-) create mode 100644 pkg/sentry/seccheck/points/syscall.proto create mode 100644 pkg/sentry/seccheck/syscall.go diff --git a/pkg/sentry/kernel/BUILD b/pkg/sentry/kernel/BUILD index 0d3093688..6249de0bf 100644 --- a/pkg/sentry/kernel/BUILD +++ b/pkg/sentry/kernel/BUILD @@ -291,6 +291,7 @@ go_library( "//pkg/tcpip/stack", "//pkg/usermem", "//pkg/waiter", + "@org_golang_google_protobuf//proto:go_default_library", "@org_golang_x_sys//unix:go_default_library", ], ) diff --git a/pkg/sentry/kernel/syscalls.go b/pkg/sentry/kernel/syscalls.go index b3a78bed4..35da667fc 100644 --- a/pkg/sentry/kernel/syscalls.go +++ b/pkg/sentry/kernel/syscalls.go @@ -17,11 +17,15 @@ package kernel import ( "fmt" + "google.golang.org/protobuf/proto" "gvisor.dev/gvisor/pkg/abi" "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/bits" + "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/sentry/arch" + "gvisor.dev/gvisor/pkg/sentry/seccheck" + pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto" "gvisor.dev/gvisor/pkg/sync" ) @@ -31,7 +35,9 @@ const ( // The types below create fast lookup slices for all syscalls. This maximum // serves as a sanity check that we don't allocate huge slices for a very large // syscall. This is checked during registration. + // LINT.IfChange maxSyscallNum = 2000 + // LINT.ThenChange(../seccheck/syscall.go) ) // SyscallSupportLevel is a syscall support levels. @@ -77,6 +83,9 @@ type Syscall struct { Note string // URLs is set of URLs to any relevant bugs or issues. URLs []string + // PointCallback is an optional callback that converts syscall arguments + // to a proto that can be used with seccheck.Checker. + PointCallback SyscallToProto } // SyscallFn is a syscall implementation. @@ -242,6 +251,11 @@ type SyscallTable struct { // their numbers). It is used for fast look ups. lookup [maxSyscallNum + 1]SyscallFn + // pointCallbacks is a fixed-size array that holds SyscallToProto callbacks + // (indexed by syscall numbers). It is used for fast lookups when + // seccheck.Point is enabled for the syscall. + pointCallbacks [maxSyscallNum + 1]SyscallToProto + // Emulate is a collection of instruction addresses to emulate. The // keys are addresses, and the values are system call numbers. Emulate map[hostarch.Addr]uintptr @@ -320,10 +334,13 @@ func (s *SyscallTable) Init() { s.Emulate = make(map[hostarch.Addr]uintptr) } - // Initialize the fast-lookup table. + // Initialize the fast-lookup tables. for num, sc := range s.Table { s.lookup[num] = sc.Fn } + for num, sc := range s.Table { + s.pointCallbacks[num] = sc.PointCallback + } // Initialize all features. s.FeatureEnable.init(s.Table) @@ -369,3 +386,25 @@ func (s *SyscallTable) mapLookup(sysno uintptr) SyscallFn { } return nil } + +// LookupSyscallToProto looks up the SyscallToProto callback for the given +// syscall. It may return nil if none is registered. +func (s *SyscallTable) LookupSyscallToProto(sysno uintptr) SyscallToProto { + if sysno > maxSyscallNum { + return nil + } + return s.pointCallbacks[sysno] +} + +// SyscallToProto is a callback function that converts generic syscall data to +// schematized protobuf for the corresponding syscall. +type SyscallToProto func(context.Context, seccheck.FieldSet, *pb.ContextData, SyscallInfo) proto.Message + +// SyscallInfo provides generic information about the syscall. +type SyscallInfo struct { + Exit bool + Sysno uintptr + Args arch.SyscallArguments + Rval uintptr + Errno int +} diff --git a/pkg/sentry/kernel/task_syscall.go b/pkg/sentry/kernel/task_syscall.go index 2b85fe1ca..28b235705 100644 --- a/pkg/sentry/kernel/task_syscall.go +++ b/pkg/sentry/kernel/task_syscall.go @@ -29,6 +29,8 @@ import ( "gvisor.dev/gvisor/pkg/metric" "gvisor.dev/gvisor/pkg/sentry/arch" "gvisor.dev/gvisor/pkg/sentry/memmap" + "gvisor.dev/gvisor/pkg/sentry/seccheck" + pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto" ) // SyscallRestartBlock represents the restart block for a syscall restartable @@ -88,6 +90,44 @@ func (t *Task) executeSyscall(sysno uintptr, args arch.SyscallArguments) (rval u straceContext = s.Stracer.SyscallEnter(t, sysno, args, fe) } + if seccheck.Global.SyscallEnabled(seccheck.SyscallRawEnter, sysno) { + info := pb.Syscall{ + Sysno: uint64(sysno), + Arg1: args[0].Uint64(), + Arg2: args[1].Uint64(), + Arg3: args[2].Uint64(), + Arg4: args[3].Uint64(), + Arg5: args[4].Uint64(), + Arg6: args[5].Uint64(), + } + fields := seccheck.Global.GetFieldSet(seccheck.GetPointForSyscall(seccheck.SyscallRawEnter, sysno)) + if !fields.Context.Empty() { + info.ContextData = &pb.ContextData{} + LoadSeccheckData(t, fields.Context, info.ContextData) + } + seccheck.Global.SendToCheckers(func(c seccheck.Checker) error { + return c.RawSyscall(t, fields, &info) + }) + } + if seccheck.Global.SyscallEnabled(seccheck.SyscallEnter, sysno) { + fields := seccheck.Global.GetFieldSet(seccheck.GetPointForSyscall(seccheck.SyscallEnter, sysno)) + var ctxData *pb.ContextData + if !fields.Context.Empty() { + ctxData = &pb.ContextData{} + LoadSeccheckData(t, fields.Context, ctxData) + } + info := SyscallInfo{ + Sysno: sysno, + Args: args, + } + // TODO(fvoznika): Make cb take a *Task instead of Context. + cb := t.SyscallTable().LookupSyscallToProto(sysno) + msg := cb(t, fields, ctxData, info) + seccheck.Global.SendToCheckers(func(c seccheck.Checker) error { + return c.Syscall(t, fields, ctxData, msg) + }) + } + if bits.IsOn32(fe, ExternalBeforeEnable) && (s.ExternalFilterBefore == nil || s.ExternalFilterBefore(t, sysno, args)) { t.invokeExternal() // Ensure we check for stops, then invoke the syscall again. @@ -119,6 +159,50 @@ func (t *Task) executeSyscall(sysno uintptr, args arch.SyscallArguments) (rval u s.Stracer.SyscallExit(straceContext, t, sysno, rval, err) } + if seccheck.Global.SyscallEnabled(seccheck.SyscallRawExit, sysno) { + info := pb.Syscall{ + Sysno: uint64(sysno), + Arg1: args[0].Uint64(), + Arg2: args[1].Uint64(), + Arg3: args[2].Uint64(), + Arg4: args[3].Uint64(), + Arg5: args[4].Uint64(), + Arg6: args[5].Uint64(), + Exit: &pb.Exit{ + Result: int64(rval), + Errorno: int64(ExtractErrno(err, int(sysno))), + }, + } + fields := seccheck.Global.GetFieldSet(seccheck.GetPointForSyscall(seccheck.SyscallRawEnter, sysno)) + if !fields.Context.Empty() { + info.ContextData = &pb.ContextData{} + LoadSeccheckData(t, fields.Context, info.ContextData) + } + seccheck.Global.SendToCheckers(func(c seccheck.Checker) error { + return c.RawSyscall(t, fields, &info) + }) + } + if seccheck.Global.SyscallEnabled(seccheck.SyscallExit, sysno) { + cb := t.SyscallTable().LookupSyscallToProto(sysno) + fields := seccheck.Global.GetFieldSet(seccheck.GetPointForSyscall(seccheck.SyscallExit, sysno)) + var ctxData *pb.ContextData + if !fields.Context.Empty() { + ctxData = &pb.ContextData{} + LoadSeccheckData(t, fields.Context, ctxData) + } + info := SyscallInfo{ + Exit: true, + Sysno: sysno, + Args: args, + Rval: rval, + Errno: ExtractErrno(err, int(sysno)), + } + msg := cb(t, fields, ctxData, info) + seccheck.Global.SendToCheckers(func(c seccheck.Checker) error { + return c.Syscall(t, fields, ctxData, msg) + }) + } + return } diff --git a/pkg/sentry/seccheck/BUILD b/pkg/sentry/seccheck/BUILD index 972215062..32ef90384 100644 --- a/pkg/sentry/seccheck/BUILD +++ b/pkg/sentry/seccheck/BUILD @@ -21,6 +21,7 @@ go_library( "metadata.go", "seccheck.go", "seqatomic_checkerslice_unsafe.go", + "syscall.go", ], visibility = ["//:sandbox"], deps = [ diff --git a/pkg/sentry/seccheck/checkers/remote/remote.go b/pkg/sentry/seccheck/checkers/remote/remote.go index ceb072d36..d919d677f 100644 --- a/pkg/sentry/seccheck/checkers/remote/remote.go +++ b/pkg/sentry/seccheck/checkers/remote/remote.go @@ -185,3 +185,15 @@ func (r *Remote) ContainerStart(_ context.Context, _ seccheck.FieldSet, info *pb r.write(info) return nil } + +// RawSyscall implements seccheck.Checker. +func (r *Remote) RawSyscall(_ context.Context, _ seccheck.FieldSet, info *pb.Syscall) error { + r.write(info) + return nil +} + +// Syscall implements seccheck.Checker. +func (r *Remote) Syscall(ctx context.Context, fields seccheck.FieldSet, ctxData *pb.ContextData, msg proto.Message) error { + r.write(msg) + return nil +} diff --git a/pkg/sentry/seccheck/metadata.go b/pkg/sentry/seccheck/metadata.go index 7309094b3..649376042 100644 --- a/pkg/sentry/seccheck/metadata.go +++ b/pkg/sentry/seccheck/metadata.go @@ -21,6 +21,31 @@ import ( "gvisor.dev/gvisor/pkg/fd" ) +// PointX represents the checkpoint X. +const ( + PointCloneProcess Point = iota + PointContainerStart + PointExecve + PointExitNotifyParent + PointTaskExit + + // Add new Points above this line. + pointLengthBeforeSyscalls +) + +// FieldCtxtX represents a data field that comes from the Context. +const ( + FieldCtxtContainerID Field = iota + FieldCtxtCredentials + FieldCtxtCwd + FieldCtxtProcessName + FieldCtxtThreadGroupID + FieldCtxtThreadGroupStartTime + FieldCtxtThreadID + FieldCtxtThreadStartTime + FieldCtxtTime +) + const ( // ContainerStartFieldEnv is an optional field to collect list of environment // variables set for the container start process. diff --git a/pkg/sentry/seccheck/points/BUILD b/pkg/sentry/seccheck/points/BUILD index d584d55fa..b2eb8aa7c 100644 --- a/pkg/sentry/seccheck/points/BUILD +++ b/pkg/sentry/seccheck/points/BUILD @@ -10,5 +10,6 @@ proto_library( "common.proto", "container.proto", "sentry.proto", + "syscall.proto", ], ) diff --git a/pkg/sentry/seccheck/points/syscall.proto b/pkg/sentry/seccheck/points/syscall.proto new file mode 100644 index 000000000..1931d94e8 --- /dev/null +++ b/pkg/sentry/seccheck/points/syscall.proto @@ -0,0 +1,36 @@ +// Copyright 2022 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. + +syntax = "proto3"; + +package gvisor.syscall; + +import "pkg/sentry/seccheck/points/common.proto"; + +message Exit { + int64 result = 1; + int64 errorno = 2; +} + +message Syscall { + gvisor.common.ContextData context_data = 1; + Exit exit = 2; + uint64 sysno = 4; + uint64 arg1 = 5; + uint64 arg2 = 6; + uint64 arg3 = 7; + uint64 arg4 = 8; + uint64 arg5 = 9; + uint64 arg6 = 10; +} diff --git a/pkg/sentry/seccheck/seccheck.go b/pkg/sentry/seccheck/seccheck.go index 986aee34e..9b4a7351d 100644 --- a/pkg/sentry/seccheck/seccheck.go +++ b/pkg/sentry/seccheck/seccheck.go @@ -17,6 +17,7 @@ package seccheck import ( + "google.golang.org/protobuf/proto" "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/context" pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto" @@ -28,28 +29,8 @@ type Point uint // PointX represents the checkpoint X. const ( - PointCloneProcess Point = iota - PointExecve - PointExitNotifyParent - PointContainerStart - PointTaskExit - // Add new Points above this line. - pointLength - - numPointBitmaskUint32s = (int(pointLength)-1)/32 + 1 -) - -// FieldCtxtX represents a data field that comes from the Context. -const ( - FieldCtxtTime Field = iota - FieldCtxtThreadID - FieldCtxtThreadStartTime - FieldCtxtThreadGroupID - FieldCtxtThreadGroupStartTime - FieldCtxtContainerID - FieldCtxtCredentials - FieldCtxtCwd - FieldCtxtProcessName + totalPoints = int(pointLengthBeforeSyscalls) + syscallPoints + numPointBitmaskUint32s = (totalPoints-1)/32 + 1 ) // FieldSet contains all optional fields to be collected by a given Point. @@ -119,7 +100,11 @@ type Checker interface { Execve(ctx context.Context, fields FieldSet, info *pb.ExecveInfo) error ExitNotifyParent(ctx context.Context, fields FieldSet, info *pb.ExitNotifyParentInfo) error TaskExit(context.Context, FieldSet, *pb.TaskExit) error + ContainerStart(context.Context, FieldSet, *pb.Start) error + + Syscall(context.Context, FieldSet, *pb.ContextData, proto.Message) error + RawSyscall(context.Context, FieldSet, *pb.Syscall) error } // CheckerDefaults may be embedded by implementations of Checker to obtain @@ -153,6 +138,16 @@ func (CheckerDefaults) TaskExit(context.Context, FieldSet, *pb.TaskExit) error { return nil } +// RawSyscall implements Checker.RawSyscall. +func (CheckerDefaults) RawSyscall(context.Context, FieldSet, *pb.Syscall) error { + return nil +} + +// Syscall implements Checker.Syscall. +func (CheckerDefaults) Syscall(context.Context, FieldSet, *pb.ContextData, proto.Message) error { + return nil +} + // PointReq indicates what Point a corresponding Checker runs at, and what // information it requires at those Points. type PointReq struct { @@ -209,6 +204,9 @@ func (s *State) AppendChecker(c Checker, reqs []PointReq) { // Enabled returns true if any Checker is registered for the given checkpoint. func (s *State) Enabled(p Point) bool { word, bit := p/32, p%32 + if int(word) >= len(s.enabledPoints) { + return false + } return s.enabledPoints[word].Load()&(uint32(1)< {path, flags, mode}. +// Raw uses a generic schema that contains syscall number and 6 arguments. Each +// of these type have a corresponding enter and exit points. Exit points include +// return value and errno information. +type SyscallType int + +const ( + // SyscallEnter represents schematized/enter syscall. + SyscallEnter SyscallType = iota + // SyscallExit represents schematized/exit syscall. + SyscallExit + // SyscallRawEnter represents raw/enter syscall. + SyscallRawEnter + // SyscallRawExit represents raw/exit syscall. + SyscallRawExit + + syscallTypesCount +) + +const ( + // Copied from kernel.maxSyscallNum to avoid reverse dependency. + syscallsMax = 2000 + syscallPoints = syscallsMax * int(syscallTypesCount) +) + +// GetPointForSyscall translates the syscall number to the corresponding Point. +func GetPointForSyscall(typ SyscallType, sysno uintptr) Point { + return Point(sysno)*Point(syscallTypesCount) + Point(typ) + pointLengthBeforeSyscalls +} + +// SyscallEnabled checks if the corresponding point for the syscall is enabled. +func (s *State) SyscallEnabled(typ SyscallType, sysno uintptr) bool { + return s.Enabled(GetPointForSyscall(typ, sysno)) +}