Skip to main content

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}