Skip to content

Commit 79e1dee

Browse files
More journal work
1 parent ec68551 commit 79e1dee

File tree

8 files changed

+359
-29
lines changed

8 files changed

+359
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Added `Ext4::uuid` to get the filesystem UUID.
1010
* Made the `Corrupt` type opaque. It is no longer possible to `match` on
1111
specific types of corruption.
12+
* Added support for reading filesystems that weren't cleanly unmounted.
1213

1314
## 0.7.0
1415

src/checksum.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ impl Checksum {
5454
self.digest.update(data);
5555
}
5656

57+
/// Extend the digest with a big-endian `u32`.
58+
pub(crate) fn update_u32_be(&mut self, data: u32) {
59+
self.update(&data.to_be_bytes());
60+
}
61+
5762
/// Extend the digest with a little-endian `u16`.
5863
pub(crate) fn update_u16_le(&mut self, data: u16) {
5964
self.update(&data.to_le_bytes());

src/error.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,24 @@ pub(crate) enum CorruptKind {
235235
/// Journal superblock checksum is invalid.
236236
JournalSuperblockChecksum,
237237

238+
/// Journal block size does not match the filesystem block size.
239+
JournalBlockSize,
240+
241+
/// Journal does not have the expected number of blocks.
242+
JournalTruncated,
243+
244+
/// Journal first commit doesn't match the sequence number in the superblock.
245+
JournalSequence,
246+
247+
/// Journal commit block checksum is invalid.
248+
JournalCommitBlockChecksum,
249+
250+
/// Journal descriptor block checksum is invalid.
251+
JournalDescriptorBlockChecksum,
252+
253+
/// Journal has a descriptor block that contains no tag with the last-tag flag set.
254+
JournalDescriptorBlockMissingLastTag,
255+
238256
/// An inode's checksum is invalid.
239257
InodeChecksum(
240258
/// Inode number.
@@ -351,6 +369,27 @@ impl Display for CorruptKind {
351369
Self::JournalSuperblockChecksum => {
352370
write!(f, "journal superblock checksum is invalid")
353371
}
372+
Self::JournalBlockSize => {
373+
write!(
374+
f,
375+
"journal block size does not match filesystem block size"
376+
)
377+
}
378+
Self::JournalTruncated => write!(f, "journal is truncated"),
379+
Self::JournalSequence => write!(
380+
f,
381+
"journal's first commit doesn't match the expected sequence"
382+
),
383+
Self::JournalCommitBlockChecksum => {
384+
write!(f, "journal commit block checksum is invalid")
385+
}
386+
Self::JournalDescriptorBlockChecksum => {
387+
write!(f, "journal descriptor block checksum is invalid")
388+
}
389+
Self::JournalDescriptorBlockMissingLastTag => write!(
390+
f,
391+
"a journal descriptor block has no tag with the last-tag flag set"
392+
),
354393
Self::InodeChecksum(inode) => {
355394
write!(f, "invalid checksum for inode {inode}")
356395
}

src/journal.rs

Lines changed: 165 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,189 @@
66
// option. This file may not be copied, modified, or distributed
77
// except according to those terms.
88

9-
#[expect(unused)] // TODO
109
mod block_header;
11-
#[expect(unused)] // TODO
10+
mod descriptor_block;
1211
mod superblock;
1312

14-
use crate::{Ext4, Ext4Error};
13+
use crate::checksum::Checksum;
14+
use crate::inode::Inode;
15+
use crate::iters::file_blocks::FileBlocks;
16+
use crate::util::{read_u32be, usize_from_u32};
17+
use crate::{CorruptKind, Ext4, Ext4Error};
18+
use alloc::collections::BTreeMap;
19+
use alloc::vec;
20+
use block_header::{JournalBlockHeader, JournalBlockType};
21+
use descriptor_block::{
22+
is_descriptor_block_checksum_valid, JournalDescriptorBlockTag,
23+
};
24+
use superblock::JournalSuperblock;
1525

1626
#[derive(Debug)]
1727
pub(crate) struct Journal {
18-
// TODO: add journal data.
28+
block_map: BTreeMap<u64, u64>,
1929
}
2030

2131
impl Journal {
22-
/// Create an empty journal.
2332
pub(crate) fn empty() -> Self {
24-
Self {}
33+
Self {
34+
block_map: BTreeMap::new(),
35+
}
2536
}
2637

27-
/// Load a journal from the filesystem.
38+
/// Load the journal.
39+
///
40+
/// If the filesystem has no journal, an empty journal is returned.
41+
///
42+
/// Note: ext4 is all little-endian, except for the journal, which
43+
/// is all big-endian.
2844
pub(crate) fn load(fs: &Ext4) -> Result<Self, Ext4Error> {
29-
let Some(_journal_inode) = fs.0.superblock.journal_inode else {
45+
let Some(journal_inode) = fs.0.superblock.journal_inode else {
3046
// Return an empty journal if this filesystem does not have
3147
// a journal.
3248
return Ok(Self::empty());
3349
};
3450

35-
// TODO: actually load the journal.
51+
let journal_inode = Inode::read(fs, journal_inode)?;
52+
let superblock = JournalSuperblock::load(fs, &journal_inode)?;
3653

37-
Ok(Self {})
54+
// Ensure the journal block size matches the rest of the
55+
// filesystem.
56+
let block_size = fs.0.superblock.block_size;
57+
if superblock.block_size != block_size {
58+
return Err(CorruptKind::JournalBlockSize.into());
59+
}
60+
61+
// Get an iterator over the journal's block indices.
62+
let journal_block_iter = FileBlocks::new(fs.clone(), &journal_inode)?;
63+
64+
// Skip forward to the start block.
65+
let mut journal_block_iter =
66+
journal_block_iter.skip(usize_from_u32(superblock.start_block));
67+
68+
// TODO: the loop below currently returns an error if something
69+
// bad is encountered (e.g. a wrong checksum). We should
70+
// actually still apply valid commits, and just stop reading the
71+
// journal when bad data is encountered.
72+
73+
let mut block = vec![0; block_size.to_usize()];
74+
let mut block_map = BTreeMap::new();
75+
let mut uncommitted_block_map = BTreeMap::new();
76+
let mut sequence = superblock.sequence;
77+
while let Some(block_index) = journal_block_iter.next() {
78+
let block_index = block_index?;
79+
80+
fs.read_from_block(block_index, 0, &mut block)?;
81+
82+
let Some(header) = JournalBlockHeader::read_bytes(&block)? else {
83+
// Journal block magic is not present, so we've reached
84+
// the end of the journal.
85+
break;
86+
};
87+
88+
if header.sequence != sequence {
89+
return Err(CorruptKind::JournalSequence.into());
90+
}
91+
92+
if header.block_type == JournalBlockType::DESCRIPTOR {
93+
if !is_descriptor_block_checksum_valid(&superblock, &block) {
94+
return Err(
95+
CorruptKind::JournalDescriptorBlockChecksum.into()
96+
);
97+
}
98+
99+
let tags =
100+
JournalDescriptorBlockTag::read_bytes_to_vec(&block[12..])
101+
.unwrap();
102+
103+
for tag in &tags {
104+
let block_index = journal_block_iter
105+
.next()
106+
.ok_or(CorruptKind::JournalTruncated)??;
107+
108+
// TODO: is it a good idea to do this here, vs when
109+
// the data is actually needed?
110+
// TODO: either way, we definitely shouldn't fail if
111+
// not committed yet, right?
112+
let mut checksum = Checksum::new();
113+
checksum.update(superblock.uuid.as_bytes());
114+
checksum.update_u32_be(sequence);
115+
fs.read_from_block(block_index, 0, &mut block)?;
116+
checksum.update(&block);
117+
if checksum.finalize() != tag.checksum {
118+
// TODO
119+
panic!();
120+
}
121+
122+
uncommitted_block_map.insert(tag.block_number, block_index);
123+
}
124+
} else if header.block_type == JournalBlockType::COMMIT {
125+
if !is_commit_block_checksum_valid(&superblock, &block) {
126+
return Err(CorruptKind::JournalCommitBlockChecksum.into());
127+
}
128+
129+
// Move the entries from `uncommitted_block_map` to `block_map`.
130+
block_map.extend(uncommitted_block_map.iter());
131+
uncommitted_block_map.clear();
132+
133+
// TODO: unwrap
134+
sequence = sequence.checked_add(1).unwrap();
135+
} else {
136+
todo!()
137+
}
138+
}
139+
140+
Ok(Self { block_map })
141+
}
142+
143+
/// Map from an absolute block index to a block in the journal.
144+
///
145+
/// If the journal does not contain a replacement for the input
146+
/// block, the input block is returned.
147+
pub(crate) fn map_block_index(&self, block_index: u64) -> u64 {
148+
*self.block_map.get(&block_index).unwrap_or(&block_index)
149+
}
150+
}
151+
152+
fn is_commit_block_checksum_valid(
153+
superblock: &JournalSuperblock,
154+
block: &[u8],
155+
) -> bool {
156+
// The kernel documentation says that fields 0xc and 0xd contain the
157+
// checksum type and size, but this is not correct. If the
158+
// superblock features include `CHECKSUM_V3`, the type/size fields
159+
// are both zero.
160+
161+
const CHECKSUM_OFFSET: usize = 16;
162+
const CHECKSUM_SIZE: usize = 4;
163+
164+
let expected_checksum = read_u32be(block, CHECKSUM_OFFSET);
165+
166+
let mut checksum = Checksum::new();
167+
checksum.update(superblock.uuid.as_bytes());
168+
checksum.update(&block[..CHECKSUM_OFFSET]);
169+
checksum.update(&[0; CHECKSUM_SIZE]);
170+
checksum.update(&block[CHECKSUM_OFFSET + CHECKSUM_SIZE..]);
171+
172+
checksum.finalize() == expected_checksum
173+
}
174+
175+
#[cfg(all(test, feature = "std"))]
176+
mod tests {
177+
use crate::test_util::load_compressed_filesystem;
178+
use alloc::rc::Rc;
179+
180+
#[test]
181+
fn test_journal() {
182+
let mut fs =
183+
load_compressed_filesystem("test_disk_4k_block_journal.bin.zst");
184+
185+
let test_dir = "/dir500";
186+
187+
// With the journal in place, this directory exists.
188+
assert!(fs.exists(test_dir).unwrap());
189+
190+
// Clear the journal, and verify that the directory no longer exists.
191+
Rc::get_mut(&mut fs.0).unwrap().journal.block_map.clear();
192+
assert!(!fs.exists(test_dir).unwrap());
38193
}
39194
}

src/journal/descriptor_block.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5+
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6+
// option. This file may not be copied, modified, or distributed
7+
// except according to those terms.
8+
9+
use crate::checksum::Checksum;
10+
use crate::error::{CorruptKind, Ext4Error};
11+
use crate::journal::superblock::JournalSuperblock;
12+
use crate::util::{read_u32be, u64_from_hilo};
13+
use alloc::vec::Vec;
14+
use bitflags::bitflags;
15+
16+
pub(super) fn is_descriptor_block_checksum_valid(
17+
superblock: &JournalSuperblock,
18+
block: &[u8],
19+
) -> bool {
20+
// OK to unwrap: minimum block length is 1024.
21+
let checksum_offset = block.len().checked_sub(4).unwrap();
22+
let expected_checksum = read_u32be(block, checksum_offset);
23+
let mut checksum = Checksum::new();
24+
checksum.update(superblock.uuid.as_bytes());
25+
checksum.update(&block[..checksum_offset]);
26+
checksum.update_u32_be(0);
27+
28+
checksum.finalize() == expected_checksum
29+
}
30+
31+
// TODO: the kernel docs for this are a mess
32+
#[derive(Debug)]
33+
pub(super) struct JournalDescriptorBlockTag {
34+
pub(super) block_number: u64,
35+
pub(super) flags: JournalDescriptorBlockTagFlags,
36+
pub(super) checksum: u32,
37+
#[expect(dead_code)] // TODO
38+
uuid: [u8; 16],
39+
}
40+
41+
impl JournalDescriptorBlockTag {
42+
pub(super) fn read_bytes(bytes: &[u8]) -> (Self, usize) {
43+
// TODO: for now assuming the `incompat_features` assert above.
44+
45+
let t_blocknr = read_u32be(bytes, 0);
46+
let t_flags = read_u32be(bytes, 4);
47+
let t_blocknr_high = read_u32be(bytes, 8);
48+
let t_checksum = read_u32be(bytes, 12);
49+
50+
let flags = JournalDescriptorBlockTagFlags::from_bits_retain(t_flags);
51+
let mut size: usize = 16;
52+
53+
let mut uuid = [0; 16];
54+
if !flags.contains(JournalDescriptorBlockTagFlags::UUID_OMITTED) {
55+
// OK to unwrap: length is 16.
56+
uuid = bytes[16..32].try_into().unwrap();
57+
// TODO: unwrap
58+
size = size.checked_add(16).unwrap();
59+
}
60+
61+
(
62+
Self {
63+
block_number: u64_from_hilo(t_blocknr_high, t_blocknr),
64+
flags,
65+
checksum: t_checksum,
66+
uuid,
67+
},
68+
size,
69+
)
70+
}
71+
72+
// TODO: this could be an iterator instead of allocating.
73+
pub(super) fn read_bytes_to_vec(
74+
mut bytes: &[u8],
75+
) -> Result<Vec<Self>, Ext4Error> {
76+
let mut v = Vec::new();
77+
78+
while !bytes.is_empty() {
79+
let (tag, size) = Self::read_bytes(bytes);
80+
let is_end =
81+
tag.flags.contains(JournalDescriptorBlockTagFlags::LAST_TAG);
82+
v.push(tag);
83+
84+
if is_end {
85+
return Ok(v);
86+
}
87+
88+
bytes = &bytes[size..];
89+
}
90+
91+
Err(CorruptKind::JournalDescriptorBlockMissingLastTag.into())
92+
}
93+
}
94+
95+
bitflags! {
96+
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
97+
pub struct JournalDescriptorBlockTagFlags: u32 {
98+
const ESCAPED = 0x1;
99+
const UUID_OMITTED = 0x2;
100+
const DELETED = 0x4;
101+
const LAST_TAG = 0x8;
102+
}
103+
}

0 commit comments

Comments
 (0)