20#include <sys/syscall.h>
26static const char* filesystem;
28static void require(
int condition,
const char* operation) {
30 printf(
"FS-MUTATION-CONTRACT: FAIL %s %s errno=%d\n", filesystem, operation, errno);
35static int create_file(
const char* name,
const char* contents) {
36 int file = open(name, O_CREAT | O_EXCL | O_RDWR, 0600);
37 require(file >= 0,
"create file");
38 const size_t length = strlen(contents);
39 require(write(file, contents, length) == (ssize_t)length,
"initial file contents");
43static void require_contents(
int file,
const char* expected,
const char* operation) {
44 char buffer[64] = {0};
45 const size_t length = strlen(expected);
46 require(length <
sizeof(buffer) && pread(file, buffer,
sizeof(buffer), 0) == (ssize_t)length &&
47 !memcmp(buffer, expected, length),
51static void replacement_contracts(
int hardlinks) {
52 int source = create_file(
"source",
"source-data");
53 int replaced = create_file(
"destination",
"old-data");
56 require(fstat(source, &original) == 0,
"source inode");
57 require(rename(
"source",
"destination") == 0,
"replace destination");
58 require(stat(
"destination", ¤t) == 0 && current.st_ino == original.st_ino,
59 "rename preserves inode");
61 require(stat(
"source", ¤t) == -1 && errno == ENOENT,
"old name removed");
62 require_contents(source,
"source-data",
"open source after rename");
63 require_contents(replaced,
"old-data",
"open replaced file survives");
64 require(pwrite(replaced,
"OLD", 3, 0) == 3,
"write replaced open file");
65 require_contents(replaced,
"OLD-data",
"replaced inode remains writable");
66 int destination = open(
"destination", O_RDONLY);
67 require(destination >= 0,
"open renamed destination");
68 require_contents(destination,
"source-data",
"destination isolation");
69 require(rename(
"destination",
"destination") == 0,
"same-name rename");
72 require(link(
"destination",
"alias") == 0,
"create hardlink");
73 require(rename(
"destination",
"alias") == 0,
"same-inode rename");
74 require(stat(
"destination", ¤t) == 0 && current.st_ino == original.st_ino,
75 "same-inode rename keeps source name");
76 require(stat(
"alias", ¤t) == 0 && current.st_ino == original.st_ino,
77 "same-inode rename keeps destination name");
78 int alias = open(
"alias", O_RDWR);
79 require(alias >= 0,
"open hardlink alias");
80 require(fstat(source, ¤t) == 0 && current.st_nlink == 2,
81 "hardlinks report inode link count");
82 require(fchmod(alias, 0640) == 0 && fstat(source, ¤t) == 0 &&
83 (current.st_mode & 0777) == 0640,
84 "hardlinks share permission changes");
85 require(ftruncate(alias, 6) == 0,
"truncate hardlink alias");
86 require(fstat(source, ¤t) == 0 && current.st_size == 6,
"hardlinks share size");
87 require_contents(source,
"source",
"hardlinks share truncated contents");
88 require(pwrite(alias,
"S", 1, 0) == 1,
"write hardlink alias");
89 require_contents(source,
"Source",
"hardlinks share writes");
90 require(unlink(
"alias") == 0,
"unlink alias with open descriptor");
91 require(fstat(alias, ¤t) == 0 && current.st_nlink == 1,
92 "open alias observes decremented link count");
93 require_contents(alias,
"Source",
"unlinked alias retains shared inode");
100 require(unlink(
"destination") == 0,
"remove renamed file");
101 printf(
"FS-MUTATION-CONTRACT: PASS %s replacement-and-open-inodes\n", filesystem);
104static void hardlink_mapping_contracts(
void) {
105 const size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
106 int first = create_file(
"mapped-first",
"");
107 require(ftruncate(first, 2 * page_size) == 0 && link(
"mapped-first",
"mapped-second") == 0,
108 "create mapped hardlink layout");
109 int second = open(
"mapped-second", O_RDWR);
110 require(second >= 0,
"open mapped alias");
111 unsigned char* left = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE, MAP_SHARED, first, 0);
112 unsigned char* right = mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE, MAP_SHARED, second, 0);
113 require(left != MAP_FAILED && right != MAP_FAILED,
"map hardlink aliases");
114 require(left[17] == 0 && right[17] == 0,
"fault initial alias pages");
116 require(right[17] == 0x6b,
"mapped aliases share writes immediately");
117 const unsigned char value = 0x9e;
118 require(pwrite(second, &value, 1, 29) == 1 && left[29] == value && right[29] == value,
119 "descriptor writes update both mapped aliases");
120 require(msync(left, 2 * page_size, MS_SYNC) == 0,
"sync shared alias changes");
121 require(unlink(
"mapped-first") == 0 && close(first) == 0 && munmap(left, 2 * page_size) == 0,
122 "retire first mapped alias");
124 require(fstat(second, &status) == 0 && status.st_nlink == 1,
"surviving mapped alias link count");
126 require(fsync(second) == 0 && munmap(right, 2 * page_size) == 0 && close(second) == 0,
127 "fsync surviving alias after first alias retirement");
128 second = open(
"mapped-second", O_RDONLY);
129 unsigned char buffer[32] = {0};
130 require(second >= 0 && pread(second, buffer,
sizeof(buffer), 0) ==
sizeof(buffer) &&
131 buffer[17] == 0x6b && buffer[29] == 0x9e && buffer[31] == 0x4a,
132 "reopen retains all alias writes");
134 require(unlink(
"mapped-second") == 0,
"mapped alias cleanup");
135 printf(
"FS-MUTATION-CONTRACT: PASS %s hardlink-metadata-and-mapped-cache\n", filesystem);
138static void directory_contracts(
void) {
139 require(mkdir(
"first", 0700) == 0 && mkdir(
"second", 0700) == 0 &&
140 mkdir(
"first/child", 0700) == 0 && mkdir(
"second/child", 0700) == 0,
141 "create directory move layout");
142 int child = open(
"first/child", O_RDONLY | O_DIRECTORY);
143 int old_destination = open(
"second/child", O_RDONLY | O_DIRECTORY);
144 int parent = open(
"second", O_RDONLY | O_DIRECTORY);
145 require(child >= 0 && old_destination >= 0 && parent >= 0,
"open directory move layout");
146 int leaf = create_file(
"first/child/leaf",
"leaf");
148 struct stat original;
150 struct stat parent_status;
151 require(fstat(child, &original) == 0 && fstat(parent, &parent_status) == 0,
152 "directory identities");
153 require(rename(
"first/child",
"second/child") == 0,
"cross-parent directory replacement");
154 require(stat(
"second/child", ¤t) == 0 && current.st_ino == original.st_ino,
155 "moved directory retains inode");
156 int moved_parent = openat(child,
"..", O_RDONLY | O_DIRECTORY);
157 require(moved_parent >= 0 && fstat(moved_parent, ¤t) == 0 &&
158 current.st_ino == parent_status.st_ino,
159 "open moved directory follows new parent");
160 require(fstat(old_destination, ¤t) == 0 && S_ISDIR(current.st_mode),
161 "replaced open directory survives");
162 leaf = openat(child,
"leaf", O_RDONLY);
163 require(leaf >= 0,
"open child through retained directory");
164 require_contents(leaf,
"leaf",
"moved directory retains children");
167 close(old_destination);
171 require(mkdir(
"empty", 0700) == 0,
"create empty directory");
173 require(rename(
"empty",
"second/child") == -1 && errno == ENOTEMPTY,
174 "nonempty replacement fails");
175 require(stat(
"empty", ¤t) == 0 && stat(
"second/child/leaf", ¤t) == 0,
176 "failed replacement preserves both directories");
178 require(rename(
"second",
"second/child/descendant") == -1 && errno == EINVAL,
179 "directory descendant cycle rejected");
180 require(stat(
"second/child/leaf", ¤t) == 0,
"cycle failure preserves namespace");
182 require(rename(
"missing",
"second/child/leaf") == -1 && errno == ENOENT,
183 "missing source leaves destination");
184 leaf = open(
"second/child/leaf", O_RDONLY);
185 require(leaf >= 0,
"destination after missing-source failure");
186 require_contents(leaf,
"leaf",
"failed rename retains destination contents");
189 require(rename(
"second/child/leaf",
"empty") == -1 && errno == EISDIR,
190 "file cannot replace directory");
192 require(rename(
"empty",
"second/child/leaf") == -1 && errno == ENOTDIR,
193 "directory cannot replace file");
195 require(unlink(
"second/child/leaf") == 0 && rmdir(
"second/child") == 0 && rmdir(
"second") == 0 &&
196 rmdir(
"first") == 0 && rmdir(
"empty") == 0,
197 "directory cleanup");
198 printf(
"FS-MUTATION-CONTRACT: PASS %s directory-moves-and-failure-preservation\n", filesystem);
201static void symlink_contracts(
void) {
202 int target = create_file(
"target",
"unchanged");
203 require(symlink(
"target",
"source-link") == 0 && symlink(
"absent",
"destination-link") == 0,
204 "create symlink layout");
205 require(rename(
"source-link",
"destination-link") == 0,
"rename terminal symlink");
206 char value[32] = {0};
207 require(readlink(
"destination-link", value,
sizeof(value)) == 6 && !memcmp(value,
"target", 6),
208 "rename moves symlink itself");
209 require_contents(target,
"unchanged",
"rename does not alter symlink target");
212 require(lstat(
"source-link", &status) == -1 && errno == ENOENT,
"old symlink name removed");
214 require(unlink(
"destination-link") == 0 && unlink(
"target") == 0,
"symlink cleanup");
215 printf(
"FS-MUTATION-CONTRACT: PASS %s terminal-symlinks\n", filesystem);
218static void truncate_contracts(
void) {
219 const off_t extent = 80 * 1024 + 17;
220 const off_t prefix = 4099;
221 int file = create_file(
"resize",
"");
222 unsigned char buffer[4096];
223 memset(buffer, 0x5a,
sizeof(buffer));
224 for (off_t offset = 0; offset < extent;) {
225 size_t amount = extent - offset < (off_t)
sizeof(buffer) ? extent - offset :
sizeof(buffer);
226 require(write(file, buffer, amount) == (ssize_t)amount,
"write multi-block file");
229 require(lseek(file, 55, SEEK_SET) == 55,
"set offset before truncate");
230 require(ftruncate(file, prefix) == 0,
"shrink through indirect blocks");
231 require(lseek(file, 0, SEEK_CUR) == 55,
"truncate preserves file offset");
233 require(fstat(file, &status) == 0 && status.st_size == prefix,
"shrunk size");
234 require(ftruncate(file, extent) == 0,
"grow truncated file");
235 for (off_t offset = 0; offset < extent;) {
236 size_t amount = extent - offset < (off_t)
sizeof(buffer) ? extent - offset :
sizeof(buffer);
237 require(pread(file, buffer, amount, offset) == (ssize_t)amount,
"read regrown extent");
238 for (
size_t i = 0; i < amount; ++i) {
239 require(buffer[i] == (offset + (off_t)i < prefix ? 0x5a : 0),
240 "truncate preserves prefix and zeroes regrowth");
245 require(ftruncate(file, -1) == -1 && errno == EINVAL,
"negative truncate length");
246 require(fstat(file, &status) == 0 && status.st_size == extent,
"failed truncate preserves size");
247 int readonly = open(
"resize", O_RDONLY);
248 require(readonly >= 0,
"open read-only truncate descriptor");
250 require(ftruncate(readonly, 0) == -1 && errno == EINVAL,
"read-only truncate rejected");
252 int truncated = open(
"resize", O_WRONLY | O_TRUNC);
253 require(truncated >= 0 && fstat(file, &status) == 0 && status.st_size == 0,
254 "O_TRUNC changes existing open inode");
256 require(ftruncate(file, 8193) == 0,
"grow empty file");
257 memset(buffer, 0xff,
sizeof(buffer));
258 require(pread(file, buffer,
sizeof(buffer), 4096) ==
sizeof(buffer),
"read empty-file growth");
259 for (
size_t i = 0; i <
sizeof(buffer); ++i) {
260 require(buffer[i] == 0,
"empty-file growth is zero filled");
262 require(fsync(file) == 0,
"sync resized file");
264 file = open(
"resize", O_RDONLY);
265 require(file >= 0 && fstat(file, &status) == 0 && status.st_size == 8193,
"reopen resized inode");
267 require(unlink(
"resize") == 0,
"resize cleanup");
268 printf(
"FS-MUTATION-CONTRACT: PASS %s truncation-and-zero-filling\n", filesystem);
271static void metadata_contracts(
int hardlinks) {
272 int file = create_file(
"metadata",
"data");
275 require(link(
"metadata",
"metadata-alias") == 0,
"create metadata alias");
276 writer = open(
"metadata-alias", O_RDWR);
277 require(writer >= 0,
"open metadata alias");
279 const struct utimbuf historical = {.actime = 11, .modtime = 22};
280 const struct timeval fractional[2] = {{33, 999999}, {44, 999999}};
283 require(syscall(SYS_utime,
"metadata", &historical) == 0 && fstat(file, &after) == 0 &&
284 after.st_atime == 11 && after.st_mtime == 22,
285 "utime stores seconds");
286 require(syscall(SYS_utimes,
"metadata", fractional) == 0 && fstat(file, &after) == 0 &&
287 after.st_atime == 33 && after.st_mtime == 44 && !after.st_atim.tv_nsec &&
288 !after.st_mtim.tv_nsec,
289 "utimes rounds down to filesystem seconds");
290 require(syscall(SYS_futimesat, AT_FDCWD,
"metadata", fractional) == 0 &&
291 fstat(file, &before) == 0 && before.st_atime == 33 && before.st_mtime == 44,
292 "futimesat stores seconds");
293 const struct utimbuf invalid_seconds[] = {{-1, 22}, {11, 0x100000000LL}};
294 for (
size_t i = 0; i <
sizeof(invalid_seconds) /
sizeof(invalid_seconds[0]); ++i) {
296 require(syscall(SYS_utime,
"metadata", &invalid_seconds[i]) == -1 && errno == EINVAL,
297 "utime rejects unrepresentable seconds");
299 const struct timeval invalid_fractional[][2] = {{{33, -1}, {44, 0}},
300 {{33, 0}, {44, 1000000}},
302 {{33, 0}, {0x100000000LL, 0}}};
303 for (
size_t i = 0; i <
sizeof(invalid_fractional) /
sizeof(invalid_fractional[0]); ++i) {
305 require(syscall(SYS_futimesat, AT_FDCWD,
"metadata", invalid_fractional[i]) == -1 &&
307 "futimesat rejects invalid timestamps");
309 require(fstat(file, &after) == 0 && after.st_atime == before.st_atime &&
310 after.st_mtime == before.st_mtime && after.st_ctime == before.st_ctime,
311 "invalid timestamps preserve metadata");
312 require(syscall(SYS_futimesat, AT_FDCWD,
"metadata", NULL) == 0 && fstat(file, &after) == 0 &&
313 after.st_atime > 44 && after.st_atime == after.st_mtime &&
314 after.st_mtime <= after.st_ctime,
315 "implicit timestamp uses current seconds");
317 require(syscall(SYS_utime,
"metadata", &historical) == 0 && fstat(file, &before) == 0,
318 "seed write timestamps");
320 write(writer,
"", 0) == 0 && pwrite(writer,
"", 0, 0) == 0 && writev(writer, NULL, 0) == 0,
321 "empty writes succeed");
323 require(pwrite(writer,
"x", 1, -1) == -1 && errno == EINVAL,
"negative write offset rejected");
324 require(fstat(file, &after) == 0 && after.st_atime == before.st_atime &&
325 after.st_mtime == before.st_mtime && after.st_ctime == before.st_ctime &&
326 after.st_size == before.st_size && after.st_blocks == before.st_blocks,
327 "zero-progress writes preserve metadata");
330 struct iovec vector[2] = {{bytes, 1}, {bytes + 1, 1}};
331 for (
int operation = 0; operation < 4; ++operation) {
332 require(syscall(SYS_utime,
"metadata", &historical) == 0 && lseek(writer, 0, SEEK_SET) == 0,
333 "reset overwrite timestamp");
335 if (operation == 1) {
336 written = pwrite(writer, bytes, 2, 0);
337 }
else if (operation == 2) {
338 written = writev(writer, vector, 2);
340 if (operation == 3) {
341 require(fcntl(writer, F_SETFL, O_APPEND) == 0,
"enable metadata append");
343 written = write(writer, bytes, 2);
345 require(written == 2 && fstat(file, &after) == 0 && after.st_atime == 11 &&
346 after.st_mtime > 22 && after.st_ctime == after.st_mtime,
347 "successful writes update modification and change times");
348 require(fstat(writer, &before) == 0 && before.st_mtime == after.st_mtime &&
349 before.st_ctime == after.st_ctime && before.st_blocks == after.st_blocks &&
350 before.st_size == after.st_size,
351 "write metadata agrees through both descriptors");
354 require(fcntl(writer, F_SETFL, 0) == 0 && pwrite(writer, bytes, 1, 3 * after.st_blksize) == 1 &&
355 fstat(writer, &before) == 0 && fstat(file, &after) == 0 &&
356 before.st_blocks == after.st_blocks &&
357 after.st_blocks == 4 * (after.st_blksize / 512),
358 "hardlinks share extended allocation counts");
360 require(unlink(
"metadata-alias") == 0,
"remove metadata alias");
363 require(unlink(
"metadata") == 0,
"remove metadata file");
364 printf(
"FS-MUTATION-CONTRACT: PASS %s ordinary-write-metadata\n", filesystem);
367static void ramfs_allocation_contracts(
void) {
368 const size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
369 const blkcnt_t sectors_per_page = page_size / 512;
370 int file = create_file(
"allocation",
"");
372 require(fstat(file, &status) == 0 && status.st_blocks == 0 &&
373 ftruncate(file, 3 * page_size) == 0 && fstat(file, &status) == 0 &&
374 status.st_blocks == 0,
375 "untouched ramfs growth allocates no pages");
376 require(pwrite(file,
"x", 1, 2 * page_size + 17) == 1 && fstat(file, &status) == 0 &&
377 status.st_blocks == sectors_per_page,
378 "ramfs accounts one touched page");
379 require(pwrite(file,
"x", 1, 0) == 1 && fstat(file, &status) == 0 &&
380 status.st_blocks == 2 * sectors_per_page,
381 "ramfs excludes untouched middle page");
382 require(ftruncate(file, page_size) == 0 && fstat(file, &status) == 0 &&
383 status.st_blocks == sectors_per_page && ftruncate(file, 0) == 0 &&
384 fstat(file, &status) == 0 && status.st_blocks == 0,
385 "ramfs truncation retires allocated pages");
387 require(unlink(
"allocation") == 0,
"remove allocation file");
388 printf(
"FS-MUTATION-CONTRACT: PASS %s allocated-page-accounting\n", filesystem);
391static void filesystem_contracts(
const char* base,
const char* label,
int hardlinks) {
394 snprintf(path,
sizeof(path),
"%s/fs-mutation-contract-%d", base, getpid());
395 int previous = open(
".", O_RDONLY | O_DIRECTORY);
396 require(previous >= 0 && mkdir(path, 0700) == 0 && chdir(path) == 0,
397 "create isolated contract directory");
398 replacement_contracts(hardlinks);
399 directory_contracts();
402 hardlink_mapping_contracts();
404 printf(
"FS-MUTATION-CONTRACT: SKIP %s terminal-symlinks unsupported-backend\n", filesystem);
406 truncate_contracts();
407 metadata_contracts(hardlinks);
409 ramfs_allocation_contracts();
411 require(fchdir(previous) == 0,
"restore working directory");
413 require(rmdir(path) == 0,
"remove isolated contract directory");
416void test_fs_mutation_contracts(
void) {
417 filesystem_contracts(
"/tmp",
"ramfs", 0);
418 filesystem_contracts(
"",
"root", 1);
419 int device = open(
"/dev/null", O_WRONLY | O_TRUNC);
420 require(device >= 0 && write(device,
"x", 1) == 1,
"O_TRUNC ignores character device");
422 require(ftruncate(device, 0) == -1 && errno == EINVAL,
"device truncate rejected");
424 puts(
"FS-MUTATION-CONTRACT: PASS all");