离奇事件薄之Irp死亡未遂

白天在外面耗了一整天,和各色朋友们讨论了一整天的新公司初创、经营、账务、税收等相关的问题,竟也蛮有兴致,也费了相当的口水,等回到家时间已很晚了,可精神却还是异常兴奋,只得爬起来找点事打发一下过剩的精力,第一直觉上便想到了前阵子曾遇到的一个稍许离奇的问题。

内容上可接承以前的一篇日志《离奇死亡事件薄之CoCreate篇》,于是继续取题《离奇事件薄》。细节如下:

驱动程序中要转向IRP_MJ_DIRECTORY_CONTROL/IRP_MN_QUERY_DIRECTORY,即Directory Enumeration操作,以方便处理此目录下所有的文件项。

由于会涉及文件I/O操作,不得不将操作移至系统线程中来做(至于为什么,请读者自己思考),所以在用户请求线程中不得不将原Irp请求的完成延后(pending),函数原型可简化成如下二个函数:

/* 用户线程环境:直接处理 IRP_MJ_DIR_CTRL / IRP_MN_QUERY_DIRECTORY */

NTSTATUS MyQueryDirectory(DevObj, Irp)

{

        ……
        MyQueueToSystemThread(DevObj, Irp);
        return STATUS_PENDING:

}

/* 系统线程,用以处理面向新路径(Dcb)的IRP_MN_QUEYR_DIRECTORY操作 */

NTSTATUS MySystemThreadHandler(DevObj, Irp)

{
        PIRP newIrp = IoAllocateIrp(newDevObj);
        ……
        IoCallDriver(newDevObj, newIrp);
        KeWaitForSingleObject(…):

        /* 处理返回的目录项 */
        ……
        /* 完成原用户的IRP请求 */
       IoCompleteRequest(irp, IO_NO_INCREMENT);

}

代码在逻辑上并没有问题,MySystemThreadHandler()正确获取目录项内容并实施对目标项的逐一检查,之后再完成用户所发的Irp请求,Irp的状态被系统(I/O Manager)修改为完成状态,此过程中并没有异常。但最早发出IRP_MJ_DIR_CTRL / IRP_MN_QUERY_DIRECTORY请求的用户线程却被永远地挂起来了,原Irp内容依然有效,虽然其状态已被标记为“完成”。

调试发现,由于系统线程优先级稍高,MySystemThreadHandler()完成Irp的操作竟然先于MyQueryDirectory()返回至I/O Manager。

执行顺序如下:
时间1:MyQueryDirectory() 添加一个新任务至系统线程队列
时间2:MyQueryDirectory() 被系统挂起了
时间3:系统线程执行MySystemThreadHandler()来处理时刻时间1所添加的任务
时间4:MySystemThreadHandler()执行完毕
时间5:原用户线程MyQueryDirectory()被唤醒,并返回STATUS_PENDING给I/O Manager

看到此序列,对于熟悉Irp操作规则的开发者来说,问题已经明了:显然是对此Irp的第二阶段用户线程环境相关的处理没有进行(Stage 2),也就是说,添加Kernel Apc的部分被跳过了。

一般Irp的完成要经过两个过程(可参见本文末所附文档链接):
Stage 1: IofCompleteRequest(): 完成与请求线程环境无关的操作
Stage 2: IopCompleteRequest(): 与Kernel Apc中执行,用以完成与请求线程相关的操作

之所以Stage 2的IopCompleteRequest会被跳过,有两方面的原因:

  1. Windows内核在处理IRP_MJ_DIR_CTRL/IRP_MN_QUERY_DIRECTORY请求时会默认加上标志位IRP_DEFER_IO_COMPLETION。据我所知,仅对DeviceIoControl操作不加此标志。当此标志位被设置的同时,如果Irp的Pending位没有设置,IoCompleteRequest()(此函数会调用IofCompleteRequest())则会直接返回,从面不在继续处理第二阶段(stage 2)。
  2. 原用户线程IoCallDriver()返回之后,由于返回值为STATUS_PENDING,I/O Manager将不在调用IopCompleteRequest()

所以二者同时跳过Stage 2,导致Irp的Stage 2处理不会再有机会执行,从而原用户线程也得不到被唤醒的机会了。

解决办法却是异常简单,在MyQueryDirectory()调用IoCallDriver()之前,或MySystemThreadHandler()调用IoCompleteRequest()之前将Irp设为PendingReturened非状态即可,此操作由系统支持函数IoMarkIrpPending()来做的。

最后,不妨再复习一下OSR介绍Irp操作的经典之作吧:《Secrets of the Universe Revealed! - How NT Handles I/O Completion

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注