diff --git a/pkg/bpf/input_bytes.go b/pkg/bpf/input_bytes.go index 2ccbe368c..ec4f0e616 100644 --- a/pkg/bpf/input_bytes.go +++ b/pkg/bpf/input_bytes.go @@ -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 } diff --git a/pkg/bpf/interpreter.go b/pkg/bpf/interpreter.go index beb15ecdb..380b78388 100644 --- a/pkg/bpf/interpreter.go +++ b/pkg/bpf/interpreter.go @@ -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} } diff --git a/pkg/bpf/interpreter_test.go b/pkg/bpf/interpreter_test.go index 0801ee633..0887de734 100644 --- a/pkg/bpf/interpreter_test.go +++ b/pkg/bpf/interpreter_test.go @@ -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) } } diff --git a/pkg/seccomp/BUILD b/pkg/seccomp/BUILD index 44630fd30..fc19e666c 100644 --- a/pkg/seccomp/BUILD +++ b/pkg/seccomp/BUILD @@ -18,7 +18,6 @@ go_library( deps = [ "//pkg/abi/linux", "//pkg/bpf", - "//pkg/hostarch", "//pkg/log", "@org_golang_x_sys//unix:go_default_library", ], diff --git a/pkg/seccomp/seccomp.go b/pkg/seccomp/seccomp.go index 771202bda..36b6fce9d 100644 --- a/pkg/seccomp/seccomp.go +++ b/pkg/seccomp/seccomp.go @@ -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()] } diff --git a/pkg/seccomp/seccomp_test.go b/pkg/seccomp/seccomp_test.go index 1d212df59..11fdc13a2 100644 --- a/pkg/seccomp/seccomp_test.go +++ b/pkg/seccomp/seccomp_test.go @@ -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) } } } diff --git a/pkg/sentry/kernel/seccomp.go b/pkg/sentry/kernel/seccomp.go index bd9b1dd13..da45b3455 100644 --- a/pkg/sentry/kernel/seccomp.go +++ b/pkg/sentry/kernel/seccomp.go @@ -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) diff --git a/runsc/specutils/seccomp/seccomp_test.go b/runsc/specutils/seccomp/seccomp_test.go index d06e025e3..bbadba5fa 100644 --- a/runsc/specutils/seccomp/seccomp_test.go +++ b/runsc/specutils/seccomp/seccomp_test.go @@ -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 } diff --git a/test/secbench/secbench.go b/test/secbench/secbench.go index c8352fc05..c9f13732c 100644 --- a/test/secbench/secbench.go +++ b/test/secbench/secbench.go @@ -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{