Skip to content

Commit 0fe0a5d

Browse files
authored
[rustjava-cp-tag-switch-passthrough-mutation-detectable] test(classfile): 태그 pass-through 개악을 end-to-end 로 「잡히게」 한다 (#49)
[rustjava-cp-tag-switch-passthrough-mutation-detectable] test(classfile): 태그 pass-through 개악을 end-to-end 로 「잡히게」 한다
2 parents d9f45eb + 0d34ccc commit 0fe0a5d

9 files changed

Lines changed: 281 additions & 12 deletions

‎REPORT.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# REPORT
22

3+
## [2026-09-16] 「알 수 없는 태그를 거부한다」는 테스트가 ★**그것을 지키지 않았다** — 지키게 했다 (rustjava-cp-tag-switch-passthrough-mutation-detectable)
4+
- 무엇을: `test_unsupported_constant_pool_tag_raises_class_format_error` 가 ★**상수풀 태그 switch 의 pass-through 가지를
5+
개악해도 green** 이었다. 그 가지가 «끝에서 끝까지» 관측되도록 **픽스처를 바꿔** 이제 **red** 가 되게 했다.
6+
★**제품 코드 변경 0**(개악은 실증용 임시 · 전부 되돌렸다 · `git status` 로 확인).
7+
- 왜: 채택 제안 `2026-09-16-ldc-tags-15-16-17#p1`. ★**「소비되지 않는 경보는 장식이다」의 테스트판** — 상수 pass 로 바꿔도
8+
통과하는 단언은 «없는 것과 같다».
9+
- 사용자 영향: **없다**(테스트만 바뀐다). 바뀐 것은 ★**그 단언이 실제로 무엇을 잠그는가**다.
10+
- ★★**근인 — 「통과한 진짜 이유」를 판별 실험으로 좁혔다.** 옛 테스트는 `Hello.class` **상수풀 1번**의 태그 바이트를 덮었는데,
11+
그 슬롯은 ★**코드가 `invokespecial` 로 «참조»하는 Methodref** 다 ⇒ 덮는 순간 파일이 **여러 경로로 동시에** 깨진다.
12+
`ClassFileError` 는 모든 파싱 실패를 ★**「Invalid class file」로 평탄화**하므로(그 파일이 스스로 적어 둔 사실)
13+
단언이 ★**「태그가 미지라 거부」와 「클래스가 무너져 거부」를 구별하지 못한다.**
14+
★**바이트 폭 어긋남(desync)은 근인이 «아니었다»** — 4바이트를 정확히 소비하는 개악으로도 **여전히 green** 이었다(판별 실험 B).
15+
- ★**처방은 단언 조이기가 «아니다»**(문면이 평탄해 불가능하다) — ★**입력이 그 가지에 «유일한 결함»으로 도달하게** 했다:
16+
`test-data/cp/UnreferencedTag{13,14,19}.class`(신규 생성기 `test-data/src/cp/make_cp_fixtures.py`) =
17+
★**참조되지 않고 · 페이로드가 없고 · 상수풀 «맨 끝»**인 엔트리 하나만 미지 태그다.
18+
★그 세 성질이 «전부» 값한다 — 맨 끝 + 페이로드 0이라야 pass-through 개악이 **정상 동작하는 클래스**를 만들고, 그래야 red 가 된다.
19+
- ★★**전/후 — 같은 개악, 다른 결과**: ⑴**전**: pass-through 개악 → 그 테스트 **ok**(스위트에서 무는 것은 `classfile` 단위 테스트 1건뿐)
20+
⑵**후**: 같은 개악 → ★**red**(실패 문면이 `must be rejected: ""` = 클래스가 «성공적으로 실행»됐다는 뜻).
21+
⑶**다른 가지 개악**(태그 16 거부) → **6 테스트 red** ⇒ 스위트가 여전히 switch 전체를 지킨다.
22+
- 검증: `cargo test --all` **568 / 0 failed / 1 ignored**(수 불변 — 테스트 1개 치환) · DoD 7줄 rc=0 · 픽스처 재생성 멱등 ·
23+
★**옛 픽스처(`BadTag*`)를 쓰던 다른 테스트 0건**(전수 확인) · `hello_class()`·`fixture()` 헬퍼는 여전히 4·5회 쓰인다(고아 0).
24+
- 후속 추천: ⑴같은 자를 다른 «조용한» 단언에 대 보기(개악 내성 감사) ⑵`ClassFileError` 에 원인 변종을 되살릴지 판정(상류 과제).
25+
상세 = `docs/worklog/2026-09-16-cp-tag-passthrough-detectable.md`.
326
## [2026-09-16] `bootstrap_method_attr_index` 가 «실재하는» 부트스트랩 메서드를 가리키게 했다 (rustjava-bound-bootstrap-method-attr-index)
427
- 무엇을: `Dynamic`/`InvokeDynamic` 상수의 `bootstrap_method_attr_index` 가 **BootstrapMethods 테이블 안**을 가리키는지
528
검사한다(JVMS 4.4.10·4.7.23). ★**두 축이 한 술어다** — 속성이 «아예 없는» 경우는 «항목 0개짜리 표»여서 어떤 인덱스도 못 가리킨다.

‎STATE.md‎

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

66
## 완료
7+
- [rustjava-cp-tag-switch-passthrough-mutation-detectable] ★★**「알 수 없는 태그를 거부한다」는 테스트가 그것을 «지키지 않았다» — 지키게 했다.**
8+
채택 제안 `2026-09-16-ldc-tags-15-16-17#p1`(worklog json `adoptedProposals` 기록).
9+
★**제품 코드 변경 «0»** — 개악은 실증용 임시이고 전부 되돌렸다(`git status classfile/ jvm-bytecode/` **0건**으로 확인).
10+
★★**대전제를 «먼저» 실증했다**(티켓 ⓐ): 태그 switch 의 `_ => Err(...)` 를 `_ => Ok(Integer(0))` 로 개악하니
11+
★**그 테스트는 «ok»** 였고 스위트에서 무는 것은 `constant_pool::tests::tags_outside_the_accepted_set_are_still_rejected`
12+
**단 1건**이었다 ⇒ ★**end-to-end 층에는 그 가지를 무는 것이 «없었다»**(직전 회차가 「M5 층 어긋남」으로 남긴 그것).
13+
★★**근인 — 판별 실험으로 좁혔다(추측 아님)**: 옛 테스트는 `Hello.class` **상수풀 1번**의 태그를 덮는데
14+
그 슬롯은 ★**코드가 참조하는 Methodref** 라 덮는 순간 파일이 **여러 경로로 동시에** 깨진다. 그리고 `ClassFileError` 가
15+
모든 파싱 실패를 ★**「Invalid class file」로 평탄화**하므로(그 테스트 파일이 스스로 적어 둔 사실) 단언이
16+
★**「태그가 미지라 거부」와 「클래스가 무너져 거부」를 구별하지 못한다.**
17+
★**바이트 어긋남(desync)은 근인이 «아니다»** — 4바이트를 정확히 소비하는 개악(B)으로도 **여전히 green** 이었다.
18+
⇒ ★**두 가설 중 하나를 실험으로 기각했다.**
19+
★★**그러므로 처방이 «단언 조이기»가 아니다** — 문면이 평탄해 조일 것이 없다. 티켓 계약 1 이 예측한 대로
20+
★**입력이 그 가지에 «유일한 결함»으로 도달하게** 만들었다: `test-data/cp/UnreferencedTag{13,14,19}.class`
21+
(생성기 `test-data/src/cp/make_cp_fixtures.py` 신규) = ★**참조되지 않고 · 페이로드 0 · 상수풀 «맨 끝»** 인 엔트리 하나.
22+
★**세 성질이 전부 값한다** — 맨 끝 + 페이로드 0 이라야 pass-through 개악이 ★**«정상 동작하는» 클래스**를 만들고,
23+
그래야 테스트가 red 가 된다(그렇지 않으면 «다르게 깨진» 파일이 되어 또 green 이다).
24+
★★**전/후 — 같은 개악, 다른 결과**: **전** = 그 테스트 **ok** / **후** = ★**red**
25+
(실패 문면 `a tag that cannot appear in a class file must be rejected: ""` — ★빈 출력 = 클래스가 «성공적으로 실행»됐다).
26+
★**⒞ 다른 가지 개악**(태그 16 거부) → **6 테스트 red** ⇒ 스위트가 여전히 switch 전체를 지킨다(이 회차가 좁히지 않았다).
27+
★`cargo test --all` **568 / 0 failed / 1 ignored**(★수 불변 — 테스트 1개 «치환») · DoD **7줄 전건 rc=0** · 픽스처 재생성 **멱등**.
28+
★**계약 2⒜ 전수 확인**: 옛 픽스처(`BadTag*`)를 쓰던 다른 테스트 **0건** · `hello_class()`·`fixture()` 헬퍼는 여전히 **4·5회** 쓰여 고아 0.
29+
★**계약 2⒝ 오탐**: 새 단언은 ★**오탐이 늘지 않는다** — 픽스처가 «유일한 결함»만 갖도록 지어져 있어 다른 변경이 이 테스트를 흔들 경로가 좁다.
30+
★★**게이트③ 착지 — PR #49 · `--merge`**(등재 repo `contracts/upstream-sync-repos.conf:22` — 스쿼시는 부모 2개를 1개로 접어 계보를 지운다).
31+
게이트② **1회차 approve**(반려 0) · 핀 `eb8b4eb6` **불이동**(착수 실측 09:03Z — 로컬·원격·PR head 일치).
32+
★★**그러나 «충돌 해소»가 이 회차의 본체였다** — 검수 «도중» #47 이 착지해 PR 이 `CONFLICTING/DIRTY` 가 됐다.
33+
★**충돌은 원장 2파일뿐**(측정 09:03:56Z · `REPORT.md`·`STATE.md` 의 «맨 위 새 항목») — 선행 PLAN 의 예측
34+
「코드 충돌은 없다 — 파일이 갈린다」가 **맞았다**. 해소 = 전건 보존·합집합·시간순(`eb8b4eb` 16:20 > `3b3667d` 14:45).
35+
★★**`tests/test_class_format.rs` 는 «자동 병합»됐고 그것을 믿지 않고 쟀다**(계약 12): 양방향 hunk 동일성 —
36+
`base..theirs` 델타 == `ours..merged` 델타 · `base..ours` == `theirs..merged` **둘 다 일치**.
37+
★★**그리고 «줄 소실 3건»을 발견해 전건 해명했다** — 둘 다 **상대가 base 대비 «지운» 줄**이다(`deleted_by_other=True`):
38+
⑴우리가 남긴 「`bootstrap_method_attr_index` 는 여전히 경계 검사되지 않는다」를 ★**#47 이 그것을 구현하며 고쳐 썼다**
39+
⑵#47 이 남긴 「M5 층 어긋남 — pass-through 개악을 `test_class_format` 이 못 잡는다」를 ★**이 회차가 닫으며 고쳐 썼다**.
40+
⇒ ★**소실이 아니라 «각자 자기가 닫은 구멍을 갱신»한 것**이고, 부활·조작 줄은 **0**이다.
41+
★**해소 외 변경 0** · `--delete-branch` 미사용 · 형제 PR **#48·#50** 은 만지지 않았다(각자 base 당김이 필요하다).
742
- [rustjava-bound-bootstrap-method-attr-index] ★★**`bootstrap_method_attr_index` 가 «실재하는» 부트스트랩 메서드를 가리키게 했다 — 「파손」을 되찾았다.**
843
채택 제안 **둘**을 한 회차가 닫았다(worklog json `adoptedProposals` 에 **전건** 기록):
944
`2026-09-16-bootstrap-methods-and-method-handle#p1` · `2026-09-16-ldc-tags-15-16-17#p0` —
@@ -933,8 +968,7 @@ green 전건 rc=0 · `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3
933968
참조 JVM 은 `Missing BootstrapMethods attribute` 인데 우리는 **여전히 「미지원」**이다.
934969
★그 경계 검사는 **속성 파싱이 정말로 필요**하므로 ④-1(PR #45 리니지) 몫이다 — ★**픽스처와 테스트로 «현재 답»을 잠가 뒀으니
935970
그 회차가 닫으면 그 단언이 «시끄럽게» 진다.**
936-
★**남긴 것 둘 더**: ⑵★**M5 층 어긋남**: 상수풀 태그 pass-through 개악을
937-
`test_class_format` 이 **못 잡는다**(무는 것은 `classfile` 단위 테스트) ⑶서드파티 생성기 corpus **미측정**(이 머신에 jar 0개).
971+
★**남긴 것 둘 더**: ⑵★**M5 층 어긋남**: ★**[2026-09-16 닫힘 · `rustjava-cp-tag-switch-passthrough-mutation-detectable`] 이제 `test_class_format` 이 «잡는다»**(픽스처를 «유일한 결함»으로 다시 지었다 — 종전엔 참조되는 Methodref 슬롯을 덮어 «다른 이유»로 통과했다) ⑶서드파티 생성기 corpus **미측정**(이 머신에 jar 0개).
938972
3. ★InputStreamReader 디코더 — 아래 사료 절 셋째 항목 그대로 **살아 있다**(별건).
939973

940974
---- 이하 사료(2026-08-16 기재 · 크레이트 경로·태그 서술은 낡았다) ----
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"schema": "worklog/v1",
3+
"date": "2026-09-16",
4+
"taskId": "rustjava-cp-tag-switch-passthrough-mutation-detectable",
5+
"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.",
6+
"changes": [
7+
"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",
8+
"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"
9+
],
10+
"verification": [
11+
"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",
12+
"competing hypothesis rejected by experiment: a mutation consuming exactly 4 bytes (no desync) also left the test green",
13+
"after: same mutation -> test red, failing with 'must be rejected: \"\"' (empty output = the class ran)",
14+
"a different branch (tag 16) mutated -> 6 tests red, so the suite still guards the whole switch",
15+
"cargo test --all: 568 passed / 0 failed / 1 ignored (count unchanged: a test was replaced)",
16+
"product code unchanged in the final diff; constant_pool.rs restored to sha 393e0594d7eb647e",
17+
"no other test referenced the old BadTag* fixtures; hello_class()/fixture() helpers still used",
18+
"DoD 7 commands all rc=0; fixture regeneration idempotent"
19+
],
20+
"issues": [
21+
"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."
22+
],
23+
"adoptedProposals": [
24+
"2026-09-16-ldc-tags-15-16-17#p1"
25+
],
26+
"proposals": [
27+
{
28+
"title": "Audit the other end-to-end assertions for mutation resistance",
29+
"plainSummary": "Check whether other tests in the class-format suite also pass for reasons other than the one they claim.",
30+
"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.",
31+
"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.",
32+
"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.",
33+
"effort": "M",
34+
"target": "tests/test_class_format.rs"
35+
},
36+
{
37+
"title": "Decide whether ClassFileError should carry a cause again",
38+
"plainSummary": "Every parse failure currently reports the same flat message, so tests cannot assert why a file was rejected.",
39+
"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.",
40+
"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.",
41+
"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.",
42+
"effort": "M",
43+
"target": "classfile/src/error.rs"
44+
}
45+
]
46+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# 2026-09-16 — Making the constant-pool tag switch's pass-through branch observable
2+
3+
`taskId: rustjava-cp-tag-switch-passthrough-mutation-detectable`
4+
5+
## The premise, demonstrated before touching anything
6+
7+
`ConstantPoolItem::parse_tagged` ends in `_ => Err(...)` — the branch that rejects tags that cannot
8+
appear in a class file. Mutating it to accept:
9+
10+
```rust
11+
_ => Ok((data, Self::Integer(0))),
12+
```
13+
14+
`test_unsupported_constant_pool_tag_raises_class_format_error` — the test whose stated job is
15+
"we still reject unknown constant pool tags" — **still passed**. Across the whole suite exactly one
16+
test caught the mutation, and it was a `classfile` unit test, not the end-to-end one.
17+
18+
## Why it passed — narrowed by experiment, not by guessing
19+
20+
The old test overwrote the tag byte of `Hello.class`'s **first** constant pool entry. That entry is
21+
a `Methodref` the code invokes, so overwriting it breaks the file along several independent paths
22+
at once. `ClassFileError` flattens every parse failure into `"Invalid class file"` (a limitation
23+
the test file already documented), so the assertion cannot distinguish *rejected because the tag is
24+
unknown* from *rejected because the class fell apart*.
25+
26+
The obvious competing hypothesis — that a pass-through consuming zero bytes desynchronises the
27+
reader and breaks the pool that way — was **tested and rejected**: a mutation consuming exactly
28+
four bytes, matching the `Methodref` payload it replaced, still left the test green.
29+
30+
## The fix is not a tighter assertion
31+
32+
There is nothing to tighten; the message is flat. The input has to reach the branch as the **only**
33+
defect. `test-data/src/cp/make_cp_fixtures.py` emits `UnreferencedTag{13,14,19}.class`: a class
34+
valid in every respect except one pool entry that is
35+
36+
* **unreferenced** — nothing points at it, so no downstream consumer can reject it instead;
37+
* **payload-free** and **last** — which is what an unassigned tag looks like, and which means a
38+
pass-through consuming nothing leaves the reader correctly positioned at `access_flags`.
39+
40+
All three properties are load-bearing. Without the last two, a mutated parser produces a
41+
*differently broken* class and the test goes green again for a new wrong reason.
42+
43+
## Evidence
44+
45+
| | pass-through mutated | result |
46+
|---|---|---|
47+
| before this round | reject → accept | test **ok** (the defect) |
48+
| after this round | reject → accept | test **red**, failing with `must be rejected: ""` — the empty output meaning the class ran to completion |
49+
| after this round | a *different* branch (tag 16) mutated | **6 tests red** — the suite still guards the whole switch |
50+
51+
`cargo test --all`: 568 passed / 0 failed / 1 ignored — unchanged, because one test was replaced.
52+
The count is not the evidence; the before/after mutation pair is.
53+
54+
**Product code is untouched in the final diff.** The mutations were temporary demonstrations and
55+
were reverted; `git status classfile/ jvm-bytecode/` reports zero changed files, and the restored
56+
`constant_pool.rs` hashes back to `393e0594d7eb647e`.
57+
58+
## Fallout check
59+
60+
No other test used the old in-test fixtures (`BadTag*`: zero references). The `hello_class()` and
61+
`fixture()` helpers are still used 4 and 5 times respectively, so nothing was orphaned.
62+
63+
The new assertion should not add false positives: the fixture is built to carry exactly one defect,
64+
so there is little surface for an unrelated change to disturb it.
65+
66+
## Follow-ups
67+
68+
See `proposals` in the sibling `.json`.
136 Bytes
Binary file not shown.
136 Bytes
Binary file not shown.
136 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)