Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: Implement journal support #358

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## Unreleased

* Removed `Ext4Error::as_corrupt`.
* Renamed `Incompatible::Missing` to `Incompatible::MissingRequiredFeatures`.
* Renamed `Incompatible::Incompatible` to `Incompatible::UnsupportedFeatures`.
* Removed `Incompatible::Unknown`; these errors are now reported as
`Incompatible::UnsupportedFeatures`.

## 0.8.0

Expand All @@ -11,6 +15,7 @@
* Added `Ext4::uuid` to get the filesystem UUID.
* Made the `Corrupt` type opaque. It is no longer possible to `match` on
specific types of corruption.
* Added support for reading filesystems that weren't cleanly unmounted.

## 0.7.0

Expand Down
5 changes: 5 additions & 0 deletions src/checksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ impl Checksum {
self.digest.update(data);
}

/// Extend the digest with a big-endian `u32`.
pub(crate) fn update_u32_be(&mut self, data: u32) {
self.update(&data.to_be_bytes());
}

/// Extend the digest with a little-endian `u16`.
pub(crate) fn update_u16_le(&mut self, data: u16) {
self.update(&data.to_le_bytes());
Expand Down
89 changes: 72 additions & 17 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,30 @@ pub(crate) enum CorruptKind {
/// Journal superblock checksum is invalid.
JournalSuperblockChecksum,

/// Journal block size does not match the filesystem block size.
JournalBlockSize,

/// Journal does not have the expected number of blocks.
JournalTruncated,

/// Journal first commit doesn't match the sequence number in the superblock.
JournalSequence,

/// Journal commit block checksum is invalid.
JournalCommitBlockChecksum,

/// Journal descriptor block checksum is invalid.
JournalDescriptorBlockChecksum,

/// Journal descriptor tag checksum is invalid.
JournalDescriptorTagChecksum,

/// Journal has a descriptor block that contains no tag with the last-tag flag set.
JournalDescriptorBlockMissingLastTag,

/// Journal sequence number overflowed.
JournalSequenceOverflow,

/// An inode's checksum is invalid.
InodeChecksum(InodeIndex),

Expand Down Expand Up @@ -315,6 +339,31 @@ impl Display for CorruptKind {
Self::JournalSuperblockChecksum => {
write!(f, "journal superblock checksum is invalid")
}
Self::JournalBlockSize => {
write!(
f,
"journal block size does not match filesystem block size"
)
}
Self::JournalTruncated => write!(f, "journal is truncated"),
Self::JournalSequence => write!(
f,
"journal's first commit doesn't match the expected sequence"
),
Self::JournalCommitBlockChecksum => {
write!(f, "journal commit block checksum is invalid")
}
Self::JournalDescriptorBlockChecksum => {
write!(f, "journal descriptor block checksum is invalid")
}
Self::JournalDescriptorTagChecksum => {
write!(f, "journal descriptor tag checksum is invalid")
}
Self::JournalDescriptorBlockMissingLastTag => write!(
f,
"a journal descriptor block has no tag with the last-tag flag set"
),
Self::JournalSequenceOverflow => write!(f, "journal sequence number overflowed"),
Self::InodeChecksum(inode) => {
write!(f, "invalid checksum for inode {inode}")
}
Expand Down Expand Up @@ -411,22 +460,16 @@ impl From<Incompatible> for Ext4Error {
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Incompatible {
/// One or more unknown bits are set in the incompatible feature flags.
Unknown(
/// The unknown features.
IncompatibleFeatures,
),

/// One or more required incompatible features are missing.
Missing(
/// One or more required features are missing.
MissingRequiredFeatures(
/// The missing features.
IncompatibleFeatures,
),

/// One or more disallowed incompatible features are present.
/// One or more unsupported features are present.
#[allow(clippy::enum_variant_names)]
Incompatible(
/// The incompatible features.
UnsupportedFeatures(
/// The unsupported features.
IncompatibleFeatures,
),

Expand Down Expand Up @@ -467,19 +510,25 @@ pub enum Incompatible {
/// The unsupported feature bits.
u32,
),

/// The journal contains an unsupported block type.
JournalBlockType(
/// Raw journal block type.
u32,
),

/// The journal contains an escaped block.
JournalBlockEscaped,
}

impl Display for Incompatible {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Unknown(feat) => {
write!(f, "unknown features: {feat:?}")
}
Self::Missing(feat) => {
Self::MissingRequiredFeatures(feat) => {
write!(f, "missing required features: {feat:?}")
}
Self::Incompatible(feat) => {
write!(f, "incompatible features: {feat:?}")
Self::UnsupportedFeatures(feat) => {
write!(f, "unsupported features: {feat:?}")
}
Self::DirectoryHash(algorithm) => {
write!(f, "unsupported directory hash algorithm: {algorithm}")
Expand All @@ -490,6 +539,12 @@ impl Display for Incompatible {
Self::JournalSuperblockType(val) => {
write!(f, "journal superblock type is not supported: {val}")
}
Self::JournalBlockType(val) => {
write!(f, "journal block type is not supported: {val}")
}
Self::JournalBlockEscaped => {
write!(f, "journal contains an escaped data block")
}
Self::JournalChecksumType(val) => {
write!(f, "journal checksum type is not supported: {val}")
}
Expand Down
186 changes: 176 additions & 10 deletions src/journal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,200 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

#[expect(unused)] // TODO
mod block_header;
#[expect(unused)] // TODO
mod descriptor_block;
mod superblock;

use crate::{Ext4, Ext4Error};
use crate::checksum::Checksum;
use crate::error::{CorruptKind, Ext4Error, Incompatible};
use crate::inode::Inode;
use crate::iters::file_blocks::FileBlocks;
use crate::util::{read_u32be, usize_from_u32};
use crate::Ext4;
use alloc::collections::BTreeMap;
use alloc::vec;
use block_header::{JournalBlockHeader, JournalBlockType};
use descriptor_block::{
validate_descriptor_block_checksum, DescriptorBlockTagIter,
};
use superblock::JournalSuperblock;

#[derive(Debug)]
pub(crate) struct Journal {
// TODO: add journal data.
block_map: BTreeMap<u64, u64>,
}

impl Journal {
/// Create an empty journal.
pub(crate) fn empty() -> Self {
Self {}
Self {
block_map: BTreeMap::new(),
}
}

/// Load a journal from the filesystem.
/// Load the journal.
///
/// If the filesystem has no journal, an empty journal is returned.
///
/// Note: ext4 is all little-endian, except for the journal, which
/// is all big-endian.
pub(crate) fn load(fs: &Ext4) -> Result<Self, Ext4Error> {
let Some(_journal_inode) = fs.0.superblock.journal_inode else {
let Some(journal_inode) = fs.0.superblock.journal_inode else {
// Return an empty journal if this filesystem does not have
// a journal.
return Ok(Self::empty());
};

// TODO: actually load the journal.
let journal_inode = Inode::read(fs, journal_inode)?;
let superblock = JournalSuperblock::load(fs, &journal_inode)?;

Ok(Self {})
// Ensure the journal block size matches the rest of the
// filesystem.
let block_size = fs.0.superblock.block_size;
if superblock.block_size != block_size {
return Err(CorruptKind::JournalBlockSize.into());
}

let block_map = load_block_map(fs, &superblock, &journal_inode)?;

Ok(Self { block_map })
}

/// Map from an absolute block index to a block in the journal.
///
/// If the journal does not contain a replacement for the input
/// block, the input block is returned.
pub(crate) fn map_block_index(&self, block_index: u64) -> u64 {
*self.block_map.get(&block_index).unwrap_or(&block_index)
}
}

fn load_block_map(
fs: &Ext4,
superblock: &JournalSuperblock,
journal_inode: &Inode,
) -> Result<BTreeMap<u64, u64>, Ext4Error> {
// Get an iterator over the journal's block indices.
let journal_block_iter = FileBlocks::new(fs.clone(), journal_inode)?;

// Skip forward to the start block.
let mut journal_block_iter =
journal_block_iter.skip(usize_from_u32(superblock.start_block));

// TODO: the loop below currently returns an error if something
// bad is encountered (e.g. a wrong checksum). We should
// actually still apply valid commits, and just stop reading the
// journal when bad data is encountered.

let mut block = vec![0; fs.0.superblock.block_size.to_usize()];
let mut data_block = vec![0; fs.0.superblock.block_size.to_usize()];
let mut block_map = BTreeMap::new();
let mut uncommitted_block_map = BTreeMap::new();
let mut sequence = superblock.sequence;
while let Some(block_index) = journal_block_iter.next() {
let block_index = block_index?;

fs.read_from_block(block_index, 0, &mut block)?;

let Some(header) = JournalBlockHeader::read_bytes(&block)? else {
// Journal block magic is not present, so we've reached
// the end of the journal.
break;
};

if header.sequence != sequence {
return Err(CorruptKind::JournalSequence.into());
}

if header.block_type == JournalBlockType::DESCRIPTOR {
validate_descriptor_block_checksum(superblock, &block)?;

let tags = DescriptorBlockTagIter::new(&block[12..]);

for tag in tags {
let tag = tag?;

let block_index = journal_block_iter
.next()
.ok_or(CorruptKind::JournalTruncated)??;

// Check the data block checksum.
let mut checksum = Checksum::new();
checksum.update(superblock.uuid.as_bytes());
checksum.update_u32_be(sequence);
fs.read_from_block(block_index, 0, &mut data_block)?;
checksum.update(&data_block);
if checksum.finalize() != tag.checksum {
return Err(
CorruptKind::JournalDescriptorTagChecksum.into()
);
}

uncommitted_block_map.insert(tag.block_index, block_index);
}
} else if header.block_type == JournalBlockType::COMMIT {
validate_commit_block_checksum(superblock, &block)?;

// Move the entries from `uncommitted_block_map` to `block_map`.
block_map.extend(uncommitted_block_map.iter());
uncommitted_block_map.clear();

sequence = sequence
.checked_add(1)
.ok_or(CorruptKind::JournalSequenceOverflow)?;
} else {
return Err(
Incompatible::JournalBlockType(header.block_type.0).into()
);
}
}

Ok(block_map)
}

fn validate_commit_block_checksum(
superblock: &JournalSuperblock,
block: &[u8],
) -> Result<(), Ext4Error> {
// The kernel documentation says that fields 0xc and 0xd contain the
// checksum type and size, but this is not correct. If the
// superblock features include `CHECKSUM_V3`, the type/size fields
// are both zero.

const CHECKSUM_OFFSET: usize = 16;
const CHECKSUM_SIZE: usize = 4;

let expected_checksum = read_u32be(block, CHECKSUM_OFFSET);

let mut checksum = Checksum::new();
checksum.update(superblock.uuid.as_bytes());
checksum.update(&block[..CHECKSUM_OFFSET]);
checksum.update(&[0; CHECKSUM_SIZE]);
checksum.update(&block[CHECKSUM_OFFSET + CHECKSUM_SIZE..]);

if checksum.finalize() == expected_checksum {
Ok(())
} else {
Err(CorruptKind::JournalCommitBlockChecksum.into())
}
}

#[cfg(all(test, feature = "std"))]
mod tests {
use crate::test_util::load_compressed_filesystem;
use alloc::rc::Rc;

#[test]
fn test_journal() {
let mut fs =
load_compressed_filesystem("test_disk_4k_block_journal.bin.zst");

let test_dir = "/dir500";

// With the journal in place, this directory exists.
assert!(fs.exists(test_dir).unwrap());

// Clear the journal, and verify that the directory no longer exists.
Rc::get_mut(&mut fs.0).unwrap().journal.block_map.clear();
assert!(!fs.exists(test_dir).unwrap());
}
}
Loading