1 /* 2 * PROJECT: ReactOS kernel-mode tests 3 * LICENSE: LGPL-2.1+ (https://spdx.org/licenses/LGPL-2.1+) 4 * PURPOSE: Kernel-Mode Test Suite driver 5 * COPYRIGHT: Copyright 2011-2018 Thomas Faber <thomas.faber@reactos.org> 6 * Copyright 2013 Nikolay Borisov <nib9@aber.ac.uk> 7 */ 8 9 #include <ntddk.h> 10 #include <ntifs.h> 11 #include <ndk/ketypes.h> 12 #include <ntstrsafe.h> 13 #include <limits.h> 14 #include <pseh/pseh2.h> 15 16 #define NDEBUG 17 #include <debug.h> 18 19 #include <kmt_public.h> 20 #define KMT_DEFINE_TEST_FUNCTIONS 21 #include <kmt_test.h> 22 23 /* Usermode callback definitions */ 24 typedef struct _KMT_USER_WORK_ENTRY 25 { 26 LIST_ENTRY ListEntry; 27 KEVENT WorkDoneEvent; 28 KMT_CALLBACK_REQUEST_PACKET Request; 29 PKMT_RESPONSE Response; 30 } KMT_USER_WORK_ENTRY, *PKMT_USER_WORK_ENTRY; 31 32 typedef struct _KMT_USER_WORK_LIST 33 { 34 LIST_ENTRY ListHead; 35 FAST_MUTEX Lock; 36 KEVENT NewWorkEvent; 37 } KMT_USER_WORK_LIST, *PKMT_USER_WORK_LIST; 38 39 /* Prototypes */ 40 DRIVER_INITIALIZE DriverEntry; 41 static DRIVER_UNLOAD DriverUnload; 42 __drv_dispatchType(IRP_MJ_CREATE) 43 static DRIVER_DISPATCH DriverCreate; 44 __drv_dispatchType(IRP_MJ_CLEANUP) 45 static DRIVER_DISPATCH DriverCleanup; 46 __drv_dispatchType(IRP_MJ_CLOSE) 47 static DRIVER_DISPATCH DriverClose; 48 __drv_dispatchType(IRP_MJ_DEVICE_CONTROL) 49 static DRIVER_DISPATCH DriverIoControl; 50 static VOID KmtCleanUsermodeCallbacks(VOID); 51 52 /* Globals */ 53 static PDEVICE_OBJECT MainDeviceObject; 54 PDRIVER_OBJECT KmtDriverObject = NULL; 55 static KMT_USER_WORK_LIST WorkList; 56 static ULONG RequestId = 0; 57 58 /* Entry */ 59 /** 60 * @name DriverEntry 61 * 62 * Driver Entry point. 63 * 64 * @param DriverObject 65 * Driver Object 66 * @param RegistryPath 67 * Driver Registry Path 68 * 69 * @return Status 70 */ 71 NTSTATUS 72 NTAPI 73 DriverEntry( 74 IN PDRIVER_OBJECT DriverObject, 75 IN PUNICODE_STRING RegistryPath) 76 { 77 NTSTATUS Status = STATUS_SUCCESS; 78 UNICODE_STRING DeviceName; 79 PKMT_DEVICE_EXTENSION DeviceExtension; 80 PKPRCB Prcb; 81 82 PAGED_CODE(); 83 84 UNREFERENCED_PARAMETER(RegistryPath); 85 86 DPRINT("DriverEntry\n"); 87 88 Prcb = KeGetCurrentPrcb(); 89 KmtIsCheckedBuild = (Prcb->BuildType & PRCB_BUILD_DEBUG) != 0; 90 KmtIsMultiProcessorBuild = (Prcb->BuildType & PRCB_BUILD_UNIPROCESSOR) == 0; 91 KmtDriverObject = DriverObject; 92 93 RtlInitUnicodeString(&DeviceName, KMTEST_DEVICE_DRIVER_PATH); 94 Status = IoCreateDevice(DriverObject, sizeof(KMT_DEVICE_EXTENSION), 95 &DeviceName, 96 FILE_DEVICE_UNKNOWN, 97 FILE_DEVICE_SECURE_OPEN | FILE_READ_ONLY_DEVICE, 98 FALSE, &MainDeviceObject); 99 100 if (!NT_SUCCESS(Status)) 101 goto cleanup; 102 103 DPRINT("DriverEntry. Created DeviceObject %p. DeviceExtension %p\n", 104 MainDeviceObject, MainDeviceObject->DeviceExtension); 105 DeviceExtension = MainDeviceObject->DeviceExtension; 106 DeviceExtension->ResultBuffer = NULL; 107 DeviceExtension->Mdl = NULL; 108 109 DriverObject->DriverUnload = DriverUnload; 110 DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreate; 111 DriverObject->MajorFunction[IRP_MJ_CLEANUP] = DriverCleanup; 112 DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverClose; 113 DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverIoControl; 114 115 ExInitializeFastMutex(&WorkList.Lock); 116 KeInitializeEvent(&WorkList.NewWorkEvent, NotificationEvent, FALSE); 117 InitializeListHead(&WorkList.ListHead); 118 119 cleanup: 120 if (MainDeviceObject && !NT_SUCCESS(Status)) 121 { 122 IoDeleteDevice(MainDeviceObject); 123 MainDeviceObject = NULL; 124 } 125 126 return Status; 127 } 128 129 /* Dispatch functions */ 130 /** 131 * @name DriverUnload 132 * 133 * Driver cleanup funtion. 134 * 135 * @param DriverObject 136 * Driver Object 137 */ 138 static 139 VOID 140 NTAPI 141 DriverUnload( 142 IN PDRIVER_OBJECT DriverObject) 143 { 144 PAGED_CODE(); 145 146 UNREFERENCED_PARAMETER(DriverObject); 147 148 DPRINT("DriverUnload\n"); 149 150 KmtCleanUsermodeCallbacks(); 151 152 if (MainDeviceObject) 153 { 154 #if DBG 155 PKMT_DEVICE_EXTENSION DeviceExtension = MainDeviceObject->DeviceExtension; 156 ASSERT(!DeviceExtension->Mdl); 157 ASSERT(!DeviceExtension->ResultBuffer); 158 #endif 159 ASSERT(!ResultBuffer); 160 IoDeleteDevice(MainDeviceObject); 161 } 162 } 163 164 /** 165 * @name DriverCreate 166 * 167 * Driver Dispatch function for IRP_MJ_CREATE 168 * 169 * @param DeviceObject 170 * Device Object 171 * @param Irp 172 * I/O request packet 173 * 174 * @return Status 175 */ 176 static 177 NTSTATUS 178 NTAPI 179 DriverCreate( 180 IN PDEVICE_OBJECT DeviceObject, 181 IN PIRP Irp) 182 { 183 NTSTATUS Status = STATUS_SUCCESS; 184 PIO_STACK_LOCATION IoStackLocation; 185 186 PAGED_CODE(); 187 188 IoStackLocation = IoGetCurrentIrpStackLocation(Irp); 189 190 DPRINT("DriverCreate. DeviceObject=%p, RequestorMode=%d, FileObject=%p, FsContext=%p, FsContext2=%p\n", 191 DeviceObject, Irp->RequestorMode, IoStackLocation->FileObject, 192 IoStackLocation->FileObject->FsContext, IoStackLocation->FileObject->FsContext2); 193 194 Irp->IoStatus.Status = Status; 195 Irp->IoStatus.Information = 0; 196 197 IoCompleteRequest(Irp, IO_NO_INCREMENT); 198 199 return Status; 200 } 201 202 /** 203 * @name DriverCleanup 204 * 205 * Driver Dispatch function for IRP_MJ_CLEANUP 206 * 207 * @param DeviceObject 208 * Device Object 209 * @param Irp 210 * I/O request packet 211 * 212 * @return Status 213 */ 214 static 215 NTSTATUS 216 NTAPI 217 DriverCleanup( 218 IN PDEVICE_OBJECT DeviceObject, 219 IN PIRP Irp) 220 { 221 NTSTATUS Status = STATUS_SUCCESS; 222 PIO_STACK_LOCATION IoStackLocation; 223 PKMT_DEVICE_EXTENSION DeviceExtension; 224 225 PAGED_CODE(); 226 227 IoStackLocation = IoGetCurrentIrpStackLocation(Irp); 228 229 DPRINT("DriverCleanup. DeviceObject=%p, RequestorMode=%d, FileObject=%p, FsContext=%p, FsContext2=%p\n", 230 DeviceObject, Irp->RequestorMode, IoStackLocation->FileObject, 231 IoStackLocation->FileObject->FsContext, IoStackLocation->FileObject->FsContext2); 232 233 ASSERT(IoStackLocation->FileObject->FsContext2 == NULL); 234 DeviceExtension = DeviceObject->DeviceExtension; 235 if (DeviceExtension->Mdl && IoStackLocation->FileObject->FsContext == DeviceExtension->Mdl) 236 { 237 MmUnlockPages(DeviceExtension->Mdl); 238 IoFreeMdl(DeviceExtension->Mdl); 239 DeviceExtension->Mdl = NULL; 240 ResultBuffer = DeviceExtension->ResultBuffer = NULL; 241 } 242 else 243 { 244 ASSERT(IoStackLocation->FileObject->FsContext == NULL); 245 } 246 247 Irp->IoStatus.Status = Status; 248 Irp->IoStatus.Information = 0; 249 250 IoCompleteRequest(Irp, IO_NO_INCREMENT); 251 252 return Status; 253 } 254 255 /** 256 * @name DriverClose 257 * 258 * Driver Dispatch function for IRP_MJ_CLOSE 259 * 260 * @param DeviceObject 261 * Device Object 262 * @param Irp 263 * I/O request packet 264 * 265 * @return Status 266 */ 267 static 268 NTSTATUS 269 NTAPI 270 DriverClose( 271 IN PDEVICE_OBJECT DeviceObject, 272 IN PIRP Irp) 273 { 274 NTSTATUS Status = STATUS_SUCCESS; 275 276 PAGED_CODE(); 277 278 DPRINT("DriverClose. DeviceObject=%p, RequestorMode=%d\n", 279 DeviceObject, Irp->RequestorMode); 280 281 Irp->IoStatus.Status = Status; 282 Irp->IoStatus.Information = 0; 283 284 IoCompleteRequest(Irp, IO_NO_INCREMENT); 285 286 return Status; 287 } 288 289 /** 290 * @name DriverIoControl 291 * 292 * Driver Dispatch function for IRP_MJ_DEVICE_CONTROL 293 * 294 * @param DeviceObject 295 * Device Object 296 * @param Irp 297 * I/O request packet 298 * 299 * @return Status 300 */ 301 static 302 NTSTATUS 303 NTAPI 304 DriverIoControl( 305 IN PDEVICE_OBJECT DeviceObject, 306 IN PIRP Irp) 307 { 308 NTSTATUS Status = STATUS_SUCCESS; 309 PIO_STACK_LOCATION IoStackLocation; 310 SIZE_T Length = 0; 311 312 PAGED_CODE(); 313 314 IoStackLocation = IoGetCurrentIrpStackLocation(Irp); 315 316 DPRINT("DriverIoControl. Code=0x%08X, DeviceObject=%p, FileObject=%p, FsContext=%p, FsContext2=%p\n", 317 IoStackLocation->Parameters.DeviceIoControl.IoControlCode, 318 DeviceObject, IoStackLocation->FileObject, 319 IoStackLocation->FileObject->FsContext, IoStackLocation->FileObject->FsContext2); 320 321 switch (IoStackLocation->Parameters.DeviceIoControl.IoControlCode) 322 { 323 case IOCTL_KMTEST_GET_TESTS: 324 { 325 PCKMT_TEST TestEntry; 326 LPSTR OutputBuffer = Irp->AssociatedIrp.SystemBuffer; 327 size_t Remaining = IoStackLocation->Parameters.DeviceIoControl.OutputBufferLength; 328 329 DPRINT("DriverIoControl. IOCTL_KMTEST_GET_TESTS, outlen=%lu\n", 330 IoStackLocation->Parameters.DeviceIoControl.OutputBufferLength); 331 332 for (TestEntry = TestList; TestEntry->TestName; ++TestEntry) 333 { 334 RtlStringCbCopyExA(OutputBuffer, Remaining, TestEntry->TestName, &OutputBuffer, &Remaining, 0); 335 if (Remaining) 336 { 337 *OutputBuffer++ = '\0'; 338 --Remaining; 339 } 340 } 341 if (Remaining) 342 { 343 *OutputBuffer++ = '\0'; 344 --Remaining; 345 } 346 Length = IoStackLocation->Parameters.DeviceIoControl.OutputBufferLength - Remaining; 347 break; 348 } 349 case IOCTL_KMTEST_RUN_TEST: 350 { 351 ANSI_STRING TestName; 352 PCKMT_TEST TestEntry; 353 354 DPRINT("DriverIoControl. IOCTL_KMTEST_RUN_TEST, inlen=%lu, outlen=%lu\n", 355 IoStackLocation->Parameters.DeviceIoControl.InputBufferLength, 356 IoStackLocation->Parameters.DeviceIoControl.OutputBufferLength); 357 TestName.Length = TestName.MaximumLength = (USHORT)min(IoStackLocation->Parameters.DeviceIoControl.InputBufferLength, USHRT_MAX); 358 TestName.Buffer = Irp->AssociatedIrp.SystemBuffer; 359 DPRINT("DriverIoControl. Run test: %Z\n", &TestName); 360 361 for (TestEntry = TestList; TestEntry->TestName; ++TestEntry) 362 { 363 ANSI_STRING EntryName; 364 if (TestEntry->TestName[0] == '-') 365 RtlInitAnsiString(&EntryName, TestEntry->TestName + 1); 366 else 367 RtlInitAnsiString(&EntryName, TestEntry->TestName); 368 369 if (!RtlCompareString(&TestName, &EntryName, FALSE)) 370 { 371 DPRINT1("DriverIoControl. Starting test %Z\n", &EntryName); 372 TestEntry->TestFunction(); 373 DPRINT1("DriverIoControl. Finished test %Z\n", &EntryName); 374 break; 375 } 376 } 377 378 if (!TestEntry->TestName) 379 Status = STATUS_OBJECT_NAME_INVALID; 380 381 break; 382 } 383 case IOCTL_KMTEST_SET_RESULTBUFFER: 384 { 385 PKMT_DEVICE_EXTENSION DeviceExtension = DeviceObject->DeviceExtension; 386 387 DPRINT("DriverIoControl. IOCTL_KMTEST_SET_RESULTBUFFER, buffer=%p, inlen=%lu, outlen=%lu\n", 388 IoStackLocation->Parameters.DeviceIoControl.Type3InputBuffer, 389 IoStackLocation->Parameters.DeviceIoControl.InputBufferLength, 390 IoStackLocation->Parameters.DeviceIoControl.OutputBufferLength); 391 392 if (DeviceExtension->Mdl) 393 { 394 if (IoStackLocation->FileObject->FsContext != DeviceExtension->Mdl) 395 { 396 Status = STATUS_ACCESS_DENIED; 397 break; 398 } 399 MmUnlockPages(DeviceExtension->Mdl); 400 IoFreeMdl(DeviceExtension->Mdl); 401 IoStackLocation->FileObject->FsContext = NULL; 402 ResultBuffer = DeviceExtension->ResultBuffer = NULL; 403 } 404 405 DeviceExtension->Mdl = IoAllocateMdl(IoStackLocation->Parameters.DeviceIoControl.Type3InputBuffer, 406 IoStackLocation->Parameters.DeviceIoControl.InputBufferLength, 407 FALSE, FALSE, NULL); 408 if (!DeviceExtension->Mdl) 409 { 410 Status = STATUS_INSUFFICIENT_RESOURCES; 411 break; 412 } 413 414 _SEH2_TRY 415 { 416 MmProbeAndLockPages(DeviceExtension->Mdl, UserMode, IoModifyAccess); 417 } 418 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) 419 { 420 Status = _SEH2_GetExceptionCode(); 421 IoFreeMdl(DeviceExtension->Mdl); 422 DeviceExtension->Mdl = NULL; 423 break; 424 } _SEH2_END; 425 426 ResultBuffer = DeviceExtension->ResultBuffer = MmGetSystemAddressForMdlSafe(DeviceExtension->Mdl, NormalPagePriority); 427 IoStackLocation->FileObject->FsContext = DeviceExtension->Mdl; 428 429 DPRINT("DriverIoControl. ResultBuffer: %ld %ld %ld %ld\n", 430 ResultBuffer->Successes, ResultBuffer->Failures, 431 ResultBuffer->LogBufferLength, ResultBuffer->LogBufferMaxLength); 432 break; 433 } 434 case IOCTL_KMTEST_USERMODE_AWAIT_REQ: 435 { 436 PLIST_ENTRY Entry; 437 PKMT_USER_WORK_ENTRY WorkItem; 438 439 DPRINT("DriverIoControl. IOCTL_KMTEST_USERMODE_AWAIT_REQ, len=%lu\n", 440 IoStackLocation->Parameters.DeviceIoControl.OutputBufferLength); 441 442 /* TODO: prevent multiple concurrent invocations */ 443 Status = KeWaitForSingleObject(&WorkList.NewWorkEvent, UserRequest, UserMode, FALSE, NULL); 444 if (Status == STATUS_USER_APC || Status == STATUS_KERNEL_APC) 445 break; 446 447 if (IoStackLocation->Parameters.DeviceIoControl.OutputBufferLength < sizeof(KMT_CALLBACK_REQUEST_PACKET)) 448 { 449 Status = STATUS_INVALID_BUFFER_SIZE; 450 break; 451 } 452 453 ASSERT(!IsListEmpty(&WorkList.ListHead)); 454 455 Entry = WorkList.ListHead.Flink; 456 WorkItem = CONTAINING_RECORD(Entry, KMT_USER_WORK_ENTRY, ListEntry); 457 458 Length = sizeof(WorkItem->Request); 459 RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, &WorkItem->Request, Length); 460 Status = STATUS_SUCCESS; 461 462 KeClearEvent(&WorkList.NewWorkEvent); 463 break; 464 465 } 466 case IOCTL_KMTEST_USERMODE_SEND_RESPONSE: 467 { 468 PLIST_ENTRY Entry; 469 PKMT_USER_WORK_ENTRY WorkEntry; 470 PVOID Response; 471 ULONG ResponseSize = IoStackLocation->Parameters.DeviceIoControl.OutputBufferLength; 472 473 DPRINT("DriverIoControl. IOCTL_KMTEST_USERMODE_SEND_RESPONSE, inlen=%lu, outlen=%lu\n", 474 IoStackLocation->Parameters.DeviceIoControl.InputBufferLength, 475 IoStackLocation->Parameters.DeviceIoControl.OutputBufferLength); 476 477 if (IoStackLocation->Parameters.DeviceIoControl.InputBufferLength != sizeof(ULONG) || ResponseSize != sizeof(KMT_RESPONSE)) 478 { 479 Status = STATUS_INVALID_BUFFER_SIZE; 480 break; 481 } 482 483 /* FIXME: don't misuse the output buffer as an input! */ 484 Response = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); 485 if (Response == NULL) 486 { 487 Status = STATUS_INSUFFICIENT_RESOURCES; 488 break; 489 } 490 491 ExAcquireFastMutex(&WorkList.Lock); 492 493 Status = STATUS_OBJECTID_NOT_FOUND; 494 495 Entry = WorkList.ListHead.Flink; 496 while (Entry != &WorkList.ListHead) 497 { 498 WorkEntry = CONTAINING_RECORD(Entry, KMT_USER_WORK_ENTRY, ListEntry); 499 if (WorkEntry->Request.RequestId == *(PULONG)Irp->AssociatedIrp.SystemBuffer) 500 { 501 WorkEntry->Response = ExAllocatePoolWithTag(PagedPool, sizeof(KMT_RESPONSE), 'pseR'); 502 if (WorkEntry->Response == NULL) 503 { 504 Status = STATUS_INSUFFICIENT_RESOURCES; 505 break; 506 } 507 508 RtlCopyMemory(WorkEntry->Response, Response, ResponseSize); 509 KeSetEvent(&WorkEntry->WorkDoneEvent, IO_NO_INCREMENT, FALSE); 510 Status = STATUS_SUCCESS; 511 break; 512 } 513 514 Entry = Entry->Flink; 515 } 516 517 ExReleaseFastMutex(&WorkList.Lock); 518 519 break; 520 } 521 default: 522 DPRINT1("DriverIoControl. Invalid IoCtl code 0x%08X\n", 523 IoStackLocation->Parameters.DeviceIoControl.IoControlCode); 524 Status = STATUS_INVALID_DEVICE_REQUEST; 525 break; 526 } 527 528 Irp->IoStatus.Status = Status; 529 Irp->IoStatus.Information = Length; 530 531 IoCompleteRequest(Irp, IO_NO_INCREMENT); 532 533 return Status; 534 } 535 536 /** 537 * @name KmtUserModeCallback 538 * 539 * Enqueue a request to the usermode callback queue and blocks until the work 540 * is finished. 541 * 542 * @param Operation 543 * TODO 544 * @param Parameters 545 * TODO 546 * TODO: why is this PVOID? 547 * 548 * @return Response from user mode 549 */ 550 PKMT_RESPONSE 551 KmtUserModeCallback( 552 IN KMT_CALLBACK_INFORMATION_CLASS Operation, 553 IN PVOID Parameters) 554 { 555 PKMT_RESPONSE Result; 556 NTSTATUS Status; 557 PKMT_USER_WORK_ENTRY WorkEntry; 558 LARGE_INTEGER Timeout; 559 560 PAGED_CODE(); 561 562 WorkEntry = ExAllocatePoolWithTag(PagedPool, sizeof(KMT_USER_WORK_ENTRY), 'ekrW'); 563 if (WorkEntry == NULL) 564 return NULL; 565 566 KeInitializeEvent(&WorkEntry->WorkDoneEvent, NotificationEvent, FALSE); 567 WorkEntry->Request.RequestId = RequestId++; 568 WorkEntry->Request.OperationClass = Operation; 569 WorkEntry->Request.Parameters = Parameters; 570 WorkEntry->Response = NULL; 571 572 ExAcquireFastMutex(&WorkList.Lock); 573 InsertTailList(&WorkList.ListHead, &WorkEntry->ListEntry); 574 ExReleaseFastMutex(&WorkList.Lock); 575 576 KeSetEvent(&WorkList.NewWorkEvent, IO_NO_INCREMENT, FALSE); 577 578 Timeout.QuadPart = -10 * 1000 * 1000 * 10; //wait for 10 seconds 579 Status = KeWaitForSingleObject(&WorkEntry->WorkDoneEvent, Executive, UserMode, FALSE, &Timeout); 580 581 if (Status == STATUS_USER_APC || Status == STATUS_KERNEL_APC || Status == STATUS_TIMEOUT) 582 { 583 DPRINT1("Unexpected callback abortion! Reason: %lx\n", Status); 584 } 585 586 ExAcquireFastMutex(&WorkList.Lock); 587 RemoveEntryList(&WorkEntry->ListEntry); 588 ExReleaseFastMutex(&WorkList.Lock); 589 590 Result = WorkEntry->Response; 591 592 ExFreePoolWithTag(WorkEntry, 'ekrW'); 593 594 return Result; 595 } 596 597 /** 598 * @name KmtFreeCallbackResponse 599 * 600 * TODO 601 * 602 * @param Response 603 * TODO 604 */ 605 VOID 606 KmtFreeCallbackResponse( 607 PKMT_RESPONSE Response) 608 { 609 PAGED_CODE(); 610 611 ExFreePoolWithTag(Response, 'pseR'); 612 } 613 614 /** 615 * @name KmtCleanUsermodeCallbacks 616 * 617 * TODO 618 */ 619 static 620 VOID 621 KmtCleanUsermodeCallbacks(VOID) 622 { 623 PLIST_ENTRY Entry; 624 625 PAGED_CODE(); 626 627 ExAcquireFastMutex(&WorkList.Lock); 628 629 Entry = WorkList.ListHead.Flink; 630 while (Entry != &WorkList.ListHead) 631 { 632 PKMT_USER_WORK_ENTRY WorkEntry = CONTAINING_RECORD(Entry, KMT_USER_WORK_ENTRY, ListEntry); 633 if (WorkEntry->Response != NULL) 634 { 635 KmtFreeCallbackResponse(WorkEntry->Response); 636 } 637 638 Entry = Entry->Flink; 639 640 ExFreePoolWithTag(WorkEntry, 'ekrW'); 641 } 642 643 ExReleaseFastMutex(&WorkList.Lock); 644 } 645