Skip to content

Commit d9f45eb

Browse files
authored
[rustjava-bound-bootstrap-method-attr-index] feat(classfile): bootstrap_method_attr_index 가 「실재하는」 부트스트랩 메서드를 가리키게 한다 (#47)
[rustjava-bound-bootstrap-method-attr-index] feat(classfile): bootstrap_method_attr_index 가 「실재하는」 부트스트랩 메서드를 가리키게 한다
2 parents f140107 + 0fe10c7 commit d9f45eb

8 files changed

Lines changed: 253 additions & 28 deletions

‎REPORT.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# REPORT
22

3+
## [2026-09-16] `bootstrap_method_attr_index` 가 «실재하는» 부트스트랩 메서드를 가리키게 했다 (rustjava-bound-bootstrap-method-attr-index)
4+
- 무엇을: `Dynamic`/`InvokeDynamic` 상수의 `bootstrap_method_attr_index` 가 **BootstrapMethods 테이블 안**을 가리키는지
5+
검사한다(JVMS 4.4.10·4.7.23). ★**두 축이 한 술어다** — 속성이 «아예 없는» 경우는 «항목 0개짜리 표»여서 어떤 인덱스도 못 가리킨다.
6+
- 왜: 채택 제안 `2026-09-16-bootstrap-methods-and-method-handle#p1` · `2026-09-16-ldc-tags-15-16-17#p0`(서로 다른 회차가 **독립으로** 같은 결함에 닿았다).
7+
- 사용자 영향: ★**진단이 바뀐다** — 그 두 형상이 `UnsupportedOperationException`(「이 런타임이 아직 못 한다」) →
8+
★`ClassFormatError`(「이 파일이 깨졌다」). ★**어떤 JVM 도 못 읽는 파일을 «미지원»이라 부르던 것을 그만둔다.**
9+
- ★★**의도된 «거부 확대»다 — 하류에는 회귀로 보인다.** 지금까지 조용히 「미지원」으로 넘어가던 클래스 파일이 **거부**된다.
10+
★그 전환이 이 회차의 산출물 자체이고, OpenJDK 26 은 같은 파일에 `ClassFormatError: Missing BootstrapMethods attribute` 를 낸다.
11+
- ★**검증 지점을 하나로 모았다**(계약 1): `validate_class` 안의 `bootstrap_method_indices_resolve` **한 함수** ·
12+
★**새 에러 타입 0**(기존 `ClassFileError::InvalidFormat` 에 접었다 — 호출부 변종 증가 0).
13+
★`validate_constant_pool` 이 아니라 `validate_class` 인 이유 = **풀과 클래스 속성을 «둘 다» 쥔 유일한 자리**이고,
14+
그 교차가 이 검사가 여태 없던 이유였다(그 자리의 낡은 주석이 그렇게 적고 「원하는 회차에 맡긴다」고 했다 — 이 회차가 그것이다).
15+
- ★**개악 3종**: 상수 `true` → 새 테스트 red(축 ⒜⒞) · 상수 `false` → **24 테스트** red(축 ⒝⒟) ·
16+
★내부 술어만 `false` → **8 테스트** red이고 `test_hello` 류는 **green** ⇒ ★**⒝ 와 ⒟ 가 갈린다**(`false` 하나로는 둘이 겹친다).
17+
- 검증: `cargo test --all` **568 / 0 failed / 1 ignored**(수 불변 — 테스트 1개를 «치환»했다) · DoD 7줄 rc=0 ·
18+
★픽스처 재생성 **멱등**(기존 10개 바이트 불변 · 신규 1개만 추가).
19+
- 후속 추천: ⑴`BootstrapMethods` 중복 선언 거부(JVMS 4.7.23 은 «최대 1개» — 지금은 첫 것만 본다)
20+
⑵`MethodHandleKind` 의 위치 재판정(형제 티켓) ⑶`StringConcatFactory` 콜사이트 링크(L · 별 티켓).
21+
상세 = `docs/worklog/2026-09-16-bound-bootstrap-method-attr-index.md`.
22+
323
## [2026-09-16] `BootstrapMethods` 를 «구조»로 읽는다 — 콜사이트 링크는 0줄 (rustjava-invokedynamic-bootstrapmethods-and-methodhandle)
424
- 무엇을: `AttributeInfo::BootstrapMethods` 를 **`Vec<u8>` → `Vec<BootstrapMethod>`**(JVMS 4.7.23)로 파싱하고,
525
`CONSTANT_MethodHandle` 을 `MethodHandleRef`(`MethodHandleKind` 9종 + 클래스·이름·서술자)로 **해독**한다.

‎STATE.md‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,39 @@
44
(없음 — 2026-09-16 실측: 착수 시 진행 티켓 0 · 열린 PR 0. ※「열린 PR 0」은 ★**이 회차 PR 착지 시점 기준**이다 — 회신 시점에는 그 PR 자신이 열려 있다)
55

66
## 완료
7+
- [rustjava-bound-bootstrap-method-attr-index] ★★**`bootstrap_method_attr_index` 가 «실재하는» 부트스트랩 메서드를 가리키게 했다 — 「파손」을 되찾았다.**
8+
채택 제안 **둘**을 한 회차가 닫았다(worklog json `adoptedProposals` 에 **전건** 기록):
9+
`2026-09-16-bootstrap-methods-and-method-handle#p1` · `2026-09-16-ldc-tags-15-16-17#p0` —
10+
★**서로 다른 회차가 «독립으로» 같은 결함에 닿았다**(그래서 총괄이 둘을 합쳐 발권했다).
11+
★**전/후**: `UnsupportedOperationException`(「이 런타임이 아직 못 한다」) → ★`ClassFormatError`(「이 파일이 깨졌다」).
12+
★**JVMS 근거**: **4.4.10**(Dynamic/InvokeDynamic 의 `bootstrap_method_attr_index` 는 `BootstrapMethods` 의
13+
`bootstrap_methods` 배열에 대한 «유효한 인덱스»여야 한다) · **4.7.23**(그 상수를 가진 클래스는 그 속성을 «가져야» 한다).
14+
★**참조 JVM**: OpenJDK 26 은 부재 형상에 `ClassFormatError: Missing BootstrapMethods attribute` 를 낸다.
15+
★★**두 축이 «한 술어»다 — 분기를 둘로 만들지 않았다**(티켓 계약 1): 속성 부재 = «항목 0개짜리 표»라
16+
어떤 인덱스도 못 가리킨다 ⇒ `bootstrap_method_count.is_some_and(|count| (index as usize) < count)` 한 줄이 둘을 다 문다.
17+
★**자리 = `validate_class` 의 `bootstrap_method_indices_resolve`** — `validate_constant_pool` 이 아닌 이유는
18+
★**풀과 «클래스 속성»을 둘 다 쥔 유일한 자리**이기 때문이고, 그 교차가 이 검사가 여태 없던 이유였다
19+
(그 자리의 낡은 주석이 스스로 그렇게 적고 「원하는 회차에 맡긴다」고 했다 — ★**이 회차가 그 회차다**).
20+
★**새 에러 타입 0** — 기존 `ClassFileError::InvalidFormat` 에 접었다(계약 2⒞: 늘리는 대신 접을 수 있으면 그쪽이 낫다).
21+
★**픽스처 +1**(`LdcDynamicBSMIndexPastEnd` = 1항목 표에 인덱스 1) — ★**생성기를 통해서만 만들 수 있다**(표를 쓰는 자리가 거기뿐).
22+
★**재생성 멱등 확인**: 기존 10개 **바이트 불변** · 신규 1개만 추가.
23+
★★**개악 3종 — `false` 하나로는 ⒝⒟ 가 «겹친다»**(그래서 셋을 돌렸다):
24+
상수 `true` → 새 테스트 red(**⒜⒞**) · 상수 `false` → **24 테스트** red(⒝⒟ 혼재) ·
25+
★**내부 술어만 `false`** → **8 테스트** red(전건 dynamic 보유 클래스 = **⒝**)이고 `test_hello`·`test_switch`·
26+
`test_odd_even`·`test_superclass` 는 **green**(= **⒟** 무영향) ⇒ ★**두 축이 실제로 갈렸다.**
27+
★`cargo test --all` **568 / 0 failed / 1 ignored**(★**수 불변** — 테스트를 «치환»했다. 수로는 안 보이므로 개악으로 물었다) · DoD **7줄 전건 rc=0**.
28+
★★**남긴 것**: `BootstrapMethods` **중복 선언**은 여전히 거부하지 않는다(JVMS 4.7.23 은 «최대 1개» · 지금은 `find_map` 이 첫 것만 본다) —
29+
★**이 회차 범위 밖이고 후속 추천에 적었다.**
30+
★★**게이트③ 착지 — PR #47 · `--merge`**(등재 repo `contracts/upstream-sync-repos.conf:22` · 스쿼시는 부모 2개를 1개로 접어 계보를 지운다).
31+
게이트② **1회차 approve**(반려 0) · 핀 `3b3667d6` **불이동**(동봉 전 실측 — 로컬·원격·PR head·리뷰 줄2 **4값 일치**) ·
32+
`ci-presence` **rc=0 CI_GREEN** · `mergeable` **MERGEABLE/CLEAN** · 자식 PR **0건** ·
33+
★**배포 워크플로 0개 ⇒ 배포 0**(착지 diff 8파일 · `.github/workflows/` 6개 전건 deploy 어휘 0건).
34+
★★**묶지 «못한» 이유가 이 리니지의 산물이다** — `rustjava` 는 upstream 동기 등재라 묶음 경로에 `merge_strategy:` 를
35+
담을 파일이 없고, 그러면 `bin/queue-lint` 검사22 와 집행 STOP **두 방어선이 «둘 다» 사라진다**
36+
(`orch-upstream-sync-repos-cannot-bundle-gate3-ever`). ⇒ 별 `-merge` 티켓이 그 «선언을 담을 파일»이다.
37+
★★**형제 «둘»이 열려 있다 — 이 착지가 그 둘을 깬다**: **#48**(`StringConcatFactory` 링크 · 게이트② 대기) ·
38+
**#49**(상수풀 태그 pass-through 개악 탐지). ★**셋 다 `tests/test_class_format.rs` + 원장 2파일을 만진다**
39+
(코드 파일은 갈린다 ⇒ **기능 의존 0**). ⇒ ★**그 둘은 각자 base 당기기가 필요하다** — 해소는 그쪽 회차 몫이고 여기서 만지지 않았다.
740
- [rustjava-invokedynamic-bootstrapmethods-and-methodhandle] ★★**`BootstrapMethods` 를 «구조»로 읽는다 — ④-1 의 ⒜ 를 닫았다. ★콜사이트 링크 0줄.**
841
채택 제안 `2026-09-16-cp-tags-15-18-parse#p0`(worklog json `adoptedProposals` 에 기록).
942
★**`AttributeInfo::BootstrapMethods(Vec<u8>)` → `Vec<BootstrapMethod>`** · 신규 공개 타입 3종
@@ -867,7 +900,7 @@ green 전건 rc=0 · `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3
867900
★**안 되는 것(전부 이름으로)**: ⑴클래스 로드 0 ⑵멤버 조회 0(그 메서드가 실재하는지 아무도 안 본다) ⑶접근 검사 0(JVMS 5.4.3.5) ·
868901
⑷★**`java.lang.invoke` 런타임 클래스가 «0개»다**(실측: `rustjava-runtime/src/classes/java/` 에 `invoke` 디렉터리 **부재** · 문자열 참조 **0건**) ⇒ **`MethodHandle` «객체»는 만들 수 없다** ·
869902
⑸종류↔대상 짝짓기는 **`validation.rs` 가 진다**(파서는 일부러 중복하지 않는다 — 테스트로 잠갔다) ·
870-
⑹`Dynamic`/`InvokeDynamic` 의 `bootstrap_method_attr_index` 는 **여전히 배열 크기로 «경계 검사되지 않는다»**(④-2 후속과 한 묶음) ·
903+
⑹`Dynamic`/`InvokeDynamic` 의 `bootstrap_method_attr_index` 는 ★**[2026-09-16 닫힘 · `rustjava-bound-bootstrap-method-attr-index`] 경계 검사된다**(부재 = 0항목 표로 함께 문다) ·
871904
⑺★**부트스트랩 «정적 인자»는 «인덱스 그대로»** 둔다(아래 ★).
872905
★★**⑺ 이 이 회차의 급소다 — 「인덱스를 `ConstantPoolReference` 로 풀어라」는 «회귀»다**(M3 로 측정):
873906
`LambdaMetafactory.metafactory` 의 인자는 **MethodType·MethodHandle·MethodType** 이라 해석을 강제하면

‎classfile/src/validation.rs‎

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
1515
|| class.interfaces.iter().any(|name| !is_internal_class_name(name))
1616
|| !validate_constant_pool(&class.constant_pool)
1717
|| !constant_pool_tags_fit_the_class_file_version(class)
18+
|| !bootstrap_method_indices_resolve(class)
1819
{
1920
return Err(ClassFileError::InvalidFormat);
2021
}
@@ -97,6 +98,42 @@ fn constant_pool_tags_fit_the_class_file_version(class: &ClassInfo) -> bool {
9798
})
9899
}
99100

101+
/// JVMS 4.4.10 and 4.7.23: `bootstrap_method_attr_index` is an index into the `bootstrap_methods`
102+
/// array of the `BootstrapMethods` attribute, and that attribute must be present whenever the pool
103+
/// holds a Dynamic or InvokeDynamic entry. Both halves are the same sentence — the index has to
104+
/// name a real entry — so they are one predicate rather than two: an absent attribute is a table of
105+
/// no entries, which no index can name.
106+
///
107+
/// This lives in `validate_class` rather than `validate_constant_pool` because it is the only place
108+
/// that holds both the pool and the class attributes. That crossing is the whole reason the check
109+
/// did not exist before; the note it replaces said as much and left it to the round that wanted it.
110+
///
111+
/// What this rejects was already being rejected by every real JVM — OpenJDK 26 answers
112+
/// `ClassFormatError: Missing BootstrapMethods attribute` for the absent case. What we answered
113+
/// was `UnsupportedOperationException`, i.e. *this runtime cannot do that yet*, about a file no
114+
/// runtime can read. That sentence is what the lineage exists to make true, so narrowing it here
115+
/// is the point rather than a side effect.
116+
fn bootstrap_method_indices_resolve(class: &ClassInfo) -> bool {
117+
let bootstrap_method_count = class.attributes.iter().find_map(|attribute| match attribute {
118+
AttributeInfo::BootstrapMethods(methods) => Some(methods.len()),
119+
_ => None,
120+
});
121+
122+
class.constant_pool.values().all(|item| {
123+
let index = match item {
124+
ConstantPoolItem::Dynamic {
125+
bootstrap_method_attr_index, ..
126+
}
127+
| ConstantPoolItem::InvokeDynamic {
128+
bootstrap_method_attr_index, ..
129+
} => *bootstrap_method_attr_index,
130+
_ => return true,
131+
};
132+
133+
bootstrap_method_count.is_some_and(|count| (index as usize) < count)
134+
})
135+
}
136+
100137
fn validate_constant_pool(constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> bool {
101138
constant_pool.values().all(|item| match item {
102139
ConstantPoolItem::Class { name_index } => constant_pool
@@ -142,13 +179,9 @@ fn validate_constant_pool(constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> bo
142179
.get(descriptor_index)
143180
.and_then(ConstantPoolItem::utf8)
144181
.is_some_and(|descriptor| is_method_descriptor(&descriptor)),
145-
// The bootstrap method index is still not checked here, but the reason changed:
146-
// `BootstrapMethods` is no longer a byte blob (see `AttributeInfo::BootstrapMethods`), so
147-
// there now *is* something to bound it against — it just is not reachable from this
148-
// function, which only gets the constant pool. Doing it needs `validate_class` to cross
149-
// the pool with the class attributes, and that is a behaviour change (files that parse
150-
// today would start being rejected), so it is left to the round that wants it. Tracked
151-
// alongside the tag-vs-major-version check in `STATE.md` ④-2.
182+
// The bootstrap method index is bounded by `bootstrap_method_indices_resolve`, not here:
183+
// it needs the class attributes, and this function only gets the pool. What is left for
184+
// this arm is the half that the pool alone can answer.
152185
ConstantPoolItem::Dynamic { name_and_type_index, .. } | ConstantPoolItem::InvokeDynamic { name_and_type_index, .. } => {
153186
constant_pool.get(name_and_type_index).and_then(ConstantPoolItem::name_and_type).is_some()
154187
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"schema": "worklog/v1",
3+
"date": "2026-09-16",
4+
"taskId": "rustjava-bound-bootstrap-method-attr-index",
5+
"summary": "Bound bootstrap_method_attr_index against the BootstrapMethods table, and require that attribute when the pool needs it — one predicate, because an absent attribute is a zero-entry table.",
6+
"changes": [
7+
"classfile/src/validation.rs: new bootstrap_method_indices_resolve, called from validate_class; replaces the note that deferred this check",
8+
"test-data/src/ldc/make_ldc_fixtures.py: dynamic() takes attr_index; emits LdcDynamicBSMIndexPastEnd",
9+
"test-data/ldc/LdcDynamicBSMIndexPastEnd.class: new fixture (index 1 into a one-entry table)",
10+
"tests/test_class_format.rs: the deferred assertion flipped — both failure shapes now assert ClassFormatError"
11+
],
12+
"verification": [
13+
"cargo test --all: 568 passed / 0 failed / 1 ignored (count unchanged: a test was replaced, not added)",
14+
"mutation whole-predicate=true -> 1 test red (axes a, c)",
15+
"mutation whole-predicate=false -> 24 tests red (axes b and d, conflated)",
16+
"mutation inner-predicate=false -> 8 tests red, all dynamic-carrying; test_hello/test_switch stay green (axis b alone)",
17+
"fixture regeneration idempotent: 10 existing .class files byte-identical, 1 added",
18+
"DoD 7 commands all rc=0"
19+
],
20+
"issues": [
21+
"BootstrapMethods is still not rejected when declared more than once; JVMS 4.7.23 allows at most one, and find_map takes the first. Out of scope for this round, filed as a proposal below."
22+
],
23+
"adoptedProposals": [
24+
"2026-09-16-bootstrap-methods-and-method-handle#p1",
25+
"2026-09-16-ldc-tags-15-16-17#p0"
26+
],
27+
"proposals": [
28+
{
29+
"title": "Reject a class declaring BootstrapMethods more than once",
30+
"plainSummary": "A class file is only allowed one BootstrapMethods attribute. We currently read the first one and ignore any others.",
31+
"userBenefit": "A hand-built or corrupted class file that carries two bootstrap tables is reported as broken instead of being silently read using whichever table came first.",
32+
"why": "JVMS 4.7.23 says at most one BootstrapMethods attribute may appear in the attributes table of a ClassFile. bootstrap_method_indices_resolve uses find_map, which takes the first and never looks for a second, so the index is bounded against a table that may not be the only one.",
33+
"tradeoff": "Another parse-time rejection of files that currently load, in a shape no compiler emits — so the benefit is narrow and the risk of surprising a real user is correspondingly small. It also needs a fixture the generator cannot currently build, since the attribute list is assembled per fixture.",
34+
"effort": "S",
35+
"target": "classfile/src/validation.rs, test-data/src/ldc/make_ldc_fixtures.py"
36+
},
37+
{
38+
"title": "Bound the static-argument indices of each bootstrap method",
39+
"plainSummary": "Each bootstrap method carries a list of constant pool indices for its arguments. Nothing checks those point at real pool entries.",
40+
"userBenefit": "The same honesty this round bought for the bootstrap method index, applied to its arguments: a file naming an argument that does not exist is broken, not unsupported.",
41+
"why": "BootstrapMethod.arguments is deliberately kept as raw u16 indices (see the doc comment in attribute.rs) because resolving them would turn every lambda-carrying class from unsupported back into corrupt. Bounding them is not resolving them: checking that an index is present in the pool costs nothing semantic and closes the same class of lie.",
42+
"tradeoff": "Must not drift into resolution. The attribute.rs comment explains at length why resolving is a regression; a bounds check has to stay a bounds check, and a future reader may not see the difference.",
43+
"effort": "S",
44+
"target": "classfile/src/validation.rs"
45+
}
46+
]
47+
}

0 commit comments

Comments
 (0)