Implements the core design: a mount table published as an atomically- swapped snapshot; mem/dir/pack/overlay backends; copy-on-write overlay with copy-up and whiteout deletion; a checksummed append journal; compaction with exact-duplicate elimination; single-writer/wait-free- reader concurrency with a structural/content write split; openat2/ Landlock path containment for dir mounts; and load-time pack integrity validation. Zero required third-party dependencies. Sanitizer testing (ASan/UBSan) caught and led to fixing a genuine heap-use-after-free in the snapshot-reclamation path: the textbook "load pointer, then increment its refcount" pattern left a gap a concurrent writer could free through. Closed with a small reclaim_gate rwlock, documented in internal.h and CLAUDE.md since it's a pattern every refcounted structure in the codebase now follows. zip/tar import/export backends, recommended in concept.md Section 11, will not be built — a permanent project decision recorded in CLAUDE.md since concept.md itself is frozen and cannot be edited to reflect it. Includes a runnable demo (examples/demo.c, `make demo`) exercising the library end to end and proving cross-run persistence through the pack file, plus open-source scaffolding: MIT license, README, CONTRIBUTING, and a CI workflow running the test suite under ASan/UBSan/TSan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqJpkdJ6Njnt1pw3CbghzB
87 lines
3.1 KiB
C
87 lines
3.1 KiB
C
/* test_concurrency.c — exercises Section 5.3's core promise: readers
|
|
* never observe a torn snapshot while writers race structural changes,
|
|
* and Section 5.7's buffer-growth guard doesn't crash or corrupt under
|
|
* concurrent grow-and-read. Not a formal proof, but real thread
|
|
* interleavings under a sanitizer-friendly build catch real races. */
|
|
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "packfs.h"
|
|
#include "test_harness.h"
|
|
|
|
#define WRITER_THREADS 4
|
|
#define READER_THREADS 4
|
|
#define ITERATIONS 500
|
|
|
|
static Vfs *g_v;
|
|
|
|
static void *writer_thread(void *arg) {
|
|
long id = (long)arg;
|
|
char path[64];
|
|
for (int i = 0; i < ITERATIONS; i++) {
|
|
snprintf(path, sizeof(path), "/t%ld/f%d.txt", id, i % 8);
|
|
int err = 0;
|
|
VfsFile *f = vfs_open(g_v, path, VFS_O_WRONLY | VFS_O_CREAT | VFS_O_TRUNC, &err);
|
|
if (!f) continue;
|
|
char payload[128];
|
|
int n = snprintf(payload, sizeof(payload), "thread=%ld iter=%d %.*s", id, i, (i % 50), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
|
|
vfs_write(f, payload, (pfs_usize)n);
|
|
vfs_close(f);
|
|
if (i % 37 == 0) vfs_unlink(g_v, path);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void *reader_thread(void *arg) {
|
|
(void)arg;
|
|
char path[64];
|
|
for (int i = 0; i < ITERATIONS; i++) {
|
|
for (long id = 0; id < WRITER_THREADS; id++) {
|
|
snprintf(path, sizeof(path), "/t%ld/f%d.txt", id, i % 8);
|
|
VfsStat st;
|
|
if (vfs_stat(g_v, path, &st) != VFS_OK) continue;
|
|
int err = 0;
|
|
VfsFile *f = vfs_open(g_v, path, VFS_O_RDONLY, &err);
|
|
if (!f) continue;
|
|
char buf[256];
|
|
pfs_isize r = vfs_read(f, buf, sizeof(buf) - 1);
|
|
/* No CHECK on content here: a concurrent writer may legitimately
|
|
* replace the file between stat and open/read (Section 5.3
|
|
* guarantees a consistent snapshot per call, not across calls).
|
|
* What we're really testing is the absence of a crash, a
|
|
* negative-but-uncaught read length, or a hang. */
|
|
CHECK(r >= 0);
|
|
vfs_close(f);
|
|
}
|
|
VfsDir dir;
|
|
if (vfs_readdir(g_v, "/", &dir) == VFS_OK) vfs_dir_free(&dir);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int main(void) {
|
|
g_v = vfs_new();
|
|
Backend *mem = backend_mem_new();
|
|
CHECK_EQ_INT(vfs_mount(g_v, "/", mem), VFS_OK);
|
|
|
|
pthread_t writers[WRITER_THREADS], readers[READER_THREADS];
|
|
for (long i = 0; i < WRITER_THREADS; i++) pthread_create(&writers[i], NULL, writer_thread, (void *)i);
|
|
for (long i = 0; i < READER_THREADS; i++) pthread_create(&readers[i], NULL, reader_thread, (void *)i);
|
|
for (int i = 0; i < WRITER_THREADS; i++) pthread_join(writers[i], NULL);
|
|
for (int i = 0; i < READER_THREADS; i++) pthread_join(readers[i], NULL);
|
|
|
|
/* sanity: the store is still fully consistent after the race */
|
|
VfsDir dir;
|
|
CHECK_EQ_INT(vfs_readdir(g_v, "/", &dir), VFS_OK);
|
|
CHECK(dir.count > 0);
|
|
vfs_dir_free(&dir);
|
|
|
|
vfs_unmount(g_v, "/");
|
|
backend_free(mem);
|
|
vfs_free(g_v);
|
|
TEST_MAIN_END();
|
|
}
|