/* * Virtual Gamepad Emulation Framework - Windows kernel-mode bus driver * Copyright (C) 2016-2018 Benjamin Höglinger-Stelzer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "busenum.h" #include #include #include "busenum.tmh" #ifdef ALLOC_PRAGMA #pragma alloc_text (PAGE, Bus_PlugInDevice) #pragma alloc_text (PAGE, Bus_UnPlugDevice) #endif // // Simulates a device plug-in event. // NTSTATUS Bus_PlugInDevice( _In_ WDFDEVICE Device, _In_ WDFREQUEST Request, _In_ BOOLEAN IsInternal, _Out_ size_t* Transferred) { PDO_IDENTIFICATION_DESCRIPTION description; NTSTATUS status; PVIGEM_PLUGIN_TARGET plugIn; WDFFILEOBJECT fileObject; PFDO_FILE_DATA pFileData; size_t length = 0; WDF_OBJECT_ATTRIBUTES requestAttribs; PFDO_PLUGIN_REQUEST_DATA pReqData; PFDO_DEVICE_DATA pFdoData; PAGED_CODE(); TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_BUSENUM, "%!FUNC! Entry"); pFdoData = FdoGetData(Device); status = WdfRequestRetrieveInputBuffer(Request, sizeof(VIGEM_PLUGIN_TARGET), (PVOID)&plugIn, &length); if (!NT_SUCCESS(status)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "WdfRequestRetrieveInputBuffer failed with status %!STATUS!", status); return status; } if ((sizeof(VIGEM_PLUGIN_TARGET) != plugIn->Size) || (length != plugIn->Size)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "sizeof(VIGEM_PLUGIN_TARGET) buffer size mismatch [%d != %d]", sizeof(VIGEM_PLUGIN_TARGET), plugIn->Size); return STATUS_INVALID_PARAMETER; } if (plugIn->SerialNo == 0) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "Serial no. 0 not allowed"); return STATUS_INVALID_PARAMETER; } *Transferred = length; fileObject = WdfRequestGetFileObject(Request); if (fileObject == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "WdfRequestGetFileObject failed to fetch WDFFILEOBJECT from request 0x%p", Request); return STATUS_INVALID_PARAMETER; } pFileData = FileObjectGetData(fileObject); if (pFileData == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "FileObjectGetData failed to get context data for 0x%p", fileObject); return STATUS_INVALID_PARAMETER; } // // Initialize the description with the information about the newly // plugged in device. // WDF_CHILD_IDENTIFICATION_DESCRIPTION_HEADER_INIT(&description.Header, sizeof(description)); description.SerialNo = plugIn->SerialNo; description.TargetType = plugIn->TargetType; description.OwnerProcessId = CURRENT_PROCESS_ID(); description.SessionId = pFileData->SessionId; description.OwnerIsDriver = IsInternal; // Set default IDs if supplied values are invalid if (plugIn->VendorId == 0 || plugIn->ProductId == 0) { switch (plugIn->TargetType) { case Xbox360Wired: description.VendorId = 0x045E; description.ProductId = 0x028E; break; case DualShock4Wired: description.VendorId = 0x054C; description.ProductId = 0x05C4; break; case XboxOneWired: description.VendorId = 0x0E6F; description.ProductId = 0x0139; #if !DBG // TODO: implement and remove! return STATUS_NOT_SUPPORTED; #endif break; } } else { description.VendorId = plugIn->VendorId; description.ProductId = plugIn->ProductId; } TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "New PDO properties: serial = %d, type = %d, pid = %d, session = %d, internal = %d, vid = 0x%04X, pid = 0x%04X", description.SerialNo, description.TargetType, description.OwnerProcessId, description.SessionId, description.OwnerIsDriver, description.VendorId, description.ProductId ); WdfSpinLockAcquire(pFdoData->PendingPluginRequestsLock); TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "Current pending requests count: %d", WdfCollectionGetCount(pFdoData->PendingPluginRequests)); status = WdfChildListAddOrUpdateChildDescriptionAsPresent(WdfFdoGetDefaultChildList(Device), &description.Header, NULL); if (!NT_SUCCESS(status)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "WdfChildListAddOrUpdateChildDescriptionAsPresent failed with status %!STATUS!", status); goto pluginEnd; } // // The requested serial number is already in use // if (status == STATUS_OBJECT_NAME_EXISTS) { status = STATUS_INVALID_PARAMETER; TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "The described PDO already exists (%!STATUS!)", status); goto pluginEnd; } WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&requestAttribs, FDO_PLUGIN_REQUEST_DATA); // // Allocate context data to request // status = WdfObjectAllocateContext(Request, &requestAttribs, (PVOID)&pReqData); if (!NT_SUCCESS(status)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "WdfObjectAllocateContext failed with status %!STATUS!", status); goto pluginEnd; } // // Glue current serial to request // pReqData->Serial = plugIn->SerialNo; // // Timestamp the request to track its age // pReqData->Timestamp = KeQueryPerformanceCounter(&pReqData->Frequency); // // Keep track of pending request in collection // status = WdfCollectionAdd(pFdoData->PendingPluginRequests, Request); if (!NT_SUCCESS(status)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "WdfCollectionAdd failed with status %!STATUS!", status); goto pluginEnd; } TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_BUSENUM, "Added item with serial: %d", plugIn->SerialNo); // // At least one request present in the collection; start clean-up timer // WdfTimerStart( pFdoData->PendingPluginRequestsCleanupTimer, WDF_REL_TIMEOUT_IN_MS(ORC_TIMER_PERIODIC_DUE_TIME) ); TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_DRIVER, "Started periodic timer"); status = NT_SUCCESS(status) ? STATUS_PENDING : status; pluginEnd: WdfSpinLockRelease(pFdoData->PendingPluginRequestsLock); TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_BUSENUM, "%!FUNC! Exit with status %!STATUS!", status); return status; } // // Simulates a device unplug event. // NTSTATUS Bus_UnPlugDevice( _In_ WDFDEVICE Device, _In_ WDFREQUEST Request, _In_ BOOLEAN IsInternal, _Out_ size_t* Transferred) { NTSTATUS status; WDFDEVICE hChild; WDFCHILDLIST list; WDF_CHILD_LIST_ITERATOR iterator; WDF_CHILD_RETRIEVE_INFO childInfo; PDO_IDENTIFICATION_DESCRIPTION description; BOOLEAN unplugAll; PVIGEM_UNPLUG_TARGET unPlug; WDFFILEOBJECT fileObject; PFDO_FILE_DATA pFileData = NULL; size_t length = 0; PAGED_CODE(); TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_BUSENUM, "%!FUNC! Entry"); status = WdfRequestRetrieveInputBuffer(Request, sizeof(VIGEM_UNPLUG_TARGET), (PVOID)&unPlug, &length); if (!NT_SUCCESS(status)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "WdfRequestRetrieveInputBuffer failed with status %!STATUS!", status); return status; } if ((sizeof(VIGEM_UNPLUG_TARGET) != unPlug->Size) || (length != unPlug->Size)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "sizeof(VIGEM_UNPLUG_TARGET) buffer size mismatch [%d != %d]", sizeof(VIGEM_UNPLUG_TARGET), unPlug->Size); return STATUS_INVALID_PARAMETER; } *Transferred = length; unplugAll = (unPlug->SerialNo == 0); if (!IsInternal) { fileObject = WdfRequestGetFileObject(Request); if (fileObject == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "WdfRequestGetFileObject failed to fetch WDFFILEOBJECT from request 0x%p", Request); return STATUS_INVALID_PARAMETER; } pFileData = FileObjectGetData(fileObject); if (pFileData == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "FileObjectGetData failed to get context data for 0x%p", fileObject); return STATUS_INVALID_PARAMETER; } } TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "Starting child list traversal"); list = WdfFdoGetDefaultChildList(Device); WDF_CHILD_LIST_ITERATOR_INIT(&iterator, WdfRetrievePresentChildren); WdfChildListBeginIteration(list, &iterator); for (;;) { WDF_CHILD_RETRIEVE_INFO_INIT(&childInfo, &description.Header); WDF_CHILD_IDENTIFICATION_DESCRIPTION_HEADER_INIT(&description.Header, sizeof(description)); status = WdfChildListRetrieveNextDevice(list, &iterator, &hChild, &childInfo); // Error or no more children, end loop if (!NT_SUCCESS(status) || status == STATUS_NO_MORE_ENTRIES) { TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "WdfChildListRetrieveNextDevice returned with status %!STATUS!", status); break; } // If unable to retrieve device if (childInfo.Status != WdfChildListRetrieveDeviceSuccess) { TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "childInfo.Status = %d", childInfo.Status); continue; } // Child isn't the one we looked for, skip if (!unplugAll && description.SerialNo != unPlug->SerialNo) { TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "Seeking serial mismatch: %d != %d", description.SerialNo, unPlug->SerialNo); continue; } TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "description.SessionId = %d, pFileData->SessionId = %d", description.SessionId, pFileData->SessionId); // Only unplug owned children if (IsInternal || description.SessionId == pFileData->SessionId) { // Unplug child status = WdfChildListUpdateChildDescriptionAsMissing(list, &description.Header); if (!NT_SUCCESS(status)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "WdfChildListUpdateChildDescriptionAsMissing failed with status %!STATUS!", status); } } } WdfChildListEndIteration(list, &iterator); TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "Finished child list traversal"); TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_BUSENUM, "%!FUNC! Exit with status %!STATUS!", STATUS_SUCCESS); return STATUS_SUCCESS; } // // Sends a report update to an XUSB PDO. // NTSTATUS Bus_XusbSubmitReport(WDFDEVICE Device, ULONG SerialNo, PXUSB_SUBMIT_REPORT Report, BOOLEAN FromInterface) { TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "%!FUNC! Entry"); return Bus_SubmitReport(Device, SerialNo, Report, FromInterface); } // // Queues an inverted call to receive XUSB-specific updates. // NTSTATUS Bus_QueueNotification(WDFDEVICE Device, ULONG SerialNo, WDFREQUEST Request) { NTSTATUS status = STATUS_INVALID_PARAMETER; WDFDEVICE hChild; PPDO_DEVICE_DATA pdoData; PXUSB_DEVICE_DATA xusbData; PDS4_DEVICE_DATA ds4Data; TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_BUSENUM, "%!FUNC! Entry"); hChild = Bus_GetPdo(Device, SerialNo); // Validate child if (hChild == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "Bus_GetPdo: PDO with serial %d not found", SerialNo); return STATUS_NO_SUCH_DEVICE; } // Check common context pdoData = PdoGetData(hChild); if (pdoData == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "PdoGetData failed"); return STATUS_INVALID_PARAMETER; } // Check if caller owns this PDO if (!IS_OWNER(pdoData)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "PDO & Request ownership mismatch: %d != %d", pdoData->OwnerProcessId, CURRENT_PROCESS_ID()); return STATUS_ACCESS_DENIED; } // Queue the request for later completion by the PDO and return STATUS_PENDING switch (pdoData->TargetType) { case Xbox360Wired: xusbData = XusbGetData(hChild); if (xusbData == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "XusbGetData failed"); break; } status = WdfRequestForwardToIoQueue(Request, pdoData->PendingNotificationRequests); break; case DualShock4Wired: ds4Data = Ds4GetData(hChild); if (ds4Data == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "Ds4GetData failed"); break; } status = WdfRequestForwardToIoQueue(Request, pdoData->PendingNotificationRequests); break; default: status = STATUS_NOT_SUPPORTED; TraceEvents(TRACE_LEVEL_WARNING, TRACE_BUSENUM, "Unknown target type: %d (%!STATUS!)", pdoData->TargetType, status); break; } if (!NT_SUCCESS(status)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "WdfRequestForwardToIoQueue failed with status %!STATUS!", status); } status = (NT_SUCCESS(status)) ? STATUS_PENDING : status; TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_BUSENUM, "%!FUNC! Exit with status %!STATUS!", status); return status; } // // Sends a report update to a DS4 PDO. // NTSTATUS Bus_Ds4SubmitReport(WDFDEVICE Device, ULONG SerialNo, PDS4_SUBMIT_REPORT Report, BOOLEAN FromInterface) { TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "%!FUNC! Entry"); return Bus_SubmitReport(Device, SerialNo, Report, FromInterface); } NTSTATUS Bus_XgipSubmitReport(WDFDEVICE Device, ULONG SerialNo, PXGIP_SUBMIT_REPORT Report, BOOLEAN FromInterface) { TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "%!FUNC! Entry"); return Bus_SubmitReport(Device, SerialNo, Report, FromInterface); } NTSTATUS Bus_XgipSubmitInterrupt(WDFDEVICE Device, ULONG SerialNo, PXGIP_SUBMIT_INTERRUPT Report, BOOLEAN FromInterface) { TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "%!FUNC! Entry"); return Bus_SubmitReport(Device, SerialNo, Report, FromInterface); } WDFDEVICE Bus_GetPdo(IN WDFDEVICE Device, IN ULONG SerialNo) { WDFCHILDLIST list; WDF_CHILD_RETRIEVE_INFO info; list = WdfFdoGetDefaultChildList(Device); PDO_IDENTIFICATION_DESCRIPTION description; WDF_CHILD_IDENTIFICATION_DESCRIPTION_HEADER_INIT(&description.Header, sizeof(description)); description.SerialNo = SerialNo; WDF_CHILD_RETRIEVE_INFO_INIT(&info, &description.Header); return WdfChildListRetrievePdo(list, &info); } NTSTATUS Bus_SubmitReport(WDFDEVICE Device, ULONG SerialNo, PVOID Report, BOOLEAN FromInterface) { NTSTATUS status = STATUS_SUCCESS; WDFDEVICE hChild; PPDO_DEVICE_DATA pdoData; WDFREQUEST usbRequest; PIRP pendingIrp; BOOLEAN changed; TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "%!FUNC! Entry"); hChild = Bus_GetPdo(Device, SerialNo); // Validate child if (hChild == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "Bus_GetPdo: PDO with serial %d not found", SerialNo); return STATUS_NO_SUCH_DEVICE; } // Check common context pdoData = PdoGetData(hChild); if (pdoData == NULL) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "PdoGetData failed"); return STATUS_INVALID_PARAMETER; } // Check if caller owns this PDO if (!FromInterface && !IS_OWNER(pdoData)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_BUSENUM, "PDO & Request ownership mismatch: %d != %d", pdoData->OwnerProcessId, CURRENT_PROCESS_ID()); return STATUS_ACCESS_DENIED; } // Check if input is different from previous value switch (pdoData->TargetType) { case Xbox360Wired: changed = (RtlCompareMemory(&XusbGetData(hChild)->Packet.Report, &((PXUSB_SUBMIT_REPORT)Report)->Report, sizeof(XUSB_REPORT)) != sizeof(XUSB_REPORT)); break; case DualShock4Wired: changed = TRUE; break; case XboxOneWired: // TODO: necessary? changed = TRUE; break; default: changed = FALSE; break; } // Don't waste pending IRP if input hasn't changed if (!changed) { TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "Input report hasn't changed since last update, aborting with %!STATUS!", status); return status; } TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "Received new report, processing"); // Get pending USB request switch (pdoData->TargetType) { case Xbox360Wired: status = WdfIoQueueRetrieveNextRequest(pdoData->PendingUsbInRequests, &usbRequest); break; case DualShock4Wired: status = WdfIoQueueRetrieveNextRequest(pdoData->PendingUsbInRequests, &usbRequest); break; case XboxOneWired: // Request is control data if (((PXGIP_SUBMIT_INTERRUPT)Report)->Size == sizeof(XGIP_SUBMIT_INTERRUPT)) { PXGIP_DEVICE_DATA xgip = XgipGetData(hChild); PXGIP_SUBMIT_INTERRUPT interrupt = (PXGIP_SUBMIT_INTERRUPT)Report; WDFMEMORY memory; WDF_OBJECT_ATTRIBUTES memAttribs; WDF_OBJECT_ATTRIBUTES_INIT(&memAttribs); memAttribs.ParentObject = hChild; // Allocate kernel memory status = WdfMemoryCreate(&memAttribs, NonPagedPool, VIGEM_POOL_TAG, interrupt->InterruptLength, &memory, NULL); if (!NT_SUCCESS(status)) { KdPrint((DRIVERNAME "WdfMemoryCreate failed with status 0x%X\n", status)); goto endSubmitReport; } // Copy interrupt buffer to memory object status = WdfMemoryCopyFromBuffer(memory, 0, interrupt->Interrupt, interrupt->InterruptLength); if (!NT_SUCCESS(status)) { KdPrint((DRIVERNAME "WdfMemoryCopyFromBuffer failed with status 0x%X\n", status)); goto endSubmitReport; } // Add memory object to collection status = WdfCollectionAdd(xgip->XboxgipSysInitCollection, memory); if (!NT_SUCCESS(status)) { KdPrint((DRIVERNAME "WdfCollectionAdd failed with status 0x%X\n", status)); goto endSubmitReport; } // Check if all packets have been received xgip->XboxgipSysInitReady = WdfCollectionGetCount(xgip->XboxgipSysInitCollection) == XGIP_SYS_INIT_PACKETS; // If all packets are cached, start initialization timer if (xgip->XboxgipSysInitReady) { WdfTimerStart(xgip->XboxgipSysInitTimer, XGIP_SYS_INIT_PERIOD); } goto endSubmitReport; } status = WdfIoQueueRetrieveNextRequest(XgipGetData(hChild)->PendingUsbInRequests, &usbRequest); break; default: status = STATUS_NOT_SUPPORTED; TraceEvents(TRACE_LEVEL_WARNING, TRACE_BUSENUM, "Unknown target type: %d (%!STATUS!)", pdoData->TargetType, status); goto endSubmitReport; } if (status == STATUS_PENDING) goto endSubmitReport; else if (!NT_SUCCESS(status)) goto endSubmitReport; TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "Processing pending IRP"); // Get pending IRP pendingIrp = WdfRequestWdmGetIrp(usbRequest); // Get USB request block PURB urb = (PURB)URB_FROM_IRP(pendingIrp); // Get transfer buffer PUCHAR Buffer = (PUCHAR)urb->UrbBulkOrInterruptTransfer.TransferBuffer; switch (pdoData->TargetType) { case Xbox360Wired: urb->UrbBulkOrInterruptTransfer.TransferBufferLength = sizeof(XUSB_INTERRUPT_IN_PACKET); // Copy submitted report to cache RtlCopyBytes(&XusbGetData(hChild)->Packet.Report, &((PXUSB_SUBMIT_REPORT)Report)->Report, sizeof(XUSB_REPORT)); // Copy cached report to URB transfer buffer RtlCopyBytes(Buffer, &XusbGetData(hChild)->Packet, sizeof(XUSB_INTERRUPT_IN_PACKET)); break; case DualShock4Wired: urb->UrbBulkOrInterruptTransfer.TransferBufferLength = DS4_REPORT_SIZE; /* Copy report to cache and transfer buffer * Skip first byte as it contains the never changing report id */ RtlCopyBytes(Ds4GetData(hChild)->Report + 1, &((PDS4_SUBMIT_REPORT)Report)->Report, sizeof(DS4_REPORT)); if (Buffer) RtlCopyBytes(Buffer, Ds4GetData(hChild)->Report, DS4_REPORT_SIZE); break; case XboxOneWired: // Request is input report if (((PXGIP_SUBMIT_REPORT)Report)->Size == sizeof(XGIP_SUBMIT_REPORT)) { urb->UrbBulkOrInterruptTransfer.TransferBufferLength = XGIP_REPORT_SIZE; // Increase event counter on every call (can roll-over) XgipGetData(hChild)->Report[2]++; /* Copy report to cache and transfer buffer * Skip first four bytes as they are not part of the report */ RtlCopyBytes(XgipGetData(hChild)->Report + 4, &((PXGIP_SUBMIT_REPORT)Report)->Report, sizeof(XGIP_REPORT)); RtlCopyBytes(Buffer, XgipGetData(hChild)->Report, XGIP_REPORT_SIZE); break; } break; default: status = STATUS_INVALID_PARAMETER; break; } // Complete pending request WdfRequestComplete(usbRequest, status); endSubmitReport: TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_BUSENUM, "%!FUNC! Exit with status %!STATUS!", status); return status; }