Skip to content

fix(settings): retry section writes against latest disk state (R-LOGIC-01)#429

Merged
ndycode merged 4 commits intomainfrom
fix/unified-settings-stale-overwrite
Apr 18, 2026
Merged

fix(settings): retry section writes against latest disk state (R-LOGIC-01)#429
ndycode merged 4 commits intomainfrom
fix/unified-settings-stale-overwrite

Conversation

@ndycode
Copy link
Copy Markdown
Owner

@ndycode ndycode commented Apr 18, 2026

Addresses R-LOGIC-01 and R-TEST-01 from the deep runtime audit.

The async unified-settings save paths (saveUnifiedPluginConfig, saveUnifiedDashboardSettings) used a read-modify-write flow with only an in-process queue. If another process wrote a newer settings.json after the read but before the rename, the stale in-memory record would overwrite unrelated sections.

This PR adds an optimistic mtime check:

  • capture the observed settings mtime after read
  • check the current mtime immediately before rename
  • on mismatch (ESTALE), re-read the latest record and re-apply the section-scoped mutation
  • retry up to 3 times

Includes a regression test that injects an external write between temp-file creation and rename, and verifies the external writer's unrelated sections are preserved.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr patches the async read-modify-write paths in saveUnifiedPluginConfig and saveUnifiedDashboardSettings with an optimistic mtime check: capture mtime before the read, re-stat immediately before rename, and retry up to 3 times on mismatch (ESTALE). the mtime-before-read ordering is correct in both the initial path and the retry path, addressing the toctou concern raised previously. all remaining findings are p2.

Confidence Score: 5/5

safe to merge; prior toctou concern is resolved, remaining findings are p2 style/coverage gaps

the mtime-before-read ordering is correct in both initial and retry paths; estale sentinel is synthetic so no windows compat issue; only open items are a missing dashboard test and a fat32 mtime granularity edge case, both p2

test/unified-settings.test.ts — missing symmetric regression test for saveUnifiedDashboardSettings

Important Files Changed

Filename Overview
lib/unified-settings.ts adds optimistic mtime check before rename and 3-attempt retry loop to both async save paths; mtime-then-read order is correct; mtime granularity on fat32/network mounts is a latent windows risk
test/unified-settings.test.ts adds regression test for saveUnifiedPluginConfig stale-overwrite scenario; saveUnifiedDashboardSettings has no symmetric test despite identical retry logic

Sequence Diagram

sequenceDiagram
    participant Caller
    participant saveUnifiedPluginConfig
    participant fs

    Caller->>saveUnifiedPluginConfig: saveUnifiedPluginConfig(pluginConfig)
    saveUnifiedPluginConfig->>fs: stat(settings.json) → T1
    saveUnifiedPluginConfig->>fs: readFile(settings.json) → record
    loop attempt 0..2
        saveUnifiedPluginConfig->>saveUnifiedPluginConfig: merge pluginConfig into record
        saveUnifiedPluginConfig->>fs: writeFile(temp.tmp, merged)
        Note over fs: external process may write settings.json here (mtime → T2)
        saveUnifiedPluginConfig->>fs: stat(settings.json) → currentMtime
        alt currentMtime ≠ expectedMtime (ESTALE)
            saveUnifiedPluginConfig->>fs: unlink(temp.tmp)
            saveUnifiedPluginConfig->>fs: stat(settings.json) → T2
            saveUnifiedPluginConfig->>fs: readFile(settings.json) → fresh record
        else mtime matches
            saveUnifiedPluginConfig->>fs: rename(temp.tmp → settings.json)
            saveUnifiedPluginConfig-->>Caller: done
        end
    end
    saveUnifiedPluginConfig-->>Caller: throw ESTALE (after 3 failed attempts)
Loading

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: test/unified-settings.test.ts
Line: 739-824

Comment:
**missing regression test for `saveUnifiedDashboardSettings`**

`saveUnifiedDashboardSettings` received the same optimistic-retry block (lines 583–603 of `lib/unified-settings.ts`), but there is no corresponding regression test that injects a concurrent write and verifies unrelated sections are preserved. the existing test only exercises `saveUnifiedPluginConfig`. adding a symmetric test (same spy pattern, asserting `pluginConfig` is untouched after an external write between temp creation and rename) would complete R-TEST-01 coverage for both save paths.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: lib/unified-settings.ts
Line: 400-416

Comment:
**mtime granularity silently bypasses conflict detection on low-res filesystems**

`mtimeMs` precision depends on the underlying filesystem. fat32 rounds mtime to 2-second boundaries; some smb/nfs mounts round to 1 second. if an external process writes `settings.json` within the same granularity window as the captured `expectedMtimeMs`, `currentMtimeMs === options.expectedMtimeMs` holds and the stale-overwrite detection silently passes — exactly the lost-update this check is meant to prevent. this is a real windows filesystem risk (fat32 on removable drives, some mapped network drives). consider also comparing file size or a content hash as a secondary check, or documenting the known limitation in the function jsdoc.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (3): Last reviewed commit: "fix(settings): snapshot mtime before asy..." | Re-trigger Greptile

…C-01)

Addresses R-LOGIC-01 / R-TEST-01 from the deep runtime audit.

The async unified-settings save paths read the current settings record, mutate
one section, and write it back without checking whether another process wrote
a newer settings.json in the meantime. That creates a stale-overwrite race.

Fix: capture the observed settings mtime, check it immediately before rename
inside writeSettingsRecordAsync, and on ESTALE re-read the latest settings
record and re-apply the section-scoped mutation up to 3 times.

Added a regression test that injects a concurrent external write between the
read and write and verifies the unrelated sections from the external write are
preserved while the intended pluginConfig mutation still lands.
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 18, 2026

Warning

Rate limit exceeded

@ndycode has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 47 minutes and 51 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 47 minutes and 51 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: abeb92c9-2356-4e69-8601-e033869a78e5

📥 Commits

Reviewing files that changed from the base of the PR and between 3f1c1fe and e6de13d.

📒 Files selected for processing (2)
  • lib/unified-settings.ts
  • test/unified-settings.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/unified-settings-stale-overwrite
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/unified-settings-stale-overwrite

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread lib/unified-settings.ts
ndycode added 2 commits April 18, 2026 17:03
…429)

Responds to the PR #429 review comment about the optimistic mtime check.

The previous patch captured the expected mtime in a separate fs.stat() after the
read, leaving a TOCTOU window between reading the file contents and sampling
its metadata. readSettingsRecordAsyncInternal now uses the mtime from the same
open file handle used for the async read and threads that value through the
ESTALE retry loop. Regression test remains green.
…429)

Responds to the PR #429 review comment.

The optimistic mtime check was taking expectedMtimeMs after the async read,
leaving a window where an external write could update the file between the read
and the mtime snapshot. Swap the order so the expected mtime is sampled first,
then the record is read, and mirror that order on ESTALE retry.

This preserves the existing settings contract and test behavior while closing
the specific stale-overwrite hole called out in review.
@ndycode ndycode merged commit 045a914 into main Apr 18, 2026
1 of 2 checks passed
ndycode added a commit that referenced this pull request Apr 18, 2026
Merge the reviewed rollup fix branch after deep audit, full verification battery, and real pack/install smoke test.

This lands the integrated fix set from PRs #414-#429 and #431 via one reviewed branch, including the two rollup-only integration commits:
- 7f50455 fix(rollup): add missing numeric tracker cleanup helpers from PR #419
- 056ad18 test(rollup): restore auth-list empty-state expectation to current behavior

Rationale: the rollup branch was the only branch deep-audited end-to-end and pack/install smoke-tested as a complete artifact. Baseline failures in codex-bin-wrapper / benchmark-runtime-path-script already exist on main and are not regressions from this merge.
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.

1 participant