1use crate::error::{Error, Result};
62use crate::fs::Filesystem;
63use crate::inode::Inode;
64use crate::jbd2::{
65 self, JournalSuperblock, JBD2_COMMIT_BLOCK, JBD2_DESCRIPTOR_BLOCK, JBD2_MAGIC_NUMBER,
66 JBD2_REVOKE_BLOCK,
67};
68
69pub const TAG_ESCAPED: u32 = 0x1;
71pub const TAG_SAME_UUID: u32 = 0x2;
72pub const TAG_DELETED: u32 = 0x4;
73pub const TAG_LAST: u32 = 0x8;
74
75#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ReplayEntry {
80 pub transaction: u32,
82 pub fs_block: u64,
84 pub journal_block: u64,
86 pub flags: u32,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct RevokeEntry {
95 pub transaction: u32,
96 pub fs_block: u64,
97}
98
99#[derive(Debug, Default, Clone)]
101pub struct ReplayPlan {
102 pub writes: Vec<ReplayEntry>,
103 pub revokes: Vec<RevokeEntry>,
104 pub last_commit: u32,
106 pub next_sequence: Option<u32>,
108 pub blocks_walked: u64,
110}
111
112impl ReplayPlan {
113 pub fn filter_revoked(&mut self) {
117 if self.revokes.is_empty() {
118 return;
119 }
120 let mut highest: std::collections::HashMap<u64, u32> = std::collections::HashMap::new();
122 for r in &self.revokes {
123 let e = highest.entry(r.fs_block).or_insert(r.transaction);
124 if sequence_after(r.transaction, *e) {
125 *e = r.transaction;
126 }
127 }
128 self.writes.retain(|w| match highest.get(&w.fs_block) {
129 Some(&t) => sequence_after(w.transaction, t),
130 None => true,
131 });
132 }
133}
134
135fn sequence_after(a: u32, b: u32) -> bool {
138 (a.wrapping_sub(b) as i32) > 0
139}
140
141pub fn walk(fs: &Filesystem, jsb: &JournalSuperblock) -> Result<ReplayPlan> {
148 let mut plan = ReplayPlan::default();
149 if jsb.is_clean() {
150 return Ok(plan);
151 }
152
153 if jsb.unsupported_incompat() != 0 {
154 return Err(Error::Unsupported(
155 "journal declares an incompat feature this driver cannot replay",
156 ));
157 }
158 if let Some(why) = jsb.checksum_declaration_error() {
159 return Err(Error::Corrupt(why));
160 }
161 if jsb.block_size != fs.sb.block_size() {
162 return Err(Error::Corrupt(
163 "journal block size differs from the filesystem's",
164 ));
165 }
166 if jsb.first == 0
167 || jsb.first >= jsb.max_len
168 || jsb.start < jsb.first
169 || jsb.start >= jsb.max_len
170 {
171 return Err(Error::Corrupt("invalid journal geometry"));
172 }
173
174 let raw = fs.read_inode_raw(fs.sb.journal_inode)?;
175 let jinode = Inode::parse(&raw)?;
176 let block_size = jsb.block_size as u64;
177 let mut cur = jsb.start as u64;
178 let mut expect_seq = jsb.sequence;
179 let seed = jsb.uses_csum_v2_or_v3().then(|| jsb.csum_seed());
180 let mut pending = ReplayPlan::default();
185 let mut pending_corrupt: Option<&'static str> = None;
186
187 let limit = u64::from(jsb.max_len - jsb.first);
192
193 while plan.blocks_walked < limit {
194 let block_buf = read_journal_block(fs, &jinode, cur, block_size)?;
195 plan.blocks_walked += 1;
196
197 let Some(hdr) = try_parse_header(&block_buf) else {
201 break;
202 };
203
204 if hdr.sequence != expect_seq {
207 break;
208 }
209
210 match hdr.block_type {
211 JBD2_DESCRIPTOR_BLOCK => {
212 let torn = seed.is_some_and(|seed| !tail_checksum_matches(seed, &block_buf));
218 let tags = match parse_descriptor_tags(&block_buf, &mut cur, jsb, expect_seq) {
219 Ok(tags) => tags,
220 Err(_) if torn => break,
221 Err(e) => return Err(e),
222 };
223 if torn {
224 pending_corrupt = Some("journal descriptor block of a committed transaction");
225 }
226 plan.blocks_walked += tags.len() as u64;
227 if plan.blocks_walked > limit {
228 return Err(Error::Corrupt("journal transaction exceeds ring"));
229 }
230 for (entry, stored) in tags {
231 if let Some(seed) = seed {
232 let data =
233 read_journal_block(fs, &jinode, entry.journal_block, block_size)?;
234 let computed = jbd2::tag_checksum(seed, expect_seq, &data);
235 let computed = if jsb.uses_csum_v3() {
236 computed
237 } else {
238 computed & 0xFFFF
239 };
240 if computed != stored {
241 pending_corrupt = Some("journal data block of a committed transaction");
242 }
243 }
244 pending.writes.push(entry);
245 }
246 }
247 JBD2_COMMIT_BLOCK => {
248 if let Some(seed) = seed {
249 let stored = u32::from_be_bytes(
250 block_buf[jbd2::COMMIT_CHECKSUM_AT..jbd2::COMMIT_CHECKSUM_AT + 4]
251 .try_into()
252 .unwrap(),
253 );
254 if jbd2::commit_block_checksum(seed, &block_buf) != stored {
255 break;
256 }
257 }
258 if let Some(what) = pending_corrupt {
259 return Err(Error::BadChecksum { what });
260 }
261 plan.writes.append(&mut pending.writes);
262 plan.revokes.append(&mut pending.revokes);
263 plan.last_commit = expect_seq;
264 expect_seq = expect_seq.wrapping_add(1);
265 plan.next_sequence = Some(expect_seq);
266 }
267 JBD2_REVOKE_BLOCK => {
268 if seed.is_some_and(|seed| !tail_checksum_matches(seed, &block_buf)) {
269 pending_corrupt = Some("journal revoke block of a committed transaction");
270 }
271 let revokes = parse_revoke_block(&block_buf, jsb, expect_seq)?;
272 pending.revokes.extend(revokes);
273 }
274 _ => break,
275 }
276
277 cur = advance(cur, 1, jsb);
278 }
279
280 plan.filter_revoked();
281 Ok(plan)
282}
283
284fn advance(cur: u64, n: u64, jsb: &JournalSuperblock) -> u64 {
290 let first = jsb.first as u64;
291 let capacity = (jsb.max_len as u64).saturating_sub(first);
292 if capacity == 0 {
293 return cur; }
295 let offset = cur - first;
296 let new_off = (offset + n) % capacity;
297 first + new_off
298}
299
300struct JournalHeader {
301 block_type: u32,
302 sequence: u32,
303}
304
305fn tail_checksum_matches(seed: u32, block: &[u8]) -> bool {
307 let at = block.len() - jbd2::BLOCK_TAIL_BYTES;
308 let stored = u32::from_be_bytes(block[at..].try_into().unwrap());
309 jbd2::block_tail_checksum(seed, block) == stored
310}
311
312fn try_parse_header(block: &[u8]) -> Option<JournalHeader> {
313 if block.len() < 12 {
314 return None;
315 }
316 let magic = u32::from_be_bytes(block[0..4].try_into().ok()?);
317 if magic != JBD2_MAGIC_NUMBER {
318 return None;
319 }
320 Some(JournalHeader {
321 block_type: u32::from_be_bytes(block[4..8].try_into().ok()?),
322 sequence: u32::from_be_bytes(block[8..12].try_into().ok()?),
323 })
324}
325
326fn read_journal_block(
329 fs: &Filesystem,
330 jinode: &Inode,
331 journal_block: u64,
332 block_size: u64,
333) -> Result<Vec<u8>> {
334 let phys = jbd2::journal_block_to_physical(fs, jinode, journal_block)?
335 .ok_or(Error::Corrupt("journal block unmapped"))?;
336 let mut buf = vec![0u8; block_size as usize];
337 fs.dev.read_at(phys * block_size, &mut buf)?;
338 Ok(buf)
339}
340
341fn parse_descriptor_tags(
347 block: &[u8],
348 cur: &mut u64,
349 jsb: &JournalSuperblock,
350 transaction: u32,
351) -> Result<Vec<(ReplayEntry, u32)>> {
352 let mut out = Vec::new();
353 let uses_64bit = jsb.uses_64bit();
354 let uses_v3 = jsb.uses_csum_v3();
355 let tag_size = jsb.tag_bytes();
356 let usable = if jsb.uses_csum_v2_or_v3() {
358 block.len() - jbd2::BLOCK_TAIL_BYTES
359 } else {
360 block.len()
361 };
362
363 let mut pos = 12usize; loop {
365 if pos + tag_size > usable {
366 return Err(Error::Corrupt("descriptor tag overruns block"));
367 }
368
369 let blocknr_lo = u32::from_be_bytes(block[pos..pos + 4].try_into().unwrap());
370
371 let (flags, checksum) = if uses_v3 {
372 (
373 u32::from_be_bytes(block[pos + 4..pos + 8].try_into().unwrap()),
374 u32::from_be_bytes(block[pos + 12..pos + 16].try_into().unwrap()),
375 )
376 } else {
377 (
379 u16::from_be_bytes(block[pos + 6..pos + 8].try_into().unwrap()) as u32,
380 u16::from_be_bytes(block[pos + 4..pos + 6].try_into().unwrap()) as u32,
381 )
382 };
383 let uuid_follows_size = if flags & TAG_SAME_UUID != 0 { 0 } else { 16 };
386
387 let blocknr = if uses_64bit {
388 let hi = u32::from_be_bytes(block[pos + 8..pos + 12].try_into().unwrap());
389 ((hi as u64) << 32) | (blocknr_lo as u64)
390 } else {
391 blocknr_lo as u64
392 };
393
394 *cur = advance(*cur, 1, jsb);
396 let data_journal_block = *cur;
397
398 out.push((
399 ReplayEntry {
400 transaction,
401 fs_block: blocknr,
402 journal_block: data_journal_block,
403 flags,
404 },
405 checksum,
406 ));
407
408 pos += tag_size + uuid_follows_size;
409
410 if flags & TAG_LAST != 0 {
411 break;
412 }
413 if pos + tag_size > usable {
414 break;
416 }
417 }
418 Ok(out)
419}
420
421fn parse_revoke_block(
422 block: &[u8],
423 jsb: &JournalSuperblock,
424 transaction: u32,
425) -> Result<Vec<RevokeEntry>> {
426 if block.len() < 16 {
428 return Err(Error::Corrupt("revoke block too small"));
429 }
430 let r_count = u32::from_be_bytes(block[12..16].try_into().unwrap()) as usize;
431 let record_size = if jsb.uses_64bit() { 8 } else { 4 };
432 let usable = if jsb.uses_csum_v2_or_v3() {
436 block.len() - jbd2::BLOCK_TAIL_BYTES
437 } else {
438 block.len()
439 };
440 if r_count > usable {
441 return Err(Error::Corrupt(
442 "journal revoke block count exceeds the block",
443 ));
444 }
445 let mut out = Vec::new();
446 let mut pos = 16usize;
447 let end = r_count;
448 while pos + record_size <= end {
449 let fs_block = if jsb.uses_64bit() {
450 u64::from_be_bytes(block[pos..pos + 8].try_into().unwrap())
451 } else {
452 u32::from_be_bytes(block[pos..pos + 4].try_into().unwrap()) as u64
453 };
454 out.push(RevokeEntry {
455 transaction,
456 fs_block,
457 });
458 pos += record_size;
459 }
460 Ok(out)
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use crate::jbd2::{JbdIncompat, JBD2_SUPERBLOCK_V2};
467
468 fn mk_jsb(incompat: u32, max_len: u32) -> JournalSuperblock {
469 JournalSuperblock {
470 block_type: JBD2_SUPERBLOCK_V2,
471 header_sequence: 1,
472 block_size: 4096,
473 max_len,
474 first: 1,
475 sequence: 10,
476 start: 1,
477 errno: 0,
478 feature_compat: 0,
479 feature_incompat: incompat,
480 feature_ro_compat: 0,
481 uuid: [0; 16],
482 nr_users: 1,
483 checksum_type: 4,
484 num_fc_blocks: 0,
485 checksum: 0,
486 }
487 }
488
489 fn header(buf: &mut [u8], block_type: u32, seq: u32) {
490 buf[0..4].copy_from_slice(&JBD2_MAGIC_NUMBER.to_be_bytes());
491 buf[4..8].copy_from_slice(&block_type.to_be_bytes());
492 buf[8..12].copy_from_slice(&seq.to_be_bytes());
493 }
494
495 #[test]
496 fn revoke_order_wraps_like_jbd2_transaction_ids() {
497 let mut plan = ReplayPlan {
498 writes: vec![
499 ReplayEntry {
500 transaction: u32::MAX,
501 fs_block: 3,
502 journal_block: 1,
503 flags: 0,
504 },
505 ReplayEntry {
506 transaction: 1,
507 fs_block: 3,
508 journal_block: 2,
509 flags: 0,
510 },
511 ],
512 revokes: vec![
513 RevokeEntry {
514 transaction: u32::MAX - 1,
515 fs_block: 3,
516 },
517 RevokeEntry {
518 transaction: 0,
519 fs_block: 3,
520 },
521 ],
522 ..Default::default()
523 };
524 plan.filter_revoked();
525 assert_eq!(plan.writes.len(), 1);
526 assert_eq!(plan.writes[0].transaction, 1);
527 }
528
529 #[test]
530 fn revoke_records_scale_with_r_count() {
531 let jsb = mk_jsb(JbdIncompat::REVOKE.bits(), 128);
532 let mut blk = vec![0u8; 4096];
533 header(&mut blk, JBD2_REVOKE_BLOCK, 10);
534 blk[12..16].copy_from_slice(&28u32.to_be_bytes());
536 blk[16..20].copy_from_slice(&100u32.to_be_bytes());
537 blk[20..24].copy_from_slice(&200u32.to_be_bytes());
538 blk[24..28].copy_from_slice(&300u32.to_be_bytes());
539 let out = parse_revoke_block(&blk, &jsb, 10).unwrap();
540 assert_eq!(out.len(), 3);
541 assert_eq!(out[0].fs_block, 100);
542 assert_eq!(out[2].fs_block, 300);
543 }
544
545 fn revoke_block_overrunning_its_records(
548 r_count: u32,
549 tail_at: usize,
550 fs_block: u32,
551 ) -> Vec<u8> {
552 let mut blk = vec![0u8; 4096];
553 header(&mut blk, JBD2_REVOKE_BLOCK, 10);
554 blk[12..16].copy_from_slice(&r_count.to_be_bytes());
555 blk[16..20].copy_from_slice(&100u32.to_be_bytes());
556 blk[tail_at..tail_at + 4].copy_from_slice(&fs_block.to_be_bytes());
557 blk
558 }
559
560 #[test]
565 fn a_csum_v3_revoke_count_reaching_the_tail_is_refused() {
566 let jsb = mk_jsb(
567 JbdIncompat::REVOKE.bits() | JbdIncompat::CSUM_V3.bits(),
568 128,
569 );
570 let blk = revoke_block_overrunning_its_records(4096, 4096 - jbd2::BLOCK_TAIL_BYTES, 9000);
571 let parsed = parse_revoke_block(&blk, &jsb, 10);
572 let mut plan = ReplayPlan {
573 writes: vec![ReplayEntry {
574 transaction: 10,
575 fs_block: 9000,
576 journal_block: 2,
577 flags: 0,
578 }],
579 revokes: parsed.as_ref().map(Vec::clone).unwrap_or_default(),
580 ..Default::default()
581 };
582 plan.filter_revoked();
583 assert!(
584 matches!(parsed, Err(Error::Corrupt(_))),
585 "r_count covering the checksum tail parsed as {} records; writes left in the plan: {}",
586 parsed.map(|r| r.len()).unwrap_or_default(),
587 plan.writes.len()
588 );
589 }
590
591 #[test]
594 fn a_csum_v3_revoke_count_up_to_the_tail_is_read() {
595 let jsb = mk_jsb(
596 JbdIncompat::REVOKE.bits() | JbdIncompat::CSUM_V3.bits(),
597 128,
598 );
599 let usable = 4096 - jbd2::BLOCK_TAIL_BYTES;
600 let blk = revoke_block_overrunning_its_records(usable as u32, usable - 4, 9000);
601 let out = parse_revoke_block(&blk, &jsb, 10).unwrap();
602 assert_eq!(out.len(), (usable - 16) / 4);
603 assert_eq!(out[0].fs_block, 100);
604 assert_eq!(out.last().unwrap().fs_block, 9000);
605 }
606
607 #[test]
609 fn a_revoke_count_past_the_block_is_refused() {
610 let jsb = mk_jsb(JbdIncompat::REVOKE.bits(), 128);
611 let blk = revoke_block_overrunning_its_records(4097, 4092, 9000);
612 let parsed = parse_revoke_block(&blk, &jsb, 10);
613 assert!(
614 matches!(parsed, Err(Error::Corrupt(_))),
615 "r_count past the block parsed as {} records",
616 parsed.map(|r| r.len()).unwrap_or_default()
617 );
618 let blk = revoke_block_overrunning_its_records(4096, 4092, 9000);
620 let out = parse_revoke_block(&blk, &jsb, 10).unwrap();
621 assert_eq!(out.len(), (4096 - 16) / 4);
622 assert_eq!(out.last().unwrap().fs_block, 9000);
623 }
624
625 #[test]
626 fn filter_revoked_drops_older_writes() {
627 let mut plan = ReplayPlan::default();
628 plan.writes.push(ReplayEntry {
629 transaction: 5,
630 fs_block: 100,
631 journal_block: 1,
632 flags: 0,
633 });
634 plan.writes.push(ReplayEntry {
635 transaction: 5,
636 fs_block: 200,
637 journal_block: 2,
638 flags: 0,
639 });
640 plan.writes.push(ReplayEntry {
641 transaction: 7,
642 fs_block: 100,
643 journal_block: 3,
644 flags: 0,
645 });
646 plan.revokes.push(RevokeEntry {
647 transaction: 6,
648 fs_block: 100,
649 });
650 plan.filter_revoked();
651 assert_eq!(plan.writes.len(), 2);
653 assert!(plan.writes.iter().any(|w| w.fs_block == 200));
654 assert!(plan
655 .writes
656 .iter()
657 .any(|w| w.fs_block == 100 && w.transaction == 7));
658 }
659
660 #[test]
661 fn parse_v2_tags_without_bit64() {
662 let jsb = mk_jsb(0, 128);
665 let mut blk = vec![0u8; 4096];
666 header(&mut blk, JBD2_DESCRIPTOR_BLOCK, 10);
667 blk[12..16].copy_from_slice(&1000u32.to_be_bytes());
669 blk[16..18].copy_from_slice(&0u16.to_be_bytes()); blk[18..20].copy_from_slice(&0u16.to_be_bytes()); blk[36..40].copy_from_slice(&2000u32.to_be_bytes());
674 blk[40..42].copy_from_slice(&0u16.to_be_bytes());
675 blk[42..44].copy_from_slice(&((TAG_SAME_UUID | TAG_LAST) as u16).to_be_bytes());
676
677 let mut cur = 1u64;
678 let out = parse_descriptor_tags(&blk, &mut cur, &jsb, 10).unwrap();
679 assert_eq!(out.len(), 2);
680 assert_eq!(out[0].0.fs_block, 1000);
681 assert_eq!(out[1].0.fs_block, 2000);
682 assert_eq!(out[1].0.flags & TAG_LAST, TAG_LAST);
683 }
684
685 #[test]
688 fn parse_v3_tags_without_bit64_skip_the_uuid() {
689 let jsb = mk_jsb(JbdIncompat::CSUM_V3.bits(), 128);
690 let mut blk = vec![0u8; 4096];
691 header(&mut blk, JBD2_DESCRIPTOR_BLOCK, 10);
692 blk[12..16].copy_from_slice(&1000u32.to_be_bytes());
693 blk[24..28].copy_from_slice(&0xAABB_CCDDu32.to_be_bytes()); blk[28..44].fill(0xEE); blk[44..48].copy_from_slice(&2000u32.to_be_bytes());
696 blk[48..52].copy_from_slice(&(TAG_SAME_UUID | TAG_LAST).to_be_bytes());
697
698 let mut cur = 1u64;
699 let out = parse_descriptor_tags(&blk, &mut cur, &jsb, 10).unwrap();
700 assert_eq!(out.len(), 2);
701 assert_eq!(
702 out[0],
703 (
704 ReplayEntry {
705 transaction: 10,
706 fs_block: 1000,
707 journal_block: 2,
708 flags: 0
709 },
710 0xAABB_CCDD
711 )
712 );
713 assert_eq!(out[1].0.fs_block, 2000);
714 assert_eq!(out[1].0.journal_block, 3);
715 }
716
717 #[test]
718 fn try_parse_header_rejects_bad_magic() {
719 let mut blk = vec![0u8; 12];
720 blk[0] = 1;
721 assert!(try_parse_header(&blk).is_none());
722 }
723
724 #[test]
725 fn try_parse_header_accepts_valid() {
726 let mut blk = vec![0u8; 12];
727 header(&mut blk, JBD2_COMMIT_BLOCK, 42);
728 let h = try_parse_header(&blk).unwrap();
729 assert_eq!(h.block_type, JBD2_COMMIT_BLOCK);
730 assert_eq!(h.sequence, 42);
731 }
732
733 #[test]
734 fn advance_wraps_around_the_log() {
735 let jsb = mk_jsb(0, 8); assert_eq!(advance(1, 1, &jsb), 2);
737 assert_eq!(advance(7, 1, &jsb), 1); assert_eq!(advance(6, 5, &jsb), 4); }
740
741 #[test]
742 fn clean_journal_produces_empty_plan() {
743 let mut jsb = mk_jsb(0, 128);
745 jsb.start = 0; assert!(jsb.is_clean());
750 let plan = ReplayPlan::default();
751 assert!(plan.writes.is_empty());
752 assert_eq!(plan.last_commit, 0);
753 }
754}