fs_ext4/journal_apply.rs
1//! JBD2 journal replay applier.
2//!
3//! Turns a [`journal::ReplayPlan`] (plan-layer output from the walker) into
4//! actual `write_at` calls on a writable [`BlockDevice`]. The corresponding
5//! read-side walker is [`crate::journal::walk`]; the write-side producer is
6//! [`crate::transaction::Transaction`]. Closing the loop:
7//!
8//! Transaction::commit ──(journal bytes)──▶ disk
9//! │
10//! journal::walk ◀──────┘
11//! │
12//! ▼
13//! journal_apply::apply ──▶ final-location disk writes
14//!
15//! Mount flow (when journal is dirty), in [`replay_if_dirty`]:
16//!
17//! ```text
18//! 1. A read-only device: return, replaying nothing here. Mount replays
19//! into the buffer cache instead ([`replay_into_cache`], #72).
20//! 2. Parse the JBD2 superblock via jbd2::read_superblock; clean: clear a
21//! leftover needs_recovery and return.
22//! 3. Refuse a volume with a write-breaking INCOMPAT bit.
23//! 4. plan = journal::walk(&fs, &jsb)?; journal_apply::apply(&fs, &plan)?
24//! 5. Mark the journal clean, then clear needs_recovery.
25//! 6. The mount continues, read-write.
26//! ```
27//!
28//! Once the writes are flushed, the journal superblock gets `s_start = 0`
29//! and the sequence one past the first uncommitted transaction, and the ext4
30//! superblock loses `needs_recovery` (#228). Left dirty, every later mount
31//! replayed the same log again and `e2fsck` reported a journal with data
32//! in it. The order matters: a crash between the two leaves a clean
33//! journal flagged for recovery, which recovers nothing; the reverse would
34//! be a live journal Linux wipes.
35
36use crate::error::{Error, Result};
37use crate::fs::Filesystem;
38use crate::inode::Inode;
39use crate::jbd2::{self, JournalSuperblock};
40use crate::journal::ReplayPlan;
41
42/// Set or clear `INCOMPAT_RECOVER` (`needs_recovery`) in a 1024-byte
43/// superblock image, redoing its checksum under `metadata_csum`.
44pub(crate) fn set_needs_recovery_in(sb: &mut [u8], on: bool) {
45 let recover = crate::features::Incompat::RECOVER.bits();
46 let incompat = u32::from_le_bytes(sb[0x60..0x64].try_into().unwrap());
47 let incompat = if on {
48 incompat | recover
49 } else {
50 incompat & !recover
51 };
52 sb[0x60..0x64].copy_from_slice(&incompat.to_le_bytes());
53 let ro_compat = u32::from_le_bytes(sb[0x64..0x68].try_into().unwrap());
54 if ro_compat & crate::features::RoCompat::METADATA_CSUM.bits() != 0 {
55 let csum = crate::checksum::linux_crc32c(!0, &sb[..0x3FC]);
56 sb[0x3FC..0x400].copy_from_slice(&csum.to_le_bytes());
57 }
58}
59
60/// Set or clear `needs_recovery` in the primary superblock on `dev`. Linux
61/// replays a journal only when this is set, and wipes one when it is not.
62pub(crate) fn write_needs_recovery(dev: &dyn crate::block_io::BlockDevice, on: bool) -> Result<()> {
63 let at = crate::superblock::SUPERBLOCK_OFFSET;
64 let mut sb = vec![0u8; 1024];
65 dev.read_at(at, &mut sb)?;
66 set_needs_recovery_in(&mut sb, on);
67 dev.write_at(at, &sb)
68}
69
70/// Record a replayed journal as done: `s_start = 0` and `s_sequence` the one
71/// past the first uncommitted transaction, checksum redone, flushed.
72fn mark_journal_clean(fs: &Filesystem, jsb: &JournalSuperblock, plan: &ReplayPlan) -> Result<()> {
73 let raw = fs.read_inode_raw(fs.sb.journal_inode)?;
74 let jinode = Inode::parse(&raw)?;
75 let phys = jbd2::journal_block_to_physical(fs, &jinode, 0)?
76 .ok_or(Error::Corrupt("journal_apply: journal superblock unmapped"))?;
77 let at = byte_offset_of(fs, phys)?;
78 let mut buf = vec![0u8; 1024];
79 fs.dev.read_at(at, &mut buf)?;
80 if u32::from_be_bytes(buf[0..4].try_into().unwrap()) != jbd2::JBD2_MAGIC_NUMBER {
81 return Err(Error::Corrupt(
82 "journal_apply: journal superblock lost its magic during replay",
83 ));
84 }
85 // Restart the log one past the first transaction the walk did not find
86 // committed, skipping that ID rather than reusing it: a torn tail may
87 // already carry it, and
88 // reusing it could let a stale block pass as part of the next transaction.
89 // `next_sequence` rather than `last_commit`, whose 0 is also a real ID.
90 let sequence = plan.next_sequence.unwrap_or(jsb.sequence).wrapping_add(1);
91 buf[0x18..0x1C].copy_from_slice(&sequence.to_be_bytes());
92 buf[0x1C..0x20].copy_from_slice(&0u32.to_be_bytes());
93 if jsb.uses_csum_v2_or_v3() {
94 buf[0xFC..0x100].fill(0);
95 let csum = crate::checksum::linux_crc32c(!0, &buf);
96 buf[0xFC..0x100].copy_from_slice(&csum.to_be_bytes());
97 }
98 fs.dev.write_at(at, &buf)?;
99 fs.dev.flush()
100}
101
102/// Where a block number lands on the device, or a refusal.
103///
104/// Replay is the one operation that writes to a block number the image
105/// itself chose. It runs at mount, before anybody has asked for a file,
106/// and the number comes from a descriptor tag inside the journal --
107/// which is to say, from whoever wrote the image.
108///
109/// Two things go wrong when that number is taken on trust:
110///
111/// - a block past the end of the filesystem writes past the end of the
112/// device. Against a file-backed image that is not an error at all;
113/// the file simply grows, and a tag naming block 0xDEADBEEF turns a
114/// 16 MiB image into a 15 TB one.
115/// - a block number above `2^64 / block_size` wraps. At a 4 KiB block
116/// size, block `2^52` multiplies out to exactly 0 -- the boot sector
117/// and the primary superblock -- and the replay overwrites them with
118/// contents the image supplied, then reports success.
119///
120/// Neither is a write to be clamped into range. A destination the
121/// filesystem does not contain means the journal is not describing this
122/// filesystem, so the replay stops and the mount fails rather than
123/// applying the part of it that happened to be in range.
124pub(crate) fn byte_offset_of(fs: &Filesystem, block: u64) -> Result<u64> {
125 byte_offset_in(
126 block,
127 fs.sb.blocks_count,
128 fs.sb.block_size(),
129 fs.dev.size_bytes(),
130 )
131}
132
133/// The same rule, for callers that hold the pieces rather than the
134/// `Filesystem` -- `journal_writer` writes through a `&dyn BlockDevice`
135/// after the mount has gone out of scope.
136pub(crate) fn byte_offset_in(
137 block: u64,
138 blocks_count: u64,
139 block_size: u32,
140 device_bytes: u64,
141) -> Result<u64> {
142 if block >= blocks_count {
143 return Err(Error::Corrupt(
144 "journal: names a block past the end of the filesystem",
145 ));
146 }
147 let block_size = block_size as u64;
148 let offset = block
149 .checked_mul(block_size)
150 .ok_or(Error::Corrupt("journal: block offset overflows"))?;
151 // `blocks_count` is the image's own claim about its size, so a
152 // truncated image passes the check above and still reaches past the
153 // device. The device is asked directly.
154 let end = offset
155 .checked_add(block_size)
156 .ok_or(Error::Corrupt("journal: block offset overflows"))?;
157 if end > device_bytes {
158 return Err(Error::Corrupt(
159 "journal: names a block past the end of the device",
160 ));
161 }
162 Ok(offset)
163}
164
165/// Apply all writes in `plan` to `fs.dev`. Each `ReplayEntry` names a
166/// journal-block source and a fs-block destination; we read the source
167/// contents and overwrite the destination. Revoked writes are already
168/// filtered out of `plan.writes` by [`ReplayPlan::filter_revoked`] (called
169/// by [`crate::journal::walk`]).
170///
171/// Returns the number of blocks written. Errors propagate from either the
172/// journal read or the final-location write.
173pub fn apply(fs: &Filesystem, plan: &ReplayPlan) -> Result<usize> {
174 if plan.writes.is_empty() {
175 return Ok(0);
176 }
177 if !fs.dev.is_writable() {
178 return Err(Error::Corrupt(
179 "journal_apply: device is not writable; cannot replay",
180 ));
181 }
182
183 let block_size = fs.sb.block_size() as u64;
184
185 // EVERY ENTRY IS RESOLVED BEFORE ANY IS WRITTEN. Checked inside the
186 // write loop, a bad destination was found after the entries before it
187 // were already on disk, and the mount failed over a filesystem it had
188 // half replayed (#150). A refusal is only free before the first write.
189 //
190 // Source: journal_block is a logical block inside the journal inode,
191 // resolved to a physical fs block via the extent tree. Destination:
192 // fs_block, bounds-checked against the filesystem and the device.
193 let resolved = resolve(fs, plan)?;
194
195 let mut applied = 0usize;
196 for (w, &(source, destination)) in plan.writes.iter().zip(&resolved) {
197 let mut buf = vec![0u8; block_size as usize];
198 fs.dev.read_at(source, &mut buf)?;
199
200 // If the ESCAPED flag is set, the first 4 bytes of the journal block
201 // were zeroed during write to keep them from colliding with the
202 // JBD2 magic. Restore them to the magic before writing to final.
203 if w.flags & crate::journal::TAG_ESCAPED != 0 {
204 buf[0..4].copy_from_slice(&crate::jbd2::JBD2_MAGIC_NUMBER.to_be_bytes());
205 }
206
207 fs.dev.write_at(destination, &buf)?;
208 applied += 1;
209 }
210
211 // Best-effort durability: flush before we claim success. If the caller
212 // wants full crash-consistent ordering (journal writes → fsync → final
213 // writes → fsync), they should sequence it themselves; this module is
214 // the single final-location pass after the journal is already on disk.
215 fs.dev.flush()?;
216
217 Ok(applied)
218}
219
220/// Replay a dirty journal into the device's buffer cache, writing nothing
221/// (#72).
222///
223/// A transaction in a dirty journal is committed: the filesystem told its
224/// caller the change was durable and had not yet checkpointed it. A
225/// read-only mount that skipped replay read the superseded metadata and
226/// reported it as fact. This walks the journal exactly as
227/// [`replay_if_dirty`] does, with every refusal the walk makes, and pins
228/// each replayed block's bytes in the cache (`populate_cache`) instead of
229/// writing them, so reads see the committed state and the device stays
230/// untouched.
231///
232/// Returns the number of blocks pinned: 0 for a clean journal or none.
233pub fn replay_into_cache(fs: &Filesystem) -> Result<usize> {
234 let Some(jsb) = jbd2::read_superblock(fs)? else {
235 return Ok(0);
236 };
237 if jsb.is_clean() {
238 return Ok(0);
239 }
240 let plan = crate::journal::walk(fs, &jsb)?;
241 if plan.writes.is_empty() {
242 return Ok(0);
243 }
244 let block_size = fs.sb.block_size() as u64;
245 let resolved = resolve(fs, &plan)?;
246 // Read every source before pinning any, so a failed read leaves the
247 // cache as it was rather than half replayed.
248 let mut blocks = Vec::with_capacity(plan.writes.len());
249 for (w, &(source, _)) in plan.writes.iter().zip(&resolved) {
250 let mut buf = vec![0u8; block_size as usize];
251 fs.dev.read_at(source, &mut buf)?;
252 if w.flags & crate::journal::TAG_ESCAPED != 0 {
253 buf[0..4].copy_from_slice(&crate::jbd2::JBD2_MAGIC_NUMBER.to_be_bytes());
254 }
255 blocks.push((w.fs_block, buf));
256 }
257 // In log order, so a block a later transaction rewrote ends with its
258 // later bytes.
259 let n = blocks.len();
260 for (block, bytes) in blocks {
261 fs.dev.populate_cache(block, bytes);
262 }
263 Ok(n)
264}
265
266/// Each entry's journal source and filesystem destination as device byte
267/// offsets, or the refusal for the first that has none.
268fn resolve(fs: &Filesystem, plan: &ReplayPlan) -> Result<Vec<(u64, u64)>> {
269 let raw = fs.read_inode_raw(fs.sb.journal_inode)?;
270 let jinode = Inode::parse(&raw)?;
271 plan.writes
272 .iter()
273 .map(|w| {
274 let phys = jbd2::journal_block_to_physical(fs, &jinode, w.journal_block)?
275 .ok_or(Error::Corrupt("journal_apply: journal block unmapped"))?;
276 Ok((byte_offset_of(fs, phys)?, byte_offset_of(fs, w.fs_block)?))
277 })
278 .collect()
279}
280
281/// Convenience: mount-time entry point. Reads the journal superblock and,
282/// if the journal is dirty AND the device is writable, walks + applies in
283/// one shot. Returns the number of blocks replayed (0 if clean or device
284/// is read-only — the latter is not an error; mount proceeds read-only).
285pub fn replay_if_dirty(fs: &Filesystem) -> Result<usize> {
286 // A read-only device cannot take a replay. Mount replays a dirty
287 // journal into the cache instead (`replay_into_cache`, #72); this entry
288 // point is the one that writes.
289 if !fs.dev.is_writable() {
290 return Ok(0);
291 }
292 let Some(jsb) = jbd2::read_superblock(fs)? else {
293 return Ok(0); // no journal inode → nothing to replay
294 };
295 if jsb.is_clean() {
296 // A crash between the clean journal and the cleared flag leaves
297 // the flag behind with nothing to recover.
298 if fs.sb.feature_incompat & crate::features::Incompat::RECOVER.bits() != 0 {
299 write_needs_recovery(fs.dev.as_ref(), false)?;
300 fs.dev.flush()?;
301 }
302 return Ok(0);
303 }
304 // Replay writes to the volume, so it refuses what every other write
305 // refuses. The mount checks these bits too, but a lazy mount checked
306 // while it was read-only and calls this once it is writable (#117).
307 let write_breaking = crate::features::write_breaking_incompat(fs.sb.feature_incompat);
308 if write_breaking != 0 {
309 return Err(crate::Error::UnsupportedIncompat(write_breaking));
310 }
311 let plan = crate::journal::walk(fs, &jsb)?;
312 let applied = apply(fs, &plan)?;
313 mark_journal_clean(fs, &jsb, &plan)?;
314 write_needs_recovery(fs.dev.as_ref(), false)?;
315 fs.dev.flush()?;
316 Ok(applied)
317}
318
319/// Describe the JBD2 superblock fields most relevant to replay decisions.
320/// Useful for diagnostics and for tests that want to assert on the state
321/// before and after replay.
322#[derive(Debug, Clone)]
323pub struct ReplaySummary {
324 pub clean: bool,
325 pub sequence: u32,
326 pub start: u32,
327 pub max_len: u32,
328 pub has_revoke: bool,
329 pub uses_64bit: bool,
330 pub uses_csum_v2_or_v3: bool,
331}
332
333impl From<&JournalSuperblock> for ReplaySummary {
334 fn from(jsb: &JournalSuperblock) -> Self {
335 Self {
336 clean: jsb.is_clean(),
337 sequence: jsb.sequence,
338 start: jsb.start,
339 max_len: jsb.max_len,
340 has_revoke: jsb.has_revoke(),
341 uses_64bit: jsb.uses_64bit(),
342 uses_csum_v2_or_v3: jsb.uses_csum_v2_or_v3(),
343 }
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::journal::{ReplayEntry, ReplayPlan};
351
352 #[test]
353 fn empty_plan_applies_zero_blocks() {
354 // We can exercise the early-return branch without a real Filesystem.
355 let plan = ReplayPlan::default();
356 // The function requires a Filesystem; but the early-return path never
357 // touches it. Skipping — the integration test in tests/ covers the
358 // live path.
359 assert!(plan.writes.is_empty());
360 }
361
362 /// The overflow branch itself. `blocks_count` and the device size are
363 /// both `u64::MAX` so neither bounds check can refuse the block first:
364 /// only `checked_mul` stands between this block and a byte offset that
365 /// wraps to 0, the boot sector and the primary superblock.
366 #[test]
367 fn byte_offset_in_refuses_a_block_whose_offset_wraps() {
368 let block_size = 4096u32;
369 let wraps_to_zero = u64::MAX / block_size as u64 + 1;
370 assert_eq!(wraps_to_zero.wrapping_mul(block_size as u64), 0);
371 match byte_offset_in(wraps_to_zero, u64::MAX, block_size, u64::MAX) {
372 Err(Error::Corrupt(msg)) => assert_eq!(msg, "journal: block offset overflows"),
373 other => panic!("block {wraps_to_zero} was not refused as an overflow: {other:?}"),
374 }
375 }
376
377 #[test]
378 fn replay_summary_mirrors_jsb() {
379 let jsb = JournalSuperblock {
380 block_type: crate::jbd2::JBD2_SUPERBLOCK_V2,
381 header_sequence: 1,
382 block_size: 4096,
383 max_len: 8192,
384 first: 1,
385 sequence: 42,
386 start: 100,
387 errno: 0,
388 feature_compat: 0,
389 feature_incompat: crate::jbd2::JbdIncompat::REVOKE.bits()
390 | crate::jbd2::JbdIncompat::BIT64.bits()
391 | crate::jbd2::JbdIncompat::CSUM_V3.bits(),
392 feature_ro_compat: 0,
393 uuid: [0; 16],
394 nr_users: 1,
395 checksum_type: 4,
396 num_fc_blocks: 0,
397 checksum: 0,
398 };
399 let s = ReplaySummary::from(&jsb);
400 assert!(!s.clean);
401 assert_eq!(s.sequence, 42);
402 assert_eq!(s.start, 100);
403 assert!(s.has_revoke);
404 assert!(s.uses_64bit);
405 assert!(s.uses_csum_v2_or_v3);
406 }
407
408 #[test]
409 fn clean_summary() {
410 let mut jsb = JournalSuperblock {
411 block_type: crate::jbd2::JBD2_SUPERBLOCK_V2,
412 header_sequence: 1,
413 block_size: 4096,
414 max_len: 8192,
415 first: 1,
416 sequence: 1,
417 start: 0,
418 errno: 0,
419 feature_compat: 0,
420 feature_incompat: 0,
421 feature_ro_compat: 0,
422 uuid: [0; 16],
423 nr_users: 1,
424 checksum_type: 0,
425 num_fc_blocks: 0,
426 checksum: 0,
427 };
428 jsb.start = 0;
429 let s = ReplaySummary::from(&jsb);
430 assert!(s.clean);
431 }
432
433 #[test]
434 fn replay_entry_structure_stable() {
435 let e = ReplayEntry {
436 transaction: 1,
437 fs_block: 100,
438 journal_block: 5,
439 flags: 0,
440 };
441 assert_eq!(e.transaction, 1);
442 assert_eq!(e.fs_block, 100);
443 assert_eq!(e.journal_block, 5);
444 }
445}