golang/go

runtime: AddCleanup func is never executed if ptr points to struct with a single numeric field

Closed this issue · 1 comments

Go version

go version go1.25.3 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='x86_64-linux-gnu-gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='x86_64-linux-gnu-g++'
GCCGO='/usr/bin/gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/ext_zdilap/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/ext_zdilap/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2719480727=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/ext_zdilap/stuff/go/test/go.mod'
GOMODCACHE='/home/ext_zdilap/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/ext_zdilap/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/lib/go-1.25'
GOSUMDB='sum.golang.org'
GOTELEMETRY='on'
GOTELEMETRYDIR='/home/ext_zdilap/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/usr/lib/go-1.25/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25.3'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

When ptr argument to runtime.AddCleanup is a struct with a single numeric field, the cleanup function is never called.
Cleanup is called if the field is of type string, or if a struct has two numeric fields.

Runnable example:

https://go.dev/play/p/71v_vLeVMUo

This may be problematic in practice if someone, for some odd reason, writes a file-descriptor-wrapper structure and relies on GC to close the file descriptor.
For example, this program starts printing Read error: too many open files on my machine after reaching N open files, where N is the configured ulimit -n:

package main

import (
	"fmt"
	"runtime"
	"syscall"
)

type File struct {
	fd int
}

func main() {
	for {
		OpenAndProcessFile("/dev/zero")
	}
}

func OpenAndProcessFile(path string) {
	fd, err := syscall.Open(path, syscall.O_RDONLY, 0)
	if err != nil {
		fmt.Printf("Read error: %v\n", err)
		return
	}
	ProcessFile(fd)
}

func ProcessFile(fd int) {
	var called bool
	func() {
		f := new(File)
		f.fd = fd
		runtime.AddCleanup(f, func(fd int) {
			syscall.Close(fd)
			called = true
		}, fd)
		// Do something with f...
		b := make([]byte, 10)
		n, err := syscall.Read(f.fd, b)
		if err != nil {
			fmt.Printf("Read error: %v\n", err)
			return
		}
		fmt.Printf("Read %d bytes: %v\n", n, b[:n])
	}()
	// Force garbage collection multiple times to ensure cleanup is called
	for range 10 {
		runtime.GC()
	}
	fmt.Printf("Cleanup called for File: %v\n", called)
}

What did you see happen?

Cleanup is not called for structures with a single numerical field.

What did you expect to see?

Cleanup is called for all structures, regardless of their internals.