Add tooling to compile seccomp-bpf programs at bazel build time.

This adds a `precompiledseccomp` library which provides tooling to compile
`seccomp-bpf` programs and generate Go source code that contains the
resulting bytecode embedded into it. In turn, this bytecode can be used in
Go libraries.

This avoids spending time compiling and optimizing `seccomp-bpf` programs
at runsc container creation time.

This library also contains support for "variables", which are `uint32`s whose
values are part of the seccomp filters but only known at runtime. To support
this, the program is compiled twice with placeholder values for these
variables, and we verify that the offsets at which these values show up in the
bytecode is consistent across these two compilation attempts.

PiperOrigin-RevId: 583117683
This commit is contained in:
Etienne Perot
2023-11-16 12:00:44 -08:00
committed by gVisor bot
parent e60464fdfa
commit 0a3bced479
13 changed files with 1157 additions and 1 deletions
+8 -1
View File
@@ -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)
}
+52
View File
@@ -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",
],
)
+62
View File
@@ -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.
+89
View File
@@ -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"],
)
@@ -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",
],
)
@@ -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
}
@@ -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",
],
)
@@ -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
}
@@ -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)
}
}
@@ -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)
}
}
}
@@ -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
}
@@ -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))
}
@@ -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")
}
})
}
}