From 2eced240fa5e01db9c2e96edb44470d96a97f158 Mon Sep 17 00:00:00 2001 From: Richard Wall Date: Thu, 1 Oct 2026 06:38:05 +0000 Subject: [PATCH] Fix deadlock from taking db lock while holding authz lock in Authz() Authz() held the authorization's write lock while it looked up the account, which takes the MemoryStore read lock. FindValidAuthorization takes the same two locks in the opposite order: it holds the MemoryStore read lock while it read-locks every authorization. Once a writer such as AddAccount queues on the MemoryStore lock, the three goroutines wait on each other and every later request that touches the db hangs. Authz() also read-locked the order while holding the authz lock, which is the reverse of the order used by Order.GetStatus. Do the order and account lookups before taking the authz lock. Co-Authored-By: Claude Signed-off-by: Richard Wall --- wfe/wfe.go | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/wfe/wfe.go b/wfe/wfe.go index f4ebbe8c..ddbe18fc 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -2356,6 +2356,21 @@ func (wfe *WebFrontEndImpl) Authz( return } + // Do the account and order lookups before locking the authz. + // MemoryStore.FindValidAuthorization and Order.GetStatus take the db + // lock, then the order lock, then the authz lock. Taking either of the + // first two while holding the authz lock deadlocks if a writer queues up. + var account *core.Account + if postData.postAsGet { + account, prob = wfe.validPOSTAsGET(postData) + } else { + account, prob = wfe.getAcctByKey(postData.jwk) + } + if prob != nil { + wfe.sendError(prob, response) + return + } + authzID := strings.TrimPrefix(request.URL.Path, authzPath) authz := wfe.db.GetAuthorizationByID(authzID) if authz == nil { @@ -2363,22 +2378,17 @@ func (wfe *WebFrontEndImpl) Authz( return } - authz.Lock() - defer authz.Unlock() authz.Order.RLock() orderAcctID := authz.Order.AccountID authz.Order.RUnlock() + authz.Lock() + defer authz.Unlock() + // If the postData is not a POST-as-GET, treat this as case A) and update // the authorization based on the postData if !postData.postAsGet { - existingAcct, prob := wfe.getAcctByKey(postData.jwk) - if prob != nil { - wfe.sendError(prob, response) - return - } - - if orderAcctID != existingAcct.ID { + if orderAcctID != account.ID { wfe.sendError(acme.UnauthorizedProblem( "Account does not own authorization"), response) return @@ -2406,12 +2416,6 @@ func (wfe *WebFrontEndImpl) Authz( // Otherwise this was a POST-as-GET request and we need to verify it // accordingly and ensure the authorized account owns the authorization // being fetched. - account, prob := wfe.validPOSTAsGET(postData) - if prob != nil { - wfe.sendError(prob, response) - return - } - if orderAcctID != account.ID { wfe.sendError(acme.UnauthorizedProblem( "Account authorizing the request is not the owner of the authorization"),