โฌข DragonFlyBSD Kernel Audit
DF-2552 / arith_proof.c
โ† back to finding โ†“ download raw
/*
 * DF-2552 โ€” Arithmetic proof of the sbuf_extend/sbuf_extendsize int-truncation
 * defect.
 *
 * This program replicates the EXACT arithmetic of:
 *   sys/kern/subr_sbuf.c: sbuf_extendsize(int size)
 *   sys/kern/subr_sbuf.c: sbuf_extend(struct sbuf *s, int addlen)
 *
 * It shows that when s->s_size (ssize_t) + addlen (int) exceeds INT_MAX,
 * the narrowing conversion to `int` (the parameter type of
 * sbuf_extendsize) produces a negative value.  sbuf_extendsize then takes
 * the `size < 4096` branch and returns SBUF_MINEXTENDSIZE (16).  The
 * subsequent memcpy(newbuf, s->s_buf, s->s_size) in sbuf_extend copies
 * s->s_size bytes (e.g. 4096) into the 16-byte buffer โ€” a heap overflow.
 *
 * This proves the arithmetic defect WITHOUT needing a kernel.  The
 * companion kernel-module harness (sbuftest_mod/) demonstrates the actual
 * heap overflow in the running kernel.
 *
 * Reachability note: sbuf_bcopyin/sbuf_copyin/sbuf_uionew โ€” the only
 * functions that pass user-influenced addlen to sbuf_extend โ€” have ZERO
 * in-tree callers (verified via grep).  All live sbuf growth happens one
 * byte at a time via sbuf_put_byte -> sbuf_extend(s, 1), which can never
 * overflow int.  The defect is therefore a LATENT code-level bug, not
 * reachable from unprivileged userspace on the current kernel.
 */

#include <stdio.h>
#include <stdint.h>
#include <limits.h>

/* --- Exact replicas from sys/kern/subr_sbuf.c --- */

#define SBUF_MINEXTENDSIZE  16
#define SBUF_MAXEXTENDSIZE  4096
#define SBUF_MAXEXTENDINCR  4096

#define roundup2(x, y)  (((x) + ((y) - 1)) & (~((uintptr_t)(y) - 1)))

/* Replica of sbuf_extendsize (subr_sbuf.c:131-145) โ€” takes int, narrows */
static int
sbuf_extendsize_buggy(int size)
{
    int newsize;

    if (size < (int)SBUF_MAXEXTENDSIZE) {
        newsize = SBUF_MINEXTENDSIZE;
        while (newsize < size)
            newsize *= 2;
    } else {
        newsize = roundup2(size, SBUF_MAXEXTENDINCR);
    }
    /* KASSERT(newsize >= size) โ€” we check and report */
    return (newsize);
}

/* Replica of the FIXED sbuf_extendsize โ€” takes ssize_t, no truncation */
static int
sbuf_extendsize_fixed(long size)  /* ssize_t == long on 64-bit */
{
    int newsize;

    if (size < 0)
        return (-1);  /* caller checks for error */
    if (size < (long)SBUF_MAXEXTENDSIZE) {
        newsize = SBUF_MINEXTENDSIZE;
        while (newsize < size)
            newsize *= 2;
    } else {
        newsize = roundup2(size, SBUF_MAXEXTENDINCR);
    }
    return (newsize);
}

/*
 * Simulate the extend arithmetic.
 *   s_size: ssize_t (the sbuf's current buffer size)
 *   addlen: int (bytes needed)
 * Returns the newsize that sbuf_extend would kmalloc.
 */
