- 示例工程
【免费下载链接】Windows-driver-samples
This repo contains driver samples prepared for use with Microsoft Visual Studio and the Windows Driver Kit (WDK). It contains both Universal Windows Driver and desktop-only driver samples.
Delete 是一个以"文件删除/流删除检测"为核心目标的文件系统微过滤驱动(Minifilter)示例,位于本仓库 filesys/miniFilter/delete 目录。它不拦截删除,而是在 IRP_MJ_CREATE、IRP_MJ_SET_INFORMATION 与 IRP_MJ_CLEANUP 的回调中标记"删除候选",并在操作完成后验证删除是否真正发生,最终以内核调试输出(DbgPrint)的形式上报。阅读本文后,你将掌握:文件删除在 Windows 上不可提前预测的根本原因、两种删除路径(FILE_DELETE_ON_CLOSE 与 FileDispositionInformation/FileDispositionInformationEx)的检测原理、并发 SetDisposition 竞态的识别方法、整文件删除与单流删除的区分技巧,以及事务(TxF)环境下删除上报的提交/回滚处理。
概述:一个只做"事后确认"的删除检测过滤器
该示例在 README 中定义为 "Demonstrates how to detect deletions of files or streams. Deletions are reported as debug output."(README.md),即:
- 检测对象:文件(file)与流(stream,含命名数据流 ADS)的删除;
- 输出方式:删除事件通过
DbgPrintEx(DPFLTR_FLTMGR_ID、DPFLTR_ERROR_LEVEL)写入内核调试输出,参见 delete.c 中的DF_PRINT宏; - 运行形态:构建出的
delete.sys是一个完整的文件系统微过滤驱动,全部代码集中在一个 delete.c(约 3282 行)中。
整个驱动只有两个"事件入口"和一个"验证出口":
| 阶段 | 关注对象 | 作用 |
|---|---|---|
| IRP_MJ_CREATE | FILE_DELETE_ON_CLOSE标志 | 标记"关闭即删"的删除候选 |
| IRP_MJ_SET_INFORMATION | FileDispositionInformation/FileDispositionInformationEx | 记录删除处置(delete disposition)状态变化 |
| IRP_MJ_CLEANUP(post) | 删除候选的最终状态 | 验证文件/流是否真的被删除并上报 |
这三个入口在 Callbacks 操作注册表 中登记:
CONST FLT_OPERATION_REGISTRATION Callbacks[] = { { IRP_MJ_CREATE, 0, DfPreCreateCallback, DfPostCreateCallback }, { IRP_MJ_SET_INFORMATION, FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO, DfPreSetInfoCallback, DfPostSetInfoCallback }, { IRP_MJ_CLEANUP, 0, DfPreCleanupCallback, DfPostCleanupCallback }, { IRP_MJ_OPERATION_END } };注意IRP_MJ_SET_INFORMATION使用了FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO,跳过分页 I/O,避免在内存不足的换页路径上增加额外负担。
核心难点:为什么删除"无法提前预知"
README 特别强调了一个设计约束(README.md 中的 NOTE):
由于 Windows 操作系统删除文件的机制,minifilter 无法提前预知某个文件或流将被删除;它只能检测"可能导致删除的操作",然后在该操作完成后判断删除是否真的发生了。
原因在于 Windows 的删除模型是延迟删除:删除并不是在"发出删除请求"时立即完成,而是先把文件/流置为"删除待定"(delete-pending),待最后一个句柄关闭后才真正从卷上移除。因此:
- 一次
FILE_DELETE_ON_CLOSE创建可能最终没有删除(例如后续又通过FileDispositionInformationEx清除了删除标志); - 一次
SetFileDisposition也可能因为后续句柄仍打开而迟迟不生效; - 同一流上的多个删除处置操作还可能互相覆盖(race)。
所以本示例的全部逻辑都遵循同一思路:先标记候选,再在 post-cleanup 时做最终验证,验证手段是查询FileStandardInformation(返回STATUS_FILE_DELETED即证明已删除),必要时再通过文件 ID 打开或查询对象 ID 来区分"整文件删除"与"仅流删除"。
三种上下文:实例、流与事务
驱动通过 FltMgr 上下文(Context)机制保存跨操作的状态,注册于 Contexts 注册表:
- FLT_INSTANCE_CONTEXT(
DF_INSTANCE_CONTEXT):缓存卷 GUID 名称VolumeGuidName,避免反复查询; - FLT_STREAM_CONTEXT(
DF_STREAM_CONTEXT):这是删除检测的核心载体,字段包括:NameInfo:最后一次 pre-cleanup 时取得的打开名(FLT_FILE_NAME_INFORMATION),用于上报;FileId:文件 ID(DF_FILE_REFERENCE,兼容 NTFS 64 位与 ReFS 128 位);NumOps:正在进行的删除处置操作计数(竞态检测用);IsNotified:是否已上报过删除(防止重复上报);SetDisp/DeleteOnClose:删除处置与关闭即删状态。
- FLT_TRANSACTION_CONTEXT(
DF_TRANSACTION_CONTEXT):维护事务内待上报的删除通知链表DeleteNotifyList与保护它的ERESOURCE。
一个值得学习的细节:DF_TRANSACTION_CONTEXT中的ERESOURCE被设计成指针 + 单独分配,而不是直接内嵌在结构里(delete.c 的注释说明了原因)。因为ERESOURCE必须从NonPagedPool分配,若直接内嵌,整个事务上下文都要提升为非分页内存;用指针方式则主体仍可放在PagedPool(DF_CONTEXT_POOL_TYPE = PagedPool),仅ERESOURCE本身走非分页池,从而节省非分页内存。
上下文的获取/分配/附加统一封装在DfGetOrSetContext(delete.c)中,它实现了"无则取、有则建、建后设、竞态则复用他人已设置的那份"的完整语义;当上下文类型是FLT_TRANSACTION_CONTEXT时还会额外调用FltEnlistInTransaction加入事务,注册DF_NOTIFICATION_MASK(即TRANSACTION_NOTIFY_COMMIT_FINALIZE | TRANSACTION_NOTIFY_ROLLBACK,见 delete.c)。
检测路径一:FILE_DELETE_ON_CLOSE(创建即删)
应用以FILE_DELETE_ON_CLOSE打开文件时,文件会在最后一个句柄关闭时被删除。DfPreCreateCallback(delete.c)检查创建选项:
if (FlagOn( Data->Iopb->Parameters.Create.Options, FILE_DELETE_ON_CLOSE )) { status = DfAllocateContext( FLT_STREAM_CONTEXT, &streamContext ); if (NT_SUCCESS( status )) { *CompletionContext = (PVOID)streamContext; return FLT_PREOP_SYNCHRONIZE; } ... } *CompletionContext = NULL; return FLT_PREOP_SUCCESS_NO_CALLBACK;关键点:
- 只有命中
FILE_DELETE_ON_CLOSE才预分配流上下文并通过CompletionContext传给 post 回调; - 返回
FLT_PREOP_SYNCHRONIZE强制同步 post 回调,保证创建结果立即可见; - 在
DfPostCreateCallback(delete.c)中,只有创建真正成功且不是重解析点(STATUS_REPARSE)时才把上下文附加到流,并置streamContext->DeleteOnClose = TRUE。代码中的FLTFL_POST_OPERATION_DRAINING断言则用于确保没有处于"排空"(unload 中的残余操作)状态。
检测路径二:FileDispositionInformation / FileDispositionInformationEx
另一种删除方式是IRP_MJ_SET_INFORMATION设置删除处置。DfPreSetInfoCallback(delete.c)只对FileDispositionInformation与FileDispositionInformationEx两类信息类感兴趣,并在此完成竞态检测:
race = (InterlockedIncrement( &streamContext->NumOps ) > 1); if (!race) { // 唯一在途操作,结果是确定的,做 postop *CompletionContext = (PVOID)streamContext; return FLT_PREOP_SYNCHRONIZE; } else { // 存在并发操作,删除处置的最终状态不可知 FltReleaseContext( streamContext ); } // FALL_THROUGH return FLT_PREOP_SUCCESS_NO_CALLBACK;这里的逻辑非常精巧:NumOps记录在途的删除处置修改数。当检测到并发(race)时,post 回调不会被调用,NumOps也就永远不会被递减,于是该值保持 >= 2,永久地把这个流标记为"必须检查删除"的候选——因为既然无法确定处置状态的最终结果,就宁可多做一次删除验证。而在非竞态情况下,DfPostSetInfoCallback(delete.c)会在操作成功后把结果写入上下文:
FileDispositionInformationEx:若设置了FILE_DISPOSITION_ON_CLOSE,则按FILE_DISPOSITION_DELETE更新DeleteOnClose;否则更新SetDisp;FileDispositionInformation:直接用FILE_DISPOSITION_INFORMATION.DeleteFile更新SetDisp。
最后InterlockedDecrement( &streamContext->NumOps )并释放上下文引用,保持计数与引用平衡。
验证出口:post-cleanup 才是真相时刻
无论文件是通过哪种方式进入"删除候选"状态,真正的判定都在DfPostCleanupCallback(delete.c)。前置的DfPreCleanupCallback(delete.c)只做一件事:为带流上下文的文件抓取名称信息(DfGetFileNameInformation,内部使用FltGetFileNameInformation(FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_DEFAULT)+FltParseFileNameInformation),以便上报时有名字可用。
post-cleanup 中的候选判定条件(源码注释原话归纳):
NumOps > 0—— 存在过竞态,处置状态未知,保守起见必须检查;SetDisp == TRUE—— 已设置删除处置;DeleteOnClose == TRUE—— 曾以FILE_DELETE_ON_CLOSE打开(注意FileDispositionInformationEx可清除该标志)。
三者满足其一,且IsNotified == 0(尚未上报过)时,执行验证:
status = FltQueryInformationFile( Data->Iopb->TargetInstance, Data->Iopb->TargetFileObject, &fileInfo, sizeof(fileInfo), FileStandardInformation, NULL ); if (STATUS_FILE_DELETED == status) { status = DfProcessDelete( Data, FltObjects, streamContext ); ... }FileStandardInformation查询返回STATUS_FILE_DELETED即证明流已被删除,随后进入DfProcessDelete做最终定性。
整文件删除 vs 单流删除的区分
这是本示例最有技术含量的一环。场景是:当最后一个句柄恰好是某个"删除待定的命名数据流(ADS)"的句柄时,关闭它会连带整个文件一起消失——此时应上报"整文件删除",而不是"流删除"。
DfProcessDelete(delete.c)先判断是否处于事务中(FltObjects->Transaction != NULL),然后调用DfIsFileDeleted(delete.c)来区分。该函数按文件系统类型与事务状态选择验证手段:
| 场景 | 手段 | 判定 |
|---|---|---|
| 非事务 + NTFS | FSCTL_GET_OBJECT_ID(FltFsControlFile) | STATUS_OBJECTID_NOT_FOUND→ 文件仍存在(只是没有对象 ID);其余错误码透传(STATUS_FILE_DELETED表示已删除) |
| 事务中 或 ReFS | 按文件 ID 打开(DfDetectDeleteByFileId) | STATUS_INVALID_PARAMETER→ 文件已删除(映射为STATUS_FILE_DELETED);STATUS_DELETE_PENDING→ 文件仍在但删除待定(映射为成功) |
之所以分两条路径,代码注释给出了两个关键原因:
FSCTL_GET_OBJECT_ID在事务内删除时不会返回STATUS_FILE_DELETED,所以事务场景必须改用打开文件 ID 的方式;- ReFS 不支持对象 ID,因此 ReFS 卷上始终走按 ID 打开。
按文件 ID 打开的核心实现在DfDetectDeleteByFileId(delete.c):
status = DfBuildFileIdString( Data, FltObjects, StreamContext, &fileIdString ); ... IoInitializeDriverCreateContext( &driverCreateContext ); driverCreateContext.TxnParameters = IoGetTransactionParameterBlock( Data->Iopb->TargetFileObject ); status = FltCreateFileEx2( gFilterHandle, Data->Iopb->TargetInstance, &handle, NULL, FILE_READ_ATTRIBUTES, &objectAttributes, &ioStatus, (PLARGE_INTEGER) NULL, 0L, FILE_SHARE_VALID_FLAGS, FILE_OPEN, FILE_OPEN_REPARSE_POINT | FILE_OPEN_BY_FILE_ID, (PVOID) NULL, 0L, IO_IGNORE_SHARE_ACCESS_CHECK, &driverCreateContext ); if (NT_SUCCESS( status )) { status = FltClose( handle ); ... }关键点:
- 打开路径由卷 GUID 名 + 反斜杠 + 文件 ID 组成(
DfBuildFileIdString,delete.c); - 必须用
FILE_OPEN_BY_FILE_ID按 ID 打开; - 必须从当前文件对象继承事务参数块(
IoGetTransactionParameterBlock),以保证打开操作发生在同一事务内; - 文件已删除时,
DfBuildFileIdString内部查询文件 ID 的FltQueryInformationFile会直接返回STATUS_FILE_DELETED,短路整个打开过程(见 delete.c 注释);而按 ID 打开一个不存在的文件则返回STATUS_INVALID_PARAMETER。
NTFS 与 ReFS 的文件 ID 兼容
DF_FILE_REFERENCE联合体(delete.c)专门处理两种文件 ID:NTFS 的 64 位 ID 与 ReFS 的 128 位 ID。DfGetFileId(delete.c)先查询FileInternalInformation;若返回FILE_INVALID_FILE_ID(说明该 ID 必须用 128 位表示),再改用FileIdInformation查询。代码还通过KeMemoryBarrier()保证"写入 128 位 ID"与"置位FileIdSet"的次序(因为编译器不支持原生 128 位值),并用DfSizeofFileId宏动态判断 ID 长度,确保 ReFS 按 ID 打开时 64/128 位皆可用。
事务(TxF)环境下的延迟上报
文件在事务内被删除时,提交前文件仍是"活着"的,只有提交才真正生效,回滚则相当于没删。因此驱动不能在 post-cleanup 立刻上报,而要挂起等待事务终局。
机制如下:
DfProcessDelete检测到处于事务中时,通过DfGetOrSetContext取得事务上下文,并(经由FltEnlistInTransaction)登记了TRANSACTION_NOTIFY_COMMIT_FINALIZE | TRANSACTION_NOTIFY_ROLLBACK通知;DfNotifyDelete(delete.c)此时不直接打印"deleted",而是打印"deleted in a transaction!",并调用DfAddTransDeleteNotify(delete.c)把一条DF_DELETE_NOTIFY(内含StreamContext引用与FileDelete标志)挂入事务上下文的DeleteNotifyList,该链表由独立分配的ERESOURCE保护;DfTransactionNotificationCallback(delete.c)在事务提交或回滚时被 FltMgr 回调:提交则打印 "deleted due to a transaction commit!";回滚则先把IsNotified递减(让"删除候选"回到未上报状态),再打印 "saved due to a transaction rollback!"(见DfNotifyDeleteOnTransactionEnd,delete.c);- 事务上下文清理回调(
DfTransactionContextCleanupCallback,delete.c)负责清空链表、释放每个DF_DELETE_NOTIFY及其StreamContext引用、销毁ERESOURCE。
这一整套"挂起—提交/回滚时结算"的设计,是事务型文件删除上报的正确范式,也解释了为什么事务上下文要同时维护列表与锁。
只挂接可写 NTFS/ReFS 卷
DfInstanceSetup(delete.c)决定了过滤器挂接范围:
- 先调用
FltIsVolumeWritable判断卷是否可写——挂接到只读卷没有意义,因为只读卷上不可能删除文件; - 再按
VolumeFilesystemType限定为FLT_FSTYPE_NTFS与FLT_FSTYPE_REFS; - 其他情况一律返回
STATUS_FLT_DO_NOT_ATTACH。
INF 安装配置解读
delete.inf 展示了典型的 UWD minifilter 安装配置:
[Version] Signature = "$Windows NT$" Class = "ActivityMonitor" ;This is determined by the work this filter driver does ClassGuid = {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2} ;This value is determined by the Class Provider = %ProviderString% DriverVer = 06/16/2007,1.0.0.1 CatalogFile = delete.cat PnpLockdown = 1- 设备类:
ActivityMonitor(活动监视类),与"观察但不拦截"的定位一致; - 服务配置(
MiniFilter.Service):ServiceType = 2(SERVICE_FILE_SYSTEM_DRIVER)、StartType = 3(SERVICE_DEMAND_START 按需启动)、ErrorControl = 1、Dependencies = "FltMgr"、LoadOrderGroup = "FSFilter Activity Monitor"; - 注册表配置(
MiniFilter.AddRegistry):SupportedFeatures = 0x3,并在Parameters\Instances下注册默认实例,实例名"delete Instance"、海拔(Altitude)= 370150、Flags = 0x0(允许所有挂接); - 同时提供面向新版 Windows 的
DefaultInstall.NT$ARCH$.10.0...25952通用安装节,以及面向旧系统的 downlevel 安装/卸载节(含LegacyUninstall与DelService0x200 停止后删除)。
海拔 370150 落在 INF 中声明的FSFilter Activity Monitor加载顺序组区间内,且与LoadOrderGroup声明保持一致,确保过滤器在文件系统栈中的加载次序正确。
工程配置与构建
- 工程文件 delete.vcxproj 声明
DriverTargetPlatform = Universal、PlatformToolset = WindowsKernelModeDriver10.0、ConfigurationType = Driver,支持 Debug/Release × x64/ARM64 四套配置,链接fltMgr.lib,启用POOL_NX_OPTIN=1、WarningLevel = Level4且TreatWarningAsError = true,驱动签名摘要算法为 sha256; - delete.sln 与 delete.rc(VFT_DRV / VFT2_DRV_SYSTEM,文件描述 "Delete Notification Filter Driver")配套齐全;
- README 明确说明该示例是Universal Windows Driver(UWD),仅使用 OneCoreUAP 包含的 API/DDI,这意味着它可被用于需要通用驱动形态的 Windows 版本与架构组合;
- 整个仓库的构建流程可参考根目录的 Building-Locally.md 与 Build-Samples.ps1,配合 Visual Studio + WDK 完成编译与签名。
调试输出与观察方法
驱动的所有删除事件通过DF_DBG_PRINT(DFDBG_TRACE_ERRORS, ...)宏输出(delete.c)。gTraceFlags默认值为DFDBG_TRACE_ERRORS,三个可用位为:
| 标志 | 值 | 用途 |
|---|---|---|
DFDBG_TRACE_ERRORS | 0x00000001 | 错误与删除事件上报(默认开启) |
DFDBG_TRACE_ROUTINES | 0x00000002 | 例程进入/离开轨迹 |
DFDBG_TRACE_OPERATION_STATUS | 0x00000004 | 操作状态跟踪 |
输出示例(源码实际格式)包括:
delete!DfPostCleanupCallback: A file "%wZ" (%p) has been deleted!delete!DfPostCleanupCallback: An alternate data stream "%wZ" (%p) has been deleted!- 事务场景:
A file "%wZ" (%p) has been deleted in a transaction!,随后在提交/回滚时打印deleted due to a transaction commit!或saved due to a transaction rollback!
上报同时打印文件打开名与流上下文指针(%p),后者可用于在重命名等边界情况下辅助区分对象(参见DfPreCleanupCallback注释)。若在 WinDbg/内核调试会话中加载该驱动并执行删除操作,即可在调试输出流中观察到上述信息。
源码结构速查
| 函数(均在 delete.c) | 职责 |
|---|---|
DriverEntry/DfUnload | FltMgr 注册、开始过滤 / 反注册 |
DfInstanceSetup系列 | 实例挂接策略与拆解回调 |
DfGetOrSetContext/DfAllocateContext/DfSetContext/DfGetContext | 三类上下文的生命周期管理 |
DfGetFileNameInformation/DfGetFileId/DfBuildFileIdString/DfGetVolumeGuidName | 名称、文件 ID、卷 GUID 获取与缓存 |
DfDetectDeleteByFileId/DfIsFileDeleted | 按 ID 打开探测 / 整文件 vs 流删除判定 |
DfProcessDelete/DfNotifyDelete/DfAddTransDeleteNotify/DfNotifyDeleteOnTransactionEnd | 删除定性、上报与事务挂起结算 |
DfPre/PostCreateCallback | FILE_DELETE_ON_CLOSE 候选标记 |
DfPre/PostSetInfoCallback | 删除处置跟踪与竞态检测 |
DfPre/PostCleanupCallback | 名称采集与最终删除验证 |
DfTransactionNotificationCallback | 提交/回滚时的延迟上报 |
小结
Delete 示例用约三千行代码完整演示了文件系统微过滤驱动中"删除检测"这一经典难题的工业级解法:候选标记(CREATE 的FILE_DELETE_ON_CLOSE+ SET_INFORMATION 的处置信息)→ 竞态兜底(NumOps永不归零策略)→ 事后验证(FileStandardInformation/ 对象 ID / 按文件 ID 打开)→ 事务结算(commit/rollback 通知)。它同时兼顾了 NTFS 与 ReFS 的文件 ID 差异、整文件与流删除的语义区分,以及 UWD 兼容性要求,是学习IRP_MJ_CLEANUP语义、FltMgr 上下文与 TxF 事务通知的最佳起点之一。对于要在自己的驱动中实现"删除监控"(如审计、数据保护、同步备份触发)的开发者,可以直接以本示例为骨架,把DfNotifyDelete中的DbgPrint替换为实际业务上报逻辑。
- 示例工程
【免费下载链接】Windows-driver-samples
This repo contains driver samples prepared for use with Microsoft Visual Studio and the Windows Driver Kit (WDK). It contains both Universal Windows Driver and desktop-only driver samples.
相关推荐
Windows-driver-samples I2C驱动:SkeletonI2C示例深度解析
Windows driver samples I2C驱动:SkeletonI2C示例深度解析 概述 SkeletonI2C是Windows驱动示例项目中的一个关
示例工程性能优化指南:Encrypted Core Data缓存配置与数据库迁移技巧 🚀
性能优化指南:Encrypted Core Data缓存配置与数据库迁移技巧 🚀 Encrypted Core Data 是一个强大的iOS加密数据库解决方案
应用安全密码学Windows-driver-samples智慧博物馆:文物展示设备驱动开发
Windows driver samples智慧博物馆:文物展示设备驱动开发 传统博物馆展览常面临文物保护与观众体验的矛盾,Windows driver sam
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考