Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions cbpfc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 47 additions & 0 deletions cbpfc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
18 changes: 16 additions & 2 deletions ebpf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading