sk_buff Memory Layout and Headroom
Every skb points at a single contiguous head buffer that is carved into four regions by four markers —
head,data,tail, andend. The space betweenheadanddatais the headroom; the space betweendataandtailis the data (the packet bytes currently in play); the space betweentailandendis the tailroom; and sitting exactly atendis theskb_shared_infotrailer. The genius of this layout is that adding or removing a header is just moving a marker — no bytes are copied. As an outgoing packet descends the stack, each layer callsskb_push()to retreatdataleftward into the pre-reserved headroom and writes its header there; as an incoming packet climbs, each layer callsskb_pull()to advancedatarightward past the header it just consumed. The kernel documentation calls this the “Basic sk_buff geometry,” and it is verified here againstinclude/linux/skbuff.hat the v6.12 and v6.18 long-term-support tags (the geometry definitions and theskb_push/skb_pull/skb_put/skb_reservehelpers are byte-identical between the two LTS branches, diffed 2026-06-05).
This note owns the linear-buffer geometry and the pointer operations. Its sibling struct sk_buff owns the field anatomy of the metadata struct; skb_shared_info and Paged Fragments owns the skb_shared_info trailer that lives at end and the paged/scatter-gather data beyond the linear buffer. Read those for their respective depths.
Mental Model — Four Markers, One Buffer
Picture the head buffer as a single allocation. The kernel’s own ASCII diagram, lifted from the “Basic sk_buff geometry” documentation block in skbuff.h (per the source, v6.12), is:
---------------
| sk_buff |
---------------
,--------------------------- + head
/ ,----------------- + data
/ / ,----------- + tail
| | | , + end
| | | |
v v v v
-----------------------------------------------
| headroom | data | tailroom | skb_shared_info |
-----------------------------------------------
flowchart LR subgraph BUF["head buffer (one allocation)"] direction LR HR["headroom<br/>(free, before data)"] DATA["data<br/>(the packet bytes)"] TR["tailroom<br/>(free, after data)"] SI["skb_shared_info<br/>(trailer at end)"] end HEAD(["head →"]) -.-> HR D(["data →"]) -.-> DATA TAIL(["tail →"]) -.-> TR END(["end →"]) -.-> SI HR -. "skb_push: data moves LEFT<br/>(prepend header)" .-> DATA DATA -. "skb_pull: data moves RIGHT<br/>(strip header)" .-> TR
The four markers and the three regions they delimit. What it shows: head is fixed at the buffer start and end is fixed at the buffer end (where skb_shared_info sits); data and tail are the two movable markers that bound the live packet bytes. skb_push retreats data to the left, eating into headroom to make room for a prepended header; skb_pull advances data to the right, returning consumed-header space to the headroom; skb_put advances tail to the right, eating into tailroom to append data. The insight to take: prepending and stripping headers — the most frequent operation in the whole stack — costs a pointer adjustment and a few writes, never a memcpy of the payload.
The Four Markers
Two of the markers are stored as raw pointers and two as offsets, all referenced from the struct sk_buff:
head(unsigned char *) — the start of the head buffer. Fixed for the buffer’s life (until a reallocation bypskb_expand_head). All offsets in the skb are measured relative to it.data(unsigned char *) — the start of the current packet data. This is the marker layers move.skb->datais where “the packet, as this layer sees it,” begins.tail(sk_buff_data_t) — the end of the current data. On 64-bit buildssk_buff_data_tis a 32-bit offset fromhead(becauseNET_SKBUFF_DATA_USES_OFFSETis defined whenBITS_PER_LONG > 32), sotailis fetched viaskb_tail_pointer(skb)which returnsskb->head + skb->tail(per the helper, v6.12). Storing it as an offset both saves 4 bytes and lets it survive a head reallocation.end(sk_buff_data_t) — the end of the usable buffer, i.e. where theskb_shared_infotrailer begins. Also an offset on 64-bit;skb_end_pointer(skb)returnsskb->head + skb->end.
From these, three derived quantities define the regions:
- Headroom =
data - head— free bytes before the data, available for prepending headers. Returned byskb_headroom(skb), whose entire body isreturn skb->data - skb->head;. - Data length (linear) =
tail - data— the live packet bytes (this isskb_headlen(), the linear portion of the totallen). - Tailroom =
end - tail— free bytes after the data, available for appending. Returned byskb_tailroom(skb), which isreturn skb_is_nonlinear(skb) ? 0 : skb->end - skb->tail;— note it returns 0 for a non-linear skb, because once data has spilled into paged fragments you cannot meaningfully append to the linear tail (per the helpers, v6.12).
The Four Operations — All Pointer Moves, No Copies
The four canonical operations are tiny inline functions. The point of reproducing them is that they are almost nothing — which is exactly the lesson.
skb_put(skb, len) — append len bytes at the tail
Grows the data region toward end. The __skb_put inline is literally:
static inline void *__skb_put(struct sk_buff *skb, unsigned int len)
{
void *tmp = skb_tail_pointer(skb); /* remember the old tail */
SKB_LINEAR_ASSERT(skb); /* must be linear */
skb->tail += len; /* advance tail right */
skb->len += len; /* total length grows */
return tmp; /* pointer to the new space */
}The checked variant skb_put() (in skbuff.c) adds a guard: if (unlikely(skb->tail > skb->end)) skb_over_panic(...) — overrunning the tail past end is a kernel BUG, because it would scribble over the skb_shared_info trailer (per skb_put in net/core/skbuff.c, v6.12). skb_put returns a pointer to the start of the appended region so the caller can write into it — the idiom is hdr = skb_put(skb, sizeof(*hdr)); then fill hdr. This is how a layer appends (used on the build side and for trailers).
skb_push(skb, len) — prepend len bytes at the head
Grows the data region toward head by eating into headroom — the workhorse of header prepending on transmit:
void *skb_push(struct sk_buff *skb, unsigned int len)
{
skb->data -= len; /* retreat data left */
skb->len += len; /* total length grows */
if (unlikely(skb->data < skb->head)) /* ran out of headroom? */
skb_under_panic(skb, len, __builtin_return_address(0));
return skb->data; /* pointer to the new header space */
}data moves left into the headroom; the new bytes are now part of the packet, and skb->data points at the first of them so the caller writes its header there. If there is not enough headroom (data would go below head), the kernel panics via skb_under_panic — which is precisely why headroom is pre-reserved (below). On transmit, TCP skb_pushes its header, then IP skb_pushes the IP header, then the link layer skb_pushes the Ethernet header, each prepend retreating data a little further into the reserved headroom.
skb_pull(skb, len) — strip len bytes from the head
The inverse of push, the workhorse of header consumption on receive:
static inline void *__skb_pull(struct sk_buff *skb, unsigned int len)
{
skb->len -= len; /* total length shrinks */
/* (debug check that len <= linear len) */
return skb->data += len; /* advance data right, return new data */
}data moves right, past the header the current layer just finished with; that header’s bytes are now back in the (logical) headroom and skb->data points at the next layer’s header. As a received frame climbs the stack — Ethernet, then IP, then TCP — each skb_pull peels off one header. Crucially, the header-offset bookmarks (network_header, etc., on struct sk_buff) are not disturbed by the pull, so a higher layer can still locate a lower header it has already pulled past. The checked pskb_may_pull(skb, len) variant is what defensive code uses: it ensures len bytes are actually present in the linear region, dragging paged data into the linear part if necessary, and returns false (so the caller drops the packet) if the packet is too short — this is the standard guard before reading any header field off a possibly-non-linear skb.
skb_reserve(skb, len) — create headroom up front
static inline void skb_reserve(struct sk_buff *skb, int len)
{
skb->data += len; /* move BOTH data ... */
skb->tail += len; /* ... and tail to the right */
}skb_reserve is called on a fresh, empty skb (one with no data yet, where data == tail) to shift the empty data region rightward, opening up headroom before it. Because it moves data and tail together by the same amount, the data length (tail - data) stays zero while the headroom (data - head) grows by len. The documentation is explicit that this is “only allowed for an empty buffer.” This is the call that establishes the headroom so that subsequent skb_pushes have room to prepend headers without panicking.
Why Headroom Is Pre-Reserved — NET_SKB_PAD
Here is the crux. If a freshly allocated skb had data == head (zero headroom), the first skb_push of any header would immediately panic — there would be nowhere to prepend. To avoid that, every skb is allocated with headroom already reserved, so each layer can prepend its header cheaply, and only an unusually deep header stack ever forces a reallocation.
The standard amount is NET_SKB_PAD, defined as max(32, L1_CACHE_BYTES) (per include/linux/skbuff.h, v6.12 — identical in v6.18). The source comment explains the two reasons for the value precisely:
- Avoid reallocation on header growth. “The networking layer reserves some headroom in skb data … used to avoid having to reallocate skb data when the header has to grow. In the default case, if the header has to grow 32 bytes or less we avoid the reallocation.” A typical received Ethernet+IPv4+TCP packet, once delivered, may need a few bytes of headroom for retransmit or tunnel encapsulation; 32+ bytes covers the common cases.
- Cache-line packing. “Using
max(32, L1_CACHE_BYTES)makes sense (especially with RPS) to reduce average number of cache lines per packet.” Aligning the headroom to a cache line keeps the hot header bytes (NET_IP_ALIGN(2) + ethernet(14) + IP(20) + ports(8)) within one 64-byte block thatget_rps_cpu()and the flow dissector touch.
The allocator wires this in: __netdev_alloc_skb does len += NET_SKB_PAD and then skb_reserve(skb, NET_SKB_PAD), so a driver that asks for an N-byte buffer transparently gets NET_SKB_PAD bytes of headroom plus its N bytes — “Users should allocate the headroom they think they need without accounting for the built-in space,” as the __netdev_alloc_skb documentation puts it. The NAPI-context allocator napi_alloc_skb reserves a little more, NET_SKB_PAD + NET_IP_ALIGN, folding in the IP-alignment pad as well (all per net/core/skbuff.c, v6.12).
There is a related constant, NET_IP_ALIGN (default 2). Because the Ethernet header is 14 bytes, reserving 2 extra bytes of headroom makes the IP header land on a 4-byte boundary, which matters on architectures that fault or slow down on unaligned multi-byte loads. So a receive skb’s data typically starts at head + NET_SKB_PAD + NET_IP_ALIGN. The two constants serve different goals — NET_SKB_PAD is room for prepending and cache packing, NET_IP_ALIGN is alignment of the IP header — and on most x86 builds NET_IP_ALIGN is 2 while some architectures override it to 0 because their hardware tolerates unaligned access at no cost.
For the transmit side, the device’s dev->needed_headroom lets a stacked/tunneling device advertise extra headroom it will need (for an encapsulation header it intends to prepend), and the higher layers honor it when allocating, so the eventual encapsulation skb_push finds room without reallocating.
The Trailer at end — skb_shared_info
The bytes from end to the true end of the allocation are not tailroom — they hold the struct skb_shared_info trailer, reachable via skb_shinfo(skb), which is defined as ((struct skb_shared_info *)(skb_end_pointer(skb))) — i.e. the shared-info struct sits exactly at end (per the macro, v6.12). This trailer holds the array of paged fragments (frags[]), the frag_list chain pointer, Generic Segmentation Offload sizing, and the dataref count that tracks sharing of the data buffer. The allocator deliberately places it “exactly at the end of the allocated zone, to allow max possible filling before reallocation” — meaning the linear data region can grow toward end as far as possible before the shared-info trailer is hit. The whole reason skb_tailroom() is end - tail (and not the literal end of the allocation) is to stop skb_put from overwriting this trailer. The trailer’s full contents and the paged-fragment model are the subject of skb_shared_info and Paged Fragments.
The macro SKB_WITH_OVERHEAD(X) captures the relationship: for a head allocation of X bytes, the data area available is X - SKB_DATA_ALIGN(sizeof(struct skb_shared_info)) — the allocation minus the aligned trailer.
Worked Example — Building a Packet on Transmit
Following the source’s own description of the build sequence and the helpers above, a transmit skb’s geometry evolves like this:
- Allocate a buffer with
NET_SKB_PAD(and, for tunnels,dev->needed_headroom) bytes of headroom reserved viaskb_reserve. Nowdata == tail, headroom = the reserved amount, data length = 0. skb_putthe payload — copy the user’s bytes in, advancingtail. Now data length = payload size.skb_pushthe TCP header —dataretreats left into headroom; write the TCP header there;skb_reset_transport_header()bookmarks it.skb_pushthe IP header —dataretreats further; write the IP header;skb_reset_network_header()bookmarks it.skb_pushthe Ethernet header —dataretreats again; write the MAC header;skb_reset_mac_header()bookmarks it.
Each step that prepended a header was a pointer subtraction and a memory write into the pre-reserved headroom — the payload, sitting between data and tail, was never touched or copied. On receive the mirror runs with skb_pull peeling headers off in the opposite order. This is the concrete payoff of the headroom design.
Failure Modes
skb_under_panic— ran out of headroom. Askb_pushof more bytes thanskb_headroom()provides. Symptom: a kernel panic/oops withskb_under_panicin the trace. Cause: a device or tunnel that prepends a header the allocated headroom did not account for — the fix is to advertisedev->needed_headroomor callskb_cow_head()/pskb_expand_head()to grow the headroom (which does reallocate and copy) before pushing.skb_over_panic— ran out of tailroom. Askb_putpastend, which would clobberskb_shared_info. Same class of bug on the append side.- Reading paged bytes as linear. Dereferencing
skb->data + offsetfor a field that lives pastskb_headlen()on a non-linear skb reads garbage (or faults). The discipline is to callpskb_may_pull(skb, needed)first; it returns false (→ drop) if the bytes are not present and pulls them linear if they are. - Assuming tailroom on a non-linear skb.
skb_tailroom()returns 0 oncedata_len > 0; code that ignores this and tries toskb_putwill not find the space it expects. - Headroom destroyed by
pskb_expand_head. Reallocating the head buffer (to grow head/tailroom) copies the data and resets the markers; raw pointers into the old buffer dangle. This is why the kernel stores header positions as offsets and re-derives pointers through the accessors after any operation that might expand the head.
See Also
- struct sk_buff — the metadata struct whose
head/data/tail/endfields are the markers described here - skb_shared_info and Paged Fragments — the trailer that lives at
end, the paged fragments holdingdata_lenworth of bytes, and thefrag_listchain - sk_buff Clones and Copies — when
pskb_expand_headmust copy the buffer, and the headerless-clone headroom dance - sk_buff Allocation and Lifetime —
__netdev_alloc_skb/napi_alloc_skbreservingNET_SKB_PAD,build_skb, and freeing - The Network Transmit Path · The Network Receive Path — where the push/pull sequence actually runs
- Checksum Offloads —
csum_start/csum_offsetmeasured fromhead, the same offset scheme - MOC: Linux Networking Stack MOC