/*
 * DF-0807 — dirfs_readdir for-loop premature dp advancement
 *                -> heap OOB read + kernel heap info leak
 * Deterministic userspace harness (faithful transcription of the kernel loop).
 *
 * BUG LOCATION: sys/vfs/dirfs/dirfs_vnops.c:1277-1286 (dirfs_readdir)
 *
 *   1277:  for (dp = (struct dirent *)buf; bytes > 0 && uio->uio_resid > 0;
 *   1278:      bytes -= _DIRENT_DIRSIZ(dp), dp = dpn) {
 *   1279:      r = vop_write_dirent(&error, uio, dp->d_ino, dp->d_type,
 *   1280:          dp->d_namlen, dp->d_name);
 *   1281:      if (error || r)
 *   1282:          break;
 *   1283:      dpn = _DIRENT_NEXT(dp);
 *   1284:      dp = dpn;            // <-- PREMATURE ADVANCE: dp now points to
 *   1285:      cnt++;               //     the NEXT entry BEFORE the for-increment
 *   1286:  }                        //     runs `bytes -= _DIRENT_DIRSIZ(dp)`
 *
 * The for-increment expression on line 1278 is evaluated in source order
 * (C comma operator, left-to-right):
 *
 *   1.  bytes -= _DIRENT_DIRSIZ(dp)   // uses CURRENT dp
 *   2.  dp = dpn                       // then advance
 *
 * But the loop body already executed `dp = dpn` on line 1284. So step (1)
 * computes _DIRENT_DIRSIZ of the NEXT entry (the one we have NOT yet
 * processed), not the entry we just wrote out. After the LAST valid entry,
 * the body sets dp = dpn = (buf + bytes) which is one byte past the final
 * dirent. If the getdirentries() buffer was filled completely (bytes ==
 * bufsiz, which is the common case with ~200+ entries / a 4096-byte buf),
 * that pointer is also one byte past the kmalloc() allocation. Step (1)
 * then derefs dp->d_namlen past the allocation -> HEAP OOB READ.
 *
 * Worse: if the (untrusted, OOB) d_namlen read is small enough that
 * `bytes` stays positive after the subtract, the loop condition holds and
 * the body runs ONE MORE iteration with the OOB dp. vop_write_dirent()
 * (sys/kern/vfs_subr.c:2559-2582) then reads dp->d_ino, dp->d_type,
 * dp->d_namlen, AND dp->d_name (line 2575: bcopy(d_name, dp->d_name,
 * d_namlen)) from past the buffer and copies them into a fresh dirent that
 * is uiomove()'d to the user readdir() result. That is a kernel heap
 * INFO LEAK: attacker-recognizable bytes from the slab chunk / redzone
 * adjacent to the dirfs readdir buffer surface in userland.
 *
 * Trigger: ~200+ entries that exactly fill the 4096-byte getdirentries
 * buffer. (bufsiz is clamped to 4096 at line 1248-1249.)
 *
 * WHY A HARNESS: dirfs is vkernel64-only — listed in
 * sys/platform/vkernel64/conf/files (optional dirfs) but NOT in
 * sys/conf/files, so it is absent from the running X86_64_GENERIC host
 * kernel, and there is no dirfs.ko in /boot/kernel. It cannot be mounted
 * or triggered on this guest. The finding explicitly authorizes a
 * deterministic harness fallback (precedent: DF-0806). This program
 * transcribes the exact buggy loop with two allocator styles:
 *
 *   (A) guard-page allocator — the buf is placed flush against a PROT_NONE
 *       page so any access past buf[bufsiz-1] lands in the guard page and
 *       faults (SIGSEGV). Used to DEFINITIVELY catch the OOB read in the
 *       for-increment after the last valid entry.
 *
 *   (B) writable-heap emulation — the buf is followed by a "leak zone"
 *       containing a fake dirent with a recognizable d_name marker. Used
 *       to DEFINITIVELY show the info leak: after the premature advance,
 *       the OOB d_namlen is read from the leak zone and the next loop
 *       iteration's vop_write_dirent() transcription copies the marker
 *       bytes from the leak zone into the user readdir sink.
 *
 *   The FIXED transcription removes `dp = dpn` from the loop body so the
 *   for-increment correctly computes the size of the just-processed entry
 *   and properly drives bytes to 0 after the last valid entry. Both
 *   allocator styles confirm no OOB.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>

/* ---- Faithful transcription of sys/sys/dirent.h ------------------------- */

