279 lines
9.0 KiB
C
279 lines
9.0 KiB
C
/*
|
|
* Test: ptools_set_backend_file_dates() errPtr freed between calls (pcl-dls)
|
|
*
|
|
* Verifies the fix in 11cecaa:
|
|
* - errPtr is freed (and NULLed) between the two ptools_backend_call() calls
|
|
* - errPtr is freed after the final call
|
|
* - No double-free occurs
|
|
* - NULL errPtr (no error on first call) is handled safely (no free of NULL)
|
|
*
|
|
* ptools_backend_call() needs a live backend so we replicate the exact
|
|
* errPtr management pattern with a mock backend call and use --wrap=malloc/free
|
|
* to track allocations.
|
|
*/
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
|
|
/* Suppress LSAN reports for intentional leaks used to verify bug behaviour.
|
|
* GCC sets __SANITIZE_ADDRESS__; Clang exposes __has_feature as a built-in.
|
|
* Use nested #if so the __has_feature() call is only evaluated when the
|
|
* compiler actually understands it (avoids "missing binary operator" on GCC). */
|
|
#ifdef __SANITIZE_ADDRESS__
|
|
# include <sanitizer/lsan_interface.h>
|
|
# define LSAN_DISABLE() __lsan_disable()
|
|
# define LSAN_ENABLE() __lsan_enable()
|
|
#elif defined(__has_feature)
|
|
# if __has_feature(address_sanitizer)
|
|
# include <sanitizer/lsan_interface.h>
|
|
# define LSAN_DISABLE() __lsan_disable()
|
|
# define LSAN_ENABLE() __lsan_enable()
|
|
# else
|
|
# define LSAN_DISABLE() do {} while (0)
|
|
# define LSAN_ENABLE() do {} while (0)
|
|
# endif
|
|
#else
|
|
# define LSAN_DISABLE() do {} while (0)
|
|
# define LSAN_ENABLE() do {} while (0)
|
|
#endif
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Allocation tracking via --wrap */
|
|
/* ------------------------------------------------------------------ */
|
|
int g_malloc_calls = 0;
|
|
int g_free_calls = 0;
|
|
void *g_freed[64];
|
|
int g_freed_count = 0;
|
|
void *g_alloced[64];
|
|
int g_alloced_count = 0;
|
|
|
|
void *__real_malloc(size_t);
|
|
void __real_free(void *);
|
|
|
|
void *__wrap_malloc(size_t sz) {
|
|
void *p = __real_malloc(sz);
|
|
if (p && g_alloced_count < 64)
|
|
g_alloced[g_alloced_count++] = p;
|
|
g_malloc_calls++;
|
|
return p;
|
|
}
|
|
|
|
void __wrap_free(void *p) {
|
|
if (p && g_freed_count < 64)
|
|
g_freed[g_freed_count++] = p;
|
|
g_free_calls++;
|
|
__real_free(p);
|
|
}
|
|
|
|
static void reset(void) {
|
|
g_malloc_calls = 0;
|
|
g_free_calls = 0;
|
|
g_freed_count = 0;
|
|
g_alloced_count = 0;
|
|
memset(g_freed, 0, sizeof(g_freed));
|
|
memset(g_alloced, 0, sizeof(g_alloced));
|
|
}
|
|
|
|
static int was_freed(void *p) {
|
|
for (int i = 0; i < g_freed_count; i++)
|
|
if (g_freed[i] == p) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Mock backend call */
|
|
/* Returns -1 and sets *errPtr when inject_error != 0. */
|
|
/* ------------------------------------------------------------------ */
|
|
static int inject_error_call1 = 0;
|
|
static int inject_error_call2 = 0;
|
|
|
|
static int mock_backend_call_1(char **errPtr) {
|
|
if (inject_error_call1) {
|
|
*errPtr = (char *)malloc(32);
|
|
if (*errPtr) strcpy(*errPtr, "ctime error");
|
|
return -1;
|
|
}
|
|
/* success: errPtr unchanged (stays NULL or previous value) */
|
|
return 0;
|
|
}
|
|
|
|
static int mock_backend_call_2(char **errPtr) {
|
|
if (inject_error_call2) {
|
|
*errPtr = (char *)malloc(32);
|
|
if (*errPtr) strcpy(*errPtr, "mtime error");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Replica of ptools_set_backend_file_dates() errPtr lifecycle (fixed) */
|
|
/* ------------------------------------------------------------------ */
|
|
static int run_fixed(void) {
|
|
char *errPtr = NULL;
|
|
int callRes;
|
|
|
|
callRes = mock_backend_call_1(&errPtr);
|
|
(void)callRes;
|
|
|
|
/* Fix: free errPtr between calls */
|
|
if (errPtr) {
|
|
free(errPtr);
|
|
errPtr = NULL;
|
|
}
|
|
|
|
callRes = mock_backend_call_2(&errPtr);
|
|
(void)callRes;
|
|
|
|
if (errPtr)
|
|
free(errPtr);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Replica of the OLD (unfixed) pattern — leaks errPtr between calls */
|
|
/* ------------------------------------------------------------------ */
|
|
static int run_unfixed(void) {
|
|
char *errPtr = NULL;
|
|
int callRes;
|
|
|
|
/* Intentional leak: suppress LSAN so the process exits cleanly and
|
|
* stdio flushes before the sanitizer reports it. */
|
|
LSAN_DISABLE();
|
|
callRes = mock_backend_call_1(&errPtr);
|
|
(void)callRes;
|
|
|
|
/* BUG: errPtr NOT freed here — overwritten by call 2 */
|
|
|
|
callRes = mock_backend_call_2(&errPtr);
|
|
(void)callRes;
|
|
|
|
if (errPtr)
|
|
free(errPtr);
|
|
LSAN_ENABLE();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
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)
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Tests */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* Both calls succeed: no allocations, no frees */
|
|
static void test_both_succeed_no_leak(void) {
|
|
reset();
|
|
inject_error_call1 = 0;
|
|
inject_error_call2 = 0;
|
|
run_fixed();
|
|
if (g_malloc_calls == 0 && g_free_calls == 0)
|
|
PASS("both succeed: no malloc/free, no leak");
|
|
else
|
|
FAIL("both succeed no leak", "malloc=%d free=%d", g_malloc_calls, g_free_calls);
|
|
}
|
|
|
|
/* Call 1 sets errPtr, fixed code frees it before call 2 */
|
|
static void test_call1_error_freed_before_call2(void) {
|
|
reset();
|
|
inject_error_call1 = 1;
|
|
inject_error_call2 = 0;
|
|
run_fixed();
|
|
|
|
/* 1 malloc (call1), 1 free (between calls) */
|
|
void *p = g_alloced_count > 0 ? g_alloced[0] : NULL;
|
|
if (g_malloc_calls == 1 && g_free_calls == 1 && p && was_freed(p))
|
|
PASS("call1 error: errPtr freed before call2 (no leak)");
|
|
else
|
|
FAIL("call1 error freed before call2",
|
|
"malloc=%d free=%d freed_ptr=%d", g_malloc_calls, g_free_calls, p ? was_freed(p) : -1);
|
|
}
|
|
|
|
/* Call 2 sets errPtr, freed after final call */
|
|
static void test_call2_error_freed_after(void) {
|
|
reset();
|
|
inject_error_call1 = 0;
|
|
inject_error_call2 = 1;
|
|
run_fixed();
|
|
|
|
void *p = g_alloced_count > 0 ? g_alloced[0] : NULL;
|
|
if (g_malloc_calls == 1 && g_free_calls == 1 && p && was_freed(p))
|
|
PASS("call2 error: errPtr freed after final call (no leak)");
|
|
else
|
|
FAIL("call2 error freed after",
|
|
"malloc=%d free=%d", g_malloc_calls, g_free_calls);
|
|
}
|
|
|
|
/* Both calls set errPtr: call1 ptr freed between, call2 ptr freed after */
|
|
static void test_both_error_both_freed(void) {
|
|
reset();
|
|
inject_error_call1 = 1;
|
|
inject_error_call2 = 1;
|
|
run_fixed();
|
|
|
|
/* 2 mallocs, 2 frees, both pointers freed */
|
|
void *p1 = g_alloced_count > 0 ? g_alloced[0] : NULL;
|
|
void *p2 = g_alloced_count > 1 ? g_alloced[1] : NULL;
|
|
if (g_malloc_calls == 2 && g_free_calls == 2
|
|
&& p1 && was_freed(p1) && p2 && was_freed(p2))
|
|
PASS("both errors: both errPtrs freed, no leak, no double-free");
|
|
else
|
|
FAIL("both errors both freed",
|
|
"malloc=%d free=%d p1_freed=%d p2_freed=%d",
|
|
g_malloc_calls, g_free_calls,
|
|
p1 ? was_freed(p1) : -1, p2 ? was_freed(p2) : -1);
|
|
}
|
|
|
|
/* Confirm the unfixed pattern LEAKS (documents what the fix corrects) */
|
|
static void test_unfixed_leaks(void) {
|
|
reset();
|
|
inject_error_call1 = 1;
|
|
inject_error_call2 = 1;
|
|
run_unfixed();
|
|
|
|
/* 2 mallocs, only 1 free — p1 is leaked */
|
|
void *p1 = g_alloced_count > 0 ? g_alloced[0] : NULL;
|
|
void *p2 = g_alloced_count > 1 ? g_alloced[1] : NULL;
|
|
if (g_malloc_calls == 2 && g_free_calls == 1
|
|
&& p1 && !was_freed(p1) && p2 && was_freed(p2))
|
|
PASS("(unfixed pattern confirmed): call1 errPtr leaks when not freed between calls");
|
|
else
|
|
FAIL("unfixed pattern leak demonstration",
|
|
"malloc=%d free=%d p1_freed=%d",
|
|
g_malloc_calls, g_free_calls, p1 ? was_freed(p1) : -1);
|
|
}
|
|
|
|
/* NULL errPtr after free: second call on NULL is safe (no double-free) */
|
|
static void test_no_double_free(void) {
|
|
reset();
|
|
inject_error_call1 = 1;
|
|
inject_error_call2 = 0; /* call2 succeeds, errPtr stays NULL after mid-free */
|
|
run_fixed();
|
|
|
|
/* Only 1 malloc, 1 free */
|
|
if (g_malloc_calls == 1 && g_free_calls == 1)
|
|
PASS("no double-free: errPtr NULLed after mid-free, call2 success leaves it NULL");
|
|
else
|
|
FAIL("no double-free", "malloc=%d free=%d", g_malloc_calls, g_free_calls);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
int main(void) {
|
|
test_both_succeed_no_leak();
|
|
test_call1_error_freed_before_call2();
|
|
test_call2_error_freed_after();
|
|
test_both_error_both_freed();
|
|
test_unfixed_leaks();
|
|
test_no_double_free();
|
|
|
|
printf("\n%d passed, %d failed\n", passes, failures);
|
|
return failures ? 1 : 0;
|
|
}
|