#include <linux/bitmap.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kallsyms.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/mm_types.h>
#include <linux/mmu_notifier.h>
#include <linux/module.h>
#include <linux/pagemap.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/swap.h>
#include <linux/swapops.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
#include <linux/sched/signal.h>
#endif
#include <asm/mman.h>
#include <asm/tlb.h>

#include "graphene-ipc.h"
#include "ksyms.h"

MODULE_LICENSE("Dual BSD/GPL");

#define FILE_POISON LIST_POISON1

struct kmem_cache* gipc_queue_cachep;
struct kmem_cache* gipc_send_buffer_cachep;

#define GIPC_DEBUG 0

#if defined(GIPC_DEBUG) && GIPC_DEBUG == 1
#define DEBUG(...)        printk(KERN_INFO __VA_ARGS__)
#define GIPC_BUG_ON(cond) BUG_ON(cond)
#else
#define DEBUG(...)
#define GIPC_BUG_ON(cond)
#endif

#if defined(CONFIG_GRAPHENE_BULK_IPC) || LINUX_VERSION_CODE < KERNEL_VERSION(3, 4, 0)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 9, 0)
#define DO_MMAP_PGOFF(file, addr, len, prot, flags, pgoff)                                \
    ({                                                                                    \
        unsigned long populate;                                                           \
        unsigned long rv = do_mmap_pgoff(file, addr, len, prot, flags, pgoff, &populate); \
        rv;                                                                               \
    })
#else
#define DO_MMAP_PGOFF(file, addr, len, prot, flags, pgoff) \
    do_mmap_pgoff(file, addr, len, prot, flags, pgoff)
#endif /* kernel_version < 3.9.0 */
#else
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
#define MY_DO_MMAP
#define DO_MMAP_PGOFF(file, addr, len, prot, flags, pgoff)                           \
    ({                                                                               \
        unsigned long populate;                                                      \
        unsigned long rv;                                                            \
        rv = KSYM(do_mmap)(file, addr, len, prot, flags, 0, pgoff, &populate, NULL); \
        rv;                                                                          \
    })

#elif LINUX_VERSION_CODE >= KERNEL_VERSION(4, 3, 0)
#define MY_DO_MMAP
#define DO_MMAP_PGOFF(file, addr, len, prot, flags, pgoff)                     \
    ({                                                                         \
        unsigned long populate;                                                \
        unsigned long rv;                                                      \
        rv = KSYM(do_mmap)(file, addr, len, prot, flags, 0, pgoff, &populate); \
        rv;                                                                    \
    })
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(3, 9, 0)
#define MY_DO_MMAP_PGOFF
#define DO_MMAP_PGOFF(file, addr, len, prot, flags, pgoff)                        \
    ({                                                                            \
        unsigned long populate;                                                   \
        unsigned long rv;                                                         \
        rv = KSYM(do_mmap_pgoff)(file, addr, len, prot, flags, pgoff, &populate); \
        rv;                                                                       \
    })
#else
#define MY_DO_MMAP_PGOFF
#define DO_MMAP_PGOFF(file, addr, len, prot, flags, pgoff) \
    KSYM(do_mmap_pgoff)(file, addr, len, prot, flags, pgoff)
#endif /* kernel version < 3.9 */
#endif /* !CONFIG_GRAPHENE_BULK_IPC && kernel version > 3.4.0 */

#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 7, 0)
#ifdef CONFIG_GRAPHENE_BULK_IPC
#define FLUSH_TLB_MM_RANGE flush_tlb_mm_range
#else
#define MY_FLUSH_TLB_MM_RANGE
#define FLUSH_TLB_MM_RANGE KSYM(flush_tlb_mm_range)
#endif
#else /* LINUX_VERSION_CODE < 3.7.0 */
#if defined(CONFIG_GRAPHENE_BULK_IPC) || LINUX_VERSION_CODE < KERNEL_VERSION(3, 2, 0)
#define FLUSH_TLB_PAGE flush_tlb_page
#else
#define MY_FLUSH_TLB_PAGE
#define FLUSH_TLB_PAGE KSYM(flush_tlb_page)
#endif
#endif