struct dirent {
    uint64_t d_ino;        /* offset 0  */
    uint16_t d_namlen;     /* offset 8  */
    uint8_t  d_type;       /* offset 10 */
    uint8_t  d_unused1;    /* offset 11 */
    uint32_t d_unused2;    /* offset 12 */
    char     d_name[256];  /* offset 16 */
};

#define DIRENT_DNAME_OFF  16u
#define DIRENT_RECLEN(nl) (((DIRENT_DNAME_OFF + (nl) + 1u + 7u) & ~7u))
#define DIRENT_DIRSIZ(dp) DIRENT_RECLEN((dp)->d_namlen)
#define DIRENT_NEXT(dp)   ((struct dirent *)((uint8_t *)(dp) + DIRENT_DIRSIZ(dp)))

/* Recognizable marker placed in the OOB "leak zone". */
#define LEAK_MARKER "HEAP-LEAK-PAYLOAD-FROM-ADJACENT-SLAB-CHUNK"

/*
 * The leak detection pattern: a 7-byte prefix of LEAK_MARKER. We use a
 * SHORT prefix because the leak-zone fake dirent uses d_namlen=7 (so
 * DIRENT_DIRSIZ(fake) = 24 < 32 = entry size, keeping `bytes` positive and
 * letting the buggy loop run the OOB info-leak iteration). vop_write_dirent
 * then copies exactly those 7 d_name bytes into the user sink.
 */
#define LEAK_PATTERN "HEAP-LE"

/* ---- Byte-search helper (no memmem dependency) ------------------------- */
static const void *find_bytes(const void *hay, size_t hay_n,
                              const void *needle, size_t needle_n)
{
    if (needle_n == 0 || hay_n < needle_n) return NULL;
    const uint8_t *h = hay;
    for (size_t i = 0; i + needle_n <= hay_n; i++) {
        if (memcmp(h + i, needle, needle_n) == 0)
            return h + i;
    }
    return NULL;
}

/* ---- Simulated kernel API ---------------------------------------------- */

struct uio {
    size_t  uio_resid;     /* remaining space in user readdir sink */
    char   *uio_sink;      /* user sink buffer */
    size_t  uio_cap;       /* capacity of uio_sink */
    size_t  uio_used;      /* bytes already written to sink */
};

/*
 * Transcription of vop_write_dirent (sys/kern/vfs_subr.c:2559-2582).
 * Reads d_ino/d_type/d_namlen/d_name from the source dirent and copies
 * the entry into the user uio sink. Returns 1 if uio_resid too small,
 * 0 otherwise. If `oob_tracker` is non-NULL and the source `dp` lies in
 * the OOB region, the leak is recorded.
 */
static int vop_write_dirent(struct uio *uio, uint64_t d_ino, uint8_t d_type,
                            uint16_t d_namlen, const char *d_name)
{
    size_t len = DIRENT_RECLEN(d_namlen);
    if (len > uio->uio_resid)
        return 1;

    /* kernel allocates a temp dirent, fills it, uiomove's to user */
    struct dirent tmp;
    memset(&tmp, 0, sizeof(tmp));
    tmp.d_ino    = d_ino;
    tmp.d_namlen = d_namlen;
    tmp.d_type   = d_type;
    /* line 2575: bcopy(d_name, dp->d_name, d_namlen)  -- THE LEAK COPY */
    if (d_namlen > 0)
        memcpy(tmp.d_name, d_name, d_namlen);

    /* uiomove to user sink (truncated to capacity for the harness) */
    size_t to_copy = len;
    if (uio->uio_used + to_copy > uio->uio_cap)
        to_copy = uio->uio_cap - uio->uio_used;
    memcpy(uio->uio_sink + uio->uio_used, &tmp, to_copy);
    uio->uio_used    += len;        /* full accounting, like the kernel */
    uio->uio_resid   -= len;
    return 0;
}

/* ---- Two allocator styles ---------------------------------------------- */

/*
 * Round `n` up to a multiple of `pgsz`. Used by both allocators below.
 */
