Skip to main content

ufs/
superblock.rs

1//! UFS/FFS superblock (`struct fs`) parse, geometry, and version/endian detect.
2//!
3//! The primary superblock sits at a version-fixed byte offset from the
4//! filesystem start: **UFS1 at 8192** (magic `0x00011954`), **UFS2 at 65536**
5//! (magic `0x19540119`). Field offsets follow `struct fs` in the FreeBSD kernel
6//! header `sys/ufs/ffs/fs.h` (`CTASSERT(sizeof(struct fs) == 1376)`); `fs_magic`
7//! is the last field, at offset 1372.
8
9use crate::bytes::{u8_at, Endian};
10use crate::error::UfsError;
11
12/// UFS1 superblock magic (`FS_UFS1_MAGIC`), read at `fs_magic` (offset 1372).
13pub const FS_UFS1_MAGIC: u32 = 0x0001_1954;
14
15/// UFS2 superblock magic (`FS_UFS2_MAGIC`), read at `fs_magic` (offset 1372).
16pub const FS_UFS2_MAGIC: u32 = 0x1954_0119;
17
18/// Byte offset of the primary UFS1 superblock from the filesystem start
19/// (`SBLOCK_UFS1`).
20pub const SBLOCK_UFS1: usize = 8192;
21
22/// Byte offset of the primary UFS2 superblock from the filesystem start
23/// (`SBLOCK_UFS2`).
24pub const SBLOCK_UFS2: usize = 65536;
25
26/// The UFS root inode number (`UFS_ROOTINO`).
27pub const UFS_ROOTINO: u64 = 2;
28
29/// `fs_magic` is the final field of the 1376-byte `struct fs`.
30const FS_MAGIC_OFF: usize = 1372;
31
32/// Minimum bytes required to parse every field this reader extracts (through
33/// `fs_magic` at offset 1372, +4).
34const SB_MIN_LEN: usize = 1376;
35
36// ── verified `struct fs` field offsets (see docs/RESEARCH.md §1) ─────────────
37const OFF_SBLKNO: usize = 8;
38const OFF_CBLKNO: usize = 12;
39const OFF_IBLKNO: usize = 16;
40const OFF_DBLKNO: usize = 20;
41const OFF_OLD_TIME: usize = 32;
42const OFF_OLD_SIZE: usize = 36;
43const OFF_OLD_DSIZE: usize = 40;
44const OFF_NCG: usize = 44;
45const OFF_BSIZE: usize = 48;
46const OFF_FSIZE: usize = 52;
47const OFF_FRAG: usize = 56;
48const OFF_BSHIFT: usize = 80;
49const OFF_FSHIFT: usize = 84;
50const OFF_FRAGSHIFT: usize = 96;
51const OFF_FSBTODB: usize = 100;
52const OFF_SBSIZE: usize = 104;
53const OFF_NINDIR: usize = 116;
54const OFF_INOPB: usize = 120;
55const OFF_IPG: usize = 184;
56const OFF_FPG: usize = 188;
57const OFF_SIZE: usize = 1080;
58const OFF_DSIZE: usize = 1088;
59const OFF_CSADDR: usize = 1096;
60const OFF_SBLOCKLOC: usize = 1000;
61const OFF_MAXSYMLINKLEN: usize = 1320;
62
63/// The on-disk UFS version, resolved from the superblock magic.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum UfsVersion {
66    /// UFS1 — 4.4BSD/FreeBSD legacy: 128-byte inodes, 32-bit block pointers,
67    /// primary superblock at byte 8192, magic `0x00011954`.
68    Ufs1,
69    /// UFS2 — FreeBSD 5+: 256-byte inodes, 64-bit block pointers, birthtime,
70    /// primary superblock at byte 65536, magic `0x19540119`.
71    Ufs2,
72}
73
74/// Parsed UFS superblock — geometry and addressing fields the cylinder-group
75/// and inode decode (P1) need.
76///
77/// This carries the subset of `struct fs` the reader currently uses; it is
78/// `#[non_exhaustive]` so later phases add fields without a breaking change.
79#[derive(Debug, Clone, PartialEq, Eq)]
80#[non_exhaustive]
81pub struct Superblock {
82    /// On-disk version (UFS1 vs UFS2), from the magic.
83    pub version: UfsVersion,
84    /// On-disk byte order, from the magic.
85    pub endian: Endian,
86    /// `fs_sblkno` — superblock offset within a cylinder group (frags).
87    pub sblkno: i32,
88    /// `fs_cblkno` — cylinder-group-block offset within a cg (frags).
89    pub cblkno: i32,
90    /// `fs_iblkno` — inode-blocks offset within a cg (frags).
91    pub iblkno: i32,
92    /// `fs_dblkno` — first data-block offset within a cg (frags).
93    pub dblkno: i32,
94    /// `fs_ncg` — number of cylinder groups.
95    pub ncg: u32,
96    /// `fs_bsize` — basic block size in bytes.
97    pub bsize: i32,
98    /// `fs_fsize` — fragment size in bytes.
99    pub fsize: i32,
100    /// `fs_frag` — number of fragments in a block (`bsize / fsize`).
101    pub frag: i32,
102    /// `fs_bshift` — `log2(bsize)`.
103    pub bshift: i32,
104    /// `fs_fshift` — `log2(fsize)`.
105    pub fshift: i32,
106    /// `fs_fragshift` — `log2(frag)`.
107    pub fragshift: i32,
108    /// `fs_fsbtodb` — fsblock→disk-block shift constant.
109    pub fsbtodb: i32,
110    /// `fs_sbsize` — actual on-disk superblock size in bytes.
111    pub sbsize: i32,
112    /// `fs_nindir` — pointers per indirect block.
113    pub nindir: i32,
114    /// `fs_inopb` — inodes per block.
115    pub inopb: u32,
116    /// `fs_ipg` — inodes per cylinder group.
117    pub ipg: i32,
118    /// `fs_fpg` — fragments per cylinder group.
119    pub fpg: i32,
120    /// Total size of the filesystem in fragments (`fs_size` on UFS2, the 32-bit
121    /// `fs_old_size` on UFS1).
122    pub size: i64,
123    /// Data-region size in fragments (`fs_dsize` / `fs_old_dsize`).
124    pub dsize: i64,
125    /// `fs_csaddr` — fragment address of the cylinder-summary area (UFS2).
126    pub csaddr: i64,
127    /// `fs_sblockloc` — the byte offset at which this superblock records itself
128    /// (self-locating; `SBLOCK_UFS2` on UFS2). `0` on UFS1 (field absent).
129    pub sblockloc: i64,
130    /// `fs_maxsymlinklen` — inline (fast) symlink length threshold.
131    pub maxsymlinklen: i32,
132}
133
134impl Superblock {
135    /// Parse a superblock from the start of `data` (i.e. `data` begins at the
136    /// superblock, not at the filesystem start — callers slice from
137    /// `SBLOCK_UFS1`/`SBLOCK_UFS2`).
138    ///
139    /// Detects the UFS version and byte order from `fs_magic` (offset 1372) by
140    /// trying both interpretations against both known magics, then decodes the
141    /// geometry in the resolved byte order.
142    ///
143    /// # Errors
144    ///
145    /// - [`UfsError::BadMagic`] if the value at offset 1372 is neither UFS
146    ///   magic in either byte order — the offending bytes are carried.
147    /// - [`UfsError::Truncated`] if `data` is shorter than the fields read.
148    /// - [`UfsError::ImpossibleGeometry`] if a geometry field is out of range.
149    pub fn parse(data: &[u8]) -> Result<Self, UfsError> {
150        // Length-check first: `fs_magic` is the final field (offset 1372, struct
151        // end 1376 == SB_MIN_LEN), so a buffer that cannot even hold the whole
152        // superblock cannot carry a matching magic — validate the length loudly
153        // rather than let a partial magic read masquerade as a bad image.
154        if data.len() < SB_MIN_LEN {
155            return Err(UfsError::Truncated {
156                structure: "superblock",
157                need: SB_MIN_LEN,
158                have: data.len(),
159            });
160        }
161
162        // Detect version + byte order from `fs_magic`, trying both byte orders
163        // against both known magics. Read via bounds-checked helpers so a hostile
164        // buffer never panics; a wrong image fails loud carrying the offending
165        // bytes (fail-loud with the value).
166        let bytes = [
167            u8_at(data, FS_MAGIC_OFF),
168            u8_at(data, FS_MAGIC_OFF + 1),
169            u8_at(data, FS_MAGIC_OFF + 2),
170            u8_at(data, FS_MAGIC_OFF + 3),
171        ];
172        let le = u32::from_le_bytes(bytes);
173        let be = u32::from_be_bytes(bytes);
174        let (version, endian) = match (le, be) {
175            (FS_UFS1_MAGIC, _) => (UfsVersion::Ufs1, Endian::Little),
176            (FS_UFS2_MAGIC, _) => (UfsVersion::Ufs2, Endian::Little),
177            (_, FS_UFS1_MAGIC) => (UfsVersion::Ufs1, Endian::Big),
178            (_, FS_UFS2_MAGIC) => (UfsVersion::Ufs2, Endian::Big),
179            _ => {
180                return Err(UfsError::BadMagic {
181                    offset: FS_MAGIC_OFF,
182                    bytes,
183                    le,
184                    be,
185                })
186            }
187        };
188
189        // UFS1 stores size/time in the 32-bit `fs_old_*` fields; UFS2 in the
190        // 64-bit fields. Branch on the detected version.
191        let (size, dsize) = match version {
192            UfsVersion::Ufs1 => (
193                i64::from(endian.i32(data, OFF_OLD_SIZE)),
194                i64::from(endian.i32(data, OFF_OLD_DSIZE)),
195            ),
196            UfsVersion::Ufs2 => (endian.i64(data, OFF_SIZE), endian.i64(data, OFF_DSIZE)),
197        };
198        let sblockloc = match version {
199            UfsVersion::Ufs1 => 0,
200            UfsVersion::Ufs2 => endian.i64(data, OFF_SBLOCKLOC),
201        };
202        // Silence the unused-constant lints for fields reserved for later phases
203        // by touching them here; they document the verified offsets in-place.
204        let _ = (OFF_OLD_TIME, OFF_CSADDR);
205
206        let sb = Self {
207            version,
208            endian,
209            sblkno: endian.i32(data, OFF_SBLKNO),
210            cblkno: endian.i32(data, OFF_CBLKNO),
211            iblkno: endian.i32(data, OFF_IBLKNO),
212            dblkno: endian.i32(data, OFF_DBLKNO),
213            ncg: endian.u32(data, OFF_NCG),
214            bsize: endian.i32(data, OFF_BSIZE),
215            fsize: endian.i32(data, OFF_FSIZE),
216            frag: endian.i32(data, OFF_FRAG),
217            bshift: endian.i32(data, OFF_BSHIFT),
218            fshift: endian.i32(data, OFF_FSHIFT),
219            fragshift: endian.i32(data, OFF_FRAGSHIFT),
220            fsbtodb: endian.i32(data, OFF_FSBTODB),
221            sbsize: endian.i32(data, OFF_SBSIZE),
222            nindir: endian.i32(data, OFF_NINDIR),
223            inopb: endian.u32(data, OFF_INOPB),
224            ipg: endian.i32(data, OFF_IPG),
225            fpg: endian.i32(data, OFF_FPG),
226            size,
227            dsize,
228            csaddr: endian.i64(data, OFF_CSADDR),
229            sblockloc,
230            maxsymlinklen: endian.i32(data, OFF_MAXSYMLINKLEN),
231        };
232
233        sb.validate_geometry()?;
234        Ok(sb)
235    }
236
237    /// Reject absurd geometry (corruption / allocation-bomb) with the offending
238    /// value, so downstream address math never overflows or over-allocates.
239    fn validate_geometry(&self) -> Result<(), UfsError> {
240        // Block size must be a sane power-of-two frame; UFS allows 4 KiB..64 KiB.
241        if self.bsize <= 0 || self.bsize > 65536 {
242            return Err(UfsError::ImpossibleGeometry {
243                field: "fs_bsize",
244                value: self.bsize as u64,
245                limit: 65536,
246            });
247        }
248        if self.fsize <= 0 || self.fsize > self.bsize {
249            return Err(UfsError::ImpossibleGeometry {
250                field: "fs_fsize",
251                value: self.fsize as u64,
252                limit: self.bsize as u64,
253            });
254        }
255        // A single UFS volume never has this many cylinder groups; cap to guard
256        // any per-cg iteration against an allocation-bomb count.
257        const MAX_NCG: u64 = 1 << 24;
258        if u64::from(self.ncg) > MAX_NCG {
259            return Err(UfsError::ImpossibleGeometry {
260                field: "fs_ncg",
261                value: u64::from(self.ncg),
262                limit: MAX_NCG,
263            });
264        }
265        Ok(())
266    }
267
268    /// The inode size in bytes for this version: 128 (UFS1) or 256 (UFS2).
269    #[must_use]
270    pub fn inode_size(&self) -> u32 {
271        match self.version {
272            UfsVersion::Ufs1 => 128,
273            UfsVersion::Ufs2 => 256,
274        }
275    }
276
277    /// The primary-superblock byte offset for this version from the filesystem
278    /// start (`SBLOCK_UFS1` / `SBLOCK_UFS2`).
279    #[must_use]
280    pub fn primary_offset(&self) -> usize {
281        match self.version {
282            UfsVersion::Ufs1 => SBLOCK_UFS1,
283            UfsVersion::Ufs2 => SBLOCK_UFS2,
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    /// Build a synthetic 1376-byte superblock with the given magic bytes (in the
293    /// given byte order) and a minimal valid geometry, so version/endian detect
294    /// and geometry decode can be exercised without a real image.
295    fn synthetic(magic: u32, endian: Endian, ufs2: bool) -> Vec<u8> {
296        let mut d = vec![0u8; SB_MIN_LEN];
297        let wr32 = |d: &mut [u8], off: usize, v: i32| {
298            let b = match endian {
299                Endian::Little => v.to_le_bytes(),
300                Endian::Big => v.to_be_bytes(),
301            };
302            d[off..off + 4].copy_from_slice(&b);
303        };
304        let wr64 = |d: &mut [u8], off: usize, v: i64| {
305            let b = match endian {
306                Endian::Little => v.to_le_bytes(),
307                Endian::Big => v.to_be_bytes(),
308            };
309            d[off..off + 8].copy_from_slice(&b);
310        };
311        // magic (already in the caller's chosen order value; write raw so the
312        // parser's dual-order detect sees it correctly).
313        let mb = match endian {
314            Endian::Little => magic.to_le_bytes(),
315            Endian::Big => magic.to_be_bytes(),
316        };
317        d[FS_MAGIC_OFF..FS_MAGIC_OFF + 4].copy_from_slice(&mb);
318        wr32(&mut d, OFF_SBLKNO, 24);
319        wr32(&mut d, OFF_CBLKNO, 32);
320        wr32(&mut d, OFF_IBLKNO, 40);
321        wr32(&mut d, OFF_DBLKNO, 48);
322        wr32(&mut d, OFF_NCG, 4);
323        wr32(&mut d, OFF_BSIZE, 32768);
324        wr32(&mut d, OFF_FSIZE, 4096);
325        wr32(&mut d, OFF_FRAG, 8);
326        wr32(&mut d, OFF_BSHIFT, 15);
327        wr32(&mut d, OFF_FSHIFT, 12);
328        wr32(&mut d, OFF_INOPB, 128);
329        wr32(&mut d, OFF_IPG, 128);
330        wr32(&mut d, OFF_FPG, 256);
331        wr32(&mut d, OFF_MAXSYMLINKLEN, 120);
332        if ufs2 {
333            wr64(&mut d, OFF_SIZE, 1022);
334            wr64(&mut d, OFF_DSIZE, 901);
335            wr64(&mut d, OFF_SBLOCKLOC, SBLOCK_UFS2 as i64);
336        } else {
337            wr32(&mut d, OFF_OLD_SIZE, 1000);
338            wr32(&mut d, OFF_OLD_DSIZE, 900);
339        }
340        d
341    }
342
343    #[test]
344    fn detects_ufs2_little_endian() {
345        let d = synthetic(FS_UFS2_MAGIC, Endian::Little, true);
346        let sb = Superblock::parse(&d).unwrap();
347        assert_eq!(sb.version, UfsVersion::Ufs2);
348        assert_eq!(sb.endian, Endian::Little);
349        assert_eq!(sb.bsize, 32768);
350        assert_eq!(sb.ncg, 4);
351        assert_eq!(sb.size, 1022);
352        assert_eq!(sb.sblockloc, SBLOCK_UFS2 as i64);
353        assert_eq!(sb.inode_size(), 256);
354        assert_eq!(sb.primary_offset(), SBLOCK_UFS2);
355    }
356
357    #[test]
358    fn detects_ufs2_big_endian() {
359        let d = synthetic(FS_UFS2_MAGIC, Endian::Big, true);
360        let sb = Superblock::parse(&d).unwrap();
361        assert_eq!(sb.version, UfsVersion::Ufs2);
362        assert_eq!(sb.endian, Endian::Big);
363        assert_eq!(sb.bsize, 32768);
364        assert_eq!(sb.fpg, 256);
365    }
366
367    #[test]
368    fn detects_ufs1_and_uses_old_size_fields() {
369        let d = synthetic(FS_UFS1_MAGIC, Endian::Little, false);
370        let sb = Superblock::parse(&d).unwrap();
371        assert_eq!(sb.version, UfsVersion::Ufs1);
372        assert_eq!(sb.size, 1000, "UFS1 uses fs_old_size@36");
373        assert_eq!(sb.dsize, 900, "UFS1 uses fs_old_dsize@40");
374        assert_eq!(sb.sblockloc, 0, "UFS1 has no fs_sblockloc");
375        assert_eq!(sb.inode_size(), 128);
376        assert_eq!(sb.primary_offset(), SBLOCK_UFS1);
377    }
378
379    #[test]
380    fn detects_ufs1_big_endian() {
381        let d = synthetic(FS_UFS1_MAGIC, Endian::Big, false);
382        let sb = Superblock::parse(&d).unwrap();
383        assert_eq!(sb.version, UfsVersion::Ufs1);
384        assert_eq!(sb.endian, Endian::Big);
385        assert_eq!(sb.size, 1000, "UFS1 old_size decoded big-endian");
386        assert_eq!(sb.inode_size(), 128);
387    }
388
389    #[test]
390    fn bad_magic_fails_loud_with_bytes() {
391        let mut d = vec![0u8; SB_MIN_LEN];
392        d[FS_MAGIC_OFF..FS_MAGIC_OFF + 4].copy_from_slice(&0xdead_beef_u32.to_le_bytes());
393        let err = Superblock::parse(&d).unwrap_err();
394        assert!(
395            matches!(
396                &err,
397                UfsError::BadMagic { offset, bytes, le, .. }
398                    if *offset == FS_MAGIC_OFF
399                        && *le == 0xdead_beef
400                        && *bytes == 0xdead_beef_u32.to_le_bytes()
401            ),
402            "expected BadMagic with offending bytes, got {err:?}"
403        );
404    }
405
406    #[test]
407    fn truncated_buffer_fails_loud_not_panic() {
408        // A buffer one byte shorter than the struct cannot hold the whole
409        // superblock (magic is the tail field), so the length guard fires first
410        // — a loud Truncated, never a panic.
411        let d = vec![0u8; SB_MIN_LEN - 1];
412        let err = Superblock::parse(&d).unwrap_err();
413        assert!(
414            matches!(
415                &err,
416                UfsError::Truncated { structure, need, .. }
417                    if *structure == "superblock" && *need == SB_MIN_LEN
418            ),
419            "expected Truncated superblock, got {err:?}"
420        );
421    }
422
423    #[test]
424    fn empty_buffer_reports_truncated_not_panic() {
425        assert!(matches!(
426            Superblock::parse(&[]),
427            Err(UfsError::Truncated { .. })
428        ));
429    }
430
431    #[test]
432    fn impossible_block_size_rejected() {
433        let mut d = synthetic(FS_UFS2_MAGIC, Endian::Little, true);
434        // bsize = 1 << 20 (over the 64 KiB cap).
435        d[OFF_BSIZE..OFF_BSIZE + 4].copy_from_slice(&(1_048_576_i32).to_le_bytes());
436        assert!(matches!(
437            Superblock::parse(&d),
438            Err(UfsError::ImpossibleGeometry {
439                field: "fs_bsize",
440                ..
441            })
442        ));
443    }
444
445    #[test]
446    fn impossible_fragment_size_rejected() {
447        let mut d = synthetic(FS_UFS2_MAGIC, Endian::Little, true);
448        // fsize > bsize.
449        d[OFF_FSIZE..OFF_FSIZE + 4].copy_from_slice(&(65536_i32).to_le_bytes());
450        assert!(matches!(
451            Superblock::parse(&d),
452            Err(UfsError::ImpossibleGeometry {
453                field: "fs_fsize",
454                ..
455            })
456        ));
457    }
458
459    #[test]
460    fn absurd_cg_count_rejected() {
461        let mut d = synthetic(FS_UFS2_MAGIC, Endian::Little, true);
462        d[OFF_NCG..OFF_NCG + 4].copy_from_slice(&0x7fff_ffff_u32.to_le_bytes());
463        assert!(matches!(
464            Superblock::parse(&d),
465            Err(UfsError::ImpossibleGeometry {
466                field: "fs_ncg",
467                ..
468            })
469        ));
470    }
471}