#ifdef MY_DO_MMAP
IMPORT_KSYM(do_mmap);
#endif
#ifdef MY_DO_MMAP_PGOFF
IMPORT_KSYM(do_mmap_pgoff);
#endif
#ifdef MY_FLUSH_TLB_MM_RANGE
IMPORT_KSYM(flush_tlb_mm_range);
#endif
#ifdef MY_FLUSH_TLB_PAGE
IMPORT_KSYM(flush_tlb_page);
#endif

#ifndef gipc_get_session
u64 (*my_gipc_get_session)(struct task_struct*) = NULL;
#endif

struct gipc_queue {
    struct list_head list;
    s64 token;
    u64 owner;
    atomic_t count;

    struct mutex send_lock, recv_lock;
    wait_queue_head_t send, recv;
    volatile int next, last;

    struct {
        struct page* page;
        struct file* file;
        u64 pgoff;
    } pages[PAGE_QUEUE];
};

struct gipc_send_buffer {
    unsigned long page_bit_map[PAGE_BITS];
    struct page* pages[PAGE_QUEUE];
    struct vm_area_struct* vmas[PAGE_QUEUE];
    struct file* files[PAGE_QUEUE];
    unsigned long pgoffs[PAGE_QUEUE];
};

struct {
    spinlock_t lock;
    /*
     * For now, just make them monotonically increasing.  XXX: At
     * some point, do something smarter for security.
     */
    u64 max_token;
    struct list_head channels;  // gipc_queue structs
} gdev;

#ifdef gipc_get_session
#define GIPC_OWNER gipc_get_session(current)
#else
#define GIPC_OWNER (my_gipc_get_session ? my_gipc_get_session(current) : 0)
#endif

static inline struct gipc_queue* create_gipc_queue(struct file* creator) {
    struct gipc_queue* gq = kmem_cache_alloc(gipc_queue_cachep, GFP_KERNEL);

    if (!gq)
        return gq;

    memset(gq, 0, sizeof(*gq));
    INIT_LIST_HEAD(&gq->list);
    mutex_init(&gq->send_lock);
    mutex_init(&gq->recv_lock);
    init_waitqueue_head(&gq->send);
    init_waitqueue_head(&gq->recv);
    gq->owner             = GIPC_OWNER;
    creator->private_data = gq;
    atomic_set(&gq->count, 1);

    spin_lock(&gdev.lock);
    list_add(&gq->list, &gdev.channels);
    gq->token = gdev.max_token++;
    spin_unlock(&gdev.lock);

    return gq;
}

static inline void release_gipc_queue(struct gipc_queue* gq, bool locked) {
    int idx;

    if (!atomic_dec_and_test(&gq->count))
        return;

    if (!locked)
        spin_lock(&gdev.lock);

    while (gq->next != gq->last) {
        idx = gq->next;
        if (gq->pages[idx].page) {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 6, 0)
            put_page(gq->pages[idx].page);
#else
            page_cache_release(gq->pages[idx].page);
#endif
            gq->pages[idx].page = NULL;
        }
        if (gq->pages[idx].file) {
            fput_atomic(gq->pages[idx].file);
            gq->pages[idx].file  = NULL;
            gq->pages[idx].pgoff = 0;
        }
        gq->next++;
        gq->next &= (PAGE_QUEUE - 1);
    }

    list_del(&gq->list);

    if (!locked)
        spin_unlock(&gdev.lock);

    kmem_cache_free(gipc_queue_cachep, gq);
}

#if defined(SPLIT_RSS_COUNTING)
static void add_mm_counter_fast(struct mm_struct* mm, int member, int val) {
    struct task_struct* task = current;

    if (likely(task->mm == mm))
        task->rss_stat.count[member] += val;
    else
        add_mm_counter(mm, member, val);
}
#else
#define add_mm_counter_fast(mm, member, val) add_mm_counter(mm, member, val)
#endif

