[PATCH v2] erofs-rs: fix inline xattr size for tail offsets

priyena.programming
2026-04-10T15:41:56Z

From: Transcendental-Programmer <priyena.programming@gmail.com>

---
erofs/src/async/filesystem.rs | 64 ++++++++++++++++++++++++++++++++++-
erofs/src/filesystem.rs | 53 +++++++++++++++++++++++++++--
erofs/src/sync/filesystem.rs | 34 ++++++++++++++++++-
erofs/src/types.rs | 7 ++++
4 files changed, 153 insertions(+), 5 deletions(-)

diff --git a/erofs/src/async/filesystem.rs b/erofs/src/async/filesystem.rs
index 1987f09..c791b49 100644
--- a/erofs/src/async/filesystem.rs
+++ b/erofs/src/async/filesystem.rs
@@ -1,5 +1,7 @@
use alloc::format;
use alloc::vec::Vec;
+use binrw::{BinRead, io::Cursor};
+use core::mem::size_of;
use typed_path::Component;

use bytes::Buf;
@@ -83,6 +85,44 @@ impl<I: AsyncImage> EroFS {
self.core.block_size
}

+ async fn xattr_ibody_size(&self, inode: &Inode) -> Result<usize> {
+ let total_count = inode.xattr_count();
+ if total_count == 0 {
+ return Ok(0);
+ }
+
+ let inode_offset = self.core.get_inode_offset(inode.id()) as usize;
+ let xattr_start = inode_offset + inode.size();
+
+ let mut header_buf = vec![0u8; size_of::<XattrHeader>()];
+ self.image.read_exact_at(&mut header_buf, xattr_start).await?;
+ let header = XattrHeader::read(&mut Cursor::new(&header_buf))?;
+
+ let shared_count = header.shared_count as usize;
+ let total = total_count as usize;
+ if shared_count > total {
+ return Err(Error::CorruptedData(
+ "xattr shared count exceeds total count".to_string(),
+ ));
+ }
+
+ let mut offset = xattr_start + size_of::<XattrHeader>() + shared_count * size_of::<u32>();
+ let inline_count = total - shared_count;
+ for _ in 0..inline_count {
+ let mut entry_buf = vec![0u8; size_of::<XattrEntry>()];
+ self.image.read_exact_at(&mut entry_buf, offset).await?;
+ let entry = XattrEntry::read(&mut Cursor::new(&entry_buf))?;
+ offset += size_of::<XattrEntry>();
+
+ let payload = entry.name_len as usize + entry.value_len as usize;
+ offset = offset
+ .checked_add(payload)
+ .ok_or_else(|| Error::CorruptedData("xattr size overflow".to_string()))?;
+ }
+
+ Ok(offset - xattr_start)
+ }
+
pub async fn get_inode(&self, nid: u64) -> Result<Inode> {
let offset = self.core.get_inode_offset(nid) as usize;
let mut buf = vec![0u8; InodeExtended::size()];
@@ -91,7 +131,29 @@ impl<I: AsyncImage> EroFS {
}

pub(crate) async fn read_inode_block(&self, inode: &Inode, offset: usize) -> Result<Vec<u8>> {
- match self.core.plan_inode_block_read(inode, offset)? {
+ let layout = inode.layout()?;
+ let xattr_size = if inode.xattr_count() == 0 {
+ 0
+ } else {
+ match layout {
+ Layout::FlatInline => {
+ let block_count = inode.data_size().div_ceil(self.core.block_size);
+ let block_index = offset / self.core.block_size;
+ if block_count != 0 && block_index == block_count - 1 {
+ self.xattr_ibody_size(inode).await?
+ } else {
+ 0
+ }
+ }
+ Layout::ChunkBased => self.xattr_ibody_size(inode).await?,
+ _ => 0,
+ }
+ };
+
+ match self
+ .core
+ .plan_inode_block_read(inode, offset, xattr_size)?
+ {
BlockPlan::Direct { offset, size } => {
if size > self.core.block_size {
return Err(Error::CorruptedData(format!(
diff --git a/erofs/src/filesystem.rs b/erofs/src/filesystem.rs
index dfe22aa..d44a5cd 100644
--- a/erofs/src/filesystem.rs
+++ b/erofs/src/filesystem.rs
@@ -1,4 +1,5 @@
use alloc::{format, string::ToString};
+use core::mem::size_of;

use binrw::BinRead;
use binrw::BinReaderExt;
@@ -87,7 +88,12 @@ impl EroFSCore {
/// Returns a `BlockPlan` describing what bytes to read.
/// For `BlockPlan::Chunked`, the caller must perform an additional
/// read and call `resolve_chunk_read()`.
- pub(crate) fn plan_inode_block_read(&self, inode: &Inode, offset: usize) -> Result<BlockPlan> {
+ pub(crate) fn plan_inode_block_read(
+ &self,
+ inode: &Inode,
+ offset: usize,
+ xattr_size: usize,
+ ) -> Result<BlockPlan> {
match inode.layout()? {
Layout::FlatPlain => {
let block_count = inode.data_size().div_ceil(self.block_size);
@@ -112,7 +118,7 @@ impl EroFSCore {
// tail block
let inode_offset = self.get_inode_offset(inode.id());
let buf_size = inode.data_size() % self.block_size;
- let offset = inode_offset as usize + inode.size() + inode.xattr_size();
+ let offset = inode_offset as usize + inode.size() + xattr_size;
return Ok(BlockPlan::Direct {
offset,
size: buf_size,
@@ -151,7 +157,7 @@ impl EroFSCore {

let inode_offset = self.get_inode_offset(inode.id());
let addr_offset =
- inode_offset as usize + inode.size() + inode.xattr_size() + (chunk_index * 4);
+ inode_offset as usize + inode.size() + xattr_size + (chunk_index * 4);

Ok(BlockPlan::Chunked {
addr_offset,
@@ -201,4 +207,45 @@ impl EroFSCore {
pub(crate) fn block_offset(&self, block: u32) -> u64 {
(block as u64) << self.super_block.blk_size_bits
}
+
+ pub(crate) fn xattr_ibody_size_from_slice(
+ data: &[u8],
+ total_count: u16,
+ ) -> Result<usize> {
+ if total_count == 0 {
+ return Ok(0);
+ }
+
+ let header = XattrHeader::read(&mut Cursor::new(data))?;
+ let shared_count = header.shared_count as usize;
+ let total = total_count as usize;
+ if shared_count > total {
+ return Err(Error::CorruptedData(
+ "xattr shared count exceeds total count".to_string(),
+ ));
+ }
+
+ let mut offset = size_of::<XattrHeader>() + shared_count * size_of::<u32>();
+ let inline_count = total - shared_count;
+ for _ in 0..inline_count {
+ if data.len() < offset + size_of::<XattrEntry>() {
+ return Err(Error::CorruptedData(
+ "xattr entry header out of range".to_string(),
+ ));
+ }
+
+ let entry = XattrEntry::read(&mut Cursor::new(&data[offset..]))?;
+ offset += size_of::<XattrEntry>();
+
+ let payload = entry.name_len as usize + entry.value_len as usize;
+ if data.len() < offset + payload {
+ return Err(Error::CorruptedData(
+ "xattr entry payload out of range".to_string(),
+ ));
+ }
+ offset += payload;
+ }
+
+ Ok(offset)
+ }
}
diff --git a/erofs/src/sync/filesystem.rs b/erofs/src/sync/filesystem.rs
index 2727383..7d94727 100644
--- a/erofs/src/sync/filesystem.rs
+++ b/erofs/src/sync/filesystem.rs
@@ -143,6 +143,16 @@ impl<I: Image> EroFS {
self.core.block_size
}

+ fn xattr_ibody_size(&self, inode: &Inode) -> Result<usize> {
+ let inode_offset = self.core.get_inode_offset(inode.id()) as usize;
+ let xattr_start = inode_offset + inode.size();
+ let data = self
+ .image
+ .get(xattr_start..)
+ .ok_or_else(|| Error::OutOfBounds("failed to read xattr data".to_string()))?;
+ EroFSCore::xattr_ibody_size_from_slice(data, inode.xattr_count())
+ }
+
pub fn get_inode(&self, nid: u64) -> Result<Inode> {
let offset = self.core.get_inode_offset(nid) as usize;
let data = self
@@ -153,7 +163,29 @@ impl<I: Image> EroFS {
}

pub(crate) fn get_inode_block(&self, inode: &Inode, offset: usize) -> Result<&[u8]> {
- match self.core.plan_inode_block_read(inode, offset)? {
+ let layout = inode.layout()?;
+ let xattr_size = if inode.xattr_count() == 0 {
+ 0
+ } else {
+ match layout {
+ Layout::FlatInline => {
+ let block_count = inode.data_size().div_ceil(self.core.block_size);
+ let block_index = offset / self.core.block_size;
+ if block_count != 0 && block_index == block_count - 1 {
+ self.xattr_ibody_size(inode)?
+ } else {
+ 0
+ }
+ }
+ Layout::ChunkBased => self.xattr_ibody_size(inode)?,
+ _ => 0,
+ }
+ };
+
+ match self
+ .core
+ .plan_inode_block_read(inode, offset, xattr_size)?
+ {
BlockPlan::Direct { offset, size } => self
.image
.get(offset..offset + size)
diff --git a/erofs/src/types.rs b/erofs/src/types.rs
index f4cfcf6..5bb6df1 100644
--- a/erofs/src/types.rs
+++ b/erofs/src/types.rs
@@ -185,6 +185,13 @@ impl Inode {
}
}

+ pub fn xattr_count(&self) -> u16 {
+ match self {
+ Self::Compact((_, n)) => n.xattr_count,
+ Self::Extended((_, n)) => n.xattr_count,
+ }
+ }
+
pub fn file_type(&self) -> FileType {
match self {
Self::Compact((_, n)) => FileType::from_raw_mode(n.mode as _),
--
2.43.0