From 8d14d792797f3a0da76405e43f84158826f460d7 Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 17 Sep 2026 14:41:58 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[rustjava-adopt-bound-bootstrap-static-argu?= =?UTF-8?q?ments-p0]=20feat(classfile):=20=EB=B6=80=ED=8A=B8=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=9E=A9=20=EC=A0=95=EC=A0=81=20=EC=9D=B8=EC=9E=90?= =?UTF-8?q?=EB=8A=94=20=C2=AB=EC=A0=81=EC=9E=AC=20=EA=B0=80=EB=8A=A5=20?= =?UTF-8?q?=EC=83=81=EC=88=98=C2=BB=EC=97=AC=EC=95=BC=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 채택 제안 2026-09-16-bound-bootstrap-static-arguments#p0. 직전 회차가 «경계»만 보고 멈춘 자리를 JVMS 4.7.23 이 요구하는 «종류»까지 넓힌다(적재 가능: Integer·Float·Long·Double·Class·String· MethodHandle·MethodType·Dynamic). ★제안이 지목한 위험을 먼저 쟀다: attribute.rs 는 인자를 «해석하지 않는» 것이 설계이고, 그것을 어기면 람다 클래스가 전부 corrupt 가 된다. ⇒ 태그 검사는 «변형이 무엇인가»만 묻고 «안을 읽지» 않으므로 그 설계를 어기지 않는다. 실측: 커밋된 클래스 156개 중 파싱 성공 144 · 실패 12 로 ★이 변경 전후가 «동일»하다(새로 거부되는 파일 0 · 람다 포함). ★OpenJDK 26 실측이 근거다: 인자를 Utf8 로 돌린 StringConcat 에 대해 「ClassFormatError: argument_index 4 has bad constant type」. 즉 그 파일은 «미지원»이 아니라 «파손»이고, 이 검사가 없으면 우리는 나중에 링커에서 UnsupportedOperationException 으로 «우리가 지원하지 않는다»고 말한다 — 이 저장소가 반복해 가른 그 두 낱말이다. 테스트는 classfile/tests/test.rs 에 바이트 패치로 넣었다(새 픽스처 0 · 길이 필드 불변 ⇒ 인자의 «종류»만이 유일한 결함이다). --- classfile/src/validation.rs | 41 ++++++++++++++++++++++++++++++------ classfile/tests/test.rs | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/classfile/src/validation.rs b/classfile/src/validation.rs index 941de2da..03569b56 100644 --- a/classfile/src/validation.rs +++ b/classfile/src/validation.rs @@ -110,18 +110,47 @@ fn constant_pool_tags_fit_the_class_file_version(class: &ClassInfo) -> bool { /// question. It reads no entry, needs no payload, and cannot fail for a class whose arguments point /// somewhere real — which is every class any compiler emits. /// -/// Presence is also all that is checked. JVMS additionally requires the entry to be a *loadable* -/// constant; that is a kind check, it is a different sentence, and this round was scoped to the -/// bound. A file whose argument names a Utf8 is still accepted here. +/// The entry also has to be a **loadable constant** (JVMS 4.7.23, and the loadable column of the +/// 4.4 table: Integer, Float, Long, Double, Class, String, MethodHandle, MethodType, Dynamic). +/// A file whose argument names a Utf8 or a Methodref is rejected here. +/// +/// ★ That is still not resolution, and the distinction is the whole reason this is safe. Reading a +/// *tag* asks which variant the entry is; it never looks inside one. `attribute.rs` keeps the +/// arguments unresolved because a lambda's are method handles and types this crate has no payload +/// for — and this check is exactly what does not need that payload. Measured rather than argued: +/// with the rule in place, every committed fixture still parses, lambdas included (144 parse / 12 +/// fail, unchanged before and after). +/// +/// Why it is worth having: without it, a malformed file is not diagnosed here but *later*, where +/// `ConstantPoolReference::from_constant_pool` returns `None` and the linker declines to link it — +/// which this runtime reports as `UnsupportedOperationException`. That says "we do not support +/// this file" about a file that is simply broken, and keeping those two apart is a line this +/// repository has drawn repeatedly. OpenJDK 26 agrees it is the file: +/// `ClassFormatError: argument_index 4 has bad constant type in class file StringConcat`. /// /// One consequence worth naming: a long or double occupies two pool slots and only the first is /// usable (JVMS 4.4.5), so the second has no entry in this map and an argument naming it is /// rejected. That is the intended reading of "valid index" rather than an accident of the map. fn bootstrap_method_static_arguments_are_in_the_pool(class: &ClassInfo) -> bool { class.attributes.iter().all(|attribute| match attribute { - AttributeInfo::BootstrapMethods(methods) => methods - .iter() - .all(|method| method.arguments.iter().all(|index| class.constant_pool.contains_key(index))), + AttributeInfo::BootstrapMethods(methods) => methods.iter().all(|method| { + method.arguments.iter().all(|index| { + class.constant_pool.get(index).is_some_and(|item| { + matches!( + item, + ConstantPoolItem::Integer(_) + | ConstantPoolItem::Float(_) + | ConstantPoolItem::Long(_) + | ConstantPoolItem::Double(_) + | ConstantPoolItem::Class { .. } + | ConstantPoolItem::String { .. } + | ConstantPoolItem::MethodHandle { .. } + | ConstantPoolItem::MethodType { .. } + | ConstantPoolItem::Dynamic { .. } + ) + }) + }) + }), _ => true, }) } diff --git a/classfile/tests/test.rs b/classfile/tests/test.rs index aad4814c..9e33bee2 100644 --- a/classfile/tests/test.rs +++ b/classfile/tests/test.rs @@ -302,3 +302,45 @@ fn test_bootstrap_method_reference_kinds_outside_the_set_and_mispaired_kinds_are // and a kind that does pair with a Methodref still parses, so the above is not vacuous assert_eq!(parse_with_kind(5), None); } + +// JVMS 4.7.23 requires each `bootstrap_arguments` entry to be a **loadable** constant, not merely +// an index that lands somewhere. The bound was checked before; the kind was not, so a file whose +// argument named a Utf8 parsed fine and only fell over later — at the linker, which reports +// "unsupported" about a file that is actually broken. +// +// OpenJDK 26.0.1 on this exact mutation: `ClassFormatError: argument_index 4 has bad constant type +// in class file StringConcat`. So the rule is real and it is a *format* error. +// +// The mutation repoints the single bootstrap argument at pool entry #4, a Utf8. That keeps every +// other byte — including the length fields — exactly as it was, so the argument's kind is the only +// thing wrong with the file, which is what makes this test able to fail for the right reason. +#[test] +fn test_a_bootstrap_argument_that_is_not_a_loadable_constant_is_rejected() { + let string_concat = include_bytes!("../../test-data/indy/StringConcat.class"); + // unmutated: parses, and the argument really is the String this repoints away from + let class = ClassInfo::parse(string_concat).unwrap(); + assert!(matches!( + ConstantPoolReference::from_constant_pool(&class.constant_pool, bootstrap_methods(&class)[0].arguments[0]), + Some(ConstantPoolReference::String(_)) + )); + + // `num_bootstrap_methods=1, bootstrap_method_ref=#34, num_arguments=1, argument=#32` + let window = [0u8, 1, 0, 34, 0, 1, 0, 32]; + let at = string_concat + .windows(window.len()) + .position(|candidate| candidate == window) + .expect("test-data/indy/StringConcat.class layout changed; the BootstrapMethods entry moved"); + + let mut mutated = string_concat.to_vec(); + mutated[at + 6..at + 8].copy_from_slice(&[0, 4]); // entry #4 is a Utf8 ("java/lang/Object") + assert!( + matches!(class.constant_pool.get(&4), Some(_)), + "the replacement index must be in the pool" + ); + + assert_eq!( + ClassInfo::parse(&mutated).err(), + Some(ClassFileError::InvalidFormat), + "a bootstrap argument naming a Utf8 is not a loadable constant" + ); +} From 5688012ee03602efc1cdc00e63cf69e0f4ba4a3b Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 17 Sep 2026 14:45:38 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[rustjava-adopt-bound-bootstrap-static-argu?= =?UTF-8?q?ments-p0]=20docs(state):=20=ED=9A=8C=EC=B0=A8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=C2=B7=20worklog=20=EC=8C=8D=20=C2=B7=20=EC=A0=9C?= =?UTF-8?q?=EC=95=88=20#p0=20=EC=B1=84=ED=83=9D=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REPORT.md | 15 +++++ STATE.md | 6 ++ ...26-09-17-loadable-bootstrap-arguments.json | 34 ++++++++++ ...2026-09-17-loadable-bootstrap-arguments.md | 66 +++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 docs/worklog/2026-09-17-loadable-bootstrap-arguments.json create mode 100644 docs/worklog/2026-09-17-loadable-bootstrap-arguments.md diff --git a/REPORT.md b/REPORT.md index e7dceb24..83da242c 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,4 +1,19 @@ # REPORT +## [2026-09-17] 부트스트랩 정적 인자는 «적재 가능 상수»여야 한다 — 경계에서 «종류»로 (rustjava-adopt-bound-bootstrap-static-arguments-p0) +- 무엇을: 채택 제안 `2026-09-16-bound-bootstrap-static-arguments#p0`. ★**제품 동작이 바뀐다** — 인자가 적재 불가 상수를 가리키는 클래스 파일이 **`ClassFormatError`** 로 거부된다. +- 왜: JVMS 4.7.23 이 요구하는 것은 «인덱스가 어딘가에 닿는다»가 아니라 ★**「적재 가능 상수」**다(Integer·Float·Long·Double·Class·String·MethodHandle·MethodType·Dynamic). + 직전 회차는 «경계»까지만 하고 «종류»를 남겨 두었고, 그 사실을 **함수 주석이 스스로 적어 두었다**. +- ★★**제안이 스스로 적은 위험을 «먼저» 쟀다** — 「틀리면 람다 클래스가 전부 corrupt 가 된다」. + 그 위험의 실체는 `attribute.rs` 의 설계(인자를 **해석하지 않는다**)이고, ★**태그 검사는 «어느 변형인가»만 묻고 «안을 읽지» 않는다** ⇒ 그 설계를 어기지 않는다. + ★**말이 아니라 수로**: 커밋된 클래스 전건 파싱이 **전 144 / 실패 12 → 후 144 / 실패 12** 로 ★**동일**(새로 거부되는 파일 **0** · 람다 포함). +- ★**OpenJDK 26.0.1 실측이 근거다**: 인자를 Utf8 로 돌리자 ★**`ClassFormatError: argument_index 4 has bad constant type`** ⇒ 그 파일은 «미지원»이 아니라 **«파손»**이다. +- ★**안 하면 무엇이 나쁜가**: 파일이 통과한 뒤 **나중에·틀린 낱말로** 실패한다 — 링커가 `None` 을 받아 링크를 포기하고 + 검증기가 **`UnsupportedOperationException`**(= 「우리가 지원하지 않는다」)을 낸다. ★**이 저장소가 반복해 가른 그 두 낱말**이다. +- ★**개악 2종 전건 red**: M1 «존재만»으로 되돌리기 · ★**M2 적재 가능 집합에 Utf8 한 칸 추가** — M2 가 요지다(단언이 «경계»에 걸려 있음을 보인다). +- 검증: `cargo test --all` **575 passed / 0 failed / 1 ignored** · DoD 7명령 rc=0 · ★**새 픽스처 0**(기존 파일 바이트 패치 · 길이 필드 불변). +- ★**여기서 더 갈 수 없는 자리도 적는다**: `Dynamic` 인자의 서술자가 필드 서술자인지, `MethodHandle` 인자가 실제 멤버로 해석되는지는 **payload 가 필요**해 이 술어의 밖이다(설계이지 누락이 아니다). +- ★후속: `ClassFormatError` 에 **사유를 실어라**(M) — OpenJDK 는 인덱스와 이유를 말한다. ★p2-fix 회차가 낸 같은 제안과 **묶어서** 하는 편이 낫다. + ## [2026-09-17] `StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다» (rustjava-adopt-link-stringconcatfactory-p0) - 무엇을: 레시피 없는 진입점 `makeConcat` 을 링크한다. ★**실행기 무접촉** — 콜사이트 인자 수로 **레시피를 합성**한다. - 왜: 채택 제안 `2026-09-16-link-stringconcatfactory#p0`. diff --git a/STATE.md b/STATE.md index 1d37cacf..9bf5b8a1 100644 --- a/STATE.md +++ b/STATE.md @@ -4,6 +4,12 @@ (없음 — 2026-09-16 실측: 착수 시 진행 티켓 0 · 열린 PR 0. ※「열린 PR 0」은 ★**이 회차 PR 착지 시점 기준**이다 — 회신 시점에는 그 PR 자신이 열려 있다) ## 완료 +- [rustjava-adopt-bound-bootstrap-static-arguments-p0] ★★**부트스트랩 정적 인자 = «적재 가능 상수» — 경계에서 «종류»로.** + 채택 제안 `2026-09-16-bound-bootstrap-static-arguments#p0`(worklog json `adoptedProposals` 기록). ★**제품 동작 변경 있음.** + ★**제안이 적은 위험을 먼저 쟀다**(「틀리면 람다가 전부 corrupt」): 태그 검사는 payload 를 읽지 않으므로 `attribute.rs` 설계와 충돌하지 않고, + ★**커밋된 클래스 파싱이 전/후 «144/12 동일»**(새로 거부 0). ★OpenJDK 26 은 같은 파일을 `ClassFormatError: argument_index 4 has bad constant type` 로 거부한다. + ★**안 하면**: 링커에서 `UnsupportedOperationException` — 「파손」을 「미지원」이라 말하게 된다. + ★개악 2종 red(존재만 되돌리기 · ★집합에 Utf8 한 칸 추가) · `--all` **575/0/1** · 새 픽스처 **0**(바이트 패치). - [rustjava-adopt-link-stringconcatfactory-p0] ★★**`StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다».** 채택 제안 `2026-09-16-link-stringconcatfactory#p0`(worklog json `adoptedProposals` 기록). ★**제품 동작 변경**: `makeConcat` 콜사이트가 **거부 대신 실행**된다. ★**실행기(`concat_with_constants`)는 한 줄도 안 바뀌었다.** diff --git a/docs/worklog/2026-09-17-loadable-bootstrap-arguments.json b/docs/worklog/2026-09-17-loadable-bootstrap-arguments.json new file mode 100644 index 00000000..fe317bbc --- /dev/null +++ b/docs/worklog/2026-09-17-loadable-bootstrap-arguments.json @@ -0,0 +1,34 @@ +{ + "schema": "worklog/v1", + "date": "2026-09-17", + "taskId": "rustjava-adopt-bound-bootstrap-static-arguments-p0", + "summary": "Bootstrap arguments must be loadable constants, not merely indices that land somewhere (JVMS 4.7.23). A tag test, not resolution — which is what makes it safe for the lambda fixtures the previous round was protecting.", + "changes": [ + "classfile/src/validation.rs: bootstrap_method_static_arguments_are_in_the_pool also requires the entry's kind to be loadable (Integer, Float, Long, Double, Class, String, MethodHandle, MethodType, Dynamic)", + "classfile/tests/test.rs: the rule is asserted by repointing StringConcat.class's single bootstrap argument at a Utf8 entry — byte patch, no new committed fixture" + ], + "verification": [ + "OpenJDK 26.0.1 on the same mutation: ClassFormatError: argument_index 4 has bad constant type in class file StringConcat", + "risk the proposal named, measured: 156 committed class files parse 144 / fail 12 — identical before and after, so no lambda class became corrupt", + "mutation: revert to presence-only -> red; widen the loadable set by one (add Utf8) -> red, so the boundary is observable and not just the check's presence", + "cargo test --all: 575 passed / 0 failed / 1 ignored" + ], + "issues": [ + "This tightens a file that open PR #61 also edits (a different function in classfile/src/validation.rs). The two should auto-merge, but whichever lands second should check.", + "The kind check is the last thing this predicate can do without payload access. Whether a Dynamic argument's own descriptor is a field descriptor, or whether a MethodHandle argument resolves, remains out of reach here by design." + ], + "adoptedProposals": [ + "2026-09-16-bound-bootstrap-static-arguments#p0" + ], + "proposals": [ + { + "title": "Say which bootstrap argument was bad, and why", + "plainSummary": "The file is now rejected, but the message is the same flat 'Invalid class file' every other parse failure gets.", + "userBenefit": "OpenJDK names the index and the problem ('argument_index 4 has bad constant type'); a person holding a rejected class file currently has to bisect it to find out which argument.", + "why": "This round added a rule whose value is precision, and the report of it carries none. The predicate knows the failing index at the moment it returns false.", + "tradeoff": "ClassFileError is flat by an upstream cut (822504b); adding variants touches the error type every validator shares. The same follow-up is already proposed from the p2-fix round, so the two should be done together rather than twice.", + "effort": "M", + "target": "classfile/src/error.rs, classfile/src/validation.rs" + } + ] +} diff --git a/docs/worklog/2026-09-17-loadable-bootstrap-arguments.md b/docs/worklog/2026-09-17-loadable-bootstrap-arguments.md new file mode 100644 index 00000000..e7ecc090 --- /dev/null +++ b/docs/worklog/2026-09-17-loadable-bootstrap-arguments.md @@ -0,0 +1,66 @@ +# 2026-09-17 — Bootstrap arguments have to be constants you can actually load + +`taskId: rustjava-adopt-bound-bootstrap-static-arguments-p0` · +adopts `2026-09-16-bound-bootstrap-static-arguments#p0` + +The previous round checked that every `bootstrap_arguments` entry **points at something**. JVMS +4.7.23 asks for more: each one has to be a **loadable constant** — Integer, Float, Long, Double, +Class, String, MethodHandle, MethodType, Dynamic. A file whose argument named a Utf8 parsed fine. + +## The proposal's own worry, measured first + +> "getting it wrong turns every lambda class corrupt" + +That worry is exact, and it is why `attribute.rs` keeps bootstrap arguments unresolved: a lambda's +are MethodHandle and MethodType entries this crate has no payload for. Resolving them would turn +every lambda from *unsupported* into *corrupt*. + +A tag test is not that. It asks **which variant** an entry is; it never looks inside one. To show +that rather than assert it, every committed class file was parsed before and after: + +| | parses | fails | +|---|---|---| +| before | 144 | 12 | +| after | **144** | **12** | + +Identical — the 12 are the deliberately-corrupt fixtures that already failed. **Zero** files changed +side, lambdas included. + +## What the real JVM says + +Repointing `StringConcat.class`'s single bootstrap argument at pool entry #4 (a Utf8): + +``` +$ java -cp . StringConcat # OpenJDK 26.0.1 +java.lang.ClassFormatError: argument_index 4 has bad constant type in class file StringConcat +``` + +So it is a **format** error, and the message names the same concept this predicate now applies. + +## Why it is worth having at all + +Without the rule the file is not accepted forever — it fails **later and in the wrong words**. +`ConstantPoolReference::from_constant_pool` returns `None` for a Utf8, the linker declines to link +the call site, and the verifier reports `UnsupportedOperationException`: *we do not support this +file*. About a file that is simply broken. Keeping "unsupported" and "malformed" apart is a line +this repository has drawn in several rounds, and this is the same line. + +## Mutations + +| | mutation | result | +|---|---|---| +| **M1** | back to presence-only | **red** | +| **M2** | widen the loadable set by one (admit Utf8) | **red** | + +M2 is the one that matters: it shows the assertion is about the **boundary**, not merely that some +check exists. + +## Scope + +The test is a byte patch on an existing fixture — no new committed binary, and every length field +untouched, so the argument's *kind* is the only thing wrong with the mutated file. `cargo test +--all`: **575 passed / 0 failed / 1 ignored**. + +What this predicate still cannot ask is anything needing a payload: whether a `Dynamic` argument's +own descriptor is a field descriptor, or whether a `MethodHandle` argument resolves to a real +member. That is by design, not an omission.