Skip to main content

fs_ext4/
inline_data.rs

1//! Inline data reading.
2//!
3//! Spec: kernel.org/doc/html/latest/filesystems/ext4/inlinedata.html
4//!
5//! When the `INCOMPAT_INLINE_DATA` feature is enabled and an inode has the
6//! `EXT4_INLINE_DATA_FL` flag, the file's contents live inside the inode
7//! itself instead of being stored in extent-allocated data blocks.
8//!
9//! Layout:
10//!   - First **60 bytes** of the file are stored in the `i_block[60]` array
11//!     (the same field that normally holds the extent header / direct block
12//!     pointers).
13//!   - If the file is larger than 60 bytes, the remainder is stored as the
14//!     value of a special xattr named `system.data`. Concatenate the two to
15//!     get the full file content.
16//!   - Maximum inline file size = 60 + (in-inode-xattr-region-size minus
17//!     headers and other entries). Typically 60–~150 bytes for inode_size=256.
18
19use crate::block_io::BlockDevice;
20use crate::error::Result;
21use crate::inode::Inode;
22use crate::xattr;
23
24/// Read the contents of an inline-data file in full.
25///
26/// Returns the concatenation of:
27/// 1. `inode.block` (60 bytes), truncated to the file's `size`
28/// 2. The `system.data` xattr value (if file size > 60)
29///
30/// Caller must verify the inode actually has `INLINE_DATA_FL` set before
31/// calling — otherwise the returned bytes are garbage (extent header etc.).
32pub fn read_all(
33    dev: &dyn BlockDevice,
34    inode: &Inode,
35    inode_raw: &[u8],
36    inode_size: u16,
37    block_size: u32,
38) -> Result<Vec<u8>> {
39    let total = inode.size as usize;
40
41    // Up to 60 bytes from i_block.
42    let inline_max = 60;
43    let from_block = total.min(inline_max);
44    // Reserved for what inline data can actually be -- the 60 bytes of
45    // i_block plus the in-inode xattr region, which is bounded by the
46    // inode size -- rather than for whatever `i_size` claimed. The
47    // vector grows to what is really there.
48    let mut out = Vec::with_capacity(total.min(64 * 1024));
49    out.extend_from_slice(&inode.block[..from_block]);
50
51    if total <= inline_max {
52        return Ok(out);
53    }
54
55    // Overflow lives in the system.data xattr.
56    //
57    // A missing or short xattr is CORRUPTION, not an empty tail. The
58    // inode's size field says the file is `total` bytes; if the bytes
59    // are not there, returning the 60 we do have would hand the caller
60    // a silently truncated file that still reports its full length —
61    // the worst of both, since nothing downstream can tell.
62    let need = total - inline_max;
63    let extra = xattr::get(dev, inode, inode_raw, inode_size, block_size, "system.data")?.ok_or(
64        crate::error::Error::Corrupt("inline file larger than 60 bytes has no system.data xattr"),
65    )?;
66    if extra.len() < need {
67        return Err(crate::error::Error::Corrupt(
68            "inline file's system.data xattr is shorter than its size claims",
69        ));
70    }
71    out.extend_from_slice(&extra[..need]);
72
73    Ok(out)
74}
75
76/// Read a range from an inline-data file.
77/// Returns the bytes copied into `dst`, or `Ok(0)` if `offset >= size`.
78pub fn read_range(
79    dev: &dyn BlockDevice,
80    inode: &Inode,
81    inode_raw: &[u8],
82    inode_size: u16,
83    block_size: u32,
84    offset: u64,
85    dst: &mut [u8],
86) -> Result<usize> {
87    let total = inode.size;
88    if offset >= total {
89        return Ok(0);
90    }
91    let full = read_all(dev, inode, inode_raw, inode_size, block_size)?;
92    let want = ((total - offset) as usize).min(dst.len());
93    let avail = full.len().saturating_sub(offset as usize);
94    let n = want.min(avail);
95    dst[..n].copy_from_slice(&full[offset as usize..offset as usize + n]);
96    Ok(n)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::inode::{Inode, OFF_MODE, OFF_SIZE_LO};
103
104    /// A device with nothing on it. The inline path only reaches the
105    /// device to look for the `system.data` xattr, and these tests are
106    /// about what happens when that xattr is not there.
107    struct EmptyDev;
108
109    impl BlockDevice for EmptyDev {
110        fn read_at(&self, _off: u64, buf: &mut [u8]) -> Result<()> {
111            buf.fill(0);
112            Ok(())
113        }
114        fn write_at(&self, _off: u64, _buf: &[u8]) -> Result<()> {
115            Ok(())
116        }
117        fn size_bytes(&self) -> u64 {
118            1 << 20
119        }
120    }
121
122    /// A 128-byte inode claiming `size` bytes of a regular file, with
123    /// `i_block` filled with a recognisable pattern.
124    fn inline_inode(size: u32) -> (Inode, Vec<u8>) {
125        let mut raw = vec![0u8; 128];
126        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&0o100_644u16.to_le_bytes());
127        raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].copy_from_slice(&size.to_le_bytes());
128        // i_block at 0x28, 60 bytes.
129        for (i, b) in raw[0x28..0x28 + 60].iter_mut().enumerate() {
130            *b = b'A'.wrapping_add((i % 26) as u8);
131        }
132        let inode = Inode::parse(&raw).expect("parse synthetic inode");
133        (inode, raw)
134    }
135
136    /// A file that fits entirely in `i_block` needs no xattr, so an
137    /// empty device is fine.
138    #[test]
139    fn a_file_within_the_inline_area_reads_without_an_xattr() {
140        let (inode, raw) = inline_inode(60);
141        let got =
142            read_all(&EmptyDev, &inode, &raw, 128, 4096).expect("60 bytes are all in i_block");
143        assert_eq!(got.len(), 60);
144    }
145
146    /// **The fix.** A file claiming more than 60 bytes whose
147    /// `system.data` xattr is absent is corrupt, and must say so.
148    ///
149    /// Before this, the missing xattr was skipped silently and the
150    /// caller received 60 bytes for a file whose size field said more
151    /// — a truncated read that nothing downstream could detect,
152    /// because the length in the metadata still claimed the full size.
153    #[test]
154    fn a_missing_spill_xattr_is_corruption_not_an_empty_tail() {
155        let (inode, raw) = inline_inode(100);
156        let err = read_all(&EmptyDev, &inode, &raw, 128, 4096)
157            .expect_err("100 bytes cannot fit in the 60-byte inline area without a spill");
158        assert!(
159            format!("{err:?}").contains("system.data"),
160            "the error should name the missing xattr, got: {err:?}"
161        );
162    }
163}