From 0cd648674c1f329e4a7651d1646b2acf042d35bd Mon Sep 17 00:00:00 2001 From: Bastien Dhiver Date: Wed, 8 Jul 2026 15:18:54 +0100 Subject: [PATCH] Add EBPFOpts.PacketStartOffset for non-zero-based PacketStart ToEBPF's indirect packet-load guard assumed PacketStart points at offset 0 of the packet. It bounds the data-dependent index against maxStartOffset() = maxPacketOffset - length + 1, relying on the verifier's rule that a packet pointer only gets a range when off + umax_value <= MAX_PACKET_OFF (0xFFFF). When the caller sets PacketStart to a pointer that already carries a fixed offset (e.g. it points at the L3 header rather than the start of the frame), that fixed offset is added on top of the guarded index. An indirect load with a data-dependent index (such as walking TCP options) can then push off + umax_value past MAX_PACKET_OFF, and the verifier rejects the program with "invalid access to packet" even though the access is safe. Add EBPFOpts.PacketStartOffset so callers can declare PacketStart's fixed offset. maxStartOffsetWithBase() reserves that many bytes of the packet-offset budget in the indirect guard so the bounded range stays within MAX_PACKET_OFF. It fails closed (returns 0, i.e. noMatch) when no room remains, rather than emitting an unbounded, unverifiable load. maxStartOffsetWithBase also rejects a negative base offset (PacketStart cannot precede the packet) and fails closed, so bogus input never widens the guard's budget. PacketStartOffset defaults to 0, preserving existing behaviour. --- cbpfc.go | 35 +++++++++++++++++++++++++++++++++++ cbpfc_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ ebpf.go | 18 ++++++++++++++++-- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/cbpfc.go b/cbpfc.go index 592dee3..8e3c489 100644 --- a/cbpfc.go +++ b/cbpfc.go @@ -258,6 +258,41 @@ func (p packetGuardIndirect) maxStartOffset() int32 { return int32(maxPacketOffset) - int32(length) + 1 } +// maxStartOffsetWithBase is like maxStartOffset but reserves baseOffset bytes of +// the packet-offset budget for a non-zero fixed offset of PacketStart (e.g. an +// L3 offset). +// +// The eBPF verifier refuses to compute a range for a packet pointer when +// off + umax_value > maxPacketOffset. When PacketStart already carries a fixed +// offset (baseOffset), the variable part (int32(RegX) + p.start) must be bounded +// so that baseOffset + (int32(RegX) + p.start) + p.length() <= maxPacketOffset. +// +// Returns 0 (check always false, i.e. noMatch) when there is no room left once +// baseOffset is accounted for, so we fail closed rather than emit an unbounded +// (and unverifiable) load. A negative baseOffset is invalid (PacketStart cannot +// sit before the start of the packet) and also fails closed, so we never grant +// extra budget from bogus input. +func (p packetGuardIndirect) maxStartOffsetWithBase(baseOffset int32) int32 { + // A negative base offset would widen the budget rather than shrink it: reject + // it and fail closed. + if baseOffset < 0 { + return 0 + } + + max := p.maxStartOffset() + // maxStartOffset already hit the fail-closed sentinel: keep failing. + if max == 0 { + return 0 + } + + max -= baseOffset + if max <= 0 { + return 0 + } + + return max +} + // packet_start + (int32(x) + p.start) + p.length() must be <= packet_end. // This lets us reuse the (int32(x) + p.start) from the maxStartOffset() check, to keep the bounds info. func (p packetGuardIndirect) length() int32 { diff --git a/cbpfc_test.go b/cbpfc_test.go index d6b76ef..6978361 100644 --- a/cbpfc_test.go +++ b/cbpfc_test.go @@ -1423,6 +1423,53 @@ func join(insns ...[]instruction) []instruction { return res } +// maxStartOffsetWithBase reserves the base offset of PacketStart from the +// packet-offset budget, and fails closed when no room remains. +func TestMaxStartOffsetWithBase(t *testing.T) { + // guard [8:14], length 6. + guard := packetGuardIndirect{start: 8, end: 14} + base := guard.maxStartOffset() + if base <= 0 { + t.Fatalf("expected positive maxStartOffset, got %d", base) + } + + tests := []struct { + name string + baseOffset int32 + want int32 + }{ + {"zero offset matches maxStartOffset", 0, base}, + {"offset reserves budget", 18, base - 18}, + {"offset just within budget", base - 1, 1}, + {"offset consumes whole budget fails closed", base, 0}, + {"offset beyond budget fails closed", base + 100, 0}, + {"negative offset fails closed", -1, 0}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := guard.maxStartOffsetWithBase(tc.baseOffset); got != tc.want { + t.Fatalf("maxStartOffsetWithBase(%d) = %d, want %d", tc.baseOffset, got, tc.want) + } + }) + } +} + +// A guard whose length already exceeds maxPacketOffset fails closed regardless +// of the base offset (maxStartOffset returns the 0 sentinel). +func TestMaxStartOffsetWithBaseOversizedGuard(t *testing.T) { + guard := packetGuardIndirect{start: 0, end: maxPacketOffset + 10} + if got := guard.maxStartOffset(); got != 0 { + t.Fatalf("expected oversized guard maxStartOffset to be 0, got %d", got) + } + if got := guard.maxStartOffsetWithBase(0); got != 0 { + t.Fatalf("expected fail-closed for oversized guard, got %d", got) + } + if got := guard.maxStartOffsetWithBase(18); got != 0 { + t.Fatalf("expected fail-closed for oversized guard with base, got %d", got) + } +} + // matchBlock checks a block has the given instructions and jumps func matchBlock(t *testing.T, b *block, expected []instruction, jumps map[pos]*block) { t.Helper() diff --git a/ebpf.go b/ebpf.go index 9f8fd4d..f8ad0b6 100644 --- a/ebpf.go +++ b/ebpf.go @@ -58,6 +58,19 @@ type EBPFOpts struct { // LabelPrefix is the prefix to prepend to labels used internally. LabelPrefix string + + // PacketStartOffset is the fixed offset (in bytes) of PacketStart within the + // packet, when this is known at compile time. It defaults to 0 (PacketStart + // points at the very start of the packet). + // + // The eBPF verifier refuses to grant a range to a packet pointer whenever + // off + umax_value > MAX_PACKET_OFF (0xFFFF). When PacketStart already carries + // a non-zero fixed offset (e.g. it points at the L3 header rather than the + // start of the frame), an indirect load with a data-dependent index can push + // off + umax_value past that limit, and the program fails to verify even + // though it is safe. Setting PacketStartOffset reserves that many bytes of the + // packet-offset budget in the indirect load guards so verification succeeds. + PacketStartOffset int } // ebpfOpts is the internal version of EBPFOpts @@ -288,9 +301,10 @@ func insnToEBPF(insn instruction, blk *block, opts ebpfOpts) (asm.Instructions, asm.LSh.Imm(opts.regIndirect, 32), asm.ArSh.Imm(opts.regIndirect, 32), - // Check maxStartOffset() + // Check maxStartOffset(), reserving PacketStartOffset bytes of the + // packet-offset budget for PacketStart's own fixed offset. asm.Add.Imm(opts.regIndirect, i.start), - asm.JGE.Imm(opts.regIndirect, i.maxStartOffset(), opts.label(noMatchLabel)), + asm.JGE.Imm(opts.regIndirect, i.maxStartOffsetWithBase(int32(opts.PacketStartOffset)), opts.label(noMatchLabel)), // packet_start + signed x + start // This will have a smin_value >= 0