#define inc_mm_counter_fast(mm, member) add_mm_counter_fast(mm, member, 1)
#define dec_mm_counter_fast(mm, member) add_mm_counter_fast(mm, member, -1)

inline int make_page_cow(struct mm_struct* mm, struct vm_area_struct* vma, unsigned long addr) {
    pgd_t* pgd;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
    p4d_t* p4d;
#endif
    pud_t* pud;
    pmd_t* pmd;
    pte_t* pte;
    spinlock_t* ptl;

    pgd = pgd_offset(mm, addr);
    if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
        goto no_page;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
    p4d = p4d_offset(pgd, addr);
    if (p4d_none(*p4d) || unlikely(p4d_bad(*p4d)))
        goto no_page;

    pud = pud_offset(p4d, addr);
#else
    pud = pud_offset(pgd, addr);
#endif
    if (pud_none(*pud) || unlikely(pud_bad(*pud)))
        goto no_page;

    pmd = pmd_offset(pud, addr);
    if (pmd_none(*pmd) || unlikely(pmd_bad(*pmd)))
        goto no_page;

    BUG_ON(pmd_trans_huge(*pmd));

    pte = pte_offset_map_lock(mm, pmd, addr, &ptl);
    if (!pte_present(*pte)) {
        spin_unlock(ptl);
        goto no_page;
    }

    ptep_set_wrprotect(mm, addr, pte);
    spin_unlock(ptl);
    DEBUG("make page COW at %lx\n", addr);
    return 0;
no_page:
    return -EFAULT;
}

static void fill_page_bit_map(struct mm_struct* mm, unsigned long addr, unsigned long nr_pages,
                              unsigned long page_bit_map[PAGE_BITS]) {
    int i = 0;

    DEBUG("GIPC_SEND fill_page_bit_map %lx - %lx\n", addr, addr + (nr_pages << PAGE_SHIFT));

    do {
        struct vm_area_struct* vma;
        pgd_t* pgd;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
        p4d_t* p4d;
#endif
        pud_t* pud;
        pmd_t* pmd;
        pte_t* pte;
        spinlock_t* ptl;
        bool has_page = false;

        vma = find_vma(mm, addr);
        if (!vma)
            goto next;

        BUG_ON(vma->vm_flags & VM_HUGETLB);

        pgd = pgd_offset(mm, addr);
        if (pgd_none(*pgd) || pgd_bad(*pgd))
            goto next;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
        p4d = p4d_offset(pgd, addr);
        if (p4d_none(*p4d) || unlikely(p4d_bad(*p4d)))
            goto next;

        pud = pud_offset(p4d, addr);
#else
        pud = pud_offset(pgd, addr);
#endif
        if (pud_none(*pud) || pud_bad(*pud))
            goto next;

        pmd = pmd_offset(pud, addr);
        if (pmd_none(*pmd))
            goto next;

        if (unlikely(pmd_trans_huge(*pmd))) {
            has_page = true;
            goto next;
        }

        if (pmd_bad(*pmd))
            goto next;

        pte = pte_offset_map_lock(mm, pmd, addr, &ptl);

        if (pte_none(*pte))
            goto next_locked;
#if 0
        if (unlikely(!pte_present(*pte)) && pte_file(*pte))
            goto next_locked;
#endif
        has_page = true;
    next_locked:
        spin_unlock(ptl);
    next:
        if (has_page) {
            DEBUG("found a page at %lx\n", addr);
            set_bit(i, page_bit_map);
        } else {
            clear_bit(i, page_bit_map);
        }
    } while (i++, addr += PAGE_SIZE, i < nr_pages);
}

static int get_pages(struct task_struct* task, unsigned long start, unsigned long nr_pages,
                     unsigned long page_bit_map[PAGE_BITS], struct page* pages[PAGE_QUEUE],
                     struct vm_area_struct* vmas[PAGE_QUEUE]) {
    struct mm_struct* mm       = task->mm;
    struct vm_area_struct* vma = NULL;
    unsigned long addr         = start, nr;
    int i                      = 0, j, rv;

    while (i < nr_pages) {
        unsigned long flushed, vmflags;
        int last = i;

        if (test_bit(last, page_bit_map)) {
            i = find_next_zero_bit(page_bit_map, PAGE_QUEUE, last + 1);
            if (i > nr_pages)
                i = nr_pages;
            nr = i - last;

            DEBUG("GIPC_SEND get_user_pages %ld pages at %lx\n", addr, nr);

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 9, 0)
            rv = get_user_pages(addr, nr, FOLL_GET | FOLL_FORCE | FOLL_SPLIT, pages + last,
                                vmas + last);
#else
            rv = __get_user_pages(task, mm, addr, nr, FOLL_GET | FOLL_FORCE | FOLL_SPLIT,
                                  pages + last, vmas + last, NULL);
#endif
            if (rv <= 0) {
                printk(KERN_ERR "Graphene error: get_user_pages at 0x%016lx-0x%016lx\n",
                       addr, addr + (nr << PAGE_SHIFT));
                return rv;
            }

            if (rv != nr) {
                printk(KERN_ERR "Graphene error: get_user_pages at 0x%016lx\n",
                       addr + (rv << PAGE_SHIFT));
                return -EACCES;
            }

            flushed = addr;
            vmflags = 0;
            for (j = 0; j < nr; j++) {
                unsigned long target = addr + (j << PAGE_SHIFT);

                /* Mark source COW */
                rv = make_page_cow(mm, vmas[last + j], target);
                if (rv)
                    return rv;

                if (PageAnon(pages[last + j])) {
                    /* Fix up the counters */
                    inc_mm_counter_fast(mm, MM_FILEPAGES);
                    dec_mm_counter_fast(mm, MM_ANONPAGES);
                    pages[last + j]->mapping = NULL;
                }

#ifdef FLUSH_TLB_MM_RANGE
                if (vmflags == vmas[last + j]->vm_flags)
                    continue;
                if (flushed < target)
                    FLUSH_TLB_MM_RANGE(mm, flushed, target, vmflags);
                flushed = target;
                vmflags = vmas[last + j]->vm_flags;
#else
                FLUSH_TLB_PAGE(vmas[last + j], target);
#endif
            }

#ifdef FLUSH_TLB_MM_RANGE
            if (flushed < addr + (nr << PAGE_SHIFT))
                FLUSH_TLB_MM_RANGE(mm, flushed, addr + (nr << PAGE_SHIFT), vmflags);
#endif
            vma = vmas[i - 1];
            addr += nr << PAGE_SHIFT;
        } else {
            /* This is the case where a page (or pages) are not
             * currently mapped.
             * Handle the hole appropriately. */
            i = find_next_bit(page_bit_map, PAGE_QUEUE, last + 1);
            if (i > nr_pages)
                i = nr_pages;
            nr = i - last;

            DEBUG("GIPC_SEND skip %ld pages at %lx\n", addr, nr);

            for (j = 0; j < nr; j++) {
                if (!vma) {
                    vma = find_vma(mm, addr);
                } else {
                    /* DEP 6/17/13 - these addresses should
                     * be monotonically increasing. */
                    for (; vma && addr >= vma->vm_end; vma = vma->vm_next)
                        ;

                    /* Leverage monotonic increasing vmas
                     * to more quickly detect holes in the
                     * address space. */
                    if (vma && addr < vma->vm_start)
                        vma = NULL;
                }

                pages[last + j] = NULL;
                vmas[last + j]  = vma;
                addr += PAGE_SIZE;
            }
        }
    }

    return i;
}

static int do_gipc_send(struct task_struct* task, struct gipc_queue* gq,
                        struct gipc_send_buffer* gbuf, unsigned long __user* uaddr,
                        unsigned long __user* ulen, unsigned long* copied_pages) {
    struct mm_struct* mm = task->mm;
    unsigned long addr, len, nr_pages;
    int rv, i;

    DEBUG("GIPC_SEND uaddr = %p, ulen = %p\n", uaddr, ulen);

    rv = copy_from_user(&addr, uaddr, sizeof(unsigned long));
    if (rv) {
        printk(KERN_ALERT "Graphene SEND: bad buffer %p\n", uaddr);
        return -EFAULT;
    }

    rv = copy_from_user(&len, ulen, sizeof(unsigned long));
    if (rv) {
        printk(KERN_ALERT "Graphene SEND: bad buffer %p\n", ulen);
        return -EFAULT;
    }

    if (addr > addr + len) {
        printk(KERN_ALERT
               "Graphene SEND: attempt to send %p - %p by thread %d FAIL: bad argument\n",
               (void*)addr, (void*)(addr + len), task->pid);
        return -EINVAL;
    }

    DEBUG("GIPC_SEND addr = %lx, len = %ld\n", addr, len);

    nr_pages = len >> PAGE_SHIFT;

    if (!access_ok(VERIFY_READ, addr, len)) {
        printk(KERN_ALERT "Graphene SEND: attempt to send %p - %p (%ld pages) by thread %d FAIL: "
               "bad permission\n",
               (void*)addr, (void*)(addr + len), nr_pages, task->pid);
        return -EFAULT;
    }

    DEBUG("    %p - %p (%ld pages) sent by thread %d\n", (void*)addr, (void*)(addr + len), nr_pages,
          task->pid);

    while (nr_pages) {
        unsigned long nr = (nr_pages <= PAGE_QUEUE) ? nr_pages : PAGE_QUEUE;

        /* for each of these addresses - check if
         * demand faulting will be triggered
         * if vma is present, but there is no page
         * present(pmd/pud not present or PTE_PRESENT
         * is off) then get_user_pages will trigger
         * the creation of those */

        down_write(&mm->mmap_sem);

        fill_page_bit_map(mm, addr, nr, gbuf->page_bit_map);

        rv = get_pages(task, addr, nr, gbuf->page_bit_map, gbuf->pages, gbuf->vmas);
        if (rv < 0) {
            up_write(&mm->mmap_sem);
            break;
        }

        for (i = 0; i < nr; i++) {
            BUG_ON((!gbuf->vmas[i]) && (!!gbuf->pages[i]));
            if (gbuf->vmas[i] && gbuf->vmas[i]->vm_file) {
                gbuf->files[i] = get_file(gbuf->vmas[i]->vm_file);
                gbuf->pgoffs[i] =
                    ((addr - gbuf->vmas[i]->vm_start) >> PAGE_SHIFT) + gbuf->vmas[i]->vm_pgoff;
            } else {
                gbuf->files[i]  = NULL;
                gbuf->pgoffs[i] = 0;
            }
            addr += PAGE_SIZE;
        }

        up_write(&mm->mmap_sem);

        for (i = 0; i < nr; i++) {
            /* Put in the pending buffer*/
            if (((gq->last + 1) & (PAGE_QUEUE - 1)) == gq->next) {
                /* The blocking condition for send
                 * and recv can't both be true! */
                wake_up_all(&gq->recv);
                wait_event_interruptible(gq->send, ((gq->last + 1) & (PAGE_QUEUE - 1)) != gq->next);
                if (signal_pending(task)) {
                    rv = -ERESTARTSYS;
                    goto out;
                }
            }
            gq->pages[gq->last].page  = gbuf->pages[i];
            gq->pages[gq->last].file  = gbuf->files[i];
            gq->pages[gq->last].pgoff = gbuf->pgoffs[i];
            gq->last++;
            gq->last &= PAGE_QUEUE - 1;
            (*copied_pages)++;
        }

        wake_up_all(&gq->recv);
        nr_pages -= nr;
    }

out:
    return rv;
}

static inline int recv_next(struct task_struct* task, struct gipc_queue* gq) {
    if (gq->next == gq->last) {
        /* The blocking condition for send & recv can't both be true */
        wake_up_all(&gq->send);
        wait_event_interruptible(gq->recv, gq->next != gq->last);
        if (signal_pending(task))
            return -ERESTARTSYS;
    }

    return gq->next;
}

