diff --git a/CHANGELOG.md b/CHANGELOG.md index 632089c..a2e4f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.0] - 2026-03-15 + +### Added +- **WP1e: Process tree construction from tracepoint events** (`daemon/process_tree.c`): open-addressing hash map (linear probing, 1024 slots) storing PID -> {parent_pid, comm, birth_time}. Fed by fork/exec/exit events via `owl_ptree_on_event()` in the pipeline. Provides `owl_ptree_is_descendant()` and `owl_ptree_get_chain()` for correlation engine ancestry queries. +- `daemon/process_tree.h`: public API (init, destroy, insert, remove, lookup, is_descendant, get_chain, on_event) +- `tests/test_process_tree.c`: 8 unit tests (TDD) — insert/lookup, parent chain, ancestry chain, descendant check, remove, capacity, null inputs, reinsert after exit + +### Changed +- `daemon/event_pipeline.h`: added `struct owl_ptree *ptree` field to pipeline context, added ptree parameter to `owl_pipeline_init()` +- `daemon/event_pipeline.c`: calls `owl_ptree_on_event()` after LD_PRELOAD check for process lifecycle events +- `daemon/main.c`: instantiates `struct owl_ptree`, passes to pipeline init, destroys on cleanup +- `daemon/Makefile`: added `process_tree.c` to SRCS +- `tests/Makefile`: added `test_process_tree` suite, linked `process_tree.o` into `test_event_pipeline` +- `tests/test_event_pipeline.c`: updated `owl_pipeline_init()` calls with NULL ptree parameter + ## [2.1.0] - 2026-03-15 ### Added diff --git a/README.md b/README.md index a82df93..ef60b7c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Runs on Graviton3 (c7g.large), Ubuntu 24.04, kernel 6.17. - **kernel/** - loadable module. Kprobes on ptrace, /proc/pid/mem, process_vm_readv/writev, mmap, module load/unload. ARM64 system register monitoring. Chardev for event delivery. - **ebpf/** - BPF LSM hooks returning -EPERM (ptrace_access_check, file_open for /proc/pid/mem + /dev/mem + /dev/kmem, file_mprotect). Tracepoints. Kprobe on do_init_module. Ring buffer to userspace. -- **daemon/** - epoll on chardev + BPF ring buffer. Policy engine. Signature scanner. CRC32 code integrity. Self-protection watchdog. TracerPid debugger detection. LD_PRELOAD environ scanning. +- **daemon/** - epoll on chardev + BPF ring buffer. Policy engine. Signature scanner. CRC32 code integrity. Self-protection watchdog. TracerPid debugger detection. LD_PRELOAD environ scanning. Process ancestry tree for correlation. - **game/** - ncurses test target. Mutable state, function pointers, exported address. - **cheats/** - 9 attack programs: process_vm_readv, /proc/pid/mem, /dev/mem, ptrace read, ptrace write, process_vm_writev, LD_PRELOAD, mprotect injection, debug registers. - **platform/** - Lambda + API Gateway + DynamoDB telemetry receiver. @@ -18,7 +18,7 @@ Runs on Graviton3 (c7g.large), Ubuntu 24.04, kernel 6.17. ## Status -v2.1.0. 107 unit tests, 12 suites. eBPF LSM returns EPERM on ptrace, /proc/pid/mem, /dev/mem, /dev/kmem, process_vm_writev. Module can't be unloaded while daemon runs. TracerPid polling detects debuggers attached before daemon start. LD_PRELOAD detection on exec. +v2.2.0. 115 unit tests, 13 suites. eBPF LSM returns EPERM on ptrace, /proc/pid/mem, /dev/mem, /dev/kmem, process_vm_writev. Module can't be unloaded while daemon runs. TracerPid polling detects debuggers attached before daemon start. LD_PRELOAD detection on exec. Process tree tracks ancestry for correlation engine. Prototype limitations: linear signature scan, CRC32 not cryptographic, no fleet management. diff --git a/daemon/Makefile b/daemon/Makefile index 5d61d61..1a10cd7 100644 --- a/daemon/Makefile +++ b/daemon/Makefile @@ -19,6 +19,7 @@ DEPFLAGS = -MMD -MP -MF $(@:.o=.d) SRCS := main.c \ bpf_loader.c \ event_pipeline.c \ + process_tree.c \ sig_loader.c \ integrity.c \ self_protect.c \ diff --git a/daemon/event_pipeline.c b/daemon/event_pipeline.c index 3c5ad67..37e2d5c 100644 --- a/daemon/event_pipeline.c +++ b/daemon/event_pipeline.c @@ -17,6 +17,7 @@ #include "event_pipeline.h" #include "preload_detect.h" +#include "process_tree.h" static void pipeline_check_preload(struct owl_pipeline *pipe, const struct owlbear_event *exec_ev); @@ -28,11 +29,13 @@ static void pipeline_check_preload(struct owl_pipeline *pipe, void owl_pipeline_init(struct owl_pipeline *pipe, struct owl_policy *policy, struct owl_sig_db *sig_db, + struct owl_ptree *ptree, pid_t target, bool enforce, FILE *logf) { memset(pipe, 0, sizeof(*pipe)); pipe->policy = policy; pipe->sig_db = sig_db; + pipe->ptree = ptree; pipe->target_pid = target; pipe->enforce = enforce; pipe->log_file = logf; @@ -94,6 +97,10 @@ enum owl_policy_action owl_pipeline_process(struct owl_pipeline *pipe, if (ev->event_type == OWL_EVENT_PROCESS_EXEC) pipeline_check_preload(pipe, ev); + /* Feed process lifecycle events into the process tree */ + if (pipe->ptree) + owl_ptree_on_event(pipe->ptree, ev); + return action; } diff --git a/daemon/event_pipeline.h b/daemon/event_pipeline.h index 357bc66..5ca26de 100644 --- a/daemon/event_pipeline.h +++ b/daemon/event_pipeline.h @@ -17,6 +17,7 @@ #include "owlbear_events.h" #include "policy.h" +#include "process_tree.h" #include "scanner.h" /* Maximum size of game .text to scan (8 MB) */ @@ -26,6 +27,7 @@ struct owl_pipeline { struct owl_policy *policy; struct owl_sig_db *sig_db; + struct owl_ptree *ptree; pid_t target_pid; bool enforce; FILE *log_file; @@ -42,6 +44,7 @@ struct owl_pipeline { * @pipe: Pipeline context * @policy: Policy engine (ownership retained by caller) * @sig_db: Signature database (ownership retained by caller) + * @ptree: Process tree (may be NULL; ownership retained by caller) * @target: PID of the protected process * @enforce: Whether to take enforcement actions * @logf: Log file (may be NULL for stdout only) @@ -49,6 +52,7 @@ struct owl_pipeline { void owl_pipeline_init(struct owl_pipeline *pipe, struct owl_policy *policy, struct owl_sig_db *sig_db, + struct owl_ptree *ptree, pid_t target, bool enforce, FILE *logf); /** diff --git a/daemon/main.c b/daemon/main.c index f6802df..21f4209 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -28,6 +28,7 @@ #include "bpf_loader.h" #include "event_pipeline.h" #include "integrity.h" +#include "process_tree.h" #include "policy.h" #include "scanner.h" #include "self_protect.h" @@ -678,6 +679,7 @@ int main(int argc, char *argv[]) struct owl_policy policy; struct owl_sig_db sig_db; + struct owl_ptree ptree; struct owl_pipeline pipeline; struct owl_integrity integrity; struct owl_self_protect selfprot; @@ -717,8 +719,11 @@ int main(int argc, char *argv[]) /* Non-fatal: scanner will just not match anything */ } + /* Initialize process tree */ + owl_ptree_init(&ptree); + /* Initialize event pipeline */ - owl_pipeline_init(&pipeline, &policy, &sig_db, + owl_pipeline_init(&pipeline, &policy, &sig_db, &ptree, cfg.target_pid, cfg.enforce, log_file); /* Open the kernel device */ @@ -786,6 +791,7 @@ int main(int argc, char *argv[]) pipeline.actions_kill, pipeline.sig_matches); cleanup: + owl_ptree_destroy(&ptree); if (bpf) owl_bpf_destroy(bpf); if (dev_fd >= 0) diff --git a/daemon/process_tree.c b/daemon/process_tree.c new file mode 100644 index 0000000..056354f --- /dev/null +++ b/daemon/process_tree.c @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * process_tree.c - Process ancestry tree + * + * Open-addressing hash map with linear probing. Single-threaded + * (called from the epoll event loop), no locking needed. + */ + +#include + +#include "process_tree.h" + +/* ------------------------------------------------------------------------- + * Hash + probe helpers + * ----------------------------------------------------------------------- */ + +static inline uint32_t hash_pid(uint32_t pid) +{ + return pid % OWL_PTREE_CAPACITY; +} + +/* Find slot index for pid, or first empty slot if not present. + * Returns -1 if table is full and pid is not found. */ +static int find_slot(const struct owl_ptree *tree, uint32_t pid) +{ + uint32_t idx = hash_pid(pid); + + for (uint32_t i = 0; i < OWL_PTREE_CAPACITY; i++) { + uint32_t probe = (idx + i) % OWL_PTREE_CAPACITY; + + if (tree->nodes[probe].pid == 0) + return (int)probe; + if (tree->nodes[probe].pid == pid) + return (int)probe; + } + + return -1; /* full */ +} + +/* Find slot containing exactly this pid, or -1 */ +static int find_exact(const struct owl_ptree *tree, uint32_t pid) +{ + uint32_t idx = hash_pid(pid); + + for (uint32_t i = 0; i < OWL_PTREE_CAPACITY; i++) { + uint32_t probe = (idx + i) % OWL_PTREE_CAPACITY; + + if (tree->nodes[probe].pid == 0) + return -1; + if (tree->nodes[probe].pid == pid) + return (int)probe; + } + + return -1; +} + +/* ------------------------------------------------------------------------- + * Public API + * ----------------------------------------------------------------------- */ + +int owl_ptree_init(struct owl_ptree *tree) +{ + if (!tree) + return -1; + + memset(tree, 0, sizeof(*tree)); + return 0; +} + +void owl_ptree_destroy(struct owl_ptree *tree) +{ + if (tree) + memset(tree, 0, sizeof(*tree)); +} + +int owl_ptree_insert(struct owl_ptree *tree, uint32_t pid, + uint32_t parent_pid, const char *comm, + uint64_t birth_time) +{ + if (!tree || pid == 0) + return -1; + + int slot = find_slot(tree, pid); + if (slot < 0) + return -1; /* table full */ + + int is_new = (tree->nodes[slot].pid == 0); + + if (!is_new && tree->count >= OWL_PTREE_CAPACITY) { + /* Slot holds same PID — overwrite is fine */ + } else if (is_new && tree->count >= OWL_PTREE_CAPACITY) { + return -1; /* truly full */ + } + + tree->nodes[slot].pid = pid; + tree->nodes[slot].parent_pid = parent_pid; + tree->nodes[slot].birth_time = birth_time; + + memset(tree->nodes[slot].comm, 0, sizeof(tree->nodes[slot].comm)); + if (comm) + strncpy(tree->nodes[slot].comm, comm, + sizeof(tree->nodes[slot].comm) - 1); + + if (is_new) + tree->count++; + + return 0; +} + +int owl_ptree_remove(struct owl_ptree *tree, uint32_t pid) +{ + if (!tree || pid == 0) + return -1; + + int slot = find_exact(tree, pid); + if (slot < 0) + return -1; + + /* Backward-shift deletion: clear the slot, then fix up any + * displaced entries in the cluster to maintain probe chains. */ + memset(&tree->nodes[slot], 0, sizeof(tree->nodes[slot])); + tree->count--; + + uint32_t empty = (uint32_t)slot; + + for (uint32_t i = 1; i < OWL_PTREE_CAPACITY; i++) { + uint32_t probe = (empty + i) % OWL_PTREE_CAPACITY; + + if (tree->nodes[probe].pid == 0) + break; /* end of cluster */ + + uint32_t home = hash_pid(tree->nodes[probe].pid); + + /* Check if 'probe' needs to move back to fill 'empty'. + * This is true when 'home' is not in (empty, probe] on + * the circular table. */ + int needs_shift; + if (empty <= probe) + needs_shift = (home <= empty || home > probe); + else + needs_shift = (home <= empty && home > probe); + + if (needs_shift) { + tree->nodes[empty] = tree->nodes[probe]; + memset(&tree->nodes[probe], 0, + sizeof(tree->nodes[probe])); + empty = probe; + } + } + + return 0; +} + +const struct owl_ptree_node *owl_ptree_lookup(const struct owl_ptree *tree, + uint32_t pid) +{ + if (!tree || pid == 0) + return NULL; + + int slot = find_exact(tree, pid); + if (slot < 0) + return NULL; + + return &tree->nodes[slot]; +} + +int owl_ptree_is_descendant(const struct owl_ptree *tree, + uint32_t pid, uint32_t ancestor) +{ + if (!tree) + return -1; + + uint32_t cur = pid; + + for (int i = 0; i < OWL_PTREE_MAX_CHAIN; i++) { + if (cur == 0) + return 0; + + const struct owl_ptree_node *n = owl_ptree_lookup(tree, cur); + if (!n) + return 0; + + if (n->parent_pid == ancestor) + return 1; + + /* Self-loop guard */ + if (n->parent_pid == cur) + return 0; + + cur = n->parent_pid; + } + + return 0; +} + +int owl_ptree_get_chain(const struct owl_ptree *tree, + uint32_t pid, struct owl_ptree_chain *chain) +{ + if (!tree || !chain) + return -1; + + memset(chain, 0, sizeof(*chain)); + + uint32_t cur = pid; + + for (int i = 0; i < OWL_PTREE_MAX_CHAIN; i++) { + if (cur == 0) + break; + + const struct owl_ptree_node *n = owl_ptree_lookup(tree, cur); + if (!n) + break; + + chain->pids[chain->len++] = cur; + + /* Self-loop guard */ + if (n->parent_pid == cur) + break; + + cur = n->parent_pid; + } + + return 0; +} + +int owl_ptree_on_event(struct owl_ptree *tree, + const struct owlbear_event *ev) +{ + if (!tree || !ev) + return -1; + + switch (ev->event_type) { + case OWL_EVENT_PROCESS_CREATE: + return owl_ptree_insert(tree, ev->pid, + ev->payload.process.parent_pid, + ev->comm, ev->timestamp_ns); + + case OWL_EVENT_PROCESS_EXEC: { + /* Update comm if PID exists with same birth_time */ + int slot = find_exact(tree, ev->pid); + if (slot >= 0 && + tree->nodes[slot].birth_time == ev->timestamp_ns) { + memset(tree->nodes[slot].comm, 0, + sizeof(tree->nodes[slot].comm)); + strncpy(tree->nodes[slot].comm, ev->comm, + sizeof(tree->nodes[slot].comm) - 1); + return 0; + } + /* PID not found or different birth_time — insert */ + return owl_ptree_insert(tree, ev->pid, + ev->payload.process.parent_pid, + ev->comm, ev->timestamp_ns); + } + + case OWL_EVENT_PROCESS_EXIT: + return owl_ptree_remove(tree, ev->pid); + + default: + return 0; /* ignored event type */ + } +} diff --git a/daemon/process_tree.h b/daemon/process_tree.h new file mode 100644 index 0000000..f18fe75 --- /dev/null +++ b/daemon/process_tree.h @@ -0,0 +1,124 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * process_tree.h - Process ancestry tree + * + * Open-addressing hash map (linear probing) storing PID -> parent/comm/birth. + * Fed by fork/exec/exit events. Provides ancestry queries for the + * correlation engine to detect cheat processes spawned near the game. + */ + +#ifndef OWLBEAR_PROCESS_TREE_H +#define OWLBEAR_PROCESS_TREE_H + +#include + +#include "owlbear_events.h" + +#define OWL_PTREE_CAPACITY 1024 +#define OWL_PTREE_MAX_CHAIN 32 + +struct owl_ptree_node { + uint32_t pid; /* 0 = empty slot */ + uint32_t parent_pid; + uint64_t birth_time; /* timestamp_ns from CREATE/EXEC event */ + char comm[16]; +}; + +struct owl_ptree { + struct owl_ptree_node nodes[OWL_PTREE_CAPACITY]; + uint32_t count; +}; + +struct owl_ptree_chain { + uint32_t pids[OWL_PTREE_MAX_CHAIN]; + int len; +}; + +/** + * owl_ptree_init - Zero-initialize the process tree + * @tree: Tree to initialize + * + * Returns 0 on success, -1 on NULL input. + */ +int owl_ptree_init(struct owl_ptree *tree); + +/** + * owl_ptree_destroy - Release resources (no-op for static allocation) + * @tree: Tree to destroy + */ +void owl_ptree_destroy(struct owl_ptree *tree); + +/** + * owl_ptree_insert - Insert or update a process node + * @tree: Process tree + * @pid: Process ID (must be > 0) + * @parent_pid: Parent PID + * @comm: Process name (up to 15 chars + NUL) + * @birth_time: Timestamp in nanoseconds + * + * On PID collision with different birth_time, overwrites (PID reuse). + * Returns 0 on success, -1 on error (NULL tree, pid==0, or table full). + */ +int owl_ptree_insert(struct owl_ptree *tree, uint32_t pid, + uint32_t parent_pid, const char *comm, + uint64_t birth_time); + +/** + * owl_ptree_remove - Remove a process by PID + * @tree: Process tree + * @pid: PID to remove + * + * Uses backward-shift deletion to maintain probe chain integrity. + * Returns 0 on success, -1 on error or not found. + */ +int owl_ptree_remove(struct owl_ptree *tree, uint32_t pid); + +/** + * owl_ptree_lookup - Find a process node by PID + * @tree: Process tree + * @pid: PID to look up + * + * Returns pointer to the node, or NULL if not found. + */ +const struct owl_ptree_node *owl_ptree_lookup(const struct owl_ptree *tree, + uint32_t pid); + +/** + * owl_ptree_is_descendant - Check if pid descends from ancestor + * @tree: Process tree + * @pid: PID to check + * @ancestor: Potential ancestor PID + * + * Walks parent_pid chain up to MAX_CHAIN steps. + * Returns 1 if descendant, 0 if not, -1 on error. + */ +int owl_ptree_is_descendant(const struct owl_ptree *tree, + uint32_t pid, uint32_t ancestor); + +/** + * owl_ptree_get_chain - Build ancestry chain from pid to root + * @tree: Process tree + * @pid: Starting PID + * @chain: Output chain (caller-allocated) + * + * Fills chain->pids[] from target to root, sets chain->len. + * Returns 0 on success, -1 on error. + */ +int owl_ptree_get_chain(const struct owl_ptree *tree, + uint32_t pid, struct owl_ptree_chain *chain); + +/** + * owl_ptree_on_event - Dispatch an event to the process tree + * @tree: Process tree + * @ev: Event to process + * + * PROCESS_CREATE: insert(pid, parent_pid, comm, timestamp_ns) + * PROCESS_EXEC: update comm if PID exists; else insert + * PROCESS_EXIT: remove(pid) + * + * Returns 0 on success, -1 on error. + */ +int owl_ptree_on_event(struct owl_ptree *tree, + const struct owlbear_event *ev); + +#endif /* OWLBEAR_PROCESS_TREE_H */ diff --git a/scripts/verify.sh b/scripts/verify.sh index 4c31172..df2ba36 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -297,7 +297,7 @@ preflight() { | grep -o '"accountId" *: *"[^"]*"' | cut -d'"' -f4 || echo "local") cat > "${OUT_DIR}/summary.txt" <
+ +#include "test_harness.h" +#include "owlbear_events.h" +#include "process_tree.h" + +/* ------------------------------------------------------------------------- + * Basic insert / lookup + * ----------------------------------------------------------------------- */ + +TEST(ptree_insert_and_lookup) { + struct owl_ptree tree; + owl_ptree_init(&tree); + + ASSERT_EQ(owl_ptree_insert(&tree, 100, 1, "bash", 1000), 0); + + const struct owl_ptree_node *n = owl_ptree_lookup(&tree, 100); + ASSERT_TRUE(n != NULL); + ASSERT_EQ(n->pid, 100); + ASSERT_EQ(n->parent_pid, 1); + ASSERT_EQ(n->birth_time, 1000); + ASSERT_STR_EQ(n->comm, "bash"); + ASSERT_EQ(tree.count, 1); + + owl_ptree_destroy(&tree); +} + +TEST(ptree_lookup_parent) { + struct owl_ptree tree; + owl_ptree_init(&tree); + + owl_ptree_insert(&tree, 1, 0, "init", 100); + owl_ptree_insert(&tree, 100, 1, "bash", 200); + + const struct owl_ptree_node *child = owl_ptree_lookup(&tree, 100); + ASSERT_TRUE(child != NULL); + ASSERT_EQ(child->parent_pid, 1); + + const struct owl_ptree_node *parent = owl_ptree_lookup(&tree, 1); + ASSERT_TRUE(parent != NULL); + ASSERT_EQ(parent->pid, 1); + + owl_ptree_destroy(&tree); +} + +/* ------------------------------------------------------------------------- + * Ancestry chain + * ----------------------------------------------------------------------- */ + +TEST(ptree_get_chain) { + struct owl_ptree tree; + owl_ptree_init(&tree); + + owl_ptree_insert(&tree, 1, 0, "init", 100); + owl_ptree_insert(&tree, 10, 1, "bash", 200); + owl_ptree_insert(&tree, 20, 10, "sh", 300); + owl_ptree_insert(&tree, 30, 20, "cheat", 400); + + struct owl_ptree_chain chain; + ASSERT_EQ(owl_ptree_get_chain(&tree, 30, &chain), 0); + ASSERT_EQ(chain.len, 4); + ASSERT_EQ(chain.pids[0], 30); + ASSERT_EQ(chain.pids[1], 20); + ASSERT_EQ(chain.pids[2], 10); + ASSERT_EQ(chain.pids[3], 1); + + owl_ptree_destroy(&tree); +} + +TEST(ptree_is_descendant) { + struct owl_ptree tree; + owl_ptree_init(&tree); + + owl_ptree_insert(&tree, 1, 0, "init", 100); + owl_ptree_insert(&tree, 10, 1, "bash", 200); + owl_ptree_insert(&tree, 20, 10, "sh", 300); + owl_ptree_insert(&tree, 30, 20, "cheat", 400); + + ASSERT_EQ(owl_ptree_is_descendant(&tree, 30, 1), 1); + ASSERT_EQ(owl_ptree_is_descendant(&tree, 30, 10), 1); + ASSERT_EQ(owl_ptree_is_descendant(&tree, 30, 20), 1); + ASSERT_EQ(owl_ptree_is_descendant(&tree, 30, 999), 0); + ASSERT_EQ(owl_ptree_is_descendant(&tree, 10, 30), 0); + + owl_ptree_destroy(&tree); +} + +/* ------------------------------------------------------------------------- + * Remove / exit + * ----------------------------------------------------------------------- */ + +TEST(ptree_remove_on_exit) { + struct owl_ptree tree; + owl_ptree_init(&tree); + + owl_ptree_insert(&tree, 100, 1, "bash", 1000); + ASSERT_EQ(tree.count, 1); + + ASSERT_EQ(owl_ptree_remove(&tree, 100), 0); + ASSERT_TRUE(owl_ptree_lookup(&tree, 100) == NULL); + ASSERT_EQ(tree.count, 0); + + owl_ptree_destroy(&tree); +} + +/* ------------------------------------------------------------------------- + * Capacity + * ----------------------------------------------------------------------- */ + +TEST(ptree_capacity) { + struct owl_ptree tree; + owl_ptree_init(&tree); + + for (uint32_t i = 1; i <= OWL_PTREE_CAPACITY; i++) + ASSERT_EQ(owl_ptree_insert(&tree, i, 0, "proc", i * 10), 0); + + ASSERT_EQ(tree.count, OWL_PTREE_CAPACITY); + + /* 1025th insert must fail */ + ASSERT_EQ(owl_ptree_insert(&tree, OWL_PTREE_CAPACITY + 1, 0, + "excess", 99999), -1); + + /* All 1024 still findable */ + for (uint32_t i = 1; i <= OWL_PTREE_CAPACITY; i++) + ASSERT_TRUE(owl_ptree_lookup(&tree, i) != NULL); + + owl_ptree_destroy(&tree); +} + +/* ------------------------------------------------------------------------- + * Null / invalid inputs + * ----------------------------------------------------------------------- */ + +TEST(ptree_null_inputs) { + struct owl_ptree tree; + struct owl_ptree_chain chain; + + owl_ptree_init(&tree); + + ASSERT_EQ(owl_ptree_init(NULL), -1); + ASSERT_EQ(owl_ptree_insert(NULL, 1, 0, "x", 0), -1); + ASSERT_EQ(owl_ptree_insert(&tree, 0, 0, "x", 0), -1); + ASSERT_EQ(owl_ptree_remove(NULL, 1), -1); + ASSERT_TRUE(owl_ptree_lookup(NULL, 1) == NULL); + ASSERT_EQ(owl_ptree_is_descendant(NULL, 1, 0), -1); + ASSERT_EQ(owl_ptree_get_chain(NULL, 1, &chain), -1); + ASSERT_EQ(owl_ptree_get_chain(&tree, 1, NULL), -1); + ASSERT_EQ(owl_ptree_on_event(NULL, NULL), -1); + + owl_ptree_destroy(&tree); +} + +/* ------------------------------------------------------------------------- + * Reinsert after exit (PID reuse) + * ----------------------------------------------------------------------- */ + +TEST(ptree_reinsert_after_exit) { + struct owl_ptree tree; + owl_ptree_init(&tree); + + owl_ptree_insert(&tree, 100, 1, "old_proc", 1000); + owl_ptree_remove(&tree, 100); + ASSERT_TRUE(owl_ptree_lookup(&tree, 100) == NULL); + + owl_ptree_insert(&tree, 100, 50, "new_proc", 2000); + const struct owl_ptree_node *n = owl_ptree_lookup(&tree, 100); + ASSERT_TRUE(n != NULL); + ASSERT_EQ(n->parent_pid, 50); + ASSERT_EQ(n->birth_time, 2000); + ASSERT_STR_EQ(n->comm, "new_proc"); + ASSERT_EQ(tree.count, 1); + + owl_ptree_destroy(&tree); +} + +/* ------------------------------------------------------------------------- + * Runner + * ----------------------------------------------------------------------- */ + +int main(void) +{ + printf("=== Owlbear Process Tree Tests ===\n"); + + RUN_TEST(ptree_insert_and_lookup); + RUN_TEST(ptree_lookup_parent); + RUN_TEST(ptree_get_chain); + RUN_TEST(ptree_is_descendant); + RUN_TEST(ptree_remove_on_exit); + RUN_TEST(ptree_capacity); + RUN_TEST(ptree_null_inputs); + RUN_TEST(ptree_reinsert_after_exit); + + TEST_SUMMARY(); + return test_failures; +}