An eBPF program calls bpf_map_lookup_elem() and dereferences the returned pointer straight away, and the kernel refuses to load it with an invalid memory access mentioning map_value_or_null. What is the verifier objecting to, and what does it want the code to do?
answer
- a lookup can miss
- the register carries a type, per path
- or_null is part of that type
- the branch is what narrows it
- check, then dereference
basics
~20 sbpf_map_lookup_elem() returns NULL when the key is absent, so the verifier types its result as a maybe-NULL pointer and refuses any dereference of it. An explicit NULL check on the returned pointer refines the type on the surviving branch, after which the dereference is allowed.
solid answer
~50 sThe verifier tracks a **type for every register on every path**, not just a value. The return of `bpf_map_lookup_elem()` gets the type "pointer to map value, or NULL", because a lookup on a missing key legitimately returns NULL. Dereferencing a register of that type would be a NULL dereference in kernel context — an oops — so the verifier rejects the load rather than hoping the key exists. The fix is not a cast or a compiler hint: you write the check the verifier is asking for, `if (!val) return 0;`. Verification then forks at the branch, discards the NULL path, and on the remaining path *refines* the register to a plain map-value pointer with known size bounds, so reads and writes within the value's size are provably safe. This is the single most common eBPF rejection, and the shape of the fix — check, then use — generalises to every maybe-NULL helper return.
code
c · 25 lines#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u64);
} counts SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_openat")
int count_openat(void *ctx)
{
__u32 key = 0;
__u64 *val = bpf_map_lookup_elem(&counts, &key);
/* Without this branch the verifier rejects the load. */
if (!val)
return 0;
__sync_fetch_and_add(val, 1);
return 0;
}
char LICENSE[] SEC("license") = "GPL";go deeper
Recall that a map lookup can return NULL and that eBPF makes you check for it before using the pointer. Say the fix out loud: an if on the returned pointer, then the dereference.
Explain that the verifier types every register per path, that the helper's return type is maybe-NULL, and that the branch is what refines it to a bounded map-value pointer. Mention that access width is still checked against the value size.
Generalise the pattern — NULL checks, packet bounds against data_end, masked indices — as the verifier asking for the missing step in its proof, and point out what it does not cover, such as concurrent updates or entries deleted from under you.
Frame it as the cost of load-time proof: the analyser is conservative by design, so idiomatic eBPF code is shaped by what is provable, and teams need shared patterns and helper wrappers rather than each engineer rediscovering rejections.
## What the verifier is actually tracking The verifier does abstract interpretation. For every instruction it reaches, it holds a state: for each register, what *kind* of thing it holds (a scalar, a pointer to the context, a pointer to the stack, a pointer to a map value, a pointer into a packet) and, for scalars, what range of values it could take. Instructions are checked against that state. Adding two scalars is fine. Dereferencing a scalar is not. Dereferencing a pointer is fine only if the offset provably lies inside the object the pointer points at. Helper functions are described to the verifier by a signature that includes the type of their return value. `bpf_map_lookup_elem()` is declared as returning a *map value pointer or NULL*, because that is the honest contract: if the key is not in the map, you get NULL. ## Why the dereference is rejected When your C does this: ```c __u64 *val = bpf_map_lookup_elem(&counts, &key); *val += 1; /* rejected */ ``` the verifier reaches the store instruction with the destination register still typed as maybe-NULL. There is a reachable path — the key was absent — on which that store writes to address 0 in kernel context. In user space that is a segfault of one process; here it is a kernel fault in whatever context the hook fired, possibly an interrupt. So the verifier refuses the load, and the log names the register and the offending type. Note what it is *not* saying. It is not saying your key is missing. It is saying it cannot prove it is present, and the design of eBPF is that unproven equals rejected. ## How the check changes the proof Add the branch: ```c if (!val) return 0; *val += 1; /* accepted */ ``` At the conditional jump the verifier splits its state into two: on one path the register is known-NULL, on the other it is known-non-NULL. The NULL path immediately returns, so nothing is dereferenced there. On the surviving path the register's type is **refined** from map-value-or-null to map-value, and it carries the map's value size, so the verifier can check the offset of every subsequent access against that size. The store is now provably in bounds and the program loads. This refinement-on-branch behaviour is the heart of how you argue with the verifier generally: you do not assert safety, you add code whose control flow makes the safety derivable. ## The variations that still fail - **Checking a different copy.** If you stash the pointer somewhere the verifier loses track of, then check the stashed copy, the refinement may not reach the register you dereference. Check the value you are about to use. - **Overrunning the value.** After the check the pointer is good only for the map's declared value size. Reading a `__u64` out of a map whose value is 4 bytes is a separate rejection about access size. - **Racing with delete.** Verification proves memory safety, not that the entry is stable. For a `BPF_MAP_TYPE_HASH`, another CPU may delete the entry while you hold the pointer; the update to the value itself should be atomic (`__sync_fetch_and_add()`) if concurrent CPUs touch the same key, and per-CPU maps avoid the contention entirely. The verifier will not warn you about any of this. - **Reusing it after a helper call.** Some pointer types are invalidated by intervening helper calls, and the verifier will tell you so; re-look-up rather than caching across calls. ## The general pattern Maybe-NULL is one member of a family of "refine before use" rules that the verifier enforces: - a packet pointer is unusable until you compare it against the packet end; - an index into an array must be range-checked (often masked with `& (SIZE - 1)`) before it is used as an offset; - a pointer that might be NULL must be compared against NULL. In all three cases the verifier is not asking for defensive programming as a style preference. It is asking for the exact comparison that lets its abstract interpreter narrow a range, because that narrowing is the proof. Once you see rejections as "the proof is missing a step", the error messages stop being arbitrary and become a list of the facts the verifier still needs.
- Once the NULL check has passed, how much memory may the program touch through that pointer?Exactly the map's declared value size, starting at the returned address. The verifier carries that size with the refined pointer type and checks every offset and access width against it, so reading eight bytes out of a four-byte value is rejected as an out-of-bounds access even though the pointer itself is now known non-NULL.
- Does the same requirement apply to packet pointers in a networking program?Yes, in the same shape. A packet pointer is unusable until the program compares the intended end of the access against the context's data_end; that comparison is what narrows the verifier's bounds. Skip it and you get a rejection for the same underlying reason — the proof of in-bounds access is missing.
- The verifier accepted my increment. Is the update also safe against another CPU doing the same thing?No — verification proves memory safety, not concurrency correctness. A plain read-modify-write on a shared hash-map value can lose updates. Use an atomic such as `__sync_fetch_and_add()`, or a per-CPU map and aggregate in user space. The verifier is silent on races because they are not a kernel-safety problem.
It is the same discipline as a null-checked optional in a typed language: the value has a maybe-type until a check narrows it, and only inside the checked branch does the compiler let you use it.
saying these in an interview costs you the question
- Tries to silence it with a cast or a volatile
- Thinks the error means the key is missing
- Believes a comment or annotation can assert non-NULL
- Assumes the checked pointer is then unbounded
- Treats verifier acceptance as proof of race-freedom