Skip to content

Unclosed DBus connections lead to leaked goroutines and open file descriptors #541

@rcjsuen

Description

@rcjsuen
  1. Clone this repository.
  2. Create main.go with the following content in a new folder named ./store/keychain/cmd/dbusleak/.
  3. Run the test program with dbus-run-session.
$ dbus-run-session -- go run ./store/keychain/cmd/dbusleak -n 200
before  200 calls: goroutines=  1 open-fds=  5
after   200 calls: goroutines=401 open-fds=207
//go:build linux

// Command dbusleak reproduces the D-Bus connection leak in the Linux keychain
// store.
//
// Every keychain operation (Get/Save/Delete/Filter/GetAllMetadata in
// keychain_linux.go) starts by calling secretservice.NewService(), which opens
// a private session-bus connection via dbus.ConnectSessionBus(). That
// connection is never closed: SecretService has CloseSession() (which only
// closes the remote Secret Service *session*) but no Close() for the underlying
// *dbus.Conn. Each leaked connection keeps a socket FD open plus two goroutines
// (a reader and a signal handler) alive until the process exits.
//
// The leak happens at connect time, before any Secret Service call, so this
// needs only a session bus -- no gnome-keyring/KWallet required:
//
//	dbus-run-session -- go run ./store/keychain/cmd/dbusleak -n 200
//
// Expected output against the current code: goroutines grow by ~2*N and open
// FDs by ~N. Once SecretService gains a Close() that the keychain_linux.go
// callers defer, both counts stay flat.
package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"runtime"

	"github.com/docker/secrets-engine/store/keychain/internal/go-keychain/secretservice"
)

func openFDs() int {
	entries, _ := os.ReadDir(filepath.Join("/proc", fmt.Sprint(os.Getpid()), "fd"))
	return len(entries)
}

func main() {
	n := flag.Int("n", 200, "number of NewService() calls")
	flag.Parse()

	fmt.Printf("before %4d calls: goroutines=%3d open-fds=%3d\n", *n, runtime.NumGoroutine(), openFDs())

	for i := 0; i < *n; i++ {
		// Exactly what every keychainStore method does first. The returned
		// *SecretService owns a *dbus.Conn that nothing ever closes.
		if _, err := secretservice.NewService(); err != nil {
			fmt.Fprintln(os.Stderr, "NewService:", err)
			os.Exit(1)
		}
	}

	runtime.GC() // prove the leak survives GC: the blocked goroutines pin everything
	fmt.Printf("after  %4d calls: goroutines=%3d open-fds=%3d\n", *n, runtime.NumGoroutine(), openFDs())
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions