Skip to content

policy_fn: join the callback thread before the sandbox returns - #205

Merged
congwang-mk merged 1 commit into
mainfrom
policy-fn-worker-lifetime
Aug 23, 2026
Merged

policy_fn: join the callback thread before the sandbox returns#205
congwang-mk merged 1 commit into
mainfrom
policy-fn-worker-lifetime

Conversation

@congwang-mk

Copy link
Copy Markdown
Contributor

Problem

CI segfaulted in the Python suite right after test_passthrough_no_modification passed, on a thread with no Python frame.

The sandlock-policy-fn thread was detached and only exited once every clone of its event sender dropped, which happens when the last Arc<SupervisorCtx> goes away, not when run() returns. Non-gated events are fire-and-forget, so the thread could still be draining them after the caller had its result. Through the Python binding that is a use-after-free: the Sandbox temporary in the test is collected as soon as run() returns, freeing the ctypes trampoline the thread is about to call.

Fix

Give the thread an owner. PolicyFnWorker lives in the sandbox Runtime next to the other supervisor handles; its Drop sends an in-band Shutdown message and joins. In-band rather than channel closure because the supervisor's sender clones outlive abort(). Every entry point goes through the existing teardown, so the guarantee is not per-path and the Python side needs no change.

Tests

  • New worker_drop_waits_for_queued_callbacks: queues an event whose callback sleeps, drops the worker, asserts the callback ran (fails without the join).
  • cargo test --release --lib: all pass. Python suite: 410 passed; test_policy_fn.py run 5x without a crash.

The policy-fn thread was detached and only exited once every clone of its
event sender had dropped, which happens whenever the last Arc<SupervisorCtx>
goes away, not when run() returns. Non-gated events are fire-and-forget, so
the thread could still be draining them after the caller had its result.
Through the Python binding that is a use-after-free: the Sandbox temporary in
test_passthrough_no_modification is collected as soon as run() returns, which
frees the ctypes trampoline the thread is about to call. CI caught it as a
SIGSEGV on a thread with no Python frame.

Give the thread an owner. PolicyFnWorker lives in the sandbox Runtime next to
the other supervisor handles and its Drop sends an in-band Shutdown message
and joins; an in-band message rather than channel closure because the
supervisor's sender clones outlive an abort(). Every entry point goes through
the existing teardown, so the guarantee is not per-path.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk merged commit f6a3e39 into main Aug 23, 2026
17 checks passed
@congwang-mk
congwang-mk deleted the policy-fn-worker-lifetime branch August 23, 2026 21:34
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