static int do_gipc_recv(struct task_struct* task, struct gipc_queue* gq,
                        unsigned long __user* uaddr, unsigned long __user* ulen,
                        unsigned long __user* uprot, unsigned long* copied_pages) {
    struct mm_struct* mm       = task->mm;
    struct vm_area_struct* vma = NULL;
    unsigned long start, addr, len, nr_pages, prot, pgoff;
    struct page* page = NULL;
    struct file* file = NULL;
    int i             = 0, rv;

    rv = copy_from_user(&addr, uaddr, sizeof(unsigned long));
    if (rv) {
        printk(KERN_ALERT "Graphene RECV: bad buffer %p\n", uaddr);
        return -EFAULT;
    }

    rv = copy_from_user(&len, ulen, sizeof(unsigned long));
    if (rv) {
        printk(KERN_ALERT "Graphene RECV: bad buffer %p\n", ulen);
        return -EFAULT;
    }

    rv = copy_from_user(&prot, uprot, sizeof(unsigned long));
    if (rv) {
        printk(KERN_ALERT "Graphene RECV: bad buffer %p\n", uprot);
        return -EFAULT;
    }

    nr_pages = len >> PAGE_SHIFT;
    start    = addr;
    down_write(&mm->mmap_sem);

    while (i < nr_pages) {
        int found    = recv_next(task, gq);
        int need_map = 1;

        if (found < 0) {
            rv = found;
            goto finish;
        }

        page  = gq->pages[found].page;
        file  = gq->pages[found].file;
        pgoff = gq->pages[found].pgoff;
        gq->next++;
        gq->next &= PAGE_QUEUE - 1;
        wake_up_all(&gq->send);

        if (vma) {
            need_map = 0;
            if (vma->vm_file != file)
                need_map = 1;
            if (file && vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT) != addr)
                need_map = 1;
            if (prot != (vma->vm_flags & (VM_READ | VM_WRITE | VM_EXEC)))
                need_map = 1;
        }

        if (need_map) {
            unsigned long flags = MAP_PRIVATE;

            if (addr)
                flags |= MAP_FIXED;
            if (file)
                flags |= MAP_FILE;
            else
                flags |= MAP_ANONYMOUS;

            addr = DO_MMAP_PGOFF(file, addr, (nr_pages - i) << PAGE_SHIFT, prot, flags, pgoff);

            if (IS_ERR_VALUE(addr)) {
                rv = PTR_ERR((void*)addr);
                printk(KERN_ERR "Graphene error: failed to mmap (%d)\n", -rv);
                goto finish;
            }

            if (file)
                DEBUG("map %08lx-%08lx file %p\n", addr, addr + ((nr_pages - i) << PAGE_SHIFT),
                      file);
            else
                DEBUG("map %08lx-%08lx\n", addr, addr + ((nr_pages - i) << PAGE_SHIFT));

            if (!start)
                start = addr;

            vma = find_vma(mm, addr);
            if (!vma) {
                printk(KERN_ERR "Graphene error: can't find vma at %p\n", (void*)addr);
                rv = -ENOENT;
                goto finish;
            }
        } else {
            BUG_ON(!vma);
        }

        if (page) {
            rv = vm_insert_page(vma, addr, page);
            if (rv) {
                printk(KERN_ERR "Graphene error: fail to insert page %d\n", rv);
                goto finish;
            }
            rv = make_page_cow(mm, vma, addr);
            if (rv) {
                printk(KERN_ERR "Graphene error: can't make vma copy-on-write at %p\n",
                       (void*)addr);
                goto finish;
            }
        }

    finish:
        /* Drop the kernel's reference to this page */
        if (page)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 6, 0)
            put_page(page);
#else
            page_cache_release(page);