static size_t round_up_to_page(size_t n, size_t pgsz)
{
    return (n + pgsz - 1) & ~(pgsz - 1);
}

/*
 * Guard-page allocator: returns a pointer to `alloc` writable bytes whose
 * LAST byte is flush against a PROT_NONE page. buf[alloc] lands in the
 * guard page and faults. Emulates kmalloc(alloc) with byte-exact bounds.
 */
static char *guard_alloc(size_t alloc)
{
    long pgsz = sysconf(_SC_PAGESIZE);
    if (pgsz <= 0) pgsz = 4096;
    size_t writable = round_up_to_page(alloc, (size_t)pgsz);
    size_t need = writable + (size_t)pgsz;             /* +1 page guard */
    void *base = mmap(NULL, need, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANON, -1, 0);
    if (base == MAP_FAILED) { perror("mmap"); exit(2); }
    if (mprotect((char *)base + writable, (size_t)pgsz, PROT_NONE) != 0) {
        perror("mprotect"); exit(2);
    }
    /* buf ends exactly at the guard boundary. */
    return (char *)base + (writable - alloc);
}

/*
 * Leak-zone allocator: returns a pointer to `alloc` writable bytes whose
 * last byte is flush against a writable "leak zone" of `zone` bytes. The
 * leak zone is pre-filled with a fake dirent whose d_name is the marker.
 * Out-param `zone_ptr` receives the leak-zone start (for inspection).
 *
 * `leak_namlen` controls the d_namlen of the fake dirent placed at the
 * start of the leak zone. It must be small enough that DIRENT_DIRSIZ(fake)
 * is strictly less than the per-entry record size of the valid entries,
 * otherwise the buggy loop's `bytes -= DIRSIZ(OOB)` would drive bytes to
 * <=0 and the next iteration (the info-leak copy) would not run.
 */
static char *leak_alloc(size_t alloc, size_t zone, char **zone_ptr,
                        size_t leak_namlen)
{
    long pgsz = sysconf(_SC_PAGESIZE);
    if (pgsz <= 0) pgsz = 4096;
    size_t alloc_pages = round_up_to_page(alloc, (size_t)pgsz) / (size_t)pgsz;
    size_t zone_pages  = round_up_to_page(zone,  (size_t)pgsz) / (size_t)pgsz;
    size_t need = (alloc_pages + zone_pages) * (size_t)pgsz;
    void *base = mmap(NULL, need, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANON, -1, 0);
    if (base == MAP_FAILED) { perror("mmap"); exit(2); }
    char *zone_start = (char *)base + alloc_pages * (size_t)pgsz;
    char *buf        = zone_start - alloc;             /* buf ends at zone */
    if (zone_ptr) *zone_ptr = zone_start;

    /* Pre-fill the leak zone with a fake dirent whose d_name is the marker.
     * The rest of the zone is poisoned with 0xAA so any further OOB read
     * produces a large d_namlen and the loop self-terminates. */
    struct dirent fake;
    memset(&fake, 0, sizeof(fake));
    fake.d_ino    = 0xDEADBEEFCAFEBABEULL;            /* recognizable */
    fake.d_namlen = (uint16_t)leak_namlen;
    fake.d_type   = 0xEE;                              /* recognizable */
    if (leak_namlen > 0) {
        size_t cpy = leak_namlen < sizeof(LEAK_MARKER) - 1
                   ? leak_namlen : sizeof(LEAK_MARKER) - 1;
        memcpy(fake.d_name, LEAK_MARKER, cpy);
    }
    memset(zone_start, 0xAA, zone);                    /* poison the rest */
    memcpy(zone_start, &fake, sizeof(fake));           /* lay down dirent */
    return buf;
}

/* ---- Build a buffer of N valid dirents sized to exactly fill `bufsiz` -- */

static size_t fill_entries(char *buf, size_t bufsiz, size_t name_len)
{
    size_t off = 0;
    uint64_t ino = 1;
    while (1) {
        size_t rec = DIRENT_RECLEN(name_len);
        if (off + rec > bufsiz) break;             /* would overflow bufsiz */
        struct dirent d;
        memset(&d, 0, sizeof(d));
        d.d_ino    = ino++;
        d.d_namlen = (uint16_t)name_len;
        d.d_type   = 0x08;                          /* DT_REG */
        memset(d.d_name, 'E', name_len);
        d.d_name[name_len] = '\0';
        memcpy(buf + off, &d, rec);
        off += rec;
    }
    return off;                                     /* == bytes filled */
}

