Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# 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] `bootstrap_method_attr_index` 가 «실재하는» 부트스트랩 메서드를 가리키게 했다 (rustjava-bound-bootstrap-method-attr-index)
- 무엇을: `Dynamic`/`InvokeDynamic` 상수의 `bootstrap_method_attr_index` 가 **BootstrapMethods 테이블 안**을 가리키는지
검사한다(JVMS 4.4.10·4.7.23). ★**두 축이 한 술어다** — 속성이 «아예 없는» 경우는 «항목 0개짜리 표»여서 어떤 인덱스도 못 가리킨다.
Expand Down
38 changes: 36 additions & 2 deletions STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,41 @@
(없음 — 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⒝ 오탐**: 새 단언은 ★**오탐이 늘지 않는다** — 픽스처가 «유일한 결함»만 갖도록 지어져 있어 다른 변경이 이 테스트를 흔들 경로가 좁다.
★★**게이트③ 착지 — 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` —
Expand Down Expand Up @@ -933,8 +968,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 기재 · 크레이트 경로·태그 서술은 낡았다) ----
Expand Down
46 changes: 46 additions & 0 deletions docs/worklog/2026-09-16-cp-tag-passthrough-detectable.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
68 changes: 68 additions & 0 deletions docs/worklog/2026-09-16-cp-tag-passthrough-detectable.md
Original file line number Diff line number Diff line change
@@ -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`.
Binary file added test-data/cp/UnreferencedTag13.class
Binary file not shown.
Binary file added test-data/cp/UnreferencedTag14.class
Binary file not shown.
Binary file added test-data/cp/UnreferencedTag19.class
Binary file not shown.
Loading