Netdev List
 help / color / mirror / Atom feed
* [PATCH net v3] ip: validate options before echoing them
@ 2026-09-02  5:58 Daehyeon Ko
  2026-09-02  8:44 ` Eric Dumazet
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Daehyeon Ko @ 2026-09-02  5:58 UTC (permalink / raw)
  To: netdev
  Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, linux-kernel

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

^ permalink raw reply related	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-09-02 22:05 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-02  5:58 [PATCH net v3] ip: validate options before echoing them Daehyeon Ko
2026-09-02  8:44 ` Eric Dumazet
2026-09-02 15:53   ` Ido Schimmel
2026-09-02  9:29 ` Jiayuan Chen
2026-09-02 22:05 ` [syzbot ci] " syzbot ci

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox