// jfs_probe.c — root-only diagnostic module for the diWrite/lmWriteRecord
// JFS OOB primitives. Cheats by exposing kernel internals userland can't
// see, so we can answer:
//   - what cache/page does our OOB destination kva live in?
//   - what's at pfn-1 and pfn+1?
//   - where do cred / pid / msg_msg slab pages currently live?
//   - what's the pfn of *this* task's cred?
//
// Build:
//   make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
// Use:
//   sudo insmod jfs_probe.ko
//   ./jfs_probe_ctl <ioctl-args>
//
// All ioctls return -EPERM unless caller has CAP_SYS_ADMIN — keeps it
// honest about what we're doing.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/miscdevice.h>
#include <linux/xarray.h>
#include <linux/uaccess.h>
#include <linux/cred.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/mm_types.h>
#include <linux/mmzone.h>
#include <linux/page-flags.h>
#include <linux/pagemap.h>
#include <linux/string.h>
#include <linux/sched.h>
#include <linux/sched/task.h>
#include <linux/kprobes.h>

/* kallsyms-via-kprobe trick to look up unexported symbols at module init.
 * kallsyms_lookup_name itself is unexported since 5.7, but kprobes can resolve it.
 * Then we use it to find get_pfnblock_migratetype for migrate-type queries. */
typedef int (*get_pfnblock_migratetype_t)(const struct page *page, unsigned long pfn);
static get_pfnblock_migratetype_t resolved_get_pfnblock_migratetype = NULL;
typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);

static int resolve_unexported_symbols(void)
{
    struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };
    kallsyms_lookup_name_t kln;
    int r = register_kprobe(&kp);
    if (r < 0) {
        pr_warn("jfs_probe: register_kprobe(kallsyms_lookup_name) failed: %d\n", r);
        return r;
    }
    kln = (kallsyms_lookup_name_t)kp.addr;
    unregister_kprobe(&kp);
    if (!kln) {
        pr_warn("jfs_probe: kallsyms_lookup_name addr was NULL\n");
        return -ENOENT;
    }
    resolved_get_pfnblock_migratetype =
        (get_pfnblock_migratetype_t)kln("get_pfnblock_migratetype");
    if (resolved_get_pfnblock_migratetype)
        pr_info("jfs_probe: resolved get_pfnblock_migratetype @ 0x%px\n",
                resolved_get_pfnblock_migratetype);
    else
        pr_warn("jfs_probe: get_pfnblock_migratetype not found via kallsyms\n");
    return 0;
}

static const char *migrate_type_str(int mt)
{
    switch (mt) {
    case 0: return "UNMOVABLE";
    case 1: return "MOVABLE";
    case 2: return "RECLAIMABLE";
    case 3: return "HIGHATOMIC";
    case 4: return "CMA";
    case 5: return "ISOLATE";
    default: return "?";
    }
}

#define DEV_NAME "jfs_probe"

#define JFS_PROBE_IOC_MAGIC 'J'
#define JFS_PROBE_IOC_KVA_PFN     _IOWR(JFS_PROBE_IOC_MAGIC, 1, struct kva_pfn)
#define JFS_PROBE_IOC_PAGE_INFO   _IOWR(JFS_PROBE_IOC_MAGIC, 2, struct page_info)
#define JFS_PROBE_IOC_ADJACENCY   _IOWR(JFS_PROBE_IOC_MAGIC, 3, struct adjacency)
#define JFS_PROBE_IOC_MY_CRED     _IOR (JFS_PROBE_IOC_MAGIC, 4, struct my_cred)
#define JFS_PROBE_IOC_DUMP_CACHE  _IOW (JFS_PROBE_IOC_MAGIC, 5, char[64])
#define JFS_PROBE_IOC_WALK_FD_MAP _IOW (JFS_PROBE_IOC_MAGIC, 6, int)
#define JFS_PROBE_IOC_WALK_IPIMAP _IOW (JFS_PROBE_IOC_MAGIC, 7, int)

