writeonce/runtime/src/obj.c
shoney.arickathil 4cf548d6a0 fix: copy-on-push closes the container double-free; line-buffer program output
The unsoundness is closed. All four MCP tools now answer correctly over HTTP
(get_running_crons, list_logs, tail_log -> ["info two","error three"],
search_log -> its match) where `tail_log` used to return
{"isError":true,"text":"tool failed: not a text value"}. corpus 71/0,
woc 565/0, wovm gates green, ASan clean on the container fixtures.

- builtin.c: multi_push, map_set (key AND value) and multi_set COPY a TEXT
  element into the container. The container's declared kinds already make it
  the owner of what it holds, so storing a caller-owned pointer gave one
  string two owners — `push(res, e.log_path)` freed a record's field out from
  under it. OWNED/GCREF elements still move (not copyable; the @gc escape
  keeps their counting), so `set`'s @gc gap is untouched and still recorded
- emit.ml: `drop_fresh_text` — after push/set and the `m[k] = v` / `m[i] = v`
  sugar, a value that was freshly BUILT (call result, `..` chain,
  interpolation) is dropped here, while a value read out of a place is left to
  its owner. That asymmetry is the point: before the copy the borrowed case
  double freed and the fresh case leaked
- obj.c: the runtime's output stream is line-buffered. A long-running program
  writing progress with `print` was invisible when stdout was a file or a pipe
  (full buffering), and a killed one lost its log entirely; byte-exact
  fixtures are unaffected
- scripts/log-watcher-accept.sh + `just log-watcher`: the acceptance test for
  the sample — compile, watch (alert), run (schedule), and three MCP checks.
  Hardened after it lied to me: a per-run port (a stale server on a fixed port
  answered for it), a connect-probe that fails loudly when OUR server did not
  come up, replies read by Content-Length rather than to EOF (the sample never
  closes), and kill -9 on teardown
- docs: the copy rule is in the builtin surface; the status board records the
  gap as closed and adds the new one — a blocking accept/read swallows SIGTERM,
  which belongs to the shard-actor runtime's event loop, not to a patch here

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 18:50:18 +02:00

117 lines
3.4 KiB
C

#include "obj.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static size_t round16(size_t n) { return (n + 15u) & ~(size_t)15u; }
int wo_arena_init(wo_arena *a, size_t cap) {
memset(a, 0, sizeof(*a));
a->base = malloc(cap ? cap : 1);
if (!a->base) return -1;
a->cap = cap;
return 0;
}
void wo_arena_destroy(wo_arena *a) {
free(a->base);
memset(a, 0, sizeof(*a));
}
void *wo_arena_alloc(wo_arena *a, size_t size) {
size = round16(size ? size : 1);
if (size > WO_ARENA_MAX_CLASS) return malloc(size);
size_t cls = size / 16u - 1u;
if (a->freelist[cls]) {
void *p = a->freelist[cls];
memcpy(&a->freelist[cls], p, sizeof(void *));
return p;
}
if (a->used + size > a->cap) return NULL; /* region OOM -> trap upstream */
void *p = a->base + a->used;
a->used += size;
return p;
}
void wo_arena_free(wo_arena *a, void *p, size_t size) {
if (!p) return;
size = round16(size ? size : 1);
if (size > WO_ARENA_MAX_CLASS) {
free(p);
return;
}
size_t cls = size / 16u - 1u;
memcpy(p, &a->freelist[cls], sizeof(void *));
a->freelist[cls] = p;
}
int wo_rt_init(wo_rt *rt, size_t heap_cap, const wo_classdesc *classes,
uint32_t class_cnt) {
memset(rt, 0, sizeof(*rt));
if (wo_arena_init(&rt->arena, heap_cap) != 0) return -1;
rt->classes = classes;
rt->class_cnt = class_cnt;
rt->out = stdout;
/* Line-buffered, always: a long-running program (the driving workload's
* `watch`/`run`/`mcp` modes) writes progress with `print`, and stdio's
* default full buffering when stdout is a file or a pipe meant that output
* sat in a buffer until exit — so a redirected service looked silent, and
* a killed one lost its log entirely. Content is unchanged, so every
* byte-exact fixture still compares equal. */
setvbuf(stdout, NULL, _IOLBF, 0);
return 0;
}
void wo_rt_destroy(wo_rt *rt) {
free(rt->cycbuf.items);
wo_arena_destroy(&rt->arena);
memset(rt, 0, sizeof(*rt));
}
wo_hdr *wo_obj_new(wo_rt *rt, uint32_t class_id) {
const wo_classdesc *c = &rt->classes[class_id];
size_t sz = wo_obj_size(c);
wo_hdr *o = wo_arena_alloc(&rt->arena, sz);
if (!o) return NULL;
memset(o, 0, sz);
o->class_id = class_id;
if (c->flags & WO_CLASSF_GC) {
o->flags = WO_F_GC;
o->rc = 1; /* the creating reference */
}
return o;
}
wo_str *wo_str_alloc(wo_rt *rt, uint32_t len) {
wo_str *s = wo_arena_alloc(&rt->arena, sizeof(wo_str) + len);
if (!s) return NULL;
memset(&s->h, 0, sizeof(s->h));
s->h.class_id = WO_CLS_STR;
s->len = len;
return s;
}
wo_str *wo_str_new(wo_rt *rt, const char *bytes, uint32_t len) {
wo_str *s = wo_str_alloc(rt, len);
if (!s) return NULL;
memcpy(s->data, bytes, len);
return s;
}
wo_str *wo_str_concat(wo_rt *rt, const wo_str *a, const wo_str *b) {
wo_str *s = wo_str_alloc(rt, a->len + b->len);
if (!s) return NULL;
memcpy(s->data, a->data, a->len);
memcpy(s->data + a->len, b->data, b->len);
return s;
}
int wo_str_eq(const wo_str *a, const wo_str *b) {
return a->len == b->len && memcmp(a->data, b->data, a->len) == 0;
}
void wo_str_free(wo_rt *rt, wo_str *s) {
if (!s || (s->h.flags & WO_F_CONST)) return; /* interned: outlives all */
wo_arena_free(&rt->arena, s, sizeof(wo_str) + s->len);
}