Skip to content

Commit 8c74bba

Browse files
authored
Merge pull request #88 from Jun025/feat/rustjava-string-array-overflow-investigation
[rustjava-error-path-string-array-hiding-overflows-stack-p1] docs(jvm): cap 은 이 재귀를 «끝낸다» — 지난 회차 원인 기재를 반증한다
2 parents e6f923b + 8c8ecf5 commit 8c74bba

5 files changed

Lines changed: 160 additions & 5 deletions

File tree

‎REPORT.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
# REPORT
2+
## [2026-09-20] `[Ljava/lang/String;` 오버플로 — ★**원인을 찾았고, 지난 회차 기재가 «틀렸다»** (rustjava-error-path-string-array-hiding-overflows-stack-p1)
3+
- 무엇을: 채택 제안 `2026-09-20-error-path-class-closure#p1`(★**조사 회차** — 제품 코드 **0줄**). cap 20 에서 `stack overflow, aborting`(rc 134)을 **재현**하고 원인을 규명했다.
4+
- ★★**결론 — 지난 회차의 원인 기재 「its recursion does not come back through the loader, so the cap cannot end it」은 «반증»됐다.** 매 턴 로더로 **돌아오고**, cap 이 **끝낸다** — 살아남은 전 실행에서 `asked = cap + 1`(= `[C` 와 **같은 모양**). 넘치는 이유는 모양이 아니라 ★**턴당 비용**이다.
5+
- ★**두 손잡이가 «따로» 임계를 움직인다**(무한 재귀면 둘 다 안 움직인다 ⇒ 가설이 갈린다): ⒜cap(스택 2 MiB 고정) **18 생존 ↔ 19 오버플로** ⒝스택(cap 20 고정) **2 MiB 오버플로 ↔ 4 MiB 생존**(asked=21 · 8/32/128 MiB 도 생존).
6+
- ★**순환을 «백트레이스로» 보였다**(추측 아님 · 계측은 되돌렸다): `fillInStackTrace` → `instantiate_array("Ljava/lang/String;")` → `resolve_class` → `load_class`(None) → `exception` → `new_class` → **`invoke_special` ×4**(`NoClassDefFoundError`→`LinkageError`→`Error`→`Throwable.<init>`) → `init_with_message` → `invoke_virtual` → `fillInStackTrace`. ★**한 턴 ≈ 57 프레임** — `[C` 는 `from_rust_string` **안에서** 도는 짧은 경로라 같은 스택에 20턴이 들어간다.
7+
- ★**「이 타입이 특별한가」를 대조로 답했다**(cap 20): `[C` 재귀·생존 · `[B` 는 `bootstrap_classes` 소속이라 **형제 `#p0` 의 결함** · `[I`·`[Ljava/lang/Object;`·`[Ljava/lang/StackTraceElement;`·`java/lang/Integer`·`java/lang/StringBuilder` 는 **asked=0**(구성이 묻지도 않는다). ⇒ ★**「배열이면 난다」가 아니다** — 오류 경로가 실제로 필요로 하는 **두 배열**만 돈다.
8+
- ★**cap 20 의 출처**: `test_error_path_class_sweep.rs` 의 `const GIVE_UP_AFTER: u32 = 20` → `test_jvm_hiding` → `HidesOneClass`. ★**테스트 전용**이고 **제품에는 상한이 없다**.
9+
- ★**고친 것 = 스윕 헤더의 거짓 문장 하나.** 배열 제외는 **그대로** 둔다(이유는 원래부터 「로더가 **합성**하므로 어떤 클래스 집합도 배열을 못 빠뜨린다」였고 이번 측정과 무관하게 유효하다). ★**개악 양방향은 «해당 없음»** — 동작을 바꾸지 않았다.
10+
- ★**대가**: 새 잠금 0. 순환은 **제품에 바닥이 없는 채로 남는다**(오늘 닿을 수 없을 뿐) ⇒ 후속 **1건**: **`Jvm::exception` 에 바닥을 주라**(M). 상세 = `docs/worklog/2026-09-20-string-array-hiding-overflows-stack.{md,json}`.
11+
- 검증: DoD 9명령 rc 0 · 실행 31회(전부 1초 미만) · 빌드 1회(3m26s · 워밍된 target 재사용).
12+
213
## [2026-09-20] 없는 부트스트랩 클래스의 «이름»을 말하게 했다 (rustjava-error-path-name-the-missing-bootstrap-class-p0)
314
- 무엇을: 채택 제안 `2026-09-20-error-path-class-closure#p0`. `Jvm::new` 의 `bootstrap_classes` 루프가 `.unwrap()` 으로 죽어 **어느 클래스가 없는지 말하지 않던** 것을 `let … else` 로 바꿔 ⑴이름 ⑵여섯 중 하나라는 것 ⑶★**어디에 물었는지**(= `Jvm::new` 에 넘긴 **호스트 자신의 부트스트랩 로더**이지 클래스패스가 **아니다** — `java.class.path` 는 시스템 클래스로더 몫이고 그건 이 함수 뒤쪽에서 만들어져 여기서 실패하면 안 돈다)를 말하게 했다.
415
- 왜 지금: 지난 회차 스윕이 「이름을 대고 거절 12건」으로 셌는데 ★**그중 5건의 실제 문면이 `called Option::unwrap() on a None value`** 였다. 구멍 난 호스트는 **여섯을 이분**해야 했고, 그중 둘(`Object`·`Serializable`)은 오류 경로 폐포에도 있어 ★**어느 루프에 먼저 걸리느냐로 문면의 품질이 갈렸다**.

‎STATE.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
(둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다)
88

99
## 완료
10+
- [rustjava-error-path-string-array-hiding-overflows-stack-p1] ★★**`[Ljava/lang/String;` 오버플로의 원인 — 지난 회차 기재가 «틀렸다».** 채택 제안 `2026-09-20-error-path-class-closure#p1` · ★**조사 회차 · 제품 코드 0줄**.
11+
★**재현**: cap 20 · 기본 스택 → `stack overflow, aborting` rc **134**.
12+
★★**반증**: 「로더로 안 돌아와 cap 이 못 끝낸다」는 **거짓** — 살아남은 전 실행에서 `asked = cap+1`(= `[C` 와 같은 모양). 원인은 ★**턴당 비용**.
13+
★**두 손잡이가 따로 움직인다**(가설 분리): cap **18↔19** · 스택 **2 MiB↔4 MiB**(cap 20 고정).
14+
★**순환을 백트레이스로**: `fillInStackTrace`→`instantiate_array`→`resolve_class`→`load_class`(None)→`exception`→`new_class`→`invoke_special`×4→`Throwable.<init>`→`fillInStackTrace` · ★**한 턴 ≈ 57 프레임**.
15+
★**대조**: `[C` 만 같이 돈다 · `[I`·`[Ljava/lang/Object;`·`java/lang/Integer` 등은 **asked=0** ⇒ **「배열이면 난다」가 아니다**.
16+
★**고침 = 스윕 헤더의 거짓 문장 하나** · 배열 제외는 유지(이유가 다르고 여전히 유효) · **개악 양방향 해당 없음**(동작 무변경).
17+
★**남긴 것**: 순환은 제품에 **바닥이 없다**(오늘 닿을 수 없을 뿐) ⇒ 후속 카드 「`Jvm::exception` 에 바닥을」(M).
1018
- [rustjava-error-path-name-the-missing-bootstrap-class-p0] ★★**없는 부트스트랩 클래스의 «이름»을 말하게 했다.** 채택 제안 `2026-09-20-error-path-class-closure#p0`.
1119
★`Jvm::new` 의 `bootstrap_classes` 루프 `.unwrap()` → `let … else` · 문면 = 이름 + 여섯 중 하나 + ★**어디에 물었는지**(호스트 로더이지 `java.class.path` 가 **아니다**).
1220
★**가족을 세고 골랐다**: 이 축 **1곳 고침** ↔ 같은 «모양»의 레지스트리 조회 **4곳**은 다른 축(내부 불변식)이고 스윕이 **닿지 않음을 실측**(익명 거절 0/37) ⇒ **blind spot 으로 적고 두었다**.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"date": "2026-09-20",
3+
"taskId": "rustjava-error-path-string-array-hiding-overflows-stack-p1",
4+
"summary": "Adopted 2026-09-20-error-path-class-closure#p1, an investigation. Hiding [Ljava/lang/String; at a cap of 20 does abort with `stack overflow` -- reproduced -- but the reason recorded last round is wrong. The recursion does come back through the loader on every turn and the cap does end it; what differs from [C is the cost of one turn. The sweep's doc comment said the opposite and is corrected; no product code changed.",
5+
"decision": "Cause found, nothing to fix in the product. The cycle is Throwable.fillInStackTrace -> instantiate_array(\"Ljava/lang/String;\") -> resolve_class -> load_class -> exception -> new_class -> Throwable.<init> -> fillInStackTrace, and it is bounded by whatever ends the failure. No host can enter it through a class set, because the bootstrap loader synthesises array classes. The false sentence in the sweep's header is the only thing this round changes.",
6+
"measurements": {
7+
"cited_tree": "origin/main 97707f26",
8+
"reproduced": "yes -- cap 20, default stack: `thread 'probe' has overflowed its stack` / `fatal runtime error: stack overflow, aborting`, rc 134",
9+
"where_the_cap_comes_from": "GIVE_UP_AFTER: u32 = 20, a const in jvm/tests/test_error_path_class_sweep.rs, passed to test_utils::test_jvm_hiding -> HidesOneClass.give_up_after. Test-only; the product has no such cap",
10+
"cap_axis_default_2MiB_stack": {
11+
"0": "survived, asked=1, construction OK (cap 0 never hides)",
12+
"1": "survived, asked=2", "2": "survived, asked=3", "3": "survived, asked=4", "5": "survived, asked=6",
13+
"10": "survived, asked=11", "11": "survived, asked=12", "12": "survived, asked=13", "13": "survived, asked=14",
14+
"14": "survived, asked=15", "15": "survived, asked=16", "16": "survived, asked=17", "17": "survived, asked=18",
15+
"18": "survived, asked=19", "19": "STACK OVERFLOW", "20": "STACK OVERFLOW"
16+
},
17+
"threshold_cap": "18 survives / 19 overflows",
18+
"stack_axis_at_cap_20": { "2MiB": "overflow", "4MiB": "survived, asked=21", "8MiB": "survived, asked=21", "32MiB": "survived, asked=21", "128MiB": "survived, asked=21" },
19+
"asked_is_always_cap_plus_one": "every surviving run, which is the same shape [C shows at cap 20 (asked=21)",
20+
"frames_per_cycle": "~57 backtrace frames between one fillInStackTrace and the next",
21+
"contrast_at_cap_20": {
22+
"[C": "recursed, asked=21 (survives -- cheaper turn, reached inside from_rust_string)",
23+
"[Ljava/lang/String;": "stack overflow at 2 MiB, asked=21 at 4 MiB",
24+
"[B": "panics (it is in bootstrap_classes -- that is the sibling #p0 defect, not this one)",
25+
"[I, [Ljava/lang/Object;, [Ljava/lang/StackTraceElement;, java/lang/Integer, java/lang/StringBuilder": "asked=0, construction succeeds -- construction never asks for them"
26+
},
27+
"runs": "31 process runs total, each a sub-second JVM construction; one 3m26s build reusing the warm target dir of the main checkout instead of a ~11m cold build"
28+
},
29+
"verification": {
30+
"hypotheses_separated_by_experiment": "unbounded recursion would be insensitive to both knobs. Both knobs move the outcome independently (cap 18/19 at fixed stack; 2 MiB/4 MiB at fixed cap 20), so the recursion is bounded and merely deep",
31+
"cycle_shown_not_inferred": "backtrace from a scratch panic at the 3rd entry to Throwable::fill_in_stack_trace, RUST_BACKTRACE=1: fill_in_stack_trace -> instantiate_array -> resolve_class -> resolve_class_internal -> load_class -> exception -> new_class -> invoke_special x4 (NoClassDefFoundError -> LinkageError -> Error -> Throwable) -> init_with_message -> invoke_virtual -> fill_in_stack_trace. The scratch instrumentation was reverted; nothing of it is committed",
32+
"bidirectional": "not applicable -- no behaviour was changed. The only change is a doc comment that was false; the measurements above are what makes it true",
33+
"dod": "all 9 DoD commands rc 0"
34+
},
35+
"changes": [
36+
"jvm/tests/test_error_path_class_sweep.rs: the header paragraph claiming the cap cannot end this recursion is replaced by what was measured. Arrays stay excluded, for the reason that was always the real one -- no class set can lack a synthesised class"
37+
],
38+
"issues": [
39+
"The cycle has no floor in the product: the cap that ends it is a test-only device. It is unreachable through a class set, but that is an argument about one axis, not about the cycle. Recorded as proposal p0.",
40+
"The frame count (~57) and the 2 MiB stack are this host's numbers on a debug build. A release build inlines and an OS with a different default thread stack moves the threshold; the shape does not move, which is the part that matters."
41+
],
42+
"adoptedProposals": [
43+
"2026-09-20-error-path-class-closure#p1"
44+
],
45+
"proposals": [
46+
{
47+
"title": "Give Jvm::exception a floor, so a failure while reporting a failure cannot recurse",
48+
"plainSummary": "If something goes wrong while the runtime is building an error message, it can start building another one, and so on until the process dies.",
49+
"userBenefit": "A host sees the first failure reported instead of a process that aborts with `stack overflow` and says nothing about what actually went wrong.",
50+
"why": "Measured this round: Jvm::exception -> new_class -> Throwable.<init> -> fillInStackTrace -> instantiate_array -> resolve_class -> load_class -> Jvm::exception is a closed cycle, ~57 stack frames per turn, with nothing in the product that ends it -- the cap that ends it in tests is a test-only device in test_utils::HidesOneClass. The class-set axis is already closed (Jvm::new walks the error path's closure, and array classes are synthesised so no class set can lack one), which is why this is not reachable today. But that closes one entrance, not the cycle: any failure raised from inside exception construction re-enters it.",
51+
"tradeoff": "A re-entrancy guard has to decide what to return when it trips, and the honest answer is a non-Java error -- which `JavaError` cannot currently express (single variant, carries a ClassInstance). That is the same obstacle the sibling round hit, so the two should probably be decided together rather than each inventing an answer.",
52+
"effort": "M",
53+
"target": "jvm/src/jvm.rs, jvm/src/error.rs"
54+
}
55+
]
56+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# 2026-09-20 — `[Ljava/lang/String;` 를 숨기면 왜 스택이 넘치나 (조사)
2+
3+
채택 제안 `2026-09-20-error-path-class-closure#p1` (티켓 `rustjava-error-path-string-array-hiding-overflows-stack-p1`).
4+
5+
## 재현 — 했다
6+
`origin/main 97707f26` · cap **20** · 기본 스택(2 MiB) ⇒
7+
`thread 'probe' has overflowed its stack` / `fatal runtime error: stack overflow, aborting` · **rc 134**.
8+
9+
## cap 20 은 어디서 오는 수인가
10+
`jvm/tests/test_error_path_class_sweep.rs` 의 **`const GIVE_UP_AFTER: u32 = 20`** →
11+
`test_utils::test_jvm_hiding(name, give_up_after)` → `HidesOneClass.give_up_after`.
12+
★**테스트 전용 상수**다 — 제품에는 이런 상한이 **없다**.
13+
14+
## ★★결론 — 지난 회차가 적은 원인은 «틀렸다»
15+
> 지난 회차 문안: 「its recursion does not come back through the loader, so the cap cannot end it」
16+
17+
★**반증됐다. 매 턴 로더로 돌아오고, cap 이 끝낸다** — 살아남은 모든 실행에서 `asked = cap + 1`
18+
(= `[C` 가 보이는 바로 그 모양). 넘치는 이유는 **모양이 아니라 턴당 비용**이다.
19+
20+
**두 손잡이가 «따로» 임계를 움직인다** — 이것이 「무한」과 「유한하지만 깊다」를 가른다:
21+
22+
| 손잡이 | 고정 | 결과 |
23+
|---|---|---|
24+
| cap | 스택 2 MiB | **18 생존**(asked=19) ↔ ★**19 오버플로** |
25+
| 스택 | cap 20 | 2 MiB **오버플로** ↔ ★**4 MiB 생존**(asked=**21**) · 8/32/128 MiB 도 생존 |
26+
27+
무한 재귀였다면 **둘 중 어느 것도** 결과를 바꾸지 못한다.
28+
29+
## 순환 — 추측이 아니라 백트레이스
30+
`Throwable::fill_in_stack_trace` 3회 진입 시점에 스크래치 패닉 + `RUST_BACKTRACE=1`(계측은 되돌렸다):
31+
32+
```
33+
fill_in_stack_trace
34+
└ instantiate_array("Ljava/lang/String;")
35+
└ resolve_class → resolve_class_internal → load_class ← 로더가 None(숨김)
36+
└ exception("java/lang/NoClassDefFoundError", …)
37+
└ new_class → invoke_special ×4
38+
(NoClassDefFoundError → LinkageError → Error → Throwable.<init>)
39+
└ init_with_message → invoke_virtual "fillInStackTrace"
40+
└ fill_in_stack_trace ← 같은 자리, 다음 턴
41+
```
42+
한 턴 ≈ **57 프레임**. `[C` 는 `JavaLangString::from_rust_string` **안에서** 도는 훨씬 짧은 경로라
43+
같은 2 MiB 에 20턴이 들어간다. ⇒ ★**`[Ljava/lang/String;` 는 「배열이라서」가 아니라
44+
「오류 경로의 «더 깊은 곳»에서 필요해서」 비싸다.**
45+
46+
## 이 타입이 특별한가 — 대조로 답한다 (cap 20)
47+
| 숨긴 이름 | 결과 |
48+
|---|---|
49+
| `[C` | 재귀 · asked=**21** · 생존 |
50+
| `[Ljava/lang/String;` | 재귀 · 2 MiB **오버플로** / 4 MiB asked=**21** |
51+
| `[B` | `Option::unwrap()` 패닉 — ★`bootstrap_classes` 소속이라 **형제 `#p0` 의 결함**이다(이 회차 아님) |
52+
| `[I` · `[Ljava/lang/Object;` · `[Ljava/lang/StackTraceElement;` · `java/lang/Integer` · `java/lang/StringBuilder` | asked=**0** · 구성 성공 — 구성이 아예 묻지 않는다 |
53+
54+
⇒ ★**「배열이면 난다」가 아니다.** 오류 경로가 실제로 필요로 하는 **두 배열**만 돈다.
55+
56+
## 고쳤나
57+
★**제품 코드 0줄.** 고친 것은 **스윕 헤더의 거짓 문장 하나**다 — 위 측정이 그것을 참으로 만든다.
58+
배열 제외는 **그대로 둔다**: 이유는 원래부터 「로더가 **합성**하므로 어떤 클래스 집합도 배열을
59+
빠뜨릴 수 없다」였고, 그 이유는 이번 측정과 **무관하게** 유효하다.
60+
61+
## 왜 지금인가 · 대가
62+
- **왜 지금**: 그 거짓 문장이 **제품 트리에 커밋돼** 있었고, 「이 축은 잴 수 없다」고 선언해
63+
★**다음 회차가 재보지 않게 만드는** 형태였다.
64+
- **대가**: 조사 회차라 새 잠금은 없다. 순환 자체는 **제품에 바닥이 없는 채로 남는다** —
65+
오늘 닿을 수 없을 뿐이다(제안 카드).

