From ecfa9a86aed6e87687fc984f97856094a25b6a84 Mon Sep 17 00:00:00 2001 From: Arthur Fabre Date: Fri, 24 Jul 2026 21:16:34 +0200 Subject: [PATCH] Account for PacketStart offsets in packet guards The verifier rejects any programs that accesses packets with offsets greater than 0xFFFF. For absolute loads we can detect this statically, for indirect loads we insert a runtime check. But our checks didn't account for any pre-existing offset to the packet pointer the caller passes in! (eg because the caller has parsed / skipped the ethernet header). This must be accounted for in the maximum offset check. Add an option to both EBPFOpts and COpts to allow users to specify the maximum offset the packet pointer may have, so it can be included in the static analysis for absolute loads, and the runtime checks for indirect loads. Thank you to @Dhiver for figuring this out in #45. --- c.go | 10 ++++- c_example_test.go | 7 +++- c_test.go | 4 +- cbpfc.go | 54 ++++++++++++++++--------- cbpfc_test.go | 75 ++++++++++++++++++++--------------- ebpf.go | 8 +++- ebpf_example_test.go | 11 ++++-- ebpf_test.go | 4 +- insn_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++-- kernel_test.go | 8 +++- 10 files changed, 208 insertions(+), 66 deletions(-) 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)