feat: bounded mailboxes + WO_T_ACTOR (trap 13); try-arm place-copy fix
- cap 1024 (WO_MAILBOX override at boot): sender-side atomic reserve/release on every path — same-shard, cross-shard envelope, OOM rollbacks; full mailbox traps the SENDER catchably; delivery pop releases; overshoot bounded by in-flight sends (disclosed) - test_mailbox 12/0: exact cap single-threaded, two racing senders win exactly cap slots, drain/refill clean - corpus run/mailbox-full-trap: parked sleeper, send loop catches "actor mailbox full" after >= 1024 sends - pre-existing compiler bug found + fixed: a try ARM yielding a Text PLACE (bare e.msg, try box.field) aliased a register the arm's scope end freed — ASan use-after-free, SEGV on the next unwind's double-walk; emit_try now applies copy_place_text to both arm results; pinned by corpus run/catch-msg-place - db-bench driver: msgrate keeps iteration 22's unbounded-flood contract via WO_MAILBOX=MSG_N (the cap is 24's policy, not 22's) - battery 12/12 fresh-built Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5fc32b4926
commit
dd7dd42bd1
11 changed files with 216 additions and 6 deletions
|
|
@ -2600,6 +2600,12 @@ and emit_try (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : Ast
|
|||
(match expected with
|
||||
| Some t -> emit_expr p f v ~dst ~expected:t body
|
||||
| None -> emit_expr p f v ~dst body);
|
||||
(* iteration 24 fix: a try ARM's value crosses an ownership boundary (the
|
||||
binding the whole try feeds), but the outer binding only sees the Try
|
||||
node — it cannot apply its own place-copy. A body arm that is a Text
|
||||
place (`try r.field catch ...`) must copy here or the binding aliases
|
||||
a register the arm's scope end frees. Same rule as any binding. *)
|
||||
copy_place_text p f dst body;
|
||||
f.f_cur_line <- e.pos.line;
|
||||
put f (ins_abc op_endtry 0 0 0);
|
||||
emit_join_drops p f v ~node:e.id ~label:"TRYBODY";
|
||||
|
|
@ -2635,7 +2641,13 @@ and emit_try (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : Ast
|
|||
f.f_cur_line <- last.Ast.s_pos.line;
|
||||
(match expected with
|
||||
| Some t -> emit_expr p f v ~dst ~expected:t ve
|
||||
| None -> emit_expr p f v ~dst ve)
|
||||
| None -> emit_expr p f v ~dst ve);
|
||||
(* iteration 24 fix (the catch half of the arm-copy rule): a bare
|
||||
`e.msg` arm aliases the Error record's field, and the record is
|
||||
dropped at CATCH scope end below — ASan-confirmed use-after-free
|
||||
(then a double-walk SEGV when a later trap unwinds the frame).
|
||||
Copy the place out before the record dies. *)
|
||||
copy_place_text p f dst ve
|
||||
| _ -> emit_stmt p f v last));
|
||||
emit_scope_drops p f v ~node:e.id ~label:"CATCH";
|
||||
f.f_nlocals <- saved_locals;
|
||||
|
|
|
|||
|
|
@ -216,6 +216,15 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
VM.rt.wal = &WAL;
|
||||
}
|
||||
/* iteration 24: the one mailbox cap; WO_MAILBOX shrinks it in soak
|
||||
* tests to force the fail-fast policy (0/garbage keeps the default) */
|
||||
{
|
||||
const char *me = getenv("WO_MAILBOX");
|
||||
if (me && me[0]) {
|
||||
unsigned long v = strtoul(me, NULL, 10);
|
||||
if (v >= 1 && v <= 0x7FFFFFFFul) wo_mailbox_cap = (uint32_t)v;
|
||||
}
|
||||
}
|
||||
/* the arc's stage 2: all cores by default (the brave landing), one
|
||||
* pinned worker vm per extra core; WO_SHARDS caps or forces it */
|
||||
{
|
||||
|
|
|
|||
|
|
@ -92,9 +92,14 @@ static int wo_vm_adopt(wo_vm *vm) {
|
|||
e->actor->next_all = vm->actors;
|
||||
vm->actors = e->actor;
|
||||
break;
|
||||
case 0: /* a cross-shard send: mailbox + activation on the HOME thread */
|
||||
if (actor_push(e->actor, e->payload) == 0 && !e->actor->active)
|
||||
(void)actor_activate(vm, e->actor);
|
||||
case 0: /* a cross-shard send: mailbox + activation on the HOME thread.
|
||||
The sender already reserved the cap slot; a failed push
|
||||
(OOM) must hand it back or the slot leaks forever. */
|
||||
if (actor_push(e->actor, e->payload) == 0) {
|
||||
if (!e->actor->active) (void)actor_activate(vm, e->actor);
|
||||
} else {
|
||||
wo_mbox_release(e->actor);
|
||||
}
|
||||
break;
|
||||
case 2: /* a home-routed free: this arena owns the object */
|
||||
wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)e->payload);
|
||||
|
|
@ -600,10 +605,30 @@ static void fib_reap_all(wo_vm *vm) {
|
|||
|
||||
/* ---- actors (arc stage 1 Task 3) -------------------------------------- */
|
||||
|
||||
/* iteration 24: fail-fast backpressure. The SENDER reserves a slot before
|
||||
* anything is enqueued anywhere (same-shard push or cross-shard envelope);
|
||||
* the home thread releases it when the message is popped for delivery.
|
||||
* Reserve/release are the unit-testable core (test_mailbox.c). */
|
||||
uint32_t wo_mailbox_cap = 1024;
|
||||
|
||||
int wo_mbox_reserve(wo_actor *a) {
|
||||
uint32_t old = __atomic_fetch_add(&a->pending, 1, __ATOMIC_ACQ_REL);
|
||||
if (old >= wo_mailbox_cap) {
|
||||
__atomic_fetch_sub(&a->pending, 1, __ATOMIC_ACQ_REL);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void wo_mbox_release(wo_actor *a) {
|
||||
__atomic_fetch_sub(&a->pending, 1, __ATOMIC_ACQ_REL);
|
||||
}
|
||||
|
||||
static uint64_t actor_pop(wo_actor *a) {
|
||||
uint64_t m = a->msgs[a->mhead];
|
||||
a->mhead = (a->mhead + 1) % a->mcap;
|
||||
a->mlen--;
|
||||
wo_mbox_release(a);
|
||||
return m;
|
||||
}
|
||||
|
||||
|
|
@ -689,12 +714,19 @@ int wo_vm_actor_send(wo_vm *vm, uint64_t addr, uint64_t msg_val, const char **ms
|
|||
*msg = "send: nil message";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
/* iteration 24: the cap check happens SENDER-side on every path, so
|
||||
* the sender always learns — fail-fast backpressure, catchable. */
|
||||
if (wo_mbox_reserve(a) != 0) {
|
||||
*msg = "actor mailbox full";
|
||||
return WO_T_ACTOR;
|
||||
}
|
||||
if (a->home != vm->shard_id) {
|
||||
/* cross-shard: the HOME thread owns the mailbox — send travels as
|
||||
* an inbox envelope, ownership moves with it (the mutex is the
|
||||
* happens-before edge TSan sees) */
|
||||
wo_envelope *e = calloc(1, sizeof *e);
|
||||
if (!e) {
|
||||
wo_mbox_release(a);
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
|
|
@ -705,6 +737,7 @@ int wo_vm_actor_send(wo_vm *vm, uint64_t addr, uint64_t msg_val, const char **ms
|
|||
return 0;
|
||||
}
|
||||
if (actor_push(a, msg_val) != 0) {
|
||||
wo_mbox_release(a);
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,12 +96,24 @@ typedef struct wo_actor {
|
|||
uint64_t instance; /* the moved-in state object (runtime-owned) */
|
||||
uint32_t method; /* receive's method index (self + msg = 2 args) */
|
||||
uint32_t home; /* the shard whose thread owns mailbox + delivery */
|
||||
uint64_t *msgs; /* FIFO ring, growable */
|
||||
uint64_t *msgs; /* FIFO ring, growable up to the cap */
|
||||
uint32_t mhead, mlen, mcap;
|
||||
/* iteration 24: sent-but-not-delivered count, incremented by the
|
||||
* SENDER on any shard (the cap check), decremented by the home
|
||||
* thread at delivery pop. Accessed ONLY through __atomic builtins
|
||||
* (wo_mbox_reserve/release) because senders race; the cap can
|
||||
* overshoot by at most the number of in-flight sends — disclosed. */
|
||||
uint32_t pending;
|
||||
wo_fiber *active; /* the delivery fiber, NULL when idle */
|
||||
struct wo_actor *next_all; /* the vm's all-actors list */
|
||||
} wo_actor;
|
||||
|
||||
/* iteration 24: the one mailbox cap (default 1024, WO_MAILBOX overrides
|
||||
* at boot — soak tests shrink it to force the fail-fast policy). */
|
||||
extern uint32_t wo_mailbox_cap;
|
||||
int wo_mbox_reserve(wo_actor *a); /* 0 = slot reserved; -1 = full */
|
||||
void wo_mbox_release(wo_actor *a); /* delivery pop / failed enqueue */
|
||||
|
||||
typedef struct wo_vm {
|
||||
const wo_module *mod;
|
||||
wo_rt rt;
|
||||
|
|
|
|||
|
|
@ -190,6 +190,11 @@ enum {
|
|||
honesty precedent DIV0 set (trap, never x86's silent count%64).
|
||||
Literal counts never get here: woc rejects them (WO-E223). */
|
||||
WO_T_SHIFT = 12,
|
||||
/* iteration 24 (absorbing 31): the actor lifecycle's one trap kind —
|
||||
a send/call against a full mailbox (fail-fast backpressure, the
|
||||
sender always learns), call to a dead actor, callee died mid-call.
|
||||
The message names which. Catchable like every trap. */
|
||||
WO_T_ACTOR = 13,
|
||||
};
|
||||
|
||||
/* ---- opcodes (spec section 5; semantics in the format doc) ---- */
|
||||
|
|
|
|||
53
runtime/test/test_mailbox.c
Normal file
53
runtime/test/test_mailbox.c
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/* test_mailbox — the fail-fast cap core (iteration 24). Reserve/release
|
||||
* arithmetic single-threaded, then two racing senders: successful
|
||||
* reserves never exceed the cap (each success must observe old < cap,
|
||||
* and increments are permanent — see wo_mbox_reserve). */
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "t.h"
|
||||
#include "vm.h"
|
||||
|
||||
static wo_actor A;
|
||||
|
||||
static void *hammer(void *arg) {
|
||||
(void)arg;
|
||||
long wins = 0;
|
||||
for (int i = 0; i < 1000; i++)
|
||||
if (wo_mbox_reserve(&A) == 0) wins++;
|
||||
return (void *)wins;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
/* single-threaded: cap honored exactly, release frees a slot */
|
||||
wo_mailbox_cap = 4;
|
||||
memset(&A, 0, sizeof A);
|
||||
T_EQ(wo_mbox_reserve(&A), 0);
|
||||
T_EQ(wo_mbox_reserve(&A), 0);
|
||||
T_EQ(wo_mbox_reserve(&A), 0);
|
||||
T_EQ(wo_mbox_reserve(&A), 0);
|
||||
T_EQ(wo_mbox_reserve(&A), -1); /* full */
|
||||
wo_mbox_release(&A);
|
||||
T_EQ(wo_mbox_reserve(&A), 0); /* freed slot reusable */
|
||||
T_EQ(wo_mbox_reserve(&A), -1);
|
||||
T_EQ(A.pending, 4);
|
||||
|
||||
/* two racing senders against cap 8: exactly 8 wins, pending == 8 */
|
||||
wo_mailbox_cap = 8;
|
||||
memset(&A, 0, sizeof A);
|
||||
pthread_t t1, t2;
|
||||
void *w1, *w2;
|
||||
pthread_create(&t1, NULL, hammer, NULL);
|
||||
pthread_create(&t2, NULL, hammer, NULL);
|
||||
pthread_join(t1, &w1);
|
||||
pthread_join(t2, &w2);
|
||||
T_EQ((long)w1 + (long)w2, 8);
|
||||
T_EQ(A.pending, 8);
|
||||
/* drain and refill: the counter did not corrupt under the race */
|
||||
for (int i = 0; i < 8; i++) wo_mbox_release(&A);
|
||||
T_EQ(A.pending, 0);
|
||||
T_EQ(wo_mbox_reserve(&A), 0);
|
||||
|
||||
return t_report("test_mailbox");
|
||||
}
|
||||
|
|
@ -127,7 +127,12 @@ def campaign():
|
|||
# msgrate once per shard count, RAM only (no store dependency)
|
||||
for shards in (1, ncores):
|
||||
tag = f"msg.s{'1' if shards == 1 else 'N'}"
|
||||
rc, lines, _, _ = run(["msgrate", str(MSG_N)], {"WO_SHARDS": str(shards)}, 300)
|
||||
# iteration 24 gave mailboxes a cap (default 1024, fail-fast trap);
|
||||
# msgrate's contract is an UNBOUNDED one-way flood, so the driver
|
||||
# raises the cap to the flood size — the measured number keeps
|
||||
# iteration 22's semantics exactly.
|
||||
rc, lines, _, _ = run(["msgrate", str(MSG_N)],
|
||||
{"WO_SHARDS": str(shards), "WO_MAILBOX": str(MSG_N)}, 300)
|
||||
if rc != 0:
|
||||
bad(tag, f"rc={rc}")
|
||||
else:
|
||||
|
|
|
|||
3
tests/corpus/run/catch-msg-place/fixture.out
Normal file
3
tests/corpus/run/catch-msg-place/fixture.out
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
caught: No such file or directory
|
||||
place: boxed
|
||||
still: boxed
|
||||
25
tests/corpus/run/catch-msg-place/fixture.wo
Normal file
25
tests/corpus/run/catch-msg-place/fixture.wo
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- iteration 24 fix pin: a try ARM's value that is a Text PLACE must be
|
||||
-- copied out before the arm's scope dies. The catch half: bare `e.msg`
|
||||
-- aliased the Error record's field, the record dropped at arm end, and
|
||||
-- the binding dangled (ASan use-after-free, then a double-walk SEGV on
|
||||
-- the next unwind). The body half: `try box.name catch ...` aliased the
|
||||
-- box's field across the same boundary.
|
||||
use fs
|
||||
|
||||
class Box {
|
||||
name: Text
|
||||
}
|
||||
|
||||
fn read_place(b: Box) -> Text {
|
||||
return try b.name catch (e) "unreachable";
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let r = try fs.read_all("/nonexistent-woc-fixture", 10) catch (e) e.msg;
|
||||
print("caught: ${r}");
|
||||
let b = Box { name: "boxed" };
|
||||
let t = read_place(b);
|
||||
print("place: ${t}");
|
||||
print("still: ${b.name}");
|
||||
return 0;
|
||||
}
|
||||
2
tests/corpus/run/mailbox-full-trap/fixture.out
Normal file
2
tests/corpus/run/mailbox-full-trap/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
trap: actor mailbox full
|
||||
bounded: cap respected
|
||||
51
tests/corpus/run/mailbox-full-trap/fixture.wo
Normal file
51
tests/corpus/run/mailbox-full-trap/fixture.wo
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use time
|
||||
|
||||
-- iteration 24: fail-fast backpressure from .wo. The sleeper parks in its
|
||||
-- first receive, so the mailbox only ever drains by one; a send loop must
|
||||
-- hit the cap (default 1024) and catch WO_T_ACTOR. The exact send count
|
||||
-- at the first trap depends on scheduling, so the fixture prints the
|
||||
-- trap's message once plus a bound check, never the count. `try` is an
|
||||
-- expression, so the send is wrapped in a helper whose success is nil.
|
||||
class Tick {
|
||||
n: Int
|
||||
}
|
||||
|
||||
class Sleeper {
|
||||
pad: Int
|
||||
fn receive(msg: Tick) {
|
||||
time.sleep(5000);
|
||||
}
|
||||
}
|
||||
|
||||
fn send_one(s: actor Tick, n: Int) -> Text {
|
||||
send(s, Tick { n: n });
|
||||
return "";
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let s: actor Tick = spawn Sleeper { pad: 0 };
|
||||
let sent = 0;
|
||||
let caught = "";
|
||||
let i = 0;
|
||||
while i < 2000 {
|
||||
i = i + 1;
|
||||
let r = try send_one(s, i) catch (e) e.msg;
|
||||
if r == "" {
|
||||
sent = sent + 1;
|
||||
} else {
|
||||
caught = "${r}";
|
||||
i = 2000;
|
||||
}
|
||||
}
|
||||
if caught == "actor mailbox full" {
|
||||
print("trap: ${caught}");
|
||||
} else {
|
||||
print("NO TRAP after ${sent} sends");
|
||||
}
|
||||
if sent >= 1024 {
|
||||
print("bounded: cap respected");
|
||||
} else {
|
||||
print("bounded: TRAPPED EARLY at ${sent}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue