HidHide 驱动分析 - drivers 篇(二):同步与并发控制
一、并发场景分析
内核驱动在运行过程中面临多种并发挑战:
| 并发来源 | 说明 | 影响 |
|---|---|---|
| 多处理器系统 | 不同 CPU 核心可同时执行驱动代码 | 共享数据结构需要同步保护 |
| IRQL 提升 | 代码在高 IRQL 被中断,可能被重新调度 | 锁的选择需匹配 IRQL 级别 |
| 异步回调 | 进程/镜像回调在任意线程上下文触发 | 回调函数必须可重入 |
| 多线程应用 | 多个用户态线程同时访问控制设备 | IOCTL 处理需要序列化 |
HidHide 通过组合使用多种同步机制应对这些挑战。
二、WDFWAITLOCK:全局同步锁
2.1 锁的创建与类型
WdfWaitLockCreate(&wdfObjectAttributes,&s_criticalSectionLock);WdfWaitLock在 WDF 内部实现为快速互斥体(Fast Mutex),具有以下特性:
- 可以在 PASSIVE_LEVEL IRQL 获取和释放。
- 支持递归获取(同一线程可多次获取而不会死锁)。
- 获取锁时若被其他线程持有,当前线程进入等待状态(不会自旋)。
2.2 锁保护的关键数据
所有共享数据结构都在s_criticalSectionLock保护之下:
- BST(进程-路径映射树):插入、查找、删除、缓存刷新操作。
- 控制设备上下文:白名单集合、黑名单集合的原子替换。
- 引用计数:
numberOfDevicesCreated的增减。
2.3 加锁模式与粒度
HidHide 采用粗粒度锁策略,整个驱动只有一个全局锁。理由如下:
- 并发度低:BST 操作发生在进程创建/终止时,频率远低于 I/O 请求。
- 实现简单:避免复杂的多锁嵌套和死锁分析。
- 锁持有时间短:所有临界区操作(查找、比较、指针交换)都在微秒级完成。
加锁示例(HidHideProcessIdRegister):
WdfWaitLockAcquire(wdfWaitLock,NULL);node=BstLookup(s_ProcessIdToFullLoadImageNameMappingTree,processId);if(NULL!=node){WdfWaitLockRelease(wdfWaitLock);returnSTATUS_PROCESS_IN_JOB;}ntstatus=BstNewNode(processId,fullImageName,&node);ntstatus=BstInsert(&s_ProcessIdToFullLoadImageNameMappingTree,node);WdfWaitLockRelease(wdfWaitLock);锁在函数入口获取,在函数出口释放,最小化了锁持有时间。
三、IRQL 管理与锁兼容性
3.1 不同回调函数的 IRQL 级别
| 回调函数 | IRQL 级别 | 说明 |
|---|---|---|
DriverEntry | PASSIVE_LEVEL | 驱动加载阶段 |
EvtDriverDeviceAdd | PASSIVE_LEVEL | PnP 设备枚举 |
OnDeviceFileCreate | PASSIVE_LEVEL | 用户态文件创建请求 |
OnSystemLoadImage | PASSIVE_LEVEL | 镜像加载通知(APC 级别) |
OnSystemProcessChange | PASSIVE_LEVEL | 进程创建/终止通知 |
OnControlDeviceIoDeviceControl | DISPATCH_LEVEL | IOCTL 处理(可达 DISPATCH_LEVEL) |
所有回调都运行在 PASSIVE_LEVEL 或更低,因此WdfWaitLock(需要 PASSIVE_LEVEL)在所有上下文中都是安全的。
3.2 避免在提升 IRQL 时持有锁
OnControlDeviceIoDeviceControl虽然声明为_IRQL_requires_max_(DISPATCH_LEVEL),但在实际执行中,WDF 默认队列将请求从 DISPATCH_LEVEL 降级到 PASSIVE_LEVEL 处理。因此,持有WdfWaitLock是安全的。
驱动文档明确标注了 IRQL 要求:
_IRQL_requires_same__IRQL_requires_max_(PASSIVE_LEVEL)NTSTATUSHidHideProcessIdRegister(...);这提醒调用者必须在 PASSIVE_LEVEL 调用此函数。
四、WDF 自动同步机制
4.1 设备对象的同步范围
wdfObjectAttributes.SynchronizationScope=WdfSynchronizationScopeNone;HidHide 的过滤设备明确设置WdfSynchronizationScopeNone,即 WDF 框架不为该设备提供自动同步保护。决策依据是:
- 性能优先:自动同步会序列化所有请求,影响 I/O 吞吐量。
- 手动保护充分:
OnDeviceFileCreate只读取共享数据(白名单/黑名单),且读取操作使用原子指针交换,无需额外锁。
4.2 控制设备的独占模式
WdfDeviceInitSetExclusive(wdfDeviceInit,TRUE);控制设备设置为独占模式,确保同一时刻只有一个用户态进程可以打开\\.\HidHide。这避免了以下竞态:
- 两个配置工具同时修改白名单,导致数据不一致。
- 在配置更新过程中,另一个进程读取到半完成状态。
五、原子指针交换:无锁读取
5.1 双缓冲技术的核心
配置更新的核心操作是对集合指针的原子替换:
WdfWaitLockAcquire(s_criticalSectionLock,NULL);WdfObjectDelete(pControlDeviceContext->whitelistedFullImageNames);pControlDeviceContext->whitelistedFullImageNames=newCollection;WdfWaitLockRelease(s_criticalSectionLock);关键设计:指针赋值在 x64 上是原子操作。因此,当读取线程(OnDeviceFileCreate)访问whitelistedFullImageNames时,它总是看到完整的旧集合或完整的新集合,而不会看到中间状态。
5.2 读取端的无锁访问
// 在 OnDeviceFileCreate 中BOOLEANWhitelisted(HANDLE processId,BOOLEAN*cacheHit){pControlDeviceContext=ControlDeviceGetContext(s_wdfControlDevice);// 直接读取指针,无需加锁BOOLEAN result=HidHideProcessIdCheckFullImageNameAgainstWhitelist(s_criticalSectionLock,processId,pControlDeviceContext->whitelistedFullImageNames,cacheHit);returnGetInverse()?!result:result;}读取时未持有s_criticalSectionLock,但仍安全,因为:
- 指针赋值是原子的。
- 旧的集合对象在指针交换后不会被立即释放(通过
WdfObjectDelete延迟释放)。 WDFCOLLECTION对象在删除时引用计数为零才会真正释放。
这种设计实现了读操作无锁,对高频 I/O 路径影响极小。
六、进程回调的上下文隔离
6.1 系统线程上下文
OnSystemLoadImage和OnSystemProcessChange运行在系统进程上下文中,而非调用者进程上下文。这意味着:
- 不能使用
PsGetCurrentProcessId()获取调用者 PID,因为当前进程是 System。 - 回调参数明确提供了
HANDLE processId,必须使用此值。
6.2 安全的数据传递
BST 节点存储的fullImageName字符串在插入时被完整复制到非分页池中:
ntstatus=RtlStringCchCopyUnicodeStringEx(&temp->fullImageName[0],_countof(temp->fullImageName),fullImageName,...);这确保了在回调函数返回后,字符串数据依然有效,不受栈空间释放的影响。
七、自检与防御性编程
7.1 启动时 BST 自检
HidHideVerifyInternalConsistency在驱动启动时执行完整 BST 验证。这种Fail-Fast策略确保:
- 若 BST 算法实现存在缺陷,驱动在加载阶段即失败,而非在运行时触发蓝屏。
- 为后续的进程追踪功能提供正确性保证。
7.2 断言与验证
虽然代码中未直接使用ASSERT宏,但通过NT_SUCCESS检查和LOG_AND_RETURN_NTSTATUS实现了相似效果:
if(!NT_SUCCESS(ntstatus)){LOG_AND_RETURN_NTSTATUS(L"WdfDriverOpenParametersRegistryKey",ntstatus);}所有 API 调用的返回值都被检查,失败时记录详细错误信息并返回。