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:

  1. Any future modification to ipv6_change_dsfield that 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.

  2. Incorrect security contract: the kernel explicitly guarantees via skb_ensure_writable that 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.

  3. Inconsistency with dscp_tg6 (line 53) which correctly uses sizeof(struct ipv6hdr), confirming this is an unintentional oversight.


Proof of Concept (Trigger Path)

  1. An ip6tables -t mangle -j TOS rule is configured.
  2. A cloned IPv6 skb (e.g. from skb_clone() in bridge forwarding, multicast duplication, or IPVS) passes through the mangle table.
  3. tos_tg6 is invoked. The first 20 bytes of the clone were already made writable by a prior operation.
  4. skb_clone_writable(skb, 20) returns true → no COW triggered.
  5. Bytes 21–40 of the IPv6 header remain shared with the original skb owner.
  6. ipv6_change_dsfield writes 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.hipv6_change_dsfield, ipv6_get_dsfield
  • net/core/skbuff.cskb_ensure_writable, skb_clone_writable
  • Comparison: dscp_tg6 at line 53 uses the correct sizeof(struct ipv6hdr)