fix: 데몬 cwd 삭제로 인한 프로세스 오염 방지 + 자가회복 - #62
Merged
Merged
Conversation
existsSync(".")는 디렉토리가 unlink돼도 프로세스가 쥔 cwd 파일 디스크립터가
inode를 살려두므로 true를 반환한다 — 오염을 원리적으로 못 잡는다. 스테일
경로 문자열을 검사하는 방식만이 유일하게 잡는다.
감지는 라이브러리가, 복구는 프로세스를 소유한 쪽이 한다. process.chdir()이 전역 부작용이라 startDiffServer가 직접 부르면 이 서버를 임베드한 호스트의 cwd를 말없이 옮기게 된다. 커버리지 게이트가 branch를 안 세므로 호출/미호출 양쪽 테스트를 따로 뒀다.
cc-statusline이 띄운 데몬은 기동 시점의 worktree를 cwd로 물고 있다가 그 worktree가 삭제되면 자신과 무관한 repo까지 전부 400을 낸다. repo를 읽은 직후 "/"로 옮기면 이후 잃을 정보가 없다 — git 호출 17곳은 전부 repo를 명시적으로 넘긴다. 호출 순서가 계약이라 테스트로 고정했다. toSafeCwd가 cwd()보다 먼저면 repo가 "/"가 되고, startServer보다 나중이면 오염 창이 남는다.
기동 cwd와 서빙 repo를 분리해야 의미가 있다 — 같으면 그 디렉토리를 지웠을 때 repo 자체가 사라져 400이 정답이 되어버린다. launchViewer에 선택적 기동 cwd를 추가해 둘을 갈랐고, 기본값이 기존 동작이라 다른 스펙은 무변경이다. 예방 코드를 임시로 빼고 이 스펙이 실제로 빨간불이 되는 것을 확인했다.
호출부에서 고칠 수 없다는 것과 existsSync(".")로는 감지할 수 없다는 것이
핵심이다 — 둘 다 그럴듯한 오답이 먼저 떠오르는 자리다.
REAL_CWD_DEPS의 process.cwd()는 darwin Node에서 삭제된 cwd에 대해 uv_cwd ENOENT를 throw한다(Bun은 스테일 문자열을 돌려줘 통과하지만 실측상 런타임 구현 세부일 뿐). isCwdAlive가 이를 무방비로 두면 Bun.serve에 error 핸들러가 없는 지금 모든 라우트가 500이 되어, 고치려던 400보다 나쁜 실패 모드가 된다. isCwdAlive를 try/catch로 감싸 throw 자체를 "cwd가 죽었다"는 신호로 다뤄 false를 반환한다. 곁들여 서버 핸들러의 탐지 가드를 repairCwd가 주입됐을 때만 돌게 단축평가한다 — startDiffServer를 임베드하며 repairCwd를 안 넘긴 호스트는 정의상 cwd 탐지 비용도 원하지 않는다. 이 변경이 REAL_CWD_DEPS.cwd를 호출하는 유일한 경로를 없애 함수 커버리지를 깨뜨려, 그 경로를 직접 찌르는 테스트를 추가했다. 테스트 제목 하나도 실제로 증명하는 바에 맞게 정정했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ijQqu6NC54uajJktYKDcK
리뷰에서 코드로 직접 검증된 정밀도 3건을 기존 cwd 오염 항목 문장에
이어 붙인다: (1) git 호출 17곳이 전부 -C는 아니다 — 16곳은
git -C ${repo}지만 prBaseName(gh pr view)은 .cwd(repo)로만 조준한다.
(2) server.ts의 resolve(cfg.viewerDir)가 chdir 이후에 도는 두 번째
cwd 의존 지점이지만 프로덕션·테스트 값이 둘 다 절대경로라 현재는
안전하다. (3) /api/blob의 경로 탈출 가드는 resolve 가드와 실제 읽기가
같은 cwd로 함께 움직여 안전하다 — cwd가 조용히 바뀌며 보안 버그가 될
뻔했던 유일한 지점이라 기록해 둔다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ijQqu6NC54uajJktYKDcK
server.ts의 repairCwd 단축평가 가드가 왜 그 순서인지(repairCwd 미주입 호스트에게 감지 자체가 순수 비용이 되면 안 됨, diff-server.test.ts가 이를 단언)를 코드 옆에 남기고, ~1µs 비용 근거를 repairCwd가 주입된 경우로 한정한다. startDiffServer opts의 repairCwd 필드에 프로덕션 배선(cli.ts의 toSafeCwd) 주석을 달아 앞뒤 테스트 전용 필드로 오독되지 않게 하고, brace-less if를 이 파일의 다른 관례에 맞춰 중괄호로 감싼다. cwd.ts에 CwdDeps 인터페이스를 export해 세 곳(isCwdAlive 파라미터, createHandler cfg, startDiffServer opts)의 중복 인라인 타입을 제거한다. daemon-cwd.e2e.ts의 finally 블록은 픽스처의 cleanup() 계약을 우회해 같은 rmSync를 직접 부르고 있었다 — 픽스처가 반환하는 cleanup()으로 바꿔 정리 로직이 픽스처 소유로 남게 한다(force:true라 이미 삭제된 디렉토리에도 안전). 런타임 동작은 바뀌지 않는다.
"핸들러 진입부가 isCwdAlive로 감지해 repairCwd를 부른다"는 서술이 항상 감지한다고 읽혔는데, 실제로는 repairCwd가 주입된 경우에만 감지 자체를 수행한다(M-4 단축평가). diff-server.test.ts를 "호출/미호출을 각각 찌른다"로만 소개했던 것도 지금은 cwd 테스트가 4종이고 그중 둘이 단축평가·REAL_CWD_DEPS 경로의 함수 커버리지 확보용이라 보강한다.
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.
문제
장수 데몬이 기동 시점의 worktree를 cwd로 물고 있다가 그 worktree가 삭제되면, 자신과 무관한 repo까지 전부
/api/diff400not a git repository를 낸다. 실제로 겪었다 — 삭제된 worktree와 아무 관계 없는 다른 리포도 함께 죽었다./api/ping(204)과 정적 페이지(200)가 멀쩡한 이유는 서버가 건강해서가 아니라 그 두 경로가 서브프로세스를 안 띄우기 때문이다. "서버는 살아있는데 diff만 안 된다"로 오진하기 쉽다.원인
cwd가 unlink된 프로세스는 자식 프로세스를 하나도 생성할 수 없다 — 자식이 상속할 cwd를 커널이 해석하지 못한다. OS 제약이라
.cwd(repo)를 명시해도, 셸을 안 거치는Bun.spawn을 써도ENOENT: posix_spawn으로 죽는다(실측). 호출부(git 17곳)에서 고칠 수 있는 방법이 없다.변경
cli.ts가repo를 읽은 직후/로 이탈한다. 이후 git 호출은 전부repo를 명시적으로 조준하므로 잃을 정보가 없다.server.ts가 요청 경계에서 감지하고, 복구는 주입받은 함수가 수행한다.process.chdir()이 프로세스 전역 부작용이라 라이브러리가 임베드한 호스트의 cwd를 말없이 옮기지 않도록 권한을 분리했다.감지는
existsSync(process.cwd())여야 한다.existsSync(".")·statSync(".")는 unlink된 inode를 살려두는 cwd 파일 디스크립터 때문에 항상 true라 원리적으로 못 잡는다.isCwdAlive는cwd()호출을 try/catch로 감싼다 — 동일 darwin에서 Node는 삭제된 cwd에uv_cwd ENOENT를 던지고 Bun은 캐시된 스테일 문자열을 돌려준다(실측). 삼키지 않으면Bun.serve에 error 핸들러가 없어 모든 라우트가 500이 된다.테스트
diff-cwd.test.ts— 판정 3분기(살아있음/죽음/cwd()throw) +"."을 검사하지 않는다는 계약diff-server.test.ts— 가드 3분기(복구 호출/미호출/repairCwd없으면 탐지 자체 생략)cli-run.test.ts— 호출 순서(cwd→toSafeCwd→startServer)와 주입daemon-cwd.e2e.ts— 기동 디렉토리를 지운 뒤 다른 repo가 200으로 응답게이트: 564 pass / 0 fail, 커버리지 100%, typecheck·lint·format 클린.
e2e의 한계를 명시해 둔다.
daemon-cwd.e2e.ts는 사용자에게 보이는 계약만 검증하고 예방과 자가회복을 구분하지 못한다 — 둘 중 하나만 살아 있어도 통과하고 둘 다 없어야 400으로 빨간불이 된다(실측). 가드가 라우트 분기보다 먼저 돌기 때문이다. 기제별 격리는 유닛이 맡는다.한계
이미 떠 있는 데몬은 못 고친다 — 배포 후 재기동돼야 효력이 생긴다.
🤖 Generated with Claude Code
https://claude.ai/code/session_011ijQqu6NC54uajJktYKDcK