Use proto structs for seccheck points

Given that in most cases points are serialized to another process,
point data is now created diretly into protos.

As part of this change, infrastructure to track optional and context
fields was created to facilitate addition of lots of Points which is
needed for upcomming of changes.

Updates #4805

Currently the SST code is converting seccheck protos into SST protos
in the sentry before sending it to the API. After this change, SST
checker will be changed to send seccheck protos to the API and the
API then converts these into SST on the way to pubsub.

PiperOrigin-RevId: 442688320
This commit is contained in:
Fabricio Voznika
2022-04-18 19:03:15 -07:00
committed by gVisor bot
parent 59c7cd5ddf
commit 8a24f200e9
18 changed files with 528 additions and 316 deletions
+2
View File
@@ -165,6 +165,7 @@ go_library(
"ptrace_amd64.go",
"ptrace_arm64.go",
"rseq.go",
"seccheck.go",
"seccomp.go",
"seqatomic_taskgoroutineschedinfo_unsafe.go",
"session_list.go",
@@ -272,6 +273,7 @@ go_library(
"//pkg/sentry/pgalloc",
"//pkg/sentry/platform",
"//pkg/sentry/seccheck",
"//pkg/sentry/seccheck/points:points_go_proto",
"//pkg/sentry/socket/netlink/port",
"//pkg/sentry/socket/unix/transport",
"//pkg/sentry/time",
+2
View File
@@ -65,6 +65,8 @@ go_library(
"//pkg/context",
"//pkg/errors/linuxerr",
"//pkg/log",
"//pkg/sentry/seccheck",
"//pkg/sentry/seccheck/points:points_go_proto",
"//pkg/sync",
],
)
+16
View File
@@ -17,6 +17,8 @@ package auth
import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
)
// Credentials contains information required to authorize privileged operations
@@ -260,3 +262,17 @@ func (c *Credentials) SetGID(gid GID) error {
c.SavedKGID = kgid
return nil
}
// LoadSeccheckData sets credential data based on mask.
func (c *Credentials) LoadSeccheckData(mask seccheck.FieldMask, info *pb.ContextData) {
if mask.Contains(seccheck.FieldCtxtCredentials) {
info.Credentials = &pb.Credentials{
RealUid: uint32(c.RealKUID),
EffectiveUid: uint32(c.EffectiveKUID),
SavedUid: uint32(c.SavedKUID),
RealGid: uint32(c.RealKGID),
EffectiveGid: uint32(c.EffectiveKGID),
SavedGid: uint32(c.SavedKGID),
}
}
}
+67
View File
@@ -0,0 +1,67 @@
// 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.
package kernel
import (
"gvisor.dev/gvisor/pkg/sentry/seccheck"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
)
// LoadSeccheckData sets info from the task based on mask.
func LoadSeccheckData(t *Task, mask seccheck.FieldMask, info *pb.ContextData) {
t.k.tasks.mu.RLock()
defer t.k.tasks.mu.RUnlock()
LoadSeccheckDataLocked(t, mask, info)
}
// LoadSeccheckDataLocked sets info from the task based on mask.
//
// Preconditions: The TaskSet mutex must be locked.
func LoadSeccheckDataLocked(t *Task, mask seccheck.FieldMask, info *pb.ContextData) {
if mask.Contains(seccheck.FieldCtxtTime) {
info.TimeNs = t.k.RealtimeClock().Now().Nanoseconds()
}
if t == nil {
return
}
if mask.Contains(seccheck.FieldCtxtThreadID) {
info.ThreadId = int32(t.k.tasks.Root.tids[t])
}
if mask.Contains(seccheck.FieldCtxtThreadStartTime) {
info.ThreadStartTimeNs = t.startTime.Nanoseconds()
}
if mask.Contains(seccheck.FieldCtxtThreadGroupID) {
info.ThreadGroupId = int32(t.k.tasks.Root.tgids[t.tg])
}
if mask.Contains(seccheck.FieldCtxtThreadGroupStartTime) {
info.ThreadGroupStartTimeNs = t.tg.leader.startTime.Nanoseconds()
}
if mask.Contains(seccheck.FieldCtxtContainerID) {
info.ContainerId = t.tg.leader.ContainerID()
}
if mask.Contains(seccheck.FieldCtxtCwd) {
root := t.FSContext().RootDirectoryVFS2()
defer root.DecRef(t)
wd := t.FSContext().WorkingDirectoryVFS2()
defer wd.DecRef(t)
vfsObj := root.Mount().Filesystem().VirtualFilesystem()
info.Cwd, _ = vfsObj.PathnameWithDeleted(t, root, wd)
}
if mask.Contains(seccheck.FieldCtxtProcessName) {
info.ProcessName = t.Name()
}
t.Credentials().LoadSeccheckData(mask, info)
}
-21
View File
@@ -31,7 +31,6 @@ import (
"gvisor.dev/gvisor/pkg/sentry/kernel/sched"
ktime "gvisor.dev/gvisor/pkg/sentry/kernel/time"
"gvisor.dev/gvisor/pkg/sentry/platform"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
"gvisor.dev/gvisor/pkg/sentry/usage"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/sync"
@@ -880,23 +879,3 @@ func (t *Task) ResetKcov() {
t.kcov = nil
}
}
// Preconditions: The TaskSet mutex must be locked.
func (t *Task) loadSeccheckInfoLocked(req seccheck.TaskFieldSet, mask *seccheck.TaskFieldSet, info *seccheck.TaskInfo) {
if req.Contains(seccheck.TaskFieldThreadID) {
info.ThreadID = int32(t.k.tasks.Root.tids[t])
mask.Add(seccheck.TaskFieldThreadID)
}
if req.Contains(seccheck.TaskFieldThreadStartTime) {
info.ThreadStartTime = t.startTime
mask.Add(seccheck.TaskFieldThreadStartTime)
}
if req.Contains(seccheck.TaskFieldThreadGroupID) {
info.ThreadGroupID = int32(t.k.tasks.Root.tgids[t.tg])
mask.Add(seccheck.TaskFieldThreadGroupID)
}
if req.Contains(seccheck.TaskFieldThreadGroupStartTime) {
info.ThreadGroupStartTime = t.tg.leader.startTime
mask.Add(seccheck.TaskFieldThreadGroupStartTime)
}
}
+18 -14
View File
@@ -24,6 +24,7 @@ import (
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/sentry/inet"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
"gvisor.dev/gvisor/pkg/usermem"
)
@@ -247,8 +248,8 @@ func (t *Task) Clone(args *linux.CloneArgs) (ThreadID, *SyscallControl, error) {
defer nt.Start(tid)
if seccheck.Global.Enabled(seccheck.PointClone) {
mask, info := getCloneSeccheckInfo(t, nt, args)
if err := seccheck.Global.Clone(t, mask, &info); err != nil {
mask, info := getCloneSeccheckInfo(t, nt)
if err := seccheck.Global.Clone(t, mask, info); err != nil {
// nt has been visible to the rest of the system since NewTask, so
// it may be blocking execve or a group stop, have been notified
// for group signal delivery, had children reparented to it, etc.
@@ -306,20 +307,23 @@ func (t *Task) Clone(args *linux.CloneArgs) (ThreadID, *SyscallControl, error) {
return ntid, nil, nil
}
func getCloneSeccheckInfo(t, nt *Task, args *linux.CloneArgs) (seccheck.CloneFieldSet, seccheck.CloneInfo) {
req := seccheck.Global.CloneReq()
info := seccheck.CloneInfo{
Credentials: t.Credentials(),
Args: *args,
}
var mask seccheck.CloneFieldSet
mask.Add(seccheck.CloneFieldCredentials)
mask.Add(seccheck.CloneFieldArgs)
func getCloneSeccheckInfo(t, nt *Task) (seccheck.FieldSet, *pb.CloneInfo) {
fields := seccheck.Global.GetFieldSet(seccheck.PointClone)
t.k.tasks.mu.RLock()
defer t.k.tasks.mu.RUnlock()
t.loadSeccheckInfoLocked(req.Invoker, &mask.Invoker, &info.Invoker)
nt.loadSeccheckInfoLocked(req.Created, &mask.Created, &info.Created)
return mask, info
info := &pb.CloneInfo{
CreatedThreadId: int32(nt.k.tasks.Root.tids[nt]),
CreatedThreadGroupId: int32(nt.k.tasks.Root.tgids[nt.tg]),
CreatedThreadStartTimeNs: nt.startTime.Nanoseconds(),
}
if !fields.Context.Empty() {
info.ContextData = &pb.ContextData{}
LoadSeccheckDataLocked(t, fields.Context, info.ContextData)
}
return fields, info
}
// maybeBeginVforkStop checks if a previously-started vfork child is still
+19 -33
View File
@@ -69,9 +69,9 @@ import (
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/fs"
"gvisor.dev/gvisor/pkg/sentry/fsbridge"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/mm"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
"gvisor.dev/gvisor/pkg/sentry/vfs"
)
@@ -97,7 +97,7 @@ func (t *Task) Execve(newImage *TaskImage, argv, env []string, executable fsbrid
// We can't clearly hold kernel package locks while stat'ing executable.
if seccheck.Global.Enabled(seccheck.PointExecve) {
mask, info := getExecveSeccheckInfo(t, argv, env, executable, pathname)
if err := seccheck.Global.Execve(t, mask, &info); err != nil {
if err := seccheck.Global.Execve(t, mask, info); err != nil {
newImage.release()
return nil, err
}
@@ -302,44 +302,28 @@ func (t *Task) promoteLocked() {
oldLeader.exitNotifyLocked(false)
}
func getExecveSeccheckInfo(t *Task, argv, env []string, executable fsbridge.File, pathname string) (seccheck.ExecveFieldSet, seccheck.ExecveInfo) {
req := seccheck.Global.ExecveReq()
info := seccheck.ExecveInfo{
Credentials: t.Credentials(),
Argv: argv,
Env: env,
func getExecveSeccheckInfo(t *Task, argv, env []string, executable fsbridge.File, pathname string) (seccheck.FieldSet, *pb.ExecveInfo) {
fields := seccheck.Global.GetFieldSet(seccheck.PointExecve)
info := &pb.ExecveInfo{
Argv: argv,
Env: env,
}
var mask seccheck.ExecveFieldSet
mask.Add(seccheck.ExecveFieldCredentials)
mask.Add(seccheck.ExecveFieldArgv)
mask.Add(seccheck.ExecveFieldEnv)
if executable != nil {
info.BinaryPath = pathname
mask.Add(seccheck.ExecveFieldBinaryPath)
if vfs2bridgeFile, ok := executable.(*fsbridge.VFSFile); ok {
if req.Contains(seccheck.ExecveFieldBinaryMode) || req.Contains(seccheck.ExecveFieldBinaryUID) || req.Contains(seccheck.ExecveFieldBinaryGID) {
var statOpts vfs.StatOptions
if req.Contains(seccheck.ExecveFieldBinaryMode) {
statOpts.Mask |= linux.STATX_TYPE | linux.STATX_MODE
}
if req.Contains(seccheck.ExecveFieldBinaryUID) {
statOpts.Mask |= linux.STATX_UID
}
if req.Contains(seccheck.ExecveFieldBinaryGID) {
statOpts.Mask |= linux.STATX_GID
if fields.Local.Contains(seccheck.ExecveFieldBinaryInfo) {
statOpts := vfs.StatOptions{
Mask: linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_UID | linux.STATX_GID,
}
if stat, err := vfs2bridgeFile.FileDescription().Stat(t, statOpts); err == nil {
if stat.Mask&(linux.STATX_TYPE|linux.STATX_MODE) == (linux.STATX_TYPE | linux.STATX_MODE) {
info.BinaryMode = stat.Mode
mask.Add(seccheck.ExecveFieldBinaryMode)
info.BinaryMode = uint32(stat.Mode)
}
if stat.Mask&linux.STATX_UID != 0 {
info.BinaryUID = auth.KUID(stat.UID)
mask.Add(seccheck.ExecveFieldBinaryUID)
info.BinaryUid = stat.UID
}
if stat.Mask&linux.STATX_GID != 0 {
info.BinaryGID = auth.KGID(stat.GID)
mask.Add(seccheck.ExecveFieldBinaryGID)
info.BinaryGid = stat.GID
}
}
}
@@ -347,8 +331,10 @@ func getExecveSeccheckInfo(t *Task, argv, env []string, executable fsbridge.File
// SHA256, which is very expensive.
}
}
t.k.tasks.mu.RLock()
defer t.k.tasks.mu.RUnlock()
t.loadSeccheckInfoLocked(req.Invoker, &mask.Invoker, &info.Invoker)
return mask, info
if !fields.Context.Empty() {
info.ContextData = &pb.ContextData{}
LoadSeccheckData(t, fields.Context, info.ContextData)
}
return fields, info
}
+13 -9
View File
@@ -33,6 +33,7 @@ import (
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
"gvisor.dev/gvisor/pkg/waiter"
)
@@ -646,7 +647,7 @@ func (t *Task) exitNotifyLocked(fromPtraceDetach bool) {
}
if seccheck.Global.Enabled(seccheck.PointExitNotifyParent) {
mask, info := getExitNotifyParentSeccheckInfo(t)
seccheck.Global.ExitNotifyParent(t, mask, &info)
seccheck.Global.ExitNotifyParent(t, mask, info)
}
}
}
@@ -697,15 +698,18 @@ func (t *Task) exitNotificationSignal(sig linux.Signal, receiver *Task) *linux.S
}
// Preconditions: The TaskSet mutex must be locked.
func getExitNotifyParentSeccheckInfo(t *Task) (seccheck.ExitNotifyParentFieldSet, seccheck.ExitNotifyParentInfo) {
req := seccheck.Global.ExitNotifyParentReq()
info := seccheck.ExitNotifyParentInfo{
ExitStatus: t.tg.exitStatus,
func getExitNotifyParentSeccheckInfo(t *Task) (seccheck.FieldSet, *pb.ExitNotifyParentInfo) {
fields := seccheck.Global.GetFieldSet(seccheck.PointExitNotifyParent)
info := &pb.ExitNotifyParentInfo{
ExitStatus: int32(t.tg.exitStatus),
}
var mask seccheck.ExitNotifyParentFieldSet
mask.Add(seccheck.ExitNotifyParentFieldExitStatus)
t.loadSeccheckInfoLocked(req.Exiter, &mask.Exiter, &info.Exiter)
return mask, info
if !fields.Context.Empty() {
info.ContextData = &pb.ContextData{}
LoadSeccheckDataLocked(t, fields.Context, info.ContextData)
}
return fields, info
}
// ExitStatus returns t's exit status, which is only guaranteed to be
+5 -17
View File
@@ -1,21 +1,8 @@
load("//tools:defs.bzl", "go_library", "go_test")
load("//tools/go_fieldenum:defs.bzl", "go_fieldenum")
load("//tools/go_generics:defs.bzl", "go_template_instance")
licenses(["notice"])
go_fieldenum(
name = "seccheck_fieldenum",
srcs = [
"clone.go",
"execve.go",
"exit.go",
"task.go",
],
out = "seccheck_fieldenum.go",
package = "seccheck",
)
go_template_instance(
name = "seqatomic_checkerslice",
out = "seqatomic_checkerslice_unsafe.go",
@@ -34,17 +21,15 @@ go_library(
"execve.go",
"exit.go",
"seccheck.go",
"seccheck_fieldenum.go",
"seqatomic_checkerslice_unsafe.go",
"task.go",
],
visibility = ["//:sandbox"],
deps = [
"//pkg/abi/linux",
"//pkg/context",
"//pkg/gohacks",
"//pkg/sentry/kernel/auth",
"//pkg/sentry/kernel/time",
"//pkg/sentry/seccheck/points:points_go_proto",
"//pkg/sync",
],
)
@@ -54,5 +39,8 @@ go_test(
size = "small",
srcs = ["seccheck_test.go"],
library = ":seccheck",
deps = ["//pkg/context"],
deps = [
"//pkg/context",
"//pkg/sentry/seccheck/points:points_go_proto",
],
)
+3 -26
View File
@@ -15,37 +15,14 @@
package seccheck
import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
)
// CloneInfo contains information used by the Clone checkpoint.
//
// +fieldenum Clone
type CloneInfo struct {
// Invoker identifies the invoking thread.
Invoker TaskInfo
// Credentials are the invoking thread's credentials.
Credentials *auth.Credentials
// Args contains the arguments to kernel.Task.Clone().
Args linux.CloneArgs
// Created identifies the created thread.
Created TaskInfo
}
// CloneReq returns fields required by the Clone checkpoint.
func (s *State) CloneReq() CloneFieldSet {
return s.cloneReq.Load()
}
// Clone is called at the Clone checkpoint.
func (s *State) Clone(ctx context.Context, mask CloneFieldSet, info *CloneInfo) error {
func (s *State) Clone(ctx context.Context, fields FieldSet, info *pb.CloneInfo) error {
for _, c := range s.getCheckers() {
if err := c.Clone(ctx, mask, *info); err != nil {
if err := c.Clone(ctx, fields, info); err != nil {
return err
}
}
+8 -43
View File
@@ -16,54 +16,19 @@ package seccheck
import (
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
)
// ExecveInfo contains information used by the Execve checkpoint.
//
// +fieldenum Execve
type ExecveInfo struct {
// Invoker identifies the invoking thread.
Invoker TaskInfo
// Credentials are the invoking thread's credentials.
Credentials *auth.Credentials
// BinaryPath is a path to the executable binary file being switched to in
// the mount namespace in which it was opened.
BinaryPath string
// Argv is the new process image's argument vector.
Argv []string
// Env is the new process image's environment variables.
Env []string
// BinaryMode is the executable binary file's mode.
BinaryMode uint16
// BinaryUID is the executable binary file's owner.
BinaryUID auth.KUID
// BinaryGID is the executable binary file's group.
BinaryGID auth.KGID
// BinarySHA256 is the SHA-256 hash of the executable binary file.
//
// Note that this requires reading the entire file into memory, which is
// likely to be extremely slow.
BinarySHA256 [32]byte
}
// ExecveReq returns fields required by the Execve checkpoint.
func (s *State) ExecveReq() ExecveFieldSet {
return s.execveReq.Load()
}
const (
// ExecveFieldBinaryInfo is an optional field to collect information about the
// binary being executed.
ExecveFieldBinaryInfo Field = iota
)
// Execve is called at the Execve checkpoint.
func (s *State) Execve(ctx context.Context, mask ExecveFieldSet, info *ExecveInfo) error {
func (s *State) Execve(ctx context.Context, mask FieldSet, info *pb.ExecveInfo) error {
for _, c := range s.getCheckers() {
if err := c.Execve(ctx, mask, *info); err != nil {
if err := c.Execve(ctx, mask, info); err != nil {
return err
}
}
+3 -25
View File
@@ -15,41 +15,19 @@
package seccheck
import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
)
// ExitNotifyParentInfo contains information used by the ExitNotifyParent
// checkpoint.
//
// +fieldenum ExitNotifyParent
type ExitNotifyParentInfo struct {
// Exiter identifies the exiting thread. Note that by the checkpoint's
// definition, Exiter.ThreadID == Exiter.ThreadGroupID and
// Exiter.ThreadStartTime == Exiter.ThreadGroupStartTime, so requesting
// ThreadGroup* fields is redundant.
Exiter TaskInfo
// ExitStatus is the exiting thread group's exit status, as reported
// by wait*().
ExitStatus linux.WaitStatus
}
// ExitNotifyParentReq returns fields required by the ExitNotifyParent
// checkpoint.
func (s *State) ExitNotifyParentReq() ExitNotifyParentFieldSet {
return s.exitNotifyParentReq.Load()
}
// ExitNotifyParent is called at the ExitNotifyParent checkpoint.
//
// The ExitNotifyParent checkpoint occurs when a zombied thread group leader,
// not waiting for exit acknowledgement from a non-parent ptracer, becomes the
// last non-dead thread in its thread group and notifies its parent of its
// exiting.
func (s *State) ExitNotifyParent(ctx context.Context, mask ExitNotifyParentFieldSet, info *ExitNotifyParentInfo) error {
func (s *State) ExitNotifyParent(ctx context.Context, fields FieldSet, info *pb.ExitNotifyParentInfo) error {
for _, c := range s.getCheckers() {
if err := c.ExitNotifyParent(ctx, mask, *info); err != nil {
if err := c.ExitNotifyParent(ctx, fields, info); err != nil {
return err
}
}
+13
View File
@@ -0,0 +1,13 @@
load("//tools:defs.bzl", "proto_library")
licenses(["notice"])
package(default_visibility = ["//:sandbox"])
proto_library(
name = "points",
srcs = [
"common.proto",
"sentry.proto",
],
)
+47
View File
@@ -0,0 +1,47 @@
// 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.common;
message Credentials {
uint32 real_uid = 1;
uint32 effective_uid = 2;
uint32 saved_uid = 3;
uint32 real_gid = 4;
uint32 effective_gid = 5;
uint32 saved_gid = 6;
}
message ContextData {
int64 time_ns = 1;
int32 thread_id = 2;
int64 thread_start_time_ns = 3;
int32 thread_group_id = 4;
int64 thread_group_start_time_ns = 5;
string container_id = 6;
Credentials credentials = 7;
string cwd = 8;
string process_name = 9;
}
+67
View File
@@ -0,0 +1,67 @@
// 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.sentry;
import "pkg/sentry/seccheck/points/common.proto";
// CloneInfo contains information used by the Clone checkpoint.
message CloneInfo {
gvisor.common.ContextData context_data = 1;
// CreatedThreadID is the thread's ID in the root PID namespace.
int32 created_thread_id = 3;
int32 created_thread_group_id = 4;
// CreatedThreadStartTime is the thread's CLOCK_REALTIME start time.
int64 created_thread_start_time_ns = 5;
}
// ExecveInfo contains information used by the Execve checkpoint.
message ExecveInfo {
gvisor.common.ContextData context_data = 1;
// BinaryPath is a path to the executable binary file being switched to in
// the mount namespace in which it was opened.
string binary_path = 2;
// Argv is the new process image's argument vector.
repeated string argv = 3;
// Env is the new process image's environment variables.
repeated string env = 4;
// BinaryMode is the executable binary file's mode.
uint32 binary_mode = 5;
uint32 binary_uid = 6;
uint32 binary_gid = 7;
// binary_sha256 is the SHA-256 hash of the executable binary file.
//
// Note that this requires reading the entire file into memory, which is
// likely to be extremely slow.
bytes binary_sha256 = 8;
}
message ExitNotifyParentInfo {
gvisor.common.ContextData context_data = 1;
// ExitStatus is the exiting thread group's exit status, as reported
// by wait*().
int32 exit_status = 2;
}
+97 -36
View File
@@ -20,6 +20,7 @@ import (
"sync/atomic"
"gvisor.dev/gvisor/pkg/context"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
"gvisor.dev/gvisor/pkg/sync"
)
@@ -37,6 +38,71 @@ const (
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
)
// FieldSet contains all optional fields to be collected by a given Point.
type FieldSet struct {
// Local indicates which optional fields from the Point that needs to be
// collected, e.g. resolving path from an FD, or collecting a large field.
Local FieldMask
// Context indicates which optional fields from the Context that needs to be
// collected, e.g. PID, credentials, current time.
Context FieldMask
}
// Field represents the index of a single optional field to be collect for a
// Point.
type Field uint
// FieldMask is a bitmask with a single bit representing an optional field to be
// collected. The meaning of each bit varies per point. The mask is currently
// limited to 64 fields. If more are needed, FieldMask can be expanded to
// support additional fields.
type FieldMask struct {
mask uint64
}
// MakeFieldMask creates a FieldMask from a set of Fields.
func MakeFieldMask(fields ...Field) FieldMask {
var m FieldMask
for _, field := range fields {
m.Add(field)
}
return m
}
// Contains returns true if the mask contains the Field.
func (fm *FieldMask) Contains(field Field) bool {
return fm.mask&(1<<field) != 0
}
// Add adds a Field to the mask.
func (fm *FieldMask) Add(field Field) {
fm.mask |= 1 << field
}
// Remove removes a Field from the mask.
func (fm *FieldMask) Remove(field Field) {
fm.mask &^= 1 << field
}
// Empty returns true if no bits are set.
func (fm *FieldMask) Empty() bool {
return fm.mask == 0
}
// A Checker performs security checks at checkpoints.
//
// Each Checker method X is called at checkpoint X; if the method may return a
@@ -44,48 +110,41 @@ const (
// immediately (without calling subsequent Checkers) and return the error. The
// info argument contains information relevant to the check. The mask argument
// indicates what fields in info are valid; the mask should usually be a
// superset of fields requested by the Checker's corresponding CheckerReq, but
// superset of fields requested by the Checker's corresponding PointReq, but
// may be missing requested fields in some cases (e.g. if the Checker is
// registered concurrently with invocations of checkpoints).
type Checker interface {
Clone(ctx context.Context, mask CloneFieldSet, info CloneInfo) error
Execve(ctx context.Context, mask ExecveFieldSet, info ExecveInfo) error
ExitNotifyParent(ctx context.Context, mask ExitNotifyParentFieldSet, info ExitNotifyParentInfo) error
Clone(ctx context.Context, fields FieldSet, info *pb.CloneInfo) error
Execve(ctx context.Context, fields FieldSet, info *pb.ExecveInfo) error
ExitNotifyParent(ctx context.Context, fields FieldSet, info *pb.ExitNotifyParentInfo) error
}
// CheckerDefaults may be embedded by implementations of Checker to obtain
// no-op implementations of Checker methods that may be explicitly overridden.
type CheckerDefaults struct{}
var _ Checker = (*CheckerDefaults)(nil)
// Clone implements Checker.Clone.
func (CheckerDefaults) Clone(ctx context.Context, mask CloneFieldSet, info CloneInfo) error {
func (CheckerDefaults) Clone(context.Context, FieldSet, *pb.CloneInfo) error {
return nil
}
// Execve implements Checker.Execve.
func (CheckerDefaults) Execve(ctx context.Context, mask ExecveFieldSet, info ExecveInfo) error {
func (CheckerDefaults) Execve(context.Context, FieldSet, *pb.ExecveInfo) error {
return nil
}
// ExitNotifyParent implements Checker.ExitNotifyParent.
func (CheckerDefaults) ExitNotifyParent(ctx context.Context, mask ExitNotifyParentFieldSet, info ExitNotifyParentInfo) error {
func (CheckerDefaults) ExitNotifyParent(context.Context, FieldSet, *pb.ExitNotifyParentInfo) error {
return nil
}
// CheckerReq indicates what checkpoints a corresponding Checker runs at, and
// what information it requires at those checkpoints.
type CheckerReq struct {
// Points are the set of checkpoints for which the corresponding Checker
// must be called. Note that methods not specified in Points may still be
// called; implementations of Checker may embed CheckerDefaults to obtain
// no-op implementations of Checker methods.
Points []Point
// All of the following fields indicate what fields in the corresponding
// XInfo struct will be requested at the corresponding checkpoint.
Clone CloneFields
Execve ExecveFields
ExitNotifyParent ExitNotifyParentFields
// PointReq indicates what Point a corresponding Checker runs at, and what
// information it requires at those Points.
type PointReq struct {
Pt Point
Fields FieldSet
}
// Global is the method receiver of all seccheck functions.
@@ -95,7 +154,7 @@ var Global State
type State struct {
// registrationMu serializes all changes to the set of registered Checkers
// for all checkpoints.
registrationMu sync.Mutex
registrationMu sync.RWMutex
// enabledPoints is a bitmask of checkpoints for which at least one Checker
// is registered.
@@ -113,30 +172,25 @@ type State struct {
// Mutation of checkers is serialized by registrationMu.
checkers []Checker
// All of the following xReq variables indicate what fields in the
// corresponding XInfo struct have been requested by any registered
// checker, are accessed using atomic memory operations, and are mutated
// with registrationMu locked.
cloneReq CloneFieldSet
execveReq ExecveFieldSet
exitNotifyParentReq ExitNotifyParentFieldSet
pointFields map[Point]FieldSet
}
// AppendChecker registers the given Checker to execute at checkpoints. The
// Checker will execute after all previously-registered Checkers, and only if
// those Checkers return a nil error.
func (s *State) AppendChecker(c Checker, req *CheckerReq) {
func (s *State) AppendChecker(c Checker, reqs []PointReq) {
s.registrationMu.Lock()
defer s.registrationMu.Unlock()
s.cloneReq.AddFieldsLoadable(req.Clone)
s.execveReq.AddFieldsLoadable(req.Execve)
s.exitNotifyParentReq.AddFieldsLoadable(req.ExitNotifyParent)
s.appendCheckerLocked(c)
for _, p := range req.Points {
word, bit := p/32, p%32
if s.pointFields == nil {
s.pointFields = make(map[Point]FieldSet)
}
for _, req := range reqs {
word, bit := req.Pt/32, req.Pt%32
atomic.StoreUint32(&s.enabledPoints[word], s.enabledPoints[word]|(uint32(1)<<bit))
s.pointFields[req.Pt] = req.Fields
}
}
@@ -156,3 +210,10 @@ func (s *State) appendCheckerLocked(c Checker) {
s.checkers = append(s.checkers, c)
s.registrationSeq.EndWrite()
}
// GetFieldSet returns the FieldSet that has been configured for a given Point.
func (s *State) GetFieldSet(p Point) FieldSet {
s.registrationMu.RLock()
defer s.registrationMu.RUnlock()
return s.pointFields[p]
}
+148 -53
View File
@@ -19,20 +19,21 @@ import (
"testing"
"gvisor.dev/gvisor/pkg/context"
pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto"
)
type testChecker struct {
CheckerDefaults
onClone func(ctx context.Context, mask CloneFieldSet, info CloneInfo) error
onClone func(ctx context.Context, fields FieldSet, info *pb.CloneInfo) error
}
// Clone implements Checker.Clone.
func (c *testChecker) Clone(ctx context.Context, mask CloneFieldSet, info CloneInfo) error {
func (c *testChecker) Clone(ctx context.Context, fields FieldSet, info *pb.CloneInfo) error {
if c.onClone == nil {
return nil
}
return c.onClone(ctx, mask, info)
return c.onClone(ctx, fields, info)
}
func TestNoChecker(t *testing.T) {
@@ -44,7 +45,7 @@ func TestNoChecker(t *testing.T) {
func TestCheckerNotRegisteredForPoint(t *testing.T) {
var s State
s.AppendChecker(&testChecker{}, &CheckerReq{})
s.AppendChecker(&testChecker{}, nil)
if s.Enabled(PointClone) {
t.Errorf("Enabled(PointClone): got true, wanted false")
}
@@ -53,23 +54,28 @@ func TestCheckerNotRegisteredForPoint(t *testing.T) {
func TestCheckerRegistered(t *testing.T) {
var s State
checkerCalled := false
s.AppendChecker(&testChecker{onClone: func(ctx context.Context, mask CloneFieldSet, info CloneInfo) error {
checkerCalled = true
return nil
}}, &CheckerReq{
Points: []Point{PointClone},
Clone: CloneFields{
Credentials: true,
checker := &testChecker{
onClone: func(context.Context, FieldSet, *pb.CloneInfo) error {
checkerCalled = true
return nil
},
})
}
req := []PointReq{
{
Pt: PointClone,
Fields: FieldSet{Context: MakeFieldMask(FieldCtxtCredentials)},
},
}
s.AppendChecker(checker, req)
if !s.Enabled(PointClone) {
t.Errorf("Enabled(PointClone): got false, wanted true")
}
if !s.CloneReq().Contains(CloneFieldCredentials) {
t.Errorf("CloneReq().Contains(CloneFieldCredentials): got false, wanted true")
fields := s.GetFieldSet(PointClone)
if !fields.Context.Contains(FieldCtxtCredentials) {
t.Errorf("fields.Context.Contains(PointContextCredentials): got false, wanted true")
}
if err := s.Clone(context.Background(), CloneFieldSet{}, &CloneInfo{}); err != nil {
if err := s.Clone(context.Background(), fields, &pb.CloneInfo{}); err != nil {
t.Errorf("Clone(): got %v, wanted nil", err)
}
if !checkerCalled {
@@ -80,40 +86,33 @@ func TestCheckerRegistered(t *testing.T) {
func TestMultipleCheckersRegistered(t *testing.T) {
var s State
checkersCalled := [2]bool{}
s.AppendChecker(&testChecker{onClone: func(ctx context.Context, mask CloneFieldSet, info CloneInfo) error {
checkersCalled[0] = true
return nil
}}, &CheckerReq{
Points: []Point{PointClone},
Clone: CloneFields{
Args: true,
checker := &testChecker{
onClone: func(context.Context, FieldSet, *pb.CloneInfo) error {
checkersCalled[0] = true
return nil
},
})
s.AppendChecker(&testChecker{onClone: func(ctx context.Context, mask CloneFieldSet, info CloneInfo) error {
}
reqs := []PointReq{
{Pt: PointClone},
}
s.AppendChecker(checker, reqs)
checker = &testChecker{onClone: func(context.Context, FieldSet, *pb.CloneInfo) error {
checkersCalled[1] = true
return nil
}}, &CheckerReq{
Points: []Point{PointClone},
Clone: CloneFields{
Created: TaskFields{
ThreadID: true,
},
},
})
}}
reqs = []PointReq{
{Pt: PointClone},
}
s.AppendChecker(checker, reqs)
if !s.Enabled(PointClone) {
t.Errorf("Enabled(PointClone): got false, wanted true")
}
// CloneReq() should return the union of requested fields from all calls to
// AppendChecker.
req := s.CloneReq()
if !req.Contains(CloneFieldArgs) {
t.Errorf("req.Contains(CloneFieldArgs): got false, wanted true")
}
if !req.Created.Contains(TaskFieldThreadID) {
t.Errorf("req.Created.Contains(TaskFieldThreadID): got false, wanted true")
}
if err := s.Clone(context.Background(), CloneFieldSet{}, &CloneInfo{}); err != nil {
fields := s.GetFieldSet(PointClone)
if err := s.Clone(context.Background(), fields, &pb.CloneInfo{}); err != nil {
t.Errorf("Clone(): got %v, wanted nil", err)
}
for i := range checkersCalled {
@@ -129,23 +128,30 @@ func TestCheckpointReturnsFirstCheckerError(t *testing.T) {
var s State
checkersCalled := [2]bool{}
s.AppendChecker(&testChecker{onClone: func(ctx context.Context, mask CloneFieldSet, info CloneInfo) error {
checkersCalled[0] = true
return errFirstChecker
}}, &CheckerReq{
Points: []Point{PointClone},
})
s.AppendChecker(&testChecker{onClone: func(ctx context.Context, mask CloneFieldSet, info CloneInfo) error {
checkersCalled[1] = true
return errSecondChecker
}}, &CheckerReq{
Points: []Point{PointClone},
})
checker := &testChecker{
onClone: func(context.Context, FieldSet, *pb.CloneInfo) error {
checkersCalled[0] = true
return errFirstChecker
},
}
reqs := []PointReq{
{Pt: PointClone},
}
s.AppendChecker(checker, reqs)
checker = &testChecker{
onClone: func(context.Context, FieldSet, *pb.CloneInfo) error {
checkersCalled[1] = true
return errSecondChecker
},
}
s.AppendChecker(checker, reqs)
if !s.Enabled(PointClone) {
t.Errorf("Enabled(PointClone): got false, wanted true")
}
if err := s.Clone(context.Background(), CloneFieldSet{}, &CloneInfo{}); err != errFirstChecker {
if err := s.Clone(context.Background(), FieldSet{}, &pb.CloneInfo{}); err != errFirstChecker {
t.Errorf("Clone(): got %v, wanted %v", err, errFirstChecker)
}
if !checkersCalled[0] {
@@ -155,3 +161,92 @@ func TestCheckpointReturnsFirstCheckerError(t *testing.T) {
t.Errorf("Clone() called second Checker")
}
}
func TestFieldMaskEmpty(t *testing.T) {
fd := FieldMask{}
if !fd.Empty() {
t.Errorf("new FieldMask must be empty: %+v", fd)
}
}
func TestFieldMaskMake(t *testing.T) {
zero := Field(0)
one := Field(1)
two := Field(2)
fd := MakeFieldMask(zero, two)
if fd.Empty() {
t.Errorf("FieldMask must not be empty: %+v", fd)
}
if want := zero; !fd.Contains(want) {
t.Errorf("FieldMask must contain %v: %+v", want, fd)
}
if want := two; !fd.Contains(want) {
t.Errorf("FieldMask must contain %v: %+v", want, fd)
}
if want := one; fd.Contains(want) {
t.Errorf("FieldMask must not contain %v: %+v", want, fd)
}
}
func TestFieldMask(t *testing.T) {
zero := Field(0)
one := Field(1)
two := Field(2)
fd := FieldMask{}
fd.Add(zero)
if fd.Empty() {
t.Errorf("FieldMask must not be empty: %+v", fd)
}
if want := zero; !fd.Contains(want) {
t.Errorf("FieldMask must contain %v: %+v", want, fd)
}
if want := one; fd.Contains(want) {
t.Errorf("FieldMask must not contain %v: %+v", want, fd)
}
if want := two; fd.Contains(want) {
t.Errorf("FieldMask must not contain %v: %+v", want, fd)
}
fd.Add(two)
if fd.Empty() {
t.Errorf("FieldMask must not be empty: %+v", fd)
}
if want := zero; !fd.Contains(want) {
t.Errorf("FieldMask must contain %v: %+v", want, fd)
}
if want := one; fd.Contains(want) {
t.Errorf("FieldMask must not contain %v: %+v", want, fd)
}
if want := two; !fd.Contains(want) {
t.Errorf("FieldMask must contain %v: %+v", want, fd)
}
fd.Remove(zero)
if fd.Empty() {
t.Errorf("FieldMask must not be empty: %+v", fd)
}
if want := zero; fd.Contains(want) {
t.Errorf("FieldMask must not contain %v: %+v", want, fd)
}
if want := one; fd.Contains(want) {
t.Errorf("FieldMask must not contain %v: %+v", want, fd)
}
if want := two; !fd.Contains(want) {
t.Errorf("FieldMask must contain %v: %+v", want, fd)
}
fd.Remove(two)
if !fd.Empty() {
t.Errorf("FieldMask must be empty: %+v", fd)
}
if want := zero; fd.Contains(want) {
t.Errorf("FieldMask must not contain %v: %+v", want, fd)
}
if want := one; fd.Contains(want) {
t.Errorf("FieldMask must not contain %v: %+v", want, fd)
}
if want := two; fd.Contains(want) {
t.Errorf("FieldMask must not contain %v: %+v", want, fd)
}
}
-39
View File
@@ -1,39 +0,0 @@
// Copyright 2021 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 seccheck
import (
ktime "gvisor.dev/gvisor/pkg/sentry/kernel/time"
)
// TaskInfo contains information unambiguously identifying a single thread
// and/or its containing process.
//
// +fieldenum Task
type TaskInfo struct {
// ThreadID is the thread's ID in the root PID namespace.
ThreadID int32
// ThreadStartTime is the thread's CLOCK_REALTIME start time.
ThreadStartTime ktime.Time
// ThreadGroupID is the thread's group leader's ID in the root PID
// namespace.
ThreadGroupID int32
// ThreadGroupStartTime is the thread's group leader's CLOCK_REALTIME start
// time.
ThreadGroupStartTime ktime.Time
}