Skip to content

fix(secrets): timeout keyring.Open() on macOS to prevent indefinite hang (#513)#515

Closed
sardoru wants to merge 1 commit intosteipete:mainfrom
sardoru:fix/macos-keyring-hang
Closed

fix(secrets): timeout keyring.Open() on macOS to prevent indefinite hang (#513)#515
sardoru wants to merge 1 commit intosteipete:mainfrom
sardoru:fix/macos-keyring-hang

Conversation

@sardoru
Copy link
Copy Markdown
Contributor

@sardoru sardoru commented Apr 21, 2026

Fixes #513.

The bug

After upgrading gogcli via Homebrew, every command that reads stored tokens (gog auth list, gog gmail search, etc.) hangs indefinitely on macOS and is eventually SIGKILL'd — no output, no prompt, no actionable error.

The root cause is that the keyring.Open() timeout safety net introduced for headless Linux (#XXX) only fires when goos == "linux". On macOS, the Security framework waits for a Keychain GUI permission prompt that can't surface in non-interactive contexts (cron jobs, agents, subprocesses) — or, per the existing comment referencing #86, after a Homebrew upgrade installs a new binary hash and the previous "Always Allow" grant no longer applies. The process just hangs.

The fix

Extend shouldUseKeyringTimeout to include darwin when the backend is auto or keychain. If the Keychain doesn't respond within the timeout, return a clear error with platform-specific guidance instead of hanging forever.

Also bump the timeout from 5s → 10s to tolerate slower macOS Keychain responses on first post-upgrade access (still short enough to feel like a real error, long enough to not false-positive on a cold keychain).

Error message before

$ gog auth list
(hangs forever, then SIGKILL with no output)

Error message after

$ gog auth list
Error: keyring open timed out after 10s (macOS Keychain may be waiting for a
permission prompt — run `gog auth list` from a terminal and click "Always Allow"
when prompted (common after Homebrew upgrades; see
https://github.com/steipete/gogcli/issues/86)); set GOG_KEYRING_BACKEND=file
and GOG_KEYRING_PASSWORD=<password> to use encrypted file storage instead

What's covered

Platform / backend Pre-PR timeout Post-PR timeout
Linux auto + D-Bus ✅ 5s ✅ 10s
Linux auto, no D-Bus n/a (forced to file) n/a
Linux explicit file n/a n/a
macOS auto ❌ hangs ✅ 10s
macOS keychain ❌ hangs ✅ 10s
macOS explicit file n/a n/a
Windows auto n/a n/a

Tests

  • Extends TestKeyringDbusGuards with three darwin cases: auto → timeout, keychain → timeout, file → no timeout
  • New TestKeyringTimeoutHint verifies platform-specific hint text for darwin/linux/windows
  • Existing TestOpenKeyringWithTimeout_Timeout still passes (my change kept the GOG_KEYRING_BACKEND=file fallback text the test asserts on)

Why 10s instead of 5s

On macOS, Keychain can briefly block when it's cold (first access after login, or first access after a binary-hash change). 5s was occasionally false-positive even when the prompt would eventually surface. 10s is comfortably long enough for a real prompt to complete while still failing fast on a deadlocked Security framework.

Caveat / follow-up

The hang is really in keyring.Keys() on subsequent calls if the permission grant isn't cached — but in practice keyring.Open() probes the backend during construction, which is where the GUI prompt is triggered, so timing out Open() catches almost all real-world cases. A full-coverage fix would also wrap Keys()/Get() with a timeout, but that's more invasive and deserves a separate PR.

Discovery

I hit this the night after #513 — upgraded from v0.12.0 to v0.13.0 via brew upgrade, and every gog command that touched auth hung. Rolled back to v0.12.0 (same OAuth tokens in Keychain), everything worked instantly again. Didn't re-upgrade until I could trace it.

Tested manually on:

  • macOS 15.3.1 (build 24D70) — Apple Silicon (arm64)
  • Existing keychain items from a prior v0.12.0 install

Fixes steipete#513.

Previously the keyring.Open() timeout safety net only covered Linux with
a D-Bus session. On macOS, if the Security framework waits for a GUI
permission prompt that cannot surface (e.g., post-Homebrew-upgrade
re-authorization, non-interactive runs like crons/agents), every auth-
requiring command hangs indefinitely until the process is killed —
with no output and no actionable error.

This extends shouldUseKeyringTimeout to include macOS when the backend
is 'auto' or 'keychain'. The timeout is also bumped from 5s to 10s to
tolerate slower macOS Keychain responses on first post-upgrade access.

When the timeout fires, the error now includes platform-specific
guidance:

  - darwin: run from a terminal and click 'Always Allow' when prompted
    (common after Homebrew upgrades; links to steipete#86)
  - linux:  existing 'D-Bus SecretService may be unresponsive' hint
  - other:  generic 'keyring backend may be unresponsive' hint

All cases continue to suggest GOG_KEYRING_BACKEND=file as a fallback.

Adds test cases covering darwin auto/keychain/file in TestKeyringDbusGuards
and a new TestKeyringTimeoutHint covering the platform-specific hints.
@steipete
Copy link
Copy Markdown
Owner

Thanks @sardoru. I landed this as a maintainer rewrite on main in 6430dd1, with lint/test cleanup in c2ea4f5.

I changed the implementation from an Open() timeout to operation-level timeouts for macOS keyring reads/writes/lists. The macOS keyring backend constructs the object in Open(); the blocking Security framework calls happen on Keys / Get / Set / Remove, so this covers the actual hang point. Added tests, docs, and changelog.

Closing this as landed.

@steipete steipete closed this Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v0.13.0: keyring backend hangs indefinitely on macOS — all auth-requiring commands SIGKILL'd (rollback to v0.12.0 restores)

2 participants