Skip to content

fix: preserve ENOTEMPTY error code in extension rename (fixes #323155)#323162

Open
vs-code-engineering[bot] wants to merge 1 commit into
mainfrom
fix/extension-rename-enotempty-323155-291a08943c61329d
Open

fix: preserve ENOTEMPTY error code in extension rename (fixes #323155)#323162
vs-code-engineering[bot] wants to merge 1 commit into
mainfrom
fix/extension-rename-enotempty-323155-291a08943c61329d

Conversation

@vs-code-engineering

Copy link
Copy Markdown
Contributor

Summary

extractUserExtension installs an extension by extracting the VSIX into a temporary directory and then renaming that temp directory to the final extension location. When two sources install the same extension concurrently (for example a built-in language pack being auto-updated while it is also installed by another path), the final directory already exists and the rename fails with ENOTEMPTY. The call site explicitly treats this as a benign race and swallows it — but the check error.code === 'ENOTEMPTY' never matched, so the benign race was re-thrown as an unhandled error into telemetry:

ENOTEMPTY: directory not empty, rename '.../extensions/.6de5c025-...' -> '.../extensions/ms-ceintl.vscode-language-pack-ja-1.126....'

The root cause is that the private rename() helper wraps every failure with toExtensionManagementError(error, ExtensionManagementErrorCode.Rename), which constructs a new ExtensionManagementError whose .code is set to the enum value 'Rename'. This clobbers the original 'ENOTEMPTY' code before the caller ever sees it, so the benign-race branch is effectively dead code.

Fixes #323155
Recommended reviewer: @sandy081

Culprit Commit

The error-code-clobbering bug is latent / pre-existing: both the benign-race handler in extractUserExtension and the wrapping rename() helper already exist unchanged in 7f503b81 and earlier, so neither was introduced inside the observed telemetry window.

What turned this latent bug into a high-volume bucket is 143b631d — "Enable auto-update for built-in extensions" (Sandeep Somavarapu / @sandy081, 2026-03-31), a confirmed ancestor of the shipped commit 7e7950df (1.126.0). Enabling auto-update for built-in extensions routes bundled language packs such as ms-ceintl.vscode-language-pack-ja through the same extract→rename path, dramatically increasing the concurrent-install race that produces the benign ENOTEMPTY. The bucket first appears in 1.119.0 (the first stable release carrying this feature) and persists through 1.126.0.

Later commits in the same feature line (7f503b81, 88e21a0 "handle edge cases while updating built in extensions") touched neighbouring update logic but did not repair the broken ENOTEMPTY detection.

Code Flow

flowchart TD
    A[extractUserExtension extracts VSIX to temp dir] --> B[calls rename temp to final]
    B --> C[pfs.Promises.rename]
    C -- final dir already exists --> D[throws raw error with code ENOTEMPTY]
    D --> E{rename helper catch block}
    E -- before fix --> F[wraps via toExtensionManagementError with code Rename]
    F --> G{caller catch checks for ENOTEMPTY}
    G -- no the code is now Rename --> H[else branch re-throws]
    H --> I[unhandled error reaches telemetry]
    E -- after fix --> J[re-throws raw ENOTEMPTY]
    J --> K{caller catch checks for ENOTEMPTY}
    K -- yes --> L[logs info deletes temp swallows benign race]
Loading

Affected Files

  • src/vs/platform/extensionManagement/node/extensionManagementService.ts — the ExtensionsScanner.rename() helper (producer of the mis-coded error) and the extractUserExtension() benign-race handler that consumes error.code. Modified.

Supporting context (not modified):

  • src/vs/platform/extensionManagement/common/abstractExtensionManagementService.tstoExtensionManagementError() constructs the wrapped error that dropped the original .code.
  • src/vs/platform/extensionManagement/common/extensionManagement.tsExtensionManagementError sets .code/.name to the enum value.
  • src/vs/base/node/pfs.tsrename/renameWithRetry retries only EACCES/EPERM/EBUSY, so ENOTEMPTY surfaces immediately with its raw code.

Repro Steps

  1. On a remote/server install, have a built-in language pack (for example ms-ceintl.vscode-language-pack-ja) eligible for auto-update.
  2. Cause the extension to be installed/updated by two sources at once (auto-update of the built-in plus another install path), so the final extension directory is created by one path while the other is mid-extract.
  3. The second path finishes extracting into its temp dir and calls rename(temp → final); the final directory already exists, so pfs.Promises.rename throws ENOTEMPTY.
  4. Before this fix, the helper rewraps the error as Rename, the caller's error.code === 'ENOTEMPTY' benign-race check fails, and the error is re-thrown into telemetry instead of being swallowed.

How the Fix Works

Chosen approachsrc/vs/platform/extensionManagement/node/extensionManagementService.ts, ExtensionsScanner.rename():

The mis-coded error object is produced inside rename() at the toExtensionManagementError(error, ...Rename) call — that is where a raw ENOTEMPTY is turned into an ExtensionManagementError whose .code becomes 'Rename'. The fix adds a guard at that producer, before the wrapping throw: if the caught error's code is ENOTEMPTY, re-throw it unchanged; otherwise keep the existing Rename wrapping. This restores the original author's intent — the caller's benign-race branch (error.code === 'ENOTEMPTY') becomes reachable again, so the concurrent-install race is logged and swallowed, while every genuine rename failure is still wrapped as Rename and its telemetry classification is preserved.

This follows the fix-at-the-data-producer principle rather than patching the crash site: the guard lives where the bad error object is created, not at the caller's catch where the symptom surfaces. No try/catch was added (the existing one is reused), no logService call was removed, and no genuine error is silenced — only the already-intended benign race is allowed to reach its existing handler.

Alternatives considered:

  • Matching the wrapped error by message text at the caller's catch — rejected: a consumer-side string-match hack at the crash site that does not address where .code is lost.
  • Making toExtensionManagementError() itself preserve ENOTEMPTY — rejected: it is a shared helper used by many unrelated callers, so special-casing one filesystem code there is too broad.

Recommended Owner

@sandy081 (Sandeep Somavarapu) — owns the extension management area, authored the built-in-extension auto-update feature that made this race hot, and is the author of essentially every recent change to extensionManagementService.ts.

Generated by errors-fix · 1.9K AIC · ⌖ 82.8 AIC · ⊞ 69.4K ·

The ENOTEMPTY benign-race handler in extractUserExtension was defeated because the private rename() helper wrapped every failure via toExtensionManagementError(..., Rename), clobbering error.code from 'ENOTEMPTY' to 'Rename'. Preserve the raw ENOTEMPTY error so the caller can detect the benign concurrent-install race; genuine failures are still wrapped for telemetry classification.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 26, 2026 16:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@vs-code-engineering vs-code-engineering Bot marked this pull request as ready for review June 26, 2026 16:31
@vs-code-engineering vs-code-engineering Bot enabled auto-merge (squash) June 26, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Error] unhandlederror-ENOTEMPTY: directory not empty, rename '<REDACTED: user-file-path>/.vscode-server/ext...

2 participants