VYPR
Unrated severityNVD Advisory· Published Jun 16, 2026

CVE-2026-46331

CVE-2026-46331

Description

The Linux kernel's pedit action in net/sched has a COW range miscalculation that can cause page cache corruption, fixed by commit 899ee91156e5.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

The Linux kernel's pedit action in net/sched has a COW range miscalculation that can cause page cache corruption, fixed by commit 899ee91156e5.

Vulnerability

In the Linux kernel's net/sched subsystem, the pedit action (tcf_pedit_act()) computes the copy-on-write (COW) range for skb_ensure_writable() once before iterating over keys, using tcfp_off_max_hint. However, this hint does not account for the runtime header offset added by typed keys, potentially leaving part of the write region un-COW'd, leading to page cache corruption. The issue affects kernel versions prior to the inclusion of commit 899ee91156e5 [1].

Exploitation

An attacker with the ability to send network packets to a system that uses tc filters with the pedit action can trigger the bug. By crafting packets that cause the pedit action to modify headers with typed keys that shift the write offset, the attacker can cause the kernel to write to a page cache without proper COW, leading to corruption. No special privileges beyond network access are required if the system is configured with such filters [1].

Impact

Successful exploitation can corrupt the page cache, potentially leading to data corruption, system instability, or denial of service. In some scenarios, this could be leveraged for privilege escalation if the corruption affects sensitive kernel structures [1].

Mitigation

The vulnerability is fixed in the Linux kernel by commit 899ee91156e5 ('net/sched: fix pedit partial COW leading to page cache corruption') [1]. Users should apply the patch or update to a kernel version containing this commit. No workaround is available; the fix must be applied.

AI Insight generated on Jun 16, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

2
899ee91156e5

