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