static void
simulate_extend(long s_size, int addlen, const char *label)
{
    long  ssize_sum;    /* s->s_size + addlen  computed as ssize_t */
    int   narrowed;     /* (int)(s->s_size + addlen) โ€” what sbuf_extendsize sees */
    int   newsize_buggy;
    int   newsize_fixed;

    ssize_sum  = s_size + (long)addlen;
    narrowed   = (int)ssize_sum;     /* implicit narrowing in the real code */

    newsize_buggy = sbuf_extendsize_buggy(narrowed);

    printf("=== %s ===\n", label);
    printf("  s_size (ssize_t)  = %ld\n", s_size);
    printf("  addlen (int)      = %d\n", addlen);
    printf("  s_size + addlen   = %ld  (as ssize_t, no overflow in 64-bit)\n", ssize_sum);
    printf("  (int)(sum)        = %d  (narrowed โ€” what sbuf_extendsize receives)\n", narrowed);
    printf("  BUGGY newsize     = %d  (sbuf_extendsize returns this)\n", newsize_buggy);

    if (newsize_buggy < s_size && newsize_buggy < ssize_sum) {
        long overflow_bytes = s_size - newsize_buggy;
        printf("  *** HEAP OVERFLOW: kmalloc(%d) then memcpy(%ld bytes) => %ld-byte overflow! ***\n",
               newsize_buggy, s_size, overflow_bytes);
    } else {
        printf("  (no overflow โ€” allocation is large enough)\n");
    }

    /* Now show the fixed version */
    if (ssize_sum > INT_MAX || ssize_sum < 0) {
        newsize_fixed = -1;  /* fixed code rejects */
        printf("  FIXED newsize     = ENOMEM (overflow detected, extend refused)\n");
    } else {
        newsize_fixed = sbuf_extendsize_fixed(ssize_sum);
        printf("  FIXED newsize     = %d  (correct)\n", newsize_fixed);
    }
    printf("\n");
}

int main(void)
{
    long s_size;
    int  addlen;
    long trigger_len;

    printf("DF-2552: sbuf_extend / sbuf_extendsize int-truncation proof\n");
    printf("=============================================================\n\n");

    /* Scenario 1: normal operation (no overflow) */
    s_size = 16; addlen = 1;
    simulate_extend(s_size, addlen, "Normal: s_size=16, addlen=1 (sbuf_put_byte path)");

    /* Scenario 2: the bug โ€” s_size=4096, addlen crafted via sbuf_bcopyin */
    /*
     * sbuf_bcopyin(s, uaddr, len) computes addlen = (int)(len - SBUF_FREESPACE(s)).
     * With s_size=4096, s_len=0: SBUF_FREESPACE = 4095.
     * Choose len = INT_MAX + 4095 = 2147487742 so addlen = INT_MAX = 2147483647.
     * Then s_size + addlen = 4096 + 2147483647 = 2147487743 > INT_MAX.
     * Narrowed to int: -2147479553 (negative).
     * sbuf_extendsize(-2147479553) returns 16.
     * kmalloc(16), memcpy(old 4096 bytes) => 4080-byte heap overflow.
     */
    s_size = 4096;
    trigger_len = (long)INT_MAX + 4095;  /* = 2147487742 */
    /* In sbuf_bcopyin: addlen = (int)(trigger_len - SBUF_FREESPACE(s)) */
    long freespace = s_size - 1;  /* SBUF_FREESPACE = s_size - (s_len + 1) = 4095 */
    addlen = (int)(trigger_len - freespace);  /* should be INT_MAX */

    printf("sbuf_bcopyin path: s_size=%ld, len=%ld, SBUF_FREESPACE=%ld\n",
           s_size, trigger_len, freespace);
    printf("  addlen = (int)(len - SBUF_FREESPACE) = (int)(%ld) = %d\n\n",
           trigger_len - freespace, addlen);

    simulate_extend(s_size, addlen, "BUG: sbuf_bcopyin with crafted len");

    /* Scenario 3: larger s_size, same pattern */
    s_size = 65536;
    freespace = s_size - 1;
    trigger_len = (long)INT_MAX + freespace;
    addlen = (int)(trigger_len - freespace);
    printf("sbuf_bcopyin path: s_size=%ld, len=%ld, addlen=%d\n\n",
           s_size, trigger_len, addlen);
    simulate_extend(s_size, addlen, "BUG: larger s_size=65536");

    printf("=============================================================\n");
    printf("CONCLUSION: The int-truncation in sbuf_extendsize(int) is real.\n");
    printf("When s_size + addlen > INT_MAX, the narrowed argument is negative,\n");
    printf("sbuf_extendsize returns 16, and sbuf_extend's memcpy overflows the\n");
    printf("16-byte allocation by s_size - 16 bytes.\n");
    printf("REACHABILITY: sbuf_bcopyin/sbuf_copyin/sbuf_uionew (the only callers\n");
    printf("that pass large addlen) have ZERO in-tree callers. Not reachable from\n");
    printf("unprivileged userspace. Latent code defect.\n");

    return 0;
}