diff --git a/pkg/sentry/seccheck/BUILD b/pkg/sentry/seccheck/BUILD index d82e67f65..338edf894 100644 --- a/pkg/sentry/seccheck/BUILD +++ b/pkg/sentry/seccheck/BUILD @@ -45,12 +45,14 @@ go_test( name = "seccheck_test", size = "small", srcs = [ + "config_test.go", "metadata_test.go", "seccheck_test.go", ], library = ":seccheck", deps = [ "//pkg/context", + "//pkg/fd", "//pkg/sentry/seccheck/points:points_go_proto", ], ) diff --git a/pkg/sentry/seccheck/config.go b/pkg/sentry/seccheck/config.go index 8271773ea..4e688f4e1 100644 --- a/pkg/sentry/seccheck/config.go +++ b/pkg/sentry/seccheck/config.go @@ -39,6 +39,13 @@ type SessionConfig struct { 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"` } @@ -93,17 +100,21 @@ func Create(conf *SessionConfig, force bool) error { 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) + 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) + mask, err = setFields(ptConfig.ContextFields, desc.ContextFields, conf.IgnoreMissing) if err != nil { return fmt.Errorf("configuring point %q: %w", ptConfig.Name, err) } @@ -212,11 +223,15 @@ func findField(name string, fields []FieldDesc) (FieldDesc, error) { return FieldDesc{}, fmt.Errorf("field %q not found", name) } -func setFields(names []string, fields []FieldDesc) (FieldMask, error) { +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) diff --git a/pkg/sentry/seccheck/config_test.go b/pkg/sentry/seccheck/config_test.go new file mode 100644 index 000000000..901ef250f --- /dev/null +++ b/pkg/sentry/seccheck/config_test.go @@ -0,0 +1,190 @@ +// 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 ( + "strings" + "testing" +) + +func TestLifecycle(t *testing.T) { + for _, tc := range []struct { + name string + conf SessionConfig + }{ + { + name: "all-fields", + conf: SessionConfig{ + Name: "Default", + Points: []PointConfig{ + { + Name: "syscall/sysno/0/enter", + }, + { + Name: "syscall/openat/enter", + OptionalFields: []string{"fd_path"}, + }, + { + Name: "syscall/sysno/1/enter", + ContextFields: []string{"time"}, + }, + { + Name: "syscall/openat/enter", + OptionalFields: []string{"fd_path"}, + ContextFields: []string{"time"}, + }, + }, + Sinks: []SinkConfig{ + {Name: "test-sink"}, + }, + }, + }, + { + name: "no-sink", + conf: SessionConfig{ + Name: "Default", + Points: []PointConfig{ + {Name: "syscall/sysno/0/enter"}, + }, + }, + }, + { + name: "no-points", + conf: SessionConfig{ + Name: "Default", + Sinks: []SinkConfig{ + {Name: "test-sink"}, + }, + }, + }, + { + name: "ignore-errors", + conf: SessionConfig{ + Name: "Default", + IgnoreMissing: true, + Points: []PointConfig{ + { + Name: "foobar", + }, + { + Name: "syscall/sysno/1/enter", + ContextFields: []string{"foobar"}, + }, + { + Name: "syscall/openat/enter", + ContextFields: []string{"foobar"}, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + if err := Create(&tc.conf, false); err != nil { + t.Errorf("Create(): %v", err) + } + + var got []SessionConfig + List(&got) + if len(got) != 1 { + t.Errorf("only one session should exist, got: %d", len(got)) + } else { + if got[0].Name != tc.conf.Name { + t.Errorf("wrong name, want: %q, got: %q", tc.conf.Name, got[0].Name) + } + } + + if err := Delete(tc.conf.Name); err != nil { + t.Errorf("Delete(%q): %v", tc.conf.Name, err) + } + }) + } +} + +func TestFailure(t *testing.T) { + for _, tc := range []struct { + name string + conf SessionConfig + err string + }{ + { + name: "point", + err: `point "foobar" not found`, + conf: SessionConfig{ + Name: "Default", + Points: []PointConfig{ + {Name: "foobar"}, + }, + }, + }, + { + name: "optional-field", + err: `field "foobar" not found`, + conf: SessionConfig{ + Name: "Default", + Points: []PointConfig{ + { + Name: "syscall/openat/enter", + OptionalFields: []string{"foobar"}, + }, + }, + }, + }, + { + name: "context-field", + err: `field "foobar" not found`, + conf: SessionConfig{ + Name: "Default", + Points: []PointConfig{ + { + Name: "syscall/sysno/1/enter", + ContextFields: []string{"foobar"}, + }, + }, + }, + }, + { + name: "sink", + err: `sink "foobar" not found`, + conf: SessionConfig{ + Name: "Default", + Sinks: []SinkConfig{ + {Name: "foobar"}, + }, + }, + }, + { + name: "sink-ignore-missing", + err: `sink "foobar" not found`, + conf: SessionConfig{ + Name: "Default", + IgnoreMissing: true, + Sinks: []SinkConfig{ + {Name: "foobar"}, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + err := Create(&tc.conf, false) + if err == nil { + _ = Delete(tc.conf.Name) + t.Fatal("Create() should have failed") + } + if !strings.Contains(err.Error(), tc.err) { + t.Errorf("invalid error, want: %q, got: %q", tc.err, err) + } + }) + } +} diff --git a/pkg/sentry/seccheck/seccheck_test.go b/pkg/sentry/seccheck/seccheck_test.go index f839083d6..975d6a38b 100644 --- a/pkg/sentry/seccheck/seccheck_test.go +++ b/pkg/sentry/seccheck/seccheck_test.go @@ -19,15 +19,29 @@ import ( "testing" "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/fd" pb "gvisor.dev/gvisor/pkg/sentry/seccheck/points/points_go_proto" ) +func init() { + RegisterSink(SinkDesc{ + Name: "test-sink", + New: newTestSink, + }) +} + type testSink struct { SinkDefaults onClone func(ctx context.Context, fields FieldSet, info *pb.CloneInfo) error } +var _ Sink = (*testSink)(nil) + +func newTestSink(_ map[string]any, _ *fd.FD) (Sink, error) { + return &testSink{}, nil +} + // Name implements Sink.Name. func (c *testSink) Name() string { return "test-sink"