{"slug": "catching-resources-held-across-async-suspension", "title": "Catching Resources Held Across Async Suspension", "summary": "Bifrost's RQL policy language can detect resources held across an async suspension boundary in TypeScript, flagging a violation when a resource acquired via acquireResource() is still in the held state at an await. The policy models the resource lifecycle as a typestate with transitions from held to released on release and from held to violated on suspend, and a later releaseResource() call does not erase the finding. The technique is demonstrated against a refresh() function that acquires a resource, awaits rebuildIndex(), and only then releases it.", "body_md": "# Catching Resources Held Across Async Suspension\n\nAn asynchronous function can release every resource before it finishes and still hold one at the wrong time. Borrow a connection from a small pool, then wait for an unrelated network request, and that connection remains occupied while the function is paused. A release call later in the function does little for the other callers waiting now.\n\nWhether that is a problem depends on the resource. Plenty of APIs are designed to stay open across asynchronous work. For this example, we will require resources acquired through our API to be released before the acquiring function suspends.\n\nIn [Following untrusted data through a database](https://blog.brokk.ai/following-untrusted-data-through-a-database/), the policy followed a value from a write to a later read. Here we need to follow an object's state: has this particular resource been released at this particular point?\n\nHere is the problem:\n\n``` js\nimport { acquireResource, releaseResource } from \"./resource\";\n\nexport async function refresh() {\n  const resource = acquireResource();\n  await rebuildIndex();\n  releaseResource(resource);\n}\n```\n\nAt the `await`, `resource` is still held. The awaited expression need not use it. JavaScript [pauses the surrounding async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await?ref=blog.brokk.ai#description), leaving the resource's lifecycle unfinished while execution is suspended.\n\n## Describe the resource\n\nBifrost's [RQL policy language](https://bifrost.brokk.ai/static-analysis-policies/?ref=blog.brokk.ai#typestate-endpoint-reuse-plus-protocol-rules) lets us describe this API's resource lifecycle. Our policy treats the object returned by `acquireResource()` as acquired, and the first argument to `releaseResource()` as released. We supply those meanings when we define the policy's endpoints. A function called `releaseResource` does not acquire special powers by having a reassuring name.\n\nThe demo keeps the API declarations in `src/resource.ts`. Its acquisition selector is:\n\n```\n:selector (rql :schema-version 1\n  (language typescript\n    (call-sites-to :proof proven\n      (enclosing-decl\n        (where \"src/resource.ts\"\n          (function :name \"acquireResource\"))))))\n:binding return-value\n```\n\nThe file and function select a declaration. `call-sites-to :proof proven` then selects calls that the resolver binds to that declaration, including through the import. Release uses the same structure for `releaseResource`, with `:binding (argument :index 0)`.\n\nAnother API can use the same names. The reproduction includes an unrelated `acquireResource` and `releaseResource`; those calls do not become resource events for this policy.\n\nThe helpers give us concrete declarations and returned objects for the example. The policy defines the protocol we want to check. A production resource pool would need to implement the actual acquisition and release.\n\n## Observe the state at suspension\n\nThe resource starts in `held`. A successful release moves it to `released`. Bifrost can observe async suspension as a protocol event:\n\n```\n(event :id suspend\n  :on (suspension-boundary :scope analysis-root)\n  :supersedes [])\n```\n\nThe relevant transitions are:\n\n```\n(transition :from held :on release :to released)\n(transition :from held :on suspend :to violated)\n(transition :from released :on suspend :to released)\n```\n\nRelease is observed after the call returns normally. At suspension, the policy checks the current state of the tracked object. In the first example, that state is still `held`, so the transition produces a finding at the `await`.\n\nThe release later in the function does not erase that finding. A separate check adds a second `await` after release: the first suspension remains the violation, while the second sees the released state.\n\nThe recordings show the actual released CLI running against each example. The first catches the resource held at suspension. Playback timing is adjusted for readability.\n\nThe first run completes with one finding at `src/held.ts:6`. It reports `certainty: possible`, `proof: proven`, and `completeness: complete`. The proof is a typestate witness under our declared protocol; the result does not predict a deadlock or how long the scheduler will leave the function paused.\n\n## Release the same object\n\nMoving a release above the `await` only helps if it releases the resource being tracked:\n\n``` js\nexport async function refresh() {\n  const resource = acquireResource();\n  const alias = resource;\n  releaseResource(alias);\n  await rebuildIndex();\n}\n```\n\nThis run completes with zero findings. Bifrost proves that `alias` refers to the acquired object, so release changes that object's protocol state. The variable name can change without losing the connection.\n\nReleasing a different resource is another matter. In a two-resource check, releasing `second` before suspension leaves `first` held, and the policy reports one finding. Counting release calls, or checking that one appears before the `await`, would miss the distinction.\n\nA conditional alias also needs care:\n\n``` js\nconst chosen = flag ? first : second;\nreleaseResource(chosen);\nawait rebuildIndex();\n```\n\nOn v0.12.0, this check returns two possible findings and an inconclusive run with `partial_discovery`. The release cannot certify both objects as released. The uncertainty remains visible rather than becoming a clean result.\n\n## Check the resource at the pause\n\nThis policy observes suspension within the analysis root. It makes no eventual-release obligation, and the demonstrated scope does not cover cancellation cleanup, deferred callback effects, or resource ownership transported between procedures. Adapting it to a library requires selecting the actual API and checking that its resource semantics fit the model.\n\nChecking that a function eventually calls `releaseResource` would miss the problem. We need to know whether the same object is still held when the function pauses. Tracking the object's state catches the first case, accepts release through an exact alias, and leaves the conditional alias inconclusive.\n\n## Appendix: reproduce the examples\n\nThe examples and both recordings were run with the public [Bifrost v0.12.0 release](https://github.com/BrokkAi/bifrost/releases/tag/v0.12.0?ref=blog.brokk.ai), after verifying its download checksum.\n\nThe full reproduction includes the source, policy, validation script, and JSON reports. Seven checks cover the violation, exact alias, wrong-object release, unrelated API, second suspension, conditional alias, and unsupported async iteration.\n\nThe `for await` control returns zero findings with `capability_incomplete`: that suspension shape is not lowered in this release. Those zero findings establish nothing about safety.", "url": "https://wpnews.pro/news/catching-resources-held-across-async-suspension", "canonical_source": "https://blog.brokk.ai/catching-resources-held-across-async-suspension/", "published_at": "2026-10-06 15:00:14+00:00", "updated_at": "2026-10-06 15:18:51.590928+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Bifrost", "RQL", "TypeScript", "JavaScript", "acquireResource", "releaseResource", "brokk.ai"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/catching-resources-held-across-async-suspension", "markdown": "https://wpnews.pro/news/catching-resources-held-across-async-suspension.md", "text": "https://wpnews.pro/news/catching-resources-held-across-async-suspension.txt", "jsonld": "https://wpnews.pro/news/catching-resources-held-across-async-suspension.jsonld"}}