Skip to content

Commit

Permalink
More journal work
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasbishop committed Jan 29, 2025
1 parent 89c8a8b commit f3386ef
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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
54 changes: 54 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,27 @@ 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,

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

Expand Down Expand Up @@ -319,6 +340,30 @@ 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::InodeChecksum(inode) => {
write!(f, "invalid checksum for inode {inode}")
}
Expand Down Expand Up @@ -464,6 +509,12 @@ pub enum Incompatible {
/// Raw feature bits.
u32,
),

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

impl Display for Incompatible {
Expand All @@ -487,6 +538,9 @@ 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::JournalChecksumType(val) => {
write!(f, "journal checksum type is not supported: {val}")
}
Expand Down
183 changes: 173 additions & 10 deletions src/journal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,197 @@
// 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::{
is_descriptor_block_checksum_valid, JournalDescriptorBlockTag,
};
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 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 {
if !is_descriptor_block_checksum_valid(superblock, &block) {
return Err(CorruptKind::JournalDescriptorBlockChecksum.into());
}

let tags =
JournalDescriptorBlockTag::read_bytes_to_vec(&block[12..])
.unwrap();

for tag in &tags {
let block_index = journal_block_iter
.next()
.ok_or(CorruptKind::JournalTruncated)??;

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

uncommitted_block_map.insert(tag.block_number, block_index);
}
} else if header.block_type == JournalBlockType::COMMIT {
if !is_commit_block_checksum_valid(superblock, &block) {
return Err(CorruptKind::JournalCommitBlockChecksum.into());
}

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

// TODO: unwrap
sequence = sequence.checked_add(1).unwrap();
} else {
return Err(
Incompatible::JournalBlockType(header.block_type.0).into()
);
}
}

Ok(block_map)
}

fn is_commit_block_checksum_valid(
superblock: &JournalSuperblock,
block: &[u8],
) -> bool {
// 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..]);

checksum.finalize() == expected_checksum
}

#[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

0 comments on commit f3386ef

Please sign in to comment.