net/ipv4/ip_options.c | 54 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-)
IPv4 option metadata stores absolute offsets from the network header in the
skb control block. That metadata is only safe to use while it still
describes the header at skb_network_header().
This invariant can be broken in more than one way. An IPv6 UDP packet can
remain queued while IPV6_ADDRFORM converts its socket to IPv4, after which
IP_RETOPTS interprets inet6_skb_parm as inet_skb_parm. Also,
ipmr_cache_report() retains the control block when it builds a PIM
register whole-packet report, but pushes a new 20-byte header without
recompiling the option offsets.
In the latter case, a Record-Route offset of 20 points at the original IPv4
header after the push. __ip_options_echo() then reads the original TOS
byte as the option length. KASAN reported a 212-byte write into the 40-byte
stack option-data area on three fresh boots.
__ip_options_echo() currently trusts both the compiled offsets and the
length bytes found at those offsets. Its fixed-size callers reserve 40
bytes for option data, while the TCP caller allocates only sopt->optlen
bytes.
Require the compiled option length to match the current IPv4 header, and
validate each option's offset, kind, minimum length, source span, and
remaining destination capacity before copying it. Use sopt->optlen as the
destination bound so the validation also covers the smaller TCP allocation.
Keep this check local to option echoing. Rejecting every SOL_IP cmsg based
on the current network-header version drops supported metadata, including
the physical egress IP_PKTINFO on IPv4 TX timestamps taken after IPv6
tunnel encapsulation.
With this change, the original IPV6_ADDRFORM input and the PIM register-vif
input were KASAN-clean. The latter returned MSG_CTRUNC on three fresh
boots, a normal IPv4 Record-Route IP_RETOPTS cmsg was preserved, and
tunneled TX timestamp IP_PKTINFO was restored.
Queued IPv6 payloads can still be returned with bounded but incorrect IPv4
peer or error-queue address metadata after IPV6_ADDRFORM. This change only
establishes the memory-safety invariant required by option echoing.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Closes: https://lore.kernel.org/r/20260825221922.85651-1-4ncienth@gmail.com
Link: https://lore.kernel.org/r/20260829144847.1738294-1-4ncienth@gmail.com
Link: https://lore.kernel.org/r/20260901141352.236286-1-pabeni@redhat.com
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Daehyeon Ko <4ncienth@gmail.com>
---
v3:
- replace the global cmsg version guard with class-wide validation in
__ip_options_echo()
- bound each option by the current header span and the sopt->optlen
destination capacity used by TCP
- cover the PIM register-vif stale-offset trigger and restore tunneled TX
timestamp IP_PKTINFO
- document the bounded ADDRFORM peer/error address confusion that remains
v2: https://lore.kernel.org/r/20260829144847.1738294-1-4ncienth@gmail.com
- use the network header version instead of skb->protocol
- preserve IPv4 multicast-report and software-VLAN timestamp IP_PKTINFO
v1: https://lore.kernel.org/r/20260825221922.85651-1-4ncienth@gmail.com
- reject all SOL_IP cmsgs unless skb->protocol is ETH_P_IP
---
net/ipv4/ip_options.c | 54 +++++++++++++++++++++++++++++++++++++++----
1 file changed, 49 insertions(+), 5 deletions(-)
diff --git a/net/ipv4/ip_options.c b/net/ipv4/ip_options.c
index 09d745112c15..916259b833a3 100644
--- a/net/ipv4/ip_options.c
+++ b/net/ipv4/ip_options.c
@@ -74,10 +74,32 @@ void ip_options_build(struct sk_buff *skb, struct ip_options *opt,
* NOTE: dopt cannot point to skb.
*/
+static int ip_options_echo_len(const unsigned char *sptr,
+ const struct ip_options *sopt,
+ const struct ip_options *dopt,
+ unsigned int offset, unsigned int option,
+ unsigned int minlen)
+{
+ unsigned int end = sizeof(struct iphdr) + sopt->optlen;
+ unsigned int optlen;
+
+ if (offset < sizeof(struct iphdr) || offset + minlen > end ||
+ sptr[offset] != option || dopt->optlen > sopt->optlen)
+ return -EINVAL;
+
+ optlen = sptr[offset + 1];
+ if (optlen < minlen || optlen > end - offset ||
+ optlen > sopt->optlen - dopt->optlen)
+ return -EINVAL;
+
+ return optlen;
+}
+
int __ip_options_echo(struct net *net, struct ip_options *dopt,
struct sk_buff *skb, const struct ip_options *sopt)
{
unsigned char *sptr, *dptr;
+ unsigned int hlen;
int soffset, doffset;
int optlen;
@@ -86,11 +108,21 @@ int __ip_options_echo(struct net *net, struct ip_options *dopt,
if (sopt->optlen == 0)
return 0;
+ if (ip_hdr(skb)->version != IPVERSION || ip_hdr(skb)->ihl < 5)
+ return -EINVAL;
+ hlen = ip_hdrlen(skb);
+ if (sopt->optlen != hlen - sizeof(struct iphdr) ||
+ !pskb_network_may_pull(skb, hlen))
+ return -EINVAL;
+
sptr = skb_network_header(skb);
dptr = dopt->__data;
if (sopt->rr) {
- optlen = sptr[sopt->rr+1];
+ optlen = ip_options_echo_len(sptr, sopt, dopt,
+ sopt->rr, IPOPT_RR, 3);
+ if (optlen < 0)
+ return optlen;
soffset = sptr[sopt->rr+2];
dopt->rr = dopt->optlen + sizeof(struct iphdr);
memcpy(dptr, sptr+sopt->rr, optlen);
@@ -104,7 +136,10 @@ int __ip_options_echo(struct net *net, struct ip_options *dopt,
dopt->optlen += optlen;
}
if (sopt->ts) {
- optlen = sptr[sopt->ts+1];
+ optlen = ip_options_echo_len(sptr, sopt, dopt,
+ sopt->ts, IPOPT_TIMESTAMP, 4);
+ if (optlen < 0)
+ return optlen;
soffset = sptr[sopt->ts+2];
dopt->ts = dopt->optlen + sizeof(struct iphdr);
memcpy(dptr, sptr+sopt->ts, optlen);
@@ -141,10 +176,16 @@ int __ip_options_echo(struct net *net, struct ip_options *dopt,
dopt->optlen += optlen;
}
if (sopt->srr) {
- unsigned char *start = sptr+sopt->srr;
+ unsigned char *start;
+ unsigned int option;
__be32 faddr;
- optlen = start[1];
+ option = sopt->is_strictroute ? IPOPT_SSRR : IPOPT_LSRR;
+ optlen = ip_options_echo_len(sptr, sopt, dopt,
+ sopt->srr, option, 3);
+ if (optlen < 0)
+ return optlen;
+ start = sptr + sopt->srr;
soffset = start[2];
doffset = 0;
if (soffset > optlen)
@@ -173,7 +214,10 @@ int __ip_options_echo(struct net *net, struct ip_options *dopt,
}
}
if (sopt->cipso) {
- optlen = sptr[sopt->cipso+1];
+ optlen = ip_options_echo_len(sptr, sopt, dopt,
+ sopt->cipso, IPOPT_CIPSO, 2);
+ if (optlen < 0)
+ return optlen;
dopt->cipso = dopt->optlen+sizeof(struct iphdr);
memcpy(dptr, sptr+sopt->cipso, optlen);
dptr += optlen;
base-commit: 70f3995830d3f1e79faa14eb0605914f778feca9
--
2.55.0
syzbot ci has tested the following series [v3] ip: validate options before echoing them https://lore.kernel.org/all/20260902055802.3724915-1-4ncienth@gmail.com * [PATCH net v3] ip: validate options before echoing them and found the following issue: WARNING in __ip_options_echo Full report is available here: https://ci.syzbot.org/series/58d17295-c695-49b2-826d-b0fe305808e9 *** WARNING in __ip_options_echo tree: linux-next URL: https://kernel.googlesource.com/pub/scm/linux/kernel/git/next/linux-next base: 70f3995830d3f1e79faa14eb0605914f778feca9 arch: amd64 compiler: Debian clang version 22.1.8 (++20260613092233+e80beda6e255-1~exp1~20260613092250.77), Debian LLD 22.1.8 config: https://ci.syzbot.org/builds/9a250d6c-0a45-4565-bbc0-cac6bc3e42ee/config syz repro: https://ci.syzbot.org/findings/5fa937c9-0c72-4a82-8221-cd01d8dc83d5/syz_repro ------------[ cut here ]------------ len > ((int)(~0U >> 1)) WARNING: ./include/linux/skbuff.h:2864 at pskb_may_pull_reason include/linux/skbuff.h:2864 [inline], CPU#1: syz.0.17/5791 WARNING: ./include/linux/skbuff.h:2864 at pskb_network_may_pull_reason include/linux/skbuff.h:3294 [inline], CPU#1: syz.0.17/5791 WARNING: ./include/linux/skbuff.h:2864 at pskb_network_may_pull include/linux/skbuff.h:3299 [inline], CPU#1: syz.0.17/5791 WARNING: ./include/linux/skbuff.h:2864 at __ip_options_echo+0x10e4/0x1870 net/ipv4/ip_options.c:115, CPU#1: syz.0.17/5791 Modules linked in: CPU: 1 UID: 0 PID: 5791 Comm: syz.0.17 Not tainted syzkaller #0 PREEMPT(full) Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014 RIP: 0010:pskb_may_pull_reason include/linux/skbuff.h:2864 [inline] RIP: 0010:pskb_network_may_pull_reason include/linux/skbuff.h:3294 [inline] RIP: 0010:pskb_network_may_pull include/linux/skbuff.h:3299 [inline] RIP: 0010:__ip_options_echo+0x10e4/0x1870 net/ipv4/ip_options.c:115 Code: 00 00 00 fc ff df 43 0f b6 04 3c 84 c0 0f 85 64 07 00 00 41 88 1e 4c 8b 64 24 10 41 83 c4 04 e9 a7 fe ff ff e8 7d 13 84 f7 90 <0f> 0b 90 e9 35 f1 ff ff 44 89 e7 44 89 f6 e8 79 15 84 f7 45 39 f4 RSP: 0018:ffffc90004efef40 EFLAGS: 00010293 RAX: ffffffff8a43a023 RBX: ffff8881b89a6b58 RCX: ffff888111125a00 RDX: 0000000000000000 RSI: 00000000fffffff8 RDI: 0000000000000000 RBP: ffffc90004eff070 R08: ffffc90004eff2c7 R09: 0000000000000000 R10: ffffc90004eff2b8 R11: fffff520009dfe59 R12: 00000000fffffff8 R13: ffff8881b89a6a80 R14: ffff8881b89a6b00 R15: 000000000000001c FS: 00007f39f8af86c0(0000) GS:ffff8882a8cdf000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: 00007f39f8af7ff8 CR3: 0000000112830000 CR4: 00000000000006f0 Call Trace: <TASK> ip_options_echo include/net/ip.h:798 [inline] icmp_reply+0x2da/0xc90 net/ipv4/icmp.c:429 icmp_timestamp+0x246/0x370 net/ipv4/icmp.c:1430 icmp_rcv+0xd33/0x1290 net/ipv4/icmp.c:1543 ip_protocol_deliver_rcu+0x2dc/0x440 net/ipv4/ip_input.c:207 ip_local_deliver_finish+0x3bb/0x6f0 net/ipv4/ip_input.c:241 NF_HOOK+0x336/0x3c0 include/linux/netfilter.h:325 NF_HOOK+0x336/0x3c0 include/linux/netfilter.h:325 __netif_receive_skb_one_core net/core/dev.c:6264 [inline] __netif_receive_skb net/core/dev.c:6377 [inline] netif_receive_skb_internal net/core/dev.c:6463 [inline] netif_receive_skb+0x45b/0xbf0 net/core/dev.c:6522 tun_rx_batched+0x1de/0x790 drivers/net/tun.c:1571 tun_get_user+0x2be0/0x44d0 drivers/net/tun.c:2045 tun_chr_write_iter+0x113/0x200 drivers/net/tun.c:2091 new_sync_write fs/read_write.c:595 [inline] vfs_write+0x612/0xba0 fs/read_write.c:687 ksys_write+0x150/0x270 fs/read_write.c:739 do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline] do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84 entry_SYSCALL_64_after_hwframe+0x77/0x7f RIP: 0033:0x7f39f7b5e98e Code: 08 0f 85 a5 a8 ff ff 49 89 fb 48 89 f0 48 89 d7 48 89 ce 4c 89 c2 4d 89 ca 4c 8b 44 24 08 4c 8b 4c 24 10 4c 89 5c 24 08 0f 05 <c3> 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 80 00 00 00 00 48 83 ec 08 RSP: 002b:00007f39f8af7fb8 EFLAGS: 00000246 ORIG_RAX: 0000000000000001 RAX: ffffffffffffffda RBX: 00007f39f8af86c0 RCX: 00007f39f7b5e98e RDX: 000000000000003e RSI: 0000200000000780 RDI: 00000000000000c8 RBP: 00007f39f7c35024 R08: 0000000000000000 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000 R13: 00007f39f7e26038 R14: 00007f39f7e25fa0 R15: 00007fff66b2b028 </TASK> *** If these findings have caused you to resend the series or submit a separate fix, please add the following tag to your commit message: Tested-by: syzbot@syzkaller.appspotmail.com --- This report is generated by a bot. It may contain errors. syzbot ci engineers can be reached at syzkaller@googlegroups.com. To test a fix for this bug, please reply with `#syz test` (on a separate line) and attach the patch to the email. Notes: - The patch will be applied on top of the tested series (as an incremental fix). - To test a new version of the whole series, please send it directly to syzbot@lists.linux.dev. - Arguments like custom git repos and branches are not supported.
on 9/2/26 1:58 PM, Daehyeon Ko wrote:
> IPv4 option metadata stores absolute offsets from the network header in the
> skb control block. That metadata is only safe to use while it still
> describes the header at skb_network_header().
>
> This invariant can be broken in more than one way. An IPv6 UDP packet can
> remain queued while IPV6_ADDRFORM converts its socket to IPv4, after which
For IPV6_ADDRFORM, we already reject this operation if outgoing ipv6
packets still exist.
case IPV6_ADDRFORM: ...... if (sk->sk_protocol == IPPROTO_UDP) { if
(udp_sk(sk)->pending == AF_INET6) { retv = -EBUSY; break; }
May be we can also reject this operationif ipv6 skbs still exist in
read/recv queue ?
On Wed, Sep 2, 2026 at 7:58 AM Daehyeon Ko <4ncienth@gmail.com> wrote:
>
> IPv4 option metadata stores absolute offsets from the network header in the
> skb control block. That metadata is only safe to use while it still
> describes the header at skb_network_header().
>
> This invariant can be broken in more than one way. An IPv6 UDP packet can
> remain queued while IPV6_ADDRFORM converts its socket to IPv4, after which
> IP_RETOPTS interprets inet6_skb_parm as inet_skb_parm. Also,
> ipmr_cache_report() retains the control block when it builds a PIM
> register whole-packet report, but pushes a new 20-byte header without
> recompiling the option offsets.
>
> In the latter case, a Record-Route offset of 20 points at the original IPv4
> header after the push. __ip_options_echo() then reads the original TOS
> byte as the option length. KASAN reported a 212-byte write into the 40-byte
> stack option-data area on three fresh boots.
>
> __ip_options_echo() currently trusts both the compiled offsets and the
> length bytes found at those offsets. Its fixed-size callers reserve 40
> bytes for option data, while the TCP caller allocates only sopt->optlen
> bytes.
>
> Require the compiled option length to match the current IPv4 header, and
> validate each option's offset, kind, minimum length, source span, and
> remaining destination capacity before copying it. Use sopt->optlen as the
> destination bound so the validation also covers the smaller TCP allocation.
>
> Keep this check local to option echoing. Rejecting every SOL_IP cmsg based
> on the current network-header version drops supported metadata, including
> the physical egress IP_PKTINFO on IPv4 TX timestamps taken after IPv6
> tunnel encapsulation.
>
> With this change, the original IPV6_ADDRFORM input and the PIM register-vif
> input were KASAN-clean. The latter returned MSG_CTRUNC on three fresh
> boots, a normal IPv4 Record-Route IP_RETOPTS cmsg was preserved, and
> tunneled TX timestamp IP_PKTINFO was restored.
>
> Queued IPv6 payloads can still be returned with bounded but incorrect IPv4
> peer or error-queue address metadata after IPV6_ADDRFORM. This change only
> establishes the memory-safety invariant required by option echoing.
>
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Closes: https://lore.kernel.org/r/20260825221922.85651-1-4ncienth@gmail.com
> Link: https://lore.kernel.org/r/20260829144847.1738294-1-4ncienth@gmail.com
> Link: https://lore.kernel.org/r/20260901141352.236286-1-pabeni@redhat.com
> Cc: stable@vger.kernel.org
> Assisted-by: LLM
> Signed-off-by: Daehyeon Ko <4ncienth@gmail.com>
Oh well. Ai reviews will probably raise many bugs in this patch :/
ipmr_cache_report() is a distinct bug and needs its own one liner fix.
When ipmr_cache_report() builds an IGMPMSG_WHOLEPKT or IGMPMSG_WRVIFWHOLE
report, it reallocates headroom and pushes a 20-byte iphdr with ihl = 5, but
inherits pkt->cb with stale IPCB(skb)->opt offsets.
Even if __ip_options_echo() rejects it, ip_cmsg_recv_opts() will still read
stale IPCB(skb)->opt.optlen bytes from ip_hdr(skb) + 1 and leak packet data
as IP_RECVOPTS.
ipmr_cache_report() must clear IPCB(skb)->opt (or memset IPCB(skb)) when
prepending the report header.
Something like:
diff --git a/net/ipv4/ipmr.c b/net/ipv4/ipmr.c
index e5f2b1c6150d2f6b2084c2ebaef6047db869cc87..72f1d620b8760722f805935d0299bac24a9f720c
100644
--- a/net/ipv4/ipmr.c
+++ b/net/ipv4/ipmr.c
@@ -1102,6 +1102,7 @@ static int ipmr_cache_report(const struct mr_table *mrt,
ip_hdr(skb)->ihl = sizeof(struct iphdr) >> 2;
ip_hdr(skb)->tot_len = htons(ntohs(ip_hdr(pkt)->tot_len) +
sizeof(struct iphdr));
+ memset(&IPCB(skb)->opt, 0, sizeof(IPCB(skb)->opt));
} else {
/* Copy the IP header */
skb_set_network_header(skb, skb->len);
Then the IPV6_ADDRFORM problem must be dealt without adding defensive
programming in net/ipv4/ip_options.c
On Wed, Sep 02, 2026 at 10:44:45AM +0200, Eric Dumazet wrote:
> Oh well. Ai reviews will probably raise many bugs in this patch :/
>
> ipmr_cache_report() is a distinct bug and needs its own one liner fix.
>
> When ipmr_cache_report() builds an IGMPMSG_WHOLEPKT or IGMPMSG_WRVIFWHOLE
> report, it reallocates headroom and pushes a 20-byte iphdr with ihl = 5, but
> inherits pkt->cb with stale IPCB(skb)->opt offsets.
> Even if __ip_options_echo() rejects it, ip_cmsg_recv_opts() will still read
> stale IPCB(skb)->opt.optlen bytes from ip_hdr(skb) + 1 and leak packet data
> as IP_RECVOPTS.
> ipmr_cache_report() must clear IPCB(skb)->opt (or memset IPCB(skb)) when
> prepending the report header.
>
> Something like:
>
> diff --git a/net/ipv4/ipmr.c b/net/ipv4/ipmr.c
> index e5f2b1c6150d2f6b2084c2ebaef6047db869cc87..72f1d620b8760722f805935d0299bac24a9f720c
> 100644
> --- a/net/ipv4/ipmr.c
> +++ b/net/ipv4/ipmr.c
> @@ -1102,6 +1102,7 @@ static int ipmr_cache_report(const struct mr_table *mrt,
> ip_hdr(skb)->ihl = sizeof(struct iphdr) >> 2;
> ip_hdr(skb)->tot_len = htons(ntohs(ip_hdr(pkt)->tot_len) +
> sizeof(struct iphdr));
> + memset(&IPCB(skb)->opt, 0, sizeof(IPCB(skb)->opt));
> } else {
> /* Copy the IP header */
> skb_set_network_header(skb, skb->len);
Yes, this was raised in the past [1].
Zhiling Zou, please send v4 of [2].
[1] https://lore.kernel.org/netdev/20260805081737.GA1284302@shredder/
[2] https://lore.kernel.org/netdev/e93d0d2fa5725ddd06b20e3e4223ab68b93b48d1.1785719031.git.zhilinz@nebusec.ai/
© 2016 - 2026 Red Hat, Inc.