diff --git a/pkg/bpf/bpf_unsafe.go b/pkg/bpf/bpf_unsafe.go index 418fe218c..964518212 100644 --- a/pkg/bpf/bpf_unsafe.go +++ b/pkg/bpf/bpf_unsafe.go @@ -19,10 +19,17 @@ import ( "unsafe" ) +// sizeOfInstruction is the size of a BPF instruction struct. +const sizeOfInstruction = int(unsafe.Sizeof(Instruction{})) + +// ToBytecode converts BPF instructions into raw BPF bytecode. +func ToBytecode(insns []Instruction) []byte { + return ([]byte)(unsafe.Slice((*byte)(unsafe.Pointer(&insns[0])), len(insns)*sizeOfInstruction)) +} + // ParseBytecode converts raw BPF bytecode into BPF instructions. // It verifies that the resulting set of instructions is a valid program. func ParseBytecode(bytecode []byte) ([]Instruction, error) { - sizeOfInstruction := int(unsafe.Sizeof(Instruction{})) if len(bytecode)%sizeOfInstruction != 0 { return nil, fmt.Errorf("bytecode size (%d bytes) is not a multiple of BPF instruction size of %d bytes", len(bytecode), sizeOfInstruction) } diff --git a/pkg/seccomp/precompiledseccomp/BUILD b/pkg/seccomp/precompiledseccomp/BUILD new file mode 100644 index 000000000..984c02845 --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/BUILD @@ -0,0 +1,52 @@ +load("//tools:defs.bzl", "bzl_library", "go_library", "go_test") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_library( + name = "precompiledseccomp", + srcs = ["precompiledseccomp.go"], + visibility = ["//:sandbox"], + deps = [ + "//pkg/bpf", + "//pkg/log", + "//pkg/seccomp", + ], +) + +# @unused +glaze_ignore = [ + "precompiled_lib.tmpl.go", + "precompile_gen.go", +] + +exports_files( + srcs = [ + "precompile_gen.go", + "precompiled_lib.tmpl.go", + ], + visibility = [ + "//:sandbox", + ], +) + +bzl_library( + name = "defs_bzl", + srcs = ["defs.bzl"], + visibility = [ + "//:sandbox", + ], +) + +go_test( + name = "precompiledseccomp_test", + srcs = ["precompiledseccomp_test.go"], + library = ":precompiledseccomp", + deps = [ + "//pkg/abi/linux", + "//pkg/seccomp", + "@org_golang_x_sys//unix:go_default_library", + ], +) diff --git a/pkg/seccomp/precompiledseccomp/README.md b/pkg/seccomp/precompiledseccomp/README.md new file mode 100644 index 000000000..ea6d76004 --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/README.md @@ -0,0 +1,62 @@ +# precompiledseccomp + +This package provides build tooling to embed precompiled seccomp-bpf programs +inside Go binaries. Within gVisor, this is useful to keep startup time fast. + +It also features basic support for runtime-modifiable `uint32` variables within +embedded programs. This allows having values that are only known at runtime +(e.g. FD numbers) to remain usable within seccomp filtering rules. + +## Usage + +You will need two Go libraries: One where you'll list the seccomp-bpf programs +that you want embedded, and one where those precompiled programs will be +embedded. This allows you to define the list of seccomp-bpf programs +programmatically. + +### 1: Define a Go library returning a set of programs to precompile + +You need to define a `go_library` target which declares a package-level +function: + +```go +func PrecompiledPrograms() ([]precompiledseccomp.Program, error) +``` + +Look at [example.go](example/example.go) for a documented example. + +### 2: Call `precompiled_seccomp_rules` + +The [`precompiled_seccomp_rules`](defs.bzl) BUILD macro will auto-generate a +`.go` file which contains the precompiled seccomp-bpf binary that your first Go +library specifies. + +Look at [example/usage/BUILD](example/usage/BUILD) for an example. + +### 3: Use the generated library to access embedded programs + +Use the auto-generated `.go` file from step 2 in the second `go_library`. A new +package-level function will be defined: + +```go +func GetPrecompiled(programName string) (precompiledseccomp.Program, bool) +``` + +You can call it to get the precompiled seccomp-bpf program. + +See [example/usage/usage.go](example/usage/usage.go) for a documented example. + +## How does it work? + +See the [`precompiled_seccomp_rules`](defs.bzl) BUILD macro for the precise +logic. At a high level, it generates a `go_binary` target that imports your +first `go_library` (expressing your desired seccomp-bpf program). When this Go +binary runs, it will compile the programs and output Go code that contains the +compiled programs. Lastly, we use a `genrule` to execute this generated program +and direct its output to a file of your choosing, which you can now embed in +your second `go_library`. + +In order to support variables, the compilation step actually compiles the +program twice, using different placeholder values for all variables. It looks at +the places in the BPF bytecode where these values show up, and ensures that +these offsets are consistent across both compilation attempts. diff --git a/pkg/seccomp/precompiledseccomp/defs.bzl b/pkg/seccomp/precompiledseccomp/defs.bzl new file mode 100644 index 000000000..70ed8d72f --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/defs.bzl @@ -0,0 +1,89 @@ +"""Macro for precompiling seccomp-bpf programs.""" + +load("//tools:defs.bzl", "go_binary") + +def precompiled_seccomp_rules( + name, + programs_to_compile_go_library, + programs_to_compile_go_import, + out, + out_package_name): + """Generates a Go source file containing precompiled seccomp-bpf programs. + + Args: + name: Name of the final genrule. + programs_to_compile_go_library: go_library target which describes the + set of seccomp-bpf programs that you wish to precompile. This must + define the following package-level function: + func PrecompiledPrograms() ([]precompiledseccomp.Program, error) + programs_to_compile_go_import: Go-style import path to + `programs_to_compile_go_library`. + out: Name of the Go source file (with the precompiled seccomp-bpf + programs embedded in it) to generate. You can add this file as + source to a `go_library` rule. This will define a package-level + function: + GetPrecompiled(programName string) (precompiledseccomp.Program, bool) + out_package_name: Go package name that `out` belongs to. + """ + + # This genrule copies precompiled_lib.tmpl.go to the directory of wherever + # `precompiled_seccomp_rules` is called. + # This allows the go:embed directive inside the `.gen.go` file below to + # work without rewriting the full path. + native.genrule( + name = name + "_gen_lib", + outs = [out + ".gen.lib.tmpl.go"], + cmd = "cat < $(SRCS) > $@", + srcs = [ + "//pkg/seccomp/precompiledseccomp:precompiled_lib.tmpl.go", + ], + ) + + # This genrule generates the Go file of the binary that, when run, + # precompiles rules and writes them to a designated file. + native.genrule( + name = name + "_gen", + outs = [out + ".gen.go"], + cmd = ( + " while IFS= read -r line; do" + + " if echo \"$$line\" | grep -q 'REPLACED_IMPORT_THIS_IS_A_LOAD_BEARING_COMMENT'; then" + + " echo -e \"\\\\trules \\\"" + programs_to_compile_go_import + "\\\"\";" + + " elif echo \"$$line\" | grep -q 'PROGRAMS_FUNC_THIS_IS_A_LOAD_BEARING_COMMENT'; then" + + " echo -e \"var loadProgramsFn = rules.PrecompiledPrograms\";" + + " elif echo \"$$line\" | grep -q 'go:embed precompiled_lib.tmpl.go'; then" + + " echo -e \"//go:embed " + out + ".gen.lib.tmpl.go\";" + + " else" + + " echo \"$$line\";" + + " fi;" + + " done" + + " < $(SRCS)" + + " > $@" + ), + srcs = [ + "//pkg/seccomp/precompiledseccomp:precompile_gen.go", + ], + ) + + # This defines the go_binary for the Go file we just generated. + go_binary( + name = name + "_gen_bin", + srcs = [out + ".gen.go"], + deps = [ + programs_to_compile_go_library, + "//runsc/flag", + ], + embedsrcs = [ + ":" + out + ".gen.lib.tmpl.go", + ], + ) + + # This genrule actually runs the go_binary we just declared, and writes + # its output (containing the precompiled rules) to the desired `out` file. + native.genrule( + name = name, + outs = [out], + cmd = ( + "$(location :" + name + "_gen_bin) --package='" + out_package_name + "' --out=$@" + ), + tools = [":" + name + "_gen_bin"], + ) diff --git a/pkg/seccomp/precompiledseccomp/example/BUILD b/pkg/seccomp/precompiledseccomp/example/BUILD new file mode 100644 index 000000000..d20a65252 --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/example/BUILD @@ -0,0 +1,20 @@ +load("//tools:defs.bzl", "go_library") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_library( + name = "example", + srcs = ["example.go"], + visibility = [ + "//pkg/seccomp/precompiledseccomp/example:__subpackages__", + ], + deps = [ + "//pkg/abi/linux", + "//pkg/seccomp", + "//pkg/seccomp/precompiledseccomp", + "@org_golang_x_sys//unix:go_default_library", + ], +) diff --git a/pkg/seccomp/precompiledseccomp/example/example.go b/pkg/seccomp/precompiledseccomp/example/example.go new file mode 100644 index 000000000..20644a60c --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/example/example.go @@ -0,0 +1,103 @@ +// Copyright 2023 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 example defines two seccomp programs ("example_program1" and +// "example_program2") to be embedded in the `usage` package in this +// directory. +package example + +import ( + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp" +) + +// Variable names used in the precompiled programs. +// In this example, we have two file descriptors, which fit in 32 bits. +// If you need a 64-bit variable, simply declare two 32-bit variables and +// concatenate them to a single 64-bit number in the function that +// generates the `ProgramDesc`. +const ( + FD1 = "fd1" + FD2 = "fd2" +) + +// Name of the example programs. +const ( + // Program1Name is the name of the first example program. + // It allows reading from `FD1` and `FD2`, but writing only to `FD1`. + Program1Name = "example_program1" + + // Program2Name is the name of the second example program. + // It allows reading from `FD1` and `FD2`, but writing only to `FD2`. + Program2Name = "example_program2" +) + +// Program1 returns a program that allows reading from FDs `FD1` and `FD2`, +// but writing only to FD `FD1`. +func Program1(values precompiledseccomp.Values) precompiledseccomp.ProgramDesc { + return precompiledseccomp.ProgramDesc{ + Rules: []seccomp.RuleSet{{ + Rules: seccomp.NewSyscallRules().Add( + unix.SYS_READ, + seccomp.Or{ + seccomp.PerArg{seccomp.EqualTo(values[FD1])}, + seccomp.PerArg{seccomp.EqualTo(values[FD2])}, + }, + ).Add( + unix.SYS_WRITE, + seccomp.PerArg{seccomp.EqualTo(values[FD1])}, + ), + Action: linux.SECCOMP_RET_ALLOW, + }}, + SeccompOptions: seccomp.DefaultProgramOptions(), + } +} + +// Program2 returns a program that allows reading from FDs `FD1` and `FD2`, +// but writing only to FD `FD2`. +func Program2(values precompiledseccomp.Values) precompiledseccomp.ProgramDesc { + return precompiledseccomp.ProgramDesc{ + Rules: []seccomp.RuleSet{{ + Rules: seccomp.NewSyscallRules().Add( + unix.SYS_READ, + seccomp.Or{ + seccomp.PerArg{seccomp.EqualTo(values[FD1])}, + seccomp.PerArg{seccomp.EqualTo(values[FD2])}, + }, + ).Add( + unix.SYS_WRITE, + seccomp.PerArg{seccomp.EqualTo(values[FD2])}, + ), + Action: linux.SECCOMP_RET_ALLOW, + }}, + SeccompOptions: seccomp.DefaultProgramOptions(), + } +} + +// PrecompiledPrograms defines the seccomp-bpf programs to precompile. +// This function is called by the generated `go_binary` rule. +func PrecompiledPrograms() ([]precompiledseccomp.Program, error) { + vars := []string{FD1, FD2} + example1, err := precompiledseccomp.Precompile(Program1Name, vars, Program1) + if err != nil { + return nil, err + } + example2, err := precompiledseccomp.Precompile(Program2Name, vars, Program2) + if err != nil { + return nil, err + } + return []precompiledseccomp.Program{example1, example2}, nil +} diff --git a/pkg/seccomp/precompiledseccomp/example/usage/BUILD b/pkg/seccomp/precompiledseccomp/example/usage/BUILD new file mode 100644 index 000000000..81332a0ab --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/example/usage/BUILD @@ -0,0 +1,41 @@ +load("//pkg/seccomp/precompiledseccomp:defs.bzl", "precompiled_seccomp_rules") +load("//tools:defs.bzl", "go_library", "go_test") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +precompiled_seccomp_rules( + name = "example_precompiled", + out = "usage_embedded.go", + out_package_name = "usage", + programs_to_compile_go_import = "gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp/example", + programs_to_compile_go_library = "//pkg/seccomp/precompiledseccomp/example", +) + +go_library( + name = "usage", + srcs = [ + "usage.go", + "usage_embedded.go", + ], + deps = [ + "//pkg/bpf", + "//pkg/seccomp/precompiledseccomp", + "//pkg/seccomp/precompiledseccomp/example", + "//pkg/sync", + ], +) + +go_test( + name = "usage_test", + srcs = ["usage_test.go"], + library = ":usage", + deps = [ + "//pkg/bpf", + "//pkg/seccomp", + "//pkg/seccomp/precompiledseccomp", + "//pkg/seccomp/precompiledseccomp/example", + ], +) diff --git a/pkg/seccomp/precompiledseccomp/example/usage/usage.go b/pkg/seccomp/precompiledseccomp/example/usage/usage.go new file mode 100644 index 000000000..0851c2c4c --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/example/usage/usage.go @@ -0,0 +1,56 @@ +// Copyright 2023 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 usage shows how to use precompiled seccomp-bpf programs. +package usage + +import ( + "fmt" + + "gvisor.dev/gvisor/pkg/bpf" + "gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp" + "gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp/example" +) + +// LoadProgram1 loads the program1 program with the given FDs. +func LoadProgram1(fd1, fd2 uint32) []bpf.Instruction { + program, ok := GetPrecompiled(example.Program1Name) + if !ok { + panic(fmt.Sprintf("precompiled program %q not found", example.Program1Name)) + } + insns, err := program.RenderInstructions(precompiledseccomp.Values{ + example.FD1: fd1, + example.FD2: fd2, + }) + if err != nil { + panic(fmt.Errorf("failed to render instructions of precompiled program %q: %v", example.Program1Name, err)) + } + return insns +} + +// LoadProgram2 loads the program2 program with the given FDs. +func LoadProgram2(fd1, fd2 uint32) []bpf.Instruction { + program, ok := GetPrecompiled(example.Program2Name) + if !ok { + panic(fmt.Sprintf("precompiled program %q not found", example.Program2Name)) + } + insns, err := program.RenderInstructions(precompiledseccomp.Values{ + example.FD1: fd1, + example.FD2: fd2, + }) + if err != nil { + panic(fmt.Errorf("failed to render instructions of precompiled program %q: %v", example.Program2Name, err)) + } + return insns +} diff --git a/pkg/seccomp/precompiledseccomp/example/usage/usage_test.go b/pkg/seccomp/precompiledseccomp/example/usage/usage_test.go new file mode 100644 index 000000000..deee57830 --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/example/usage/usage_test.go @@ -0,0 +1,93 @@ +// Copyright 2023 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 usage + +import ( + "math/rand" + "reflect" + "testing" + + "gvisor.dev/gvisor/pkg/bpf" + "gvisor.dev/gvisor/pkg/seccomp" + "gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp" + "gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp/example" +) + +// comparePrograms verifies that the precompiled and freshly-compiled programs +// match byte-for-byte. If not, it prints them side-by-side. +func comparePrograms(t *testing.T, precompiled, freshlyCompiled []bpf.Instruction) { + t.Helper() + if !reflect.DeepEqual(precompiled, freshlyCompiled) { + t.Error("Precompiled and freshly-compiled versions of the program do not match:") + t.Errorf(" Offset | %-32s | %-32s", "Freshly-compiled", "Compiled") + for i := 0; i < max(len(precompiled), len(freshlyCompiled)); i++ { + switch { + case i < len(precompiled) && i < len(freshlyCompiled): + if reflect.DeepEqual(precompiled[i], freshlyCompiled[i]) { + t.Errorf(" OK %04d | %-32s | %-32s", i, freshlyCompiled[i].String(), precompiled[i].String()) + } else { + t.Errorf(" DIFF %04d | %-32s | %-32s", i, freshlyCompiled[i].String(), precompiled[i].String()) + } + case i < len(precompiled): + t.Errorf(" DIFF %04d | %-32s | %-32s", i, "(end)", precompiled[i].String()) + case i < len(freshlyCompiled): + t.Errorf(" DIFF %04d | %-32s | %-32s", i, freshlyCompiled[i].String(), "(end)") + } + } + } +} + +// TestProgram1 verifies that the precompiled version of the Program1 program +// matches a freshly-compiled version byte-for-byte. +func TestProgram1(t *testing.T) { + fd1 := rand.Uint32() + fd2 := fd1 + 1 + precompiled := LoadProgram1(fd1, fd2) + prog := example.Program1(precompiledseccomp.Values{ + example.FD1: fd1, + example.FD2: fd2, + }) + freshlyCompiled, _, err := seccomp.BuildProgram(prog.Rules, prog.SeccompOptions) + if err != nil { + t.Fatalf("cannot freshly compile the program: %v", err) + } + comparePrograms(t, precompiled, freshlyCompiled) +} + +// TestProgram2 verifies that the precompiled version of the Program2 program +// matches a freshly-compiled version byte-for-byte. +func TestProgram2(t *testing.T) { + fd1 := rand.Uint32() + fd2 := fd1 + 1 + precompiled := LoadProgram2(fd1, fd2) + prog := example.Program2(precompiledseccomp.Values{ + example.FD1: fd1, + example.FD2: fd2, + }) + freshlyCompiled, _, err := seccomp.BuildProgram(prog.Rules, prog.SeccompOptions) + if err != nil { + t.Fatalf("cannot freshly compile the program: %v", err) + } + comparePrograms(t, precompiled, freshlyCompiled) +} + +// TestNonExistentProgram verifies that invalid program names don't exist. +func TestNonExistentProgram(t *testing.T) { + const nonExistentProgram = "this program name does not exist" + got, found := GetPrecompiled(nonExistentProgram) + if found { + t.Fatalf("unexpectedly found program named %q: %v", nonExistentProgram, got) + } +} diff --git a/pkg/seccomp/precompiledseccomp/precompile_gen.go b/pkg/seccomp/precompiledseccomp/precompile_gen.go new file mode 100644 index 000000000..eab2ca030 --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/precompile_gen.go @@ -0,0 +1,114 @@ +// Copyright 2023 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. + +// precompile_gen generates a Go library that contains precompiled seccomp +// programs. +package main + +import ( + _ "embed" + "fmt" + "os" + "sort" + "strings" + + "gvisor.dev/gvisor/runsc/flag" + + // This import will be replaced by the one specified in the genrule. + "gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp/example" // REPLACED_IMPORT_THIS_IS_A_LOAD_BEARING_COMMENT +) + +//go:embed precompiled_lib.tmpl.go +var precompiledLibTemplate []byte + +// Constants referring to how things are named in precompiled_lib.tmpl.go. +const ( + packageNameStandin = "precompiled" + precompiledseccompPackageName = "precompiledseccomp" + registrationComment = "PROGRAM_REGISTRATION_GOES_HERE_THIS_IS_A_LOAD_BEARING_COMMENT" + programsMapVarName = "programs" +) + +// Flags. +var ( + output = flag.String("out", "/dev/stdout", "output file") + packageName = flag.String("package", "", "output package name") +) + +// loadProgramsFn loads seccomp programs to be precompiled. +var loadProgramsFn = example.PrecompiledPrograms // PROGRAMS_FUNC_THIS_IS_A_LOAD_BEARING_COMMENT + +func main() { + flag.Parse() + + // Get a sorted list of programs. + programs, err := loadProgramsFn() + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot get list of programs to precompile: %v\n", err) + os.Exit(1) + } + programNames := make(map[string]struct{}, len(programs)) + for _, program := range programs { + if _, alreadySeen := programNames[program.Name]; alreadySeen { + fmt.Fprintf(os.Stderr, "duplicate program name %q", program.Name) + os.Exit(1) + } + programNames[program.Name] = struct{}{} + } + sort.Slice(programs, func(i, j int) bool { + return programs[i].Name < programs[j].Name + }) + + // Open the output file. + outFile, err := os.Create(*output) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot open output file %q: %v\n", *output, err) + os.Exit(1) + } + defer outFile.Close() + + // Write Go code to the output file. + processedPackageComment := false + packageStandinLine := fmt.Sprintf("package %s", packageNameStandin) + packageCommentPrefix := fmt.Sprintf("// Package %s ", packageNameStandin) + lines := strings.Split(string(precompiledLibTemplate), "\n") + for i := 0; i < len(lines); i++ { + line := lines[i] + switch { + case line == packageStandinLine: + fmt.Fprintf(outFile, "package %s\n", *packageName) + case !processedPackageComment && strings.HasPrefix(line, packageCommentPrefix): + // Do not output package comment, as this would conflict with + // other package comments from other files in the same package. + // Skip over all the next lines until we get to the "package" line. + for ; i+1 < len(lines) && !strings.HasPrefix(lines[i+1], "package "); i++ { + } + processedPackageComment = true + case strings.Contains(line, registrationComment): + var indent string + for { + var found bool + if line, found = strings.CutPrefix(line, "\t"); !found { + break + } + indent += "\t" + } + for _, program := range programs { + fmt.Fprint(outFile, program.Registration(indent, precompiledseccompPackageName, programsMapVarName)) + } + default: + fmt.Fprintf(outFile, "%s\n", line) + } + } +} diff --git a/pkg/seccomp/precompiledseccomp/precompiled_lib.tmpl.go b/pkg/seccomp/precompiledseccomp/precompiled_lib.tmpl.go new file mode 100644 index 000000000..9be4bff27 --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/precompiled_lib.tmpl.go @@ -0,0 +1,47 @@ +// Copyright 2023 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 precompiled does not exist. This file is used in a go:embed +// directive inside `precompile_gen.go`. +package precompiled + +import ( + "gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp" + "gvisor.dev/gvisor/pkg/sync" +) + +var ( + // precompiledPrograms holds registered programs. + // It is populated in `registerPrograms`. + precompiledPrograms map[string]precompiledseccomp.Program = nil + + // registerPrecompiledProgramsOnce ensures that program registration + // happens only once. + registerPrecompiledProgramsOnce sync.Once +) + +// GetPrecompiled returns the precompiled program for the given name, +// and whether that program name exists. +func GetPrecompiled(programName string) (precompiledseccomp.Program, bool) { + registerPrecompiledProgramsOnce.Do(registerPrograms) + program, ok := precompiledPrograms[programName] + return program, ok +} + +// registerPrograms registers available programs inside `precompiledPrograms`. +func registerPrograms() { + programs := make(map[string]precompiledseccomp.Program) + // PROGRAM_REGISTRATION_GOES_HERE_THIS_IS_A_LOAD_BEARING_COMMENT + precompiledPrograms = programs +} diff --git a/pkg/seccomp/precompiledseccomp/precompiledseccomp.go b/pkg/seccomp/precompiledseccomp/precompiledseccomp.go new file mode 100644 index 000000000..89f3498f0 --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/precompiledseccomp.go @@ -0,0 +1,269 @@ +// Copyright 2023 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 precompiledseccomp provides tooling to precompile seccomp-bpf +// programs that can be embedded inside Go source code. +package precompiledseccomp + +import ( + "encoding/binary" + "fmt" + "sort" + "strings" + + "gvisor.dev/gvisor/pkg/bpf" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/seccomp" +) + +// ProgramDesc describes a program to be compiled. +type ProgramDesc struct { + // Rules contains the seccomp-bpf rulesets to compile. + Rules []seccomp.RuleSet + + // SeccompOptions is the seccomp-bpf program options used in compilation. + SeccompOptions seccomp.ProgramOptions +} + +// Program is a precompiled seccomp-bpf program. +// To get actual BPF instructions, call the `RenderInstructions` function. +type Program struct { + // Name is the name of this program within a set of embedded programs. + Name string + + // Bytecode32 is the raw BPF bytecode represented as a sequence of uint32s. + Bytecode32 []uint32 + + // VarOffsets maps variable names to the uint32-based offsets where these + // variables show up in `Bytecode32`. + VarOffsets map[string][]int +} + +// Values is an assignment of variables to uint32 values. +// It is used when rendering seccomp-bpf program instructions. +type Values map[string]uint32 + +// Precompile compiles a `ProgramDesc` with the given values. +// +// It supports the notion of "variables", which are named in `vars`. +// Variables are uint32s which are only known at runtime, and whose value +// shows up in the BPF bytecode. +// +// `fn` takes in a mapping of variable names to their assigned values, +// and should return a `ProgramDesc` describing the seccomp-bpf program +// to be compiled. +// +// Precompile verifies that all variables in `vars` show up consistently in +// the bytecode by compiling the program twice, ensures that the offsets at +// which some stand-in values is consistent across these two compilation +// attempts, and that nothing else about the BPF bytecode is different. +func Precompile(name string, varNames []string, fn func(Values) ProgramDesc) (Program, error) { + vars := make(map[string]struct{}, len(varNames)) + for _, varName := range varNames { + vars[varName] = struct{}{} + } + if len(vars) != len(varNames) { + return Program{}, fmt.Errorf("non-unique variable names: %q", varNames) + } + + // These constants are chosen to be recognizable and unique within + // seccomp-bpf programs. + // These could of course show up in seccomp-bpf programs for legitimate + // reasons other than being part the variable being matched against (e.g. a + // jump of this many instructions forward, or a static equality match that + // happens to check against this exact value), but it is very unlikely that + // integers this large actually occur. + // If it does happen, we'll catch it here because one compilation attempt + // will find its placeholder values show up less often than the other. + // Assuming that the reason this occurred is legitimate, update these + // constants to even-less-likely values in order to fix this issue. + const ( + varStart1 uint32 = 0x13371337 + varStart2 uint32 = 0x42424243 + ) + + // Render the program with one set of values. + // Remember at which offsets we saw these values show up in the bytecode. + values1 := Values(make(map[string]uint32, len(vars))) + v := varStart1 + for varName := range vars { + values1[varName] = v + v += 2 + } + program1, err := precompile(name, values1, fn) + if err != nil { + return Program{}, err + } + + // Do the same, but with a different set of values. + values2 := Values(make(map[string]uint32, len(vars))) + v = varStart2 + for _, varName := range varNames { + values2[varName] = v + v += 2 + } + program2, err := precompile(name, values2, fn) + if err != nil { + return Program{}, err + } + + // Ensure that the offsets we got is consistent. + for _, varName := range varNames { + offsets1 := program1.VarOffsets[varName] + offsets2 := program2.VarOffsets[varName] + if len(offsets1) != len(offsets2) { + return Program{}, fmt.Errorf("var %q has different number of offsets depending on its value: with value 0x%08x it showed up %d times, but with value %d it showed up %d times", varName, values1[varName], len(offsets1), values2[varName], len(offsets2)) + } + for i := 0; i < len(offsets1); i++ { + if offsets1[i] != offsets2[i] { + return Program{}, fmt.Errorf("var %q has different offsets depending on its value: with value 0x%08x it showed up at offsets %v, but with value %d it showed up at offsets %v", varName, values1[varName], offsets1, values2[varName], offsets2) + } + } + } + + // Ensure that the rest of the bytecode is exactly equal. + if len(program1.Bytecode32) != len(program2.Bytecode32) { + return Program{}, fmt.Errorf("compiled programs do not have the same bytecode size: %d vs %d", len(program1.Bytecode32), len(program2.Bytecode32)) + } + knownOffsets := map[int]struct{}{} + for _, varName := range varNames { + for _, offset := range program1.VarOffsets[varName] { + knownOffsets[offset] = struct{}{} + } + } + for i := 0; i < len(program1.Bytecode32); i++ { + if _, isVarOffset := knownOffsets[i]; isVarOffset { + continue + } + if program1.Bytecode32[i] != program2.Bytecode32[i] { + return Program{}, fmt.Errorf("compiled programs do not have the same bytecode at uint32 offset %d (which is not any of the offsets where a variable shows up: %v)", i, knownOffsets) + } + } + + return program1, nil +} + +// precompile compiles a `ProgramDesc` with the given values. +func precompile(name string, values Values, fn func(Values) ProgramDesc) (Program, error) { + precompileOpts := fn(values) + insns, _, err := seccomp.BuildProgram(precompileOpts.Rules, precompileOpts.SeccompOptions) + if err != nil { + return Program{}, err + } + if log.IsLogging(log.Debug) { + log.Debugf("Compiled program with values %v (%d instructions):", values, len(insns)) + for i, insn := range insns { + log.Debugf(" %04d: %s\n", i, insn.String()) + } + } + bytecode32 := instructionsToUint32Slice(insns) + varOffsets := make(map[string][]int, len(values)) + for varName, value := range values { + for i, v := range bytecode32 { + if v == value { + varOffsets[varName] = append(varOffsets[varName], i) + } + } + } + for varName := range values { + if len(varOffsets[varName]) == 0 { + return Program{}, fmt.Errorf("var %q does not show up in the BPF bytecode", varName) + } + } + return Program{ + Name: name, + Bytecode32: bytecode32, + VarOffsets: varOffsets, + }, nil +} + +// Expr renders a Go expression encoding this `Program`. +// It is used when embedding a precompiled `Program` into a Go library file. +// `pkgName` is the package name under which the precompiledseccomp package is +// imported. +func (program Program) Expr(indentPrefix, pkgName string) string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("%s.Program{\n", pkgName)) + sb.WriteString(fmt.Sprintf("%s\tName: %q,\n", indentPrefix, program.Name)) + sb.WriteString(fmt.Sprintf("%s\tBytecode32: []uint32{\n", indentPrefix)) + for _, v := range program.Bytecode32 { + sb.WriteString(fmt.Sprintf("%s\t\t0x%08x,\n", indentPrefix, v)) + } + sb.WriteString(fmt.Sprintf("%s\t},\n", indentPrefix)) + sb.WriteString(fmt.Sprintf("%s\tVarOffsets: map[string][]int{\n", indentPrefix)) + varNames := make([]string, 0, len(program.VarOffsets)) + for varName := range program.VarOffsets { + varNames = append(varNames, varName) + } + sort.Strings(varNames) + for _, varName := range varNames { + sb.WriteString(fmt.Sprintf("%s\t\t%q: []int{\n", indentPrefix, varName)) + for _, v := range program.VarOffsets[varName] { + sb.WriteString(fmt.Sprintf("%s\t\t\t%d,\n", indentPrefix, v)) + } + sb.WriteString(fmt.Sprintf("%s\t\t},\n", indentPrefix)) + } + sb.WriteString(fmt.Sprintf("%s\t},\n", indentPrefix)) + sb.WriteString(fmt.Sprintf("%s}", indentPrefix)) + return sb.String() +} + +// RenderInstructions builds the set of precompiled BPF instructions, +// replacing the variables with their values as given in `values`. +// This must be called with the exact same set of variable names as was used +// during `Precompile`. +func (program Program) RenderInstructions(values Values) ([]bpf.Instruction, error) { + if len(values) != len(program.VarOffsets) { + return nil, fmt.Errorf("called with inconsistent vars: got %v expected %v", values, program.VarOffsets) + } + for varName, value := range values { + offsets, found := program.VarOffsets[varName] + if !found { + return nil, fmt.Errorf("var %q was not defined in precompiled instructions (defined: %v)", varName, program.VarOffsets) + } + for _, offset := range offsets { + program.Bytecode32[offset] = value + } + } + return uint32SliceToInstructions(program.Bytecode32) +} + +// instructionsToUint32Slice converts a slice of BPF instructions into a slice +// of uint32s containing the same binary data. +func instructionsToUint32Slice(insns []bpf.Instruction) []uint32 { + bytecode := bpf.ToBytecode(insns) + bytecode32 := make([]uint32, len(bytecode)/4) + for i := 0; i < len(bytecode); i += 4 { + bytecode32[i/4] = binary.NativeEndian.Uint32(bytecode[i : i+4]) + } + return bytecode32 +} + +// uint32SliceToInstructions converts a slice of uint32s into a slice of +// BPF instructions containing the same binary data. +func uint32SliceToInstructions(bytecode32 []uint32) ([]bpf.Instruction, error) { + bytecode := make([]byte, len(bytecode32)*4) + for i, v := range bytecode32 { + binary.NativeEndian.PutUint32(bytecode[i*4:], v) + } + return bpf.ParseBytecode(bytecode) +} + +// Registration outputs Go code that registers this programs in a +// `map[string]Program` variable named `programsMapVarName` which maps +// programs names to their `Program` struct. +// It is used when embedding precompiled programs into a Go library file. +func (program Program) Registration(indentPrefix, pkgName, programsMapVarName string) string { + return fmt.Sprintf("%s%s[%q] = %s\n", indentPrefix, programsMapVarName, program.Name, program.Expr(indentPrefix, pkgName)) +} diff --git a/pkg/seccomp/precompiledseccomp/precompiledseccomp_test.go b/pkg/seccomp/precompiledseccomp/precompiledseccomp_test.go new file mode 100644 index 000000000..442565c8f --- /dev/null +++ b/pkg/seccomp/precompiledseccomp/precompiledseccomp_test.go @@ -0,0 +1,203 @@ +// Copyright 2023 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 precompiledseccomp + +import ( + "testing" + + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/seccomp" +) + +// TestPrecompile verifies that precompilation works and verifies that variable +// offsets are verified across compilation attempts. +func TestPrecompile(t *testing.T) { + // Used in some tests below that need statefulness in order to return + // purposefully-inconsistent results across calls. + counter := 0 + + for _, test := range []struct { + name string + vars []string + fn func(Values) ProgramDesc + wantErr bool + }{ + { + name: "simple case", + fn: func(Values) ProgramDesc { + return ProgramDesc{ + Rules: []seccomp.RuleSet{{ + Rules: seccomp.NewSyscallRules().Add( + unix.SYS_READ, + seccomp.MatchAll{}, + ), + Action: linux.SECCOMP_RET_ALLOW, + }}, + SeccompOptions: seccomp.DefaultProgramOptions(), + } + }, + }, + { + name: "one variable", + vars: []string{"var1"}, + fn: func(values Values) ProgramDesc { + return ProgramDesc{ + Rules: []seccomp.RuleSet{{ + Rules: seccomp.NewSyscallRules().Add( + unix.SYS_READ, + seccomp.PerArg{ + seccomp.EqualTo(values["var1"]), + }, + ), + Action: linux.SECCOMP_RET_ALLOW, + }}, + SeccompOptions: seccomp.DefaultProgramOptions(), + } + }, + }, + { + name: "duplicate variable name", + vars: []string{"var1", "var1"}, + fn: func(values Values) ProgramDesc { + return ProgramDesc{} + }, + wantErr: true, + }, + { + name: "multiple variables showing up multiple times", + vars: []string{"var1", "var2"}, + fn: func(values Values) ProgramDesc { + return ProgramDesc{ + Rules: []seccomp.RuleSet{{ + Rules: seccomp.NewSyscallRules().Add( + unix.SYS_READ, + seccomp.Or{ + seccomp.PerArg{seccomp.EqualTo(values["var1"])}, + seccomp.PerArg{seccomp.EqualTo(values["var2"])}, + }, + ).Add( + unix.SYS_WRITE, + seccomp.PerArg{seccomp.EqualTo(values["var1"])}, + ), + Action: linux.SECCOMP_RET_ALLOW, + }}, + SeccompOptions: seccomp.DefaultProgramOptions(), + } + }, + }, + { + name: "unused variable", + vars: []string{"var1"}, + fn: func(values Values) ProgramDesc { + return ProgramDesc{ + Rules: []seccomp.RuleSet{{ + Rules: seccomp.NewSyscallRules().Add( + unix.SYS_READ, + seccomp.MatchAll{}, + ), + Action: linux.SECCOMP_RET_ALLOW, + }}, + SeccompOptions: seccomp.DefaultProgramOptions(), + } + }, + wantErr: true, + }, + { + name: "inconsistent offsets", + vars: []string{"var1"}, + fn: func(values Values) ProgramDesc { + var pa seccomp.PerArg + if counter == 0 { + pa[0] = seccomp.EqualTo(values["var1"]) + } + if counter == 1 { + pa[0] = seccomp.EqualTo(values["var1"]) + pa[1] = seccomp.EqualTo(values["var1"]) + } + counter++ + return ProgramDesc{ + Rules: []seccomp.RuleSet{{ + Rules: seccomp.NewSyscallRules().Add( + unix.SYS_READ, + pa, + ), + Action: linux.SECCOMP_RET_ALLOW, + }}, + SeccompOptions: seccomp.DefaultProgramOptions(), + } + }, + wantErr: true, + }, + { + name: "inconsistent program size", + vars: []string{"var1"}, + fn: func(values Values) ProgramDesc { + pa := seccomp.PerArg{seccomp.EqualTo(values["var1"])} + if counter == 1 { + pa[1] = seccomp.EqualTo(123) + } + counter++ + return ProgramDesc{ + Rules: []seccomp.RuleSet{{ + Rules: seccomp.NewSyscallRules().Add( + unix.SYS_READ, + pa, + ), + Action: linux.SECCOMP_RET_ALLOW, + }}, + SeccompOptions: seccomp.DefaultProgramOptions(), + } + }, + wantErr: true, + }, + { + name: "inconsistent program bytecode", + vars: []string{"var1"}, + fn: func(values Values) ProgramDesc { + pa := seccomp.PerArg{seccomp.EqualTo(values["var1"])} + if counter == 0 { + pa[1] = seccomp.EqualTo(1337) + } + if counter == 1 { + pa[1] = seccomp.EqualTo(42) + } + counter++ + return ProgramDesc{ + Rules: []seccomp.RuleSet{{ + Rules: seccomp.NewSyscallRules().Add( + unix.SYS_READ, + pa, + ), + Action: linux.SECCOMP_RET_ALLOW, + }}, + SeccompOptions: seccomp.DefaultProgramOptions(), + } + }, + wantErr: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + counter = 0 + _, err := Precompile("", test.vars, test.fn) + if err != nil && !test.wantErr { + t.Fatalf("Precompile failed: %v", err) + } + if err == nil && test.wantErr { + t.Fatal("Precompile succeeded but want error") + } + }) + } +}