use crate::error::{Error, Result};
use crate::fs::Filesystem;
use crate::inode::Inode;
pub const JBD2_MAGIC_NUMBER: u32 = 0xc03b_3998;
pub const JBD2_DESCRIPTOR_BLOCK: u32 = 1;
pub const JBD2_COMMIT_BLOCK: u32 = 2;
pub const JBD2_SUPERBLOCK_V1: u32 = 3;
pub const JBD2_SUPERBLOCK_V2: u32 = 4;
pub const JBD2_REVOKE_BLOCK: u32 = 5;
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JbdIncompat: u32 {
const REVOKE = 0x0000_0001;
const BIT64 = 0x0000_0002;
const ASYNC_COMMIT = 0x0000_0004;
const CSUM_V2 = 0x0000_0008;
const CSUM_V3 = 0x0000_0010;
const FAST_COMMIT = 0x0000_0020;
}
}
#[derive(Debug, Clone)]
pub struct JournalSuperblock {
pub block_type: u32,
pub header_sequence: u32,
pub block_size: u32,
pub max_len: u32,
pub first: u32,
pub sequence: u32,
pub start: u32,
pub errno: u32,
pub feature_compat: u32,
pub feature_incompat: u32,
pub feature_ro_compat: u32,
pub uuid: [u8; 16],
pub nr_users: u32,
pub checksum_type: u8,
pub num_fc_blocks: u32,
pub checksum: u32,
}
impl JournalSuperblock {
pub fn validate_plain_recovery(&self, block_size: u32, blocks_count: u64) -> Result<()> {
let supported = JbdIncompat::REVOKE.bits() | JbdIncompat::BIT64.bits();
if self.feature_compat != 0
|| self.feature_ro_compat != 0
|| self.feature_incompat & !supported != 0
{
return Err(Error::Unsupported("checked lifecycle supports plain JBD2 only; checksummed/async/fast-commit journals need separate qualification"));
}
if self.block_type != JBD2_SUPERBLOCK_V2
|| self.block_size != block_size
|| self.first != 1
|| self.max_len <= self.first
|| u64::from(self.max_len) > blocks_count
|| (self.start != 0 && (self.start < self.first || self.start >= self.max_len))
|| self.nr_users != 1
{
return Err(Error::Corrupt("unsupported internal journal geometry"));
}
if self.errno != 0 {
return Err(Error::Corrupt("journal records an outstanding error"));
}
Ok(())
}
pub fn is_clean(&self) -> bool {
self.start == 0
}
pub fn uses_csum_v3(&self) -> bool {
self.feature_incompat & JbdIncompat::CSUM_V3.bits() != 0
}
pub fn uses_csum_v2_or_v3(&self) -> bool {
self.feature_incompat & (JbdIncompat::CSUM_V2.bits() | JbdIncompat::CSUM_V3.bits()) != 0
}
pub fn uses_64bit(&self) -> bool {
self.feature_incompat & JbdIncompat::BIT64.bits() != 0
}
pub fn has_revoke(&self) -> bool {
self.feature_incompat & JbdIncompat::REVOKE.bits() != 0
}
pub fn unsupported_incompat(&self) -> u32 {
self.feature_incompat & !SUPPORTED_JBD_INCOMPAT
}
pub fn checksum_declaration_error(&self) -> Option<&'static str> {
let has = |bit: JbdIncompat| self.feature_incompat & bit.bits() != 0;
let per_block = [has(JbdIncompat::CSUM_V2), has(JbdIncompat::CSUM_V3)];
let commit_only = self.feature_compat & JBD2_FEATURE_COMPAT_CHECKSUM != 0;
match per_block {
[false, false] => None,
[true, true] => Some("journal declares both CSUM_V2 and CSUM_V3"),
_ if commit_only => Some("journal declares v1 checksums alongside CSUM_V2/V3"),
_ if self.checksum_type != JBD2_CRC32C_CHKSUM => {
Some("journal checksum type is not crc32c")
}
_ => None,
}
}
pub fn csum_seed(&self) -> u32 {
crate::checksum::linux_crc32c(!0, &self.uuid)
}
pub fn tag_bytes(&self) -> usize {
let per_block_scheme = if self.uses_csum_v3() {
TagChecksum::V3
} else if self.feature_incompat & JbdIncompat::CSUM_V2.bits() != 0 {
TagChecksum::V2
} else {
TagChecksum::None
};
match (per_block_scheme, self.uses_64bit()) {
(TagChecksum::None, false) => 8,
(TagChecksum::None, true) => 12,
(TagChecksum::V2, false) => 10,
(TagChecksum::V2, true) => 14,
(TagChecksum::V3, _) => 16,
}
}
}
#[derive(Clone, Copy)]
enum TagChecksum {
None,
V2,
V3,
}
pub const JBD2_FEATURE_COMPAT_CHECKSUM: u32 = 0x1;
pub const JBD2_CRC32C_CHKSUM: u8 = 4;
pub const SUPPORTED_JBD_INCOMPAT: u32 = JbdIncompat::REVOKE.bits()
| JbdIncompat::BIT64.bits()
| JbdIncompat::CSUM_V2.bits()
| JbdIncompat::CSUM_V3.bits();
pub const BLOCK_TAIL_BYTES: usize = 4;
pub const COMMIT_CHECKSUM_AT: usize = 16;
pub fn tag_checksum(seed: u32, sequence: u32, data: &[u8]) -> u32 {
let seq = crate::checksum::linux_crc32c(seed, &sequence.to_be_bytes());
crate::checksum::linux_crc32c(seq, data)
}
pub fn block_tail_checksum(seed: u32, block: &[u8]) -> u32 {
let body = block.len().saturating_sub(BLOCK_TAIL_BYTES);
let crc = crate::checksum::linux_crc32c(seed, &block[..body]);
crate::checksum::linux_crc32c(crc, &[0u8; BLOCK_TAIL_BYTES])
}
pub fn commit_block_checksum(seed: u32, block: &[u8]) -> u32 {
let at = COMMIT_CHECKSUM_AT;
let crc = crate::checksum::linux_crc32c(seed, &block[..at]);
let crc = crate::checksum::linux_crc32c(crc, &[0u8; 4]);
crate::checksum::linux_crc32c(crc, &block[at + 4..])
}
impl JournalSuperblock {
pub fn parse(raw: &[u8]) -> Result<Self> {
if raw.len() < 0x100 {
return Err(Error::Corrupt("jbd2 superblock buffer too small"));
}
let h_magic = u32::from_be_bytes(raw[0x00..0x04].try_into().unwrap());
if h_magic != JBD2_MAGIC_NUMBER {
return Err(Error::Corrupt("jbd2 superblock magic mismatch"));
}
let block_type = u32::from_be_bytes(raw[0x04..0x08].try_into().unwrap());
if block_type != JBD2_SUPERBLOCK_V1 && block_type != JBD2_SUPERBLOCK_V2 {
return Err(Error::Corrupt("jbd2 header is not a superblock block type"));
}
let header_sequence = u32::from_be_bytes(raw[0x08..0x0C].try_into().unwrap());
let block_size = u32::from_be_bytes(raw[0x0C..0x10].try_into().unwrap());
let max_len = u32::from_be_bytes(raw[0x10..0x14].try_into().unwrap());
let first = u32::from_be_bytes(raw[0x14..0x18].try_into().unwrap());
let sequence = u32::from_be_bytes(raw[0x18..0x1C].try_into().unwrap());
let start = u32::from_be_bytes(raw[0x1C..0x20].try_into().unwrap());
let errno = u32::from_be_bytes(raw[0x20..0x24].try_into().unwrap());
let mut out = Self {
block_type,
header_sequence,
block_size,
max_len,
first,
sequence,
start,
errno,
feature_compat: 0,
feature_incompat: 0,
feature_ro_compat: 0,
uuid: [0; 16],
nr_users: 0,
checksum_type: 0,
num_fc_blocks: 0,
checksum: 0,
};
if block_type == JBD2_SUPERBLOCK_V2 {
out.feature_compat = u32::from_be_bytes(raw[0x24..0x28].try_into().unwrap());
out.feature_incompat = u32::from_be_bytes(raw[0x28..0x2C].try_into().unwrap());
out.feature_ro_compat = u32::from_be_bytes(raw[0x2C..0x30].try_into().unwrap());
out.uuid.copy_from_slice(&raw[0x30..0x40]);
out.nr_users = u32::from_be_bytes(raw[0x40..0x44].try_into().unwrap());
out.checksum_type = raw[0x50];
out.num_fc_blocks = u32::from_be_bytes(raw[0x54..0x58].try_into().unwrap());
out.checksum = u32::from_be_bytes(raw[0xFC..0x100].try_into().unwrap());
}
Ok(out)
}
}
pub fn journal_block_to_physical(
fs: &Filesystem,
journal_inode: &Inode,
journal_block: u64,
) -> Result<Option<u64>> {
crate::indirect::map_logical_any(
&journal_inode.block,
journal_inode.flags,
fs.dev.as_ref(),
fs.sb.block_size(),
journal_block,
)
}
pub fn read_superblock(fs: &Filesystem) -> Result<Option<JournalSuperblock>> {
let jino_num = fs.sb.journal_inode;
if jino_num == 0 {
return Ok(None);
}
let raw = fs.read_inode_raw(jino_num)?;
let jinode = Inode::parse(&raw)?;
let phys = journal_block_to_physical(fs, &jinode, 0)?
.ok_or(Error::Corrupt("journal block 0 unmapped (sparse journal?)"))?;
let block_size = fs.sb.block_size() as u64;
let mut buf = vec![0u8; block_size as usize];
fs.dev.read_at(phys * block_size, &mut buf)?;
Ok(Some(JournalSuperblock::parse(&buf)?))
}
#[cfg(test)]
mod tests {
use super::*;
fn jsb_with(incompat: u32) -> JournalSuperblock {
let mut jsb = JournalSuperblock::parse(&make_v2_sb()).unwrap();
jsb.feature_incompat = incompat;
jsb
}
#[test]
fn tag_sizes_are_the_measured_table() {
let b64 = JbdIncompat::BIT64.bits();
let v2 = JbdIncompat::CSUM_V2.bits();
let v3 = JbdIncompat::CSUM_V3.bits();
for (incompat, bytes) in [
(0, 8),
(b64, 12),
(v2, 10),
(v2 | b64, 14),
(v3, 16),
(v3 | b64, 16),
] {
assert_eq!(
jsb_with(incompat).tag_bytes(),
bytes,
"incompat {incompat:#x}"
);
}
}
#[test]
fn checksum_declarations_follow_the_observed_table() {
let v2 = JbdIncompat::CSUM_V2.bits();
let v3 = JbdIncompat::CSUM_V3.bits();
let v1 = JBD2_FEATURE_COMPAT_CHECKSUM;
let with = |compat: u32, incompat: u32, kind: u8| {
let mut jsb = jsb_with(incompat);
jsb.feature_compat = compat;
jsb.checksum_type = kind;
jsb.checksum_declaration_error().is_none()
};
let accepted = [
(0, 0, 0),
(0, 0, 4),
(v1, 0, 0),
(v1, 0, 1),
(v1, 0, 4),
(0, v2, 4),
(0, v3, 4),
];
let refused = [
(0, v2 | v3, 4),
(v1, v2, 4),
(v1, v3, 4),
(0, v2, 1),
(0, v3, 1),
(0, v3, 2),
(0, v3, 0),
];
for (compat, incompat, kind) in accepted {
assert!(
with(compat, incompat, kind),
"{compat:#x}/{incompat:#x}/{kind}"
);
}
for (compat, incompat, kind) in refused {
assert!(
!with(compat, incompat, kind),
"{compat:#x}/{incompat:#x}/{kind}"
);
}
}
#[test]
fn async_commit_and_fast_commit_are_unsupported() {
let known =
JbdIncompat::REVOKE.bits() | JbdIncompat::BIT64.bits() | JbdIncompat::CSUM_V3.bits();
assert_eq!(jsb_with(known).unsupported_incompat(), 0);
for bit in [
JbdIncompat::ASYNC_COMMIT.bits(),
JbdIncompat::FAST_COMMIT.bits(),
0x8000_0000,
] {
assert_eq!(jsb_with(known | bit).unsupported_incompat(), bit);
}
}
#[test]
fn block_checksums_skip_only_their_own_field() {
let seed = jsb_with(0).csum_seed();
let mut block = vec![0x5Au8; 4096];
let tail = block_tail_checksum(seed, &block);
block[4092..].fill(0xFF);
assert_eq!(block_tail_checksum(seed, &block), tail);
let commit = commit_block_checksum(seed, &block);
block[COMMIT_CHECKSUM_AT..COMMIT_CHECKSUM_AT + 4].fill(0x00);
assert_eq!(commit_block_checksum(seed, &block), commit);
block[4091] ^= 1;
assert_ne!(block_tail_checksum(seed, &block), tail);
assert_ne!(commit_block_checksum(seed, &block), commit);
assert_ne!(tag_checksum(seed, 1, &block), tag_checksum(seed, 2, &block));
}
fn make_v2_sb() -> Vec<u8> {
let mut buf = vec![0u8; 1024];
buf[0x00..0x04].copy_from_slice(&JBD2_MAGIC_NUMBER.to_be_bytes());
buf[0x04..0x08].copy_from_slice(&JBD2_SUPERBLOCK_V2.to_be_bytes());
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(
&(JbdIncompat::REVOKE.bits() | JbdIncompat::CSUM_V3.bits()).to_be_bytes(),
);
buf[0x2C..0x30].copy_from_slice(&0u32.to_be_bytes()); buf[0x30..0x40].copy_from_slice(&[0xAA; 16]);
buf[0x40..0x44].copy_from_slice(&1u32.to_be_bytes()); buf[0x50] = 4; buf[0xFC..0x100].copy_from_slice(&0xDEADBEEFu32.to_be_bytes());
buf
}
#[test]
fn parses_v2_superblock() {
let buf = make_v2_sb();
let sb = JournalSuperblock::parse(&buf).unwrap();
assert_eq!(sb.block_type, JBD2_SUPERBLOCK_V2);
assert_eq!(sb.block_size, 4096);
assert_eq!(sb.max_len, 8192);
assert_eq!(sb.first, 1);
assert_eq!(sb.sequence, 42);
assert!(sb.is_clean());
assert!(sb.has_revoke());
assert!(sb.uses_csum_v2_or_v3());
assert!(!sb.uses_64bit());
assert_eq!(sb.checksum_type, 4);
assert_eq!(sb.checksum, 0xDEADBEEF);
}
#[test]
fn rejects_bad_magic() {
let mut buf = make_v2_sb();
buf[0] = 0;
assert!(JournalSuperblock::parse(&buf).is_err());
}
#[test]
fn rejects_non_superblock_block_type() {
let mut buf = make_v2_sb();
buf[0x04..0x08].copy_from_slice(&JBD2_DESCRIPTOR_BLOCK.to_be_bytes());
assert!(JournalSuperblock::parse(&buf).is_err());
}
#[test]
fn v1_leaves_feature_fields_zero() {
let mut buf = make_v2_sb();
buf[0x04..0x08].copy_from_slice(&JBD2_SUPERBLOCK_V1.to_be_bytes());
let sb = JournalSuperblock::parse(&buf).unwrap();
assert_eq!(sb.feature_incompat, 0);
assert_eq!(sb.uuid, [0; 16]);
assert_eq!(sb.checksum, 0);
}
#[test]
fn dirty_sb_is_not_clean() {
let mut buf = make_v2_sb();
buf[0x1C..0x20].copy_from_slice(&100u32.to_be_bytes()); let sb = JournalSuperblock::parse(&buf).unwrap();
assert!(!sb.is_clean());
assert_eq!(sb.start, 100);
}
}