google-gemini / google-gemini/gemini-cli

bug(tools): concurrent file writes suffer lost-update race (no atomic write or per-path locking)

Open
#29,078 3 comments 0 reactions 0 assignees View on GitHub
area/core effort/large kind/bug priority/p1 status/manual-triage
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.