在Driver中呼叫I/O API的時候你考慮到了嗎

Editor發表於2018-03-09
去年學習了之後就給忘了,現在又花了半天時間熟悉了這塊的知識,防止自己再忘記先記錄下來警醒自己。

本文主題在於指出在驅動中呼叫I/O函式時存在的問題,輕者卡死,重者BSOD


1. 前言

驅動中我們經常使用一些I/O函式來查詢檔案、裝置的資訊,比如IoQueryFileDosDeviceName獲取程式的DOS路徑,IoVolumeDeviceToDosName獲取卷的DOS名稱等等,一般使用這些函式的場景無外乎在LoadImage回撥,CreateProcess回撥,微檔案過濾器/傳統檔案過濾器註冊的callback,甚至會在一些核心Hook點中呼叫。可能不做線上產品使用者不多的時候很多問題不會被察覺,自己嘗試的時候基本上也無法出現問題。但是如果使用者一多可能會遇到各種奇奇怪怪的反饋,那麼到底哪裡容易出問題呢?


2. I/O API的特殊性

    Windows中的I/O管理器提供的API大多數都是非同步完成的,而其內部氾濫地使用APC,導致很多I/O函式對使用場景有很高的要求,例如:

    IoVolumeDeviceToDosName

       Starting with Windows Vista, you must ensure that APCs are not disabled before calling this routine. The KeAreAllApcsDisabled routine can be used to verify that APCs are not disabled.

    意思很明確,就是說這個API內部需要用到KernelApc,在呼叫時得確保當前執行緒的APCs可以執行,可以用 KeAreAllApcsDisabled這個API做判斷。

    你無法預知當前的程式碼執行時的環境是怎麼樣的,比如說在LoadImage回撥,你無法確保當前的IRQL一定是PASSIVE_LEVEL,或者沒有在一些核心的臨界區範圍內,說到核心的臨界區,現在常用的兩種: KeEnterCriticalRegion與KeEnterGuardedRegion。


//

// Enters a Guarded Region

//

#define KeEnterGuardedRegionThread(_Thread)                                 \

{                                                                           \

/* Sanity checks */                                                     \

ASSERT(KeGetCurrentIrql() <= APC_LEVEL);                                \

ASSERT(_Thread == KeGetCurrentThread());                                \

ASSERT((_Thread->SpecialApcDisable <= 0) &&                             \

(_Thread->SpecialApcDisable != -32768));                         \

\

/* Disable Special APCs */                                              \

_Thread->SpecialApcDisable--;                                           \

}

#define KeEnterGuardedRegion()                                              \

{                                                                           \

PKTHREAD _Thread = KeGetCurrentThread();                                \

KeEnterGuardedRegionThread(_Thread);                                    \

}

//

// Leaves a Guarded Region

//

#define KeLeaveGuardedRegionThread(_Thread)                                 \

{                                                                           \

/* Sanity checks */                                                     \

ASSERT(KeGetCurrentIrql() <= APC_LEVEL);                                \

ASSERT(_Thread == KeGetCurrentThread());                                \

ASSERT(_Thread->SpecialApcDisable < 0);                                 \

\

/* Leave region and check if APCs are OK now */                         \

if (!(++_Thread->SpecialApcDisable))                                    \

{                                                                       \

/* Check for Kernel APCs on the list */                             \

if (!IsListEmpty(&_Thread->ApcState.                                \

ApcListHead[KernelMode]))                          \

{                                                                   \

/* Check for APC Delivery */                                    \

KiCheckForKernelApcDelivery();                                  \

}                                                                   \

}                                                                       \

}

#define KeLeaveGuardedRegion()                                              \

{                                                                           \

PKTHREAD _Thread = KeGetCurrentThread();                                \

KeLeaveGuardedRegionThread(_Thread);                                    \

}

//

// Enters a Critical Region

//

#define KeEnterCriticalRegionThread(_Thread)                                \

