DragonFlyBSD Kernel Audit
DF-0916 / trigger.c
← back to finding ↓ download raw
/*
 * DF-0916 - PoC trigger / defect demonstrator.
 *
 * smb_time_unix2dos() in sys/vfs/smbfs/smbfs_subr.c lines 173-178 contains an
 * unbounded year-computation loop:
 *
 *   for (year = 1970;; year++) {
 *       inc = year & 0x03 ? 365 : 366;
 *       if (days < inc) break;
 *       days -= inc;
 *   }
 *
 * `days` is `u_long t / (24*60*60)`. On x86_64, `u_long` is 64 bits, and
 * smb_time_local2server() does `*seconds = tsp->tv_sec - tzoff*60;` with no
 * range guard, so a caller-supplied tv_sec (signed time_t) of INT64_MAX or -1
 * produces `days ~= 1e14` / `days ~= 2e17`, requiring ~3e11 / ~6e14 loop
 * iterations - i.e. minutes-to-days of kernel CPU per syscall.
 *
 * The kernel-reachable trigger path is:
 *   utimensat(fd, {INT64_MAX,0})  ->  VOP_SETATTR(smbfs vp)
 *       -> smbfs_setattr()                   [sys/vfs/smbfs/smbfs_vnops.c:297]
 *       -> smbfs_smb_setpattr/setftime/setptime2 (mtime)
 *       -> smb_time_unix2dos(mtime, ...)     [sys/vfs/smbfs/smbfs_smb.c:326/421]
 *
 * SMBFS is a kld module (not built into GENERIC) and the SMB-client mount
 * requires an SMB server endpoint, so the full unprivileged utimensat path is
 * not exercised end-to-end in this guest (no SMB server).  This PoC
 * reproduces the EXACT loop body from smb_time_unix2dos in userspace to
 * demonstrate the algorithmic defect directly: with INT64_MAX (or -1) input
 * the loop does not terminate in any human-reasonable timeframe.
 *
 * Build: cc -O2 -o trigger trigger.c -lm
 * Run  : ./trigger [seconds_value]
 *        ./trigger            # defaults to INT64_MAX
 *        ./trigger -1         # signed -1 reinterpreted as u_long = UINT64_MAX
 *        ./trigger 0          # sane value: returns instantly
 *
 * The harness runs the loop with an iteration cap (LOOP_CAP), so you can see
 * the iteration count required; set LOOP_CAP high enough on a long input to
 * prove the loop is unbounded.
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <sys/time.h>

typedef unsigned long  u_long;
typedef unsigned short u_short;
typedef unsigned int   u_int;
typedef uint16_t       u_int16_t;
typedef uint8_t        u_int8_t;

#define DT_2SECONDS_SHIFT 0
#define DT_MINUTES_SHIFT  5
#define DT_HOURS_SHIFT    11
#define DD_DAY_SHIFT      0
#define DD_MONTH_SHIFT    5
#define DD_YEAR_SHIFT     9

static u_short regyear[] = {
    31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365
};
static u_short leapyear[] = {
    31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366
};

/* Iteration cap. Override with $LOOP_CAP env. */
static unsigned long long LOOP_CAP = 500000000ULL; /* 5e8 - ~1s on a fast CPU */

static volatile int timed_out = 0;
static void on_alarm(int s) { (void)s; timed_out = 1; }

/*
 * EXACT copy of smb_time_unix2dos year-loop region (sys/vfs/smbfs/smbfs_subr.c
 * lines 157..194). We instrument the loop with an iteration counter and honor
 * LOOP_CAP / SIGALRM so the harness cannot livelock the test machine.
 */
