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 --- 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 470100ae710773..7db3aff33f2ba5 100644 --- a/include/linux/net.h +++ b/include/linux/net.h @@ -70,6 +70,38 @@ 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; +} + +int sockptr_to_sockopt(sockopt_t *opt, sockptr_t optval, sockptr_t optlen, + struct kvec *kvec); + struct poll_table_struct; struct pipe_inode_info; struct inode; diff --git a/net/socket.c b/net/socket.c index c05d86e63abf7d..29a0f7f8e2cabe 100644 --- a/net/socket.c +++ b/net/socket.c @@ -2437,8 +2437,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; -- 2.53.0-Meta