#endif
        if (file)
            fput_atomic(file);
        if (rv)
            break;
        i++;
        addr += PAGE_SIZE;
        (*copied_pages)++;
    }

    up_write(&mm->mmap_sem);

    if (i)
        DEBUG("    %p - %p (%d pages) received by thread %d\n", (void*)start,
              (void*)start + (i << PAGE_SHIFT), i, task->pid);

    if (start) {
        rv = copy_to_user(uaddr, &start, sizeof(unsigned long));
        if (rv) {
            printk(KERN_ERR "Graphene error: bad buffer %p\n", uaddr);
            return -EFAULT;
        }
    }

    return rv;
}

static long gipc_ioctl(struct file* file, unsigned int cmd, unsigned long arg) {
    struct task_struct* task = current;
    struct gipc_queue* gq    = NULL;
    long rv                  = 0;

    switch (cmd) {
        case GIPC_SEND: {
            struct gipc_send gs;
            struct gipc_send_buffer* gbuf;
            int i;
            unsigned long nr_pages = 0;

            rv = copy_from_user(&gs, (void*)arg, sizeof(gs));
            if (rv) {
                printk(KERN_ALERT "Graphene SEND: bad buffer %p\n", (void*)arg);
                return -EFAULT;
            }

            /* Find/allocate the gipc_pages struct for our recipient */
            gq = (struct gipc_queue*)file->private_data;
            if (!gq)
                return -EFAULT;

            gbuf = kmem_cache_alloc(gipc_send_buffer_cachep, GFP_KERNEL);
            if (!gbuf)
                return -ENOMEM;

            DEBUG("GIPC_SEND %ld entries to token %lld by thread %d\n", gs.entries, gq->token,
                  task->pid);

            mutex_lock(&gq->send_lock);

            for (i = 0; i < gs.entries; i++) {
                rv = do_gipc_send(task, gq, gbuf, gs.addr + i, gs.len + i, &nr_pages);
                if (rv < 0)
                    break;
            }

            mutex_unlock(&gq->send_lock);
            DEBUG("GIPC_SEND return to thread %d, %ld pages are sent\n", task->pid, nr_pages);
            kmem_cache_free(gipc_send_buffer_cachep, gbuf);
            rv = nr_pages ?: rv;
            break;
        }

        case GIPC_RECV: {
            struct gipc_recv gr;
            int i;
            unsigned long nr_pages = 0;

            rv = copy_from_user(&gr, (void*)arg, sizeof(gr));
            if (rv) {
                printk(KERN_ERR "Graphene error: bad buffer %p\n", (void*)arg);
                return -EFAULT;
            }

            gq = (struct gipc_queue*)file->private_data;
            if (!gq)
                return -EBADF;

            DEBUG("GIPC_RECV %ld entries to token %lld by thread %d\n", gr.entries, gq->token,
                  task->pid);
            mutex_lock(&gq->recv_lock);

            for (i = 0; i < gr.entries; i++) {
                rv = do_gipc_recv(task, gq, gr.addr + i, gr.len + i, gr.prot + i, &nr_pages);
                if (rv < 0)
                    break;
            }

            mutex_unlock(&gq->recv_lock);
            DEBUG("GIPC_RECV return to thread %d, %ld pages are received\n", task->pid, nr_pages);
            rv = nr_pages ?: rv;
            break;
        }

        case GIPC_CREATE: {
            gq = create_gipc_queue(file);
            if (!gq) {
                rv = -ENOMEM;
                break;
            }

            DEBUG("GIPC_CREATE token %lld by thread %d\n", gq->token, task->pid);
            rv = gq->token;
            break;
        }

        case GIPC_JOIN: {
            struct gipc_queue* q;
            u64 token   = arg;
            u64 session = GIPC_OWNER;

            if (file->private_data != NULL)
                return -EBUSY;

            /* Search for this token */
            spin_lock(&gdev.lock);
            list_for_each_entry(q, &gdev.channels, list) {
                if (q->token == token) {
                    gq = q;
                    break;
                }
            }

            /* Fail if we didn't find it */
            if (!gq) {
                spin_unlock(&gdev.lock);
                return -ENOENT;
            }

            if (gq->owner != session) {
                spin_unlock(&gdev.lock);
                return -EPERM;
            }

            atomic_inc(&gq->count);
            file->private_data = gq;

            /* Hold the lock until we allocate so only one process
             * gets the queue */
            spin_unlock(&gdev.lock);
            DEBUG("GIPC_JOIN token %lld by thread %d\n", token, task->pid);
            rv = 0;
            break;
        }

        default:
            printk(KERN_ALERT "Graphene unknown ioctl %u %lu\n", cmd, arg);
            rv = -ENOSYS;
            break;
    }

    return rv;
}

