Repository navigation
fix: stop a session's directory read from killing the app on Windows - #124
Merged
Merged
Conversation
Tracking a session's working directory walks the child process's PEB and reads DosPath.Length out of that process's memory. x/sys documents the length as "should always be even"; a 32-bit child, or one exiting mid-walk, hands back whatever happens to sit there. An odd length passed the `== 0` guard, sized the buffer to zero, and panicked on `&buf[0]` — on the bare goroutine watchCwd runs on, so it took the whole process with it. The window survived, still rendering its terminal, with nothing behind it. The crash left no evidence: the runtime prints a panic to stderr, the windowsgui build has no console, and slog never sees it — lich.log simply stopped mid-session. Init now points the runtime's crash output at the same file, so the next one is readable. The length check is a pure helper outside the build-tagged file, following the chromium candidate lists, so the guard is tested on every OS.
SetCrashOutput keeps a duplicate of the log file's handle for as long as the registration stands. Windows refuses to remove a file another handle still holds, so TempDir's cleanup failed in the two tests that let Init open a file. The clear is registered after TempDir so the LIFO order runs it first.
This was referenced Jul 28, 2026
Merged
4 of 5 tasks
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 happened
A Windows
lich.logends mid-session — nowindow closed, exiting, nochromium shellerror, notransport listener stopped. The window stayed onscreen still rendering its terminal, but nothing updated again and a reload
found nothing listening.
Two things were wrong, and the second is why the first went undiagnosed for so
long.
The crash
processCwd(internal/terminal/cwd_windows.go) walks the child's PEB andreads
CurrentDirectory.DosPathout of that process's memory.NTUnicodeString.Lengthis a byte count that x/sys documents as "shouldalways be even" — a
shouldabout a value read from another process, which a32-bit child or one exiting mid-walk does not honour.
The guard only rejected zero. An odd length sized the buffer to zero and the
read went straight into
&buf[0]:That runs on the bare goroutine
watchCwdspawns per session, every 300ms, sothe panic ended the process.
net/httprecovers panics in its handlers, whichis why RPC and WebSocket traffic never showed this — the poller was outside
that net.
The length now goes through
dosPathRunes, which refuses anything a UTF-16buffer cannot have. It sits in the untagged
cwd.go, followingwindowsBrowserCandidatesininternal/chromium, so the guard is tested onevery OS rather than only where it runs.
Why the log said nothing
The runtime prints a panic to stderr. The Windows build is
-H=windowsgui(
Taskfile.yml), so there is no console and fd 2 goes nowhere; slog never seesa panic, so the file half of the log was never going to carry it.
logging.Initnow callsdebug.SetCrashOutputwith the log file it alreadyholds. Verified end to end — the child half of the new test writes a real trace
into
lich.log:Test plan
gofmt -l .andgo vet ./...cleango test ./...— 531 pass-race:TestDosPathRunesRejectsUnusableLengths,TestInitRoutesPanicToLogFile(subprocess, asserts the trace lands in thefile)
GOOS=linux|darwin|windows—go build+go vetcleanbiome check,vitest run(501 pass),tsc && vite build— untouched bythis change, run for the gate
ci:os— the backend suite on a real Windows runnerit used to disappear
🤖 Generated with Claude Code