{                                                                           \

/* Sanity checks */                                                     \

ASSERT(_Thread == KeGetCurrentThread());                                \

ASSERT((_Thread->KernelApcDisable <= 0) &&                              \

(_Thread->KernelApcDisable != -32768));                          \

\

/* Disable Kernel APCs */                                               \

_Thread->KernelApcDisable--;                                            \

}

#define KeEnterCriticalRegion()                                             \

{                                                                           \

PKTHREAD _Thread = KeGetCurrentThread();                                \

KeEnterCriticalRegionThread(_Thread);                                   \

}

//

// Leaves a Critical Region

//

#define KeLeaveCriticalRegionThread(_Thread)                                \

{                                                                           \

/* Sanity checks */                                                     \

ASSERT(_Thread == KeGetCurrentThread());                                \

ASSERT(_Thread->KernelApcDisable < 0);                                  \

\

/* Enable Kernel APCs */                                                \

_Thread->KernelApcDisable++;                                            \

\

/* Check if Kernel APCs are now enabled */                              \

if (!(_Thread->KernelApcDisable))                                       \

{                                                                       \

/* Check if we need to request an APC Delivery */                   \

if (!(IsListEmpty(&_Thread->ApcState.ApcListHead[KernelMode])) &&   \

!(_Thread->SpecialApcDisable))                                  \

{                                                                   \

/* Check for the right environment */                           \

KiCheckForKernelApcDelivery();                                  \

}                                                                   \

}                                                                       \

}

#define KeLeaveCriticalRegion()                                             \

{                                                                           \

PKTHREAD _Thread = KeGetCurrentThread();                                \

KeLeaveCriticalRegionThread(_Thread);                                   \

}


我們看到在呼叫KeEnterCriticalRegion後當前執行緒的KernelApcDisable是非零值,而 呼叫 KeEnterGuardedRegion之後 當前執行緒的SpecialApcDisable是非零值
這兩個是什麼玩意呢??


我們接著來看Apc分發的時候如何使用這兩個值的


VOID

NTAPI

KiDeliverApc(IN KPROCESSOR_MODE DeliveryMode,

IN PKEXCEPTION_FRAME ExceptionFrame,

IN PKTRAP_FRAME TrapFrame)

{

PKTHREAD Thread = KeGetCurrentThread();

PKPROCESS Process = Thread->ApcState.Process;

PKTRAP_FRAME OldTrapFrame;

PLIST_ENTRY ApcListEntry;

PKAPC Apc;

KLOCK_QUEUE_HANDLE ApcLock;

PKKERNEL_ROUTINE KernelRoutine;

PVOID NormalContext;

PKNORMAL_ROUTINE NormalRoutine;

PVOID SystemArgument1;

PVOID SystemArgument2;

ASSERT_IRQL_EQUAL(APC_LEVEL);

/* Save the old trap frame and set current one */

OldTrapFrame = Thread->TrapFrame;

Thread->TrapFrame = TrapFrame;

/* Clear Kernel APC Pending */

Thread->ApcState.KernelApcPending = FALSE;

/* Check if Special APCs are disabled */

if (Thread->SpecialApcDisable) goto Quickie;    // 總開關,如果SpecialApcDisable那麼整個執行緒的APC都不會被執行

/* Do the Kernel APCs first */

while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))

{

/* Lock the APC Queue */

KiAcquireApcLockAtApcLevel(Thread, &ApcLock);

/* Check if the list became empty now */

if (IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))

{

/* It is, release the lock and break out */

KiReleaseApcLock(&ApcLock);

break;

}

/* Kernel APC is not pending anymore */

Thread->ApcState.KernelApcPending = FALSE;

/* Get the next Entry */

ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;

Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);

/* Save Parameters so that it's safe to free the Object in the Kernel Routine*/

NormalRoutine = Apc->NormalRoutine;

KernelRoutine = Apc->KernelRoutine;

