Skip to content

Commit 9e8574a

Browse files
More journal work
1 parent c768a92 commit 9e8574a

File tree

8 files changed

+398
-29
lines changed

8 files changed

+398
-29
lines changed

CHANGELOG.md

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

1516
## 0.7.0
1617

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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,27 @@ pub(crate) enum CorruptKind {
232232
/// Journal superblock checksum is invalid.
233233
JournalSuperblockChecksum,
234234

235+
/// Journal block size does not match the filesystem block size.
236+
JournalBlockSize,
237+
238+
/// Journal does not have the expected number of blocks.
239+
JournalTruncated,
240+
241+
/// Journal first commit doesn't match the sequence number in the superblock.
242+
JournalSequence,
243+
244+
/// Journal commit block checksum is invalid.
245+
JournalCommitBlockChecksum,
246+
247+
/// Journal descriptor block checksum is invalid.
248+
JournalDescriptorBlockChecksum,
249+
250+
/// Journal descriptor tag checksum is invalid.
251+
JournalDescriptorTagChecksum,
252+
253+
/// Journal has a descriptor block that contains no tag with the last-tag flag set.
254+
JournalDescriptorBlockMissingLastTag,
255+
235256
/// An inode's checksum is invalid.
236257
InodeChecksum(InodeIndex),
237258

@@ -315,6 +336,30 @@ impl Display for CorruptKind {
315336
Self::JournalSuperblockChecksum => {
316337
write!(f, "journal superblock checksum is invalid")
317338
}
339+
Self::JournalBlockSize => {
340+
write!(
341+
f,
342+
"journal block size does not match filesystem block size"
343+
)
344+
}
345+
Self::JournalTruncated => write!(f, "journal is truncated"),
346+
Self::JournalSequence => write!(
347+
f,
348+
"journal's first commit doesn't match the expected sequence"
349+
),
350+
Self::JournalCommitBlockChecksum => {
351+
write!(f, "journal commit block checksum is invalid")
352+
}
353+
Self::JournalDescriptorBlockChecksum => {
354+
write!(f, "journal descriptor block checksum is invalid")
355+
}
356+
Self::JournalDescriptorTagChecksum => {
357+
write!(f, "journal descriptor tag checksum is invalid")
358+
}
359+
Self::JournalDescriptorBlockMissingLastTag => write!(
360+
f,
361+
"a journal descriptor block has no tag with the last-tag flag set"
362+
),
318363
Self::InodeChecksum(inode) => {
319364
write!(f, "invalid checksum for inode {inode}")
320365
}
@@ -467,6 +512,12 @@ pub enum Incompatible {
467512
/// The unsupported feature bits.
468513
u32,
469514
),
515+
516+
/// The journal contains an unsupported block type.
517+
JournalBlockType(
518+
/// Raw journal block type.
519+
u32,
520+
),
470521
}
471522

472523
impl Display for Incompatible {
@@ -490,6 +541,9 @@ impl Display for Incompatible {
490541
Self::JournalSuperblockType(val) => {
491542
write!(f, "journal superblock type is not supported: {val}")
492543
}
544+
Self::JournalBlockType(val) => {
545+
write!(f, "journal block type is not supported: {val}")
546+
}
493547
Self::JournalChecksumType(val) => {
494548
write!(f, "journal checksum type is not supported: {val}")
495549
}

src/journal.rs

Lines changed: 173 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,197 @@
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::error::{CorruptKind, Ext4Error, Incompatible};
15+
use crate::inode::Inode;
16+
use crate::iters::file_blocks::FileBlocks;
17+
use crate::util::{read_u32be, usize_from_u32};
18+
use crate::Ext4;
19+
use alloc::collections::BTreeMap;
20+
use alloc::vec;
21+
use block_header::{JournalBlockHeader, JournalBlockType};
22+
use descriptor_block::{
23+
is_descriptor_block_checksum_valid, JournalDescriptorBlockTag,
24+
};
25+
use superblock::JournalSuperblock;
1526

1627
#[derive(Debug)]
1728
pub(crate) struct Journal {
18-
// TODO: add journal data.
29+
block_map: BTreeMap<u64, u64>,
1930
}
2031

2132
impl Journal {
22-
/// Create an empty journal.
2333
pub(crate) fn empty() -> Self {
24-
Self {}
34+
Self {
35+
block_map: BTreeMap::new(),
36+
}
2537
}
2638

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

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

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

0 commit comments

Comments
 (0)