Skip to content

fix: handle ids with multiple null bytes#22687

Open
SujalXplores wants to merge 1 commit into
vitejs:mainfrom
SujalXplores:fix/wrap-id-null-bytes
Open

fix: handle ids with multiple null bytes#22687
SujalXplores wants to merge 1 commit into
vitejs:mainfrom
SujalXplores:fix/wrap-id-null-bytes

Conversation

@SujalXplores

@SujalXplores SujalXplores commented Jun 16, 2026

Copy link
Copy Markdown

Fixes #22690

Description

wrapId and unwrapId (packages/vite/src/shared/utils.ts) and the Lightning CSS filename encoder (packages/vite/src/node/plugins/css.ts) encode/decode null bytes with String.prototype.replace and a string pattern, which only replaces the first occurrence.

When an id contains more than one null byte, only the first is handled. This happens when a virtual module is wrapped by another plugin, for example a CommonJS proxy around a virtual module produces an id like \0commonjs-proxy:\0virtual.

Consequences:

  • wrapId is documented to make an id URL-safe by encoding every \0 as __x00__ (see NULL_BYTE_PLACEHOLDER in shared/constants.ts), but the wrapped id still contained a raw null byte, which is not valid in an import URL.
  • unwrapId only restored the first placeholder, so decoding was not symmetric.
  • In css.ts the existing comment notes Lightning CSS treats a null byte as a string terminator, so a leftover null byte truncates the filename.

Fix

Use replaceAll in all three places so every null byte is encoded and decoded. replaceAll is already used widely in the codebase and is supported by the engines field (^20.19.0 || >=22.12.0).

Tests

Added packages/vite/src/shared/__tests__/utils.spec.ts covering wrapId/unwrapId, including ids with multiple null bytes. The multi-null-byte cases fail on main and pass with this change.

wrapId, unwrapId, and the Lightning CSS filename encoder used
String.prototype.replace with a string pattern, which only replaces the
first match. Ids that contain more than one null byte (for example a
CommonJS proxy wrapping a virtual module, \0commonjs-proxy:\0virtual)
were left with raw null bytes after the first one.

That breaks the documented contract that a wrapped id is a valid import
URL with every \0 encoded as __x00__, and Lightning CSS treats a raw
null byte as a string terminator.

Use replaceAll so every null byte is encoded and decoded symmetrically.
@SujalXplores

Copy link
Copy Markdown
Author

The failing job (Build&Test node-24, macos-latest) is a flaky beforeAll startup timeout in playground/import-assertion, unrelated to this change. The same commit passed on the other 5 matrix configs and all unit tests. A re-run should clear it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wrapId and unwrapId only encode the first null byte

1 participant