Skip to content

auto-GC goroutine never stops after Close() (v1.2.3+), and crashes with SIGSEGV (v1.2.6+) #27

Description

@kristijanexads

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:833case <-q.autoGCQuit: (the goroutine's exit path)
  • filequeue.go:705if 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions