Skip to content

Commit debea72

Browse files
nullstyleclaude
andcommitted
test: keep the site-2 staging probes alive past the peer they outlive
The amd64 ReleaseSafe SEGV (CI 30606010609) was in the TEST, not the lift. The Windows trace named it exactly: deliverLocalException (mod.zig:3775) forceCancelAllQuestions Peer.deinit (mod.zig:2062) ImportOwnerVat.deinitAll -> self.remote.deinit() test.L17 site 2 ... -> defer vat.deinitAll() `stageSite2Provision` deliberately drops the staged call's Return in flight (it severs the link so the owner's resolved_answers entry stays live, which is the whole point of the site-2 shape). That leaves the question OUTSTANDING until `vat.remote.deinit()`, where `forceCancelAllQuestions` delivers a local exception THROUGH the question's stored `ctx` pointer -- and that pointer was `&call_probe`, a local of the helper, whose frame died when the helper returned. `deliverLocalException` then writes into a vanished stack frame: a segfault on amd64 Linux and Windows under ReleaseSafe, and on arm64 macOS a silent scribble into a still-mapped page, which is why every local gate and the arm64 tier passed. Both probes now live in the test's frame and are passed in, so they outlive the peer that cancels the question. The helper's doc comment says why, since the next person will be tempted to tidy them back into it. Only this helper has the shape: it is the one that deliberately leaves a question unanswered, and exactly its two tests crashed while the site-1 and OOM fixtures in the same run stayed green. The lift's own accounting is untouched by this commit -- `git diff` covers the test file only. Local gates pass, but they passed before too; amd64 CI on this PR is the oracle that matters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 7195c52 commit debea72

1 file changed

Lines changed: 21 additions & 6 deletions

File tree

tests/rpc/peer/rpc_three_party_handoff_vatc_test.zig

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3024,19 +3024,28 @@ const DeferringService = struct {
30243024
/// provision whose stored target is `.promised` ops naming that answer — the
30253025
/// state the serve arm re-resolves at Accept time. Returns the remote's
30263026
/// import of the deferring service (for teardown).
3027+
/// `bprobe` and `call_probe` are supplied BY THE CALLER, and that is
3028+
/// load-bearing rather than stylistic. The staged call's Return is
3029+
/// deliberately dropped in flight, so its question stays outstanding until
3030+
/// `vat.remote.deinit()` — where `forceCancelAllQuestions` delivers a local
3031+
/// exception THROUGH the question's stored `ctx` pointer. Locals of this
3032+
/// helper would be dead stack by then: `deliverLocalException` writes into a
3033+
/// vanished frame, which on amd64 (Linux and Windows, ReleaseSafe) is a
3034+
/// segfault and on arm64 macOS merely scribbles unnoticed. The probes must
3035+
/// outlive the peer, so they live in the test's frame.
30273036
fn stageSite2Provision(
30283037
vat: *ImportOwnerVat,
30293038
host: *FrameHost,
30303039
deferring: *DeferringService,
3040+
bprobe: *CapImportProbe,
3041+
call_probe: *TolerantCall,
30313042
token: []const u8,
30323043
provide_qid: u32,
30333044
) !u32 {
30343045
_ = try vat.owner.setBootstrap(.{ .ctx = deferring, .on_call = DeferringService.onCall });
3035-
var bprobe = CapImportProbe{};
3036-
_ = try vat.remote.sendBootstrap(&bprobe, CapImportProbe.onReturn);
3046+
_ = try vat.remote.sendBootstrap(bprobe, CapImportProbe.onReturn);
30373047
const service_import = bprobe.import_id orelse return error.ServiceBootstrapFailed;
3038-
var call_probe = TolerantCall{};
3039-
_ = try vat.remote.sendCall(service_import, NUMBER_INTERFACE_ID, GET_NUMBER_METHOD_ID, &call_probe, null, TolerantCall.onReturn);
3048+
_ = try vat.remote.sendCall(service_import, NUMBER_INTERFACE_ID, GET_NUMBER_METHOD_ID, call_probe, null, TolerantCall.onReturn);
30403049
const answer_qid = deferring.question_id orelse return error.CallNotDeferred;
30413050
try std.testing.expect(!call_probe.returned);
30423051

@@ -3072,7 +3081,11 @@ test "L17 site 2: a stored .promised target re-resolving to an IMPORT is served
30723081
var deferring = DeferringService{};
30733082
const token = try host.mintToken(allocator, "l17-promised-receiverhosted");
30743083
defer allocator.free(token);
3075-
const service_import = try stageSite2Provision(&vat, &host, &deferring, token, 91);
3084+
// Declared here, not inside the helper: they back an outstanding question
3085+
// that `vat.deinitAll()` cancels, so they must outlive it.
3086+
var bprobe = CapImportProbe{};
3087+
var call_probe = TolerantCall{};
3088+
const service_import = try stageSite2Provision(&vat, &host, &deferring, &bprobe, &call_probe, token, 91);
30763089

30773090
// SITE 2 takes NO Provide-time pin: the target was `.promised` at
30783091
// registration (there was no import to pin yet), so the entry carries
@@ -3150,7 +3163,9 @@ test "L17 witness: a stored .promised target whose re-resolved import has DIED s
31503163
var deferring = DeferringService{};
31513164
const token = try host.mintToken(allocator, "l17-vanished-import");
31523165
defer allocator.free(token);
3153-
const service_import = try stageSite2Provision(&vat, &host, &deferring, token, 94);
3166+
var bprobe = CapImportProbe{};
3167+
var call_probe = TolerantCall{};
3168+
const service_import = try stageSite2Provision(&vat, &host, &deferring, &bprobe, &call_probe, token, 94);
31543169

31553170
// The target import dies for real BEFORE the Accept: site 2 has no
31563171
// Provide-time pin (by design — the answer's cap was still a promise at

0 commit comments

Comments
 (0)