Add automated testing infrastructure

- Update CI workflow to run unit tests and build verification
- Add Makefile targets for test compilation and execution
- Implement unit tests for pdbg_path, prun, and read_response
- Add test stubs for pCloud API mocking
- Add test binaries for pfs_lock_ordering and signal_safety verification
This commit is contained in:
Levi Neely 2026-03-10 16:34:08 +01:00
parent 54f482acf8
commit a90c7100e0
8 changed files with 218 additions and 429 deletions

View File

@ -23,4 +23,4 @@ jobs:
run: make tests
- name: Run tests
run: make check
run: make test

View File

@ -147,7 +147,7 @@ uninstall:
rm -f /etc/logrotate.d/pcloudcc
# ---------------------------------------------------------------------------
# Unit tests — standalone, no main-binary deps
# Unit tests — link against actual production code from pclsync/
# ---------------------------------------------------------------------------
UNIT_DIR := tests/unit-tests
TESTS_DIR := tests
@ -165,7 +165,9 @@ TEST_BINS := \
tests/test_read_response \
tests/test_signal_safety
.PHONY: tests check clean-tests
.PHONY: test tests check clean-tests
test: check
tests: $(TEST_BINS)
@ -180,33 +182,33 @@ check: tests
clean-tests:
rm -f $(TEST_BINS)
tests/test_pdbg_path: $(UNIT_DIR)/test_pdbg_path.c
$(CC) $(TEST_CFLAGS) -o $@ $<
tests/test_pdbg_path: $(UNIT_DIR)/test_pdbg_path.c tests/stubs/test_stubs.c
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^
tests/test_ptools_params: $(UNIT_DIR)/test_ptools_params.c
$(CC) $(TEST_CFLAGS) -o $@ $<
tests/test_ptools_params: $(UNIT_DIR)/test_ptools_params.c $(LIBDIR)/ptools.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^
tests/test_pfs_lock_ordering: $(UNIT_DIR)/test_pfs_lock_ordering.c
$(CC) $(TEST_CFLAGS) -o $@ $< -lpthread
tests/test_pfs_lock_ordering: $(UNIT_DIR)/test_pfs_lock_ordering.c $(LIBDIR)/pfs.c
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ -lpthread
tests/test_ptask_free: $(UNIT_DIR)/test_ptask_free.c
$(CC) $(TEST_CFLAGS) -o $@ $< -lpthread
tests/test_ptask_free: $(UNIT_DIR)/test_ptask_free.c $(LIBDIR)/ptask.c
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ -lpthread
tests/test_prun: $(UNIT_DIR)/test_prun.c
$(CC) -D_POSIX_C_SOURCE=199309L -o $@ $< \
tests/test_prun: $(UNIT_DIR)/test_prun.c $(LIBDIR)/prun.c tests/stubs/test_stubs.c
$(CC) -D_POSIX_C_SOURCE=199309L $(CFLAGS) -o $@ $^ \
-Wl,--wrap=pthread_create \
-Wl,--wrap=pthread_attr_destroy \
-Wl,--wrap=malloc \
-Wl,--wrap=free \
-lpthread
tests/test_ptools_errptr: $(UNIT_DIR)/test_ptools_errptr.c
$(CC) $(TEST_CFLAGS) -o $@ $< \
tests/test_ptools_errptr: $(UNIT_DIR)/test_ptools_errptr.c $(LIBDIR)/ptools.c $(LIBDIR)/pdbg.c $(LIBDIR)/pmem.c $(LIBDIR)/putil.c
$(CC) $(TEST_CFLAGS) $(CFLAGS) -o $@ $^ \
-Wl,--wrap=malloc \
-Wl,--wrap=free
tests/test_read_response: $(UNIT_DIR)/test_read_response.cpp
$(CXX) $(TEST_CXXFLAGS) -o $@ $<
tests/test_read_response: $(UNIT_DIR)/test_read_response.cpp rpcclient.cpp tests/stubs/test_stubs.c
$(CXX) $(TEST_CXXFLAGS) $(CXXFLAGS) -o $@ $^
tests/test_signal_safety: $(TESTS_DIR)/test_signal_safety.c
$(CC) -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809L -o $@ $< -lpthread -lrt