/* CVE-2026-31607 usbip layout-recon additions (2026-05-05) */
#define JFS_PROBE_IOC_VICTIMS_ALLOC _IOWR(JFS_PROBE_IOC_MAGIC, 8, struct victims_alloc)
#define JFS_PROBE_IOC_VICTIMS_CHECK _IOWR(JFS_PROBE_IOC_MAGIC, 9, struct victims_check)
#define JFS_PROBE_IOC_VICTIMS_FREE  _IO  (JFS_PROBE_IOC_MAGIC, 10)
#define JFS_PROBE_IOC_READ_BYTES    _IOWR(JFS_PROBE_IOC_MAGIC, 11, struct read_bytes)
#define JFS_PROBE_IOC_VICTIMS_PUNCH _IOWR(JFS_PROBE_IOC_MAGIC, 12, struct victims_punch)

struct kva_pfn {
    __u64 kva;
    __u64 pfn;        // OUT
    __s32 ok;         // OUT 1=ok 0=invalid
};

struct page_info {
    __u64 pfn;        // IN
    char  kind[16];   // OUT: "slab" | "buddy" | "anon" | "pagecache" | "other"
    __u64 cache_ptr;  // OUT: kmem_cache* if slab, else 0
    __u32 cache_size; // OUT: kmem_cache_size() if slab, else 0
    __u32 migrate_type;
    __u64 page_flags;
    __s32 refcount;
    __s32 mapcount;
    __s32 in_slab;
    __s32 in_lru;
    __s32 ok;
};

struct adjacency {
    __u64 pfn;        // IN
    struct page_info prev;  // OUT for pfn-1
    struct page_info self;  // OUT for pfn
    struct page_info next;  // OUT for pfn+1
};

struct my_cred {
    __u64 cred_kva;       // OUT
    __u64 cred_pfn;       // OUT
    __u64 cred_cache_ptr; // OUT: kmem_cache *
    __u32 cred_size;      // OUT
    __u32 my_uid;         // OUT
    __u64 task_kva;
    __u64 mm_kva;
};

/* ---- usbip layout recon ---- */

#define VICTIM_MAX        16384
#define VICTIM_SIZE       208     /* 208B alloc lands in kmalloc-256 cache */
#define VICTIM_MAGIC0     0x5649435449aabbccULL  /* "VICTI\xaa\xbb\xcc" */

struct victims_alloc {
    __u32 want_count;     // IN: how many victims to spray
    __u32 got_count;      // OUT
    __u32 stride_pat;     // IN: 0 = uniform 0xa5; nonzero = stride_pat repeating
    __u32 _pad;
    __u64 first_kva;      // OUT: first victim's KVA
    __u64 first_pfn;      // OUT
    __u64 cache_ptr;      // OUT: kmem_cache* the victims landed in
    __u32 cache_size;     // OUT
    __u32 _pad2;
};

struct victims_check {
    __u32 max_report;       // IN: max smashed-slot dumps to copy out
    __u32 smashed_count;    // OUT: how many victims have non-magic bytes
    __u32 reported_count;   // OUT: how many we copied to user buffer
    __u32 _pad;
    /* user must allocate report_buf of size max_report * sizeof(victim_report) */
    __u64 report_buf;       // IN: __user pointer to victim_report[]
};

struct victim_report {
    __u32 slot_index;       // which victim
    __u32 first_bad_offset; // first byte offset that doesn't match expected
    __u64 victim_kva;
    __u64 victim_pfn;
    __u8  bytes[VICTIM_SIZE]; // copy of slot's full contents at check time
};

struct read_bytes {
    __u64 kva;            // IN
    __u32 len;            // IN: bounded by 4096
    __u32 ok;             // OUT
    __u64 user_buf;       // IN: __user destination
};

struct victims_punch {
    __u32 stride;         // IN: free every Nth victim
    __u32 freed_count;    // OUT
    __u32 remaining;      // OUT
    __u32 _pad;
};

/* migrate-type lookup needs get_pfnblock_migratetype (unexported).
 * We omit it here — userland can read /proc/pagetypeinfo if needed.
 */

// `struct kmem_cache` is opaque outside mm/slab.h.  We extract size via the
// public `kmem_cache_size()` and pass the pointer back to userland so it can
// match against /proc/slabinfo if it wants the human name.
struct slab_info_pub {
    unsigned long  cache_ptr;   // kmem_cache *
    unsigned int   size;        // kmem_cache_size()
};

// Returns 1 if the page belongs to a slab and fills *out; otherwise 0.
// In 7.0 mm/slab.h, struct slab layout is:
//   memdesc_flags_t flags;        // 8 bytes
//   struct kmem_cache *slab_cache; // 8 bytes
//   ... (slab_list / freelist_counters / rcu_head etc.)
// Pages and slabs alias the same memory, so this cast works for slab pages.
struct kp_slab {
    unsigned long __flags;
    struct kmem_cache *slab_cache;
};

static int page_to_slab_info(struct page *p, struct slab_info_pub *out)
{
    if (!p) return 0;
    if (!PageSlab(p)) return 0;
    {
        struct kp_slab *sl = (struct kp_slab *)p;
        struct kmem_cache *s = (struct kmem_cache *)sl->slab_cache;
        out->cache_ptr = (unsigned long)s;
        out->size = s ? kmem_cache_size(s) : 0;
    }
    return 1;
}

static const char *page_kind_str(struct page *p)
{
    if (!p) return "(null)";
    if (PageSlab(p))     return "slab";
    if (PageBuddy(p))    return "buddy";
    if (PageAnon(p))     return "anon";
    if (folio_mapping(page_folio(p))) return "pagecache";
    return "other";
}

static void fill_page_info(unsigned long pfn, struct page_info *out)
{
    struct page *p;
    struct slab_info_pub si = {0};

    memset(out, 0, sizeof(*out));
    out->pfn = pfn;
    if (!pfn_valid(pfn)) {
        strncpy(out->kind, "invalid", sizeof(out->kind) - 1);
        out->ok = 0;
        return;
    }
    p = pfn_to_page(pfn);
    out->ok = 1;
    strncpy(out->kind, page_kind_str(p), sizeof(out->kind) - 1);
    if (page_to_slab_info(p, &si)) {
        out->in_slab = 1;
        out->cache_ptr = si.cache_ptr;
        out->cache_size = si.size;
    }
    out->migrate_type = resolved_get_pfnblock_migratetype
                        ? resolved_get_pfnblock_migratetype(p, pfn) : -1;
    out->page_flags = p->flags.f;
    out->refcount = page_ref_count(p);
    out->mapcount = atomic_read(&p->_mapcount) + 1;  /* page_mapcount removed in 7.0 */
    out->in_lru = PageLRU(p);
}

/* ---- victim spray state ---- */
static DEFINE_MUTEX(victims_lock);
static void **victims;            /* heap-allocated array of size VICTIM_MAX */
static unsigned int victims_n;

static void victim_fill(void *p, unsigned int slot, unsigned int stride_pat)
{
    u8 *b = p;
    u64 *q;
    unsigned int i;
    /* header: magic + slot index, so a smashed slot still tells us
     * which slot (until the OOB writes the first 8 bytes itself) */
    q = (u64 *)b;
    q[0] = VICTIM_MAGIC0;
    q[1] = (u64)slot;
    /* payload: stride_pat==0 -> 0xa5, else repeating low byte of stride_pat
     * but we want to detect 16-byte iso_packet_descriptor writes, so make
     * the payload distinguishable from the attacker-supplied magic 0xdeadbeef
     * 0xcafebabe 0x41414141 0x00000000 (BE on wire, becomes those values when
     * the kernel byte-swaps to host order). Use 0xa5 as default. */
    for (i = 16; i < VICTIM_SIZE; i++)
        b[i] = stride_pat ? (u8)stride_pat : 0xa5;
}

/* returns first byte offset where the slot deviates from expected; -1 if intact */
static int victim_first_bad(const void *p, unsigned int slot,
                            unsigned int stride_pat)
{
    const u8 *b = p;
    const u64 *q = (const u64 *)b;
    unsigned int i;
    u8 expect;
    if (q[0] != VICTIM_MAGIC0) return 0;
    if (q[1] != (u64)slot)     return 8;
    expect = stride_pat ? (u8)stride_pat : 0xa5;
    for (i = 16; i < VICTIM_SIZE; i++)
        if (b[i] != expect) return (int)i;
    return -1;
}

static void victims_free_all(void)
{
    unsigned int i;
    mutex_lock(&victims_lock);
    for (i = 0; i < victims_n; i++) {
        kfree(victims[i]);
        victims[i] = NULL;
    }
    victims_n = 0;
    mutex_unlock(&victims_lock);
}

