Skip to content

fix(parser/hwp5): 문단↔표↔셀 상호재귀 깊이 상한 — export-structure 스택 오버플로 DoS (#4827) - #4830

Closed
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:gym_structure_recursion_depth
Closed

fix(parser/hwp5): 문단↔표↔셀 상호재귀 깊이 상한 — export-structure 스택 오버플로 DoS (#4827)#4830
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:gym_structure_recursion_depth

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

무엇을 / 왜

손상된 .hwprhwp export-structure (및 본문을 파싱하는 모든 명령: info·export-text 등)로
처리하면 스택 오버플로로 크래시한다(SIGSEGV — 패닉과 달리 catch_unwind 로 못 잡는 프로세스
종료). HWP5 본문 파서의 문단↔표↔셀 상호재귀에 깊이 상한이 없는 것이 근본 원인이다.

#4822 §2 가 코퍼스 퍼징으로 관측한 export-structure 스택 오버플로의 정밀 원인 규명 겸 수정이다
(#4822 는 "구조 분류(clause/outline) 재귀"로 추정했으나, 실제 무한 재귀는 그 이전 단계인 본문
파싱
에 있다). Fixes #4827.

재귀 경로 (무한 하강)

parse_paragraph            (body_text.rs)
  → parse_ctrl_header       (body_text.rs)
    → parse_control          (control.rs)
      → parse_table_control   (control.rs)
        → parse_cell           (control.rs)
          → parse_paragraph_list (body_text.rs)
            → parse_paragraph    (재귀 ↩)

셀 안의 문단이 다시 표를 품는 사이클이며, 머리말/꼬리말·각주/미주·글상자(GSO)·캡션의 내부 문단도
모두 parse_paragraph_list 를 경유하므로 같은 사이클에 속한다. 레코드 헤더 레벨은 10비트(≤1023)
(record.rs)라 표 한 겹이 레벨을 3 파므로 손상 문서로 최대 ~341겹까지 도달 가능하다. 이 깊이가
스레드 기본 스택 한계 근처라 스택 크기·레이아웃에 따라 크래시하거나 과대 트리를 방출하며 완주한다
#4822 의 "비결정적 크래시 / 3000+ 줄 중첩 JSON" 관측과 일치한다.

수정

이 재귀 계열이 전부 경유하는 parse_paragraph 진입점에 스레드-로컬 RAII 깊이 가드
(SectionDepthGuard) 를 둔다(상한 MAX_HWP5_SECTION_DEPTH = 64). 파라미터를 여러 호출부에
관통시키지 않고 한 곳에서 전 경로를 봉인한다. 상한 초과 시 BodyTextError 로 거부하면, 상위
parse_paragraph_listif let Ok(..)해당 하위 트리만 우아하게 절단(truncate) 하고 나머지는
정상 파싱한다.

이미 형제 파서들이 같은 종류의 상호재귀를 상한으로 봉인해 두었고, HWP5 본문 경로만 빠져 있었다:
HWPX 섹션 MAX_HWPX_SECTION_DEPTH=64 (#4759) · HWP3 그리기 개체 (#4285) · HWP5 묶음 개체
parse_container_children (#4761) · HML max_depth=256. 값 64 는 가장 가까운 형제인 HWPX 섹션
가드와 동일하다.

검증

  • 결정론적 재현(수정 전): 합성 레코드 스트림을 실제 진입점 parse_body_text_section 에 먹여
    재현. 표 250겹 → 파싱 깊이 250(상한 없음 확정); 341겹 + 작은 스택 스레드 →
    thread has overflowed its stack, 종료 코드 0xC00000FD (STATUS_STACK_OVERFLOW).
  • 회귀 테스트 2건 추가 (src/parser/body_text/tests.rs):
    nested_table_recursion_is_depth_capped(상한 초과 → 절단·무크래시),
    shallow_table_nesting_is_preserved(정상 5겹 → 깊이 보존, 과잉 차단 없음).
  • 동작 불변: 정상 샘플 30개 × 3모드(auto/outline/clause) = 90개 export-structure 출력이
    수정 전/후 바이너리에서 바이트 단위로 완전 동일.
  • cargo test --lib 3704 통과 / 0 실패, rustfmt --check·cargo clippy --lib clean.

범위

HWP5 본문(body_text/control)의 표 상호재귀만 다룬다. #4822 §1(파서 무한루프)은 근본 원인이
달라(커서 미전진) 별도 작업 대상이다.

…dwardkim#4827)

손상된 .hwp 를 `export-structure`(및 본문을 파싱하는 모든 명령)로 처리하면
HWP5 본문 파서의 상호재귀에 깊이 상한이 없어 스택 오버플로(SIGSEGV, 패닉과
달리 catch_unwind 로 못 잡음)로 크래시한다. 재귀 경로:

  parse_paragraph → parse_ctrl_header → parse_control → parse_table_control
  → parse_cell → parse_paragraph_list → parse_paragraph (↩)

레코드 레벨은 10비트(≤1023)라 표 중첩이 최대 ~341겹까지 파일로 도달 가능하고,
그 깊이가 스레드 기본 스택 한계 근처라 크래시/완주가 비결정적으로 갈린다
(edwardkim#4822 §2 관측과 일치).

수정: 이 재귀 계열이 전부 경유하는 `parse_paragraph` 진입점에 스레드-로컬
RAII 깊이 가드를 둔다(상한 64, HWPX edwardkim#4759·HWP3 edwardkim#4285·HWP5 묶음개체 edwardkim#4761
형제 가드와 동일 값). 상한 초과 시 BodyTextError 로 거부하면 상위
`parse_paragraph_list` 의 `if let Ok(..)` 가 해당 하위 트리만 절단하고 나머지는
정상 파싱한다. 정상(얕은 중첩) 문서 동작은 불변.

검증:
- 결정론적 재현: 표 250겹 파싱이 상한 없이 250 그대로 내려감(수정 전);
  341겹 + 작은 스택 → STATUS_STACK_OVERFLOW(0xC00000FD).
- 회귀 테스트 2건 추가(상한 초과 절단 / 정상 깊이 보존).
- 정상 샘플 30개 × 3모드 = 90개 export-structure 출력이 수정 전후 완전 동일.
- cargo test --lib 3704 통과·0 실패, rustfmt·clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@jangster77

Copy link
Copy Markdown
Collaborator

통합 PR #4883(4412546)로 병합 완료했습니다.

원 head와 CI를 다시 확인해 누적 반영했고, 상세 검토·메인터너 보정·검증 근거는 archive 검토 기록에 남겼습니다.

중복 병합을 막기 위해 이 원 PR을 닫습니다. 감사합니다.

@jangster77 jangster77 closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants