From 0093f7db9c47177fd5a269858002c89c6a3fa340 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 14 Sep 2026 05:20:07 -0700 Subject: [PATCH 1/2] net: add sockopt_expand_out() Some getsockopt options size their reply from a count the caller left in optval rather than from optlen, and so write past the optlen the caller declared. Userspace relies on that, so the sockopt_t conversion has to keep doing it. IP_MSFILTER is the first one to convert: its reply covers the imsf_numsrc sources the caller asked for, while optlen only has to cover the fixed header. Add sockopt_expand_out() to grow opt->iter_out mid-air, so the quirk sits in one place instead of each protocol assuming it implicitly. It is a no-op unless the reply outruns optlen. Growing re-anchors the iterator at the head of optval, so it has to be called before anything is written through iter_out. Only a user buffer can be longer than optlen says. A kernel-backed optval keeps the bounded iterator, and a callback that asks to grow one gets a WARN_ON_ONCE() and -EINVAL. Nothing in tree can trip that WARN: the only kernel-backed path into do_ip_getsockopt() is sol_ip_sockopt(), which takes IP_TOS and IP_TRANSPARENT only. It is an assert for the in-kernel callers BPF and io_uring gain once the conversion is done, so they declare an optlen covering the whole buffer instead of repeating the userspace mistake. The conversion in the next patch calls sockptr_to_sockopt() from net/ipv4/, so drop its static and declare it in net.h. Signed-off-by: Breno Leitao Acked-by: Stanislav Fomichev Link: https://patch.msgid.link/20260914-getsockopt_phase6-v2-1-e48befc9602e@debian.org Signed-off-by: Jakub Kicinski --- include/linux/net.h | 32 ++++++++++++++++++++++++++++++++ net/socket.c | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/include/linux/net.h b/include/linux/net.h index 470100ae7107..e2a866fbcfa5 100644 --- a/include/linux/net.h +++ b/include/linux/net.h @@ -47,6 +47,9 @@ typedef struct sockopt { int optlen; } sockopt_t; +int sockptr_to_sockopt(sockopt_t *opt, sockptr_t optval, sockptr_t optlen, + struct kvec *kvec); + /* * Initialize a user-backed sockopt_t from the (optval, optlen) __user pair of * a getsockopt() callback. Used by transitional __user getsockopt wrappers @@ -70,6 +73,35 @@ static inline int sockopt_init_user(sockopt_t *opt, char __user *optval, return 0; } +/* + * Grow optval to @size, for the options whose reply is sized by a count the + * caller left in optval rather than by optlen. Those write past optlen today + * and userspace relies on it. + * + * Call it before writing through opt->iter_out: it re-anchors the iterator at + * the head of optval. Only a user buffer can be longer than the optlen the + * caller declared, so a kernel-backed optval is refused with -EINVAL. + */ +static inline int sockopt_expand_out(sockopt_t *opt, size_t size) +{ + if (size <= (size_t)opt->optlen) + return 0; + + if (size > INT_MAX) + return -EINVAL; + + /* Re-anchoring reads iter_out.ubuf, so the iterator has to be a user + * buffer that nothing has written through yet. + */ + if (WARN_ON_ONCE(!iter_is_ubuf(&opt->iter_out) || + iov_iter_count(&opt->iter_out) != (size_t)opt->optlen)) + return -EINVAL; + + iov_iter_ubuf(&opt->iter_out, ITER_DEST, opt->iter_out.ubuf, size); + + return 0; +} + struct poll_table_struct; struct pipe_inode_info; struct inode; diff --git a/net/socket.c b/net/socket.c index 4cb8662869f0..bb0e1e118ef6 100644 --- a/net/socket.c +++ b/net/socket.c @@ -2436,8 +2436,8 @@ INDIRECT_CALLABLE_DECLARE(bool tcp_bpf_bypass_getsockopt(int level, * It is important to remember that both iov points to the same data, but, * .iter_in is read-only and .iter_out is write-only by the protocol callbacks */ -static int sockptr_to_sockopt(sockopt_t *opt, sockptr_t optval, - sockptr_t optlen, struct kvec *kvec) +int sockptr_to_sockopt(sockopt_t *opt, sockptr_t optval, + sockptr_t optlen, struct kvec *kvec) { int koptlen; From e12e04f5ab54cd6b4c1819b2a8e870245406022a Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 14 Sep 2026 05:20:08 -0700 Subject: [PATCH 2/2] ipv4: igmp: convert ip_mc_msfget() to sockopt_t IP_MSFILTER reads its reply through ip_mc_msfget(), reached from do_ip_getsockopt() and from nowhere else. Convert it, and build the sockopt_t at the call site for as long as the caller still carries a sockptr_t pair. optlen here only has to cover the header, and the real reply size comes from the imsf_numsrc field inside it. This is nasty, but userspace relies on it, so sockopt_expand_out() preserves the same mechanism: it grows optval only for a user address, and assumes the caller left room for the size its own header asked for. The *optlen store moves out of ip_mc_msfget() and into the call site, guarded by !err so the -EINVAL, -ENODEV and -EADDRNOTAVAIL returns still leave the caller's optlen word untouched. The source list also moves from copy_to_sockptr_offset() to a sequential copy_to_iter(). IP_MSFILTER_SIZE(0) and offsetof(struct ip_msfilter, imsf_slist_flex) are both 16, so the bytes land where they did. Signed-off-by: Breno Leitao Acked-by: Stanislav Fomichev Link: https://patch.msgid.link/20260914-getsockopt_phase6-v2-2-e48befc9602e@debian.org Signed-off-by: Jakub Kicinski --- include/linux/igmp.h | 3 ++- net/ipv4/igmp.c | 23 ++++++++++++++--------- net/ipv4/ip_sockglue.c | 10 +++++++++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/include/linux/igmp.h b/include/linux/igmp.h index a0cf0398519f..e075611344ef 100644 --- a/include/linux/igmp.h +++ b/include/linux/igmp.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -273,7 +274,7 @@ extern int ip_mc_source(int add, int omode, struct sock *sk, struct ip_mreq_source *mreqs, int ifindex); extern int ip_mc_msfilter(struct sock *sk, struct ip_msfilter *msf,int ifindex); extern int ip_mc_msfget(struct sock *sk, struct ip_msfilter *msf, - sockptr_t optval, sockptr_t optlen); + sockopt_t *opt); extern int ip_mc_gsfget(struct sock *sk, struct group_filter *gsf, sockptr_t optval, size_t offset); extern int ip_mc_sf_allow(const struct sock *sk, __be32 local, __be32 rmt, diff --git a/net/ipv4/igmp.c b/net/ipv4/igmp.c index d56355aca797..144fca158adc 100644 --- a/net/ipv4/igmp.c +++ b/net/ipv4/igmp.c @@ -2709,8 +2709,8 @@ done: err = ip_mc_leave_group(sk, &imr); return err; } -int ip_mc_msfget(struct sock *sk, struct ip_msfilter *msf, - sockptr_t optval, sockptr_t optlen) + +int ip_mc_msfget(struct sock *sk, struct ip_msfilter *msf, sockopt_t *opt) { int err, len, count, copycount, msf_size; struct ip_mreqn imr; @@ -2755,14 +2755,19 @@ int ip_mc_msfget(struct sock *sk, struct ip_msfilter *msf, len = flex_array_size(psl, sl_addr, copycount); msf->imsf_numsrc = count; msf_size = IP_MSFILTER_SIZE(copycount); - if (copy_to_sockptr(optlen, &msf_size, sizeof(int)) || - copy_to_sockptr(optval, msf, IP_MSFILTER_SIZE(0))) { + + /* The source list is sized by the imsf_numsrc the caller left in + * optval, not by optlen, which only has to cover the fixed part. + */ + err = sockopt_expand_out(opt, msf_size); + if (err) + return err; + + opt->optlen = msf_size; + if (copy_to_iter(msf, IP_MSFILTER_SIZE(0), &opt->iter_out) != + IP_MSFILTER_SIZE(0)) return -EFAULT; - } - if (len && - copy_to_sockptr_offset(optval, - offsetof(struct ip_msfilter, imsf_slist_flex), - psl->sl_addr, len)) + if (len && copy_to_iter(psl->sl_addr, len, &opt->iter_out) != len) return -EFAULT; return 0; done: diff --git a/net/ipv4/ip_sockglue.c b/net/ipv4/ip_sockglue.c index a55ef327ec93..e06c1f48ecad 100644 --- a/net/ipv4/ip_sockglue.c +++ b/net/ipv4/ip_sockglue.c @@ -1706,6 +1706,8 @@ int do_ip_getsockopt(struct sock *sk, int level, int optname, case IP_MSFILTER: { struct ip_msfilter msf; + struct kvec kvec; + sockopt_t opt; if (len < IP_MSFILTER_SIZE(0)) { err = -EINVAL; @@ -1715,7 +1717,13 @@ int do_ip_getsockopt(struct sock *sk, int level, int optname, err = -EFAULT; goto out; } - err = ip_mc_msfget(sk, &msf, optval, optlen); + err = sockptr_to_sockopt(&opt, optval, optlen, &kvec); + if (err) + goto out; + + err = ip_mc_msfget(sk, &msf, &opt); + if (!err && copy_to_sockptr(optlen, &opt.optlen, sizeof(int))) + err = -EFAULT; goto out; } case MCAST_MSFILTER: