Skip to content

test: give each async launch an owned callback context - #49

Open
owenthcarey wants to merge 1 commit into
mainfrom
test/adopt-async-callback-context
Open

owenthcarey wants to merge 1 commit into
mainfrom
test/adopt-async-callback-context

Conversation

@owenthcarey

Copy link
Copy Markdown
Contributor

Fixes the intermittent SIGSEGV in crates/weaveffi/tests/runtime that hit the macOS Build and test job on #48 (the same tree passed on main and on re-run).

Root cause

The async tests handed the completion callback a raw pointer to a Box<mpsc::Sender> and freed that box on the test thread right after recv returned:

extern "C" fn cb(ctx: *mut c_void, err: *mut weaveffi_error, result: i32) {
    let tx = unsafe { &*(ctx as *const mpsc::Sender<Msg>) };
    tx.send(...).unwrap();
}
// ...
let (..) = rx.recv_timeout(...).unwrap();
unsafe { drop(Box::from_raw(tx_ptr)) };   // test returns, `rx` dropped

recv unblocks as soon as the message is queued, before the producer thread has finished send (it still notifies waiters afterward). If the test thread frees the Sender and then the Receiver in that window, the channel is deallocated under the producer thread. The window is tiny and load dependent, which is why it only ever showed up under cargo insta test --workspace on a busy runner.

samples/async-demo had already met this race and worked around it by deliberately leaking the context (leak_ctx). crates/weaveffi/tests/runtime.rs and samples/kvstore never got that treatment.

Fix

Test code only. Each async launch gets its own boxed clone of the sender via ctx_for(&tx), and the callback adopts it with adopt_ctx(ctx) (Box::from_raw) and drops it when done. The channel is then torn down by whichever side releases last, through the channel's own refcount, and no borrowed reference outlives the callback. This is the ownership the ABI's "consumer owns context" rule intends. The leak_ctx workaround in async-demo is removed in favour of the same pattern.

The generated launchers were already correct: async_fns.rs carries context as an opaque usize, never dereferences it, and fires the callback exactly once. No runtime or generator changes.

Verification

  • cargo clippy -p weaveffi -p kvstore -p async-demo --all-targets -- -D warnings: clean
  • cargo test -p weaveffi --test runtime (42), -p kvstore (26), -p async-demo (10): all pass
  • Stress: the three test binaries run 100 times each with --test-threads=16 under 8 busy-loop threads: 0 failures in 300 runs

The async tests passed the completion callback a raw pointer to a boxed
mpsc::Sender, then freed that box on the test thread right after recv
returned. recv unblocks as soon as the message is queued, before the
producer thread has finished send, so the test could free the Sender and
then the whole channel out from under an in-flight send. The window is
tiny and load dependent, which is why it surfaced as an intermittent
SIGSEGV in the weaveffi runtime tests on macOS CI and nowhere else.

Every launch now gets its own boxed clone of the sender and the callback
adopts it with Box::from_raw, so the channel is torn down by whichever
side releases last and no borrowed reference outlives the callback. This
is the ownership the ABI's "consumer owns context" rule intends, and it
replaces the deliberate leak async-demo used to sidestep the same race.
The generated launchers were already correct: they carry context as an
opaque usize and never touch it after the callback fires.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant