vck_loader/hook/mod.rs
1// SPDX-FileCopyrightText: 2026 JC-Lab <joseph@jc-lab.net>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Block IO hooking engine.
6//!
7//! See docs/architecture.md "Block IO 후킹 메커니즘". The engine:
8//!
9//! 1. enumerates Block IO devices via `LocateHandleBuffer(EFI_BLOCK_IO_PROTOCOL)`,
10//! 2. matches the target partition by GPT partition GUID,
11//! 3. saves the original `ReadBlocks` / `WriteBlocks` function pointers and
12//! replaces the protocol vtable entries with our hooks,
13//! 4. on a hooked read, decrypts after the original fills the buffer;
14//! on a hooked write, encrypts a copy of the plaintext before forwarding.
15//!
16//! Hooked-read decision (all comparisons are in data-region relative sectors):
17//!
18//! ```text
19//! lba in metadata region -> original read, passthrough (plaintext)
20//! rel = lba - offset_sector
21//! rel < encrypted_offset.sector -> original read, then AES-XTS decrypt
22//! rel >= encrypted_offset.sector -> original read, passthrough (plaintext)
23//! ```
24//!
25//! Hooked-write decision (symmetric):
26//!
27//! ```text
28//! lba in metadata region -> passthrough to original write (plaintext)
29//! rel = lba - offset_sector
30//! rel < encrypted_offset.sector -> encrypt a copy, then original write
31//! rel >= encrypted_offset.sector -> passthrough to original write (plaintext)
32//! ```
33
34pub mod block_io;
35pub mod block_io2;
36
37use alloc::boxed::Box;
38use alloc::vec::Vec;
39
40use vck_common::{VckResult, VolumeCipherSupplier};
41
42use crate::provider::HookGeometry;
43
44/// Installs and removes Block IO read/write hooks for the target volume, and
45/// holds the cipher supplier used by the hooked paths.
46///
47/// The engine owns the saved original function pointers so [`uninstall`] can
48/// restore the protocol vtables to their pristine state before chainloading.
49///
50/// [`uninstall`]: BlockIoHookEngine::uninstall
51pub struct BlockIoHookEngine {
52 /// Region geometry for transparent encryption/decryption.
53 geometry: HookGeometry,
54 /// Cipher supplier: produces a short-lived cipher per read/write call.
55 /// The default JVCK suite uses [`StaticCipherSupplier`](vck_common::StaticCipherSupplier)
56 /// (AES-256-XTS); a RAM-encryption vendor supplies a custom implementation.
57 cipher_supplier: Box<dyn VolumeCipherSupplier>,
58 /// Saved `EFI_BLOCK_IO_PROTOCOL` hook state (original `ReadBlocks`/`WriteBlocks`, vtable ptr).
59 block_io: Option<block_io::BlockIoHook>,
60 /// Saved `EFI_BLOCK_IO2_PROTOCOL` hook state (original `ReadBlocksEx`, vtable ptr).
61 block_io2: Option<block_io2::BlockIo2Hook>,
62}
63
64impl BlockIoHookEngine {
65 /// Creates an engine bound to the given geometry and the sample-selected
66 /// cipher supplier.
67 pub fn new(
68 geometry: HookGeometry,
69 cipher_supplier: Box<dyn VolumeCipherSupplier>,
70 ) -> VckResult<Self> {
71 Ok(Self {
72 geometry,
73 cipher_supplier,
74 block_io: None,
75 block_io2: None,
76 })
77 }
78
79 /// Locates the target volume's Block IO protocol(s), saves the original
80 /// `ReadBlocks` / `WriteBlocks` function pointers, and replaces the vtable
81 /// entries with our hooks.
82 ///
83 /// Matching is done by GPT partition GUID (carried by the handover payload /
84 /// loader config). Both `EFI_BLOCK_IO_PROTOCOL` read and write are hooked.
85 pub fn install(&mut self) -> VckResult<()> {
86 use alloc::format;
87 use uefi::boot::{self, open_protocol_exclusive, SearchType};
88 use uefi::proto::media::block::BlockIO;
89 use uefi::proto::media::partition::PartitionInfo;
90 use vck_common::types::guid_from_windows_bytes;
91 use vck_common::VckError;
92
93 let target = self.geometry.partition_guid;
94 let engine_ptr: *const BlockIoHookEngine = self;
95
96 let handles = boot::locate_handle_buffer(SearchType::from_proto::<BlockIO>())
97 .map_err(|e| VckError::Io(format!("locate BlockIO handles failed: {e:?}")))?;
98
99 for &handle in handles.iter() {
100 // Match the target partition by GPT unique GUID via PartitionInfo.
101 let matched = match open_protocol_exclusive::<PartitionInfo>(handle) {
102 Ok(pinfo) => pinfo
103 .gpt_partition_entry()
104 .map(|gpt| {
105 guid_from_windows_bytes(gpt.unique_partition_guid.to_bytes()) == target
106 })
107 .unwrap_or(false),
108 Err(_) => false,
109 };
110 if !matched {
111 continue;
112 }
113
114 // Obtain the raw `BlockIoProtocol` instance pointer and patch its
115 // `read_blocks` and `write_blocks` fields.
116 let mut scoped = open_protocol_exclusive::<BlockIO>(handle)
117 .map_err(|e| VckError::Io(format!("open BlockIO for hook failed: {e:?}")))?;
118 let proto = scoped
119 .get_mut()
120 .ok_or(VckError::Io(alloc::string::String::from(
121 "BlockIO interface is null",
122 )))? as *mut BlockIO
123 as *mut uefi_raw::protocol::block::BlockIoProtocol;
124
125 self.block_io = Some(block_io::BlockIoHook::install(proto, engine_ptr)?);
126 // Keep the protocol open (and the patch live) past this scope.
127 core::mem::forget(scoped);
128 // Block IO2 (async) is not hooked: Windows boot reads the volume via
129 // the synchronous Block IO / SimpleFileSystem path. Documented gap.
130 return Ok(());
131 }
132
133 Err(vck_common::VckError::NotFound(
134 "no Block IO partition matched the target GUID for hooking",
135 ))
136 }
137
138 /// Restores all hooked vtables to their original function pointers.
139 ///
140 /// NOTE: in the normal boot flow the hooks intentionally remain installed
141 /// across the chainload (the OS loader keeps reading/writing through them),
142 /// so this is only used for error-path cleanup.
143 pub fn uninstall(&mut self) -> VckResult<()> {
144 if let Some(hook) = self.block_io.take() {
145 hook.uninstall()?;
146 }
147 if let Some(hook) = self.block_io2.take() {
148 hook.uninstall()?;
149 }
150 Ok(())
151 }
152
153 /// Shared hooked-read decision logic invoked by both protocol hooks after
154 /// the original read has filled `buf` with on-disk (possibly ciphertext)
155 /// bytes.
156 ///
157 /// `lba` is the absolute starting LBA of the request. `buf` length must be a
158 /// multiple of the sector size. A short-lived cipher is acquired once for the
159 /// whole call and destroyed immediately after.
160 pub(crate) fn decrypt_after_read(
161 &self,
162 lba: u64,
163 sector_size: usize,
164 buf: &mut [u8],
165 ) -> VckResult<()> {
166 if sector_size == 0 {
167 return Ok(());
168 }
169 let mut cipher = match self.cipher_supplier.get_cipher() {
170 Some(c) => c,
171 None => return Ok(()),
172 };
173 let offset_sector = self.geometry.offset_sector;
174 let total = self.geometry.encrypted_offset.total_sectors;
175 for (i, sector) in buf.chunks_mut(sector_size).enumerate() {
176 let abs = lba + i as u64;
177 let Some(rel) = abs.checked_sub(offset_sector) else {
178 continue;
179 };
180 if rel >= total {
181 continue;
182 }
183 if self.geometry.encrypted_offset.is_encrypted(rel) {
184 cipher.decrypt_sector(rel, sector);
185 }
186 }
187 cipher.destroy();
188 Ok(())
189 }
190
191 /// Shared hooked-write logic: returns an encrypted copy of `buf` to pass to
192 /// the original `WriteBlocks`. The caller's buffer is never modified.
193 ///
194 /// Sectors outside the encrypted region are copied verbatim (plaintext
195 /// passthrough). A short-lived cipher is acquired once for the whole call and
196 /// destroyed immediately after.
197 pub(crate) fn encrypt_before_write(
198 &self,
199 lba: u64,
200 sector_size: usize,
201 buf: &[u8],
202 ) -> VckResult<Vec<u8>> {
203 let mut out = Vec::from(buf);
204 if sector_size == 0 {
205 return Ok(out);
206 }
207 let mut cipher = match self.cipher_supplier.get_cipher() {
208 Some(c) => c,
209 None => return Ok(out),
210 };
211 let offset_sector = self.geometry.offset_sector;
212 let total = self.geometry.encrypted_offset.total_sectors;
213 for (i, sector) in out.chunks_mut(sector_size).enumerate() {
214 let abs = lba + i as u64;
215 let Some(rel) = abs.checked_sub(offset_sector) else {
216 continue;
217 };
218 if rel >= total {
219 continue;
220 }
221 if self.geometry.encrypted_offset.is_encrypted(rel) {
222 cipher.encrypt_sector(rel, sector);
223 }
224 }
225 cipher.destroy();
226 Ok(out)
227 }
228}