tcf_csum_ipv6_hopopts() takes the payload length out of a hop-by-hop
jumbo option and stores it unchecked:
case IPV6_TLV_JUMBO:
optlen = xh[off + 1] + 2;
if (optlen != 6 || len < 6 || (off & 3) != 2)
/* wrong jumbo option length/alignment */
return 0;
*pl = ntohl(*(__be32 *)(xh + off + 2));
tcf_csum_ipv6() then hands ipl = *pl + sizeof(struct ipv6hdr) to the
per-protocol handlers as the extent of the packet, so a 32-bit value
chosen by whoever built the header reaches the length they read.
Both guards downstream fail on such a value. In
tcf_csum_skb_nextlayer(), ipl + ntkoff is an unsigned int plus an int, so
an ipl just under 2^32 wraps to a small number and pskb_may_pull()
succeeds without pulling anything, while ipl < hl stays false precisely
because ipl is huge. tcf_csum_ipv6_udp()'s UDPLITE arm then admits any
16-bit length, since ul <= ipl - ihl compares against ~4.29e9, and
csum_partial() reads that many bytes off the end of the skb. (The plain
UDP arm is not affected: it requires ul == ipl - ihl.)
With payload_len 0xFFFFFFCA, a UDPLITE header claiming 60000 bytes and a
704-byte skb, KASAN reports:
BUG: KASAN: slab-out-of-bounds in csum_partial+0x103/0x1f0 lib/checksum.c:119
Read of size 4 at addr ffff8880259642c0 by task syz-executor/13111
tcf_csum_ipv6_udp+0x19e/0x250 net/sched/act_csum.c
tcf_csum_ipv6+0x63c/0x780 net/sched/act_csum.c:549
tcf_csum_act+0x34d/0x480 net/sched/act_csum.c:601
tcf_action_exec+0x109/0x390 net/sched/act_api.c:1161
tcf_classify+0x13d/0x440 net/sched/cls_api.c:1875
tc_run+0x31a/0x460 net/core/dev.c:4491
__dev_queue_xmit+0x1a7e/0x2390 net/core/dev.c:4890
ip6_output+0x280/0x400 net/ipv6/ip6_output.c:248
rawv6_send_hdrinc+0xb90/0x1170 net/ipv6/raw.c:663
rawv6_sendmsg+0x10d3/0x1260 net/ipv6/raw.c:908
The buggy address belongs to the object at ffff888025964000
which belongs to the cache skbuff_small_head of size 704
The buggy address is located 0 bytes to the right of
allocated 704-byte region [ffff888025964000, ffff8880259642c0)
The packet is generated locally through a raw AF_INET6 socket with
IPV6_HDRINCL and leaves an Ethernet device, so skb_network_offset() is
non-zero and the sum above wraps. Reaching it needs a clsact qdisc with
a csum action carrying TCA_CSUM_UPDATE_FLAG_UDPLITE, which is
CAP_NET_ADMIN in the network namespace - reachable by an unprivileged
user through a user namespace. The folded checksum of the out-of-bounds
bytes is written into the packet and transmitted.
Bound the length the way nf_ip6_check_hbh_len() already does for the same
option: a jumbogram carries payload_len 0 and more than IPV6_MAXPLEN
bytes, and the length it claims must be backed by the skb. Once *pl is
bounded, ipl cannot exceed skb->len, the sum in
tcf_csum_skb_nextlayer() no longer wraps, and the UDPLITE comparison is a
real bound again.
Per Documentation/process/generated-content.rst: the out-of-bounds read
was found by an AI-driven fuzzing harness, developed
jointly by Kapil Vaswani and myself, which exercised the csum action
under KASAN on x86_64. The root-cause analysis, this changelog and the
patch were written with the assistance of a large language model,
and reviewed by hand; the before/after testing below
the --- was run by us.
Fixes: eb4d40654505 ("net/sched: add ACT_CSUM action to update packets checksums")
Signed-off-by: Kapil Vaswani <redacted>
Signed-off-by: Piyus Kedia <redacted>
---
Tested on 7.3.1 with KASAN, in a single-VM qemu rig, same .config and
same image before and after; only net/sched/act_csum.o and the relink differ
(kernel build #2 unpatched, #3 patched):
jumbo length payload_len before after
variant A 0xFFFFFFCA 16 KASAN, VM lost ok, exit 0
variant B 0xFFFFFFCA 0 KASAN, VM lost ok, exit 0
control none (PadN) 16 not run ok
before, variant A: Read of size 4 at addr ffff888024cb6340, task 11851
before, variant B: Read of size 4 at addr ffff88805fe8e9c0, task 9555
Both faulting runs reach csum_partial() through the trace above. After the
patch both programs run to completion with every setup step succeeding and
all four sendto() calls accepted, so the packets do travel the egress path
and the action simply refuses the length:
ntkoff=14 pl=0xffffffca ipl=0xfffffff2 ipl+ntkoff=0x00000000 \
ipl-ihl=0xffffffc2 ul=60000
The control is the same program with the jumbo option replaced by a PadN and
a UDP-Lite length of 8, which is inside the packet. It was run on the
patched kernel only, since what it is there to show is that the patch did
not break the path: the walker still accepts an ordinary hop-by-hop option
and csum_partial() still runs over the 8 bytes the packet really has. Only
an unbacked jumbo length is rejected.
Reproducer: not included here. This bug was found with AI assistance, which
Documentation/process/security-bugs.rst says to treat as public; the same
paragraph says not to post the reproducer publicly. It is a short shell
script plus a ~40-line C sender, tested, and available on request.
Conditions it needs: a clsact qdisc with a csum action carrying
TCA_CSUM_UPDATE_FLAG_UDPLITE, on an Ethernet-type device, with the packet
locally generated so that skb_network_offset() is 14 and the sum in
tcf_csum_skb_nextlayer() can wrap. CAP_NET_ADMIN in the network namespace,
which CONFIG_USER_NS makes reachable by an unprivileged user.
net/sched/act_csum.c | 25 +++++++++++++++++++++----
1 file changed, 21 insertions(+), 4 deletions(-)
diff --git a/net/sched/act_csum.c b/net/sched/act_csum.c
index 5fff52a8ca90..eab8cdf6745b 100644
--- a/net/sched/act_csum.c
+++ b/net/sched/act_csum.c
@@ -449,7 +449,8 @@ static int tcf_csum_ipv4(struct sk_buff *skb, u32 update_flags)
return 0;
}
-static int tcf_csum_ipv6_hopopts(struct ipv6_opt_hdr *ip6xh, unsigned int ixhl,
+static int tcf_csum_ipv6_hopopts(struct sk_buff *skb,
+ struct ipv6_opt_hdr *ip6xh, unsigned int ixhl,
unsigned int *pl)
{
int off, len, optlen;@@ -463,13 +464,29 @@ static int tcf_csum_ipv6_hopopts(struct ipv6_opt_hdr *ip6xh, unsigned int ixhl,
case IPV6_TLV_PAD1:
optlen = 1;
break;
- case IPV6_TLV_JUMBO:
+ case IPV6_TLV_JUMBO: {
+ unsigned int avail = skb->len - skb_network_offset(skb);
+ u32 pkt_len;
+
optlen = xh[off + 1] + 2;
if (optlen != 6 || len < 6 || (off & 3) != 2)
/* wrong jumbo option length/alignment */
return 0;
- *pl = ntohl(*(__be32 *)(xh + off + 2));
+ pkt_len = ntohl(*(__be32 *)(xh + off + 2));
+ /* RFC 2675: a jumbogram carries payload_len 0 and more
+ * than IPV6_MAXPLEN bytes. Reject anything else, and
+ * anything the skb cannot back: callers take this as
+ * the extent of the packet and read that far into it.
+ */
+ if (pkt_len <= IPV6_MAXPLEN ||
+ ipv6_hdr(skb)->payload_len)
+ return 0;
+ if (avail < sizeof(struct ipv6hdr) ||
+ pkt_len > avail - sizeof(struct ipv6hdr))
+ return 0;
+ *pl = pkt_len;
goto done;
+ }
default:
optlen = xh[off + 1] + 2;
if (optlen > len)@@ -521,7 +538,7 @@ static int tcf_csum_ipv6(struct sk_buff *skb, u32 update_flags)
goto fail;
ip6xh = (void *)(skb_network_header(skb) + hl);
if ((nexthdr == NEXTHDR_HOP) &&
- !(tcf_csum_ipv6_hopopts(ip6xh, ixhl, &pl)))
+ !(tcf_csum_ipv6_hopopts(skb, ip6xh, ixhl, &pl)))
goto fail;
nexthdr = ip6xh->nexthdr;
hl += ixhl;
base-commit: a2fd526d644233d6a892c28173f07c61e21e4c93
--
2.34.1