From d989669e930f7e0176e8009153f1e32628a2ce17 Mon Sep 17 00:00:00 2001 From: Ashwin Nandan Date: Tue, 14 Jul 2026 12:40:09 -0400 Subject: [PATCH 1/2] Add cross cage memory isolation tests --- .../deterministic/cross_cage_fork_no_leak.c | 171 ++++++++++++++++++ .../deterministic/cross_cage_syscall_efault.c | 58 ++++++ .../fail/cross_cage_wild_pointer.c | 42 +++++ 3 files changed, 271 insertions(+) create mode 100755 tests/unit-tests/memory_tests/deterministic/cross_cage_fork_no_leak.c create mode 100755 tests/unit-tests/memory_tests/deterministic/cross_cage_syscall_efault.c create mode 100755 tests/unit-tests/memory_tests/fail/cross_cage_wild_pointer.c diff --git a/tests/unit-tests/memory_tests/deterministic/cross_cage_fork_no_leak.c b/tests/unit-tests/memory_tests/deterministic/cross_cage_fork_no_leak.c new file mode 100755 index 000000000..5d5dfc3eb --- /dev/null +++ b/tests/unit-tests/memory_tests/deterministic/cross_cage_fork_no_leak.c @@ -0,0 +1,171 @@ +#undef _GNU_SOURCE +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +// Test: fork memory isolation +// Verifies that parent and child cages don't share writable memory + +#define PARENT_SENTINEL 0xA1 +#define CHILD_SENTINEL 0xB2 + +#define BUF_SIZE (256 * 1024) + +static void wait_child(pid_t pid) +{ + int status; + pid_t ret = waitpid(pid, &status, 0); + + assert(ret >= 0); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == 0); +} + +static void assert_all(const unsigned char *buf, size_t size, + unsigned char value) +{ + for (size_t i = 0; i < size; i++) + assert(buf[i] == value); +} + +int main(void) +{ + pid_t pid; + + // Test 1: Heap memory + unsigned char *heap = malloc(BUF_SIZE); + assert(heap != NULL); + + memset(heap, PARENT_SENTINEL, BUF_SIZE); + + pid = fork(); + assert(pid >= 0); + + if (pid == 0) { + // Child sees initial contents + assert_all(heap, BUF_SIZE, PARENT_SENTINEL); + + // Child writes should not affect parent memory + memset(heap, CHILD_SENTINEL, BUF_SIZE); + assert_all(heap, BUF_SIZE, CHILD_SENTINEL); + + exit(0); + } + + wait_child(pid); + + // Parent should still see its original data + assert_all(heap, BUF_SIZE, PARENT_SENTINEL); + + free(heap); + + + // Test 2: anonymous mmap + unsigned char *mapped = mmap(NULL, BUF_SIZE, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); + assert(mapped != MAP_FAILED); + + memset(mapped, PARENT_SENTINEL, BUF_SIZE); + + pid = fork(); + assert(pid >= 0); + + if (pid == 0) { + assert_all(mapped, BUF_SIZE, PARENT_SENTINEL); + + memset(mapped, CHILD_SENTINEL, BUF_SIZE); + assert_all(mapped, BUF_SIZE, CHILD_SENTINEL); + + exit(0); + } + + wait_child(pid); + + assert_all(mapped, BUF_SIZE, PARENT_SENTINEL); + + assert(munmap(mapped, BUF_SIZE) == 0); + + + // Test 3: allocation after fork + unsigned char *before = malloc(BUF_SIZE); + assert(before != NULL); + + memset(before, PARENT_SENTINEL, BUF_SIZE); + + pid = fork(); + assert(pid >= 0); + + if (pid == 0) { + unsigned char *child_buf = malloc(BUF_SIZE); + assert(child_buf != NULL); + + memset(child_buf, CHILD_SENTINEL, BUF_SIZE); + assert_all(child_buf, BUF_SIZE, CHILD_SENTINEL); + + free(child_buf); + exit(0); + } + + wait_child(pid); + + assert_all(before, BUF_SIZE, PARENT_SENTINEL); + + free(before); + + + // Test 4: same virtual address mapped separately + void *slot = mmap(NULL, BUF_SIZE, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); + assert(slot != MAP_FAILED); + + assert(munmap(slot, BUF_SIZE) == 0); + + pid = fork(); + assert(pid >= 0); + + if (pid == 0) { + unsigned char *child_map = mmap(slot, BUF_SIZE, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | + MAP_FIXED, + -1, 0); + + assert(child_map == slot); + + memset(child_map, CHILD_SENTINEL, BUF_SIZE); + assert_all(child_map, BUF_SIZE, CHILD_SENTINEL); + + exit(0); + } + + unsigned char *parent_map = mmap(slot, BUF_SIZE, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | + MAP_FIXED, + -1, 0); + + assert(parent_map == slot); + + memset(parent_map, PARENT_SENTINEL, BUF_SIZE); + + wait_child(pid); + + // Same address, different cages, no shared data + assert_all(parent_map, BUF_SIZE, PARENT_SENTINEL); + + assert(munmap(parent_map, BUF_SIZE) == 0); + + + printf("cross_cage_fork_no_leak test: PASS\n"); + return 0; +} \ No newline at end of file diff --git a/tests/unit-tests/memory_tests/deterministic/cross_cage_syscall_efault.c b/tests/unit-tests/memory_tests/deterministic/cross_cage_syscall_efault.c new file mode 100755 index 000000000..5376b3f73 --- /dev/null +++ b/tests/unit-tests/memory_tests/deterministic/cross_cage_syscall_efault.c @@ -0,0 +1,58 @@ +#undef _GNU_SOURCE +#define _GNU_SOURCE + +#include +#include +#include +#include +#include + +// Test: syscall pointer validation +// Verifies syscalls reject buffers outside the current cage + +#define REGION_SIZE (64 * 1024) + +int main(void) +{ + // Create an address that is no longer mapped + unsigned char *invalid = mmap(NULL, REGION_SIZE, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); + + assert(invalid != MAP_FAILED); + assert(munmap(invalid, REGION_SIZE) == 0); + + int fds[2]; + assert(pipe(fds) == 0); + + + // write() should fail because the buffer is unmapped + errno = 0; + + ssize_t ret = write(fds[1], invalid, REGION_SIZE); + + assert(ret == -1); + assert(errno == EFAULT); + + + // read() should also fail when writing into an invalid buffer + unsigned char value = 0x5A; + + ret = write(fds[1], &value, 1); + assert(ret == 1); + + errno = 0; + + ret = read(fds[0], invalid, REGION_SIZE); + + assert(ret == -1); + assert(errno == EFAULT); + + + close(fds[0]); + close(fds[1]); + + printf("cross_cage_syscall_efault test: PASS\n"); + return 0; +} \ No newline at end of file diff --git a/tests/unit-tests/memory_tests/fail/cross_cage_wild_pointer.c b/tests/unit-tests/memory_tests/fail/cross_cage_wild_pointer.c new file mode 100755 index 000000000..3405df8d6 --- /dev/null +++ b/tests/unit-tests/memory_tests/fail/cross_cage_wild_pointer.c @@ -0,0 +1,42 @@ +#include +#include +#include + +// Test: wild pointer isolation +// Verifies accesses outside cage memory are trapped + +#define BEYOND_CAGE (256u * 1024 * 1024) + +int main(void) +{ + /* + Allocate a valid pointer inside this cage + Move it outside the cage memory range and verify the access traps + */ + volatile unsigned char *ptr = + (volatile unsigned char *)malloc(64); + + if (ptr == NULL) + return 1; + + + volatile unsigned char *wild = + (volatile unsigned char *)((uintptr_t)ptr + BEYOND_CAGE); + + + // This access should trap + *wild = 0x41; + + + /* + Reaching here means the invalid write wasn't blocked + The read is included to detect possible data leakage + */ + unsigned char value = *wild; + + printf("LEAK: read 0x%02x outside cage memory\n", value); + + free((void *)ptr); + + return 0; +} \ No newline at end of file From 7c6be917c9eeda955d3bec917854f15e7faffb4c Mon Sep 17 00:00:00 2001 From: Ashwin Nandan Date: Mon, 24 Aug 2026 15:47:05 -0400 Subject: [PATCH 2/2] Address review comments --- .../deterministic/cross_cage_fork_no_leak.c | 2 +- .../deterministic/cross_cage_syscall_efault.c | 13 +++--- .../memory_tests/fail/invalid_access_direct.c | 42 ++++++++++++++++--- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/tests/unit-tests/memory_tests/deterministic/cross_cage_fork_no_leak.c b/tests/unit-tests/memory_tests/deterministic/cross_cage_fork_no_leak.c index 5d5dfc3eb..8f3ce6805 100755 --- a/tests/unit-tests/memory_tests/deterministic/cross_cage_fork_no_leak.c +++ b/tests/unit-tests/memory_tests/deterministic/cross_cage_fork_no_leak.c @@ -168,4 +168,4 @@ int main(void) printf("cross_cage_fork_no_leak test: PASS\n"); return 0; -} \ No newline at end of file +} diff --git a/tests/unit-tests/memory_tests/deterministic/cross_cage_syscall_efault.c b/tests/unit-tests/memory_tests/deterministic/cross_cage_syscall_efault.c index 5376b3f73..b7271a38a 100755 --- a/tests/unit-tests/memory_tests/deterministic/cross_cage_syscall_efault.c +++ b/tests/unit-tests/memory_tests/deterministic/cross_cage_syscall_efault.c @@ -12,16 +12,15 @@ #define REGION_SIZE (64 * 1024) -int main(void) +int main(void)\ { - // Create an address that is no longer mapped + unsigned char *invalid = mmap(NULL, REGION_SIZE, - PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, - -1, 0); + PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); assert(invalid != MAP_FAILED); - assert(munmap(invalid, REGION_SIZE) == 0); int fds[2]; assert(pipe(fds) == 0); @@ -55,4 +54,4 @@ int main(void) printf("cross_cage_syscall_efault test: PASS\n"); return 0; -} \ No newline at end of file +} diff --git a/tests/unit-tests/memory_tests/fail/invalid_access_direct.c b/tests/unit-tests/memory_tests/fail/invalid_access_direct.c index c16ebcaa1..059519a84 100644 --- a/tests/unit-tests/memory_tests/fail/invalid_access_direct.c +++ b/tests/unit-tests/memory_tests/fail/invalid_access_direct.c @@ -3,13 +3,45 @@ * Exercises lind-wasm's PROT_NONE linear memory model: pages not explicitly * mapped by rawposix vmmap are inaccessible, and the access should trigger a * wasm trap (on wasm) or SIGSEGV (on native). + * + * The invalid address is derived from a real, valid allocation inside this + * cage, offset far enough to land outside the cage's mapped range. This + * models a cross-cage wild pointer rather than an arbitrary fixed address. */ +#include #include +#include -int main(void) { - volatile int *addr = (volatile int *)0x1234567; - int val = *addr; /* expected to trap / fault */ - printf("val=%d\n", val); - return 0; +#define BEYOND_CAGE (256u * 1024 * 1024) + +int main(void) +{ + /* + Allocate a valid pointer inside this cage + Move it outside the cage memory range and verify the access traps + */ + volatile unsigned char *ptr = + (volatile unsigned char *)malloc(64); + + if (ptr == NULL) + return 0; + + volatile unsigned char *wild = + (volatile unsigned char *)((uintptr_t)ptr + BEYOND_CAGE); + + // This access should trap + *wild = 0x41; + + /* + Reaching here means the invalid write wasn't blocked + The read is included to detect possible data leakage + */ + unsigned char value = *wild; + + printf("LEAK: read 0x%02x outside cage memory\n", value); + + free((void *)ptr); + + return 0; }