From 1da1379e92786f4fb55da0db9abc1200b07f84ae Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 17 Sep 2026 22:45:40 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[rustjava-adopt-reject-duplicate-bootstrap-?= =?UTF-8?q?methods-p0]=20fix(classfile):=20=EB=8B=A8=EC=9D=BC=EA=B0=92=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=86=8D=EC=84=B1=EC=9D=98=20?= =?UTF-8?q?=C2=AB=EA=B0=9C=EC=88=98=20=EA=B7=9C=EC=B9=99=C2=BB=20=E2=80=94?= =?UTF-8?q?=20=EC=A7=84=EC=A7=9C=20JVM=20=EC=9D=B4=20=EA=B1=B0=EB=B6=80?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=8B=A4=EC=84=AF=EC=97=90=EB=A7=8C,=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REPORT.md | 14 +++ STATE.md | 8 ++ classfile/src/attribute.rs | 4 + classfile/src/validation.rs | 80 ++++++++++--- ...026-09-17-class-attribute-cardinality.json | 38 ++++++ .../2026-09-17-class-attribute-cardinality.md | 73 ++++++++++++ test-data/attr/DuplicateInnerClasses.class | Bin 0 -> 170 bytes test-data/attr/DuplicateNestHost.class | Bin 0 -> 162 bytes .../attr/DuplicateNestHostOldMajor.class | Bin 0 -> 170 bytes test-data/attr/DuplicateNestMembers.class | Bin 0 -> 168 bytes .../attr/DuplicateSourceDebugExtension.class | Bin 0 -> 184 bytes test-data/attr/DuplicateSourceFile.class | Bin 0 -> 178 bytes test-data/attr/DuplicateSynthetic.class | Bin 0 -> 160 bytes test-data/class-file-versions.txt | 7 ++ test-data/src/attr/make_attr_fixtures.py | 108 ++++++++++++++++++ tests/test_class_format.rs | 50 ++++++++ 16 files changed, 365 insertions(+), 17 deletions(-) create mode 100644 docs/worklog/2026-09-17-class-attribute-cardinality.json create mode 100644 docs/worklog/2026-09-17-class-attribute-cardinality.md create mode 100644 test-data/attr/DuplicateInnerClasses.class create mode 100644 test-data/attr/DuplicateNestHost.class create mode 100644 test-data/attr/DuplicateNestHostOldMajor.class create mode 100644 test-data/attr/DuplicateNestMembers.class create mode 100644 test-data/attr/DuplicateSourceDebugExtension.class create mode 100644 test-data/attr/DuplicateSourceFile.class create mode 100644 test-data/attr/DuplicateSynthetic.class create mode 100644 test-data/src/attr/make_attr_fixtures.py diff --git a/REPORT.md b/REPORT.md index 6faef6eb..1a56f613 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,4 +1,18 @@ # REPORT +## [2026-09-17] 다른 클래스 수준 속성에도 개수 규칙이 필요한가 — ★**다섯에 «예». 그리고 제안의 값 전제가 거짓이었다** (rustjava-adopt-reject-duplicate-bootstrap-methods-p0) +- 무엇을: 채택 제안 `2026-09-16-reject-duplicate-bootstrap-methods#p0`(제목이 **「Decide whether …」** = 이 회차가 지는 것은 **판정**). ★**제품 동작 변경 있음** — 아래. +- ★**제안의 기준만으로는 «아니오»가 나온다**: 기존 검사 주석이 적은 기준은 「중복이 **하류에서 임의의 선택을 관측 가능하게 만드는가**」이고, 소비자 전수 실측 결과 그런 속성은 ★**`BootstrapMethods` 하나뿐**이다(`validation.rs` + `string_concat.rs:110 resolve_bootstrap_methods` 의 `find_map`). 나머지 6종은 `attribute.rs` 밖 소비자 **0**. +- ★★**판정을 바꾼 것은 «진짜 JVM»이다** — OpenJDK **26.0.1** 에 한 속성씩 물었다(★런처 메시지는 원인을 가리므로 `Class.forName` 으로 진단문을 받았다): + `SourceFile`·`InnerClasses`·`SourceDebugExtension`·`BootstrapMethods`(52) · `NestHost`·`NestMembers`(55) → ★**전건 `ClassFormatError: Multiple … attributes`**. + ⇒ ★**제안의 `tradeoff` 「Rejecting more files that load today」는 그 다섯에 대해 «거짓»이다** — 그 파일들은 **오늘도 진짜 JVM 에서 로드되지 않는다.** 받아들이던 쪽이 **우리**였다. +- ★★**그래서 «표»이고 «버전 게이트»다 — 두 통제군이 그 이유다**: + ⒜★`NestHost` **major 52** 에서는 ★**로드된다**(그 버전엔 정의되지 않아 무시 · JVMS 4.7.1) ⇒ 게이트 없이 세면 **모든 JVM 이 받는 파일을 거부**한다. + ⒝★`Synthetic` 은 JVMS 4.7.8 이 at-most-one 이라는데 ★**HotSpot 은 둘을 받는다** ⇒ ★**빠뜨린 것이 아니라 «근거로 뺐다»**(넣으면 맞추려는 JVM 보다 엄격해진다). +- ★**반대 방향도 의도적이다** — 필드·메서드·Code 소속 속성은 클래스 속성표에 나타나도 **세지 않는다**(정의되지 않은 자리의 속성 = 무시). +- ★★**양방향 개악 4종 전건 red**: **M1** 버전 게이트 제거 → 통제군 `DuplicateNestHostOldMajor` red · **M2** `Synthetic` 추가 → 통제군 red · **M3** `SourceFile` 한 칸 제거 → 그 픽스처만 red · **M4** 호출부를 종전으로 되돌림 → 다섯 red · 복원 **17/0 green**. ★**M1·M2 가 요지다** — 통제군이 없으면 「전부 세면 된다」가 통과한다. +- ★**대가**: ⒜**제품 동작 변경** — 그 다섯을 둘씩 가진 클래스가 **로드되지 않는다**(전부 OpenJDK 도 거부하지만 변경은 변경이다) ⒝**표는 «적어 둔 목록»** 이라 속성이 늘어도 행을 더하기 전엔 안 덮이고 **울어 주는 것이 없다** ⒞`Synthetic` 배제는 **JVM 하나의 행동**에 걸려 있다 ⒟★**`attribute.rs` 를 만졌다**(제안 `target` 은 `validation.rs` 뿐) — `SourceDebugExtension` 파싱 팔 **한 줄**로 기존 **dead variant** 를 살린 것이고, 없으면 중복이 `Unknown` 에 섞여 **구별 불가**다. 최소였지만 **명시 파일 밖**이라 신고한다. +- 검증: `cargo test --all` **578 → 579 / 0 failed / 1 ignored** · `test_fixture_pins` **3/0** · 픽스처 재생성 **멱등** · 버전 표 **+7행**(AGENTS.md 의무) · DoD 7명령 rc=0. + ## [2026-09-17] base 를 당겼다 — ★**막고 있던 코드 충돌은 «이미 없었다»**(rustjava-adopt-link-stringconcatfactory-p1-fix2) - 무엇을: `origin/main` 당김(뒤처짐 **9**) + 그 당김이 만든 `test-data/class-file-versions.txt` **3행**. ★제품 코드 **0줄** · 픽스처 바이트 **불변**. - ★★**전제가 반증됐다**: 이 회차는 「`make_indy_fixtures.py` 4구역 코드 충돌」을 풀라고 발권됐는데, 지금 당기면 그 파일은 **충돌하지 않는다**. `-p1-fix` 회차가 **14:10 에 `0f06b93f` 로 이미 합집합 해소**했고 게이트②가 **15:43 에 그 head 를 approve** 했다 — 발권 근거였던 12:12 blocked 회신이 그 사이 **낡았다**. diff --git a/STATE.md b/STATE.md index 24f4e80a..bd528917 100644 --- a/STATE.md +++ b/STATE.md @@ -7,6 +7,14 @@ (둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다) ## 완료 +- [rustjava-adopt-reject-duplicate-bootstrap-methods-p0] ★★**클래스 수준 속성 개수 규칙 — 다섯에 «예».** 채택 제안 `…-reject-duplicate-bootstrap-methods#p0`(worklog json 기록). ★**제품 동작 변경 있음.** + ★**제안 기준만으로는 «아니오»였다**(하류 `find_map` 소비자가 있는 것은 `BootstrapMethods` 뿐 · 나머지 6종 소비자 0). + ★★**판정을 바꾼 것은 진짜 JVM 이다** — OpenJDK 26.0.1 이 `SourceFile`·`InnerClasses`·`SourceDebugExtension`·`BootstrapMethods`(52)·`NestHost`·`NestMembers`(55) 중복을 ★**전건 `ClassFormatError`** 로 거부한다 + ⇒ ★**제안의 「오늘 로드되는 파일을 더 거부한다」는 «거짓»**(그 파일들은 진짜 JVM 에서도 안 열린다). + ★★**통제군 둘이 표·버전게이트의 이유**: `NestHost`@**52** 는 ★**로드된다**(미정의 ⇒ 무시) · `Synthetic` 은 스펙이 하나라는데 ★**HotSpot 이 둘을 받는다**(근거로 뺐다). + ★**개악 4종 전건 red**(M1 게이트 제거·M2 Synthetic 추가 → **통제군** red · M3 한 칸 제거 · M4 호출부 원복) · 복원 17/0. + ★**대가**: 동작 변경 · 표는 수동 목록(늘어도 안 울린다) · Synthetic 배제는 JVM 하나에 의존 · ★**`attribute.rs` 한 줄**(제안 target 밖 — 신고). + ★`--all` **579/0/1** · 버전 표 **+7행** · DoD 7명령 rc=0. - [rustjava-adopt-link-stringconcatfactory-p1-fix2] ★★**base 당김 — 그런데 막고 있던 코드 충돌은 «이미 없었다»(PR #60).** ★**전제 반증**: 「`make_indy_fixtures.py` 4구역 충돌」은 `-p1-fix` 가 **14:10 `0f06b93f`** 로 합집합 해소했고 게이트②가 **15:43 그 head 를 approve** 했다. 발권 근거(12:12 blocked 회신)가 그 사이 낡은 것이다. ★**재발 불가**도 확인 — 뒤진 9커밋 중 그 파일을 만진 것 **0건**. diff --git a/classfile/src/attribute.rs b/classfile/src/attribute.rs index 5de11ac8..0d7e613b 100644 --- a/classfile/src/attribute.rs +++ b/classfile/src/attribute.rs @@ -285,6 +285,10 @@ impl AttributeInfo { AttributeInfo::LineNumberTable(length_count(be_u16, AttributeInfoLineNumberTableEntry::parse).parse(info)?.1) } "SourceFile" => AttributeInfo::SourceFile(Self::parse_source_file(info, constant_pool)?.1), + // The body is an unstructured UTF-8 blob nothing here reads; the variant exists so the + // at-most-one rule in `validation.rs` can see it. Without this arm it lands in `Unknown`, + // where a duplicate is indistinguishable from two attributes we do not recognise. + "SourceDebugExtension" => AttributeInfo::SourceDebugExtension, "LocalVariableTable" => AttributeInfo::LocalVariableTable(Self::parse_local_variable_table(info, constant_pool)?.1), "StackMap" => AttributeInfo::StackMap(info.to_vec()), "StackMapTable" => AttributeInfo::StackMapTable(info.to_vec()), diff --git a/classfile/src/validation.rs b/classfile/src/validation.rs index 03569b56..8ae8ede6 100644 --- a/classfile/src/validation.rs +++ b/classfile/src/validation.rs @@ -17,7 +17,7 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> { || !constant_pool_tags_fit_the_class_file_version(class) || !bootstrap_method_static_arguments_are_in_the_pool(class) || !bootstrap_method_indices_resolve(class) - || !at_most_one_bootstrap_methods_attribute(class) + || !at_most_one_of_each_single_class_attribute(class) { return Err(ClassFileError::InvalidFormat); } @@ -191,25 +191,71 @@ fn bootstrap_method_indices_resolve(class: &ClassInfo) -> bool { }) } -/// JVMS 4.7.23: at most one `BootstrapMethods` attribute may appear in a ClassFile's attributes -/// table. A file carrying two is broken, not a file using a feature this runtime lacks. +/// JVMS 4.7: several ClassFile attributes may appear at most once. A file carrying two is broken, +/// not a file using a feature this runtime lacks. /// /// Kept separate from `bootstrap_method_indices_resolve` because it is a different sentence: that -/// one asks whether an index names a real entry, this one asks how many tables exist. Folding it in -/// would also mean renaming that function for a rule it did not previously make. +/// one asks whether an index names a real entry, this one asks how many tables exist. Counting is +/// the same shape `validate_class` already uses for the per-member "at most one" rules +/// (`ConstantValue` on a field, `Code` on a method). /// -/// It matters because `bootstrap_method_indices_resolve` resolves the table with `find_map`, which -/// stops at the first one. With two tables that choice is arbitrary — the index would be bounded -/// against whichever came first and the other silently ignored — so the honest answer is to reject -/// the file rather than pick. Counting is the same shape `validate_class` already uses for the -/// per-member "at most one" rules (`ConstantValue` on a field, `Code` on a method). -fn at_most_one_bootstrap_methods_attribute(class: &ClassInfo) -> bool { - class - .attributes - .iter() - .filter(|attribute| matches!(attribute, AttributeInfo::BootstrapMethods(_))) - .count() - <= 1 +/// `BootstrapMethods` came first, because a duplicate there makes an arbitrary choice observable: +/// `bootstrap_method_indices_resolve` and `string_concat`'s `resolve_bootstrap_methods` both take +/// the table with `find_map`, which stops at the first one. That argument covers one attribute, and +/// the round that added it left the others as an open question. +/// +/// ## Which others, and why not "all of them" +/// +/// The list below is **what OpenJDK 26.0.1 actually rejects**, measured one attribute at a time +/// rather than read off the spec, because the two do not agree: +/// +/// | duplicated attribute | class file version | OpenJDK 26.0.1 | +/// |---|---|---| +/// | `SourceFile`, `InnerClasses`, `SourceDebugExtension`, `BootstrapMethods` | 52 | `ClassFormatError: Multiple … attributes` | +/// | `NestHost`, `NestMembers` | 55 | `ClassFormatError: Multiple … attributes` | +/// | `NestHost` | **52** | **loads** — the attribute is not defined before 55, so it is ignored (JVMS 4.7.1) | +/// | `Synthetic` | 52 | **loads**, though JVMS 4.7.8 marks it at-most-one | +/// +/// Two of those rows are the whole reason this is a table and not a `matches!` over every variant: +/// +/// * **The version gate is load-bearing.** Counting `NestHost` at major 52 would reject a file every +/// real JVM accepts. An attribute that does not exist at that version is not duplicated, it is +/// unrecognised, and unrecognised attributes are ignored. +/// * **`Synthetic` is excluded on evidence, not oversight.** The spec says at most one; HotSpot +/// takes two. Rejecting it would make us stricter than the JVM we are trying to agree with, and +/// nothing here reads the attribute, so there is no arbitrary choice to make observable either. +/// +/// The other direction is deliberate too: attributes that belong to a field, method or Code table +/// (`ConstantValue`, `Code`, `Exceptions`, `MethodParameters`, `StackMapTable`, `LineNumberTable`, +/// `LocalVariableTable`) are not listed even when they turn up in a class's attribute table, because +/// there they are attributes in a place they are not defined for — ignored, not counted. +fn at_most_one_of_each_single_class_attribute(class: &ClassInfo) -> bool { + // (discriminant, the class file version that introduced the attribute) + fn single_valued(attribute: &AttributeInfo) -> Option<(u8, u16)> { + Some(match attribute { + AttributeInfo::SourceFile(_) => (0, 45), + AttributeInfo::InnerClasses(_) => (1, 45), + AttributeInfo::SourceDebugExtension => (2, 49), + AttributeInfo::BootstrapMethods(_) => (3, 51), + AttributeInfo::NestHost(_) => (4, 55), + AttributeInfo::NestMembers(_) => (5, 55), + _ => return None, + }) + } + + class.attributes.iter().enumerate().all(|(position, attribute)| { + let Some((kind, introduced_in)) = single_valued(attribute) else { + return true; + }; + if class.major_version < introduced_in { + return true; + } + + // Only the first of each kind looks behind it, so one duplicate is reported once. + !class.attributes[..position] + .iter() + .any(|earlier| single_valued(earlier).is_some_and(|(earlier_kind, _)| earlier_kind == kind)) + }) } fn validate_constant_pool(constant_pool: &BTreeMap) -> bool { diff --git a/docs/worklog/2026-09-17-class-attribute-cardinality.json b/docs/worklog/2026-09-17-class-attribute-cardinality.json new file mode 100644 index 00000000..158464c4 --- /dev/null +++ b/docs/worklog/2026-09-17-class-attribute-cardinality.json @@ -0,0 +1,38 @@ +{ + "schema": "worklog/v1", + "date": "2026-09-17", + "taskId": "rustjava-adopt-reject-duplicate-bootstrap-methods-p0", + "summary": "The proposal asked whether the other at-most-one ClassFile attributes deserve the rule BootstrapMethods got, and guessed the answer might be no for the ones nothing reads. Measured against OpenJDK 26.0.1 one attribute at a time, the answer is yes for five of them — and the proposal's stated cost, 'rejecting more files that load today', is false: a real JVM already refuses every one of those files. The interesting part is the two it does not refuse, which is why this is a version-gated table rather than a rule over every single-valued attribute.", + "changes": [ + "classfile/src/validation.rs: at_most_one_bootstrap_methods_attribute -> at_most_one_of_each_single_class_attribute, a version-gated table over SourceFile, InnerClasses, SourceDebugExtension, BootstrapMethods, NestHost, NestMembers", + "classfile/src/attribute.rs: one parse arm for SourceDebugExtension, which makes an existing but unreachable enum variant live. Without it a duplicate lands in Unknown, where it is indistinguishable from two attributes we do not recognise.", + "test-data/src/attr/make_attr_fixtures.py: new generator for duplicate-attribute fixtures, carrying the measurement in its docstring", + "test-data/attr/: 7 fixtures — 5 that must be rejected, 2 controls that must keep loading", + "tests/test_class_format.rs: test_a_class_declaring_a_single_valued_attribute_twice_is_malformed, a table with a `loads` column", + "test-data/class-file-versions.txt: +7 rows for the new fixtures (AGENTS.md requires the table move with the fixture)" + ], + "verification": [ + "ORACLE, measured one attribute at a time with OpenJDK 26.0.1 via Class.forName rather than the launcher (whose 'no main method' message hides the real cause): duplicated SourceFile / InnerClasses / SourceDebugExtension / BootstrapMethods at major 52 -> ClassFormatError: Multiple attributes; duplicated NestHost / NestMembers at major 55 -> same", + "CONTROL 1 — duplicated NestHost at major 52 LOADS. The attribute is not defined before 55, so it is ignored (JVMS 4.7.1) and there is nothing to duplicate. A rule without a version gate would reject a file every JVM accepts.", + "CONTROL 2 — duplicated Synthetic at major 52 LOADS, though JVMS 4.7.8 marks it at-most-one. The spec and HotSpot disagree; we follow the JVM. Nothing here reads the attribute either, so there is no arbitrary choice to make observable.", + "so the proposal's tradeoff premise is measurably false for the five: those files do not 'load today' on a real JVM — our accepting them was the deviation", + "criterion re-derived from the existing check's own comment (a duplicate makes an arbitrary choice observable downstream) and then measured: the only class-level attribute read with a first-wins selector is BootstrapMethods (validation.rs find_map and jvm-bytecode/src/string_concat.rs resolve_bootstrap_methods find_map). Consumers outside attribute.rs for SourceFile / InnerClasses / SourceDebugExtension / NestHost / NestMembers / Synthetic: zero. So the downstream-observability argument alone would have stopped at 'no' — the JVM oracle is what moved it.", + "MUTATION M1, delete the version gate: control DuplicateNestHostOldMajor turns red (we would reject what the JVM loads)", + "MUTATION M2, add Synthetic to the table: control DuplicateSynthetic turns red", + "MUTATION M3, remove the SourceFile row: that fixture alone stops being rejected, red", + "MUTATION M4, revert the call site to the BootstrapMethods-only form: the five turn red", + "restore: test_class_format 17 passed / 0 failed", + "generator is idempotent: a second run leaves the fixture bytes unchanged", + "cargo test --all and the DoD commands: see the round's REPORT.md entry" + ], + "issues": [ + "Product behaviour changes: a class carrying two of those five attributes now fails to load where it used to run. That is the point, and every such file is one OpenJDK also refuses — but it is a behaviour change, not a no-op.", + "The table is a written-down list, not derived. An attribute added to AttributeInfo tomorrow is not covered until someone adds a row, and nothing flags that.", + "Synthetic is excluded on one JVM's behaviour. If HotSpot tightened, or another JVM already rejects it, our control fixture would be pinning the wrong answer.", + "attribute.rs was touched even though the proposal's target is validation.rs. One arm, and it was the minimum to cover an attribute the JVM rejects — but it is outside the named file and is reported as such." + ], + "adoptedProposals": [ + "2026-09-16-reject-duplicate-bootstrap-methods#p0" + ], + "proposals": [] +} diff --git a/docs/worklog/2026-09-17-class-attribute-cardinality.md b/docs/worklog/2026-09-17-class-attribute-cardinality.md new file mode 100644 index 00000000..7eb9cfdd --- /dev/null +++ b/docs/worklog/2026-09-17-class-attribute-cardinality.md @@ -0,0 +1,73 @@ +# 2026-09-17 — 다른 클래스 수준 속성에도 «개수 규칙»이 필요한가 (rustjava-adopt-reject-duplicate-bootstrap-methods-p0) + +채택 제안 `2026-09-16-reject-duplicate-bootstrap-methods#p0` — 제목이 **「Decide whether …」**, +즉 이 회차가 지는 것은 **판정**이다. 제안은 자기 `why` 에 「**아무도 읽지 않는 속성에 대해서는 답이 +«아니오»일 수 있다**」고 적어 두었다. + +★**재서 보니 답은 «다섯에 대해 예»다. 그리고 제안이 적은 값 전제가 «거짓»이었다.** + +## 1. 제안의 기준을 먼저 적용해 봤다 — 그 기준만으로는 «아니오»가 나온다 + +기존 검사의 주석이 스스로 기준을 적어 두었다: 「중복이 ★**하류에서 «임의의 선택»을 관측 가능하게 만드는가**」. +실측(소비자 전수 · `attribute.rs` 제외): + +| 속성 | `find_map` 류 선택자로 읽는 소비자 | +|---|---| +| `BootstrapMethods` | ★**있다** — `validation.rs` + `jvm-bytecode/src/string_concat.rs:110 resolve_bootstrap_methods` | +| `SourceFile` · `InnerClasses` · `SourceDebugExtension` · `NestHost` · `NestMembers` · `Synthetic` | **0** | + +⇒ ★**그 기준만 쓰면 제안의 추측대로 «아니오»다.** 판정을 바꾼 것은 **다른 오라클**이다. + +## 2. ★진짜 JVM 에 물었다 — 그리고 값 전제가 뒤집혔다 + +OpenJDK **26.0.1** 에 한 속성씩 물었다. ★**런처 메시지(「기본 메소드를 찾을 수 없습니다」)는 진짜 원인을 가린다** — +`Class.forName` 으로 잡아 진단문을 받았다. + +| 중복시킨 속성 | major | OpenJDK 26.0.1 | +|---|---|---| +| `SourceFile` · `InnerClasses` · `SourceDebugExtension` · `BootstrapMethods` | 52 | ★**`ClassFormatError: Multiple … attributes`** | +| `NestHost` · `NestMembers` | 55 | ★**`ClassFormatError`** | +| ★`NestHost` | **52** | ★**로드된다** — 그 버전엔 속성이 **정의되지 않아 무시**된다(JVMS 4.7.1) | +| ★`Synthetic` | 52 | ★**로드된다** — JVMS 4.7.8 은 at-most-one 인데 **HotSpot 은 둘을 받는다** | + +⇒ ★★**제안의 `tradeoff` 「Rejecting more files that load today」는 다섯에 대해 «거짓»이다.** +그 파일들은 **오늘도 진짜 JVM 에서 로드되지 않는다.** 받아들이던 쪽이 **우리**였다. + +## 3. 그래서 «표»이고 «버전 게이트»다 — 두 줄이 그 이유다 + +⒜★**버전 게이트가 하중을 받는다**: `NestHost` 를 major 52 에서 세면 ★**모든 JVM 이 받아들이는 파일을 거부**한다. +정의되지 않은 속성은 «중복»이 아니라 «미인식»이고, 미인식은 무시한다. +⒝★**`Synthetic` 은 «빠뜨린» 것이 아니라 «근거로 뺀» 것이다**: 스펙은 하나라는데 HotSpot 은 둘을 받는다. +넣으면 **우리가 맞추려는 그 JVM 보다 엄격**해지고, 읽는 곳도 없어 관측될 임의 선택도 없다. + +★반대 방향도 의도적이다 — 필드·메서드·Code 소속 속성(`ConstantValue`·`Code`·`Exceptions`·`MethodParameters`· +`StackMapTable`·`LineNumberTable`·`LocalVariableTable`)은 클래스 속성표에 나타나도 **세지 않는다**. +거기서 그것들은 «정의되지 않은 자리의 속성» = 무시 대상이다. + +## 4. 양방향 — 통제군이 «하중을 받는지»까지 쟀다 + +| 개악(제품 호출부) | 결과 | +|---|---| +| **M1** 버전 게이트 제거 | ★**red** — 통제군 `DuplicateNestHostOldMajor` | +| **M2** `Synthetic` 을 표에 추가 | ★**red** — 통제군 `DuplicateSynthetic` | +| **M3** `SourceFile` 한 칸 제거 | ★**red** — 그 픽스처만 | +| **M4** 호출부를 종전(BootstrapMethods 전용)으로 되돌림 | ★**red** — 다섯 건 | +| 복원 | **green** 17/0 | + +★**M1·M2 가 이 회차의 요지다** — 통제군이 없으면 「전부 세면 된다」가 통과해 버린다. + +## 5. ★대가 + +- ★**제품 동작이 바뀐다**: 그 다섯을 둘씩 가진 클래스가 **로드되지 않는다**. 전부 OpenJDK 도 거부하는 파일이지만, + **동작 변경은 동작 변경이다**. +- ★**표는 «적어 둔 목록»이지 «유도된 것»이 아니다** — 내일 `AttributeInfo` 에 속성이 늘어도 행을 더하기 전엔 덮이지 않고, + 그것을 **울어 주는 것이 없다**. +- ★**`Synthetic` 배제는 «JVM 하나의 행동»에 걸려 있다** — HotSpot 이 조이거나 다른 JVM 이 이미 거부한다면 + 그 통제군 픽스처가 **틀린 답을 고정**하게 된다. +- ★**`attribute.rs` 를 만졌다** — 제안의 `target` 은 `validation.rs` 뿐이다. 팔 **한 줄**이고, JVM 이 거부하는 속성을 + 덮기 위한 **최소**였지만 **명시한 파일 밖**이라 그대로 신고한다. + +## 6. 안 하면 무엇이 나쁜가 + +「같은 정직함을 일관되게」가 **한 속성에서 멈춘다.** 스펙이 하나라는 것을 둘 가진 파일이, +★**진짜 JVM 은 거부하는데 우리만 실행**한다 — 이 리니지가 반복해 가른 「미지원 ↔ 파손」의 반대편 실수다. diff --git a/test-data/attr/DuplicateInnerClasses.class b/test-data/attr/DuplicateInnerClasses.class new file mode 100644 index 0000000000000000000000000000000000000000..83548c65e600e6edc3c7ed0529ced6e66853c0ec GIT binary patch literal 170 zcmX^0Z`VEs1_l!bPDTb%m(qfq%;dz9RL{J;)FS7c#Ny)AVs-{bMh1bb#Ii*FoW#6z zegCAa)Z`L&24+SEmfXb5JVpjFjc6Z)d~iuoW?s6rW*AW3IX@+pk%0%$c3Mn(pKti-ZJ{hY+SbbbG% ztkmQZb_Qlf2A15!%sfU0F^y;+gnV#GQD$DcwPqMl-Z?)dm63rHZWe ClassFormatError + Multiple NestHost / NestMembers (55) -> ClassFormatError + Multiple NestHost (52) -> loads + Multiple Synthetic (52) -> loads + +These are hand-assembled because no compiler emits a duplicate attribute; that is the whole point. + +Regenerate with: python3 test-data/src/attr/make_attr_fixtures.py +""" + +import struct +from pathlib import Path + +OUT = Path(__file__).resolve().parents[2] / "attr" + +u1 = lambda x: struct.pack(">B", x) +u2 = lambda x: struct.pack(">H", x) +u4 = lambda x: struct.pack(">I", x) + + +class Pool: + def __init__(self): + self.entries = [] + + def add(self, entry): + self.entries.append(entry) + return len(self.entries) + + def utf8(self, text): + raw = text.encode() + return self.add(u1(1) + u2(len(raw)) + raw) + + def klass(self, name): + return self.add(u1(7) + u2(self.utf8(name))) + + def bytes(self): + return u2(len(self.entries) + 1) + b"".join(self.entries) + + +def payload(attribute, pool, this_class): + """The smallest well-formed body for each attribute — the duplication is what is under test.""" + if attribute == "Synthetic": + return b"" + if attribute == "SourceDebugExtension": + return b"\x00" # one arbitrary byte; the attribute is an unstructured UTF-8 blob + if attribute in ("InnerClasses", "NestMembers"): + return u2(0) # an empty table + if attribute == "NestHost": + return u2(this_class) + if attribute == "SourceFile": + return u2(pool.utf8("Attr.java")) + raise SystemExit(f"no payload for {attribute}") + + +def duplicated_attribute(name, attribute, major, times=2): + """A class that is valid in every respect except for carrying `attribute` `times` over.""" + pool = Pool() + this_class = pool.klass(name) + super_class = pool.klass("java/lang/Object") + main_name, main_desc, code_name = pool.utf8("main"), pool.utf8("([Ljava/lang/String;)V"), pool.utf8("Code") + + body = b"\xb1" # return + code_attr = u2(0) + u2(1) + u4(len(body)) + body + u2(0) + u2(0) + method = u2(0x0009) + u2(main_name) + u2(main_desc) + u2(1) + u2(code_name) + u4(len(code_attr)) + code_attr + + attribute_name = pool.utf8(attribute) + info = payload(attribute, pool, this_class) + attributes = (u2(attribute_name) + u4(len(info)) + info) * times + + return ( + b"\xca\xfe\xba\xbe" + u2(0) + u2(major) + pool.bytes() + + u2(0x0021) + u2(this_class) + u2(super_class) + + u2(0) + u2(0) + u2(1) + method + + u2(times) + attributes + ) + + +# (filename, attribute, class file version) — the last two must keep loading, see the module docstring. +FIXTURES = { + "DuplicateSourceFile.class": ("DuplicateSourceFile", "SourceFile", 52), + "DuplicateInnerClasses.class": ("DuplicateInnerClasses", "InnerClasses", 52), + "DuplicateSourceDebugExtension.class": ("DuplicateSourceDebugExtension", "SourceDebugExtension", 52), + "DuplicateNestHost.class": ("DuplicateNestHost", "NestHost", 55), + "DuplicateNestMembers.class": ("DuplicateNestMembers", "NestMembers", 55), + "DuplicateNestHostOldMajor.class": ("DuplicateNestHostOldMajor", "NestHost", 52), + "DuplicateSynthetic.class": ("DuplicateSynthetic", "Synthetic", 52), +} + +if __name__ == "__main__": + OUT.mkdir(parents=True, exist_ok=True) + for filename, args in FIXTURES.items(): + (OUT / filename).write_bytes(duplicated_attribute(*args)) + print(f"wrote {OUT / filename}") diff --git a/tests/test_class_format.rs b/tests/test_class_format.rs index 8b47244f..864e5a2c 100644 --- a/tests/test_class_format.rs +++ b/tests/test_class_format.rs @@ -414,3 +414,53 @@ async fn test_lambda_class_reports_unsupported_feature_not_malformed() { "a class javac emits for `x -> x + 1` is not malformed, got: {err}" ); } + +// JVMS 4.7 marks several ClassFile attributes as at-most-one, and the round that rejected a second +// `BootstrapMethods` left "do the others need the same rule?" as an open question. They do — but not +// all of them, and not at every version. +// +// The list `validation.rs` enforces is what OpenJDK 26.0.1 rejects, measured one attribute at a time +// rather than read off the spec, because the two disagree. The two controls below are that +// disagreement, and they are the reason this test is a table with a `loads` column instead of a loop +// over every single-valued attribute: +// +// * `DuplicateNestHostOldMajor` — two `NestHost` attributes at major 52, before the attribute +// exists. JVMS 4.7.1 says an attribute that is not defined is ignored, so it is not a duplicate +// of anything. Counting without the version gate would reject a file every JVM accepts. +// * `DuplicateSynthetic` — JVMS 4.7.8 says at most one; HotSpot takes two. We follow the JVM. +// +// Fixtures: test-data/src/attr/make_attr_fixtures.py, which records the same measurement. +#[tokio::test] +async fn test_a_class_declaring_a_single_valued_attribute_twice_is_malformed() { + for (fixture, rejected) in [ + ("DuplicateSourceFile", true), + ("DuplicateInnerClasses", true), + ("DuplicateSourceDebugExtension", true), + ("DuplicateNestHost", true), + ("DuplicateNestMembers", true), + // controls — these must keep loading, see above + ("DuplicateNestHostOldMajor", false), + ("DuplicateSynthetic", false), + ] { + let path = PathBuf::from(format!("test-data/attr/{fixture}.class")); + let result = run_class(&path, &[Path::new("./test-data/attr/")], &[]).await; + + if rejected { + let err = result.map(|_| ()).err().map(|e| e.to_string()).unwrap_or_default(); + assert!( + err.contains("java.lang.ClassFormatError"), + "{fixture}: expected ClassFormatError, got: {err:?}" + ); + assert!( + !err.contains("UnsupportedOperationException"), + "{fixture}: two of a single-valued attribute is a broken file, not an unsupported feature, got: {err}" + ); + } else { + assert!( + result.is_ok(), + "{fixture}: OpenJDK 26.0.1 loads this one, so we must too — got: {:?}", + result.err().map(|e| e.to_string()) + ); + } + } +} From ee082572e97802d78323af0fa8482b5f94904322 Mon Sep 17 00:00:00 2001 From: jun0 Date: Fri, 18 Sep 2026 01:50:13 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[rustjava-adopt-reject-duplicate-bootstrap-?= =?UTF-8?q?methods-p0]=20docs(state):=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=E2=91=A2=20=EC=B0=A9=EC=A7=80=20=EA=B8=B0=EB=A1=9D=20=EB=8F=99?= =?UTF-8?q?=EB=B4=89=20(PR=20#66=20=C2=B7=20--merge=20=C2=B7=20=ED=95=80?= =?UTF-8?q?=201da1379e)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- STATE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/STATE.md b/STATE.md index 7bb04179..de5b7443 100644 --- a/STATE.md +++ b/STATE.md @@ -15,6 +15,10 @@ ★**개악 4종 전건 red**(M1 게이트 제거·M2 Synthetic 추가 → **통제군** red · M3 한 칸 제거 · M4 호출부 원복) · 복원 17/0. ★**대가**: 동작 변경 · 표는 수동 목록(늘어도 안 울린다) · Synthetic 배제는 JVM 하나에 의존 · ★**`attribute.rs` 한 줄**(제안 target 밖 — 신고). ★`--all` **579/0/1** · 버전 표 **+7행** · DoD 7명령 rc=0. + ★★**게이트③ 착지 — PR #66 · `--merge`**(등재 repo `contracts/upstream-sync-repos.conf:22` · 티켓 `merge_strategy: merge` 선언분 ⇒ ★**계보 보존**). + 게이트② **approve** · 핀 **`1da1379e`** ↔ 착수 시 PR head **동일**(불이동) · ★**`CONFLICTING`** ⇒ 예외 사유 ⓖ 로 별 `-merge` 가 발권된 회차다. + ★**충돌은 원장 2파일뿐**(`STATE.md`·`REPORT.md` 상단 삽입 · 제품 코드 **0**) — base 당김(뒤처짐 **3**) + 합집합 해소. 보존 증명 양방향 소실 **0** · 기여 불변 `--numstat` 정확 일치. + ★핀에서 `ci-presence` **rc=0 CI_GREEN** · 자식 PR **0건** · 배포 **0**(배포 워크플로 없음) · 주기 자동 커밋 **0건**(최다 규칙성 author cv **0.57** > 0.1) · 라이브 실행 주체 **없음**. - [rustjava-adopt-ldc-tags-real-world-generator-survey-p0] ★★**「ldc 픽스처를 ASM 으로 재생성」 제안 — 기각.** ★**제품 코드 0줄**(`declinedProposals` 기록). ★**사유 ⑴ 더 센 오라클이 이미 있다** — ★**OpenJDK 26.0.1 이 양성 4건을 실행한다(전건 rc=0)** · 위법 5건은 전건 rc=1 · 「ASM 이 낸다」는 선행 회차가 이미 동적 실증. ★**⑵ 손의 흔적은 «옮겨갈» 뿐이다**(ASM 도 드라이버 15줄이 모양을 고른다) ★**⑶ 생성기가 «두 기구»가 된다**(음성 픽스처는 ASM 불가) ·