diff --git a/examples/fdtables-test-grate/Cargo.lock b/examples/fdtables-test-grate/Cargo.lock new file mode 100644 index 0000000..91a3b24 --- /dev/null +++ b/examples/fdtables-test-grate/Cargo.lock @@ -0,0 +1,203 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", + "serde", +] + +[[package]] +name = "fdtables" +version = "0.1.0" +source = "git+https://github.com/Lind-Project/lind-wasm?branch=main#1bed551e3ad0668804c79d066548b9c603a7ac4c" +dependencies = [ + "dashmap", + "lazy_static", + "libc", +] + +[[package]] +name = "fdtables-test-grate" +version = "0.1.0" +dependencies = [ + "fdtables", + "grate-rs", + "libc", +] + +[[package]] +name = "grate-rs" +version = "0.1.0" +source = "git+https://github.com/Lind-Project/lind-wasm-example-grates?branch=main#124ecb8092d363864af93b3c2319715de2fcec6d" +dependencies = [ + "libc", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" diff --git a/examples/fdtables-test-grate/Cargo.toml b/examples/fdtables-test-grate/Cargo.toml new file mode 100644 index 0000000..60ce909 --- /dev/null +++ b/examples/fdtables-test-grate/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "fdtables-test-grate" +version = "0.1.0" +edition = "2024" + +[dependencies] +grate-rs = { git = "https://github.com/Lind-Project/lind-wasm-example-grates", branch = "main", subdir = "lib/grate-rs" } +fdtables = { git = "https://github.com/Lind-Project/lind-wasm", branch = "main", subdir = "src/fdtables" } +libc = "0.2" diff --git a/examples/fdtables-test-grate/src/main.rs b/examples/fdtables-test-grate/src/main.rs new file mode 100644 index 0000000..34f510a --- /dev/null +++ b/examples/fdtables-test-grate/src/main.rs @@ -0,0 +1,273 @@ +//! fdtables stress-test grate +//! +//! Exercises fdtables operations under single-cage and cross-fork scenarios +//! to isolate Lind-WASM threading/DashMap issues from grate-specific logic. +//! +//! Intercepts: open, close, dup, dup2, fork, exec, read, write +//! All handlers do fdtables bookkeeping + forward the real syscall. +//! Minimal output — only prints on errors. + +use std::sync::Mutex; +use std::sync::atomic::{AtomicU64, Ordering}; + +use grate_rs::constants::*; +use grate_rs::{GrateBuilder, GrateError, copy_data_between_cages, getcageid, make_threei_call}; + +const FDT_KIND: u32 = 1; + +// ===================================================================== +// Mutex / atomic contention probes +// +// The write handler increments these on every call. After fork, both +// parent and child cage write through this grate, so the counters get +// hit from two runtime threads. +// +// MUTEX_COUNT: incremented under std::sync::Mutex. +// ATOMIC_COUNT: incremented with AtomicU64::fetch_add. +// UNSYNC_COUNT: incremented with NO synchronisation (plain read-modify-write). +// +// If Mutex actually works cross-thread: +// MUTEX_COUNT == ATOMIC_COUNT == total writes +// If Mutex is a no-op across threads: +// MUTEX_COUNT < ATOMIC_COUNT (lost updates) +// If atomics don't work either: +// ATOMIC_COUNT < total writes +// UNSYNC_COUNT is the control — expected to lose updates under concurrency. +// ===================================================================== + +static MUTEX_COUNTER: Mutex = Mutex::new(0); +static ATOMIC_COUNTER: AtomicU64 = AtomicU64::new(0); +static UNSYNC_COUNTER: AtomicU64 = AtomicU64::new(0); // abused as plain u64 via load+store + +/// Read counter values and write them back to the cage's buffer. +/// Triggered by read() on fd 99 (magic fd, see C test). +fn report_counters(cage_id: u64, buf_ptr: u64, buf_cage: u64) -> i32 { + let mutex_val = *MUTEX_COUNTER.lock().unwrap(); + let atomic_val = ATOMIC_COUNTER.load(Ordering::SeqCst); + let unsync_val = UNSYNC_COUNTER.load(Ordering::Relaxed); + + let report = format!("mutex={} atomic={} unsync={}\n", mutex_val, atomic_val, unsync_val); + let bytes = report.as_bytes(); + + let grate_cage = getcageid(); + let _ = copy_data_between_cages( + grate_cage, buf_cage, + bytes.as_ptr() as u64, grate_cage, + buf_ptr, buf_cage, + bytes.len() as u64, 0, + ); + + bytes.len() as i32 +} + +fn forward( + nr: u64, calling_cage: u64, + args: &[u64; 6], arg_cages: &[u64; 6], +) -> i32 { + let grate_cage = getcageid(); + match make_threei_call( + nr as u32, 0, grate_cage, calling_cage, + args[0], arg_cages[0], args[1], arg_cages[1], args[2], arg_cages[2], + args[3], arg_cages[3], args[4], arg_cages[4], args[5], arg_cages[5], 0, + ) { + Ok(r) => r, + Err(_) => -1, + } +} + +// ===================================================================== +// Handlers — quiet fdtables bookkeeping + forward +// ===================================================================== + +pub extern "C" fn open_handler( + _cageid: u64, + arg1: u64, arg1cage: u64, arg2: u64, arg2cage: u64, + arg3: u64, arg3cage: u64, arg4: u64, arg4cage: u64, + arg5: u64, arg5cage: u64, arg6: u64, arg6cage: u64, +) -> i32 { + let cage_id = arg1cage; + let ret = forward(SYS_OPEN, cage_id, + &[arg1, arg2, arg3, arg4, arg5, arg6], + &[arg1cage, arg2cage, arg3cage, arg4cage, arg5cage, arg6cage]); + if ret >= 0 { + let _ = fdtables::get_specific_virtual_fd( + cage_id, ret as u64, FDT_KIND, ret as u64, false, arg2); + } + ret +} + +pub extern "C" fn close_handler( + _cageid: u64, + arg1: u64, arg1cage: u64, arg2: u64, arg2cage: u64, + arg3: u64, arg3cage: u64, arg4: u64, arg4cage: u64, + arg5: u64, arg5cage: u64, arg6: u64, arg6cage: u64, +) -> i32 { + let cage_id = arg1cage; + let fd = arg1; + // Translate before close to stress fdtables lookups. + if fdtables::check_cage_exists(cage_id) { + let _ = fdtables::translate_virtual_fd(cage_id, fd); + } + let ret = forward(SYS_CLOSE, cage_id, + &[arg1, arg2, arg3, arg4, arg5, arg6], + &[arg1cage, arg2cage, arg3cage, arg4cage, arg5cage, arg6cage]); + if ret >= 0 && fdtables::check_cage_exists(cage_id) { + let _ = fdtables::close_virtualfd(cage_id, fd); + } + ret +} + +pub extern "C" fn dup_handler( + _cageid: u64, + arg1: u64, arg1cage: u64, arg2: u64, arg2cage: u64, + arg3: u64, arg3cage: u64, arg4: u64, arg4cage: u64, + arg5: u64, arg5cage: u64, arg6: u64, arg6cage: u64, +) -> i32 { + let cage_id = arg1cage; + let fd = arg1; + let ret = forward(SYS_DUP, cage_id, + &[arg1, arg2, arg3, arg4, arg5, arg6], + &[arg1cage, arg2cage, arg3cage, arg4cage, arg5cage, arg6cage]); + if ret >= 0 { + if let Ok(entry) = fdtables::translate_virtual_fd(cage_id, fd) { + let _ = fdtables::get_specific_virtual_fd( + cage_id, ret as u64, entry.fdkind, entry.underfd, + entry.should_cloexec, entry.perfdinfo); + } + } + ret +} + +pub extern "C" fn dup2_handler( + _cageid: u64, + arg1: u64, arg1cage: u64, arg2: u64, arg2cage: u64, + arg3: u64, arg3cage: u64, arg4: u64, arg4cage: u64, + arg5: u64, arg5cage: u64, arg6: u64, arg6cage: u64, +) -> i32 { + let cage_id = arg1cage; + let oldfd = arg1; + let newfd = arg2; + let ret = forward(SYS_DUP2, cage_id, + &[arg1, arg2, arg3, arg4, arg5, arg6], + &[arg1cage, arg2cage, arg3cage, arg4cage, arg5cage, arg6cage]); + if ret >= 0 { + if let Ok(entry) = fdtables::translate_virtual_fd(cage_id, oldfd) { + let _ = fdtables::get_specific_virtual_fd( + cage_id, newfd, entry.fdkind, entry.underfd, + entry.should_cloexec, entry.perfdinfo); + } + } + ret +} + +pub extern "C" fn fork_handler( + _cageid: u64, + arg1: u64, arg1cage: u64, arg2: u64, arg2cage: u64, + arg3: u64, arg3cage: u64, arg4: u64, arg4cage: u64, + arg5: u64, arg5cage: u64, arg6: u64, arg6cage: u64, +) -> i32 { + let cage_id = arg1cage; + let ret = forward(SYS_CLONE, cage_id, + &[arg1, arg2, arg3, arg4, arg5, arg6], + &[arg1cage, arg2cage, arg3cage, arg4cage, arg5cage, arg6cage]); + if ret <= 0 { return ret; } + + let child_cage_id = ret as u64; + let _ = fdtables::copy_fdtable_for_cage(cage_id, child_cage_id); + child_cage_id as i32 +} + +pub extern "C" fn exec_handler( + _cageid: u64, + arg1: u64, arg1cage: u64, arg2: u64, arg2cage: u64, + arg3: u64, arg3cage: u64, arg4: u64, arg4cage: u64, + arg5: u64, arg5cage: u64, arg6: u64, arg6cage: u64, +) -> i32 { + let cage_id = arg1cage; + fdtables::empty_fds_for_exec(cage_id); + for fd in 0..3u64 { + let _ = fdtables::get_specific_virtual_fd(cage_id, fd, 0, fd, false, 0); + } + forward(SYS_EXEC, cage_id, + &[arg1, arg2, arg3, arg4, arg5, arg6], + &[arg1cage, arg2cage, arg3cage, arg4cage, arg5cage, arg6cage]) +} + +pub extern "C" fn read_handler( + _cageid: u64, + arg1: u64, arg1cage: u64, arg2: u64, arg2cage: u64, + arg3: u64, arg3cage: u64, arg4: u64, arg4cage: u64, + arg5: u64, arg5cage: u64, arg6: u64, arg6cage: u64, +) -> i32 { + let cage_id = arg1cage; + let fd = arg1; + + // Magic fd 99: report counter values instead of forwarding. + if fd == 99 { + return report_counters(cage_id, arg2, arg2cage); + } + + if fdtables::check_cage_exists(cage_id) { + let _ = fdtables::translate_virtual_fd(cage_id, fd); + } + forward(SYS_READ, cage_id, + &[arg1, arg2, arg3, arg4, arg5, arg6], + &[arg1cage, arg2cage, arg3cage, arg4cage, arg5cage, arg6cage]) +} + +pub extern "C" fn write_handler( + _cageid: u64, + arg1: u64, arg1cage: u64, arg2: u64, arg2cage: u64, + arg3: u64, arg3cage: u64, arg4: u64, arg4cage: u64, + arg5: u64, arg5cage: u64, arg6: u64, arg6cage: u64, +) -> i32 { + let cage_id = arg1cage; + let fd = arg1; + + // Bump contention counters on every write (except stdout/stderr). + if fd > 2 { + // Mutex-protected increment. + { + let mut count = MUTEX_COUNTER.lock().unwrap(); + *count += 1; + } + // Atomic increment. + ATOMIC_COUNTER.fetch_add(1, Ordering::SeqCst); + // Unsynchronised increment (deliberate race — control group). + let v = UNSYNC_COUNTER.load(Ordering::Relaxed); + UNSYNC_COUNTER.store(v + 1, Ordering::Relaxed); + } + + if fdtables::check_cage_exists(cage_id) { + let _ = fdtables::translate_virtual_fd(cage_id, fd); + } + forward(SYS_WRITE, cage_id, + &[arg1, arg2, arg3, arg4, arg5, arg6], + &[arg1cage, arg2cage, arg3cage, arg4cage, arg5cage, arg6cage]) +} + +fn main() { + let argv: Vec = std::env::args().skip(1).collect(); + GrateBuilder::new() + .register(SYS_OPEN, open_handler) + .register(SYS_CLOSE, close_handler) + .register(SYS_DUP, dup_handler) + .register(SYS_DUP2, dup2_handler) + .register(SYS_READ, read_handler) + .register(SYS_WRITE, write_handler) + .register(SYS_CLONE, fork_handler) + .register(SYS_EXEC, exec_handler) + .preexec(|child_cage: i32| { + let cage_id = child_cage as u64; + fdtables::init_empty_cage(cage_id); + for fd in 0..3u64 { + let _ = fdtables::get_specific_virtual_fd(cage_id, fd, 0, fd, false, 0); + } + }) + .teardown(|result: Result| { + if let Err(e) = result { + eprintln!("[fdt-test] error: {:?}", e); + } + }) + .run(argv); +} diff --git a/examples/fdtables-test-grate/test/fdtables_test.c b/examples/fdtables-test-grate/test/fdtables_test.c new file mode 100644 index 0000000..086cbc3 --- /dev/null +++ b/examples/fdtables-test-grate/test/fdtables_test.c @@ -0,0 +1,854 @@ +/* + * fdtables stress test — exercises fdtables operations to isolate + * cross-thread DashMap issues from grate-specific logic. + * + * Tests are ordered from simple (single-cage, no fork) to complex + * (fork + concurrent fd operations). Each test prints PASS/FAIL so + * we can see exactly where fdtables breaks. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +static int tests_run = 0; +static int tests_passed = 0; + +#define CHECK(desc, cond) do { \ + tests_run++; \ + if (cond) { \ + printf(" PASS: %s\n", desc); \ + tests_passed++; \ + } else { \ + printf(" FAIL: %s (errno=%d)\n", desc, errno); \ + } \ +} while (0) + +/* ── Test 0: Mutex/atomic contention probe ─────────────────────────── */ +/* Parent and child both write 500 times to a file. The grate bumps + * three counters on each write: one under Mutex, one with fetch_add, + * one with unsynchronised load+store. After both finish, we read the + * counters via magic fd 99 and check if Mutex/atomic actually work. + * Expected total = 1000 (500 parent + 500 child). */ + +static void test_mutex_atomic_probe(void) { + printf("\n[test_mutex_atomic_probe]\n"); + + int fd = open("/tmp/fdt_probe.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + CHECK("open succeeds", fd >= 0); + + pid_t pid = fork(); + if (pid == 0) { + /* Child: 500 writes. */ + for (int i = 0; i < 500; i++) { + write(fd, "c", 1); + } + close(fd); + _exit(0); + } + + /* Parent: 500 writes (concurrent with child). */ + for (int i = 0; i < 500; i++) { + write(fd, "p", 1); + } + + int status; + waitpid(pid, &status, 0); + CHECK("child exited cleanly", WIFEXITED(status) && WEXITSTATUS(status) == 0); + + close(fd); + unlink("/tmp/fdt_probe.txt"); + + /* Read counters from the grate via magic fd 99. */ + char buf[128] = {0}; + ssize_t nr = read(99, buf, sizeof(buf) - 1); + if (nr <= 0) { + printf(" FAIL: couldn't read counters from grate (nr=%zd)\n", nr); + tests_run++; + return; + } + buf[nr] = '\0'; + + unsigned long mutex_val = 0, atomic_val = 0, unsync_val = 0; + sscanf(buf, "mutex=%lu atomic=%lu unsync=%lu", &mutex_val, &atomic_val, &unsync_val); + + printf(" counters: mutex=%lu atomic=%lu unsync=%lu (expected 1000)\n", + mutex_val, atomic_val, unsync_val); + + CHECK("atomic counter == 1000", atomic_val == 1000); + CHECK("mutex counter == 1000 (mutex works cross-thread)", mutex_val == 1000); + if (unsync_val < 1000) { + printf(" (unsync=%lu < 1000 — expected, confirms real concurrency)\n", unsync_val); + } else { + printf(" (unsync=%lu == 1000 — no concurrency detected, single-threaded?)\n", unsync_val); + } +} + +/* ── Test 1: Single open/close ─────────────────────────────────────── */ + +static void test_single_open_close(void) { + printf("\n[test_single_open_close]\n"); + + int fd = open("/tmp/fdt_test1.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + CHECK("open succeeds", fd >= 0); + + int ret = close(fd); + CHECK("close succeeds", ret == 0); + + unlink("/tmp/fdt_test1.txt"); +} + +/* ── Test 2: Multiple opens — fdtables tracks many fds ─────────────── */ + +static void test_many_opens(void) { + printf("\n[test_many_opens]\n"); + + int fds[20]; + char path[64]; + int i; + + for (i = 0; i < 20; i++) { + snprintf(path, sizeof(path), "/tmp/fdt_many_%d.txt", i); + fds[i] = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fds[i] < 0) break; + } + CHECK("opened 20 fds", i == 20); + + /* Close in reverse order. */ + for (int j = 19; j >= 0; j--) { + close(fds[j]); + snprintf(path, sizeof(path), "/tmp/fdt_many_%d.txt", j); + unlink(path); + } + CHECK("closed all 20", 1); +} + +/* ── Test 3: Dup — fdtables tracks duplicated fds ──────────────────── */ + +static void test_dup(void) { + printf("\n[test_dup]\n"); + + int fd = open("/tmp/fdt_dup.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + CHECK("open succeeds", fd >= 0); + + int fd2 = dup(fd); + CHECK("dup succeeds", fd2 >= 0); + CHECK("dup returns different fd", fd2 != fd); + + /* Write through dup'd fd, read through original. */ + write(fd2, "hello", 5); + lseek(fd, 0, SEEK_SET); + char buf[16] = {0}; + int nr = read(fd, buf, 5); + CHECK("read through original fd", nr == 5); + CHECK("data matches", memcmp(buf, "hello", 5) == 0); + + close(fd2); + close(fd); + unlink("/tmp/fdt_dup.txt"); +} + +/* ── Test 4: Dup2 — fdtables handles fd replacement ────────────────── */ + +static void test_dup2(void) { + printf("\n[test_dup2]\n"); + + int fd1 = open("/tmp/fdt_dup2_a.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + int fd2 = open("/tmp/fdt_dup2_b.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + CHECK("open both", fd1 >= 0 && fd2 >= 0); + + /* dup2 fd1 onto fd2 — fd2 now points to fd1's file. */ + int ret = dup2(fd1, fd2); + CHECK("dup2 succeeds", ret == fd2); + + write(fd2, "dup2data", 8); + lseek(fd1, 0, SEEK_SET); + char buf[16] = {0}; + int nr = read(fd1, buf, 8); + CHECK("write via dup2'd fd visible on original", nr == 8); + + close(fd1); + close(fd2); + unlink("/tmp/fdt_dup2_a.txt"); + unlink("/tmp/fdt_dup2_b.txt"); +} + +/* ── Test 5: Rapid open/close cycle — stress fdtables allocation ───── */ + +static void test_rapid_cycle(void) { + printf("\n[test_rapid_cycle]\n"); + + int ok = 1; + for (int i = 0; i < 100; i++) { + int fd = open("/tmp/fdt_rapid.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { ok = 0; break; } + close(fd); + } + CHECK("100 open/close cycles", ok); + unlink("/tmp/fdt_rapid.txt"); +} + +/* ── Test 6: Fork — child inherits parent's fds ───────────────────── */ + +static void test_fork_inherit(void) { + printf("\n[test_fork_inherit]\n"); + + int fd = open("/tmp/fdt_fork.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + CHECK("open succeeds", fd >= 0); + + write(fd, "parent wrote this", 17); + + pid_t pid = fork(); + if (pid == 0) { + /* Child: read from the inherited fd. */ + lseek(fd, 0, SEEK_SET); + char buf[32] = {0}; + int nr = read(fd, buf, 17); + if (nr == 17 && memcmp(buf, "parent wrote this", 17) == 0) { + _exit(0); /* success */ + } + _exit(1); /* failure */ + } + + int status; + waitpid(pid, &status, 0); + CHECK("child read inherited fd", WIFEXITED(status) && WEXITSTATUS(status) == 0); + + close(fd); + unlink("/tmp/fdt_fork.txt"); +} + +/* ── Test 7: Fork + close in child — fdtables tracks per-cage ──────── */ + +static void test_fork_close_in_child(void) { + printf("\n[test_fork_close_in_child]\n"); + + int fd = open("/tmp/fdt_fork_close.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + CHECK("open succeeds", fd >= 0); + + pid_t pid = fork(); + if (pid == 0) { + /* Child closes the fd. */ + close(fd); + _exit(0); + } + + int status; + waitpid(pid, &status, 0); + CHECK("child exited cleanly", WIFEXITED(status) && WEXITSTATUS(status) == 0); + + /* Parent's fd should still be valid. */ + int ret = write(fd, "still open", 10); + CHECK("parent fd still valid after child close", ret == 10); + + close(fd); + unlink("/tmp/fdt_fork_close.txt"); +} + +/* ── Test 8: Fork + open in child — child gets its own fds ─────────── */ + +static void test_fork_open_in_child(void) { + printf("\n[test_fork_open_in_child]\n"); + + pid_t pid = fork(); + if (pid == 0) { + int fd = open("/tmp/fdt_child_open.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) _exit(1); + write(fd, "child", 5); + close(fd); + _exit(0); + } + + int status; + waitpid(pid, &status, 0); + CHECK("child opened+wrote+closed its own fd", + WIFEXITED(status) && WEXITSTATUS(status) == 0); + + unlink("/tmp/fdt_child_open.txt"); +} + +/* ── Test 9: Fork + dup in child ───────────────────────────────────── */ + +static void test_fork_dup_in_child(void) { + printf("\n[test_fork_dup_in_child]\n"); + + int fd = open("/tmp/fdt_fork_dup.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + CHECK("open succeeds", fd >= 0); + + pid_t pid = fork(); + if (pid == 0) { + int fd2 = dup(fd); + if (fd2 < 0) _exit(1); + write(fd2, "duped", 5); + close(fd2); + close(fd); + _exit(0); + } + + int status; + waitpid(pid, &status, 0); + CHECK("child dup'd and wrote", WIFEXITED(status) && WEXITSTATUS(status) == 0); + + close(fd); + unlink("/tmp/fdt_fork_dup.txt"); +} + +/* ── Test 10: Stress — rapid fork + fd ops ─────────────────────────── */ + +static void test_fork_stress(void) { + printf("\n[test_fork_stress]\n"); + + int ok = 1; + for (int i = 0; i < 5; i++) { + int fd = open("/tmp/fdt_stress.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { ok = 0; break; } + + pid_t pid = fork(); + if (pid == 0) { + write(fd, "x", 1); + close(fd); + _exit(0); + } + + int status; + waitpid(pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + ok = 0; + break; + } + close(fd); + } + CHECK("5 fork+write+close cycles", ok); + unlink("/tmp/fdt_stress.txt"); +} + +/* ── Test 11: Multiple concurrent fds across fork ──────────────────── */ + +static void test_fork_many_fds(void) { + printf("\n[test_fork_many_fds]\n"); + + int fds[10]; + char path[64]; + + for (int i = 0; i < 10; i++) { + snprintf(path, sizeof(path), "/tmp/fdt_fmany_%d.txt", i); + fds[i] = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fds[i] < 0) { + printf(" FAIL: couldn't open fd %d\n", i); + tests_run++; + return; + } + } + + pid_t pid = fork(); + if (pid == 0) { + /* Child: close odd fds, write to even fds. */ + for (int i = 0; i < 10; i++) { + if (i % 2 == 1) { + close(fds[i]); + } else { + write(fds[i], "c", 1); + } + } + /* Close remaining. */ + for (int i = 0; i < 10; i += 2) { + close(fds[i]); + } + _exit(0); + } + + int status; + waitpid(pid, &status, 0); + CHECK("child handled 10 fds (close odd, write even)", + WIFEXITED(status) && WEXITSTATUS(status) == 0); + + /* Parent closes all. */ + for (int i = 0; i < 10; i++) { + close(fds[i]); + snprintf(path, sizeof(path), "/tmp/fdt_fmany_%d.txt", i); + unlink(path); + } +} + +/* ── Test 12: Rapid fork+close stress (50 iterations) ──────────────── */ + +static void test_rapid_fork_close(void) { + printf("\n[test_rapid_fork_close]\n"); + + int ok = 1; + for (int i = 0; i < 50; i++) { + int fd = open("/tmp/fdt_rfc.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { ok = 0; break; } + + pid_t pid = fork(); + if (pid == 0) { + close(fd); + _exit(0); + } + int status; + waitpid(pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + ok = 0; break; + } + /* Parent verifies its fd is still valid. */ + if (write(fd, "x", 1) != 1) { ok = 0; break; } + close(fd); + } + CHECK("50 rapid fork+close cycles", ok); + unlink("/tmp/fdt_rfc.txt"); +} + +/* ── Test 13: Child does heavy fd churn after fork ─────────────────── */ + +static void test_fork_child_churn(void) { + printf("\n[test_fork_child_churn]\n"); + + /* Open some fds before fork. */ + int pre_fds[5]; + char path[64]; + for (int i = 0; i < 5; i++) { + snprintf(path, sizeof(path), "/tmp/fdt_churn_pre_%d.txt", i); + pre_fds[i] = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); + } + + pid_t pid = fork(); + if (pid == 0) { + /* Child: close inherited, open new, dup, close, repeat. */ + for (int i = 0; i < 5; i++) close(pre_fds[i]); + for (int round = 0; round < 20; round++) { + snprintf(path, sizeof(path), "/tmp/fdt_churn_c_%d.txt", round); + int fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) _exit(1); + int fd2 = dup(fd); + if (fd2 < 0) _exit(2); + write(fd, "x", 1); + write(fd2, "y", 1); + close(fd2); + close(fd); + unlink(path); + } + _exit(0); + } + + int status; + waitpid(pid, &status, 0); + CHECK("child did 20 rounds of open/dup/write/close", + WIFEXITED(status) && WEXITSTATUS(status) == 0); + + for (int i = 0; i < 5; i++) { + close(pre_fds[i]); + snprintf(path, sizeof(path), "/tmp/fdt_churn_pre_%d.txt", i); + unlink(path); + } +} + +/* ── Test 14: Parent and child both do fd ops after fork ───────────── */ + +static void test_fork_concurrent_ops(void) { + printf("\n[test_fork_concurrent_ops]\n"); + + int fds[10]; + char path[64]; + for (int i = 0; i < 10; i++) { + snprintf(path, sizeof(path), "/tmp/fdt_conc_%d.txt", i); + fds[i] = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fds[i] < 0) { + printf(" FAIL: setup open %d\n", i); + tests_run++; + return; + } + } + + pid_t pid = fork(); + if (pid == 0) { + /* Child: write to even fds, close odd fds, open new fds. */ + for (int i = 0; i < 10; i++) { + if (i % 2 == 0) { + write(fds[i], "child", 5); + } else { + close(fds[i]); + } + } + /* Open some new fds. */ + for (int i = 0; i < 5; i++) { + snprintf(path, sizeof(path), "/tmp/fdt_conc_child_%d.txt", i); + int fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd >= 0) { + write(fd, "new", 3); + close(fd); + unlink(path); + } + } + for (int i = 0; i < 10; i += 2) close(fds[i]); + _exit(0); + } + + /* Parent: write to odd fds, close even fds simultaneously. */ + for (int i = 0; i < 10; i++) { + if (i % 2 == 1) { + write(fds[i], "parent", 6); + } else { + close(fds[i]); + } + } + + int status; + waitpid(pid, &status, 0); + CHECK("parent+child concurrent fd ops", + WIFEXITED(status) && WEXITSTATUS(status) == 0); + + for (int i = 1; i < 10; i += 2) close(fds[i]); + for (int i = 0; i < 10; i++) { + snprintf(path, sizeof(path), "/tmp/fdt_conc_%d.txt", i); + unlink(path); + } +} + +/* ── Test 15: Chain of forks — grandchild fd inheritance ───────────── */ + +static void test_fork_chain(void) { + printf("\n[test_fork_chain]\n"); + + int fd = open("/tmp/fdt_chain.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { CHECK("open", 0); return; } + + pid_t pid = fork(); + if (pid == 0) { + /* Child: write, then fork again. */ + write(fd, "child1-", 7); + + pid_t pid2 = fork(); + if (pid2 == 0) { + /* Grandchild: write and exit. */ + write(fd, "child2", 6); + close(fd); + _exit(0); + } + int status; + waitpid(pid2, &status, 0); + close(fd); + _exit(WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : 1); + } + + int status; + waitpid(pid, &status, 0); + CHECK("grandchild fork chain", + WIFEXITED(status) && WEXITSTATUS(status) == 0); + + /* Verify both wrote. */ + lseek(fd, 0, SEEK_SET); + char buf[32] = {0}; + int nr = read(fd, buf, sizeof(buf)); + CHECK("chain wrote 13 bytes", nr == 13); + + close(fd); + unlink("/tmp/fdt_chain.txt"); +} + +/* ── Test 16: Dup2 storm — rapid fd replacement ────────────────────── */ + +static void test_dup2_storm(void) { + printf("\n[test_dup2_storm]\n"); + + int fd = open("/tmp/fdt_dup2storm.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { CHECK("open", 0); return; } + + /* Dup2 the fd onto a bunch of targets repeatedly. */ + int ok = 1; + for (int round = 0; round < 10; round++) { + for (int target = 100; target < 110; target++) { + if (dup2(fd, target) != target) { ok = 0; break; } + } + if (!ok) break; + /* Close the targets. */ + for (int target = 100; target < 110; target++) { + close(target); + } + } + CHECK("10 rounds of dup2 onto fds 100-109", ok); + + close(fd); + unlink("/tmp/fdt_dup2storm.txt"); +} + +/* ── Test 17: Fork bomb — many sequential forks ────────────────────── */ + +static void test_fork_bomb(void) { + printf("\n[test_fork_bomb]\n"); + + int ok = 1; + for (int i = 0; i < 20; i++) { + pid_t pid = fork(); + if (pid < 0) { ok = 0; break; } + if (pid == 0) { + _exit(0); + } + int status; + waitpid(pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + ok = 0; break; + } + } + CHECK("20 sequential forks", ok); +} + +/* ── Test 18: Fork + immediate close all inherited fds ─────────────── */ + +static void test_fork_close_all(void) { + printf("\n[test_fork_close_all]\n"); + + int fds[15]; + char path[64]; + for (int i = 0; i < 15; i++) { + snprintf(path, sizeof(path), "/tmp/fdt_fca_%d.txt", i); + fds[i] = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fds[i] < 0) { CHECK("setup", 0); return; } + } + + pid_t pid = fork(); + if (pid == 0) { + /* Child: immediately close all 15. */ + for (int i = 0; i < 15; i++) close(fds[i]); + _exit(0); + } + + int status; + waitpid(pid, &status, 0); + CHECK("child closed all 15 inherited fds", + WIFEXITED(status) && WEXITSTATUS(status) == 0); + + /* Parent: verify its fds still work. */ + int ok = 1; + for (int i = 0; i < 15; i++) { + if (write(fds[i], "p", 1) != 1) { ok = 0; break; } + close(fds[i]); + snprintf(path, sizeof(path), "/tmp/fdt_fca_%d.txt", i); + unlink(path); + } + CHECK("parent fds still valid", ok); +} + +/* ── Test 19: Fork + child does dup2 storm ─────────────────────────── */ + +static void test_fork_child_dup2_storm(void) { + printf("\n[test_fork_child_dup2_storm]\n"); + + int fd = open("/tmp/fdt_fcd2.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { CHECK("open", 0); return; } + + pid_t pid = fork(); + if (pid == 0) { + for (int round = 0; round < 10; round++) { + for (int target = 50; target < 60; target++) { + if (dup2(fd, target) != target) _exit(1); + } + for (int target = 50; target < 60; target++) { + close(target); + } + } + close(fd); + _exit(0); + } + + int status; + waitpid(pid, &status, 0); + CHECK("child did 10 rounds of dup2 storm", + WIFEXITED(status) && WEXITSTATUS(status) == 0); + + close(fd); + unlink("/tmp/fdt_fcd2.txt"); +} + +/* ── Test 20: Deep fork chain (4 levels) ───────────────────────────── */ + +static void test_deep_fork_chain(void) { + printf("\n[test_deep_fork_chain]\n"); + + int fd = open("/tmp/fdt_deep.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { CHECK("open", 0); return; } + + pid_t pid = fork(); + if (pid == 0) { + write(fd, "L1-", 3); + pid_t p2 = fork(); + if (p2 == 0) { + write(fd, "L2-", 3); + pid_t p3 = fork(); + if (p3 == 0) { + write(fd, "L3-", 3); + pid_t p4 = fork(); + if (p4 == 0) { + write(fd, "L4", 2); + close(fd); + _exit(0); + } + int s; waitpid(p4, &s, 0); + close(fd); + _exit(WIFEXITED(s) && WEXITSTATUS(s) == 0 ? 0 : 1); + } + int s; waitpid(p3, &s, 0); + close(fd); + _exit(WIFEXITED(s) && WEXITSTATUS(s) == 0 ? 0 : 1); + } + int s; waitpid(p2, &s, 0); + close(fd); + _exit(WIFEXITED(s) && WEXITSTATUS(s) == 0 ? 0 : 1); + } + + int status; + waitpid(pid, &status, 0); + CHECK("4-level fork chain", WIFEXITED(status) && WEXITSTATUS(status) == 0); + + lseek(fd, 0, SEEK_SET); + char buf[32] = {0}; + int nr = read(fd, buf, sizeof(buf)); + CHECK("all 4 levels wrote (11 bytes)", nr == 11); + + close(fd); + unlink("/tmp/fdt_deep.txt"); +} + +/* ── Test 21: Fork + both parent and child open new fds concurrently ─ */ + +static void test_fork_concurrent_opens(void) { + printf("\n[test_fork_concurrent_opens]\n"); + + pid_t pid = fork(); + if (pid == 0) { + /* Child: open 10 files rapidly. */ + char path[64]; + int ok = 1; + for (int i = 0; i < 10; i++) { + snprintf(path, sizeof(path), "/tmp/fdt_co_c_%d.txt", i); + int fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { ok = 0; break; } + write(fd, "child", 5); + close(fd); + unlink(path); + } + _exit(ok ? 0 : 1); + } + + /* Parent: also open 10 files concurrently. */ + char path[64]; + int ok = 1; + for (int i = 0; i < 10; i++) { + snprintf(path, sizeof(path), "/tmp/fdt_co_p_%d.txt", i); + int fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { ok = 0; break; } + write(fd, "parent", 6); + close(fd); + unlink(path); + } + + int status; + waitpid(pid, &status, 0); + CHECK("concurrent parent+child opens", + ok && WIFEXITED(status) && WEXITSTATUS(status) == 0); +} + +/* ── Test 22: Multiple children from one parent ────────────────────── */ + +static void test_multi_child(void) { + printf("\n[test_multi_child]\n"); + + int fd = open("/tmp/fdt_mc.txt", O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { CHECK("open", 0); return; } + + pid_t pids[5]; + for (int i = 0; i < 5; i++) { + pids[i] = fork(); + if (pids[i] == 0) { + char c = '0' + i; + write(fd, &c, 1); + close(fd); + _exit(0); + } + } + + int ok = 1; + for (int i = 0; i < 5; i++) { + int status; + waitpid(pids[i], &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) ok = 0; + } + CHECK("5 children from one parent", ok); + + lseek(fd, 0, SEEK_SET); + char buf[16] = {0}; + int nr = read(fd, buf, sizeof(buf)); + CHECK("all 5 children wrote (5 bytes)", nr == 5); + + close(fd); + unlink("/tmp/fdt_mc.txt"); +} + +/* ── Test 23: Fork + child immediately forks again (rapid double) ──── */ + +static void test_rapid_double_fork(void) { + printf("\n[test_rapid_double_fork]\n"); + + int ok = 1; + for (int i = 0; i < 10; i++) { + pid_t pid = fork(); + if (pid == 0) { + pid_t pid2 = fork(); + if (pid2 == 0) _exit(0); + int s; waitpid(pid2, &s, 0); + _exit(WIFEXITED(s) && WEXITSTATUS(s) == 0 ? 0 : 1); + } + int status; + waitpid(pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + ok = 0; break; + } + } + CHECK("10 rapid double-forks", ok); +} + +/* ═══════════════════════════════════════════════════════════════════ */ + +int main(void) { + printf("=== fdtables stress test ===\n"); + + /* Mutex/atomic probe — run first to diagnose cross-thread sync. */ + test_mutex_atomic_probe(); + + /* Single-cage tests (no fork). */ + test_single_open_close(); + test_many_opens(); + test_dup(); + test_dup2(); + test_rapid_cycle(); + + /* Basic fork tests. */ + test_fork_inherit(); + test_fork_close_in_child(); + test_fork_open_in_child(); + test_fork_dup_in_child(); + test_fork_stress(); + test_fork_many_fds(); + + /* Heavy stress tests. */ + test_rapid_fork_close(); + test_fork_child_churn(); + test_fork_concurrent_ops(); + test_fork_chain(); + test_dup2_storm(); + + /* Fork/exec heavy tests. */ + test_fork_bomb(); + test_fork_close_all(); + test_fork_child_dup2_storm(); + test_deep_fork_chain(); + test_fork_concurrent_opens(); + test_multi_child(); + test_rapid_double_fork(); + + printf("\n=== Results: %d/%d passed ===\n", tests_passed, tests_run); + return (tests_passed == tests_run) ? 0 : 1; +}