惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
H
Hacker News: Front Page
N
News and Events Feed by Topic
Know Your Adversary
Know Your Adversary
Cisco Talos Blog
Cisco Talos Blog
SecWiki News
SecWiki News
C
Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
K
Kaspersky official blog
Forbes - Security
Forbes - Security
Webroot Blog
Webroot Blog
Schneier on Security
Schneier on Security
P
Privacy & Cybersecurity Law Blog
H
Heimdal Security Blog
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
V
Vulnerabilities – Threatpost
T
Tenable Blog
T
Tailwind CSS Blog
P
Privacy International News Feed
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
博客园 - Franky
Hacker News: Ask HN
Hacker News: Ask HN
Jina AI
Jina AI
C
Cybersecurity and Infrastructure Security Agency CISA
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
Vercel News
Vercel News
A
About on SuperTechFans
爱范儿
爱范儿
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
The Last Watchdog
The Last Watchdog
Engineering at Meta
Engineering at Meta
Spread Privacy
Spread Privacy
Security Archives - TechRepublic
Security Archives - TechRepublic
博客园 - 司徒正美
量子位
博客园 - 三生石上(FineUI控件)
J
Java Code Geeks
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recorded Future
Recorded Future
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Martin Fowler
Martin Fowler
Project Zero
Project Zero

博客园 - 唐宋元明清2188

Electron 桌面客户端 ASAR 热更新:替换一个文件完成版本切换 SDD-skills执行遗漏问题 SDD基于规范编程-OpenSpec及SuperPowers .NET 磁盘BitLocker加密-技术选型 .NET Win32磁盘动态卷触发“函数不正确”问题排查 .NET SqlSugar多线程下SqlSugarClient 的线程安全陷阱 .NET 本地Db数据库-技术方案选型 .NET 磁盘Bitlocker加密-Powershell操作 .NET 磁盘管理-技术方案选型 .NET 记录Amazon上传S3异常问题 .NET 记录多框架下的Json序列化属性标记问题 .NET 阻止Windows关机以及阻止失败的一些原因 网络虚拟存储 Iscsi实现方案 Windows 网络存储ISCSI介绍 Windows 本地虚拟磁盘Vhdx .NET 数据拷贝方案选择 .NET 窗口置于最顶层 如何做好软件架构师 .NET开发一些书箱推荐 Windows应用开发-常用工具 WPF 记录鼠标、触摸多设备混合输入场景问题 .NET Bios相关数据读写
.NET Win32设置只读未对齐,导致NTFS文件系统识别异常
唐宋元明清2188 · 2026-06-04 · via 博客园 - 唐宋元明清2188

问题现象

在Windows平台上,通过Win32 API IOCTL_DISK_SET_DISK_ATTRIBUTES 将磁盘设置为只读后,出现了意料之外的行为:磁盘属性面板显示已只读,但NTFS文件系统仍允许写入;或者反过来,磁盘已取消只读,但NTFS仍拒绝写入。这种"磁盘层"与"文件系统层"状态不一致的现象,在iSCSI磁盘热插拔场景下尤为突出——同一LUN路径上先后挂载不同磁盘,前一块盘的只读状态会"残留"给后一块盘。

表面上看,只读设置已经成功返回,Get-Disk 也确认为只读,但实际写入操作却不受控制。问题的根源在于Windows磁盘架构的分层设计。

Windows磁盘只读的两层架构与刷新机制

Windows的磁盘只读实际上分为两层:

  • 磁盘设备驱动层:通过 IOCTL_DISK_SET_DISK_ATTRIBUTES 设置,直接修改磁盘设备对象的属性。这一层是"物理级"的,一旦设置成功,所有对该磁盘设备的I/O请求都会被拦截。
  • NTFS文件系统驱动层:NTFS在内存中维护一个VCB(Volume Control Block)结构,缓存了磁盘的只读状态。文件系统的读写判断依赖的是VCB中的缓存值,而非实时去查询磁盘设备属性。

关键问题在于:NTFS不会主动轮询磁盘属性的变化。当通过Win32 IOCTL修改了磁盘只读属性后,NTFS的VCB缓存仍然是旧值,直到某种事件触发它重新加载。

能触发NTFS重新加载磁盘属性的事件有四种:

触发方式作用层级副作用
磁盘Offline/Online 磁盘设备层 卷会短暂不可用,可能导致I/O错误
卷Dismount/Mount 文件系统层 卷短暂不可用,文件句柄失效
FSCTL_LOCK_VOLUME / FSCTL_UNLOCK_VOLUME 文件系统层 短暂独占,但影响最小
PnP设备事件 即插即用层 不受控,依赖硬件事件

