Skip to content

Commit 86631de

Browse files
authored
[rustjava-adopt-bound-bootstrap-method-attr-index-p0] feat(classfile): BootstrapMethods 를 「두 번」 선언한 클래스를 거부한다 (#53)
[rustjava-adopt-bound-bootstrap-method-attr-index-p0] feat(classfile): BootstrapMethods 를 「두 번」 선언한 클래스를 거부한다
2 parents 5fbae91 + 89890b7 commit 86631de

8 files changed

Lines changed: 247 additions & 0 deletions

‎REPORT.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,29 @@
11
# REPORT
2+
## [2026-09-16] `BootstrapMethods` 를 두 번 선언한 클래스를 거부한다 (rustjava-adopt-bound-bootstrap-method-attr-index-p0)
3+
- 무엇을: JVMS 4.7.23 은 `BootstrapMethods` 를 **최대 한 개**만 허용한다. 두 개를 실은 파일을 ★**거부**한다
4+
(종전에는 `find_map` 이 **첫 표**를 쓰고 나머지를 조용히 무시했다).
5+
- 왜: 채택 제안 `2026-09-16-bound-bootstrap-method-attr-index#p0`(운영자 tower 패널 채택).
6+
- 사용자 영향: ★**진단이 바뀐다** — `UnsupportedOperationException`(「이 런타임이 아직 못 한다」) →
7+
★`ClassFormatError`(「이 파일이 깨졌다」). ★**참조 JVM 과 같은 판정**이 된다.
8+
- ★★**참조 JVM 이 근거다**(observable behavior · OpenJDK 소스 미참조): OpenJDK 26.0.1 은 같은 파일에
9+
**`ClassFormatError: Multiple BootstrapMethods attributes in class file`** 를 내고, ★**표가 하나뿐인 대조군은
10+
rc=0 으로 로드**한다 ⇒ 픽스처의 결함이 «둘이라는 사실» 하나임이 참조 구현으로 확증된다.
11+
- ★**고친 자리는 «한 곳»**: `validate_class` 에 술어 `at_most_one_bootstrap_methods_attribute` 를 이었다.
12+
★`bootstrap_method_indices_resolve` 는 **무접촉** — 그 함수의 doc 이 스스로 「인덱스가 실재 항목을 가리키는가」라는
13+
**한 문장**임을 선언하고, 「표가 몇 개인가」는 **다른 문장**이다(접어 넣으면 이름까지 바꿔야 한다).
14+
★**새 관용 0** — 필드 `ConstantValue`·메서드 `Code` 가 이미 쓰는 **개수 세기** 모양 그대로다.
15+
- ★**픽스처는 «결함이 하나»가 되게 지었다**: `LdcDynamicDuplicateBSM.class` = 같은 유효한 표를 ★**바이트 동일**하게 두 번.
16+
어느 한 표만 있어도 정상 파일이라 ★**거부 원인이 «둘»로 고정**된다(둘째 표를 다르게 하면 다른 규칙이 먼저 물어
17+
테스트가 «이름과 다른 이유»로 통과한다). 구조 실측: 속성 `['BootstrapMethods','BootstrapMethods']` · 본문 동일 `True`.
18+
- ★★**제안의 한 문장은 «과했다»**: 「생성기가 만들 수 없는 픽스처가 필요하다」 — ★**8줄 래퍼로 됐다**
19+
(속성 목록이 빌더에 그대로 전달된다). **관측은 맞았고 비용 추정이 틀렸다.**
20+
- 검증: 개악 **양방향** — ⑴호출부에서 술어 제거(=제안 이전 상태) **red** ⑵술어 본문을 **`true`(상수 통과)** 로 **red**
21+
(★⑵가 없으면 「검사가 상수로 뭉개진」 축을 못 잡는다) · 정상 **green** ·
22+
`cargo test --all` **571 passed / 0 failed / 1 ignored**(27 스위트 **전건 합산**) · 픽스처 재생성 **멱등**(기존 11 바이트 동일) · DoD 7명령 rc=0.
23+
- 후속 추천: 클래스 수준의 **다른 「최대 1개」 속성**(SourceFile·EnclosingMethod·Signature…)도 같은 규칙을 받아야 하는지 **판정**
24+
— ★이번 건이 검사를 얻은 이유는 「중복이 하류에 임의 선택을 만든다」이고, 파서가 무시하는 속성엔 그 논거가 **전이되지 않는다**.
25+
상세 = `docs/worklog/2026-09-16-reject-duplicate-bootstrap-methods.md`.
26+
227
## [2026-09-16] `ldc` 태그 15/16/17 — ★**ASM 은 «낸다»**(Kotlin·Scala·Lombok 산출물은 0) (rustjava-ldc-tags-15-16-17-real-world-generator-survey)
328
- 무엇을: 선행 회차가 남긴 **「못 쟀다」**(ASM·Kotlin·Scala·Lombok)를 **쟀다**. 조사 회차 — ★**크레이트 무접촉**(파서·테스트 0).
429
- 왜: 채택 제안 `2026-09-16-ldc-tags-15-16-17#p2`. javac 은 안 낸다가 이미 증명됐고, **직접 바이트코드를 짜는 도구**가 남아 있었다.

‎STATE.md‎

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

66
## 완료
7+
- [rustjava-adopt-bound-bootstrap-method-attr-index-p0] ★★**`BootstrapMethods` 를 «두 번» 선언한 클래스를 거부한다 — 임의 선택을 없앴다.**
8+
채택 제안 `2026-09-16-bound-bootstrap-method-attr-index#p0`(운영자 tower 패널 채택 · worklog json `adoptedProposals` 기록).
9+
★**JVMS 4.7.23 = 최대 한 개.** 종전에는 `find_map` 이 **첫 표**를 쓰고 나머지를 **조용히 무시**했다 ⇒
10+
★**`bootstrap_method_attr_index` 가 «어느 표»에 대해 경계 검사되는지가 임의**였고 그 사실이 아무 데도 드러나지 않았다.
11+
★**전/후**: `UnsupportedOperationException`(「아직 못 한다」) → ★`ClassFormatError`(「이 파일이 깨졌다」).
12+
★★**참조 JVM 이 근거다**(observable behavior · OpenJDK 소스 미참조): OpenJDK 26.0.1 →
13+
**`ClassFormatError: Multiple BootstrapMethods attributes in class file`** · ★**표 하나짜리 대조군은 rc=0 로드**.
14+
★**고친 자리 «한 곳»** — `validate_class` 에 술어 `at_most_one_bootstrap_methods_attribute` 를 이었다.
15+
★`bootstrap_method_indices_resolve` **무접촉**(그 doc 이 스스로 「인덱스가 실재 항목을 가리키는가」라는 한 문장임을
16+
선언한다 — 「표가 몇 개인가」는 다른 문장이고, 접어 넣으면 **이름까지 바꿔야** 한다) · ★**새 관용 0**
17+
(필드 `ConstantValue`·메서드 `Code` 가 이미 쓰는 **개수 세기** 모양 그대로).
18+
★★**픽스처를 «결함이 하나»가 되게 지었다** — `LdcDynamicDuplicateBSM.class` = **같은 유효한 표를 바이트 동일하게 두 번**.
19+
어느 한 표만 있어도 정상 파일이라 ★거부 원인이 «둘이라는 사실»로 **고정**된다(둘째를 다르게 하면 다른 규칙이 먼저 물어
20+
테스트가 «이름과 다른 이유»로 통과한다 — 이 저장소가 #49 에서 세운 그 규율).
21+
구조를 **측정**했다: 속성 `['BootstrapMethods','BootstrapMethods']` · 두 본문 **바이트 동일 True**.
22+
★★**제안의 한 문장은 «과했다»** — 「생성기가 만들 수 없는 픽스처가 필요하다」는 **거짓**이고 ★**8줄 래퍼**로 됐다
23+
(속성 목록이 빌더에 그대로 전달된다). ⇒ **관측은 맞았고 «비용 추정»이 틀렸다** — 다음 사람이 같은 이유로 미루지 않게 적는다.
24+
★**개악 대조 양방향**: ⑴호출부에서 술어 제거(= 제안 이전 상태) **red** ⑵술어 본문을 **`true`(상수 통과)** 로 **red** ·
25+
정상 **green**. ★**⑵가 없으면 「검사가 상수로 뭉개진」 축을 못 잡는다**(⑴만으로는 호출 삭제만 잡힌다).
26+
★`cargo test --all` **571 / 0 failed / 1 ignored**(27 스위트 **전건 합산** — 꼬리만 세지 않았다) ·
27+
픽스처 재생성 **멱등**(기존 11 전건 바이트 동일 · 신규 1) · DoD **7줄 전건 rc=0**.
28+
★**잃는 것**: 지금까지 «로드되던» 파일 하나가 거부된다 — 다만 그 형상은 어제 이 저장소가 잰 대로
29+
**javac·kotlinc·scalac·Lombok 산출물 5,479 클래스에 0**이고 ASM 으로도 «일부러» 만들어야 나온다.
30+
★★**게이트③ 착지 — PR #53 · `--merge`**(등재 repo `contracts/upstream-sync-repos.conf:22` — 스쿼시는 부모 2개를 접어 계보를 지운다).
31+
게이트② **1회차 approve**(반려 0) · 핀 `f2c83174` **불이동**(착수 실측 17:39:54Z · ★워밍 후 재조회 `MERGEABLE/CLEAN`).
32+
★**충돌 0 · base 당김 0**(`merge-tree` rc=0) — 이 브랜치가 `origin/main` 위에서 갈렸고 그 뒤 착지한 형제가 없다.
33+
★**여파**: 이 착지가 형제 **#54**(BSM 정적 인자)·**#55**(변이 감사)의 base 를 낡게 만든다.
34+
★**#54 는 «코드 파일이 자동 병합»되도록 그 회차가 삽입 위치를 미리 갈라 뒀고**(그 done 회신의 `merge-tree` 실측),
35+
**#55 는 원장 3파일만 만진다** ⇒ 두 형제 모두 충돌은 **원장 계열에 국한**된다(게이트③ 계약 2-c⒜ 범위).
736
- [rustjava-ldc-tags-15-16-17-real-world-generator-survey] ★★**「못 쟀다」를 «쟀다»로 바꿨다 — ASM 은 태그 15/16/17 을 «낸다».**
837
채택 제안 `2026-09-16-ldc-tags-15-16-17#p2`(worklog json `adoptedProposals` 기록). ★**조사 회차 · 크레이트 무접촉**(파서·테스트 0).
938
★★**ASM 9.7.1 = 낸다(실증)** — `visitLdcInsn(Handle)`·`(Type.getMethodType)`·`(ConstantDynamic)` 15줄로 만든 클래스에서

‎classfile/src/validation.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
1616
|| !validate_constant_pool(&class.constant_pool)
1717
|| !constant_pool_tags_fit_the_class_file_version(class)
1818
|| !bootstrap_method_indices_resolve(class)
19+
|| !at_most_one_bootstrap_methods_attribute(class)
1920
{
2021
return Err(ClassFileError::InvalidFormat);
2122
}
@@ -134,6 +135,27 @@ fn bootstrap_method_indices_resolve(class: &ClassInfo) -> bool {
134135
})
135136
}
136137

