diff --git a/pkg/sentry/seccheck/checkers/remote/BUILD b/pkg/sentry/seccheck/checkers/remote/BUILD index cd21bed9c..fcf9ca788 100644 --- a/pkg/sentry/seccheck/checkers/remote/BUILD +++ b/pkg/sentry/seccheck/checkers/remote/BUILD @@ -5,7 +5,6 @@ package(licenses = ["notice"]) go_library( name = "remote", srcs = ["remote.go"], - marshal = True, visibility = ["//:sandbox"], deps = [ "//pkg/cleanup", diff --git a/pkg/sentry/seccheck/checkers/remote/remote.go b/pkg/sentry/seccheck/checkers/remote/remote.go index 2c6b2cb9b..65ecaf688 100644 --- a/pkg/sentry/seccheck/checkers/remote/remote.go +++ b/pkg/sentry/seccheck/checkers/remote/remote.go @@ -28,7 +28,6 @@ import ( "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/sentry/seccheck" "gvisor.dev/gvisor/pkg/sentry/seccheck/checkers/remote/header" - pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto" ) @@ -101,6 +100,15 @@ func New(_ map[string]interface{}, endpoint *fd.FD) (seccheck.Checker, error) { return &Remote{endpoint: endpoint}, nil } +// Stop implements seccheck.Checker. +func (r *Remote) Stop() { + if r.endpoint != nil { + // It's possible to race with Point firing, but in the worst case they will + // simply fail to be delivered. + r.endpoint.Close() + } +} + func (r *Remote) write(msg proto.Message, msgType pb.MessageType) { out, err := proto.Marshal(msg) if err != nil { @@ -116,7 +124,7 @@ func (r *Remote) write(msg proto.Message, msgType pb.MessageType) { // TODO(gvisor.dev/issue/4805): Change to non-blocking write. Count as dropped // if write fails. - if _, err = unix.Writev(r.endpoint.FD(), [][]byte{hdrOut[:], out}); err != nil { + if _, err := unix.Writev(r.endpoint.FD(), [][]byte{hdrOut[:], out}); err != nil { log.Debugf("write(%+v, %v): %v", msg, msgType, err) return } diff --git a/pkg/sentry/seccheck/checkers/remote/test/server.go b/pkg/sentry/seccheck/checkers/remote/test/server.go index 94a8b33b5..48835c95d 100644 --- a/pkg/sentry/seccheck/checkers/remote/test/server.go +++ b/pkg/sentry/seccheck/checkers/remote/test/server.go @@ -169,6 +169,16 @@ func (s *Server) Count() int { return len(s.points) } +// Reset throws aways all points received so far and returns the number of +// points discarded. +func (s *Server) Reset() int { + s.mu.Lock() + defer s.mu.Unlock() + count := len(s.points) + s.points = nil + return count +} + // GetPoints returns all points that it has received. func (s *Server) GetPoints() []Message { s.mu.Lock() diff --git a/pkg/sentry/seccheck/config.go b/pkg/sentry/seccheck/config.go index 3e83afdba..6ef10573d 100644 --- a/pkg/sentry/seccheck/config.go +++ b/pkg/sentry/seccheck/config.go @@ -17,11 +17,21 @@ package seccheck import ( "fmt" "os" + "sync" "gvisor.dev/gvisor/pkg/fd" "gvisor.dev/gvisor/pkg/log" ) +// 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) +) + // 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 { @@ -57,13 +67,18 @@ type SinkConfig struct { FD *fd.FD `json:"-"` } -// Configure reads the session configuration and applies it to the system. -func Configure(conf *SessionConfig) error { - log.Debugf("Configuring seccheck: %+v", conf) - state, err := findSession(conf.Name) - if err != nil { - return err +// Create reads the session configuration and applies it to the system. +func Create(conf *SessionConfig) error { + log.Debugf("Creating seccheck: %+v", conf) + sessionsMu.Lock() + defer sessionsMu.Unlock() + if _, ok := sessions[conf.Name]; ok { + return fmt.Errorf("session %q already exists", conf.Name) } + if conf.Name != DefaultSessionName { + return fmt.Errorf(`only a single "Default" session is supported`) + } + state := &Global var reqs []PointReq for _, ptConfig := range conf.Points { @@ -100,11 +115,31 @@ func Configure(conf *SessionConfig) error { state.AppendChecker(checker, reqs) } + sessions[conf.Name] = state return nil } -// SetupSink runs the setup step for a given sink. -func SetupSink(config SinkConfig) (*os.File, error) { +// 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 @@ -115,11 +150,30 @@ func SetupSink(config SinkConfig) (*os.File, error) { return sink.Setup(config.Config) } -func findSession(name string) (*State, error) { - if name != "Default" { - return nil, fmt.Errorf(`only a single "Default" session is supported`) +// Delete deletes an existing session. +func Delete(name string) error { + sessionsMu.Lock() + defer sessionsMu.Unlock() + + session := sessions[name] + if session == nil { + return fmt.Errorf("session %q not found", name) + } + + session.clearCheckers() + delete(sessions, name) + return nil +} + +// List lists all existing sessions. +func List(out *[]SessionConfig) { + sessionsMu.Lock() + defer sessionsMu.Unlock() + + for name := range sessions { + // Only report session name. Consider adding rest of the fields as needed. + *out = append(*out, SessionConfig{Name: name}) } - return &Global, nil } func findPointDesc(name string) (PointDesc, error) { diff --git a/pkg/sentry/seccheck/seccheck.go b/pkg/sentry/seccheck/seccheck.go index ec2858a4a..64310e3b7 100644 --- a/pkg/sentry/seccheck/seccheck.go +++ b/pkg/sentry/seccheck/seccheck.go @@ -96,6 +96,9 @@ func (fm *FieldMask) Empty() bool { // may be missing requested fields in some cases (e.g. if the Checker is // registered concurrently with invocations of checkpoints). type Checker interface { + // Stop requests the checker 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 @@ -113,6 +116,9 @@ type CheckerDefaults struct{} var _ Checker = (*CheckerDefaults)(nil) +// Stop implements Checker.Stop. +func (CheckerDefaults) Stop() {} + // Clone implements Checker.Clone. func (CheckerDefaults) Clone(context.Context, FieldSet, *pb.CloneInfo) error { return nil @@ -201,6 +207,24 @@ func (s *State) AppendChecker(c Checker, reqs []PointReq) { } } +func (s *State) clearCheckers() { + s.registrationMu.Lock() + defer s.registrationMu.Unlock() + + for i := range s.enabledPoints { + s.enabledPoints[i].Store(0) + } + s.pointFields = nil + + oldCheckers := s.getCheckers() + s.registrationSeq.BeginWrite() + s.checkers = nil + s.registrationSeq.EndWrite() + for _, checker := range oldCheckers { + checker.Stop() + } +} + // 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 diff --git a/runsc/boot/controller.go b/runsc/boot/controller.go index 22686f711..bfa96b4db 100644 --- a/runsc/boot/controller.go +++ b/runsc/boot/controller.go @@ -29,6 +29,7 @@ import ( controlpb "gvisor.dev/gvisor/pkg/sentry/control/control_go_proto" "gvisor.dev/gvisor/pkg/sentry/fs" "gvisor.dev/gvisor/pkg/sentry/kernel" + "gvisor.dev/gvisor/pkg/sentry/seccheck" "gvisor.dev/gvisor/pkg/sentry/socket/netstack" "gvisor.dev/gvisor/pkg/sentry/state" "gvisor.dev/gvisor/pkg/sentry/time" @@ -80,6 +81,15 @@ const ( // ContMgrRootContainerStart starts a new sandbox with a root container. ContMgrRootContainerStart = "containerManager.StartRoot" + + // ContMgrCreateTraceSession starts a trace session. + ContMgrCreateTraceSession = "containerManager.CreateTraceSession" + + // ContMgrDeleteTraceSession deletes a trace session. + ContMgrDeleteTraceSession = "containerManager.DeleteTraceSession" + + // ContMgrListTraceSessions lists a trace session. + ContMgrListTraceSessions = "containerManager.ListTraceSessions" ) const ( @@ -590,3 +600,37 @@ func (cm *containerManager) Signal(args *SignalArgs, _ *struct{}) error { log.Debugf("containerManager.Signal: cid: %s, PID: %d, signal: %d, mode: %v", args.CID, args.PID, args.Signo, args.Mode) return cm.l.signal(args.CID, args.PID, args.Signo, args.Mode) } + +// CreateTraceSessionArgs are arguments to the CreateTraceSession method. +type CreateTraceSessionArgs struct { + Config seccheck.SessionConfig + urpc.FilePayload +} + +// CreateTraceSession creates a new trace session. +func (cm *containerManager) CreateTraceSession(args *CreateTraceSessionArgs, _ *struct{}) error { + log.Debugf("containerManager.CreateTraceSession: config: %+v", args.Config) + for i, sinkFile := range args.Files { + if sinkFile != nil { + fd, err := fd.NewFromFile(sinkFile) + if err != nil { + return err + } + args.Config.Sinks[i].FD = fd + } + } + return seccheck.Create(&args.Config) +} + +// DeleteTraceSession deletes an existing trace session. +func (cm *containerManager) DeleteTraceSession(name *string, _ *struct{}) error { + log.Debugf("containerManager.DeleteTraceSession: name: %q", *name) + return seccheck.Delete(*name) +} + +// ListTraceSessions lists trace sessions. +func (cm *containerManager) ListTraceSessions(_ *struct{}, out *[]seccheck.SessionConfig) error { + log.Debugf("containerManager.ListTraceSessions") + seccheck.List(out) + return nil +} diff --git a/runsc/boot/seccheck.go b/runsc/boot/seccheck.go index 52a0d45e2..82977ceb5 100644 --- a/runsc/boot/seccheck.go +++ b/runsc/boot/seccheck.go @@ -20,7 +20,6 @@ import ( "os" "gvisor.dev/gvisor/pkg/fd" - "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/sentry/seccheck" // Register supported of checkers. @@ -41,7 +40,7 @@ func setupSeccheck(configFD int, sinkFDs []int) error { if err != nil { return err } - return initConf.configure(sinkFDs) + return initConf.create(sinkFDs) } // LoadInitConfig loads an InitConfig struct from a json formatted file. @@ -66,29 +65,14 @@ func loadInitConfig(reader io.Reader) (*InitConfig, error) { // Setup performs the actions defined in the InitConfig, e.g. setup seccheck // session. func (c *InitConfig) Setup() ([]*os.File, error) { - var files []*os.File - for _, sink := range c.TraceSession.Sinks { - sinkFile, err := seccheck.SetupSink(sink) - if err != nil { - if !sink.IgnoreSetupError { - return nil, err - } - log.Warningf("Ignoring sink setup failure: %v", err) - // Ensure 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 + return seccheck.SetupSinks(c.TraceSession.Sinks) } -func (c *InitConfig) configure(sinkFDs []int) error { +func (c *InitConfig) create(sinkFDs []int) error { for i, sinkFD := range sinkFDs { if sinkFD >= 0 { c.TraceSession.Sinks[i].FD = fd.New(sinkFD) } } - return seccheck.Configure(&c.TraceSession) - + return seccheck.Create(&c.TraceSession) } diff --git a/runsc/cli/BUILD b/runsc/cli/BUILD index f2ae169e5..ba44d7b44 100644 --- a/runsc/cli/BUILD +++ b/runsc/cli/BUILD @@ -16,6 +16,7 @@ go_library( "//pkg/refsvfs2", "//pkg/sentry/platform", "//runsc/cmd", + "//runsc/cmd/trace", "//runsc/cmd/util", "//runsc/config", "//runsc/flag", diff --git a/runsc/cli/main.go b/runsc/cli/main.go index 550bc6f10..609198865 100644 --- a/runsc/cli/main.go +++ b/runsc/cli/main.go @@ -33,6 +33,7 @@ import ( "gvisor.dev/gvisor/pkg/refsvfs2" "gvisor.dev/gvisor/pkg/sentry/platform" "gvisor.dev/gvisor/runsc/cmd" + "gvisor.dev/gvisor/runsc/cmd/trace" "gvisor.dev/gvisor/runsc/cmd/util" "gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/flag" @@ -83,11 +84,12 @@ func Main(version string) { subcommands.Register(new(cmd.VerityPrepare), "") subcommands.Register(new(cmd.Wait), "") - // Installation helpers. + // Helpers. const helperGroup = "helpers" subcommands.Register(new(cmd.Install), helperGroup) subcommands.Register(new(cmd.Mitigate), helperGroup) subcommands.Register(new(cmd.Uninstall), helperGroup) + subcommands.Register(new(trace.Trace), helperGroup) const debugGroup = "debug" subcommands.Register(new(cmd.Debug), debugGroup) diff --git a/runsc/cmd/trace/BUILD b/runsc/cmd/trace/BUILD new file mode 100644 index 000000000..835cb0dd5 --- /dev/null +++ b/runsc/cmd/trace/BUILD @@ -0,0 +1,25 @@ +load("//tools:defs.bzl", "go_library") + +package(licenses = ["notice"]) + +go_library( + name = "trace", + srcs = [ + "create.go", + "delete.go", + "list.go", + "metadata.go", + "trace.go", + ], + visibility = [ + "//runsc:__subpackages__", + ], + deps = [ + "//pkg/sentry/seccheck", + "//runsc/cmd/util", + "//runsc/config", + "//runsc/container", + "//runsc/flag", + "@com_github_google_subcommands//:go_default_library", + ], +) diff --git a/runsc/cmd/trace/create.go b/runsc/cmd/trace/create.go new file mode 100644 index 000000000..a07a42201 --- /dev/null +++ b/runsc/cmd/trace/create.go @@ -0,0 +1,95 @@ +// Copyright 2020 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 trace + +import ( + "context" + "encoding/json" + "os" + + "github.com/google/subcommands" + "gvisor.dev/gvisor/pkg/sentry/seccheck" + "gvisor.dev/gvisor/runsc/cmd/util" + "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/container" + "gvisor.dev/gvisor/runsc/flag" +) + +// create implements subcommands.Command for the "create" command. +type create struct { + config string +} + +// Name implements subcommands.Command. +func (*create) Name() string { + return "create" +} + +// Synopsis implements subcommands.Command. +func (*create) Synopsis() string { + return "create a trace session" +} + +// Usage implements subcommands.Command. +func (*create) Usage() string { + return `create [flags] - create a trace session +` +} + +// SetFlags implements subcommands.Command. +func (l *create) SetFlags(f *flag.FlagSet) { + f.StringVar(&l.config, "config", "", "path to the JSON file that describes the session being created") +} + +// Execute implements subcommands.Command. +func (l *create) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + if f.NArg() != 1 { + f.Usage() + return subcommands.ExitUsageError + } + if len(l.config) == 0 { + f.Usage() + return util.Errorf("missing path to configuration file, please set --config=[path]") + } + + file, err := os.Open(l.config) + if err != nil { + return util.Errorf(err.Error()) + } + defer file.Close() + decoder := json.NewDecoder(file) + sessionConfig := &seccheck.SessionConfig{} + if err := decoder.Decode(sessionConfig); err != nil { + return util.Errorf("invalid configuration file: %v", err) + } + + id := f.Arg(0) + conf := args[0].(*config.Config) + + opts := container.LoadOpts{ + SkipCheck: true, + RootContainer: true, + } + c, err := container.Load(conf.RootDir, container.FullID{ContainerID: id}, opts) + if err != nil { + util.Fatalf("loading sandbox: %v", err) + } + + if err := c.Sandbox.CreateTraceSession(sessionConfig); err != nil { + util.Fatalf("creating session: %v", err) + } + + return subcommands.ExitSuccess +} diff --git a/runsc/cmd/trace/delete.go b/runsc/cmd/trace/delete.go new file mode 100644 index 000000000..08f1bc0b7 --- /dev/null +++ b/runsc/cmd/trace/delete.go @@ -0,0 +1,80 @@ +// Copyright 2020 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 trace + +import ( + "context" + + "github.com/google/subcommands" + "gvisor.dev/gvisor/runsc/cmd/util" + "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/container" + "gvisor.dev/gvisor/runsc/flag" +) + +// delete implements subcommands.Command for the "delete" command. +type delete struct { + name string +} + +// Name implements subcommands.Command. +func (*delete) Name() string { + return "delete" +} + +// Synopsis implements subcommands.Command. +func (*delete) Synopsis() string { + return "delete a trace session" +} + +// Usage implements subcommands.Command. +func (*delete) Usage() string { + return `delete [flags] - delete a trace session +` +} + +// SetFlags implements subcommands.Command. +func (l *delete) SetFlags(f *flag.FlagSet) { + f.StringVar(&l.name, "name", "", "name of session to be deleted") +} + +// Execute implements subcommands.Command. +func (l *delete) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + if f.NArg() != 1 { + f.Usage() + return subcommands.ExitUsageError + } + if len(l.name) == 0 { + f.Usage() + return util.Errorf("missing session name, please set --name") + } + + id := f.Arg(0) + conf := args[0].(*config.Config) + + opts := container.LoadOpts{ + SkipCheck: true, + RootContainer: true, + } + c, err := container.Load(conf.RootDir, container.FullID{ContainerID: id}, opts) + if err != nil { + util.Fatalf("loading sandbox: %v", err) + } + + if err := c.Sandbox.DeleteTraceSession(l.name); err != nil { + util.Fatalf("deleting session: %v", err) + } + return subcommands.ExitSuccess +} diff --git a/runsc/cmd/trace/list.go b/runsc/cmd/trace/list.go new file mode 100644 index 000000000..e45d1919b --- /dev/null +++ b/runsc/cmd/trace/list.go @@ -0,0 +1,78 @@ +// 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 trace + +import ( + "context" + "fmt" + + "github.com/google/subcommands" + "gvisor.dev/gvisor/runsc/cmd/util" + "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/container" + "gvisor.dev/gvisor/runsc/flag" +) + +// list implements subcommands.Command for the "list" command. +type list struct{} + +// Name implements subcommands.Command. +func (*list) Name() string { + return "list" +} + +// Synopsis implements subcommands.Command. +func (*list) Synopsis() string { + return "list all trace sessions" +} + +// Usage implements subcommands.Command. +func (*list) Usage() string { + return `list - list all trace sessions +` +} + +// SetFlags implements subcommands.Command. +func (*list) SetFlags(*flag.FlagSet) {} + +// Execute implements subcommands.Command. +func (l *list) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + if f.NArg() != 1 { + f.Usage() + return subcommands.ExitUsageError + } + + id := f.Arg(0) + conf := args[0].(*config.Config) + + opts := container.LoadOpts{ + SkipCheck: true, + RootContainer: true, + } + c, err := container.Load(conf.RootDir, container.FullID{ContainerID: id}, opts) + if err != nil { + util.Fatalf("loading sandbox: %v", err) + } + + sessions, err := c.Sandbox.ListTraceSessions() + if err != nil { + util.Fatalf("listing sessions: %v", err) + } + fmt.Printf("SESSIONS (%d)\n", len(sessions)) + for _, session := range sessions { + fmt.Printf("%q\n", session.Name) + } + return subcommands.ExitSuccess +} diff --git a/runsc/cmd/trace/metadata.go b/runsc/cmd/trace/metadata.go new file mode 100644 index 000000000..868f3ffff --- /dev/null +++ b/runsc/cmd/trace/metadata.go @@ -0,0 +1,76 @@ +// 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 trace + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/google/subcommands" + "gvisor.dev/gvisor/pkg/sentry/seccheck" + "gvisor.dev/gvisor/runsc/flag" +) + +// metadata implements subcommands.Command for the "metadata" command. +type metadata struct{} + +// Name implements subcommands.Command. +func (*metadata) Name() string { + return "metadata" +} + +// Synopsis implements subcommands.Command. +func (*metadata) Synopsis() string { + return "list all trace points configuration information" +} + +// Usage implements subcommands.Command. +func (*metadata) Usage() string { + return `metadata - list all trace points configuration information +` +} + +// SetFlags implements subcommands.Command. +func (*metadata) SetFlags(*flag.FlagSet) {} + +// Execute implements subcommands.Command. +func (l *metadata) Execute(context.Context, *flag.FlagSet, ...interface{}) subcommands.ExitStatus { + // Sort to keep related points together. + points := make([]seccheck.PointDesc, 0, len(seccheck.Points)) + for _, pt := range seccheck.Points { + points = append(points, pt) + } + sort.Slice(points, func(i int, j int) bool { + return points[i].Name < points[j].Name + }) + + fmt.Printf("POINTS (%d)\n", len(seccheck.Points)) + for _, pt := range points { + optFields := fieldNames(pt.OptionalFields) + ctxFields := fieldNames(pt.ContextFields) + fmt.Printf("Name: %s, optional fields: [%s], context fields: [%s]\n", pt.Name, strings.Join(optFields, "|"), strings.Join(ctxFields, "|")) + } + return subcommands.ExitSuccess +} + +func fieldNames(fields []seccheck.FieldDesc) []string { + names := make([]string, 0, len(fields)) + for _, f := range fields { + names = append(names, f.Name) + } + return names +} diff --git a/runsc/cmd/trace/trace.go b/runsc/cmd/trace/trace.go new file mode 100644 index 000000000..45e121107 --- /dev/null +++ b/runsc/cmd/trace/trace.go @@ -0,0 +1,69 @@ +// 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 trace provides subcommands for the trace command. +package trace + +import ( + "bytes" + "context" + + "github.com/google/subcommands" + "gvisor.dev/gvisor/runsc/flag" +) + +// Trace implements subcommands.Command for the "trace" command. +type Trace struct{} + +// Name implements subcommands.Command. +func (*Trace) Name() string { + return "trace" +} + +// Synopsis implements subcommands.Command. +func (*Trace) Synopsis() string { + return "manages trace sessions for a given sandbox" +} + +// Usage implements subcommands.Command. +func (*Trace) Usage() string { + buf := bytes.Buffer{} + buf.WriteString("Usage: trace \n\n") + + cdr := createCommander(&flag.FlagSet{}) + cdr.VisitGroups(func(grp *subcommands.CommandGroup) { + cdr.ExplainGroup(&buf, grp) + }) + + return buf.String() +} + +// SetFlags implements subcommands.Command. +func (*Trace) SetFlags(f *flag.FlagSet) {} + +// Execute implements subcommands.Command. +func (*Trace) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + return createCommander(f).Execute(ctx, args...) +} + +func createCommander(f *flag.FlagSet) *subcommands.Commander { + cdr := subcommands.NewCommander(f, "trace") + cdr.Register(cdr.HelpCommand(), "") + cdr.Register(cdr.FlagsCommand(), "") + cdr.Register(new(create), "") + cdr.Register(new(delete), "") + cdr.Register(new(list), "") + cdr.Register(new(metadata), "") + return cdr +} diff --git a/runsc/container/BUILD b/runsc/container/BUILD index ca6f6a7d3..04fdd0c1d 100644 --- a/runsc/container/BUILD +++ b/runsc/container/BUILD @@ -43,6 +43,7 @@ go_test( "container_test.go", "multi_container_test.go", "shared_volume_test.go", + "trace_test.go", ], # Only run the default platform for the tsan test, which should # be compatible. For non-tsan builds, run all platforms. @@ -70,6 +71,9 @@ go_test( "//pkg/sentry/kernel", "//pkg/sentry/kernel/auth", "//pkg/sentry/platform", + "//pkg/sentry/seccheck", + "//pkg/sentry/seccheck/checkers/remote/test", + "//pkg/sentry/seccheck/points:points_go_proto", "//pkg/sync", "//pkg/test/testutil", "//pkg/unet", @@ -81,6 +85,7 @@ go_test( "@com_github_cenkalti_backoff//:go_default_library", "@com_github_kr_pty//:go_default_library", "@com_github_opencontainers_runtime_spec//specs-go:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", "@org_golang_x_sys//unix:go_default_library", ], ) diff --git a/runsc/container/state_file.go b/runsc/container/state_file.go index 23810f593..4888dd454 100644 --- a/runsc/container/state_file.go +++ b/runsc/container/state_file.go @@ -38,6 +38,11 @@ type LoadOpts struct { // SkipCheck tells Load() to skip checking if container is runnning. SkipCheck bool + + // RootContainer when true matches the search only with the root container of + // a sandbox. This is used when looking for a sandbox given that root + // container and sandbox share the same ID. + RootContainer bool } // Load loads a container with the given id from a metadata file. "id" may @@ -77,6 +82,10 @@ func Load(rootDir string, id FullID, opts LoadOpts) (*Container, error) { return nil, fmt.Errorf("reading container metadata file %q: %v", state.statePath(), err) } + if opts.RootContainer && c.ID != c.Sandbox.ID { + return nil, fmt.Errorf("ID %q doesn't belong to a sandbox", id) + } + if !opts.SkipCheck { // If the status is "Running" or "Created", check that the sandbox/container // is still running, setting it to Stopped if not. diff --git a/runsc/container/trace_test.go b/runsc/container/trace_test.go new file mode 100644 index 000000000..c63a5a2b6 --- /dev/null +++ b/runsc/container/trace_test.go @@ -0,0 +1,219 @@ +// 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 container + +import ( + "encoding/json" + "io/ioutil" + "testing" + "time" + + "google.golang.org/protobuf/proto" + "gvisor.dev/gvisor/pkg/sentry/seccheck" + "gvisor.dev/gvisor/pkg/sentry/seccheck/checkers/remote/test" + pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto" + "gvisor.dev/gvisor/pkg/test/testutil" + "gvisor.dev/gvisor/runsc/boot" +) + +// Test that setting up a trace session configuration in PodInitConfig creates +// a session before container creation. +func TestTraceStartup(t *testing.T) { + // Test on all configurations to ensure that point can be sent to an outside + // process in all cases. Rest of the tests don't require all configs. + for name, conf := range configs(t, false /* noOverlay */) { + t.Run(name, func(t *testing.T) { + server, err := test.NewServer() + if err != nil { + t.Fatalf("newServer(): %v", err) + } + defer server.Close() + + podInitConfig, err := ioutil.TempFile(testutil.TmpDir(), "config") + if err != nil { + t.Fatalf("error creating tmp file: %v", err) + } + defer podInitConfig.Close() + + initConfig := boot.InitConfig{ + TraceSession: seccheck.SessionConfig{ + Name: seccheck.DefaultSessionName, + Points: []seccheck.PointConfig{ + { + Name: "container/start", + ContextFields: []string{"container_id"}, + }, + }, + Sinks: []seccheck.SinkConfig{ + { + Name: "remote", + Config: map[string]interface{}{ + "endpoint": server.Path, + }, + }, + }, + }, + } + encoder := json.NewEncoder(podInitConfig) + if err := encoder.Encode(&initConfig); err != nil { + t.Fatalf("JSON encode: %v", err) + } + conf.PodInitConfig = podInitConfig.Name() + + spec := testutil.NewSpecWithArgs("/bin/true") + if err := run(spec, conf); err != nil { + t.Fatalf("Error running container: %v", err) + } + + // Wait for the point to be received and then check that fields match. + if err := server.WaitForCount(1); err != nil { + t.Fatalf("WaitForCount(1): %v", err) + } + pt := server.GetPoints()[0] + if want := pb.MessageType_MESSAGE_CONTAINER_START; pt.MsgType != want { + t.Errorf("wrong message type, want: %v, got: %v", want, pt.MsgType) + } + got := &pb.Start{} + if err := proto.Unmarshal(pt.Msg, got); err != nil { + t.Errorf("proto.Unmarshal(Start): %v", err) + } + if want := "/bin/true"; len(got.Args) != 1 || want != got.Args[0] { + t.Errorf("container.Start.Args, want: %q, got: %q", want, got.Args) + } + if want, got := got.Id, got.ContextData.ContainerId; want != got { + t.Errorf("Mismatched container ID, want: %v, got: %v", want, got) + } + }) + } +} + +func TestTraceLifecycle(t *testing.T) { + spec, conf := sleepSpecConf(t) + _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer cleanup() + + // Create and start the container. + args := Args{ + ID: testutil.RandomContainerID(), + Spec: spec, + BundleDir: bundleDir, + } + cont, err := New(conf, args) + if err != nil { + t.Fatalf("error creating container: %v", err) + } + defer cont.Destroy() + if err := cont.Start(conf); err != nil { + t.Fatalf("error starting container: %v", err) + } + + // Check that no session are created. + if sessions, err := cont.Sandbox.ListTraceSessions(); err != nil { + t.Fatalf("ListTraceSessions(): %v", err) + } else if len(sessions) != 0 { + t.Fatalf("no session should exist, got: %+v", sessions) + } + + // Create a new trace session on the fly. + server, err := test.NewServer() + if err != nil { + t.Fatalf("newServer(): %v", err) + } + defer server.Close() + + session := seccheck.SessionConfig{ + Name: "Default", + Points: []seccheck.PointConfig{ + { + Name: "sentry/task_exit", + ContextFields: []string{"container_id"}, + }, + }, + Sinks: []seccheck.SinkConfig{ + { + Name: "remote", + Config: map[string]interface{}{ + "endpoint": server.Path, + }, + }, + }, + } + if err := cont.Sandbox.CreateTraceSession(&session); err != nil { + t.Fatalf("CreateTraceSession(): %v", err) + } + + // Trigger the configured point and want to receive it in the server. + if ws, err := execute(conf, cont, "/bin/true"); err != nil || ws != 0 { + t.Fatalf("exec: true, ws: %v, err: %v", ws, err) + } + if err := server.WaitForCount(1); err != nil { + t.Fatalf("WaitForCount(1): %v", err) + } + pt := server.GetPoints()[0] + if want := pb.MessageType_MESSAGE_SENTRY_TASK_EXIT; pt.MsgType != want { + t.Errorf("wrong message type, want: %v, got: %v", want, pt.MsgType) + } + got := &pb.TaskExit{} + if err := proto.Unmarshal(pt.Msg, got); err != nil { + t.Errorf("proto.Unmarshal(TaskExit): %v", err) + } + if got.ExitStatus != 0 { + t.Errorf("Wrong TaskExit.ExitStatus, want: 0, got: %+v", got) + } + if want, got := cont.ID, got.ContextData.ContainerId; want != got { + t.Errorf("Wrong TaskExit.ContextData.ContainerId, want: %v, got: %v", want, got) + } + + // Check that no more points were received and reset the server for the + // remaining tests. + if want, got := 1, server.Reset(); want != got { + t.Errorf("wrong number of points, want: %d, got: %d", want, got) + } + + // List and check that trace session is reported. + sessions, err := cont.Sandbox.ListTraceSessions() + if err != nil { + t.Fatalf("ListTraceSessions(): %v", err) + } + if len(sessions) != 1 { + t.Fatalf("expected a single session, got: %+v", sessions) + } + if got := sessions[0].Name; seccheck.DefaultSessionName != got { + t.Errorf("wrong session, want: %v, got: %v", seccheck.DefaultSessionName, got) + } + + if err := cont.Sandbox.DeleteTraceSession("Default"); err != nil { + t.Fatalf("DeleteTraceSession(): %v", err) + } + + // Check that session was indeed deleted. + if sessions, err := cont.Sandbox.ListTraceSessions(); err != nil { + t.Fatalf("ListTraceSessions(): %v", err) + } else if len(sessions) != 0 { + t.Fatalf("no session should exist, got: %+v", sessions) + } + + // Trigger the point again and check that it's not received. + if ws, err := execute(conf, cont, "/bin/true"); err != nil || ws != 0 { + t.Fatalf("exec: true, ws: %v, err: %v", ws, err) + } + time.Sleep(time.Second) // give some time to receive the point. + if server.Count() > 0 { + t.Errorf("point received after session was deleted: %+v", server.GetPoints()) + } +} diff --git a/runsc/sandbox/BUILD b/runsc/sandbox/BUILD index 48a3ec5a7..8c5b6501e 100644 --- a/runsc/sandbox/BUILD +++ b/runsc/sandbox/BUILD @@ -23,6 +23,7 @@ go_library( "//pkg/log", "//pkg/sentry/control", "//pkg/sentry/platform", + "//pkg/sentry/seccheck", "//pkg/sync", "//pkg/tcpip/header", "//pkg/tcpip/stack", diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index 648e5a689..06fda28d2 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -41,6 +41,7 @@ import ( "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/sentry/control" "gvisor.dev/gvisor/pkg/sentry/platform" + "gvisor.dev/gvisor/pkg/sentry/seccheck" "gvisor.dev/gvisor/pkg/sync" "gvisor.dev/gvisor/pkg/unet" "gvisor.dev/gvisor/pkg/urpc" @@ -196,7 +197,7 @@ func New(conf *config.Config, args *Args) (*Sandbox, error) { if len(conf.PodInitConfig) > 0 { initConf, err := boot.LoadInitConfig(conf.PodInitConfig) if err != nil { - return nil, err + return nil, fmt.Errorf("loading init config file: %w", err) } args.SinkFiles, err = initConf.Setup() if err != nil { @@ -388,6 +389,69 @@ func (s *Sandbox) Processes(cid string) ([]*control.Process, error) { return pl, nil } +// CreateTraceSession creates a new trace session. +func (s *Sandbox) CreateTraceSession(config *seccheck.SessionConfig) error { + log.Debugf("Creating trace session in sandbox %q", s.ID) + + sinkFiles, err := seccheck.SetupSinks(config.Sinks) + if err != nil { + return err + } + defer func() { + for _, f := range sinkFiles { + _ = f.Close() + } + }() + + conn, err := s.sandboxConnect() + if err != nil { + return err + } + defer conn.Close() + + arg := boot.CreateTraceSessionArgs{ + Config: *config, + FilePayload: urpc.FilePayload{ + Files: sinkFiles, + }, + } + if err := conn.Call(boot.ContMgrCreateTraceSession, &arg, nil); err != nil { + return fmt.Errorf("creating trace session: %w", err) + } + return nil +} + +// DeleteTraceSession deletes an existing trace session. +func (s *Sandbox) DeleteTraceSession(name string) error { + log.Debugf("Deleting trace session %q in sandbox %q", name, s.ID) + conn, err := s.sandboxConnect() + if err != nil { + return err + } + defer conn.Close() + + if err := conn.Call(boot.ContMgrDeleteTraceSession, name, nil); err != nil { + return fmt.Errorf("deleting trace session: %w", err) + } + return nil +} + +// ListTraceSessions lists all trace sessions. +func (s *Sandbox) ListTraceSessions() ([]seccheck.SessionConfig, error) { + log.Debugf("Listing trace sessions in sandbox %q", s.ID) + conn, err := s.sandboxConnect() + if err != nil { + return nil, err + } + defer conn.Close() + + var sessions []seccheck.SessionConfig + if err := conn.Call(boot.ContMgrListTraceSessions, nil, &sessions); err != nil { + return nil, fmt.Errorf("listing trace session: %w", err) + } + return sessions, nil +} + // NewCGroup returns the sandbox's Cgroup, or an error if it does not have one. func (s *Sandbox) NewCGroup() (cgroup.Cgroup, error) { return cgroup.NewFromPid(s.Pid.load(), false /* useSystemd */)