runtime: state_get leaked its value on every call #140

Merged
will.anderson merged 1 commits from fix/state-get-leak into dev 2026-08-16 13:10:00 +00:00
Owner
char* result = el_strdup_persist(e ? e->value : "");   // never freed
pthread_mutex_unlock(&_state_mu);
char* copy = el_strdup(result);                        // arena-tracked
return el_wrap_str(copy);

Two copies. result existed only as the source for copy — never returned, never freed — and el_strdup_persist bypasses the arena by design, so arena-pop could never reclaim it. Every state_get leaked its full value string, permanently.

Measured — 200,000 calls against a 64-byte value:

peak RSS growth
before 15 MB (~75 B/call — the value plus overhead)
after 0 MB

Impact. The soul's awareness loop has 68 state_get call sites and ticks every 200 ms. Live before the fix: RSS climbing 112 MB per 20 s ≈ 19 GB/hour in awareness_run → one_cycle → perceive, while node_count stayed flat at ~13,479 — growth with no data behind it. Drove the host from 20 GB free to 4.3 GB in about an hour.

Why now, since the code is old: the soul used to restart constantly (no write-through, divergent graph). Stabilising it in neuron #162 let it stay up long enough to accumulate. The fix didn't cause this leak — it removed the crashes hiding it. Same pattern as the test framework surfacing math_log.

Found by Ishikawa, not by reading the nearest code: method — arena push/pop is correctly paired per tick; material — node count flat, so not data growth; environment — 19 GB/hr ÷ 18,000 ticks = ~1.1 MB/tick, so per-tick not one-shot; machine — an allocator that bypasses the arena. The evidence pointed at machine.

Verified: fixpoint byte-identical; state round-trip correct for hit, miss, and overwrite.

```c char* result = el_strdup_persist(e ? e->value : ""); // never freed pthread_mutex_unlock(&_state_mu); char* copy = el_strdup(result); // arena-tracked return el_wrap_str(copy); ``` Two copies. `result` existed **only** as the source for `copy` — never returned, never freed — and `el_strdup_persist` bypasses the arena *by design*, so arena-pop could never reclaim it. **Every `state_get` leaked its full value string, permanently.** **Measured** — 200,000 calls against a 64-byte value: | | peak RSS growth | |---|---| | before | **15 MB** (~75 B/call — the value plus overhead) | | after | **0 MB** | **Impact.** The soul's awareness loop has **68 `state_get` call sites** and ticks every 200 ms. Live before the fix: RSS climbing **112 MB per 20 s ≈ 19 GB/hour** in `awareness_run → one_cycle → perceive`, while `node_count` stayed flat at ~13,479 — growth with no data behind it. Drove the host from 20 GB free to 4.3 GB in about an hour. **Why now, since the code is old:** the soul used to restart constantly (no write-through, divergent graph). Stabilising it in neuron #162 let it stay up long enough to accumulate. **The fix didn't cause this leak — it removed the crashes hiding it.** Same pattern as the test framework surfacing `math_log`. **Found by Ishikawa, not by reading the nearest code:** *method* — arena push/pop **is** correctly paired per tick; *material* — node count flat, so not data growth; *environment* — 19 GB/hr ÷ 18,000 ticks = ~1.1 MB/tick, so per-tick not one-shot; *machine* — an allocator that bypasses the arena. The evidence pointed at machine. Verified: fixpoint byte-identical; state round-trip correct for hit, miss, and overwrite.
will.anderson added 1 commit 2026-08-16 13:09:42 +00:00
runtime: state_get leaked its value on every call
El SDK CI - dev / build-and-test (pull_request) Failing after 14m26s
9c07970943
char* result = el_strdup_persist(e ? e->value : "");   // never freed
    pthread_mutex_unlock(&_state_mu);
    char* copy = el_strdup(result);                        // arena-tracked
    return el_wrap_str(copy);

Two copies were made. `result` existed only as the source for `copy` — never
returned, never freed — and el_strdup_persist bypasses the arena BY DESIGN
("state_set, engram internals"), so arena-pop could never reclaim it. Every
state_get leaked its full value string, permanently.

MEASURED: 200,000 state_get calls against a 64-byte value.
    before   15 MB peak RSS growth   (~75 bytes/call — the value plus overhead)
    after     0 MB

IMPACT. The soul's awareness loop has 68 state_get call sites and ticks every
200ms. Live measurement before the fix: RSS climbing 112 MB per 20s, about
19 GB/hour, in awareness_run -> one_cycle -> perceive, while node_count stayed
flat at ~13,479 — growth with no data behind it. It drove the host from 20 GB
free to 4.3 GB in roughly an hour.

WHY NOW, since the code is old: the soul used to restart constantly (no
write-through, divergent graph, 2.11 GB). Stabilising it (neuron #162) let it
stay up long enough to accumulate. The fix did not cause this leak; it removed
the crashes that were hiding it. Same pattern as the test framework surfacing
math_log — the defect was always there, something finally made it visible.

Found by Ishikawa rather than by reading the nearest code: method (arena
push/pop IS correctly paired per tick), material (node count flat, so not data
growth), environment (19 GB/hr / 18,000 ticks = ~1.1 MB per tick, so per-tick
not one-shot), machine (an allocator that bypasses the arena) — which is where
the evidence pointed.

el_strdup tracks into the thread-local arena, which touches no shared state, so
taking the single copy under _state_mu is safe and removes the temporary
entirely.

Verified: self-hosting fixpoint byte-identical; state round-trip correct for
hit, miss, and overwrite.
will.anderson merged commit 1119295238 into dev 2026-08-16 13:10:00 +00:00
Sign in to join this conversation.