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//!
19//! A directory uses the same two areas with one difference: the first 4
20//! bytes of `i_block` are its parent's inode number, and its entries start
21//! at byte 4 (see [`dir_entries`]).
22
23use crate::block_io::BlockDevice;
24use crate::dir::{DirBlockIter, DirEntry, DirEntryType};
25use crate::error::{Error, Result};
26use crate::inode::Inode;
27use crate::xattr;
28
29/// Read the contents of an inline-data file in full.
30///
31/// Returns the concatenation of:
32/// 1. `inode.block` (60 bytes), truncated to the file's `size`
33/// 2. The `system.data` xattr value (if file size > 60)
34///
35/// Caller must verify the inode actually has `INLINE_DATA_FL` set before
36/// calling — otherwise the returned bytes are garbage (extent header etc.).
37pub fn read_all(
38    dev: &dyn BlockDevice,
39    inode: &Inode,
40    inode_raw: &[u8],
41    inode_size: u16,
42    block_size: u32,
43) -> Result<Vec<u8>> {
44    let total = inode.size as usize;
45
46    // Up to 60 bytes from i_block.
47    let inline_max = 60;
48    let from_block = total.min(inline_max);
49    // Reserved for what inline data can actually be -- the 60 bytes of
50    // i_block plus the in-inode xattr region, which is bounded by the
51    // inode size -- rather than for whatever `i_size` claimed. The
52    // vector grows to what is really there.
53    let mut out = Vec::with_capacity(total.min(64 * 1024));
54    out.extend_from_slice(&inode.block[..from_block]);
55
56    if total <= inline_max {
57        return Ok(out);
58    }
59
60    // Overflow lives in the system.data xattr.
61    //
62    // A missing or short xattr is CORRUPTION, not an empty tail. The
63    // inode's size field says the file is `total` bytes; if the bytes
64    // are not there, returning the 60 we do have would hand the caller
65    // a silently truncated file that still reports its full length —
66    // the worst of both, since nothing downstream can tell.
67    let need = total - inline_max;
68    let extra = xattr::get(dev, inode, inode_raw, inode_size, block_size, "system.data")?.ok_or(
69        crate::error::Error::Corrupt("inline file larger than 60 bytes has no system.data xattr"),
70    )?;
71    if extra.len() < need {
72        return Err(crate::error::Error::Corrupt(
73            "inline file's system.data xattr is shorter than its size claims",
74        ));
75    }
76    out.extend_from_slice(&extra[..need]);
77
78    Ok(out)
79}
80
81/// Bytes at the head of an inline directory's `i_block` that are not
82/// entries: the parent directory's inode number, the implicit `..`
83/// (format documentation, "Inline Directories": "The first four bytes of
84/// i_block are the inode number of the parent directory").
85pub const INLINE_DOTDOT_SIZE: usize = 4;
86
87/// Size of the `i_block` area that holds inline data.
88pub const INLINE_BLOCK_SIZE: usize = 60;
89
90/// The entries of an inline-data directory, in the order the kernel's
91/// readdir reports them: `.` (the directory itself), `..` (read from
92/// bytes 0..4 of `i_block`), the entries in `i_block[4..60]`, then those
93/// in `continuation` -- the `system.data` xattr value, empty when the
94/// directory fits in `i_block`.
95///
96/// An inline directory holds no `.` or `..` records: `.` is implicit, and
97/// `..` is a bare inode number. Entries start at byte 4, so parsing from
98/// byte 0 reads the parent's inode number as a record and fails with
99/// `bad rec_len` (#427).
100pub fn dir_entries(
101    dir_ino: u32,
102    inode: &Inode,
103    continuation: &[u8],
104    has_filetype: bool,
105) -> Result<Vec<DirEntry>> {
106    let parent = u32::from_le_bytes(inode.block[..INLINE_DOTDOT_SIZE].try_into().unwrap());
107    if parent == 0 {
108        return Err(Error::CorruptDirEntry(
109            "inline directory: parent inode number is 0",
110        ));
111    }
112    let mut out = vec![
113        DirEntry {
114            inode: dir_ino,
115            name: b".".to_vec(),
116            file_type: DirEntryType::Directory,
117        },
118        DirEntry {
119            inode: parent,
120            name: b"..".to_vec(),
121            file_type: DirEntryType::Directory,
122        },
123    ];
124    for area in [&inode.block[INLINE_DOTDOT_SIZE..], continuation] {
125        for entry in DirBlockIter::new(area, has_filetype) {
126            out.push(entry?);
127        }
128    }
129    Ok(out)
130}
131
132/// The `system.data` bytes an inline directory's entries continue into,
133/// or an empty vector when `i_size` says it fits in `i_block`. A size
134/// past 60 with no such xattr, or a shorter one, is corruption -- the
135/// same rule [`read_all`] applies to a file.
136pub fn dir_continuation(
137    dev: &dyn BlockDevice,
138    inode: &Inode,
139    inode_raw: &[u8],
140    inode_size: u16,
141    block_size: u32,
142) -> Result<Vec<u8>> {
143    let total = inode.size as usize;
144    if total <= INLINE_BLOCK_SIZE {
145        return Ok(Vec::new());
146    }
147    let need = total - INLINE_BLOCK_SIZE;
148    let mut extra = xattr::get(dev, inode, inode_raw, inode_size, block_size, "system.data")?
149        .ok_or(Error::Corrupt(
150            "inline directory larger than 60 bytes has no system.data xattr",
151        ))?;
152    if extra.len() < need {
153        return Err(Error::Corrupt(
154            "inline directory's system.data xattr is shorter than its size claims",
155        ));
156    }
157    extra.truncate(need);
158    Ok(extra)
159}
160
161/// Every entry of the inline-data directory `dir_ino`: [`dir_entries`]
162/// over `i_block` and its [`dir_continuation`].
163pub fn read_dir(
164    dev: &dyn BlockDevice,
165    dir_ino: u32,
166    inode: &Inode,
167    inode_raw: &[u8],
168    inode_size: u16,
169    block_size: u32,
170    has_filetype: bool,
171) -> Result<Vec<DirEntry>> {
172    let continuation = dir_continuation(dev, inode, inode_raw, inode_size, block_size)?;
173    dir_entries(dir_ino, inode, &continuation, has_filetype)
174}
175
176/// Read a range from an inline-data file.
177/// Returns the bytes copied into `dst`, or `Ok(0)` if `offset >= size`.
178pub fn read_range(
179    dev: &dyn BlockDevice,
180    inode: &Inode,
181    inode_raw: &[u8],
182    inode_size: u16,
183    block_size: u32,
184    offset: u64,
185    dst: &mut [u8],
186) -> Result<usize> {
187    let total = inode.size;
188    if offset >= total {
189        return Ok(0);
190    }
191    let full = read_all(dev, inode, inode_raw, inode_size, block_size)?;
192    let want = ((total - offset) as usize).min(dst.len());
193    let avail = full.len().saturating_sub(offset as usize);
194    let n = want.min(avail);
195    dst[..n].copy_from_slice(&full[offset as usize..offset as usize + n]);
196    Ok(n)
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::inode::{Inode, OFF_MODE, OFF_SIZE_LO};
203
204    /// A device with nothing on it. The inline path only reaches the
205    /// device to look for the `system.data` xattr, and these tests are
206    /// about what happens when that xattr is not there.
207    struct EmptyDev;
208
209    impl BlockDevice for EmptyDev {
210        fn read_at(&self, _off: u64, buf: &mut [u8]) -> Result<()> {
211            buf.fill(0);
212            Ok(())
213        }
214        fn write_at(&self, _off: u64, _buf: &[u8]) -> Result<()> {
215            Ok(())
216        }
217        fn size_bytes(&self) -> u64 {
218            1 << 20
219        }
220    }
221
222    /// A 128-byte inode claiming `size` bytes of a regular file, with
223    /// `i_block` filled with a recognisable pattern.
224    fn inline_inode(size: u32) -> (Inode, Vec<u8>) {
225        let mut raw = vec![0u8; 128];
226        raw[OFF_MODE..OFF_MODE + 2].copy_from_slice(&0o100_644u16.to_le_bytes());
227        raw[OFF_SIZE_LO..OFF_SIZE_LO + 4].copy_from_slice(&size.to_le_bytes());
228        // i_block at 0x28, 60 bytes.
229        for (i, b) in raw[0x28..0x28 + 60].iter_mut().enumerate() {
230            *b = b'A'.wrapping_add((i % 26) as u8);
231        }
232        let inode = Inode::parse(&raw).expect("parse synthetic inode");
233        (inode, raw)
234    }
235
236    /// A file that fits entirely in `i_block` needs no xattr, so an
237    /// empty device is fine.
238    #[test]
239    fn a_file_within_the_inline_area_reads_without_an_xattr() {
240        let (inode, raw) = inline_inode(60);
241        let got =
242            read_all(&EmptyDev, &inode, &raw, 128, 4096).expect("60 bytes are all in i_block");
243        assert_eq!(got.len(), 60);
244    }
245
246    /// **The fix.** A file claiming more than 60 bytes whose
247    /// `system.data` xattr is absent is corrupt, and must say so.
248    ///
249    /// Before this, the missing xattr was skipped silently and the
250    /// caller received 60 bytes for a file whose size field said more
251    /// — a truncated read that nothing downstream could detect,
252    /// because the length in the metadata still claimed the full size.
253    #[test]
254    fn a_missing_spill_xattr_is_corruption_not_an_empty_tail() {
255        let (inode, raw) = inline_inode(100);
256        let err = read_all(&EmptyDev, &inode, &raw, 128, 4096)
257            .expect_err("100 bytes cannot fit in the 60-byte inline area without a spill");
258        assert!(
259            format!("{err:?}").contains("system.data"),
260            "the error should name the missing xattr, got: {err:?}"
261        );
262    }
263}