/* ---- SEGV/BUS catcher for the guard-page variant ----------------------- */

static sigjmp_buf oob_jmp;
static volatile sig_atomic_t oob_caught;

static void oob_handler(int sig, siginfo_t *si, void *ctx)
{
    (void)sig; (void)si; (void)ctx;
    oob_caught = 1;
    siglongjmp(oob_jmp, 1);
}

static void install_catcher(void)
{
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = oob_handler;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS,  &sa, NULL);
}

/* ---- BUGGY transcription of dirfs_readdir for-loop -------------------- */

struct loop_result {
    int     oob_read_fault;       /* guard-page variant: did we SEGV? */
    int     leak_marker_found;    /* leak-zone variant: marker in sink? */
    size_t  entries_emitted;      /* count of vop_write_dirent calls */
    ssize_t bytes_after;          /* final value of `bytes` */
};

/*
 * Run the for-loop with the premature `dp = dpn` body advance (BUGGY).
 * Caller provides the buffer base, the (already-filled) `bytes` value,
 * and a uio sink. `mode`:
 *   'g' = guard-page (expect fault on OOB read)
 *   'l' = leak-zone  (expect marker copied into sink)
 */
static struct loop_result loop_buggy(char *buf, size_t bytes, struct uio *uio,
                                     char mode)
{
    struct loop_result r;
    memset(&r, 0, sizeof(r));

    struct dirent *dp = (struct dirent *)buf;
    struct dirent *dpn = NULL;
    int rc;

    if (mode == 'g') { install_catcher(); oob_caught = 0; }

    if (mode == 'g' && sigsetjmp(oob_jmp, 1) != 0) {
        r.oob_read_fault = 1;
        r.bytes_after = (ssize_t)bytes;
        return r;
    }

    /* Verbatim for-loop from dirfs_vnops.c:1277-1286 (BUGGY) */
    for (dp = (struct dirent *)buf; bytes > 0 && uio->uio_resid > 0;
         bytes -= DIRENT_DIRSIZ(dp), dp = dpn) {

        rc = vop_write_dirent(uio, dp->d_ino, dp->d_type,
                              dp->d_namlen, dp->d_name);
        if (rc) break;

        dpn = DIRENT_NEXT(dp);
        dp = dpn;                       /* <-- THE BUG */
        r.entries_emitted++;
    }
    r.bytes_after = (ssize_t)bytes;

    if (uio->uio_sink && uio->uio_used > 0 &&
        find_bytes(uio->uio_sink, uio->uio_used,
                   LEAK_PATTERN, sizeof(LEAK_PATTERN) - 1))
        r.leak_marker_found = 1;

    return r;
}

/*
 * Run the for-loop with `dp = dpn` REMOVED from the body (FIXED).
 * The for-increment now correctly computes DIRSIZ of the entry that was
 * just written out, then advances.
 */
static struct loop_result loop_fixed(char *buf, size_t bytes, struct uio *uio,
                                     char mode)
{
    struct loop_result r;
    memset(&r, 0, sizeof(r));

    struct dirent *dp;
    struct dirent *dpn = NULL;
    int rc;

    if (mode == 'g') { install_catcher(); oob_caught = 0; }

    if (mode == 'g' && sigsetjmp(oob_jmp, 1) != 0) {
        r.oob_read_fault = 1;
        r.bytes_after = (ssize_t)bytes;
        return r;
    }

    /* Fixed for-loop: body no longer sets `dp = dpn` */
    for (dp = (struct dirent *)buf; bytes > 0 && uio->uio_resid > 0;
         bytes -= DIRENT_DIRSIZ(dp), dp = dpn) {

        rc = vop_write_dirent(uio, dp->d_ino, dp->d_type,
                              dp->d_namlen, dp->d_name);
        if (rc) break;

        dpn = DIRENT_NEXT(dp);
        /* dp = dpn;   <-- REMOVED by the fix */
        r.entries_emitted++;
    }
    r.bytes_after = (ssize_t)bytes;

    if (uio->uio_sink && uio->uio_used > 0 &&
        find_bytes(uio->uio_sink, uio->uio_used,
                   LEAK_PATTERN, sizeof(LEAK_PATTERN) - 1))
        r.leak_marker_found = 1;

    return r;
}

