Skip to content

policy_fn: allow asynchronous approval without stalling supervision - #298

Closed
congwang-mk wants to merge 11 commits into
mainfrom
issue-289-async-approval
Closed

congwang-mk wants to merge 11 commits into
mainfrom
issue-289-async-approval

Conversation

@congwang-mk

Copy link
Copy Markdown
Contributor

Policy callbacks waiting for an approval currently stall the notification loop. Add owned approval handles so embedders can return from a callback and answer later while supervision continues.

The supervisor retains bounded pending operations and finalizes them serially alongside new notifications. Timeout, abandonment, and shutdown fail closed. Completion rechecks mutable network and filesystem restrictions; exec approval retains the existing argv freeze. Existing synchronous callbacks remain supported.

Expose the API through Rust, C, Python (PolicyFn.deferred), and Go, with lifecycle documentation and regression coverage.

Validation: 19 focused Rust policy unit tests with minimal features, four supervisor integration regressions, five Python deferred-policy tests, and two Go policy tests passed locally. Full testing is left to GitHub CI.

Closes #289.

Policy callbacks return a verdict synchronously, so an embedder waiting
for human input occupies the callback worker and prevents delivery of
later events.

Add an owned Approval handle and a deferred callback constructor. Adapt
synchronous callbacks to the same delivery path while preserving their
return-value API. Observation events receive no handle, and the callback
context remains borrowed for the duration of the call.

Use the existing verdict channel and cancellation token to reject late
or repeated answers. Dropping an unanswered handle denies the request.
Add focused coverage for handle lifetime, cancellation and delivery of
multiple outstanding approvals.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The notification loop awaits each policy verdict before receiving
another syscall. Moving an approval to an application queue therefore
still stalls unrelated sandbox tasks.

Split notification preparation from response completion and retain
pending approvals in the supervisor. Poll verdicts alongside
notification readiness, bound outstanding work and finalize responses
serially. Keep the notification descriptor alive until pending resources
have been released.

Retain the argv freeze while an exec approval is pending and queue
conflicting exec or process-creation preparations. Deny pending requests
and release freeze state during teardown. Add a regression where a
second connection completes while the first waits for approval.

Signed-off-by: Cong Wang <cwang@multikernel.io>
C embedders can only return a verdict from the policy callback. They
need an owned handle to answer after the callback returns without
retaining borrowed event or context pointers.

Add a deferred callback registration entry point and opaque approval
handles with resolve, cancellation and release functions. Reuse event
conversion for synchronous and deferred callbacks and preserve the
existing callback ABI and integer verdict encoding.

Document exclusive handle access and the requirement to release every
handle after resolution or cancellation. Regenerate the C header for the
added entry points.

Signed-off-by: Cong Wang <cwang@multikernel.io>
Python applications currently have to wait inside the policy callback to
obtain a decision. That prevents the callback worker from delivering
another approval request.

Add PolicyFn.deferred and an Approval wrapper with allow, deny,
cancellation inspection and explicit close methods. Serialize native
handle access and release it after a decision or abandonment. Keep the
existing synchronous constructor and verdict conversion.

Close unanswered approvals when a callback raises, including handles
already retained by the application. Add focused tests for
configuration, decisions from another thread, repeated answers,
abandonment, callback exceptions and timeout.

Signed-off-by: Cong Wang <cwang@multikernel.io>
Go policy callbacks can only return an immediate decision. Embedders
need to hand pending approvals to another goroutine while the callback
worker continues processing events.

Add PolicyFn.Deferred and require exactly one callback mode. Wrap native
approvals with serialized Resolve, Close and IsCancelled methods.
Release the handle after resolution, close it on callback panic and
retain finalization as a fallback for abandoned handles.

Use the explicit destructor function pointer in the deferred C
registration signature so cbindgen emits a prototype usable by cgo. Add
focused coverage for deferred decisions and callback configuration.

Signed-off-by: Cong Wang <cwang@multikernel.io>
A prepared operation can outlive the policy state used to authorize it.
Approving it after another callback tightens restrictions would execute
the stale decision. An unpolled verdict future also starts its timeout
too late.

Recheck captured network destinations and pending open targets against
current restrictions before completion. Check pinned file identities
before injection and route continued opens through on-behalf handling
when dynamic denials require it. Preserve existing live-network
precedence and the filesystem namespace boundaries.

Capture the verdict deadline when constructing the wait and deny
immediately when it has expired. Cancel pending waits if the callback
worker exits, and keep observation-only notifications flowing when the
approval limit is reached.

Add regressions for network and path restrictions, shutdown cancellation
and delayed polling. All 19 focused policy unit tests and four deferred
supervisor integration tests pass locally. Leave the complete test
suites to remote CI.

Fixes #289

Signed-off-by: Cong Wang <cwang@multikernel.io>
The policy guide describes approvals as blocking callback loops and does
not explain how applications can retain a decision without retaining the
callback context.

Document deferred approval APIs for Python, Rust, Go and C, including
queue handoff, one-shot decisions, timeout, cancellation and explicit
release. Explain the pending-request bound and the sandbox-wide argv
freeze retained during exec approval.

Update the architecture guide to describe supervisor-owned pending
verdicts and sequential response completion.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The exec freeze treats tasks already in a kernel wait as held, but a task
blocked on a pending approval is held by the supervisor, not the kernel.
Answering its approval while an execve approval was outstanding released
an unfrozen task that could rewrite argv policy_fn had already inspected.
Only the freeze holder's verdict is now polled until the exec is decided.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk marked this pull request as draft October 14, 2026 16:50
A sibling seized by the argv freeze is still traced when execve's
de_thread kills it, so it stays a zombie until the supervisor reaps it,
and de_thread waits for it forever. Any exec under policy_fn from a
process with a running sibling thread hung. Reap those siblings after
Continue, and release any still stopped at the deadline since the exec
must have failed.

Signed-off-by: Cong Wang <cwang@multikernel.io>
Holding the sandbox-wide argv freeze across a deferred approval stopped
every sandboxed task for as long as a human took to answer. policy_fn
now decides on an argv snapshot read without freezing. After an allow,
the supervisor freezes, re-reads argv and the path, and continues only
if they still match; the freeze then lasts only until the kernel copies
argv. No freeze outlives a single notification, so overlapping exec and
process creation no longer need to queue, and other approvals no longer
need to wait for a pending exec.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The post-approval network recheck reconstructed what to check from the
prepared operation's event metadata: it parsed the protocol back from a
string, special-cased bind, and guessed which hosts had been vetted. Any
handler change would silently drift from it. check_ip_destination now
returns the destination it approved, and handlers attach that to the
prepared operation, so the recheck reruns exactly the original verdict
against the live policy.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk deleted the issue-289-async-approval branch October 16, 2026 05:43
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.

policy_fn: allow asynchronous approval without stalling supervision

1 participant