init() (output.rs lines 561 to 567) and restore() (output.rs lines 517 to 524). Evidence, the code page while Bun runs: 932 / 437 (T1)console.log output, but Bun still switches the console to 65001 while it runs (932 / 437), a forced kill still leaves 65001 (932 / 437), fs.writeSync(1), fs.write(1) and Bun.write(Bun.stdout) are still garbled (932 / 437), and restoring still loses a race now and then when the order is not fixed (timeline)Request (bun#44693): do not change the console code page at all, and write all console output with WriteConsoleW. Every verdict was the same whether the console started at 932 (Japanese Windows) or 437 (English Windows). The details follow in Conclusion; how the tests were run is in Conditions.
Bun switches the console input and output code pages to 65001 the moment it starts (before any script code runs), and on exit restores "the value it saved at start". This alone causes every harm H1 to H7 in the plan. Node.js and Deno never changed the code page and behaved correctly in every case.
The rows are in order of importance: the harms that remain even with PR #43662 come first.
| Harm | Description | Bun, canary | Bun PR #43662 | Node.js, Deno | Evidence (932 / 437) |
|---|---|---|---|---|---|
| H4 | Other processes in the same console are affected | ❌ happens | ❌ still happens: the console is at 65001 while Bun runs, and after a forced kill | ✅ never | screen / screen (T4: the neighbor script is garbled and chcp shows 65001); PR: log / log (T1, 65001 while running) |
| H6 | A forced kill leaves the code page changed | ❌ happens | ❌ still happens | ✅ never | log / log (T3); PR: screen / screen |
| H1 | The output of a piped peer is garbled | ❌ happens | ⚠️ fixed for console.log and process.stdout; still happens with fs.writeSync(1), fs.write(1) and Bun.write(Bun.stdout) | ✅ never | screen / screen (T5 ww); PR: screen / screen (T5 ww with fs.writeSync) |
| H5 | After the code page is put back to the starting value, Bun writes raw UTF-8 bytes and they are garbled | ❌ happens | ⚠️ same as H1 | ✅ never | log / log (T5 ww: the reader's code page right before writing is the starting value) |
| H3 | 65001 is left on the console after exit | ❌ happens | ⚠️ fixed in every fixed order, but with no fixed order the input or the output code page is now and then left at 65001 | ✅ never | log / log (T2, cmd, bun → bun); PR: log (932, run 19 with the watcher: after exit input 932, output 65001); counts in the table of the timeline |
| H2 | The result is decided by the start and exit order, so it changes from run to run | ❌ happens | ⚠️ with no fixed order the screen was always correct, but 65001 is now and then left (H3) | ✅ never | T5, same shape as bun#43660: the four fixed orders in T5 give four different outcomes |
| H7 | It happens even when the two Bun processes are not directly connected | ❌ happens | ✅ fixed | ✅ never | screen / screen (T5 relay, bun | node | bun) |
PR #43662 writes console output with WriteConsoleW through bun_sys::write, and at exit restores a code page only if the saved value is not 65001. Its Windows x64 CI build was run through the same suite. It fixes what bun#43660 reported, but it keeps switching the console to 65001 at startup, so the harms that come from changing shared state remain:
fs.writeSync(1), fs.write(1) and Bun.write(Bun.stdout) still write with WriteFile, so in the ww order their output is garbled (T5), as the PR description and its review saySetConsoleOutputCP and SetConsoleCP at startup when the console is already at 65001. A console at 437 or 932 is still switched to 65001, so the results in this document were measured on the build before this commit, and the request in bun#44693 is not met by it. We re-ran the full set at 932 and 437 on the build with this commit (head commit 6b5271cb1): the verdicts for the PR build did not differ from those of 576eb251a in a single case. The runs with no fixed order (asis) printed correctly and restored the code page in all 60 runs at each of 932 and 437. The harms that remain after the PR, such as 65001 staying after a forced kill, are unchangedThe data flowing through the pipe itself was never corrupted, in any combination (T2). Every harm comes from rewriting shared state: the code page of the console. The stable and canary builds gave the same verdicts in every fixed case (T6), and every fixed verdict was the same at 932 and 437.
Named in robobun's reply on PR #43662. Both were open and not merged on 2026-10-10.
ReadConsoleW instead of ReadFile. Today the REPL, prompt(), alert(), confirm(), bun init, bun update -i and bun - read the console through ReadFile, which converts input with the console input code page, and on the Windows 10 / Server 2019 conhost the 65001 conversion breaks non-ASCII input. process.stdin, Bun.stdin and fs.readSync(0) go through libuv and are not affected. robobun says this PR must come before Bun can stop setting 65001 at startup, because the input side currently depends on it. The PR itself says "the ANSI conversion is the broken piece, not the code page"src/io/windows/. robobun says that after it, the write hooks for fs.writeSync, fs.write and Bun.write can go into src/sys/windows/fs.rs::write_at and cover them all. Its list of fixed bugs includes "process.stdout.write was garbled once another process restored the code page (console.log still is)", which matches the harm measured hereinit() (output.rs lines 561 to 567) saves GetConsoleOutputCP / GetConsoleCP and calls SetConsoleOutputCP(65001) / SetConsoleCP(65001)restore() (output.rs lines 517 to 524) puts the saved values back at exitThe links point to the current main. The timeline in T5 matches this code exactly: each process saves the value it sees at start, so the second process saves 65001.
A single process runs in the console and prints abc 東京大阪 xyz. Values are input/output.
Test code: T1 (codepage.test.ts lines 35 to 51)
| Runtime (start) | Right after start | Right before writing | Right before exit | After exit | Screen |
|---|---|---|---|---|---|
| bun 1.4.3 (932) | ❌ 65001/65001 | ❌ 65001/65001 | ❌ 65001/65001 | ✅ 932/932 | ✅ |
| bun 1.4.3 (437) | ❌ 65001/65001 | ❌ 65001/65001 | ❌ 65001/65001 | ✅ 437/437 | ✅ |
| bun canary (932) | ❌ 65001/65001 | ❌ 65001/65001 | ❌ 65001/65001 | ✅ 932/932 | ✅ |
| bun canary (437) | ❌ 65001/65001 | ❌ 65001/65001 | ❌ 65001/65001 | ✅ 437/437 | ✅ |
| bun PR #43662 (932) | ❌ 65001/65001 | ❌ 65001/65001 | ❌ 65001/65001 | ✅ 932/932 | ✅ |
| bun PR #43662 (437) | ❌ 65001/65001 | ❌ 65001/65001 | ❌ 65001/65001 | ✅ 437/437 | ✅ |
| node (932) | ✅ 932/932 | ✅ 932/932 | ✅ 932/932 | ✅ 932/932 | ✅ |
| node (437) | ✅ 437/437 | ✅ 437/437 | ✅ 437/437 | ✅ 437/437 | ✅ |
| deno (932) | ✅ 932/932 | ✅ 932/932 | ✅ 932/932 | ✅ 932/932 | ✅ |
| deno (437) | ✅ 437/437 | ✅ 437/437 | ✅ 437/437 | ✅ 437/437 | ✅ |
The writer and the reader start together, and the reader writes the received bytes to a file. Each cell is "bytes, code page after exit", for a console started at 932 and at 437.
Test code: T2 (codepage.test.ts lines 53 to 81)
The command for each launcher is built here: lines 58 to 61
| Writer → reader | cmd | pwsh 7 | Windows PowerShell 5.1 |
|---|---|---|---|
| bun → bun | 932: ✅ ❌ 437: ✅ ❌ | 932: ✅ ❌ 437: ✅ ❌ | 932: ✅ ✅ 437: ✅ ✅ |
| bun → node | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| bun → deno | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| canary → canary | 932: ✅ ❌ 437: ✅ ❌ | 932: ✅ ❌ 437: ✅ ❌ | 932: ✅ ✅ 437: ✅ ✅ |
| canary → node | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| canary → deno | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| pr43662 → pr43662 | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| pr43662 → node | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| pr43662 → deno | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| node → bun | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| node → canary | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| node → pr43662 | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| node → node | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| node → deno | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| deno → bun | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| deno → canary | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| deno → pr43662 | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| deno → node | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
| deno → deno | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ | 932: ✅ ✅ 437: ✅ ✅ |
? in every combination (also for node | node; it is 5.1's own behavior). In the 5.1 column, the same bytes as node | node count as ✅. With 5.1 in between, 65001 was not left, because 5.1 does not run the two Bun processes at the same time. Running the same pipe through 5.1 20 times with the watcher, the reader started only after the writer had exited, in all 20 runs (11 to 22 ms later; watch.ndjson of run 1, results.jsonl). With no overlap, neither process can save the other's 65001A single process that has written to the screen and is waiting is stopped with taskkill /F.
Test code: T3 (codepage.test.ts lines 83 to 102)
| Runtime (start) | Code page after | chcp output | Neighbor process |
|---|---|---|---|
| bun 1.4.3 (932) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| bun 1.4.3 (437) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| bun canary (932) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| bun canary (437) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| bun PR #43662 (932) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| bun PR #43662 (437) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| node (932) | ✅ 932/932 | ✅ 現在のコード ページ: 932 | ✅ |
| node (437) | ✅ 437/437 | ✅ Active code page: 437 | ✅ |
| deno (932) | ✅ 932/932 | ✅ 現在のコード ページ: 932 | ✅ |
| deno (437) | ✅ 437/437 | ✅ Active code page: 437 | ✅ |
Because Bun restores the code page on exit, a forced kill leaves 65001 behind (H6). A cmd script that runs in the same console afterwards misreads its own source as UTF-8 and is garbled (H4).
The console after bun | bun exits normally (the lifetimes overlap and the writer exits first).
Test code: T4 (codepage.test.ts lines 104 to 121)
| Runtime (start) | Code page after | chcp output | Neighbor process |
|---|---|---|---|
| bun 1.4.3 (932) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| bun 1.4.3 (437) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| bun canary (932) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| bun canary (437) | ❌ 65001/65001 | ❌ Active code page: 65001 | ❌ |
| bun PR #43662 (932) | ✅ 932/932 | ✅ 現在のコード ページ: 932 | ✅ |
| bun PR #43662 (437) | ✅ 437/437 | ✅ Active code page: 437 | ✅ |
| node (932) | ✅ 932/932 | ✅ 現在のコード ページ: 932 | ✅ |
| node (437) | ✅ 437/437 | ✅ Active code page: 437 | ✅ |
| deno (932) | ✅ 932/932 | ✅ 現在のコード ページ: 932 | ✅ |
| deno (437) | ✅ 437/437 | ✅ Active code page: 437 | ✅ |
The screen with Bun, at 932 and at 437:
abc 譚ア莠ャ螟ァ髦ェ xyz abc µ¥▒Σ║¼σñºΘÿ¬ xyz
--- tail --- --- tail ---
Active code page: 65001 Active code page: 65001
neighbor: ������� neighbor: caf�
The same with Node.js, at 932 and at 437:
abc 東京大阪 xyz abc 東京大阪 xyz
--- tail --- --- tail ---
現在のコード ページ: 932 Active code page: 437
neighbor: 東京大阪 neighbor: café
現在のコード ページ is the Japanese form of Active code page; on Japanese Windows it switches to English when the output code page is 65001. With both Bun builds, even without a forced kill, the neighbor script and chcp in the same console were affected (H4).
The second process starts about one second later, and the exit order is decided by delays. The two lifetimes always overlap. Each combination runs three times. Cells are "screen or bytes / starting code page after exit", for a console started at 932 and at 437. The prediction for each order is in T5 of the plan: ww garbled with 65001 left, wr and rw correct, rr correct with 65001 left.
Test code: T5 (codepage.test.ts lines 123 to 210)
| Shape and order | bun 1.4.3 | bun canary | bun PR #43662 | node | deno |
|---|---|---|---|---|---|
| Writer starts first and exits first (ww) reader writes to the screen | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ |
| Writer starts first and exits first (ww) reader writes to a file | 932: ✅✅✅ / ❌❌❌ 437: ✅✅✅ / ❌❌❌ | 932: ✅✅✅ / ❌❌❌ 437: ✅✅✅ / ❌❌❌ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ |
| Writer starts first, reader exits first (wr) reader writes to the screen | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ |
| Writer starts first, reader exits first (wr) reader writes to a file | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ |
| Reader starts first, writer exits first (rw) reader writes to the screen | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ |
| Reader starts first, writer exits first (rw) reader writes to a file | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ |
| Reader starts first and exits first (rr) reader writes to the screen | 932: ✅✅✅ / ❌❌❌ 437: ✅✅✅ / ❌❌❌ | 932: ✅✅✅ / ❌❌❌ 437: ✅✅✅ / ❌❌❌ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ |
| Reader starts first and exits first (rr) reader writes to a file | 932: ✅✅✅ / ❌❌❌ 437: ✅✅✅ / ❌❌❌ | 932: ✅✅✅ / ❌❌❌ 437: ✅✅✅ / ❌❌❌ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ |
| Node.js in the middle; writer starts first and exits first (relay) reader writes to the screen | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ | 932: ✅✅✅ / ✅✅✅ 437: ✅✅✅ / ✅✅✅ |
| ww, reader writes with fs.writeSync(1) (ww-fswritesync) reader writes to the screen | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ❌❌❌ / ✅✅✅ 437: ❌❌❌ / ✅✅✅ | 932: — 437: — | 932: — 437: — |
| ww, reader writes with fs.write(1) (ww-fswrite) reader writes to the screen | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ❌❌❌ / ✅✅✅ 437: ❌❌❌ / ✅✅✅ | 932: — 437: — | 932: — 437: — |
| ww, reader writes with Bun.write(Bun.stdout) (ww-bunwrite) reader writes to the screen | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ❌❌❌ / ❌❌❌ 437: ❌❌❌ / ❌❌❌ | 932: ❌❌❌ / ✅✅✅ 437: ❌❌❌ / ✅✅✅ | 932: — 437: — | 932: — 437: — |
For Bun 1.4.3 and canary, every combination matched the prediction of the plan, and all three runs were identical, at both 932 and 437. Once the order is fixed, the result is fixed. The build of PR #43662 was correct in every order when the reader writes with process.stdout, but the last three rows, where the reader writes the same text with fs.writeSync(1), fs.write(1) or Bun.write(Bun.stdout), are garbled on every build: those APIs still use WriteFile. These rows run with Bun builds only (Bun.write exists only in Bun).
From this log. Values are input/output. w is the writer, r the reader, probe the observer Node.js process. The run at 437 gives the same picture with 437 in place of 932 (log).
0ms probe before 932/932
45ms w after start 65001/65001 ← the writer saves 932 and sets 65001
47ms w before writing 65001/65001
1093ms r after start 65001/65001 ← the reader saves 65001 as the "original" value
2050ms w before exit 65001/65001 → the writer exits and restores the saved 932
2854ms r before writing 932/932 ← the reader writes UTF-8 bytes to a 932 console (garbled)
2860ms r before exit 932/932 → the reader exits and restores the saved 65001
2989ms probe after 65001/65001 ← 65001 is left behind
WriteConsoleW, it would have printed correctly whatever the code page (Node.js and Deno print correctly at 932 and 437)The same shape as bun#43660: both processes start together, with no fixed order, ten times (60 times for the build of PR #43662). The text is abc 東京大阪 xyz, passed with \u escapes (deno eval for Deno).
bun -e "console.log('abc \u6771\u4eac\u5927\u962a xyz')" | bun -e "process.stdin.pipe(process.stdout)"
Test code: same shape as bun#43660 (codepage.test.ts lines 140 to 161)
| Runtime | Start 932: screen / after | Start 437: screen / after |
|---|---|---|
| bun 1.4.3 | ❌❌❌❌❌✅✅❌❌❌ / ❌❌❌❌❌✅✅❌❌❌ garbled 8 of 10 | ❌❌❌❌❌❌❌❌❌✅ / ❌❌❌❌❌❌❌❌❌✅ garbled 9 of 10 |
| bun canary | ❌❌❌❌❌❌❌❌❌❌ / ❌❌❌❌❌❌❌❌❌❌ garbled 10 of 10 | ❌❌❌❌❌❌✅❌❌❌ / ❌❌❌❌❌❌✅❌❌❌ garbled 9 of 10 |
| bun PR #43662 | ✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ garbled 0 of 60 | ✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ garbled 0 of 60 |
| node | ✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅ garbled 0 of 10 | ✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅ garbled 0 of 10 |
| deno | ✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅ garbled 0 of 10 | ✅✅✅✅✅✅✅✅✅✅ / ✅✅✅✅✅✅✅✅✅✅ garbled 0 of 10 |
bun | bun), though, and it is not a proof for every orderTo see which processes were on the console when the code page was left changed, the same command was run 60 more times with the build of PR #43662, starting at 932. A Node.js watcher was attached to the same console the whole time. It polled GetConsoleProcessList, GetConsoleCP and GetConsoleOutputCP continuously and logged every attach, every detach and every code page change with a UTC time (watch.ndjson). Node.js does not change the code page (T1).
The same shape was run several times, with and without the watcher. How often the code page was not restored changes a lot from day to day. Every series is listed, including those where it never happened.
| Date run (UTC) | Watcher | Runs | Not restored | Evidence |
|---|---|---|---|---|
| 2026-10-07 | no (full suite) | 20 | ❌ 1 (932 run 6, output side) | log.jsonl (at that commit) |
| 2026-10-10 | yes | 60 | ❌ 3 (runs 19, 45, 51) | results.jsonl |
| 2026-10-10 | no (full suite) | 120 | ✅ 0 (60 each at 932 and 437) | 932 / 437 |
| 2026-10-11 | yes | 20 | ❌ 1 (run 9, output side) | watch.ndjson, results.jsonl |
| 2026-10-11 | yes | 60 | ✅ 0 | results.jsonl |
| 2026-10-11 | yes (started at 437) | 60 | ❌ 2 (runs 48, 55, input) | results.jsonl |
| 2026-10-11 | yes + per-call trace | 40 | ❌ 3 (runs 6, 15, 21, output) | trace below |
Starting at 437 left the code page behind in the same way. Both times it was the input side, the same shape as runs 45 and 51 of 2026-10-10.
Run 9 on 2026-10-11 had the same shape as run 19 below: R started 7 ms after W set 65001, and after exit the console was left at input 932, output 65001. What follows analyzes the 60 runs of 2026-10-10.
| Run | After exit (in / out) | Processes on the console | Timeline |
|---|---|---|---|
| 19 | ❌ 932 / 65001 | cmd.exe running the batch file, the Node.js probes before and after, chcp and the two bun processes. No other process | watch.ndjson |
| 45 | ❌ 65001 / 932 | watch.ndjson | |
| 51 | ❌ 65001 / 932 | watch.ndjson |
The other 57 runs returned to 932 / 932 (results.jsonl, environment.json). The three runs, the bun lines only (UTC). W is the writer bun -e "console.log(...)" and R is the reader bun -e "process.stdin.pipe(process.stdout)":
run 19
13:39:12.205 attach W
13:39:12.208 codepage in 932 / out 65001
13:39:12.209 codepage in 65001 / out 65001
13:39:12.216 attach R
13:39:12.223 codepage in 932 / out 65001
13:39:12.226 detach W
13:39:12.226 codepage in 65001 / out 65001
13:39:12.263 codepage in 932 / out 65001
13:39:12.266 detach R
(left at in 932 / out 65001)
run 45
13:39:50.176 attach W
13:39:50.179 codepage in 932 / out 65001
13:39:50.181 codepage in 65001 / out 65001
13:39:50.189 attach R
13:39:50.192 codepage in 65001 / out 932
13:39:50.194 detach W
13:39:50.227 detach R
(left at in 65001 / out 932)
run 51
13:39:58.957 attach W
13:39:58.959 codepage in 932 / out 65001
13:39:58.960 codepage in 65001 / out 65001
13:39:58.970 attach R
13:39:58.974 codepage in 65001 / out 932
13:39:58.975 detach W
13:39:59.011 detach R
(left at in 65001 / out 932)
The tested build is the PR's head commit 576eb251a. Its output.rs is the only place that changes the code page:
init() (output.rs lines 581 to 585) runs four separate calls: save the output code page, set it to 65001, save the input code page, set it to 65001restore() (output.rs lines 536 to 544) sets the output and then the input back to the saved value, but skips a side whose saved value is 65001With only W and R calling these, each final state has a single order that produces it:
Up to here this is a deduction from the source and the timestamps: the watcher does not see which process made each call, and the step between R's save and R's set is shorter than its sampling interval (in run 19 the output going to 932 and back is not in the log). So the next step recorded each call.
W and R each ran under cdb (WinDbg 1.2610), with breakpoints on GetConsoleOutputCP, SetConsoleOutputCP, GetConsoleCP and SetConsoleCP in kernelbase. Each hit recorded the process, the value (the argument of Set, the return value of Get) and the caller address, then continued (trace-writer.ndjson, trace-reader.ndjson). The watcher ran as well. Of 40 runs from 932, 3 left the output code page at 65001.
Run 6 (UTC). W is pid 30744, R is pid 55480:
21:30:27.486 W GetConsoleOutputCP -> 932 (saves 932)
21:30:27.500 W SetConsoleOutputCP(65001)
21:30:27.509 W GetConsoleCP -> 932
21:30:27.511 W SetConsoleCP(65001)
21:30:27.514 R GetConsoleOutputCP -> 65001 (saves 65001)
21:30:27.525 W SetConsoleOutputCP(932) (W restores output)
21:30:27.527 W SetConsoleCP(932) (W restores input)
21:30:27.528 R SetConsoleOutputCP(65001)
21:30:27.537 R GetConsoleCP -> 932
21:30:27.539 R SetConsoleCP(65001)
21:30:27.594 R SetConsoleCP(932) (R restores input only)
(left at in 932 / out 65001)
…989ceb, …989cfc, …989d01, …989d12, the output restore from …989b72 and the input restore from …975511. Both processes go through the same init and the same restoreGetConsoleOutputCP returned 65001 and R did not restore the output. In 14 of the 37 runs that came back, one process had also saved the other's 65001, but the other's restore came later, so the code page came backThe debugger stops bun at every call, so the rate of lost races cannot be compared with runs without it. Each of the two debuggers records its own times, so the order between the two processes can be off by a few ms (in run 21, going by the recorded times alone, R saved after W's restore). The values (what each process saved) are not affected by this. The debugger prints its banner before the script hides its output, so W's banner goes into the pipe and R prints it on the screen (screen.json).
Evidence (run 6): trace-writer.ndjson, trace-reader.ndjson, watch.ndjson. All 40 runs: results.jsonl. Code: tools/50_run/asis-timeline.ts --trace, tools/50_run/cdb-trace.js, summary by tools/50_run/summarize-trace.ts
Code: the watcher (watch.ts) and the runner (asis-timeline.ts)
| Item | Value |
|---|---|
| OS | Windows 11 Home 10.0.26300 (Japanese) |
| Console | A new console is created with conhost.exe for every case. Its starting code page is set to 932 or 437 with chcp inside that dedicated console, which nothing else shares. The tests never change the code page after that |
| Bun | 1.4.3+c6da4a4d3 |
| Bun canary | 1.4.3-canary.1+bbdc5a519 (the latest at setup time) |
| Bun PR #43662 | 1.4.3-canary.1+6b5271cb1: the Windows x64 CI build of PR #43662, the open fix for bun#43660 (head commit 6b5271cb1, Buildkite build 126042). Earlier results were measured on 576eb251a (Buildkite build 119166). The timeline of every process in the console and the cdb records after it remain those of 576eb251a |
| Node.js | v26.10.0 |
| Deno | 2.9.7 |
| pwsh / Windows PowerShell | 7.6.6 / 5.1.26100.9549 |
| Text sent | abc 東京大阪 xyz ("Tokyo Osaka"). Garbled it becomes abc 譚ア莠ャ螟ァ髦ェ xyz at 932 and abc µ¥▒Σ║¼σñºΘÿ¬ xyz at 437 |
| How to run | tools\10_setup\setup-runtimes.cmd downloads the runtimes above from their official sources into _bin\, and tools\40_test\run-tests.cmd puts them first on PATH and runs everything. Nothing installed on the PC is used |
GetConsoleCP / GetConsoleOutputCP right after start, right before writing and right before exit. Before and after each case, an observer Node.js process reads them (T1 shows that Node.js does not change the code page)ReadConsoleOutputCharacterW. Nobody has to look at the screenchcp and a neighbor cmd script run in the same console. The script is saved in the starting code page (Shift_JIS neighbor: 東京大阪 for 932, CP437 neighbor: café for 437)✅ as expected (same as the expected behavior) / ❌ not as expected. Every ✅ and ❌ in the tables below is a link to the evidence file of that case: the screen (screen.json), the log with the code pages (log.jsonl) or the received bytes (out.bin).
All files are in research/evidence/, one folder per case: cp<code page>/<bun or bun-canary>/<runner>/<case>/. Each case folder holds the batch file it ran (run.cmd), the log, the screen and, if any, the received bytes. The logs hold no date and no pid, so two runs can be compared with diff. The run time and the tool versions are only in the environment.json of each pass:
All links point to commit 5b9f56b, so the line numbers do not move.
%~dp0bun:ffi, koffi and Deno.dlopen, read only86007f8, which added the choice of write API) and the relay (hold.ts)