在 LetRecovery 的底层边界上开发:从兼容性到可恢复性的完整回顾

内容提示

本文可能使用了 AI 技术生成。

LetRecovery 最早看起来像一个“把 Windows 镜像写到另一个分区”的工具,真正开发之后才发现,它更接近一个小型操作系统交接系统:正常 Windows 负责收集意图和准备环境,WinPE 负责在旧系统已经离线后完成磁盘、镜像、驱动、引导和密钥处理,重启后的新系统还要继续完成账户、个人文件和清理收尾。

因此最难的工作从来不是接一个按钮、调用一个命令或画一个进度条,而是让这些步骤在不同 Windows 版本、不同磁盘布局、不同固件模式和不同失败时刻下仍然可解释。本文回顾 LetRecovery 早期和近期开发中最值得保留的经验:哪些 API 边界最容易被误用,哪些兼容性问题只能靠真实环境暴露,哪些看似更严格的检查反而会降低成功率,以及如何把这些经验沉淀成 Rust 类型、状态机、日志和可丢弃 VM 证据。

查看 LetRecovery 源码

核心判断

系统工具的安全性不是校验数量。真正可靠的授权链应该尽量短:使用本次会话产生的随机 marker、认证配置中的同值和一次唯一精确匹配,已经足够解决的问题,不要再叠加会自然变化的磁盘号、容量、GUID 和历史布局摘要。

1. 从“能运行”到“能交接”的架构演进

LetRecovery 由三个层次组成:lr-core 共享核心、正常系统端和 PE 端。早期代码里,两端各自拥有相似的命令构建、路径校验和 Windows 适配;这种复制在功能少的时候很快,但当分区、BitLocker、日志和首登逻辑开始跨重启时,两个副本会逐渐出现不同的语义。

后来我把纯逻辑、数据契约和可测试的命令边界集中到 lr-core,两端只保留环境适配。这个调整的价值不只是复用代码,而是让危险动作先经过一个可以用普通临时目录、mock 和 DryRunCommandExecutor 验证的层。

rust
pub struct CommandRequest {
    pub program: String,
    pub args: Vec<String>,
    pub current_dir: Option<PathBuf>,
}

pub trait CommandExecutor {
    fn run(&self, request: &CommandRequest) -> anyhow::Result<CommandOutput>;
}

pub fn build_dism_add_driver(inf: &Path, target: &Path) -> anyhow::Result<CommandRequest> {
    let inf = checked_regular_path(inf)?;
    let target = checked_directory_path(target)?;
    Ok(CommandRequest {
        program: "dism.exe".to_string(),
        args: vec![
            "/English".into(),
            "/Image:".to_string() + target.to_str().unwrap(),
            "/Add-Driver".into(),
            "/Driver:".to_string() + inf.to_str().unwrap(),
        ],
        current_dir: None,
    })
}

CommandExecutor 让“命令怎么构建”和“命令是否真的执行”分离。测试可以验证参数顺序、路径边界和退出码语义,生产端再把同一个请求交给真正的进程执行器。这个分层也阻止了旧的 DiskPart 脚本、PowerShell 文本解析和本地化命令输出重新进入承重路径。

2. Windows 7 兼容性不是一个编译参数

LetRecovery 的正常端需要覆盖 Windows 7,而 PE 端是另一套普通 Windows 目标。早期最容易犯的错,是在现代机器上直接执行普通 cargo build --release,再把生成的 EXE 当成 Win7 版本发布。编译目标、CRT、链接器导入表和构建日期都会影响实际兼容性。

现在正常端发布构建统一使用固定的 Rust 版本、x86_64-win7-windows-msvc、静态 CRT、rust-src 和 -Z build-std=std,panic_abort,返回成功前还会扫描导入表;PE 端继续使用普通 x86_64-pc-windows-msvc 单独构建。

powershell
$target = 'x86_64-win7-windows-msvc'
$env:RUSTFLAGS = '-C target-feature=+crt-static'

cargo +1.88.0 build `
  -p letrecovery `
  --release `
  --locked `
  --target $target `
  -Z build-std=std,panic_abort

& .\.github\scripts\verify-win7-imports.ps1 `
  -Executable ".\target\$target\release\LetRecovery.exe"
if ($LASTEXITCODE -ne 0) {
    throw 'Win7 import boundary verification failed'
}

真正的兼容性还包括 API 可用性、清单、资源、语言文件、默认配置和外部运行库。一个只在编译机上启动成功的二进制,不能证明它能在 Win7、精简 WinPE 或没有 WLAN 服务的环境中完成核心任务。

3. 磁盘和分区:provider 的事实高于经验布局

磁盘代码是整个项目里最容易把“经验”误当成“契约”的部分。真实 provider 返回的 extent 可能不是整 MiB,分区起点也可能因为固件、克隆、历史分区操作或虚拟磁盘几何而变化。把 offset % 1MiB == 0 当作合法性条件,只会把正常设备变成误报。

正确的检查集中在范围安全:非空、checked overflow、完全包含于授权 envelope、不能覆盖受保护区域,操作后必须按 canonical 布局回读。

rust
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Extent {
    pub offset: u64,
    pub length: u64,
}

impl Extent {
    pub fn end(self) -> Option<u64> {
        self.offset.checked_add(self.length)
    }

    pub fn contains(self, child: Extent) -> bool {
        let Some(parent_end) = self.end() else { return false };
        let Some(child_end) = child.end() else { return false };
        child.length > 0
            && child.offset >= self.offset
            && child_end <= parent_end
    }
}

CreatePartitionEx 成功启动异步操作后,Wait、Refresh 或首次布局回读失败,都不能证明分区没有创建。恢复流程必须重新读取当前布局,寻找唯一新增、角色正确、容量足够且完全位于本次授权 envelope 内的分区。找不到、找到多个、范围越界或回读失败,都应该报告 partial state 并保留现场,不能按请求 offset 猜测删除。

缩卷也遵循同一原则。Shrink 返回共享错误后,正常端必须重新读取同一当前卷,确认磁盘和起点没有变化,只能按实际观察到的尾部缩小量扩回。范围未变时不操作,查询失败或范围增长时严禁按请求值盲目扩容。

text
请求:shrink C: 释放 10,117,709,824 bytes
回读:offset=78,778,449,408 length=68,659,707,904
恢复:只按实际 delta=10,117,709,824 bytes 扩回

这类设计看起来比“先对齐、再比较、最后写入”更朴素,却能覆盖更多真实设备,因为它尊重 Windows provider 已经返回的当前事实。

4. VDS、IOCTL 和句柄生命周期

Windows 存储 API 的难点常常不在函数名,而在参数单位、success warning、对象生命周期和操作后的权威回读。例如 IVdsCreatePartitionEx::CreatePartitionEx 的 ulAlign 是字节数,0 表示让 provider 决定;把 1 当成“启用默认对齐”会在某些设备上产生错误几何。

格式化入口必须从实际 IVdsVolume 对象核对 marker 当前匹配的卷 extent。成功后再回读 extent、文件系统和卷标。这个确认完成后,镜像释放、驱动、更新、PCA、引导和无人值守步骤不应再次用易变库存字段重复认证同一目标。

原始磁盘移动则更严格:首次写入前,要在同一个已打开的 PhysicalDrive 写句柄上复核 layout、capacity 和 StorageIdAssocDevice 身份;关闭句柄后重新打开,也必须用移动前的强卷身份和规范快照复核,不能继续相信可能已经复用的磁盘号。

rust
pub struct OpenPhysicalDrive {
    handle: OwnedHandle,
    layout: CanonicalLayout,
    capacity: u64,
    storage_id: Option<Vec<u8>>,
}

impl OpenPhysicalDrive {
    pub fn verify_before_write(&self, current: &CanonicalLayout) -> Result<()> {
        ensure!(self.layout == *current, "disk layout changed before raw write");
        ensure!(self.capacity == current.capacity, "disk capacity changed");
        Ok(())
    }
}

句柄不是实现细节。一个 volume 对象、一个 IOCTL 回读和一次写操作之间如果混入重新枚举,磁盘号就可能指向另一个对象;把句柄生命周期和回读关系写进类型或状态结构,才能让调用图保持可审计。

5. 自动暂存分区:小事务里包含完整的交接哲学

当目标磁盘只有一个可用系统数据分区时,正常端要先缩出暂存空间,再把安装任务交给 PE。这个过程同时涉及 shrink、create、marker、盘符、跨重启认证和回收扩容,是最容易出现“每一步单看都成功,合在一起却无法恢复”的流程。

暂存 marker 不能只记录盘符。正常端必须把当前 SessionId、目标完整 extent、暂存完整 extent、规范目标布局摘要和授权 envelope 原子写入;PE 删除前要重新读取 marker,并用当前实时身份核对。旧版、缺字段、重复字段、异会话 marker 或范围变化,都应该保留分区并记录 warning。

json
{
  "schema": 3,
  "session_id": "8b1f...",
  "target_extent": { "offset": 78223360000, "length": 68659707904 },
  "staging_extent": { "offset": 146883174400, "length": 10117709824 },
  "authorized_envelope": { "offset": 146883174400, "length": 10117709824 }
}

删除成功后不能再按旧盘符清理路径。Mount Manager 可能立刻把同一个盘符分配给无关卷;之后只允许清理仍然通过当前目标身份绑定的 marker。这种“删除之后不相信盘符”的细节,是长期调试后留下的非常重要的经验。

6. 驱动预算与启动路径证明:部署工具只是执行边界

这里真正困难的不是调用部署工具,而是让驱动预算在选盘、暂存创建、导出物化和阶段回读之间保持同一语义。查询结果只能形成候选预算,权威事实仍来自实际文件树、启动存储拓扑和操作后回读。驱动导出的 Driver Store 视图可能小于 /Online /Export-Driver 物化后的普通文件树,把两者差值当成损坏会误报容量不足。当前做法是先用只读库存形成预算,真正导出后再用普通文件树的逻辑大小替换预算中的驱动项,并立即用 GetDiskFreeSpaceExW 回读 caller 可用空间。

预算也必须贯穿选择、创建和阶段回读:镜像全部 span、XP 文件树、在线 OEM 驱动、PCA、版本化用户驱动和 UefiSeven 文件大小,最后只加一次固定 2 GiB 操作余量。不能用磁盘总容量比例、固定 8–12 GiB 或重复叠加“安全保留”。

普通保存驱动的恢复流程也经历过重构。早期很容易想给每个 INF 叠加 CAT、WinVerifyTrust 和 SetupAPI 预验签,但精简 WinPE 里信任服务缺失会把整棵合法驱动树判成不可信。现在先做结构安全检查,再让 DISM 导入;批量失败时逐个隔离精确 INF,只有仍有包失败才读取目标镜像库存确认真实启动路径覆盖。打印机、虚拟机、网卡和 WLAN 等非启动存储驱动失败只能进入有界 warning。

text
结构安全检查
      ↓
DISM 目录导入
      ↓(批量失败)
逐个精确 INF 隔离
      ↓(仍有失败)
DismGetDrivers / dism.exe /Get-DriverInfo
      ↓
只对真实启动存储要求做承重判断

这个流程的核心是不要用独立启发式替代微软 API 的真实结果。DISM 成功导入一个精确 INF 后,应该接受这个权威结果,而不是再用另一套容易误报的签名链把它否决。

7. API 不可用时:能力降级,而不是语义降级

VDS、Storage Management、部分 SetupAPI 或精简 WinPE 中的管理接口并不保证存在。真正难处理的不是“找不到 DLL 就换一条命令”,而是先判断缺失的 API 属于哪一层事实:枚举能力、诊断能力,还是承重写入能力。能力探测结果必须进入显式 capability matrix,不能在调用失败后把未知状态强行映射成成功。

rust
pub enum Probe { Available, Unsupported, Failed(HResult) }

pub struct CapabilityMatrix {
    pub canonical_layout: Probe,
    pub vds_mutation: Probe,
    pub ioctl_identity: Probe,
    pub volume_format: Probe,
}

回退顺序按权威性固定:优先使用同一对象句柄上的 VDS 属性和 IOCTL 回读;VDS 枚举不可用时,若已经持有并验证了 volume/physical-drive 句柄,则只允许使用 IOCTL 获取 extent、容量和卷身份;对象句柄也无法建立时,只能返回 Unknown 并保留现场。不能因为 Get-Volume、盘符存在、PowerShell provider 或磁盘号看起来正常,就把它们提升为写盘授权。

text
VDS object + post-readback
        ↓ unavailable
validated handle + IOCTL extent/capacity
        ↓ unavailable
read-only diagnostic snapshot
        ↓
Unknown / preserve state

回退有三个硬边界:查询可以降级,写入不能伪造;管理 API 不可用不等于自动切换 DiskPart;可选属性缺失只能在不影响 canonical extent 和目标身份时记录 Unsupported。句柄、extent、容量或后置布局回读缺失,则是承重事实缺失,必须进入 partial/failed 状态。

rust
pub fn reconcile_without_vds(
    handle: &ValidatedHandle,
    authorization: &AuthorizationEnvelope,
    original_error: ApiError,
) -> Result<Reconciliation> {
    let observed = ioctl_read_canonical_extent(handle)
        .map_err(|e| Reconciliation::partial(original_error, e))?;
    if !authorization.contains(observed.extent) {
        return Ok(Reconciliation::partial(
            original_error,
            ApiError::UnauthorizedExtent,
        ));
    }
    Ok(Reconciliation::observed(original_error, observed))
}

管理 API 失败后的结果收束仍保存原始 HRESULT/Win32 错误,重新读取当前布局,比较实际 delta,并把恢复结果和原始失败同时写入阶段证据。不能用备用 API 返回的盘符覆盖前一个错误,也不能用请求 offset、历史 GUID 或估算容量补全缺失事实。最终报告必须区分 unsupported capability、query failed、mutation failed 和 partial state。

7. BitLocker:受认证载荷与句柄生命周期

BitLocker 是正常端和 PE 端之间最敏感的一条链。ViaPE 安装和备份需要在正常系统收集当前 Windows 已持有的 48 位 RecoveryPassword,再通过受认证的私有启动 WIM 载荷交给 PE。PE 使用共享 FVEAPI 打开卷并用恢复密钥解锁,不能依赖 WinPE 里可能不存在的 manage-bde.exe。

这里最容易出现的错误是把“解锁”误写成“解密”:调用 manage-bde -off、删除保护器或暂停保护都会改变用户的数据保护状态,远远超出安装工具的职责。

rust
pub fn unlock_for_install(api: &FveApi, volume: &Path, key: &str) -> Result<()> {
    let handle = api.open_volume(volume)?;
    handle.unlock_with_recovery_key(key)
        .context("unlock BitLocker volume for the current operation")?;
    // 只保留当前句柄生命周期内的解锁状态,不修改保护器和加密策略。
    Ok(())
}

没有密钥时,既有 best-effort 语义可以记录有界 warning,但不能因为所谓“安全”删除透传能力;有密钥时也不能把它写入公开数据分区、普通日志或命令行参数。密钥只存在于受认证的私有载荷和短生命周期内存中。

8. 驱动和控制器:分类名不是拓扑证明

VMD、HDC、SCSIAdapter 这些设备类名只能帮助定位问题,不能单独证明某个驱动是启动必需的。真正承重的存储驱动要求,要由源系统卷的 VOLUME_DISK_EXTENTS 对应磁盘 devnode 当前 ConfigMgr 父链证明,并且设备类属于存储控制器。

随包固定的 VMD 资源继续使用锁定清单、固定哈希和确定签名者验证;用户导出的普通 OEM INF 则走 DISM 目录导入和硬件 ID 完整等值匹配。不能把供应链固定资源的严格策略泛化到所有用户驱动,也不能因为 WinPE 缺少某项验证服务就把普通驱动树全部判成不可信。