static int smb_time_unix2dos_instrumented(long long tv_sec, int tzoff,
                                          u_int16_t *ddp, u_int16_t *dtp,
                                          unsigned long long *iters)
{
    u_long t, days, year, inc;
    u_short *months;
    u_short lastdtime, lastddate;
    u_long lastday;
    unsigned long long n = 0;

    /* smb_time_local2server: */
    t = (u_long)(tv_sec - (long long)tzoff * 60);
    t &= ~1;

    lastdtime = (u_short)((((t / 2) % 30) << DT_2SECONDS_SHIFT)
                + (((t / 60) % 60) << DT_MINUTES_SHIFT)
                + (((t / 3600) % 24) << DT_HOURS_SHIFT));

    days = t / (24 * 60 * 60);
    lastday = days;

    /* The buggy loop. */
    for (year = 1970;; year++) {
        inc = year & 0x03 ? 365 : 366;
        if (days < inc)
            break;
        days -= inc;
        if (++n >= LOOP_CAP) {
            *iters = n;
            return -1; /* hit cap => loop unbounded for this input */
        }
        if (timed_out) {
            *iters = n;
            return -2;
        }
    }

    months = year & 0x03 ? regyear : leapyear;
    /* (the month subloop is bounded by days, so safe.) */
    u_long month;
    for (month = 0; days >= months[month]; month++)
        ;
    if (month > 0)
        days -= months[month - 1];
    lastddate = (u_short)(((days + 1) << DD_DAY_SHIFT)
                + ((month + 1) << DD_MONTH_SHIFT));
    if (year > 1980)
        lastddate += (u_short)((year - 1980) << DD_YEAR_SHIFT);

    *ddp = lastddate;
    *dtp = lastdtime;
    *iters = n;
    return 0;
}

int main(int argc, char **argv)
{
    long long input;
    if (argc >= 2) {
        if (strcmp(argv[2 % argc], "-") == 0 || strstr(argv[1],"-1")) {
            /* allow "-1" string */
            input = -1LL;
        } else {
            input = strtoll(argv[1], NULL, 0);
        }
    } else {
        input = (long long)INT64_MAX;
    }
    const char *cap_env = getenv("LOOP_CAP");
    if (cap_env) LOOP_CAP = strtoull(cap_env, NULL, 0);

    printf("DF-0916 smb_time_unix2dos livelock demonstrator\n");
    printf("  input tv_sec  = %lld  (reinterpreted as u_long = 0x%016llx, days=%llu)\n",
           input, (unsigned long long)(u_long)input,
           (unsigned long long)((u_long)input / (24UL*60*60)));
    printf("  LOOP_CAP      = %llu iterations\n", LOOP_CAP);
    printf("  iter cap simulates a hard watchdog (real kernel code has NONE)\n");

    u_int16_t ddp = 0, dtp = 0;
    unsigned long long iters = 0;
    timed_out = 0;

    struct sigaction sa; memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_alarm; sigaction(SIGALRM, &sa, NULL);
    /* Wall-clock safety: 10s alarm so the harness never wedges. */
    alarm(10);

    struct timeval t0, t1;
    gettimeofday(&t0, NULL);
    int rc = smb_time_unix2dos_instrumented(input, /*tzoff*/0, &ddp, &dtp, &iters);
    gettimeofday(&t1, NULL);
    double dt = (t1.tv_sec - t0.tv_sec) + (t1.tv_usec - t0.tv_usec) / 1e6;

    if (rc == 0) {
        printf("RESULT: TERMINATED  in %llu iterations, %.6fs; ddp=0x%04x dtp=0x%04x\n",
               iters, dt, ddp, dtp);
        return 0;
    } else if (rc == -1) {
        printf("RESULT: LIVELOCK    - hit LOOP_CAP after %llu iterations (%.2fs)\n",
               iters, dt);
        printf("        Kernel code (no cap) would continue past %llu iterations;\n",
               iters);
        printf("        full iteration count for this input ~= %llu => minutes of kernel CPU.\n",
               (unsigned long long)((u_long)input / (24UL*60*60) / 365));
        return 2;
    } else {
        printf("RESULT: TIMEOUT     - 10s wall alarm fired after %llu iterations\n", iters);
        return 3;
    }
}