fix(lsp): address codex review round 3

Final review pass on the local provider:
- Tear the instance down when `initialize` REJECTS (utf-8 negotiation, malformed
  result), not only on abort, so a permanently-rejecting `ready` is never pooled.
- Use the group-aware SIGKILL on a framing failure so helpers are reached.
- Validate maxMessageBytes and maxDocumentBytes positive at load alongside the
  other byte caps.
- Fix the location renderer's outside-workspace check to match a `..` segment
  exactly, so an in-workspace path like `..generated/a.ts` stays relative.
- Document the accepted ancestor-directory symlink-swap TOCTOU under the
  trusted-host model (O_NOFOLLOW guards only the final component).
This commit is contained in:
Dudu-0223
2026-07-16 14:17:21 +08:00
parent 0f3f0efd9c
commit 43d419ac5c
7 changed files with 35 additions and 11 deletions

View File

@@ -108,16 +108,17 @@ export class LspInstance {
if (this.disposed) throw new Error('LSP instance was disposed')
/* v8 ignore next -- the abortable queue wait rejects a pre-aborted signal before runQuery; this is a belt-and-suspenders guard. */
if (signal?.aborted) throw abortError(signal)
// Observe abort during the handshake wait: a server that never answers `initialize` must not
// block the tool-timeout signal here (the timeout policy awaits our quiescence, not the promise).
// If abort wins, the handshake is still pending on a live process, so tear the instance down —
// otherwise its poisoned `ready` would make every later query for this workspace re-wait.
// Observe abort during the handshake wait, and never pool a poisoned instance: if the wait ends
// in failure — an abort on a still-pending handshake, OR `initialize` rejecting (utf-8
// negotiation, malformed result) without the process exiting — tear the instance down so a
// permanently-rejecting/pending `ready` can't make every later query for this workspace fail.
try {
await this.abortable(this.ready, signal)
} catch (error) {
if (signal?.aborted && !this.dead) {
if (!this.dead) {
this.disposed = true
await this.tearDown(abortError(signal))
/* v8 ignore next -- ready rejects with an Error (abort reason or initialize failure); the String() fallback is defensive. */
await this.tearDown(error instanceof Error ? error : new Error(String(error)))
}
throw error
}