rust
pub fn hardware_id_covers_device(
    device_ids: &[String],
    candidate_ids: &[String],
) -> bool {
    device_ids.iter().any(|source| {
        candidate_ids.iter().any(|candidate| {
            source.eq_ignore_ascii_case(candidate)
        })
    })
}

完整 Hardware ID 或 Compatible ID 的不区分大小写等值匹配,才足以证明覆盖。截断成 VEN/DEV、前缀、后缀或只比较 SUBSYS 都会把不相关硬件误认为可启动覆盖。

9. WLAN 动态加载:可选能力不能拖垮核心安装

WePE 可能缺少 wlanapi.dll、某些导出或 WLAN 服务。正常端的 ative_wifi` 因此不能静态导入 WLAN API,而应该动态加载系统目录中的 DLL 和六个需要的导出。模块生命周期必须覆盖 WLAN 分配内存的使用期,先释放 profile、关闭句柄,再卸载模块。

rust
let wlan = match WlanApi::load_system() {
    Ok(api) => Some(api),
    Err(error) => {
        log::warn!("Wi-Fi migration disabled: {error:#}");
        None
    }
};

if let Some(api) = wlan.as_ref() {
    let profiles = api.capture_profiles()?
        .into_iter()
        .filter(|profile| profile.is_current_session())
        .collect::<Vec<_>>();
    stage_profiles(profiles)?;
}

Wi-Fi 迁移失败不应该让镜像释放、启动存储、引导和最小系统配置失败。这个错误分级原则也适用于打印机、虚拟机增强、可选语言包和非承重更新:记录有界 warning,继续得到可启动系统。

10. PCA、UefiSeven 和 Windows 7 引导

启动修复是另一个不能只看退出码的边界。BCD、BCDBoot、Bootsect 和 XP/2003 引导写入都属于承重步骤:进程启动、非零退出、关键文件缺失、活动分区写入或结果文件回读任一步失败,都必须停止。

Windows 7 x64 UEFI 还涉及 PCA2011/PCA2023、VMware 原生 Microsoft EFI 和 UefiSeven。经过多轮兼容处理后,逻辑变成:先根据 CPUID、SMBIOS 和 SetupAPI 只读识别 VMware;确认 VMware 后恢复微软原生 bootmgfw.efi 和 bootx64.efi 双入口;其它环境才使用锁定的 UefiSeven,并事务化保留两个原始 EFI 文件。

text
环境探测
  ├─ 明确 VMware → BCDBoot + Microsoft x64 EFI 双入口
  ├─ 明确其它环境 → UefiSeven 双入口事务部署
  └─ Unknown → 不猜测,按安全路径停止或交给用户选择

Unknown 不能被猜成 VMware,也不能因为固件通常会遵循 NVRAM 中的 Windows Boot Manager,就省略 EFI\Boot\bootx64.efi。固件行为的差异必须由真实文件和回读结果收束。

11. 个人文件和内置管理员:两次登录之间的状态机

个人文件保留不是简单复制 Users 目录。它要处理 Known Folder、Public 文件、快捷方式目标、未知顶层数据、desktop.ini、用户 SID、profile 删除和首登时机。FirstLogonCommands 启动早于桌面稳定,不能承担任意长度的恢复;现代 Windows 还会异步启动这些命令。

因此首登阶段只负责捕获精确 SID、准备一次性任务和完成账户状态切换,真正的资料恢复在第二次登录由受认证的高权限任务执行。进度 Shell 由原生 helper 创建真实顶层窗口,不依赖 cmd.exe、conhost.exe 或“进程存在”来假设用户看到了界面。

text
第一次登录
  → 证明临时账户 token 和 profile
  → 捕获 SID
  → 必要时准备 RID-500 改名和 LSA secret
  → 注册精确 SID 的一次性任务
  → 持久化回读后强制内部重启

第二次登录
  → 证明当前 token 是目标账户
  → 恢复个人目录和快捷方式
  → 删除临时账户与 profile
  → 恢复原 Shell
  → 等待稳定 GetShellWindow
  → 删除剩余 staging 和 launcher

内置 Administrator 模式尤其不能把密码放进 Winlogon\DefaultPassword,因为 Windows 会在第一次登录后消费并删除它。当前做法是由 SYSTEM helper 使用 LsaStorePrivateData 保存本地加密 secret,第二次登录取回并在成功后退役 AutoLogon、LSA secret 和 marker。

12. 日志交接:每个阶段都要留下可读证据

跨重启安装最难调试的 bug,往往不是代码没有写日志,而是日志写到了即将被清理的卷,或者正常端快照还没有真正落盘,PE 就已经开始格式化。

当前日志流程分成几个阶段:正常端完成 flush barrier 后,把脱敏、限长、哈希绑定的日志写成内容寻址 blob;manifest 最后原子替换;PE 在清理数据分区前把快照复制到 RAM 盘;镜像或 XP 文件树一旦可用,就向新系统固定入口发布第一个检查点;最终终态再生成合并日志。

rust
pub fn stage_desktop_log_contents(
    contents: &[u8],
    data_root: &Path,
    session_id: &str,
) -> Result<DesktopLogManifest> {
    let digest = sha256_hex(contents);
    let dir = session_handoff_directory(data_root, session_id)?;
    let blob = dir.join(format!("normal-{digest}.log"));

    write_new_regular_file(&blob, contents)?;
    let manifest = DesktopLogManifest {
        schema: 2,
        session_id: session_id.to_owned(),
        blob_file: blob.file_name().unwrap().to_string_lossy().into_owned(),
        sha256: digest,
        bytes: contents.len() as u64,
    };
    atomic_replace_manifest(&dir.join("desktop-manifest.json"), &manifest)?;
    Ok(manifest)
}

如果 manifest 丢失但目录中只有一个文件名带完整 SHA-256 的 blob,读取端可以重新验证并恢复;普通同名日志、异会话文件和多个候选都必须拒绝。日志中继失败只记录 warning,不能覆盖真正的安装错误或阻止已经安全安排的重启。

13. 发布包:构建过程也必须可审计

发布不是把一个 EXE 压缩起来。最终 pkg 需要包含正常端程序、PE WIM、DISM 库、锁定驱动、PCA 资源、语言文件、配置和运行库,同时删除旧日志、开发目录、调试中间文件和 reparse point。

配置不能从维护机的 config.json 直接复制。发布脚本必须根据确定性默认值重建配置,保留本次构建产生的 pe_cache 元数据,明确保持日志开启、简易模式提示未关闭、高级选项关闭、自动化导出关闭和 PE 维护入口关闭。压缩后还要重新解出 config.json,逐字节比较已审计的 pkg/config.json。

powershell
& .\.github\scripts\assert-release-tree-clean.ps1 -Root .\pkg
if ($LASTEXITCODE -ne 0) { throw 'release tree audit failed' }

& 7z a -t7z -mx=9 -m0=lzma2 -ms=on .\LetRecovery.7z .\pkg\*
if ($LASTEXITCODE -ne 0) { throw 'archive creation failed' }

& 7z t .\LetRecovery.7z
if ($LASTEXITCODE -ne 0) { throw 'archive integrity test failed' }

14. Hyper-V 回归:测试宿主也会失败

LetRecovery 的安装矩阵不是“启动 VM,然后看它有没有关机”。宿主要绑定唯一 RunId、检查点和证据 VHD;来宾通过启动脚本写入 install plan、阶段日志、账户和离线布局;证据采集必须放在短生命周期 child 中,避免只读挂载 VHD 和 Storage CIM 对象把宿主内存推到不可控。

宿主阶段至少要发布 PID、工作集、private bytes、句柄、线程、阶段名和 UTC。即使来宾已经关机,也要等证据盘卸载、validation 原子落盘、检查点恢复、DVD 和网络恢复、VHD 删除全部通过后,才允许发布成功。

text
initialized
  → baseline_restored
  → guest_started
  → guest_stopped
  → evidence_collection_started
  → evidence_collected
  → validation_written
  → cleanup_started
  → checkpoint_restored
  → evidence_vhd_deleted
  → completed

一次回归曾因为传入的是 LetRecovery 产品 ISO,来宾没有找到 sources\install.wim;另一次因为 26100 ISO 的实际版次是 Professional,夹具却期待 ProfessionalCountrySpecific。这些都不是产品磁盘逻辑本身的失败,却说明测试介质和夹具契约同样需要在启动前验证。测试框架必须把介质错误、宿主错误和产品错误区分开,否则一条错误日志会把整个系统带偏。

15. 最有价值的测试不是理想输入

最有价值的回归专门覆盖合法但不整齐的环境:非整 MiB 的 offset 和 length、provider 返回值与请求略有差异、旧会话 marker、同名异内容文件、异步 Wait 失败但布局已经变化、可选驱动失败、没有 WLAN 服务、没有 DismApi.dll、缺少可选语言包,以及破坏性边界之后的部分失败。

场景应观察到的行为证明的边界
provider 返回非整 MiB extent接受真实 extent没有经验几何门禁
CreatePartition Wait 失败但布局出现唯一 delta按实际 extent 精确收束没有按请求值猜测删除
旧 SessionId 的 marker 同名存在静默忽略身份只绑定当前会话
WLAN DLL 或服务缺失禁用迁移,安装继续可选能力不拖垮核心结果
普通 OEM INF 导入成功立即接受权威 DISM 结果没有重复验签误报
正常端日志缺失、PE 日志有效发布 PE 日志并保留占位单端缺失不遮挡另一端
目标已格式化后可选更新失败新系统继续收束并报告 warning破坏后不扩大失败
VM 验证 child 未退出先确认结果已原子落盘再回收不凭内存阈值杀掉活跃测试

Rust 的类型、PowerShell 的结构化 JSON、Win32 的权威回读和 VM 的证据目录,最终都服务于同一个目标:让失败也成为可以解释的状态,而不是一堆互相矛盾的猜测。

16. 技术含量来自不变量,而不是术语密度

LetRecovery 最难的部分不是把 API 名称堆在一起,而是把 Windows 的非原子现实压缩成可审计不变量:跨重启只认本次随机 session;磁盘写入只认当前句柄和 canonical layout;异步操作只认后置回读;BitLocker 只透传短生命周期恢复能力;启动存储只认拓扑证明;首登只认精确 SID 和持久化状态;Hyper-V 只认完整证据链;发布只认归一化、重建和树审计后的资产。

如果一个检查无法回答“它证明了哪个真实威胁、误报如何恢复、后置回读为何不能替代”,它就不应该进入承重路径。真正高级的系统工程,是让每个不可逆动作都拥有可验证的前置授权、可观察的后置事实和明确的 partial-state 解释。

最后的感悟

LetRecovery 让我重新理解了“简单”。简单不是少写几行代码,而是拒绝把易变的磁盘号、容量、GUID、历史长度和整盘摘要叠加成一条脆弱的身份链;拒绝把查询预估当成真实写入能力;拒绝把可选组件失败扩大成不可启动;拒绝把“进程存在”当成用户看见了窗口;也拒绝把“命令退出码为 0”当成所有后置条件都成立。

真正可靠的系统工具通常有几个共同点:输入身份短而强,写入边界明确,操作后回读权威状态,跨重启只认本次会话 marker,错误根据实际影响分级,日志在每个阶段都留下可验证证据,测试覆盖合法的非典型环境。Rust 让这些约束可以被表达成类型和纯函数,Windows API 提供真实执行边界,而最终决定质量的,仍然是对失败路径的尊重。

dddffgg的小窝

dddffgg 的小窝 —— 分享技术、生活与思考

© 2026 dddffgg的小窝

鲁ICP备2023028944号