其中,FSCTL_LOCK_VOLUME + FSCTL_UNLOCK_VOLUME 是最轻量的方案。FSCTL_LOCK_VOLUME 会强制NTFS刷新脏数据并获得独占访问权,随后的 FSCTL_UNLOCK_VOLUME 释放锁时,NTFS会重新从磁盘设备层读取最新属性并更新VCB缓存。这个过程不需要卸载卷,也不需要脱机磁盘,对业务的影响最小。

这也就解释了为什么 PowerShell Set-Disk -ReadOnly $true 看起来总能正确生效——它底层调用的是WMI的 MSFT_Disk.SetAttributes 方法(由storagewmi.dll实现),内部大概率封装了Lock/Unlock或者等效的属性刷新逻辑。而直接调用Win32 IOCTL的开发者,则需要自行处理这一层同步。

解决方案:Lock → SetReadOnly → Unlock

基于上述原理,正确的Win32只读设置流程应该是三步:

FSCTL_LOCK_VOLUME → 强制NTFS刷新脏数据,获得独占访问

IOCTL_DISK_SET_DISK_ATTRIBUTES → 设置磁盘只读属性

FSCTL_UNLOCK_VOLUME → 释放锁,NTFS重新加载最新属性到VCB

对应的核心代码实现:

 1 public static async Task<OperateResult> SetReadOnlyAsync(
 2     string volumeGuid, int diskNumber, bool isReadOnly, int timeoutSeconds = 30)
 3 {
 4     // Step 1: Lock卷 — 触发NTFS刷新脏数据 + 获取独占访问
 5     var lockResult = await LockVolumeAsync(volumeGuid, timeoutSeconds);
 6     if (!lockResult.IsResultOk || lockResult.Data == IntPtr.Zero)
 7         return OperateResult.ToError($"Lock volume failed: {lockResult.Message}");
 8 
 9     // Step 2: 设置只读 — 修改磁盘设备层属性
10     var setReadOnlyResult = await Task.Run(
11         () => SetReadOnly(diskNumber, isReadOnly)
12     ).TimeOutAsync(TimeSpan.FromSeconds(timeoutSeconds));
13 
14     if (!setReadOnlyResult.Success)
15     {
16         await UnlockVolumeAsync(lockResult.Data);
17         return setReadOnlyResult;
18     }
19 
20     // Step 3: Unlock卷 — NTFS重新加载磁盘属性到VCB缓存
21     var unlockResult = await UnlockVolumeAsync(lockResult.Data);
22     if (!unlockResult.Success)
23         return OperateResult.ToError(
24             $"SetReadOnly ok but Unlock failed: {unlockResult.Message}");
25 
26     return OperateResult.ToSuccess();
27 }

其中 LockVolume 通过卷GUID直接打开设备(如 \\?\Volume{xxx}),调用 FSCTL_LOCK_VOLUME。如果遇到 ACCESS_DENIED (0x05) 错误,说明有其他进程持有该卷上的文件句柄,需要加入重试逻辑(建议5次重试,间隔500ms):

 1 public OperateResult<IntPtr> LockVolumeByGuid(string volumeGuid)
 2 {
 3     string devicePath = volumeGuid.TrimEnd('\\');
 4     IntPtr hVolume = CreateFile(devicePath,
 5         GENERIC_READ | GENERIC_WRITE,
 6         FILE_SHARE_READ | FILE_SHARE_WRITE,
 7         IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
 8 
 9     if (hVolume == INVALID_HANDLE_VALUE)
10         return OperateResult<IntPtr>.ToWin32Error("CreateFile failed");
11 
12     if (DeviceIoControl(hVolume, FSCTL_LOCK_VOLUME, ...))
13         return OperateResult<IntPtr>.ToSuccess(hVolume);
14 
15     int err = Marshal.GetLastWin32Error();
16     CloseHandle(hVolume);
17     return OperateResult<IntPtr>.ToWin32Error($"FSCTL_LOCK_VOLUME failed after", err);
18 }

此外,在磁盘挂载流程中,操作顺序也很关键。应该在分配挂载点(AddAccessPath)之前完成只读设置,因为一旦挂载路径暴露给系统,第三方软件(如杀毒、索引)可能立即打开卷上的文件,导致后续Lock失败。推荐的挂载操作顺序为:

取消只读 → 扩容 → 设置磁盘标签 → 设置只读 → 分配挂载点

总结来说,直接使用Win32 IOCTL操作磁盘只读时,必须搭配 FSCTL_LOCK_VOLUME / FSCTL_UNLOCK_VOLUME 来同步NTFS文件系统的VCB缓存。这不是一个可选的优化,而是一个必须的处理步骤——缺少它,磁盘设备层和文件系统层的只读状态就会处于未对齐的状态,导致写入行为与预期不符。