Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# REPORT

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

## [2026-09-16] `BootstrapMethods` 를 «구조»로 읽는다 — 콜사이트 링크는 0줄 (rustjava-invokedynamic-bootstrapmethods-and-methodhandle)
- 무엇을: `AttributeInfo::BootstrapMethods` 를 **`Vec<u8>` → `Vec<BootstrapMethod>`**(JVMS 4.7.23)로 파싱하고,
`CONSTANT_MethodHandle` 을 `MethodHandleRef`(`MethodHandleKind` 9종 + 클래스·이름·서술자)로 **해독**한다.
Expand Down
35 changes: 34 additions & 1 deletion STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@
(없음 — 2026-09-16 실측: 착수 시 진행 티켓 0 · 열린 PR 0. ※「열린 PR 0」은 ★**이 회차 PR 착지 시점 기준**이다 — 회신 시점에는 그 PR 자신이 열려 있다)

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

/// JVMS 4.4.10 and 4.7.23: `bootstrap_method_attr_index` is an index into the `bootstrap_methods`
/// array of the `BootstrapMethods` attribute, and that attribute must be present whenever the pool
/// holds a Dynamic or InvokeDynamic entry. Both halves are the same sentence — the index has to
/// name a real entry — so they are one predicate rather than two: an absent attribute is a table of
/// no entries, which no index can name.
///
/// This lives in `validate_class` rather than `validate_constant_pool` because it is the only place
/// that holds both the pool and the class attributes. That crossing is the whole reason the check
/// did not exist before; the note it replaces said as much and left it to the round that wanted it.
///
/// What this rejects was already being rejected by every real JVM — OpenJDK 26 answers
/// `ClassFormatError: Missing BootstrapMethods attribute` for the absent case. What we answered
/// was `UnsupportedOperationException`, i.e. *this runtime cannot do that yet*, about a file no
/// runtime can read. That sentence is what the lineage exists to make true, so narrowing it here
/// is the point rather than a side effect.
fn bootstrap_method_indices_resolve(class: &ClassInfo) -> bool {
let bootstrap_method_count = class.attributes.iter().find_map(|attribute| match attribute {
AttributeInfo::BootstrapMethods(methods) => Some(methods.len()),
_ => None,
});

class.constant_pool.values().all(|item| {
let index = match item {
ConstantPoolItem::Dynamic {
bootstrap_method_attr_index, ..
}
| ConstantPoolItem::InvokeDynamic {
bootstrap_method_attr_index, ..
} => *bootstrap_method_attr_index,
_ => return true,
};

bootstrap_method_count.is_some_and(|count| (index as usize) < count)
})
}

fn validate_constant_pool(constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> bool {
constant_pool.values().all(|item| match item {
ConstantPoolItem::Class { name_index } => constant_pool
Expand Down Expand Up @@ -142,13 +179,9 @@ fn validate_constant_pool(constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> bo
.get(descriptor_index)
.and_then(ConstantPoolItem::utf8)
.is_some_and(|descriptor| is_method_descriptor(&descriptor)),
// The bootstrap method index is still not checked here, but the reason changed:
// `BootstrapMethods` is no longer a byte blob (see `AttributeInfo::BootstrapMethods`), so
// there now *is* something to bound it against — it just is not reachable from this
// function, which only gets the constant pool. Doing it needs `validate_class` to cross
// the pool with the class attributes, and that is a behaviour change (files that parse
// today would start being rejected), so it is left to the round that wants it. Tracked
// alongside the tag-vs-major-version check in `STATE.md` ④-2.
// The bootstrap method index is bounded by `bootstrap_method_indices_resolve`, not here:
// it needs the class attributes, and this function only gets the pool. What is left for
// this arm is the half that the pool alone can answer.
ConstantPoolItem::Dynamic { name_and_type_index, .. } | ConstantPoolItem::InvokeDynamic { name_and_type_index, .. } => {
constant_pool.get(name_and_type_index).and_then(ConstantPoolItem::name_and_type).is_some()
}
Expand Down
47 changes: 47 additions & 0 deletions docs/worklog/2026-09-16-bound-bootstrap-method-attr-index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"schema": "worklog/v1",
"date": "2026-09-16",
"taskId": "rustjava-bound-bootstrap-method-attr-index",
"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.",
"changes": [
"classfile/src/validation.rs: new bootstrap_method_indices_resolve, called from validate_class; replaces the note that deferred this check",
"test-data/src/ldc/make_ldc_fixtures.py: dynamic() takes attr_index; emits LdcDynamicBSMIndexPastEnd",
"test-data/ldc/LdcDynamicBSMIndexPastEnd.class: new fixture (index 1 into a one-entry table)",
"tests/test_class_format.rs: the deferred assertion flipped — both failure shapes now assert ClassFormatError"
],
"verification": [
"cargo test --all: 568 passed / 0 failed / 1 ignored (count unchanged: a test was replaced, not added)",
"mutation whole-predicate=true -> 1 test red (axes a, c)",
"mutation whole-predicate=false -> 24 tests red (axes b and d, conflated)",
"mutation inner-predicate=false -> 8 tests red, all dynamic-carrying; test_hello/test_switch stay green (axis b alone)",
"fixture regeneration idempotent: 10 existing .class files byte-identical, 1 added",
"DoD 7 commands all rc=0"
],
"issues": [
"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."
],
"adoptedProposals": [
"2026-09-16-bootstrap-methods-and-method-handle#p1",
"2026-09-16-ldc-tags-15-16-17#p0"
],
"proposals": [
{
"title": "Reject a class declaring BootstrapMethods more than once",
"plainSummary": "A class file is only allowed one BootstrapMethods attribute. We currently read the first one and ignore any others.",
"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.",
"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.",
"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.",
"effort": "S",
"target": "classfile/src/validation.rs, test-data/src/ldc/make_ldc_fixtures.py"
},
{
"title": "Bound the static-argument indices of each bootstrap method",
"plainSummary": "Each bootstrap method carries a list of constant pool indices for its arguments. Nothing checks those point at real pool entries.",
"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.",
"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.",
"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.",
"effort": "S",
"target": "classfile/src/validation.rs"
}
]
}
Loading