From 2ef56fee94336b2e21dfd61b935e3c5bd5709e73 Mon Sep 17 00:00:00 2001 From: Etienne Perot Date: Fri, 17 Nov 2023 14:48:40 -0800 Subject: [PATCH] Seccomp filters: Add method to uniquely identify the seccomp configuration. This adds a `Key()` method on `config.Options` which uniquely identifies the ingredients that go into the seccomp filter configuration. This is necessary for precompiling seccomp filters. At runtime, precompiled seccomp programs will be matched according to their config key. PiperOrigin-RevId: 583492673 --- runsc/boot/filter/config/config.go | 21 ++++++ runsc/boot/filter/config/config_test.go | 85 +++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/runsc/boot/filter/config/config.go b/runsc/boot/filter/config/config.go index 327e3e604..44e5c84e9 100644 --- a/runsc/boot/filter/config/config.go +++ b/runsc/boot/filter/config/config.go @@ -17,6 +17,9 @@ package config import ( + "fmt" + "strings" + "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/seccomp" "gvisor.dev/gvisor/pkg/sentry/devices/accel" @@ -36,6 +39,24 @@ type Options struct { ControllerFD int } +// ConfigKey returns a unique string representing this set of options. +// This is used for matching a set of `Options` at seccomp precompile +// time with the same set of `Options` at runtime. +// As such, it should encompass all fields that change the structure of +// the seccomp rules, but should not encompass fields that are only known +// at runtime (e.g. `ControllerFD`). +func (opt Options) ConfigKey() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Platform=%q ", opt.Platform.ConfigKey())) + sb.WriteString(fmt.Sprintf("HostNetwork=%t ", opt.HostNetwork)) + sb.WriteString(fmt.Sprintf("HostNetworkRawSockets=%t ", opt.HostNetworkRawSockets)) + sb.WriteString(fmt.Sprintf("HostFilesystem=%t ", opt.HostFilesystem)) + sb.WriteString(fmt.Sprintf("ProfileEnable=%t ", opt.ProfileEnable)) + sb.WriteString(fmt.Sprintf("NVProxy=%t ", opt.NVProxy)) + sb.WriteString(fmt.Sprintf("TPUProxy=%t ", opt.TPUProxy)) + return strings.TrimSpace(sb.String()) +} + // Warnings returns a set of warnings that may be useful to display to the // user when the given options are used. func Warnings(opt Options) []string { diff --git a/runsc/boot/filter/config/config_test.go b/runsc/boot/filter/config/config_test.go index 06d9b6ec1..bbb25f140 100644 --- a/runsc/boot/filter/config/config_test.go +++ b/runsc/boot/filter/config/config_test.go @@ -16,6 +16,7 @@ package config import ( "fmt" + "reflect" "testing" "golang.org/x/sys/unix" @@ -74,3 +75,87 @@ func TestIoctlFirstArgumentIsNonNegativeFD(t *testing.T) { }) } } + +// TestOptionsConfigKey verifies the behavior of `Options.ConfigKey`. +func TestOptionsConfigKey(t *testing.T) { + // mutateFn mutates the value of a specific Options field. + type mutateFn func(opt *Options) + + defaultOpt := Options{ + Platform: (&systrap.Systrap{}).SeccompInfo(), + } + + // Map of `Options` struct field names mapped to a function to mutate them. + // This should only contain fields which influence the configuration; + // calling the mutation function of these should change the value of + // `Options.Key`. + var configFields = map[string]mutateFn{ + "Platform": func(opt *Options) { + if defaultOpt.Platform.ConfigKey() == opt.Platform.ConfigKey() { + opt.Platform = (&kvm.KVM{}).SeccompInfo() + } else { + opt.Platform = (&systrap.Systrap{}).SeccompInfo() + } + }, + "HostNetwork": func(opt *Options) { opt.HostNetwork = !opt.HostNetwork }, + "HostNetworkRawSockets": func(opt *Options) { opt.HostNetworkRawSockets = !opt.HostNetworkRawSockets }, + "HostFilesystem": func(opt *Options) { opt.HostFilesystem = !opt.HostFilesystem }, + "ProfileEnable": func(opt *Options) { opt.ProfileEnable = !opt.ProfileEnable }, + "NVProxy": func(opt *Options) { opt.NVProxy = !opt.NVProxy }, + "TPUProxy": func(opt *Options) { opt.TPUProxy = !opt.TPUProxy }, + } + + // Map of `Options` struct field names mapped to a function to mutate them. + // This should only contain fields which are used as variables during + // filter generation; calling the mutation function of these should *not* + // change the value of `Options.Key`. + var varsFields = map[string]mutateFn{ + "ControllerFD": func(opt *Options) { opt.ControllerFD++ }, + } + + t.Run("fields are exhaustive", func(t *testing.T) { + for i := 0; i < reflect.ValueOf(defaultOpt).NumField(); i++ { + f := reflect.TypeOf(defaultOpt).Field(i) + found := false + for name := range configFields { + if f.Name == name { + found = true + break + } + } + for name := range varsFields { + if f.Name == name { + found = true + break + } + } + if !found { + t.Fatalf("field `Options.%s` is not known to TestOptionsKey; please add it", f.Name) + } + } + }) + + t.Run("mutating config fields causes ConfigKey to change", func(t *testing.T) { + opt := defaultOpt // Make a copy, as we're about to mutate it. + key := opt.ConfigKey() + for name, mutateFn := range configFields { + mutateFn(&opt) + newKey := opt.ConfigKey() + if key == newKey { + t.Fatalf("mutating config field %q did not cause the ConfigKey to change: %q", name, key) + } + key = newKey + } + }) + + t.Run("mutating vars fields does not cause ConfigKey to change", func(t *testing.T) { + opt := defaultOpt // Make a copy, as we're about to mutate it. + key := opt.ConfigKey() + for name, mutateFn := range varsFields { + mutateFn(&opt) + if newKey := opt.ConfigKey(); key != newKey { + t.Fatalf("mutating var field %q caused the ConfigKey to change: %q -> %q", name, key, newKey) + } + } + }) +}