fix: release the argv container (executable plan, Task 3)

- program mode built the entry's `multi Text` of arguments and never
  freed it: the entry only BORROWS a parameter (never a `take`, and
  the drop tables never drop one), so the runtime that built the
  container owns it
- dropped after the entry returns and after a trap alike -- the
  container outlives the unwind; `multi_free` recurses, so the
  argument strings go with it
- one site covers both invocation shapes: `self_rc` picks the argv
  offset, it does not build a second container
- measured: watch, run and the full MCP mix now report ZERO leaks
  under ASan -- the clean baseline the soak (Task 6) reads against
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, wovm-test green,
  log-watcher 6/0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-14 23:41:13 +02:00
parent ef74d157b6
commit 4a2fc2041e
3 changed files with 24 additions and 7 deletions

View file

@ -45,9 +45,11 @@ running", and every item below came from a measurement on the sample itself:
**2 112 B → 64 B** and flat from 8 s to 20 s, the full MCP mix
**21 312 B / 63 → 64 B / 1**, every handler flat from 2 to 6 requests. The
64 bytes left are item 3, on every path.
3. **The runtime leaks its own argv container** — 64 bytes in 1 allocation on
every run, `main.c`'s `multi Text` of arguments. It is now the ONLY leak the
sample reports in any mode.
3. ~~The runtime leaks its own argv container~~ — **done 2026-08-14**. The
entry only borrows its arguments, so `main.c` releases the container it
built, after the entry returns and after a trap alike. **All three modes
now report ZERO leaks under ASan** — `watch`, `run`, and the full MCP mix —
which is the clean baseline item 6's soak needs to read against.
4. **A stopping program does not stop** — `env.stopping()` sets a flag, but
`net.accept`/`net.read` restart on `EINTR`, so a server parked in `accept`
ignores SIGTERM and needs `kill -9`.

View file

@ -144,7 +144,7 @@ of the projection.
`just log-watcher` 6/0. The image grew 35 893 → 46 137 bytes — the drops
themselves.
### Task 3: The runtime's argv container has no owner
### Task 3 ✅: The runtime's argv container has no owner
**Concept & reason:** program mode builds the `multi Text` of arguments in
`runtime/src/main.c` and hands it to the entry method, which borrows it. Nobody
@ -155,9 +155,16 @@ its own allocation, and it pollutes every future ASan reading of the sample.
The runtime owns that container and must release it after the entry returns,
before the heap is torn down.
- [ ] Drop the argument container once the entry method has returned (both the
plain `wovm image.wob …` path and the single-binary path).
- [ ] `watch` under ASan reports **zero** leaks for a clean exit.
- [x] Drop the argument container once the entry method has returned — one
site covers both invocation shapes (`self_rc` only picks the argv
offset, it does not build a second container), and it runs after a trap
too: the container outlives the unwind. `multi_free` recurses, so the
argument strings go with it.
- [x] **All three modes report ZERO leaks under ASan**: `watch` and `run` over
eight seconds with a clean SIGTERM exit, and the full MCP mix (four
tools, twice each) with a clean exit. Gates: `just oop-accept` ALL
CRITERIA MET, `just oop-e2e` 71/0, `just wovm-test` green,
`just log-watcher` 6/0.
### Task 4: A stopping program must actually stop

View file

@ -195,6 +195,14 @@ int main(int argc, char **argv) {
/* the entry's return value IS the exit code (docs/plan/oop-vm/
* 08-builtin-surface.md's "Program entry"): 0..255, a trap is 1 */
int exit_code = rc == 0 ? (int)((uint64_t)ret & 0xFF) : 1;
/* the entry only BORROWS its arguments -- a parameter is never a `take`,
* and the drop tables never drop one -- so the runtime that built the
* container is the one that releases it, elements included. Without this
* the workload reported a leak on every path, in every mode, which is
* exactly the noise a soak measurement cannot afford. It runs before the
* heap is torn down, and after a trap too: the container outlives the
* unwind. */
if (argv_val) wo_drop_kind(&VM.rt, WO_K_MULTI, argv_val);
gc_pump(&VM);
wo_vm_destroy(&VM);
wo_module_free(&mod);