netlink: Always copy on mmap TX.

Checking the file f_count and the nlk->mapped count is not completely
sufficient to prevent the mmap'd area contents from changing from
under us during netlink mmap sendmsg() operations.

Be careful to sample the header's length field only once, because this
could change from under us as well.

Fixes: 5fd96123ee ("netlink: implement memory mapped sendmsg()")
Signed-off-by: David S. Miller <davem@davemloft.net>
Acked-by: Daniel Borkmann <dborkman@redhat.com>
Acked-by: Thomas Graf <tgraf@suug.ch>
This commit is contained in:
David Miller 2014-12-16 17:58:17 -05:00 committed by David S. Miller
parent 65891feac2
commit 4682a03586
1 changed files with 16 additions and 36 deletions

View File

@ -525,14 +525,14 @@ static int netlink_mmap(struct file *file, struct socket *sock,
return err; return err;
} }
static void netlink_frame_flush_dcache(const struct nl_mmap_hdr *hdr) static void netlink_frame_flush_dcache(const struct nl_mmap_hdr *hdr, unsigned int nm_len)
{ {
#if ARCH_IMPLEMENTS_FLUSH_DCACHE_PAGE == 1 #if ARCH_IMPLEMENTS_FLUSH_DCACHE_PAGE == 1
struct page *p_start, *p_end; struct page *p_start, *p_end;
/* First page is flushed through netlink_{get,set}_status */ /* First page is flushed through netlink_{get,set}_status */
p_start = pgvec_to_page(hdr + PAGE_SIZE); p_start = pgvec_to_page(hdr + PAGE_SIZE);
p_end = pgvec_to_page((void *)hdr + NL_MMAP_HDRLEN + hdr->nm_len - 1); p_end = pgvec_to_page((void *)hdr + NL_MMAP_HDRLEN + nm_len - 1);
while (p_start <= p_end) { while (p_start <= p_end) {
flush_dcache_page(p_start); flush_dcache_page(p_start);
p_start++; p_start++;
@ -714,24 +714,16 @@ static int netlink_mmap_sendmsg(struct sock *sk, struct msghdr *msg,
struct nl_mmap_hdr *hdr; struct nl_mmap_hdr *hdr;
struct sk_buff *skb; struct sk_buff *skb;
unsigned int maxlen; unsigned int maxlen;
bool excl = true;
int err = 0, len = 0; int err = 0, len = 0;
/* Netlink messages are validated by the receiver before processing.
* In order to avoid userspace changing the contents of the message
* after validation, the socket and the ring may only be used by a
* single process, otherwise we fall back to copying.
*/
if (atomic_long_read(&sk->sk_socket->file->f_count) > 1 ||
atomic_read(&nlk->mapped) > 1)
excl = false;
mutex_lock(&nlk->pg_vec_lock); mutex_lock(&nlk->pg_vec_lock);
ring = &nlk->tx_ring; ring = &nlk->tx_ring;
maxlen = ring->frame_size - NL_MMAP_HDRLEN; maxlen = ring->frame_size - NL_MMAP_HDRLEN;
do { do {
unsigned int nm_len;
hdr = netlink_current_frame(ring, NL_MMAP_STATUS_VALID); hdr = netlink_current_frame(ring, NL_MMAP_STATUS_VALID);
if (hdr == NULL) { if (hdr == NULL) {
if (!(msg->msg_flags & MSG_DONTWAIT) && if (!(msg->msg_flags & MSG_DONTWAIT) &&
@ -739,35 +731,23 @@ static int netlink_mmap_sendmsg(struct sock *sk, struct msghdr *msg,
schedule(); schedule();
continue; continue;
} }
if (hdr->nm_len > maxlen) {
nm_len = ACCESS_ONCE(hdr->nm_len);
if (nm_len > maxlen) {
err = -EINVAL; err = -EINVAL;
goto out; goto out;
} }
netlink_frame_flush_dcache(hdr); netlink_frame_flush_dcache(hdr, nm_len);
if (likely(dst_portid == 0 && dst_group == 0 && excl)) { skb = alloc_skb(nm_len, GFP_KERNEL);
skb = alloc_skb_head(GFP_KERNEL); if (skb == NULL) {
if (skb == NULL) { err = -ENOBUFS;
err = -ENOBUFS; goto out;
goto out;
}
sock_hold(sk);
netlink_ring_setup_skb(skb, sk, ring, hdr);
NETLINK_CB(skb).flags |= NETLINK_SKB_TX;
__skb_put(skb, hdr->nm_len);
netlink_set_status(hdr, NL_MMAP_STATUS_RESERVED);
atomic_inc(&ring->pending);
} else {
skb = alloc_skb(hdr->nm_len, GFP_KERNEL);
if (skb == NULL) {
err = -ENOBUFS;
goto out;
}
__skb_put(skb, hdr->nm_len);
memcpy(skb->data, (void *)hdr + NL_MMAP_HDRLEN, hdr->nm_len);
netlink_set_status(hdr, NL_MMAP_STATUS_UNUSED);
} }
__skb_put(skb, nm_len);
memcpy(skb->data, (void *)hdr + NL_MMAP_HDRLEN, nm_len);
netlink_set_status(hdr, NL_MMAP_STATUS_UNUSED);
netlink_increment_head(ring); netlink_increment_head(ring);
@ -813,7 +793,7 @@ static void netlink_queue_mmaped_skb(struct sock *sk, struct sk_buff *skb)
hdr->nm_pid = NETLINK_CB(skb).creds.pid; hdr->nm_pid = NETLINK_CB(skb).creds.pid;
hdr->nm_uid = from_kuid(sk_user_ns(sk), NETLINK_CB(skb).creds.uid); hdr->nm_uid = from_kuid(sk_user_ns(sk), NETLINK_CB(skb).creds.uid);
hdr->nm_gid = from_kgid(sk_user_ns(sk), NETLINK_CB(skb).creds.gid); hdr->nm_gid = from_kgid(sk_user_ns(sk), NETLINK_CB(skb).creds.gid);
netlink_frame_flush_dcache(hdr); netlink_frame_flush_dcache(hdr, hdr->nm_len);
netlink_set_status(hdr, NL_MMAP_STATUS_VALID); netlink_set_status(hdr, NL_MMAP_STATUS_VALID);
NETLINK_CB(skb).flags |= NETLINK_SKB_DELIVERED; NETLINK_CB(skb).flags |= NETLINK_SKB_DELIVERED;