Skip to main content

fs_ext4/
path.rs

1//! Path-to-inode resolution.
2//!
3//! Walk a slash-separated path — bytes, never decoded — from the root directory (inode 2) down to
4//! a target inode number. Used by every public-facing C API function that
5//! accepts a path (stat, dir_open, read_file, readlink).
6//!
7//! Algorithm: start at `EXT4_ROOT_INODE`, read its inode, for each non-empty
8//! path component look up the entry by name in the current directory's data
9//! blocks, then descend into the matching child inode.
10//!
11//! An htree-indexed directory is searched through its index first
12//! (`htree::lookup_leaf_with`), and a name the index does not lead to is
13//! then found by a linear scan, which is always valid because the index is
14//! an acceleration over ordinary directory blocks, not a replacement for
15//! them. Other directories are scanned linearly.
16
17use crate::block_io::BlockDevice;
18use crate::dir::{self, DirEntry};
19use crate::error::{Error, Result};
20use crate::htree;
21use crate::indirect;
22use crate::inline_data;
23use crate::inode::{Inode, InodeFlags};
24use crate::superblock::Superblock;
25
26/// Root directory inode number (always 2 on every ext[234] filesystem).
27pub const EXT4_ROOT_INODE: u32 = 2;
28
29/// Resolve a slash-separated path to an inode number.
30///
31/// `path` is expected in the form `/a/b/c` (leading slash accepted, trailing
32/// slash accepted, empty path returns the root). Non-UTF-8 bytes in directory
33/// entries are compared literally.
34///
35/// Returns:
36/// - `Ok(inode)` — inode number for the resolved path
37/// - `Err(Error::NotFound)` — any component did not exist
38/// - `Err(Error::NotADirectory)` — a non-dir component appeared mid-path
39/// - `Err(Error::Io(..))` / other corruption errors — disk I/O or malformed data
40pub fn lookup<F>(
41    dev: &dyn BlockDevice,
42    sb: &Superblock,
43    read_inode: &mut F,
44    path: &str,
45) -> Result<u32>
46where
47    F: FnMut(u32) -> Result<Inode>,
48{
49    // Backwards-compatible shim — defers to `lookup_with_csum` with a
50    // disabled `Checksummer` so callers that don't have one keep working
51    // (verification is silently skipped).
52    //
53    // FOR CALLERS THAT GENUINELY HAVE NO `Checksummer`, AND NOTHING ELSE.
54    // Sixteen sites in `fs.rs` used this from inside methods holding
55    // `&self`, and therefore holding `self.csum` — so every mutating C
56    // entry point resolved its path with verification off, while
57    // `capi::resolve_path` resolved with it on. A corrupt directory block
58    // was refused by `stat` and accepted by `unlink`, `rename`, `mkdir`,
59    // `rmdir`, `link`, `chmod` and the rest, which then edited the block
60    // and re-stamped a fresh valid checksum over it.
61    //
62    // THE SEED IS WRONG AS WELL AS THE FLAG, which is why the remedy is
63    // to pass the real `Checksummer` and never to flip `enabled` here:
64    // `seed: 0` would verify good blocks against the wrong seed and
65    // reject them.
66    let csum = crate::checksum::Checksummer {
67        seed: 0,
68        enabled: false,
69    };
70    lookup_with_csum(dev, sb, read_inode, path, &csum)
71}
72
73/// Path → inode lookup with directory-block checksum verification.
74///
75/// Identical to `lookup`, but passes the supplied `Checksummer` down to
76/// `find_entry` so each directory block's CRC32C tail is verified
77/// (when present and `csum.enabled`). Callers with a mounted `Filesystem`
78/// should pass `&fs.csum`.
79pub fn lookup_with_csum<F>(
80    dev: &dyn BlockDevice,
81    sb: &Superblock,
82    read_inode: &mut F,
83    path: &str,
84    csum: &crate::checksum::Checksummer,
85) -> Result<u32>
86where
87    F: FnMut(u32) -> Result<Inode>,
88{
89    lookup_bytes_with_csum(dev, sb, read_inode, path.as_bytes(), csum)
90}
91
92/// [`lookup_with_csum`] of a path given as bytes, which is what a path is:
93/// the components are compared byte for byte against the directory entry
94/// names, which have no encoding. A path that is not UTF-8 names exactly
95/// the file whose name bytes it holds, and a path naming nothing is
96/// `NotFound` — never the root by accident (#418).
97///
98/// The `&str` functions above are this with `str::as_bytes`, so a caller
99/// passing UTF-8 sees no difference.
100pub fn lookup_bytes_with_csum<F>(
101    dev: &dyn BlockDevice,
102    sb: &Superblock,
103    read_inode: &mut F,
104    path: &[u8],
105    csum: &crate::checksum::Checksummer,
106) -> Result<u32>
107where
108    F: FnMut(u32) -> Result<Inode>,
109{
110    let mut current_ino: u32 = EXT4_ROOT_INODE;
111
112    for name in split_path(path) {
113        let inode = read_inode(current_ino)?;
114        if !inode.is_dir() {
115            return Err(Error::NotADirectory);
116        }
117        current_ino = find_entry(dev, sb, current_ino, &inode, name, csum)?;
118    }
119
120    Ok(current_ino)
121}
122
123/// Read one directory's entries and return the inode number matching `name`.
124///
125/// Routes through the htree fast path when the directory has the
126/// `EXT4_INDEX_FL` flag, falling back to a full linear scan otherwise.
127pub(crate) fn find_entry(
128    dev: &dyn BlockDevice,
129    sb: &Superblock,
130    dir_ino: u32,
131    dir_inode: &Inode,
132    name: &[u8],
133    csum: &crate::checksum::Checksummer,
134) -> Result<u32> {
135    crate::file_io::refuse_encrypted_names(dir_inode)?;
136    // Both extent-backed and legacy direct/indirect-backed directories are
137    // supported here — `find_entry_linear` and `find_entry_htree` use
138    // `indirect::map_logical_any` for flavor-aware logical→physical mapping.
139    if dir_inode.has_inline_data() {
140        return find_inline(dev, sb, dir_ino, dir_inode, name, csum);
141    }
142
143    let has_filetype = sb.feature_incompat & crate::features::Incompat::FILETYPE.bits() != 0;
144    let block_size = sb.block_size();
145
146    // HTree fast path: indexed directories (EXT4_INDEX_FL = 0x1000).
147    if (dir_inode.flags & InodeFlags::INDEX.bits()) != 0 {
148        if let Some(found) =
149            find_entry_htree(dev, sb, dir_ino, dir_inode, name, has_filetype, csum)?
150        {
151            return Ok(found);
152        }
153        // htree said "not in expected leaf" — fall through to a full linear
154        // scan as a safety net (covers edge cases / corruption).
155    }
156
157    find_entry_linear(
158        dev,
159        sb,
160        dir_ino,
161        dir_inode,
162        name,
163        has_filetype,
164        block_size,
165        csum,
166    )
167}
168
169/// Linear scan of every directory data block.
170#[allow(clippy::too_many_arguments)]
171fn find_entry_linear(
172    dev: &dyn BlockDevice,
173    _sb: &Superblock,
174    dir_ino: u32,
175    dir_inode: &Inode,
176    name: &[u8],
177    has_filetype: bool,
178    block_size: u32,
179    csum: &crate::checksum::Checksummer,
180) -> Result<u32> {
181    let dir_size = dir_inode.size;
182    let total_blocks = dir_size.div_ceil(block_size as u64);
183    let gen = dir_inode.generation;
184
185    let mut block = vec![0u8; block_size as usize];
186    for logical in 0..total_blocks {
187        let phys = match indirect::map_logical_any(
188            &dir_inode.block,
189            dir_inode.flags,
190            dev,
191            block_size,
192            logical,
193        )? {
194            Some(p) => p,
195            None => continue,
196        };
197        dev.read_at(phys * block_size as u64, &mut block)?;
198
199        // The first block of an indexed dir is the dx_root and *cannot* be
200        // parsed as linear entries (its contents after "." and ".." are
201        // dx_entry records, not dir entries). Skip parse errors there.
202        match dir::parse_block_verified(&block, has_filetype, dir_ino, gen, csum) {
203            Ok(entries) => {
204                for entry in entries {
205                    if entry.name == name {
206                        return Ok(entry.inode);
207                    }
208                }
209            }
210            Err(_) if logical == 0 && (dir_inode.flags & InodeFlags::INDEX.bits()) != 0 => {
211                // dx_root in an indexed dir — only "." and ".." matter here,
212                // and find_entry_htree already handled the indexed path.
213                continue;
214            }
215            Err(e) => return Err(e),
216        }
217    }
218
219    Err(Error::NotFound)
220}
221
222/// HTree-indexed lookup. Returns:
223///   Ok(Some(ino)) — found
224///   Ok(None)      — htree said not in any leaf (caller may fall back)
225///   Err(..)       — corruption or I/O error
226fn find_entry_htree(
227    dev: &dyn BlockDevice,
228    sb: &Superblock,
229    dir_ino: u32,
230    dir_inode: &Inode,
231    name: &[u8],
232    has_filetype: bool,
233    csum: &crate::checksum::Checksummer,
234) -> Result<Option<u32>> {
235    let block_size = sb.block_size();
236
237    // Read logical block 0 of the directory: the dx_root. Flavor-aware
238    // dispatch — htree directories on ext2/3 (rare but legal) use the
239    // legacy block-map scheme, not extents.
240    let phys0 =
241        match indirect::map_logical_any(&dir_inode.block, dir_inode.flags, dev, block_size, 0)? {
242            Some(p) => p,
243            None => return Ok(None),
244        };
245    let mut root_block = vec![0u8; block_size as usize];
246    dev.read_at(phys0 * block_size as u64, &mut root_block)?;
247
248    // Walk the htree. lookup_leaf needs a closure for reading further dx
249    // blocks (intermediate nodes); we map logical→physical via whichever
250    // block-map scheme the dir's inode uses.
251    let read_dx_block = |logical: u32| -> Result<Vec<u8>> {
252        let phys = indirect::map_logical_any(
253            &dir_inode.block,
254            dir_inode.flags,
255            dev,
256            block_size,
257            logical as u64,
258        )?
259        .ok_or(Error::CorruptDirEntry("htree pointed at sparse block"))?;
260        let mut buf = vec![0u8; block_size as usize];
261        dev.read_at(phys * block_size as u64, &mut buf)?;
262        Ok(buf)
263    };
264
265    let leaf_logical = match htree::lookup_leaf_with(
266        name,
267        &root_block,
268        &sb.hash_seed,
269        sb.unsigned_hash(),
270        read_dx_block,
271    )? {
272        Some(b) => b,
273        None => return Ok(None),
274    };
275
276    // Read the leaf block and linear-scan it for the name.
277    let phys = indirect::map_logical_any(
278        &dir_inode.block,
279        dir_inode.flags,
280        dev,
281        block_size,
282        leaf_logical as u64,
283    )?
284    .ok_or(Error::CorruptDirEntry("htree leaf at sparse block"))?;
285    let mut leaf = vec![0u8; block_size as usize];
286    dev.read_at(phys * block_size as u64, &mut leaf)?;
287
288    // Verify the leaf block's csum tail (if present) before scanning.
289    if csum.enabled
290        && dir::has_csum_tail(&leaf)
291        && !csum.verify_dir_entry_tail(dir_ino, dir_inode.generation, &leaf)
292    {
293        return Err(Error::BadChecksum {
294            what: "directory block",
295        });
296    }
297
298    for entry in dir::DirBlockIter::new(&leaf, has_filetype) {
299        let entry: DirEntry = entry?;
300        if entry.name == name {
301            return Ok(Some(entry.inode));
302        }
303    }
304
305    // Name not in the htree-selected leaf. Could be hash collision spilling
306    // to neighbouring leaf — caller will fall back to linear scan.
307    Ok(None)
308}
309
310/// Look `name` up in an inline-data directory: `.`, `..` from bytes 0..4 of
311/// `i_block`, the entries from byte 4, then -- only when the name is not
312/// among those and `i_size` says there is more -- the entries continued in
313/// the `system.data` xattr (#427).
314fn find_inline(
315    dev: &dyn BlockDevice,
316    sb: &Superblock,
317    dir_ino: u32,
318    dir_inode: &Inode,
319    name: &[u8],
320    csum: &crate::checksum::Checksummer,
321) -> Result<u32> {
322    let has_filetype = sb.feature_incompat & crate::features::Incompat::FILETYPE.bits() != 0;
323    let in_block = inline_data::dir_entries(dir_ino, dir_inode, &[], has_filetype)?;
324    if let Some(entry) = in_block.iter().find(|e| e.name == name) {
325        return Ok(entry.inode);
326    }
327    if dir_inode.size as usize <= inline_data::INLINE_BLOCK_SIZE {
328        return Err(Error::NotFound);
329    }
330    let raw = read_raw_inode(dev, sb, dir_ino, dir_inode, csum)?;
331    let continuation =
332        inline_data::dir_continuation(dev, dir_inode, &raw, sb.inode_size, sb.block_size())?;
333    for entry in dir::DirBlockIter::new(&continuation, has_filetype) {
334        let entry = entry?;
335        if entry.name == name {
336            return Ok(entry.inode);
337        }
338    }
339    Err(Error::NotFound)
340}
341
342/// The on-disk bytes of `ino`, for the in-inode xattr an inline directory
343/// continues into. The lookup's `read_inode` hands back a parsed [`Inode`]
344/// only, so the inode table is found through the one group descriptor it
345/// needs. The bytes must be the inode the lookup was given: checksum-valid
346/// when checksums are on, and with the same `i_block`, size and generation.
347fn read_raw_inode(
348    dev: &dyn BlockDevice,
349    sb: &Superblock,
350    ino: u32,
351    expected: &Inode,
352    csum: &crate::checksum::Checksummer,
353) -> Result<Vec<u8>> {
354    if ino == 0 || ino > sb.inodes_count || sb.inodes_per_group == 0 {
355        return Err(Error::InvalidInode(ino));
356    }
357    let group = u64::from((ino - 1) / sb.inodes_per_group);
358    if group >= sb.block_group_count() {
359        return Err(Error::InvalidInode(ino));
360    }
361    let block_size = u64::from(sb.block_size());
362    let desc_size = sb.desc_size as usize;
363    let (desc_block, desc_off) = sb.descriptor_location(group);
364    let mut table = vec![0u8; block_size as usize];
365    dev.read_at(
366        desc_block
367            .checked_mul(block_size)
368            .ok_or(Error::Corrupt("group descriptor block number overflow"))?,
369        &mut table,
370    )?;
371    let desc = table
372        .get(desc_off..desc_off + desc_size)
373        .ok_or(Error::Corrupt("group descriptor outside its block"))?;
374    let bgd = crate::bgd::BlockGroupDescriptor::parse(desc, sb.desc_size)?;
375    let inode_size = u64::from(sb.inode_size);
376    let local = u64::from((ino - 1) % sb.inodes_per_group);
377    let offset = bgd
378        .inode_table
379        .checked_mul(block_size)
380        .and_then(|b| b.checked_add(local * inode_size))
381        .ok_or(Error::Corrupt("inode table offset overflow"))?;
382    let mut raw = vec![0u8; inode_size as usize];
383    dev.read_at(offset, &mut raw)?;
384    let parsed = Inode::parse(&raw)?;
385    if csum.enabled && !csum.verify_inode(ino, parsed.generation, &raw) {
386        return Err(Error::BadChecksum { what: "inode" });
387    }
388    if parsed.block != expected.block
389        || parsed.size != expected.size
390        || parsed.generation != expected.generation
391    {
392        return Err(Error::Corrupt(
393            "inline directory: the inode table disagrees with the inode being looked up",
394        ));
395    }
396    Ok(raw)
397}
398
399/// Split "/foo/bar/baz" into ["foo", "bar", "baz"]. Empty components (from
400/// doubled slashes or leading/trailing slashes) are dropped.
401fn split_path(path: &[u8]) -> impl Iterator<Item = &[u8]> {
402    path.split(|&b| b == b'/').filter(|s| !s.is_empty())
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn split_path_basic() {
411        let split = |p: &'static [u8]| split_path(p).collect::<Vec<_>>();
412        let none: Vec<&[u8]> = Vec::new();
413        assert_eq!(split(b""), none);
414        assert_eq!(split(b"/"), none);
415        assert_eq!(split(b"/foo"), vec![&b"foo"[..]]);
416        assert_eq!(split(b"/foo/bar"), vec![&b"foo"[..], b"bar"]);
417        assert_eq!(split(b"foo/bar"), vec![&b"foo"[..], b"bar"]);
418        assert_eq!(split(b"/foo//bar/"), vec![&b"foo"[..], b"bar"]);
419        assert_eq!(split(b"///"), none);
420    }
421
422    /// A component is bytes: one that is not UTF-8 survives the split
423    /// exactly, rather than being decoded or dropped (#418).
424    #[test]
425    fn split_path_keeps_bytes_that_are_not_utf8() {
426        let parts: Vec<&[u8]> = split_path(b"/d\xff/caf\xe9.txt").collect();
427        assert_eq!(parts, vec![&b"d\xff"[..], b"caf\xe9.txt"]);
428    }
429
430    /// Tests that read a fixture from `test-disks/` (`chore fixtures`).
431    mod needs_host {
432        use super::*;
433        use crate::bgd;
434        use crate::block_io::FileDevice;
435        use crate::extent;
436        use crate::fs::Filesystem;
437        use std::sync::Arc;
438
439        /// Build a read_inode closure that reads raw bytes via Filesystem and
440        /// parses them through Inode::parse.
441        fn read_inode_fn(fs: &Filesystem) -> impl FnMut(u32) -> Result<Inode> + '_ {
442            move |ino: u32| {
443                let (block, offset) = bgd::locate_inode(&fs.sb, &fs.groups, ino)?;
444                let block_data = fs.read_block(block)?;
445                let inode_size = fs.sb.inode_size as usize;
446                let off = offset as usize;
447                Inode::parse(&block_data[off..off + inode_size])
448            }
449        }
450
451        #[test]
452        fn root_resolves_to_inode_2() {
453            let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
454            let file = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
455            let dev: Arc<dyn BlockDevice> = Arc::new(file);
456            let fs = Filesystem::mount(dev.clone()).expect("mount");
457            let mut reader = read_inode_fn(&fs);
458
459            for root_path in ["/", "", "///"] {
460                let ino = lookup(dev.as_ref(), &fs.sb, &mut reader, root_path)
461                    .unwrap_or_else(|e| panic!("lookup({root_path:?}) failed: {e}"));
462                assert_eq!(ino, EXT4_ROOT_INODE, "path {root_path:?}");
463            }
464        }
465
466        #[test]
467        fn missing_path_returns_not_found() {
468            let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
469            let file = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
470            let dev: Arc<dyn BlockDevice> = Arc::new(file);
471            let fs = Filesystem::mount(dev.clone()).expect("mount");
472            let mut reader = read_inode_fn(&fs);
473
474            let result = lookup(
475                dev.as_ref(),
476                &fs.sb,
477                &mut reader,
478                "/this-does-not-exist-xyz",
479            );
480            assert!(matches!(result, Err(Error::NotFound)), "got {result:?}");
481        }
482
483        #[test]
484        fn non_dir_component_returns_not_a_directory() {
485            let path = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), "ext4-basic.img");
486            let file = FileDevice::open(&path).unwrap_or_else(|e| panic!("open {path}: {e}"));
487            let dev: Arc<dyn BlockDevice> = Arc::new(file);
488            let fs = Filesystem::mount(dev.clone()).expect("mount");
489            let mut reader = read_inode_fn(&fs);
490
491            // Find any regular file in root so we can stack a component after it.
492            let root = reader(EXT4_ROOT_INODE).expect("root inode");
493            let block_size = fs.sb.block_size();
494            let total_blocks = root.size.div_ceil(block_size as u64);
495            let has_filetype =
496                fs.sb.feature_incompat & crate::features::Incompat::FILETYPE.bits() != 0;
497
498            let mut reg_file_name: Option<Vec<u8>> = None;
499            'outer: for logical in 0..total_blocks {
500                if let Some(phys) =
501                    extent::map_logical(&root.block, dev.as_ref(), block_size, logical)
502                        .expect("map logical")
503                {
504                    let mut blk = vec![0u8; block_size as usize];
505                    dev.read_at(phys * block_size as u64, &mut blk).unwrap();
506                    for entry in dir::DirBlockIter::new(&blk, has_filetype) {
507                        let e = entry.expect("entry");
508                        if e.file_type == dir::DirEntryType::RegFile {
509                            reg_file_name = Some(e.name);
510                            break 'outer;
511                        }
512                    }
513                }
514            }
515
516            let name = reg_file_name.expect(
517                "ext4-basic.img has a regular file (test.txt) in its root \
518             (test-disks/guest-build-images.sh)",
519            );
520            let name_str = std::str::from_utf8(&name).expect("name utf8");
521            let bad_path = format!("/{name_str}/child");
522
523            let result = lookup(dev.as_ref(), &fs.sb, &mut reader, &bad_path);
524            assert!(
525                matches!(result, Err(Error::NotADirectory)),
526                "got {result:?} for path {bad_path}"
527            );
528        }
529    }
530}