feat: Terminal - #14
Merged
Merged
MacroscopeApp / Macroscope - Correctness Check
completed
Feb 12, 2026 in 7m 24s
1 issue identified (110 code objects reviewed).
• Merge Base:
16b81f7
• Head:42b2a88
Details
| ✅ | File Path | Comments Posted |
|---|---|---|
| ✅ | apps/server/src/git.ts |
0 |
| ✅ | apps/server/src/ptyAdapter.ts |
0 |
| ❌ | apps/server/src/terminalManager.ts |
1 |
| ✅ | apps/server/src/wsServer.ts |
0 |
| ✅ | apps/web/src/App.tsx |
0 |
| ✅ | apps/web/src/components/ChatView.tsx |
0 |
| ✅ | apps/web/src/components/Sidebar.tsx |
0 |
| ✅ | apps/web/src/components/ThreadTerminalDrawer.tsx |
0 |
| ✅ | apps/web/src/index.css |
0 |
| ✅ | apps/web/src/persistenceSchema.ts |
0 |
| ✅ | apps/web/src/store.ts |
0 |
| ✅ | apps/web/src/terminal-shortcuts.ts |
0 |
| ✅ | apps/web/src/types.ts |
0 |
| ✅ | apps/web/src/wsNativeApi.ts |
0 |
| ✅ | packages/contracts/src/ipc.ts |
0 |
| ✅ | packages/contracts/src/terminal.ts |
0 |
Filtered Issues Details
apps/server/src/git.ts
- line 50: When
maxBytesis reached inrunGitorrunTerminalCommand, the truncation logic continues to appendchunk.subarray(0, remaining)even whenremainingis zero or negative because the negative check isremaining <= 0, but the subsequentsubarray(0, remaining)call with a negativeremaining(ifremainingwas exactly 0, it's fine, but if it was negative,subarraybehaves differently). Actually, looking closer:remainingismaxBytes - currentBytes. Ifremaining <= 0(e.g.currentBytes >= maxBytes), it returns early (lines 45-47). This logic seems sound for the negative case. [ Already posted ] - line 50: The logic in
appendChunkWithinLimituses.toString()onchunk(line 50) or a subarray ofchunk(line 56). This assumes the default encoding (utf8). If the command outputs binary data or uses a different encoding that is invalid in UTF-8, this will insert replacement characters or garbage into thestdout/stderrstring. Furthermore, thestdoutBytesandstderrBytescounters track the length of the string (or accumulated bytes so far) pluschunk.length. However,nextBytesis calculated ascurrentBytes + chunk.length(line 51) orcurrentBytes + remaining(line 57). This tracks byte count. Butstdoutis a JS string. Later, ifmaxOutputBytesis large, the string length (in UTF-16 code units) might diverge significantly from byte length if multi-byte characters are present. Mixing byte-length limits with string concatenation is generally safe for limits, but relying onchunk.toString()inside the chunk handler without aStringDecoderguarantees corruption of multi-byte characters that span chunk boundaries. [ Already posted ] - line 88: The updated
runGitfunction enforces a new hardcoded 1MB limit onstdout. If command output exceeds this (e.g.,git branchon a large repo),stdoutis truncated and a warning is appended tostderr, but the process still exits withcode: 0. The callerlistGitBranchesignoresstderrwhencodeis 0 and proceeds to parse the truncatedstdout. This results inlistGitBranchessilently returning an incomplete list of branches, and potentially a corrupted final branch name if the truncation occurs mid-string, with no runtime error indicating data loss. [ Already posted ]
apps/server/src/terminalManager.ts
- line 60: The
capHistoryfunction can create infinite string growth or memory pressure if the inputhistorystring is large and does not contain newlines, becausehistory.split("\n")will create a single-element array (length 1) which is always<= maxLines(unless maxLines is 0), returning the original massive string without capping. WhilemaxLinesdefaults to 5,000, ifonProcessDatareceives a stream of data without newlines (e.g., a progress bar or binary output),session.historywill grow unbounded in memory until the process crashes. [ Already posted ] - line 72: The newly introduced
legacySafeThreadIdfunction sanitizes inputs using an allowlist regex/[^a-zA-Z0-9._-]/gbut fails to filter Windows reserved device names (e.g.,CON,PRN,AUX,NUL,COM1). WhenreadHistorycalls this function vialegacyHistoryPath, it may construct a path like.../CON.log. On Windows,fs.promises.readFiletreats this path as the console device driver. If the process is attached to a console, the read operation will block indefinitely waiting for input. An adversary can trigger this viaopen({ threadId: "CON" }), causing the request to hang. Furthermore, concurrent requests can exhaust the libuv thread pool, leading to a denial of service for the entire application. [ Already posted ] - line 254: The
disposemethod clearsthis.threadLockswithout waiting for or cancelling pending operations, breaking the mutual exclusion guarantee ofrunWithThreadLock. Ifdispose()is called while aclose()operation is suspended (e.g., awaitingflushPersistQueue), the lock for that thread ID is effectively released. A subsequentopen()call for the same thread ID can then immediately acquire the lock and create a new session. When the suspendedclose()operation eventually resumes, it proceeds to calldeleteHistory(if requested), which deletes the history file that the newly created session may have just loaded or initialized. This results in data loss (persisted history removal) for an active session. [ Already posted ] - line 309: Memory leak in
startSessionerror handler due to incomplete cleanup. If an exception occurs instartSessionafter the listeners are attached but before the function completes (e.g., ifthis.emitEventat line 293 throws because of a failing event listener), thecatchblock cleans up the process state (kills process, setssession.processto null) but fails to remove the event listeners by callingsession.unsubscribeDataorsession.unsubscribeExit. This leaves theonDatalistener attached to the underlyingnode-ptyobject. Since the listener closure captures thesessionobject (line 287), and thesessionobject is retained inthis.sessions, a circular reference prevents thenode-ptyprocess instance (and its associated buffers/resources) from being garbage collected until the session is manually closed or successfully restarted. [ Already posted ] - line 326: The
capHistoryfunction limits history by line count but fails to limit line length, leading to unbounded memory growth. InonProcessData, incoming data is appended tosession.historyand then passed tocapHistory. If a process outputs a large stream of data without newlines (e.g., printing a binary file or usingcat /dev/urandom),capHistorysplits the string but finds only 1 line, bypassing themaxLinescheck. This causessession.historyto grow until the Node.js process crashes with an Out Of Memory (OOM) error. [ Already posted ] - line 439: The
readHistorymethod reads the entire content of the history file into memory usingfs.promises.readFile(line 439) and subsequently processes it withcapHistory(line 440), which splits the string by newlines. If a legacy history file is significantly large (e.g., generated by a previous version without limits or by an attacker), this operation will consume excessive memory (creating millions of string objects), creating a high risk of an Out-Of-Memory (OOM) crash and Denial of Service for the server process. [ Already posted ] - line 446: The
readHistorymethod performs bothreadFileandwriteFileoperations within the sametryblock (lines 438-444). IfreadFilesucceeds (loading the history) but the subsequentwriteFile(line 442) fails with anENOENTerror (e.g., if the logs directory is deleted concurrently or permissions change such that the directory appears missing), thecatchblock (line 446) incorrectly interprets this as "file not found". It then swallows the error and falls through to the legacy file check. If the legacy file is also missing, the method returns an empty string (line 460), resulting in the silent loss of the history data that was successfully read from the new path. [ Already posted ]
apps/server/src/wsServer.ts
- line 88: In
runGitinsideapps/server/src/git.ts, the new logic uses a helper functionappendChunkWithinLimitwhich is not imported or defined in the file. The diff shows lines removingstdout += chunk.toString()and replacing them with calls toappendChunkWithinLimit, but there is no import or definition of this function in the providedgit.tsfile content. This will cause aReferenceError: appendChunkWithinLimit is not definedat runtime wheneverrunGitprocesses output. [ Already posted ]
apps/web/src/components/ThreadTerminalDrawer.tsx
- line 305: The
inputDisposablecallback inuseEffectcaptures theterminalinstance and attempts to write to it in a.catch()block after an asynchronous operation. If the component unmounts while theapi.terminal.writepromise is pending, theuseEffectcleanup will runterminal.dispose(). When the promise subsequently rejects (e.g. due to the unmount closing the connection), thecatchblock callswriteSystemMessagewhich executesterminal.write(...)on the disposed terminal instance. Accessing methods on a disposed xterm.js instance throws a runtime error (typicallyTypeErroron internal_core). [ Already posted ] - line 336: The
openTerminalfunction (line 323) is async and called viavoid openTerminal()inside auseEffect. If the effect cleanup runs (line 391) beforeopenTerminalawaitsapi.terminal.open, thedisposedflag is set to true. However,openTerminalchecksdisposed(line 335) after the await. Ifterminal.dispose()(line 399) has already run in the cleanup,terminalRef.currentis set to null (line 397). ButopenTerminalholds a local referenceactiveTerminalcaptured at the start (line 325). Thedispose()call on the terminal instance destroys it. Subsequent calls likeactiveTerminal.write(line 336, 338) oractiveTerminal.focus(line 341) on a disposed terminal instance will throw a runtime error (xterm.js throws when interacting with a disposed instance). [ Already posted ]
packages/contracts/src/terminal.ts
- line 41: The
terminalSessionSnapshotSchemadefines thehistoryfield as an unboundedz.string(). In a terminal application, output accumulates indefinitely. Consequently, for any long-running or verbose session (e.g., one running a build process or producing large logs), thehistorystring can grow to hundreds of megabytes. Attempting to validate, serialize, or transmit a snapshot with such a large string (e.g., interminalRestartedEventSchema) creates a high risk of Node.js process crashes due to Out-Of-Memory (OOM) errors or exceeding V8's maximum string length limits. [ Already posted ]
Loading