You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The three characters each have 0x5C as their CP932 trail byte, and 0x5C is the path separator. So the UTF-8 decode does not shorten the path, it lengthens it by three segments. There are 52 such characters in CPython's CP932 double-byte table, and they are ordinary ones: 表 十 能 予 構 貼.
What it costs.parseRegQueryValue matches on \S+ for the name and takes the rest of the line as the value, so a mangled value parses cleanly. The caller in main.ts is:
constfromRegistry=readWindowsUserEnvVar('HERMES_HOME')if(fromRegistry){returnnormalizeHermesHomeRoot(fromRegistry)}// LOCALAPPDATA fallback below is never reached
A corrupted string is still truthy, so the desktop resolves HERMES_HOME to a directory that does not exist rather than falling through to %LOCALAPPDATA%\hermes. That is worse than the stale-snapshot gap this function was added to close (#45471): the fallback that would have produced a working home is skipped by a value that only looks valid. parent exists utf8 = false above is that outcome on a real host.
The fix. Node exposes no API for the host ANSI or OEM code page, so rather than guess at one:
Take reg's stdout as bytes (drop encoding: 'utf8').
If every byte is < 0x80, the code page and UTF-8 agree by definition — decode and return, unchanged behaviour and no extra spawn. This is the path essentially every host takes.
If any byte is >= 0x80, the value is in a code page we cannot name. Re-read it through PowerShell base64-encoded from UTF-8 inside the child, so the bytes on the wire are ASCII and no code page is involved:
That is the same trick ui-tui/src/lib/clipboard.ts and hermes_cli/clipboard.py already use for clipboard text, for the same reason.
The trigger is a fact about the bytes, not a judgement about damage. I deliberately did not detect this by looking for U+FFFD: I measured on #89468 that the replacement output is not stable across runtimes (29 replacements on .NET Framework 4.8, 30 on .NET 8 and CPython 3.11 for identical input), so replacement characters are not a sound signal.
The name is checked against /^[A-Za-z_][A-Za-z0-9_]*$/ before it is interpolated into the PowerShell string. Today the only caller passes a literal, but the function is exported.
Note on the linter.scripts/check-windows-footguns.py cannot see this file: should_scan_file() returns True only for .py, .pyw, .pyi. Every .ts under apps/desktop/electron/ is outside the gate that exists to catch exactly this class of bug.
Related Issue
No separate issue — reporting it here with the measurement. Same class as #89878 / #89907 / #90017 / #90046, on the Electron side of the same seam. The Python half of bounded_probe_run is #90046; this is the TypeScript half, and it is independent of that PR.
Type of Change
🐛 Bug fix (non-breaking change that fixes an issue)
✨ New feature (non-breaking change that adds functionality)
🔒 Security fix
📝 Documentation update
✅ Tests (adding or improving test coverage)
♻️ Refactor (no behavior change)
🎯 New skill (bundled or hub)
Changes Made
apps/desktop/electron/windows-user-env.ts — added isAsciiBytes(); readWindowsUserEnvVar() now captures reg stdout as bytes and only decodes it as UTF-8 when it is pure ASCII.
apps/desktop/electron/windows-user-env.ts — added readUserEnvVarAsBase64(), a PowerShell re-read that base64-encodes the UTF-8 bytes inside the child, used only on the non-ASCII path. Env-var names are validated before interpolation.
apps/desktop/electron/windows-user-env.test.ts — four tests added: the ASCII fast path spawns only reg; a CP932 value is re-read as base64 and returns the correct path, with the old five-backslash result pinned as an assertion; a failed re-read returns null; a name that is not a plain identifier never reaches PowerShell.
The backslash count goes from 2 to 5, and fs.statSync on the parent of the UTF-8 result throws. Output above is from that run.
The test file, run directly under node --test with the type annotations stripped — 14 pass (10 pre-existing, unchanged). I did not run it through the repo's vitest runner: the desktop workspace's dependencies are not installed on my host, so npm test -w apps/desktop is not something I can honestly claim to have run.
Against the pre-patch source, keeping the new tests: 2 failed / 12 passed. The other two pass either way on purpose — one pins that the ASCII path still spawns only reg so the common case cannot regress, the other pins the name-validation guard, which is new but not what the bug was.
Note on the checklist below: I left the full-suite box unchecked for the reason in 2.
AI code review — automated review for reference; please use your judgment.
apps/desktop/electron/windows-user-env.ts:readUserEnvVarAsBase64 — Nit: on a machine whose HERMES_HOME contains non-ASCII, every read now pays a cold PowerShell spawn (~150-400ms) inside startup paths. Fine as a correctness fallback, but worth remembering if boot-time profiling ever points here — the value rarely changes, so an in-process memo per name would make the second read free.
Same file + tests — Positive: this is the right fix shape for an unfixable decoding problem. Rather than guessing code pages (chcp, kernel32 APIs), it detects the ASCII-safe fast path byte-wise and routes everything else through a channel that carries its own encoding (PowerShell → UTF-8 → base64). The CP932 test case is chosen with real malice — trail bytes of 0x5C surviving a UTF-8 decode as literal backslashes is exactly the failure that makes the old bug insidious (a plausible path with extra separators instead of mojibake) — and pinning the old mangled output as an assertion documents the failure mode permanently. The SAFE_ENV_NAME guard with an injection-attempt test closes the obvious PowerShell interpolation hole, and failure degrades to null so callers keep their existing fallback behavior.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
comp/desktopElectron desktop app (apps/desktop/*)P2Medium — degraded but workaround existsplatform/windowsNative Windows-specific behavior or breakagesweeper:risk-platform-windowsSweeper risk: may break or behave differently on native Windowstype/bugSomething isn't working
3 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
readWindowsUserEnvVar()readsHERMES_HOMEout ofHKCU\Environmentand decodesreg.exe's stdout as UTF-8:reg.exeis a native console program; its stdout carries the machine code page. The value being read is a filesystem path.Measured on ja-JP Windows 11 (ACP=932), Node v24.17.0.
setx HERMES_PROBE "C:\十能予\hermes", then the samereg querycaptured as bytes:The three characters each have
0x5Cas their CP932 trail byte, and0x5Cis the path separator. So the UTF-8 decode does not shorten the path, it lengthens it by three segments. There are 52 such characters in CPython's CP932 double-byte table, and they are ordinary ones: 表 十 能 予 構 貼.What it costs.
parseRegQueryValuematches on\S+for the name and takes the rest of the line as the value, so a mangled value parses cleanly. The caller inmain.tsis:A corrupted string is still truthy, so the desktop resolves
HERMES_HOMEto a directory that does not exist rather than falling through to%LOCALAPPDATA%\hermes. That is worse than the stale-snapshot gap this function was added to close (#45471): the fallback that would have produced a working home is skipped by a value that only looks valid.parent exists utf8 = falseabove is that outcome on a real host.The fix. Node exposes no API for the host ANSI or OEM code page, so rather than guess at one:
reg's stdout as bytes (dropencoding: 'utf8').< 0x80, the code page and UTF-8 agree by definition — decode and return, unchanged behaviour and no extra spawn. This is the path essentially every host takes.>= 0x80, the value is in a code page we cannot name. Re-read it through PowerShell base64-encoded from UTF-8 inside the child, so the bytes on the wire are ASCII and no code page is involved:That is the same trick
ui-tui/src/lib/clipboard.tsandhermes_cli/clipboard.pyalready use for clipboard text, for the same reason.The trigger is a fact about the bytes, not a judgement about damage. I deliberately did not detect this by looking for
U+FFFD: I measured on #89468 that the replacement output is not stable across runtimes (29 replacements on .NET Framework 4.8, 30 on .NET 8 and CPython 3.11 for identical input), so replacement characters are not a sound signal.The name is checked against
/^[A-Za-z_][A-Za-z0-9_]*$/before it is interpolated into the PowerShell string. Today the only caller passes a literal, but the function is exported.Note on the linter.
scripts/check-windows-footguns.pycannot see this file:should_scan_file()returnsTrueonly for.py,.pyw,.pyi. Every.tsunderapps/desktop/electron/is outside the gate that exists to catch exactly this class of bug.Related Issue
No separate issue — reporting it here with the measurement. Same class as #89878 / #89907 / #90017 / #90046, on the Electron side of the same seam. The Python half of
bounded_probe_runis #90046; this is the TypeScript half, and it is independent of that PR.Type of Change
Changes Made
apps/desktop/electron/windows-user-env.ts— addedisAsciiBytes();readWindowsUserEnvVar()now capturesregstdout as bytes and only decodes it as UTF-8 when it is pure ASCII.apps/desktop/electron/windows-user-env.ts— addedreadUserEnvVarAsBase64(), a PowerShell re-read that base64-encodes the UTF-8 bytes inside the child, used only on the non-ASCII path. Env-var names are validated before interpolation.apps/desktop/electron/windows-user-env.test.ts— four tests added: the ASCII fast path spawns onlyreg; a CP932 value is re-read as base64 and returns the correct path, with the old five-backslash result pinned as an assertion; a failed re-read returnsnull; a name that is not a plain identifier never reaches PowerShell.How to Test
Then, in a new shell:
The backslash count goes from 2 to 5, and
fs.statSyncon the parent of the UTF-8 result throws. Output above is from that run.The test file, run directly under
node --testwith the type annotations stripped — 14 pass (10 pre-existing, unchanged). I did not run it through the repo's vitest runner: the desktop workspace's dependencies are not installed on my host, sonpm test -w apps/desktopis not something I can honestly claim to have run.Against the pre-patch source, keeping the new tests: 2 failed / 12 passed. The other two pass either way on purpose — one pins that the ASCII path still spawns only
regso the common case cannot regress, the other pins the name-validation guard, which is new but not what the bug was.Note on the checklist below: I left the full-suite box unchecked for the reason in 2.
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
I have a ja-JP (ACP=932) Windows host and can measure anything else on that locale.
(Measured on my host. Drafted with Claude.)