diff --git a/c.go b/c.go index 68f7533..4d28de4 100644 --- a/c.go +++ b/c.go @@ -86,12 +86,18 @@ type COpts struct { // a BPF to BPF call. // Requires at least kernel 5.10 (for x86, later for other architectures) if used with tail-calls. NoInline bool + + // PacketStartMaxOffset is the maximum offset the packet start / data is at, in bytes. + PacketStartMaxOffset uint16 } // ToC compiles a cBPF filter to a C function with a signature of: // // uint32_t opts.FunctionName(const uint8_t *const data, const uint8_t *const data_end) // +// If the data pointer is offset from the original BPF context pointer, +// the maximum value of this offset must be set as COpts.PacketStartMaxOffset. +// // The function returns the filter's return value: // 0 if the packet does not match the cBPF filter, // non 0 if the packet does match. @@ -100,7 +106,9 @@ func ToC(filter []bpf.Instruction, opts COpts) (string, error) { return "", errors.Errorf("invalid FunctionName %q", opts.FunctionName) } - blocks, err := compile(filter) + blocks, err := compile(filter, compileOpts{ + packetStartMaxOffset: opts.PacketStartMaxOffset, + }) if err != nil { return "", err } diff --git a/c_example_test.go b/c_example_test.go index e05b7ed..f809a81 100644 --- a/c_example_test.go +++ b/c_example_test.go @@ -48,7 +48,7 @@ __section("xdp") int {{.ProgramName}}(struct xdp_md *ctx) { uint8_t *data = (uint8_t *)(long)ctx->data; uint8_t const *data_end = (uint8_t *)(long)ctx->data_end; - if ({{.FilterName}}(data, data_end)) { + if ({{.FilterName}}(data + {{.Offset}}, data_end)) { return XDP_DROP; } @@ -65,6 +65,10 @@ type testTemplateOpts struct { // Name of the eBPF program ProgramName string + + // Offset the packet start is advanced by before the filter is called. + // Must match the COpts.PacketStartMaxOffset the filter was compiled with. + Offset uint16 } // ExampleToC demonstrates how to use ToC() to embed a cBPF filter @@ -102,6 +106,7 @@ func buildC(filter []bpf.Instruction, programName string, opts COpts) ([]byte, e Filter: ebpfFilter, FilterName: opts.FunctionName, ProgramName: programName, + Offset: opts.PacketStartMaxOffset, }) if err != nil { return nil, errors.Wrap(err, "executing template with C filter") diff --git a/c_test.go b/c_test.go index bb968df..f4c6f04 100644 --- a/c_test.go +++ b/c_test.go @@ -72,8 +72,8 @@ func TestNoInline(t *testing.T) { const entryPoint = "xdp_filter" // cBackend compiles classic BPF to C, which is compiled with clang -func cBackend(tb testing.TB, insns []bpf.Instruction, in []byte) result { - elf, err := buildC(insns, entryPoint, COpts{FunctionName: "filter"}) +func cBackend(tb testing.TB, insns []bpf.Instruction, in []byte, opts backendOpts) result { + elf, err := buildC(insns, entryPoint, COpts{FunctionName: "filter", PacketStartMaxOffset: opts.offset}) if err != nil { tb.Fatal(err) } diff --git a/cbpfc.go b/cbpfc.go index 592dee3..f00d3fb 100644 --- a/cbpfc.go +++ b/cbpfc.go @@ -11,6 +11,13 @@ // - Division by zero is guarded by runtime checks // // The generated C / eBPF is intended to be embedded into a larger C / eBPF program. +// +// Footguns / limitations: +// - The maximum absolute offset of a packet that can be read is 0xFFFF, any accesses +// past that will be treated as out of bounds cBPF packet accesses, and the filter will +// return 0. That includes offsets added directly to the packet pointer passed in. +// - If the packet pointer passed in has an offset (eg because you've skipped past the +// ethernet header), the maximum possible offset must be passed in via EBPFOpts or COpts. package cbpfc import ( @@ -175,7 +182,7 @@ func (p packetGuardAbsolute) Assemble() (bpf.RawInstruction, error) { // // So instead we check: // - RegX + start >= 0 -// - RegX + start < maxPacketOffset - length +// - RegX + start < maxPacketOffset - packetStartMaxOffset - length // - packet_start + RegX + start + length < packet_end // // This lets us reuse packet_start + RegX + start as the packet pointer for LoadIndirect, @@ -186,9 +193,12 @@ type packetGuardIndirect struct { // Last byte read (exclusive). // int64 to avoid overflows with INT32_MAX + size end int64 + + // packetStartMaxOffset is the maximum offset the packet start / data is at, in bytes. + packetStartMaxOffset uint16 } -func newPacketGuardIndirect(off uint32, size int) packetGuardIndirect { +func newPacketGuardIndirect(off uint32, size int, opts compileOpts) packetGuardIndirect { // cBPF offsets are uint32, but are signed in reality // LoadIndirect offsets are encoded as uint32 by x/net/bpf, but are signed in reality. // Unlike LoadAbsolute, restrictions only apply to RegX + Offset and not Offset alone, @@ -196,6 +206,8 @@ func newPacketGuardIndirect(off uint32, size int) packetGuardIndirect { return packetGuardIndirect{ start: int32(off), end: int64(int32(off)) + int64(size), + + packetStartMaxOffset: opts.packetStartMaxOffset, } } @@ -246,16 +258,16 @@ func (a packetGuardIndirect) restrict(o packetGuard) packetGuard { // This checks that it is positive, and int32(RegX) + p.end doesn't exceed maxPacketOffset. // Returns 0 (check will always be false) if there is no way for the start and end of the guard to be < maxPacketOffset. func (p packetGuardIndirect) maxStartOffset() int32 { - length := p.end - int64(p.start) - // If length exceeds maxPacketOffset, there's no way for RegX + start >= 0 and RegX + end < maxPacketOffset. + m := maxPacketOffset - int64(p.packetStartMaxOffset) - (p.end - int64(p.start)) + // If m is negative, (packetStartMaxOffset + length) exceeds maxPacketOffset, there's no way for RegX + start >= 0 and RegX + end < maxPacketOffset. // Return 0 so the check fails, and we return noMatch. - if length > maxPacketOffset { + if m < 0 { return 0 } // +1 as it needs to be strictly less than. // This lets us return 0 above to get noMatch. - return int32(maxPacketOffset) - int32(length) + 1 + return int32(m) + 1 } // packet_start + (int32(x) + p.start) + p.length() must be <= packet_end. @@ -295,11 +307,16 @@ func (c checkXNotZero) Assemble() (bpf.RawInstruction, error) { return bpf.RawInstruction{}, errors.Errorf("unsupported") } +type compileOpts struct { + // packetStartMaxOffset is the maximum offset the packet start / data is at, in bytes. + packetStartMaxOffset uint16 +} + // compile compiles a cBPF program to an ordered slice of blocks, with: // - Registers zero initialized as required // - Required packet access guards added // - JumpIf and JumpIfX instructions normalized (see normalizeJumps) -func compile(insns []bpf.Instruction) ([]*block, error) { +func compile(insns []bpf.Instruction, opts compileOpts) ([]*block, error) { err := validateInstructions(insns) if err != nil { return nil, err @@ -327,11 +344,11 @@ func compile(insns []bpf.Instruction) ([]*block, error) { return nil, err } - rewriteLargePacketOffsets(&blocks) + rewriteLargePacketOffsets(&blocks, opts) // Guard packet loads addAbsolutePacketGuards(blocks) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, opts) return blocks, nil } @@ -586,7 +603,7 @@ func addDivideByZeroGuards(blocks []*block) error { // While cBPF allows bigger offsets, in practice they cannot match a packet. // This doesn't work for LoadIndirect as the actual offset is LoadIndirect.Off + RegX, // we instead rely on runtime checks (see packetGuardIndirect). -func rewriteLargePacketOffsets(blocks *[]*block) { +func rewriteLargePacketOffsets(blocks *[]*block, opts compileOpts) { // All blocks are reachable when we start. // But some blocks can become unreachable once we've rewritten load instructions to returns. // The verifier rejects unreachable instructions, track how many other blocks go to a given block @@ -604,25 +621,24 @@ func rewriteLargePacketOffsets(blocks *[]*block) { for _, insn := range block.insns { var ( - offset uint32 - size int + offset uint64 + size uint64 ) // LoadIndirect is handled by runtime checks as only RegX + offset is subject to maxPacketOffset. switch i := insn.Instruction.(type) { case bpf.LoadAbsolute: - offset = i.Off - size = i.Size + offset = uint64(i.Off) + size = uint64(i.Size) case bpf.LoadMemShift: - offset = i.Off + offset = uint64(i.Off) size = 1 default: continue } // A packetGuard will have to add size to the packet pointer, so it counts towards the limit. - // We've already validate offset isn't signed, so this can't overflow. - if offset+uint32(size) > maxPacketOffset { + if offset+size+uint64(opts.packetStartMaxOffset) > maxPacketOffset { // Mimick an out of bounds load in cBPF, returning 0 / no match. // The block now unconditionally returns, the other instructions in it don't matter. block.insns = []instruction{ @@ -674,7 +690,7 @@ func addAbsolutePacketGuards(blocks []*block) { } // addIndirectPacketGuard adds required packet guards for indirect packet accesses to blocks. -func addIndirectPacketGuards(blocks []*block) { +func addIndirectPacketGuards(blocks []*block, opts compileOpts) { addPacketGuards(blocks, packetGuardOpts{ requiredGuard: func(insns []instruction) requiredGuard { var ( @@ -687,7 +703,7 @@ func addIndirectPacketGuards(blocks []*block) { switch i := insn.Instruction.(type) { case bpf.LoadIndirect: - biggestGuard = biggestGuard.extend(newPacketGuardIndirect(i.Off, i.Size)) + biggestGuard = biggestGuard.extend(newPacketGuardIndirect(i.Off, i.Size, opts)) } // Check if we clobbered x - this invalidates the guard diff --git a/cbpfc_test.go b/cbpfc_test.go index d6b76ef..cdff1b7 100644 --- a/cbpfc_test.go +++ b/cbpfc_test.go @@ -23,7 +23,7 @@ func requireError(tb testing.TB, err error, contains string) { // Make sure we bail out with 0 instructions func TestZero(t *testing.T) { - _, err := compile([]bpf.Instruction{}) + _, err := compile([]bpf.Instruction{}, compileOpts{}) requireError(t, err, "can't compile 0 instructions") } @@ -31,7 +31,7 @@ func TestZero(t *testing.T) { func TestRaw(t *testing.T) { _, err := compile([]bpf.Instruction{ bpf.RawInstruction{}, - }) + }, compileOpts{}) requireError(t, err, "unsupported instruction 0:") } @@ -47,7 +47,7 @@ func TestLoadAbsoluteNegativeOffset(t *testing.T) { _, err := compile([]bpf.Instruction{ insn, bpf.RetA{}, - }) + }, compileOpts{}) requireError(t, err, "negative offset -1") } @@ -61,7 +61,7 @@ func TestExtension(t *testing.T) { _, err := compile([]bpf.Instruction{ bpf.LoadExtension{Num: ext}, bpf.RetA{}, - }) + }, compileOpts{}) switch ext { case bpf.ExtLen: @@ -79,7 +79,7 @@ func TestJumpOut(t *testing.T) { _, err := compile([]bpf.Instruction{ bpf.LoadConstant{Dst: bpf.RegX, Val: 0}, bpf.Jump{Skip: 0}, - }) + }, compileOpts{}) requireError(t, err, "instruction 1: ja 0 flows past last instruction") } @@ -88,7 +88,7 @@ func TestJumpIfOut(t *testing.T) { _, err := compile([]bpf.Instruction{ bpf.LoadConstant{Dst: bpf.RegA, Val: 0}, bpf.JumpIf{Cond: bpf.JumpEqual, Val: 2, SkipTrue: 0, SkipFalse: 1}, - }) + }, compileOpts{}) requireError(t, err, "instruction 1: jneq #2,1 flows past last instruction") } @@ -98,7 +98,7 @@ func TestJumpIfXOut(t *testing.T) { bpf.LoadConstant{Dst: bpf.RegA, Val: 0}, bpf.LoadConstant{Dst: bpf.RegX, Val: 3}, bpf.JumpIfX{Cond: bpf.JumpEqual, SkipTrue: 1, SkipFalse: 0}, - }) + }, compileOpts{}) requireError(t, err, "instruction 2: jeq x,1 flows past last instruction") } @@ -107,7 +107,7 @@ func TestJumpIfXOut(t *testing.T) { func TestFallthroughOut(t *testing.T) { _, err := compile([]bpf.Instruction{ bpf.LoadConstant{Dst: bpf.RegA, Val: 0}, - }) + }, compileOpts{}) requireError(t, err, "instruction 0: ld #0 flows past last instruction") } @@ -808,7 +808,7 @@ func TestDivisionByZeroParentsNOK(t *testing.T) { } func TestRewriteLargePacketOffsets(t *testing.T) { - testOK := func(t *testing.T, load bpf.Instruction) { + testOK := func(t *testing.T, load bpf.Instruction, packetOff uint16) { t.Helper() insns := toInstructions([]bpf.Instruction{ @@ -817,12 +817,14 @@ func TestRewriteLargePacketOffsets(t *testing.T) { }) blocks := mustSplitBlocks(t, 1, insns) - rewriteLargePacketOffsets(&blocks) + rewriteLargePacketOffsets(&blocks, compileOpts{ + packetStartMaxOffset: packetOff, + }) matchBlock(t, blocks[0], insns, nil) } - testOOB := func(t *testing.T, load bpf.Instruction) { + testOOB := func(t *testing.T, load bpf.Instruction, packetOff uint16) { t.Helper() insns := toInstructions([]bpf.Instruction{ @@ -831,22 +833,31 @@ func TestRewriteLargePacketOffsets(t *testing.T) { }) blocks := mustSplitBlocks(t, 1, insns) - rewriteLargePacketOffsets(&blocks) + rewriteLargePacketOffsets(&blocks, compileOpts{ + packetStartMaxOffset: packetOff, + }) matchBlock(t, blocks[0], []instruction{ {Instruction: bpf.RetConstant{}}, }, nil) } - testOK(t, bpf.LoadAbsolute{Size: 1, Off: 65534}) - testOOB(t, bpf.LoadAbsolute{Size: 1, Off: 65535}) - testOK(t, bpf.LoadAbsolute{Size: 2, Off: 65533}) - testOOB(t, bpf.LoadAbsolute{Size: 2, Off: 65534}) - testOK(t, bpf.LoadAbsolute{Size: 4, Off: 65531}) - testOOB(t, bpf.LoadAbsolute{Size: 4, Off: 65532}) + testOK(t, bpf.LoadAbsolute{Size: 1, Off: 65534}, 0) + testOOB(t, bpf.LoadAbsolute{Size: 1, Off: 65535}, 0) + testOK(t, bpf.LoadAbsolute{Size: 2, Off: 65533}, 0) + testOOB(t, bpf.LoadAbsolute{Size: 2, Off: 65534}, 0) + testOK(t, bpf.LoadAbsolute{Size: 4, Off: 65531}, 0) + testOOB(t, bpf.LoadAbsolute{Size: 4, Off: 65532}, 0) + + testOK(t, bpf.LoadMemShift{Off: 65534}, 0) + testOOB(t, bpf.LoadMemShift{Off: 65535}, 0) + + // With packet offsets. + testOK(t, bpf.LoadAbsolute{Size: 1, Off: 65530}, 4) + testOOB(t, bpf.LoadAbsolute{Size: 1, Off: 65531}, 4) - testOK(t, bpf.LoadMemShift{Off: 65534}) - testOOB(t, bpf.LoadMemShift{Off: 65535}) + testOK(t, bpf.LoadMemShift{Off: 65530}, 4) + testOOB(t, bpf.LoadMemShift{Off: 65531}, 4) } // Test unreachable blocks due to large packet offsets are removed. @@ -876,7 +887,7 @@ func TestRewriteLargePacketOffsetsDeadBlock(t *testing.T) { insns := toInstructions(filter) blocks := mustSplitBlocks(t, 6, insns) - rewriteLargePacketOffsets(&blocks) + rewriteLargePacketOffsets(&blocks, compileOpts{}) if len(blocks) != 5 { t.Fatalf("expected 5 blocks, got %v", blocks) } @@ -1068,7 +1079,7 @@ func TestIndirectGuardSize(t *testing.T) { blocks := mustSplitBlocks(t, 1, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 8, end: 14}}}, @@ -1085,7 +1096,7 @@ func TestNoIndirectGuard(t *testing.T) { blocks := mustSplitBlocks(t, 1, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], insns, nil) } @@ -1103,7 +1114,7 @@ func TestIndirectGuardClobber(t *testing.T) { blocks := mustSplitBlocks(t, 1, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 10, end: 14}}}, @@ -1138,7 +1149,7 @@ func TestIndirectGuardClobberLast(t *testing.T) { blocks := mustSplitBlocks(t, 3, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 10, end: 14}}}, @@ -1173,7 +1184,7 @@ func TestIndirectGuardParentsOK(t *testing.T) { blocks := mustSplitBlocks(t, 4, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 10, end: 14}}}, @@ -1204,7 +1215,7 @@ func TestIndirectGuardParentNoMatch(t *testing.T) { blocks := mustSplitBlocks(t, 4, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 10, end: 16}}}, @@ -1239,7 +1250,7 @@ func TestIndirectGuardParentDeepNoMatch(t *testing.T) { blocks := mustSplitBlocks(t, 5, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 10, end: 18}}}, @@ -1269,7 +1280,7 @@ func TestIndirectGuardParentMatch(t *testing.T) { blocks := mustSplitBlocks(t, 3, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 9, end: 13}}}, @@ -1307,7 +1318,7 @@ func TestIndirectGuardParentClobber(t *testing.T) { blocks := mustSplitBlocks(t, 4, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 10, end: 14}}}, @@ -1353,7 +1364,7 @@ func TestIndirectGuardExtendClobber(t *testing.T) { blocks := mustSplitBlocks(t, 5, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 10, end: 14}}}, @@ -1396,7 +1407,7 @@ func TestIndirectGuardParentsNotOK(t *testing.T) { blocks := mustSplitBlocks(t, 4, insns) - addIndirectPacketGuards(blocks) + addIndirectPacketGuards(blocks, compileOpts{}) matchBlock(t, blocks[0], join( []instruction{{Instruction: packetGuardIndirect{start: 9, end: 14}}}, diff --git a/ebpf.go b/ebpf.go index 9f8fd4d..3f57d7e 100644 --- a/ebpf.go +++ b/ebpf.go @@ -36,8 +36,12 @@ var sizeToEBPF = map[int]asm.Size{ // EBPFOpts control how a cBPF filter is converted to eBPF type EBPFOpts struct { // PacketStart is a register holding a pointer to the start of the packet. + // If this pointer is offset from the original BPF context pointer, + // the maximum value of this offset must be set as PacketStartMaxOffset. // Not modified. PacketStart asm.Register + // PacketStartMaxOffset is the maximum offset PacketStart is at, in bytes. + PacketStartMaxOffset uint16 // PacketEnd is a register holding a pointer to the end of the packet. // Not modified. PacketEnd asm.Register @@ -103,7 +107,9 @@ func (e ebpfOpts) stackOffset(scratch int) int16 { // 0 if the packet does not match the cBPF filter, // non 0 if the packet does match. func ToEBPF(filter []bpf.Instruction, opts EBPFOpts) (asm.Instructions, error) { - blocks, err := compile(filter) + blocks, err := compile(filter, compileOpts{ + packetStartMaxOffset: opts.PacketStartMaxOffset, + }) if err != nil { return nil, err } diff --git a/ebpf_example_test.go b/ebpf_example_test.go index 49e619d..943c4b1 100644 --- a/ebpf_example_test.go +++ b/ebpf_example_test.go @@ -14,7 +14,7 @@ func ExampleToEBPF() { bpf.RetConstant{Val: 1}, } - prog, err := buildEBPF(filter) + prog, err := buildEBPF(filter, 0) if err != nil { panic(err) } @@ -27,11 +27,12 @@ func ExampleToEBPF() { // buildEBPF compiles a cBPF filter to eBPF, and embeds it an eBPF program. // The XDP program XDP_DROP's incoming packets that match the filter. // Returns the eBPF program instructions -func buildEBPF(filter []bpf.Instruction) (asm.Instructions, error) { +func buildEBPF(filter []bpf.Instruction, offset uint16) (asm.Instructions, error) { ebpfFilter, err := ToEBPF(filter, EBPFOpts{ // Pass packet start and end pointers in these registers - PacketStart: asm.R2, - PacketEnd: asm.R3, + PacketStart: asm.R2, + PacketStartMaxOffset: offset, + PacketEnd: asm.R3, // Result of filter Result: asm.R4, ResultLabel: "result", @@ -48,6 +49,8 @@ func buildEBPF(filter []bpf.Instruction) (asm.Instructions, error) { // Packet start asm.LoadMem(asm.R2, asm.R1, 0, asm.Word), + // Fixed offset + asm.Add.Imm(asm.R2, int32(offset)), // Packet end asm.LoadMem(asm.R3, asm.R1, 4, asm.Word), diff --git a/ebpf_test.go b/ebpf_test.go index b5cccf2..5caecc4 100644 --- a/ebpf_test.go +++ b/ebpf_test.go @@ -8,8 +8,8 @@ import ( ) // ebpfBacked is backend that compiles classic BPF to eBPF -func ebpfBackend(tb testing.TB, insns []bpf.Instruction, in []byte) result { - prog, err := buildEBPF(insns) +func ebpfBackend(tb testing.TB, insns []bpf.Instruction, in []byte, opts backendOpts) result { + prog, err := buildEBPF(insns, opts.offset) if err != nil { tb.Fatal(err) } diff --git a/insn_test.go b/insn_test.go index 5eb1894..651717a 100644 --- a/insn_test.go +++ b/insn_test.go @@ -159,6 +159,45 @@ func TestLoadAbsoluteBigOffset(t *testing.T) { checkBackends(t, filter(bpf.LoadMemShift{Off: maxPacketOffset}), nil, noMatch) } +// Absolute load with an offset packet pointer. +func TestLoadAbsolutePacketStartMaxOffset(t *testing.T) { + t.Parallel() + + // XDP limits packets to one page, so there's no way to feed a packet big enough to test the offsets + // we want through BPF_PROG_TEST_RUN. + // All we can check is that the verifier accepts the program and it doesn't match. + filter := func(load bpf.Instruction) []bpf.Instruction { + return []bpf.Instruction{ + load, + // Make sure we return a different value if the load succeeds. + bpf.ALUOpConstant{Op: bpf.ALUOpAdd, Val: 2}, + bpf.RetA{}, + } + } + + for _, offset := range []uint16{0, 1, 14, 18, 64} { + t.Run(fmt.Sprint(offset), func(t *testing.T) { + t.Parallel() + + opts := backendOpts{offset: offset} + + // Smallest out of bounds offset once the packet start offset is accounted for, + // mirroring maxPacketOffset in TestLoadAbsoluteBigOffset. + maxOffset := uint32(maxPacketOffset - offset) + + checkBackends(t, filter(bpf.LoadAbsolute{Off: maxOffset - 1, Size: 1}), nil, noMatch, opts) + checkBackends(t, filter(bpf.LoadAbsolute{Off: maxOffset, Size: 1}), nil, noMatch, opts) + checkBackends(t, filter(bpf.LoadAbsolute{Off: maxOffset - 2, Size: 2}), nil, noMatch, opts) + checkBackends(t, filter(bpf.LoadAbsolute{Off: maxOffset - 1, Size: 2}), nil, noMatch, opts) + checkBackends(t, filter(bpf.LoadAbsolute{Off: maxOffset - 4, Size: 4}), nil, noMatch, opts) + checkBackends(t, filter(bpf.LoadAbsolute{Off: maxOffset - 3, Size: 4}), nil, noMatch, opts) + + checkBackends(t, filter(bpf.LoadMemShift{Off: maxOffset - 1}), nil, noMatch, opts) + checkBackends(t, filter(bpf.LoadMemShift{Off: maxOffset}), nil, noMatch, opts) + }) + } +} + func TestLoadIndirect(t *testing.T) { t.Parallel() @@ -308,6 +347,40 @@ func TestLoadIndirectGuardOverflow(t *testing.T) { }, nil, noMatch) } +// Indirect load with an offset packet pointer. +func TestLoadIndirectPacketStartMaxOffset(t *testing.T) { + t.Parallel() + + filter := []bpf.Instruction{ + // Variable RegX + bpf.LoadAbsolute{Off: 0, Size: 4}, + bpf.TAX{}, + bpf.LoadIndirect{Off: 4, Size: 4}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: 0xDEADBEEF, SkipTrue: 1}, + bpf.RetConstant{Val: 0}, + bpf.RetConstant{Val: 1}, + } + + // RegX is the first 4 bytes after offset. + packet := func(offset uint16, val []byte) []byte { + in := append(make([]byte, offset), 0, 0, 0, 3, 0, 0, 0) + return append(in, val...) + } + + for _, offset := range []uint16{0, 1, 14, 18, 64} { + t.Run(fmt.Sprint(offset), func(t *testing.T) { + t.Parallel() + + checkBackends(t, filter, packet(offset, []byte{0xDE, 0xAD, 0xBE, 0xEF}), match, backendOpts{ + offset: offset, + }) + checkBackends(t, filter, packet(offset, []byte{0xDE, 0xAD, 0xBE, 0xEE}), noMatch, backendOpts{ + offset: offset, + }) + }) + } +} + // The 0 scratch slot is usable. func TestScratchZero(t *testing.T) { t.Parallel() @@ -805,11 +878,16 @@ func (r result) String() string { } // True IFF packet matches filter -type backend func(testing.TB, []bpf.Instruction, []byte) result +type backend func(testing.TB, []bpf.Instruction, []byte, backendOpts) result + +type backendOpts struct { + // Fixed offset into the packet to use. + offset uint16 +} // checkBackends checks if all the backends match the packet as expected. // Input packet is 0 padded to min ethernet length. -func checkBackends(t *testing.T, filter []bpf.Instruction, in []byte, expected result) { +func checkBackends(t *testing.T, filter []bpf.Instruction, in []byte, expected result, opts ...backendOpts) { t.Helper() if len(in) < 14 { @@ -818,9 +896,18 @@ func checkBackends(t *testing.T, filter []bpf.Instruction, in []byte, expected r in = t } + var options backendOpts + switch len(opts) { + case 0: + case 1: + options = opts[0] + default: + t.Fatal("multiple backendOpts provided") + } + check := func(b backend) func(*testing.T) { return func(t *testing.T) { - if got := b(t, filter, in); got != expected { + if got := b(t, filter, in, options); got != expected { t.Fatalf("Got %q, expected %q", got, expected) } } diff --git a/kernel_test.go b/kernel_test.go index f03cf11..bc1681d 100644 --- a/kernel_test.go +++ b/kernel_test.go @@ -12,7 +12,13 @@ import ( ) // kernelBackend is a backend that runs cBPF in the kernel -func kernelBackend(tb testing.TB, insns []bpf.Instruction, in []byte) result { +func kernelBackend(tb testing.TB, insns []bpf.Instruction, in []byte, opts backendOpts) result { + // There's no easy way to support fixed offsets, we'd have to rewrite all the LoadAbsolute and LoadIndirect + // instructions. + if opts.offset != 0 { + tb.Skip() + } + filter, err := bpf.Assemble(insns) if err != nil { tb.Fatal(err)