Vulnerability Report: Wrong Size in skb_ensure_writable for IPv6 TOS Target
File: net/netfilter/xt_DSCP.c
Function: tos_tg6
Line: 102
Severity: Medium
Type: Incomplete Copy-on-Write (COW) / Wrong Size to Memory Writability Check
Summary
tos_tg6(), the IPv6 handler for the TOS xtables target, passes sizeof(struct iphdr) (20 bytes) to skb_ensure_writable() instead of sizeof(struct ipv6hdr) (40 bytes). This is a copy-paste error from the IPv4 sibling tos_tg(). As a result, the kernel only guarantees that the first 20 bytes of the packet header are writable before modifying the IPv6 Traffic Class / DSCP field.
Vulnerable Code
// net/netfilter/xt_DSCP.c, line 91–109
static unsigned int
tos_tg6(struct sk_buff *skb, const struct xt_action_param *par)
{
const struct xt_tos_target_info *info = par->targinfo;
struct ipv6hdr *iph = ipv6_hdr(skb);
u_int8_t orig, nv;
orig = ipv6_get_dsfield(iph);
nv = (orig & ~info->tos_mask) ^ info->tos_value;
if (orig != nv) {
if (skb_ensure_writable(skb, sizeof(struct iphdr))) // BUG: sizeof(struct ipv6hdr) required
return NF_DROP;
iph = ipv6_hdr(skb);
ipv6_change_dsfield(iph, 0, nv);
}
return XT_CONTINUE;
}
Compare with the correct size used in dscp_tg6() (line 53):
if (skb_ensure_writable(skb, sizeof(struct ipv6hdr))) // correct
Root Cause
sizeof(struct iphdr) = 20 bytes (IPv4 header)
sizeof(struct ipv6hdr) = 40 bytes (IPv6 header)
skb_ensure_writable(skb, len) calls skb_clone_writable(skb, len) to check whether the first len bytes of the skb’s linear data are exclusively owned. If they are not, it performs a pskb_expand_head() to COW the data. With len=20, skb_clone_writable checks only the first 20 bytes. If those happen to already be writable (e.g. from a prior partial COW), the check passes and no COW occurs—leaving bytes 21–40 of the IPv6 header still in shared/read-only memory from the cloned skb.
Impact
Write to incompletely COW’d memory in cloned skbs.
ipv6_change_dsfield() is implemented as:
static inline void ipv6_change_dsfield(struct ipv6hdr *ipv6h, __u8 mask, __u8 value)
{
__be16 *p = (__force __be16 *)ipv6h;
*p = (*p & htons((((u16)mask << 4) | 0xf00f))) | htons((u16)value << 4);
}
Currently it writes only to bytes 0–1 of the IPv6 header (the Version/TC/Flow-Label word), which falls within the incorrectly-checked 20-byte region. In current code this means no out-of-bounds write occurs in practice. However:
-
Any future modification to
ipv6_change_dsfieldthat accesses beyond byte 19 would silently write to shared memory, corrupting data visible to other processes or packet paths that hold references to the same cloned skb. -
Incorrect security contract: the kernel explicitly guarantees via
skb_ensure_writablethat a certain range is safe to write. Passing the wrong size breaks this contract and makes all code paths after this call reason incorrectly about write safety. -
Inconsistency with
dscp_tg6(line 53) which correctly usessizeof(struct ipv6hdr), confirming this is an unintentional oversight.
Proof of Concept (Trigger Path)
- An
ip6tables -t mangle -j TOSrule is configured. - A cloned IPv6 skb (e.g. from
skb_clone()in bridge forwarding, multicast duplication, or IPVS) passes through the mangle table. tos_tg6is invoked. The first 20 bytes of the clone were already made writable by a prior operation.skb_clone_writable(skb, 20)returns true → no COW triggered.- Bytes 21–40 of the IPv6 header remain shared with the original skb owner.
ipv6_change_dsfieldwrites to bytes 0–1 (currently safe), but the incomplete COW leaves the rest of the header in a dangerous state.
Fix
// Line 102: change
if (skb_ensure_writable(skb, sizeof(struct iphdr)))
// to:
if (skb_ensure_writable(skb, sizeof(struct ipv6hdr)))
This aligns tos_tg6 with dscp_tg6 and ensures the full IPv6 header is guaranteed writable before any modification.
References
include/net/dsfield.h—ipv6_change_dsfield,ipv6_get_dsfieldnet/core/skbuff.c—skb_ensure_writable,skb_clone_writable- Comparison:
dscp_tg6at line 53 uses the correctsizeof(struct ipv6hdr)