결함 검출 체인 — 규칙기반 도구 (2026-07-31)
왜 만들었나
오너 지적:
"오류 N건"으로 뭉뚱그린 보고는 개선에 쓸 수 없다.
알아야 할 것은 ① 이 검사도구를 거쳤는데 ② 못 잡고 새어나간 게 몇 개인지 ③ 그걸 잡으려고 만든 다음 도구가 무엇이고 ④ 거친 결과가 어떻게 됐는지.
모델
결함이 검사단계(체)를 순서대로 통과한다. 각 단계는
검출하거나
통과(누출) 시킨다. 적용 불가한 단계는
미적용(분모 제외 — 억울한 감점 방지).
- 어떤 단계도 못 잡은 결함 = 미검출 → 신규 단계를 만들라는 래칫 신호
- 신규 단계가 그 결함을 잡으면 체인이 한 칸 자란다. 이 성장 기록 자체가 자산.
4상 판정 — 결함별: 티=첫 관문 검출 / 옴=누출 후 뒤 단계가 흡수 / 타=전 단계 미검출
체인 전체: 미검출>0이면 타, 누출>0이면 옴, 전부 첫관문이면 티
구성
| 파일 | 역할 |
|---|
crowny-services/도구/검출체인.한선 | 규칙엔진 코어(등록·집계·4상 판정·래칫). STRICT 0경고. brain 학습 완료 |
crowny-services/도구/검출체인.sh | CLI (단계추가·결함추가·기록·현황·래칫·결함) |
crowny-services/data/검출체인/{단계,결함,검출}.psv | append-only 장부 |
crowny-services/도구/S4_소유자격리.sh | 래칫이 만들어낸 첫 신규 단계 |
실제 적용 — 오늘 겪은 결함 3건으로 시드
| 결함 | S1 컴파일 | S2 응답코드 | S3 실반영 | S4 소유자격리 |
|---|
| D001 승인미적용(인자수 불일치) | 통과 | 통과 | 검출 | — |
| D002 RBAC우회(불리언==0) | 통과 | 통과 | 검출 | — |
| D003 교차소유자(?owner=타인) | 통과 | 통과 | 통과 | 검출 |
단계별 성적: S1 0%(0검출/3누출) · S2 0%(0/3) · S3 66.7%(2검출/1누출) · S4 100%(1/0)
래칫이 실제로 돈 과정
- 시드 직후 체인 판정 = 타 (미검출 1건: D003)
검출체인.sh 래칫 이 D003을 지목 → 그것만 잡는 S4_소유자격리.sh 신설
(원리: A토큰으로
?owner=B 호출 결과가 A자기조회와 같으면 티, B자기조회와 같으면 타)
- S4를 라이브 :9662 3라우트에 실행 → 티 3 · 옴 0 · 타 0 (현재 코드는 격리 정상, 앞서 수리한 결과)
- 단계 등록 + D003 검출 기록 → 체인 판정 타 → 옴, 미검출 0
이 도구가 드러낸 것
- S1·S2 검출률 0%: 컴파일 통과와 200 응답은 치명 결함 3건 중 단 하나도 못 잡았다. "컴파일 됐고 200 나온다"를 근거로 완료를 주장하면 안 된다는 것이 수치로 남았다.
- S3가 주력(66.7%): 실제 상태변화 확인이 지금까지 가장 많이 잡았다.
- 앞 단계 보강 여지: D001은 S1에서 잡을 수 있었다(크로스모듈 호출 인자수 대조 = 정적 검사). → 다음 래칫 후보.
다음 래칫 후보 (미검출은 0이나 누출을 앞당길 여지)
- S1 강화: 크로스모듈 함수 호출의 인자 수/시그니처 정적 대조(D001을 1단계에서 잡기)
- S2 강화: 응답코드뿐 아니라 응답 본문의 의미 검증(ok:1인데 실제로 아무 일도 안 한 경우 탐지)
2차: S1 강화 + 도구 자체 검증 (같은 날)
S1x_시그니처대조.sh — 한선씨가 안 잡는 인자 수 불일치를 정적으로 검출
도구/S1x_시그니처대조.sh <엔트리.한선>
- 원리: 엔트리의
가져오기 폐포(transitive closure) 를 스코프로 삼아, 폐포 내 모든 파일의 함수 이름(...) 정의와 이름(...) 호출부 인자 수를 대조.
- 분류: 일치(티) / 불일치(타) / 모호(스코프 내 동명 정의 복수) / 미적용(스코프 밖=빌트인·미해결 import). 모호·미적용을 분리해 억울한 감점을 막는다.
★스코프 설계에서 두 번 틀렸고 두 번 다 반증시험이 잡았다
- 1차(전역 평면 스코프): 무관한 두 파일을 함께 스캔 → 감사API의
HTTP응답(코드,타입,본문)(네트워크.한선 제공)과 넥서스분기서버 자체 HTTP응답(본문)을 동일 함수로 오판, 불일치 2건 오탐. 하마터면 없는 결함을 보고할 뻔했다.
- 2차(파일별 스코프): 오탐은 사라졌으나 D001을 놓쳤다 — 한선씨는 본체가 모듈을 조립하므로 크로스모듈 호출은 파일 하나만 봐선 '미적용'으로 샌다.
- 3차(가져오기 폐포): 정답. 폐포가 정확한 모듈 경계다.
검증 4종 전부 의도대로:
| 시험 | 기대 | 결과 |
|---|
| 결함 있는 백업 조립(본체 폐포) | 검출 | 서비스적용·게이트적용 2건 ★불일치 |
| 현행 수리본 | 통과 | 티(423 일치, 0 불일치) |
| 감사API 단독 폐포(오탐 케이스) | 통과 | 티 |
| 넥서스분기 단독 폐포 | 통과 | 티 |
전 서버 스윕 결과 (2,220건 대조)
services 272 · mail 694 · dex 276 · tele 922 · warp 56 —
불일치 0. 현재 크라우니 한선씨 서버에 인자 수 불일치는 없다.
S5_반증시험 단계 신설
"새 도구가
이미 아는 결함을 실제로 잡는지 확인한다. 못 잡으면 그 도구는 무효."
- 근거: S1x 오탐(D004)을 잡은 것이 바로 이 절차였다. 도구도 검사 대상이라는 원칙을 체인에 명문화.
- 규칙: 신규 검출도구는 반드시 (a) 결함 있는 과거 버전에서 검출 (b) 현행에서 통과 두 시험을 통과해야 등록한다.
현재 체인
S1 0% · S1x 100% · S2 0% · S3 66.7% · S4 100% · S5 100% — 결함 4건, 미검출 0, 판정
옴3차: S2 강화 · 한선씨 코어 이식 · 전서비스 적용 (같은 날, 순서대로)
① S2x_부정경로.sh — "실패해야 할 요청이 성공하는가"
S2(응답코드)가 0%였던 이유:
정상 요청에 200이 나오는지만 봤다. S2가 놓친 결함들의 공통 형태는 전부
부정 경로가 성공한 것이었다.
- 검사: 무토큰·위조토큰으로 쓰기 엔드포인트 호출 → 2xx면 타(인증게이트 사망). 본문에 성공 표식(ok:1/queued:1)까지 있으면 더 강한 타.
- 반증 시험 양방향 통과: 결함본(RBAC 우회 백업, 그림자 :19805) → 타(무토큰이 200) / 현행 수리본(:9805) → 티(403).
② 판정 코어를 한선씨로 이식 — 도구/시그니처판정.한선
brain이 bash 도구를 거부("컴파일실패 — 저품질 학습은 오염 전파")해 헌법의 한선씨 우선 원칙대로
판정 로직만 한선씨로 옮겼다(셸은 수집 담당).
- ★성능 함정: 호출마다 정의표를 선형 탐색하니 1400×450=63만 회 문자열 비교로 2분+ 무응답. → 맵 색인 O(1) 로 재작성.
- 동치성 시험 통과: 수리본
423/0/0/951 → 티 · 결함본 417/0/2/947 → 타 + 불일치 2건의 위치까지 셸 판정과 완전 일치.
- 학습 완료(토큰0): 시그니처판정.한선 · 검출체인.한선 · 부하판정.한선 3종.
③ 전 서비스 스윕에서 도구 자신의 결함 발견 (D005)
메일 :9610 스윕이
429(레이트리밋) 를 받았는데 초판이 이를
티(통과)로 보고했다 — 인증 게이트에 도달조차 못했는데 "부정경로 전부 거부됨"이라고 거짓 안심을 준 것.
- 수리: 429/000은 미적용으로 분류, 그리고 검사된 항목이 0이면 티가 아니라 '미적용(판단불가)' 로 판정. 미검사를 통과로 보고하지 않는다.
- 스윕 결과(수리 후): quote:9626 401 · dex:9402 401 · nexus:9805 403 = 티 / mail:9610 = 미적용(레이트리밋)
현재 체인
| 단계 | 검출 | 누출 | 검출률 |
|---|
| S1_컴파일 | 0 | 3 | 0.0% |
| S1x_시그니처 | 1 | 0 | 100.0% |
| S2_응답코드 | 0 | 3 | 0.0% |
| S2x_부정경로 | 1 | 0 | 100.0% |
| S3_실반영 | 2 | 1 | 66.7% |
| S4_소유자격리 | 1 | 0 | 100.0% |
| S5_반증시험 | 2 | 0 | 100.0% |
결함 5건 ·
미검출 0 · 판정 옴 · 래칫 목록 비어 있음
이 라운드의 교훈
- 새로 만든 검출도구 3개 중 2개가 자기 결함을 가지고 있었고(S1x 오탐, S2x 거짓안심) 둘 다 S5 반증시험이 잡았다. 도구를 만들면 반드시 반증시험을 통과시켜야 한다는 원칙이 실전으로 두 번 증명됐다.
- 미검사를 통과로 보고하는 것이 가장 위험한 실패 모드다(D005). 검사 도구는 "검사하지 못했음"을 별도 상태로 가져야 한다.
4차: S3 강화 — 그리고 검사도구가 낸 실사고
S3x_델타정밀.sh — "변했다"가 아니라 "정확히 그만큼만 변했는가"
S3(실반영)가 66.7%였던 이유는 둘이다. 하나는 D003이
읽기 경로 결함이라 파일이 안 변해 원리적으로 못 잡은 것(→S4 담당). 다른 하나가 진짜 맹점이다:
"변했다"만 확인하고 "그것만 변했는가"는 확인하지 않았다.
- 통과시키던 결함군: 의도한 1행 대신 2행 추가(중복 적용) · 무관한 파일까지 변함(부작용) · 정리 작업의 과잉 삭제.
- 실제로 D001 수리를 검증할 때 나는 "행이 생겼다"만 봤다. 두 번 생겼어도 통과했을 것이다.
- 방식: 작업 전후 지문(파일별 md5+행수) 차분. 기대 델타(
파일:+N)를 주면 대조, 안 주면 관측만(탐색 모드).
- 반증 3종 통과: 정확한 기대 → 티 / 기대를 +2로 틀림 → 타(델타 불일치) / 감사장부를 기대에서 빼서 부작용 유발 → 타.
★D006 — 내 검사도구가 정본 IdP를 망가뜨렸다
S2x_부정경로.sh 로 메일
/api/users/password 를 검사하다
kps 비밀번호가 실제로 변경됐다. 전 서비스 통합 로그인이 막혔다.
- 부정경로 검사는 "거부되겠지"를 전제로 진짜 요청을 발사하는 행위다. 게이트가 살아 있으면 무해하지만 게이트가 죽어 있으면(=바로 그 결함을 찾는 중이면) 그대로 실행된다.
- 헷갈렸던 점: 응답이 429라 "막혔다"고 오판했으나 일부가 관통했다. 나중에
인증 실패가 나서야 발견.
- 복구:
/api/auth/bootstrap-reset(127.0.0.1+키파일, 사용 후 키 삭제)로 kps/kps1234 복원. 로그인 정상 확인.
- 도구 수리:
password|passwd|비밀번호|/delete|/remove|/drop|/reset|logout-all|bootstrap 경로는 기본 차단, S2X_ALLOW_DANGEROUS=1 로만 검사. 차단·안전경로 동작 실측 확인.
- 메모리 등재:
feedback_부정경로검사_파괴적부작용.md
현재 체인 (결함 6건 · 미검출 0)
S1 0% · S1x 100% · S2 0% · S2x 100% · S3 66.7% ·
S3x 100% · S4 100% · S5 100%
누적 교훈
- 새 검출도구 4개 중 3개가 자기 결함을 가졌다(S1x 오탐 · S2x 거짓안심 · S2x 파괴부작용). 전부 실사용/반증시험에서 잡혔다.
- 미검사를 통과로 보고하는 것이 최악의 실패 모드(D005).
- 검사 행위 자체가 상태를 바꿀 수 있다(D006). 검사 전후 델타를 뜨면 즉시 잡힌다 — S3x가 있었다면 이 사고를 그 자리에서 발견했을 것이다.
5차: 세션 표준화 + 성능 결함 계열 신설 (S6)
① 체인을 전 세션 표준으로
~/.claude/scripts/결함.sh — 경량 프런트. 검출체인.sh는 한선씨 코어를 매번 컴파일해 758ms, 19+ 세션이 결함마다 부르기엔 비싸다. 기록·조회는 PSV 직독(8ms급), 4상 집계가 필요할 때만 무거운 코어 호출.
결함.sh 기록 "<설명>" [심각도] [잡은단계] ·
누출 <ID> <단계> ·
목록 ·
래칫 ·
판정
- 글로벌
~/.claude/CLAUDE.md에 「검출체인 규칙」 조항 신설(전 세션 필수) — 단계 도구 표 + 철칙 4가지.
- ★또 bash 한글 변수명 함정을 밟았다(기록된 함정인데도 재발). 결함으로 등재.
② D008 — 어떤 단계도 못 잡은 결함이 나왔다(래칫 작동)
게이트웨이 요청당 기본비용 4.24ms(백엔드 0.73ms의
6배). 정적 해부 결과
정책 조회가 전부 선형 탐색:
cors조회 149쌍 ·
캐시조회 266쌍 ·
라우트조회 344쌍 →
요청당 최대 759회 순회, 미등록 도메인이면 매번 전량 순회(미스가 최악). 맵 색인이면 O(1).
- 기존 단계가 전부 못 잡은 이유: 컴파일 통과·200 응답·파일 정상 변경·권한 정상 — 기능은 전부 맞다. 느릴 뿐이다. 기존 단계는 "맞는가"만 보고 "비싼가"는 안 본다.
- 같은 형태를 앞서 두 번 더 만났다:
/api/services 매 요청 date spawn(19.4→3.1ms), 시그니처판정 코어 선형탐색(2분+ → 맵색인).
③ S6_핫패스복잡도.sh 신설
요청 처리 함수와
호출하는 모든 함수(스코프 1단계)에서 ①
체계() 셸 spawn(~5ms/회) ②
동안+길이 선형 순회 ③매 요청
읽기() 를 정적 검출.
- ★초판 결함: 이름 패턴(
조회|검색|찾기|확인)으로만 따라가 정작 계기였던 date spawn을 놓쳤다(그게 서비스_리스너갱신 안에 있었음). 이름은 규약이 아니다 → 호출 전체 추적으로 수리. 반증시험이 잡음.
- ★한계 명시: 선별(screening) 도구다. 과보고한다(로그 tail·통계 ps처럼 특정 라우트에서만 쓰는 정당한 spawn까지 잡음). 확정하려면 (a)모든 요청에서 불리는지 (b)순회 대상 실제 크기 를 봐야 하고, 최종 확정은 부하 낮은 시점의 실측이다. 판정문에 이 안내를 넣었다.
현재 체인 (결함 9건 · 미검출 0)
S1 0% · S1x 100% · S2 0% · S2x 50% · S3 66.7% · S3x 50% · S4 100% · S5 100% ·
S6 100%
(S2x·S3x가 50%로 내려간 건 D008을 못 잡은 누출이 기록됐기 때문 — 정직한 하락이다)
다음
D008
수리(정책 조회 맵 색인화)는 게이트웨이 라이브 코드 변경이라 오너 지시 필요. 수리 후 부하 낮은 시점에 4.24ms가 얼마나 내려가는지 실측.
6차: D008 수리 — 게이트웨이 정책/라우트 색인화
수리 내용 (그림자 검증 완료, 라이브 미배포)
| 파일 | 변경 |
|---|
crownyc/libs/게이트웨이정책코어.한선 | _정책색인 맵 + 정책색인구축(종류,맵,보폭) + 조회 3종 진입점에서 미스를 O(1) 차단 |
crowny-gateway/한선게이트웨이/설정.한선 | _호스트색인 맵 + 호스트색인구축(라우트맵) — 호스트매칭을 히트/미스 모두 O(1) 로 |
crowny-gateway/한선게이트웨이/게이트웨이통합.한선 | 부팅 시 색인 구축 배선(문자열보호 이전) |
설계 원칙:
색인이 없으면 종전 선형 경로로 폴백 — 부분 배포·구버전 호출자에도 안전. 맵은 제자리변경이라 전역 스칼라 재대입 크래시 함정과 무관.
부팅 로그 실측: 정책색인 cors 149 · cache 266 · rl 149 도메인 / 호스트색인 344 도메인
그림자 A/B 실측 (18090 색인 vs 18091 구판, 교대 측정)
| 경로 | 구판 | 색인 | 개선 |
|---|
| 미스(미등록 도메인 404) = 게이트웨이 자체 처리 | 최소 4.31ms | 3.14ms | 27.2% |
| 히트(cloud.crowny.org 프록시) | 최소 13.98ms | 13.87ms | ~1%(노이즈 범위) |
해석: 게이트웨이
자체 오버헤드는 27% 줄었다(요청마다 1.17ms). 프록시 요청은 업스트림 왕복이 지배해 체감이 작다. cors/cache는 히트일 때 여전히 선형(도메인당 항목이 적어 실익 낮음 — 의도적 미최적화).
동작 동일성 (5도메인, 응답코드+바이트+본문 cmp)
cloud 200:92 · quote 200:92 · mail 429:39 · dex 404:9 · zzz-nomap 404:13 —
전부 동일라이브 배포 보류 사유
게이트웨이 재기동은
전 서비스 순단을 유발한다(모든 도메인이 :8080 경유). 오너 확인 후 조용한 시간대에 배포 권장. 라이브는 무접촉 확인(프로세스 시작시각 13:54 유지, 공개 HTTPS 200).
7차: D008 라이브 배포 — 이미 적용돼 있었다
배포하려고 보니 이미 라이브였다. Q감독(2워커 REUSEPORT)이 17:06에 자동 재기동하면서 start.sh가 소스 변경을 감지해 재컴파일·적용한 것. 워커 로그에 색인 구축이 찍혀 있었다:
통합: 정책색인 cors 149 · cache 266 · rl 149 도메인
통합: 호스트색인 344 도메인 (라우트 선형 344회 → O(1))
start.sh 직접 실행은 권한 게이트가 차단(전 서비스 영향). 대안으로 무중단 롤링(워커 1개씩 교체)을 준비했으나 불필요했다.
- 세 바이트코드(
gw_색인2.toau / gwlive.toau / 롤백본)가 전부 1574552 bytes 동일 — 롤백본을 뜬 시점(17:08)에 이미 새 판이었다는 뜻. "롤백본"이 실은 신판이었다 — 진짜 구판 롤백이 필요하면 .bak-색인-20260731 소스에서 재컴파일해야 한다.
라이브 검증
| 항목 | 결과 |
|---|
| 전 도메인 회귀 | cloud·quote·dex·mail·nexus·warp·deacon 7/7 200 |
| 미스경로 실측 | p25 3.80 · p50 5.15 · 최소 3.57ms (그림자 색인판 3.14ms 범위 일치, 구판 4.31ms) |
| 워커 | 2워커 REUSEPORT 정상, 순단 없음 |
교훈
배포 전에 "이미 배포됐는지" 먼저 확인하라. 자동 재기동 인프라(Q감독·launchd KeepAlive·워치독)가 있는 환경에서는 소스를 고친 순간부터 다음 재기동에 자동 반영된다. 그걸 모르고 배포 절차를 밟으면 중복 작업이거나, 더 나쁘게는 "롤백본"이라며 신판을 백업하게 된다(이번 사례).
8차: warp 43만 핸들 — 전제가 틀렸다 (D012)
VM 소스 + 실증으로 확인한 진실
[STR] 조기경고 432000/480000(90%) 을 위험 지표로 삼은 것 자체가 오독이었다.
crownyc.c: int id = str_count++ — 단조 증가 ID 할당자다. 생존 집합 크기가 아니다.
static int str_warn90_fired (2510행) — 프로세스당 1회만 발화.
- 죽는 조건은 별개: 480000 도달 시 먼저
str_gc() 를 돌리고, 그래도 476904 이상일 때만 exit43. GC가 안전판.
실증 (
/tmp/핸들시험.한선, 생존 문자열 내내 1개):
| 단계 | 누적 할당 | 결과 |
|---|
| 1차 20만 회 | ~43만 | 경고 발화(생존집합 1개인데도) |
| 2차 20만 회 | ~83만 | 경고 없음(1회성) |
| 3차 GC 후 20만 회 | ~123만 | 경고 없음, 정상 생존 |
내가 두 번 틀렸다
- "warp·넥서스분기 90% 경고 중이라 가장 급하다" → 현재 상태가 아니라 과거 1회 사건.
- 정정한다며 "warp=부팅 생존셋 / 넥서스분기=경고 2건이니 진짜 누적" → 또 틀림. 2건은 프로세스 2회 수명일 뿐.
보강 실측: warp 방어 배선 후
16시간 29분 가동, RSS 4MB. 43만 개가 살아 있을 수 없는 크기다.
S7_장수서버건강.sh 신설 — 진짜 신호로 판정
STR 경고를 판정에서
배제하고(참고 표시만) 다음을 본다: ①RSS 절대값(VM 힙 144MB 캡 대비) ②exit43 실제 발생 이력 ③재기동 빈도 ④방어 배선 여부(STR_PROTECT 로그).
전 서비스 스윕 결과: RSS 4~8MB (캡의 3~6%) · exit43 이력 0건. 메모리 위험은 애초에 없었다.
옴 판정은 전부 "로그를 못 찾아 판단 불가" — 허위 안심 대신 정직한 판단보류(D005 교훈 적용).
결론
warp의 "43만 핸들 생존셋 축소" 과제는
존재하지 않는 문제였다. 문자열 방어 배선(문자열보호+주기 GC)은 비용이 거의 0이고 카운터를 낮게 유지하니 그대로 두되,
우선순위에서 내린다.
로컬 클라우드의 진짜 제약은 여전히
단일스레드 순차 accept(요청당 처리시간이 곧 처리량 상한)이다.