static int gipc_release(struct inode* inode, struct file* file) {
    struct gipc_queue* gq = (struct gipc_queue*)file->private_data;

    if (!gq)
        return 0;

    file->private_data = NULL;
    release_gipc_queue(gq, false);
    return 0;
}

static int gipc_open(struct inode* inode, struct file* file) {
    file->private_data = NULL;
    return 0;
}

static struct file_operations gipc_fops = {
    .owner          = THIS_MODULE,
    .release        = gipc_release,
    .open           = gipc_open,
    .unlocked_ioctl = gipc_ioctl,
    .compat_ioctl   = gipc_ioctl,
    .llseek         = noop_llseek,
};

static struct miscdevice gipc_dev = {
    .minor = GIPC_MINOR,
    .name  = "gipc",
    .fops  = &gipc_fops,
    .mode  = 0666,
};

static int __init gipc_init(void) {
    int rv = 0;

#ifdef MY_DO_MMAP
    LOOKUP_KSYM(do_mmap);
#endif
#ifdef MY_DO_MMAP_PGOFF
    LOOKUP_KSYM(do_mmap_pgoff);
#endif
#ifdef MY_FLUSH_TLB_MM_RANGE
    LOOKUP_KSYM(flush_tlb_mm_range);
#endif
#ifdef MY_FLUSH_TLB_PAGE
    LOOKUP_KSYM(flush_tlb_page);
#endif

#ifndef gipc_get_session
    my_gipc_get_session = (void*)kallsyms_lookup_name("gipc_get_session");
#endif

    /* Register the kmem cache */
    gipc_queue_cachep = kmem_cache_create("gipc_queue", sizeof(struct gipc_queue), 0,
                                          SLAB_HWCACHE_ALIGN |
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 12, 0)
                                              SLAB_TYPESAFE_BY_RCU,
#else
                                              SLAB_DESTROY_BY_RCU,
#endif
                                          NULL);
    if (!gipc_queue_cachep) {
        printk(KERN_ERR "Graphene error: failed to create a gipc queues cache\n");
        return -ENOMEM;
    }

    gipc_send_buffer_cachep =
        kmem_cache_create("gipc_send_buffer", sizeof(struct gipc_send_buffer), 0,
                          SLAB_HWCACHE_ALIGN |
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 12, 0)
                              SLAB_TYPESAFE_BY_RCU,
#else
                              SLAB_DESTROY_BY_RCU,
#endif
                          NULL);
    if (!gipc_send_buffer_cachep) {
        printk(KERN_ERR "Graphene error: failed to create a gipc buffers cache\n");
        return -ENOMEM;
    }

    INIT_LIST_HEAD(&gdev.channels);
    spin_lock_init(&gdev.lock);
    gdev.max_token = 1;

    rv = misc_register(&gipc_dev);
    if (rv) {
        printk(KERN_ERR "Graphene error: failed to add a char device (rv=%d)\n", rv);
        return rv;
    }

    printk(KERN_ALERT "Graphene IPC: Hello, world\n");
    return 0;
}

static void __exit gipc_exit(void) {
    struct gipc_queue *gq, *n;
    spin_lock(&gdev.lock);
    list_for_each_entry_safe(gq, n, &gdev.channels, list) {
        release_gipc_queue(gq, true);
    }
    spin_unlock(&gdev.lock);

    misc_deregister(&gipc_dev);
    kmem_cache_destroy(gipc_queue_cachep);

    printk(KERN_ALERT "Graphene IPC: Goodbye, cruel world\n");
}

module_init(gipc_init);
module_exit(gipc_exit);
