struct sk_buff — the socket buffer, universally abbreviated skb — is “the main networking structure representing a packet” (kernel docs). It is the kernel’s universal packet container: a single skb is allocated near the bottom of the receive path (or the top of the transmit path) and then carried, unchanged in identity, through every layer of the stack — driver, Internet Protocol (IP), Transmission Control Protocol (TCP) or User Datagram Protocol (UDP), netfilter, the queueing disciplines — each layer reading and rewriting the same object rather than copying the packet into a new representation. The skb is deliberately a metadata struct that holds no packet bytes itself: “struct sk_buff itself is a metadata structure and does not hold any packet data. All the data is held in associated buffers” (include/linux/skbuff.h, “DOC: Basic sk_buff geometry”, v6.12). Understanding the field groups of this one structure is the prerequisite for understanding essentially everything else in the Linux networking stack.
Version pin. Every field, constant, and code excerpt below was read directly from the v6.12 tag of the mainline tree (Linux 6.12, released 2024-11-17, a maintained long-term-support branch), fetched from raw.githubusercontent.com and verified on 2026-08-29. Where a fact differs at the newer v6.18 LTS tag it is called out and dated. The headline result of that comparison is worth stating up front: struct sk_buff is byte-for-byte textually identical at v6.12 and v6.18 — a 219-line diff with zero differences — so nothing in this note’s structural walk-through is stale for the newer branch. struct skb_shared_info, its companion, did change; see The Shared-Info Trailer, and Why Its Size Matters.
This note owns the anatomy of the structure — the field groups, what each does, what a copy copies, what a free costs. Its siblings own the mechanisms that operate on it: sk_buff Memory Layout and Headroom owns the linear-buffer geometry (the head/data/tail/end pointer scheme and the skb_reserve/skb_put/skb_push/skb_pull operations); sk_buff Allocation and Lifetime owns how skbs are born and freed; sk_buff Clones and Copies owns the clone-versus-copy distinction and the two reference counts; and skb_shared_info and Paged Fragments owns the scatter-gather trailer. Read those for depth on each; here we map the whole structure and the metadata semantics that the other notes assume.
Mental Model — One Object, Every Layer
The single most important idea is that an skb is a unit of work that flows through a pipeline, not a buffer that gets recreated at each boundary. When a frame is received, the driver (or the Generic Receive Offload path) allocates one skb; the same pointer is then handed up through ip_rcv → tcp_v4_rcv → the socket receive queue. On transmit, tcp_sendmsg builds an skb and the same pointer descends through ip_output → netfilter → the qdisc → ndo_start_xmit. Each layer manipulates the skb in place: it advances or retreats a data pointer to expose or hide its header (see sk_buff Memory Layout and Headroom), it stashes per-layer scratch state in the control buffer, and it reads metadata other layers set (the incoming device, the protocol, the checksum status, the route). Avoiding a per-layer copy of the packet payload is the entire performance reason the structure exists.
flowchart TB
subgraph META["struct sk_buff — pure metadata, one allocation from skbuff_head_cache"]
LINK["<b>Linkage</b><br/>next / prev / rbnode / list / ll_node<br/>(a union — list OR tree, never both)"]
OWN["<b>Ownership & accounting</b><br/>sk (owning socket) · destructor<br/>dev (in/out device) · truesize"]
ROUTE["<b>Attached route</b><br/>_skb_refdst (tagged dst_entry pointer)"]
CB["<b>cb[48]</b><br/>per-layer private scratch"]
LEN["<b>Lengths</b><br/>len / data_len / mac_len / hdr_len"]
HDR["<b>Header bookmarks (u16 offsets from head)</b><br/>mac_header / network_header / transport_header<br/>+ inner_* for tunnels"]
FLAGS["<b>Flags & class</b><br/>pkt_type / ip_summed / cloned / fclone<br/>protocol / encapsulation / pp_recycle"]
CSUM["<b>Checksum</b><br/>csum ∪ (csum_start, csum_offset)<br/>csum_level / csum_valid"]
STEER["<b>Steering & policy</b><br/>hash / priority / mark / tc_index<br/>queue_mapping / napi_id / skb_iif"]
EXT["<b>Extensions</b><br/>_nfct (conntrack) · extensions (skb_ext)"]
GEOM["<b>Geometry</b><br/>head / data / tail / end"]
REF["<b>users</b> (refcount_t)"]
end
GEOM -->|"point into"| BUF["<b>head buffer</b> — a second, separate allocation<br/>headroom │ data │ tailroom │ skb_shared_info"]
BUF -->|"frags[] / frag_list"| PAGES["paged fragments (pages)<br/>and chained skbs"]
OWN -.->|"charged against"| SOCK["struct sock<br/>sk_wmem_alloc / sk_rmem_alloc"]
ROUTE -.-> DST["struct dst_entry<br/>(the routing cache result)"]
The field groups of struct sk_buff at v6.12 and what each references. What it shows: the skb is pure metadata — linkage so it can sit on queues and trees, ownership pointers plus a destructor for socket-memory accounting, an attached route, a 48-byte scratch cb[], length and header-offset bookkeeping, status flags, checksum state, steering/policy tags, optional extension blobs, the four geometry pointers, and a reference count — while the actual packet bytes live in a separately allocated head buffer that the geometry pointers reference. The insight to take: because the data is decoupled from the metadata, the kernel can clone an skb (new metadata, shared data) cheaply, every layer can rewrite headers in the shared buffer without disturbing the others’ state, and the per-packet cost splits into two independent allocation problems the kernel optimizes separately.
The Struct at v6.12, In Declaration Order
Before walking the groups, it is worth seeing the order, because the order is not arbitrary: hot fields are packed toward the front so that a single 64-byte cache line covers as much of the working set as possible, and the geometry pointers are pinned to the very end with the comment “These elements must be at the end, see alloc_skb() for details” — __alloc_skb() clears the struct from tail backwards in one shot, so those fields must be contiguous and last.
L2 header length; writable header length of a clone
9
queue_mapping
2 B
TX queue selected for this packet
10
cloned:1 … pp_recycle:1
1 B
Clone/allocation status bits — not bulk-copied
11
active_extensions
1 B
Bitmap of live skb_ext entries
12
struct_group(headers, …)
~90 B
Everything __copy_skb_header() bulk-memcpys
13
tail, end
8 B
Geometry, as 32-bit offsets from head
14
head, data
16 B
Geometry, as real pointers
15
truesize
4 B
Total memory charged for this packet
16
users
4 B
Reference count on the metadata
17
extensions
8 B
Pointer to the skb_ext blob
Declaration order of struct sk_buff at v6.12, with the size each region occupies on a 64-bit build with a typical distribution config. What it shows: the struct is a carefully ordered sequence of regions, not a bag of fields — the union-heavy linkage comes first (it must, to alias struct sk_buff_head), the 48-byte scratch buffer sits in the middle, the bulk-copied headers group is one contiguous block, and the geometry pointers are forced to the end by alloc_skb()’s clearing trick. The insight to take: several apparent oddities in the struct — the unions, the struct_group, the “must be at the end” comment — exist because two different pieces of code (__alloc_skb and __copy_skb_header) want to treat contiguous ranges of the struct as single memset/memcpy targets. Layout here is a performance interface, and changing field order is an API change in practice.
The sizes above are indicative rather than authoritative because a large fraction of the struct is conditionally compiled: _nfct exists only with CONFIG_NF_CONNTRACK, active_extensions/extensions only with CONFIG_SKB_EXTENSIONS, secmark only with CONFIG_NETWORK_SECMARK, the tc_at_ingress/tc_skip_classify bits only with CONFIG_NET_XGRESS, napi_id only with CONFIG_NET_RX_BUSY_POLL or CONFIG_XPS, and so on. Two kernels built from different configs genuinely have differently sized skbs.
Linkage — How an skb Sits on Queues and Trees
The first members form a union so that one skb can be threaded onto different container types without paying for all of them at once:
union { struct { /* These two members must be first to match sk_buff_head. */ struct sk_buff *next; struct sk_buff *prev; union { struct net_device *dev; /* Some protocols might use this space to store information, * while device pointer would be NULL. * UDP receive path is one user. */ unsigned long dev_scratch; }; }; struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */ struct list_head list; struct llist_node ll_node;};
The next/prev pair is the classic doubly-linked-list embedding, and the comment above it is load-bearing. struct sk_buff_head — the queue head used for socket receive queues, qdisc queues, and driver backlogs — begins with the same two pointers, wrapped in a struct_group_tagged(sk_buff_list, …), followed by qlen and a spinlock. Because the first 16 bytes of both structures have identical layout, a queue head can be cast to an skb and spliced onto directly, and skb_queue_head_init() can point a head’s next/prev at itself to form an empty circular list. This is C type-punning used deliberately as an interface.
Alternatively the same storage is reinterpreted as an rb_node — a red-black-tree node — when the skb lives in a tree rather than a list, because some queues need ordered lookup rather than FIFO. The source comment names the three users: netem (the network-emulator qdisc, which must dequeue in delayed-departure-time order), IPv4 defragmentation (fragments arrive out of order and are indexed by offset), and the TCP stack (both the out-of-order receive queue and, since the “RB-tree for retransmit queue” rework, the write queue). struct list_head covers the small number of places that want the generic kernel list API, and llist_node covers lockless single-linked lists such as the deferred-free lists used to move skb frees off a hot path.
flowchart LR
subgraph U["The first 24 bytes — one union, four interpretations"]
direction TB
A["<b>next / prev / dev</b><br/>doubly-linked list"]
B["<b>rbnode</b><br/>red-black tree node"]
C["<b>list</b><br/>generic list_head"]
D["<b>ll_node</b><br/>lockless llist"]
end
A --> A1["socket receive queue<br/>qdisc queue · driver backlog<br/><i>aliases struct sk_buff_head</i>"]
B --> B1["TCP out-of-order queue (by seq)<br/>IPv4 defrag (by offset)<br/>netem (by departure time)"]
C --> C1["generic kernel list users"]
D --> D1["deferred-free / cross-CPU<br/>lockless handoff"]
A1 -.->|"mutually exclusive"| B1
The four alternative shapes of the skb’s first 24 bytes. What it shows: an skb is on a list, or in a tree, or on an llist — never two at once — and the list form deliberately aliases the layout of struct sk_buff_head. The insight to take: the union is safe precisely because “queued somewhere” is a single-valued property of an skb; the kernel spends zero extra bytes supporting four container types, and the aliasing with sk_buff_head is what makes __skb_queue_tail() a handful of pointer stores rather than a function call into a generic list library.
Ownership — sk, the Destructor, and Socket Memory Accounting
struct sock *sk is the owning socket. This is a far more interesting field than “a back-pointer,” because in the transmit direction it is half of the kernel’s socket-memory accounting scheme, and the other half is void (*destructor)(struct sk_buff *skb).
Consider what happens when a socket sends. skb_set_owner_w() in net/core/sock.c (v6.12) attaches the skb to the socket:
void skb_set_owner_w(struct sk_buff *skb, struct sock *sk){ skb_orphan(skb); /* detach from any previous owner first */ skb->sk = sk; ... skb->destructor = sock_wfree; /* run this when the skb is freed */ skb_set_hash_from_sk(skb, sk); /* * We used to take a refcount on sk, but following operation * is enough to guarantee sk_free() won't free this sock until * all in-flight packets are completed */ refcount_add(skb->truesize, &sk->sk_wmem_alloc);}
Read the last two lines slowly, because they encode a genuinely clever trick. sk_wmem_alloc is a refcount_t that simultaneously serves as a byte counter and a reference count. Attaching an skb adds skb->truesize (the packet’s full memory footprint) to it. The socket’s own existence is worth one unit of that counter. When the skb is finally freed — typically in the driver’s TX-completion handler, long after send() returned — sock_wfree() runs as the destructor and subtracts truesize back:
void sock_wfree(struct sk_buff *skb){ struct sock *sk = skb->sk; unsigned int len = skb->truesize; ... if (refcount_sub_and_test(len, &sk->sk_wmem_alloc)) __sk_free(sk);}
So a socket whose last in-flight packet completes after the socket was closed is freed by the packet, not by close(). There is no separate sock_hold() per skb: the byte count is the reference count, which saves an atomic operation per packet on the hottest path in the kernel. On the way, sock_wfree() also calls sk->sk_write_space(sk) to wake writers blocked in send() — which is why TX completion, not send() return, is what unblocks a full socket buffer.
The receive side is simpler and symmetric: sock_rfree() subtracts truesize from sk_rmem_alloc and calls sk_mem_uncharge(), which is how a receive queue enforces SO_RCVBUF. Note the unit of accounting throughout is truesize, notlen: a 1-byte UDP datagram sitting in a receive buffer might carry a truesize of two kilobytes or more, which is why SO_RCVBUF is documented as an approximate limit and why a flood of tiny packets can exhaust a receive buffer that, counted in payload bytes, looks nearly empty.
sequenceDiagram
autonumber
participant App as Application
participant Sock as struct sock
participant Skb as skb
participant Drv as Driver / NIC
App->>Skb: send() builds skb
Skb->>Sock: skb_set_owner_w()
Note over Skb,Sock: skb->sk = sk<br/>skb->destructor = sock_wfree<br/>sk_wmem_alloc += skb->truesize
Skb->>Drv: descends qdisc → ndo_start_xmit
App->>Sock: send() returns (buffer still charged!)
Drv-->>Skb: TX completion IRQ → consume_skb()
Skb->>Sock: destructor: sock_wfree()
Note over Sock: sk_wmem_alloc -= truesize<br/>sk_write_space(sk) wakes blocked writers
alt sk_wmem_alloc hits zero after close()
Sock->>Sock: __sk_free(sk) — the last packet frees the socket
end
The lifetime of socket-write accounting for one transmitted skb. What it shows: the charge is taken when the skb is attached to the socket and released by the skb’s own destructor at TX completion — not when send() returns. The insight to take:sk_wmem_alloc is a combined byte-counter and reference-count, so socket teardown is naturally deferred until the last in-flight packet completes; and because the accounting unit is truesize, SO_SNDBUF/SO_RCVBUF limits bite on memory footprint, not on payload bytes. A stuck TX completion (a wedged driver ring) therefore manifests as a socket that will not accept writes even though nothing appears to be queued.
The inverse operation, skb_orphan(), is worth knowing by name because it appears all over the forwarding and tunnelling code:
Orphaning runs the destructor early — releasing the socket’s accounting immediately — and severs the ownership link. Anything that will hold a packet for an unbounded time (a tunnel device, a bridge, a virtual-machine backend) orphans it, because otherwise a single slow consumer would pin the sender’s socket buffer indefinitely. The trade-off is real and well known: orphaning defeats TCP Small Queues, which relies on sk_wmem_alloc to bound how much of a flow can sit in device queues, so early orphaning in a virtual NIC can reintroduce buffer bloat for guest traffic.
A packet being forwarded — routed through the box but not destined for a local socket — has sk == NULL throughout; it belongs to no socket and pays no accounting.
struct net_device *dev is the device the packet arrived on (receive) or is leaving by (transmit). Note the union with dev_scratch: when a protocol does not need dev, it may borrow those 8 bytes for its own use rather than let them idle, and the source names the UDP receive path as one such user.
_skb_refdst — The Attached Route, and a Pointer with a Bit Stolen
unsigned long _skb_refdst holds the destination cache entry (struct dst_entry) that the routing lookup produced for this packet: the next hop, the output device, the MTU, and the cached neighbour entry. It is stored as an unsigned long rather than a typed pointer for one reason — the kernel steals the low bit:
packet-beta
0: "NOREF"
1-63: "dst_entry pointer value (a dst_entry is at least 8-byte aligned, so bits 0-2 are structurally zero and bit 0 is free to steal)"
The _skb_refdst word, drawn least-significant-bit first — mermaid’s packet-beta numbers positions left-to-right from 0, and here position 0 is deliberately mapped to the kernel’s bit 0, the least-significant bit, which is the bit SKB_DST_NOREF occupies. (The 64-bit word wraps onto two 32-bit rows in the rendered grid; that is the renderer’s fixed row width, not a structural boundary.) What it shows: a struct dst_entry * is at least 8-byte aligned, so its three low bits are structurally zero and one of them can carry a flag for free. The insight to take:SKB_DST_NOREF means “this route pointer is borrowed — no reference was taken on it.” Borrowing is legal only inside an RCU read-side critical section on the input path, where the route cannot be freed underneath you; the moment a packet is queued, deferred, or handed to another context, skb_dst_force() must convert the borrowed pointer into a counted one. This one bit saves an atomic increment and decrement per forwarded packet, which at ten million packets per second is not a micro-optimization.
skb_dst_drop() consults that bit on free and only calls dst_release() when a reference was actually taken. __skb_dst_copy(), used when cloning, does the same test before calling dst_clone() — and, notably, also sets nskb->slow_gro |= !!refdst, marking that this skb carries state (a route) that the fast GRO merge path must not silently discard. The slow_gro bit is a small but instructive example of a recurring pattern in this struct: a one-bit summary that lets a hot path skip a check entirely in the common case.
tstamp — Receive Timestamp or Earliest Departure Time
union { ktime_t tstamp; u64 skb_mstamp_ns; /* earliest departure time */};
The same 8 bytes mean two different things depending on direction, and conflating them is a common source of confusion when reading the TX path. On receive, tstamp is the packet’s arrival timestamp, populated by net_timestamp_check() when a socket has asked for timestamps (SO_TIMESTAMPNS) or when a tap requires them. On transmit, the field is the earliest departure time (EDT): the nanosecond at which this packet is allowed to leave, in the clock base named by the two-bit tstamp_type field.
EDT is the foundation of modern Linux pacing. Instead of a shaper deciding “how long shall I sleep before sending this,” TCP itself stamps each skb with the time its congestion control and pacing rate say it should depart, and the fq qdisc simply refuses to release a packet before its stamp. The consequence for reading code is that skb->tstamp on the TX side is not “when this was sent” — it is “the earliest this may be sent” — and a nonzero value in a packet capture on egress is a pacing artefact, not a measurement.
Lengths — len, data_len, mac_len, hdr_len
unsigned int len, data_len;__u16 mac_len, hdr_len;
len is the total length of the packet the skb represents — every payload byte across the linear buffer and any paged fragments and chained skbs. data_len is the length of the data held outside the linear head buffer. The difference, computed by skb_headlen() as skb->len - skb->data_len, is the linear length: the bytes in the contiguous head buffer. When data_len is zero the skb is linear; when it is non-zero the skb is non-linear (paged), and skb_is_nonlinear() tests exactly that.
This distinction is the single most common source of kernel networking bugs written by newcomers. Most helpers, and all raw pointer arithmetic off skb->data, only reach the linear region. Code that needs to read bytes that might be paged must first call pskb_may_pull(skb, n), which drags the first n bytes into the linear part (reallocating if necessary) and returns false if the packet is simply too short. Skipping that check produces a read of whatever happens to sit past the linear region — usually the skb_shared_info trailer, which is why the resulting corruption is so spectacular. The linear/paged split itself is the subject of skb_shared_info and Paged Fragments.
mac_len is the length of the link-layer (Media Access Control, MAC) header. hdr_len records the writable header length of a cloned skb and is part of the headerless-clone machinery: the transport layer marks a payload-only skb with __skb_header_release(), and any clone taken from it gets hdr_len populated with the available headroom so the lower layers know how much of the buffer they may write. The kernel’s own documentation is unusually candid about this design — “This is not a very generic construct and it depends on the transport layers doing the right thing” (skbuff.h, “DOC: dataref and headerless skbs”, v6.12). The mechanism is walked in sk_buff Clones and Copies.
These three __u16 fields are the header location bookmarks, and they are one of the most elegant pieces of the design. Each is not a pointer but a 16-bit offset measured from skb->head. The accessor skb_network_header(skb) returns skb->head + skb->network_header; skb_reset_network_header(skb) stores the current position as skb->data - skb->head. Storing offsets rather than raw pointers cuts the storage to a quarter — 2 bytes instead of 8 on a 64-bit machine, and there are seven such fields counting the inner set, so 14 bytes instead of 56 — and, crucially, makes the bookmarks survive a reallocation of the head buffer. If pskb_expand_head() moves the data into a larger allocation, the offsets remain valid relative to the new head, whereas raw pointers would dangle and every layer would have to be told to fix them up.
The reason all three coexist is that an skb in flight has several headers stacked in its buffer at once. A received TCP segment, just before delivery, has its Ethernet header, its IP header, and its TCP header all sitting in the linear buffer at known offsets. As a layer “consumes” its header on the way up (via skb_pull, see sk_buff Memory Layout and Headroom), skb->data advances past it — but the bookmark for that header stays put, so netfilter, a tap, or a checksum routine can still find the bytes. transport_header is special-cased with a sentinel: it is initialised to (u16)~0U, and skb_transport_header_was_set() tests against that value, so the stack can distinguish “transport header not yet identified” from “transport header at offset 0.”
flowchart LR
subgraph BUF["head buffer, byte offsets measured from skb->head"]
direction LR
H0["headroom"] --- MAC["Ethernet<br/>14 B"] --- IP["IPv4<br/>20 B"] --- TCP["TCP<br/>20-60 B"] --- PAY["payload"] --- TR["tailroom"] --- SI["skb_shared_info"]
end
MH["mac_header"] -.-> MAC
NH["network_header"] -.-> IP
TH["transport_header"] -.-> TCP
DP["skb->data<br/><i>(moves as layers pull)</i>"] ==> MAC
DP2["skb->data after ip_rcv pulls"] ==> TCP
The three header bookmarks over a received TCP-over-IPv4-over-Ethernet frame. What it shows:mac_header, network_header and transport_header are fixed u16 offsets that each point at one header, while skb->data is a moving cursor that advances as each layer consumes its header. The insight to take: the bookmarks decouple “where a header is” from “which layer currently owns the packet.” That is what lets tcpdump re-render the full Ethernet frame from an skb that TCP is already processing, and what lets netfilter’s POSTROUTING hook rewrite an IP address on a packet whose data pointer has long since moved past the IP header.
protocol is a __be16 (big-endian 16-bit) holding the EtherType — ETH_P_IP (0x0800) for IPv4, ETH_P_IPV6 (0x86DD) for IPv6 — and it selects which Layer-3 handler the packet is dispatched to. A small but frequently misstated detail: on Ethernet receive, eth_type_trans() does not itself assign skb->protocol; it returns the EtherType, and the caller writes it (skb->protocol = eth_type_trans(skb, dev)). What eth_type_trans() does set directly is skb->dev, the MAC header bookmark (skb_reset_mac_header), and the packet class via eth_skb_pkt_type() (net/ethernet/eth.c, v6.12).
There is a parallel set of inner_mac_header, inner_network_header, inner_transport_header offsets plus inner_protocol, used when the packet is encapsulated in a tunnel (VXLAN, GENEVE, GRE), so the stack can address both the outer and the inner header stacks. Their validity is signalled by the encapsulation flag bit. This is what makes tunnel-aware offloads possible at all: a NIC asked to segment a VXLAN-encapsulated TCP stream needs to be told where the inner TCP header lives, and these fields are how the kernel tells it (see Segmentation Offloads GSO TSO).
The Control Buffer — 48 Bytes of Per-Layer Scratch
/* * This is the control buffer. It is free to use for every * layer. Please put your private variables there. If you * want to keep them across layers you have to do a skb_clone() * first. This is owned by whoever has the skb queued ATM. */char cb[48] __aligned(8);
The control buffer is 48 bytes of per-layer scratch space, and the convention is that whatever layer currently owns the skb may cast cb[] to its own private struct and stash state there. TCP overlays struct tcp_skb_cb; the IPv4 input path overlays struct inet_skb_parm; the qdisc layer overlays struct qdisc_skb_cb; a bridge overlays struct br_input_skb_cb. The overlay is done by a macro that is simply a cast — from include/net/tcp.h at v6.12:
struct tcp_skb_cb is a good example of how much gets packed into those 48 bytes: the segment’s seq and end_seq, tcp_gso_segs/tcp_gso_size (the per-skb segment count and MSS used by the write queue), tcp_flags, the SACK state byte sacked, ip_dsfield, an ack_seq, and then a union that is either a tx sub-struct (delivery-rate-estimation state: delivered, delivered_ce, first_tx_mstamp, delivered_mstamp) for outgoing skbs, or the IPv4/IPv6 inet_skb_parm header parameters for incoming ones. Even the comment inside acknowledges the squeeze: “There is space for up to 24 bytes.”
The catch is ownership. cb[] belongs to “whoever has the skb queued at the moment,” so a later layer is entirely free to overwrite what an earlier one wrote, and the kernel enforces nothing. The documented way to keep a copy across a handover is to clone: a clone gets a fresh struct sk_buff, and __copy_skb_header() does memcpy(new->cb, old->cb, sizeof(old->cb)), so the clone carries a private snapshot the other layers will not touch.
flowchart TB
SKB["one skb, 48 bytes of cb[]"]
subgraph T["…reinterpreted at each stage of the journey"]
direction TB
S1["<b>RX, IP layer</b><br/>struct inet_skb_parm<br/>IPCB(skb): options, flags, frag_max_size"]
S2["<b>RX, TCP layer</b><br/>struct tcp_skb_cb<br/>TCP_SKB_CB(skb): seq, end_seq, tcp_flags, sacked"]
S3["<b>TX, qdisc layer</b><br/>struct qdisc_skb_cb<br/>qdisc_cb(skb): pkt_len, classifier scratch"]
S4["<b>bridge</b><br/>struct br_input_skb_cb"]
S1 --> S2 --> S3
S4 -.-> S3
end
SKB --> T
T --> W["<b>Every overlay is checked at build time</b><br/>a BUILD_BUG_ON in each user asserts<br/>sizeof(private struct) <= sizeof_field(struct sk_buff, cb)"]
The cb[] array as a sequence of type-punned overlays. What it shows: the same 48 bytes are reinterpreted as a different private structure by each layer that owns the packet, with no runtime tagging of which interpretation is currently live. The insight to take:cb[] is scratch, not storage — a value written by one layer is only guaranteed valid until the skb changes hands, which is exactly why “clone it if you need to keep it” is written into the field’s own comment. The 48-byte size is also a hard, build-enforced budget: adding a field to struct tcp_skb_cb that overflows it breaks the build, which is a recurring friction point in netdev patch review.
The Flag Bytes, Drawn Bit by Bit
Two single-byte bitfield groups carry most of the skb’s status, and both are given named zero-length markers (__cloned_offset and __pkt_type_offset) so that assembly and eBPF code can compute their positions. Those markers exist because the bits are addressed by mask from outside C — the header even defines the mask explicitly, and in doing so tells you the bit order:
On a little-endian machine cloned is bit 0 — the least-significant bit — which confirms the general rule that the first-declared bitfield member occupies the lowest-order bits. The two bytes therefore look like this:
The __cloned_offset byte at v6.12, drawn least-significant-bit first to match little-endian bitfield allocation (CLONED_MASK == 1). What it shows: eight allocation- and sharing-status bits packed into one byte, positioned so a single load-and-mask can test them. The insight to take: this byte sits deliberately above the struct_group(headers, …) marker, which means it is not part of the block that __copy_skb_header() bulk-copies — and that is correct, because “am I a clone?”, “was my head allocated from a page fragment?” and “is my page page-pool-recyclable?” are properties of this particular skb’s allocation, not of the packet it describes. Copying them into a new skb would be a use-after-free waiting to happen.
Taking those bits one at a time: cloned marks that the head buffer may be shared with another skb, and together with skb_shared_info.dataref it is what skb_cloned() tests before any code writes the buffer. nohdr marks a payload-only skb (see the headerless-clone discussion above). fclone (two bits: SKB_FCLONE_UNAVAILABLE, _ORIG, _CLONE) records “fast clone” status, an allocation optimisation in which an skb and its eventual clone are allocated together as one struct sk_buff_fclones object from a dedicated slab cache — TCP uses it because it clones essentially every transmitted segment for the retransmit queue. peeked marks that the packet has already been seen by a MSG_PEEK receive so it is not double-counted in statistics. head_frag says the head buffer came from a page fragment allocator rather than kmalloc, which determines how it must be freed. pfmemalloc says the allocation dipped into emergency memory reserves, which restricts the packet to swap-related traffic. pp_recycle says the buffer’s pages came from a page pool and should be returned to it instead of freed — the hint added by the 2021 page_pool: recycle buffers series (LWN’s copy of the posting), whose whole point was that “instead of allocating buffers specifically for SKBs we now allocate a generic buffer and either wrap it on an SKB (via build_skb) or create an XDP frame.”
The second marked byte opens the bulk-copied headers group:
The __pkt_type_offset byte at v6.12, again least-significant-bit first. What it shows: the packet’s delivery class and its checksum status share a single byte, which is why so much of the receive path can make both decisions from one memory access. The insight to take:pkt_type being three bits is the reason PKT_TYPE_MAX is 7 and the reason the PACKET_* constant space stops at PACKET_KERNEL (7) — the enumeration is not extensible without restructuring the byte.
pkt_type is the packet class, taken from the PACKET_* constants in include/uapi/linux/if_packet.h (v6.12): PACKET_HOST (0, “to us”), PACKET_BROADCAST (1), PACKET_MULTICAST (2), PACKET_OTHERHOST (3, “to someone else” — seen because the interface is promiscuous), PACKET_OUTGOING (4), PACKET_LOOPBACK (5), PACKET_USER (6) and PACKET_KERNEL (7). The mechanism that sets it on Ethernet is worth reading, because it is a study in optimising the common case:
Note what is absent: there is no assignment for PACKET_HOST. The freshly allocated skb has a zeroed pkt_type, and PACKET_HOST is zero, so the overwhelmingly common case — a unicast frame addressed to this interface — costs one comparison and no store at all. Every branch is marked unlikely(). ether_addr_equal_64bits() compares a six-byte MAC address with a single 64-bit load, tolerating the two bytes of over-read because the Ethernet header is never the last thing in the buffer.
ignore_df suppresses the IPv4 Don’t-Fragment check (used by tunnels). dst_pending_confirm asks the neighbour layer to confirm reachability, feeding the ARP/NDP state machine from TCP’s own evidence of forward progress. ooo_okay tells the transmit-queue selection code that this socket currently has no packets in flight, so it is safe to move the flow to a different hardware queue without risking reordering — the small piece of state that makes XPS (Transmit Packet Steering) reorder-safe.
Checksum State — the Field Group Everyone Misreads
Four values of ip_summed mean four entirely different things, and — this is the part that trips people — their meanings differ between the receive and transmit directions. The kernel’s own skbuff.h documentation block, “DOC: skb checksums,” is the primary text; the mechanism as a whole belongs to Checksum Offloads, but the fields belong here, because they are the direct bridge from this structure to Segmentation Offloads GSO TSO (hardware segmentation is impossible without partial-checksum offload).
On receive, ip_summed is the device telling the stack what it already verified:
CHECKSUM_NONE — “I did not check.” The packet contains a full but unverified checksum; skb->csum is undefined.
CHECKSUM_UNNECESSARY — “I parsed the headers and the checksum was good.” skb->csum is still undefined. The companion csum_level field counts how many consecutive checksums were verified, minus one: the documentation’s worked example is an IPv6→UDP→GRE→IPv4→TCP packet where a device verifying UDP, GRE and TCP sets csum_level to two. A device must never rewrite the checksum field in the packet even when it verified it.
CHECKSUM_COMPLETE — “here is the one’s-complement sum of the whole packet, in skb->csum.” This is the most general form because the hardware needs no protocol knowledge at all. The stack subtracts off the headers it has already skipped as the packet climbs, so the residue can be compared against the transport checksum. The documentation is emphatic that a partially capable device must use this rather than lying with CHECKSUM_UNNECESSARY.
CHECKSUM_PARTIAL on receive means the packet came from somewhere that never computed the checksum in the first place — most commonly another Linux kernel on the same host (a virtual machine, a container veth pair) — and csum_start/csum_offset describe where the missing checksum would go.
On transmit, CHECKSUM_PARTIAL is a request: “device, sum the bytes from skb->head + csum_start to the end of the packet, and write the 16-bit result at skb->head + csum_start + csum_offset.” Note the asymmetry in the two offsets — csum_start is measured from head, but csum_offset is measured from csum_start, not from head. Getting that wrong is a classic driver bug. The kernel guarantees only that those two values are internally consistent; the documentation explicitly says a driver “should not attempt to validate that the checksum refers to a legitimate transport layer checksum — it is the purview of the stack.” And a driver that receives CHECKSUM_PARTIAL but cannot offload it is required to fix it up in software via skb_checksum_help(), never to send the packet with a hole in it.
flowchart TB
subgraph RX["Receive — ip_summed is the device's report"]
R0["CHECKSUM_NONE<br/><i>csum undefined</i><br/>stack must verify in software"]
R1["CHECKSUM_UNNECESSARY<br/><i>csum undefined</i><br/>csum_level = (verified layers) - 1"]
R2["CHECKSUM_COMPLETE<br/><i>csum = sum of whole packet</i><br/>stack folds headers off as it climbs"]
R3["CHECKSUM_PARTIAL<br/>from a local VM/veth peer<br/>or set by GRO"]
end
subgraph TX["Transmit — ip_summed is the stack's request"]
T0["CHECKSUM_NONE / _UNNECESSARY<br/>= 'already correct, do nothing'"]
T1["CHECKSUM_PARTIAL<br/>sum from head+csum_start to end,<br/>store at head+csum_start+csum_offset"]
end
T1 -->|"device advertises NETIF_F_HW_CSUM"| HW["NIC computes and inserts"]
T1 -->|"device cannot"| SW["skb_checksum_help()<br/>CPU computes it before xmit"]
T1 ==>|"prerequisite for"| TSO["TSO / GSO<br/>each emitted segment needs its own<br/>checksum, so the splitter must be<br/>the thing that computes them"]
The two-directional meaning of ip_summed. What it shows: the same two-bit field is a report on receive and a request on transmit, and only CHECKSUM_PARTIAL carries the csum_start/csum_offset pair. The insight to take: segmentation offload is downstream of checksum offload. Because each emitted wire segment needs its own correct transport checksum, whichever component performs the split must also compute the checksums — which is why the kernel documentation states that TSO is normally disabled when transmit checksum offload is disabled, and why netif_needs_gso() forces a software split whenever ip_summed is not in a state the hardware can finish.
Two smaller companions round out the group: csum_valid records that the packet’s checksum has been validated by software, and csum_complete_sw distinguishes a CHECKSUM_COMPLETE value the software produced from one the hardware supplied. csum_not_inet marks the SCTP case, where the “checksum” is a CRC32c rather than an Internet one’s-complement sum and must be resolved by skb_crc32c_help() instead.
Steering and Policy Metadata
A cluster of fields exists purely so that other subsystems can classify, steer, and tag a packet without re-parsing it:
Metadata fields that exist so packets can be classified once and steered many times. What it shows: roughly a third of the skb’s non-geometry storage is not about the packet’s content at all — it is cached classification results and policy tags. The insight to take: every one of these fields exists to avoid re-parsing. hash is the clearest case: computing a flow hash means touching the IP addresses and ports, which is a cache miss the receive path cannot afford at ten-million-packets-per-second rates, so the NIC computes it once during DMA and the value is reused by receive steering, by the qdisc, and by any bonding or ECMP decision downstream (Documentation/networking/scaling.rst, v6.12).
The vlan_proto/vlan_tci pair deserves a specific note because it is a good illustration of “offload means metadata.” When a NIC with VLAN-stripping offload receives an 802.1Q-tagged frame, it removes the four-byte tag from the payload and hands the tag to the driver out-of-band; the driver calls __vlan_hwaccel_put_tag(), which stores it in these fields. The packet in memory is therefore untagged, and the tag lives in the skb. skb_vlan_tag_present() tests for it. Code that reads the raw bytes expecting a VLAN header — including naive eBPF programs and some capture tools — will not find one, which is a recurring source of “my filter does not match” confusion.
Extensions — _nfct and skb_ext
Two mechanisms let subsystems attach arbitrary state to a packet without growing struct sk_buff for everybody.
unsigned long _nfct holds the connection-tracking entry, and like _skb_refdst it steals its low bits: the pointer to the struct nf_conn is combined with the connection direction and state in the bottom three bits, unpacked by skb_nfct() and nf_ct_get(). It exists only when CONFIG_NF_CONNTRACK is enabled.
The general mechanism is struct skb_ext:
struct skb_ext { refcount_t refcnt; u8 offset[SKB_EXT_NUM]; /* in chunks of 8 bytes */ u8 chunks; /* same */ char data[] __aligned(8);};
This is a refcounted, variable-length side allocation holding several subsystems’ blobs end to end, with a tiny offset table saying where each one starts (in eight-byte chunks, so a single byte covers a 2 KB extension area). The skb points at it with extensions and summarises “does this skb have any extensions at all, and which” in the one-byte active_extensions bitmap — so the overwhelmingly common case, a packet with no extensions, costs one byte test rather than a pointer dereference. The registered extension IDs at v6.12 are:
ID
Config gate
What it carries
SKB_EXT_BRIDGE_NF
CONFIG_BRIDGE_NETFILTER
bridge-netfilter’s per-packet state (br_netfilter)
SKB_EXT_SEC_PATH
CONFIG_XFRM
the IPsec transform path applied to this packet
TC_SKB_EXT
CONFIG_NET_TC_SKB_EXT
traffic-control chain/classification state across a hardware-offload round trip
SKB_EXT_MPTCP
CONFIG_MPTCP
Multipath TCP per-subflow mapping metadata
SKB_EXT_MCTP
CONFIG_MCTP_FLOWS
Management Component Transport Protocol flow state
The skb_ext registry at v6.12. What it shows: five optional, config-gated blobs share one refcounted side allocation rather than each claiming a pointer in struct sk_buff. The insight to take: this is the structure’s escape valve. Every field added directly to struct sk_buff costs memory on every packet on the system, including the 99.99% that will never use it; skb_ext converts that fixed cost into a per-packet-that-needs-it cost, at the price of an extra allocation and an indirection. That trade is why the struct has stopped growing in the way it did through the 2000s and 2010s.
Geometry, Reference Count, and truesize
The tail of the struct holds the data-buffer bookkeeping:
/* These elements must be at the end, see alloc_skb() for details. */sk_buff_data_t tail;sk_buff_data_t end;unsigned char *head, *data;unsigned int truesize;refcount_t users;
head/data/tail/end are the geometry, explained fully in sk_buff Memory Layout and Headroom. The subtlety that belongs here is the type. sk_buff_data_t is conditional:
#if BITS_PER_LONG > 32#define NET_SKBUFF_DATA_USES_OFFSET 1#endif#ifdef NET_SKBUFF_DATA_USES_OFFSETtypedef unsigned int sk_buff_data_t; /* 32-bit offset from head */#elsetypedef unsigned char *sk_buff_data_t; /* a real pointer */#endif
On every 64-bit build, tail and end are 32-bit offsets from head, saving eight bytes per skb, and skb_end_pointer() reconstitutes skb->head + skb->end on demand. The same trick is applied to end for a second reason: skb_shinfo(SKB) is defined as ((struct skb_shared_info *)(skb_end_pointer(SKB))), so end doubles as the locator for the shared-info trailer.
users is a refcount_t on the metadata struct. An skb with users != 1 is shared — skb_shared() tests precisely that — and no holder may modify it. This is a different count from skb_shared_info.dataref, which tracks sharing of the data buffer; the interplay of the two is the subject of sk_buff Clones and Copies. The one-sentence version: users answers “how many people hold this skb?”, dataref answers “how many skbs point at this buffer?”, and the two are incremented by different operations (skb_get() versus skb_clone()).
truesize is the total memory footprint charged for this packet, and its definition is worth walking symbol by symbol:
SKB_DATA_ALIGN(sizeof(struct sk_buff)) — the metadata struct itself, rounded up to SMP_CACHE_BYTES (64 on x86-64). This is why the metadata counts against your socket buffer even though it holds no payload.
SKB_DATA_ALIGN(sizeof(struct skb_shared_info)) — the trailer, likewise rounded up.
SKB_DATA_ALIGN(X) is just ALIGN(X, SMP_CACHE_BYTES). The practical consequence is that a one-byte UDP datagram has a truesize in the high hundreds of bytes at minimum, and often two kilobytes or more once a real driver’s receive-buffer sizing is included — and truesize, not len, is what SO_RCVBUF and SO_SNDBUF account against. This is the mechanical explanation for the perennial question “why does my socket say its buffer is full when I have only queued a few kilobytes of data?”
The Shared-Info Trailer, and Why Its Size Matters
Every skb’s head buffer ends with a struct skb_shared_info. Its contents — the paged fragment array, the frag_list chain, the GSO parameters — belong to skb_shared_info and Paged Fragments and Segmentation Offloads GSO TSO. What belongs here is its size, because it is charged on every single packet whether or not any fragment is used.
The arithmetic at v6.12 on a 64-bit build, from the field types read out of skbuff.h:
Region
Bytes
Running offset
flags, meta_len, nr_frags, tx_flags (4 × __u8)
4
4
gso_size, gso_segs (2 × unsigned short)
4
8
frag_list (pointer)
8
16
hwtstamps ∪ xsk_meta
8
24
gso_type (unsigned int)
4
28
tskey (u32)
4
32
dataref (atomic_t)
4
36
xdp_frags_size (unsigned int)
4
40
destructor_arg (pointer)
8
48
frags[MAX_SKB_FRAGS] — 17 × skb_frag_t, each {netmem_ref, unsigned int len, unsigned int offset} = 16 B
272
320
So the trailer is 320 bytes, of which 272 — eighty-five percent — is the fragment array, and that array is fully allocated on every packet including a 64-byte ARP reply that will never use a single fragment.
MAX_SKB_FRAGS is CONFIG_MAX_SKB_FRAGS, which defaults to 17 at v6.12. Seventeen is not arbitrary: sixteen 4 KB pages cover 64 KB — the maximum IP packet size — and one spare handles the case where the payload is not page-aligned. When the BIG TCP work set out to exceed the 64 KB limit using RFC 2675 IPv6 jumbograms, raising this constant was one of the first things it had to do, proposing 45; Alexander Duyck objected that “many interface drivers encode assumptions about the maximum number of fragments that a packet may be split into,” and the resolution was to make it a build-time configuration option rather than a flat increase (Corbet, Going big with TCP packets, LWN, 14 February 2022). That history is visible in the #ifndef CONFIG_MAX_SKB_FRAGS / # define CONFIG_MAX_SKB_FRAGS 17 block in skbuff.h today.
Drift between LTS branches, verified 2026-08-29
struct sk_buff is textually identical at v6.12 and v6.18, but struct skb_shared_info is not. At v6.18 the standalone xdp_frags_size field has been folded into a union with destructor_arg, gaining a companion xdp_frags_truesize:
The union is legitimate because XDP multi-buffer frames never carry a destructor argument. The total size stays 320 bytes on 64-bit (v6.18 gains four bytes of padding where v6.12 had xdp_frags_size), so nothing above changes numerically — but code that reads both fields does not compile the same way across the two branches.
Size and Cost — Why This Structure Is Optimised So Hard
The per-packet cost story is the reason for nearly every oddity above. Jesper Brouer’s framing, quoted by LWN, sets the scale: at 100 Gb/s with the standard 1,538-byte maximum frame, an interface delivers over eight million packets per second, giving the CPU “all of about 120ns to do whatever is required to handle each packet, which is not a lot of time; a single cache miss can ruin the entire processing-time budget.” LWN’s own summary of the structure is blunt: “The sk_buff structure (‘SKB’) used to represent packets within the kernel is a large beast, since it must be able to support just about any networking feature that may be in use; that leads to significant per-packet memory use and memory-management costs” (LWN, 2022).
Allocating one packet means two allocations — the metadata struct and the head buffer — plus the 320-byte trailer that rides at the end of the second. The kernel attacks each of them separately:
flowchart TB
REQ["a packet must be represented"]
REQ --> M["<b>metadata: struct sk_buff</b>"]
REQ --> B["<b>head buffer + shared_info</b>"]
M --> M1["<b>skbuff_head_cache</b><br/>a dedicated slab, SLAB_HWCACHE_ALIGN,<br/>created with a usercopy window covering<br/>only cb[] so nothing else can leak to userspace"]
M --> M2["<b>skbuff_fclone_cache</b><br/>allocates struct sk_buff_fclones —<br/>an skb and its future clone as one object.<br/>TCP clones every segment, so this halves<br/>its allocation count"]
M --> M3["<b>napi_skb_cache</b> — per-CPU array<br/>NAPI_SKB_CACHE_SIZE = 64 entries,<br/>refilled/drained in bulk of<br/>NAPI_SKB_CACHE_BULK = 16"]
B --> B1["<b>skbuff_small_head</b> cache<br/>fixed-size slab for heads up to<br/>SKB_HEAD_ALIGN(MAX_TCP_HEADER);<br/>size deliberately not a power of two so a head's<br/>origin is identifiable from skb_end_offset()"]
B --> B2["<b>page-fragment allocator</b><br/>head_frag = 1; carve heads out of a page"]
B --> B3["<b>page_pool recycling</b><br/>pp_recycle = 1; pages go back to the pool<br/>instead of the page allocator"]
B --> B4["<b>build_skb()</b><br/>wrap metadata around a buffer the driver<br/>already DMA'd into — the second allocation<br/>disappears entirely"]
The allocation machinery behind one packet at v6.12. What it shows: four distinct optimisations on the metadata side and four on the buffer side, all serving the same goal of removing allocator work from a path with a ~120 ns budget. The insight to take: none of these change the shape of an skb — they change where its two allocations come from. That is only possible because the metadata and the data are separate objects in the first place, which is the design decision the entire structure is built around. Notice too the security seam: skbuff_head_cache is created with kmem_cache_create_usercopy(..., offsetof(struct sk_buff, cb), sizeof_field(struct sk_buff, cb), ...), so the hardened-usercopy checker permits copies to userspace only from the cb[] window of an skb, not from arbitrary metadata.
SKB_SMALL_HEAD_CACHE_SIZE earns a second look because the comment in net/core/skbuff.c explains a genuinely subtle piece of engineering: the size is deliberately nudged off a power of two (“is_power_of_2(SKB_SMALL_HEAD_SIZE) ? SKB_SMALL_HEAD_SIZE + L1_CACHE_BYTES : SKB_SMALL_HEAD_SIZE”) so that the resulting SKB_SMALL_HEAD_HEADROOM is a value no ordinary kmalloc slab produces. That makes it possible to tell, later and cheaply, whether a given head came from this dedicated cache or from a generic slab — just by looking at skb_end_offset(). A size chosen to be recognisable, rather than to be efficient, is a nice example of the kind of trick this subsystem is full of.
Uncertain
Verify: the exact byte size of struct sk_buff on a real build (the commonly quoted figure is ~232 bytes on x86-64). Reason: as the declaration-order table shows, a large fraction of the struct is CONFIG_*-gated, so there is no single answer — and the kernel’s own Documentation/networking/net_cachelines/ directory, which publishes exactly this kind of per-field layout table for net_device and tcp_sock, has no sk_buff.rst (fetched at v6.12 on 2026-08-29: HTTP 404). To resolve: build a kernel with a known config and run pahole -C sk_buff vmlinux, or print sizeof(struct sk_buff) from a module. Until then, treat “roughly two to three cache lines of metadata” as the honest statement and do not quote a specific number. #uncertain
What Copying an skb Actually Copies
__copy_skb_header() in net/core/skbuff.c is the function every clone and copy funnels through, and reading it tells you exactly which fields the kernel considers to describe the packet rather than this particular skb:
static void __copy_skb_header(struct sk_buff *new, const struct sk_buff *old){ new->tstamp = old->tstamp; /* We do not copy old->sk */ new->dev = old->dev; memcpy(new->cb, old->cb, sizeof(old->cb)); skb_dst_copy(new, old); /* route, honouring the NOREF bit */ __skb_ext_copy(new, old); /* bump skb_ext refcount */ __nf_copy(new, old, false); /* conntrack entry + a reference on it */ new->queue_mapping = old->queue_mapping; memcpy(&new->headers, &old->headers, sizeof(new->headers)); CHECK_SKB_FIELD(protocol); CHECK_SKB_FIELD(csum); ...}
flowchart LR
subgraph NOTCOPIED["Deliberately NOT copied"]
N1["<b>sk</b> and <b>destructor</b><br/>the new skb owes the socket nothing<br/>— it starts life orphaned"]
N2["<b>cloned / fclone / head_frag<br/>pfmemalloc / pp_recycle</b><br/>allocation facts about THIS skb"]
N3["<b>users</b> — starts at 1"]
N4["<b>head / data / tail / end / truesize</b><br/>set by the caller for the new buffer"]
end
subgraph EXPLICIT["Copied field by field"]
E1["tstamp · dev · cb[48]"]
E2["_skb_refdst (dst_clone if counted)"]
E3["extensions (refcount++)"]
E4["_nfct + nf_conntrack_get()"]
E5["queue_mapping"]
end
subgraph BULK["Copied by ONE memcpy of struct_group(headers)"]
B1["pkt_type · ip_summed · flags · tstamp_type<br/>csum / csum_start / csum_offset · csum_level<br/>priority · hash · mark · secmark · tc_index<br/>vlan_proto / vlan_tci · napi_id · skb_iif · alloc_cpu<br/>protocol · mac/network/transport_header<br/>inner_* headers · encapsulation"]
end
EXPLICIT --> OUT["the new skb"]
BULK --> OUT
NOTCOPIED -.->|"excluded by design"| OUT
What __copy_skb_header() transfers, split by mechanism. What it shows: the copy is three-tiered — a handful of fields needing per-field logic (a route needs a reference, an skb_ext needs a refcount bump), one contiguous memcpy of the whole headers group, and an explicit exclusion list. The insight to take: the struct_group(headers, …) marker exists purely so that ~90 bytes of packet-describing metadata can move in a single memcpy rather than forty assignments, and the exclusion of sk, the clone flags and the geometry is what makes a clone a new independent owner of the same packet rather than a shallow duplicate of another skb’s bookkeeping. The CHECK_SKB_FIELD() macros that follow the memcpy are compile-time assertions that each named field really does live inside the group — a guard against someone moving a field out of the group and silently breaking every clone in the kernel.
One exclusion is worth stating explicitly because it is load-bearing: sk is not copied, and the source says so in a comment (/* We do not copy old->sk */). A clone is born orphaned, owing no socket any accounting — which is exactly what you want when a clone is being handed to a packet tap or a bridge that may hold it for an arbitrary time. Conversely, the state that is shared gets its references handled properly on the way through: skb_dst_copy() calls dst_clone() unless the route pointer was borrowed, __skb_ext_copy() bumps the skb_ext refcount, and __nf_copy() copies _nfctand calls nf_conntrack_get() on it. (Its bool copy parameter is easy to misread — it does not gate the conntrack reference, which is always taken; it gates only whether the nf_trace debug bit is carried over.) The details of what a clone shares versus copies at the data level are in sk_buff Clones and Copies.
Freeing an skb, and Why the Kernel Cares Why
Freeing is not one function but a small family, and the distinction between them is a real API contract rather than a naming accident:
consume_skb(skb) — “this packet was successfully handled.” It emits the consume_skb tracepoint.
kfree_skb_reason(skb, reason) / sk_skb_reason_drop(sk, skb, reason) — “this packet was dropped, and here is why.” It emits the kfree_skb tracepoint carrying the reason code and, where known, the socket.
__kfree_skb(skb) — the unconditional teardown, called once the reference count has actually reached zero.
The gate in front of all of them is the reference count:
static __always_inlinebool __sk_skb_reason_drop(struct sock *sk, struct sk_buff *skb, enum skb_drop_reason reason){ if (unlikely(!skb_unref(skb))) return false; /* someone else still holds it */ ... if (reason == SKB_CONSUMED) trace_consume_skb(skb, __builtin_return_address(0)); else trace_kfree_skb(skb, __builtin_return_address(0), reason, sk); return true;}
Teardown proper runs in a fixed order, skb_release_all() → skb_release_head_state() then skb_release_data():
flowchart TB
K["kfree_skb_reason(skb, reason)"] --> U{"skb_unref(skb)<br/>users hits 0?"}
U -->|"no — another holder remains"| STOP["return; nothing is freed"]
U -->|"yes"| TP{"reason == SKB_CONSUMED?"}
TP -->|"yes"| T1["trace_consume_skb()"]
TP -->|"no"| T2["trace_kfree_skb()<br/><i>carries reason + caller address + sk</i>"]
T1 --> RA["__kfree_skb → skb_release_all()"]
T2 --> RA
RA --> HS["<b>skb_release_head_state()</b><br/>1. skb_dst_drop() — release the route<br/>2. skb->destructor(skb) — socket un-accounting<br/>3. nf_conntrack_put()<br/>4. skb_ext_put()"]
HS --> RD["<b>skb_release_data()</b><br/>decrement skb_shared_info.dataref;<br/>if it reaches zero, free/recycle the frags,<br/>walk the frag_list, free the head buffer"]
RD --> MEM["kfree_skbmem() — return the metadata<br/>to skbuff_head_cache / fclone cache /<br/>the per-CPU napi_skb_cache"]
The free path, from a drop decision to memory returned. What it shows: the two-phase teardown — head state (route, destructor, conntrack, extensions) is released before head data (fragments, buffer) — and the branch that decides whether this counts as a consumption or a drop. The insight to take: the ordering is not cosmetic. The destructor must run while the skb’s truesize is still meaningful, because that is what it un-charges from the socket; and the data cannot be released before dataref is checked, because a clone may still be pointing at the same buffer. The tracepoint split is what makes perf/eBPF-based drop monitoring possible at all: a tool can subscribe to kfree_skb and see only genuine losses, with the reason and the calling function, instead of drowning in every successful free.
The reason argument is a comparatively recent and very practical addition. enum skb_drop_reason at v6.12 defines two sentinels (SKB_NOT_DROPPED_YET, SKB_CONSUMED) plus 88 named core reasons, generated from a single DEFINE_DROP_REASON(FN, FNe) macro list so the enum and its string table cannot drift apart. They are specific enough to be diagnostic on their own: SKB_DROP_REASON_TCP_RFC7323_PAWS (a segment failed the Protect Against Wrapped Sequences timestamp check), SKB_DROP_REASON_IP_RPFILTER (reverse-path filtering), SKB_DROP_REASON_SOCKET_RCVBUFF (the receive buffer was full), SKB_DROP_REASON_TCP_OFO_QUEUE_PRUNE, SKB_DROP_REASON_NEIGH_FAILED, SKB_DROP_REASON_QDISC_DROP. The top 16 bits of the value are a subsystem namespace (SKB_DROP_REASON_SUBSYS_SHIFT is 16, SKB_DROP_REASON_SUBSYS_MASK is 0xffff0000), so mac80211 (two separate spaces, for unusable frames and for frames still going to monitor mode) and Open vSwitch can define their own reason codes without colliding with the core list.
The reason this belongs in a note about the structure: it is the clearest illustration of the skb’s role as the stack’s single unit of accounting. Because every packet is one skb and every loss is one kfree_skb call, attaching a reason code to that one call site gives you a complete, uniform drop taxonomy for the entire networking stack — something that would be impossible if each layer had its own packet representation.
The Cost That Motivated XDP
An skb is expensive enough that the highest-performance paths in the kernel are defined by not allocating one. This is the design premise of XDP: the eBPF program runs in the driver’s receive routine, on the DMA’d frame, before any skb exists. What it operates on instead is struct xdp_buff:
The two packet representations, side by side. What it shows: an xdp_buff carries only the four geometry pointers (data_hard_start/data/data_end are recognisably the same idea as head/data/tail) plus the queue context, and it is a stack variable rather than an allocation. The insight to take: for a verdict-and-drop workload — DDoS scrubbing, a software load balancer — every field in struct sk_buff beyond the geometry is dead weight, and the allocation plus initialisation plus teardown of one is a large share of the ~120 ns per-packet budget. XDP’s performance story is substantially “we deleted the packet structure,” and XDP_PASS is precisely the point at which the kernel gives up and allocates a real skb after all. The persistent form, struct xdp_frame, exists for the cases (XDP_REDIRECT, cpumap) where a frame must outlive the poll, and is still only about half the size of an skb’s metadata.
The page-pool work closed the loop from the other direction: as the 2021 posting put it, “instead of allocating buffers specifically for SKBs we now allocate a generic buffer and either wrap it on an SKB (via build_skb) or create an XDP frame” (LWN’s copy of the page_pool: recycle buffers series). That is why skb->pp_recycle and skb->head_frag exist as flags in the struct at all — they are the skb’s half of a buffer-ownership protocol shared with XDP.
How One skb Travels the Whole Stack
Putting the pieces together, follow a received TCP segment upward and watch the same object get progressively reinterpreted:
sequenceDiagram
autonumber
participant NIC
participant Drv as Driver / NAPI
participant Eth as eth_type_trans
participant IP as ip_rcv
participant TCP as tcp_v4_rcv
participant Sock as socket queue
NIC->>Drv: DMA frame into a page-pool buffer, raise IRQ
Drv->>Drv: build_skb() — wrap metadata around the DMA'd buffer
Note over Drv: head/data/tail/end set · head_frag=1 · pp_recycle=1<br/>ip_summed from the RX descriptor · hash from RSS
Drv->>Eth: skb->protocol = eth_type_trans(skb, dev)
Note over Eth: sets dev · skb_reset_mac_header()<br/>pkt_type via eth_skb_pkt_type() (no store for PACKET_HOST)<br/>pulls the 14-byte Ethernet header
Eth->>IP: netif_receive_skb / GRO
Note over IP: skb_reset_network_header()<br/>PREROUTING netfilter: may set mark, attach _nfct<br/>routing lookup attaches _skb_refdst (often NOREF)<br/>skb_pull past the IP header; set transport_header
IP->>TCP: local delivery (pkt_type == PACKET_HOST)
Note over TCP: TCP_SKB_CB(skb) overlays cb[48] with seq/end_seq/flags<br/>4-tuple lookup sets skb->sk<br/>in order → receive queue; out of order → rbnode tree
TCP->>Sock: __skb_queue_tail(), destructor = sock_rfree<br/>sk_rmem_alloc += truesize
Sock-->>Sock: recv() copies bytes out
Sock->>Sock: consume_skb(): destructor un-charges,<br/>dataref drops, pages return to the page pool
One received segment’s journey, annotated with the skb fields each stage touches. What it shows: at no point is the payload copied between layers — each stage moves a cursor, writes a bookmark, sets a flag, or stashes scratch in cb[], and the packet bytes stay exactly where the NIC put them until recv() copies them to userspace. The insight to take: this is the argument for the whole design. The structure is large and full of unions and stolen bits precisely because it must serve as the shared working state for a dozen independent subsystems, and the alternative — each layer owning its own packet representation — would mean a copy at every boundary, which at 8 Mpps is not affordable.
Common Misunderstandings
“The skb holds the packet data.” No — the skb is metadata only. The kernel’s own documentation opens with the sentence “struct sk_buff itself is a metadata structure and does not hold any packet data.” This is the single most common misconception and the reason cloning is cheap.
“The header fields are pointers.”mac_header, network_header and transport_header are __u16offsets from head, which is why they survive pskb_expand_head() and why you must use the skb_*_header() accessors rather than treating them as addresses. tail and end are likewise offsets on all 64-bit builds.
“len is the linear length.”len is the total length including paged data; the linear length is skb_headlen() = len - data_len. Dereferencing more than skb_headlen() bytes past skb->data without a pskb_may_pull() reads into the skb_shared_info trailer.
“cb[] survives down the stack.” It is per-owner scratch, and the comment says so: “This is owned by whoever has the skb queued ATM.” A later layer may overwrite it. Cloning is the documented way to keep a snapshot, and a clone does copy cb[].
“skb->truesize is about the payload.” It is payload + aligned sizeof(struct sk_buff) + aligned sizeof(struct skb_shared_info) — the memory footprint. Socket buffer limits account in truesize, which is why a burst of tiny datagrams can fill SO_RCVBUF while holding almost no data.
“skb->tstamp is when the packet was sent.” On transmit it is the earliest departure time — a scheduling instruction to the fq qdisc, not an observation.
“eth_type_trans() sets skb->protocol.” It returns the EtherType; the caller assigns it. What it sets directly is dev, the MAC header bookmark, and pkt_type.
“A VLAN-tagged packet has a VLAN header in its bytes.” Not when the NIC strips it: the tag lives in vlan_proto/vlan_tci and the payload is untagged. Test with skb_vlan_tag_present().
“skb_shared() and skb_cloned() mean the same thing.”skb_shared() tests users != 1 — several holders of this metadata struct. skb_cloned() tests whether the data buffer is shared with another skb. A packet can be either, both, or neither, and the checks a driver must make differ.