Root cause (decision 1): cross-shard send/call/monitor pointer-shared the
message into the receiver's shard (e->payload = msg_val), so a worker read and
eventually dropped an object living in the sender's arena — a double free, then
a class-0 forge, then a modulo self-route livelock, all downstream of that one
broken invariant ("VM heaps are never read cross-shard", which wo_db_rpc keeps).
- actor_marshal: the sender encodes the message into an arena-independent neutral
form (wo_db_val_encode, the same marshal wo_db_rpc uses) and drops its own
original — no pointer crosses an arena boundary, so the double-free class is
gone by construction. actor_unmarshal rebuilds it in the receiver's arena
(wo_val_decode_vm) and frees the neutral. Applied to the 4 cross-shard
producers (send x2, call, monitor) + the 3 consumers (kinds 0/5/7). Same-shard
paths untouched (the WO_SHARDS=1 fast path never failed). Call replies are
scalars by contract, so kind 6 needs no marshal.
- eng_settle_inboxes: undrained kind-0/5/7 payloads at teardown are the neutral
form now — free with wo_db_val_free, not wo_drop_obj (caught by ASan mid-fix).
- decision 2: wo_route_free traps a shard_id >= nshards header (a corrupt/freed
block) instead of self-routing it into the settle livelock.
- proof: tests/regress/lang-41/cross-shard-marshal.wo (a multi<Text> sent +
called cross-shard, both sides drop) — clean 12x/5x under WO_SHARDS=4 + ASan;
shard-settle repro still clean 8x; full runtime suite 0 fail (same-shard
byte-unchanged). `just db-actor` extended with the new fixture.
- unblocks porch 9. Follow-ups: poison-on-free (decision 3), corpus fixture (4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 63065ff75799f7f43b2bce6de61e77856799566f)
48 lines
1.6 KiB
Text
48 lines
1.6 KiB
Text
-- lang-41 phase C: a cross-shard message carrying an OWNED SUBTREE (a multi of
|
|
-- Text) sent AND called into an actor that lands on another shard. Pre-fix, the
|
|
-- payload's pointer was shared across arenas: the receiver and the sender's
|
|
-- graph both dropped the same multi/Text blocks -> a double free (ASan abort or
|
|
-- the settle livelock/hang). With the marshal fix each arena frees its own copy.
|
|
-- Needs WO_SHARDS>1 + the ASan build (scripts/db-actor-accept.sh drives it).
|
|
|
|
class Msg { tags: multi Text }
|
|
|
|
-- reads the owned subtree (forces the receiver to touch the crossed blocks),
|
|
-- then the message is dropped on the receiver's shard.
|
|
class Sink {
|
|
fn receive(msg: Msg) -> Int {
|
|
let n = len(msg.tags);
|
|
let i = 0;
|
|
let acc = 0;
|
|
while i < n { acc = acc + len(msg.tags[i]); i = i + 1; }
|
|
return acc;
|
|
}
|
|
}
|
|
|
|
-- churns cross-shard send + call, each carrying a fresh multi<Text> subtree.
|
|
class Client {
|
|
target: actor Msg
|
|
fn receive(msg: Msg) -> Int {
|
|
let j = 0;
|
|
while j < 8 {
|
|
let r = call(self.target, Msg { tags: ["a-${j}", "bb-${j}", "ccc-${j}"] });
|
|
send(self.target, Msg { tags: ["x-${j}", "yy-${j}"] });
|
|
j = j + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
fn main() -> Int {
|
|
let sink: actor Msg = spawn Sink {};
|
|
let c1: actor Msg = spawn Client { target: sink };
|
|
let c2: actor Msg = spawn Client { target: sink };
|
|
let c3: actor Msg = spawn Client { target: sink };
|
|
let c4: actor Msg = spawn Client { target: sink };
|
|
send(c1, Msg { tags: ["go"] });
|
|
send(c2, Msg { tags: ["go"] });
|
|
send(c3, Msg { tags: ["go"] });
|
|
send(c4, Msg { tags: ["go"] });
|
|
print("dispatched");
|
|
return 0;
|
|
}
|