NormalContext = Apc->NormalContext;

SystemArgument1 = Apc->SystemArgument1;

SystemArgument2 = Apc->SystemArgument2;

/* Special APC */

if (!NormalRoutine)

{

/* Remove the APC from the list */

RemoveEntryList(ApcListEntry);

Apc->Inserted = FALSE;

/* Release the APC lock */

KiReleaseApcLock(&ApcLock);

/* Call the Special APC */

KernelRoutine(Apc,

&NormalRoutine,

&NormalContext,

&SystemArgument1,

&SystemArgument2);

/* Make sure it returned correctly */

if (KeGetCurrentIrql() != ApcLock.OldIrql)

{

KeBugCheckEx(IRQL_UNEXPECTED_VALUE,

(KeGetCurrentIrql() << 16) |

(ApcLock.OldIrql << 8),

(ULONG_PTR)KernelRoutine,

(ULONG_PTR)Apc,

(ULONG_PTR)NormalRoutine);

}

}

else

{

/* Normal Kernel APC, make sure it's safe to deliver */

if ((Thread->ApcState.KernelApcInProgress) ||

(Thread->KernelApcDisable))  // 子開關,控制著Normal KernelApc的執行與否

{

/* Release lock and return */

KiReleaseApcLock(&ApcLock);

goto Quickie;

}

/* Dequeue the APC */

RemoveEntryList(ApcListEntry);

Apc->Inserted = FALSE;

/* Go back to APC_LEVEL */

KiReleaseApcLock(&ApcLock);

/* Call the Kernel APC */

KernelRoutine(Apc,

&NormalRoutine,

&NormalContext,

&SystemArgument1,

&SystemArgument2);

/* Make sure it returned correctly */

if (KeGetCurrentIrql() != ApcLock.OldIrql)

{

KeBugCheckEx(IRQL_UNEXPECTED_VALUE,

(KeGetCurrentIrql() << 16) |

(ApcLock.OldIrql << 8),

(ULONG_PTR)KernelRoutine,

(ULONG_PTR)Apc,

(ULONG_PTR)NormalRoutine);

}

/* Check if there still is a Normal Routine */

if (NormalRoutine)

{

/* At Passive Level, an APC can be prempted by a Special APC */

Thread->ApcState.KernelApcInProgress = TRUE;

KeLowerIrql(PASSIVE_LEVEL);

/* Call and Raise IRQL back to APC_LEVEL */

NormalRoutine(NormalContext, SystemArgument1, SystemArgument2);

KeRaiseIrql(APC_LEVEL, &ApcLock.OldIrql);

}

/* Set Kernel APC in progress to false and loop again */

Thread->ApcState.KernelApcInProgress = FALSE;

}

}

/* Now we do the User APCs */

if ((DeliveryMode == UserMode) &&

!(IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])) &&

(Thread->ApcState.UserApcPending))

{

/* Lock the APC Queue */

KiAcquireApcLockAtApcLevel(Thread, &ApcLock);

/* It's not pending anymore */

Thread->ApcState.UserApcPending = FALSE;

/* Check if the list became empty now */

if (IsListEmpty(&Thread->ApcState.ApcListHead[UserMode]))

{

/* It is, release the lock and break out */

KiReleaseApcLock(&ApcLock);

goto Quickie;

}

/* Get the actual APC object */

ApcListEntry = Thread->ApcState.ApcListHead[UserMode].Flink;

Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);

/* Save Parameters so that it's safe to free the Object in the Kernel Routine*/

NormalRoutine = Apc->NormalRoutine;

KernelRoutine = Apc->KernelRoutine;

NormalContext = Apc->NormalContext;

SystemArgument1 = Apc->SystemArgument1;

SystemArgument2 = Apc->SystemArgument2;

/* Remove the APC from Queue, and release the lock */

RemoveEntryList(ApcListEntry);

Apc->Inserted = FALSE;

