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>
117 lines
3.4 KiB
C
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);
|
|
}
|