Skip to content

fix(opencode): write auth.json atomically under a lock - #46131

Open
iceteaSA wants to merge 2 commits into
anomalyco:devfrom
iceteaSA:auth-atomic-write
Open

fix(opencode): write auth.json atomically under a lock#46131
iceteaSA wants to merge 2 commits into
anomalyco:devfrom
iceteaSA:auth-atomic-write

Conversation

@iceteaSA

Copy link
Copy Markdown

Issue for this PR

Fixes #46128

Type of change

  • Bug fix

What does this PR do?

Fixes two ways auth.json can lose credentials. Two commits, one per defect, independently reviewable.

2380af8adb — stop auth writes from persisting the env snapshot.

Auth.all short-circuits to OPENCODE_AUTH_CONTENT and never reads the file. set and remove both started from all(), so with that variable set they read the env snapshot and wrote it to disk — erasing anything added to auth.json after the snapshot was taken.

That is reachable in normal use: control-plane/workspace.ts spawns workspace children with OPENCODE_AUTH_CONTENT: JSON.stringify(yield* auth.all()), so an auth login or an HTTP auth-set inside a child rewrote the host's file from the child's stale copy.

set/remove now read through a file-only read(). all() and get() are unchanged — the env override still wins on reads, which workspace credential propagation depends on.

3894ab2f20 — write atomically under a lock.

set/remove read the whole file, mutate one key, and rewrite it through FSUtil.writeJson, which is writeFileString + chmod — no temp+rename, no lock. Two consequences: concurrent writers to different providers lose one, and a crash mid-write truncates the whole credential file.

The read-modify-write now runs under EffectFlock.withLock("auth"), and the write goes through a new FSUtil.writeJsonAtomic (temp file + rename). EffectFlock is the existing cross-process lock (mkdir(2) atomic-create in the shared state dir, with stale detection and heartbeat) — an in-process mutex would not have fixed this, since the racing writers are separate processes. FSUtil.writeJson is untouched for its other callers.

This is the pattern already used elsewhere in the repo for weaker data. McpAuth wraps its mcp-auth.json read-modify-write in the same flock, and the TUI's scratch key-value store (context/kv.tsx) is both locked and atomic via writeJsonAtomic. The provider credential store was the one with neither.

The temp file is created with { flag: "wx", mode }, so 0600 is applied by open(2) at create rather than by a later chmod — no window where credentials are world-readable, and O_EXCL means a pre-planted symlink fails the write instead of redirecting it.

How did you verify your code works?

  • packages/opencode auth suite: 7 pass / 0 fail; packages/core filesystem suite: 28 pass / 0 fail; full packages/opencode suite 3398 pass / 0 fail
  • bun typecheck clean in packages/opencode and packages/core
  • Each new test was checked against a revert of its own fix. Removing only flock.withLock — keeping the atomic write and the file-only read — reddens exactly preserves both providers from concurrent writes (6 pass / 1 fail), so the lock is load-bearing rather than incidentally covered.
  • Concurrency is exercised with real interleaving: two Auth.set calls under Effect.all(..., { concurrency: "unbounded" }), where the async file read is the yield point that makes the race real.

Known limitation

If a process is SIGKILLed between the write and the rename, a .tmp file holding credentials is left behind. It is created 0600, so it is not world-readable, and Effect.ensuring removes it on any normal failure. A startup sweep for stale temps is deliberately not included here — a naive sweep could delete another live process's in-flight temp, so it wants its own design.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Auth mutations previously used the OPENCODE_AUTH_CONTENT read snapshot as the source for file updates, erasing credentials written after the snapshot was created. Read the auth file directly for set and remove while retaining the environment override for read-time all and get behavior.
Auth mutations previously read and rewrote the credential file without cross-process coordination, allowing concurrent providers to overwrite each other and exposing a crash window during truncating writes. Add a same-directory temporary-file rename writer with private permissions and serialize auth read-modify-write operations with the core EffectFlock service.
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential Duplicate Found

PR #46023: fix(opencode): serialize and atomically write auth.json
#46023

This PR appears to address the same issue as the current PR #46131. Both are focused on:

  • Writing auth.json atomically
  • Preventing concurrent write issues
  • Ensuring credentials are not lost

This is a strong duplicate candidate that should be reviewed before merging PR #46131. You may want to check if PR #46023 was already merged or closed, or consolidate the work between them.

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.

[BUG]: auth.json writes are unlocked whole-file rewrites — concurrent writers silently lose credentials

1 participant