Appeasing the BPF verifier
2025-09-22
Recently, I ran into a problem while updating an existing BPF program to use BTF generated kernel headers. The change should have been as simple as replacing a handful of includes with a single vmlinux.h, or so I thought.
That was until I compiled to an object file and tried to load it into the kernel for testing. It was at this point my simple change was rejected.
; uint32_t mpls_label = bpf_ntohl( @ program.bpf.c:95
20: (61) r2 = *(u32 *)(r6 +76) ; R2_w=pkt(r=0) R6=ctx()
21: (61) r2 = *(u32 *)(r2 +14)
invalid access to packet, off=14 size=4, R2(id=0,off=14,r=0)
R2 offset is outside of the packet
That's odd. The line in question doesn't look all that strange. It's reading 4 bytes from the beginning of the packet data, offset by an ethernet header, into a pointer. There's also a check on the previous line that validates there is enough data in the packet to guarantee a successful read. So we should be golden, right?
uint32_t read_mpls_label(struct __sk_buff *skb) {
if (skb->data + ETH_HLEN + MPLS_HEADER_LEN > skb->data_end) {
return 0;
}
uint32_t mpls_label = bpf_ntohl(
*((uint32_t *)((unsigned long)skb->data + ETH_HLEN)));
mpls_label >>= 12;
return bpf_htonl(mpls_label);
}
I wasn't exactly sure why the above code failed verification, so I defaulted to swapping out direct packet access for the use of the bpf_skb_load_bytes helper - which worked. No more verifier errors!
uint32_t read_mpls_label(struct __sk_buff *skb) {
/* Safely load the 4-byte MPLS header, which is located after
* the Ethernet header, into a local variable on the stack. */
uint32_t mpls_hdr;
if (bpf_skb_load_bytes(skb, ETH_HLEN, &mpls_hdr, sizeof(mpls_hdr)) < 0) {
/* The packet is too small for an MPLS header */
return 0;
}
uint32_t mpls_label = bpf_ntohl(mpls_hdr);
mpls_label >>= 12;
return bpf_htonl(mpls_label);
}
But, I wasn't exactly sure why this had done the job. I assumed the helper was performing additional packet bounds checks and the verifier's static analysis liked this.
I got curious, and started to trawl through verifier.c until I came across the error in question. I spent some time back tracking through the many function calls that led to this location, but the file is 25k LoC long, and I didn't want to spend the afternoon on a fact finding mission. So I consulted the internet.
At this point I found some examples that both did and didn't cast the skb->data to an unsigned long first. This is a method I'd blindly copied in the past, so I wanted to truly understand the difference between the two. After reading plenty of online programming forum posts, I saw a comment that mentioned the use of casting to perform pointer arithmetic.
I did some more reading on the internet, and as it turns out, this type cast transforms the data into an integer (duh!). So from the verifier's perspective, it no longer knows that this new integer has any relationship to the packet data, so the previous packet bounds check is invalidated. When casting the integer back to a uint32_t * in order to read from it, the verifier sees an attempt to access memory from a pointer it can no longer validate is within the packet data, so it rejects the program.
I tested this logic by removing the use of the bpf helper, and reinstating the bounds check. Only this time, I assigned it to a pointer that we then use the read from. Thus allowing the verifier to guarantee we are operating on valid data.
uint32_t get_mpls_label_value(struct __sk_buff *skb) {
/* Get a pointer to the MPLS data and perform a bounds check. */
void *mpls_ptr = (void *)(long)skb->data + ETH_HLEN;
if (mpls_ptr + MPLS_HEADER_LEN > (void *)(long)skb->data_end) {
/* The packet is too small for an MPLS header */
return 0;
}
uint32_t mpls_label = bpf_ntohl(*((uint32_t *)mpls_ptr));
mpls_label >>= 12;
return bpf_htonl(mpls_label);
}
Again, it worked! Although this is a little more verbose, I find the use of the helper easier to grep.
To quote a good friend of mine "appeasing the BPF verfier is an art, not a science". This definitely rings true.