static long jfs_probe_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    if (!capable(CAP_SYS_ADMIN))
        return -EPERM;

    switch (cmd) {
    case JFS_PROBE_IOC_KVA_PFN: {
        struct kva_pfn k;
        struct page *p;
        if (copy_from_user(&k, (void __user *)arg, sizeof(k)))
            return -EFAULT;
        k.ok = 0;
        if (!virt_addr_valid((void *)k.kva)) {
            k.pfn = 0;
        } else {
            p = virt_to_page((void *)k.kva);
            k.pfn = page_to_pfn(p);
            k.ok = 1;
        }
        if (copy_to_user((void __user *)arg, &k, sizeof(k)))
            return -EFAULT;
        return 0;
    }
    case JFS_PROBE_IOC_PAGE_INFO: {
        struct page_info pi;
        if (copy_from_user(&pi, (void __user *)arg, sizeof(pi)))
            return -EFAULT;
        fill_page_info(pi.pfn, &pi);
        if (copy_to_user((void __user *)arg, &pi, sizeof(pi)))
            return -EFAULT;
        return 0;
    }
    case JFS_PROBE_IOC_ADJACENCY: {
        struct adjacency a;
        if (copy_from_user(&a, (void __user *)arg, sizeof(a)))
            return -EFAULT;
        fill_page_info(a.pfn - 1, &a.prev);
        fill_page_info(a.pfn,     &a.self);
        fill_page_info(a.pfn + 1, &a.next);
        if (copy_to_user((void __user *)arg, &a, sizeof(a)))
            return -EFAULT;
        return 0;
    }
    case JFS_PROBE_IOC_MY_CRED: {
        struct my_cred mc;
        const struct cred *c;
        struct task_struct *t = current;
        struct page *p;
        struct slab_info_pub si = {0};
        memset(&mc, 0, sizeof(mc));
        c = current_cred();
        mc.cred_kva = (u64)c;
        mc.task_kva = (u64)t;
        mc.mm_kva   = (u64)t->mm;
        mc.my_uid   = from_kuid(&init_user_ns, c->uid);
        if (virt_addr_valid((void *)c)) {
            p = virt_to_page((void *)c);
            mc.cred_pfn = page_to_pfn(p);
            if (page_to_slab_info(p, &si)) {
                mc.cred_cache_ptr = si.cache_ptr;
                mc.cred_size = si.size;
            }
        }
        if (copy_to_user((void __user *)arg, &mc, sizeof(mc)))
            return -EFAULT;
        return 0;
    }
    case JFS_PROBE_IOC_DUMP_CACHE: {
        /* dump-cache scan needs max_pfn (unexported). Skip; userland can
         * iterate via /proc/slabinfo + targeted probes if needed. */
        return -ENOSYS;
    }
    case JFS_PROBE_IOC_WALK_IPIMAP: {
        /* Layout from fs/jfs/jfs_incore.h: ipimap is the 6th pointer in
         * struct jfs_sb_info (offset = 8 [sb] + 8 [mntflag] + 8*3 [ipbmap,
         * ipaimap, ipaimap2] = 40). */
        struct jfs_sbi_lite {
            struct super_block *sb;
            unsigned long mntflag;
            struct inode *ipbmap;
            struct inode *ipaimap;
            struct inode *ipaimap2;
            struct inode *ipimap;
        };
        int fd;
        struct file *f;
        struct super_block *sb;
        struct jfs_sbi_lite *jsbi;
        struct inode *ipimap;
        struct address_space *mapping;
        struct folio *folio;
        XA_STATE(xas, NULL, 0);
        int n = 0;
        if (copy_from_user(&fd, (void __user *)arg, sizeof(fd)))
            return -EFAULT;
        f = fget(fd);
        if (!f) return -EBADF;
        sb = file_inode(f)->i_sb;
        if (!sb || !sb->s_fs_info) { fput(f); return -EINVAL; }
        jsbi = sb->s_fs_info;
        ipimap = jsbi->ipimap;
        if (!ipimap || !ipimap->i_mapping) { fput(f); return -EINVAL; }
        mapping = ipimap->i_mapping;
        pr_info("jfs_probe: ipimap=0x%px mapping=0x%px nrpages=%lu\n",
                ipimap, mapping, mapping->nrpages);
        xas.xa = &mapping->i_pages;
        rcu_read_lock();
        xas_for_each(&xas, folio, ULONG_MAX) {
            unsigned long pfn;
            if (xa_is_value(folio)) continue;
            pfn = folio_pfn(folio);
            pr_info("jfs_probe:   ipimap folio idx=%lu pfn=0x%lx order=%u\n",
                    (unsigned long)folio->index, pfn, folio_order(folio));
            n++;
            if (n >= 128) break;
        }
        rcu_read_unlock();
        pr_info("jfs_probe: ipimap walk done, %d folios\n", n);
        fput(f);
        return n;
    }
    case JFS_PROBE_IOC_WALK_FD_MAP: {
        int fd;
        struct file *f;
        struct inode *inode;
        struct address_space *mapping;
        struct folio *folio;
        XA_STATE(xas, NULL, 0);
        int n = 0;
        if (copy_from_user(&fd, (void __user *)arg, sizeof(fd)))
            return -EFAULT;
        f = fget(fd);
        if (!f)
            return -EBADF;
        inode = file_inode(f);
        if (!inode || !inode->i_mapping) {
            fput(f);
            return -EINVAL;
        }
        mapping = inode->i_mapping;
        pr_info("jfs_probe: walk fd=%d inode=0x%px mapping=0x%px nrpages=%lu\n",
                fd, inode, mapping, mapping->nrpages);
        xas.xa = &mapping->i_pages;
        rcu_read_lock();
        xas_for_each(&xas, folio, ULONG_MAX) {
            unsigned long pfn;
            if (xa_is_value(folio)) continue;
            pfn = folio_pfn(folio);
            pr_info("jfs_probe:   folio idx=%lu pfn=0x%lx order=%u\n",
                    (unsigned long)folio->index, pfn, folio_order(folio));
            n++;
            if (n >= 128) break;
        }
        rcu_read_unlock();
        pr_info("jfs_probe: walk done, %d folios\n", n);
        fput(f);
        return n;
    }
    case JFS_PROBE_IOC_VICTIMS_ALLOC: {
        struct victims_alloc va;
        unsigned int n_done = 0;
        struct page *p;
        struct slab_info_pub si = {0};

        if (copy_from_user(&va, (void __user *)arg, sizeof(va)))
            return -EFAULT;
        if (va.want_count == 0 || va.want_count > VICTIM_MAX)
            return -EINVAL;

        mutex_lock(&victims_lock);
        /* free any prior spray */
        for (; victims_n; ) {
            kfree(victims[--victims_n]);
            victims[victims_n] = NULL;
        }
        for (n_done = 0; n_done < va.want_count; n_done++) {
            void *v = kmalloc(VICTIM_SIZE, GFP_KERNEL);
            if (!v) break;
            victim_fill(v, n_done, va.stride_pat);
            victims[n_done] = v;
        }
        victims_n = n_done;

        va.got_count = n_done;
        va.first_kva = victims_n ? (u64)victims[0] : 0;
        if (victims_n && virt_addr_valid(victims[0])) {
            p = virt_to_page(victims[0]);
            va.first_pfn = page_to_pfn(p);
            if (page_to_slab_info(p, &si)) {
                va.cache_ptr  = si.cache_ptr;
                va.cache_size = si.size;
            }
        }
        mutex_unlock(&victims_lock);

        if (copy_to_user((void __user *)arg, &va, sizeof(va)))
            return -EFAULT;
        pr_info("jfs_probe: VICTIMS_ALLOC count=%u first=0x%px cache=0x%llx size=%u\n",
                n_done, victims_n ? victims[0] : NULL,
                (unsigned long long)va.cache_ptr, va.cache_size);
        return 0;
    }
    case JFS_PROBE_IOC_VICTIMS_CHECK: {
        struct victims_check vc;
        struct victim_report __user *uout;
        unsigned int i, smashed = 0, reported = 0;

        if (copy_from_user(&vc, (void __user *)arg, sizeof(vc)))
            return -EFAULT;
        uout = (struct victim_report __user *)vc.report_buf;

        mutex_lock(&victims_lock);
        for (i = 0; i < victims_n; i++) {
            int bad;
            if (!victims[i]) continue;  /* punched */
            bad = victim_first_bad(victims[i], i, 0);
            if (bad < 0) continue;
            smashed++;
            if (reported < vc.max_report && uout) {
                struct victim_report vr;
                vr.slot_index = i;
                vr.first_bad_offset = (u32)bad;
                vr.victim_kva = (u64)victims[i];
                vr.victim_pfn = virt_addr_valid(victims[i])
                                ? page_to_pfn(virt_to_page(victims[i])) : 0;
                memcpy(vr.bytes, victims[i], VICTIM_SIZE);
                if (copy_to_user(&uout[reported], &vr, sizeof(vr))) {
                    mutex_unlock(&victims_lock);
                    return -EFAULT;
                }
                reported++;
            }
        }
        mutex_unlock(&victims_lock);

        vc.smashed_count = smashed;
        vc.reported_count = reported;
        if (copy_to_user((void __user *)arg, &vc, sizeof(vc)))
            return -EFAULT;
        pr_info("jfs_probe: VICTIMS_CHECK smashed=%u reported=%u\n",
                smashed, reported);
        return 0;
    }
    case JFS_PROBE_IOC_VICTIMS_FREE: {
        victims_free_all();
        pr_info("jfs_probe: VICTIMS_FREE\n");
        return 0;
    }
    case JFS_PROBE_IOC_VICTIMS_PUNCH: {
        struct victims_punch vp;
        unsigned int i, freed = 0, remaining = 0;
        if (copy_from_user(&vp, (void __user *)arg, sizeof(vp)))
            return -EFAULT;
        if (vp.stride < 2) return -EINVAL;
        mutex_lock(&victims_lock);
        for (i = 0; i < victims_n; i++) {
            if (!victims[i]) continue;
            if (i % vp.stride == 0) {
                kfree(victims[i]);
                victims[i] = NULL;
                freed++;
            } else {
                remaining++;
            }
        }
        mutex_unlock(&victims_lock);
        vp.freed_count = freed;
        vp.remaining = remaining;
        if (copy_to_user((void __user *)arg, &vp, sizeof(vp)))
            return -EFAULT;
        pr_info("jfs_probe: VICTIMS_PUNCH stride=%u freed=%u remaining=%u\n",
                vp.stride, freed, remaining);
        return 0;
    }
    case JFS_PROBE_IOC_READ_BYTES: {
        struct read_bytes rb;
        u8 buf[4096];
        if (copy_from_user(&rb, (void __user *)arg, sizeof(rb)))
            return -EFAULT;
        if (rb.len == 0 || rb.len > sizeof(buf))
            return -EINVAL;
        rb.ok = 0;
        if (!virt_addr_valid((void *)rb.kva))
            goto rb_out;
        memcpy(buf, (void *)rb.kva, rb.len);
        if (copy_to_user((void __user *)rb.user_buf, buf, rb.len))
            return -EFAULT;
        rb.ok = 1;
    rb_out:
        if (copy_to_user((void __user *)arg, &rb, sizeof(rb)))
            return -EFAULT;
        return 0;
    }
    default:
        return -ENOTTY;
    }
}

static const struct file_operations jfs_probe_fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = jfs_probe_ioctl,
    .compat_ioctl   = jfs_probe_ioctl,
};

static struct miscdevice jfs_probe_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name  = DEV_NAME,
    .fops  = &jfs_probe_fops,
    .mode  = 0666,  /* root-only check is in ioctl; allow anyone to open */
};

static int __init jfs_probe_init(void)
{
    int r;
    victims = kvcalloc(VICTIM_MAX, sizeof(*victims), GFP_KERNEL);
    if (!victims) return -ENOMEM;
    r = misc_register(&jfs_probe_misc);
    if (r) {
        kvfree(victims);
        pr_err("jfs_probe: misc_register failed: %d\n", r);
        return r;
    }
    resolve_unexported_symbols();
    pr_info("jfs_probe: loaded, /dev/%s ready\n", DEV_NAME);
    return 0;
}

static void __exit jfs_probe_exit(void)
{
    victims_free_all();
    misc_deregister(&jfs_probe_misc);
    kvfree(victims);
    pr_info("jfs_probe: unloaded\n");
}

module_init(jfs_probe_init);
module_exit(jfs_probe_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("research");
MODULE_DESCRIPTION("JFS exploit dev probe — slab/page adjacency info");
