diff --git a/store/keychain/internal/go-keychain/secretservice/secretservice.go b/store/keychain/internal/go-keychain/secretservice/secretservice.go index 4b301aed..1381785a 100644 --- a/store/keychain/internal/go-keychain/secretservice/secretservice.go +++ b/store/keychain/internal/go-keychain/secretservice/secretservice.go @@ -78,6 +78,20 @@ func NewService() (*SecretService, error) { return &SecretService{conn: conn, signalCh: signalCh, sessionOpenTimeout: DefaultSessionOpenTimeout}, nil } +// Close releases the underlying D-Bus connection and its socket file +// descriptor. Each [NewService] call dials a private session-bus connection +// (via dbus.ConnectSessionBus), so every service MUST be closed when it is no +// longer needed; otherwise the connection — and its fd — leaks for the lifetime +// of the process. Closing the connection also tears down the signal goroutine +// that NewService starts. Close is safe to call on a service whose connection +// is nil. +func (s *SecretService) Close() error { + if s == nil || s.conn == nil { + return nil + } + return s.conn.Close() +} + // SetSessionOpenTimeout func (s *SecretService) SetSessionOpenTimeout(d time.Duration) { s.sessionOpenTimeout = d diff --git a/store/keychain/keychain_linux.go b/store/keychain/keychain_linux.go index 651bfb6e..cfa89161 100644 --- a/store/keychain/keychain_linux.go +++ b/store/keychain/keychain_linux.go @@ -150,6 +150,10 @@ func (k *keychainStore[T]) Delete(_ context.Context, id store.ID) error { if err != nil { return err } + // NewService dials a fresh private session-bus connection; close it (and + // its socket fd) when we return. Deferred before CloseSession so that, by + // LIFO order, the session is closed first and the connection last. + defer func() { _ = service.Close() }() session, err := service.OpenSession(kc.AuthenticationDHAES) if err != nil { @@ -193,6 +197,10 @@ func (k *keychainStore[T]) Get(ctx context.Context, id store.ID) (store.Secret, if err != nil { return nil, err } + // NewService dials a fresh private session-bus connection; close it (and + // its socket fd) when we return. Deferred before CloseSession so that, by + // LIFO order, the session is closed first and the connection last. + defer func() { _ = service.Close() }() session, err := service.OpenSession(kc.AuthenticationDHAES) if err != nil { @@ -256,6 +264,10 @@ func (k *keychainStore[T]) GetAllMetadata(ctx context.Context) (map[store.ID]sto if err != nil { return nil, err } + // NewService dials a fresh private session-bus connection; close it (and + // its socket fd) when we return. Deferred before CloseSession so that, by + // LIFO order, the session is closed first and the connection last. + defer func() { _ = service.Close() }() session, err := service.OpenSession(kc.AuthenticationDHAES) if err != nil { @@ -324,6 +336,10 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec if err != nil { return err } + // NewService dials a fresh private session-bus connection; close it (and + // its socket fd) when we return. Deferred before CloseSession so that, by + // LIFO order, the session is closed first and the connection last. + defer func() { _ = service.Close() }() session, err := service.OpenSession(kc.AuthenticationDHAES) if err != nil { @@ -401,6 +417,10 @@ func (k *keychainStore[T]) Filter(ctx context.Context, pattern store.Pattern) (m if err != nil { return nil, err } + // NewService dials a fresh private session-bus connection; close it (and + // its socket fd) when we return. Deferred before CloseSession so that, by + // LIFO order, the session is closed first and the connection last. + defer func() { _ = service.Close() }() session, err := service.OpenSession(kc.AuthenticationDHAES) if err != nil {