1use crate::error::{Error, Result};
50use crate::fs::Filesystem;
51use crate::inode::Inode;
52
53pub const JBD2_MAGIC_NUMBER: u32 = 0xc03b_3998;
55
56pub const JBD2_DESCRIPTOR_BLOCK: u32 = 1;
58pub const JBD2_COMMIT_BLOCK: u32 = 2;
59pub const JBD2_SUPERBLOCK_V1: u32 = 3;
60pub const JBD2_SUPERBLOCK_V2: u32 = 4;
61pub const JBD2_REVOKE_BLOCK: u32 = 5;
62
63bitflags::bitflags! {
64 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
66 pub struct JbdIncompat: u32 {
67 const REVOKE = 0x0000_0001;
68 const BIT64 = 0x0000_0002;
69 const ASYNC_COMMIT = 0x0000_0004;
70 const CSUM_V2 = 0x0000_0008;
71 const CSUM_V3 = 0x0000_0010;
72 const FAST_COMMIT = 0x0000_0020;
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct JournalSuperblock {
79 pub block_type: u32,
81 pub header_sequence: u32,
83 pub block_size: u32,
85 pub max_len: u32,
87 pub first: u32,
89 pub sequence: u32,
91 pub start: u32,
93 pub errno: u32,
95 pub feature_compat: u32,
97 pub feature_incompat: u32,
98 pub feature_ro_compat: u32,
99 pub uuid: [u8; 16],
101 pub nr_users: u32,
104 pub checksum_type: u8,
106 pub num_fc_blocks: u32,
108 pub checksum: u32,
110}
111
112impl JournalSuperblock {
113 pub fn validate_plain_recovery(&self, block_size: u32, blocks_count: u64) -> Result<()> {
116 let supported = JbdIncompat::REVOKE.bits() | JbdIncompat::BIT64.bits();
117 if self.feature_compat != 0
118 || self.feature_ro_compat != 0
119 || self.feature_incompat & !supported != 0
120 {
121 return Err(Error::Unsupported("checked lifecycle supports plain JBD2 only; checksummed/async/fast-commit journals need separate qualification"));
122 }
123 if self.block_type != JBD2_SUPERBLOCK_V2
124 || self.block_size != block_size
125 || self.first != 1
126 || self.max_len <= self.first
127 || u64::from(self.max_len) > blocks_count
128 || (self.start != 0 && (self.start < self.first || self.start >= self.max_len))
129 || self.nr_users != 1
130 {
131 return Err(Error::Corrupt("unsupported internal journal geometry"));
132 }
133 if self.errno != 0 {
134 return Err(Error::Corrupt("journal records an outstanding error"));
135 }
136 Ok(())
137 }
138
139 pub fn is_clean(&self) -> bool {
141 self.start == 0
142 }
143
144 pub fn uses_csum_v3(&self) -> bool {
152 self.feature_incompat & JbdIncompat::CSUM_V3.bits() != 0
153 }
154
155 pub fn uses_csum_v2_or_v3(&self) -> bool {
156 self.feature_incompat & (JbdIncompat::CSUM_V2.bits() | JbdIncompat::CSUM_V3.bits()) != 0
157 }
158
159 pub fn uses_64bit(&self) -> bool {
161 self.feature_incompat & JbdIncompat::BIT64.bits() != 0
162 }
163
164 pub fn has_revoke(&self) -> bool {
166 self.feature_incompat & JbdIncompat::REVOKE.bits() != 0
167 }
168
169 pub fn unsupported_incompat(&self) -> u32 {
175 self.feature_incompat & !SUPPORTED_JBD_INCOMPAT
176 }
177
178 pub fn checksum_declaration_error(&self) -> Option<&'static str> {
195 let has = |bit: JbdIncompat| self.feature_incompat & bit.bits() != 0;
196 let per_block = [has(JbdIncompat::CSUM_V2), has(JbdIncompat::CSUM_V3)];
197 let commit_only = self.feature_compat & JBD2_FEATURE_COMPAT_CHECKSUM != 0;
198 match per_block {
199 [false, false] => None,
200 [true, true] => Some("journal declares both CSUM_V2 and CSUM_V3"),
201 _ if commit_only => Some("journal declares v1 checksums alongside CSUM_V2/V3"),
202 _ if self.checksum_type != JBD2_CRC32C_CHKSUM => {
203 Some("journal checksum type is not crc32c")
204 }
205 _ => None,
206 }
207 }
208
209 pub fn csum_seed(&self) -> u32 {
212 crate::checksum::linux_crc32c(!0, &self.uuid)
213 }
214
215 pub fn tag_bytes(&self) -> usize {
227 let per_block_scheme = if self.uses_csum_v3() {
228 TagChecksum::V3
229 } else if self.feature_incompat & JbdIncompat::CSUM_V2.bits() != 0 {
230 TagChecksum::V2
231 } else {
232 TagChecksum::None
233 };
234 match (per_block_scheme, self.uses_64bit()) {
235 (TagChecksum::None, false) => 8,
236 (TagChecksum::None, true) => 12,
237 (TagChecksum::V2, false) => 10,
238 (TagChecksum::V2, true) => 14,
239 (TagChecksum::V3, _) => 16,
240 }
241 }
242}
243
244#[derive(Clone, Copy)]
246enum TagChecksum {
247 None,
248 V2,
249 V3,
250}
251
252pub const JBD2_FEATURE_COMPAT_CHECKSUM: u32 = 0x1;
254
255pub const JBD2_CRC32C_CHKSUM: u8 = 4;
257
258pub const SUPPORTED_JBD_INCOMPAT: u32 = JbdIncompat::REVOKE.bits()
260 | JbdIncompat::BIT64.bits()
261 | JbdIncompat::CSUM_V2.bits()
262 | JbdIncompat::CSUM_V3.bits();
263
264pub const BLOCK_TAIL_BYTES: usize = 4;
267
268pub const COMMIT_CHECKSUM_AT: usize = 16;
271
272pub fn tag_checksum(seed: u32, sequence: u32, data: &[u8]) -> u32 {
278 let seq = crate::checksum::linux_crc32c(seed, &sequence.to_be_bytes());
279 crate::checksum::linux_crc32c(seq, data)
280}
281
282pub fn block_tail_checksum(seed: u32, block: &[u8]) -> u32 {
286 let body = block.len().saturating_sub(BLOCK_TAIL_BYTES);
287 let crc = crate::checksum::linux_crc32c(seed, &block[..body]);
288 crate::checksum::linux_crc32c(crc, &[0u8; BLOCK_TAIL_BYTES])
289}
290
291pub fn commit_block_checksum(seed: u32, block: &[u8]) -> u32 {
294 let at = COMMIT_CHECKSUM_AT;
295 let crc = crate::checksum::linux_crc32c(seed, &block[..at]);
296 let crc = crate::checksum::linux_crc32c(crc, &[0u8; 4]);
297 crate::checksum::linux_crc32c(crc, &block[at + 4..])
298}
299
300impl JournalSuperblock {
301 pub fn parse(raw: &[u8]) -> Result<Self> {
305 if raw.len() < 0x100 {
306 return Err(Error::Corrupt("jbd2 superblock buffer too small"));
307 }
308
309 let h_magic = u32::from_be_bytes(raw[0x00..0x04].try_into().unwrap());
310 if h_magic != JBD2_MAGIC_NUMBER {
311 return Err(Error::Corrupt("jbd2 superblock magic mismatch"));
312 }
313 let block_type = u32::from_be_bytes(raw[0x04..0x08].try_into().unwrap());
314 if block_type != JBD2_SUPERBLOCK_V1 && block_type != JBD2_SUPERBLOCK_V2 {
315 return Err(Error::Corrupt("jbd2 header is not a superblock block type"));
316 }
317 let header_sequence = u32::from_be_bytes(raw[0x08..0x0C].try_into().unwrap());
318
319 let block_size = u32::from_be_bytes(raw[0x0C..0x10].try_into().unwrap());
320 let max_len = u32::from_be_bytes(raw[0x10..0x14].try_into().unwrap());
321 let first = u32::from_be_bytes(raw[0x14..0x18].try_into().unwrap());
322 let sequence = u32::from_be_bytes(raw[0x18..0x1C].try_into().unwrap());
323 let start = u32::from_be_bytes(raw[0x1C..0x20].try_into().unwrap());
324 let errno = u32::from_be_bytes(raw[0x20..0x24].try_into().unwrap());
325
326 let mut out = Self {
327 block_type,
328 header_sequence,
329 block_size,
330 max_len,
331 first,
332 sequence,
333 start,
334 errno,
335 feature_compat: 0,
336 feature_incompat: 0,
337 feature_ro_compat: 0,
338 uuid: [0; 16],
339 nr_users: 0,
340 checksum_type: 0,
341 num_fc_blocks: 0,
342 checksum: 0,
343 };
344
345 if block_type == JBD2_SUPERBLOCK_V2 {
347 out.feature_compat = u32::from_be_bytes(raw[0x24..0x28].try_into().unwrap());
348 out.feature_incompat = u32::from_be_bytes(raw[0x28..0x2C].try_into().unwrap());
349 out.feature_ro_compat = u32::from_be_bytes(raw[0x2C..0x30].try_into().unwrap());
350 out.uuid.copy_from_slice(&raw[0x30..0x40]);
351 out.nr_users = u32::from_be_bytes(raw[0x40..0x44].try_into().unwrap());
352 out.checksum_type = raw[0x50];
355 out.num_fc_blocks = u32::from_be_bytes(raw[0x54..0x58].try_into().unwrap());
357 out.checksum = u32::from_be_bytes(raw[0xFC..0x100].try_into().unwrap());
359 }
360
361 Ok(out)
362 }
363}
364
365pub fn journal_block_to_physical(
375 fs: &Filesystem,
376 journal_inode: &Inode,
377 journal_block: u64,
378) -> Result<Option<u64>> {
379 crate::indirect::map_logical_any(
380 &journal_inode.block,
381 journal_inode.flags,
382 fs.dev.as_ref(),
383 fs.sb.block_size(),
384 journal_block,
385 )
386}
387
388pub fn read_superblock(fs: &Filesystem) -> Result<Option<JournalSuperblock>> {
398 let jino_num = fs.sb.journal_inode;
399 if jino_num == 0 {
400 return Ok(None);
401 }
402
403 let raw = fs.read_inode_raw(jino_num)?;
404 let jinode = Inode::parse(&raw)?;
405
406 let phys = journal_block_to_physical(fs, &jinode, 0)?
407 .ok_or(Error::Corrupt("journal block 0 unmapped (sparse journal?)"))?;
408
409 let block_size = fs.sb.block_size() as u64;
410 let mut buf = vec![0u8; block_size as usize];
411 fs.dev.read_at(phys * block_size, &mut buf)?;
412 Ok(Some(JournalSuperblock::parse(&buf)?))
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 fn jsb_with(incompat: u32) -> JournalSuperblock {
420 let mut jsb = JournalSuperblock::parse(&make_v2_sb()).unwrap();
421 jsb.feature_incompat = incompat;
422 jsb
423 }
424
425 #[test]
429 fn tag_sizes_are_the_measured_table() {
430 let b64 = JbdIncompat::BIT64.bits();
431 let v2 = JbdIncompat::CSUM_V2.bits();
432 let v3 = JbdIncompat::CSUM_V3.bits();
433 for (incompat, bytes) in [
434 (0, 8),
435 (b64, 12),
436 (v2, 10),
437 (v2 | b64, 14),
438 (v3, 16),
439 (v3 | b64, 16),
440 ] {
441 assert_eq!(
442 jsb_with(incompat).tag_bytes(),
443 bytes,
444 "incompat {incompat:#x}"
445 );
446 }
447 }
448
449 #[test]
452 fn checksum_declarations_follow_the_observed_table() {
453 let v2 = JbdIncompat::CSUM_V2.bits();
454 let v3 = JbdIncompat::CSUM_V3.bits();
455 let v1 = JBD2_FEATURE_COMPAT_CHECKSUM;
456 let with = |compat: u32, incompat: u32, kind: u8| {
457 let mut jsb = jsb_with(incompat);
458 jsb.feature_compat = compat;
459 jsb.checksum_type = kind;
460 jsb.checksum_declaration_error().is_none()
461 };
462 let accepted = [
463 (0, 0, 0),
464 (0, 0, 4),
465 (v1, 0, 0),
466 (v1, 0, 1),
467 (v1, 0, 4),
468 (0, v2, 4),
469 (0, v3, 4),
470 ];
471 let refused = [
472 (0, v2 | v3, 4),
473 (v1, v2, 4),
474 (v1, v3, 4),
475 (0, v2, 1),
476 (0, v3, 1),
477 (0, v3, 2),
478 (0, v3, 0),
479 ];
480 for (compat, incompat, kind) in accepted {
481 assert!(
482 with(compat, incompat, kind),
483 "{compat:#x}/{incompat:#x}/{kind}"
484 );
485 }
486 for (compat, incompat, kind) in refused {
487 assert!(
488 !with(compat, incompat, kind),
489 "{compat:#x}/{incompat:#x}/{kind}"
490 );
491 }
492 }
493
494 #[test]
495 fn async_commit_and_fast_commit_are_unsupported() {
496 let known =
497 JbdIncompat::REVOKE.bits() | JbdIncompat::BIT64.bits() | JbdIncompat::CSUM_V3.bits();
498 assert_eq!(jsb_with(known).unsupported_incompat(), 0);
499 for bit in [
500 JbdIncompat::ASYNC_COMMIT.bits(),
501 JbdIncompat::FAST_COMMIT.bits(),
502 0x8000_0000,
503 ] {
504 assert_eq!(jsb_with(known | bit).unsupported_incompat(), bit);
505 }
506 }
507
508 #[test]
511 fn block_checksums_skip_only_their_own_field() {
512 let seed = jsb_with(0).csum_seed();
513 let mut block = vec![0x5Au8; 4096];
514 let tail = block_tail_checksum(seed, &block);
515 block[4092..].fill(0xFF);
516 assert_eq!(block_tail_checksum(seed, &block), tail);
517 let commit = commit_block_checksum(seed, &block);
518 block[COMMIT_CHECKSUM_AT..COMMIT_CHECKSUM_AT + 4].fill(0x00);
519 assert_eq!(commit_block_checksum(seed, &block), commit);
520 block[4091] ^= 1;
521 assert_ne!(block_tail_checksum(seed, &block), tail);
522 assert_ne!(commit_block_checksum(seed, &block), commit);
523 assert_ne!(tag_checksum(seed, 1, &block), tag_checksum(seed, 2, &block));
524 }
525
526 fn make_v2_sb() -> Vec<u8> {
528 let mut buf = vec![0u8; 1024];
529 buf[0x00..0x04].copy_from_slice(&JBD2_MAGIC_NUMBER.to_be_bytes());
530 buf[0x04..0x08].copy_from_slice(&JBD2_SUPERBLOCK_V2.to_be_bytes());
531 buf[0x08..0x0C].copy_from_slice(&1u32.to_be_bytes()); buf[0x0C..0x10].copy_from_slice(&4096u32.to_be_bytes()); buf[0x10..0x14].copy_from_slice(&8192u32.to_be_bytes()); buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); buf[0x18..0x1C].copy_from_slice(&42u32.to_be_bytes()); buf[0x1C..0x20].copy_from_slice(&0u32.to_be_bytes()); buf[0x20..0x24].copy_from_slice(&0u32.to_be_bytes()); buf[0x24..0x28].copy_from_slice(&0u32.to_be_bytes()); buf[0x28..0x2C].copy_from_slice(
540 &(JbdIncompat::REVOKE.bits() | JbdIncompat::CSUM_V3.bits()).to_be_bytes(),
541 );
542 buf[0x2C..0x30].copy_from_slice(&0u32.to_be_bytes()); buf[0x30..0x40].copy_from_slice(&[0xAA; 16]);
544 buf[0x40..0x44].copy_from_slice(&1u32.to_be_bytes()); buf[0x50] = 4; buf[0xFC..0x100].copy_from_slice(&0xDEADBEEFu32.to_be_bytes());
547 buf
548 }
549
550 #[test]
551 fn parses_v2_superblock() {
552 let buf = make_v2_sb();
553 let sb = JournalSuperblock::parse(&buf).unwrap();
554 assert_eq!(sb.block_type, JBD2_SUPERBLOCK_V2);
555 assert_eq!(sb.block_size, 4096);
556 assert_eq!(sb.max_len, 8192);
557 assert_eq!(sb.first, 1);
558 assert_eq!(sb.sequence, 42);
559 assert!(sb.is_clean());
560 assert!(sb.has_revoke());
561 assert!(sb.uses_csum_v2_or_v3());
562 assert!(!sb.uses_64bit());
563 assert_eq!(sb.checksum_type, 4);
564 assert_eq!(sb.checksum, 0xDEADBEEF);
565 }
566
567 #[test]
568 fn rejects_bad_magic() {
569 let mut buf = make_v2_sb();
570 buf[0] = 0;
571 assert!(JournalSuperblock::parse(&buf).is_err());
572 }
573
574 #[test]
575 fn rejects_non_superblock_block_type() {
576 let mut buf = make_v2_sb();
577 buf[0x04..0x08].copy_from_slice(&JBD2_DESCRIPTOR_BLOCK.to_be_bytes());
578 assert!(JournalSuperblock::parse(&buf).is_err());
579 }
580
581 #[test]
582 fn v1_leaves_feature_fields_zero() {
583 let mut buf = make_v2_sb();
584 buf[0x04..0x08].copy_from_slice(&JBD2_SUPERBLOCK_V1.to_be_bytes());
585 let sb = JournalSuperblock::parse(&buf).unwrap();
587 assert_eq!(sb.feature_incompat, 0);
588 assert_eq!(sb.uuid, [0; 16]);
589 assert_eq!(sb.checksum, 0);
590 }
591
592 #[test]
593 fn dirty_sb_is_not_clean() {
594 let mut buf = make_v2_sb();
595 buf[0x1C..0x20].copy_from_slice(&100u32.to_be_bytes()); let sb = JournalSuperblock::parse(&buf).unwrap();
597 assert!(!sb.is_clean());
598 assert_eq!(sb.start, 100);
599 }
600}