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
//! Every feature bit the crate has an opinion about, checked against a
//! filesystem that actually carries it.
//!
//! G5 from `docs/format-conformance-gaps.md`. Every gap that document
//! records was found by *reading* the supported-feature masks and their
//! comments — not one was caught by a test. This is the test that would
//! have caught them.
//!
//! The contract per feature is deliberately weak: **read it correctly,
//! or refuse to mount.** Both are acceptable; what is not acceptable is
//! mounting and then returning data derived from arithmetic the feature
//! invalidates, which is the failure mode every gap in that document
//! shares.
//!
//! # Why this builds its own image
//!
//! `test-disks/*.img` is gitignored, and `chore fixtures` builds it in the
//! fs-linux-test-harness VM because most fixtures require a real mount to
//! populate. The images here need only `mke2fs` — nothing is written into
//! them, the feature bits in the superblock are the whole point — so the
//! test builds them itself. It fails when the harness VM the e2fsprogs tools run in is unreachable.
use std::path::{Path, PathBuf};
use std::sync::Arc;
use fs_ext4::block_io::{BlockDevice, FileDevice};
use fs_ext4::Filesystem;
/// ext4's root directory is always inode 2.
const ROOT_INO: u32 = 2;
/// Build a 16 MiB image with the given `mke2fs` options.
fn build(name: &str, opts: &[&str]) -> PathBuf {
let mke2fs = "mke2fs";
let path =
fs_ext4_test_support::temp_dir().join(format!("fs-ext4-fm-{}-{name}", std::process::id()));
let _ = std::fs::remove_file(&path);
std::fs::write(&path, vec![0u8; 16 * 1024 * 1024]).expect("allocate image");
let out = fs_ext4_test_support::oracle(mke2fs)
.args(["-q", "-F"])
.args(opts)
.arg(&path)
.output();
assert!(
out.status.success(),
"mke2fs {opts:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
path
}
fn mount(path: &Path) -> fs_ext4::error::Result<Filesystem> {
let dev = FileDevice::open(path.to_str().expect("utf-8 path")).expect("open image");
let dyn_dev: Arc<dyn BlockDevice> = Arc::new(dev);
Filesystem::mount(dyn_dev)
}
/// **G1, and what it turned out to be.** A bigalloc filesystem mounts and
/// reads its root (#75).
///
/// This was a refusal, on the premise that bigalloc moves every block-group
/// offset. It does not: `s_blocks_per_group` stays the group stride in
/// blocks, and only the bitmaps and descriptor free counts count clusters.
/// With the refusal lifted this image failed with
/// `BadChecksum { what: "block group descriptor" }`, which was taken for
/// the misreading. It was a different defect: `mke2fs -t ext4` gave this
/// 16 MiB image 1 KiB blocks, bigalloc forces `s_first_data_block = 0`,
/// and the descriptor table was located after `s_first_data_block` rather
/// than after the superblock's block. Built with `-C 16384`, sixteen blocks
/// to a cluster, so that case is the one exercised.
///
/// `tests/bigalloc_read_oracle.rs` compares file contents; this is the
/// matrix entry.
#[test]
fn bigalloc_mounts_and_reads() {
// `-b 1024` explicitly: whether a 16 MiB image gets 1 KiB blocks is the
// local mke2fs.conf's choice, and CI's does not make it.
let img = build(
"bigalloc",
&["-t", "ext4", "-b", "1024", "-O", "bigalloc", "-C", "16384"],
);
let fs = mount(&img).expect("a bigalloc filesystem reads like any other");
assert_eq!(fs.sb.block_size(), 1024, "the case this pins is 1 KiB");
assert_eq!(fs.sb.first_data_block, 0);
fs.read_inode_verified(ROOT_INO)
.expect("the root inode must be readable after mounting");
let _ = std::fs::remove_file(&img);
}
/// The guard against fixing G1 too broadly.
///
/// It would be easy to refuse a feature by tightening the RO_COMPAT
/// check into "anything unrecognised", which would also refuse ordinary
/// filesystems carrying newer bits. This mounts a plain ext4 and
/// requires it to still work.
#[test]
fn an_ordinary_ext4_still_mounts_and_reads() {
let img = build("plain", &["-t", "ext4"]);
let fs = mount(&img).expect("a plain ext4 filesystem must mount");
fs.read_inode_verified(ROOT_INO)
.expect("the root inode must be readable after mounting");
let _ = std::fs::remove_file(&img);
}
/// An RO_COMPAT bit the crate has never heard of must still mount —
/// that is the compatibility model, and the reason G1's refusal had to
/// be a named list rather than a mask complement.
///
/// `project` is a real RO_COMPAT feature the reader does nothing with.
#[test]
fn a_tolerated_ro_compat_feature_still_mounts() {
let img = build("project", &["-t", "ext4", "-O", "project,quota"]);
let fs = mount(&img).expect("project/quota are ignorable on a read-only mount");
fs.read_inode_verified(ROOT_INO).expect("root readable");
let _ = std::fs::remove_file(&img);
}
/// **G4.** A filesystem with Multi-Mount Protection must not be
/// mounted *writable* by a driver that does not honour it.
///
/// MMP exists to stop two hosts writing to one filesystem at the same
/// time. Honouring it means reading the MMP block, checking its
/// sequence, claiming it with our own node name and re-checking —
/// none of which this crate does. It nevertheless has twenty-one
/// `apply_*` write entry points and a live journal writer, so
/// ignoring the bit while writing is exactly the situation MMP is
/// designed to prevent.
///
/// Read-only is still allowed and is deliberately checked below: a
/// user recovering data from a disk another machine has open is the
/// case that must keep working.
#[test]
fn an_mmp_filesystem_is_refused_for_writing_but_allowed_read_only() {
let img = build("mmp", &["-t", "ext4", "-O", "mmp"]);
// Writable: must be refused.
let dev = FileDevice::open_rw(img.to_str().unwrap()).expect("open read-write");
let writable: Arc<dyn BlockDevice> = Arc::new(dev);
assert!(
writable.is_writable(),
"the fixture must be writable, or this test proves nothing"
);
assert!(
Filesystem::mount(writable).is_err(),
"an MMP filesystem MOUNTED WRITABLE. MMP exists to stop two hosts \
writing at once, and this crate does not honour it — mounting \
writable means becoming the second writer MMP was protecting \
against."
);
// Read-only: must still work.
let ro = FileDevice::open(img.to_str().unwrap()).expect("open read-only");
let ro_dyn: Arc<dyn BlockDevice> = Arc::new(ro);
Filesystem::mount(ro_dyn).expect("a read-only mount of an MMP filesystem must still work");
let _ = std::fs::remove_file(&img);
}