KiReleaseApcLock(&ApcLock);

/* Call the kernel routine */

KernelRoutine(Apc,

&NormalRoutine,

&NormalContext,

&SystemArgument1,

&SystemArgument2);

/* Check if there's no normal routine */

if (!NormalRoutine)

{

/* Check if more User APCs are Pending */

KeTestAlertThread(UserMode);

}

else

{

/* Set up the Trap Frame and prepare for Execution in NTDLL.DLL */

KiInitializeUserApc(ExceptionFrame,

TrapFrame,

NormalRoutine,

NormalContext,

SystemArgument1,

SystemArgument2);

}

}

Quickie:

/* Make sure we're still in the same process */

if (Process != Thread->ApcState.Process)

{

/* Erm, we got attached or something! BAD! */

KeBugCheckEx(INVALID_PROCESS_ATTACH_ATTEMPT,

(ULONG_PTR)Process,

(ULONG_PTR)Thread->ApcState.Process,

Thread->ApcStateIndex,

KeGetCurrentPrcb()->DpcRoutineActive);

}

/* Restore the trap frame */

Thread->TrapFrame = OldTrapFrame;

}


An asynchronous procedure call (APC) is a function that executes asynchronously. APCs are similar to deferred procedure calls (DPCs), but unlike DPCs, APCs execute within the context of a particular thread. Drivers (other than file systems and file-system filter drivers) do not use APCs directly, but other parts of the operating system do, so you need to be aware of how APCs work.

The Windows operating system uses three kinds of APCs:


1. User APCs run strictly in user mode and only when the current thread is in an alertable wait state. The operating system uses user APCs to implement mechanisms such as overlapped I/O and the QueueUserApc Win32 routine.


2. Normal kernel APCs run in kernel mode at IRQL = PASSIVE_LEVEL. A normal kernel APC preempts all user-mode code, including user APCs. Normal kernel APCs are generally used by file systems and file-system filter drivers.


3. Special kernel APCs run in kernel mode at IRQL = APC_LEVEL. A special kernel APC preempts user-mode code and kernel-mode code that executes at IRQL = PASSIVE_LEVEL, including both user APCs and normal kernel APCs. The operating system uses special kernel APCs to handle operations such as I/O request completion.


其實總結一下很簡單,一個使用者模式的Apc,一個核心模式的Apc(分為NormalRoutine為NULL的SpecialKernelApc和不為NULL的NormalKernelApc)

區別在於, SpecialKernelApc只執行KernelRoutine, IRQL為APC_LEVEL,而 NormalKernelApc不僅僅執行 KernelRoutine還執行 NormalRoutine,在PASSIVE_LEVEL下執行NormalRoutine。


但是I/O API也沒有說明他內部用的是哪個型別的Kernel APC,要穩一點就判斷總開關 SpecialApcDisable。不過一般MSDN都會說明。


3. KeAreApcsDisabled/KeAreAllApcsDisabled



BOOLEAN

NTAPI

KeAreApcsDisabled(VOID)

{

/* Return the Kernel APC State */

return KeGetCurrentThread()->CombinedApcDisable ? TRUE : FALSE;

}

BOOLEAN

NTAPI

KeAreAllApcsDisabled(VOID)

{

/* Return the Special APC State */

return ((KeGetCurrentThread()->SpecialApcDisable) ||

(KeGetCurrentIrql() >= APC_LEVEL)) ? TRUE : FALSE;

}

typedef struct _KTHREAD

{

......

union

{

struct

{

SHORT KernelApcDisable;

SHORT SpecialApcDisable;

};

ULONG CombinedApcDisable;

};

......

};


主要看這兩個API有什麼區別,可能很多人看不出來什麼區別。。。

