Repository navigation
Deny a bunker approval that goes unanswered instead of waiting on it - #1068
Merged
Merged
Conversation
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 2 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
ServerCallbacks::request_approvalrequires the implementation to bound its own wait and reject when it expires. keep-cli, keep-web and keep-desktop each enforce that on their own approval channel with a 60 secondrecv_timeout. keep-mobile called the foreign callback straight through the FFI boundary and returned whatever it returned, so the only thing bounding the wait was the platform's good behavior. keep-android does wait 60 seconds, but nothing on this side required it to.That matters more than one stalled request. The handler reads one request at a time:
Server::runpasses a single async closure tohandle_notificationsand awaits it per notification, sohandle_nip46_eventruns to completion before the next event is read. An approval that never returned therefore stopped every later request from every client, not just its own.A synchronous foreign call cannot be interrupted, so the wait is bounded by moving the call to its own thread and timing out the answer. On expiry the request is denied. The thread outlives the timeout: it stays parked in the platform prompt until that returns, and its answer is dropped, which is safe because the channel is created per call, so nothing can read a late answer and no permission state, audit entry or rate limiter sees it.
Because those threads outlive their timeouts, the number of them is capped. The handler reads one request at a time, so more than one is outstanding only when earlier prompts timed out and their threads are still parked, which against a platform that never returns accumulates at roughly one per timeout, each holding a stack, a JVM attachment and a copy of the request. Past the ceiling the answer is a refusal without asking, so a platform that never returns costs a bounded number of threads instead of steady pressure on the whole app. The slot is released by a guard when the platform call ends, however it ends, so a callback that panics does not retire a slot for good.
The spawn itself is a refusal rather than a panic on failure.
std::thread::spawnpanics when the OS refuses a thread, and that panic unwinds through the handler and out of the notification loop, which stops the bunker while its status still reports running.The timeout is 60 seconds, matching the other three surfaces, so no answer now means a denial on every surface rather than on three of four. The trait doc states that the implementation is still expected to bound its own wait; this is the backstop for one that does not.
One consequence worth knowing when reading a denial: this timer and the platform's own 60 seconds start at nearly the same moment, so an answer arriving within a few milliseconds of the deadline can be denied here after the operator approved it. Both orderings end in a denial when nobody answers, which is the direction that matters.
Unchanged: the handler still reads one request at a time, so a slow approval still delays later ones. What changes is that the delay is now bounded. Making those concurrent is not a one-line change, because the rate limiters and per-app permission counters currently rely on that ordering, and it is tracked separately.
A refusal now names the pubkey and method that was refused, and reports no answer separately from a callback that panicked, which fail closed at once rather than after the timeout. The app name is deliberately left out of the log: it is remote-derived and unbounded, where the pubkey and method are neither.
Unchanged, and worth stating because the log reads the same either way: a prompt that times out here is recorded in the audit ring on the same branch as a real refusal, so a later read cannot tell "nobody answered" from "the operator said no". Separating them needs a distinct outcome threaded through the handler and is tracked separately.
The timeout is a field rather than the constant read directly, so the test reaches it in 200ms instead of a minute; it is a private field on a private struct, reachable from neither outside the crate nor over FFI. Reverting the timeout makes that test hang past 60 seconds rather than fail, which is the behavior being removed. Removing the ceiling, or leaking the slot on a panicking callback, each fail their own test.