Merge branch 'net-a-sockopt_t-quirk-for-the-options-that-write-past-optlen'

Breno Leitao says:

====================
net: a sockopt_t quirk for the options that write past optlen

This series continues the migration of our protocols to sockopt_t, as
described in [1].

There are some protocols that use optlen as the header size, and the real
buffer size comes from a field inside the header. This means a bug, given
that optlen should be the full buffer size, but there are indications [2]
that we have programs that use the bad behaviour above, and we want to
avoid breaking them (or, honestly, avoid being cursed by Linus).

That said, create a quirk helper that preserves the same behaviour, even
using sockopt_t. The way to do it is simple:

1) Only do it for userspace callers (ubuf), otherwise a bug here will
   corrupt the kernel instead of a simple SIGSEGV.
2) Expand optval mid-air based on the header field.

IP_MSFILTER is the first user, and the smallest one: a single caller, no
compat variant, and a reply written front to back. MCAST_MSFILTER on ipv4
and ipv6 comes next, and TCP_AO_GET_KEYS has the same shape. Do this
quirk on IP_MSFILTER to make sure the dynamic is ok, so, we can expand
it later.

None of this is meant to change what userspace sees.
====================

Link: https://patch.msgid.link/20260914-getsockopt_phase6-v2-0-e48befc9602e@debian.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Jakub Kicinski 2026-09-18 16:42:47 -07:00
commit 83f09b3bcc
5 changed files with 59 additions and 13 deletions

View File

@ -14,6 +14,7 @@
#include <linux/timer.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/net.h>
#include <linux/refcount.h>
#include <linux/sockptr.h>
#include <uapi/linux/igmp.h>
@ -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,

View File

@ -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;

View File

@ -2709,8 +2709,8 @@ int ip_mc_msfilter(struct sock *sk, struct ip_msfilter *msf, int ifindex)
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:

View File

@ -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:

View File

@ -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;