‎jvm/tests/test_error_path_class_sweep.rs‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,26 @@
2323
//!
2424
//! Array names are skipped, and the reason is not squeamishness: the bootstrap loader *synthesises*
2525
//! them (`define_array_class`), so no class set can be missing one and hiding them measures a loader
26-
//! that refuses to build an array rather than a gap a host can have. It is not a free skip -- hiding
27-
//! `[C` recurses (21 questions at a cap of 20) and hiding `[Ljava/lang/String;` overflows the stack
28-
//! *at that same cap*, i.e. its recursion does not come back through the loader at all, so the cap
29-
//! cannot end it. That is the one candidate class this sweep cannot run in-process, and it is out of
30-
//! reach of the class-set axis, so it is recorded here rather than guarded.
26+
//! that refuses to build an array rather than a gap a host can have. It is not a free skip -- two of
27+
//! them recurse. `[C` is reached through the message (`JavaLangString::from_rust_string`) and
28+
//! `[Ljava/lang/String;` through `Throwable`'s `stackTrace` field, which `fillInStackTrace` fills
29+
//! with `instantiate_array` while the exception reporting the *first* gap is still being built.
30+
//!
31+
//! Hiding `[Ljava/lang/String;` at this cap aborts with `stack overflow`, and the round that first
32+
//! saw that wrote down the wrong reason -- "its recursion does not come back through the loader, so
33+
//! the cap cannot end it". Measured since (`rustjava-error-path-string-array-hiding-overflows-stack-p1`):
34+
//! it does come back, every turn, and the cap ends it exactly like `[C`. Two knobs move the
35+
//! threshold independently, which is what tells bounded-but-deep apart from unbounded: at the
36+
//! default 2 MiB test-thread stack it survives a cap of 18 (19 questions) and overflows at 19, and
37+
//! at this cap of 20 it overflows on 2 MiB but survives on 4 MiB with `asked = 21` -- the same
38+
//! `cap + 1` shape `[C` shows. The difference between the two is cost per turn, not shape: one turn
39+
//! of the `[Ljava/lang/String;` cycle is ~57 stack frames because it runs the whole exception
40+
//! construction (`exception` -> `new_class` -> four nested `invoke_special` for
41+
//! `NoClassDefFoundError` -> `LinkageError` -> `Error` -> `Throwable.<init>` -> `fillInStackTrace`),
42+
//! where `[C` turns inside `from_rust_string`.
43+
//!
44+
//! So arrays stay skipped for the reason at the top of this paragraph -- no class set can lack one --
45+
//! and not because anything here is unmeasurable.
3146
3247
use std::{
3348
future::Future,

0 commit comments

Comments
 (0)