mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
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]>
294 lines
8.4 KiB
Go
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)
|
|
}
|