Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion libkernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ tock-registers = { version = "0.10.1", optional = true }

# fs
async-trait = { workspace = true, optional = true }
ext4plus = { version = "0.1.0-beta.3", optional = true }
ext4plus = { version = "0.1.0-rc.2", optional = true }

# proc_vm
object = { version = "0.39.0", default-features = false, features = ["core", "elf", "read_core"], optional = true }
Expand Down
70 changes: 70 additions & 0 deletions libkernel/src/fs/filesystems/ext4/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use alloc::collections::BTreeMap;
use alloc::sync::{Arc, Weak};
use core::num::NonZeroU32;

/// Shares live inodes, without retaining every inode ever visited.
pub(super) struct InodeCache<T> {
entries: BTreeMap<NonZeroU32, Weak<T>>,
}

impl<T> InodeCache<T> {
pub(super) fn new() -> Self {
Self {
entries: BTreeMap::new(),
}
}

pub(super) fn get_or_insert_with(
&mut self,
id: NonZeroU32,
create: impl FnOnce() -> T,
) -> Arc<T> {
if let Some(existing) = self.entries.get(&id).and_then(Weak::upgrade) {
return existing;
}
// Only scan on a miss. Cache hits (including the root inode) stay cheap.
// A traversal with no retained handles cannot accumulate dead entries.
self.entries.retain(|_, inode| inode.strong_count() != 0);
let inode = Arc::new(create());
self.entries.insert(id, Arc::downgrade(&inode));
inode
}

pub(super) fn get(&self, id: NonZeroU32) -> Option<Arc<T>> {
self.entries.get(&id).and_then(Weak::upgrade)
}

pub(super) fn remove(&mut self, id: NonZeroU32) {
self.entries.remove(&id);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn shares_live_inodes_and_reaps_closed_inodes() {
let mut cache = InodeCache::new();
let root_id = NonZeroU32::new(2).unwrap();
let root = cache.get_or_insert_with(root_id, || 42);
assert!(Arc::ptr_eq(&root, &cache.get_or_insert_with(root_id, || 0)));
for id in 3..4096 {
let inode = cache.get_or_insert_with(NonZeroU32::new(id).unwrap(), || id);
assert!(cache.entries.len() <= 2);
drop(inode);
}
assert_eq!(*root, 42);
}

#[test]
fn inode_number_reuse_does_not_share_unlinked_state() {
let mut cache = InodeCache::new();
let id = NonZeroU32::new(10).unwrap();
let old = cache.get_or_insert_with(id, || 1);
cache.remove(id);
let new = cache.get_or_insert_with(id, || 2);
assert!(!Arc::ptr_eq(&old, &new));
assert_eq!((*old, *new), (1, 2));
}
}
283 changes: 283 additions & 0 deletions libkernel/src/fs/filesystems/ext4/disk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
//! The small set of on-disk edits not exposed by ext4plus's public API.
//!
//! Allocation, inode freeing, block mapping and directory indexing remain in
//! ext4plus. All offsets below describe the ext4 disk format, never Rust object
//! layout. See https://docs.kernel.org/filesystems/ext4/ondisk/index.html.

use super::*;
use ext4plus::prelude::read_at;

pub(super) struct DiskLayout {
Comment thread
flowerishere marked this conversation as resolved.
pub block_size: u64,
inode_size: usize,
inodes_per_group: u32,
inodes_count: u32,
descriptor_size: usize,
descriptor_start: u64,
blocks_count: u64,
checksum_seed: Option<u32>,
}

pub(super) struct SavedInode {
offset: u64,
bytes: Vec<u8>,
}

impl SavedInode {
pub async fn restore<CPU: CpuOps>(&self, dev: &Ext4Device<CPU>) -> Result<()> {
dev.write_at(self.offset, &self.bytes).await
}
}

fn u16_at(bytes: &[u8], offset: usize) -> u16 {
u16::from_le_bytes(bytes[offset..offset + 2].try_into().unwrap())
}

fn u32_at(bytes: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap())
}

// ext4 uses the reflected Castagnoli CRC without the final complement.
fn crc32c(mut crc: u32, bytes: &[u8]) -> u32 {
for &byte in bytes {
crc ^= u32::from(byte);
for _ in 0..8 {
crc = (crc >> 1) ^ (0x82f6_3b78 & 0u32.wrapping_sub(crc & 1));
}
}
crc
}

impl DiskLayout {
pub async fn load<CPU: CpuOps>(dev: &Ext4Device<CPU>) -> Result<Self> {
let mut sb = [0; 1024];
dev.read_at(1024, &mut sb).await?;
let log_block_size = u32_at(&sb, 0x18);
let incompatible = u32_at(&sb, 0x60);
if u16_at(&sb, 0x38) != 0xef53 || log_block_size > 6 {
return Err(FsError::InvalidFs.into());
}
// META_BG relocates descriptor tables. ext4plus does not support it
// either; do not guess inode locations on such a filesystem.
if incompatible & 0x10 != 0 {
return Err(KernelError::NotSupported);
}
let block_size = 1024u64 << log_block_size;
let is_64bit = incompatible & 0x80 != 0;
let descriptor_size = if is_64bit {
usize::from(u16_at(&sb, 0xfe))
} else {
32
};
let inode_size = if u32_at(&sb, 0x4c) == 0 {
128
} else {
usize::from(u16_at(&sb, 0x58))
};
let inodes_per_group = u32_at(&sb, 0x28);
if !matches!(descriptor_size, 32 | 64)
|| (is_64bit && descriptor_size != 64)
|| inode_size < 128
|| !inode_size.is_power_of_two()
|| inode_size as u64 > block_size
|| inodes_per_group == 0
{
return Err(FsError::InvalidFs.into());
}
let blocks_count = u64::from(u32_at(&sb, 4))
| if is_64bit {
u64::from(u32_at(&sb, 0x150)) << 32
} else {
0
};
let checksum_seed = (u32_at(&sb, 0x64) & 0x400 != 0).then(|| {
if incompatible & 0x2000 != 0 {
u32_at(&sb, 0x270)
} else {
crc32c(!0, &sb[0x68..0x78])
}
});
Ok(Self {
block_size,
inode_size,
inodes_per_group,
inodes_count: u32_at(&sb, 0),
descriptor_size,
descriptor_start: (u64::from(u32_at(&sb, 0x14)) + 1) * block_size,
blocks_count,
checksum_seed,
})
}

async fn read_inode<CPU: CpuOps>(
&self,
dev: &Ext4Device<CPU>,
id: NonZeroU32,
) -> Result<(u64, Vec<u8>)> {
if id.get() > self.inodes_count {
return Err(FsError::InvalidFs.into());
}
let index = id.get() - 1;
let group = index / self.inodes_per_group;
let slot = index % self.inodes_per_group;
let mut descriptor = vec![0; self.descriptor_size];
dev.read_at(
self.descriptor_start + u64::from(group) * self.descriptor_size as u64,
&mut descriptor,
)
.await?;
let table = u64::from(u32_at(&descriptor, 8))
| if self.descriptor_size == 64 {
u64::from(u32_at(&descriptor, 0x28)) << 32
} else {
0
};
let offset = table
.checked_mul(self.block_size)
.and_then(|start| start.checked_add(u64::from(slot) * self.inode_size as u64))
.ok_or(FsError::InvalidFs)?;
let end = offset
.checked_add(self.inode_size as u64)
.ok_or(FsError::InvalidFs)?;
let disk_size = self
.blocks_count
.checked_mul(self.block_size)
.ok_or(FsError::InvalidFs)?;
if table == 0 || end > disk_size {
return Err(FsError::InvalidFs.into());
}
let mut bytes = vec![0; self.inode_size];
dev.read_at(offset, &mut bytes).await?;
Ok((offset, bytes))
}

fn inode_seed(&self, id: NonZeroU32, bytes: &[u8]) -> Option<u32> {
self.checksum_seed
.map(|seed| crc32c(crc32c(seed, &id.get().to_le_bytes()), &bytes[0x64..0x68]))
}

fn checksum_inode(&self, id: NonZeroU32, bytes: &mut [u8]) {
if let Some(seed) = self.inode_seed(id, bytes) {
let high = bytes.len() >= 132 && u16_at(bytes, 0x80) >= 4;
bytes[0x7c..0x7e].fill(0);
if high {
bytes[0x82..0x84].fill(0);
}
let crc = crc32c(seed, bytes).to_le_bytes();
bytes[0x7c..0x7e].copy_from_slice(&crc[..2]);
if high {
bytes[0x82..0x84].copy_from_slice(&crc[2..]);
}
}
}

/// Clear the inline target before passing the last unlink to the library,
/// whose generic block-map freeing otherwise treats target bytes as blocks.
/// Caller holds the namespace and inode locks and has flushed the inode.
pub async fn clear_inline_target<CPU: CpuOps>(
&self,
dev: &Ext4Device<CPU>,
id: NonZeroU32,
) -> Result<SavedInode> {
let (offset, mut bytes) = self.read_inode(dev, id).await?;
if u16_at(&bytes, 0) & 0xf000 != 0xa000
|| u16_at(&bytes, 0x1a) != 1
|| u32_at(&bytes, 0x20) & 0x80000 != 0
|| !(1..60).contains(&u32_at(&bytes, 4))
|| u32_at(&bytes, 0x6c) != 0
|| u32_at(&bytes, 0x1c) != 0
|| u16_at(&bytes, 0x74) != 0
{
return Err(FsError::InvalidFs.into());
}
let saved = SavedInode {
offset,
bytes: bytes.clone(),
};
bytes[0x28..0x64].fill(0);
self.checksum_inode(id, &mut bytes);
if let Err(error) = dev.write_at(offset, &bytes).await {
saved.restore(dev).await?;
return Err(error);
}
Ok(saved)
}

/// Change only the existing ".." record, preserving a directory's htree.
pub async fn reparent<CPU: CpuOps>(
&self,
dev: &Ext4Device<CPU>,
fs: &Ext4,
inode: &mut ExtInode,
old: NonZeroU32,
new: NonZeroU32,
) -> Result<()> {
let mut block = vec![0; self.block_size as usize];
if read_at(fs, inode, &mut block, 0).await? != block.len()
|| u32_at(&block, 0) != inode.index.get()
|| u16_at(&block, 4) != 12
|| block[6] != 1
|| block[8] != b'.'
|| u32_at(&block, 12) != old.get()
|| u16_at(&block, 16) < 12
|| usize::from(u16_at(&block, 16)) > block.len() - 12
|| block[18] != 2
|| &block[20..22] != b".."
{
return Err(FsError::InvalidFs.into());
}
let (_, bytes) = self.read_inode(dev, inode.index).await?;
let seed = self.inode_seed(inode.index, &bytes);
let checksum = |block: &[u8]| -> Result<Option<u32>> {
let Some(seed) = seed else { return Ok(None) };
let len = block.len();
if inode.flags().contains(InodeFlags::DIRECTORY_HTREE) {
let count = usize::from(u16_at(block, 0x22));
let limit = usize::from(u16_at(block, 0x20));
if block[0x1d] != 8 || count == 0 || count > limit || 32 + limit * 8 != len - 8 {
return Err(FsError::InvalidFs.into());
}
Ok(Some(crc32c(
crc32c(
crc32c(seed, &block[..32 + count * 8]),
&block[len - 8..len - 4],
),
&[0; 4],
)))
} else {
if block[len - 12..len - 4] != [0, 0, 0, 0, 12, 0, 0, 0xde] {
return Err(FsError::InvalidFs.into());
}
Ok(Some(crc32c(seed, &block[..len - 12])))
}
};
let end = block.len() - 4;
if let Some(expected) = checksum(&block)?
&& u32_at(&block, end) != expected
{
return Err(FsError::InvalidFs.into());
}
block[12..16].copy_from_slice(&new.get().to_le_bytes());
if let Some(crc) = checksum(&block)? {
block[end..].copy_from_slice(&crc.to_le_bytes());
}
if write_at(fs, inode, &block, 0).await? != block.len() {
return Err(KernelError::Other("short directory write"));
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn crc32c_ext4_convention_and_incremental_seed() {
assert_eq!(crc32c(!0, b"123456789"), !0xe3069283);
assert_eq!(
crc32c(crc32c(!0, b"1234"), b"56789"),
crc32c(!0, b"123456789")
);
}
}
Loading