{"record":{"id":"5f505f05181a07e1","repo":"cloudflare/pingora","slug":"take-write-lock-called-without-lock","errorCode":null,"errorMessage":"take_write_lock() called without lock","messagePattern":"take_write_lock\\(\\) called without lock","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pingora-cache/src/lib.rs","lineNumber":1568,"sourceCode":"        self.inner_enabled()\n            .lock_ctx\n            .as_ref()\n            .and_then(|l| l.max_retries)\n    }\n\n    /// Take the write lock from this request to transfer it to another one.\n    /// # Panic\n    ///  Call is_cache_lock_writer() to check first, will panic otherwise.\n    pub fn take_write_lock(&mut self) -> (WritePermit, &'static CacheKeyLockImpl) {\n        let lock_ctx = self\n            .inner_enabled_mut()\n            .lock_ctx\n            .as_mut()\n            .expect(\"take_write_lock() called without cache lock\");\n        let lock = lock_ctx\n            .lock\n            .take()\n            .expect(\"take_write_lock() called without lock\");\n        match lock {\n            Locked::Write(w) => (w, lock_ctx.cache_lock),\n            Locked::Read(_) => panic!(\"take_write_lock() called on read lock\"),\n        }\n    }\n\n    /// Set the write lock, which is usually transferred from [Self::take_write_lock()]\n    ///\n    /// # Panic\n    /// Panics if cache lock was not originally configured for this request.\n    // TODO: it may make sense to allow configuring the CacheKeyLock here too that the write permit\n    // is associated with\n    // (The WritePermit comes from the CacheKeyLock and should be used when releasing from the CacheKeyLock,\n    // shouldn't be possible to give a WritePermit to a request using a different CacheKeyLock)\n    pub fn set_write_lock(&mut self, write_lock: WritePermit) {\n        if let Some(lock_ctx) = self.inner_enabled_mut().lock_ctx.as_mut() {\n            lock_ctx.lock.replace(Locked::Write(write_lock));\n        }","sourceCodeStart":1550,"sourceCodeEnd":1586,"githubUrl":"https://github.com/cloudflare/pingora/blob/0046038bd402bc82912da862dadf9a479f31e9f1/pingora-cache/src/lib.rs#L1550-L1586","documentation":"The second .expect() inside HttpCache::take_write_lock() (lib.rs:1568): the request does have a lock_ctx, but lock_ctx.lock is already None because Option::take() consumed the permit earlier. That is the exact post-transfer state, so this panic ('take_write_lock() called without lock') means take_write_lock() was called a second time, or another component already transferred the permit while leaving the context behind.","triggerScenarios":"Calling take_write_lock() twice on the same request; a retry loop that re-invokes the handoff after a partial failure; two filters both attempting to transfer the write permit.","commonSituations":"Error paths that took the lock, failed to hand it off, and then retry the take; refactors where set_write_lock() was dropped so the context is never re-armed; duplicated transfer logic in request and response filters.","solutions":["Call take_write_lock() exactly once per request and move the returned (WritePermit, lock) pair onward","If a handoff can fail midway, restore the permit with session.cache.set_write_lock(permit) before any retry","Use is_cache_lock_writer() as the single guard: it is false once the permit was taken"],"exampleFix":"// before\nlet (p1, lock) = session.cache.take_write_lock();\nlet (p2, _) = session.cache.take_write_lock(); // panic: lock already taken\n\n// after\nlet (permit, lock) = session.cache.take_write_lock();\n// re-arm before any second take:\nsession.cache.set_write_lock(permit);\nlet (permit, lock) = session.cache.take_write_lock();","handlingStrategy":"validation","validationCode":"// is_cache_lock_writer() is false once the permit was taken;\n// use it to make double-takes impossible\nfn take_write_lock_once(cache: &mut HttpCache) -> Option<(WritePermit, &'static CacheKeyLockImpl)> {\n    if cache.is_cache_lock_writer() {\n        return Some(cache.take_write_lock());\n    }\n    None\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Model the write permit as move-only: after take_write_lock() this request owns nothing","If a handoff can fail midway, restore with set_write_lock(permit) before any retry","Centralize take/set in a small state machine instead of calling from multiple filters"],"tags":["rust","pingora-cache","cache-lock","panic","invariant"],"backgroundTag":"cache-lock-misuse","analyzedSha":"0046038bd402bc82912da862dadf9a479f31e9f1","analyzedAt":"2026-08-16T21:33:22.341Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}