{"record":{"id":"038b4a2f63fd7510","repo":"hashicorp/consul","slug":"semaphore-bad-release","errorCode":null,"errorMessage":"semaphore: bad release","messagePattern":"semaphore: bad release","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"lib/semaphore/semaphore.go","lineNumber":92,"sourceCode":"\t\tdefault:\n\t\t\ts.waiters.Remove(elem)\n\t\t}\n\t\ts.mu.Unlock()\n\t\treturn err\n\n\tcase <-ready:\n\t\treturn nil\n\t}\n}\n\n// Release releases the semaphore. It will panic if release is called on an\n// empty semphore.\nfunc (s *Dynamic) Release() {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif s.cur < 1 {\n\t\tpanic(\"semaphore: bad release\")\n\t}\n\n\tnext := s.waiters.Front()\n\n\t// If there are no waiters, just decrement and we're done\n\tif next == nil {\n\t\ts.cur--\n\t\treturn\n\t}\n\n\t// Need to yield our slot to the next waiter.\n\t// Remove them from the list\n\ts.waiters.Remove(next)\n\t// And trigger it's chan before we release the lock\n\tclose(next.Value.(chan struct{}))\n\t// Note we _don't_ decrement inflight since the slot was yielded directly.\n}\n","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/hashicorp/consul/blob/2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e/lib/semaphore/semaphore.go#L74-L110","documentation":"Dynamic.Release (lib/semaphore/semaphore.go:87) panics when the semaphore has zero acquired slots (cur < 1), i.e. when Release is called more often than Acquire succeeded. This mirrors golang.org/x/sync/semaphore semantics: misuse is a programming error, so the library panics instead of silently corrupting the count. The most frequent cause is deferring Release before checking the Acquire error, so a canceled context leads to a release that was never acquired.","triggerScenarios":"'defer sem.Release()' placed before sem.Acquire(ctx) returns, so Release runs even when Acquire failed with ctx.Err(); calling Release twice for one Acquire; calling Release on a fresh zero-value/newly created semaphore that nobody acquired.","commonSituations":"Refactors that hoist defers to the top of the function; early-return error paths added after the defer was written; multiple goroutines releasing on behalf of a single acquire; test teardown that releases unconditionally.","solutions":["Acquire first and only defer Release after the error check: 'if err := sem.Acquire(ctx); err != nil { return err }; defer sem.Release()'","Audit code for double-Release paths (a Release in a loop body plus one in a defer)","If pairing is hard to see locally, wrap Dynamic in a small type that hands out a release token (func()) per successful Acquire"],"exampleFix":"// before\nfunc work(ctx context.Context) error {\n\tdefer sem.Release() // registered too early\n\tif err := sem.Acquire(ctx); err != nil {\n\t\treturn err // Release still runs -> panic on empty semaphore\n\t}\n\t...\n}\n\n// after\nfunc work(ctx context.Context) error {\n\tif err := sem.Acquire(ctx); err != nil {\n\t\treturn err // nothing acquired, nothing to release\n\t}\n\tdefer sem.Release() // paired with a successful Acquire\n\t...\n}","handlingStrategy":"validation","validationCode":"if err := sem.Acquire(ctx); err != nil {\n\treturn err // no slot acquired; must NOT call Release\n}\ndefer sem.Release() // only after a successful Acquire","typeGuard":null,"tryCatchPattern":"// last-resort containment at a goroutine boundary; fix the pairing instead\ndefer func() {\n\tif r := recover(); r != nil {\n\t\tlog.Error(\"semaphore misuse\", \"panic\", r)\n\t}\n}()","preventionTips":["Always pair defer sem.Release() directly under a successful Acquire, never above the error check","Search for '.Release()' occurrences and confirm each has exactly one guarded Acquire","Wrap the semaphore in a helper returning a release func() so pairing is enforced by construction"],"tags":["go","consul","panic","concurrency","semaphore","sync","resource-pairing"],"backgroundTag":null,"analyzedSha":"2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e","analyzedAt":"2026-08-15T19:19:47.700Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}