Files
visor/pkg/sentry/seccheck/config.go
T
zeekayandHanzo Dev 3da05638df State the ZAP wire of every urpc payload
urpc encodes with encoding/json, which walks the payload on every call: a
realistic control.ExecArgs costs 7084ns and 10 allocations to cross. ZAP
reads a field at a constant offset out of the buffer that arrived, so the
same payload is 740ns and one allocation -- but only if the offsets are
stated, and a stated offset that is wrong is silent corruption rather than
a compile error. So state them from the types themselves.

//tools/zap reflects over the payloads at build time and writes the wire
as Go source with the offsets as constants. It lives inside Bazel because
pkg/sentry/control cannot be imported from outside it: the *_go_proto
packages it depends on have no source in the tree.

Six payloads had no layout to state. A map, an interface and a type from
another module have no offsets, so each is answered in the type:

  SaveOpts.Metadata and StartContainerArgs.InitialCgroups/Limits cross as
  lists of pairs; boot.Route.Destination restates net.IPNet as Subnet;
  OpenMountArgs restates the OCI mount fields the call actually uses;
  StartArgs.Spec and SinkConfig.Config carry their documents as the bytes
  they arrived as; MetricsRegistrationResponse carries the protocol
  buffer the same way; and seccheck's session options become a type
  instead of a map of any.

ExecArgs and MetricsExportData still have none: ExecArgs holds live
kernel objects behind an interface, and MetricsExportData reaches a
prometheus snapshot whose labels are a map.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-22 23:10:33 -07:00

294 lines
8.4 KiB
Go