我也理解了很久, KeAreApcsDisabled是說只要當前在核心臨界區內就是Disable狀態,這個可以是子開關KernelApcDisable或者是總開關SpecialApcDisable至少一個有值,要是 SpecialApcDisable則就是所有Apc都是無效狀態(與KeAreAllApcsDisabled判斷相同),要是 KernelApcDisable就是Normal KernelApc失效;要是用的 KeEnterCriticalRegion就只能用這個函式檢查,一般用這個就可以了,當然最好if ( KeAreApcsDisabled() || __readcr8() == APC_LEVEL )


而 KeAreAllApcsDisabled則是真正意義上的所以APC都無效,但是對於 KeEnterCriticalRegion的臨界區這個API是無法判斷的。


4. 解決辦法

     當出現無法呼叫I/O API的時候,建議使用勞務執行緒,這個執行緒的執行環境還是比較穩定的,而且就算是做同步響應也不會耗時很久。例如:


typedef struct tag_FyWorkQueueItem

{

WORK_QUEUE_ITEM WorkQueueItem;

PVOID  lpParameter1;

PVOID  lpParameter2;

PVOID  lpParameter3;

KEVENT CompleteEvent;

BOOL   bStatus;

} FyWorkQueueItem, *PFyWorkQueueItem;

PUNICODE_STRING QueryProcessObjectName(IN HANDLE ProcessId)

{

NTSTATUS Status = STATUS_SUCCESS;

PEPROCESS EProcess = NULL;

HANDLE hProcess = NULL;

ULONG ulRealSize = 0;

PUNICODE_STRING lpuniImageFileName = NULL;

BOOL bSuccess = FALSE;

if (KeGetCurrentIrql() <= APC_LEVEL)

{

Status = PsLookupProcessByProcessId(ProcessId, &EProcess);

if (NT_SUCCESS(Status) && EProcess)

{

Status = ObOpenObjectByPointer((PVOID)EProcess, OBJ_KERNEL_HANDLE, NULL, 

PROCESS_ALL_ACCESS, NULL, KernelMode, &hProcess);

if (NT_SUCCESS(Status))

{

Status = ZwQueryInformationProcess(hProcess, ProcessImageFileName, NULL, 0, &ulRealSize);

if (Status == STATUS_INFO_LENGTH_MISMATCH)

{

lpuniImageFileName = (PUNICODE_STRING)ExAllocatePoolWithTag(NonPagedPool, 

ulRealSize + sizeof(UNICODE_STRING), 'hiti');

if (lpuniImageFileName)

{

memset(lpuniImageFileName, 0, ulRealSize + sizeof(UNICODE_STRING));

Status = ZwQueryInformationProcess(hProcess, ProcessImageFileName, 

lpuniImageFileName, ulRealSize + sizeof(UNICODE_STRING), &ulRealSize);

if (NT_SUCCESS(Status))

{

bSuccess = TRUE;

}

}

}

ZwClose(hProcess);

}

ObDereferenceObject(EProcess);

}

}

if (!bSuccess)

{

ExFreePool(lpuniImageFileName);

lpuniImageFileName = NULL;

}

return lpuniImageFileName;

}

BOOL GetProcessImageFileName(

IN HANDLE ProcessId, 

OUT WCHAR* lpwzImageFileName, 

IN ULONG uMaxSize)

