1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
//! ext4 superblock parsing.
//!
//! Spec: docs/ext4-spec/superblock.md
//! Located at byte offset 1024, 1024 bytes long. Magic 0xEF53 at offset 56.
use crate::block_io::BlockDevice;
use crate::error::{Error, Result};
pub const SUPERBLOCK_OFFSET: u64 = 1024;
pub const SUPERBLOCK_SIZE: usize = 1024;
pub const EXT4_MAGIC: u16 = 0xEF53;
/// `s_state` bits (byte offset 0x3A). The kernel sets `VALID_FS` when a
/// clean unmount completes and clears it on mount; a dirty value on a
/// not-currently-mounted image therefore indicates an unclean shutdown
/// and signals the caller that journal replay (or `fsck`) is required
/// before writes are safe.
pub const EXT4_VALID_FS: u16 = 0x0001;
pub const EXT4_ERROR_FS: u16 = 0x0002;
/// Parsed in-memory representation of the ext4 superblock.
/// Field names mirror the kernel's `struct ext4_super_block` (s_ prefix dropped).
#[derive(Debug, Clone)]
pub struct Superblock {
pub inodes_count: u32,
pub blocks_count: u64, // combined lo + hi
pub free_blocks_count: u64,
pub free_inodes_count: u32,
/// `s_r_blocks_count` (lo at 0x08, hi at 0x154 — 64-bit on
/// INCOMPAT_64BIT volumes). Blocks reserved for the superuser
/// — explains the gap between `free_blocks` and what `df`
/// reports as available to a normal user.
pub r_blocks_count: u64,
pub first_data_block: u32,
pub log_block_size: u32,
pub blocks_per_group: u32,
pub inodes_per_group: u32,
pub magic: u16,
/// `s_state` (0x3A). `EXT4_VALID_FS` = cleanly unmounted. Any other
/// value means the FS was mounted and not cleanly unmounted (dirty)
/// or that the kernel marked the FS as having errors.
pub state: u16,
/// `s_errors` (0x3C). Kernel error policy: 1=continue, 2=remount-ro,
/// 3=panic. Informational from a Swift host's POV but useful in
/// diagnostics output.
pub errors_behavior: u16,
/// `s_minor_rev_level` (0x3E). Bumped by filesystem admin tools for minor format
/// tweaks within a major rev_level.
pub minor_rev_level: u16,
pub rev_level: u32,
pub inode_size: u16,
/// `s_first_ino` (0x54, dynamic-rev only). First non-reserved
/// inode number; defaults to 11 on rev_level=1+ filesystems.
pub first_inode: u32,
pub feature_compat: u32,
pub feature_incompat: u32,
pub feature_ro_compat: u32,
pub uuid: [u8; 16],
pub volume_name: String,
/// `s_last_mounted` (0x88, 64 bytes). Last directory the FS was
/// mounted at — handy for diagnostics ("when was this disk last
/// in another machine?").
pub last_mounted: String,
pub desc_size: u16, // BGD size: 32 or 64
/// `s_reserved_gdt_blocks` — blocks held back after the group
/// descriptor table so the filesystem can be grown online.
///
/// They sit between the GDT and the block bitmap in every group that
/// carries a superblock backup, and they are **not** free space:
/// anything that rebuilds a block bitmap has to mark them used or
/// the next allocation writes over the filesystem's own room to
/// expand.
pub reserved_gdt_blocks: u16,
/// `s_backup_bgs` — the only two groups carrying backups when
/// `SPARSE_SUPER2` is set. Meaningless without that feature.
pub backup_bgs: [u32; 2],
pub hash_seed: [u32; 4],
pub default_hash_version: u8,
pub checksum_seed: u32, // s_checksum_seed (used when INCOMPAT_CSUM_SEED)
pub journal_inode: u32,
/// `s_last_orphan` (0xE8). Head of the orphan-inode list — inodes
/// whose link count reached zero while still open. The kernel
/// inserts unlink-while-open targets here so that, on the next
/// mount, recovery can reclaim them. Each inode's `i_dtime` field
/// is overloaded to point at the next orphan in the chain; the
/// chain terminates with a zero `dtime`.
pub last_orphan: u32,
/// `s_mtime` (0x2C). Timestamp the FS was last mounted.
pub mtime: u32,
/// `s_wtime` (0x30). Timestamp the FS was last written to.
pub wtime: u32,
/// `s_mnt_count` (0x34). Mounts since last fsck.
pub mnt_count: u16,
/// `s_max_mnt_count` (0x36). Forced fsck after this many mounts;
/// 0 disables.
pub max_mnt_count: u16,
/// `s_lastcheck` (0x40). Timestamp of the last fsck pass.
pub lastcheck: u32,
/// `s_checkinterval` (0x44). Seconds between forced fscks; 0
/// disables time-based forced fsck.
pub checkinterval: u32,
/// `s_creator_os` (0x48). 0=Linux, 1=Hurd, 2=Masix, 3=FreeBSD,
/// 4=Lites.
pub creator_os: u32,
/// `s_def_resuid` (0x50). UID with access to reserved blocks.
pub def_resuid: u16,
/// `s_def_resgid` (0x52). GID with access to reserved blocks.
pub def_resgid: u16,
pub raw: Vec<u8>, // keep raw bytes for re-checksum on writes (future)
}
/// The classic `RO_COMPAT_SPARSE_SUPER` layout: groups 0 and 1, and
/// every power of 3, 5 or 7.
///
/// Separate from [`Superblock::group_has_super`] because `mkfs` needs the
/// rule without a filesystem to ask — it is deciding what to write, not
/// reading what someone else wrote.
pub(crate) fn classic_sparse_super(g: u64) -> bool {
fn is_power_of(mut g: u64, base: u64) -> bool {
while g.is_multiple_of(base) {
g /= base;
}
g == 1
}
g <= 1 || is_power_of(g, 3) || is_power_of(g, 5) || is_power_of(g, 7)
}
/// The largest block ext4 defines, as its base-2 log over 1024.
///
/// 6, which is a 64 KiB block.
pub const MAX_LOG_BLOCK_SIZE: u32 = 6;
/// The first inode a filesystem may hand out, before `s_first_ino`.
///
/// 11. Inodes 1 through 10 are the filesystem's own -- 2 is the root
/// directory and 8 the journal -- and the kernel refuses a superblock
/// whose `s_first_ino` is below it.
pub const GOOD_OLD_FIRST_INODE: u32 = 11;
impl Superblock {
/// Read and parse the superblock from a block device.
pub fn read<D: BlockDevice + ?Sized>(dev: &D) -> Result<Self> {
let mut buf = vec![0u8; SUPERBLOCK_SIZE];
dev.read_at(SUPERBLOCK_OFFSET, &mut buf)?;
Self::parse(buf)
}
pub fn parse(raw: Vec<u8>) -> Result<Self> {
if raw.len() < SUPERBLOCK_SIZE {
return Err(Error::Corrupt("superblock buffer too small"));
}
let magic = u16::from_le_bytes([raw[0x38], raw[0x39]]);
if magic != EXT4_MAGIC {
return Err(Error::BadMagic {
found: magic,
expected: EXT4_MAGIC,
});
}
let inodes_count = u32::from_le_bytes(raw[0x00..0x04].try_into().unwrap());
let blocks_count_lo = u32::from_le_bytes(raw[0x04..0x08].try_into().unwrap());
let r_blocks_count_lo = u32::from_le_bytes(raw[0x08..0x0C].try_into().unwrap());
let free_blocks_count_lo = u32::from_le_bytes(raw[0x0C..0x10].try_into().unwrap());
let free_inodes_count = u32::from_le_bytes(raw[0x10..0x14].try_into().unwrap());
let first_data_block = u32::from_le_bytes(raw[0x14..0x18].try_into().unwrap());
let log_block_size = u32::from_le_bytes(raw[0x18..0x1C].try_into().unwrap());
let blocks_per_group = u32::from_le_bytes(raw[0x20..0x24].try_into().unwrap());
let inodes_per_group = u32::from_le_bytes(raw[0x28..0x2C].try_into().unwrap());
let mtime = u32::from_le_bytes(raw[0x2C..0x30].try_into().unwrap());
let wtime = u32::from_le_bytes(raw[0x30..0x34].try_into().unwrap());
let mnt_count = u16::from_le_bytes(raw[0x34..0x36].try_into().unwrap());
let max_mnt_count = u16::from_le_bytes(raw[0x36..0x38].try_into().unwrap());
let state = u16::from_le_bytes(raw[0x3A..0x3C].try_into().unwrap());
let errors_behavior = u16::from_le_bytes(raw[0x3C..0x3E].try_into().unwrap());
let minor_rev_level = u16::from_le_bytes(raw[0x3E..0x40].try_into().unwrap());
let lastcheck = u32::from_le_bytes(raw[0x40..0x44].try_into().unwrap());
let checkinterval = u32::from_le_bytes(raw[0x44..0x48].try_into().unwrap());
let creator_os = u32::from_le_bytes(raw[0x48..0x4C].try_into().unwrap());
let rev_level = u32::from_le_bytes(raw[0x4C..0x50].try_into().unwrap());
let def_resuid = u16::from_le_bytes(raw[0x50..0x52].try_into().unwrap());
let def_resgid = u16::from_le_bytes(raw[0x52..0x54].try_into().unwrap());
// Dynamic-rev fields (rev_level >= 1). Pre-rev1 filesystems
// pin sensible defaults — `inode_size = 128` (the historical
// ext2 size), `first_inode = 11` (the spec-defined start of
// user-visible inodes; lower numbers are reserved).
// A `s_first_ino` below 11 would hand the reserved inodes -- 2
// is the root directory, 8 the journal -- to the allocator, so
// the kernel refuses one and this takes the floor rather than
// the field. Nothing legitimate writes a lower value; a
// filesystem that does is claiming its own root is available.
let first_inode = if rev_level >= 1 {
u32::from_le_bytes(raw[0x54..0x58].try_into().unwrap()).max(GOOD_OLD_FIRST_INODE)
} else {
GOOD_OLD_FIRST_INODE
};
let inode_size = if rev_level >= 1 {
u16::from_le_bytes(raw[0x58..0x5A].try_into().unwrap())
} else {
128
};
let feature_compat = if rev_level >= 1 {
u32::from_le_bytes(raw[0x5C..0x60].try_into().unwrap())
} else {
0
};
let feature_incompat = if rev_level >= 1 {
u32::from_le_bytes(raw[0x60..0x64].try_into().unwrap())
} else {
0
};
let feature_ro_compat = if rev_level >= 1 {
u32::from_le_bytes(raw[0x64..0x68].try_into().unwrap())
} else {
0
};
// s_reserved_gdt_blocks at 0xCE, and s_backup_bgs at 0x274. Both
// are zero on a revision-0 filesystem, which has neither.
let reserved_gdt_blocks = if rev_level >= 1 {
u16::from_le_bytes(raw[0xCE..0xD0].try_into().unwrap())
} else {
0
};
let backup_bgs = if rev_level >= 1 && raw.len() >= 0x27C {
[
u32::from_le_bytes(raw[0x274..0x278].try_into().unwrap()),
u32::from_le_bytes(raw[0x278..0x27C].try_into().unwrap()),
]
} else {
[0, 0]
};
let mut uuid = [0u8; 16];
uuid.copy_from_slice(&raw[0x68..0x78]);
let volume_name_bytes = &raw[0x78..0x88];
let nul = volume_name_bytes.iter().position(|&b| b == 0).unwrap_or(16);
let volume_name = String::from_utf8_lossy(&volume_name_bytes[..nul]).into_owned();
// s_last_mounted at 0x88, 64 bytes. The kernel writes the path
// here on every successful mount; a freshly mkfs'd filesystem
// leaves it zero-padded.
let last_mounted_bytes = &raw[0x88..0xC8];
let nul = last_mounted_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(64);
let last_mounted = String::from_utf8_lossy(&last_mounted_bytes[..nul]).into_owned();
let desc_size = u16::from_le_bytes(raw[0xFE..0x100].try_into().unwrap());
// If desc_size is 0, default to 32 (legacy); spec says 32 or 64
let desc_size = if desc_size == 0 { 32 } else { desc_size };
let mut hash_seed = [0u32; 4];
for (i, slot) in hash_seed.iter_mut().enumerate() {
let off = 0xEC + i * 4;
*slot = u32::from_le_bytes(raw[off..off + 4].try_into().unwrap());
}
let default_hash_version = raw[0xFC];
// 64-bit fields (only valid when INCOMPAT_64BIT). Pre-64bit
// filesystems leave the high halves zero so combining is safe
// unconditionally.
let blocks_count_hi = u32::from_le_bytes(raw[0x150..0x154].try_into().unwrap());
let r_blocks_count_hi = u32::from_le_bytes(raw[0x154..0x158].try_into().unwrap());
let free_blocks_count_hi = u32::from_le_bytes(raw[0x158..0x15C].try_into().unwrap());
let blocks_count = ((blocks_count_hi as u64) << 32) | (blocks_count_lo as u64);
let r_blocks_count = ((r_blocks_count_hi as u64) << 32) | (r_blocks_count_lo as u64);
let free_blocks_count =
((free_blocks_count_hi as u64) << 32) | (free_blocks_count_lo as u64);
let checksum_seed = u32::from_le_bytes(raw[0x270..0x274].try_into().unwrap());
let journal_inode = u32::from_le_bytes(raw[0xE0..0xE4].try_into().unwrap());
let last_orphan = u32::from_le_bytes(raw[0xE8..0xEC].try_into().unwrap());
// Reject impossible geometry early so downstream arithmetic never
// divides by zero. All three are required for the filesystem to
// name even a single block or inode.
if blocks_per_group == 0 {
return Err(Error::Corrupt("superblock: blocks_per_group == 0"));
}
if inodes_per_group == 0 {
return Err(Error::Corrupt("superblock: inodes_per_group == 0"));
}
if inode_size == 0 {
return Err(Error::Corrupt("superblock: inode_size == 0"));
}
// ext4 tops out at a 64 KiB block, which is `log_block_size = 6`.
// The guard used to admit 20, a 1 GiB block, on the argument that
// anything larger was "certainly a corrupt field" -- which is
// true of 20 as well. Every `vec![0u8; block_size]` in this
// crate is sized by this field, and `mount_inner` wraps the
// device in a 256-entry cache, so a 1 GiB block is 256 GiB of
// resident memory from a sparse image; three `read_block` calls
// allocated and zero-filled 3 GiB in five seconds. It is also
// what makes `ppb * ppb * ppb` overflow in the indirect-block
// map.
if log_block_size > MAX_LOG_BLOCK_SIZE {
return Err(Error::Corrupt(
"superblock: log_block_size exceeds the largest ext4 block",
));
}
// 32 bytes without the 64BIT feature, 64 or more with it, and a
// power of two either way -- the kernel's own rule. The parser
// reads fixed offsets up to 0x20, and up to 0x3C when the field
// says 64, so a smaller value indexes past the buffer: 8 gave
// "range end index 12 out of range for slice of length 8" during
// mount.
let sixty_four_bit = feature_incompat & crate::features::Incompat::BIT64.bits() != 0;
let smallest = if sixty_four_bit { 64 } else { 32 };
if desc_size < smallest || !desc_size.is_power_of_two() {
return Err(Error::Corrupt(
"superblock: desc_size is not a group descriptor size",
));
}
if blocks_count == 0 {
return Err(Error::Corrupt("superblock: blocks_count == 0"));
}
Ok(Self {
inodes_count,
blocks_count,
free_blocks_count,
free_inodes_count,
r_blocks_count,
first_data_block,
log_block_size,
blocks_per_group,
inodes_per_group,
magic,
state,
errors_behavior,
minor_rev_level,
rev_level,
inode_size,
first_inode,
feature_compat,
feature_incompat,
feature_ro_compat,
uuid,
volume_name,
last_mounted,
desc_size,
reserved_gdt_blocks,
backup_bgs,
hash_seed,
default_hash_version,
checksum_seed,
journal_inode,
last_orphan,
mtime,
wtime,
mnt_count,
max_mnt_count,
lastcheck,
checkinterval,
creator_os,
def_resuid,
def_resgid,
raw,
})
}
/// Whether the filesystem was cleanly unmounted. `false` here means
/// the FS was not cleanly unmounted and a journal replay (or fsck)
/// is required before writes are safe. Read-only consumers can
/// still mount a dirty FS; callers that intend to write should
/// surface this to the user and either run fsck or refuse to
/// mount read-write.
pub fn is_clean(&self) -> bool {
self.state & EXT4_VALID_FS != 0
}
/// Whether block group `g` carries a superblock and group-descriptor
/// backup.
///
/// Three layouts, and the filesystem's own flags decide which:
///
/// - **`SPARSE_SUPER2`**: group 0, and the two groups named by
/// `s_backup_bgs`. Nothing else.
/// - **`SPARSE_SUPER` clear**: every group. This is the old ext2
/// layout, and the one that costs most to get wrong — assuming the
/// sparse rule leaves real backups looking like free space.
/// - **otherwise**: the classic sparse rule — groups 0 and 1, and
/// every power of 3, 5 or 7.
///
/// Answering unconditionally with the classic rule is wrong in both
/// of the first two cases, and wrong in the direction that matters:
/// on a filesystem without `SPARSE_SUPER` it reports "no backup here"
/// for groups that have one, so a rebuilt bitmap offers the backup
/// superblock and its descriptor table as free blocks.
pub fn group_has_super(&self, g: u64) -> bool {
use crate::features::{Compat, RoCompat};
if g == 0 {
return true;
}
if self.feature_compat & Compat::SPARSE_SUPER2.bits() != 0 {
return self.backup_bgs.iter().any(|&b| u64::from(b) == g);
}
if self.feature_ro_compat & RoCompat::SPARSE_SUPER.bits() == 0 {
return true;
}
classic_sparse_super(g)
}
/// Block size in bytes: 1024 << log_block_size.
pub fn block_size(&self) -> u32 {
1024u32 << self.log_block_size
}
/// Number of block groups.
pub fn block_group_count(&self) -> u64 {
self.blocks_count.div_ceil(self.blocks_per_group as u64)
}
/// Whether the 64BIT incompat feature is enabled.
pub fn is_64bit(&self) -> bool {
self.feature_incompat & crate::features::Incompat::BIT64.bits() != 0
}
}
#[cfg(test)]
mod backup_layout_tests {
use super::*;
use crate::features::{Compat, RoCompat};
/// A superblock with only the fields the backup-layout rule reads.
fn sb_with(compat: u32, ro_compat: u32, backup_bgs: [u32; 2], reserved_gdt: u16) -> Superblock {
let mut raw = vec![0u8; SUPERBLOCK_SIZE];
raw[0x38..0x3A].copy_from_slice(&EXT4_MAGIC.to_le_bytes());
raw[0x00..0x04].copy_from_slice(&8192u32.to_le_bytes()); // inodes_count
raw[0x04..0x08].copy_from_slice(&65536u32.to_le_bytes()); // blocks_count
raw[0x14..0x18].copy_from_slice(&1u32.to_le_bytes()); // first_data_block
raw[0x20..0x24].copy_from_slice(&8192u32.to_le_bytes()); // blocks_per_group
raw[0x28..0x2C].copy_from_slice(&2048u32.to_le_bytes()); // inodes_per_group
raw[0x4C..0x50].copy_from_slice(&1u32.to_le_bytes()); // rev_level
raw[0x58..0x5A].copy_from_slice(&256u16.to_le_bytes()); // inode_size
raw[0x5C..0x60].copy_from_slice(&compat.to_le_bytes());
raw[0x64..0x68].copy_from_slice(&ro_compat.to_le_bytes());
raw[0xCE..0xD0].copy_from_slice(&reserved_gdt.to_le_bytes());
raw[0xFE..0x100].copy_from_slice(&64u16.to_le_bytes()); // desc_size
raw[0x274..0x278].copy_from_slice(&backup_bgs[0].to_le_bytes());
raw[0x278..0x27C].copy_from_slice(&backup_bgs[1].to_le_bytes());
Superblock::parse(raw).expect("superblock")
}
/// The layout nearly every filesystem has: groups 0 and 1, then the
/// powers of 3, 5 and 7.
#[test]
fn the_classic_sparse_layout() {
let sb = sb_with(0, RoCompat::SPARSE_SUPER.bits(), [0, 0], 0);
for g in [0, 1, 3, 5, 7, 9, 25, 27, 49, 81, 125] {
assert!(sb.group_has_super(g), "group {g} should carry a backup");
}
for g in [2, 4, 6, 8, 10, 11, 26, 50, 100] {
assert!(!sb.group_has_super(g), "group {g} should not");
}
}
/// Without `SPARSE_SUPER`, every group carries one.
///
/// This is the case that costs data rather than capacity: answering
/// with the sparse rule reports "no backup here" for groups that have
/// one, so rebuilding a bitmap offers a live backup superblock and its
/// descriptor table as free space.
#[test]
fn without_sparse_super_every_group_carries_a_backup() {
let sb = sb_with(0, 0, [0, 0], 0);
for g in 0..40 {
assert!(sb.group_has_super(g), "group {g} should carry a backup");
}
}
/// `SPARSE_SUPER2` names exactly two groups, and no rule applies
/// beyond them.
#[test]
fn sparse_super2_names_its_own_groups() {
let sb = sb_with(
Compat::SPARSE_SUPER2.bits(),
RoCompat::SPARSE_SUPER.bits(),
[4, 17],
0,
);
assert!(sb.group_has_super(0), "group 0 always carries one");
assert!(sb.group_has_super(4));
assert!(sb.group_has_super(17));
// Powers of 3, 5 and 7 are not special here, and group 1 is not
// either — which is what distinguishes this from the classic rule
// rather than merely narrowing it.
for g in [1, 3, 5, 7, 9, 25, 49] {
assert!(
!sb.group_has_super(g),
"group {g} is not named by s_backup_bgs and must not be treated as \
carrying a backup"
);
}
}
/// The field that says how much room the filesystem keeps to grow.
#[test]
fn reserved_gdt_blocks_is_read() {
assert_eq!(
sb_with(0, RoCompat::SPARSE_SUPER.bits(), [0, 0], 1024).reserved_gdt_blocks,
1024
);
assert_eq!(
sb_with(0, RoCompat::SPARSE_SUPER.bits(), [0, 0], 0).reserved_gdt_blocks,
0
);
}
}