Direct Circular Buffer Injection in mouclass.sys
The Windows mouse class driver, mouclass.sys, stores pending input in a circular buffer inside its device extension before the Raw Input Thread consumes it. Any kernel caller that acquires the driver’s own spinlock can write a MOUSE_INPUT_DATA entry directly into that buffer, or complete a pending read Input/Output Request Packet (IRP) against the device, without ever invoking MouseClassServiceCallback or touching the Human Interface Device (HID) stack. The resulting data is byte-identical to what physical hardware produces. No filter driver fires, no callback executes, and no field in the output structure marks the entry as synthetic.
This article explains the internal structure of the mouclass device extension, describes the two paths through which input reaches consumers, presents a kernel driver implementation that injects mouse movement through both paths, and evaluates the detection surface the technique leaves behind. Device extension offsets verified across Windows 10 and Windows 11 through 23H2 are provided.
Background
Every common approach to synthetic mouse input passes through at least one interface that anti cheat software monitors. User mode Application Programming Interface (API) calls such as SendInput follow well-known syscall routes. Virtual HID devices and filter drivers leave observable artifacts in the device stack. MouseClassServiceCallback itself is a frequent target for inline hooks and call stack validation. These realities motivate the search for a path that bypasses all three surfaces entirely.
The mouclass Input Pipeline
mouclass.sys sits between port drivers (which talk to hardware) and the Win32 subsystem (which delivers input to applications). Every physical pointing device feeds into mouclass through MouseClassServiceCallback. The driver enqueues each MOUSE_INPUT_DATA structure into a per-device circular buffer, and the Raw Input Thread inside win32k.sys drains that buffer by issuing read IRPs against \Device\PointerClass0.
From a memory standpoint, the buffer is an ordinary pool allocation. It carries no special page protections, no guard pages, and no integrity metadata. The only access control is a Kernel Spin Lock (KSPIN_LOCK) stored in the device extension alongside the buffer pointers.
Device Extension Fields
The device extension attached to \Device\PointerClass0 contains the circular buffer management state at fixed offsets:
+0x54 InputCount - queued entry count
+0x68 DataQueueBase - base address of the circular buffer
+0x70 WritePointer - next write position
+0x78 ReadPointer - next read position
+0x88 QueueSize - total buffer size in bytes
+0x90 SpinLock - KSPIN_LOCK protecting all fields
+0x98 PendingIrpQueue - LIST_ENTRY head for waiting read IRPs
When MouseClassServiceCallback runs, it acquires the spinlock at +0x90, copies a MOUSE_INPUT_DATA entry to the address held in WritePointer, advances that pointer with wraparound against QueueSize, and increments InputCount. If a read IRP is pending, the callback skips the buffer entirely and completes the IRP inline. None of this logic validates the identity of the caller holding the lock.
Two Delivery Paths
Input reaches mouclass consumers through one of two mechanisms, and the distinction matters for injection fidelity.
Pending IRP completion. The Raw Input Thread issues a read IRP and, when no data is queued, mouclass holds that IRP in PendingIrpQueue. A writer that finds a pending IRP can dequeue it, copy input data into the IRP buffer, and call IoCompleteRequest. The consumer receives the data with zero buffering delay, which matches the latency profile of a physical device generating an interrupt and completing the read synchronously.
Circular buffer enqueue. When no reader is waiting, the writer stores the MOUSE_INPUT_DATA at WritePointer, wraps the pointer if it reaches the buffer boundary, and increments InputCount. The next read IRP that arrives will drain this entry.
Both paths require the spinlock for the entire operation. Acquiring it through KeAcquireSpinLock raises IRQL to DISPATCH_LEVEL and acquires the same lock that mouclass uses internally, guaranteeing atomicity. The driver never sees torn writes or inconsistent pointer state. Note: mouclass itself uses KeAcquireSpinLockAtDpcLevel since it already runs at DISPATCH_LEVEL in the DPC (ISR dispatch completion) context, but the synchronization contract is equivalent.
Injection Implementation
A kernel driver calls IoGetDeviceObjectPointer on \\Device\\PointerClass0, which returns the topmost device object in the stack. On systems without upper filter drivers above mouclass, this is the mouclass device object directly; otherwise the driver walks down to locate it. The injection routine reads DeviceExtension, acquires the spinlock, probes for pending IRPs, and dispatches through the appropriate path:
NTSTATUS injectMouseInput(LONG deltaX, LONG deltaY, USHORT buttonFlags, USHORT buttonData) {
if (!g_ctx.initialized || !g_ctx.mouseDeviceExtension)
return STATUS_DEVICE_NOT_READY;
PUCHAR devExt = (PUCHAR)g_ctx.mouseDeviceExtension;
PKSPIN_LOCK spinLock = (PKSPIN_LOCK)(devExt + MOUCLASS_DEVEXT_SPINLOCK);
KIRQL oldIrql;
MOUSE_INPUT_DATA inputData = { 0 };
inputData.UnitId = 0;
inputData.Flags = MOUSE_MOVE_RELATIVE;
inputData.ButtonFlags = buttonFlags;
inputData.ButtonData = buttonData;
inputData.LastX = deltaX;
inputData.LastY = deltaY;
KeAcquireSpinLock(spinLock, &oldIrql);
PLIST_ENTRY pendingQueue = (PLIST_ENTRY)(devExt + MOUCLASS_DEVEXT_PENDING_IRP);
if (!IsListEmpty(pendingQueue)) {
// pending read IRP exists, complete it directly
PLIST_ENTRY entry = RemoveHeadList(pendingQueue);
PIRP pendingIrp = CONTAINING_RECORD(entry, IRP, Tail.Overlay.ListEntry);
PDRIVER_CANCEL oldCancelRoutine = IoSetCancelRoutine(pendingIrp, NULL);
if (oldCancelRoutine == NULL) {
// cancellation in progress, the cancel routine owns this IRP
// fall through to the circular buffer path below
} else {
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(pendingIrp);
PVOID irpBuffer = pendingIrp->MdlAddress
? MmGetSystemAddressForMdlSafe(pendingIrp->MdlAddress, NormalPagePriority)
: pendingIrp->AssociatedIrp.SystemBuffer;
if (irpBuffer && irpStack->Parameters.Read.Length >= sizeof(MOUSE_INPUT_DATA)) {
RtlCopyMemory(irpBuffer, &inputData, sizeof(MOUSE_INPUT_DATA));
KeReleaseSpinLock(spinLock, oldIrql);
pendingIrp->IoStatus.Status = STATUS_SUCCESS;
pendingIrp->IoStatus.Information = sizeof(MOUSE_INPUT_DATA);
IoCompleteRequest(pendingIrp, IO_MOUSE_INCREMENT);
return STATUS_SUCCESS;
}
}
}
// no pending IRP or completion failed, queue in circular buffer
PVOID queueBase = *(PVOID*)(devExt + MOUCLASS_DEVEXT_DATA_QUEUE_BASE);
PVOID writePtr = *(PVOID*)(devExt + MOUCLASS_DEVEXT_WRITE_POINTER);
ULONG queueSize = *(PULONG)(devExt + MOUCLASS_DEVEXT_QUEUE_SIZE);
PULONG inputCount = (PULONG)(devExt + MOUCLASS_DEVEXT_INPUT_COUNT);
ULONG maxEntries = queueSize / sizeof(MOUSE_INPUT_DATA);
if (*inputCount >= maxEntries - 1) {
KeReleaseSpinLock(spinLock, oldIrql);
return STATUS_INSUFFICIENT_RESOURCES;
}
*(PMOUSE_INPUT_DATA)writePtr = inputData;
PVOID newWritePtr = (PUCHAR)writePtr + sizeof(MOUSE_INPUT_DATA);
if (newWritePtr >= (PUCHAR)queueBase + queueSize)
newWritePtr = queueBase;
*(PVOID*)(devExt + MOUCLASS_DEVEXT_WRITE_POINTER) = newWritePtr;
(*inputCount)++;
KeReleaseSpinLock(spinLock, oldIrql);
return STATUS_SUCCESS;
}
The pending IRP path is not optional. When the Raw Input Thread is actively waiting, completing its IRP directly produces the same delivery timing as a genuine hardware interrupt. Falling back to the buffer in that scenario introduces a measurable latency gap that deviates from normal input cadence and could serve as a detection signal on its own.
The IRP cancellation check deserves attention. Between dequeuing an IRP and clearing its cancel routine, the I/O manager may have already begun cancellation on another processor. If IoSetCancelRoutine returns NULL, the cancel routine owns that IRP and the injection code must not touch it. Dropping into the buffer path in this case is the correct recovery.
Invisibility Properties
Three properties make this injection difficult to observe through conventional means.
MouseClassServiceCallback never executes. Anti cheats that place inline hooks on this function, or that walk the call stack when it fires, see no activity from injected input. The entire callback path is simply not involved.
The HID stack plays no role. mouhid.sys, hidusb.sys, and any filter drivers layered onto the mouse device stack remain idle. No IRP traverses the stack, so stack-based monitoring produces no signal.
The output is structurally identical to hardware input. When win32k.sys reads from the device, it receives a MOUSE_INPUT_DATA structure with the same layout, same field values, and same completion semantics as one produced by a physical pointing device. Nothing in the structure itself distinguishes an injected entry from a real one.
Detection Surface
Despite the invisibility at the driver interface level, the technique is not fundamentally undetectable.
Extended Page Table monitoring. A hypervisor can mark the circular buffer’s physical pages as non-writable in Extended Page Tables (EPT). Any write from outside MouseClassServiceCallback triggers an EPT violation that the hypervisor intercepts. This requires locating the buffer address for each mouclass device instance, which is feasible but demands maintenance across driver reloads and system sessions.
Device extension integrity correlation. An anti cheat can periodically sample InputCount and WritePointer, then correlate changes against hardware interrupt activity. Writes that advance these fields without a corresponding interrupt from a physical device indicate injection. Tuning this check to avoid false positives on legitimate batched input from high polling rate mice is the practical challenge.
Behavioral analysis. Input pattern analysis operates independently of the injection vector. Polling rate consistency, sub-pixel movement distributions, acceleration curves, and inter-sample timing jitter can flag synthetic input regardless of how it enters the pipeline. This class of detection is the most robust because it targets the output, not the mechanism.
As of now, no shipping anti cheat product monitors mouclass internal buffer state.
Offset Definitions
The following offsets are stable across Windows 10 and Windows 11 through 23H2:
#define MOUCLASS_DEVEXT_INPUT_COUNT 0x54
#define MOUCLASS_DEVEXT_DATA_QUEUE_BASE 0x68
#define MOUCLASS_DEVEXT_WRITE_POINTER 0x70
#define MOUCLASS_DEVEXT_READ_POINTER 0x78
#define MOUCLASS_DEVEXT_QUEUE_SIZE 0x88
#define MOUCLASS_DEVEXT_SPINLOCK 0x90
#define MOUCLASS_DEVEXT_PENDING_IRP 0x98
mouclass.sys is stable infrastructure that Microsoft seldom modifies. These offsets have not changed across multiple feature updates. Production use outside controlled testing environments should still resolve them dynamically through pattern scanning the driver binary, since any servicing update could shift internal layout without notice.
Conclusion
The circular buffer inside the mouclass device extension accepts writes from any kernel caller that holds the driver’s spinlock. Because the write bypasses MouseClassServiceCallback, the HID stack, and every filter driver in the device stack, the resulting MOUSE_INPUT_DATA is indistinguishable from hardware generated input at every layer above the driver itself. Detection is possible through hypervisor-level page monitoring, interrupt correlation, or behavioral analysis of input patterns, but no anti cheat currently implements any of these checks against mouclass internals. The root cause is simple: mouclass enforces no caller identity validation on its synchronization primitive, treating any holder of the spinlock as a legitimate writer.