use crate::block_io::BlockDevice;
use crate::error::{Error, Result};
use crate::fs::Filesystem;
use crate::inode::Inode;
pub const EXT4_XATTR_MAGIC: u32 = 0xEA02_0000;
pub const NAME_PREFIXES: &[(u8, &str)] = &[
(1, "user."),
(2, "system.posix_acl_access"),
(3, "system.posix_acl_default"),
(4, "trusted."),
(5, "lustre."),
(6, "security."),
(7, "system."),
(8, "system.richacl"),
];
pub fn prefix_for_index(idx: u8) -> Option<&'static str> {
NAME_PREFIXES
.iter()
.find(|(i, _)| *i == idx)
.map(|(_, s)| *s)
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct XattrEntry {
pub name: String,
pub value: Vec<u8>,
pub value_inum: u32,
pub value_size: u32,
}
impl XattrEntry {
pub fn new(name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
let value = value.into();
let value_size = u32::try_from(value.len()).expect("an xattr value fits in u32");
Self {
name: name.into(),
value,
value_inum: 0,
value_size,
}
}
pub fn in_ea_inode(name: impl Into<String>, value_inum: u32, value_size: u32) -> Self {
Self {
name: name.into(),
value: Vec::new(),
value_inum,
value_size,
}
}
}
pub fn read_all(
dev: &dyn BlockDevice,
inode: &Inode,
inode_raw: &[u8],
inode_size: u16,
block_size: u32,
) -> Result<Vec<XattrEntry>> {
let mut out = Vec::new();
if inode_raw.len() >= 128 + 4 {
let extra_isize = u16::from_le_bytes(inode_raw[128..130].try_into().unwrap()) as usize;
let xattr_region_start = 128 + extra_isize;
if extra_isize != 0
&& xattr_region_start + 4 <= inode_size as usize
&& xattr_region_start + 4 <= inode_raw.len()
{
let region = &inode_raw[xattr_region_start..(inode_size as usize).min(inode_raw.len())];
let magic = u32::from_le_bytes(region[..4].try_into().unwrap());
if magic == EXT4_XATTR_MAGIC {
parse_entries(®ion[4..], region.len() - 4, &mut out)?;
}
}
}
if inode.file_acl != 0 {
let mut buf = vec![0u8; block_size as usize];
dev.read_at(inode.file_acl * block_size as u64, &mut buf)?;
let magic = u32::from_le_bytes(buf[..4].try_into().unwrap());
if magic != EXT4_XATTR_MAGIC {
return Err(Error::Corrupt("xattr block magic mismatch"));
}
parse_entries_block(&buf, &mut out)?;
}
Ok(out)
}
fn parse_entries(entries_buf: &[u8], _region_len: usize, out: &mut Vec<XattrEntry>) -> Result<()> {
let mut pos = 0;
while pos + 16 <= entries_buf.len() {
let header = u32::from_le_bytes(entries_buf[pos..pos + 4].try_into().unwrap());
if header == 0 {
break;
}
let name_len = entries_buf[pos] as usize;
let name_index = entries_buf[pos + 1];
let value_offs =
u16::from_le_bytes(entries_buf[pos + 2..pos + 4].try_into().unwrap()) as usize;
let value_inum = u32::from_le_bytes(entries_buf[pos + 4..pos + 8].try_into().unwrap());
let value_size =
u32::from_le_bytes(entries_buf[pos + 8..pos + 12].try_into().unwrap()) as usize;
let entry_size = 16 + name_len;
let entry_padded = (entry_size + 3) & !3;
if pos + 16 + name_len > entries_buf.len() {
return Err(Error::Corrupt("xattr entry name overruns region"));
}
let name_bytes = &entries_buf[pos + 16..pos + 16 + name_len];
let prefix = prefix_for_index(name_index).unwrap_or("");
let suffix =
std::str::from_utf8(name_bytes).map_err(|_| Error::Corrupt("xattr name not utf-8"))?;
let full_name = format!("{prefix}{suffix}");
let value = if value_inum != 0 {
Vec::new()
} else if value_size > 0 {
if value_offs + value_size > entries_buf.len() {
return Err(Error::Corrupt("xattr value out of range"));
}
entries_buf[value_offs..value_offs + value_size].to_vec()
} else {
Vec::new()
};
out.push(XattrEntry {
name: full_name,
value,
value_inum,
value_size: value_size as u32,
});
pos += entry_padded;
}
Ok(())
}
fn parse_entries_block(block: &[u8], out: &mut Vec<XattrEntry>) -> Result<()> {
if block.len() < 32 {
return Err(Error::Corrupt("xattr block too small"));
}
let mut pos = 32; while pos + 16 <= block.len() {
let header = u32::from_le_bytes(block[pos..pos + 4].try_into().unwrap());
if header == 0 {
break;
}
let name_len = block[pos] as usize;
let name_index = block[pos + 1];
let value_offs = u16::from_le_bytes(block[pos + 2..pos + 4].try_into().unwrap()) as usize;
let value_inum = u32::from_le_bytes(block[pos + 4..pos + 8].try_into().unwrap());
let value_size = u32::from_le_bytes(block[pos + 8..pos + 12].try_into().unwrap()) as usize;
let entry_size = 16 + name_len;
let entry_padded = (entry_size + 3) & !3;
if pos + 16 + name_len > block.len() {
return Err(Error::Corrupt("xattr entry name overruns block"));
}
let name_bytes = &block[pos + 16..pos + 16 + name_len];
let prefix = prefix_for_index(name_index).unwrap_or("");
let suffix =
std::str::from_utf8(name_bytes).map_err(|_| Error::Corrupt("xattr name not utf-8"))?;
let full_name = format!("{prefix}{suffix}");
let value = if value_inum != 0 {
Vec::new()
} else if value_size > 0 {
if value_offs + value_size > block.len() {
return Err(Error::Corrupt("xattr block value out of range"));
}
block[value_offs..value_offs + value_size].to_vec()
} else {
Vec::new()
};
out.push(XattrEntry {
name: full_name,
value,
value_inum,
value_size: value_size as u32,
});
pos += entry_padded;
}
Ok(())
}
pub fn split_qualified_name(name: &str) -> Option<(u8, &str)> {
for (idx, prefix) in NAME_PREFIXES {
if let Some(rest) = name.strip_prefix(*prefix) {
return Some((*idx, rest));
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoveOutcome {
Removed,
NotFound,
}
pub fn plan_remove_in_inode_region(region: &mut [u8], name: &str) -> Result<RemoveOutcome> {
let Some((name_index, suffix)) = split_qualified_name(name) else {
return Err(Error::InvalidArgument(
"xattr name missing known namespace prefix",
));
};
if region.len() < 4 {
return Ok(RemoveOutcome::NotFound);
}
let magic = u32::from_le_bytes(region[..4].try_into().unwrap());
if magic != EXT4_XATTR_MAGIC {
return Ok(RemoveOutcome::NotFound);
}
let entries = decode_in_inode_entries(®ion[4..])?;
refuse_if_any_ea_inode_backed(&entries)?;
let before = entries.len();
let kept: Vec<DecodedEntry> = entries
.into_iter()
.filter(|e| !(e.name_index == name_index && e.name_bytes == suffix.as_bytes()))
.collect();
if kept.len() == before {
return Ok(RemoveOutcome::NotFound);
}
encode_in_inode_entries(region, &kept);
Ok(RemoveOutcome::Removed)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetOutcome {
Inserted,
Replaced,
}
pub fn plan_set_in_inode_region(region: &mut [u8], name: &str, value: &[u8]) -> Result<SetOutcome> {
let Some((name_index, suffix)) = split_qualified_name(name) else {
return Err(Error::InvalidArgument(
"xattr name missing known namespace prefix",
));
};
if suffix.is_empty() && !matches!(name_index, 2 | 3) {
return Err(Error::InvalidArgument("xattr name suffix is empty"));
}
if suffix.len() > 255 {
return Err(Error::NameTooLong);
}
if region.len() < 8 {
return Err(Error::NoSpaceLeftOnDevice);
}
let magic_present = {
let m = u32::from_le_bytes(region[..4].try_into().unwrap());
m == EXT4_XATTR_MAGIC
};
let mut entries = if magic_present {
decode_in_inode_entries(®ion[4..])?
} else {
Vec::new()
};
refuse_if_any_ea_inode_backed(&entries)?;
let mut outcome = SetOutcome::Inserted;
let suffix_bytes = suffix.as_bytes();
for e in entries.iter_mut() {
if e.name_index == name_index && e.name_bytes == suffix_bytes {
e.value = value.to_vec();
outcome = SetOutcome::Replaced;
break;
}
}
if matches!(outcome, SetOutcome::Inserted) {
entries.push(DecodedEntry {
name_index,
name_bytes: suffix_bytes.to_vec(),
value: value.to_vec(),
value_inum: 0,
});
}
let area_len = region.len() - 4;
let needed_entries: usize = entries
.iter()
.map(|e| (16 + e.name_bytes.len() + 3) & !3)
.sum();
let needed_values: usize = entries
.iter()
.filter(|e| !e.value.is_empty())
.map(|e| (e.value.len() + 3) & !3)
.sum();
if needed_entries + 4 + needed_values > area_len {
return Err(Error::NoSpaceLeftOnDevice);
}
encode_in_inode_entries(region, &entries);
Ok(outcome)
}
#[derive(Debug, Clone)]
struct DecodedEntry {
name_index: u8,
name_bytes: Vec<u8>,
value: Vec<u8>,
value_inum: u32,
}
fn refuse_if_any_ea_inode_backed(entries: &[DecodedEntry]) -> Result<()> {
if entries.iter().any(|e| e.value_inum != 0) {
return Err(Error::Unsupported(
"this inode has an extended attribute whose value lives in an EA inode; \
rewriting the attribute area would orphan it",
));
}
Ok(())
}
fn decode_in_inode_entries(entries_buf: &[u8]) -> Result<Vec<DecodedEntry>> {
let mut out = Vec::new();
let mut pos = 0usize;
while pos + 16 <= entries_buf.len() {
let header = u32::from_le_bytes(entries_buf[pos..pos + 4].try_into().unwrap());
if header == 0 {
break;
}
let name_len = entries_buf[pos] as usize;
let name_index = entries_buf[pos + 1];
let value_offs =
u16::from_le_bytes(entries_buf[pos + 2..pos + 4].try_into().unwrap()) as usize;
let value_inum = u32::from_le_bytes(entries_buf[pos + 4..pos + 8].try_into().unwrap());
let value_size =
u32::from_le_bytes(entries_buf[pos + 8..pos + 12].try_into().unwrap()) as usize;
if pos + 16 + name_len > entries_buf.len() {
return Err(Error::Corrupt("xattr entry name overruns region"));
}
let name_bytes = entries_buf[pos + 16..pos + 16 + name_len].to_vec();
let value = if value_inum != 0 || value_size == 0 {
Vec::new()
} else {
if value_offs + value_size > entries_buf.len() {
return Err(Error::Corrupt("xattr value out of range"));
}
entries_buf[value_offs..value_offs + value_size].to_vec()
};
out.push(DecodedEntry {
name_index,
name_bytes,
value,
value_inum,
});
pos += (16 + name_len + 3) & !3;
}
Ok(out)
}
fn encode_in_inode_entries(region: &mut [u8], entries: &[DecodedEntry]) {
for b in region.iter_mut() {
*b = 0;
}
region[..4].copy_from_slice(&EXT4_XATTR_MAGIC.to_le_bytes());
let entries_area = &mut region[4..];
let area_len = entries_area.len();
let mut sorted: Vec<&DecodedEntry> = entries.iter().collect();
sorted.sort_by(|a, b| {
a.name_index
.cmp(&b.name_index)
.then_with(|| a.name_bytes.cmp(&b.name_bytes))
});
let mut entry_cursor: usize = 0;
let mut value_cursor: usize = area_len;
for e in &sorted {
let name_len = e.name_bytes.len();
let entry_padded = (16 + name_len + 3) & !3;
let value_offs = if e.value.is_empty() {
0
} else {
let value_padded = (e.value.len() + 3) & !3;
value_cursor -= value_padded;
entries_area[value_cursor..value_cursor + e.value.len()].copy_from_slice(&e.value);
value_cursor
};
entries_area[entry_cursor] = name_len as u8;
entries_area[entry_cursor + 1] = e.name_index;
entries_area[entry_cursor + 2..entry_cursor + 4]
.copy_from_slice(&(value_offs as u16).to_le_bytes());
entries_area[entry_cursor + 8..entry_cursor + 12]
.copy_from_slice(&(e.value.len() as u32).to_le_bytes());
entries_area[entry_cursor + 16..entry_cursor + 16 + name_len]
.copy_from_slice(&e.name_bytes);
entry_cursor += entry_padded;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockRemoveOutcome {
Removed,
RemovedNowEmpty,
NotFound,
}
fn decode_external_block_entries(block: &[u8]) -> Result<Vec<DecodedEntry>> {
if block.len() < 32 {
return Err(Error::Corrupt("xattr block too small"));
}
let mut out = Vec::new();
let mut pos = 32usize;
while pos + 16 <= block.len() {
let header = u32::from_le_bytes(block[pos..pos + 4].try_into().unwrap());
if header == 0 {
break;
}
let name_len = block[pos] as usize;
let name_index = block[pos + 1];
let value_offs = u16::from_le_bytes(block[pos + 2..pos + 4].try_into().unwrap()) as usize;
let value_inum = u32::from_le_bytes(block[pos + 4..pos + 8].try_into().unwrap());
let value_size = u32::from_le_bytes(block[pos + 8..pos + 12].try_into().unwrap()) as usize;
if pos + 16 + name_len > block.len() {
return Err(Error::Corrupt("xattr entry name overruns block"));
}
let name_bytes = block[pos + 16..pos + 16 + name_len].to_vec();
let value = if value_inum != 0 || value_size == 0 {
Vec::new()
} else {
if value_offs + value_size > block.len() {
return Err(Error::Corrupt("xattr block value out of range"));
}
block[value_offs..value_offs + value_size].to_vec()
};
out.push(DecodedEntry {
name_index,
name_bytes,
value,
value_inum,
});
pos += (16 + name_len + 3) & !3;
}
Ok(out)
}
fn encode_external_block(block: &mut [u8], entries: &[DecodedEntry], refcount: u32) {
block.fill(0);
block[0x00..0x04].copy_from_slice(&EXT4_XATTR_MAGIC.to_le_bytes());
block[0x04..0x08].copy_from_slice(&refcount.to_le_bytes());
block[0x08..0x0C].copy_from_slice(&1u32.to_le_bytes());
let mut sorted: Vec<&DecodedEntry> = entries.iter().collect();
sorted.sort_by(|a, b| {
a.name_index
.cmp(&b.name_index)
.then_with(|| a.name_bytes.len().cmp(&b.name_bytes.len()))
.then_with(|| a.name_bytes.cmp(&b.name_bytes))
});
let mut entry_cursor: usize = 0x20;
let mut value_cursor: usize = block.len();
let mut hashes = Vec::with_capacity(sorted.len());
for e in &sorted {
let name_len = e.name_bytes.len();
let entry_padded = (16 + name_len + 3) & !3;
let value_offs = if e.value.is_empty() {
0
} else {
let value_padded = (e.value.len() + 3) & !3;
value_cursor -= value_padded;
block[value_cursor..value_cursor + e.value.len()].copy_from_slice(&e.value);
value_cursor
};
let e_hash = entry_hash(&e.name_bytes, &e.value);
hashes.push(e_hash);
block[entry_cursor] = name_len as u8;
block[entry_cursor + 1] = e.name_index;
block[entry_cursor + 2..entry_cursor + 4]
.copy_from_slice(&(value_offs as u16).to_le_bytes());
block[entry_cursor + 8..entry_cursor + 12]
.copy_from_slice(&(e.value.len() as u32).to_le_bytes());
block[entry_cursor + 12..entry_cursor + 16].copy_from_slice(&e_hash.to_le_bytes());
block[entry_cursor + 16..entry_cursor + 16 + name_len].copy_from_slice(&e.name_bytes);
entry_cursor += entry_padded;
}
block[0x0C..0x10].copy_from_slice(&block_hash(&hashes).to_le_bytes());
}
const NAME_BYTE_ROTATION: u32 = 5;
const WORD_ROTATION: u32 = 16;
fn entry_hash(name: &[u8], value: &[u8]) -> u32 {
let after_name = name.iter().fold(0u32, |acc, &byte| {
acc.rotate_left(NAME_BYTE_ROTATION) ^ u32::from(byte)
});
value.chunks(4).fold(after_name, |acc, piece| {
let mut word = [0u8; 4];
word[..piece.len()].copy_from_slice(piece);
acc.rotate_left(WORD_ROTATION) ^ u32::from_le_bytes(word)
})
}
fn block_hash(entry_hashes: &[u32]) -> u32 {
if entry_hashes.contains(&0) {
return 0;
}
entry_hashes
.iter()
.fold(0u32, |acc, &hash| acc.rotate_left(WORD_ROTATION) ^ hash)
}
pub fn plan_set_in_external_block(
block: &mut [u8],
name: &str,
value: &[u8],
refcount: u32,
) -> Result<SetOutcome> {
let Some((name_index, suffix)) = split_qualified_name(name) else {
return Err(Error::InvalidArgument(
"xattr name missing known namespace prefix",
));
};
if suffix.is_empty() && !matches!(name_index, 2 | 3) {
return Err(Error::InvalidArgument("xattr name suffix is empty"));
}
if suffix.len() > 255 {
return Err(Error::NameTooLong);
}
if block.len() < 0x40 {
return Err(Error::NoSpaceLeftOnDevice);
}
let magic_present = u32::from_le_bytes(block[..4].try_into().unwrap()) == EXT4_XATTR_MAGIC;
let mut entries = if magic_present {
decode_external_block_entries(block)?
} else {
Vec::new()
};
refuse_if_any_ea_inode_backed(&entries)?;
let mut outcome = SetOutcome::Inserted;
let suffix_bytes = suffix.as_bytes();
for e in entries.iter_mut() {
if e.name_index == name_index && e.name_bytes == suffix_bytes {
e.value = value.to_vec();
outcome = SetOutcome::Replaced;
break;
}
}
if matches!(outcome, SetOutcome::Inserted) {
entries.push(DecodedEntry {
name_index,
name_bytes: suffix_bytes.to_vec(),
value: value.to_vec(),
value_inum: 0,
});
}
let entries_capacity = block.len() - 0x20;
let needed_entries: usize = entries
.iter()
.map(|e| (16 + e.name_bytes.len() + 3) & !3)
.sum();
let needed_values: usize = entries
.iter()
.filter(|e| !e.value.is_empty())
.map(|e| (e.value.len() + 3) & !3)
.sum();
if needed_entries + 4 + needed_values > entries_capacity {
return Err(Error::NoSpaceLeftOnDevice);
}
encode_external_block(block, &entries, refcount);
Ok(outcome)
}
pub fn plan_remove_from_external_block(
block: &mut [u8],
name: &str,
refcount: u32,
) -> Result<BlockRemoveOutcome> {
let Some((name_index, suffix)) = split_qualified_name(name) else {
return Err(Error::InvalidArgument(
"xattr name missing known namespace prefix",
));
};
if block.len() < 0x20 {
return Ok(BlockRemoveOutcome::NotFound);
}
let magic = u32::from_le_bytes(block[..4].try_into().unwrap());
if magic != EXT4_XATTR_MAGIC {
return Ok(BlockRemoveOutcome::NotFound);
}
let entries = decode_external_block_entries(block)?;
refuse_if_any_ea_inode_backed(&entries)?;
let before = entries.len();
let kept: Vec<DecodedEntry> = entries
.into_iter()
.filter(|e| !(e.name_index == name_index && e.name_bytes == suffix.as_bytes()))
.collect();
if kept.len() == before {
return Ok(BlockRemoveOutcome::NotFound);
}
if kept.is_empty() {
return Ok(BlockRemoveOutcome::RemovedNowEmpty);
}
encode_external_block(block, &kept, refcount);
Ok(BlockRemoveOutcome::Removed)
}
pub(crate) fn check_external_block(
csum: &crate::checksum::Checksummer,
block_nr: u64,
block: &[u8],
) -> Result<()> {
if block.len() < 0x20 || u32::from_le_bytes(block[..4].try_into().unwrap()) != EXT4_XATTR_MAGIC
{
return Err(Error::Corrupt(
"i_file_acl names a block without the xattr magic",
));
}
if u32::from_le_bytes(block[8..12].try_into().unwrap()) != 1 {
return Err(Error::Corrupt("xattr block h_blocks is not 1"));
}
if !csum.verify_xattr_block(block_nr, block) {
return Err(Error::BadChecksum {
what: "xattr block",
});
}
Ok(())
}
fn check_block_of(fs: &Filesystem, inode: &Inode) -> Result<()> {
if inode.file_acl == 0 {
return Ok(());
}
let bs = fs.sb.block_size();
let mut block = vec![0u8; bs as usize];
fs.dev.read_at(inode.file_acl * bs as u64, &mut block)?;
check_external_block(&fs.csum, inode.file_acl, &block)
}
pub fn get(
dev: &dyn BlockDevice,
inode: &Inode,
inode_raw: &[u8],
inode_size: u16,
block_size: u32,
name: &str,
) -> Result<Option<Vec<u8>>> {
let all = read_all(dev, inode, inode_raw, inode_size, block_size)?;
Ok(all.into_iter().find(|e| e.name == name).map(|e| e.value))
}
pub fn read_all_resolved(
fs: &Filesystem,
inode: &Inode,
inode_raw: &[u8],
) -> Result<Vec<XattrEntry>> {
check_block_of(fs, inode)?;
let mut entries = read_all(
fs.dev.as_ref(),
inode,
inode_raw,
fs.sb.inode_size,
fs.sb.block_size(),
)?;
for e in entries.iter_mut() {
if e.value_inum != 0 {
e.value = crate::ea_inode::read_value_inode(fs, e.value_inum, e.value_size)?;
}
}
Ok(entries)
}
pub fn list_names(fs: &Filesystem, inode: &Inode, inode_raw: &[u8]) -> Result<Vec<String>> {
check_block_of(fs, inode)?;
Ok(read_all(
fs.dev.as_ref(),
inode,
inode_raw,
fs.sb.inode_size,
fs.sb.block_size(),
)?
.into_iter()
.map(|e| e.name)
.collect())
}
pub fn get_resolved(
fs: &Filesystem,
inode: &Inode,
inode_raw: &[u8],
name: &str,
) -> Result<Option<Vec<u8>>> {
check_block_of(fs, inode)?;
let Some(entry) = read_all(
fs.dev.as_ref(),
inode,
inode_raw,
fs.sb.inode_size,
fs.sb.block_size(),
)?
.into_iter()
.find(|e| e.name == name) else {
return Ok(None);
};
if entry.value_inum != 0 {
return Ok(Some(crate::ea_inode::read_value_inode(
fs,
entry.value_inum,
entry.value_size,
)?));
}
Ok(Some(entry.value))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prefixes_known() {
assert_eq!(prefix_for_index(1), Some("user."));
assert_eq!(prefix_for_index(7), Some("system."));
assert_eq!(prefix_for_index(99), None);
}
#[test]
fn split_qualified_name_roundtrip() {
assert_eq!(split_qualified_name("user.color"), Some((1, "color")));
assert_eq!(
split_qualified_name("user.com.apple.FinderInfo"),
Some((1, "com.apple.FinderInfo"))
);
assert_eq!(
split_qualified_name("security.selinux"),
Some((6, "selinux"))
);
assert_eq!(split_qualified_name("unknown.foo"), None);
}
#[test]
fn new_builds_what_the_parser_reads_for_an_inline_value() {
let mut region = vec![0u8; 64];
encode_in_inode_entries(
&mut region,
&[DecodedEntry {
name_index: 1,
name_bytes: b"color".to_vec(),
value: b"red".to_vec(),
value_inum: 0,
}],
);
let mut parsed = Vec::new();
parse_entries(®ion[4..], region.len() - 4, &mut parsed).unwrap();
assert_eq!(parsed, vec![XattrEntry::new("user.color", b"red".to_vec())]);
}
#[test]
fn in_ea_inode_carries_the_inode_and_declared_size_and_no_bytes() {
let e = XattrEntry::in_ea_inode("user.big", 42, 70_000);
assert_eq!(e.name, "user.big");
assert!(e.value.is_empty());
assert_eq!((e.value_inum, e.value_size), (42, 70_000));
assert_ne!(e, XattrEntry::new("user.big", Vec::new()));
}
#[test]
fn remove_in_inode_roundtrips_one_of_two() {
let mut region = vec![0u8; 96];
let entries = vec![
DecodedEntry {
name_index: 1,
name_bytes: b"color".to_vec(),
value: b"red".to_vec(),
value_inum: 0,
},
DecodedEntry {
name_index: 1,
name_bytes: b"mood".to_vec(),
value: b"happy".to_vec(),
value_inum: 0,
},
];
encode_in_inode_entries(&mut region, &entries);
let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
assert_eq!(decoded.len(), 2);
let outcome = plan_remove_in_inode_region(&mut region, "user.color").unwrap();
assert_eq!(outcome, RemoveOutcome::Removed);
let after = decode_in_inode_entries(®ion[4..]).unwrap();
assert_eq!(after.len(), 1);
assert_eq!(after[0].name_bytes, b"mood");
assert_eq!(after[0].value, b"happy");
}
#[test]
fn remove_in_inode_returns_not_found_for_missing() {
let mut region = vec![0u8; 64];
let entries = vec![DecodedEntry {
name_index: 1,
name_bytes: b"color".to_vec(),
value: b"red".to_vec(),
value_inum: 0,
}];
encode_in_inode_entries(&mut region, &entries);
let outcome = plan_remove_in_inode_region(&mut region, "user.mood").unwrap();
assert_eq!(outcome, RemoveOutcome::NotFound);
}
#[test]
fn remove_in_inode_unknown_prefix_is_einval() {
let mut region = vec![0u8; 64];
let err = plan_remove_in_inode_region(&mut region, "nope.name").unwrap_err();
assert!(matches!(err, Error::InvalidArgument(_)));
}
#[test]
fn remove_in_inode_missing_magic_is_not_found() {
let mut region = vec![0u8; 64];
let outcome = plan_remove_in_inode_region(&mut region, "user.x").unwrap();
assert_eq!(outcome, RemoveOutcome::NotFound);
}
#[test]
fn set_in_inode_inserts_new_into_empty_region() {
let mut region = vec![0u8; 64];
let outcome = plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
assert_eq!(outcome, SetOutcome::Inserted);
let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].name_bytes, b"color");
assert_eq!(decoded[0].value, b"red");
}
#[test]
fn set_in_inode_replaces_existing_value() {
let mut region = vec![0u8; 96];
plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
let outcome = plan_set_in_inode_region(&mut region, "user.color", b"emerald").unwrap();
assert_eq!(outcome, SetOutcome::Replaced);
let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].value, b"emerald");
}
#[test]
fn set_in_inode_preserves_other_entries() {
let mut region = vec![0u8; 128];
plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
plan_set_in_inode_region(&mut region, "user.mood", b"happy").unwrap();
plan_set_in_inode_region(&mut region, "user.color", b"blue").unwrap();
let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
let by_name: std::collections::BTreeMap<_, _> = decoded
.into_iter()
.map(|e| (e.name_bytes.clone(), e.value))
.collect();
assert_eq!(by_name.get(b"color".as_slice()).unwrap(), b"blue");
assert_eq!(by_name.get(b"mood".as_slice()).unwrap(), b"happy");
}
#[test]
fn set_in_inode_enospc_on_overflow() {
let mut region = vec![0u8; 32];
let err =
plan_set_in_inode_region(&mut region, "user.x", b"this_is_20_bytes_xx!").unwrap_err();
assert!(matches!(err, Error::NoSpaceLeftOnDevice));
}
#[test]
fn set_in_inode_unknown_prefix_is_einval() {
let mut region = vec![0u8; 64];
let err = plan_set_in_inode_region(&mut region, "weird.key", b"v").unwrap_err();
assert!(matches!(err, Error::InvalidArgument(_)));
}
#[test]
fn external_block_entries_are_sorted_by_name_length_before_name() {
let mut block = vec![0u8; 4096];
plan_set_in_external_block(&mut block, "user.abc", b"1", 1).unwrap();
plan_set_in_external_block(&mut block, "user.zz", b"2", 1).unwrap();
assert_eq!(
block[0x20],
2,
"the first entry must be the shorter name (zz), not {:?}",
String::from_utf8_lossy(&block[0x30..0x30 + block[0x20] as usize])
);
plan_set_in_external_block(&mut block, "trusted.a", b"3", 1).unwrap();
let order: Vec<(u8, Vec<u8>)> = decode_external_block_entries(&block)
.unwrap()
.into_iter()
.map(|e| (e.name_index, e.name_bytes))
.collect();
assert_eq!(
order,
vec![
(1, b"zz".to_vec()),
(1, b"abc".to_vec()),
(4, b"a".to_vec()),
]
);
}
}
#[cfg(test)]
mod hash_tests {
use super::{block_hash, entry_hash, NAME_BYTE_ROTATION, WORD_ROTATION};
const OBSERVED: &[(&[u8], &[u8], u32)] = &[
(b"a", b"x", 0x0061_0078),
(b"bb", b"hello", 0x6568_6021),
(b"empty", b"", 0x0667_4EF9),
(b"z", b"0123456789", 0x067C_3F3E),
(b"q", b"1", 0x0071_0031),
(b"a", b"hi", 0x0061_6968),
("q\u{e9}".as_bytes(), b"val", 0xDCA5_6177),
(b"a", &[0x00, 0x00, 0x61, 0x00], 0),
(b"zzz", &[0x01, 0x00, 0x3A, 0xE7], 0),
];
#[test]
fn entry_hashes_match_the_observed_words() {
for &(name, value, want) in OBSERVED {
assert_eq!(
entry_hash(name, value),
want,
"{:?}={value:?}",
String::from_utf8_lossy(name)
);
}
}
#[test]
fn the_block_hash_folds_the_entry_hashes_in_order() {
let hashes = [0x0061_0078, 0x6568_6021, 0x0667_4EF9, 0x067C_3F3E];
assert_eq!(block_hash(&hashes), 0x2D95_5919);
assert_eq!(
block_hash(&hashes[..1]),
hashes[0],
"one entry is its own hash"
);
assert_eq!(block_hash(&[]), 0);
}
#[test]
fn a_zero_entry_hash_anywhere_zeroes_the_block_hash() {
assert_eq!(block_hash(&[0, 0x6568_6021]), 0);
assert_eq!(block_hash(&[0x0061_6968, 0]), 0);
}
fn sign_extending_entry_hash(name: &[u8], value: &[u8]) -> u32 {
let after_name = name.iter().fold(0u32, |acc, &byte| {
acc.rotate_left(NAME_BYTE_ROTATION) ^ (i32::from(byte as i8) as u32)
});
value.chunks(4).fold(after_name, |acc, piece| {
let mut word = [0u8; 4];
word[..piece.len()].copy_from_slice(piece);
acc.rotate_left(WORD_ROTATION) ^ u32::from_le_bytes(word)
})
}
#[test]
fn a_high_name_byte_has_two_readings() {
for &(name, value, _) in OBSERVED {
if name.iter().all(|&byte| byte <= 0x7F) {
assert_eq!(
sign_extending_entry_hash(name, value),
entry_hash(name, value)
);
}
}
let name = "q\u{e9}".as_bytes();
assert_eq!(entry_hash(name, b"val"), 0xDCA5_6177);
assert_eq!(sign_extending_entry_hash(name, b"val"), 0xC3BA_6177);
}
mod needs_host {
use super::super::{
block_hash, decode_external_block_entries, encode_external_block, entry_hash,
};
use super::sign_extending_entry_hash;
use crate::block_io::FileDevice;
use crate::fs::Filesystem;
use std::sync::Arc;
type AttributeSet = (&'static str, &'static [(&'static str, &'static [u8])]);
const FILES: &[AttributeSet] = &[
(
"plain",
&[
("user.a", b"x"),
("user.bb", b"hello"),
("user.empty", b""),
("trusted.z", b"0123456789"),
("user.q\u{e9}", b"val"),
],
),
(
"zero",
&[("user.a", b"hi"), ("user.zzz", &[0x01, 0x00, 0x3A, 0xE7])],
),
];
fn volume(tag: &str) -> String {
let image = fs_ext4_test_support::temp_path!(
"fs_ext4_xattr_hash_{tag}_{}.img",
std::process::id()
);
std::fs::File::create(&image)
.and_then(|f| f.set_len(8 << 20))
.unwrap();
let out = fs_ext4_test_support::oracle("mkfs.ext4")
.args(["-q", "-F", "-b", "1024", "-I", "128", &image])
.output();
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
image
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn block_hashes(fs: &Filesystem, path: &str) -> (Vec<u32>, u32, Vec<u8>) {
let ino = fs.lookup_path_bytes(path.as_bytes()).unwrap();
let inode = fs.read_inode_verified(ino).unwrap().0;
assert_ne!(inode.file_acl, 0, "{path} has no external block");
let block = fs.read_block(inode.file_acl).unwrap();
let mut hashes = Vec::new();
let mut at = 0x20;
while u32::from_le_bytes(block[at..at + 4].try_into().unwrap()) != 0 {
hashes.push(u32::from_le_bytes(
block[at + 12..at + 16].try_into().unwrap(),
));
at += (16 + block[at] as usize + 3) & !3;
}
let h_hash = u32::from_le_bytes(block[0x0C..0x10].try_into().unwrap());
(hashes, h_hash, block)
}
#[test]
fn hashes_in_kernel_written_blocks_are_the_ones_computed_here() {
let image = volume("kernel");
let mut script = String::from("set -e\n");
for (file, attrs) in FILES {
script.push_str(&format!("touch \"$MNT/{file}\"\n"));
for (name, value) in *attrs {
script.push_str(&format!(
"setfattr -n '{name}' -v 0x{} \"$MNT/{file}\"\n",
hex(value)
));
}
}
let script = script.replace("-v 0x \"", "\"");
let out = fs_ext4_test_support::guest_kernel_write(&image, &script);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
for (file, attrs) in FILES {
let path = format!("/{file}");
let (hashes, h_hash, block) = block_hashes(&fs, &path);
assert_eq!(hashes.len(), attrs.len(), "{path}");
let entries = decode_external_block_entries(&block).unwrap();
assert_eq!(entries.len(), hashes.len(), "{path}");
let computed: Vec<u32> = entries
.iter()
.map(|entry| entry_hash(&entry.name_bytes, &entry.value))
.collect();
for ((entry, &stored), &ours) in entries.iter().zip(&hashes).zip(&computed) {
let name = String::from_utf8_lossy(&entry.name_bytes);
if stored == ours {
continue;
}
assert!(
entry.name_bytes.iter().any(|&byte| byte > 0x7F),
"{path} {name}: the kernel stored {stored:#010x}, and this crate \
computes {ours:#010x}"
);
assert_eq!(
sign_extending_entry_hash(&entry.name_bytes, &entry.value),
stored,
"{path} {name}: the kernel's hash is neither reading"
);
}
assert_eq!(block_hash(&hashes), h_hash, "{path}'s h_hash");
let mut ours = vec![0u8; block.len()];
encode_external_block(&mut ours, &entries, 1);
assert_eq!(
ours[0x0C..0x10],
block_hash(&computed).to_le_bytes(),
"{path}: h_hash re-encoded"
);
if computed == hashes {
assert_eq!(
ours[0x0C..0x10],
block[0x0C..0x10],
"{path}: h_hash re-encoded"
);
}
}
drop(fs);
fs_ext4_test_support::assert_e2fsck_clean(&image, "the kernel's blocks");
let _ = std::fs::remove_file(&image);
}
#[test]
fn blocks_this_crate_writes_carry_the_kernels_hashes() {
let image = volume("ours");
{
let fs = Filesystem::mount(Arc::new(FileDevice::open_rw(&image).unwrap())).unwrap();
for (file, attrs) in FILES {
let path = format!("/{file}");
fs.apply_create(&path, 0o644).unwrap();
for (name, value) in *attrs {
fs.apply_setxattr(&path, name, value).unwrap();
}
}
}
fs_ext4_test_support::assert_e2fsck_clean(&image, "this crate's blocks");
let expected: &[(&str, &[u32], u32)] = &[
(
"/plain",
&[
0x0061_0078,
0x6568_6021,
0xDCA5_6177,
0x0667_4EF9,
0x067C_3F3E,
],
block_hash(&[
0x0061_0078,
0x6568_6021,
0xDCA5_6177,
0x0667_4EF9,
0x067C_3F3E,
]),
),
("/zero", &[0x0061_6968, 0], 0),
];
let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
for &(path, hashes, h_hash) in expected {
let (got, got_h, _) = block_hashes(&fs, path);
assert_eq!(got, hashes, "{path}");
assert_eq!(got_h, h_hash, "{path}");
}
drop(fs);
let _ = std::fs::remove_file(&image);
}
}
}