google-gemini / google-gemini/gemini-cli
bug(tools): concurrent file writes suffer lost-update race (no atomic write or per-path locking)
- Dominant language
- TypeScript
- Stars
- 107k
- Forks
- 14.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 45
Description
### What happened?
In `packages/core/src/tools/write-file.ts` and `packages/core/src/tools/edit.ts` (main @ 812f7a2bc), file write operations are not atomic and suffer from race conditions when multiple tool calls target the same file concurrently (common with parallel agent execution):
`write-file.ts` (approx lines 80-120):
```ts
async execute({ abortSignal }): Promise {
const content = this.params.content;
const filePath = path.resolve(this.config.getTargetDir(), this.params.file_path);
// No file lock, no atomic write
await fs.promises.writeFile(filePath, content, 'utf-8');
return { llmContent: `Wrote ${content.length} chars to ${filePath}` };
}
```
`edit.ts` (approx lines 90-150):
```ts
async execute({ abortSignal }): Promise {
const fileContent = await fs.promises.readFile(filePath, 'utf-8');
const newContent = fileContent.replace(oldString, newString);
await fs.promises.writeFile(filePath, newContent, 'utf-8');
}
```
Problems:
1. **No atomicity**: `writeFile` followed by immediate `readFile` in another tool call can read partially written content (especially on large files, though `writeFile` is atomic on POSIX for small files, not guaranteed on Windows or for >4k writes)
2. **Lost update**: Two concurrent `edit` calls on same file: both read original content `v0`, both compute `v1` and `v2` based on `v0`, then both write — last writer wins, first edit lost. No optimistic locking or version check.
3. **No file locking**: Parallel sub-agents (enabled via `Kind.Agent` tools) can and do write to overlapping files (e.g., both updating `README.md` or `package.json`)
4. **Checkpoint race**: `GitService.createFileSnapshot` does `repo.add('.')` then `repo.commit` without locking — concurrent writes during snapshot can be partially committed
This is not theoretical: with `agents` mode enabled, the scheduler runs tools in parallel, and the UI's `useGeminiStream` explicitly handles parallel agent tool calls (see `fix(core): preserve empty text turns` #28892).
### What did you expect to happen?
- Use atomic write pattern: `writeFile(tmpPath) + rename(tmpPath, filePath)` (rename is atomic on POSIX)
- For `edit`, implement optimistic concurrency: read current content, compute hash, and verify file hasn't changed between read and write (or use `fs.open` with `O_EXCL` / file lock via `proper-lockfile`)
- Or, serialize file writes per-file-path via a per-path mutex in the scheduler
- For `GitService`, acquire a lock or queue snapshot creation
### Client information
- Source-level finding verified against upstream `main` at commit `812f7a2bc`
- Files: `packages/core/src/tools/write-file.ts`, `packages/core/src/tools/edit.ts`, `packages/core/src/services/gitService.ts`
- Affects all platforms, especially with `agents` mode / parallel tool execution enabled
### Login information
Not applicable.
### Anything else we need to know?
Sources:
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/core/src/tools/write-file.ts
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/core/src/tools/edit.ts
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/core/src/services/gitService.ts
**Repro:**
1. Enable agents mode, prompt: "Create two sub-agents, each append a unique line to the same file `test.txt`"
2. Observe `test.txt` contains only one of the two lines (lost update)
3. Check `git log` for checkpoint snapshots — may contain partial writes
Searched existing issues for "concurrent write", "file lock", "TOCTOU", "lost update" — no open duplicate found.
Contributor guide
Research direction
Start by reproducing the parallel-agent scenario against packages/core/src/tools/write-file.ts and packages/core/src/tools/edit.ts, then inspect packages/core/src/services/gitService.ts for checkpoint behavior. Compare the concurrent file-write and snapshot paths against the proposed atomicity or serialization options. Done means concurrent edits do not lose updates and snapshots do not capture partial writes across the affected platforms.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, typescript
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100