{"record":{"id":"1c819a76b76f0b24","repo":"denoland/deno","slug":"pledge-test-permissions-called-before-restoring-pr-1c819a","errorCode":null,"errorMessage":"pledge test permissions called before restoring previous pledge","messagePattern":"pledge test permissions called before restoring previous pledge","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/ops/testing.rs","lineNumber":94,"sourceCode":"#[derive(Clone)]\npub struct TestIsolateHandle(pub v8::IsolateHandle);\n\n#[derive(Clone)]\nstruct PermissionsHolder(Uuid, PermissionsContainer);\n\n#[op2(stack_trace)]\n#[serde]\npub fn op_pledge_test_permissions(\n  state: &mut OpState,\n  #[serde] args: ChildPermissionsArg,\n) -> Result<Uuid, deno_runtime::deno_permissions::ChildPermissionError> {\n  let token = Uuid::new_v4();\n  let parent_permissions = state.borrow_mut::<PermissionsContainer>();\n  let worker_permissions = parent_permissions.create_child_permissions(args)?;\n  let parent_permissions = parent_permissions.clone();\n\n  if state.try_take::<PermissionsHolder>().is_some() {\n    panic!(\"pledge test permissions called before restoring previous pledge\");\n  }\n  state.put::<PermissionsHolder>(PermissionsHolder(token, parent_permissions));\n\n  // NOTE: This call overrides current permission set for the worker\n  state.put::<PermissionsContainer>(worker_permissions);\n\n  Ok(token)\n}\n\n#[op2]\npub fn op_restore_test_permissions(\n  state: &mut OpState,\n  #[serde] token: Uuid,\n) -> Result<(), JsErrorBox> {\n  match state.try_take::<PermissionsHolder>() {\n    Some(permissions_holder) => {\n      if token != permissions_holder.0 {\n        panic!(","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/cli/ops/testing.rs#L76-L112","documentation":"Panic in the op_pledge_test_permissions op (cli/ops/testing.rs) backing `deno test`. Identical logic to the bench variant: pledging swaps the worker's PermissionsContainer for the test's restricted set and stores the original under PermissionsHolder; a second pledge while one is already stored panics, enforcing a single active permission pledge per worker.","triggerScenarios":"The internal test-harness op is invoked twice without an intervening op_restore_test_permissions — from modified cli/js harness code, custom runners built on internal ops, or a harness re-entry bug.","commonSituations":"Contributors changing Deno's testing bootstrap; userland code poking internal ops; edge cases like nested test-server startup double-pledging. Not reachable through normal Deno.test() usage.","solutions":["Pair every pledge with its restore (finally) before pledging again in harness code","Audit custom harness forks for unbalanced pledge/restore paths, including early-exit error paths","Report a bug with a minimal repro if stock `deno test` hits it"],"exampleFix":"// before (harness pseudo-code)\nconst t1 = pledge(args);\nconst t2 = pledge(args);          // panics\n// after\nconst t1 = pledge(args);\ntry { runTests(); } finally { restore(t1); }\nconst t2 = pledge(args);","handlingStrategy":"validation","validationCode":"// guard the testing harness entrypoint against double pledges\nconst pledged = { active: false };\nfunction safePledge(args) {\n  if (pledged.active) throw new Error('restore the previous pledge first');\n  const t = pledgeOp(args);\n  pledged.active = true;\n  return t;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Model pledge/restore as a bracket: open once, close once, on every control-flow path","Add assertions in harness forks that OpState has no holder before pledging"],"tags":["permissions","testing","internal-ops","state-machine","panic"],"backgroundTag":"invalid-operation-sequence","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}