Skip to content

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

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions