Skip to main content

ext4fs/ondisk/
journal.rs

1#![forbid(unsafe_code)]
2use crate::error::{Ext4Error, Result};
3
4pub const JOURNAL_MAGIC: u32 = 0xC03B_3998;
5
6// JBD2 feature incompat flags
7pub const JBD2_FEATURE_INCOMPAT_64BIT: u32 = 0x0000_0002;
8pub const JBD2_FEATURE_INCOMPAT_CSUM_V2: u32 = 0x0000_0008;
9pub const JBD2_FEATURE_INCOMPAT_CSUM_V3: u32 = 0x0000_0010;
10
11// ── helpers ──────────────────────────────────────────────────────────────────
12//
13// Big-endian integer fields are read via the fleet's audited `safe_read` crate
14// (bounds-checked, panic-free) rather than a hand-rolled `from_be_bytes(..)
15// .try_into().unwrap()` helper. Every call below is preceded by a `check_len`
16// guard, so the reads are in-bounds and yield the real value.
17
18fn check_len(buf: &[u8], need: usize, structure: &'static str) -> Result<()> {
19    if buf.len() < need {
20        Err(Ext4Error::TooShort {
21            structure,
22            expected: need,
23            found: buf.len(),
24        })
25    } else {
26        Ok(())
27    }
28}
29
30// ── JournalBlockType ─────────────────────────────────────────────────────────
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum JournalBlockType {
34    Descriptor,
35    Commit,
36    SuperblockV1,
37    SuperblockV2,
38    Revoke,
39    Unknown(u32),
40}
41
42impl From<u32> for JournalBlockType {
43    fn from(v: u32) -> Self {
44        match v {
45            1 => Self::Descriptor,
46            2 => Self::Commit,
47            3 => Self::SuperblockV1,
48            4 => Self::SuperblockV2,
49            5 => Self::Revoke,
50            other => Self::Unknown(other),
51        }
52    }
53}
54
55// ── JournalHeader ─────────────────────────────────────────────────────────────
56
57/// Common 12-byte header present at the start of every JBD2 block.
58#[derive(Debug, Clone)]
59pub struct JournalHeader {
60    pub magic: u32,
61    pub block_type: JournalBlockType,
62    pub sequence: u32,
63}
64
65impl JournalHeader {
66    pub fn parse(buf: &[u8]) -> Result<Self> {
67        check_len(buf, 12, "JournalHeader")?;
68        let magic = safe_read::be_u32(buf, 0);
69        if magic != JOURNAL_MAGIC {
70            return Err(Ext4Error::JournalCorrupt(format!(
71                "bad journal magic: 0x{magic:08X}"
72            )));
73        }
74        Ok(Self {
75            magic,
76            block_type: JournalBlockType::from(safe_read::be_u32(buf, 4)),
77            sequence: safe_read::be_u32(buf, 8),
78        })
79    }
80}
81
82// ── JournalSuperblock ─────────────────────────────────────────────────────────
83
84/// JBD2 superblock (journal block 0).
85///
86/// Offsets (all big-endian):
87///   0x00 header (12 bytes)
88///   0x0C `block_size`
89///   0x10 `max_len`
90///   0x14 first
91///   0x18 sequence
92///   0x1C start
93///   0x20 errno
94///   0x24 `feature_compat`
95///   0x28 `feature_incompat`
96///   0x2C `feature_ro_compat`
97///   0x30 uuid (16 bytes)
98///   0x40 `nr_users`
99///   0x44 dynsuper
100///   0x48 `max_transaction`
101///   0x4C `max_trans_data`
102///   0x50 `checksum_type` (1 byte)
103///   0xFC checksum (u32)
104#[derive(Debug, Clone)]
105pub struct JournalSuperblock {
106    pub header: JournalHeader,
107    pub block_size: u32,
108    pub max_len: u32,
109    pub first: u32,
110    pub sequence: u32,
111    pub start: u32,
112    pub errno: u32,
113    pub feature_compat: u32,
114    pub feature_incompat: u32,
115    pub feature_ro_compat: u32,
116    pub uuid: [u8; 16],
117    pub nr_users: u32,
118    pub checksum_type: u8,
119    pub checksum: u32,
120}
121
122impl JournalSuperblock {
123    pub fn parse(buf: &[u8]) -> Result<Self> {
124        check_len(buf, 0x100, "JournalSuperblock")?;
125        let header = JournalHeader::parse(buf)?;
126        let mut uuid = [0u8; 16];
127        uuid.copy_from_slice(&buf[0x30..0x40]);
128        Ok(Self {
129            header,
130            block_size: safe_read::be_u32(buf, 0x0C),
131            max_len: safe_read::be_u32(buf, 0x10),
132            first: safe_read::be_u32(buf, 0x14),
133            sequence: safe_read::be_u32(buf, 0x18),
134            start: safe_read::be_u32(buf, 0x1C),
135            errno: safe_read::be_u32(buf, 0x20),
136            feature_compat: safe_read::be_u32(buf, 0x24),
137            feature_incompat: safe_read::be_u32(buf, 0x28),
138            feature_ro_compat: safe_read::be_u32(buf, 0x2C),
139            uuid,
140            nr_users: safe_read::be_u32(buf, 0x40),
141            checksum_type: buf[0x50],
142            checksum: safe_read::be_u32(buf, 0xFC),
143        })
144    }
145
146    pub fn is_64bit(&self) -> bool {
147        self.feature_incompat & JBD2_FEATURE_INCOMPAT_64BIT != 0
148    }
149
150    pub fn has_csum_v3(&self) -> bool {
151        self.feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V3 != 0
152    }
153
154    pub fn has_csum_v2(&self) -> bool {
155        self.feature_incompat & JBD2_FEATURE_INCOMPAT_CSUM_V2 != 0
156    }
157}
158
159// ── JournalBlockTag ───────────────────────────────────────────────────────────
160
161/// A block tag inside a descriptor block.
162///
163/// v3 layout (16 bytes, `same_uuid=true`) or 32 bytes (`same_uuid=false`):
164///   0x00  `blocknr_lo` (u32)
165///   0x04  flags (u32)  bit0=escaped, `bit1=same_uuid`, `bit3=last_tag`
166///   0x08  `blocknr_hi` (u32, only meaningful when `is_64bit`)
167///   0x0C  checksum (u32)
168///   0x10  uuid (16 bytes, present only when `same_uuid=false`)
169#[derive(Debug, Clone)]
170pub struct JournalBlockTag {
171    pub blocknr: u64,
172    pub checksum: u32,
173    pub escaped: bool,
174    pub same_uuid: bool,
175    pub last_tag: bool,
176    /// Byte size consumed by this tag in the descriptor block.
177    pub tag_size: usize,
178}
179
180impl JournalBlockTag {
181    /// Parse a v3 block tag. Reads the 16-byte tag base (`blocknr_lo`, `flags`,
182    /// `blocknr_hi`, `checksum`); a shorter buffer is an invalid structure and
183    /// is rejected loudly rather than read as zeroes.
184    pub fn parse_v3(buf: &[u8], is_64bit: bool) -> Result<Self> {
185        check_len(buf, 16, "JournalBlockTag")?;
186        let blocknr_lo = u64::from(safe_read::be_u32(buf, 0x00));
187        let flags = safe_read::be_u32(buf, 0x04);
188        let escaped = flags & 0x01 != 0;
189        let same_uuid = flags & 0x02 != 0;
190        let last_tag = flags & 0x08 != 0;
191        let blocknr_hi = if is_64bit {
192            u64::from(safe_read::be_u32(buf, 0x08))
193        } else {
194            0
195        };
196        let blocknr = (blocknr_hi << 32) | blocknr_lo;
197        let checksum = safe_read::be_u32(buf, 0x0C);
198        // 16-byte base + 16-byte UUID when uuid is not shared
199        let tag_size = if same_uuid { 16 } else { 32 };
200        Ok(Self {
201            blocknr,
202            checksum,
203            escaped,
204            same_uuid,
205            last_tag,
206            tag_size,
207        })
208    }
209}
210
211// ── JournalCommit ─────────────────────────────────────────────────────────────
212
213/// Commit block — marks end of a complete transaction.
214///
215/// Offsets (beyond the 12-byte header):
216///   0x0C  `checksum_type` (u8)
217///   0x0D  `checksum_size` (u8)
218///   0x30  `commit_seconds` (i64 BE)
219///   0x38  `commit_nanoseconds` (u32 BE)
220#[derive(Debug, Clone)]
221pub struct JournalCommit {
222    pub sequence: u32,
223    pub commit_seconds: i64,
224    pub commit_nanoseconds: u32,
225}
226
227impl JournalCommit {
228    pub fn parse(buf: &[u8]) -> Result<Self> {
229        check_len(buf, 0x3C, "JournalCommit")?;
230        let header = JournalHeader::parse(buf)?;
231        Ok(Self {
232            sequence: header.sequence,
233            commit_seconds: safe_read::be_u64(buf, 0x30) as i64,
234            commit_nanoseconds: safe_read::be_u32(buf, 0x38),
235        })
236    }
237}
238
239// ── JournalRevoke ─────────────────────────────────────────────────────────────
240
241/// Revoke block — lists blocks whose old versions must not be replayed.
242///
243/// Layout:
244///   0x00  header (12 bytes)
245///   0x0C  count (u32) — total byte length of revoke data (including header)
246///   0x10  revoked block numbers (u32 or u64 each, depending on 64bit flag)
247#[derive(Debug, Clone)]
248pub struct JournalRevoke {
249    pub sequence: u32,
250    pub revoked_blocks: Vec<u64>,
251}
252
253impl JournalRevoke {
254    pub fn parse(buf: &[u8], is_64bit: bool) -> Result<Self> {
255        check_len(buf, 16, "JournalRevoke")?;
256        let header = JournalHeader::parse(buf)?;
257        let byte_count = safe_read::be_u32(buf, 12) as usize;
258        check_len(buf, byte_count, "JournalRevoke data")?;
259        let entry_size = if is_64bit { 8usize } else { 4 };
260        let data = &buf[16..byte_count];
261        let mut revoked_blocks = Vec::with_capacity(data.len() / entry_size);
262        let mut off = 0;
263        while off + entry_size <= data.len() {
264            let blk = if is_64bit {
265                safe_read::be_u64(data, off)
266            } else {
267                u64::from(safe_read::be_u32(data, off))
268            };
269            revoked_blocks.push(blk);
270            off += entry_size;
271        }
272        Ok(Self {
273            sequence: header.sequence,
274            revoked_blocks,
275        })
276    }
277}
278
279// ── Tests ─────────────────────────────────────────────────────────────────────
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn parse_journal_header() {
287        let mut buf = vec![0u8; 12];
288        buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
289        buf[4..8].copy_from_slice(&1u32.to_be_bytes()); // descriptor
290        buf[8..12].copy_from_slice(&42u32.to_be_bytes());
291        let hdr = JournalHeader::parse(&buf).unwrap();
292        assert_eq!(hdr.magic, JOURNAL_MAGIC);
293        assert_eq!(hdr.block_type, JournalBlockType::Descriptor);
294        assert_eq!(hdr.sequence, 42);
295    }
296
297    #[test]
298    fn reject_bad_journal_magic() {
299        let buf = vec![0u8; 12];
300        let err = JournalHeader::parse(&buf).unwrap_err();
301        assert!(matches!(err, crate::error::Ext4Error::JournalCorrupt(_)));
302    }
303
304    #[test]
305    fn parse_journal_superblock() {
306        let mut buf = vec![0u8; 1024];
307        buf[0..4].copy_from_slice(&JOURNAL_MAGIC.to_be_bytes());
308        buf[4..8].copy_from_slice(&4u32.to_be_bytes()); // sb_v2
309        buf[8..12].copy_from_slice(&1u32.to_be_bytes());
310        buf[0x0C..0x10].copy_from_slice(&4096u32.to_be_bytes()); // block_size
311        buf[0x10..0x14].copy_from_slice(&1024u32.to_be_bytes()); // max_len
312        buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); // first
313        let jsb = JournalSuperblock::parse(&buf).unwrap();
314        assert_eq!(jsb.block_size, 4096);
315        assert_eq!(jsb.max_len, 1024);
316        assert_eq!(jsb.first, 1);
317    }
318
319    #[test]
320    fn parse_v3_rejects_truncated_tag() {
321        // A v3 tag reads up to 16 bytes; a shorter buffer is an invalid structure
322        // and must fail loud (Err) rather than panic or silently read zeroes.
323        let short = [0u8; 8];
324        assert!(JournalBlockTag::parse_v3(&short, false).is_err());
325    }
326
327    #[test]
328    fn parse_block_tag_v3() {
329        let mut buf = vec![0u8; 16];
330        buf[0..4].copy_from_slice(&500u32.to_be_bytes()); // blocknr
331        buf[4..8].copy_from_slice(&0x08u32.to_be_bytes()); // flags: LAST_TAG
332        buf[12..16].copy_from_slice(&0xDEAD_BEEFu32.to_be_bytes());
333        let tag = JournalBlockTag::parse_v3(&buf, false).unwrap();
334        assert_eq!(tag.blocknr, 500);
335        assert!(tag.last_tag);
336        assert!(!tag.escaped);
337    }
338
339    #[test]
340    fn journal_superblock_feature_flags() {
341        let mut buf = vec![0u8; 1024];
342        buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
343        buf[4..8].copy_from_slice(&4u32.to_be_bytes()); // SuperblockV2
344        buf[8..12].copy_from_slice(&1u32.to_be_bytes()); // sequence
345        buf[0x0C..0x10].copy_from_slice(&1024u32.to_be_bytes()); // block_size
346        buf[0x10..0x14].copy_from_slice(&100u32.to_be_bytes()); // max_len
347        buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); // first_block
348                                                              // Set feature_incompat with 64BIT and CSUM_V3
349        let features: u32 = JBD2_FEATURE_INCOMPAT_64BIT | JBD2_FEATURE_INCOMPAT_CSUM_V3;
350        buf[0x28..0x2C].copy_from_slice(&features.to_be_bytes());
351
352        let jsb = JournalSuperblock::parse(&buf).unwrap();
353        assert!(jsb.is_64bit());
354        assert!(jsb.has_csum_v3());
355        // CSUM_V2 flag is NOT set
356        assert!(!jsb.has_csum_v2());
357    }
358
359    #[test]
360    fn journal_superblock_csum_v2_only() {
361        let mut buf = vec![0u8; 1024];
362        buf[0..4].copy_from_slice(&0xC03B_3998u32.to_be_bytes());
363        buf[4..8].copy_from_slice(&4u32.to_be_bytes()); // SuperblockV2
364        buf[8..12].copy_from_slice(&1u32.to_be_bytes());
365        buf[0x0C..0x10].copy_from_slice(&1024u32.to_be_bytes());
366        buf[0x10..0x14].copy_from_slice(&100u32.to_be_bytes());
367        buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes());
368        let features: u32 = JBD2_FEATURE_INCOMPAT_CSUM_V2; // CSUM_V2 only
369        buf[0x28..0x2C].copy_from_slice(&features.to_be_bytes());
370
371        let jsb = JournalSuperblock::parse(&buf).unwrap();
372        assert!(!jsb.is_64bit());
373        assert!(!jsb.has_csum_v3());
374        assert!(jsb.has_csum_v2());
375    }
376
377    #[test]
378    fn parse_commit_block() {
379        let mut buf = vec![0u8; 64];
380        buf[0..4].copy_from_slice(&JOURNAL_MAGIC.to_be_bytes());
381        buf[4..8].copy_from_slice(&2u32.to_be_bytes()); // commit
382        buf[8..12].copy_from_slice(&99u32.to_be_bytes());
383        buf[0x30..0x38].copy_from_slice(&1_700_000_000u64.to_be_bytes());
384        buf[0x38..0x3C].copy_from_slice(&500_000_000u32.to_be_bytes());
385        let commit = JournalCommit::parse(&buf).unwrap();
386        assert_eq!(commit.sequence, 99);
387        assert_eq!(commit.commit_seconds, 1_700_000_000);
388        assert_eq!(commit.commit_nanoseconds, 500_000_000);
389    }
390}