mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
This change introduces an LRU cache (ExecveHashCache) and multi-algorithm calculation engine (file_hash.go) to store and resolve SHA-256 and SHA-1 digests of executables during execve syscalls. PiperOrigin-RevId: 952257971
367 lines
11 KiB
Go
367 lines
11 KiB
Go
// 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 defines a structure for dynamically-configured security
|
|
// checks in the sentry.
|
|
package seccheck
|
|
|
|
import (
|
|
"sync/atomic"
|
|
|
|
"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"
|
|
"gvisor.dev/gvisor/pkg/sync"
|
|
)
|
|
|
|
// A Point represents a checkpoint, a point at which a security check occurs.
|
|
type Point uint
|
|
|
|
// PointX represents the checkpoint X.
|
|
const (
|
|
totalPoints = int(pointLengthBeforeSyscalls) + syscallPoints
|
|
numPointsPerUint32 = 32
|
|
numPointBitmaskUint32s = (totalPoints-1)/numPointsPerUint32 + 1
|
|
)
|
|
|
|
// 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 Sink performs security checks at checkpoints.
|
|
//
|
|
// Each Sink method X is called at checkpoint X; if the method may return a
|
|
// non-nil error and does so, it causes the checked operation to fail
|
|
// immediately (without calling subsequent Sinks) 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 Sink's corresponding PointReq, but
|
|
// may be missing requested fields in some cases (e.g. if the Sink is
|
|
// registered concurrently with invocations of checkpoints).
|
|
type Sink interface {
|
|
// Name return the sink name.
|
|
Name() string
|
|
// Status returns the sink runtime status.
|
|
Status() SinkStatus
|
|
// Stop requests the sink to stop.
|
|
Stop()
|
|
|
|
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
|
|
TaskExit(context.Context, FieldSet, *pb.TaskExit) error
|
|
Mmap(context.Context, FieldSet, *pb.MmapInfo) error
|
|
|
|
ContainerStart(context.Context, FieldSet, *pb.Start) error
|
|
|
|
Syscall(context.Context, FieldSet, *pb.ContextData, pb.MessageType, proto.Message) error
|
|
RawSyscall(context.Context, FieldSet, *pb.Syscall) error
|
|
}
|
|
|
|
// SinkStatus represents stats about each Sink instance.
|
|
type SinkStatus struct {
|
|
// DroppedCount is the number of trace points dropped.
|
|
DroppedCount uint64
|
|
}
|
|
|
|
// SinkDefaults may be embedded by implementations of Sink to obtain
|
|
// no-op implementations of Sink methods that may be explicitly overridden.
|
|
type SinkDefaults struct{}
|
|
|
|
// Add functions missing in SinkDefaults to make it possible to check for the
|
|
// implementation below to catch missing functions more easily.
|
|
type sinkDefaultsImpl struct {
|
|
SinkDefaults
|
|
}
|
|
|
|
// Name implements Sink.Name.
|
|
func (sinkDefaultsImpl) Name() string { return "" }
|
|
|
|
var _ Sink = (*sinkDefaultsImpl)(nil)
|
|
|
|
// Status implements Sink.Status.
|
|
func (SinkDefaults) Status() SinkStatus {
|
|
return SinkStatus{}
|
|
}
|
|
|
|
// Stop implements Sink.Stop.
|
|
func (SinkDefaults) Stop() {}
|
|
|
|
// Clone implements Sink.Clone.
|
|
func (SinkDefaults) Clone(context.Context, FieldSet, *pb.CloneInfo) error {
|
|
return nil
|
|
}
|
|
|
|
// Execve implements Sink.Execve.
|
|
func (SinkDefaults) Execve(context.Context, FieldSet, *pb.ExecveInfo) error {
|
|
return nil
|
|
}
|
|
|
|
// ExitNotifyParent implements Sink.ExitNotifyParent.
|
|
func (SinkDefaults) ExitNotifyParent(context.Context, FieldSet, *pb.ExitNotifyParentInfo) error {
|
|
return nil
|
|
}
|
|
|
|
// ContainerStart implements Sink.ContainerStart.
|
|
func (SinkDefaults) ContainerStart(context.Context, FieldSet, *pb.Start) error {
|
|
return nil
|
|
}
|
|
|
|
// TaskExit implements Sink.TaskExit.
|
|
func (SinkDefaults) TaskExit(context.Context, FieldSet, *pb.TaskExit) error {
|
|
return nil
|
|
}
|
|
|
|
// Mmap implements Sink.Mmap.
|
|
func (SinkDefaults) Mmap(context.Context, FieldSet, *pb.MmapInfo) error {
|
|
return nil
|
|
}
|
|
|
|
// RawSyscall implements Sink.RawSyscall.
|
|
func (SinkDefaults) RawSyscall(context.Context, FieldSet, *pb.Syscall) error {
|
|
return nil
|
|
}
|
|
|
|
// Syscall implements Sink.Syscall.
|
|
func (SinkDefaults) Syscall(context.Context, FieldSet, *pb.ContextData, pb.MessageType, proto.Message) error {
|
|
return nil
|
|
}
|
|
|
|
// PointReq indicates what Point a corresponding Sink 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.
|
|
var Global State
|
|
|
|
// State is the type of global, and is separated out for testing.
|
|
type State struct {
|
|
// registrationMu serializes all changes to the set of registered Sinks
|
|
// for all checkpoints.
|
|
registrationMu sync.RWMutex
|
|
|
|
// enabledPoints is a bitmask of checkpoints for which at least one Sink
|
|
// is registered.
|
|
//
|
|
// Mutation of enabledPoints is serialized by registrationMu.
|
|
enabledPoints [numPointBitmaskUint32s]atomicbitops.Uint32
|
|
|
|
// registrationSeq supports store-free atomic reads of registeredSinks.
|
|
registrationSeq sync.SeqCount
|
|
|
|
// sinks is the set of all registered Sinks in order of execution.
|
|
//
|
|
// sinks is accessed using instantiations of SeqAtomic functions.
|
|
// Mutation of sinks is serialized by registrationMu.
|
|
sinks []Sink
|
|
|
|
// syscallFlagListeners is the set of registered SyscallFlagListeners.
|
|
//
|
|
// They are notified when the enablement of a syscall point changes.
|
|
// Mutation of syscallFlagListeners is serialized by registrationMu.
|
|
syscallFlagListeners []SyscallFlagListener
|
|
|
|
pointFields map[Point]FieldSet
|
|
|
|
// execveHashCache holds an atomic pointer to the active ExecveHashCache.
|
|
// Nil indicates that execve binary hash caching/computation is unregistered.
|
|
execveHashCache atomic.Pointer[ExecveHashCache]
|
|
}
|
|
|
|
// ExecveHashCache returns the active ExecveHashCache, or nil if unconfigured.
|
|
func (s *State) ExecveHashCache() *ExecveHashCache {
|
|
return s.execveHashCache.Load()
|
|
}
|
|
|
|
// SetExecveHashCache atomically sets the active ExecveHashCache.
|
|
func (s *State) SetExecveHashCache(cache *ExecveHashCache) {
|
|
s.execveHashCache.Store(cache)
|
|
}
|
|
|
|
// SetupExecveHashCache initializes ExecveHashCache for the given point requirements and capacity.
|
|
func (s *State) SetupExecveHashCache(capacity int, reqs []PointReq) {
|
|
s.registrationMu.Lock()
|
|
defer s.registrationMu.Unlock()
|
|
|
|
var opts ExecveHashOptions
|
|
for _, req := range reqs {
|
|
if req.Pt == PointExecve {
|
|
if req.Fields.Local.Contains(FieldSentryExecveBinarySHA256) {
|
|
opts.SHA256 = true
|
|
}
|
|
if req.Fields.Local.Contains(FieldSentryExecveBinarySHA1) {
|
|
opts.SHA1 = true
|
|
}
|
|
}
|
|
}
|
|
if !opts.SHA256 && !opts.SHA1 {
|
|
return
|
|
}
|
|
s.SetExecveHashCache(NewExecveHashCache(capacity, opts))
|
|
}
|
|
|
|
// AppendSink registers the given Sink to execute at checkpoints. The
|
|
// Sink will execute after all previously-registered sinks, and only if
|
|
// those Sinks return a nil error.
|
|
func (s *State) AppendSink(c Sink, reqs []PointReq) {
|
|
s.registrationMu.Lock()
|
|
defer s.registrationMu.Unlock()
|
|
|
|
s.appendSinkLocked(c)
|
|
if s.pointFields == nil {
|
|
s.pointFields = make(map[Point]FieldSet)
|
|
}
|
|
updateSyscalls := false
|
|
for _, req := range reqs {
|
|
word, bit := req.Pt/numPointsPerUint32, req.Pt%numPointsPerUint32
|
|
s.enabledPoints[word].Store(s.enabledPoints[word].RacyLoad() | (uint32(1) << bit))
|
|
if req.Pt >= pointLengthBeforeSyscalls {
|
|
updateSyscalls = true
|
|
}
|
|
s.pointFields[req.Pt] = req.Fields
|
|
}
|
|
if updateSyscalls {
|
|
for _, listener := range s.syscallFlagListeners {
|
|
listener.UpdateSecCheck(s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *State) clearSink() {
|
|
s.registrationMu.Lock()
|
|
defer s.registrationMu.Unlock()
|
|
|
|
updateSyscalls := false
|
|
for i := range s.enabledPoints {
|
|
s.enabledPoints[i].Store(0)
|
|
// We use i+1 here because we want to check the last bit that may have been changed within i.
|
|
if Point((i+1)*numPointsPerUint32) >= pointLengthBeforeSyscalls {
|
|
updateSyscalls = true
|
|
}
|
|
}
|
|
if updateSyscalls {
|
|
for _, listener := range s.syscallFlagListeners {
|
|
listener.UpdateSecCheck(s)
|
|
}
|
|
}
|
|
s.pointFields = nil
|
|
s.SetExecveHashCache(nil)
|
|
|
|
oldSinks := s.getSinks()
|
|
s.registrationSeq.BeginWrite()
|
|
s.sinks = nil
|
|
s.registrationSeq.EndWrite()
|
|
for _, sink := range oldSinks {
|
|
sink.Stop()
|
|
}
|
|
}
|
|
|
|
// AddSyscallFlagListener adds a listener to the State.
|
|
//
|
|
// The listener will be notified whenever syscall point enablement changes.
|
|
func (s *State) AddSyscallFlagListener(listener SyscallFlagListener) {
|
|
s.registrationMu.Lock()
|
|
defer s.registrationMu.Unlock()
|
|
s.syscallFlagListeners = append(s.syscallFlagListeners, listener)
|
|
}
|
|
|
|
// Enabled returns true if any Sink is registered for the given checkpoint.
|
|
func (s *State) Enabled(p Point) bool {
|
|
word, bit := p/numPointsPerUint32, p%numPointsPerUint32
|
|
if int(word) >= len(s.enabledPoints) {
|
|
return false
|
|
}
|
|
return s.enabledPoints[word].Load()&(uint32(1)<<bit) != 0
|
|
}
|
|
|
|
func (s *State) getSinks() []Sink {
|
|
return SeqAtomicLoadSinkSlice(&s.registrationSeq, &s.sinks)
|
|
}
|
|
|
|
// Preconditions: s.registrationMu must be locked.
|
|
func (s *State) appendSinkLocked(c Sink) {
|
|
s.registrationSeq.BeginWrite()
|
|
s.sinks = append(s.sinks, c)
|
|
s.registrationSeq.EndWrite()
|
|
}
|
|
|
|
// SentToSinks iterates over all sinks and calls fn for each one of them.
|
|
func (s *State) SentToSinks(fn func(c Sink) error) error {
|
|
for _, c := range s.getSinks() {
|
|
if err := fn(c); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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]
|
|
}
|