Executive Summary
Lukas Maar’s post writes up a clean page-level use-after-free in the upstream drivers/accel/qaic Linux kernel driver. The bug is small and structural: qaic_gem_object_mmap walks a scatter-gather table that backs a QAIC buffer object and maps each segment into user space with remap_pfn_range, without ever clamping the running offset against vma->vm_end. remap_pfn_range itself doesn’t reject a write that overshoots the VMA in the device-mapping path it ends up on. The net effect is that a userspace caller can hand the driver an undersized VMA, the driver will happily install page-table entries for the full SGT — including pages outside the VMA — and then, when the user unmaps the VMA, the kernel will free the backing compound pages while page-table entries for the overshoot still point at them. The result is a stale user-space mapping pointing at freed kernel physical memory.
From there the exploit is straightforward. Allocate a QAIC buffer object large enough to cover an order-3 compound page, map only part of it (so the unmap-on-free leaves the overshoot stale), munmap the partial mapping, free the buffer, and have something else — a sprayed array of pipe_buffer slab pages — land on the freed compound page. The stale user PTE now points into pipe metadata, giving the exploit primitive control of pipe_buffer->page from user space. From a controlled page pointer, the exploit derives the vmemmap base, reads the kernel image to defeat KASLR via anon_pipe_buf_ops, locates init_task, walks the task list to the current process, and flips the credentials. The whole chain runs against Linux v6.18 and ships with an upstream patch already merged.
The PoC video Lukas Maar published alongside the post walks the chain end-to-end — the figure below is the original embed:
qaic page-level use-after-free. Source: original article.Hardware Virtualization
The first practical hurdle the post addresses is that the QAIC driver is a PCI driver for actual Qualcomm Cloud AI accelerator hardware — not something most researchers have on their desks. Lukas Maar’s answer is to skip the hardware entirely: a small patch (qaic-fake-online.patch) introduces a fake_online module parameter that, when set, bypasses the PCI probe path and registers a synthetic parent device with a fake DBC (data block channel) and just enough of the kernel-side scaffolding for buffer-object lifecycle to work end-to-end. None of the actual acceleration logic runs — nor needs to — because the bug lives entirely in the buffer-object mmap path, not in any code that talks to the hardware. The accompanying repository (github.com/lukasmaar/qaic-page-uaf) ships the patch alongside the QEMU setup and the PoC.
That detail is also the reason the bug matters more broadly than “Qualcomm cloud servers”: the trigger doesn’t depend on real QAIC hardware being present, so anything that exposes the QAIC device node to an attacker (a misconfigured permission, a container with the wrong device cgroup, an out-of-tree backport into a kernel that happens to ship the QAIC driver) inherits the vulnerability.
The Bug
The mmap path for a QAIC buffer object is qaic_gem_object_mmap. The function is short, and the bug is visible in the loop itself — the running offset is incremented from each scatter-gather entry’s length without ever being compared against the size of vma. Reproduced verbatim:
static int qaic_gem_object_mmap(struct drm_gem_object *obj, struct vm_area_struct *vma)
{
struct qaic_bo *bo = to_qaic_bo(obj);
unsigned long offset = 0;
struct scatterlist *sg;
int ret = 0;
if (drm_gem_is_imported(obj))
return -EINVAL;
for (sg = bo->sgt->sgl; sg; sg = sg_next(sg)) {
if (sg_page(sg)) {
ret = remap_pfn_range(vma, vma->vm_start + offset, page_to_pfn(sg_page(sg)),
sg->length, vma->vm_page_prot);
if (ret)
goto out;
offset += sg->length;
}
}
out:
return ret;
}
Source: original article (Listing 1).
The other half of the picture is how the backing SGT is built. create_sgt tries to coalesce the requested size into large compound pages first (via alloc_pages with a falling order), and only falls back to smaller orders if a larger one fails. That means individual SGT entries can cover much more memory than a single page — in particular, the entries can cover entire order-3 (32 KiB) compound pages, which is the reclamation target the exploit later relies on. Reproduced verbatim:
static int create_sgt(struct qaic_device *qdev, struct sg_table **sgt_out, u64 size)
{
struct scatterlist *sg;
struct sg_table *sgt;
struct page **pages;
int *pages_order;
int max_order;
int nr_pages;
int ret = 0;
int i, j, k;
int order;
[...]
nr_pages = DIV_ROUND_UP(size, PAGE_SIZE);
[...]
/*
* Allocate requested memory using alloc_pages. It is possible to allocate
* the requested memory in multiple chunks by calling alloc_pages
* multiple times. Use SG table to handle multiple allocated pages.
*/
i = 0;
while (nr_pages > 0) {
order = min(get_order(nr_pages * PAGE_SIZE), max_order);
while (1) {
pages[i] = alloc_pages(GFP_KERNEL | GFP_HIGHUSER |
__GFP_NOWARN | __GFP_ZERO |
(order ? __GFP_NORETRY : __GFP_RETRY_MAYFAIL),
order);
if (pages[i])
break;
if (!order--) {
ret = -ENOMEM;
goto free_partial_alloc;
}
}
max_order = order;
pages_order[i] = order;
nr_pages -= 1 << order;
[...]
i++;
}
[...]
/* Populate the SG table with the allocated memory pages */
sg = sgt->sgl;
for (k = 0; k < i; k++, sg = sg_next(sg)) {
[...]
sg_set_page(sg, pages[k], PAGE_SIZE << pages_order[k], 0);
[...]
}
[...]
}
Source: original article (Listing 2).
The two-line PoC for the bug is the cleanest way to see how the lifecycle goes wrong. The user requests a large buffer object (large enough to back an order-3 compound page somewhere in the SGT) but only mmaps a smaller VMA over part of it. The driver, blissfully unaware, calls remap_pfn_range for the entire SGT and writes PTEs all the way past vma->vm_end into adjacent address space the user has reserved. The user then writes past the “legitimate” mapping to prove the overshoot exists, unmaps the “legitimate” VMA, and frees the buffer object. The unmap of the legitimate VMA tears down the PTEs only inside vma->vm_start..vm_end — the overshoot PTEs in the adjacent VMA stay live. The buffer-object free, however, returns the order-3 page to the page allocator. End state: live user-space PTEs pointing at freed compound-page physical memory. Reproduced verbatim:
#define TARGET_BO_MAPPING (PAGE_SIZE << 10)
#define TARGET_BO_SIZE (TARGET_BO_MAPPING + (PAGE_SIZE << 3))
#define DANGLING_ADDR 0xdeadbeef000
int poc(void)
{
int fd = open_qaic("/dev/accel/accel0");
uint32_t handle = qaic_create_bo(fd, TARGET_BO_SIZE); // [1]
uint64_t mmap_offset = qaic_get_mmap_offset(fd, handle);
void *mapping = qaic_map_bo_addr(DANGLING_ADDR, fd,
TARGET_BO_MAPPING, mmap_offset); // [2]
/*
* This intentionally writes beyond the nominal VMA length to demonstrate
* that QAIC mapped more than userspace asked for.
*/
memset(mapping, 0x41, TARGET_BO_SIZE);
/*
* Keep an adjacent mapping present to stabilize page-table teardown and
* avoid losing a higher-level page table page during unmap.
*/
SYSCHK(mmap((void *)(DANGLING_ADDR + TARGET_BO_MAPPING + PAGE_SIZE),
PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1, 0)); // [5]
SYSCHK(munmap(mapping, TARGET_BO_MAPPING)); // [3]
qaic_free_bo(fd, handle); // [4]
}
Source: original article (Listing 3).
The numbered annotations in the comments match the post: [1] allocate a large enough BO, [2] map only the first TARGET_BO_MAPPING bytes, [3] unmap, [4] free the BO, [5] keep an adjacent anonymous mapping to stop higher-level page-table pages from being torn down with the unmap — that’s the trick that ensures the overshoot PTEs themselves survive the cleanup.
Exploit Path
With a stale userspace mapping pointing at a freed order-3 compound page, the rest of the exploit is a fairly standard Linux-kernel page-UAF playbook adapted to a 32 KiB target.
- Reclamation. The reclaim target is
pipe_bufferslab pages — an order-3 cache that holds the metadata behind every active pipe (eachpipe_buffercarries apage *, an offset, a length, and a function-table pointer). The exploit pre-allocates a population of pipes, triggers the bug, and then grows and fills the pipes so the kernel allocates fresh order-3 pages for theirpipe_bufferarrays. With reasonable spray hygiene the freed compound page ends up rebound as a pipe-buffer slab page, and the dangling user mapping now reads and writes pipe metadata directly. - Arbitrary physical R/W. Once the user can write into a
pipe_bufferfrom userspace, they ownpipe_buffer->page. Writing or reading from the corresponding pipe FD now performs I/O against whatever physical page the attacker pointed it at. That is a clean R/W primitive over kernel-physical memory. - KASLR leak. Default-initialised
pipe_bufferentries point theiropsatanon_pipe_buf_ops, a known symbol in the kernel image. Reading that pointer out of one of the sprayed pipe buffers gives KASLR-defeating randomness for free. - vmemmap base. The
pagepointer recovered from a pipe buffer is enough to derive the kernel’svmemmapbase — the linear-map array ofstruct pagecovering all physical memory — which then lets the exploit translate any physical address it cares about into a controllable pipe target. - Privilege escalation chain. With the kernel image base in hand, the exploit reads
init_task, walks the kernel’s circular task list looking for the calling process by PID, finds thetask_struct, and rewrites thecredpointer (or the contents behind it) to UID 0. The process returns to userspace as root.
The post emphasises that the chain is “straightforward” precisely because the primitive that comes out of the bug is so strong. Page-level UAFs that survive a clean unmap-then-free sequence are rare; they almost always collapse into a kernel-physical R/W primitive once a reclaim target lands.
Conclusion (Original)
Lukas Maar’s closing observation is the load-bearing one for anyone writing or auditing kernel drivers: any custom mmap path that walks a list of buffer-backing pages must bound the running offset against vma->vm_end before each remap_pfn_range call. remap_pfn_range alone is not enough to catch the overshoot in the device-mapping path, and the consequence of getting the bound wrong is a page-level UAF the moment the user unmaps and the buffer is freed. The vulnerability does not depend on any QAIC-specific hardware feature; it lives entirely in the driver’s buffer-object lifecycle. Patches have been merged upstream (two revisions, on lore in April 2026); a CVE was not requested.
Key Takeaways
- The root cause is a missing VMA-bound check inside
qaic_gem_object_mmap: the SGT-walk loop installs PTEs all the way through the scatter-gather table without ever clipping againstvma->vm_end, so the driver writes mappings into adjacent user VMAs. - The other half of the structural failure is on the helper side:
remap_pfn_rangedoesn’t reject the overshoot in the path the device-mapping case takes, so the driver doesn’t get a friendlyEINVALto save it. - The trigger is a four-step user sequence — allocate a BO larger than the mmap, map only part of it, unmap the partial VMA, free the BO. The adjacent anonymous mapping in step [5] of the PoC is there to keep the higher-level page-table pages alive through the unmap.
- End state of the trigger: live user-space PTEs pointing at a freed order-3 compound page. That is a strong page-level UAF, not a one-shot type-confusion.
- The natural reclaim target is
pipe_bufferslab pages — same order, sprayable, attacker-controlled metadata once the page is rebound. - From the reclaim, KASLR falls out of
anon_pipe_buf_ops,vmemmapfalls out of the recoveredpagepointer, and the kernel-physical R/W lets the exploit walk frominit_taskto the calling task’scredand flip it. - The QAIC driver is mainline (
drivers/accel/qaic) and the bug is hardware-independent — anything that exposes/dev/accel/accel0to an attacker inherits this. Patches are upstream; no CVE was requested.
Defensive Recommendations
- Apply the upstream patch series (two revisions on
lore.kernel.orgin April 2026). Anything carrying the QAIC driver below the fixed version is exploitable; the bug doesn’t care whether real hardware is present. - Audit your custom
mmaphandlers for the same pattern: any driver that walks a scatter-gather table or a list of buffer pages and callsremap_pfn_rangein a loop should explicitly clamp the running offset againstvma->vm_end - vma->vm_startbefore each iteration. Do not rely onremap_pfn_rangealone to catch the overshoot. - Restrict access to
/dev/accel/*on multi-tenant systems. On servers without legitimate QAIC users, the device nodes should not be present in the namespace at all; on systems that do use them, scope access through device cgroups and seccomp policy so non-trusted code cannot reach the ioctls. - Enable
CONFIG_SLAB_FREELIST_HARDENED,CONFIG_SLAB_FREELIST_RANDOM, andCONFIG_RANDOMIZE_KSTACK_OFFSETon production kernels. None of these stop a page-level UAF in itself, but they make the reclaim spray noisier and lift the exploit’s engineering cost. - Audit other accelerator drivers in
drivers/accel/for the same pattern. QAIC is the case study; the structural mistake (walk SGT, callremap_pfn_range, never clamp against the VMA) is generic to anything that exposes a GEM-style buffer object through a DRM mmap path. - For kernel hardening teams: consider whether the device-mapping path inside
remap_pfn_rangeshould refuse out-of-VMA writes outright. The current behaviour — that drivers are responsible for clamping — is sharp-edged enough that it produces this bug class repeatedly. - For EDR/HIDS: a sudden burst of fresh pipes (
pipe(2)) followed by F_SETPIPE_SZ growth on a process that just touched/dev/accel/*ioctls is a strong telltale of an exploit attempting a page-UAF reclaim. The pattern is not unique to QAIC but is a useful signal in general. - Track CVE-less kernel bug reports. The QAIC bug ships without a CVE because the disclosure path went straight to lore with a patch; correlating Lukas Maar’s blog and similar individual disclosures into a tracking list is the practical workaround.
Conclusion
The QAIC page-UAF is a textbook example of how thin the margin is between a correct custom mmap handler and a kernel-mode arbitrary read/write primitive. The bug is a missing bound check inside a four-line loop; the consequence is a stale userspace mapping that turns into pipe_buffer-backed physical memory access the moment a reclaim spray succeeds. Lukas Maar’s post is worth reading both as a clean exploit walkthrough and as a reminder to anyone writing driver-side mmap code: remap_pfn_range doesn’t check VMA bounds for you, and any code path that loops over a buffer list has to do it itself.
Original text: “Privilege Escalation via a Page Use-After-Free in Qualcomm’s AI Accelerator Linux Kernel Driver” by Lukas Maar.

