크라우니클라우드 대용량 JSON 본문/파일 I/O — 버퍼 기반 전환 (P2-⑧)
개요
cloud.crowny.org(:9611)의 /api/merge·/api/store·/api/files 라우트가 VM 문자열
65535B 하드캡(옛 본문읽기() 내부 64000B 체크, 읽기() FREADALL 고정버퍼)에 걸려
16KB~154KB 본문 POST가 전부 400으로 실패하던 문제를 버퍼 opcode(블롭스트림.한선의
업로드 패턴과 동일 계열) 기반으로 수정. 작업 중 캡 제거 자체보다 훨씬 심각한
두 가지 잠복 O(n²) 버그와 VM 해시맵의 문자열-값 저장 결함을 실측으로 발견·수정.
무엇을 했는지
1. 본문 수신 — 본문버퍼읽기(연결, 원시, maxBytes) 신설
- 옛
본문읽기()는 TCP 청크를누산 = 누산 + 청크문자열+(STR_CAT)로 누적 →
crownyc.c case 340이 la+lb >= STR_MAX_LEN*2(131072B)에서 조용히 핸들 -1 반환,
이후 누적이 통째로 소실. 게다가 그 위에 예상길이 > 64000 하드캡이 별도로
걸려 있어 호출자가 넘긴 maxBytes(store 524288, merge 4194304)와 무관하게
64KB 초과 본문을 전부 거부(400)하고 있었다.
- 신규 함수는
버퍼생성/버퍼추가(399, memcpy+동적 realloc, 캡 없음)로 누적 후
버퍼문자열(396, str_new 직접 호출 — 로컬 고정버퍼 없음, 풀 동적성장)로
변환. 읽기횟수 상한도 고정 64회 → 예상길이/4096 + 32(크기 비례)로 교체.
Content-Length 미달 시 빈 문자열 반환(원자성 보장은 유지).2. 파일 재읽기 — 파일읽기버퍼(경로) 신설
읽기()(FREADALL, opcode 249)는char rbuf[STR_MAX_LEN](65536B) 고정 스택버퍼에
fread하는 구현이라 65535B 초과 파일은 에러 없이 조용히 절단됨(merge/store/files
GET에서 저장된 JSON을 되읽을 때 대형 파일이 깨진 채 응답).
버퍼파일읽기(874, BUF_FILE_READ —fseek(SEEK_END)로 실제 크기 확보 후 그 크기만큼
fread, 캡 없음) + 버퍼문자열 1회 변환으로 교체. merge GET·POST의 기존내용 읽기,
files처리 전부 이걸로 전환.3. 응답 — JSON200버퍼(연결, 데이터) 신설
- 기존
JSON200()도"HTTP 헤더..." + 데이터로 헤더+본문을 STR_CAT 결합 — 데이터가
TCP쓰기(494, write(fd,str_ptr,str_len) 직접 — 이 자체는 캡 없음 확인)로 교체.4. merge 저장 — 버퍼파일쓰기 직결
- 저장 JSON(
{"key":...,"items":...,"deleted":...})도 버퍼로 결합 후
버퍼파일쓰기(857, BUF_FILE_WRITE — 자체 .tmpPID+rename 원자저장 내장) 직결로
변경. 기존의 수동 쓰기(tmp,...) + 체계("mv -f ...") 셸 왕복 제거.5. (실측 도중 발견) JSON 파싱 헬퍼의 O(n²) — JSON배열추출/항목수집 재작성
- 위 캡 제거만으로는 3000항목(~283KB) merge POST가 완료되지 않고 무한정 hang
JSON배열추출/항목수집이
글자(json, i)로 문자 인덱스를 순회하는데, 글자()(STR_GETC)는 매 호출마다
utf8_charcount로 문자열 처음부터 오프셋을 재계산(O(pos), crownyc.c 확인) —
깊이추적 루프가 배열 전체를 훑으므로 전체가 O(n²)로 폭발(283937² ≈ 8×10¹⁰).
이전엔 본문 캡(64KB)이 이 함정을 가려왔던 것.
문자열버퍼(397, BUF_FROMSTR)로 1회 변환 후버퍼바이트읽기(851, BUF_GETBYTE,
버퍼잘라+버퍼문자열(O(원소길이))로 교체. 전체 O(n)화.6. (실측 도중 발견) LWW dedup의 O(n²) — 원소인덱스/툼스톤판정 우회
- 위 수정 후에도 3000항목 요청이 CPU 풀가동 상태로 2분+ 계속됨. 원인: LWW 병합의
원소인덱스(cid, 결과id, 결과수)로 매 항목마다 지금까지 쌓인
결과배열을 처음부터 선형탐색(항목수 n에 대해 O(n²), 3000개면 최대 450만 비교)
하고 있었다. 툼스톤판정도 삭제목록 길이만큼 매번 선형탐색(O(n×nd)).7. (실측 도중 발견) VM 해시맵의 문자열-값 저장 결함 — 정수 인덱스 맵으로 우회
- 1차 수정:
맵있나/맵꺼내/맵넣어(751/414/413, 3진 해시 O(1) 평균)로 id별 dedup을
맵넣어(robj맵, id, cobj)).
단독 O(n²) 문제는 해결됐으나(3000항목 1.8초), 3000개 규모 문자열-값 맵에서
드물게(체감 30~50%/요청) 맵있나()==1인데 맵꺼내()가 빈 문자열/손상값을 반환
하는 현상을 반복 실측(응답 JSON에 },,{ 빈 슬롯 → JSON 파싱 실패). 값 유효성
검증(글자수>0) 안전망을 추가해도 재발 — 안전망 자체가 호출하는 맵꺼내도
동일 결함의 영향을 받는 것으로 추정.
crownyc.c 자체 디버깅/수정은 30+ 서비스가 공유하는 VM 코어라
이번 작업 범위 밖으로 판단해 보류.
ridx맵/didx맵), 실데이터결과id/결과ts/결과obj,
dresid/drests/dresobj)에 저장 — 꺼내()/설정()은 O(1)이고 격리
테스트로 안전함이 확인된 경로. dedup 판단(있음/없음, 어느 인덱스인지)만 맵이
O(1)로 대신하고, 실제 값 저장/조회는 전부 배열로 — 원소인덱스()의 O(n) 선형
탐색만 맵 O(1) 조회로 치환한 셈. 안전망(_병합폴백탐색) 함수는 최종안에서
불필요해져 제거.검증 결과 (라이브 9611, 원문)
- health:
{"status":"ok",...}HTTP 200. - 3000항목(283937B) merge POST 왕복 5회 연속: 전부
HTTP:200,items=3000,
- 중간크기 회귀(이전 400): 16KB(153항목)/32KB(306항목)/60KB(574항목)/150KB
HTTP:200, item수 일치, 0.1~0.85초.
- 대용량: 884KB(3000항목, padding)
HTTP:2004.5초. 1.78MB(3000항목)
HTTP:200 9.7초. 4.38MB(>4MB 상한) → HTTP:413 {"error":"too large (>4MB)"}
즉시 거부(1ms).
- files GET 왕복: merge 3000항목 POST 후
/api/filesGET →items=3000
- merge GET(?key=) 왕복: 3000항목 POST 후 명시적 GET →
items=3000, 283901B
- store 회귀: 소용량 POST/GET 정상, 300KB POST→GET 왕복
blob len=300000
- 真LWW 병합 의미론: 초기 2항목 저장 → 1항목 갱신+1항목 신규+1항목 삭제
- 인증/RCE 화이트리스트 회귀: owner 헤더 없이 POST → 401. key에 셸 메타문자
"; touch ...; echo ") 주입 시도 → 400 "bad key", /tmp/PWNED_p28 파일
생성 안 됨. owner 헤더에 동일 주입 → 401(인증 실패로 처리), 파일 생성 안 됨.
blob 경로 traversal(../../../etc/passwd) → 라우트 미매치로 랜딩페이지 반환
(파일 미노출).
- blob 업로드 회귀: 소용량 업로드 → sha256 id 정상 반환.
- 이웃 생존: 9611/9616/9615/9603 전부
/api/health200 응답.
배포 사고 및 조치 (발견·즉시수정)
재기동 시 nohup env SERVICE=cloud PORT=9611 crownyc run 서비스서버.toau를
cd(서비스 루트) 없이 실행 → 루트 = 현재경로()가 당시 셸의 cwd(빌드 작업 중이던
/Users/ef/CrownyOS/crownyc)로 잘못 고정되어, 첫 6~7회 검증 트래픽의 테스트
데이터가 /Users/ef/CrownyOS/crownyc/data/merge|store/testowner_*에 잘못
기록됨(실제 프로덕션 데이터 /Users/ef/crowny-services/data/는 훼손되지 않음 —
단지 새 쓰기가 엉뚱한 위치로 감). 발견 즉시: ①잘못된 위치의 테스트 디렉토리
전량 삭제 ②cd /Users/ef/crowny-services 후 재기동(cwd 확인:
lsof -p <pid> | grep cwd → /Users/ef/crowny-services 확인) ③전체 검증
1~11 항목을 올바른 루트에서 재실행해 재확인 ④세션에서 생성한 프로덕션 경로의
테스트 데이터(p28final_*, p28smoke_* 등)도 전부 정리. 최종 PID=81063,
SERVICE=cloud PORT=9611, cwd=/Users/ef/crowny-services.
컴파일/재기동 이력
- STRICT 컴파일(
CROWNY_STRICT=1 hanseonc_high) 매 수정마다 실행 — 신규 경고
소켓생성 arity 경고 2건만 잔존, 삽입으로 인한 줄번호 이동뿐
내용 불변 — pre-edit 백업본과 diff로 확인).
- 백업:
서비스서버.한선.bak-대용량IO,서비스서버.toau.bak-대용량IO
/Users/ef/crowny-services/).
- 최종 PID 81063 (v6, 정수-인덱스 맵 설계),
SERVICE=cloud PORT=9611,
/Users/ef/crowny-services.관련 파일
/Users/ef/crowny-services/서비스서버.한선— 본문버퍼읽기/파일읽기버퍼/
/Users/ef/crowny-services/서비스서버.한선.bak-대용량IO— 작업 전 백업./Users/ef/crowny-services/블롭스트림.한선— 참조한 원본 버퍼 업로드/다운로드
/Users/ef/CrownyOS/crownyc/crownyc.c— 근거 확인용으로만 읽음(STR_MAX_LEN,
잔여 이슈 / 한계
- VM 해시맵 문자열-값 저장 결함 미해결:
crownyc.c의HASH_SET/HASH_GET
/private/tmp/claude-501/-Users-ef/fb7b6796-b33d-437e-b3dc-49e63bbdcbf6/scratchpad/맵테스트*.한선
에 3종 남겨둠, 재현 실패했지만 출발점으로 참고 가능).
- merge 대용량 상한 = 4MB(코드상
본문버퍼읽기(..., 4194304)) — 이 이상은
- zip처리 함수도 여전히 옛
본문읽기()(64000B 하드캡 포함)를 사용 중 —
- 원소인덱스()/툼스톤판정() 함수는 정의만 남고 미사용(dead code) — 제거하지
- dedup 성능은 이제 O(n) 수준이나, 각 항목의
JSON필드/JSON숫자필드
크라우니코드 학습
crownycode-brain.sh learn "크라우니클라우드 대용량 JSON 본문 버퍼IO 캡제거" /Users/ef/crowny-services/서비스서버.한선
실행 완료 (lint 판정: 타/셀코어룰엔진).