Skip to main content

fs_ext4/
indirect.rs

1//! Legacy direct + indirect block-pointer scheme used by ext2 and ext3
2//! file inodes (and by ext4 inodes whose `EXT4_EXTENTS_FL` is unset).
3//!
4//! Layout of the inode's 60-byte `i_block` region as 15 little-endian `u32`
5//! block pointers:
6//!
7//! - `i_block[0..=11]`  — 12 direct pointers, mapping logical blocks 0..12
8//! - `i_block[12]`      — single-indirect pointer; that block is a flat array
9//!   of `block_size/4` direct pointers
10//! - `i_block[13]`      — double-indirect pointer; points at a block of
11//!   indirect-block pointers
12//! - `i_block[14]`      — triple-indirect pointer; one more level of indirection
13//!
14//! A pointer of `0` means "sparse hole" — reads return zeros without ever
15//! touching the disk. All pointers are 32 bits (no `INCOMPAT_64BIT` for
16//! ext2/3 in any production deployment), so addressable space tops out at
17//! `2^32` blocks regardless of indirection level.
18//!
19//! Spec source: kernel.org/doc/html/latest/filesystems/ext4/blockmap.html
20//! (the same scheme ext4 inherited from ext2/ext3) and Carrier,
21//! *File System Forensic Analysis*, ch. 14.
22
23use crate::block_io::BlockDevice;
24use crate::error::{Error, Result};
25
26/// Number of direct block pointers in `i_block` (slots 0..=11).
27pub const DIRECT_COUNT: u64 = 12;
28
29/// Number of `u32` pointers that fit in one filesystem block.
30#[inline]
31fn ppb(block_size: u32) -> u64 {
32    (block_size / 4) as u64
33}
34
35/// Read a `u32` block pointer from a slice of pointer bytes at index `i`.
36#[inline]
37fn ptr_at(buf: &[u8], i: usize) -> u64 {
38    let off = i * 4;
39    u32::from_le_bytes(buf[off..off + 4].try_into().unwrap()) as u64
40}
41
42/// Single-entry cache for the most-recently-read indirect block. Sequential
43/// reads through any single tier (single, double, or triple indirect) hit
44/// this 100%. Cross-tier transitions cause one cache miss each, which is fine
45/// — those happen at most twice per file (entering double, entering triple).
46///
47/// For higher-tier reads where two off-inode blocks must be touched (double
48/// indirect needs the outer + inner block; triple needs three), only the
49/// last-touched block stays cached. That trades read-coalescing within a
50/// single inner block (good) for re-reading the outer block on every inner-
51/// block transition (acceptable — `block_size/4` reads happen between such
52/// transitions, so the cost is amortized).
53pub struct IndirectCache {
54    cached: Option<(u64, Vec<u8>)>,
55}
56
57impl Default for IndirectCache {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl IndirectCache {
64    pub fn new() -> Self {
65        Self { cached: None }
66    }
67
68    /// Fetch a block from cache or read it through. The returned slice
69    /// is borrowed from the cache and is valid until the next call.
70    fn get(&mut self, dev: &dyn BlockDevice, block_no: u64, block_size: u32) -> Result<&[u8]> {
71        let needs_read = !matches!(&self.cached, Some((b, _)) if *b == block_no);
72        if needs_read {
73            let mut buf = vec![0u8; block_size as usize];
74            let byte_off = block_no
75                .checked_mul(block_size as u64)
76                .ok_or(Error::Corrupt("indirect: block byte offset overflow"))?;
77            dev.read_at(byte_off, &mut buf)?;
78            self.cached = Some((block_no, buf));
79        }
80        Ok(&self.cached.as_ref().unwrap().1)
81    }
82}
83
84/// Flavor-aware logical→physical block lookup for any inode regardless of
85/// block-mapping scheme. Dispatches on `EXT4_EXTENTS_FL` in `inode_flags`:
86///
87/// - flag set → traverses the extent tree via [`crate::extent::map_logical`]
88/// - flag absent → walks the legacy direct/indirect tree via [`lookup`]
89///
90/// Use this from any code that walks a generic inode's block map (directory
91/// scans, file reads outside the dedicated `file_io` paths, etc.). Without
92/// it, an ext2/3 inode with raw block pointers in `i_block` will be
93/// misparsed as a depth-0 extent header, returning
94/// `CorruptExtentTree("bad extent header magic")`.
95///
96/// The indirect path constructs a fresh single-entry block cache per call.
97/// Hot loops that need cross-call caching (sequential file reads) should
98/// instead branch manually on the flag and pass a long-lived
99/// [`IndirectCache`] into [`lookup`].
100pub fn map_logical_any(
101    i_block: &[u8; 60],
102    inode_flags: u32,
103    dev: &dyn BlockDevice,
104    block_size: u32,
105    logical_block: u64,
106) -> Result<Option<u64>> {
107    if (inode_flags & crate::inode::InodeFlags::EXTENTS.bits()) != 0 {
108        crate::extent::map_logical(i_block, dev, block_size, logical_block)
109    } else {
110        let mut cache = IndirectCache::new();
111        lookup(i_block, dev, block_size, logical_block, &mut cache)
112    }
113}
114
115/// Look up the physical block backing `logical_block` for an inode that uses
116/// the legacy direct/indirect scheme.
117///
118/// Returns:
119/// - `Ok(Some(physical))` if a non-zero pointer was found at every level.
120/// - `Ok(None)` for sparse holes (any pointer in the chain is zero).
121/// - `Err(Corrupt)` only on I/O failure or impossible logical-block values
122///   (i.e. beyond the triple-indirect tier's address span).
123///
124/// Caller passes a long-lived `IndirectCache` to amortize indirect-block
125/// reads across sequential `lookup` calls.
126pub fn lookup(
127    i_block: &[u8; 60],
128    dev: &dyn BlockDevice,
129    block_size: u32,
130    logical_block: u64,
131    cache: &mut IndirectCache,
132) -> Result<Option<u64>> {
133    if block_size < 1024 || !block_size.is_power_of_two() {
134        return Err(Error::Corrupt("indirect: block_size out of range"));
135    }
136    let ppb = ppb(block_size);
137
138    // Tier 1 — direct pointers (logical 0..12).
139    if logical_block < DIRECT_COUNT {
140        let phys = ptr_at(i_block, logical_block as usize);
141        return Ok(if phys == 0 { None } else { Some(phys) });
142    }
143
144    // Tier 2 — single indirect (logical [12, 12+ppb)).
145    let single_base = DIRECT_COUNT;
146    let single_end = single_base + ppb;
147    if logical_block < single_end {
148        let single_blk = ptr_at(i_block, 12);
149        if single_blk == 0 {
150            return Ok(None);
151        }
152        let idx = (logical_block - single_base) as usize;
153        let buf = cache.get(dev, single_blk, block_size)?;
154        let phys = ptr_at(buf, idx);
155        return Ok(if phys == 0 { None } else { Some(phys) });
156    }
157
158    // Tier 3 — double indirect (logical [single_end, single_end + ppb*ppb)).
159    let double_base = single_end;
160    let double_end = double_base + ppb * ppb;
161    if logical_block < double_end {
162        let double_blk = ptr_at(i_block, 13);
163        if double_blk == 0 {
164            return Ok(None);
165        }
166        let off = logical_block - double_base;
167        let outer_idx = (off / ppb) as usize;
168        let inner_idx = (off % ppb) as usize;
169
170        let outer_buf = cache.get(dev, double_blk, block_size)?;
171        let inner_blk = ptr_at(outer_buf, outer_idx);
172        if inner_blk == 0 {
173            return Ok(None);
174        }
175        let inner_buf = cache.get(dev, inner_blk, block_size)?;
176        let phys = ptr_at(inner_buf, inner_idx);
177        return Ok(if phys == 0 { None } else { Some(phys) });
178    }
179
180    // Tier 4 — triple indirect (logical [double_end, double_end + ppb^3)).
181    let triple_base = double_end;
182    let triple_end = triple_base + ppb * ppb * ppb;
183    if logical_block < triple_end {
184        let triple_blk = ptr_at(i_block, 14);
185        if triple_blk == 0 {
186            return Ok(None);
187        }
188        let off = logical_block - triple_base;
189        let l1_idx = (off / (ppb * ppb)) as usize;
190        let rem = off % (ppb * ppb);
191        let l2_idx = (rem / ppb) as usize;
192        let l3_idx = (rem % ppb) as usize;
193
194        let l1_buf = cache.get(dev, triple_blk, block_size)?;
195        let l2_blk = ptr_at(l1_buf, l1_idx);
196        if l2_blk == 0 {
197            return Ok(None);
198        }
199        let l2_buf = cache.get(dev, l2_blk, block_size)?;
200        let l3_blk = ptr_at(l2_buf, l2_idx);
201        if l3_blk == 0 {
202            return Ok(None);
203        }
204        let l3_buf = cache.get(dev, l3_blk, block_size)?;
205        let phys = ptr_at(l3_buf, l3_idx);
206        return Ok(if phys == 0 { None } else { Some(phys) });
207    }
208
209    // Beyond triple-indirect addressable range — corrupt or out-of-spec.
210    Err(Error::Corrupt(
211        "indirect: logical block exceeds triple-indirect address space",
212    ))
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::block_io::BlockDevice;
219    use std::sync::Mutex;
220
221    /// In-memory block device for unit tests.
222    struct MemDev {
223        data: Mutex<Vec<u8>>,
224    }
225
226    impl MemDev {
227        fn new(size: usize) -> Self {
228            Self {
229                data: Mutex::new(vec![0u8; size]),
230            }
231        }
232
233        fn write_block(&self, block_no: u64, block_size: u32, contents: &[u8]) {
234            let mut d = self.data.lock().unwrap();
235            let off = (block_no as usize) * (block_size as usize);
236            d[off..off + contents.len()].copy_from_slice(contents);
237        }
238    }
239
240    impl BlockDevice for MemDev {
241        fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
242            let d = self.data.lock().unwrap();
243            let start = offset as usize;
244            buf.copy_from_slice(&d[start..start + buf.len()]);
245            Ok(())
246        }
247        fn size_bytes(&self) -> u64 {
248            self.data.lock().unwrap().len() as u64
249        }
250    }
251
252    /// Build an `i_block` with the given 15 pointers.
253    fn make_iblock(ptrs: [u32; 15]) -> [u8; 60] {
254        let mut out = [0u8; 60];
255        for (i, p) in ptrs.iter().enumerate() {
256            out[i * 4..i * 4 + 4].copy_from_slice(&p.to_le_bytes());
257        }
258        out
259    }
260
261    /// Pack `ppb` pointers into a block buffer.
262    fn pack_ptrs(ptrs: &[u32], block_size: u32) -> Vec<u8> {
263        let mut buf = vec![0u8; block_size as usize];
264        for (i, p) in ptrs.iter().enumerate() {
265            buf[i * 4..i * 4 + 4].copy_from_slice(&p.to_le_bytes());
266        }
267        buf
268    }
269
270    #[test]
271    fn direct_pointer_lookup() {
272        let bs = 1024u32;
273        let dev = MemDev::new(1024 * 64);
274        let mut cache = IndirectCache::new();
275
276        // Direct[0] = block 100, Direct[5] = block 105, rest sparse.
277        let mut ptrs = [0u32; 15];
278        ptrs[0] = 100;
279        ptrs[5] = 105;
280        let i_block = make_iblock(ptrs);
281
282        assert_eq!(
283            lookup(&i_block, &dev, bs, 0, &mut cache).unwrap(),
284            Some(100)
285        );
286        assert_eq!(
287            lookup(&i_block, &dev, bs, 5, &mut cache).unwrap(),
288            Some(105)
289        );
290        // Direct[1] is zero → sparse hole.
291        assert_eq!(lookup(&i_block, &dev, bs, 1, &mut cache).unwrap(), None);
292        assert_eq!(lookup(&i_block, &dev, bs, 11, &mut cache).unwrap(), None);
293    }
294
295    #[test]
296    fn single_indirect_lookup() {
297        let bs = 1024u32; // ppb = 256
298        let dev = MemDev::new(1024 * 1024);
299        let mut cache = IndirectCache::new();
300
301        // i_block[12] = single-indirect at block 50; that block holds 256
302        // pointers, only [0]=200, [3]=203, [255]=455 set.
303        let mut indirect_ptrs = vec![0u32; 256];
304        indirect_ptrs[0] = 200;
305        indirect_ptrs[3] = 203;
306        indirect_ptrs[255] = 455;
307        dev.write_block(50, bs, &pack_ptrs(&indirect_ptrs, bs));
308
309        let mut ptrs = [0u32; 15];
310        ptrs[12] = 50;
311        let i_block = make_iblock(ptrs);
312
313        // logical 12 = single[0] = 200
314        assert_eq!(
315            lookup(&i_block, &dev, bs, 12, &mut cache).unwrap(),
316            Some(200)
317        );
318        // logical 15 = single[3] = 203
319        assert_eq!(
320            lookup(&i_block, &dev, bs, 15, &mut cache).unwrap(),
321            Some(203)
322        );
323        // logical 13 = single[1] = 0 → hole
324        assert_eq!(lookup(&i_block, &dev, bs, 13, &mut cache).unwrap(), None);
325        // logical 12+255 = single[255] = 455
326        assert_eq!(
327            lookup(&i_block, &dev, bs, 12 + 255, &mut cache).unwrap(),
328            Some(455)
329        );
330    }
331
332    #[test]
333    fn single_indirect_zero_pointer_is_hole() {
334        let bs = 1024u32;
335        let dev = MemDev::new(1024 * 64);
336        let mut cache = IndirectCache::new();
337
338        // i_block[12] = 0 → entire single-indirect tier is a hole.
339        let i_block = make_iblock([0u32; 15]);
340        assert_eq!(lookup(&i_block, &dev, bs, 12, &mut cache).unwrap(), None);
341        assert_eq!(lookup(&i_block, &dev, bs, 200, &mut cache).unwrap(), None);
342    }
343
344    #[test]
345    fn double_indirect_lookup() {
346        let bs = 1024u32; // ppb = 256
347        let dev = MemDev::new(1024 * 4096);
348        let mut cache = IndirectCache::new();
349
350        // i_block[13] = double-indirect at block 70.
351        // Outer block 70: ptrs[0] = 71 (inner block), rest zero.
352        // Inner block 71: ptrs[5] = 555.
353        let mut outer = vec![0u32; 256];
354        outer[0] = 71;
355        outer[2] = 72;
356        dev.write_block(70, bs, &pack_ptrs(&outer, bs));
357
358        let mut inner1 = vec![0u32; 256];
359        inner1[5] = 555;
360        dev.write_block(71, bs, &pack_ptrs(&inner1, bs));
361
362        let mut inner2 = vec![0u32; 256];
363        inner2[10] = 1010;
364        dev.write_block(72, bs, &pack_ptrs(&inner2, bs));
365
366        let mut ptrs = [0u32; 15];
367        ptrs[13] = 70;
368        let i_block = make_iblock(ptrs);
369
370        // double_base = 12 + 256 = 268.
371        // outer[0], inner[5] → logical 268 + 0*256 + 5 = 273
372        assert_eq!(
373            lookup(&i_block, &dev, bs, 273, &mut cache).unwrap(),
374            Some(555)
375        );
376        // outer[2], inner[10] → 268 + 2*256 + 10 = 790
377        assert_eq!(
378            lookup(&i_block, &dev, bs, 790, &mut cache).unwrap(),
379            Some(1010)
380        );
381        // outer[1] is zero → entire inner range is a hole.
382        assert_eq!(
383            lookup(&i_block, &dev, bs, 268 + 256, &mut cache).unwrap(),
384            None
385        );
386        // outer[0], inner[6] → present outer + zero inner → hole.
387        assert_eq!(lookup(&i_block, &dev, bs, 274, &mut cache).unwrap(), None);
388    }
389
390    #[test]
391    fn triple_indirect_lookup() {
392        let bs = 1024u32; // ppb = 256
393                          // ppb^3 * 1024 ≈ 16 GiB — too large for an in-mem device. Use 4096-byte
394                          // device blocks of metadata only and synthesize a small triple-indirect
395                          // chain at the very base of the tier.
396        let dev = MemDev::new(1024 * 8192);
397        let mut cache = IndirectCache::new();
398
399        // outer L1 at block 80 → [0] = 81 (L2 block)
400        // L2 at block 81      → [0] = 82 (L3 block)
401        // L3 at block 82      → [7] = 7777
402        let mut l1 = vec![0u32; 256];
403        l1[0] = 81;
404        dev.write_block(80, bs, &pack_ptrs(&l1, bs));
405
406        let mut l2 = vec![0u32; 256];
407        l2[0] = 82;
408        dev.write_block(81, bs, &pack_ptrs(&l2, bs));
409
410        let mut l3 = vec![0u32; 256];
411        l3[7] = 7777;
412        dev.write_block(82, bs, &pack_ptrs(&l3, bs));
413
414        let mut ptrs = [0u32; 15];
415        ptrs[14] = 80;
416        let i_block = make_iblock(ptrs);
417
418        // triple_base = 12 + 256 + 256*256 = 65804.
419        // l1[0], l2[0], l3[7] → 65804 + 0 + 0 + 7 = 65811
420        assert_eq!(
421            lookup(&i_block, &dev, bs, 65811, &mut cache).unwrap(),
422            Some(7777)
423        );
424        // l3[8] is zero → hole
425        assert_eq!(lookup(&i_block, &dev, bs, 65812, &mut cache).unwrap(), None);
426    }
427
428    #[test]
429    fn beyond_triple_indirect_is_corrupt() {
430        let bs = 1024u32;
431        let dev = MemDev::new(1024 * 64);
432        let mut cache = IndirectCache::new();
433
434        let i_block = make_iblock([0u32; 15]);
435        // Beyond triple range: 12 + 256 + 256^2 + 256^3 = ~16M+. Pick something
436        // safely past it.
437        let result = lookup(&i_block, &dev, bs, 1u64 << 32, &mut cache);
438        assert!(matches!(result, Err(Error::Corrupt(_))));
439    }
440}