From eb8b4eb608efb1c7ceb2f583a6c9c73825cdfaf7 Mon Sep 17 00:00:00 2001 From: jun0 Date: Wed, 16 Sep 2026 16:20:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[rustjava-cp-tag-switch-passthrough-mutatio?= =?UTF-8?q?n-detectable]=20test(classfile):=20=ED=83=9C=EA=B7=B8=20pass-th?= =?UTF-8?q?rough=20=EA=B0=9C=EC=95=85=EC=9D=84=20end-to-end=20=EB=A1=9C=20?= =?UTF-8?q?=C2=AB=EC=9E=A1=ED=9E=88=EA=B2=8C=C2=BB=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「알 수 없는 상수풀 태그를 거부한다」는 테스트가 그 가지를 개악해도 green 이었다. - 근인(판별 실험): 옛 픽스처가 Hello.class 의 «참조되는» Methodref 슬롯을 덮어 파일이 여러 경로로 동시에 깨졌고, ClassFileError 가 문면을 평탄화해 「태그가 미지라 거부」와 「클래스가 무너져 거부」를 구별하지 못했다. ★desync 가설은 기각했다 — 4바이트를 정확히 소비하는 개악도 green 이었다. - 처방: 단언을 조이는 것이 아니라 입력이 그 가지에 «유일한 결함»으로 도달하게 했다. test-data/cp/UnreferencedTag{13,14,19}.class = 참조되지 않고 · 페이로드 0 · 상수풀 맨 끝인 엔트리 하나만 미지 태그. - 전/후: 같은 개악에 대해 전 ok → 후 red(문면 must be rejected: "" = 클래스가 실행됨). 다른 가지(태그 16) 개악 → 6 테스트 red ⇒ 스위트는 여전히 switch 전체를 지킨다. ★제품 코드 변경 0 — 개악은 실증용 임시이고 전부 되돌렸다. --- REPORT.md | 24 +++++ STATE.md | 26 ++++- ...6-09-16-cp-tag-passthrough-detectable.json | 46 +++++++++ ...026-09-16-cp-tag-passthrough-detectable.md | 68 +++++++++++++ test-data/cp/UnreferencedTag13.class | Bin 0 -> 136 bytes test-data/cp/UnreferencedTag14.class | Bin 0 -> 136 bytes test-data/cp/UnreferencedTag19.class | Bin 0 -> 136 bytes test-data/src/cp/make_cp_fixtures.py | 90 ++++++++++++++++++ tests/test_class_format.rs | 28 ++++-- 9 files changed, 270 insertions(+), 12 deletions(-) create mode 100644 docs/worklog/2026-09-16-cp-tag-passthrough-detectable.json create mode 100644 docs/worklog/2026-09-16-cp-tag-passthrough-detectable.md create mode 100644 test-data/cp/UnreferencedTag13.class create mode 100644 test-data/cp/UnreferencedTag14.class create mode 100644 test-data/cp/UnreferencedTag19.class create mode 100644 test-data/src/cp/make_cp_fixtures.py diff --git a/REPORT.md b/REPORT.md index 6b24aab0..8b7a8588 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,5 +1,29 @@ # REPORT +## [2026-09-16] 「알 수 없는 태그를 거부한다」는 테스트가 ★**그것을 지키지 않았다** — 지키게 했다 (rustjava-cp-tag-switch-passthrough-mutation-detectable) +- 무엇을: `test_unsupported_constant_pool_tag_raises_class_format_error` 가 ★**상수풀 태그 switch 의 pass-through 가지를 + 개악해도 green** 이었다. 그 가지가 «끝에서 끝까지» 관측되도록 **픽스처를 바꿔** 이제 **red** 가 되게 했다. + ★**제품 코드 변경 0**(개악은 실증용 임시 · 전부 되돌렸다 · `git status` 로 확인). +- 왜: 채택 제안 `2026-09-16-ldc-tags-15-16-17#p1`. ★**「소비되지 않는 경보는 장식이다」의 테스트판** — 상수 pass 로 바꿔도 + 통과하는 단언은 «없는 것과 같다». +- 사용자 영향: **없다**(테스트만 바뀐다). 바뀐 것은 ★**그 단언이 실제로 무엇을 잠그는가**다. +- ★★**근인 — 「통과한 진짜 이유」를 판별 실험으로 좁혔다.** 옛 테스트는 `Hello.class` **상수풀 1번**의 태그 바이트를 덮었는데, + 그 슬롯은 ★**코드가 `invokespecial` 로 «참조»하는 Methodref** 다 ⇒ 덮는 순간 파일이 **여러 경로로 동시에** 깨진다. + `ClassFileError` 는 모든 파싱 실패를 ★**「Invalid class file」로 평탄화**하므로(그 파일이 스스로 적어 둔 사실) + 단언이 ★**「태그가 미지라 거부」와 「클래스가 무너져 거부」를 구별하지 못한다.** + ★**바이트 폭 어긋남(desync)은 근인이 «아니었다»** — 4바이트를 정확히 소비하는 개악으로도 **여전히 green** 이었다(판별 실험 B). +- ★**처방은 단언 조이기가 «아니다»**(문면이 평탄해 불가능하다) — ★**입력이 그 가지에 «유일한 결함»으로 도달하게** 했다: + `test-data/cp/UnreferencedTag{13,14,19}.class`(신규 생성기 `test-data/src/cp/make_cp_fixtures.py`) = + ★**참조되지 않고 · 페이로드가 없고 · 상수풀 «맨 끝»**인 엔트리 하나만 미지 태그다. + ★그 세 성질이 «전부» 값한다 — 맨 끝 + 페이로드 0이라야 pass-through 개악이 **정상 동작하는 클래스**를 만들고, 그래야 red 가 된다. +- ★★**전/후 — 같은 개악, 다른 결과**: ⑴**전**: pass-through 개악 → 그 테스트 **ok**(스위트에서 무는 것은 `classfile` 단위 테스트 1건뿐) + ⑵**후**: 같은 개악 → ★**red**(실패 문면이 `must be rejected: ""` = 클래스가 «성공적으로 실행»됐다는 뜻). + ⑶**다른 가지 개악**(태그 16 거부) → **6 테스트 red** ⇒ 스위트가 여전히 switch 전체를 지킨다. +- 검증: `cargo test --all` **568 / 0 failed / 1 ignored**(수 불변 — 테스트 1개 치환) · DoD 7줄 rc=0 · 픽스처 재생성 멱등 · + ★**옛 픽스처(`BadTag*`)를 쓰던 다른 테스트 0건**(전수 확인) · `hello_class()`·`fixture()` 헬퍼는 여전히 4·5회 쓰인다(고아 0). +- 후속 추천: ⑴같은 자를 다른 «조용한» 단언에 대 보기(개악 내성 감사) ⑵`ClassFileError` 에 원인 변종을 되살릴지 판정(상류 과제). + 상세 = `docs/worklog/2026-09-16-cp-tag-passthrough-detectable.md`. + ## [2026-09-16] `BootstrapMethods` 를 «구조»로 읽는다 — 콜사이트 링크는 0줄 (rustjava-invokedynamic-bootstrapmethods-and-methodhandle) - 무엇을: `AttributeInfo::BootstrapMethods` 를 **`Vec` → `Vec`**(JVMS 4.7.23)로 파싱하고, `CONSTANT_MethodHandle` 을 `MethodHandleRef`(`MethodHandleKind` 9종 + 클래스·이름·서술자)로 **해독**한다. diff --git a/STATE.md b/STATE.md index 0e83f532..ebca147a 100644 --- a/STATE.md +++ b/STATE.md @@ -4,6 +4,29 @@ (없음 — 2026-09-16 실측: 착수 시 진행 티켓 0 · 열린 PR 0. ※「열린 PR 0」은 ★**이 회차 PR 착지 시점 기준**이다 — 회신 시점에는 그 PR 자신이 열려 있다) ## 완료 +- [rustjava-cp-tag-switch-passthrough-mutation-detectable] ★★**「알 수 없는 태그를 거부한다」는 테스트가 그것을 «지키지 않았다» — 지키게 했다.** + 채택 제안 `2026-09-16-ldc-tags-15-16-17#p1`(worklog json `adoptedProposals` 기록). + ★**제품 코드 변경 «0»** — 개악은 실증용 임시이고 전부 되돌렸다(`git status classfile/ jvm-bytecode/` **0건**으로 확인). + ★★**대전제를 «먼저» 실증했다**(티켓 ⓐ): 태그 switch 의 `_ => Err(...)` 를 `_ => Ok(Integer(0))` 로 개악하니 + ★**그 테스트는 «ok»** 였고 스위트에서 무는 것은 `constant_pool::tests::tags_outside_the_accepted_set_are_still_rejected` + **단 1건**이었다 ⇒ ★**end-to-end 층에는 그 가지를 무는 것이 «없었다»**(직전 회차가 「M5 층 어긋남」으로 남긴 그것). + ★★**근인 — 판별 실험으로 좁혔다(추측 아님)**: 옛 테스트는 `Hello.class` **상수풀 1번**의 태그를 덮는데 + 그 슬롯은 ★**코드가 참조하는 Methodref** 라 덮는 순간 파일이 **여러 경로로 동시에** 깨진다. 그리고 `ClassFileError` 가 + 모든 파싱 실패를 ★**「Invalid class file」로 평탄화**하므로(그 테스트 파일이 스스로 적어 둔 사실) 단언이 + ★**「태그가 미지라 거부」와 「클래스가 무너져 거부」를 구별하지 못한다.** + ★**바이트 어긋남(desync)은 근인이 «아니다»** — 4바이트를 정확히 소비하는 개악(B)으로도 **여전히 green** 이었다. + ⇒ ★**두 가설 중 하나를 실험으로 기각했다.** + ★★**그러므로 처방이 «단언 조이기»가 아니다** — 문면이 평탄해 조일 것이 없다. 티켓 계약 1 이 예측한 대로 + ★**입력이 그 가지에 «유일한 결함»으로 도달하게** 만들었다: `test-data/cp/UnreferencedTag{13,14,19}.class` + (생성기 `test-data/src/cp/make_cp_fixtures.py` 신규) = ★**참조되지 않고 · 페이로드 0 · 상수풀 «맨 끝»** 인 엔트리 하나. + ★**세 성질이 전부 값한다** — 맨 끝 + 페이로드 0 이라야 pass-through 개악이 ★**«정상 동작하는» 클래스**를 만들고, + 그래야 테스트가 red 가 된다(그렇지 않으면 «다르게 깨진» 파일이 되어 또 green 이다). + ★★**전/후 — 같은 개악, 다른 결과**: **전** = 그 테스트 **ok** / **후** = ★**red** + (실패 문면 `a tag that cannot appear in a class file must be rejected: ""` — ★빈 출력 = 클래스가 «성공적으로 실행»됐다). + ★**⒞ 다른 가지 개악**(태그 16 거부) → **6 테스트 red** ⇒ 스위트가 여전히 switch 전체를 지킨다(이 회차가 좁히지 않았다). + ★`cargo test --all` **568 / 0 failed / 1 ignored**(★수 불변 — 테스트 1개 «치환») · DoD **7줄 전건 rc=0** · 픽스처 재생성 **멱등**. + ★**계약 2⒜ 전수 확인**: 옛 픽스처(`BadTag*`)를 쓰던 다른 테스트 **0건** · `hello_class()`·`fixture()` 헬퍼는 여전히 **4·5회** 쓰여 고아 0. + ★**계약 2⒝ 오탐**: 새 단언은 ★**오탐이 늘지 않는다** — 픽스처가 «유일한 결함»만 갖도록 지어져 있어 다른 변경이 이 테스트를 흔들 경로가 좁다. - [rustjava-invokedynamic-bootstrapmethods-and-methodhandle] ★★**`BootstrapMethods` 를 «구조»로 읽는다 — ④-1 의 ⒜ 를 닫았다. ★콜사이트 링크 0줄.** 채택 제안 `2026-09-16-cp-tags-15-18-parse#p0`(worklog json `adoptedProposals` 에 기록). ★**`AttributeInfo::BootstrapMethods(Vec)` → `Vec`** · 신규 공개 타입 3종 @@ -900,8 +923,7 @@ green 전건 rc=0 · `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 참조 JVM 은 `Missing BootstrapMethods attribute` 인데 우리는 **여전히 「미지원」**이다. ★그 경계 검사는 **속성 파싱이 정말로 필요**하므로 ④-1(PR #45 리니지) 몫이다 — ★**픽스처와 테스트로 «현재 답»을 잠가 뒀으니 그 회차가 닫으면 그 단언이 «시끄럽게» 진다.** - ★**남긴 것 둘 더**: ⑵★**M5 층 어긋남**: 상수풀 태그 pass-through 개악을 - `test_class_format` 이 **못 잡는다**(무는 것은 `classfile` 단위 테스트) ⑶서드파티 생성기 corpus **미측정**(이 머신에 jar 0개). + ★**남긴 것 둘 더**: ⑵★**M5 층 어긋남**: ★**[2026-09-16 닫힘 · `rustjava-cp-tag-switch-passthrough-mutation-detectable`] 이제 `test_class_format` 이 «잡는다»**(픽스처를 «유일한 결함»으로 다시 지었다 — 종전엔 참조되는 Methodref 슬롯을 덮어 «다른 이유»로 통과했다) ⑶서드파티 생성기 corpus **미측정**(이 머신에 jar 0개). 3. ★InputStreamReader 디코더 — 아래 사료 절 셋째 항목 그대로 **살아 있다**(별건). ---- 이하 사료(2026-08-16 기재 · 크레이트 경로·태그 서술은 낡았다) ---- diff --git a/docs/worklog/2026-09-16-cp-tag-passthrough-detectable.json b/docs/worklog/2026-09-16-cp-tag-passthrough-detectable.json new file mode 100644 index 00000000..2628d8bb --- /dev/null +++ b/docs/worklog/2026-09-16-cp-tag-passthrough-detectable.json @@ -0,0 +1,46 @@ +{ + "schema": "worklog/v1", + "date": "2026-09-16", + "taskId": "rustjava-cp-tag-switch-passthrough-mutation-detectable", + "summary": "A test that claimed to prove 'we still reject unknown constant pool tags' passed for a different reason. Rebuilt its fixture so the unknown tag is the only defect, making the pass-through mutation detectable end to end.", + "changes": [ + "test-data/src/cp/make_cp_fixtures.py + test-data/cp/UnreferencedTag{13,14,19}.class: unreferenced, payload-free, trailing pool entry carrying the unknown tag", + "tests/test_class_format.rs: test_unsupported_constant_pool_tag_raises_class_format_error now loads those fixtures instead of overwriting Hello.class's first (referenced) entry" + ], + "verification": [ + "before: pass-through branch mutated to accept -> the test still passed; only constant_pool::tests::tags_outside_the_accepted_set_are_still_rejected caught it", + "competing hypothesis rejected by experiment: a mutation consuming exactly 4 bytes (no desync) also left the test green", + "after: same mutation -> test red, failing with 'must be rejected: \"\"' (empty output = the class ran)", + "a different branch (tag 16) mutated -> 6 tests red, so the suite still guards the whole switch", + "cargo test --all: 568 passed / 0 failed / 1 ignored (count unchanged: a test was replaced)", + "product code unchanged in the final diff; constant_pool.rs restored to sha 393e0594d7eb647e", + "no other test referenced the old BadTag* fixtures; hello_class()/fixture() helpers still used", + "DoD 7 commands all rc=0; fixture regeneration idempotent" + ], + "issues": [ + "ClassFileError still flattens every parse failure into 'Invalid class file'. This round worked around that by constructing an input with a single defect, but any future test that wants to assert *why* a file was rejected faces the same wall." + ], + "adoptedProposals": [ + "2026-09-16-ldc-tags-15-16-17#p1" + ], + "proposals": [ + { + "title": "Audit the other end-to-end assertions for mutation resistance", + "plainSummary": "Check whether other tests in the class-format suite also pass for reasons other than the one they claim.", + "userBenefit": "Tests that cannot fail are worse than absent ones, because they are counted as coverage. Finding the rest of them is cheap now that the method is known.", + "why": "This round found one such test by mutating the branch it named and observing green. The same procedure applies mechanically to the other assertions in tests/test_class_format.rs, several of which also rely on corrupting a referenced slot of Hello.class and then asserting only the flattened error kind.", + "tradeoff": "Each one found may need its own purpose-built fixture, as this one did, so the cost is per-test rather than a single sweep. Some may turn out to be adequately guarded by classfile unit tests, in which case the honest outcome is a note rather than a change.", + "effort": "M", + "target": "tests/test_class_format.rs" + }, + { + "title": "Decide whether ClassFileError should carry a cause again", + "plainSummary": "Every parse failure currently reports the same flat message, so tests cannot assert why a file was rejected.", + "userBenefit": "Users get a diagnosis that says what is wrong with their class file, and tests can assert the specific reason instead of just the exception kind.", + "why": "The flattening is why this round could not fix the test by tightening its assertion, and it is recorded as a known limitation at the top of tests/test_class_format.rs. Restoring variants would let the assertion name the cause directly, which is a stronger lock than a carefully shaped fixture.", + "tradeoff": "The note says restoring variants needs upstream changes, so this is not purely local — and this repo is a fork whose upstream contact is deliberately read-only. It may be better solved locally, or not at all.", + "effort": "M", + "target": "classfile/src/error.rs" + } + ] +} diff --git a/docs/worklog/2026-09-16-cp-tag-passthrough-detectable.md b/docs/worklog/2026-09-16-cp-tag-passthrough-detectable.md new file mode 100644 index 00000000..2ccddc59 --- /dev/null +++ b/docs/worklog/2026-09-16-cp-tag-passthrough-detectable.md @@ -0,0 +1,68 @@ +# 2026-09-16 — Making the constant-pool tag switch's pass-through branch observable + +`taskId: rustjava-cp-tag-switch-passthrough-mutation-detectable` + +## The premise, demonstrated before touching anything + +`ConstantPoolItem::parse_tagged` ends in `_ => Err(...)` — the branch that rejects tags that cannot +appear in a class file. Mutating it to accept: + +```rust +_ => Ok((data, Self::Integer(0))), +``` + +`test_unsupported_constant_pool_tag_raises_class_format_error` — the test whose stated job is +"we still reject unknown constant pool tags" — **still passed**. Across the whole suite exactly one +test caught the mutation, and it was a `classfile` unit test, not the end-to-end one. + +## Why it passed — narrowed by experiment, not by guessing + +The old test overwrote the tag byte of `Hello.class`'s **first** constant pool entry. That entry is +a `Methodref` the code invokes, so overwriting it breaks the file along several independent paths +at once. `ClassFileError` flattens every parse failure into `"Invalid class file"` (a limitation +the test file already documented), so the assertion cannot distinguish *rejected because the tag is +unknown* from *rejected because the class fell apart*. + +The obvious competing hypothesis — that a pass-through consuming zero bytes desynchronises the +reader and breaks the pool that way — was **tested and rejected**: a mutation consuming exactly +four bytes, matching the `Methodref` payload it replaced, still left the test green. + +## The fix is not a tighter assertion + +There is nothing to tighten; the message is flat. The input has to reach the branch as the **only** +defect. `test-data/src/cp/make_cp_fixtures.py` emits `UnreferencedTag{13,14,19}.class`: a class +valid in every respect except one pool entry that is + +* **unreferenced** — nothing points at it, so no downstream consumer can reject it instead; +* **payload-free** and **last** — which is what an unassigned tag looks like, and which means a + pass-through consuming nothing leaves the reader correctly positioned at `access_flags`. + +All three properties are load-bearing. Without the last two, a mutated parser produces a +*differently broken* class and the test goes green again for a new wrong reason. + +## Evidence + +| | pass-through mutated | result | +|---|---|---| +| before this round | reject → accept | test **ok** (the defect) | +| after this round | reject → accept | test **red**, failing with `must be rejected: ""` — the empty output meaning the class ran to completion | +| after this round | a *different* branch (tag 16) mutated | **6 tests red** — the suite still guards the whole switch | + +`cargo test --all`: 568 passed / 0 failed / 1 ignored — unchanged, because one test was replaced. +The count is not the evidence; the before/after mutation pair is. + +**Product code is untouched in the final diff.** The mutations were temporary demonstrations and +were reverted; `git status classfile/ jvm-bytecode/` reports zero changed files, and the restored +`constant_pool.rs` hashes back to `393e0594d7eb647e`. + +## Fallout check + +No other test used the old in-test fixtures (`BadTag*`: zero references). The `hello_class()` and +`fixture()` helpers are still used 4 and 5 times respectively, so nothing was orphaned. + +The new assertion should not add false positives: the fixture is built to carry exactly one defect, +so there is little surface for an unrelated change to disturb it. + +## Follow-ups + +See `proposals` in the sibling `.json`. diff --git a/test-data/cp/UnreferencedTag13.class b/test-data/cp/UnreferencedTag13.class new file mode 100644 index 0000000000000000000000000000000000000000..41142edf0ed5eb0043a33b2f41a6a8a0906bf89f GIT binary patch literal 136 zcmX^0Z`VEs1_l!bPDTd7(7dA5wA7;1yyVoBki>LDV|E5cMh1bb#Ii*FoW#6zegCAa z)Z`L&24+SEmfXb5JVpjFjc6Z)d~iuoW?s6rW*AW3IX@+pmqC$%iGc-Z0Rsag11AG3 U0~?TL2l9BqB0$lNAUOsG0G2Hq_5c6? literal 0 HcmV?d00001 diff --git a/test-data/cp/UnreferencedTag14.class b/test-data/cp/UnreferencedTag14.class new file mode 100644 index 0000000000000000000000000000000000000000..32736aba4862dee5ded35ae8ae70dfbc782efd50 GIT binary patch literal 136 zcmX^0Z`VEs1_l!bPDTd7(7dA5wA7;1yyVoBki>LD6LtnhMh1bb#Ii*FoW#6zegCAa z)Z`L&24+SEmfXb5JVpjFjc6Z)d~iuoW?s6rW*AW3IX@+pk3o@viGc-Z0Rsag11AG3 U0~?TL2l9BqB0$lNAUOsG0G7fV_y7O^ literal 0 HcmV?d00001 diff --git a/test-data/cp/UnreferencedTag19.class b/test-data/cp/UnreferencedTag19.class new file mode 100644 index 0000000000000000000000000000000000000000..1b6f4821737f33e73b3729380d0aafc464bff579 GIT binary patch literal 136 zcmX^0Z`VEs1_l!bPDTd7(7dA5wA7;1yyVoBki>LDOLhiEMh1bb#Ii*FoW#6zegCAa z)Z`L&24+SEmfXb5JVpjFjc6Z)d~iuoW?s6rW*AW3IX@*;m_d<&iGc-Z0Rsag11AG3 U0~?TL2l9BqB0$lNAUOsG0GX>C0ssI2 literal 0 HcmV?d00001 diff --git a/test-data/src/cp/make_cp_fixtures.py b/test-data/src/cp/make_cp_fixtures.py new file mode 100644 index 00000000..6855fc68 --- /dev/null +++ b/test-data/src/cp/make_cp_fixtures.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Emit the constant-pool tag fixtures under `test-data/cp/`. + +These exist for one reason: to make the tag switch's pass-through branch observable end to end. + +`tests/test_class_format.rs` already had a test for "we still reject unknown constant pool tags", +built by overwriting the tag byte of `test-data/Hello.class`'s first pool entry. That entry is a +Methodref the code invokes, so overwriting it breaks the class along several independent paths at +once — the operand of `invokespecial` stops being a method reference, and so on. `ClassFileError` +collapses every parse failure into a flat "Invalid class file", so the assertion cannot tell +"rejected because the tag is unknown" from "rejected because the class fell apart". Measured: with +the pass-through branch mutated from reject to accept, that test still passed. + +The fixtures here make the unknown tag the **only** thing wrong: + + * the entry is unreferenced — nothing in the code or the class structure points at it, so no + downstream consumer can reject it on the entry's behalf; + * it is the **last** pool entry and carries **no payload**, which is what an unassigned tag + looks like — there is no defined size for it. + +Both properties are load-bearing. Being last and payload-free means a pass-through that consumes +nothing leaves the reader correctly positioned at `access_flags`, so a mutated parser produces a +*working* class rather than a differently-broken one. That is what turns the mutation into a +red test instead of a green one for the wrong reason. + +Tags 13 and 14 are unassigned by JVMS 4.4; 19 (Module) is assigned but legal only inside a +module-info, so it cannot appear here either. + +Regenerate with: python3 test-data/src/cp/make_cp_fixtures.py +""" + +import struct +from pathlib import Path + +OUT = Path(__file__).resolve().parents[2] / "cp" + +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 = [] # 1-based, no long/double so no double slots + + def add(self, blob): + self.entries.append(blob) + return len(self.entries) + + def utf8(self, s): + b = s.encode() + return self.add(u1(1) + u2(len(b)) + b) + + 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 unreferenced_unknown_tag(name, tag): + """A class that is valid in every respect except for one trailing, unreferenced pool entry + whose tag cannot appear in a class file.""" + cp = Pool() + this_class = cp.klass(name) + super_class = cp.klass("java/lang/Object") + main_name, main_desc, code_name = cp.utf8("main"), cp.utf8("([Ljava/lang/String;)V"), cp.utf8("Code") + + # Last, and payload-free — see the module docstring; both properties are load-bearing. + cp.add(u1(tag)) + + 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 + + return ( + b"\xca\xfe\xba\xbe" + u2(0) + u2(52) + cp.bytes() + + u2(0x0021) + u2(this_class) + u2(super_class) + + u2(0) + u2(0) + u2(1) + method + + u2(0) # no class attributes + ) + + +FIXTURES = {f"UnreferencedTag{tag}.class": (f"UnreferencedTag{tag}", tag) for tag in (13, 14, 19)} + +if __name__ == "__main__": + OUT.mkdir(parents=True, exist_ok=True) + for filename, args in FIXTURES.items(): + (OUT / filename).write_bytes(unreferenced_unknown_tag(*args)) + print(f"wrote {OUT / filename}") diff --git a/tests/test_class_format.rs b/tests/test_class_format.rs index f58dec2d..19357546 100644 --- a/tests/test_class_format.rs +++ b/tests/test_class_format.rs @@ -39,20 +39,28 @@ async fn test_truncated_class_raises_class_format_error() { assert!(err.contains("java.lang.ClassFormatError"), "expected ClassFormatError, got: {err}"); } +// 13 and 14 are unassigned by JVMS 4.4; 19 (Module) is assigned but only legal inside a +// module-info. Widening the parser to the method-handle tags (15..=18) must not have widened it +// to "anything goes" — a file carrying a tag that cannot appear here is still a corrupt file, not +// a file using a feature we have not implemented. +// +// The fixtures carry that tag on a **trailing, unreferenced, payload-free** pool entry, so the +// unknown tag is the only thing wrong with the file. That is what makes this test able to fail: +// the previous version overwrote the tag of `Hello.class`'s first entry, which is a Methodref the +// code invokes, so the file broke along several paths at once. `ClassFileError` flattens every +// parse failure into "Invalid class file", so that assertion could not tell "rejected because the +// tag is unknown" from "rejected because the class fell apart" — and measurably did not: with the +// tag switch's pass-through branch mutated from reject to accept, it still passed. +// See `test-data/src/cp/make_cp_fixtures.py`. #[tokio::test] async fn test_unsupported_constant_pool_tag_raises_class_format_error() { - // 13 and 14 are unassigned by JVMS 4.4; 19 (Module) is assigned but only legal inside a - // module-info. Widening the parser to the method-handle tags (15..=18) must not have - // widened it to "anything goes" — a file carrying a tag that cannot appear here is still - // a corrupt file, not a file using a feature we have not implemented. for tag in [13u8, 14, 19] { - let mut bytes = hello_class(); - // offset 10 is the first constant pool tag; 10 (Methodref) in the committed fixture - assert_eq!(bytes[10], 10, "test-data/Hello.class layout changed; adjust the mutation offset"); - bytes[10] = tag; - let (dir, path) = fixture(&format!("BadTag{tag}Hello.class"), &bytes); + let path = PathBuf::from(format!("test-data/cp/UnreferencedTag{tag}.class")); - let err = run_class(&path, &[dir.as_path()], &[]).await.unwrap_err().to_string(); + let err = run_class(&path, &[Path::new("./test-data/cp/")], &[]) + .await + .expect_err("a tag that cannot appear in a class file must be rejected") + .to_string(); assert!( err.contains("java.lang.ClassFormatError"), "tag {tag}: expected ClassFormatError, got: {err}" From 0d34ccc0e144f9fc7ff2ef152d3e3ec7c19186bb Mon Sep 17 00:00:00 2001 From: jun0 Date: Wed, 16 Sep 2026 18:06:14 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[rustjava-cp-tag-switch-passthrough-mutatio?= =?UTF-8?q?n-detectable]=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#49=20=C2=B7=20--merge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- STATE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/STATE.md b/STATE.md index e9f6d61e..a284b5fd 100644 --- a/STATE.md +++ b/STATE.md @@ -27,6 +27,18 @@ ★`cargo test --all` **568 / 0 failed / 1 ignored**(★수 불변 — 테스트 1개 «치환») · DoD **7줄 전건 rc=0** · 픽스처 재생성 **멱등**. ★**계약 2⒜ 전수 확인**: 옛 픽스처(`BadTag*`)를 쓰던 다른 테스트 **0건** · `hello_class()`·`fixture()` 헬퍼는 여전히 **4·5회** 쓰여 고아 0. ★**계약 2⒝ 오탐**: 새 단언은 ★**오탐이 늘지 않는다** — 픽스처가 «유일한 결함»만 갖도록 지어져 있어 다른 변경이 이 테스트를 흔들 경로가 좁다. + ★★**게이트③ 착지 — PR #49 · `--merge`**(등재 repo `contracts/upstream-sync-repos.conf:22` — 스쿼시는 부모 2개를 1개로 접어 계보를 지운다). + 게이트② **1회차 approve**(반려 0) · 핀 `eb8b4eb6` **불이동**(착수 실측 09:03Z — 로컬·원격·PR head 일치). + ★★**그러나 «충돌 해소»가 이 회차의 본체였다** — 검수 «도중» #47 이 착지해 PR 이 `CONFLICTING/DIRTY` 가 됐다. + ★**충돌은 원장 2파일뿐**(측정 09:03:56Z · `REPORT.md`·`STATE.md` 의 «맨 위 새 항목») — 선행 PLAN 의 예측 + 「코드 충돌은 없다 — 파일이 갈린다」가 **맞았다**. 해소 = 전건 보존·합집합·시간순(`eb8b4eb` 16:20 > `3b3667d` 14:45). + ★★**`tests/test_class_format.rs` 는 «자동 병합»됐고 그것을 믿지 않고 쟀다**(계약 12): 양방향 hunk 동일성 — + `base..theirs` 델타 == `ours..merged` 델타 · `base..ours` == `theirs..merged` **둘 다 일치**. + ★★**그리고 «줄 소실 3건»을 발견해 전건 해명했다** — 둘 다 **상대가 base 대비 «지운» 줄**이다(`deleted_by_other=True`): + ⑴우리가 남긴 「`bootstrap_method_attr_index` 는 여전히 경계 검사되지 않는다」를 ★**#47 이 그것을 구현하며 고쳐 썼다** + ⑵#47 이 남긴 「M5 층 어긋남 — pass-through 개악을 `test_class_format` 이 못 잡는다」를 ★**이 회차가 닫으며 고쳐 썼다**. + ⇒ ★**소실이 아니라 «각자 자기가 닫은 구멍을 갱신»한 것**이고, 부활·조작 줄은 **0**이다. + ★**해소 외 변경 0** · `--delete-branch` 미사용 · 형제 PR **#48·#50** 은 만지지 않았다(각자 base 당김이 필요하다). - [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` —