| From foo@baz Tue Jan 26 21:35:03 PST 2016 |
| From: willy tarreau <w@1wt.eu> |
| Date: Sun, 10 Jan 2016 07:54:56 +0100 |
| Subject: unix: properly account for FDs passed over unix sockets |
| |
| From: willy tarreau <w@1wt.eu> |
| |
| [ Upstream commit 712f4aad406bb1ed67f3f98d04c044191f0ff593 ] |
| |
| It is possible for a process to allocate and accumulate far more FDs than |
| the process' limit by sending them over a unix socket then closing them |
| to keep the process' fd count low. |
| |
| This change addresses this problem by keeping track of the number of FDs |
| in flight per user and preventing non-privileged processes from having |
| more FDs in flight than their configured FD limit. |
| |
| Reported-by: socketpair@gmail.com |
| Reported-by: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp> |
| Mitigates: CVE-2013-4312 (Linux 2.0+) |
| Suggested-by: Linus Torvalds <torvalds@linux-foundation.org> |
| Acked-by: Hannes Frederic Sowa <hannes@stressinduktion.org> |
| Signed-off-by: Willy Tarreau <w@1wt.eu> |
| Signed-off-by: David S. Miller <davem@davemloft.net> |
| Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org> |
| --- |
| include/linux/sched.h | 1 + |
| net/unix/af_unix.c | 24 ++++++++++++++++++++---- |
| net/unix/garbage.c | 13 ++++++++----- |
| 3 files changed, 29 insertions(+), 9 deletions(-) |
| |
| --- a/include/linux/sched.h |
| +++ b/include/linux/sched.h |
| @@ -830,6 +830,7 @@ struct user_struct { |
| unsigned long mq_bytes; /* How many bytes can be allocated to mqueue? */ |
| #endif |
| unsigned long locked_shm; /* How many pages of mlocked shm ? */ |
| + unsigned long unix_inflight; /* How many files in flight in unix sockets */ |
| |
| #ifdef CONFIG_KEYS |
| struct key *uid_keyring; /* UID specific keyring */ |
| --- a/net/unix/af_unix.c |
| +++ b/net/unix/af_unix.c |
| @@ -1512,6 +1512,21 @@ static void unix_destruct_scm(struct sk_ |
| sock_wfree(skb); |
| } |
| |
| +/* |
| + * The "user->unix_inflight" variable is protected by the garbage |
| + * collection lock, and we just read it locklessly here. If you go |
| + * over the limit, there might be a tiny race in actually noticing |
| + * it across threads. Tough. |
| + */ |
| +static inline bool too_many_unix_fds(struct task_struct *p) |
| +{ |
| + struct user_struct *user = current_user(); |
| + |
| + if (unlikely(user->unix_inflight > task_rlimit(p, RLIMIT_NOFILE))) |
| + return !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN); |
| + return false; |
| +} |
| + |
| #define MAX_RECURSION_LEVEL 4 |
| |
| static int unix_attach_fds(struct scm_cookie *scm, struct sk_buff *skb) |
| @@ -1520,6 +1535,9 @@ static int unix_attach_fds(struct scm_co |
| unsigned char max_level = 0; |
| int unix_sock_count = 0; |
| |
| + if (too_many_unix_fds(current)) |
| + return -ETOOMANYREFS; |
| + |
| for (i = scm->fp->count - 1; i >= 0; i--) { |
| struct sock *sk = unix_get_socket(scm->fp->fp[i]); |
| |
| @@ -1541,10 +1559,8 @@ static int unix_attach_fds(struct scm_co |
| if (!UNIXCB(skb).fp) |
| return -ENOMEM; |
| |
| - if (unix_sock_count) { |
| - for (i = scm->fp->count - 1; i >= 0; i--) |
| - unix_inflight(scm->fp->fp[i]); |
| - } |
| + for (i = scm->fp->count - 1; i >= 0; i--) |
| + unix_inflight(scm->fp->fp[i]); |
| return max_level; |
| } |
| |
| --- a/net/unix/garbage.c |
| +++ b/net/unix/garbage.c |
| @@ -120,11 +120,11 @@ void unix_inflight(struct file *fp) |
| { |
| struct sock *s = unix_get_socket(fp); |
| |
| + spin_lock(&unix_gc_lock); |
| + |
| if (s) { |
| struct unix_sock *u = unix_sk(s); |
| |
| - spin_lock(&unix_gc_lock); |
| - |
| if (atomic_long_inc_return(&u->inflight) == 1) { |
| BUG_ON(!list_empty(&u->link)); |
| list_add_tail(&u->link, &gc_inflight_list); |
| @@ -132,25 +132,28 @@ void unix_inflight(struct file *fp) |
| BUG_ON(list_empty(&u->link)); |
| } |
| unix_tot_inflight++; |
| - spin_unlock(&unix_gc_lock); |
| } |
| + fp->f_cred->user->unix_inflight++; |
| + spin_unlock(&unix_gc_lock); |
| } |
| |
| void unix_notinflight(struct file *fp) |
| { |
| struct sock *s = unix_get_socket(fp); |
| |
| + spin_lock(&unix_gc_lock); |
| + |
| if (s) { |
| struct unix_sock *u = unix_sk(s); |
| |
| - spin_lock(&unix_gc_lock); |
| BUG_ON(list_empty(&u->link)); |
| |
| if (atomic_long_dec_and_test(&u->inflight)) |
| list_del_init(&u->link); |
| unix_tot_inflight--; |
| - spin_unlock(&unix_gc_lock); |
| } |
| + fp->f_cred->user->unix_inflight--; |
| + spin_unlock(&unix_gc_lock); |
| } |
| |
| static void scan_inflight(struct sock *x, void (*func)(struct unix_sock *), |