138+
/// JVMS 4.7.23: at most one `BootstrapMethods` attribute may appear in a ClassFile's attributes
139+
/// table. A file carrying two is broken, not a file using a feature this runtime lacks.
140+
///
141+
/// Kept separate from `bootstrap_method_indices_resolve` because it is a different sentence: that
142+
/// one asks whether an index names a real entry, this one asks how many tables exist. Folding it in
143+
/// would also mean renaming that function for a rule it did not previously make.
144+
///
145+
/// It matters because `bootstrap_method_indices_resolve` resolves the table with `find_map`, which
146+
/// stops at the first one. With two tables that choice is arbitrary — the index would be bounded
147+
/// against whichever came first and the other silently ignored — so the honest answer is to reject
148+
/// the file rather than pick. Counting is the same shape `validate_class` already uses for the
149+
/// per-member "at most one" rules (`ConstantValue` on a field, `Code` on a method).
150+
fn at_most_one_bootstrap_methods_attribute(class: &ClassInfo) -> bool {
151+
class
152+
.attributes
153+
.iter()
154+
.filter(|attribute| matches!(attribute, AttributeInfo::BootstrapMethods(_)))
155+
.count()
156+
<= 1
157+
}
158+
137159
fn validate_constant_pool(constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> bool {
138160
constant_pool.values().all(|item| match item {
139161
ConstantPoolItem::Class { name_index } => constant_pool
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"schema": "worklog/v1",
3+
"date": "2026-09-16",
4+
"taskId": "rustjava-adopt-bound-bootstrap-method-attr-index-p0",
5+
"summary": "A class declaring BootstrapMethods twice is now rejected as malformed instead of being read with whichever table came first. JVMS 4.7.23 allows at most one; OpenJDK 26 answers ClassFormatError: Multiple BootstrapMethods attributes for the same file.",
6+
"changes": [
7+
"classfile/src/validation.rs: new at_most_one_bootstrap_methods_attribute predicate, wired into validate_class's rejection chain. bootstrap_method_indices_resolve is untouched — counting tables is a different sentence from bounding an index, and folding it in would have meant renaming that function",
8+
"test-data/src/ldc/make_ldc_fixtures.py: new duplicate_bootstrap_methods(inner) wrapper that re-appends the attribute its inner builder wrote",
9+
"test-data/ldc/LdcDynamicDuplicateBSM.class: new fixture, the same valid table byte-for-byte twice",
10+
"tests/test_class_format.rs: test_a_class_declaring_bootstrap_methods_twice_is_malformed"
11+
],
12+
"verification": [
13+
"proposal re-measured at start: the find_map it names is still at classfile/src/validation.rs:117, so the proposal is still live",
14+
"reference JVM, OpenJDK 26.0.1: the fixture gives 'java.lang.ClassFormatError: Multiple BootstrapMethods attributes in class file LdcDynamicDuplicateBSM', while the single-table control LdcDynamic loads with rc=0 and no output",
15+
"our runtime before the fix: UnsupportedOperationException 'ldc of a dynamically-computed constant'; after: ClassFormatError 'Invalid class file'; the single-table control's answer is unchanged, which shows the change is narrow",
16+
"fixture structure measured, not assumed: class attributes are ['BootstrapMethods', 'BootstrapMethods'] and the two bodies are byte-identical",
17+
"fixture regeneration is idempotent: 11 existing .class files byte-identical, 1 added",
18+
"mutation M1, removing the call from validate_class (i.e. the pre-proposal state): the new test goes red",
19+
"mutation M2, replacing the predicate body with `true` (constant pass): red at the same assertion — M1 alone would not catch a check flattened to a constant",
20+
"restore: test_class_format 12 passed / 0 failed",
21+
"full suite: 571 passed / 0 failed / 1 ignored, counted by summing every 'test result' line rather than reading the tail",
22+
"DoD 7 commands all rc=0"
23+
],
24+
"issues": [
25+
"The proposal's tradeoff said the fixture was one 'the generator cannot currently build'. That was too strong: the attribute list is handed to each builder, so an 8-line wrapper that re-appends what the inner builder wrote is enough. The observation was right, the cost estimate was not."
26+
],
27+
"adoptedProposals": [
28+
"2026-09-16-bound-bootstrap-method-attr-index#p0"
29+
],
30+
"proposals": [
31+
{
32+
"title": "Decide whether the other class-level attributes also need cardinality rules",
33+
"plainSummary": "We now reject a class that declares two bootstrap tables. Several other class attributes are also allowed only once each, and nothing checks those.",
34+
"userBenefit": "The same honesty applied consistently: a file carrying two of something the spec allows one of is reported as broken, whichever attribute it is.",
35+
"why": "JVMS 4.7 marks several ClassFile attributes as at-most-one (SourceFile, EnclosingMethod, Signature, SourceDebugExtension, and the module-related ones). This round added the rule for BootstrapMethods because a proposal named it and because the resolver silently picks the first of two. Whether the others deserve the same treatment is a separate question, and the answer may be no for the ones nothing reads.",
36+
"tradeoff": "Rejecting more files that load today, for attributes whose duplication changes nothing we act on — the BootstrapMethods case earned its check because a duplicate makes an arbitrary choice observable downstream, and that argument does not transfer to an attribute the parser ignores. Doing it uniformly would also need a fixture per attribute.",
37+
"effort": "S",
38+
"target": "classfile/src/validation.rs"
39+
}
40+
]
41+
}

0 commit comments

Comments
 (0)