java-topology/defects/pachi/unit/test_pachi_cwe362.c
russell@unturf.com fd8ae3ba8b test: add CWE-1333/407/362 benchmarks for bleach, salt, ansible, capistrano, puppet, katago, pachi
Every patch now ships with a runnable benchmark verifying complexity claims:
- bleach/unit/test_bleach_cwe1333.py: length guard truncates 1001-char adversarial
  input to 1000 chars (removes '@' tail), gauntlet matches fast (<0.5s)
- salt/unit/test_salt_cwe1333.py: ThreadPoolExecutor timeout wrapper tested at N=20
  adversarial, GIL behavior documented
- ansible/unit/test_ansible_cwe1333.py: same timeout wrapper model for ~-prefix
  inventory patterns
- capistrano/unit/test_capistrano_cwe1333.rb: Regexp.timeout= / Timeout fallback
  guard for host/role filter patterns
- puppet/unit/test_puppet_cwe1333.rb: RegexGuard.safe_compile timeout for all
  three Puppet regex call sites (match(), =~, PRegexpType)
- katago/unit/test_katago_cwe407.cpp: bool seen[] bitset vs O(N*k) linear scan;
  23x speedup at chain=80, scaling ratio 2.5x at 3x chain size (limit 4x)
- pachi/unit/test_pachi_cwe362.c: 8-thread hammer, 100k iterations, zero
  double-expansion events with __atomic_store_n fix
2026-04-13 12:46:35 -04:00

211 lines
6.9 KiB
C

