Skip to main content

fs_ext4/
xattr.rs

1//! Extended attribute (xattr) reading.
2//!
3//! Spec: kernel.org/doc/html/latest/filesystems/ext4/dynamic.html#extended-attributes
4//!
5//! ext4 stores xattrs in two places:
6//!
7//! 1. **In-inode** — between the end of the base 128-byte inode + extra_isize
8//!    region and the end of the on-disk inode (when inode_size > 128).
9//!    Starts with a 4-byte header containing the magic 0xEA020000.
10//!
11//! 2. **External xattr block** — when more space is needed, `i_file_acl`
12//!    (combined hi+lo, 48-bit physical block number) points at a single
13//!    block whose layout is: 32-byte header (magic 0xEA020000 + refcount
14//!    + ...) followed by `ext4_xattr_entry` records growing forward, with
15//!    values stored from the END of the block growing backward.
16//!
17//! Entry layout (variable size, padded to 4 bytes):
18//!   0x00 u8  e_name_len           (length of name in bytes, no NUL)
19//!   0x01 u8  e_name_index         (namespace prefix code; see NAME_PREFIX)
20//!   0x02 u16 e_value_offs         (offset within the block where value lives)
21//!   0x04 u32 e_value_inum         (if EA_INODE feature: inode holding the value)
22//!   0x08 u32 e_value_size         (length of value in bytes)
23//!   0x0C u32 e_hash               (hash of name + value)
24//!   0x10 ..  e_name (e_name_len bytes, no NUL, padded to 4)
25//!
26//! Phase 1: read-only, in-inode + external block. Hash verification + EA_INODE
27//! large-value support deferred.
28
29use crate::block_io::BlockDevice;
30use crate::error::{Error, Result};
31use crate::fs::Filesystem;
32use crate::inode::Inode;
33
34/// Magic number at the start of an xattr region (in-inode or external block).
35pub const EXT4_XATTR_MAGIC: u32 = 0xEA02_0000;
36
37/// Standard namespace prefixes (`e_name_index` value → string).
38pub const NAME_PREFIXES: &[(u8, &str)] = &[
39    (1, "user."),
40    (2, "system.posix_acl_access"),
41    (3, "system.posix_acl_default"),
42    (4, "trusted."),
43    (5, "lustre."),
44    (6, "security."),
45    (7, "system."),
46    (8, "system.richacl"),
47];
48
49/// Look up the human-readable prefix for a numeric name_index.
50pub fn prefix_for_index(idx: u8) -> Option<&'static str> {
51    NAME_PREFIXES
52        .iter()
53        .find(|(i, _)| *i == idx)
54        .map(|(_, s)| *s)
55}
56
57/// One parsed xattr entry: fully-qualified name + raw value bytes.
58///
59/// `#[non_exhaustive]` (#120): the fields stay public to read, but a struct
60/// literal outside this crate is refused, so the next field the on-disk entry
61/// needs is not a break. Build one with [`XattrEntry::new`] or
62/// [`XattrEntry::in_ea_inode`].
63///
64/// Equality covers every field, `value_inum` and `value_size` included: an
65/// entry whose value lives in an EA inode never equals an inline one, even
66/// with the same name.
67#[derive(Debug, Clone, PartialEq, Eq)]
68#[non_exhaustive]
69pub struct XattrEntry {
70    /// Fully-qualified name, e.g. "user.com.apple.FinderInfo".
71    pub name: String,
72    /// Raw value bytes (Finder uses binary data, ACLs are binary, etc.).
73    ///
74    /// **Empty when `value_inum` is non-zero**, because the bytes are not
75    /// in the region this was parsed from. See [`read_all`].
76    pub value: Vec<u8>,
77    /// `e_value_inum`. Zero for an ordinary entry whose value is stored
78    /// inline. Non-zero when `INCOMPAT_EA_INODE` is in play and the value
79    /// lives in the file body of that inode instead — in which case
80    /// `e_value_offs` describes nothing and `value` above is empty.
81    ///
82    /// Resolving it needs a [`crate::fs::Filesystem`], which the
83    /// buffer-level parsers do not have; [`read_all_resolved`] is the
84    /// entry point that follows it.
85    pub value_inum: u32,
86    /// `e_value_size` — how long the entry says its value is.
87    ///
88    /// KEPT EVEN THOUGH THE INLINE CASE HAS ALREADY USED IT, because
89    /// the EA-inode case has not: the value then comes from another
90    /// inode's body, and without this there is nothing to compare what
91    /// was read against. The kernel makes exactly that comparison and
92    /// fails with `EUCLEAN` when the two disagree; this driver
93    /// returned the EA inode's whole body and
94    /// reported success (#121).
95    pub value_size: u32,
96}
97
98impl XattrEntry {
99    /// An entry whose value is stored inline, as the parsers produce one:
100    /// `value_inum` zero and `value_size` the value's length.
101    ///
102    /// Panics if the value is longer than `u32::MAX` bytes, which no ext4
103    /// entry can describe.
104    pub fn new(name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
105        let value = value.into();
106        let value_size = u32::try_from(value.len()).expect("an xattr value fits in u32");
107        Self {
108            name: name.into(),
109            value,
110            value_inum: 0,
111            value_size,
112        }
113    }
114
115    /// An entry whose value lives in the body of EA inode `value_inum`, as
116    /// the parsers produce one: `value` empty, and `value_size` the length
117    /// the entry declares.
118    pub fn in_ea_inode(name: impl Into<String>, value_inum: u32, value_size: u32) -> Self {
119        Self {
120            name: name.into(),
121            value: Vec::new(),
122            value_inum,
123            value_size,
124        }
125    }
126}
127
128/// Read all extended attributes attached to an inode.
129///
130/// `inode` is the parsed metadata (we need `file_acl` for the external xattr
131/// block). `inode_raw` is the on-disk inode bytes (we need bytes past offset
132/// 128 + i_extra_isize for the in-inode xattr region).
133///
134/// Returns entries from in-inode area first, then external xattr block (if any).
135/// An inode with no xattrs returns `Ok(vec![])`.
136pub fn read_all(
137    dev: &dyn BlockDevice,
138    inode: &Inode,
139    inode_raw: &[u8],
140    inode_size: u16,
141    block_size: u32,
142) -> Result<Vec<XattrEntry>> {
143    let mut out = Vec::new();
144
145    // 1. In-inode xattrs: data between end of i_extra_isize area and end of inode.
146    if inode_raw.len() >= 128 + 4 {
147        let extra_isize = u16::from_le_bytes(inode_raw[128..130].try_into().unwrap()) as usize;
148        let xattr_region_start = 128 + extra_isize;
149        // `i_extra_isize = 0` means the extra fields are unused and there
150        // is no in-inode area: the format documentation places the area
151        // after the `i_extra_isize` bytes ("Inode Size"), the kernel and
152        // e2fsprogs see none, and neither does this. Bytes at 0x80 that look
153        // like one are
154        // not an attribute anybody else can see (#380).
155        if extra_isize != 0
156            && xattr_region_start + 4 <= inode_size as usize
157            && xattr_region_start + 4 <= inode_raw.len()
158        {
159            let region = &inode_raw[xattr_region_start..(inode_size as usize).min(inode_raw.len())];
160            let magic = u32::from_le_bytes(region[..4].try_into().unwrap());
161            if magic == EXT4_XATTR_MAGIC {
162                // Entries follow the 4-byte magic; values are at e_value_offs from
163                // the start of the entry table (== start of region + 4).
164                parse_entries(&region[4..], region.len() - 4, &mut out)?;
165            }
166        }
167    }
168
169    // 2. External xattr block: i_file_acl (combined hi+lo) → block number.
170    if inode.file_acl != 0 {
171        let mut buf = vec![0u8; block_size as usize];
172        dev.read_at(inode.file_acl * block_size as u64, &mut buf)?;
173        let magic = u32::from_le_bytes(buf[..4].try_into().unwrap());
174        if magic != EXT4_XATTR_MAGIC {
175            return Err(Error::Corrupt("xattr block magic mismatch"));
176        }
177        // External block layout: 32-byte header, then entries; values offset
178        // is from the start of the block (NOT from end-of-header).
179        parse_entries_block(&buf, &mut out)?;
180    }
181
182    Ok(out)
183}
184
185/// Parse entries from the in-inode xattr area.
186///
187/// `entries_buf` starts immediately AFTER the 4-byte magic header.
188/// In the in-inode format, `e_value_offs` is measured from the start of the
189/// entries area (i.e. directly indexes into `entries_buf`). Values are packed
190/// backward from the end of the entries area while entries grow forward.
191fn parse_entries(entries_buf: &[u8], _region_len: usize, out: &mut Vec<XattrEntry>) -> Result<()> {
192    let mut pos = 0;
193    while pos + 16 <= entries_buf.len() {
194        // The list ends at an entry whose leading fields are zero
195        // (attributes.html: "the first four fields ... are set to zero to
196        // mark the end"); the first 4-byte word, name length, name index
197        // and value offset, is what is read. We cannot short-circuit on
198        // name_len == 0 alone, because ACL xattrs (name_index 2 / 3 for
199        // system.posix_acl_{access,default}) legitimately store name_len=0
200        // — their full name is implied by the index.
201        let header = u32::from_le_bytes(entries_buf[pos..pos + 4].try_into().unwrap());
202        if header == 0 {
203            break;
204        }
205        let name_len = entries_buf[pos] as usize;
206        let name_index = entries_buf[pos + 1];
207        let value_offs =
208            u16::from_le_bytes(entries_buf[pos + 2..pos + 4].try_into().unwrap()) as usize;
209        let value_inum = u32::from_le_bytes(entries_buf[pos + 4..pos + 8].try_into().unwrap());
210        let value_size =
211            u32::from_le_bytes(entries_buf[pos + 8..pos + 12].try_into().unwrap()) as usize;
212        // pos+12..pos+16 = e_hash (ignored)
213
214        let entry_size = 16 + name_len;
215        let entry_padded = (entry_size + 3) & !3;
216        if pos + 16 + name_len > entries_buf.len() {
217            return Err(Error::Corrupt("xattr entry name overruns region"));
218        }
219
220        let name_bytes = &entries_buf[pos + 16..pos + 16 + name_len];
221        let prefix = prefix_for_index(name_index).unwrap_or("");
222        let suffix =
223            std::str::from_utf8(name_bytes).map_err(|_| Error::Corrupt("xattr name not utf-8"))?;
224        let full_name = format!("{prefix}{suffix}");
225
226        // AN EA-INODE ENTRY'S VALUE IS NOT HERE. `e_value_offs` describes
227        // nothing once `e_value_inum` is set, so slicing at it returns
228        // whatever happens to sit at that offset — quite possibly another
229        // attribute's value, which is worse than an error because the
230        // caller will act on it.
231        let value = if value_inum != 0 {
232            Vec::new()
233        } else if value_size > 0 {
234            if value_offs + value_size > entries_buf.len() {
235                return Err(Error::Corrupt("xattr value out of range"));
236            }
237            entries_buf[value_offs..value_offs + value_size].to_vec()
238        } else {
239            Vec::new()
240        };
241
242        out.push(XattrEntry {
243            name: full_name,
244            value,
245            value_inum,
246            value_size: value_size as u32,
247        });
248
249        pos += entry_padded;
250    }
251    Ok(())
252}
253
254/// Parse entries from a full external xattr block.
255/// Block layout: 32-byte header at offset 0, then entries starting at 32.
256/// `e_value_offs` here is from the START of the block, not the entry table.
257fn parse_entries_block(block: &[u8], out: &mut Vec<XattrEntry>) -> Result<()> {
258    if block.len() < 32 {
259        return Err(Error::Corrupt("xattr block too small"));
260    }
261
262    let mut pos = 32; // skip the 32-byte header
263    while pos + 16 <= block.len() {
264        // See parse_entries: terminator = first 4-byte header word all zero,
265        // NOT name_len == 0 (which is legal for ACL entries).
266        let header = u32::from_le_bytes(block[pos..pos + 4].try_into().unwrap());
267        if header == 0 {
268            break;
269        }
270        let name_len = block[pos] as usize;
271        let name_index = block[pos + 1];
272        let value_offs = u16::from_le_bytes(block[pos + 2..pos + 4].try_into().unwrap()) as usize;
273        let value_inum = u32::from_le_bytes(block[pos + 4..pos + 8].try_into().unwrap());
274        let value_size = u32::from_le_bytes(block[pos + 8..pos + 12].try_into().unwrap()) as usize;
275
276        let entry_size = 16 + name_len;
277        let entry_padded = (entry_size + 3) & !3;
278        if pos + 16 + name_len > block.len() {
279            return Err(Error::Corrupt("xattr entry name overruns block"));
280        }
281
282        let name_bytes = &block[pos + 16..pos + 16 + name_len];
283        let prefix = prefix_for_index(name_index).unwrap_or("");
284        let suffix =
285            std::str::from_utf8(name_bytes).map_err(|_| Error::Corrupt("xattr name not utf-8"))?;
286        let full_name = format!("{prefix}{suffix}");
287
288        // See `parse_entries`: with `e_value_inum` set the value is in
289        // another inode's body, not at `e_value_offs` in this block.
290        let value = if value_inum != 0 {
291            Vec::new()
292        } else if value_size > 0 {
293            if value_offs + value_size > block.len() {
294                return Err(Error::Corrupt("xattr block value out of range"));
295            }
296            block[value_offs..value_offs + value_size].to_vec()
297        } else {
298            Vec::new()
299        };
300
301        out.push(XattrEntry {
302            name: full_name,
303            value,
304            value_inum,
305            value_size: value_size as u32,
306        });
307
308        pos += entry_padded;
309    }
310    Ok(())
311}
312
313/// Split a fully-qualified xattr name (e.g. `"user.com.apple.FinderInfo"`)
314/// into (name_index, suffix). Returns `None` if no known prefix matches.
315pub fn split_qualified_name(name: &str) -> Option<(u8, &str)> {
316    for (idx, prefix) in NAME_PREFIXES {
317        if let Some(rest) = name.strip_prefix(*prefix) {
318            return Some((*idx, rest));
319        }
320    }
321    None
322}
323
324/// Result of [`plan_remove_in_inode_region`]: the entry was either removed
325/// (bytes in place updated) or wasn't present.
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub enum RemoveOutcome {
328    /// Region rewritten; caller must patch the inode checksum + write back.
329    Removed,
330    /// The name wasn't in this region.
331    NotFound,
332}
333
334/// Remove an xattr by fully-qualified name from the in-inode region.
335/// The `region` slice must span from the 4-byte magic (inclusive) to the
336/// end of the inode (exclusive of any later metadata). Returns `Removed`
337/// if the name was present and the bytes have been rewritten, `NotFound`
338/// otherwise. `Error::InvalidArgument` if the name lacks a known
339/// namespace prefix.
340pub fn plan_remove_in_inode_region(region: &mut [u8], name: &str) -> Result<RemoveOutcome> {
341    let Some((name_index, suffix)) = split_qualified_name(name) else {
342        return Err(Error::InvalidArgument(
343            "xattr name missing known namespace prefix",
344        ));
345    };
346    if region.len() < 4 {
347        return Ok(RemoveOutcome::NotFound);
348    }
349    let magic = u32::from_le_bytes(region[..4].try_into().unwrap());
350    if magic != EXT4_XATTR_MAGIC {
351        return Ok(RemoveOutcome::NotFound);
352    }
353
354    // Decode every entry (header + name + value bytes).
355    let entries = decode_in_inode_entries(&region[4..])?;
356    refuse_if_any_ea_inode_backed(&entries)?;
357    let before = entries.len();
358    let kept: Vec<DecodedEntry> = entries
359        .into_iter()
360        .filter(|e| !(e.name_index == name_index && e.name_bytes == suffix.as_bytes()))
361        .collect();
362    if kept.len() == before {
363        return Ok(RemoveOutcome::NotFound);
364    }
365
366    encode_in_inode_entries(region, &kept);
367    Ok(RemoveOutcome::Removed)
368}
369
370/// Result of [`plan_set_in_inode_region`]: the entry was either
371/// inserted (no previous entry with this name) or replaced (new value
372/// overwrote an existing entry's value).
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum SetOutcome {
375    Inserted,
376    Replaced,
377}
378
379/// Set (create-or-replace) an xattr entry in the in-inode region.
380/// `region` spans from the 4-byte magic to the end of the inode image.
381/// On success the bytes have been rewritten to include the new entry.
382///
383/// Errors:
384/// - `Error::InvalidArgument` when `name` lacks a known namespace prefix
385///   or the suffix is empty (except for ACL namespaces 2 + 3).
386/// - `Error::NameTooLong` when the suffix is longer than 255 bytes.
387/// - `Error::NoSpaceLeftOnDevice` when the rewritten region wouldn't fit
388///   (entries + values + 4-byte terminator > region capacity).
389pub fn plan_set_in_inode_region(region: &mut [u8], name: &str, value: &[u8]) -> Result<SetOutcome> {
390    let Some((name_index, suffix)) = split_qualified_name(name) else {
391        return Err(Error::InvalidArgument(
392            "xattr name missing known namespace prefix",
393        ));
394    };
395    if suffix.is_empty() && !matches!(name_index, 2 | 3) {
396        return Err(Error::InvalidArgument("xattr name suffix is empty"));
397    }
398    if suffix.len() > 255 {
399        return Err(Error::NameTooLong);
400    }
401    if region.len() < 8 {
402        return Err(Error::NoSpaceLeftOnDevice);
403    }
404
405    let magic_present = {
406        let m = u32::from_le_bytes(region[..4].try_into().unwrap());
407        m == EXT4_XATTR_MAGIC
408    };
409    let mut entries = if magic_present {
410        decode_in_inode_entries(&region[4..])?
411    } else {
412        Vec::new()
413    };
414    refuse_if_any_ea_inode_backed(&entries)?;
415
416    let mut outcome = SetOutcome::Inserted;
417    let suffix_bytes = suffix.as_bytes();
418    for e in entries.iter_mut() {
419        if e.name_index == name_index && e.name_bytes == suffix_bytes {
420            e.value = value.to_vec();
421            outcome = SetOutcome::Replaced;
422            break;
423        }
424    }
425    if matches!(outcome, SetOutcome::Inserted) {
426        entries.push(DecodedEntry {
427            name_index,
428            name_bytes: suffix_bytes.to_vec(),
429            value: value.to_vec(),
430            value_inum: 0,
431        });
432    }
433
434    let area_len = region.len() - 4;
435    let needed_entries: usize = entries
436        .iter()
437        .map(|e| (16 + e.name_bytes.len() + 3) & !3)
438        .sum();
439    let needed_values: usize = entries
440        .iter()
441        .filter(|e| !e.value.is_empty())
442        .map(|e| (e.value.len() + 3) & !3)
443        .sum();
444    if needed_entries + 4 + needed_values > area_len {
445        return Err(Error::NoSpaceLeftOnDevice);
446    }
447
448    encode_in_inode_entries(region, &entries);
449    Ok(outcome)
450}
451
452/// One fully-owned xattr entry decoded from an in-inode region.
453#[derive(Debug, Clone)]
454struct DecodedEntry {
455    name_index: u8,
456    name_bytes: Vec<u8>,
457    value: Vec<u8>,
458    /// `e_value_inum`, carried so a rewrite can tell that it must not
459    /// happen. [`encode_in_inode_entries`] repacks every value into the
460    /// region, and a value that lives in another inode cannot be repacked;
461    /// emitting the entry without its pointer would leave it describing a
462    /// value that was never there and orphan the inode holding the real
463    /// one. See [`refuse_if_any_ea_inode_backed`].
464    value_inum: u32,
465}
466
467/// Refuse to rewrite a region that holds an EA-inode-backed entry.
468///
469/// # THIS IS NOT ONLY ABOUT THE ENTRY BEING EDITED
470///
471/// The in-inode region is rewritten wholesale: every surviving entry is
472/// re-encoded and every value repacked. So removing or setting attribute
473/// *A* re-emits attribute *B* too, and if B's value lives in an EA inode
474/// there is nothing to repack — B comes back with `e_value_inum` zeroed
475/// and an `e_value_offs` pointing at bytes that are not its value, while
476/// the inode holding the real value is orphaned with its refcount
477/// untouched and its blocks unfreed.
478///
479/// Following and refcounting EA inodes on the write path is the feature
480/// that would make this work. Until then, refusing is the honest answer,
481/// and it is the one this crate already gives for the other shapes it can
482/// read but not maintain.
483fn refuse_if_any_ea_inode_backed(entries: &[DecodedEntry]) -> Result<()> {
484    if entries.iter().any(|e| e.value_inum != 0) {
485        return Err(Error::Unsupported(
486            "this inode has an extended attribute whose value lives in an EA inode; \
487             rewriting the attribute area would orphan it",
488        ));
489    }
490    Ok(())
491}
492
493/// Parse every entry out of the in-inode region's entries-area slice
494/// (starts immediately AFTER the 4-byte magic).
495fn decode_in_inode_entries(entries_buf: &[u8]) -> Result<Vec<DecodedEntry>> {
496    let mut out = Vec::new();
497    let mut pos = 0usize;
498    while pos + 16 <= entries_buf.len() {
499        let header = u32::from_le_bytes(entries_buf[pos..pos + 4].try_into().unwrap());
500        if header == 0 {
501            break;
502        }
503        let name_len = entries_buf[pos] as usize;
504        let name_index = entries_buf[pos + 1];
505        let value_offs =
506            u16::from_le_bytes(entries_buf[pos + 2..pos + 4].try_into().unwrap()) as usize;
507        let value_inum = u32::from_le_bytes(entries_buf[pos + 4..pos + 8].try_into().unwrap());
508        let value_size =
509            u32::from_le_bytes(entries_buf[pos + 8..pos + 12].try_into().unwrap()) as usize;
510        if pos + 16 + name_len > entries_buf.len() {
511            return Err(Error::Corrupt("xattr entry name overruns region"));
512        }
513        let name_bytes = entries_buf[pos + 16..pos + 16 + name_len].to_vec();
514        let value = if value_inum != 0 || value_size == 0 {
515            Vec::new()
516        } else {
517            if value_offs + value_size > entries_buf.len() {
518                return Err(Error::Corrupt("xattr value out of range"));
519            }
520            entries_buf[value_offs..value_offs + value_size].to_vec()
521        };
522        out.push(DecodedEntry {
523            name_index,
524            name_bytes,
525            value,
526            value_inum,
527        });
528        pos += (16 + name_len + 3) & !3;
529    }
530    Ok(out)
531}
532
533/// Re-emit the in-inode region from a list of entries. Zeros the entire
534/// region, stamps magic at [0..4], packs entries forward from offset 4,
535/// and packs their values backward from the end. Leaves a u32 zero
536/// terminator after the last entry (implicit via the initial zero sweep).
537///
538/// Caller must size `region` large enough; this function is only called
539/// after `decode_in_inode_entries` produced the list so the byte budget
540/// is always ≤ the original region.
541fn encode_in_inode_entries(region: &mut [u8], entries: &[DecodedEntry]) {
542    for b in region.iter_mut() {
543        *b = 0;
544    }
545    region[..4].copy_from_slice(&EXT4_XATTR_MAGIC.to_le_bytes());
546    let entries_area = &mut region[4..];
547    let area_len = entries_area.len();
548
549    // Stable sort: kernel stores entries ordered by (name_index, name).
550    let mut sorted: Vec<&DecodedEntry> = entries.iter().collect();
551    sorted.sort_by(|a, b| {
552        a.name_index
553            .cmp(&b.name_index)
554            .then_with(|| a.name_bytes.cmp(&b.name_bytes))
555    });
556
557    let mut entry_cursor: usize = 0;
558    let mut value_cursor: usize = area_len;
559
560    for e in &sorted {
561        let name_len = e.name_bytes.len();
562        let entry_padded = (16 + name_len + 3) & !3;
563
564        let value_offs = if e.value.is_empty() {
565            0
566        } else {
567            let value_padded = (e.value.len() + 3) & !3;
568            value_cursor -= value_padded;
569            entries_area[value_cursor..value_cursor + e.value.len()].copy_from_slice(&e.value);
570            value_cursor
571        };
572
573        entries_area[entry_cursor] = name_len as u8;
574        entries_area[entry_cursor + 1] = e.name_index;
575        entries_area[entry_cursor + 2..entry_cursor + 4]
576            .copy_from_slice(&(value_offs as u16).to_le_bytes());
577        // e_value_inum at +4..+8 = 0 (no EA_INODE)
578        entries_area[entry_cursor + 8..entry_cursor + 12]
579            .copy_from_slice(&(e.value.len() as u32).to_le_bytes());
580        // e_hash at +12..+16 = 0 (in-inode hash is dedup-only)
581        entries_area[entry_cursor + 16..entry_cursor + 16 + name_len]
582            .copy_from_slice(&e.name_bytes);
583        entry_cursor += entry_padded;
584    }
585    // Terminator u32 at entry_cursor is already zero from the sweep.
586}
587
588// ---------------------------------------------------------------------------
589// External xattr block: write-side
590// ---------------------------------------------------------------------------
591
592/// Outcome of [`plan_remove_from_external_block`].
593#[derive(Debug, Clone, Copy, PartialEq, Eq)]
594pub enum BlockRemoveOutcome {
595    /// Entry removed; the block still has at least one entry remaining.
596    Removed,
597    /// Entry removed AND the block is now empty — caller should free the
598    /// block and zero `i_file_acl`.
599    RemovedNowEmpty,
600    /// The named entry wasn't in this block.
601    NotFound,
602}
603
604/// Decode every entry from an external xattr block (the 32-byte header is
605/// expected at offset 0). Skips the terminator. Used by both the read path
606/// and the write path.
607fn decode_external_block_entries(block: &[u8]) -> Result<Vec<DecodedEntry>> {
608    if block.len() < 32 {
609        return Err(Error::Corrupt("xattr block too small"));
610    }
611    let mut out = Vec::new();
612    let mut pos = 32usize;
613    while pos + 16 <= block.len() {
614        let header = u32::from_le_bytes(block[pos..pos + 4].try_into().unwrap());
615        if header == 0 {
616            break;
617        }
618        let name_len = block[pos] as usize;
619        let name_index = block[pos + 1];
620        let value_offs = u16::from_le_bytes(block[pos + 2..pos + 4].try_into().unwrap()) as usize;
621        let value_inum = u32::from_le_bytes(block[pos + 4..pos + 8].try_into().unwrap());
622        let value_size = u32::from_le_bytes(block[pos + 8..pos + 12].try_into().unwrap()) as usize;
623        if pos + 16 + name_len > block.len() {
624            return Err(Error::Corrupt("xattr entry name overruns block"));
625        }
626        let name_bytes = block[pos + 16..pos + 16 + name_len].to_vec();
627        let value = if value_inum != 0 || value_size == 0 {
628            Vec::new()
629        } else {
630            if value_offs + value_size > block.len() {
631                return Err(Error::Corrupt("xattr block value out of range"));
632            }
633            block[value_offs..value_offs + value_size].to_vec()
634        };
635        out.push(DecodedEntry {
636            name_index,
637            name_bytes,
638            value,
639            value_inum,
640        });
641        pos += (16 + name_len + 3) & !3;
642    }
643    Ok(out)
644}
645
646/// Re-emit a full external xattr block from a list of entries.
647///
648/// Lays out:
649/// - `[0x00..0x04]` magic = `EXT4_XATTR_MAGIC`
650/// - `[0x04..0x08]` `h_refcount` (caller-provided; default 1)
651/// - `[0x08..0x0C]` `h_blocks` = 1 (always single-block)
652/// - `[0x0C..0x10]` `h_hash`, the [`block_hash`] of the entries' hashes
653/// - `[0x10..0x14]` `h_checksum` slot — left as 0 here; caller patches via
654///   `Checksummer::patch_xattr_block` after layout.
655/// - `[0x14..0x20]` reserved zeros
656/// - `[0x20..]`     entries growing forward, values growing backward from
657///   end of block. `e_value_offs` is BLOCK-relative
658///   (different from in-inode where it's region-relative). Every entry's
659///   `e_hash` is its [`entry_hash`].
660fn encode_external_block(block: &mut [u8], entries: &[DecodedEntry], refcount: u32) {
661    block.fill(0);
662    block[0x00..0x04].copy_from_slice(&EXT4_XATTR_MAGIC.to_le_bytes());
663    block[0x04..0x08].copy_from_slice(&refcount.to_le_bytes());
664    block[0x08..0x0C].copy_from_slice(&1u32.to_le_bytes());
665
666    // THE KERNEL'S ORDER, WHICH IS NOT ALPHABETICAL: namespace, then name
667    // LENGTH, then name bytes -- the order its own blocks are written in.
668    // Its lookup in a block stops at the first entry at or past the
669    // target in that order, so a block sorted by name alone hides every
670    // attribute that follows a longer name sorting earlier: with
671    // `user.abc` first, `getxattr("user.zz")` answered ENODATA (#379).
672    // `e2fsck` does not check the order.
673    let mut sorted: Vec<&DecodedEntry> = entries.iter().collect();
674    sorted.sort_by(|a, b| {
675        a.name_index
676            .cmp(&b.name_index)
677            .then_with(|| a.name_bytes.len().cmp(&b.name_bytes.len()))
678            .then_with(|| a.name_bytes.cmp(&b.name_bytes))
679    });
680
681    let mut entry_cursor: usize = 0x20;
682    let mut value_cursor: usize = block.len();
683    let mut hashes = Vec::with_capacity(sorted.len());
684
685    for e in &sorted {
686        let name_len = e.name_bytes.len();
687        let entry_padded = (16 + name_len + 3) & !3;
688
689        let value_offs = if e.value.is_empty() {
690            0
691        } else {
692            let value_padded = (e.value.len() + 3) & !3;
693            value_cursor -= value_padded;
694            block[value_cursor..value_cursor + e.value.len()].copy_from_slice(&e.value);
695            value_cursor
696        };
697
698        // e2fsck checks every block entry's e_hash against its name and
699        // value ("has a hash (N) which is invalid"), so it is always real.
700        let e_hash = entry_hash(&e.name_bytes, &e.value);
701        hashes.push(e_hash);
702
703        block[entry_cursor] = name_len as u8;
704        block[entry_cursor + 1] = e.name_index;
705        block[entry_cursor + 2..entry_cursor + 4]
706            .copy_from_slice(&(value_offs as u16).to_le_bytes());
707        // e_value_inum at +4..+8 = 0 (no EA_INODE)
708        block[entry_cursor + 8..entry_cursor + 12]
709            .copy_from_slice(&(e.value.len() as u32).to_le_bytes());
710        block[entry_cursor + 12..entry_cursor + 16].copy_from_slice(&e_hash.to_le_bytes());
711        block[entry_cursor + 16..entry_cursor + 16 + name_len].copy_from_slice(&e.name_bytes);
712        entry_cursor += entry_padded;
713    }
714    block[0x0C..0x10].copy_from_slice(&block_hash(&hashes).to_le_bytes());
715    // Terminator already zero from the wipe.
716}
717
718// ---------------------------------------------------------------------------
719// Entry and block hashes
720// ---------------------------------------------------------------------------
721//
722// kernel.org's attributes.html says only that `e_hash` is a hash of the
723// name and value and `h_hash` a hash of all the attributes. The exact
724// rules below were read off the harness VM: blocks the kernel wrote with
725// `setfattr`, and blocks `debugfs ea_set` wrote, dumped byte for byte,
726// with `e2fsck -fn` (which checks `e_hash` and not `h_hash`) as the judge.
727//
728// - An entry's hash starts at zero. Each byte of the name (without its
729//   namespace prefix) is folded in by rotating the running value left 5
730//   bits and XOR-ing the byte in, taken as an unsigned 0..=255. Then the
731//   value, as little-endian 32-bit words with the last one zero-padded,
732//   each folded in with a 16-bit left rotation. An empty value adds
733//   nothing.
734// - A name byte above 0x7F (`user.qé`) has two readings. debugfs writes
735//   the unsigned one, and so does the kernel of the harness's aarch64
736//   guest; the kernel of its x86_64 guest, the same Debian 12 image built
737//   for amd64, writes the hash that sign-extending the byte gives. e2fsck
738//   accepts either. This crate writes the unsigned reading on every
739//   platform, as debugfs does.
740// - The block's hash folds the entries' hashes, in on-disk order, the
741//   same way with a 16-bit rotation -- unless any entry's hash is zero
742//   (a name and value can be chosen to make it so), in which case the
743//   kernel writes a block hash of zero, wherever that entry falls.
744
745/// Left rotation applied before each name byte is folded in.
746const NAME_BYTE_ROTATION: u32 = 5;
747/// Left rotation applied before each value word, or entry hash, is folded in.
748const WORD_ROTATION: u32 = 16;
749
750/// `e_hash` of an attribute with this name (suffix after the namespace
751/// prefix) and value.
752fn entry_hash(name: &[u8], value: &[u8]) -> u32 {
753    let after_name = name.iter().fold(0u32, |acc, &byte| {
754        acc.rotate_left(NAME_BYTE_ROTATION) ^ u32::from(byte)
755    });
756    value.chunks(4).fold(after_name, |acc, piece| {
757        let mut word = [0u8; 4];
758        word[..piece.len()].copy_from_slice(piece);
759        acc.rotate_left(WORD_ROTATION) ^ u32::from_le_bytes(word)
760    })
761}
762
763/// `h_hash` of a block whose entries, in on-disk order, have these hashes.
764/// Zero when any of them is zero.
765fn block_hash(entry_hashes: &[u32]) -> u32 {
766    if entry_hashes.contains(&0) {
767        return 0;
768    }
769    entry_hashes
770        .iter()
771        .fold(0u32, |acc, &hash| acc.rotate_left(WORD_ROTATION) ^ hash)
772}
773
774/// Set (create-or-replace) an xattr in an external block buffer.
775///
776/// `block` is the full xattr block (caller has already read it from disk
777/// or freshly zeroed it for a brand-new allocation). `refcount` is what to
778/// stamp into `h_refcount` — pass 1 for a non-shared block. On success the
779/// bytes have been rewritten; the caller must (a) patch `h_checksum` via
780/// `Checksummer::patch_xattr_block` and (b) write the block back.
781///
782/// Errors:
783/// - `Error::InvalidArgument` if the name lacks a known prefix or has an
784///   empty suffix (except ACL namespaces 2 + 3).
785/// - `Error::NameTooLong` if the suffix > 255 bytes.
786/// - `Error::NoSpaceLeftOnDevice` if the new layout would not fit in the
787///   block (entries + values + terminator > block size).
788pub fn plan_set_in_external_block(
789    block: &mut [u8],
790    name: &str,
791    value: &[u8],
792    refcount: u32,
793) -> Result<SetOutcome> {
794    let Some((name_index, suffix)) = split_qualified_name(name) else {
795        return Err(Error::InvalidArgument(
796            "xattr name missing known namespace prefix",
797        ));
798    };
799    if suffix.is_empty() && !matches!(name_index, 2 | 3) {
800        return Err(Error::InvalidArgument("xattr name suffix is empty"));
801    }
802    if suffix.len() > 255 {
803        return Err(Error::NameTooLong);
804    }
805    if block.len() < 0x40 {
806        return Err(Error::NoSpaceLeftOnDevice);
807    }
808
809    let magic_present = u32::from_le_bytes(block[..4].try_into().unwrap()) == EXT4_XATTR_MAGIC;
810    let mut entries = if magic_present {
811        decode_external_block_entries(block)?
812    } else {
813        Vec::new()
814    };
815    refuse_if_any_ea_inode_backed(&entries)?;
816
817    let mut outcome = SetOutcome::Inserted;
818    let suffix_bytes = suffix.as_bytes();
819    for e in entries.iter_mut() {
820        if e.name_index == name_index && e.name_bytes == suffix_bytes {
821            e.value = value.to_vec();
822            outcome = SetOutcome::Replaced;
823            break;
824        }
825    }
826    if matches!(outcome, SetOutcome::Inserted) {
827        entries.push(DecodedEntry {
828            name_index,
829            name_bytes: suffix_bytes.to_vec(),
830            value: value.to_vec(),
831            value_inum: 0,
832        });
833    }
834
835    let entries_capacity = block.len() - 0x20;
836    let needed_entries: usize = entries
837        .iter()
838        .map(|e| (16 + e.name_bytes.len() + 3) & !3)
839        .sum();
840    let needed_values: usize = entries
841        .iter()
842        .filter(|e| !e.value.is_empty())
843        .map(|e| (e.value.len() + 3) & !3)
844        .sum();
845    if needed_entries + 4 + needed_values > entries_capacity {
846        return Err(Error::NoSpaceLeftOnDevice);
847    }
848
849    encode_external_block(block, &entries, refcount);
850    Ok(outcome)
851}
852
853/// Remove an xattr from an external block buffer.
854///
855/// Returns [`BlockRemoveOutcome::RemovedNowEmpty`] when the last entry is
856/// gone — the caller should free the underlying block + zero `i_file_acl`
857/// rather than leaving an empty xattr block on disk. Otherwise rewrites
858/// the block in place; caller must re-checksum + write back.
859pub fn plan_remove_from_external_block(
860    block: &mut [u8],
861    name: &str,
862    refcount: u32,
863) -> Result<BlockRemoveOutcome> {
864    let Some((name_index, suffix)) = split_qualified_name(name) else {
865        return Err(Error::InvalidArgument(
866            "xattr name missing known namespace prefix",
867        ));
868    };
869    if block.len() < 0x20 {
870        return Ok(BlockRemoveOutcome::NotFound);
871    }
872    let magic = u32::from_le_bytes(block[..4].try_into().unwrap());
873    if magic != EXT4_XATTR_MAGIC {
874        return Ok(BlockRemoveOutcome::NotFound);
875    }
876
877    let entries = decode_external_block_entries(block)?;
878    refuse_if_any_ea_inode_backed(&entries)?;
879    let before = entries.len();
880    let kept: Vec<DecodedEntry> = entries
881        .into_iter()
882        .filter(|e| !(e.name_index == name_index && e.name_bytes == suffix.as_bytes()))
883        .collect();
884    if kept.len() == before {
885        return Ok(BlockRemoveOutcome::NotFound);
886    }
887    if kept.is_empty() {
888        return Ok(BlockRemoveOutcome::RemovedNowEmpty);
889    }
890    encode_external_block(block, &kept, refcount);
891    Ok(BlockRemoveOutcome::Removed)
892}
893
894/// Refuse an external xattr block that is not one: no magic, an `h_blocks`
895/// other than 1, or (on `metadata_csum`) a checksum that does not verify.
896///
897/// The kernel makes the same checks before it reads or edits the block,
898/// and answers EUCLEAN ("Structure needs cleaning"). Every block ext4
899/// has ever written is exactly one block long; a larger `h_blocks` is not
900/// a layout anybody can edit in place.
901pub(crate) fn check_external_block(
902    csum: &crate::checksum::Checksummer,
903    block_nr: u64,
904    block: &[u8],
905) -> Result<()> {
906    if block.len() < 0x20 || u32::from_le_bytes(block[..4].try_into().unwrap()) != EXT4_XATTR_MAGIC
907    {
908        return Err(Error::Corrupt(
909            "i_file_acl names a block without the xattr magic",
910        ));
911    }
912    if u32::from_le_bytes(block[8..12].try_into().unwrap()) != 1 {
913        return Err(Error::Corrupt("xattr block h_blocks is not 1"));
914    }
915    if !csum.verify_xattr_block(block_nr, block) {
916        return Err(Error::BadChecksum {
917            what: "xattr block",
918        });
919    }
920    Ok(())
921}
922
923/// Check `inode`'s external xattr block, if it has one, before a read
924/// returns what is in it. See [`check_external_block`]: without it a block
925/// whose checksum fails still answers, with bytes that may be another
926/// attribute's or none at all.
927fn check_block_of(fs: &Filesystem, inode: &Inode) -> Result<()> {
928    if inode.file_acl == 0 {
929        return Ok(());
930    }
931    let bs = fs.sb.block_size();
932    let mut block = vec![0u8; bs as usize];
933    fs.dev.read_at(inode.file_acl * bs as u64, &mut block)?;
934    check_external_block(&fs.csum, inode.file_acl, &block)
935}
936
937/// Convenience: get a single xattr value by name. Returns `None` if not present.
938pub fn get(
939    dev: &dyn BlockDevice,
940    inode: &Inode,
941    inode_raw: &[u8],
942    inode_size: u16,
943    block_size: u32,
944    name: &str,
945) -> Result<Option<Vec<u8>>> {
946    let all = read_all(dev, inode, inode_raw, inode_size, block_size)?;
947    Ok(all.into_iter().find(|e| e.name == name).map(|e| e.value))
948}
949
950/// Every xattr on `inode`, with EA-inode values followed.
951///
952/// # WHY THIS EXISTS ALONGSIDE [`read_all`]
953///
954/// `INCOMPAT_EA_INODE` means an attribute whose value is too large for the
955/// xattr area does not store its value there at all: `e_value_inum` names
956/// an inode whose file body **is** the value, and `e_value_offs` stops
957/// describing anything. Following that pointer needs to read another
958/// inode, so it needs a [`Filesystem`] — which the buffer-level parsers
959/// deliberately do not take, since they are also used on a region held in
960/// memory during a rewrite.
961///
962/// So [`read_all`] reports the pointer and leaves the value empty, and
963/// this resolves it. A caller that uses [`read_all`] directly and reads
964/// `value` without checking `value_inum` gets an empty value rather than
965/// the wrong one — the failure is visible instead of plausible, which is
966/// the whole point.
967pub fn read_all_resolved(
968    fs: &Filesystem,
969    inode: &Inode,
970    inode_raw: &[u8],
971) -> Result<Vec<XattrEntry>> {
972    check_block_of(fs, inode)?;
973    let mut entries = read_all(
974        fs.dev.as_ref(),
975        inode,
976        inode_raw,
977        fs.sb.inode_size,
978        fs.sb.block_size(),
979    )?;
980    for e in entries.iter_mut() {
981        if e.value_inum != 0 {
982            e.value = crate::ea_inode::read_value_inode(fs, e.value_inum, e.value_size)?;
983        }
984    }
985    Ok(entries)
986}
987
988/// The fully-qualified names of an inode's attributes, and nothing else.
989///
990/// What `fs_ext4_listxattr` needs (#122). It used [`read_all_resolved`],
991/// which reads every EA-inode-backed VALUE from disk only for the caller to
992/// discard it -- and fails the whole listing when one of those values
993/// cannot be read, though every name was right there. A names-only listing
994/// does not depend on any value being readable.
995pub fn list_names(fs: &Filesystem, inode: &Inode, inode_raw: &[u8]) -> Result<Vec<String>> {
996    check_block_of(fs, inode)?;
997    Ok(read_all(
998        fs.dev.as_ref(),
999        inode,
1000        inode_raw,
1001        fs.sb.inode_size,
1002        fs.sb.block_size(),
1003    )?
1004    .into_iter()
1005    .map(|e| e.name)
1006    .collect())
1007}
1008
1009/// One xattr by fully-qualified name, with an EA-inode value followed.
1010/// See [`read_all_resolved`].
1011pub fn get_resolved(
1012    fs: &Filesystem,
1013    inode: &Inode,
1014    inode_raw: &[u8],
1015    name: &str,
1016) -> Result<Option<Vec<u8>>> {
1017    check_block_of(fs, inode)?;
1018    let Some(entry) = read_all(
1019        fs.dev.as_ref(),
1020        inode,
1021        inode_raw,
1022        fs.sb.inode_size,
1023        fs.sb.block_size(),
1024    )?
1025    .into_iter()
1026    .find(|e| e.name == name) else {
1027        return Ok(None);
1028    };
1029    if entry.value_inum != 0 {
1030        return Ok(Some(crate::ea_inode::read_value_inode(
1031            fs,
1032            entry.value_inum,
1033            entry.value_size,
1034        )?));
1035    }
1036    Ok(Some(entry.value))
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041    use super::*;
1042
1043    #[test]
1044    fn prefixes_known() {
1045        assert_eq!(prefix_for_index(1), Some("user."));
1046        assert_eq!(prefix_for_index(7), Some("system."));
1047        assert_eq!(prefix_for_index(99), None);
1048    }
1049
1050    #[test]
1051    fn split_qualified_name_roundtrip() {
1052        assert_eq!(split_qualified_name("user.color"), Some((1, "color")));
1053        assert_eq!(
1054            split_qualified_name("user.com.apple.FinderInfo"),
1055            Some((1, "com.apple.FinderInfo"))
1056        );
1057        assert_eq!(
1058            split_qualified_name("security.selinux"),
1059            Some((6, "selinux"))
1060        );
1061        assert_eq!(split_qualified_name("unknown.foo"), None);
1062    }
1063
1064    /// `XattrEntry` is `#[non_exhaustive]`, so its constructors are the only
1065    /// way a caller builds one. `new` must produce exactly what the parser
1066    /// does for an inline value, or a caller comparing against a parsed
1067    /// entry is told they differ when the volume agrees with them (#120).
1068    #[test]
1069    fn new_builds_what_the_parser_reads_for_an_inline_value() {
1070        let mut region = vec![0u8; 64];
1071        encode_in_inode_entries(
1072            &mut region,
1073            &[DecodedEntry {
1074                name_index: 1,
1075                name_bytes: b"color".to_vec(),
1076                value: b"red".to_vec(),
1077                value_inum: 0,
1078            }],
1079        );
1080        let mut parsed = Vec::new();
1081        parse_entries(&region[4..], region.len() - 4, &mut parsed).unwrap();
1082        assert_eq!(parsed, vec![XattrEntry::new("user.color", b"red".to_vec())]);
1083    }
1084
1085    /// An EA-inode entry carries no bytes of its own and the size its entry
1086    /// declares; it never equals an inline entry of the same name, since
1087    /// equality covers every field.
1088    #[test]
1089    fn in_ea_inode_carries_the_inode_and_declared_size_and_no_bytes() {
1090        let e = XattrEntry::in_ea_inode("user.big", 42, 70_000);
1091        assert_eq!(e.name, "user.big");
1092        assert!(e.value.is_empty());
1093        assert_eq!((e.value_inum, e.value_size), (42, 70_000));
1094        assert_ne!(e, XattrEntry::new("user.big", Vec::new()));
1095    }
1096
1097    /// Build a minimal in-inode region with two `user.*` entries, then remove
1098    /// one by name. Verify the other survives and a readback decodes cleanly.
1099    #[test]
1100    fn remove_in_inode_roundtrips_one_of_two() {
1101        // 96-byte region is plenty for two short entries (each ~24 bytes
1102        // header+name + a handful of value bytes).
1103        let mut region = vec![0u8; 96];
1104        let entries = vec![
1105            DecodedEntry {
1106                name_index: 1,
1107                name_bytes: b"color".to_vec(),
1108                value: b"red".to_vec(),
1109                value_inum: 0,
1110            },
1111            DecodedEntry {
1112                name_index: 1,
1113                name_bytes: b"mood".to_vec(),
1114                value: b"happy".to_vec(),
1115                value_inum: 0,
1116            },
1117        ];
1118        encode_in_inode_entries(&mut region, &entries);
1119
1120        // Sanity: before-remove decode returns both entries.
1121        let decoded = decode_in_inode_entries(&region[4..]).unwrap();
1122        assert_eq!(decoded.len(), 2);
1123
1124        let outcome = plan_remove_in_inode_region(&mut region, "user.color").unwrap();
1125        assert_eq!(outcome, RemoveOutcome::Removed);
1126
1127        let after = decode_in_inode_entries(&region[4..]).unwrap();
1128        assert_eq!(after.len(), 1);
1129        assert_eq!(after[0].name_bytes, b"mood");
1130        assert_eq!(after[0].value, b"happy");
1131    }
1132
1133    #[test]
1134    fn remove_in_inode_returns_not_found_for_missing() {
1135        let mut region = vec![0u8; 64];
1136        let entries = vec![DecodedEntry {
1137            name_index: 1,
1138            name_bytes: b"color".to_vec(),
1139            value: b"red".to_vec(),
1140            value_inum: 0,
1141        }];
1142        encode_in_inode_entries(&mut region, &entries);
1143        let outcome = plan_remove_in_inode_region(&mut region, "user.mood").unwrap();
1144        assert_eq!(outcome, RemoveOutcome::NotFound);
1145    }
1146
1147    #[test]
1148    fn remove_in_inode_unknown_prefix_is_einval() {
1149        let mut region = vec![0u8; 64];
1150        let err = plan_remove_in_inode_region(&mut region, "nope.name").unwrap_err();
1151        assert!(matches!(err, Error::InvalidArgument(_)));
1152    }
1153
1154    #[test]
1155    fn remove_in_inode_missing_magic_is_not_found() {
1156        let mut region = vec![0u8; 64];
1157        // all zeros → no magic
1158        let outcome = plan_remove_in_inode_region(&mut region, "user.x").unwrap();
1159        assert_eq!(outcome, RemoveOutcome::NotFound);
1160    }
1161
1162    #[test]
1163    fn set_in_inode_inserts_new_into_empty_region() {
1164        let mut region = vec![0u8; 64];
1165        let outcome = plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
1166        assert_eq!(outcome, SetOutcome::Inserted);
1167        let decoded = decode_in_inode_entries(&region[4..]).unwrap();
1168        assert_eq!(decoded.len(), 1);
1169        assert_eq!(decoded[0].name_bytes, b"color");
1170        assert_eq!(decoded[0].value, b"red");
1171    }
1172
1173    #[test]
1174    fn set_in_inode_replaces_existing_value() {
1175        let mut region = vec![0u8; 96];
1176        plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
1177        let outcome = plan_set_in_inode_region(&mut region, "user.color", b"emerald").unwrap();
1178        assert_eq!(outcome, SetOutcome::Replaced);
1179        let decoded = decode_in_inode_entries(&region[4..]).unwrap();
1180        assert_eq!(decoded.len(), 1);
1181        assert_eq!(decoded[0].value, b"emerald");
1182    }
1183
1184    #[test]
1185    fn set_in_inode_preserves_other_entries() {
1186        let mut region = vec![0u8; 128];
1187        plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
1188        plan_set_in_inode_region(&mut region, "user.mood", b"happy").unwrap();
1189        plan_set_in_inode_region(&mut region, "user.color", b"blue").unwrap();
1190        let decoded = decode_in_inode_entries(&region[4..]).unwrap();
1191        let by_name: std::collections::BTreeMap<_, _> = decoded
1192            .into_iter()
1193            .map(|e| (e.name_bytes.clone(), e.value))
1194            .collect();
1195        assert_eq!(by_name.get(b"color".as_slice()).unwrap(), b"blue");
1196        assert_eq!(by_name.get(b"mood".as_slice()).unwrap(), b"happy");
1197    }
1198
1199    #[test]
1200    fn set_in_inode_enospc_on_overflow() {
1201        let mut region = vec![0u8; 32];
1202        let err =
1203            plan_set_in_inode_region(&mut region, "user.x", b"this_is_20_bytes_xx!").unwrap_err();
1204        assert!(matches!(err, Error::NoSpaceLeftOnDevice));
1205    }
1206
1207    #[test]
1208    fn set_in_inode_unknown_prefix_is_einval() {
1209        let mut region = vec![0u8; 64];
1210        let err = plan_set_in_inode_region(&mut region, "weird.key", b"v").unwrap_err();
1211        assert!(matches!(err, Error::InvalidArgument(_)));
1212    }
1213
1214    /// attributes.html: external-block entries are sorted by
1215    /// `(name_index, name_len, name)`. The kernel's lookup stops at the
1216    /// first entry that sorts at or after the target, so the block must be
1217    /// written in that order. Sorted by name alone,
1218    /// `user.abc` goes before `user.zz`, and the kernel's lookup of `zz`
1219    /// stops at `abc` and answers ENODATA (#379).
1220    #[test]
1221    fn external_block_entries_are_sorted_by_name_length_before_name() {
1222        let mut block = vec![0u8; 4096];
1223        plan_set_in_external_block(&mut block, "user.abc", b"1", 1).unwrap();
1224        plan_set_in_external_block(&mut block, "user.zz", b"2", 1).unwrap();
1225        assert_eq!(
1226            block[0x20],
1227            2,
1228            "the first entry must be the shorter name (zz), not {:?}",
1229            String::from_utf8_lossy(&block[0x30..0x30 + block[0x20] as usize])
1230        );
1231        // And the namespace still comes first: a longer name in a lower
1232        // index sorts before a shorter one in a higher index.
1233        plan_set_in_external_block(&mut block, "trusted.a", b"3", 1).unwrap();
1234        let order: Vec<(u8, Vec<u8>)> = decode_external_block_entries(&block)
1235            .unwrap()
1236            .into_iter()
1237            .map(|e| (e.name_index, e.name_bytes))
1238            .collect();
1239        assert_eq!(
1240            order,
1241            vec![
1242                (1, b"zz".to_vec()),
1243                (1, b"abc".to_vec()),
1244                (4, b"a".to_vec()),
1245            ]
1246        );
1247    }
1248}
1249
1250/// The entry and block hashes, against words the kernel and debugfs
1251/// wrote in the harness VM (see the comment above [`entry_hash`]).
1252#[cfg(test)]
1253mod hash_tests {
1254    use super::{block_hash, entry_hash, NAME_BYTE_ROTATION, WORD_ROTATION};
1255
1256    /// `(name suffix, value, e_hash)` as found in kernel- and
1257    /// debugfs-written blocks.
1258    const OBSERVED: &[(&[u8], &[u8], u32)] = &[
1259        (b"a", b"x", 0x0061_0078),
1260        (b"bb", b"hello", 0x6568_6021),
1261        (b"empty", b"", 0x0667_4EF9),
1262        (b"z", b"0123456789", 0x067C_3F3E),
1263        (b"q", b"1", 0x0071_0031),
1264        (b"a", b"hi", 0x0061_6968),
1265        ("q\u{e9}".as_bytes(), b"val", 0xDCA5_6177),
1266        // Values chosen so the hash comes out zero.
1267        (b"a", &[0x00, 0x00, 0x61, 0x00], 0),
1268        (b"zzz", &[0x01, 0x00, 0x3A, 0xE7], 0),
1269    ];
1270
1271    #[test]
1272    fn entry_hashes_match_the_observed_words() {
1273        for &(name, value, want) in OBSERVED {
1274            assert_eq!(
1275                entry_hash(name, value),
1276                want,
1277                "{:?}={value:?}",
1278                String::from_utf8_lossy(name)
1279            );
1280        }
1281    }
1282
1283    /// The kernel's block for `user.a`, `user.bb`, `user.empty` and
1284    /// `trusted.z`, in its on-disk order.
1285    #[test]
1286    fn the_block_hash_folds_the_entry_hashes_in_order() {
1287        let hashes = [0x0061_0078, 0x6568_6021, 0x0667_4EF9, 0x067C_3F3E];
1288        assert_eq!(block_hash(&hashes), 0x2D95_5919);
1289        assert_eq!(
1290            block_hash(&hashes[..1]),
1291            hashes[0],
1292            "one entry is its own hash"
1293        );
1294        assert_eq!(block_hash(&[]), 0);
1295    }
1296
1297    #[test]
1298    fn a_zero_entry_hash_anywhere_zeroes_the_block_hash() {
1299        assert_eq!(block_hash(&[0, 0x6568_6021]), 0);
1300        assert_eq!(block_hash(&[0x0061_6968, 0]), 0);
1301    }
1302
1303    /// [`entry_hash`] with each name byte above 0x7F sign-extended
1304    /// instead, the reading the x86_64 guest's kernel writes.
1305    fn sign_extending_entry_hash(name: &[u8], value: &[u8]) -> u32 {
1306        let after_name = name.iter().fold(0u32, |acc, &byte| {
1307            acc.rotate_left(NAME_BYTE_ROTATION) ^ (i32::from(byte as i8) as u32)
1308        });
1309        value.chunks(4).fold(after_name, |acc, piece| {
1310            let mut word = [0u8; 4];
1311            word[..piece.len()].copy_from_slice(piece);
1312            acc.rotate_left(WORD_ROTATION) ^ u32::from_le_bytes(word)
1313        })
1314    }
1315
1316    /// The two readings differ only for a name byte above 0x7F, and the
1317    /// sign-extending one is the word the x86_64 guest's kernel stored for
1318    /// `user.qé` = `val`.
1319    #[test]
1320    fn a_high_name_byte_has_two_readings() {
1321        for &(name, value, _) in OBSERVED {
1322            if name.iter().all(|&byte| byte <= 0x7F) {
1323                assert_eq!(
1324                    sign_extending_entry_hash(name, value),
1325                    entry_hash(name, value)
1326                );
1327            }
1328        }
1329        let name = "q\u{e9}".as_bytes();
1330        assert_eq!(entry_hash(name, b"val"), 0xDCA5_6177);
1331        assert_eq!(sign_extending_entry_hash(name, b"val"), 0xC3BA_6177);
1332    }
1333
1334    /// Tests that ask the kernel and e2fsprogs in the harness VM.
1335    mod needs_host {
1336        use super::super::{
1337            block_hash, decode_external_block_entries, encode_external_block, entry_hash,
1338        };
1339        use super::sign_extending_entry_hash;
1340        use crate::block_io::FileDevice;
1341        use crate::fs::Filesystem;
1342        use std::sync::Arc;
1343
1344        /// Two attribute sets: one ordinary (with an empty value, a value
1345        /// that is not a whole number of words, and a name byte above 0x7F),
1346        /// and one whose last attribute hashes to zero.
1347        /// A file name and the `(attribute, value)` pairs set on it.
1348        type AttributeSet = (&'static str, &'static [(&'static str, &'static [u8])]);
1349
1350        const FILES: &[AttributeSet] = &[
1351            (
1352                "plain",
1353                &[
1354                    ("user.a", b"x"),
1355                    ("user.bb", b"hello"),
1356                    ("user.empty", b""),
1357                    ("trusted.z", b"0123456789"),
1358                    ("user.q\u{e9}", b"val"),
1359                ],
1360            ),
1361            (
1362                "zero",
1363                &[("user.a", b"hi"), ("user.zzz", &[0x01, 0x00, 0x3A, 0xE7])],
1364            ),
1365        ];
1366
1367        fn volume(tag: &str) -> String {
1368            let image = fs_ext4_test_support::temp_path!(
1369                "fs_ext4_xattr_hash_{tag}_{}.img",
1370                std::process::id()
1371            );
1372            std::fs::File::create(&image)
1373                .and_then(|f| f.set_len(8 << 20))
1374                .unwrap();
1375            // 128-byte inodes leave no in-inode room: every attribute goes
1376            // to the external block.
1377            let out = fs_ext4_test_support::oracle("mkfs.ext4")
1378                .args(["-q", "-F", "-b", "1024", "-I", "128", &image])
1379                .output();
1380            assert!(
1381                out.status.success(),
1382                "{}",
1383                String::from_utf8_lossy(&out.stderr)
1384            );
1385            image
1386        }
1387
1388        fn hex(bytes: &[u8]) -> String {
1389            bytes.iter().map(|b| format!("{b:02x}")).collect()
1390        }
1391
1392        /// `(e_hash of each entry in on-disk order, h_hash)` of `path`'s
1393        /// external block.
1394        fn block_hashes(fs: &Filesystem, path: &str) -> (Vec<u32>, u32, Vec<u8>) {
1395            let ino = fs.lookup_path_bytes(path.as_bytes()).unwrap();
1396            let inode = fs.read_inode_verified(ino).unwrap().0;
1397            assert_ne!(inode.file_acl, 0, "{path} has no external block");
1398            let block = fs.read_block(inode.file_acl).unwrap();
1399            let mut hashes = Vec::new();
1400            let mut at = 0x20;
1401            while u32::from_le_bytes(block[at..at + 4].try_into().unwrap()) != 0 {
1402                hashes.push(u32::from_le_bytes(
1403                    block[at + 12..at + 16].try_into().unwrap(),
1404                ));
1405                at += (16 + block[at] as usize + 3) & !3;
1406            }
1407            let h_hash = u32::from_le_bytes(block[0x0C..0x10].try_into().unwrap());
1408            (hashes, h_hash, block)
1409        }
1410
1411        /// The kernel writes each attribute set. Every hash it stored is the
1412        /// one computed here, except that for a name with a byte above 0x7F
1413        /// it may be the sign-extending reading instead (the x86_64 guest's
1414        /// kernel writes that one), and its block hash folds the hashes it
1415        /// stored. Re-encoding its entries reproduces its block hash
1416        /// whenever its entry hashes are this crate's.
1417        #[test]
1418        fn hashes_in_kernel_written_blocks_are_the_ones_computed_here() {
1419            let image = volume("kernel");
1420            let mut script = String::from("set -e\n");
1421            for (file, attrs) in FILES {
1422                script.push_str(&format!("touch \"$MNT/{file}\"\n"));
1423                for (name, value) in *attrs {
1424                    script.push_str(&format!(
1425                        "setfattr -n '{name}' -v 0x{} \"$MNT/{file}\"\n",
1426                        hex(value)
1427                    ));
1428                }
1429            }
1430            let script = script.replace("-v 0x \"", "\"");
1431            let out = fs_ext4_test_support::guest_kernel_write(&image, &script);
1432            assert!(
1433                out.status.success(),
1434                "{}",
1435                String::from_utf8_lossy(&out.stderr)
1436            );
1437
1438            let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
1439            for (file, attrs) in FILES {
1440                let path = format!("/{file}");
1441                let (hashes, h_hash, block) = block_hashes(&fs, &path);
1442                assert_eq!(hashes.len(), attrs.len(), "{path}");
1443                let entries = decode_external_block_entries(&block).unwrap();
1444                assert_eq!(entries.len(), hashes.len(), "{path}");
1445                let computed: Vec<u32> = entries
1446                    .iter()
1447                    .map(|entry| entry_hash(&entry.name_bytes, &entry.value))
1448                    .collect();
1449                for ((entry, &stored), &ours) in entries.iter().zip(&hashes).zip(&computed) {
1450                    let name = String::from_utf8_lossy(&entry.name_bytes);
1451                    if stored == ours {
1452                        continue;
1453                    }
1454                    assert!(
1455                        entry.name_bytes.iter().any(|&byte| byte > 0x7F),
1456                        "{path} {name}: the kernel stored {stored:#010x}, and this crate \
1457                         computes {ours:#010x}"
1458                    );
1459                    assert_eq!(
1460                        sign_extending_entry_hash(&entry.name_bytes, &entry.value),
1461                        stored,
1462                        "{path} {name}: the kernel's hash is neither reading"
1463                    );
1464                }
1465                assert_eq!(block_hash(&hashes), h_hash, "{path}'s h_hash");
1466
1467                let mut ours = vec![0u8; block.len()];
1468                encode_external_block(&mut ours, &entries, 1);
1469                assert_eq!(
1470                    ours[0x0C..0x10],
1471                    block_hash(&computed).to_le_bytes(),
1472                    "{path}: h_hash re-encoded"
1473                );
1474                if computed == hashes {
1475                    assert_eq!(
1476                        ours[0x0C..0x10],
1477                        block[0x0C..0x10],
1478                        "{path}: h_hash re-encoded"
1479                    );
1480                }
1481            }
1482            drop(fs);
1483            fs_ext4_test_support::assert_e2fsck_clean(&image, "the kernel's blocks");
1484            let _ = std::fs::remove_file(&image);
1485        }
1486
1487        /// This crate writes the same attribute sets: e2fsck accepts every
1488        /// e_hash, and the hashes are the unsigned readings, the ones debugfs
1489        /// and the aarch64 guest's kernel store for the same attributes.
1490        #[test]
1491        fn blocks_this_crate_writes_carry_the_kernels_hashes() {
1492            let image = volume("ours");
1493            {
1494                let fs = Filesystem::mount(Arc::new(FileDevice::open_rw(&image).unwrap())).unwrap();
1495                for (file, attrs) in FILES {
1496                    let path = format!("/{file}");
1497                    fs.apply_create(&path, 0o644).unwrap();
1498                    for (name, value) in *attrs {
1499                        fs.apply_setxattr(&path, name, value).unwrap();
1500                    }
1501                }
1502            }
1503            fs_ext4_test_support::assert_e2fsck_clean(&image, "this crate's blocks");
1504
1505            // The unsigned readings for these sets, as observed.
1506            let expected: &[(&str, &[u32], u32)] = &[
1507                (
1508                    "/plain",
1509                    &[
1510                        0x0061_0078,
1511                        0x6568_6021,
1512                        0xDCA5_6177,
1513                        0x0667_4EF9,
1514                        0x067C_3F3E,
1515                    ],
1516                    block_hash(&[
1517                        0x0061_0078,
1518                        0x6568_6021,
1519                        0xDCA5_6177,
1520                        0x0667_4EF9,
1521                        0x067C_3F3E,
1522                    ]),
1523                ),
1524                ("/zero", &[0x0061_6968, 0], 0),
1525            ];
1526            let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
1527            for &(path, hashes, h_hash) in expected {
1528                let (got, got_h, _) = block_hashes(&fs, path);
1529                assert_eq!(got, hashes, "{path}");
1530                assert_eq!(got_h, h_hash, "{path}");
1531            }
1532            drop(fs);
1533            let _ = std::fs::remove_file(&image);
1534        }
1535    }
1536}