Skip to main content

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}