diff --git a/docs/00-status.md b/docs/00-status.md index 8417268..eda66c0 100644 --- a/docs/00-status.md +++ b/docs/00-status.md @@ -180,18 +180,21 @@ recorded, not silently owed: - **`using` extensions and `#if` build flags are absent**, and the reject rows (`extends`/`cast`/`Dynamic`/…) still have no doctrine-citing diagnostics — plan 8 Tasks 7–8's remainder. -- **A borrowed non-constant Text pushed into a container is a double-free - hazard** — `push(m, v)`'s open gap, now shared by list literals (`[a, b]`). - **Reached for real on 2026-08-14**: the MCP server answers `initialize`, - `tools/list` and an unauthorized request correctly, but a `tools/call` of - `tail_log` returns `{"isError":true,"text":"tool failed: not a text value"}` - — `Mcp.allowed_paths` pushes `e.log_path` (a Text the entry record owns) - into a fresh `multi Text`, so two owners free one string and a later read - finds recycled memory. The fix is a coordinated pair, not a one-liner: - `multi_push`/`map_set` must COPY a TEXT element (as `slice` already does), - and `owner.ml` must then stop treating a pushed Text as escaping so the - caller's own fresh temporaries are still dropped. Until then, treat - `push`-of-a-borrowed-Text as unsound. +- ~~A borrowed non-constant Text pushed into a container is a double-free + hazard~~ — **closed 2026-08-14 by copy-on-push**: `push`/`set`/`m[i] = v` + copy a TEXT element, key or value into the container, and the compiler drops + a *freshly built* Text right after the call (a value read out of a place + keeps its owner). The failure it fixed was real: a `tools/call` of `tail_log` + used to answer `{"isError":true,"text":"tool failed: not a text value"}`; all + four MCP tools now return `isError:false` with correct payloads. + `OWNED`/`GCREF` elements still move, and `set`'s `@gc` retention gap is still + open (see [`oop-vm/08-builtin-surface.md`](plan/oop-vm/08-builtin-surface.md)). +- **A blocking `accept`/`read` swallows SIGTERM.** `env.stopping()` installs a + handler that only sets a flag, and `net.accept`/`net.read` retry on `EINTR`, + so a server parked in `accept` never observes it: a plain TERM does not stop + the process (`timeout -k` / `kill -9` does). Graceful shutdown needs an + interruptible wait — the shard-actor runtime's event loop (iteration 8) is + where that belongs, not a patch to the blocking calls. - **A temporary record whose field is iterated is never dropped** — `for e in parse_dir(dir).entries` keeps the entries alive (good) but leaks the `ParseResult` shell (its drop is recorded for no register). Found in the diff --git a/docs/plan/oop-vm/08-builtin-surface.md b/docs/plan/oop-vm/08-builtin-surface.md index aef1200..059054d 100644 --- a/docs/plan/oop-vm/08-builtin-surface.md +++ b/docs/plan/oop-vm/08-builtin-surface.md @@ -68,6 +68,20 @@ Reads differ by container, deliberately (amended 2026-08-14): **Shadowing.** A user-declared free `fn` of the same name always wins. A declared name is never silently replaced by a builtin. +**Containers copy the Text they are given (2026-08-14).** `push(m, v)`, +`set(m, k, v)` and the `m[i] = v` element write COPY a `TEXT` element, key or +value into the container. The container's declared kinds already make it the +owner of what it holds, so storing a pointer the caller still owns gave one +string two owners — the driving workload's `push(res, e.log_path)` freed a +record's field out from under it, and a later read of the recycled memory +trapped `BOUNDS "not a text value"`. Copying is the only rule correct for both +shapes: a value read out of a place keeps its owner, and a freshly built Text +(a call result, a `..` chain, an interpolation) stays the caller's — the +compiler emits that drop right after the call (emit.ml's `drop_fresh_text`). +`OWNED`/`GCREF` elements still MOVE: they are not copyable, and the `@gc` +escape below is what keeps their counting right. Only the `@gc` half of the +old hazard remains open. + **`push` and `@gc` elements.** `push(m, v)`'s value argument is never a resolved callee parameter (`push` has no declared signature), so the owner pass's ordinary Take-gated transfer never reaches it; a `@gc` value