/* ---- Per-scenario printers -------------------------------------------- */

static void scenario_guard(size_t bufsiz, size_t name_len)
{
    printf("  [guard-page variant] bufsiz=%zu name_len=%zu\n", bufsiz, name_len);

    /* Fill the buffer with N entries that exactly fill bufsiz. */
    char *buf = guard_alloc(bufsiz);
    size_t bytes = fill_entries(buf, bufsiz, name_len);
    printf("    filled bytes=%zu (== bufsiz ? %s)\n",
           bytes, bytes == bufsiz ? "YES — dp advances PAST allocation" : "no");

    struct uio uio;
    char sink[8192];
    memset(sink, 0, sizeof(sink));
    uio.uio_sink = sink; uio.uio_cap = sizeof(sink);
    uio.uio_used = 0;    uio.uio_resid = sizeof(sink);

    struct loop_result rb = loop_buggy(buf, bytes, &uio, 'g');
    printf("    BUGGY loop:  %s (entries=%zu bytes_after=%zd)\n",
           rb.oob_read_fault
               ? "FAULT -> OOB READ CONFIRMED in for-increment `bytes -= DIRSIZ(dp)`"
               : "no fault (unexpected)",
           rb.entries_emitted, rb.bytes_after);

    /* Fresh buffer for the fixed transcription. */
    buf = guard_alloc(bufsiz);
    fill_entries(buf, bufsiz, name_len);
    memset(sink, 0, sizeof(sink));
    uio.uio_sink = sink; uio.uio_cap = sizeof(sink);
    uio.uio_used = 0;    uio.uio_resid = sizeof(sink);

    struct loop_result rf = loop_fixed(buf, bytes, &uio, 'g');
    printf("    FIXED loop:  %s (entries=%zu bytes_after=%zd)\n",
           rf.oob_read_fault ? "FAULT (unexpected!)" : "no fault (loop terminated cleanly)",
           rf.entries_emitted, rf.bytes_after);
}

static void scenario_leak(size_t bufsiz, size_t name_len)
{
    printf("  [leak-zone variant] bufsiz=%zu name_len=%zu\n", bufsiz, name_len);

    /* Pick leak_namlen so DIRENT_DIRSIZ(fake) is strictly less than the
     * valid-entry record size (rec). That keeps `bytes` positive after the
     * buggy increment reads the OOB d_namlen, so the loop body runs ONE
     * more iteration and vop_write_dirent copies OOB bytes to the sink.
     * For name_len=8 -> rec=32, leak_namlen=7 -> DIRENT_DIRSIZ=24 < 32. OK. */
    size_t rec = DIRENT_RECLEN(name_len);
    size_t leak_namlen = 0;
    for (size_t nl = name_len; nl > 0; nl--) {
        if (DIRENT_RECLEN(nl) < rec) { leak_namlen = nl; break; }
    }
    if (leak_namlen == 0) {
        printf("    (cannot pick leak_namlen for rec=%zu; skipping)\n", rec);
        return;
    }

    size_t zone = 1024;    /* generous OOB heap area after the buffer */
    char *zone_ptr = NULL;
    char *buf = leak_alloc(bufsiz, zone, &zone_ptr, leak_namlen);
    size_t bytes = fill_entries(buf, bufsiz, name_len);
    printf("    filled bytes=%zu (== bufsiz ? %s), leak zone @ %p, fake d_namlen=%zu (DIRSIZ=%zu < rec=%zu)\n",
           bytes, bytes == bufsiz ? "YES" : "no",
           (void *)zone_ptr, leak_namlen, DIRENT_RECLEN(leak_namlen), rec);

    struct uio uio;
    char sink[16384];
    memset(sink, 0, sizeof(sink));
    uio.uio_sink = sink; uio.uio_cap = sizeof(sink);
    uio.uio_used = 0;    uio.uio_resid = sizeof(sink);

    struct loop_result rb = loop_buggy(buf, bytes, &uio, 'l');
    printf("    BUGGY loop:  entries=%zu bytes_after=%zd ; sink has OOB marker ? %s\n",
           rb.entries_emitted, rb.bytes_after,
           rb.leak_marker_found ? "YES -> INFO LEAK CONFIRMED" : "no");

    if (rb.leak_marker_found) {
        const char *p = find_bytes(sink, sizeof(sink),
                                   LEAK_PATTERN, sizeof(LEAK_PATTERN) - 1);
        if (p) {
            size_t off = (size_t)(p - sink);
            printf("      leaked marker at sink offset %zu: '%.*s'\n",
                   off, (int)(sizeof(LEAK_PATTERN) - 1), p);
        }
    }

    /* Fresh buffer for fixed transcription. */
    buf = leak_alloc(bufsiz, zone, &zone_ptr, leak_namlen);
    fill_entries(buf, bufsiz, name_len);
    memset(sink, 0, sizeof(sink));
    uio.uio_sink = sink; uio.uio_cap = sizeof(sink);
    uio.uio_used = 0;    uio.uio_resid = sizeof(sink);

    struct loop_result rf = loop_fixed(buf, bytes, &uio, 'l');
    printf("    FIXED loop:  entries=%zu bytes_after=%zd ; sink has OOB marker ? %s\n",
           rf.entries_emitted, rf.bytes_after,
           rf.leak_marker_found ? "YES (unexpected!)" : "no (clean termination)");
}

