[macOS][Desktop 26.901.20858] Renderer freezes at 100% CPU restoring a Go file editor tab: catastrophic backtracking in bundled shiki Go grammar (fixed in @shikijs/langs 4.0.2)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What version of the Codex App are you using (From “About Codex” dialog)?
26.901.20858 (build 7658)
What subscription do you have?
ChatGPT Pro
What platform is your computer?
Darwin 25.3.0 arm64 arm (macOS, Apple Silicon)
What issue are you seeing?
The Desktop renderer (main window Codex (Renderer) process) pegs one CPU core at 100% forever and the whole UI freezes as soon as a thread is opened. Force-quitting and relaunching does not help: the app auto-resumes the same thread and freezes again within ~10 s, so the thread becomes permanently unopenable from the UI. The app-server side is fine (thread/resume answers in ~300 ms in the desktop log); the hang is purely in renderer JavaScript.
Root cause (verified with a real JS stack, see below): the thread had a text-file-editor tab open in the right panel on a .go file. Restoring that tab runs the shiki syntax highlighter (JavaScript regex engine) over the whole file, and the Go TextMate grammar bundled with the app has a catastrophic-backtracking regex in the struct-field rule. One RegExp.exec() on a single line never returns, so nothing (not even shiki's tokenizeTimeLimit) can interrupt it.
Symptom-wise this is the same as #40559 (Go files, persisted editor tabs, thread repeatedly unopenable); this report adds the root cause and a fix.
JS stack captured from the hung renderer via CDP (--remote-debugging-port, Debugger.pause), identical at every pause, CPU profile 99.9% self time in the top frame:
findNextMatchSync app-initial-7a6c8787453d.js:307 <- one RegExp.exec() that never returns
findNextMatchSync app-initial-7a6c8787453d.js:165 (shiki JS regex engine scanner)
uGe / lGe / f / sGe (vscode-textmate rule matching)
_tokenize / tokenizeLine2
KKe / GKe / UKe / eqe / nqe
codeToHast
f1e
renderFileWithHighlighter app-initial-7a6c8787453d.js:315
asyncHighlight
-- async --
renderFile / renderPreparedFile / computeRenderRangeAndEmit (file viewer / text editor tab)
Hot frame locals: t = 0 (scan position never advances), this.regexps[10] is the compiled struct-field rule:
(?<=\{)((?:\p{space}*(?:(?:(?:[\p{L}\p{M}\p{N}\p{Pc}]+,\p{space}*)+)?[\p{L}\p{M}\p{N}\p{Pc}]+\p{space}+)?(?:(?:\p{space}*(?:[\]\*\[]+)?(?:<-\p{space}*)?\bchan\b(?:\p{space}*<-)?\p{space}*)+)?[^\/\p{space}]+;?)+)\p{space}*(?=\})
The trailing [^/\s]+ (no " / backtick exclusion) lets the class run into a raw-string struct tag that contains spaces, and the nested (...)+ groups with optional \s* explode combinatorially.
What steps can reproduce the bug?
In the app:
- Have a Go file in the workspace containing a struct field of type
interface{}orstruct{}followed by a raw-string tag with spaces, e.g.type Payload struct { Data interface{} `json:"data" a:"b" c:"d e f g h i j k l m n o"` } - In a thread, click the file so it opens in the right-panel text editor → renderer freezes at 100% CPU.
- Force-quit, relaunch → the thread auto-resumes with the persisted editor tab and freezes again.
Standalone reproduction with the same shiki version family the app ships (grammar from @shikijs/langs 4.0.1; anything from 3.9.1 up to 4.0.1 has the same rule):
npm i shiki@4.0.1
node repro.mjs 0 # -> TIMEOUT (hangs forever)
node repro.mjs 500 # -> still TIMEOUT: tokenizeTimeLimit cannot interrupt a single exec()
// repro.mjs
import { createHighlighter } from "shiki";
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
import { Worker, isMainThread, workerData, parentPort } from "node:worker_threads";
const code = `type Payload struct {
\tData interface{} \`json:"data" a:"b" c:"d e f g h i j k l m n o"\`
}
`;
const timeLimit = Number(process.argv[2] ?? 0);
if (isMainThread) {
const w = new Worker(new URL(import.meta.url), { workerData: code, argv: [String(timeLimit)] });
const t0 = performance.now();
const res = await new Promise((r) => {
const t = setTimeout(() => { w.terminate(); r("TIMEOUT after 10 s"); }, 10000);
w.on("message", (m) => { clearTimeout(t); r(m); });
});
console.log(`tokenizeTimeLimit=${timeLimit}: ${res} (${(performance.now() - t0).toFixed(0)} ms)`);
} else {
const hl = await createHighlighter({ themes: ["github-dark"], langs: ["go"], engine: createJavaScriptRegexEngine({ forgiving: true }) });
hl.codeToHast(workerData, { lang: "go", theme: "github-dark", tokenizeTimeLimit: timeLimit });
parentPort.postMessage("highlighted OK");
}
Same script, other configurations (all measured):
| Configuration | Result |
|---|---|
| shiki 4.0.1, JS regex engine | hangs (both tokenizeTimeLimit 0 and 500) |
shiki 4.0.1, Oniguruma WASM engine (createOnigurumaEngine) |
OK, 70 ms |
shiki 4.0.2 (@shikijs/langs 4.0.2), JS regex engine |
OK, 110 ms |
Pure-regex version (no dependencies): new RegExp(<source above>, "dgv").exec("\tData interface{} json:"data" a:"b" c:"d e f g h i j k l m n o"\n") never returns; a benign line such as \tName string json:"name"`` returns in <1 ms.
What is the expected behavior?
Opening or restoring a code editor tab must never block the conversation renderer, and a thread must never become unopenable because of a persisted UI tab.
Proposed fix, in order of impact:
- Bump
@shikijs/langsto ≥ 4.0.2. The Go grammar there includes upstream fix worlpaker/go-syntax#23 (commitc74e22e, 2026-02-04, "fix: catastrophic backtracking with struct tags after interface{}/struct{}"), which changes the struct-field class from[^\s/]+to[^\s/\"]+. The vulnerable variant was introduced by [go-syntax#21](https://github.com/worlpaker/go-syntax/pull/21) (2025-06) and reached shiki via the vscode grammar sync;@shikijs/langs` 3.9.1 … 4.0.1 ship it, 4.0.2 (2026-03-09) ships the fix. - Prefer the Oniguruma WASM engine for the file viewer (or as fallback): it is immune to this class of JS-regex blowups and handled the same grammar/file in 70 ms. The JS engine is documented by shiki as not guaranteeing compatibility/performance for every grammar.
- Defensive UI: tokenize file-viewer content off the main thread (worker) with a hard per-file timeout that falls back to plain text, and restore persisted
text-file-editortabs lazily/after the conversation renders, so a failing tab cannot make a thread unopenable (also requested in #40559).
Additional information
- Workaround that reliably recovers the thread: quit the app and delete
electron-persisted-atom-state["thread-tab-routes-v1:<threadId>"]from~/.codex/.codex-global-state.json; the thread then opens normally. Removing items from the rollout does not help, because the file content is read from disk for the editor tab, not from the conversation. - The freeze reproduces on any file that contains such a struct tag line; real-world Go code with
interface{}fields carrying multi-key struct tags (with comments/spaces inside) triggers it easily. - Desktop log shows a burst of
ResizeObserver loop completed with undelivered notificationsright before the renderer stops logging, then nothing; norender-process-gone.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the standalone repro.mjs and confirm the hang with shiki 4.0.1, then compare it with @shikijs/langs 4.0.2 and the Oniguruma configuration. Trace the renderer's renderFileWithHighlighter path and determine which proposed mitigation fits the file viewer. Done means opening and restoring the reproducing Go tab no longer freezes the renderer or makes the thread unopenable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, javascript
- Domain
- desktop, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100