auto-GC goroutine never stops after Close() (v1.2.3+), and crashes with SIGSEGV (v1.2.6+)
Affected: goroutine leak since v1.2.3; SIGSEGV since v1.2.6; both still present in v1.2.7.
Last good version: v1.2.2.
| Version |
autoGCQuit created |
Gc() writes to metaFile.data |
Behaviour after Close() |
| v1.2.0 – v1.2.2 |
yes |
no |
clean exit |
| v1.2.3 – v1.2.5 |
no |
no |
goroutine leaks |
| v1.2.6 |
no |
yes ([9:16]) |
SIGSEGV |
| v1.2.7 |
no |
yes ([8:16]) |
SIGSEGV |
Verified by running the reproduction below against each published version.
Summary
autoGC() no longer creates the autoGCQuit channel, so it stays nil. A receive on a
nil channel blocks forever, so the goroutine's quit case can never fire and the goroutine
keeps running after Close(). On its next tick it calls Gc(), which writes to
q.metaFile.data — already unmapped by Close() — and the process dies with SIGSEGV.
What changed
v1.2.3 removed the channel creation from autoGC(). Up to v1.2.2 it read:
func (q *FileQueue) autoGC() {
ticker := time.NewTicker(time.Second * time.Duration(q.options.AutoGCBySeconds))
q.autoGCQuit = make(chan int) // <-- removed in v1.2.3
...
From v1.2.3 onward that line is gone, but the two places that use the channel are unchanged
(line numbers below are from v1.2.7):
filequeue.go:833 — case <-q.autoGCQuit: (the goroutine's exit path)
filequeue.go:705 — if q.autoGCQuit != nil { q.autoGCQuit <- 1 } in Close()
Because the channel is nil, Close() skips the send (so no deadlock), and the goroutine
never sees a quit signal.
On v1.2.3 – v1.2.5 this is only a leak. It became a crash in v1.2.6, which added a write
to the mapped metadata at the end of Gc():
q.tailIndex = frontIndex
// update to mapped data
copy(q.metaFile.data[8:16], IntToBytes(q.tailIndex)) // [9:16] in v1.2.6
After Close() that memory is unmapped, so the copy dereferences nil.
Reproducing it
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/jhunters/bigqueue"
)
func main() {
dir, _ := os.MkdirTemp("", "bqgc")
defer os.RemoveAll(dir)
// Pre-created because MkdirAll is called with a mode lacking +x; unrelated to this bug.
for _, sub := range []string{"", "index", "data", "meta_data", "front_index"} {
os.MkdirAll(filepath.Join(dir, "testq", sub), 0o755)
}
q := &bigqueue.FileQueue{}
if err := q.Open(dir, "testq", &bigqueue.Options{
DataPageSize: 128 * 1024, IndexItemsPerPage: 17, AutoGCBySeconds: 1,
}); err != nil {
panic(err)
}
q.Enqueue([]byte("hello"))
q.Dequeue() // move frontIndex so Gc() has work to do
q.Close()
fmt.Println("queue closed, waiting for the next auto-GC tick...")
time.Sleep(3 * time.Second)
fmt.Println("still alive (expected on v1.2.2, not reached on v1.2.7)")
}
On v1.2.2 (and earlier):
2026/08/31 14:05:17 Auto gc goroutine exit to end
queue closed, waiting for the next auto-GC tick...
still alive
On v1.2.6 and v1.2.7 (identical stack, differing only in line offsets):
queue closed, waiting for the next auto-GC tick...
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x1022c81b0]
goroutine 5 [running]:
github.com/jhunters/bigqueue.(*FileQueue).Gc(0x1400007c000)
filequeue.go:771 +0x190
github.com/jhunters/bigqueue.(*FileQueue).autoGC.func1()
filequeue.go:832 +0x34
created by github.com/jhunters/bigqueue.(*FileQueue).autoGC in goroutine 1
filequeue.go:828 +0x80
Why this may not have been reported
autoGC() only runs when Options.AutoGCBySeconds > 0, and DefaultOptions sets it to 0.
Anyone using the defaults, or running their own GC ticker and calling Gc() directly, never
starts the goroutine and so never hits this. It only affects callers who explicitly opt in to
the library's own auto-GC and then call Close().
Suggested fix
func (q *FileQueue) autoGC() {
ticker := time.NewTicker(time.Second * time.Duration(q.options.AutoGCBySeconds))
+ q.autoGCQuit = make(chan int)
go func() {
I verified this: with that single line added to v1.2.7, the repro above logs
Auto gc goroutine exit to end and completes normally, matching v1.2.2.
It may also be worth having Gc() return early if the queue is closed, so a late tick
cannot touch unmapped memory even if the quit signal is missed.
auto-GC goroutine never stops after Close() (v1.2.3+), and crashes with SIGSEGV (v1.2.6+)
Affected: goroutine leak since v1.2.3; SIGSEGV since v1.2.6; both still present in v1.2.7.
Last good version: v1.2.2.
autoGCQuitcreatedGc()writes tometaFile.dataClose()[9:16])[8:16])Verified by running the reproduction below against each published version.
Summary
autoGC()no longer creates theautoGCQuitchannel, so it staysnil. A receive on anil channel blocks forever, so the goroutine's quit case can never fire and the goroutine
keeps running after
Close(). On its next tick it callsGc(), which writes toq.metaFile.data— already unmapped byClose()— and the process dies with SIGSEGV.What changed
v1.2.3 removed the channel creation from
autoGC(). Up to v1.2.2 it read:From v1.2.3 onward that line is gone, but the two places that use the channel are unchanged
(line numbers below are from v1.2.7):
filequeue.go:833—case <-q.autoGCQuit:(the goroutine's exit path)filequeue.go:705—if q.autoGCQuit != nil { q.autoGCQuit <- 1 }inClose()Because the channel is nil,
Close()skips the send (so no deadlock), and the goroutinenever sees a quit signal.
On v1.2.3 – v1.2.5 this is only a leak. It became a crash in v1.2.6, which added a write
to the mapped metadata at the end of
Gc():After
Close()that memory is unmapped, so the copy dereferences nil.Reproducing it
On v1.2.2 (and earlier):
On v1.2.6 and v1.2.7 (identical stack, differing only in line offsets):
Why this may not have been reported
autoGC()only runs whenOptions.AutoGCBySeconds > 0, andDefaultOptionssets it to 0.Anyone using the defaults, or running their own GC ticker and calling
Gc()directly, neverstarts the goroutine and so never hits this. It only affects callers who explicitly opt in to
the library's own auto-GC and then call
Close().Suggested fix
func (q *FileQueue) autoGC() { ticker := time.NewTicker(time.Second * time.Duration(q.options.AutoGCBySeconds)) + q.autoGCQuit = make(chan int) go func() {I verified this: with that single line added to v1.2.7, the repro above logs
Auto gc goroutine exit to endand completes normally, matching v1.2.2.It may also be worth having
Gc()return early if the queue is closed, so a late tickcannot touch unmapped memory even if the quit signal is missed.