{

NTSTATUS                   Status = STATUS_SUCCESS;

HANDLE                     FileHandle = NULL;

IO_STATUS_BLOCK            IoStatusBlock = { 0 };

PUNICODE_STRING            lpuniProcessObjectName = NULL;

OBJECT_ATTRIBUTES          oa = { 0 };

PFILE_OBJECT               FileObject = NULL;

POBJECT_NAME_INFORMATION   ObjectNameInformation = NULL;

BOOL                       bStatus = FALSE;

if (KeGetCurrentIrql() > PASSIVE_LEVEL) {

return FALSE;

}

lpuniProcessObjectName = QueryProcessObjectName(ProcessId);

if (!lpuniProcessObjectName) {

return FALSE;

}

InitializeObjectAttributes(&oa, lpuniProcessObjectName, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);

Status = IoCreateFile(

&FileHandle,

FILE_READ_ATTRIBUTES,

&oa,

&IoStatusBlock,

NULL,

FILE_ATTRIBUTE_NORMAL,

FILE_SHARE_READ | FILE_SHARE_WRITE,

FILE_OPEN,

FILE_NON_DIRECTORY_FILE,

NULL,

0,

CreateFileTypeNone,

NULL,

IO_NO_PARAMETER_CHECKING);

if (!NT_SUCCESS(Status))

{

ExFreePool(lpuniProcessObjectName);

return FALSE;

}

Status = ObReferenceObjectByHandle(FileHandle, FILE_ANY_ACCESS, *IoFileObjectType, 

KernelMode, (PVOID*)&FileObject, NULL);

if (NT_SUCCESS(Status) && FileObject)

{

Status = IoQueryFileDosDeviceName(FileObject, &ObjectNameInformation);

if (NT_SUCCESS(Status))

{

if (ObjectNameInformation)

{

if (ObjectNameInformation->Name.Length <= sizeof(WCHAR) * uMaxSize)

{

memset(lpwzImageFileName, 0, 2 * uMaxSize);

memcpy(lpwzImageFileName, ObjectNameInformation->Name.Buffer, 

ObjectNameInformation->Name.Length);

bStatus = TRUE;

}

ExFreePool(ObjectNameInformation);

ObjectNameInformation = NULL;

}

}

ObDereferenceObject(FileObject);

}

ObCloseHandle(FileHandle, KernelMode);

FileHandle = NULL;

ExFreePool(lpuniProcessObjectName);

return bStatus;

}

VOID QueryProcessFileNameWorkItem(IN PFyWorkQueueItem lpFyWorkQueueItem)

{

lpFyWorkQueueItem->bStatus = GetProcessImageFileName(

(HANDLE)lpFyWorkQueueItem->lpParameter1,

(WCHAR*)lpFyWorkQueueItem->lpParameter2,

(ULONG)lpFyWorkQueueItem->lpParameter3);

KeSetEvent(&lpFyWorkQueueItem->CompleteEvent, IO_NO_INCREMENT, FALSE);

}

BOOL GetProcessImageFileNameSafeIrql(

IN HANDLE ProcessId, 

OUT WCHAR* lpwzImageFileName, 

IN ULONG uMaxSize)

{

BOOL            bStatus;

FyWorkQueueItem WorkItem;

if (KeGetCurrentIrql() <= APC_LEVEL)

{

if (KeAreApcsDisabled() || KeGetCurrentIrql() == APC_LEVEL)

{

memset(&WorkItem, 0, sizeof(WorkItem));

KeInitializeEvent(&WorkItem.CompleteEvent, NotificationEvent, FALSE);

WorkItem.bStatus = FALSE;

WorkItem.WorkQueueItem.List.Flink = NULL;

WorkItem.WorkQueueItem.WorkerRoutine = (PWORKER_THREAD_ROUTINE)QueryProcessFileNameWorkItem;

WorkItem.lpParameter1 = (PVOID)ProcessId;

WorkItem.lpParameter2 = (PVOID)lpwzImageFileName;

WorkItem.lpParameter3 = (PVOID)uMaxSize;

WorkItem.WorkQueueItem.Parameter = &WorkItem;

ExQueueWorkItem(&WorkItem.WorkQueueItem, DelayedWorkQueue);

KeWaitForSingleObject(&WorkItem.CompleteEvent, Executive, KernelMode, FALSE, NULL);

bStatus = WorkItem.bStatus;

}

else

{

bStatus = GetProcessImageFileName(ProcessId, lpwzImageFileName, uMaxSize);

}

}

else

{

bStatus = FALSE;

}

return bStatus;

}



本文由看雪論壇 FaEry 原創

轉載請註明來自看雪社群


相關文章