net/sched: fix pedit partial COW leading to page cache corruption

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitRajat GuptaMay 31, 2026Fixed in 7.1via kernel-cna
2 files changed · +41 38
  • include/net/tc_act/tc_pedit.h+0 1 modified
    diff --git a/include/net/tc_act/tc_pedit.h b/include/net/tc_act/tc_pedit.h
    index f58ee15cd858c..cb7b82f2cbc7f 100644
    --- a/include/net/tc_act/tc_pedit.h
    +++ b/include/net/tc_act/tc_pedit.h
    @@ -15,7 +15,6 @@ struct tcf_pedit_parms {
     	struct tc_pedit_key	*tcfp_keys;
     	struct tcf_pedit_key_ex	*tcfp_keys_ex;
     	int action;
    -	u32 tcfp_off_max_hint;
     	unsigned char tcfp_nkeys;
     	unsigned char tcfp_flags;
     	struct rcu_head rcu;
    
  • net/sched/act_pedit.c+41 37 modified
    diff --git a/net/sched/act_pedit.c b/net/sched/act_pedit.c
    index bc20f08a27890..bd3b1da3cd63b 100644
    --- a/net/sched/act_pedit.c
    +++ b/net/sched/act_pedit.c
    @@ -16,6 +16,8 @@
     #include <linux/ip.h>
     #include <linux/ipv6.h>
     #include <linux/slab.h>
    +#include <linux/overflow.h>
    +#include <linux/unaligned.h>
     #include <net/ipv6.h>
     #include <net/netlink.h>
     #include <net/pkt_sched.h>
    @@ -242,7 +244,6 @@ static int tcf_pedit_init(struct net *net, struct nlattr *nla,
     		goto out_free_ex;
     	}
     
    -	nparms->tcfp_off_max_hint = 0;
     	nparms->tcfp_flags = parm->flags;
     	nparms->tcfp_nkeys = parm->nkeys;
     
    @@ -268,14 +269,6 @@ static int tcf_pedit_init(struct net *net, struct nlattr *nla,
     						   BITS_PER_TYPE(int) - 1,
     						   nparms->tcfp_keys[i].shift);
     
    -		/* The AT option can read a single byte, we can bound the actual
    -		 * value with uchar max.
    -		 */
    -		cur += (0xff & offmask) >> nparms->tcfp_keys[i].shift;
    -
    -		/* Each key touches 4 bytes starting from the computed offset */
    -		nparms->tcfp_off_max_hint =
    -			max(nparms->tcfp_off_max_hint, cur + 4);
     	}
     
     	p = to_pedit(*a);
    @@ -318,15 +311,12 @@ static void tcf_pedit_cleanup(struct tc_action *a)
     		call_rcu(&parms->rcu, tcf_pedit_cleanup_rcu);
     }
     
    -static bool offset_valid(struct sk_buff *skb, int offset)
    +static bool offset_valid(struct sk_buff *skb, int offset, int len)
     {
    -	if (offset > 0 && offset > skb->len)
    -		return false;
    -
    -	if  (offset < 0 && -offset > skb_headroom(skb))
    +	if (offset < -(int)skb_headroom(skb))
     		return false;
     
    -	return true;
    +	return offset <= (int)skb->len - len;
     }
     
     static int pedit_l4_skb_offset(struct sk_buff *skb, int *hoffset, const int header_type)
    @@ -393,18 +383,10 @@ TC_INDIRECT_SCOPE int tcf_pedit_act(struct sk_buff *skb,
     	struct tcf_pedit_key_ex *tkey_ex;
     	struct tcf_pedit_parms *parms;
     	struct tc_pedit_key *tkey;
    -	u32 max_offset;
     	int i;
     
     	parms = rcu_dereference_bh(p->parms);
     
    -	max_offset = (skb_transport_header_was_set(skb) ?
    -		      skb_transport_offset(skb) :
    -		      skb_network_offset(skb)) +
    -		     parms->tcfp_off_max_hint;
    -	if (skb_ensure_writable(skb, min(skb->len, max_offset)))
    -		goto done;
    -
     	tcf_lastuse_update(&p->tcf_tm);
     	tcf_action_update_bstats(&p->common, skb);
     
    @@ -412,10 +394,11 @@ TC_INDIRECT_SCOPE int tcf_pedit_act(struct sk_buff *skb,
     	tkey_ex = parms->tcfp_keys_ex;
     
     	for (i = parms->tcfp_nkeys; i > 0; i--, tkey++) {
    +		int write_offset, write_len;
     		int offset = tkey->off;
     		int hoffset = 0;
    -		u32 *ptr, hdata;
    -		u32 val;
    +		u32 cur_val, val;
    +		u32 *ptr;
     		int rc;
     
     		if (tkey_ex) {
    @@ -433,13 +416,15 @@ TC_INDIRECT_SCOPE int tcf_pedit_act(struct sk_buff *skb,
     
     		if (tkey->offmask) {
     			u8 *d, _d;
    +			int at_offset;
     
    -			if (!offset_valid(skb, hoffset + tkey->at)) {
    +			if (check_add_overflow(hoffset, (int)tkey->at, &at_offset) ||
    +			    !offset_valid(skb, at_offset, sizeof(_d))) {
     				pr_info_ratelimited("tc action pedit 'at' offset %d out of bounds\n",
     						    hoffset + tkey->at);
     				goto bad;
     			}
    -			d = skb_header_pointer(skb, hoffset + tkey->at,
    +			d = skb_header_pointer(skb, at_offset,
     					       sizeof(_d), &_d);
     			if (!d)
     				goto bad;
    @@ -451,31 +436,51 @@ TC_INDIRECT_SCOPE int tcf_pedit_act(struct sk_buff *skb,
     			}
     		}
     
    -		if (!offset_valid(skb, hoffset + offset)) {
    -			pr_info_ratelimited("tc action pedit offset %d out of bounds\n", hoffset + offset);
    +		if (check_add_overflow(hoffset, offset, &write_offset)) {
    +			pr_info_ratelimited("tc action pedit offset overflow\n");
     			goto bad;
     		}
     
    -		ptr = skb_header_pointer(skb, hoffset + offset,
    -					 sizeof(hdata), &hdata);
    -		if (!ptr)
    +		if (!offset_valid(skb, write_offset, sizeof(*ptr))) {
    +			pr_info_ratelimited("tc action pedit offset %d out of bounds\n",
    +					    write_offset);
     			goto bad;
    +		}
    +
    +		if (write_offset < 0) {
    +			if (skb_cow(skb, -write_offset))
    +				goto bad;
    +			if (write_offset + (int)sizeof(*ptr) > 0) {
    +				if (skb_ensure_writable(skb,
    +							min_t(int, skb->len,
    +							      write_offset + (int)sizeof(*ptr))))
    +					goto bad;
    +			}
    +		} else {
    +			if (check_add_overflow(write_offset, (int)sizeof(*ptr),
    +					       &write_len))
    +				goto bad;
    +			if (skb_ensure_writable(skb, min_t(int, skb->len,
    +							   write_len)))
    +				goto bad;
    +		}
    +
    +		ptr = (u32 *)(skb->data + write_offset);
    +		cur_val = get_unaligned(ptr);
     		/* just do it, baby */
     		switch (cmd) {
     		case TCA_PEDIT_KEY_EX_CMD_SET:
     			val = tkey->val;
     			break;
     		case TCA_PEDIT_KEY_EX_CMD_ADD:
    -			val = (*ptr + tkey->val) & ~tkey->mask;
    +			val = (cur_val + tkey->val) & ~tkey->mask;
     			break;
     		default:
     			pr_info_ratelimited("tc action pedit bad command (%d)\n", cmd);
     			goto bad;
     		}
     
    -		*ptr = ((*ptr & tkey->mask) ^ val);
    -		if (ptr == &hdata)
    -			skb_store_bits(skb, hoffset + offset, ptr, 4);
    +		put_unaligned((cur_val & tkey->mask) ^ val, ptr);
     	}
     
     	goto done;
    -- 
    cgit 1.3-korg
    
    
    
899ee91156e5

net/sched: fix pedit partial COW leading to page cache corruption

2 files changed · +41 38
  • include/net/tc_act/tc_pedit.h+0 1 modified
    diff --git a/include/net/tc_act/tc_pedit.h b/include/net/tc_act/tc_pedit.h
    index f58ee15cd858c..cb7b82f2cbc7f 100644
    --- a/include/net/tc_act/tc_pedit.h
    +++ b/include/net/tc_act/tc_pedit.h
    @@ -15,7 +15,6 @@ struct tcf_pedit_parms {
     	struct tc_pedit_key	*tcfp_keys;
     	struct tcf_pedit_key_ex	*tcfp_keys_ex;
     	int action;
    -	u32 tcfp_off_max_hint;
     	unsigned char tcfp_nkeys;
     	unsigned char tcfp_flags;
     	struct rcu_head rcu;
    
  • net/sched/act_pedit.c+41 37 modified
    diff --git a/net/sched/act_pedit.c b/net/sched/act_pedit.c
    index bc20f08a27890..bd3b1da3cd63b 100644
    --- a/net/sched/act_pedit.c
    +++ b/net/sched/act_pedit.c
    @@ -16,6 +16,8 @@
     #include <linux/ip.h>
     #include <linux/ipv6.h>
     #include <linux/slab.h>
    +#include <linux/overflow.h>
    +#include <linux/unaligned.h>
     #include <net/ipv6.h>
     #include <net/netlink.h>
     #include <net/pkt_sched.h>
    @@ -242,7 +244,6 @@ static int tcf_pedit_init(struct net *net, struct nlattr *nla,
     		goto out_free_ex;
     	}
     
    -	nparms->tcfp_off_max_hint = 0;
     	nparms->tcfp_flags = parm->flags;
     	nparms->tcfp_nkeys = parm->nkeys;
     
    @@ -268,14 +269,6 @@ static int tcf_pedit_init(struct net *net, struct nlattr *nla,
     						   BITS_PER_TYPE(int) - 1,
     						   nparms->tcfp_keys[i].shift);
     
    -		/* The AT option can read a single byte, we can bound the actual
    -		 * value with uchar max.
    -		 */
    -		cur += (0xff & offmask) >> nparms->tcfp_keys[i].shift;
    -
    -		/* Each key touches 4 bytes starting from the computed offset */
    -		nparms->tcfp_off_max_hint =
    -			max(nparms->tcfp_off_max_hint, cur + 4);
     	}
     
     	p = to_pedit(*a);
    @@ -318,15 +311,12 @@ static void tcf_pedit_cleanup(struct tc_action *a)
     		call_rcu(&parms->rcu, tcf_pedit_cleanup_rcu);
     }
     
    -static bool offset_valid(struct sk_buff *skb, int offset)
    +static bool offset_valid(struct sk_buff *skb, int offset, int len)
     {
    -	if (offset > 0 && offset > skb->len)
    -		return false;
    -
    -	if  (offset < 0 && -offset > skb_headroom(skb))
    +	if (offset < -(int)skb_headroom(skb))
     		return false;
     
    -	return true;
    +	return offset <= (int)skb->len - len;
     }
     
     static int pedit_l4_skb_offset(struct sk_buff *skb, int *hoffset, const int header_type)
    @@ -393,18 +383,10 @@ TC_INDIRECT_SCOPE int tcf_pedit_act(struct sk_buff *skb,
     	struct tcf_pedit_key_ex *tkey_ex;
     	struct tcf_pedit_parms *parms;
     	struct tc_pedit_key *tkey;
    -	u32 max_offset;
     	int i;
     
     	parms = rcu_dereference_bh(p->parms);
     
    -	max_offset = (skb_transport_header_was_set(skb) ?
    -		      skb_transport_offset(skb) :
    -		      skb_network_offset(skb)) +
    -		     parms->tcfp_off_max_hint;
    -	if (skb_ensure_writable(skb, min(skb->len, max_offset)))
    -		goto done;
    -
     	tcf_lastuse_update(&p->tcf_tm);
     	tcf_action_update_bstats(&p->common, skb);
     
    @@ -412,10 +394,11 @@ TC_INDIRECT_SCOPE int tcf_pedit_act(struct sk_buff *skb,
     	tkey_ex = parms->tcfp_keys_ex;
     
     	for (i = parms->tcfp_nkeys; i > 0; i--, tkey++) {
    +		int write_offset, write_len;
     		int offset = tkey->off;
     		int hoffset = 0;
    -		u32 *ptr, hdata;
    -		u32 val;
    +		u32 cur_val, val;
    +		u32 *ptr;
     		int rc;
     
     		if (tkey_ex) {
    @@ -433,13 +416,15 @@ TC_INDIRECT_SCOPE int tcf_pedit_act(struct sk_buff *skb,
     
     		if (tkey->offmask) {
     			u8 *d, _d;
    +			int at_offset;
     
    -			if (!offset_valid(skb, hoffset + tkey->at)) {
    +			if (check_add_overflow(hoffset, (int)tkey->at, &at_offset) ||
    +			    !offset_valid(skb, at_offset, sizeof(_d))) {
     				pr_info_ratelimited("tc action pedit 'at' offset %d out of bounds\n",
     						    hoffset + tkey->at);
     				goto bad;
     			}
    -			d = skb_header_pointer(skb, hoffset + tkey->at,
    +			d = skb_header_pointer(skb, at_offset,
     					       sizeof(_d), &_d);
     			if (!d)
     				goto bad;
    @@ -451,31 +436,51 @@ TC_INDIRECT_SCOPE int tcf_pedit_act(struct sk_buff *skb,
     			}
     		}
     
    -		if (!offset_valid(skb, hoffset + offset)) {
    -			pr_info_ratelimited("tc action pedit offset %d out of bounds\n", hoffset + offset);
    +		if (check_add_overflow(hoffset, offset, &write_offset)) {
    +			pr_info_ratelimited("tc action pedit offset overflow\n");
     			goto bad;
     		}
     
    -		ptr = skb_header_pointer(skb, hoffset + offset,
    -					 sizeof(hdata), &hdata);
    -		if (!ptr)
    +		if (!offset_valid(skb, write_offset, sizeof(*ptr))) {
    +			pr_info_ratelimited("tc action pedit offset %d out of bounds\n",
    +					    write_offset);
     			goto bad;
    +		}
    +
    +		if (write_offset < 0) {
    +			if (skb_cow(skb, -write_offset))
    +				goto bad;
    +			if (write_offset + (int)sizeof(*ptr) > 0) {
    +				if (skb_ensure_writable(skb,
    +							min_t(int, skb->len,
    +							      write_offset + (int)sizeof(*ptr))))
    +					goto bad;
    +			}
    +		} else {
    +			if (check_add_overflow(write_offset, (int)sizeof(*ptr),
    +					       &write_len))
    +				goto bad;
    +			if (skb_ensure_writable(skb, min_t(int, skb->len,
    +							   write_len)))
    +				goto bad;
    +		}
    +
    +		ptr = (u32 *)(skb->data + write_offset);
    +		cur_val = get_unaligned(ptr);
     		/* just do it, baby */
     		switch (cmd) {
     		case TCA_PEDIT_KEY_EX_CMD_SET:
     			val = tkey->val;
     			break;
     		case TCA_PEDIT_KEY_EX_CMD_ADD:
    -			val = (*ptr + tkey->val) & ~tkey->mask;
    +			val = (cur_val + tkey->val) & ~tkey->mask;
     			break;
     		default:
     			pr_info_ratelimited("tc action pedit bad command (%d)\n", cmd);
     			goto bad;
     		}
     
    -		*ptr = ((*ptr & tkey->mask) ^ val);
    -		if (ptr == &hdata)
    -			skb_store_bits(skb, hoffset + offset, ptr, 4);
    +		put_unaligned((cur_val & tkey->mask) ^ val, ptr);
     	}
     
     	goto done;
    -- 
    cgit 1.3-korg
    
    
    

Vulnerability mechanics

Root cause

"The COW range for skb_ensure_writable() was computed once before the key loop using a static hint that did not account for the per-key runtime header offset, leaving write regions uncopied."

Attack vector

An attacker can craft a packet and install a tc pedit action with typed keys whose runtime header offset (`hoffset`) pushes the write offset beyond the COW range that was precomputed from `tcfp_off_max_hint`. Because `skb_ensure_writable()` was called only once before the key loop, the kernel writes into a page‑cache page without having copied it first, corrupting the page cache. No authentication is required if the attacker already has the privileges to configure tc filters [patch_id=6162958].

Affected code

The defect is in `net/sched/act_pedit.c` in the `tcf_pedit_act()` function and the `offset_valid()` helper. The pre-loop `skb_ensure_writable()` computed a COW range from `tcfp_off_max_hint` (initialized in `tcf_pedit_init()`) plus a transport/network offset, but this static hint did not incorporate the per-key runtime header offset (`hoffset`) added by typed keys, leaving write regions un‑COW'd [patch_id=6162958].

What the fix does

The patch moves `skb_ensure_writable()` inside the per‑key loop so the exact write offset (including the runtime header offset) is used to COW each region. For negative offsets (e.g., Ethernet header edits at ingress), `skb_cow()` is called to COW the headroom instead. Overflow checking via `check_add_overflow()` is added, and `offset_valid()` is refactored to accept a length parameter and to avoid undefined negation of `INT_MIN`. The stale `tcfp_off_max_hint` field is removed from the parameter struct [patch_id=6162958].

Preconditions

  • authThe attacker must be able to configure tc (traffic control) pedit actions on the system, which typically requires CAP_NET_ADMIN.
  • configThe system must have tc filter rules using the pedit action with typed keys that produce a runtime header offset (hoffset) not accounted for by the static tcfp_off_max_hint.
  • networkThe attacker must be able to send or trigger packets that match the configured tc filter, enabling the vulnerable code path in tcf_pedit_act() to execute en...,

Generated on Jun 16, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.