Repository navigation
policy_fn: approve held syscalls asynchronously - #302
Open
congwang-mk wants to merge 6 commits into
Open
congwang-mk wants to merge 6 commits into
congwang-mk wants to merge 6 commits into
Conversation
The notification loop awaited every held policy verdict inline, so one pending approval stopped supervision of the whole sandbox. The argv freeze for execve was also taken before the verdict and held across it. Held notifications now park in a set owned by the loop and are finished there once their verdict lands. policy_fn judges an execve on argv read without freezing; after an allow the supervisor freezes, re-reads argv and the path, and continues only if they still match. The freeze is taken, used and released with no await in between: its tracer is the loop's thread, and a task parked in seccomp is skipped by the freeze, so deferred handler results are also sent by the loop rather than by their workers, never while a freeze is held. Siblings frozen for an allowed execve are killed by de_thread while still traced, and de_thread waits until their tracer reaps them. Reap them before releasing peers, which also keeps peers stopped until the kernel has copied argv. Signed-off-by: Cong Wang <cwang@multikernel.io>
A policy callback had to return its verdict, so an embedder waiting on a human or another service blocked the callback thread, and every later event queued behind that one decision. PolicyFn::deferred hands each held syscall an Approval the embedder may keep and resolve later from any thread, while the callback returns and the next event is delivered. Dropping it unresolved denies, and is_cancelled tells a pending approver that shutdown or the verdict timeout already decided. PolicyFn::new is built on top of it and keeps its behaviour. Signed-off-by: Cong Wang <cwang@multikernel.io>
A C policy callback could only return its verdict, so an embedder that answers through a UI or another service had to block the callback thread until the decision arrived. sandlock_sandbox_builder_policy_fn_deferred passes each held syscall an owned sandlock_approval_t the caller resolves later from any thread with sandlock_approval_resolve, or abandons with sandlock_approval_free, which denies. sandlock_approval_is_cancelled reports a shutdown or an expired verdict timeout. Both callback flavours share the event conversion, and the existing entry point is unchanged. Signed-off-by: Cong Wang <cwang@multikernel.io>
A Python policy callback had to return its verdict, so an application asking a user or another service held the callback thread until the answer came back. PolicyFn(callback, deferred=True) calls callback(event, ctx, approval), where approval is an Approval for held syscalls that may be answered later from any thread with allow, deny or resolve. Closing it, dropping it, or raising from the callback denies, and cancelled reports a shutdown or an expired verdict timeout. Synchronous callbacks keep their return value encoding, now shared with resolve. Signed-off-by: Cong Wang <cwang@multikernel.io>
A Go PolicyFunc had to return its decision, so a program that asks a user or another service blocked the callback goroutine, and with it the next policy event, until the answer arrived. PolicyFn.Deferred receives an Approval for each held syscall that any goroutine may Resolve later. Close denies it, a panicking callback or an abandoned Approval is denied, IsCancelled reports a shutdown or an expired verdict timeout, and a second Resolve returns ErrApprovalAnswered. A PolicyFn must set exactly one of Callback and Deferred. Signed-off-by: Cong Wang <cwang@multikernel.io>
The policy guide only showed approvals as a callback blocking on a queue and still described the argv freeze as held while the callback decides. Document deferred approval handles across Python, Rust, Go and C, that a verdict decides only the event it answers, and that argv is judged unfrozen and re-checked under a freeze after an allow. Point the Python and Go READMEs at the new section. Signed-off-by: Cong Wang <cwang@multikernel.io>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #289. Replaces #298, reworked from scratch around the argv freeze.
Problem
The supervisor loop awaited every held policy verdict inline, so one pending approval (exec, open, connect/bind/send) stopped supervision of the whole sandbox. The argv freeze for
execvewas also taken before the verdict and held across it, stopping every sandboxed task for as long as an approver took to answer.Design
JoinSetthe loop owns and is finished on the loop when its verdict lands. Each sandbox thread has at most one pending notification, so no extra bound is needed; dropping the set on exit aborts every pending wait, and shutdown cancellation still denies them.execveon argv read without freezing. After an allow the supervisor freezes, re-reads argv and the path, and continues only if they still match (otherwiseEPERM). Freeze, re-check, response and release happen with no.awaitin between, since the tracer is the loop's thread.de_threadwhile still traced wait for their tracer to reap them, andde_threadwaits for that. The supervisor reaps them before detaching peers, which also keeps peers stopped until the kernel has copied argv.API changes
PolicyFn::deferred(|event, ctx, approval: Option<Approval>|);Approval::{resolve, allow, deny, is_cancelled}. Dropping an unresolved approval denies.PolicyFn::newis built on it and unchanged.sandlock_sandbox_builder_policy_fn_deferred,sandlock_approval_resolve,sandlock_approval_is_cancelled,sandlock_approval_free.PolicyFn(cb, deferred=True)callscb(event, ctx, approval);Approval.allow/deny/resolve/closeandcancelled. Closing, collecting or raising denies.PolicyFn.Deferred;(*Approval).Resolve/Close/IsCancelled,ErrApprovalAnswered. Exactly one ofCallbackandDeferredmust be set.Known limits (not introduced here)
de_threadcan stall the loop up to 2s while its siblings are released.Testing
Rust core (922 unit, 554 integration), FFI and C smoke tests, Python (450) and Go suites pass locally. New tests cover a pending approval not stalling other syscalls, argv rewritten during an approval being refused, exec with a live sibling thread, approvals answered from another thread or by a later event, dropped/closed/panicking approvals denying, and timeout cancellation.
🤖 Generated with Claude Code