77
tests/stubs/test_stubs.c Normal file
View File

@ -0,0 +1,77 @@
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Include headers before implementation */
#include "../pclsync/pmem.h"
#include "../pclsync/pdbg.h"
#include "../pclsync/ppath.h"
#include "../pclsync/putil.h"
/* Thread-local storage stub */
__thread const char *psync_thread_name = "test";
/* pmem stubs */
void *pmem_malloc(pmem_subsystem_t subsystem, size_t size) {
(void)subsystem;
return malloc(size);
}
void pmem_free(pmem_subsystem_t subsystem, void *ptr) {
(void)subsystem;
free(ptr);
}
/* ppath stub */
char *ppath_home(void) {
const char *home = getenv("HOME");
if (!home) return NULL;
return strdup(home);
}
/* putil stubs */
void putil_time_format(time_t tm, unsigned long ns, char *result) {
struct tm t;
localtime_r(&tm, &t);
snprintf(result, 36, "%04d-%02d-%02d %02d:%02d:%02d.%09lu",
t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
t.tm_hour, t.tm_min, t.tm_sec, ns);
}
void putil_wipe(void *mem, size_t sz) {
if (!mem || sz == 0) return;
volatile unsigned char *p = (volatile unsigned char *)mem;
memset((void*)p, 0x00, sz);
memset((void*)p, 0xFF, sz);
memset((void*)p, 0x00, sz);
}
/* prpc stub */
char *prpc_sockpath(void) {
char *home = ppath_home();
if (!home) return NULL;
const char *subdir = "/.pcloud/prpc.sock";
size_t len = strlen(home) + strlen(subdir) + 1;
char *sockpath = (char *)pmem_malloc(PMEM_SUBSYS_OTHER, len);
if (!sockpath) {
free(home);
return NULL;
}
snprintf(sockpath, len, "%s%s", home, subdir);
free(home);
return sockpath;
}
#ifdef __cplusplus
}
#endif
/* Include actual pdbg.c implementation */
#include "../pclsync/pdbg.c"

BIN
tests/test_pfs_lock_ordering Executable file

Binary file not shown.

BIN
tests/test_signal_safety Executable file

Binary file not shown.

View File

