From a71054eaebf14394cf66af4b871b5020e78b5bcf Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 15 Mar 2026 07:08:24 +0000 Subject: [PATCH] Process tree construction from tracepoint events --- CHANGELOG.md | 15 +++ README.md | 4 +- daemon/Makefile | 1 + daemon/event_pipeline.c | 7 + daemon/event_pipeline.h | 4 + daemon/main.c | 8 +- daemon/process_tree.c | 261 ++++++++++++++++++++++++++++++++++++ daemon/process_tree.h | 124 +++++++++++++++++ scripts/verify.sh | 4 +- tests/Makefile | 10 +- tests/test_event_pipeline | Bin 26864 -> 27344 bytes tests/test_event_pipeline.c | 16 +-- tests/test_process_tree | Bin 0 -> 25536 bytes tests/test_process_tree.c | 200 +++++++++++++++++++++++++++ 14 files changed, 638 insertions(+), 16 deletions(-) create mode 100644 daemon/process_tree.c create mode 100644 daemon/process_tree.h create mode 100755 tests/test_process_tree create mode 100644 tests/test_process_tree.c 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" <
*VVc4E6&n;loh>V8KX^cR?;Izot^Pj5fPImyEZ|?V{&beo; zbL!Nox(zKy)cQ}=4FfEZ#ZM3Xs?)h43V+TGVcpcho%V!_*FE7iQ`B5Fe4Z-a=dtH3 zUq?3#-u`Uj>P4TA{vTyaR_4)^x6|$#TeGgiA0yP3n$g`eEbLfpa@5%bMbWFBE${#A z)eV>1NMhk}Jx8mFMBT)x)o(Yw2WGW1m4M^Bu!YL_-Q;I{?L3SeCDkF3&~Y4!WV7N@ z?LYj}3zbcC0yK@!Na`U-8P3k2n|#y_XCw)^obdoOf87+6QJi8c(9KO;!sPFHM!jzA zugfOEYHfWQfblM{7dYms1zmA(9rTme>(XA$QxvjjYR?06>{AWx86di8^~}@LLv9`b zo=tP#&)$qpQzO~Fo-wQ?HZ1DFc(UzJNtd2HB~DKLg_Mw|b}6ap3QN9}JbLv8DY*xQUAN>Vu{Q_{zNg;J8H z`%;nujHe_PNb{qna~ez=`{4XPS&ym0`Lq3f&bM<+I{)tV*Z1w{Gu~V&=NeBm}=jK99*|5dvy7wszhx6URB;JvM{0_ z?WA- zrC0>enfo4mV8LWG`x&cDgLur6Oam_l0wwu{w^)jQ!;Y zO4z|kzJ%=t#uK)S&F#_MQuPB;G=RBMlkH~rpfsh&@nC}c*EYEQh5mIR96Cr7vQ3hE zDU;N(?(T%zpnb7{^j*%)0GwwE$|z9gV^>n6;vyy&kpVs?0$kaLj8UWdEyrYl&s?Se zH&65hxE>e}&s-&zr-5^lyY?6Ij5j$lAwU6qUX!23rC4qqP5d|?K znsrTzv$u$9sz(UBsbAGmAbXV%l5@GoahsdC&FZ&F2p$T^Nr z=ew*wOoH!|7^yf3fc0z+OS4ebOl>UOK(%-F}?Nb02)u#b~s z@}Cn1=W;V-a4E;6!7^^}5h$yWGqoka_(GKcX}?gzzvp%N*PgL-p4fBfU~JpMO`=q{ zQP9;fU3w9-OE&^ysp7>d2)^un{OGLbitSFIIb`O>%d@DX(qFDmtD;&uU}ah{7t#_d zgQe$d-?tNcF)3M9*q)>T*4ITYPNWcza&duUG8dOFQgS`xe7QIejOXGE5X$XFk=v~) zz1WoG*!(wywGwWFtd(<2T3gPoJq}fsXSNI&w^jF}#f9Y27vJMKrN8_sp0n|$P@YyX9&aKoOk zMx6vd^T#7K{+H2~vA-v{D8ddcfGwfBHJ_t{dR6~nb?t)#hHQgc5Go6X6f zUQUuzw`KRWRt%mSCjE)aC%=PGAPJcgWJ8SQiW9Wd_n6cG1)Xt<`<1XMcRiu z@bj|+`Rl<)9#-2qQQHiIE^ReNdQ9yRVBG0aAZ{JUn5iuWDxDVijegdXVwREK=5w0q z=N|~m_#B$*>-u zWW2Z5+DQGPZUiFOOHt4v--)6GRW}PEA1P6&6@*E=2{@;lg~yG8c%nwUQ`oq+pr;gw zgh1*B-gVf-epxZc(7C~Kn>hoj3sl04f&t9v*JIpR+^Gy}E0H4J$?556QCID=b#YX- zs>?%N)>i8H#M8~3R2_Nj9sNwYRsDqJrVmy7uodZfgR*gx+?xZ_{#gy306%i;*7Peja~2#hM01XRm=d!@AZ{xmH=xiQ7GU zgr;j+ahG(tb+C2Ax=nXfZc{3vP3u|^KRLw)4(@eN)hX~C9&bewq_>guM<p4liLyr9IKRG*e*2Mk z#EW0oOv`#$MbZupovkL;^uDE=MSXw`AHGPP%6>OIxywrW>=pOPm0z+Ch8L=dtatWn zgI3|2EqHZL9D|E$=PqyCCyzMG*M7+^XJ^KKLwv)z1ANoD9V7B_;20Zq>tuTZScebN zy~C)yYFe+}?ClWHny53SqBFUTiqod`G}Mlw1`MkQ8y}G<&680MCYnG!*E6hF5vkiO zysGa%sp)FHiOyP~!EAFzd&WrXX>jadb#uVL%F0&qynGj5zku6N`w83`Jb}D_V*;U~ zCayOHyO>?j6#j7S6wocvogI}Pr9v9(fp|Q1VO0U!H}VTqA7v$I_HC-R*lTs{#~Zy~ z(T2t4Pn-mlZ-aVKe&aO5S`W`|#n#bLd0MHE+hr_Tky&_}vMM%mJ~bArhvw+pQi;Bv zUg*(_j|MDB#KwIQ@9e{BIqa2Jm5M07HdP9O8AFTka4!eQHVV;gwvm@h*M|@yX9bu& zK-FPsPp#Lh3qnk5)Ac!YR@v)y!$K$7W_^E?25~yH+^C#7d~mgO#@f$Oz>dmB#g=xh zaCK*L+*lVu^TJ)+_}K0K%#pt6r6LARolYKAA;FuJF7zaq0;z$jQ<09ro4Bw2rc5ahT3Cf1m?XrFcL(177 zF_?vJ6R*C#h2tKYNP64eidfYIx%PBy)1mmG66=dxj8A ztyaO^2Zwu0bBd5Ci?~KY2skQR6kCaKc$;d++0v* z#_Ps-Y=JxCY29dvgEM3IAAVnaN?qK86gUv`%F6Gk5+hb$L0LDe8+_~4wp_lp6u37+ zxmfeCpp8+7YaW$4N_vG9=#D7GLfOT9sN#ZltKALLSvUDN8+A6A zYBF=e9G(ugd<+q5`hw{2sJ5;Sk0oN27oN{XNjcpdLE?I~p32nryRG% zJ_m_P2XXX=UhhWWdp`1d_W*tX*Z?@X-s^oF@a7|4FEyVzIO8Mni^*|>kqOva0cK+d^_6!p9S0qI2OOMyao6G zU;|(iU@KrNevXRP6y@_v(AVZDiU&Va%mCqL{BmFe%m&;D_zvJ(fFtm6-vC$+*b4Xx zU^IU3jQ79*;2|V@24F9gkqz+w05<}zN55IQ%_}j+P zCiTzqe1Mb?Sw6TID>nSKfW8bV_dTgV*5NOUt(?@yuFHi{tX(5lTPAkt*r8nQ92%zJ zD^;n--*zmRhqV3t3x0kJ$d6+I?+fA=`uUNd*P#=v3*tNc{7e-K*AK^4dXPYGzd!-# zS*VCqta#f5;w-)iQdn82kgGxZ-~079fZpXxuQxY{KZf&DVRDy>O1Oo+YV9-r3JS^r zlHZNxQS!p`K;NIUal?)dn#N83X%PJm7M`1;KE~287O^~x@3K;ij2Ref+0NV)OP{ms z|8tX7gPjG*#lk12s8?7T#wS@GMw69dJk1P@jcg~z=h@-ODfSmoii3h8=lqUJ{h3Y4`u z*a!m;gP$45f1r*37WiwxuLufYu|I$Y@Q-6Jy&lAW#m{dI!SS&Gduw)}1IrBm45P7^ zUIl-95dV2Ue;D{_t@r>5;_vhGXMlec{KY{NDCQF&6{Y6SH%;rn!t-ynG>xm7ke{sC zsgcn$0nmbfx7deIU3&OF0W6jD+IUV@`2GO;qNK;-rvrNUz5t|uVj(*(<3}8N_;vt1 zDd`{5X2ip{fuaml!ccIea4uOec>M!Sm-Mr#f}YF5r}tOiWLeYWR4>b$J~^s?mf%bm zK-t0`njXjMr-v}x^yu;Q8#dVuA0gPhs(>Dp^h8OolG-0i`W8ullC3OA>C_0SszfWb zdF^yUj|aUQrqcTsL8sqS=o!GCo)M>x z>{+@;F9n0@e4j)Vs{kAsXr2))ftsY z9%qddI=rHRCKKHfr8JEbbl=#Qy$Rho_Vlc{gmF^0e2Cy$rOyXStG-`sLZza8v4gfr-1W^9Lj?9cp2tiR(EQ!*W zLvmwjw%R~&Q`wuA?pY&&MOhM*usKpdf?TP;W3(Xf%?ETA>1UNn?qQjt`=pkgtii96 zBM6uxf7WxYtx{|CAVEDRBY9KOuihr;yb}ODKw9djd~~ymv*PUklrg7v7m_kK<(#z8 zQ?4BERS>x<={v^=dENqmMs~zsK$QqCdOhfrwTUvW)sm4Z=!2CZy#=KiWk%0%sc<}7 zyz?#%w36tQ;NerHgeIr3T?i`0lKzvd>kz&@aGd%y?dzpP*;t{%`xDULN&5c|7xZ2- zje8}%R+gYCJsky|me{u?Bw6`N%1@PrwL&t^fu4$+U5zDbcxb3d$Qqeg-ozmnFX@lU zZn8tpVKC^uDQ-*e4mD+W?YCqsD_dGzQeIwUD=R4(T&ygZTfU%ZS?N4we(92VMdgc^ z+5}ee$lPLE&Cq*?t5HiIUR<<{#Ea&Yl-tUduBe$m8~^L3jl6j2(nS@^*!uhK2_Tl0 zJh*gu33K0<(YsBubV;dAX?uO3#8y{HWO)flD~c9%7NpW8V52$!c{Ti<&>An?6Y- x(g$r?&7JdqR=X^~e-EH134cS_>;>tacHGSgN?s^?Wg42(4W|rv260TwJ=MkO*x@S*q zA5yH=mS&RQcY)S((G`-s95eiLQF|_A_7SyR8Z3TdCI9n#^*+JbhAe*WA z?m5R+C1kMrxTKsjHZmJX#xn{0@e0WZXL+{hHn|{Yfn3Z87r99I)A=jpVpBMqYwsLQ zX-3eIl!kupIFOlR*%o_@Ya|J(wKU9A1bH<_GRT6n6k~tgA7d{d9%B|j(=5YX3QL~4 z|1-g(6lEkie+dOAobTXBI)D8)a=vG{-}!bR?)+tdOB4LqZ#==MHfu;Mg%n8ehW}7c zYdau^PP@ak8^K<-p6a1-<$6K_|GRpcC{jH>%^&R~j%2ixd$drv_YU)idpi&hcLKnr z;bt*we5TsK!c!6>UyW`PZ|T3349zf5GGnfi;Z8+*FB@(ZG%hQ+|4M@K5E$-~%hW=< zD<~afoaH;W9&T`f(0FM7V~*pJiB& zim+?Esg?^~5i{#;wV**B+p$U~bkCPt@{#!dxCY;CYi&=D-Z$6W}%!FzovyCW%u1v_F(mx}Q1U1$mvemX}o`mECw zecoVy^l3mm`a}RaUKYV7PkjjhQeO_DvT2Tt;^&0nl_`G18#$7OU;dd4zdFco_$452 zcn!d%X?@^lN~@(Vghixu&Oe9Oi+=I+ST5>kHTkcO89`2}J6=abWb_j&t4Ow4j3}zB zG1H)S+3eknTzQ?R71h#CuYT|}9FVF=V~|Ya3Dlm)JsNz2_ef)d^HHxJ_X_Y4E$x=2`v1CvUE&_FOGJBrl0Vux z9LZ?sHdC|<2l%6%4aB3J0dQ%wL!kWrTR%r6oh{QnjY#2qVaj**CpwI^<@`|_ilJVu zrwfFdp``+GqX_`ME~supivx7m&2(Vc9ZCMFNqgdO`36Rtk|1{mCQ6@6;NQpH1XTA|^FfI86{vC&Hdr z8;M>xEd=6$_5yJEmf?6bv^-$xv@y{i=`xOFBo}^^k-igbI#Lzh2->e z^upHVeIo^d2DY z^mYK>AzV8QtpZRwT_3QyFQHYSIX{>#;dEg@VOwj3aN+a`;WQ2gy;?AO;k2c%bovv3 zuMBr}L;D_3I=wz%b9`%?wcpD?^GWXF2kYS03d^8iXe(gI==covF;AnrO73K`5 z&nR+Km#Bn1B@U1Vqoe~TI$87Sh9X{S@$@Ego+e(zwxVbjnVt|6rz3S?LaivSsl9G! z+1Ik7^bt9A@vW9^Mw6T{j^GyrhTGJD+f=qaJ$20b) zkr>6-_XKlhmaCC$Rc31ZqdrntGL01Q#^rkatMY4BpJ`V&urrx&UDKU=ck7UPO1i@y zsn^bhF}ZYJpUUmDbyUqxW{t2$dfbl->onddie#g*d#i7-dD%|&SLV&`sjg#Zvq$vU zj_;V=MX(vTEl&A{PkzhqL}H$&?;P>PoBS=C)_q9$4V)v(ZzbE){ccNTBg^TLqJGFq zde{=u8clPDiH35b&(moC=V@Xio8MzJGVoE4KInsU`X@!|)y^%RgYfe{zAxI&`z|NGYp58$K`@&jhM^6Li`Jfx2sy^mb93#7zNUeTE}eSbF?N4$nngcW^K9;& zYFZjD#7$kW6}u6z12_mE8ZD7PdjNYU)-``|eAZD)g!8a&6d$O~<$30pwS0k59)cy&6sI(Sj567XfQ0*;v!7u!$3z(-Bm?F=;DH2De_rdAt+%7+!pRP%|hT^ADZSQ&<{XogBBhz%~a5C z2Tk*7(29>y1E3#&Vw#6R>pnBh7SQNJrWuFJ(=&%n^BU0YUzlb&=zh%U51?=U!!(zI zp8FExLBGLr)LRsnG7&3w9*of_uFmaX07Y5|x(pj&B4`S>;%v}x9Azs&$ARtw{Sveu zbP~?-^Pqb{JL6s#gEw3u=o`2wOa!G{%xutGad%n)Is@0sU7+J}$E~M7ryj<2=zI_x zHnfX9@*~qsLo$@1?eT%4D5>~ejb96!H#9xJ1S#Td5}~24m6`bI;7>ye{LKNd2tN<| zVrW;_Y9w$vNuA-53oS!B2DiIQ?a)3%!BMXq!0!|i`(qnvc^wj6+D1MqAddw9 z^rxmdzm5EnfILq{g2#Vunkj7*;sOdK;NO6ii^T$5oM1#iaU`*M9 z&)ID_JE`%d1ief0*Tr$R!j}+mrR2BZKTT?U=|Fc%{w!P3Awc`zX|+KnAn=0f}bhH=ZMd? zTB6hrY)`38&E$Q5-e1pB%53&~Q7l#^UWu#_Lr@*O)w1i$Z0cODxt#Z#c>fOD3jPO# z?O$PRCyQ07Dv*ODY2=!RnRUF)u3+2K%ueM9p;PKal2q-+bqb_T5kChMg_k7sMsuB8 zdB30AoWb$q)K@+uM-AyEOh#kXsBM(|8|V~4J0^&-1N=w@B~T$5lDs4J_jMBtzF`0l zkr%BAQU_(weiq1Tu`3}Ja-bybSxUJr->!)`b-B>DNNFzkR%Ada5)2sOZ2*n3kCCIa z9)kOWjG>aG>P)Wl1au;?LM0VK$KUT}Tgz>gi=@=QetzjvX?ZWbCDHg61>Ge1U!`!i z(u6gnwpH?P>LZ?e$eA4FbMQ*(ELm7Vs4Hp>B+Wl1rG0w~u74jz1dE&$$T=87taN4S z1e-lqO83c{eMCwJu>1+O#BwS9P3CihoX>sWccJyL{9>tjYQi2%&D@)>R%_n9B|~NA ztrOVPifp!{qKG{;D2jEhw6WsKJ8RyqyhpQ4No74J=Y;udwW@$koxBj+;^gGqPR&Dv vZ$FI=s_8l9g4$6J6?}Y|DcP+5)a!=-l60d+#%64oT$e*FWz4 zZZ_Y{T6?Xv*IIk+wfF3O&dFi3vt&`4&8Dd`OuJm;+C)JqRE+N_MAHhjx!QPqPtnfS zMu0pVpF+vD!p=0oP;iu#8wN=2vIyx1o+-$hg33dJ)NY_sIZ|*G)G|b!+Sv(}bt{~8 zfsj*B@eO;0f^Y5UHdd->3YyzdY?_ydcFotP!Uh?a770~5s#3fAWxM-jI|X}XAQV*b zq?piWu8gM@=7L8>L2J88+0F_Vie{REWP{4`!y)fKW3N=U+g&8|SmASmtSPAMU4wQM zmy<1On3qX=1C2wSjK6|1yxPnv|Ef#!W>$G;RQYSd^)u?{UNYm7yqsWd&TMvp{Bqz! z!++(~Wg6795lx0sOKT8yrH^>ukbIKwI(+25vZAs#r#9ujvhBIOM~c25sV5ooo7#{J z71F0tgzAKk#fM}>JZ=UiXao=Xp06f2P?-Y8Dg7`>SAX;d&>i?#3)$~ae!>9sD+iz# z4nV(p0QxyiKx%QXKFumtu7o2YQa#TrlM|x=5_}|o{F{ZipsU_)gFHp z2pIngq6R|lYLCALl!|(fd$qsDQ{}(Wr&ar^gT9bf7x34FR+FBJ>N<_uP>NilU0G5* ze}Q{;PM(3y&bipY=j3Vb;uTBWUSGhs#vcs%0xOm*sH&~;t?;a>^1;-a>e?D<+$}`= z6-2+%kRdjFhT}v3Y+R%K7^YDMsd?}XzkfWf5N=5ePxgRd6J5<2;>ApKIB1k!6Mc+9)wDhn zo#urqyCj`{4$Sjsw~0>kO_dfCT`uX&YBSNrd=o96G128u4l@pz=qeV<#t9r8zS)c^s_&AMS4cLbo*;dHO>FTh zb^4Tr8rw@`e}&_Kx&Eo8e#BCL-%@|uQhz0-KI^06=)oIY(RW>uL%pRdoU__y?RPa5 zEI11t&-^V!kFCy0pK>dlM1YzKUIWF|WPg#syuMH-Lbro(gl=@AD}71>$vq_@)=bC3A*04|QuBOaqW{@WbTWR?B zc)3+aY#j;ibVVC?Qp=9U`vJhxT)Lp-GbL2!(zmmO&h_Hx0avuWl}^YAZHN``1J=k` zM+<3)IndD#`=%x0B%#|wz#?t7sC_Mv_)%n?^w4l-%rwxVjl0N|$P)y#a8c{&!pa2J zO;#2#Catu7NmjC;lKQ3FCjetBb|CR6S;l(y zu1gmt2^U7QVRB(IW737kk5dp%g*_33(ZJXR4M_Y}5M>Ztdf_Mp;eha<6M&OL7nJ-) zqOb=np>th&Fj;u8y2XQA7D({E@0btFIKj12gbIy0_jdSO~|*g zI=OUbpVxJvn6e>z?C9r-#QvvFW~LVoJkuAGVl~q_qnk2Uv5yB^;k&knyNWPxGYdY$ z-O7A?I(5skBvd2?jhR{JAVHC*ktZlUX=b7|1t-K>XkZ>ap@gGrJ zF_YvH#97wCStk3*ltJ?_M|d!r?ehF*OnUHmAJ2c76CR8P#vW)uQu6RxOeSj{u0iXQ z$wT}ELH)p~_#~DHYbw5a9%#rgxSvc#8#$qJ_8IP3<^@uaI^6l%omXdFd@PtEj3rsOk) z^N{O0*JU0SkzkYEe=Y$3MBK=j9G}d8(D(!wsqygxdNE9~Db)DmO^iwNli2)v*b?Tefw6fnkho3}X{=J_ zONIHB03?mnV@iG_QTQi{>pIt^d881TKk`l0`vhas{7;Wj@B1)9!u%0nZ2mBil%*jb z8!hY7P=eMcvou6{$_HK-9z2a4)`hrPP_7H7NI~-6SQpyNf^uC5 zNWqkK;U{K6xh|AQ!IX8OCMhU3$ZEm(D|TKyE$tO*=6cLsF*M78@z5*=5?_&KM6H$kq&zB89_>3&;y zbgx5s^eAJ}qhj{x!9wNHy};O`JAuUS(~=(1fEg*P)9j+yw+}gX}_|#?3`zip%rnO5RFY zHkTvdfiCys|4g>5*2s|E#=2>(J%uqDiC-P1NQ}KgMZyM*Bhhy`#-=uzEyl*Sn<7HP z^3NnnrEn&Mffhgdjf_cW9PG>{ScYAsZU`7VvldAFU2ZEjpS&hw7c(Xkif|MvYV`}v zlUW~ib17p|^S&e0&1q00G*1S`nkNDo=*R<&l>T3zY~L5Q@1tV?Z)F~1Oxj+|ws#dM z+xG!u+uMPpBwIIKudpQB@sCb6*-8igJ^3~ickua~x*u=Bm%g1=!RUeZt~A>LTU&Im z{x}h2O44fFl=&U*hWN>$&E>z5e_uZ%>Cy)WE#+KTFM_lI=R*;sA2KF`l#On7*dyOk zLD~R}gH#73{si2P2>)+>g50D>LDFxVo4`@%#Hm3{j2lmuK^M{+g-h?#=9_u;U*LD*q!!kV!o!8^ z7iD!BW75OkZu0PIxFfQ9Au#qZ9|&i4N8_ImE#h{2^zDR8pQC&s;aS|=#`Fna-g=8L z^agGAITRUYLtEI;;meevH-NFB7lCw7jR-3@KS2g4`s;oy%}@X`CqSnqtIvhPlUYJ< z?s35RffIYvcuji}u4}OnFp-;^&`F#X0jSUfCJKHn0C?BIb>t@Ma@TRY=>1!?PvG7k zVN8ZO`XPn+gL$g=Zvo@pzXT-iL4S*AM$syp$MI`#r7EA0g>&FcZX12$SzS1T*H`jt|v~;Erur|2MU3)bC&{%KaHPwBTLCCWN9-ums!d* z4ATAMz@seHHnWGL0H`2*Od0ej_haGVeOb^U)<4Fihl|<6U2~O(_W@%Mw*!e!rA-LA zE+W;WB9*b3{m25K0za5C=$YPAIkJ!tY2kMcT2Mdw4zo74(oP zgB}}q2xs!x1?r)bG3iX#2h_vMVOjJr4;VXhA&__rh{%QV&9D{SYwGJh|<;PZF?8jyxDZ7G~M?ryQ{e1uM$*rG}o)yzj8@$Wwja z28{b20TSPVNn^NmU?f#CKj7vETW3SBVXlXHsT;!2aPY(2;;l?XY!z@d@)6 z;3yDvJ3@q&Z%34IW8OtX(Nx}!Py*t1#4MI+Z+%Qy-eg#Qfic<4qn=9BmqHh&F_ZSDdRdmg`(y8V|xrOl|JbGcdP z8Oo;L;9p_pf6>ML{4Ue%*F#^2{VIG5n+3qwW*(4OxQA@c0xE508#@1%K|PlJd=NUh zpLT=)teL+>*!+TN_V`S)`6|AJ&HcdGW*d;$Ask|L`x8K=%|CN18qc+6opY4UUmAQm zrWrn+Ds0}vG2?oLY4c}>&UCZRSKpTX+-&f_m|%*-AHdh`6QM^R zKbF2TN8@|?!RX%T^YE|2tV?Iv_zX?E{waFCAiD9m?mV8Iz`pf*<5nb|EpWJ^8)MP% zA>DZhJD3*}I9xmXI-JL-AKh2#WqrEynC$?!i#m^EDT(Q2$6f93+FiB-I{Fi~Lv8m( zR>nN%A;CE%g7!CWJVu7s?QVg7^V(yzzbw^$@k!Vp$vRKggGW#FAOjKTb451xX<@1B z3|cW;M>WVz6>aTG-by5RK{G9!k)CAJ&}k>O$wHgv6!}~k>}Xk+E4rk!BA!EoP(?N# z*TNH7ra?nj3TOn@pF`vjbugwU$?K%nr!nYOt$Qgm(yUZmXDJ=7JG=Vl;ISq2ESiQe z;>^(AWruVIsX)@SV!nyZD#i8K8<@J0jh$L(`ia6#QXEgkhFoQ_HWJJ1wBmEK?5s|LIBOX!bvB<__Mx?(-GR|YN zAJM%eOxrHvHuh>EpRf~S z`%U9$oH{Y@L^Ti95ib2&JWOJ9MX$lYE&}D+ELXG$$$w224H}K_0CCBD9V?cz4*`gv z$2V{6p(ISd?QVxAQvS*$xRWlFT-fee}C8>GOg=%jg+DA`hGpHfb< zk7k{k?wcsXoxN(&^HNm6LnyWGReD)30wO0OEl)12k7&-T1&dq(ejoPO^PF91#Unt= zp&<1tMUx~6hG<1ZhE3VDl8s43D9X|>`s`3rq0$|f$))!X3?Xa6lQZL z+&r|N6z}gu)D7=LE>fd6G(b<23lpfUGx|fS5a!ycFX@eryCvC;(3$=Ce|PuD=I-Im z1COLD8Wy1rQ7^iTUeF2$eO)*eMEZt>mf$OW%QO%peYVhqNMBm|tqHgipD5*A`dkus zHMtzzthA6p9Ujqh#)_kT6oc(}3MGD+I{?L{%o3CONBH@bxl+H8TF@_;v1qy?ZOCfM z`YxA_CkOY2uS+JAk*hqxN^H@BA$MJ%w!#+-xg|EizF=PV1>e_WaH~xOtSC8%p!3s~Er@|lFAgeXu zswz0de}XIo0zT#6YWy{@XgVg_J2Ttspg{>b>O8@q&pSr5oiQx`IdnY>pStI9I}tmV zF2M5vdjSgpOJBfK*x=7QkVvdV{hKc(67_%&V}r00a340yw10dENB2&^({M?t53mQ9 z+OlwU`6AqrnhW?FTpn5pcrPC4s0Vx$a3^36E~m5r?gs1x>;~)uJOdB=(EA*k|Bm*6 z-@=1xD*^8UtOqQ>V|zORN8sVb7CybI<`n@VJA|2=$mLyj8~5rya> ze0G6e4yX#*+J?_-u=gSP=A4jmTiSy0BZj>*3@))RkAVDhT)Ehaz9#vb(-fb^W$ep| z#2LfQ{8NT2ehE4Ew{XY$QSUtH3Ws4pTpp{GS{AZQ#F&T)y6{KW&BK-y`5pLXQ6;g@3ofe-8Y6 z!QY?4Z!`Ek;4i}5s7kS4!S<=iP#KEAPy~h|Fcg8I2nA zoP_GdK6oCC%e2b`#KUV`)cbqzm?9V2qfwbGfTrH#L;E5sivP)1iCU`frad4EZ6&Gn z(ViA%gaBHn`N)A^xxqpZ&lW?h;>z%TrxbUv{+!EVGuJ4h2TrlOj>W5VS zF9Yg5Z&ro5@(@!fVX1`W64puBAfdJW*VRXr3yCWiEV#^Ze%Y#UO(^V`H793IPVS6L z!d#ubX?9*tZeGr;>5?&+PRiTi+Q)QtJ*Ck2*udE)1L%-kJPOmVJdIx(l0BCX9a0#; zA?f87Iv&trc{P7f3^kfVbJ~IQ-GmAw3tDN|JRtEhFN&l{#n0!VCeTAg2lM4}_jX|%L^y?g` zpzxU(aEqkxo62O3&%!|Omh^kF1)a~pKz}3YZ(bnieD($UIX?Zx?VyxjGF!;2^m<*= zZ;f&qpC|!&Qi_PAO;t{rU4H%b%`U*M$+haGt8E_a`bflR&p& z^;PSuqR#{!k3OAHN(P{pgYGy%TGNg)9ltW=smz-O(9;BZhBnEVcdFH$ET5s>O%oO+ zR{-rliJmr5Q@>}DtkK!KKmR)hpuau<{UgxHAFKbL4y@iq(AD?f*-tF;GBNqu z7?^IUr(DL9uc3fmBk3RE5oaoVZ36HXsXwEfsLAppDZdI2{7~U*L4ZG&^kS)puYrL6 zu%!QfE=l7W3-Xo99?&U1R{j44x`QQBzYa+~&r1LK8V*E00exz!DAy`UJ`KAe>+jw2 zia=;qPAy)d=vn1=hdgUE5Q3rb>eaZIoqXY^2eoDObEr49jJ26UVsrZK9bkfSbtuT?MbijR|+1->V_qUU#snHY6}# zy{96CFfUqGw8ZIlUcJzbH)Sro?&_i?#S1{j`%~Rcm*lz@F4NpsmMooLRN`K`Xwh=# z3ipbl`6W)Yu)gQBP`$Ym@930o2Q^6a3eUmb+ez2+Q-ytAPspRyuCH>7k-*)8P@ops zLAe9tsc#)M+UCjkiW&s6EZ#VpLPF+3zj#rodMbpuEZ@6yks=#l)N3{ zE?x>vUVn`{j5`e`z8G2!Np7a{&6=c=WVX27t8n*--<+!Pn^TiQso9gH)6J4~beqNP z#*0wZVA2~>`+eVPHHF0~HMJb?h8maaf27| zRe7j^#OkU-S`G&?2WSrNq-l%?b%JvGX zeF4c*QTtC((=_^-HMghV1u6 ZoU*HA=@{2v`>yW@33~qCTr5@Xe*lb(QNsWL literal 0 HcmV?d00001 diff --git a/tests/test_process_tree.c b/tests/test_process_tree.c new file mode 100644 index 0000000..8e53f24 --- /dev/null +++ b/tests/test_process_tree.c @@ -0,0 +1,200 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * test_process_tree.c - Tests for process tree construction + */ + +#include + +#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; +}