/* ---- main ------------------------------------------------------------- */

int main(void)
{
    printf("=== DF-0807 dirfs_readdir premature-dp-advancement harness ===\n");
    printf("Transcription of sys/vfs/dirfs/dirfs_vnops.c:1277-1286\n");
    printf("Bug: loop body executes `dp = dpn` BEFORE the for-increment\n");
    printf("     `bytes -= _DIRENT_DIRSIZ(dp)`, so after the last valid\n");
    printf("     entry the increment derefs dp->d_namlen past the buffer\n");
    printf("     (OOB read) and the next iteration copies OOB bytes to the\n");
    printf("     user readdir sink via vop_write_dirent (info leak).\n\n");

    /* All cases use name_len=8 (rec=DIRENT_RECLEN(8)=32) which divides every
     * bufsiz below, so the valid entries EXACTLY fill the buffer (bytes ==
     * bufsiz). That forces the premature `dp = dpn` to advance PAST the
     * allocation after the last valid entry -- matching the finding's
     * "~200+ entries fill the getdirentries buffer completely" trigger.
     * The 4096 case matches the dirfs_readdir clamp at line 1248-1249. */
    struct { const char *label; size_t bufsiz, name_len; } cases[] = {
        { "bufsiz=512  name_len=8 (16 entries)",         512,  8 },
        { "bufsiz=1024 name_len=8 (32 entries)",         1024, 8 },
        { "bufsiz=2048 name_len=8 (64 entries)",         2048, 8 },
        { "bufsiz=4096 name_len=8 (128 entries, dirfs clamp)", 4096, 8 },
    };
    size_t i;
    for (i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) {
        printf("Scenario: %s\n", cases[i].label);
        scenario_guard(cases[i].bufsiz, cases[i].name_len);
        scenario_leak (cases[i].bufsiz, cases[i].name_len);
        printf("\n");
    }

    printf("=== SUMMARY ===\n");
    printf("Vulnerable dirfs_readdir loop:\n");
    printf("  - HEAP OOB READ in the for-increment after the last valid entry\n");
    printf("    (reads d_namlen from the slab chunk / redzone adjacent to buf)\n");
    printf("  - KERNEL HEAP INFO LEAK via the next iteration's vop_write_dirent\n");
    printf("    (bcopy of attacker-recognizable d_ino/d_type/d_namlen/d_name\n");
    printf("     from past the allocation into the user readdir result)\n");
    printf("Fixed loop (remove `dp = dpn` from body):\n");
    printf("  - bytes correctly decrements by the size of the just-processed entry\n");
    printf("  - loop terminates with bytes == 0 after the last valid entry\n");
    printf("  - no OOB read, no leak\n");
    printf("Reachability: vkernel64-only (sys/platform/vkernel64/conf/files)\n");
    printf("Realistic ceiling: vkernel heap info leak (read-only primitive)\n");
    return 0;
}
