docs: record copy-on-push (gap closed) and the SIGTERM-in-accept gap

- 08-builtin-surface.md: containers copy a TEXT element/key/value; a freshly
  built Text is the caller's to drop, a value read out of a place keeps its
  owner; OWNED/GCREF still move and set's @gc retention gap stays open
- 00-status.md: the borrowed-Text double free is struck through as closed by
  copy-on-push, with the concrete failure it fixed; new gap recorded — a
  blocking accept/read swallows SIGTERM, which belongs to iteration 8's event
  loop rather than a patch to the blocking calls

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-14 18:50:54 +02:00
parent 09047b26fb
commit b6c9b9e379
2 changed files with 29 additions and 12 deletions

View file

@ -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

View file

@ -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