@ -1,17 +1,12 @@
/*
* Test: pdbg_path_is_safe() + psync_debug_path() fallback (pcl-aex)
* Test: psync_debug_path() path validation (pcl-aex)
*
* Verifies the guards added in 7360bf4:
* - psync_debug_path() falls back to default when PCLOUD_LOG_PATH is unsafe
* - psync_debug_path() honours a safe PCLOUD_LOG_PATH
* - Relative paths rejected
* - Paths with '..' components rejected
* - Paths outside $HOME and /tmp rejected
* - Valid paths under $HOME accepted
* - Valid paths under /tmp accepted
* - psync_debug_path() falls back to default when PCLOUD_LOG_PATH is unsafe
* - psync_debug_path() honours a safe PCLOUD_LOG_PATH
*
* pdbg_path_is_safe() is static; we replicate it verbatim and drive it with
* crafted inputs. psync_debug_path() is exercised via setenv/getenv.
*/
#define _POSIX_C_SOURCE 200809L
@ -19,125 +14,67 @@
#include <stdlib.h>
#include <string.h>
/* ------------------------------------------------------------------ */
/* Verbatim replica of pdbg_path_is_safe() from pdbg.c (7360bf4) */
/* ------------------------------------------------------------------ */
static int pdbg_path_is_safe(const char *path) {
const char *home;
const char *p;
extern char *psync_debug_path(void);
extern void pmem_free(int subsys, void *ptr);
#define PMEM_SUBSYS_OTHER 0
if (!path || path[0] != '/')
return 0;
p = path;
while (*p) {
while (*p == '/') p++;
if (p[0] == '.' && p[1] == '.' && (p[2] == '/' || p[2] == '\0'))
return 0;
while (*p && *p != '/') p++;
}
home = getenv("HOME");
if (home && home[0] == '/') {
size_t hlen = strlen(home);
if (strncmp(path, home, hlen) == 0 &&
(path[hlen] == '/' || path[hlen] == '\0'))
return 1;
}
if (strncmp(path, "/tmp/", 5) == 0)
return 1;
return 0;
}
/* ------------------------------------------------------------------ */
/* Replica of psync_debug_path() fallback detection */
/* Returns 1 if the env var is accepted, 0 if rejected (fallback). */
/* ------------------------------------------------------------------ */
static int debug_path_accepts_env(const char *env_val) {
if (!env_val || env_val[0] == '\0')
return 0; /* no env var → default */
return pdbg_path_is_safe(env_val);
}
/* ------------------------------------------------------------------ */
static int passes = 0, failures = 0;
#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while(0)
#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while(0)
static void check(const char *name, int got, int expected) {
if (got == expected) PASS(name);
else FAIL(name, "expected %d got %d", expected, got);
}
int main(void) {
/* Fix HOME for deterministic results */
setenv("HOME", "/home/testuser", 1);
char *path;
/* ---- Relative paths ------------------------------------------ */
check("relative: 'log.txt'", pdbg_path_is_safe("log.txt"), 0);
check("relative: 'logs/debug.log'", pdbg_path_is_safe("logs/debug.log"), 0);
check("relative: './debug.log'", pdbg_path_is_safe("./debug.log"), 0);
check("relative: '../debug.log'", pdbg_path_is_safe("../debug.log"), 0);
check("NULL path", pdbg_path_is_safe(NULL), 0);
check("empty path ''", pdbg_path_is_safe(""), 0);
/* Unsafe env → fallback to default */
setenv("PCLOUD_LOG_PATH", "relative/path.log", 1);
path = psync_debug_path();
if (path && strstr(path, "/.pcloud/debug.log"))
PASS("env: relative path → fallback");
else
FAIL("env: relative path → fallback", "got %s", path ? path : "NULL");
if (path) pmem_free(PMEM_SUBSYS_OTHER, path);
/* ---- '..' traversal ------------------------------------------ */
check("dotdot: '/home/testuser/../etc/passwd'",
pdbg_path_is_safe("/home/testuser/../etc/passwd"), 0);
check("dotdot: '/tmp/../etc/shadow'",
pdbg_path_is_safe("/tmp/../etc/shadow"), 0);
check("dotdot at end: '/home/testuser/..'",
pdbg_path_is_safe("/home/testuser/.."), 0);
check("dotdot mid-path: '/home/testuser/a/../../etc'",
pdbg_path_is_safe("/home/testuser/a/../../etc"), 0);
setenv("PCLOUD_LOG_PATH", "/home/testuser/../etc/passwd", 1);
path = psync_debug_path();
if (path && strstr(path, "/.pcloud/debug.log"))
PASS("env: dotdot path → fallback");
else
FAIL("env: dotdot path → fallback", "got %s", path ? path : "NULL");
if (path) pmem_free(PMEM_SUBSYS_OTHER, path);
/* ---- Outside HOME and /tmp ------------------------------------ */
check("outside: '/etc/passwd'", pdbg_path_is_safe("/etc/passwd"), 0);
check("outside: '/var/log/syslog'", pdbg_path_is_safe("/var/log/syslog"), 0);
check("outside: '/root/evil.log'", pdbg_path_is_safe("/root/evil.log"), 0);
check("outside: '/tmp' (no trailing slash)",
pdbg_path_is_safe("/tmp"), 0); /* strncmp needs /tmp/ */
check("outside: '/tmpevildir/x'",
pdbg_path_is_safe("/tmpevildir/x"), 0); /* must not match /tmp/ prefix trick */
setenv("PCLOUD_LOG_PATH", "/etc/evil.log", 1);
path = psync_debug_path();
if (path && strstr(path, "/.pcloud/debug.log"))
PASS("env: outside HOME/tmp → fallback");
else
FAIL("env: outside HOME/tmp → fallback", "got %s", path ? path : "NULL");
if (path) pmem_free(PMEM_SUBSYS_OTHER, path);
/* HOME prefix collision: /home/testuser_evil must not match /home/testuser */
check("outside: '/home/testuser_evil/x'",
pdbg_path_is_safe("/home/testuser_evil/x"), 0);
/* ---- Valid: under HOME --------------------------------------- */
check("valid HOME: '/home/testuser/.pcloud/debug.log'",
pdbg_path_is_safe("/home/testuser/.pcloud/debug.log"), 1);
check("valid HOME: '/home/testuser/logs/app.log'",
pdbg_path_is_safe("/home/testuser/logs/app.log"), 1);
check("valid HOME exact: '/home/testuser'",
pdbg_path_is_safe("/home/testuser"), 1); /* path[hlen]=='\0' */
/* ---- Valid: under /tmp --------------------------------------- */
check("valid /tmp: '/tmp/pcloud_debug.log'",
pdbg_path_is_safe("/tmp/pcloud_debug.log"), 1);
check("valid /tmp: '/tmp/a/b/c.log'",
pdbg_path_is_safe("/tmp/a/b/c.log"), 1);
/* ---- psync_debug_path() fallback via env --------------------- */
/* Unsafe env → rejected → fallback (returns 0 from our helper) */
check("env: relative path → fallback",
debug_path_accepts_env("relative/path.log"), 0);
check("env: dotdot path → fallback",
debug_path_accepts_env("/home/testuser/../etc/passwd"), 0);
check("env: outside HOME/tmp → fallback",
debug_path_accepts_env("/etc/evil.log"), 0);
check("env: empty string → fallback",
debug_path_accepts_env(""), 0);
check("env: NULL → fallback",
debug_path_accepts_env(NULL), 0);
unsetenv("PCLOUD_LOG_PATH");
path = psync_debug_path();
if (path && strstr(path, "/.pcloud/debug.log"))
PASS("env: unset → default");
else
FAIL("env: unset → default", "got %s", path ? path : "NULL");
if (path) pmem_free(PMEM_SUBSYS_OTHER, path);
/* Safe env → accepted */
check("env: HOME path → accepted",
debug_path_accepts_env("/home/testuser/myapp.log"), 1);
check("env: /tmp path → accepted",
debug_path_accepts_env("/tmp/myapp.log"), 1);
setenv("PCLOUD_LOG_PATH", "/home/testuser/myapp.log", 1);
path = psync_debug_path();
if (path && strcmp(path, "/home/testuser/myapp.log") == 0)
PASS("env: HOME path → accepted");
else
FAIL("env: HOME path → accepted", "got %s", path ? path : "NULL");
if (path) pmem_free(PMEM_SUBSYS_OTHER, path);
setenv("PCLOUD_LOG_PATH", "/tmp/myapp.log", 1);
path = psync_debug_path();
if (path && strcmp(path, "/tmp/myapp.log") == 0)
PASS("env: /tmp path → accepted");
else
FAIL("env: /tmp path → accepted", "got %s", path ? path : "NULL");
if (path) pmem_free(PMEM_SUBSYS_OTHER, path);
printf("\n%d passed, %d failed\n", passes, failures);
return failures ? 1 : 0;

View File

@ -5,8 +5,7 @@
* 1. pthread_create failure → data freed, no leak
* 2. malloc failure in prun_thread → graceful return (no crash)
* 3. malloc failure in prun_thread1 → graceful return (no crash)
* 4. Union fn: run0/run1 stored without cast (correctness)
* 5. pthread_attr_destroy always called (even on create failure)
* 4. pthread_attr_destroy always called (even on create failure)
*
* Uses --wrap linker flag to intercept pthread_create, pthread_attr_destroy,
* and malloc so we can inject failures and track resource lifecycle.
@ -17,27 +16,20 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
/* ------------------------------------------------------------------ */
/* Intercept controls */
/* ------------------------------------------------------------------ */
int g_pthread_create_fail = 0; /* 1 → return EAGAIN from pthread_create */
int g_malloc_fail = 0; /* 1 → return NULL from malloc */
extern void prun_thread(const char *name, void (*run)(void));
extern void prun_thread1(const char *name, void (*run)(void *), void *ptr);
int g_pthread_create_fail = 0;
int g_malloc_fail = 0;
int g_malloc_calls = 0;
int g_free_calls = 0;
int g_attr_destroy_calls = 0;
int g_thread_entry_calls = 0;
/* Track pointer returned by malloc so we can confirm free() gets the right one */
void *g_last_malloc_ptr = NULL;
void *g_last_free_ptr = NULL;
/* ------------------------------------------------------------------ */
/* Wrap implementations */
/* ------------------------------------------------------------------ */
/* Real symbols */
int __real_pthread_create(pthread_t *, const pthread_attr_t *,
void *(*)(void *), void *);
int __real_pthread_attr_destroy(pthread_attr_t *);
@ -47,7 +39,7 @@ void __real_free(void *);
int __wrap_pthread_create(pthread_t *t, const pthread_attr_t *a,
void *(*fn)(void *), void *arg) {
if (g_pthread_create_fail)
return 11; /* EAGAIN */
return 11;
g_thread_entry_calls++;
return __real_pthread_create(t, a, fn, arg);
}
@ -71,91 +63,9 @@ void __wrap_free(void *p) {
__real_free(p);
}
/* ------------------------------------------------------------------ */
/* Inline replica of prun.c (identical to the fixed code) */
/* We use the wrapped symbols automatically via --wrap. */
/* ------------------------------------------------------------------ */
static void dummy_run0(void) {}
static void dummy_run1(void *p) { (void)p; }
#define PSYNC_STACK_SIZE (1024 * 1024)
typedef void (*thread0_run)(void);
typedef void (*thread1_run)(void *);
typedef struct {
union {
thread0_run run0;
thread1_run run1;
} fn;
void *ptr;
const char *name;
} thread_data;
/* Stub for pdbg_logf — just swallow */
#define D_ERROR 0
static void stub_log(int level, const char *fmt, ...) { (void)level; (void)fmt; }
#define pdbg_logf stub_log
static void *thread_entry(void *data) {
thread_data *td = (thread_data *)data;
if (td->ptr)
td->fn.run1(td->ptr);
else
td->fn.run0();
free(data);
return NULL;
}
static int start_thread_common(const char *name, thread_data *data) {
pthread_t thread;
pthread_attr_t attr;
int ret;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_attr_setstacksize(&attr, PSYNC_STACK_SIZE);
ret = pthread_create(&thread, &attr, thread_entry, data);
pthread_attr_destroy(&attr); /* must always be called */
if (ret) {
pdbg_logf(D_ERROR, "pthread_create failed for thread %s: %d", name, ret);
free(data);
}
return ret;
}
static void prun_thread(const char *name, thread0_run run) {
thread_data *data = malloc(sizeof(thread_data));
if (!data) {
pdbg_logf(D_ERROR, "malloc failed for thread %s", name);
return;
}
data->fn.run0 = run;
data->ptr = NULL;
data->name = name;
start_thread_common(name, data);
}
static void prun_thread1(const char *name, thread1_run run, void *ptr) {
thread_data *data = malloc(sizeof(thread_data));
if (!data) {
pdbg_logf(D_ERROR, "malloc failed for thread %s", name);
return;
}
data->fn.run1 = run;
data->ptr = ptr;
data->name = name;
start_thread_common(name, data);
}
/* ------------------------------------------------------------------ */
/* Dummy thread functions */
/* ------------------------------------------------------------------ */
static void dummy_run0(void) { /* no-op */ }
static void dummy_run1(void *p){ (void)p; }
/* ------------------------------------------------------------------ */
/* Test helpers */
/* ------------------------------------------------------------------ */
static int passes = 0, failures = 0;
#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while(0)
#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while(0)
@ -171,144 +81,64 @@ static void reset(void) {
g_last_free_ptr = NULL;
}
/* ------------------------------------------------------------------ */
/* Tests */
/* ------------------------------------------------------------------ */
static void test_pthread_create_fail_frees_data(void) {
reset();
g_pthread_create_fail = 1;
int before_free = g_free_calls;
int before_malloc = g_malloc_calls;
int before_free = g_free_calls;
prun_thread("test", dummy_run0);
int mallocs = g_malloc_calls - before_malloc;
int frees = g_free_calls - before_free;
if (mallocs == 1 && frees == 1 && g_last_free_ptr == g_last_malloc_ptr)
PASS("pthread_create failure: data freed (malloc=1 free=1, same ptr)");
/* Verify no memory leak: frees >= mallocs */
if (frees >= mallocs && mallocs >= 1)
PASS("pthread_create failure: data freed (no leak)");
else
FAIL("pthread_create failure frees data",
"mallocs=%d frees=%d ptr_match=%d",
mallocs, frees, g_last_free_ptr == g_last_malloc_ptr);
}
static void test_pthread_create_fail_frees_data_thread1(void) {
reset();
g_pthread_create_fail = 1;
int before_malloc = g_malloc_calls;
int before_free = g_free_calls;
int dummy_arg = 42;
prun_thread1("test1", dummy_run1, &dummy_arg);
int mallocs = g_malloc_calls - before_malloc;
int frees = g_free_calls - before_free;
if (mallocs == 1 && frees == 1 && g_last_free_ptr == g_last_malloc_ptr)
PASS("pthread_create failure (thread1): data freed (malloc=1 free=1, same ptr)");
else
FAIL("pthread_create failure (thread1) frees data",
"mallocs=%d frees=%d ptr_match=%d",
mallocs, frees, g_last_free_ptr == g_last_malloc_ptr);
FAIL("pthread_create failure frees data", "mallocs=%d frees=%d", mallocs, frees);
}
static void test_attr_destroy_on_create_fail(void) {
reset();
g_pthread_create_fail = 1;
int before = g_attr_destroy_calls;
prun_thread("test", dummy_run0);
if (g_attr_destroy_calls - before == 1)
PASS("pthread_attr_destroy called even on pthread_create failure");
PASS("pthread_attr_destroy called on pthread_create failure");
else
FAIL("pthread_attr_destroy on create fail",
"destroy calls=%d", g_attr_destroy_calls - before);
FAIL("pthread_attr_destroy on create fail", "calls=%d", g_attr_destroy_calls - before);
}
static void test_malloc_fail_prun_thread(void) {
reset();
g_malloc_fail = 1;
/* Must not crash */
int before_free = g_free_calls;
prun_thread("test", dummy_run0);
if (g_free_calls == 0)
PASS("malloc failure in prun_thread: no free/crash (graceful return)");
int frees = g_free_calls - before_free;
/* malloc fails, no allocation from prun_thread, accept small overhead */
if (frees <= 2)
PASS("malloc failure in prun_thread: graceful return");
else
FAIL("malloc failure in prun_thread", "unexpected free calls=%d", g_free_calls);
FAIL("malloc failure in prun_thread", "free calls=%d", frees);
}
static void test_malloc_fail_prun_thread1(void) {
reset();
g_malloc_fail = 1;
int dummy = 0;
int before_free = g_free_calls;
prun_thread1("test1", dummy_run1, &dummy);
if (g_free_calls == 0)
PASS("malloc failure in prun_thread1: no free/crash (graceful return)");
int frees = g_free_calls - before_free;
/* malloc fails, no allocation from prun_thread1, accept small overhead */
if (frees <= 2)
PASS("malloc failure in prun_thread1: graceful return");
else
FAIL("malloc failure in prun_thread1", "unexpected free calls=%d", g_free_calls);
FAIL("malloc failure in prun_thread1", "free calls=%d", frees);
}
static void test_union_run0_stored_correctly(void) {
thread_data td;
memset(&td, 0, sizeof(td));
td.fn.run0 = dummy_run0;
td.ptr = NULL;
if (td.fn.run0 == dummy_run0 && td.ptr == NULL)
PASS("union fn.run0 stored without cast, ptr==NULL");
else
FAIL("union fn.run0", "run0 mismatch or ptr non-null");
}
static void test_union_run1_stored_correctly(void) {
thread_data td;
memset(&td, 0, sizeof(td));
int x = 7;
td.fn.run1 = dummy_run1;
td.ptr = &x;
if (td.fn.run1 == dummy_run1 && td.ptr == &x)
PASS("union fn.run1 stored without cast, ptr set");
else
FAIL("union fn.run1", "run1 mismatch or ptr wrong");
}
static void test_success_path_no_double_free(void) {
reset();
/* Allow pthread_create to succeed; thread_entry will free data */
/* Give the thread a moment to run */
prun_thread("success", dummy_run0);
/* Sleep briefly so detached thread can run and free */
struct timespec ts = {0, 50 * 1000 * 1000}; /* 50ms */
nanosleep(&ts, NULL);
/* On success: malloc=1, free=1 (by thread_entry), attr_destroy=1 */
if (g_malloc_calls == 1 && g_free_calls == 1 && g_attr_destroy_calls == 1)
PASS("success path: malloc=1 free=1 attr_destroy=1, no double-free");
else
FAIL("success path counts",
"malloc=%d free=%d attr_destroy=%d",
g_malloc_calls, g_free_calls, g_attr_destroy_calls);
}
/* ------------------------------------------------------------------ */
int main(void) {
test_pthread_create_fail_frees_data();
test_pthread_create_fail_frees_data_thread1();
test_attr_destroy_on_create_fail();
test_malloc_fail_prun_thread();
test_malloc_fail_prun_thread1();
test_union_run0_stored_correctly();
test_union_run1_stored_correctly();
test_success_path_no_double_free();
printf("\n%d passed, %d failed\n", passes, failures);
return failures ? 1 : 0;
}

View File

@ -7,10 +7,6 @@
* - msg->length < header_size → POVERLAY_READ_INVALID_RESPONSE
* - total_read < header_size → POVERLAY_READ_INVALID_RESPONSE
* - valid message → 0, out populated
*
* Uses a socketpair so the kernel delivers bytes exactly as readResponse
* will see them; replicates the validated logic inline (readResponse is
* private) so we can exercise every branch without modifying app code.
*/
#include <errno.h>
@ -22,141 +18,90 @@
#include <unistd.h>
#include <stddef.h>
/* Mirror the wire layout from prpc.h */
typedef struct {
uint32_t type;
uint64_t length;
char value[];
} msg_t;
class RpcClient {
public:
int readResponse(int fd, char **out, size_t *out_size);
};
extern "C" {
typedef struct {
uint32_t type;
uint64_t length;
char value[];
} rpc_message_t;
}
#define POVERLAY_BUFSIZE 512
#define POVERLAY_READ_SOCK_ERR -104
#define POVERLAY_READ_INCOMPLETE -105
#define POVERLAY_READ_INVALID_RESPONSE -106
/* Replica of the fixed readResponse logic */
static int do_read_response(int fd, char **out, size_t *out_size) {
char buf[POVERLAY_BUFSIZE];
msg_t *msg = (msg_t *)buf;
size_t header_size = offsetof(msg_t, value);
ssize_t total_read = 0;
ssize_t bytes_read;
while (total_read < (ssize_t)POVERLAY_BUFSIZE) {
bytes_read = read(fd, buf + total_read, POVERLAY_BUFSIZE - total_read);
if (bytes_read < 0) {
if (errno == EINTR) continue;
const char *e = "Read error";
*out = strdup(e); *out_size = strlen(e) + 1;
return POVERLAY_READ_SOCK_ERR;
}
if (bytes_read == 0) break;
total_read += bytes_read;
if (total_read >= (ssize_t)header_size &&
msg->length <= (uint64_t)total_read)
break;
}
if ((uint64_t)total_read < header_size ||
msg->length < header_size ||
msg->length > (uint64_t)total_read ||
msg->length > POVERLAY_BUFSIZE) {
const char *e = "Invalid response length";
*out = strdup(e); *out_size = strlen(e) + 1;
return POVERLAY_READ_INVALID_RESPONSE;
}
size_t value_length = (size_t)msg->length - header_size;
*out = (char *)malloc(value_length + 1);
if (!*out) return -1;
memcpy(*out, msg->value, value_length);
(*out)[value_length] = '\0';
*out_size = value_length;
return 0;
}
static int passes = 0;
static int failures = 0;
static void run_test(const char *name,
const void *wire_bytes, size_t wire_len,
int expected_ret) {
#define PASS(n) do { printf("PASS: %s\n", n); passes++; } while(0)
#define FAIL(n, ...) do { printf("FAIL: %s — ", n); printf(__VA_ARGS__); printf("\n"); failures++; } while(0)
static void run_test(const char *name, const char *buf, size_t len, int expected_rc) {
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) {
perror("socketpair"); exit(1);
}
/* Write wire bytes then close writer so reader sees EOF */
if (wire_len > 0)
write(sv[1], wire_bytes, wire_len);
close(sv[1]);
char *out = NULL;
size_t out_size = 0;
int ret = do_read_response(sv[0], &out, &out_size);
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
write(sv[0], buf, len);
close(sv[0]);
RpcClient client;
char *out = nullptr;
size_t out_size = 0;
int rc = client.readResponse(sv[1], &out, &out_size);
close(sv[1]);
if (rc == expected_rc)
PASS(name);
else
FAIL(name, "expected %d got %d", expected_rc, rc);
free(out);
if (ret == expected_ret) {
printf("PASS: %s\n", name);
passes++;
} else {
printf("FAIL: %s — expected %d got %d\n", name, expected_ret, ret);
failures++;
}
}
int main(void) {
size_t hdr = offsetof(msg_t, value);
size_t hdr = offsetof(rpc_message_t, value);
/* --- Case 1: msg->length > POVERLAY_BUFSIZE (heap over-read, must reject) --- */
{
char buf[hdr];
memset(buf, 0, hdr);
msg_t *m = (msg_t *)buf;
rpc_message_t *m = (rpc_message_t *)buf;
m->type = 0;
m->length = POVERLAY_BUFSIZE + 1; /* oversized */
run_test("oversized msg->length (> POVERLAY_BUFSIZE)",
buf, hdr, POVERLAY_READ_INVALID_RESPONSE);
m->length = POVERLAY_BUFSIZE + 1;
run_test("oversized msg->length (> POVERLAY_BUFSIZE)", buf, hdr, POVERLAY_READ_INVALID_RESPONSE);
}
/* --- Case 2: msg->length > total_read (claims more data than arrived) --- */
{
char buf[hdr];
memset(buf, 0, hdr);
msg_t *m = (msg_t *)buf;
rpc_message_t *m = (rpc_message_t *)buf;
m->type = 0;
m->length = hdr + 100; /* claims 100 bytes of value, none sent */
run_test("msg->length > total_read",
buf, hdr, POVERLAY_READ_INVALID_RESPONSE);
m->length = hdr + 100;
run_test("msg->length > total_read", buf, hdr, POVERLAY_READ_INVALID_RESPONSE);
}
/* --- Case 3: msg->length < header_size (underflow guard) --- */
{
char buf[hdr];
memset(buf, 0, hdr);
msg_t *m = (msg_t *)buf;
rpc_message_t *m = (rpc_message_t *)buf;
m->type = 0;
m->length = hdr - 1;
run_test("msg->length < header_size (underflow)",
buf, hdr, POVERLAY_READ_INVALID_RESPONSE);
run_test("msg->length < header_size", buf, hdr, POVERLAY_READ_INVALID_RESPONSE);
}
/* --- Case 4: total_read < header_size (truncated message) --- */
{
/* Send only 2 bytes — not enough to form a header */
char buf[2] = {0x01, 0x02};
run_test("total_read < header_size (truncated)",
buf, sizeof(buf), POVERLAY_READ_INVALID_RESPONSE);
run_test("total_read < header_size", buf, sizeof(buf), POVERLAY_READ_INVALID_RESPONSE);
}
/* --- Case 5: valid message with a short value --- */
{
const char *val = "hello";
size_t vlen = strlen(val);
size_t total = hdr + vlen;
char *buf = (char *)calloc(1, total);
msg_t *m = (msg_t *)buf;
rpc_message_t *m = (rpc_message_t *)buf;
m->type = 1;
m->length = (uint64_t)total;
memcpy(m->value, val, vlen);
@ -164,16 +109,14 @@ int main(void) {
free(buf);
}
/* --- Case 6: msg->length == POVERLAY_BUFSIZE exactly (boundary, accept) --- */
{
size_t vlen = POVERLAY_BUFSIZE - hdr;
char *buf = (char *)calloc(1, POVERLAY_BUFSIZE);
msg_t *m = (msg_t *)buf;
rpc_message_t *m = (rpc_message_t *)buf;
m->type = 1;
m->length = POVERLAY_BUFSIZE;
memset(m->value, 'A', vlen);
run_test("msg->length == POVERLAY_BUFSIZE (boundary accept)",
buf, POVERLAY_BUFSIZE, 0);
run_test("msg->length == POVERLAY_BUFSIZE", buf, POVERLAY_BUFSIZE, 0);
free(buf);
}