diff --git a/include/linux/net.h b/include/linux/net.h index f268f395ce47..277188a40c72 100644 --- a/include/linux/net.h +++ b/include/linux/net.h @@ -47,6 +47,29 @@ typedef struct sockopt { int optlen; } sockopt_t; +/* + * Initialize a user-backed sockopt_t from the (optval, optlen) __user pair of + * a getsockopt() callback. Used by transitional __user getsockopt wrappers + * while the proto-layer callbacks are converted to take a sockopt_t; the + * caller writes opt->optlen back to the user optlen after the callback. + */ +static inline int sockopt_init_user(sockopt_t *opt, char __user *optval, + int __user *optlen) +{ + int len; + + if (get_user(len, optlen)) + return -EFAULT; + if (len < 0) + return -EINVAL; + + iov_iter_ubuf(&opt->iter_out, ITER_DEST, optval, len); + iov_iter_ubuf(&opt->iter_in, ITER_SOURCE, optval, len); + opt->optlen = len; + + return 0; +} + struct poll_table_struct; struct pipe_inode_info; struct inode; diff --git a/include/net/udp.h b/include/net/udp.h index 8262e2b215b4..1fee17274745 100644 --- a/include/net/udp.h +++ b/include/net/udp.h @@ -430,7 +430,7 @@ struct sk_buff *skb_udp_tunnel_segment(struct sk_buff *skb, netdev_features_t features, bool is_ipv6); int udp_lib_getsockopt(struct sock *sk, int level, int optname, - char __user *optval, int __user *optlen); + sockopt_t *opt); int udp_lib_setsockopt(struct sock *sk, int level, int optname, sockptr_t optval, unsigned int optlen, int (*push_pending_frames)(struct sock *)); diff --git a/net/ipv4/raw.c b/net/ipv4/raw.c index e9fbab6ad914..2aebaf8297e0 100644 --- a/net/ipv4/raw.c +++ b/net/ipv4/raw.c @@ -809,23 +809,18 @@ static int raw_seticmpfilter(struct sock *sk, sockptr_t optval, int optlen) return 0; } -static int raw_geticmpfilter(struct sock *sk, char __user *optval, int __user *optlen) +static int raw_geticmpfilter(struct sock *sk, sockopt_t *opt) { - int len, ret = -EFAULT; + int len = opt->optlen; - if (get_user(len, optlen)) - goto out; - ret = -EINVAL; if (len < 0) - goto out; + return -EINVAL; if (len > sizeof(struct icmp_filter)) len = sizeof(struct icmp_filter); - ret = -EFAULT; - if (put_user(len, optlen) || - copy_to_user(optval, &raw_sk(sk)->filter, len)) - goto out; - ret = 0; -out: return ret; + opt->optlen = len; + if (copy_to_iter(&raw_sk(sk)->filter, len, &opt->iter_out) != len) + return -EFAULT; + return 0; } static int do_raw_setsockopt(struct sock *sk, int optname, @@ -848,14 +843,13 @@ static int raw_setsockopt(struct sock *sk, int level, int optname, return do_raw_setsockopt(sk, optname, optval, optlen); } -static int do_raw_getsockopt(struct sock *sk, int optname, - char __user *optval, int __user *optlen) +static int do_raw_getsockopt(struct sock *sk, int optname, sockopt_t *opt) { if (optname == ICMP_FILTER) { if (inet_sk(sk)->inet_num != IPPROTO_ICMP) return -EOPNOTSUPP; else - return raw_geticmpfilter(sk, optval, optlen); + return raw_geticmpfilter(sk, opt); } return -ENOPROTOOPT; } @@ -863,9 +857,24 @@ static int do_raw_getsockopt(struct sock *sk, int optname, static int raw_getsockopt(struct sock *sk, int level, int optname, char __user *optval, int __user *optlen) { + sockopt_t opt; + int err; + if (level != SOL_RAW) return ip_getsockopt(sk, level, optname, optval, optlen); - return do_raw_getsockopt(sk, optname, optval, optlen); + + err = sockopt_init_user(&opt, optval, optlen); + if (err) + return err; + + err = do_raw_getsockopt(sk, optname, &opt); + if (err) + return err; + + if (put_user(opt.optlen, optlen)) + return -EFAULT; + + return 0; } static int raw_ioctl(struct sock *sk, int cmd, int *karg) diff --git a/net/ipv4/udp.c b/net/ipv4/udp.c index 70f6cbd4ef73..59248a59358c 100644 --- a/net/ipv4/udp.c +++ b/net/ipv4/udp.c @@ -76,6 +76,7 @@ #include #include +#include #include #include #include @@ -2995,14 +2996,13 @@ static int udp_setsockopt(struct sock *sk, int level, int optname, sockptr_t opt } int udp_lib_getsockopt(struct sock *sk, int level, int optname, - char __user *optval, int __user *optlen) + sockopt_t *opt) { struct udp_sock *up = udp_sk(sk); int val, len; - if (get_user(len, optlen)) - return -EFAULT; - + len = opt->optlen; + /* keep the check so direct sockopt_t callers stay covered. */ if (len < 0) return -EINVAL; @@ -3037,9 +3037,8 @@ int udp_lib_getsockopt(struct sock *sk, int level, int optname, return -ENOPROTOOPT; } - if (put_user(len, optlen)) - return -EFAULT; - if (copy_to_user(optval, &val, len)) + opt->optlen = len; + if (copy_to_iter(&val, len, &opt->iter_out) != len) return -EFAULT; return 0; } @@ -3047,9 +3046,29 @@ int udp_lib_getsockopt(struct sock *sk, int level, int optname, static int udp_getsockopt(struct sock *sk, int level, int optname, char __user *optval, int __user *optlen) { - if (level == SOL_UDP) - return udp_lib_getsockopt(sk, level, optname, optval, optlen); - return ip_getsockopt(sk, level, optname, optval, optlen); + sockopt_t opt; + int err; + + /* + * keep the old __user pointers, until ip_getsockopt() moves + * to sockopt_t + */ + if (level != SOL_UDP) + return ip_getsockopt(sk, level, optname, optval, optlen); + + err = sockopt_init_user(&opt, optval, optlen); + if (err) + return err; + + err = udp_lib_getsockopt(sk, level, optname, &opt); + if (err) + return err; + + /* optval was written by copy_to_iter() in udp_lib_getsockopt() */ + if (put_user(opt.optlen, optlen)) + return -EFAULT; + + return 0; } /** diff --git a/net/ipv6/udp.c b/net/ipv6/udp.c index 15e032194ecc..392e18b97045 100644 --- a/net/ipv6/udp.c +++ b/net/ipv6/udp.c @@ -1826,9 +1826,22 @@ static int udpv6_setsockopt(struct sock *sk, int level, int optname, static int udpv6_getsockopt(struct sock *sk, int level, int optname, char __user *optval, int __user *optlen) { - if (level == SOL_UDP) - return udp_lib_getsockopt(sk, level, optname, optval, optlen); - return ipv6_getsockopt(sk, level, optname, optval, optlen); + sockopt_t opt; + int err; + + if (level != SOL_UDP) + return ipv6_getsockopt(sk, level, optname, optval, optlen); + + err = sockopt_init_user(&opt, optval, optlen); + if (err) + return err; + + err = udp_lib_getsockopt(sk, level, optname, &opt); + if (err) + return err; + if (put_user(opt.optlen, optlen)) + return -EFAULT; + return 0; } diff --git a/tools/testing/selftests/net/getsockopt_iter.c b/tools/testing/selftests/net/getsockopt_iter.c index 209569354d0e..fe5a5268bc34 100644 --- a/tools/testing/selftests/net/getsockopt_iter.c +++ b/tools/testing/selftests/net/getsockopt_iter.c @@ -11,6 +11,8 @@ * that always reports the required buffer length back via optlen, * even when the user buffer is too small to receive any group bits. * - vsock: SO_VM_SOCKETS_BUFFER_SIZE covers the u64 path. + * - raw: ICMP_FILTER covers a fixed-size struct payload that clamps + * the length down on a short buffer instead of failing. * * Author: Breno Leitao */ @@ -24,12 +26,20 @@ #include #include #include +#include +#include #include #include "kselftest_harness.h" #ifndef AF_VSOCK #define AF_VSOCK 40 #endif +#ifndef SOL_RAW +#define SOL_RAW 255 +#endif +#ifndef ICMP_FILTER +#define ICMP_FILTER 1 +#endif /* ---------- netlink ---------- */ @@ -297,4 +307,91 @@ TEST_F(vsock, connect_timeout_old_exact) ASSERT_EQ(sizeof(tv), optlen); } +/* ---------- raw (ipv4) ---------- */ + +FIXTURE(raw) +{ + int fd; +}; + +FIXTURE_SETUP(raw) +{ + struct icmp_filter filt = { .data = 0xdeadbeef }; + + self->fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); + if (self->fd < 0) + SKIP(return, "SOCK_RAW/ICMP socket: %s", strerror(errno)); + + if (setsockopt(self->fd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt)) < 0) + SKIP(return, "set ICMP_FILTER: %s", strerror(errno)); +} + +FIXTURE_TEARDOWN(raw) +{ + if (self->fd >= 0) + close(self->fd); +} + +TEST_F(raw, icmpfilter_exact) +{ + struct icmp_filter filt = {}; + socklen_t optlen = sizeof(filt); + + ASSERT_EQ(0, getsockopt(self->fd, SOL_RAW, ICMP_FILTER, + &filt, &optlen)); + ASSERT_EQ(sizeof(filt), optlen); + ASSERT_EQ(0xdeadbeef, filt.data); +} + +TEST_F(raw, icmpfilter_oversize_clamped) +{ + char buf[16] = {}; + socklen_t optlen = sizeof(buf); + + ASSERT_EQ(0, getsockopt(self->fd, SOL_RAW, ICMP_FILTER, + buf, &optlen)); + ASSERT_EQ(sizeof(struct icmp_filter), optlen); +} + +/* Unlike the int/u64 options above, ICMP_FILTER clamps the length down + * to the user buffer instead of returning EINVAL: a short buffer + * succeeds and reports the truncated length back via optlen. + */ +TEST_F(raw, icmpfilter_undersize_clamped) +{ + char buf[2] = {}; + socklen_t optlen = sizeof(buf); + + ASSERT_EQ(0, getsockopt(self->fd, SOL_RAW, ICMP_FILTER, + buf, &optlen)); + ASSERT_EQ(sizeof(buf), optlen); +} + +TEST_F(raw, icmpfilter_wrong_proto) +{ + struct icmp_filter filt; + socklen_t optlen = sizeof(filt); + int fd; + + fd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); + if (fd < 0) + SKIP(return, "SOCK_RAW/UDP socket: %s", strerror(errno)); + + ASSERT_EQ(-1, getsockopt(fd, SOL_RAW, ICMP_FILTER, &filt, &optlen)); + ASSERT_EQ(EOPNOTSUPP, errno); + close(fd); +} + +TEST_F(raw, bad_optname) +{ + socklen_t optlen; + int val; + + optlen = sizeof(val); + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_RAW, 0x7fff, &val, &optlen)); + ASSERT_EQ(ENOPROTOOPT, errno); + ASSERT_EQ(sizeof(val), optlen); +} + TEST_HARNESS_MAIN