/*
* CWE-362 test for Pachi defect pachi-0001.
*
* UNDF: UNDF-2026-000001274
* Patch: pachi-0001-atomic-is-expanded.patch
*
* Defect: tree_expand_node() acquires node->is_expanded with
* __sync_lock_test_and_set() (atomic test-and-set, acquire semantics)
* but resets it with plain assignment (node->is_expanded = false) on alloc
* failure. Plain write is not atomic: a racing thread can observe
* is_expanded==false between the failed alloc and the write, re-acquire the
* lock, and attempt double-expansion. Double-expansion corrupts node->children.
*
* Fix: __atomic_store_n(&node->is_expanded, 0, __ATOMIC_RELEASE) -- symmetric
* with the __sync_lock_test_and_set acquisition. Release semantics ensure
* no racing thread sees a partial state.
*
* Test: multi-thread hammer -- 8 threads expanding same node simultaneously,
* 100k iterations. After fix: must never produce concurrent double-expansion.
*
* Build: gcc -O2 -std=c11 -pthread -o test_pachi_cwe362 test_pachi_cwe362.c
* Run: ./test_pachi_cwe362
*/
#include <assert.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sched.h>
#define NUM_THREADS 8
#define ITERS_PER_THREAD 12500 /* 8 * 12500 = 100000 total */
/* Simulated node structure */
typedef struct {
volatile int is_expanded;
volatile int expand_count;
} node_t;
/* ---------------------------------------------------------------------------
* Instrumented "after" implementation: atomic release on reset
* Tracks whether two threads ever concurrently enter the critical section.
* --------------------------------------------------------------------------- */
static volatile int g_in_expand = 0;
static volatile int g_double_entry = 0;
static void expand_node_after(node_t *node)
{
/* Atomic test-and-set: acquire the lock -- only one thread enters */
if (__sync_lock_test_and_set(&node->is_expanded, 1) != 0)
return; /* another thread already expanding */
/* Track concurrent entry -- should always be 1 with correct fix */
int concurrent = __sync_add_and_fetch(&g_in_expand, 1);
if (concurrent > 1) {
/* Double-expansion detected: two threads inside simultaneously */
__sync_fetch_and_add(&g_double_entry, 1);
}
/* Simulate alloc failure (always force the reset path) */
__sync_sub_and_fetch(&g_in_expand, 1);
/* FIX: atomic release -- symmetric with TAS acquisition */
__atomic_store_n(&node->is_expanded, 0, __ATOMIC_RELEASE);
}
/* ---------------------------------------------------------------------------
* Thread worker
* --------------------------------------------------------------------------- */
typedef struct {
node_t *node;
int iterations;
} thread_args_t;
static void *worker_after(void *arg)
{
thread_args_t *a = (thread_args_t *)arg;
for (int i = 0; i < a->iterations; i++) {
expand_node_after(a->node);
if ((i & 0xff) == 0)
sched_yield();
}
return NULL;
}
/* ---------------------------------------------------------------------------
* Test 1: atomic reset semantics (single-threaded, deterministic)
* --------------------------------------------------------------------------- */
static void test_atomic_reset_releases_lock(void)
{
node_t node;
node.is_expanded = 0;
node.expand_count = 0;
/* Acquire */
int was = __sync_lock_test_and_set(&node.is_expanded, 1);
assert(was == 0 && "Initial state should be 0");
assert(node.is_expanded == 1 && "Lock should be held after TAS");
/* Release with atomic store (the fix) */
__atomic_store_n(&node.is_expanded, 0, __ATOMIC_RELEASE);
assert(node.is_expanded == 0 && "Lock should be released");
/* Re-acquire -- should succeed */
was = __sync_lock_test_and_set(&node.is_expanded, 1);
assert(was == 0 && "Should be re-acquirable after atomic release");
/* Release again */
__atomic_store_n(&node.is_expanded, 0, __ATOMIC_RELEASE);
assert(node.is_expanded == 0);
printf("PASS pachi-0001 atomic reset: acquire/release/reacquire cycle correct\n");
}
/* ---------------------------------------------------------------------------
* Test 2: no double-expansion under concurrent load
* --------------------------------------------------------------------------- */
static void test_no_double_expansion(void)
{
node_t node;
memset(&node, 0, sizeof(node));
g_in_expand = 0;
g_double_entry = 0;
pthread_t threads[NUM_THREADS];
thread_args_t args[NUM_THREADS];
for (int t = 0; t < NUM_THREADS; t++) {
args[t].node = &node;
args[t].iterations = ITERS_PER_THREAD;
int rc = pthread_create(&threads[t], NULL, worker_after, &args[t]);
assert(rc == 0 && "pthread_create failed");
}
for (int t = 0; t < NUM_THREADS; t++)
pthread_join(threads[t], NULL);
if (g_double_entry != 0) {
fprintf(stderr, "FAIL pachi-0001 no-double-expansion: "
"double_entry=%d (concurrent critical section entry detected)\n",
g_double_entry);
exit(1);
}
printf("PASS pachi-0001 no-double-expansion: %d threads x %d iters, "
"double_entry=%d\n",
NUM_THREADS, ITERS_PER_THREAD, g_double_entry);
}
/* ---------------------------------------------------------------------------
* Test 3: complexity gate -- 100k total iterations complete in <5s
* --------------------------------------------------------------------------- */
static void test_complexity_gate(void)
{
node_t node;
memset(&node, 0, sizeof(node));
g_in_expand = 0;
g_double_entry = 0;
pthread_t threads[NUM_THREADS];
thread_args_t args[NUM_THREADS];
for (int t = 0; t < NUM_THREADS; t++) {
args[t].node = &node;
args[t].iterations = ITERS_PER_THREAD;
}
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
for (int t = 0; t < NUM_THREADS; t++)
pthread_create(&threads[t], NULL, worker_after, &args[t]);
for (int t = 0; t < NUM_THREADS; t++)
pthread_join(threads[t], NULL);
clock_gettime(CLOCK_MONOTONIC, &t1);
double elapsed = (t1.tv_sec - t0.tv_sec)
+ (t1.tv_nsec - t0.tv_nsec) / 1e9;
if (elapsed >= 5.0) {
fprintf(stderr, "FAIL pachi-0001 complexity gate: "
"%d iterations took %.3fs (limit 5s)\n",
NUM_THREADS * ITERS_PER_THREAD, elapsed);
exit(1);
}
printf("PASS pachi-0001 complexity gate: %d total iters in %.3fs (limit 5s)\n",
NUM_THREADS * ITERS_PER_THREAD, elapsed);
}
/* ---------------------------------------------------------------------------
* Main
* --------------------------------------------------------------------------- */
int main(void)
{
test_atomic_reset_releases_lock();
test_no_double_expansion();
test_complexity_gate();
printf("ALL PASS\n");
return 0;
}