// 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 seccheck
import (
"encoding/json"
"fmt"
"os"
"sync"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/metric"
)
// DefaultSessionName is the name of the only session that can exist in the
// system for now. When multiple sessions are supported, this can be removed.
const DefaultSessionName = "Default"
var (
sessionsMu = sync.Mutex{}
sessions = make(map[string]*State)
)
var sessionCounter = metric.MustCreateNewUint64Metric("/trace/sessions_created",
metric.Uint64Metadata{
Cumulative: true,
Description: "Counts the number of trace sessions created.",
})
// SessionConfig describes a new session configuration. A session consists of a
// set of points to be enabled and sinks where the points are sent to.
type SessionConfig struct {
// Name is the unique session name.
Name string `json:"name,omitempty"`
// Points is the set of points to enable in this session.
Points []PointConfig `json:"points,omitempty"`
// IgnoreMissing skips point and optional/context fields not found. This can
// be used to apply a single configuration file with newer points/fields with
// older versions which do not have them yet. Note that it may hide typos in
// the configuration.
//
// This field does NOT apply to sinks.
IgnoreMissing bool `json:"ignore_missing,omitempty"`
// Sinks are the sinks that will process the points enabled above.
Sinks []SinkConfig `json:"sinks,omitempty"`
// Options holds session-level configuration options.
Options Options `json:"options,omitempty"`
}
// PointConfig describes a point to be enabled in a given session.
type PointConfig struct {
// Name is the point to be enabled. The point must exist in the system.
Name string `json:"name,omitempty"`
// OptionalFields is the list of optional fields to collect from the point.
OptionalFields []string `json:"optional_fields,omitempty"`
// ContextFields is the list of context fields to collect.
ContextFields []string `json:"context_fields,omitempty"`
}
// Options are the session-level settings a trace configuration may set.
type Options struct {
// ExecveHashCacheCapacity bounds the execve hash cache. Zero takes
// DefaultExecveHashCacheCapacity.
ExecveHashCacheCapacity int `json:"execve_hash_cache_capacity,omitempty"`
}
// SinkConfig describes the sink that will process the points in a given
// session.
type SinkConfig struct {
// Name is the sink to be created. The sink must exist in the system.
Name string `json:"name,omitempty"`
// Config is a opaque json object that is passed to the sink. It stays the
// bytes it arrived as: what they mean is the sink's to say.
Config json.RawMessage `json:"config,omitempty"`
// IgnoreSetupError makes errors during sink setup to be ignored. Otherwise,
// failures will prevent the container from starting.
IgnoreSetupError bool `json:"ignore_setup_error,omitempty"`
// Status is the runtime status for the sink.
Status SinkStatus `json:"status,omitempty"`
// FD is the endpoint returned from Setup. It may be nil.
FD *fd.FD `json:"-"`
}
// Create reads the session configuration and applies it to the system.
func Create(conf *SessionConfig, force bool) error {
log.Debugf("Creating seccheck: %+v", conf)
sessionsMu.Lock()
defer sessionsMu.Unlock()
if _, ok := sessions[conf.Name]; ok {
if !force {
return fmt.Errorf("session %q already exists", conf.Name)
}
if err := deleteLocked(conf.Name); err != nil {
return err
}
log.Infof("Trace session %q was deleted to be replaced", conf.Name)
}
if conf.Name != DefaultSessionName {
return fmt.Errorf(`only a single "Default" session is supported`)
}
state := &Global
capacity := DefaultExecveHashCacheCapacity
if conf.Options.ExecveHashCacheCapacity > 0 {
capacity = conf.Options.ExecveHashCacheCapacity
}
var reqs []PointReq
for _, ptConfig := range conf.Points {
desc, err := findPointDesc(ptConfig.Name)
if err != nil {
if conf.IgnoreMissing {
log.Warningf("Skipping point %q: %v", ptConfig.Name, err)
continue
}
return err
}
req := PointReq{Pt: desc.ID}
mask, err := setFields(ptConfig.OptionalFields, desc.OptionalFields, conf.IgnoreMissing)
if err != nil {
return fmt.Errorf("configuring point %q: %w", ptConfig.Name, err)
}
req.Fields.Local = mask
mask, err = setFields(ptConfig.ContextFields, desc.ContextFields, conf.IgnoreMissing)
if err != nil {
return fmt.Errorf("configuring point %q: %w", ptConfig.Name, err)
}
req.Fields.Context = mask
reqs = append(reqs, req)
}
for _, sinkConfig := range conf.Sinks {
desc, err := findSinkDesc(sinkConfig.Name)
if err != nil {
return err
}
opts, err := sinkConfig.options()
if err != nil {
return err
}
sink, err := desc.New(opts, sinkConfig.FD)
if err != nil {
return fmt.Errorf("creating event sink: %w", err)
}
state.AppendSink(sink, reqs)
}
if len(conf.Sinks) > 0 {
state.SetupExecveHashCache(capacity, reqs)
}
sessions[conf.Name] = state
sessionCounter.Increment()
return nil
}
// SetupSinks runs the setup step of all sinks in the configuration.
func SetupSinks(sinks []SinkConfig) ([]*os.File, error) {
var files []*os.File
for _, sink := range sinks {
sinkFile, err := setupSink(sink)
if err != nil {
if !sink.IgnoreSetupError {
return nil, err
}
log.Warningf("Ignoring sink setup failure: %v", err)
// Set sinkFile is nil and append it to the list to ensure the file
// order is preserved.
sinkFile = nil
}
files = append(files, sinkFile)
}
return files, nil
}
// setupSink runs the setup step for a given sink.
func setupSink(config SinkConfig) (*os.File, error) {
sink, err := findSinkDesc(config.Name)
if err != nil {
return nil, err
}
if sink.Setup == nil {
return nil, nil
}
opts, err := config.options()
if err != nil {
return nil, err
}
return sink.Setup(opts)
}
// options reads the sink's opaque object. What the keys mean is the sink's to
// say, so this is the one place the bytes become values.
func (c SinkConfig) options() (map[string]any, error) {
if len(c.Config) == 0 {
return nil, nil
}
var opts map[string]any
if err := json.Unmarshal(c.Config, &opts); err != nil {
return nil, fmt.Errorf("sink %q config: %w", c.Name, err)
}
return opts, nil
}
// Delete deletes an existing session.
func Delete(name string) error {
sessionsMu.Lock()
defer sessionsMu.Unlock()
return deleteLocked(name)
}
// +checklocks:sessionsMu
func deleteLocked(name string) error {
session := sessions[name]
if session == nil {
return fmt.Errorf("session %q not found", name)
}
session.clearSink()
delete(sessions, name)
return nil
}
// List lists all existing sessions.
func List(out *[]SessionConfig) {
sessionsMu.Lock()
defer sessionsMu.Unlock()
for name, state := range sessions {
// Only report session name. Consider adding rest of the fields as needed.
session := SessionConfig{Name: name}
for _, sink := range state.getSinks() {
session.Sinks = append(session.Sinks, SinkConfig{
Name: sink.Name(),
Status: sink.Status(),
})
}
*out = append(*out, session)
}
}
func findPointDesc(name string) (PointDesc, error) {
if desc, ok := Points[name]; ok {
return desc, nil
}
return PointDesc{}, fmt.Errorf("point %q not found", name)
}
func findField(name string, fields []FieldDesc) (FieldDesc, error) {
for _, f := range fields {
if f.Name == name {
return f, nil
}
}
return FieldDesc{}, fmt.Errorf("field %q not found", name)
}
func setFields(names []string, fields []FieldDesc, ignoreMissing bool) (FieldMask, error) {
fm := FieldMask{}
for _, name := range names {
desc, err := findField(name, fields)
if err != nil {
if ignoreMissing {
log.Warningf("Skipping field %q: %v", name, err)
continue
}
return FieldMask{}, err
}
fm.Add(desc.ID)
}
return fm, nil
}
func findSinkDesc(name string) (SinkDesc, error) {
if desc, ok := Sinks[name]; ok {
return desc, nil
}
return SinkDesc{}, fmt.Errorf("sink %q not found", name)
}