google-gemini / google-gemini/gemini-cli
bug: concurrent extension installs race past the existence check and corrupt each other's destination
- Dominant language
- TypeScript
- Stars
- 107k
- Forks
- 14.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 45
Description
## What happened?
`installOrUpdateExtension()` performs its duplicate-name existence check, then has multiple `await` points before writing into the destination. Two concurrent installs of the same extension (two terminals; or `update --all`, which fans out with `Promise.all` in `update.ts`) both pass the check and interleave writes into the same directory, corrupting files/metadata; the loser typically only fails later at `loadExtension()` ("already loaded") — after having clobbered the winner's contents on disk.
## Affected code
`packages/cli/src/config/extension-manager.ts:358-365` (check):
```ts
if (
(!isUpdate || newExtensionName !== previousName) &&
fs.existsSync(destinationPath)
) {
throw new Error(`Cannot install extension "${newExtensionName}" because a directory with that name already exists. ...`);
}
```
…then, much later:
```ts
await fs.promises.mkdir(destinationPath, { recursive: true }); // line ~393
// ...
await copyExtension(localSourcePath, destinationPath); // line ~434
// ...
await fs.promises.writeFile(metadataPath, metadataString); // line ~442
extension = await this.loadExtension(destinationPath); // dup name detected here, too late
```
## How can this be reproduced?
1. In two terminals simultaneously run `gemini extensions install `.
2. Both pass the `existsSync` guard; interleaved `cp`/metadata writes race.
3. One process fails at load-time while both have written to the same tree — final on-disk state can mix files from both attempts.
## What did you expect to happen?
Exactly one install succeeds atomically; the other fails fast with the friendly "already exists" error and touches nothing.
## Suggested direction
Acquire an exclusive lock around install/update (e.g., `fs.open(lockPath, 'wx')` lockfile next to the extensions dir with stale-lock timeout), or perform the copy into a unique staging dir and swap via atomic rename, re-checking the destination inside the critical section.
---
*Found by source audit on current `main` (commit `5411f113c`); platform-independent. No open issue/PR covering this was found (searched: concurrent extension install).*
Contributor guide
Research direction
Start in packages/cli/src/config/extension-manager.ts at installOrUpdateExtension(), then inspect update.ts to understand the Promise.all fan-out. Reproduce two simultaneous installs of the same repository and trace the existence check through mkdir, copyExtension, metadata writing, and loadExtension. Done means one install succeeds atomically, the other fails with the existing-directory error, and the failed attempt leaves the destination untouched.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100