bpf: Do not use interfaces to define byte ordering in BPF evaluation.

This speeds up BPF evaluation by avoiding interface call overhead.

```
            │   initial   │         hey_look_no_interfaces         │         even_fewer_interfaces          │
            │   sec/op    │   sec/op     vs base                   │   sec/op     vs base                   │
Interpreter   26.31n ± 0%   21.87n ± 0%  -16.88% (p=0.000 n=21+20)   14.26n ± 0%  -45.80% (p=0.000 n=21+20)
```

PiperOrigin-RevId: 576591719
This commit is contained in:
Etienne Perot
2023-10-25 11:32:34 -07:00
committed by gVisor bot
parent ecbf37d037
commit 663e69bde4
9 changed files with 103 additions and 82 deletions
+56 -24
View File
@@ -22,47 +22,79 @@ import (
// documentation sometimes refers to the input data as the "packet" due to its
// origins as a packet processing DSL.)
// Unaligned loads are supported.
type Input struct {
// Data is the data accessed through the Input interface.
Data []byte
type Input []byte
// Order is the byte order the data is accessed with.
Order binary.ByteOrder
// These type definitions must have different GC shapes to ensure that
// the Go compiler generates distinct code paths for them.
// These do not have anything to do with the bit sizes of the loads
// later on; all that matters is that these types have distinct sizes
// from one another.
type (
// BigEndian uses big-endian byte ordering.
BigEndian uint8
// LittleEndian uses little-endian byte ordering.
LittleEndian uint16
// NativeEndian uses native byte ordering.
NativeEndian uint32
)
// Endianness represents a byte order.
type Endianness interface {
BigEndian | LittleEndian | NativeEndian
}
// Load32 implements Input.Load32.
// load32 loads a 32-bit value.
//
//go:nosplit
func (i *Input) Load32(off uint32) (uint32, bool) {
if uint64(off)+4 > uint64(len(i.Data)) {
func load32[endian Endianness](in Input, off uint32) (uint32, bool) {
if uint64(off)+4 > uint64(len(in)) {
return 0, false
}
return i.Order.Uint32(i.Data[int(off):]), true
// Casting to any is needed here to avoid a compilation error:
// https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#why-not-permit-type-assertions-on-values-whose-type-is-a-type-parameter
var e endian
switch any(e).(type) {
case BigEndian:
return binary.BigEndian.Uint32(in[int(off):]), true
case LittleEndian:
return binary.LittleEndian.Uint32(in[int(off):]), true
case NativeEndian:
return binary.NativeEndian.Uint32(in[int(off):]), true
default:
panic("unreachable")
}
}
// Load16 implements Input.Load16.
// load16 loads a 16-bit value.
//
//go:nosplit
func (i *Input) Load16(off uint32) (uint16, bool) {
if uint64(off)+2 > uint64(len(i.Data)) {
func load16[endian Endianness](in Input, off uint32) (uint16, bool) {
if uint64(off)+2 > uint64(len(in)) {
return 0, false
}
return i.Order.Uint16(i.Data[int(off):]), true
// Casting to any is needed here to avoid a compilation error:
// https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#why-not-permit-type-assertions-on-values-whose-type-is-a-type-parameter
var e endian
switch any(e).(type) {
case BigEndian:
return binary.BigEndian.Uint16(in[int(off):]), true
case LittleEndian:
return binary.LittleEndian.Uint16(in[int(off):]), true
case NativeEndian:
return binary.NativeEndian.Uint16(in[int(off):]), true
default:
panic("unreachable")
}
}
// Load8 implements Input.Load8.
// load8 loads a single byte.
//
//go:nosplit
func (i *Input) Load8(off uint32) (uint8, bool) {
if uint64(off)+1 > uint64(len(i.Data)) {
func load8(in Input, off uint32) (uint8, bool) {
if uint64(off)+1 > uint64(len(in)) {
return 0, false
}
return i.Data[int(off)], true
}
// Length implements Input.Length.
//
//go:nosplit
func (i *Input) Length() uint32 {
return uint32(len(i.Data))
return in[int(off)], true
}
+21 -21
View File
@@ -238,7 +238,7 @@ func conditionalJumpOffset(insn Instruction, cond bool) int {
// Exec executes a BPF program over the given input and returns its return
// value.
func Exec(p Program, in Input) (uint32, error) {
func Exec[endian Endianness](p Program, in Input) (uint32, error) {
var m machine
var pc int
for ; pc < len(p.instructions); pc++ {
@@ -247,37 +247,37 @@ func Exec(p Program, in Input) (uint32, error) {
case Ld | Imm | W:
m.A = i.K
case Ld | Abs | W:
val, ok := in.Load32(i.K)
val, ok := load32[endian](in, i.K)
if !ok {
return 0, Error{InvalidLoad, pc}
}
m.A = val
case Ld | Abs | H:
val, ok := in.Load16(i.K)
val, ok := load16[endian](in, i.K)
if !ok {
return 0, Error{InvalidLoad, pc}
}
m.A = uint32(val)
case Ld | Abs | B:
val, ok := in.Load8(i.K)
val, ok := load8(in, i.K)
if !ok {
return 0, Error{InvalidLoad, pc}
}
m.A = uint32(val)
case Ld | Ind | W:
val, ok := in.Load32(m.X + i.K)
val, ok := load32[endian](in, m.X+i.K)
if !ok {
return 0, Error{InvalidLoad, pc}
}
m.A = val
case Ld | Ind | H:
val, ok := in.Load16(m.X + i.K)
val, ok := load16[endian](in, m.X+i.K)
if !ok {
return 0, Error{InvalidLoad, pc}
}
m.A = uint32(val)
case Ld | Ind | B:
val, ok := in.Load8(m.X + i.K)
val, ok := load8(in, m.X+i.K)
if !ok {
return 0, Error{InvalidLoad, pc}
}
@@ -285,15 +285,15 @@ func Exec(p Program, in Input) (uint32, error) {
case Ld | Mem | W:
m.A = m.M[int(i.K)]
case Ld | Len | W:
m.A = in.Length()
m.A = uint32(len(in))
case Ldx | Imm | W:
m.X = i.K
case Ldx | Mem | W:
m.X = m.M[int(i.K)]
case Ldx | Len | W:
m.X = in.Length()
m.X = uint32(len(in))
case Ldx | Msh | B:
val, ok := in.Load8(i.K)
val, ok := load8(in, i.K)
if !ok {
return 0, Error{InvalidLoad, pc}
}
@@ -497,10 +497,10 @@ func (e *ExecutionMetrics) markInputRead(offset uint32, bytesRead int) {
// InstrumentedExec executes a BPF program over the given input while
// instrumenting it: recording memory accesses and lines executed.
// This is slower than Exec, but should return equivalent results.
func InstrumentedExec(p Program, in Input) (ExecutionMetrics, error) {
func InstrumentedExec[endian Endianness](p Program, in Input) (ExecutionMetrics, error) {
ret := ExecutionMetrics{
Coverage: make([]bool, len(p.instructions)),
InputAccessed: make([]bool, in.Length()),
InputAccessed: make([]bool, len(in)),
}
var m machine
var pc int
@@ -511,42 +511,42 @@ func InstrumentedExec(p Program, in Input) (ExecutionMetrics, error) {
case Ld | Imm | W:
m.A = i.K
case Ld | Abs | W:
val, ok := in.Load32(i.K)
val, ok := load32[endian](in, i.K)
if !ok {
return ret, Error{InvalidLoad, pc}
}
ret.markInputRead(i.K, 4)
m.A = val
case Ld | Abs | H:
val, ok := in.Load16(i.K)
val, ok := load16[endian](in, i.K)
if !ok {
return ret, Error{InvalidLoad, pc}
}
ret.markInputRead(i.K, 2)
m.A = uint32(val)
case Ld | Abs | B:
val, ok := in.Load8(i.K)
val, ok := load8(in, i.K)
if !ok {
return ret, Error{InvalidLoad, pc}
}
ret.markInputRead(i.K, 1)
m.A = uint32(val)
case Ld | Ind | W:
val, ok := in.Load32(m.X + i.K)
val, ok := load32[endian](in, m.X+i.K)
if !ok {
return ret, Error{InvalidLoad, pc}
}
ret.markInputRead(m.X+i.K, 4)
m.A = val
case Ld | Ind | H:
val, ok := in.Load16(m.X + i.K)
val, ok := load16[endian](in, m.X+i.K)
if !ok {
return ret, Error{InvalidLoad, pc}
}
ret.markInputRead(m.X+i.K, 2)
m.A = uint32(val)
case Ld | Ind | B:
val, ok := in.Load8(m.X + i.K)
val, ok := load8(in, m.X+i.K)
if !ok {
return ret, Error{InvalidLoad, pc}
}
@@ -555,15 +555,15 @@ func InstrumentedExec(p Program, in Input) (ExecutionMetrics, error) {
case Ld | Mem | W:
m.A = m.M[int(i.K)]
case Ld | Len | W:
m.A = in.Length()
m.A = uint32(len(in))
case Ldx | Imm | W:
m.X = i.K
case Ldx | Mem | W:
m.X = m.M[int(i.K)]
case Ldx | Len | W:
m.X = in.Length()
m.X = uint32(len(in))
case Ldx | Msh | B:
val, ok := in.Load8(i.K)
val, ok := load8(in, i.K)
if !ok {
return ret, Error{InvalidLoad, pc}
}
+16 -18
View File
@@ -15,7 +15,6 @@
package bpf
import (
"encoding/binary"
"reflect"
"testing"
@@ -153,12 +152,12 @@ func TestExecErrors(t *testing.T) {
if err != nil {
t.Fatalf("unexpected compilation error: %v", err)
}
inp := Input{nil, binary.BigEndian}
execution, err := InstrumentedExec(p, inp)
inp := Input{}
execution, err := InstrumentedExec[NativeEndian](p, inp)
if err != test.expectedErr {
t.Fatalf("expected execution error %q, got (%v, %v)", test.expectedErr, execution, err)
}
ret, err := Exec(p, inp)
ret, err := Exec[NativeEndian](p, inp)
if err != test.expectedErr {
t.Fatalf("expected execution error %q, got (%d, %v)", test.expectedErr, ret, err)
}
@@ -166,7 +165,7 @@ func TestExecErrors(t *testing.T) {
if err != nil {
t.Fatalf("unexpected compilation error: %v", err)
}
if _, err := InstrumentedExec(optimizedProgram, inp); err != test.expectedErr {
if _, err := InstrumentedExec[NativeEndian](optimizedProgram, inp); err != test.expectedErr {
t.Fatalf("expected execution error from optimized program %q, got (%v, %v)", test.expectedErr, execution, err)
}
})
@@ -200,7 +199,7 @@ func TestValidInstructions(t *testing.T) {
insns []Instruction
// input is the input data. Note that input will be read as big-endian.
input []byte
input Input
// expected is the expected result of executing the BPF program.
// It takes in the instructions and input that the test will run.
@@ -251,7 +250,7 @@ func TestValidInstructions(t *testing.T) {
expected: want(ExecutionMetrics{
Coverage: []bool{true, true},
InputAccessed: []bool{false, true, true, true, true, false},
ReturnValue: 0x11223344,
ReturnValue: hostarch.ByteOrder.Uint32([]byte{0x11, 0x22, 0x33, 0x44}),
}),
},
{
@@ -264,7 +263,7 @@ func TestValidInstructions(t *testing.T) {
expected: want(ExecutionMetrics{
Coverage: []bool{true, true},
InputAccessed: []bool{false, true, true, false},
ReturnValue: 0x1122,
ReturnValue: uint32(hostarch.ByteOrder.Uint16([]byte{0x11, 0x22})),
}),
},
{
@@ -291,7 +290,7 @@ func TestValidInstructions(t *testing.T) {
expected: want(ExecutionMetrics{
Coverage: []bool{true, true, true},
InputAccessed: []bool{false, false, true, true, true, true, false},
ReturnValue: 0x22334455,
ReturnValue: hostarch.ByteOrder.Uint32([]byte{0x22, 0x33, 0x44, 0x55}),
}),
},
{
@@ -305,7 +304,7 @@ func TestValidInstructions(t *testing.T) {
expected: want(ExecutionMetrics{
Coverage: []bool{true, true, true},
InputAccessed: []bool{false, false, true, true, false},
ReturnValue: 0x2233,
ReturnValue: uint32(hostarch.ByteOrder.Uint16([]byte{0x22, 0x33})),
}),
},
{
@@ -862,15 +861,14 @@ func TestValidInstructions(t *testing.T) {
t.Fatalf("unexpected compilation error: %v", err)
}
want := test.expected(test.insns, test.input)
inp := Input{test.input, binary.BigEndian}
execution, err := InstrumentedExec(p, inp)
execution, err := InstrumentedExec[NativeEndian](p, test.input)
if err != nil {
t.Fatalf("unexpected execution error: %v", err)
}
if !reflect.DeepEqual(execution, want) {
t.Fatalf("expected %s, got %s", want.String(), execution.String())
}
retFast, err := Exec(p, inp)
retFast, err := Exec[NativeEndian](p, test.input)
if err != nil {
t.Fatalf("unexpected execution error during fast execution: %v", err)
}
@@ -881,7 +879,7 @@ func TestValidInstructions(t *testing.T) {
if err != nil {
t.Fatalf("unexpected compilation error: %v", err)
}
retOptimized, err := InstrumentedExec(optimizedProgram, inp)
retOptimized, err := InstrumentedExec[NativeEndian](optimizedProgram, test.input)
if err != nil {
t.Fatalf("unexpected execution error: %v", err)
}
@@ -1031,7 +1029,7 @@ func TestSimpleFilter(t *testing.T) {
},
} {
t.Run(test.desc, func(t *testing.T) {
execution, err := InstrumentedExec(p, dataAsInput(&test.data))
execution, err := InstrumentedExec[NativeEndian](p, dataAsInput(&test.data))
if err != nil {
t.Fatalf("expected return value of %d, got execution error: %v", test.expected.ReturnValue, err)
}
@@ -1044,7 +1042,7 @@ func TestSimpleFilter(t *testing.T) {
// asInput converts a seccompData to a bpf.Input.
func dataAsInput(data *linux.SeccompData) Input {
return Input{marshal.Marshal(data), hostarch.ByteOrder}
return marshal.Marshal(data)
}
// BenchmarkInterpreter benchmarks the execution of the sample filter
@@ -1057,7 +1055,7 @@ func BenchmarkInterpreter(b *testing.B) {
data := dataAsInput(&linux.SeccompData{Nr: 231 /* __NR_exit_group */, Arch: 0xc000003e})
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := Exec(p, data); err != nil {
if _, err := Exec[NativeEndian](p, data); err != nil {
b.Fatalf("Unexpected execution error: %v", err)
}
}
@@ -1071,7 +1069,7 @@ func BenchmarkInstrumentedInterpreter(b *testing.B) {
data := dataAsInput(&linux.SeccompData{Nr: 231 /* __NR_exit_group */, Arch: 0xc000003e})
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := InstrumentedExec(p, data); err != nil {
if _, err := InstrumentedExec[NativeEndian](p, data); err != nil {
b.Fatalf("Unexpected execution error: %v", err)
}
}
-1
View File
@@ -18,7 +18,6 @@ go_library(
deps = [
"//pkg/abi/linux",
"//pkg/bpf",
"//pkg/hostarch",
"//pkg/log",
"@org_golang_x_sys//unix:go_default_library",
],
+1 -5
View File
@@ -23,7 +23,6 @@ import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/bpf"
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/log"
)
@@ -501,8 +500,5 @@ func DataAsBPFInput(d *linux.SeccompData, buf []byte) bpf.Input {
panic(fmt.Sprintf("buffer must be at least %d bytes long", d.SizeBytes()))
}
d.MarshalUnsafe(buf)
return bpf.Input{
Data: buf,
Order: hostarch.ByteOrder,
}
return buf[:d.SizeBytes()]
}
+5 -5
View File
@@ -877,9 +877,9 @@ func TestBasic(t *testing.T) {
t.Fatalf("bpf.Compile got error: %v", err)
}
for _, spec := range test.specs {
got, err := bpf.Exec(p, DataAsBPFInput(&spec.data, buf))
got, err := bpf.Exec[bpf.NativeEndian](p, DataAsBPFInput(&spec.data, buf))
if err != nil {
t.Fatalf("%s: bpf.Exec() got error: %v", spec.desc, err)
t.Fatalf("%s: bpf.Exec got error: %v", spec.desc, err)
}
if got != uint32(spec.want) {
// Include a decoded version of the program in output for debugging purposes.
@@ -920,9 +920,9 @@ func TestRandom(t *testing.T) {
buf := make([]byte, (&linux.SeccompData{}).SizeBytes())
for i := uint32(0); i < 200; i++ {
data := linux.SeccompData{Nr: int32(i), Arch: LINUX_AUDIT_ARCH}
got, err := bpf.Exec(p, DataAsBPFInput(&data, buf))
got, err := bpf.Exec[bpf.NativeEndian](p, DataAsBPFInput(&data, buf))
if err != nil {
t.Errorf("bpf.Exec() got error: %v, for syscall %d", err, i)
t.Errorf("bpf.Exec got error: %v, for syscall %d", err, i)
continue
}
want := linux.SECCOMP_RET_TRAP
@@ -930,7 +930,7 @@ func TestRandom(t *testing.T) {
want = linux.SECCOMP_RET_ALLOW
}
if got != uint32(want) {
t.Errorf("bpf.Exec() = %d, want: %d, for syscall %d", got, want, i)
t.Errorf("bpf.Exec = %d, want: %d, for syscall %d", got, want, i)
}
}
}
+2 -6
View File
@@ -32,11 +32,7 @@ const maxSyscallFilterInstructions = 1 << 15
func dataAsBPFInput(t *Task, d *linux.SeccompData) bpf.Input {
buf := t.CopyScratchBuffer(d.SizeBytes())
d.MarshalUnsafe(buf)
return bpf.Input{
Data: buf,
// Go-marshal always uses the native byte order.
Order: hostarch.ByteOrder,
}
return buf[:d.SizeBytes()]
}
func seccompSiginfo(t *Task, errno, sysno int32, ip hostarch.Addr) *linux.SignalInfo {
@@ -127,7 +123,7 @@ func (t *Task) evaluateSyscallFilters(sysno int32, args arch.SyscallArguments, i
// "Every filter successfully installed will be evaluated (in reverse
// order) for each system call the task makes." - kernel/seccomp.c
for i := len(f.([]bpf.Program)) - 1; i >= 0; i-- {
thisRet, err := bpf.Exec(f.([]bpf.Program)[i], input)
thisRet, err := bpf.Exec[bpf.NativeEndian](f.([]bpf.Program)[i], input)
if err != nil {
t.Debugf("seccomp-bpf filter %d returned error: %v", i, err)
thisRet = uint32(linux.SECCOMP_RET_KILL_THREAD)
+1 -1
View File
@@ -390,7 +390,7 @@ func TestRunscSeccomp(t *testing.T) {
// checkProgram runs the given program over the given input and checks the
// result against the expected output.
func checkProgram(p bpf.Program, in bpf.Input, expected uint32) error {
result, err := bpf.Exec(p, in)
result, err := bpf.Exec[bpf.NativeEndian](p, in)
if err != nil {
return err
}
+1 -1
View File
@@ -106,7 +106,7 @@ func runRequest(runReq secbenchdef.BenchRunRequest) (secbenchdef.BenchRunRespons
}
func evalSyscall(program bpf.Program, arch uint32, sc secbenchdef.Syscall, buf []byte) (uint32, error) {
return bpf.Exec(program, seccomp.DataAsBPFInput(&linux.SeccompData{
return bpf.Exec[bpf.NativeEndian](program, seccomp.DataAsBPFInput(&linux.SeccompData{
Nr: int32(sc.Sysno),
Arch: arch,
Args: [6]uint64{