google-gemini / google-gemini/gemini-cli

bug: concurrent extension installs race past the existence check and corrupt each other's destination

Open
#29,036 1 comment 0 reactions 0 assignees View on GitHub
area/extensions effort/medium status/bot-triaged
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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.