Skip to content

Commit 06fa865

Browse files
authored
[rustjava-adopt-bound-bootstrap-static-arguments-p0] feat(classfile): 부트스트랩 정적 인자는 「적재 가능 상수」여야 한다 (#62)
[rustjava-adopt-bound-bootstrap-static-arguments-p0] feat(classfile): 부트스트랩 정적 인자는 「적재 가능 상수」여야 한다
2 parents 26db563 + 5688012 commit 06fa865

6 files changed

Lines changed: 198 additions & 6 deletions

File tree

‎REPORT.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
# REPORT
2+
## [2026-09-17] 부트스트랩 정적 인자는 «적재 가능 상수»여야 한다 — 경계에서 «종류»로 (rustjava-adopt-bound-bootstrap-static-arguments-p0)
3+
- 무엇을: 채택 제안 `2026-09-16-bound-bootstrap-static-arguments#p0`. ★**제품 동작이 바뀐다** — 인자가 적재 불가 상수를 가리키는 클래스 파일이 **`ClassFormatError`** 로 거부된다.
4+
- 왜: JVMS 4.7.23 이 요구하는 것은 «인덱스가 어딘가에 닿는다»가 아니라 ★**「적재 가능 상수」**다(Integer·Float·Long·Double·Class·String·MethodHandle·MethodType·Dynamic).
5+
직전 회차는 «경계»까지만 하고 «종류»를 남겨 두었고, 그 사실을 **함수 주석이 스스로 적어 두었다**.
6+
- ★★**제안이 스스로 적은 위험을 «먼저» 쟀다** — 「틀리면 람다 클래스가 전부 corrupt 가 된다」.
7+
그 위험의 실체는 `attribute.rs` 의 설계(인자를 **해석하지 않는다**)이고, ★**태그 검사는 «어느 변형인가»만 묻고 «안을 읽지» 않는다** ⇒ 그 설계를 어기지 않는다.
8+
★**말이 아니라 수로**: 커밋된 클래스 전건 파싱이 **전 144 / 실패 12 → 후 144 / 실패 12** 로 ★**동일**(새로 거부되는 파일 **0** · 람다 포함).
9+
- ★**OpenJDK 26.0.1 실측이 근거다**: 인자를 Utf8 로 돌리자 ★**`ClassFormatError: argument_index 4 has bad constant type`** ⇒ 그 파일은 «미지원»이 아니라 **«파손»**이다.
10+
- ★**안 하면 무엇이 나쁜가**: 파일이 통과한 뒤 **나중에·틀린 낱말로** 실패한다 — 링커가 `None` 을 받아 링크를 포기하고
11+
검증기가 **`UnsupportedOperationException`**(= 「우리가 지원하지 않는다」)을 낸다. ★**이 저장소가 반복해 가른 그 두 낱말**이다.
12+
- ★**개악 2종 전건 red**: M1 «존재만»으로 되돌리기 · ★**M2 적재 가능 집합에 Utf8 한 칸 추가** — M2 가 요지다(단언이 «경계»에 걸려 있음을 보인다).
13+
- 검증: `cargo test --all` **575 passed / 0 failed / 1 ignored** · DoD 7명령 rc=0 · ★**새 픽스처 0**(기존 파일 바이트 패치 · 길이 필드 불변).
14+
- ★**여기서 더 갈 수 없는 자리도 적는다**: `Dynamic` 인자의 서술자가 필드 서술자인지, `MethodHandle` 인자가 실제 멤버로 해석되는지는 **payload 가 필요**해 이 술어의 밖이다(설계이지 누락이 아니다).
15+
- ★후속: `ClassFormatError` 에 **사유를 실어라**(M) — OpenJDK 는 인덱스와 이유를 말한다. ★p2-fix 회차가 낸 같은 제안과 **묶어서** 하는 편이 낫다.
16+
217
## [2026-09-17] `StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다» (rustjava-adopt-link-stringconcatfactory-p0)
318
- 무엇을: 레시피 없는 진입점 `makeConcat` 을 링크한다. ★**실행기 무접촉** — 콜사이트 인자 수로 **레시피를 합성**한다.
419
- 왜: 채택 제안 `2026-09-16-link-stringconcatfactory#p0`.

‎STATE.md‎

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

66
## 완료
7+
- [rustjava-adopt-bound-bootstrap-static-arguments-p0] ★★**부트스트랩 정적 인자 = «적재 가능 상수» — 경계에서 «종류»로.**
8+
채택 제안 `2026-09-16-bound-bootstrap-static-arguments#p0`(worklog json `adoptedProposals` 기록). ★**제품 동작 변경 있음.**
9+
★**제안이 적은 위험을 먼저 쟀다**(「틀리면 람다가 전부 corrupt」): 태그 검사는 payload 를 읽지 않으므로 `attribute.rs` 설계와 충돌하지 않고,
10+
★**커밋된 클래스 파싱이 전/후 «144/12 동일»**(새로 거부 0). ★OpenJDK 26 은 같은 파일을 `ClassFormatError: argument_index 4 has bad constant type` 로 거부한다.
11+
★**안 하면**: 링커에서 `UnsupportedOperationException` — 「파손」을 「미지원」이라 말하게 된다.
12+
★개악 2종 red(존재만 되돌리기 · ★집합에 Utf8 한 칸 추가) · `--all` **575/0/1** · 새 픽스처 **0**(바이트 패치).
713
- [rustjava-adopt-link-stringconcatfactory-p0] ★★**`StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다».**
814
채택 제안 `2026-09-16-link-stringconcatfactory#p0`(worklog json `adoptedProposals` 기록).
915
★**제품 동작 변경**: `makeConcat` 콜사이트가 **거부 대신 실행**된다. ★**실행기(`concat_with_constants`)는 한 줄도 안 바뀌었다.**

‎classfile/src/validation.rs‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,47 @@ fn constant_pool_tags_fit_the_class_file_version(class: &ClassInfo) -> bool {
110110
/// question. It reads no entry, needs no payload, and cannot fail for a class whose arguments point
111111
/// somewhere real — which is every class any compiler emits.
112112
///
113-
/// Presence is also all that is checked. JVMS additionally requires the entry to be a *loadable*
114-
/// constant; that is a kind check, it is a different sentence, and this round was scoped to the
115-
/// bound. A file whose argument names a Utf8 is still accepted here.
113+
/// The entry also has to be a **loadable constant** (JVMS 4.7.23, and the loadable column of the
114+
/// 4.4 table: Integer, Float, Long, Double, Class, String, MethodHandle, MethodType, Dynamic).
115+
/// A file whose argument names a Utf8 or a Methodref is rejected here.
116+
///
117+
/// ★ That is still not resolution, and the distinction is the whole reason this is safe. Reading a
118+
/// *tag* asks which variant the entry is; it never looks inside one. `attribute.rs` keeps the
119+
/// arguments unresolved because a lambda's are method handles and types this crate has no payload
120+
/// for — and this check is exactly what does not need that payload. Measured rather than argued:
121+
/// with the rule in place, every committed fixture still parses, lambdas included (144 parse / 12
122+
/// fail, unchanged before and after).
123+
///
124+
/// Why it is worth having: without it, a malformed file is not diagnosed here but *later*, where
125+
/// `ConstantPoolReference::from_constant_pool` returns `None` and the linker declines to link it —
126+
/// which this runtime reports as `UnsupportedOperationException`. That says "we do not support
127+
/// this file" about a file that is simply broken, and keeping those two apart is a line this
128+
/// repository has drawn repeatedly. OpenJDK 26 agrees it is the file:
129+
/// `ClassFormatError: argument_index 4 has bad constant type in class file StringConcat`.
116130
///
117131
/// One consequence worth naming: a long or double occupies two pool slots and only the first is
118132
/// usable (JVMS 4.4.5), so the second has no entry in this map and an argument naming it is
119133
/// rejected. That is the intended reading of "valid index" rather than an accident of the map.
120134
fn bootstrap_method_static_arguments_are_in_the_pool(class: &ClassInfo) -> bool {
121135
class.attributes.iter().all(|attribute| match attribute {
122-
AttributeInfo::BootstrapMethods(methods) => methods
123-
.iter()
124-
.all(|method| method.arguments.iter().all(|index| class.constant_pool.contains_key(index))),
136+
AttributeInfo::BootstrapMethods(methods) => methods.iter().all(|method| {
137+
method.arguments.iter().all(|index| {
138+
class.constant_pool.get(index).is_some_and(|item| {
139+
matches!(
140+
item,
141+
ConstantPoolItem::Integer(_)
142+
| ConstantPoolItem::Float(_)
143+
| ConstantPoolItem::Long(_)
144+
| ConstantPoolItem::Double(_)
145+
| ConstantPoolItem::Class { .. }
146+
| ConstantPoolItem::String { .. }
147+
| ConstantPoolItem::MethodHandle { .. }
148+
| ConstantPoolItem::MethodType { .. }
149+
| ConstantPoolItem::Dynamic { .. }
150+
)
151+
})
152+
})
153+
}),
125154
_ => true,
126155
})
127156
}

‎classfile/tests/test.rs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,45 @@ fn test_bootstrap_method_reference_kinds_outside_the_set_and_mispaired_kinds_are
302302
// and a kind that does pair with a Methodref still parses, so the above is not vacuous
303303
assert_eq!(parse_with_kind(5), None);
304304
}
305+
306+
// JVMS 4.7.23 requires each `bootstrap_arguments` entry to be a **loadable** constant, not merely
307+
// an index that lands somewhere. The bound was checked before; the kind was not, so a file whose
308+
// argument named a Utf8 parsed fine and only fell over later — at the linker, which reports
309+
// "unsupported" about a file that is actually broken.
310+
//
311+
// OpenJDK 26.0.1 on this exact mutation: `ClassFormatError: argument_index 4 has bad constant type
312+
// in class file StringConcat`. So the rule is real and it is a *format* error.
313+
//
314+
// The mutation repoints the single bootstrap argument at pool entry #4, a Utf8. That keeps every
315+
// other byte — including the length fields — exactly as it was, so the argument's kind is the only
316+
// thing wrong with the file, which is what makes this test able to fail for the right reason.
317+
#[test]
318+
fn test_a_bootstrap_argument_that_is_not_a_loadable_constant_is_rejected() {
319+
let string_concat = include_bytes!("../../test-data/indy/StringConcat.class");
320+
// unmutated: parses, and the argument really is the String this repoints away from
321+
let class = ClassInfo::parse(string_concat).unwrap();
322+
assert!(matches!(
323+
ConstantPoolReference::from_constant_pool(&class.constant_pool, bootstrap_methods(&class)[0].arguments[0]),
324+
Some(ConstantPoolReference::String(_))
325+
));
326+
327+
// `num_bootstrap_methods=1, bootstrap_method_ref=#34, num_arguments=1, argument=#32`
328+
let window = [0u8, 1, 0, 34, 0, 1, 0, 32];
329+
let at = string_concat
330+
.windows(window.len())
331+
.position(|candidate| candidate == window)
332+
.expect("test-data/indy/StringConcat.class layout changed; the BootstrapMethods entry moved");
333+
334+
let mut mutated = string_concat.to_vec();
335+
mutated[at + 6..at + 8].copy_from_slice(&[0, 4]); // entry #4 is a Utf8 ("java/lang/Object")
336+
assert!(
337+
matches!(class.constant_pool.get(&4), Some(_)),
338+
"the replacement index must be in the pool"
339+
);
340+
341+
assert_eq!(
342+
ClassInfo::parse(&mutated).err(),
343+
Some(ClassFileError::InvalidFormat),
344+
"a bootstrap argument naming a Utf8 is not a loadable constant"
345+
);
346+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"schema": "worklog/v1",
3+
"date": "2026-09-17",
4+
"taskId": "rustjava-adopt-bound-bootstrap-static-arguments-p0",
5+
"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.",
6+
"changes": [
7+
"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)",
8+
"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"
9+
],
10+
"verification": [
11+
"OpenJDK 26.0.1 on the same mutation: ClassFormatError: argument_index 4 has bad constant type in class file StringConcat",
12+
"risk the proposal named, measured: 156 committed class files parse 144 / fail 12 — identical before and after, so no lambda class became corrupt",
13+
"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",
14+
"cargo test --all: 575 passed / 0 failed / 1 ignored"
15+
],
16+
"issues": [
17+
"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.",
18+
"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."
19+
],
20+
"adoptedProposals": [
21+
"2026-09-16-bound-bootstrap-static-arguments#p0"
22+
],
23+
"proposals": [
24+
{
25+
"title": "Say which bootstrap argument was bad, and why",
26+
"plainSummary": "The file is now rejected, but the message is the same flat 'Invalid class file' every other parse failure gets.",
27+
"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.",
28+
"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.",
29+
"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.",
30+
"effort": "M",
31+
"target": "classfile/src/error.rs, classfile/src/validation.rs"
32+
}
33+
]
34+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# 2026-09-17 — Bootstrap arguments have to be constants you can actually load
2+
3+
`taskId: rustjava-adopt-bound-bootstrap-static-arguments-p0` ·
4+
adopts `2026-09-16-bound-bootstrap-static-arguments#p0`
5+
6+
The previous round checked that every `bootstrap_arguments` entry **points at something**. JVMS
7+
4.7.23 asks for more: each one has to be a **loadable constant** — Integer, Float, Long, Double,
8+
Class, String, MethodHandle, MethodType, Dynamic. A file whose argument named a Utf8 parsed fine.
9+
10+
## The proposal's own worry, measured first
11+
12+
> "getting it wrong turns every lambda class corrupt"
13+
14+
That worry is exact, and it is why `attribute.rs` keeps bootstrap arguments unresolved: a lambda's
15+
are MethodHandle and MethodType entries this crate has no payload for. Resolving them would turn
16+
every lambda from *unsupported* into *corrupt*.
17+
18+
A tag test is not that. It asks **which variant** an entry is; it never looks inside one. To show
19+
that rather than assert it, every committed class file was parsed before and after:
20+
21+
| | parses | fails |
22+
|---|---|---|
23+
| before | 144 | 12 |
24+
| after | **144** | **12** |
25+
26+
Identical — the 12 are the deliberately-corrupt fixtures that already failed. **Zero** files changed
27+
side, lambdas included.
28+
29+
## What the real JVM says
30+
31+
Repointing `StringConcat.class`'s single bootstrap argument at pool entry #4 (a Utf8):
32+
33+
```
34+
$ java -cp . StringConcat # OpenJDK 26.0.1
35+
java.lang.ClassFormatError: argument_index 4 has bad constant type in class file StringConcat
36+
```
37+
38+
So it is a **format** error, and the message names the same concept this predicate now applies.
39+
40+
## Why it is worth having at all
41+
42+
Without the rule the file is not accepted forever — it fails **later and in the wrong words**.
43+
`ConstantPoolReference::from_constant_pool` returns `None` for a Utf8, the linker declines to link
44+
the call site, and the verifier reports `UnsupportedOperationException`: *we do not support this
45+
file*. About a file that is simply broken. Keeping "unsupported" and "malformed" apart is a line
46+
this repository has drawn in several rounds, and this is the same line.
47+
48+
## Mutations
49+
50+
| | mutation | result |
51+
|---|---|---|
52+
| **M1** | back to presence-only | **red** |
53+
| **M2** | widen the loadable set by one (admit Utf8) | **red** |
54+
55+
M2 is the one that matters: it shows the assertion is about the **boundary**, not merely that some
56+
check exists.
57+
58+
## Scope
59+
60+
The test is a byte patch on an existing fixture — no new committed binary, and every length field
61+
untouched, so the argument's *kind* is the only thing wrong with the mutated file. `cargo test
62+
--all`: **575 passed / 0 failed / 1 ignored**.
63+
64+
What this predicate still cannot ask is anything needing a payload: whether a `Dynamic` argument's
65+
own descriptor is a field descriptor, or whether a `MethodHandle` argument resolves to a real
66+
member. That is by design, not an omission.

0 commit comments

Comments
 (0)