diff --git a/AGENTS.md b/AGENTS.md index ec149106..489754ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,3 +50,7 @@ the consumer does not read them, so they are for humans and the next round. "if a `.json` exists, is it well-formed and does it have its `.md` sibling" — it never demands a `.json` for an existing `.md`. Lock: `scripts/check-worklog-json.py`, run by the `worklog_json` CI job (`cargo test` does not cover docs). +## Testing Boundaries +- Keep `java_runtime/tests/classes` limited to Java standard library class and API behavior. +- Test JVM and interpreter semantics, including class initialization, bytecode execution, and monitor behavior, with compiled Java fixtures under `test_data/src` and expected output under `test_data`, executed by `tests/test_class.rs`. +- Do not place JVM core behavior tests in the `java_runtime` standard library test tree. diff --git a/Cargo.lock b/Cargo.lock index fb4a3833..3f260002 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,12 +572,14 @@ dependencies = [ "anyhow", "async-trait", "bytemuck", + "classfile", "java_class_proto", "java_runtime", "jvm", "jvm_rust", "test_utils", "tokio", + "tracing", "tracing-subscriber", ] @@ -652,10 +654,12 @@ name = "test_utils" version = "0.0.1" dependencies = [ "async-trait", + "classfile", "java_runtime", "jvm", "jvm_rust", "tokio", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4bb49eb1..4e5bdbf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,10 +42,12 @@ async-trait = { workspace = true } bytemuck = { workspace = true } anyhow = { workspace = true } +tracing = { workspace = true } tracing-subscriber = { version = "^0.3", features = ["env-filter"] } jvm = { workspace = true } jvm_rust = { workspace = true } +classfile = { workspace = true } java_class_proto = { workspace = true } java_runtime = { workspace = true } diff --git a/REPORT.md b/REPORT.md index c6c19c69..53c640df 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,5 +1,42 @@ # REPORT +## [2026-08-27] upstream 동기 S3 — 컷 `822504b` 머지 (rustjava-upstream-sync-s3) +- 무엇을: upstream `822504b`(#180 Harden JVM runtime correctness) 1커밋을 머지했다. 충돌 **11** 해소. + `classfile/{class,constant_pool,error,lib}.rs` · `jvm_rust/class_definition.rs` · `src/runtime.rs` · + `test_utils/lib.rs` 는 **upstream 채택**(우리 `ParseError` 5변형 → upstream `ClassFileError` + + `ClassDefinitionError`). `java/lang/string.rs` 는 **upstream 골격을 우리 `charset::Charset` 으로 + 라우팅**해 중복 charset 표를 지웠고, `test_string.rs` 는 **양쪽 테스트 합집합**, + `thread.rs` 는 **upstream 본문 + PR #4 의 수동 span**, `AGENTS.md` 는 **양쪽 절 합집합**이다. + 부수: `tests/test_class_format.rs` 의 **문구 단정 3건 삭제**(종류 단정은 유지). +- 왜: 계획서 §3-B 가 판정한 대로 **Java 관측면에서 upstream 이 이긴다** — 우리 `ParseError` 는 Rust + 변형이 5종이지만 Java 예외는 `ClassFormatError` **1종**뿐이고, upstream 은 `ClassFormatError` · + `UnsupportedClassVersionError` · `VerifyError` · `UnsupportedOperationException` **4종**으로 나눈다. + JVM 구현체에서 값이 큰 쪽은 관측면이다. PR #3 의 목적(「패닉 대신 `ClassFormatError`」)은 upstream + 에서도 그대로 성립한다(미지원 상수풀 태그 → `ErrorKind::Switch` → `InvalidFormat`, 패닉 0). + ★**치른 값은 진단 문구다** — 「Truncated」·「tag 18」·「magic」이 전부 `"Invalid class file"` 로 평탄해졌고, + §4-A 가 예고한 대로 `tests/test_class_format.rs` 3건이 **충돌 마커 없이** 그것 때문에 깨졌다. +- 사용자 영향: 클래스파일 검증이 세분화된다 — 지원하지 않는 클래스파일 버전은 이제 + `UnsupportedClassVersionError`, 바이트코드 검증 실패는 `VerifyError`, `invokedynamic` 은 + `UnsupportedOperationException` 으로 **깔끔히 거부**된다(구판은 인터프리터 `todo!()` 패닉까지 갔다). + 대신 `ClassFormatError` 메시지는 원인별 문구를 잃고 `"Invalid class file"` 평문이 된다. + ★**charset 동작 변경 1건**: 기본 charset 경로(`new String(byte[])`·`getBytes()`)는 미지원 이름에도 + 더 이상 `UnsupportedEncodingException` 을 던지지 않고 UTF-8 로 폴백한다 — **JDK 규격이 그렇다**. + 명시 charset 경로(`new String(byte[],String)`·`getBytes(String)`)는 그대로 던진다. + ISO-8859-1·US-ASCII 는 우리 `Charset` 이 정본으로 남아 계속 동작한다(종단 픽스처 green). +- 검증: `cargo fmt --all -- --check` · `cargo clippy --all -- -D warnings` · + `cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings` · + `cargo test --all` **4/4 rc=0** · **216 passed / 0 failed / 1 ignored**(S2 191 → +25, 우리 테스트 유실 0). + `tests/test_class_format.rs` **4/4** · `git grep 'tracing::instrument\|tracing-attributes'` **0건**(§4-C 불변). + 추가로 「base `af4f6f8` 이후 우리가 추가한 .rs 321줄이 머지 트리에 살아 있는가」를 기계로 전수 대조했고, + 부재 81줄은 **전건 의도한 해소**였다(`ParseError` 기구 · `thread.rs` 구본문 · 완화한 문구 단정 3줄). +- 후속 추천: ⑴**게이트③ 순서 주의** — `rustjava-upstream-sync-s2-merge`(#13)가 **먼저**고 S3 PR 이 그 위다. + ★#13 이 스쿼시로 착지하면 `af4f6f8` 조상이 다시 끊기므로, S3 PR 이 `main` 으로 리타깃된 뒤 + **S2 가 한 `-s ours` 를 다시 해야 할 수 있다**(착지 후 `merge-base` 를 재라). + ⑵**S4**(컷 `3296139`) — 계획서 예측 **새 충돌 0**. 검수는 「우리 해소분 0 증명 + green」. + ⑶★**계획서의 「새 충돌」 예측은 하한이다** — S3 는 +9 예측에 `AGENTS.md`·`thread.rs` **2건이 더 붙었다**. + 전자는 계획서 이후 우리가 만든 파일이고, 후자는 **앞 회차가 이미 닫은 파일의 재충돌**이다. + ⑷`charset.rs` dead-code red 축은 **닫아도 된다** — S3 에서 오히려 upstream 중복 표를 흡수했다. + ## [2026-08-26] 회차 워크로그 `.json` + `proposals` 규약 이식 (rustjava-worklog-json-proposals-convention) - 무엇을: `AGENTS.md` 에 「Round Worklog `docs/worklog/`」 절(소비처가 읽는 키 표 · 소급 없음), `scripts/check-worklog-json.py` 잠금 6축, `rust.yml` 에 `worklog_json` job 1개(ubuntu 단일 러너), diff --git a/STATE.md b/STATE.md index 950c816f..5e430f42 100644 --- a/STATE.md +++ b/STATE.md @@ -1,20 +1,30 @@ # STATE ## 진행중 -- [rustjava-upstream-sync-s2] upstream 컷 `af4f6f8`(#177 CLDC 1.1) 머지 — 충돌 **5** 해소. - ★**PR #11 이 스쿼시 머지돼 upstream 조상이 끊겨 있었다** — `-s ours` 로 `1f356ae` 를 부모로 기록해 - 복원한 뒤 머지했다(트리 무변경). 복원 전 충돌 **15** → 복원 후 **5**. **PR 대기 — 게이트③ 미착지.** -- [rustjava-worklog-json-proposals-convention] 회차 워크로그 `docs/worklog/` `.md`+`.json` 한 쌍 규약 - 이식(qts 2026-08-23 규약 복제 · 스키마 발명 0) — `AGENTS.md` 절 + `scripts/check-worklog-json.py` - 잠금 + `rust.yml` `worklog_json` job. **PR 대기 — 게이트③ 미착지.** -- [rustjava-ci-beta-clippy-double-must-use-red] beta clippy `double_must_use` 13건 red 해소 — - `async-trait` 0.1.89→0.1.92 + `async_recursion` 7지점 국소 `allow` + matrix `fail-fast: false`. - **PR 대기 — 게이트③ 미착지.** +- [rustjava-upstream-sync-s3] upstream 컷 `822504b`(#180 오류 분류) 머지 — 충돌 **11** 해소. + ★**S2(PR #13) 브랜치 «위에» 쌓았다** — 당시 `main` 에 S2 가 없어 base 를 `main` 으로 잡으면 S2 의 충돌 + 5건을 다시 만나기 때문이다. **PR #16 · 게이트② approve(핀 `3cf944d`) · 게이트③ 집행 중.** + ★★**착수 시 「upstream 조상 무손상이라 `-s ours` 불필요」로 적었는데, «축을 하나 놓쳤다»** — + #13 이 스쿼시로 착지하자 **upstream 조상(`822504b`)은 그대로인데 `origin/main` 과의 조상이 끊겼다** + (`merge-base` = `b3a4cf4` · `11ef501` 이 조상 **아님**) ⇒ main 과 **6충돌**(원장 1 + 코드 5, 내용은 전부 동일). + ⇒ 게이트③이 `git merge -s ours --no-ff 11ef501`(트리 무변경 실측)로 복원해 **충돌 0**으로 만들었다. + ★**교훈: 조상은 «upstream 축»과 «origin/main 축» 둘이다. 스쿼시가 끊는 것은 후자다.** - [rustjava-coverage-workflow-codecov-token-red] `coverage` 상시 red 해소 — `fail_ci_if_error: false`. ★**실증: 착지 전 브랜치에서 «이 저장소 최초의 green coverage»** (25번째 run, 앞선 24건 전부 red). **PR 대기 — 게이트③ 미착지.** ## 완료 +- [rustjava-upstream-sync-s2] upstream 컷 `af4f6f8`(#177 CLDC 1.1) 머지 — 충돌 **5** 해소. + ★**PR #11 이 스쿼시 머지돼 upstream 조상이 끊겨 있었다** — `-s ours` 로 `1f356ae` 를 부모로 기록해 + 복원한 뒤 머지했다(트리 무변경). 복원 전 충돌 **15** → 복원 후 **5**. + ★게이트③ 완료: PR #13 스쿼시 머지 → main `11ef501`(2026-08-26). ★착지 전 base 를 `main` 으로 당겨 + **#14(beta clippy)를 들여와** CI red 를 풀었다(핀 `eaa5668` rc=1 CI_RED → `df3b04a` rc=0 CI_GREEN). +- [rustjava-worklog-json-proposals-convention] 회차 워크로그 `docs/worklog/` `.md`+`.json` 한 쌍 규약 이식. + ★게이트③ 완료: PR #15 스쿼시 머지 → main `b3a4cf4`. +- [rustjava-ci-beta-clippy-double-must-use-red] beta clippy `double_must_use` 13건 red 해소. + ★게이트③ 완료: PR #14 스쿼시 머지 → main `dde85ce`. +- [rustjava-claude-md-prune] `CLAUDE.md` 프룬(autonomous-sop 삭제 + Goal/Constraints/DoD 신설). + ★게이트③ 완료: PR #8 스쿼시 머지 → main `00bddf3`(2026-08-18). ※구판 「좌초 중」 기재는 폐기. - [rustjava-upstream-sync-s1-tracing-cut-1f356ae] upstream 컷 `1f356ae` 머지(충돌 2 해소 · tracing 축 · `System.setProperty` 서술자 파손 1건 추가 처리). ★게이트③ 완료: PR #11 스쿼시 머지 → main `6bfe97c`(2026-08-17). ※원격 브랜치는 repo 설정 `deleteBranchOnMerge=true` 로 자동 삭제됨. @@ -43,9 +53,24 @@ ## 다음 -### ①(최우선) upstream 동기화 — ★**S2 착지 대기(2026-08-24)**. 정본 = `docs/upstream-sync-approach.md` +### ①(최우선) upstream 동기화 — ★**S3 착지 대기(2026-08-27)**. 정본 = `docs/upstream-sync-approach.md` + +★**S3(`822504b` · 오류 분류 축)까지 머지 완료 · PR 대기 중이다. 다음은 S4(`3296139` · 물량 회차).** -★**S2(`af4f6f8` · charset 축)는 머지 완료 · PR 대기 중이다. 다음은 S3(`822504b` · 오류 분류 축).** +**S3 실측(2026-08-27)**: ★**조상은 끊기지 않았다** — S2 가 PR 로만 열려 있고 아직 스쿼시되지 않아 +`merge-base HEAD upstream/main` = `af4f6f8` 그대로였다. ⇒ ★**`-s ours` 복원은 «불필요»했고 하지 않았다.** +★**대신 브랜치를 `feat/rustjava-upstream-sync-s2` 위에 쌓았다**(base = `main` 으로 잡으면 S2 의 충돌 5건을 +다시 만난다). `origin/main` 의 신규 2커밋(#14 beta clippy · #15 worklog json)은 따로 머지해 얹었다. +충돌 **11** — 계획서 예측 **+9** 에 **2건이 더 붙었다**: ⑴`AGENTS.md`(#15 가 만든 워크로그 절 ↔ upstream +`Testing Boundaries` 절 · 계획서 작성 시점에 없던 파일) ⑵★**`thread.rs` 가 «다시» 충돌했다** — S1 이 이미 +해소한 자리인데 `822504b` 가 같은 함수를 재작성했다. ⇒ ★**「앞 회차가 닫은 파일은 다시 안 나온다」는 전제는 틀렸다.** +green 전건 rc=0 · `cargo test --all` **216 passed / 0 failed / 1 ignored**(S2 191 → +25). +★**계획서 §4-A 가 예고한 대로 `tests/test_class_format.rs` 가 «충돌 0으로» 깨질 뻔했다** — 문구 단정 3건 +(`"Truncated"`·`"tag 18"`·`"magic"`)을 **삭제**하고 `ClassFormatError` **종류 단정은 유지**해 4/4 통과. +★**§4-B 의 `charset.rs` dead-code red 도 발동하지 않았다** — upstream 의 `decode_str`/`encode_str` 중복 +표를 **지우고** 우리 `charset::Charset` 으로 라우팅했다(호출자 7건 유지). 단 ★**기본 charset 경로는 +upstream 의미를 취했다** — JDK 는 `new String(byte[])`·`getBytes()` 에서 미지원 charset 에 예외를 던지지 +않는다(명시 charset 경로만 던진다). 우리 구판은 네 경로 전부에서 던졌다. ★★**S3 착수자에게 — 조상 복원을 먼저 확인하라.** S2 의 PR 도 스쿼시로 착지하면 `1f356ae`·`af4f6f8` 둘 다 다시 조상에서 끊긴다. 착수 시 `git merge-base origin/main upstream/main` 이 `af4f6f8` 가 아니면 @@ -161,21 +186,24 @@ green 기준은 전 회차 CI `rust.yml` 4종 동일(문서 §5 에 회차별 ※**전역 수리는 불가** — `Deref` 는 `Result` 를 못 돌려준다. upstream 방식(진입부 `is_null()` 가드)이 정답이다. ### ③다음 회차 발권 후보(우선순위 순) -0. ★★**`rustjava-pr8-claude-md-prune-disposition`**(P1·S·low) — ★**PR #8 처분 «판정»**. - ★**이 항목이 목록 맨 위인 이유**: 아래 1~3 은 전부 «앞으로 할 일»인데 #8 은 **이미 벌어져 멈춰 있는 일**이다. - 재료(2026-08-16 자력 실측, 판정은 하지 않았다): - - `Jun025/RustJava#8` `[rustjava-claude-md-prune]` · head `feat/rustjava-claude-md-prune`(`7e73a1c`) · - `createdAt`/`updatedAt` **둘 다 `2026-08-05T07:56:55Z`** · `OPEN`/`MERGEABLE` · draft 아님. - - ⇒ **생성 후 11일간 손대지 않은 채 열려 있다.** 커밋도 push 도 코멘트도 그날 이후 0. - - ★**원장 비대칭**: `reports/rustjava-claude-md-prune.done.md` 는 실재하는데 - **`reports/rustjava-claude-md-prune.review.md` 가 없다** ⇒ ★**게이트②가 아예 돌지 않고 좌초했다.** - - 내용은 `CLAUDE.md` **문서 전용**(autonomous-sop 절 삭제 + Goal/Constraints/DoD 신설). - ★현 `main` 의 `CLAUDE.md` 에는 그 변경이 **없다** — 즉 #8 이 닫히지 않는 한 레인 헌장이 두 판본으로 갈린다. - - ★**충돌 전망**: `CLAUDE.md` 는 위 「충돌 17파일」에 **없다** ⇒ ①머지와 독립적으로 처분 가능하다. - ★**처분(게이트② 재발권 / 닫기 / 재작성)은 이 항목이 «판정할 몫»이다 — 여기서 정하지 않았다.** -1. ★**`rustjava-upstream-sync-s1` … `-s7`**(구판 `-32-commits` **폐기**) — ①의 머지를 + +★★**0번 항목(`rustjava-pr8-claude-md-prune-disposition`)은 «해소됨» — 2026-08-27 S3 회차가 닫았다.** +구판은 「`reports/rustjava-claude-md-prune.review.md` 가 **없다** ⇒ 게이트②가 아예 돌지 않고 좌초」를 +근거로 이 항목을 **최우선**에 뒀는데, 그 근거가 **둘 다 사실이 아니게 됐다**(실측): +- `Jun025/RustJava#8` = ★**`MERGED`**(`mergedAt` **2026-08-18T19:26:08Z** · 머지커밋 `00bddf3`). + head `feat/rustjava-claude-md-prune` 는 머지와 함께 **삭제**됐다(`git ls-remote --heads origin` 잔존 0). +- `reports/rustjava-claude-md-prune.review.md` **실재**(2026-08-19 03:42) · 승계 `-fix` 리니지의 + `.done.md`/`.review.md` 도 **둘 다 실재** ⇒ ★**게이트②는 돌았고 3게이트를 완주했다.** +- 「현 `main` 의 `CLAUDE.md` 에 그 변경이 없다」도 해소 — `origin/main` 의 `CLAUDE.md` 에 + `## Goal` 절이 실재한다(= 프룬 판본이 정본). +★**교훈: 이 절이 «이미 끝난 일»을 최우선으로 가리키면 레인이 조용해진다** — 실제로 발권이 멈춘 채 +18시간(`LANE_IDLE rustjava`)이 지났다. 「다음」 절 항목은 **닫히는 즉시** 닫아라. + +⇒ **재부여된 순서**(위 0번이 빠지고 1→3 이 한 칸씩 올라온다): + +1. ★**`rustjava-upstream-sync-s4` … `-s7`**(S1·S2·S3 **완료** · 구판 `-32-commits` **폐기**) — ①의 머지를 `docs/upstream-sync-approach.md` §5 의 **7회차**로 쪼갠다. **한 티켓 = 한 컷**이고, - ★**S1(`1f356ae`) 부터 순서대로**다. 각 회차 완료 정의 = 그 컷의 충돌 해소 + CI `rust.yml` 4종 green + ★**순서대로**다 — **다음은 S4(`3296139`)**. 각 회차 완료 정의 = 그 컷의 충돌 해소 + CI `rust.yml` 4종 green + 문서 §5 의 회차별 추가 조건(S1 tracing 0건 / S2 charset 잠금 / S3 `test_class_format.rs` / S4~S7 해소분 0 증명). ★**착수 시 충돌을 재측정하라** — 앞 회차 착지로 기준선이 바뀐다. 2. **`rustjava-null-guard-string-init-and-arraycopy`**(P2·S·low) — ②의 유효 잔존 2건 + 형제 전수. @@ -228,29 +256,31 @@ green 기준은 전 회차 CI `rust.yml` 4종 동일(문서 §5 에 회차별 ※단 upstream 은 완화책을 갖고 있다(`endOfInput` 필드 + UTF-8 lead-byte 역주사 · EUC-KR `>=0x81` 홀드백). ⇒ **①의 머지로 함께 들어온다.** 별건 발권 전에 ① 이후 상태를 다시 재라. -### ⑤운영 메모 — ★2026-08-16 자력 실측으로 교체(구판 「열린 PR 0」은 **거짓이었다**) +### ⑤운영 메모 — ★2026-08-27 S3 게이트③ 실측으로 교체(구판 「열린 PR = #13 하나」는 **낡았다**) -★**열린 PR = 2건**(`gh pr list -R Jun025/RustJava --state open`): +★**열린 PR = 1건**(`gh pr list -R Jun025/RustJava --state open`): -| PR | 브랜치 | 생성 | 마지막 갱신 | 상태 | 원장 | -|---|---|---|---|---|---| -| **#8** `[rustjava-claude-md-prune]` | `feat/rustjava-claude-md-prune` | `2026-08-05T07:56:55Z` | **같은 시각** | `OPEN`/`MERGEABLE` | `.done.md` 有 · ★**`.review.md` 無** | -| ~~#9~~ `[rustjava-lane-restart-upstream-sync-precondition]` | — | `2026-08-15T14:49:33Z` | `2026-08-15T14:49:33Z` | ★**MERGED**(→ main `85f294a`) | 3게이트 완주 | +| PR | 브랜치 | 상태 | 원장 | +|---|---|---|---| +| **#16** `[rustjava-upstream-sync-s3]` | `feat/rustjava-upstream-sync-s3` | `OPEN` · ★게이트② **approve**(핀 `3cf944d`) · 게이트③ 집행 중 | `.done.md`·`.review.md` 둘 다 有 | +| ~~#13~~ `[rustjava-upstream-sync-s2]` | — | ★**MERGED**(`2026-08-26T21:51:38Z` → main `11ef501`) | 3게이트 완주 | +| ~~#8~~ · ~~#14~~ · ~~#15~~ | — | **MERGED**(→ `00bddf3` · `dde85ce` · `b3a4cf4`) | 완주 | -★**#8 은 12일째 좌초 중이고 게이트②가 아예 돌지 않았다** ⇒ 처분은 **브리프 ③-0** 으로 넘겼다. -★#9 는 착지했으므로 **열린 PR 은 #8 하나**다(2026-08-16 실측 `gh pr list -R Jun025/RustJava`). +★★**스택 PR 을 다룰 때 반드시 기억할 것**(2026-08-27 실사고급 근접): #16 의 base 가 +`feat/rustjava-upstream-sync-s2` 였고 이 저장소는 `deleteBranchOnMerge=true` 다 ⇒ ★**#13 을 그냥 머지했으면 +#16 이 base 소멸로 자동 CLOSED 될 자리**였다(reopen 불가). S2 게이트③이 **머지 «전»에** +`gh pr edit 16 --base main` 으로 선제 재타깃해 막았다. ⇒ ★**스택 PR 은 부모 머지 «전»에 자식 base 를 옮겨라.** -★**원격 브랜치**(`git ls-remote --heads origin`): `main` + 아래. +★**원격 브랜치**(`git ls-remote --heads origin` · 2026-08-27 실측) = **3건**: +`main` · `feat/rustjava-upstream-sync-s3`(PR #16 의 head) · `wie-ktf-hardening`. | 브랜치 | 성격 | 처분 | |---|---|---| +| `feat/rustjava-upstream-sync-s3` | PR #16 의 head — 게이트③ 집행 중 | 머지와 함께 자동 삭제(`deleteBranchOnMerge`) | | `wie-ktf-hardening` | 보존 판정(2026-07-25) | 위 ②로 **잔존 가치가 2건까지 줄었다** — 브리프 ③-2 가 그 2건을 새 브랜치로 옮겨 심으면 ★**보존 근거가 소멸**한다 | -| `feat/rustjava-claude-md-prune` | ★**PR #8 의 head — 좌초 중** | 브리프 ③-0 이 판정 | -★**2026-08-16 재실측(`git ls-remote --heads origin`) = 3건**: `main` · `wie-ktf-hardening` · -`feat/rustjava-claude-md-prune`. PR #9 의 head `feat/rustjava-lane-restart-upstream-sync-precondition` -은 **머지와 함께 삭제됐다**(잔존 0). +⇒ ★**다음은 S4(`3296139`)** — 남은 upstream 커밋 **26**. 계획서 예측 **새 충돌 0**이나 +★**예측은 하한이다**(S3 가 +9 예측에 11 이었다). -⇒ ★**「발권 대기 태스크 없음」이 아니다** — 처분 대기 1건(#8) + 브리프 3건이 서 있다. - ★PR 발권 시 `--repo Jun025/RustJava` 명시(2026-07-22 upstream 오발행 사고 재발 방지). - ★upstream 발신(PR·이슈·코멘트·push)은 **티켓이 명시 허가할 때만**. 기본은 조회뿐. diff --git a/classfile/src/attribute.rs b/classfile/src/attribute.rs index 645abe01..dac5a71d 100644 --- a/classfile/src/attribute.rs +++ b/classfile/src/attribute.rs @@ -19,20 +19,20 @@ pub struct CodeAttributeExceptionTable { impl CodeAttributeExceptionTable { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { - map((be_u16, be_u16, be_u16, be_u16), |(start_pc, end_pc, handler_pc, catch_type)| { + map_res((be_u16, be_u16, be_u16, be_u16), |(start_pc, end_pc, handler_pc, catch_type)| { let catch_type = if catch_type != 0 { - let index = constant_pool.get(&catch_type).unwrap().class_name_index(); - Some(constant_pool.get(&index).unwrap().utf8()) + let index = constant_pool.get(&catch_type).and_then(ConstantPoolItem::class_name_index).ok_or(())?; + Some(constant_pool.get(&index).and_then(ConstantPoolItem::utf8).ok_or(())?) } else { None }; - Self { + Ok::<_, ()>(Self { start_pc, end_pc, handler_pc, catch_type, - } + }) }) .parse(data) } @@ -52,7 +52,7 @@ impl AttributeInfoCode { ( be_u16, be_u16, - map(flat_map(be_u32, take), |x: &[u8]| Self::parse_code(x, constant_pool)), + map_res(flat_map(be_u32, take), |x: &[u8]| Self::parse_code(x, constant_pool)), length_count(be_u16, |x| CodeAttributeExceptionTable::parse(x, constant_pool)), length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)), ), @@ -67,22 +67,21 @@ impl AttributeInfoCode { .parse(data) } - fn parse_code(code: &[u8], constant_pool: &BTreeMap) -> BTreeMap { + fn parse_code(code: &[u8], constant_pool: &BTreeMap) -> Result, ()> { let mut result = BTreeMap::new(); let mut data = code; - loop { + while !data.is_empty() { let offset = unsafe { data.as_ptr().offset_from(code.as_ptr()) } as usize; - if let Ok((remaining, opcode)) = Opcode::parse(data, offset, constant_pool) { - result.insert(offset as _, opcode); - - data = remaining; - } else { - break; + let (remaining, opcode) = Opcode::parse(data, offset, constant_pool).map_err(|_| ())?; + if remaining.len() >= data.len() { + return Err(()); } + result.insert(offset as _, opcode); + data = remaining; } - result + Ok(result) } } @@ -114,8 +113,8 @@ impl LocalVariableTableEntry { ( be_u16, be_u16, - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), be_u16, ), |(start_pc, length, name, descriptor, index)| Self { @@ -152,7 +151,10 @@ pub enum AttributeInfo { impl AttributeInfo { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { map_res( - (map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), flat_map(be_u32, take)), + ( + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + flat_map(be_u32, take), + ), |(name, info): (_, &[u8])| { Ok::<_, nom::Err<_>>(match name.as_str() { "ConstantValue" => AttributeInfo::ConstantValue(Self::parse_constant_value(info, constant_pool)?.1), @@ -180,11 +182,11 @@ impl AttributeInfo { } fn parse_source_file<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()).parse(data) + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())).parse(data) } fn parse_constant_value<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], ConstantPoolReference> { - map(be_u16, |x| ConstantPoolReference::from_constant_pool(constant_pool, x as _)).parse(data) + map_res(be_u16, |x| ConstantPoolReference::from_constant_pool(constant_pool, x).ok_or(())).parse(data) } fn parse_local_variable_table<'a>( diff --git a/classfile/src/class.rs b/classfile/src/class.rs index 01f3a15a..059d6b70 100644 --- a/classfile/src/class.rs +++ b/classfile/src/class.rs @@ -2,6 +2,7 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; use nom::{ IResult, Parser, + error::{Error, ErrorKind}, multi::length_count, number::complete::{be_u16, be_u32}, }; @@ -9,22 +10,38 @@ use nom::{ use java_constants::ClassAccessFlags; use crate::{ - attribute::AttributeInfo, constant_pool::ConstantPoolItem, error::ParseError, field::FieldInfo, interface::parse_interface, method::MethodInfo, + ClassFileError, attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo, + validation::validate_class, }; fn parse_this_class<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { let (data, this_class) = be_u16(data)?; - let class_name_index = constant_pool.get(&this_class).unwrap().class_name_index(); + let class_name_index = constant_pool + .get(&this_class) + .and_then(ConstantPoolItem::class_name_index) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; + let class_name = constant_pool + .get(&class_name_index) + .and_then(ConstantPoolItem::utf8) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; - Ok((data, constant_pool.get(&class_name_index).unwrap().utf8())) + Ok((data, class_name)) } fn parse_super_class<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Option>> { let (data, super_class) = be_u16(data)?; let super_class = if super_class != 0 { - let class_name_index = constant_pool.get(&super_class).unwrap().class_name_index(); - Some(constant_pool.get(&class_name_index).unwrap().utf8()) + let class_name_index = constant_pool + .get(&super_class) + .and_then(ConstantPoolItem::class_name_index) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; + Some( + constant_pool + .get(&class_name_index) + .and_then(ConstantPoolItem::utf8) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?, + ) } else { None }; @@ -46,56 +63,59 @@ pub struct ClassInfo { pub attributes: Vec, } -type ClassBody = ( - u16, - Arc, - Option>, - Vec>, - Vec, - Vec, - Vec, -); - impl ClassInfo { - fn parse_body<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], ClassBody> { - let (data, access_flags) = be_u16(data)?; - let (data, this_class) = parse_this_class(data, constant_pool)?; - let (data, super_class) = parse_super_class(data, constant_pool)?; - let (data, interfaces) = length_count(be_u16, |x| parse_interface(x, constant_pool)).parse(data)?; - let (data, fields) = length_count(be_u16, |x| FieldInfo::parse(x, constant_pool)).parse(data)?; - let (data, methods) = length_count(be_u16, |x| MethodInfo::parse(x, constant_pool)).parse(data)?; - let (data, attributes) = length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)).parse(data)?; - - Ok((data, (access_flags, this_class, super_class, interfaces, fields, methods, attributes))) - } - - pub fn parse(file: &[u8]) -> Result { - let (data, magic) = be_u32::<_, nom::error::Error<&[u8]>>(file).map_err(ParseError::from_nom)?; + fn parse_info(data: &[u8]) -> IResult<&[u8], Self> { + let (data, magic) = be_u32(data)?; if magic != 0xCAFEBABE { - return Err(ParseError::BadMagic(magic)); + return Err(nom::Err::Error(nom::error::Error::new(data, nom::error::ErrorKind::Verify))); } - let (data, minor_version) = be_u16::<_, nom::error::Error<&[u8]>>(data).map_err(ParseError::from_nom)?; - let (data, major_version) = be_u16::<_, nom::error::Error<&[u8]>>(data).map_err(ParseError::from_nom)?; + let (data, minor_version) = be_u16(data)?; + let (data, major_version) = be_u16(data)?; let (data, constant_pool) = ConstantPoolItem::parse_all(data)?; - let (remaining, (access_flags, this_class, super_class, interfaces, fields, methods, attributes)) = - Self::parse_body(data, &constant_pool).map_err(ParseError::from_nom)?; + let (data, access_flags) = be_u16(data)?; + let (data, this_class) = parse_this_class(data, &constant_pool)?; + let (data, super_class) = parse_super_class(data, &constant_pool)?; + let (data, interfaces) = length_count(be_u16, |x| parse_interface(x, &constant_pool)).parse(data)?; + let (data, fields) = length_count(be_u16, |x| FieldInfo::parse(x, &constant_pool)).parse(data)?; + let (data, methods) = length_count(be_u16, |x| MethodInfo::parse(x, &constant_pool)).parse(data)?; + let (data, attributes) = length_count(be_u16, |x| AttributeInfo::parse(x, &constant_pool)).parse(data)?; + + Ok(( + data, + Self { + magic, + minor_version, + major_version, + constant_pool, + access_flags: ClassAccessFlags::from_bits_truncate(access_flags), + this_class, + super_class, + interfaces, + fields, + methods, + attributes, + }, + )) + } + + pub fn parse(file: &[u8]) -> Result { + let (remaining, result) = Self::parse_info(file).map_err(|_| ClassFileError::InvalidFormat)?; if !remaining.is_empty() { - return Err(ParseError::TrailingData); + return Err(ClassFileError::InvalidFormat); + } + if result.major_version < 45 { + return Err(ClassFileError::InvalidFormat); } + if result.major_version > 70 { + return Err(ClassFileError::UnsupportedVersion(result.major_version)); + } + validate_class(&result)?; + + Ok(result) + } - Ok(Self { - magic, - minor_version, - major_version, - constant_pool, - access_flags: ClassAccessFlags::from_bits_truncate(access_flags), - this_class, - super_class, - interfaces, - fields, - methods, - attributes, - }) + pub fn validate(&self) -> Result<(), ClassFileError> { + validate_class(self) } } diff --git a/classfile/src/constant_pool.rs b/classfile/src/constant_pool.rs index 6ff5f7ec..7b7e4e05 100644 --- a/classfile/src/constant_pool.rs +++ b/classfile/src/constant_pool.rs @@ -3,19 +3,14 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc}; use nom::{ IResult, Parser, bytes::complete::take, + combinator::map_res, error::{Error, ErrorKind}, number::complete::{be_f32, be_f64, be_i32, be_i64, be_u16, u8}, }; -use crate::error::ParseError; - fn parse_utf8(data: &[u8]) -> IResult<&[u8], Arc> { let (data, length) = be_u16(data)?; - let (data, utf8) = take(length as usize).parse(data)?; - - let utf8 = String::from_utf8(utf8.to_vec()).map_err(|_| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; - - Ok((data, Arc::new(utf8))) + map_res(take(length as usize), |utf8: &[u8]| String::from_utf8(utf8.to_vec()).map(Arc::new)).parse(data) } #[derive(Debug)] @@ -112,19 +107,20 @@ impl ConstantPoolItem { } } - pub fn parse_all(data: &[u8]) -> Result<(&[u8], BTreeMap), ParseError> { - let (remaining, count) = be_u16::<_, Error<&[u8]>>(data).map_err(ParseError::from_nom)?; + pub fn parse_all(data: &[u8]) -> IResult<&[u8], BTreeMap> { + let (remaining, count) = be_u16(data)?; + if count == 0 { + return Err(nom::Err::Error(Error::new(remaining, ErrorKind::Verify))); + } + if count == 1 { + return Ok((remaining, BTreeMap::new())); + } let mut data = remaining; let mut result = BTreeMap::new(); let mut i = 1; loop { - let (remaining, tag) = u8::<_, Error<&[u8]>>(data).map_err(ParseError::from_nom)?; - let (remaining, item) = Self::parse_tagged(remaining, tag).map_err(|err| match err { - // parse_tagged signals an unrecognized tag with ErrorKind::Switch - nom::Err::Error(e) if e.code == ErrorKind::Switch => ParseError::UnsupportedConstantPoolTag { index: i, tag }, - other => ParseError::from_nom(other), - })?; + let (remaining, item) = Self::parse_with_tag(data)?; let is_double_entry = match &item { Self::Long(_) | Self::Double(_) => { // long or double constant takes two constant pool entries.... @@ -140,7 +136,10 @@ impl ConstantPoolItem { i += 1; } - if i >= count { + if i > count { + return Err(nom::Err::Error(Error::new(data, ErrorKind::Verify))); + } + if i == count { break; } } @@ -153,31 +152,27 @@ impl ConstantPoolItem { Self::parse_tagged(data, tag) } - pub fn utf8(&self) -> Arc { - if let ConstantPoolItem::Utf8(x) = self { - x.clone() - } else { - panic!("Invalid constant pool item"); - } + pub fn utf8(&self) -> Option> { + if let ConstantPoolItem::Utf8(x) = self { Some(x.clone()) } else { None } } - pub fn class_name_index(&self) -> u16 { + pub fn class_name_index(&self) -> Option { if let ConstantPoolItem::Class { name_index } = self { - *name_index + Some(*name_index) } else { - panic!("Invalid constant pool item"); + None } } - pub fn name_and_type(&self) -> (u16, u16) { + pub fn name_and_type(&self) -> Option<(u16, u16)> { if let ConstantPoolItem::NameAndType { name_index, descriptor_index, } = self { - (*name_index, *descriptor_index) + Some((*name_index, *descriptor_index)) } else { - panic!("Invalid constant pool item"); + None } } } @@ -196,40 +191,39 @@ pub enum ConstantPoolReference { } impl ConstantPoolReference { - pub fn from_constant_pool(constant_pool: &BTreeMap, index: u16) -> Self { - match &constant_pool.get(&index).unwrap() { - ConstantPoolItem::Integer(x) => Self::Integer(*x), - ConstantPoolItem::Float(x) => Self::Float(*x), - ConstantPoolItem::Long(x) => Self::Long(*x), - ConstantPoolItem::Double(x) => Self::Double(*x), - ConstantPoolItem::String { string_index } => Self::String(constant_pool.get(string_index).unwrap().utf8()), - ConstantPoolItem::Class { name_index } => Self::Class(constant_pool.get(name_index).unwrap().utf8()), - ConstantPoolItem::Utf8(x) => Self::String(x.clone()), + pub fn from_constant_pool(constant_pool: &BTreeMap, index: u16) -> Option { + match constant_pool.get(&index)? { + ConstantPoolItem::Integer(x) => Some(Self::Integer(*x)), + ConstantPoolItem::Float(x) => Some(Self::Float(*x)), + ConstantPoolItem::Long(x) => Some(Self::Long(*x)), + ConstantPoolItem::Double(x) => Some(Self::Double(*x)), + ConstantPoolItem::String { string_index } => Some(Self::String(constant_pool.get(string_index)?.utf8()?)), + ConstantPoolItem::Class { name_index } => Some(Self::Class(constant_pool.get(name_index)?.utf8()?)), ConstantPoolItem::Methodref { class_index, name_and_type_index, - } => Self::Method(FieldMethodref::from_reference_info( + } => Some(Self::Method(FieldMethodref::from_reference_info( constant_pool, - *class_index as _, - *name_and_type_index as _, - )), + *class_index, + *name_and_type_index, + )?)), ConstantPoolItem::Fieldref { class_index, name_and_type_index, - } => Self::Field(FieldMethodref::from_reference_info( + } => Some(Self::Field(FieldMethodref::from_reference_info( constant_pool, - *class_index as _, - *name_and_type_index as _, - )), + *class_index, + *name_and_type_index, + )?)), ConstantPoolItem::InterfaceMethodref { class_index, name_and_type_index, - } => Self::InterfaceMethodref(FieldMethodref::from_reference_info( + } => Some(Self::InterfaceMethodref(FieldMethodref::from_reference_info( constant_pool, - *class_index as _, - *name_and_type_index as _, - )), - _ => panic!("Invalid constant pool item {:?}", constant_pool.get(&index).unwrap()), + *class_index, + *name_and_type_index, + )?)), + _ => None, } } @@ -274,18 +268,36 @@ pub struct FieldMethodref { } impl FieldMethodref { - pub fn from_reference_info(constant_pool: &BTreeMap, class_index: u16, name_and_type_index: u16) -> Self { - let class_name_index = constant_pool.get(&class_index).unwrap().class_name_index(); - let class_name = constant_pool.get(&class_name_index).unwrap().utf8(); + pub fn from_reference_info(constant_pool: &BTreeMap, class_index: u16, name_and_type_index: u16) -> Option { + let class_name_index = constant_pool.get(&class_index)?.class_name_index()?; + let class_name = constant_pool.get(&class_name_index)?.utf8()?; - let (name_index, descriptor_index) = constant_pool.get(&name_and_type_index).unwrap().name_and_type(); - let name = constant_pool.get(&name_index).unwrap().utf8(); - let descriptor = constant_pool.get(&descriptor_index).unwrap().utf8(); + let (name_index, descriptor_index) = constant_pool.get(&name_and_type_index)?.name_and_type()?; + let name = constant_pool.get(&name_index)?.utf8()?; + let descriptor = constant_pool.get(&descriptor_index)?.utf8()?; - Self { + Some(Self { class: class_name, name, descriptor, - } + }) + } +} + +#[cfg(test)] +mod tests { + use super::ConstantPoolItem; + + #[test] + fn empty_constant_pool_is_valid() { + let (remaining, constant_pool) = ConstantPoolItem::parse_all(&[0x00, 0x01, 0xff]).unwrap(); + + assert!(constant_pool.is_empty()); + assert_eq!(remaining, &[0xff]); + } + + #[test] + fn long_must_fit_in_two_constant_pool_slots() { + assert!(ConstantPoolItem::parse_all(&[0x00, 0x02, 0x05, 0, 0, 0, 0, 0, 0, 0, 0]).is_err()); } } diff --git a/classfile/src/error.rs b/classfile/src/error.rs index ea7da01e..def88250 100644 --- a/classfile/src/error.rs +++ b/classfile/src/error.rs @@ -1,42 +1,5 @@ -use core::fmt::{self, Display, Formatter}; - -/// Reason a class file could not be parsed. Rendered into the -/// `java.lang.ClassFormatError` message, so each variant carries enough -/// context to diagnose the offending file. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ParseError { - Truncated, - BadMagic(u32), - UnsupportedConstantPoolTag { index: u16, tag: u8 }, - Malformed, - TrailingData, -} - -impl ParseError { - pub(crate) fn from_nom(err: nom::Err>) -> Self { - match err { - nom::Err::Incomplete(_) => Self::Truncated, - nom::Err::Error(e) | nom::Err::Failure(e) => { - if e.code == nom::error::ErrorKind::Eof { - Self::Truncated - } else { - Self::Malformed - } - } - } - } -} - -impl Display for ParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - Self::Truncated => write!(f, "Truncated class file"), - Self::BadMagic(magic) => write!(f, "Incompatible magic value 0x{magic:08X} in class file"), - Self::UnsupportedConstantPoolTag { index, tag } => { - write!(f, "Unknown or unsupported constant pool tag {tag} at index {index} in class file") - } - Self::Malformed => write!(f, "Malformed class file"), - Self::TrailingData => write!(f, "Extra bytes at end of class file"), - } - } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ClassFileError { + InvalidFormat, + UnsupportedVersion(u16), } diff --git a/classfile/src/field.rs b/classfile/src/field.rs index ea8edebf..91489fa9 100644 --- a/classfile/src/field.rs +++ b/classfile/src/field.rs @@ -1,6 +1,11 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; -use nom::{IResult, Parser, combinator::map, multi::length_count, number::complete::be_u16}; +use nom::{ + IResult, Parser, + combinator::{map, map_res}, + multi::length_count, + number::complete::be_u16, +}; use java_constants::FieldAccessFlags; @@ -17,13 +22,13 @@ impl FieldInfo { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { map( ( - be_u16, - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), + map_res(be_u16, |x| FieldAccessFlags::from_bits(x).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)), ), |(access_flags, name, descriptor, attributes)| Self { - access_flags: FieldAccessFlags::from_bits(access_flags).unwrap(), + access_flags, name, descriptor, attributes, diff --git a/classfile/src/interface.rs b/classfile/src/interface.rs index def3a034..428483f6 100644 --- a/classfile/src/interface.rs +++ b/classfile/src/interface.rs @@ -1,13 +1,13 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc}; -use nom::{IResult, Parser, combinator::map, number::complete::be_u16}; +use nom::{IResult, Parser, combinator::map_res, number::complete::be_u16}; use crate::constant_pool::ConstantPoolItem; pub fn parse_interface<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { - map(be_u16, |x| { - let class_name_index = constant_pool.get(&x).unwrap().class_name_index(); - constant_pool.get(&class_name_index).unwrap().utf8() + map_res(be_u16, |x| { + let class_name_index = constant_pool.get(&x).and_then(ConstantPoolItem::class_name_index).ok_or(())?; + constant_pool.get(&class_name_index).and_then(ConstantPoolItem::utf8).ok_or(()) }) .parse(data) } diff --git a/classfile/src/lib.rs b/classfile/src/lib.rs index 8d69b85a..be214387 100644 --- a/classfile/src/lib.rs +++ b/classfile/src/lib.rs @@ -9,12 +9,13 @@ mod field; mod interface; mod method; mod opcode; +mod validation; pub use { attribute::{AttributeInfo, AttributeInfoCode}, class::ClassInfo, constant_pool::{ConstantPoolReference, FieldMethodref}, - error::ParseError, + error::ClassFileError, field::FieldInfo, method::MethodInfo, opcode::Opcode, diff --git a/classfile/src/method.rs b/classfile/src/method.rs index 770656db..620590d5 100644 --- a/classfile/src/method.rs +++ b/classfile/src/method.rs @@ -1,6 +1,11 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; -use nom::{IResult, Parser, combinator::map, multi::length_count, number::complete::be_u16}; +use nom::{ + IResult, Parser, + combinator::{map, map_res}, + multi::length_count, + number::complete::be_u16, +}; use java_constants::MethodAccessFlags; @@ -17,13 +22,13 @@ impl MethodInfo { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { map( ( - be_u16, - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), + map_res(be_u16, |x| MethodAccessFlags::from_bits(x).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)), ), |(access_flags, name, descriptor, attributes)| Self { - access_flags: MethodAccessFlags::from_bits(access_flags).unwrap(), + access_flags, name, descriptor, attributes, diff --git a/classfile/src/opcode.rs b/classfile/src/opcode.rs index a57532b2..cdc6733a 100644 --- a/classfile/src/opcode.rs +++ b/classfile/src/opcode.rs @@ -3,7 +3,8 @@ use alloc::{collections::BTreeMap, vec::Vec}; use nom::{ IResult, Parser, bytes::complete::take, - combinator::{flat_map, map, success}, + combinator::{flat_map, map, map_res, success}, + error::{Error, ErrorKind}, multi::count, number::complete::{be_i16, be_i32, be_u16, i8, u8}, }; @@ -180,8 +181,9 @@ impl Opcode { 0x2b => success(Opcode::Aload(1)).parse(data), 0x2c => success(Opcode::Aload(2)).parse(data), 0x2d => success(Opcode::Aload(3)).parse(data), - 0xbd => map(be_u16, |x| { - Opcode::Anewarray(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xbd => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::Anewarray(reference)), + _ => Err(()), }) .parse(data), 0xb0 => success(Opcode::Areturn).parse(data), @@ -197,8 +199,9 @@ impl Opcode { 0x10 => map(i8, Opcode::Bipush).parse(data), 0x34 => success(Opcode::Caload).parse(data), 0x55 => success(Opcode::Castore).parse(data), - 0xc0 => map(be_u16, |x| { - Opcode::Checkcast(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xc0 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::Checkcast(reference)), + _ => Err(()), }) .parse(data), 0x90 => success(Opcode::D2f).parse(data), @@ -260,12 +263,14 @@ impl Opcode { 0x45 => success(Opcode::Fstore(2)).parse(data), 0x46 => success(Opcode::Fstore(3)).parse(data), 0x66 => success(Opcode::Fsub).parse(data), - 0xb4 => map(be_u16, |x| { - Opcode::Getfield(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb4 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Getfield(reference)), + _ => Err(()), }) .parse(data), - 0xb2 => map(be_u16, |x| { - Opcode::Getstatic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb2 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Getstatic(reference)), + _ => Err(()), }) .parse(data), 0xa7 => map(be_i16, Opcode::Goto).parse(data), @@ -312,28 +317,34 @@ impl Opcode { 0x1d => success(Opcode::Iload(3)).parse(data), 0x68 => success(Opcode::Imul).parse(data), 0x74 => success(Opcode::Ineg).parse(data), - 0xc1 => map(be_u16, |x| { - Opcode::Instanceof(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xc1 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::Instanceof(reference)), + _ => Err(()), }) .parse(data), - 0xba => map((be_u16, be_u16), |(x, _)| { - Opcode::Invokedynamic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) - }) - .parse(data), - 0xb9 => map((be_u16, u8, u8), |(x, count, zero)| { - Opcode::Invokeinterface(ConstantPoolReference::from_constant_pool(constant_pool, x as _), count, zero) + 0xba => map_res((be_u16, be_u16), |_: (u16, u16)| Err::(())).parse(data), + 0xb9 => map_res((be_u16, u8, u8), |(x, count, zero)| { + match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::InterfaceMethodref(_)) if count != 0 && zero == 0 => { + Ok(Opcode::Invokeinterface(reference, count, zero)) + } + _ => Err(()), + } }) .parse(data), - 0xb7 => map(be_u16, |x| { - Opcode::Invokespecial(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb7 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Method(_)) => Ok(Opcode::Invokespecial(reference)), + _ => Err(()), }) .parse(data), - 0xb8 => map(be_u16, |x| { - Opcode::Invokestatic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb8 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Method(_)) => Ok(Opcode::Invokestatic(reference)), + _ => Err(()), }) .parse(data), - 0xb6 => map(be_u16, |x| { - Opcode::Invokevirtual(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb6 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Method(_)) => Ok(Opcode::Invokevirtual(reference)), + _ => Err(()), }) .parse(data), 0x80 => success(Opcode::Ior).parse(data), @@ -361,10 +372,29 @@ impl Opcode { 0x94 => success(Opcode::Lcmp).parse(data), 0x09 => success(Opcode::Lconst(0)).parse(data), 0x0a => success(Opcode::Lconst(1)).parse(data), - 0x12 => map(u8, |x| Opcode::Ldc(ConstantPoolReference::from_constant_pool(constant_pool, x as _))).parse(data), - 0x13 => map(be_u16, |x| Opcode::LdcW(ConstantPoolReference::from_constant_pool(constant_pool, x as _))).parse(data), - 0x14 => map(be_u16, |x| { - Opcode::Ldc2W(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0x12 => map_res(u8, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x as u16) { + Some( + reference @ (ConstantPoolReference::Integer(_) + | ConstantPoolReference::Float(_) + | ConstantPoolReference::String(_) + | ConstantPoolReference::Class(_)), + ) => Ok(Opcode::Ldc(reference)), + _ => Err(()), + }) + .parse(data), + 0x13 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some( + reference @ (ConstantPoolReference::Integer(_) + | ConstantPoolReference::Float(_) + | ConstantPoolReference::String(_) + | ConstantPoolReference::Class(_)), + ) => Ok(Opcode::LdcW(reference)), + _ => Err(()), + }) + .parse(data), + 0x14 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ (ConstantPoolReference::Long(_) | ConstantPoolReference::Double(_))) => Ok(Opcode::Ldc2W(reference)), + _ => Err(()), }) .parse(data), 0x6d => success(Opcode::Ldiv).parse(data), @@ -376,7 +406,12 @@ impl Opcode { 0x69 => success(Opcode::Lmul).parse(data), 0x75 => success(Opcode::Lneg).parse(data), 0xab => flat_map((take((4 - (offset + 1) % 4) % 4), be_i32, be_i32), |(_, default, npairs)| { - move |x| map(count((be_i32, be_i32), npairs as _), |offsets| Opcode::Lookupswitch(default, offsets)).parse(x) + move |x: &'a [u8]| { + if npairs < 0 || npairs as usize > x.len() / 8 { + return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); + } + map(count((be_i32, be_i32), npairs as usize), |offsets| Opcode::Lookupswitch(default, offsets)).parse(x) + } }) .parse(data), 0x81 => success(Opcode::Lor).parse(data), @@ -394,21 +429,37 @@ impl Opcode { 0x83 => success(Opcode::Lxor).parse(data), 0xc2 => success(Opcode::Monitorenter).parse(data), 0xc3 => success(Opcode::Monitorexit).parse(data), - 0xc5 => map((be_u16, u8), |(index, dimensions)| { - Opcode::Multianewarray(ConstantPoolReference::from_constant_pool(constant_pool, index as _), dimensions) + 0xc5 => map_res((be_u16, u8), |(index, dimensions)| { + match ConstantPoolReference::from_constant_pool(constant_pool, index) { + Some(reference @ ConstantPoolReference::Class(_)) if dimensions != 0 => Ok(Opcode::Multianewarray(reference, dimensions)), + _ => Err(()), + } + }) + .parse(data), + 0xbb => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::New(reference)), + _ => Err(()), + }) + .parse(data), + 0xbc => map_res(u8, |array_type| { + if (4..=11).contains(&array_type) { + Ok(Opcode::Newarray(array_type)) + } else { + Err(()) + } }) .parse(data), - 0xbb => map(be_u16, |x| Opcode::New(ConstantPoolReference::from_constant_pool(constant_pool, x as _))).parse(data), - 0xbc => map(u8, Opcode::Newarray).parse(data), 0x00 => success(Opcode::Nop).parse(data), 0x57 => success(Opcode::Pop).parse(data), 0x58 => success(Opcode::Pop2).parse(data), - 0xb5 => map(be_u16, |x| { - Opcode::Putfield(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb5 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Putfield(reference)), + _ => Err(()), }) .parse(data), - 0xb3 => map(be_u16, |x| { - Opcode::Putstatic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb3 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Putstatic(reference)), + _ => Err(()), }) .parse(data), 0xa9 => map(u8, |x| Opcode::Ret(x as u16)).parse(data), @@ -418,8 +469,14 @@ impl Opcode { 0x11 => map(be_i16, Opcode::Sipush).parse(data), 0x5f => success(Opcode::Swap).parse(data), 0xaa => flat_map((take((4 - (offset + 1) % 4) % 4), be_i32, be_i32, be_i32), |(_, default, low, high)| { - move |x| { - map(count(be_i32, ((high - low) + 1) as _), |offsets| { + move |x: &'a [u8]| { + let Some(entry_count) = high.checked_sub(low).and_then(|range| range.checked_add(1)) else { + return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); + }; + if entry_count <= 0 || entry_count as usize > x.len() / 4 { + return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); + } + map(count(be_i32, entry_count as usize), |offsets| { Opcode::Tableswitch(default, (low..=high).zip(offsets).collect()) }) .parse(x) @@ -427,7 +484,7 @@ impl Opcode { }) .parse(data), 0xc4 => Self::parse_wide(data), - _ => panic!("Unknown opcode: {:02x}", opcode), + _ => Err(nom::Err::Error(Error::new(data, ErrorKind::Switch))), } } @@ -448,7 +505,7 @@ impl Opcode { 0x39 => map(be_u16, Opcode::Dstore).parse(data), 0x3a => map(be_u16, Opcode::Astore).parse(data), 0xa9 => map(be_u16, Opcode::Ret).parse(data), - _ => panic!("Invalid wide opcode: {:02x}", opcode), + _ => Err(nom::Err::Error(Error::new(data, ErrorKind::Switch))), } } } @@ -501,11 +558,8 @@ mod test { } #[test] - fn test_invokedynamic_consumes_reserved_bytes() { - let (remaining, opcode) = Opcode::parse(&[0xba, 0x00, 0x07, 0x00, 0x00], 0, &constant_pool()).unwrap(); - - assert!(remaining.is_empty()); - assert!(matches!(opcode, Opcode::Invokedynamic(_))); + fn test_invokedynamic_is_rejected() { + assert!(Opcode::parse(&[0xba, 0x00, 0x07, 0x00, 0x00], 0, &constant_pool()).is_err()); } #[test] @@ -525,4 +579,14 @@ mod test { assert!(matches!(opcode, Opcode::Iinc(0x012c, 1000))); assert!(remaining.is_empty()); } + + #[test] + fn test_unknown_opcode_is_rejected() { + assert!(Opcode::parse(&[0xfe], 0, &constant_pool()).is_err()); + } + + #[test] + fn test_invalid_wide_opcode_is_rejected() { + assert!(Opcode::parse(&[0xc4, 0x00], 0, &constant_pool()).is_err()); + } } diff --git a/classfile/src/validation.rs b/classfile/src/validation.rs new file mode 100644 index 00000000..b017351a --- /dev/null +++ b/classfile/src/validation.rs @@ -0,0 +1,234 @@ +use alloc::collections::BTreeMap; + +use java_constants::MethodAccessFlags; + +use crate::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, constant_pool::ConstantPoolItem}; + +enum MemberKind { + Field, + Method, +} + +pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> { + if !is_internal_class_name(&class.this_class) + || class.super_class.as_ref().is_some_and(|name| !is_internal_class_name(name)) + || class.interfaces.iter().any(|name| !is_internal_class_name(name)) + || !validate_constant_pool(&class.constant_pool) + { + return Err(ClassFileError::InvalidFormat); + } + + for field in &class.fields { + if !is_field_descriptor(&field.descriptor) { + return Err(ClassFileError::InvalidFormat); + } + + let constant_values = field + .attributes + .iter() + .filter_map(|attribute| match attribute { + AttributeInfo::ConstantValue(value) => Some(value), + _ => None, + }) + .collect::>(); + if constant_values.len() > 1 + || constant_values.first().is_some_and(|value| { + !matches!( + (field.descriptor.as_str(), *value), + ("Z" | "B" | "C" | "S" | "I", ConstantPoolReference::Integer(_)) + | ("J", ConstantPoolReference::Long(_)) + | ("F", ConstantPoolReference::Float(_)) + | ("D", ConstantPoolReference::Double(_)) + | ("Ljava/lang/String;", ConstantPoolReference::String(_)) + ) + }) + { + return Err(ClassFileError::InvalidFormat); + } + } + + for method in &class.methods { + if !is_method_descriptor(&method.descriptor) { + return Err(ClassFileError::InvalidFormat); + } + + let code_attributes = method + .attributes + .iter() + .filter(|attribute| matches!(attribute, AttributeInfo::Code(_))) + .count(); + if method.access_flags.intersects(MethodAccessFlags::ABSTRACT | MethodAccessFlags::NATIVE) { + if code_attributes != 0 { + return Err(ClassFileError::InvalidFormat); + } + } else if code_attributes != 1 { + return Err(ClassFileError::InvalidFormat); + } + } + + Ok(()) +} + +fn validate_constant_pool(constant_pool: &BTreeMap) -> bool { + constant_pool.values().all(|item| match item { + ConstantPoolItem::Class { name_index } => constant_pool + .get(name_index) + .and_then(ConstantPoolItem::utf8) + .is_some_and(|name| is_class_constant_name(&name)), + ConstantPoolItem::String { string_index } => constant_pool.get(string_index).and_then(ConstantPoolItem::utf8).is_some(), + ConstantPoolItem::Fieldref { + class_index, + name_and_type_index, + } => validate_member_reference(constant_pool, *class_index, *name_and_type_index, MemberKind::Field), + ConstantPoolItem::Methodref { + class_index, + name_and_type_index, + } + | ConstantPoolItem::InterfaceMethodref { + class_index, + name_and_type_index, + } => validate_member_reference(constant_pool, *class_index, *name_and_type_index, MemberKind::Method), + ConstantPoolItem::NameAndType { + name_index, + descriptor_index, + } => { + let name = constant_pool.get(name_index).and_then(ConstantPoolItem::utf8); + let descriptor = constant_pool.get(descriptor_index).and_then(ConstantPoolItem::utf8); + name.is_some_and(|name| !name.is_empty()) + && descriptor.is_some_and(|descriptor| is_field_descriptor(&descriptor) || is_method_descriptor(&descriptor)) + } + _ => true, + }) +} + +fn validate_member_reference(constant_pool: &BTreeMap, class_index: u16, name_and_type_index: u16, kind: MemberKind) -> bool { + let class_name = constant_pool + .get(&class_index) + .and_then(ConstantPoolItem::class_name_index) + .and_then(|index| constant_pool.get(&index)) + .and_then(ConstantPoolItem::utf8); + let name_and_type = constant_pool.get(&name_and_type_index).and_then(ConstantPoolItem::name_and_type); + let Some((name_index, descriptor_index)) = name_and_type else { + return false; + }; + let name = constant_pool.get(&name_index).and_then(ConstantPoolItem::utf8); + let descriptor = constant_pool.get(&descriptor_index).and_then(ConstantPoolItem::utf8); + + class_name.is_some_and(|name| is_class_constant_name(&name)) + && name.is_some_and(|name| !name.is_empty()) + && descriptor.is_some_and(|descriptor| match kind { + MemberKind::Field => is_field_descriptor(&descriptor), + MemberKind::Method => is_method_descriptor(&descriptor), + }) +} + +fn is_internal_class_name(name: &str) -> bool { + !name.is_empty() && !name.starts_with('[') && !name.contains(['.', ';', '[']) +} + +fn is_class_constant_name(name: &str) -> bool { + is_internal_class_name(name) || array_dimensions(name).is_some() +} + +fn is_field_descriptor(descriptor: &str) -> bool { + let mut cursor = 0; + parse_field_type(descriptor.as_bytes(), &mut cursor) && cursor == descriptor.len() +} + +fn is_method_descriptor(descriptor: &str) -> bool { + let bytes = descriptor.as_bytes(); + if bytes.first() != Some(&b'(') { + return false; + } + + let mut cursor = 1; + while bytes.get(cursor).is_some_and(|byte| *byte != b')') { + if !parse_field_type(bytes, &mut cursor) { + return false; + } + } + if bytes.get(cursor) != Some(&b')') { + return false; + } + cursor += 1; + + if bytes.get(cursor) == Some(&b'V') { + cursor += 1; + } else if !parse_field_type(bytes, &mut cursor) { + return false; + } + + cursor == bytes.len() +} + +fn array_dimensions(descriptor: &str) -> Option { + let bytes = descriptor.as_bytes(); + let dimensions = bytes.iter().take_while(|byte| **byte == b'[').count(); + if dimensions == 0 || dimensions > u8::MAX as usize { + return None; + } + + let mut cursor = 0; + if parse_field_type(bytes, &mut cursor) && cursor == bytes.len() { + Some(dimensions) + } else { + None + } +} + +fn parse_field_type(bytes: &[u8], cursor: &mut usize) -> bool { + let mut dimensions = 0; + while bytes.get(*cursor) == Some(&b'[') { + dimensions += 1; + if dimensions > u8::MAX as usize { + return false; + } + *cursor += 1; + } + + match bytes.get(*cursor) { + Some(b'B' | b'C' | b'D' | b'F' | b'I' | b'J' | b'S' | b'Z') => { + *cursor += 1; + true + } + Some(b'L') => { + let name_start = *cursor + 1; + let Some(relative_end) = bytes[name_start..].iter().position(|byte| *byte == b';') else { + return false; + }; + let name_end = name_start + relative_end; + if name_end == name_start || bytes[name_start..name_end].iter().any(|byte| matches!(byte, b'.' | b'[' | b';')) { + return false; + } + *cursor = name_end + 1; + true + } + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::{array_dimensions, is_field_descriptor, is_method_descriptor}; + + #[test] + fn validates_field_and_method_descriptors() { + assert!(is_field_descriptor("Ljava/lang/String;")); + assert!(is_field_descriptor("[[I")); + assert!(!is_field_descriptor("V")); + assert!(!is_field_descriptor("[V")); + assert!(!is_field_descriptor("Igarbage")); + + assert!(is_method_descriptor("([Ljava/lang/String;I)V")); + assert!(!is_method_descriptor("(V)V")); + assert!(!is_method_descriptor("(I")); + assert!(!is_method_descriptor("()")); + } + + #[test] + fn counts_valid_array_dimensions() { + assert_eq!(array_dimensions("[[Ljava/lang/String;"), Some(2)); + assert_eq!(array_dimensions("java/lang/String"), None); + assert_eq!(array_dimensions("[V"), None); + } +} diff --git a/classfile/tests/test.rs b/classfile/tests/test.rs index 77e04e51..d01ac510 100644 --- a/classfile/tests/test.rs +++ b/classfile/tests/test.rs @@ -1,6 +1,8 @@ +use std::collections::BTreeMap; + use java_constants::ClassAccessFlags; -use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference, Opcode}; +use classfile::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, Opcode}; #[test] fn test_hello() { @@ -114,6 +116,16 @@ fn test_switch() { } } +#[test] +fn test_switch_rejects_entry_counts_larger_than_remaining_input() { + let constant_pool = BTreeMap::new(); + let lookup_switch = [0xab, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0xff, 0xff, 0xff]; + assert!(Opcode::parse(&lookup_switch, 0, &constant_pool).is_err()); + + let table_switch = [0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0xff, 0xff, 0xff]; + assert!(Opcode::parse(&table_switch, 0, &constant_pool).is_err()); +} + #[test] fn test_invokeinterface() { let interface = include_bytes!("../../test_data/Interface.class"); @@ -132,3 +144,57 @@ fn test_invokeinterface() { panic!("Expected code attribute"); } } + +#[test] +fn test_malformed_class_files_return_structured_errors() { + let hello = include_bytes!("../../test_data/Hello.class"); + + assert_eq!(ClassInfo::parse(&[]).err(), Some(ClassFileError::InvalidFormat)); + + let mut invalid_magic = hello.to_vec(); + invalid_magic[0] = 0; + assert_eq!(ClassInfo::parse(&invalid_magic).err(), Some(ClassFileError::InvalidFormat)); + + let mut unsupported_version = hello.to_vec(); + unsupported_version[6..8].copy_from_slice(&71u16.to_be_bytes()); + assert_eq!(ClassInfo::parse(&unsupported_version).err(), Some(ClassFileError::UnsupportedVersion(71))); + + assert_eq!(ClassInfo::parse(&hello[..hello.len() / 2]).err(), Some(ClassFileError::InvalidFormat)); + + let minimal_class = vec![ + 0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x05, 0x01, 0x00, 0x04, b'T', b'e', b's', b't', 0x07, 0x00, 0x01, 0x01, 0x00, 0x10, + b'j', b'a', b'v', b'a', b'/', b'l', b'a', b'n', b'g', b'/', b'O', b'b', b'j', b'e', b'c', b't', 0x07, 0x00, 0x03, 0x00, 0x21, 0x00, 0x02, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + assert!(ClassInfo::parse(&minimal_class).is_ok()); + + let mut invalid_constant_pool_index = minimal_class.clone(); + invalid_constant_pool_index[44..46].copy_from_slice(&99u16.to_be_bytes()); + assert_eq!(ClassInfo::parse(&invalid_constant_pool_index).err(), Some(ClassFileError::InvalidFormat)); + + let mut invalid_constant_pool_type = minimal_class; + invalid_constant_pool_type[44..46].copy_from_slice(&1u16.to_be_bytes()); + assert_eq!(ClassInfo::parse(&invalid_constant_pool_type).err(), Some(ClassFileError::InvalidFormat)); +} + +#[test] +fn test_class_info_validation_rejects_invalid_names_descriptors_and_code_layout() { + let hello = include_bytes!("../../test_data/Hello.class"); + + let mut invalid_name = ClassInfo::parse(hello).unwrap(); + invalid_name.this_class = "[I".to_string().into(); + assert_eq!(invalid_name.validate(), Err(ClassFileError::InvalidFormat)); + + let mut invalid_descriptor = ClassInfo::parse(hello).unwrap(); + invalid_descriptor.methods[0].descriptor = "(V)V".to_string().into(); + assert_eq!(invalid_descriptor.validate(), Err(ClassFileError::InvalidFormat)); + + let mut missing_code = ClassInfo::parse(hello).unwrap(); + missing_code.methods[0].attributes.clear(); + assert_eq!(missing_code.validate(), Err(ClassFileError::InvalidFormat)); +} + +#[test] +fn test_array_clone_method_owner_is_a_valid_class_constant() { + assert!(ClassInfo::parse(include_bytes!("../../test_data/Array.class")).is_ok()); +} diff --git a/docs/worklog/2026-08-27-upstream-sync-s3.json b/docs/worklog/2026-08-27-upstream-sync-s3.json new file mode 100644 index 00000000..f87a928b --- /dev/null +++ b/docs/worklog/2026-08-27-upstream-sync-s3.json @@ -0,0 +1,52 @@ +{ + "schema": 1, + "date": "2026-08-27", + "title": "S3 — upstream 컷 822504b 머지(오류 분류 축, 충돌 11) + STATE.md 「다음」 절 현실화", + "services": [ + "RustJava" + ], + "taskId": "rustjava-upstream-sync-s3", + "summary": "docs/upstream-sync-approach.md §5 의 7회차 분할 중 S3. upstream 822504b(#180) 1커밋을 머지하고 충돌 11건을 해소했다. 우리 ParseError(Rust 변형 5 · Java 예외 1종)를 upstream ClassFileError/ClassDefinitionError(Java 예외 4종)로 교체 — 계획서 §3-B 의 판정 그대로다. string.rs 는 upstream 골격을 우리 charset::Charset 으로 라우팅해 중복 charset 표를 흡수했고, thread.rs 는 upstream 본문 위에 PR #4 의 수동 span 을 다시 얹었다. 계획서 §4-A 가 예고한 tests/test_class_format.rs 의 «충돌 0 파손»이 실제로 나서 문구 단정 3건을 완화했다(종류 단정 유지). 부수로 STATE.md 「다음」 절의 최우선 항목이 이미 끝난 PR #8 을 가리키던 것을 닫았다.", + "changes": [ + "머지 커밋 — upstream 822504b 1커밋. 충돌 11: AGENTS.md · classfile/src/{class,constant_pool,error,lib}.rs · java/lang/string.rs · java/lang/thread.rs · tests/.../test_string.rs · jvm_rust/class_definition.rs · src/runtime.rs · test_utils/src/lib.rs.", + "classfile 오류형 교체 — ParseError(Truncated/BadMagic/UnsupportedConstantPoolTag/Malformed/TrailingData) 삭제, upstream ClassFileError{InvalidFormat, UnsupportedVersion} + ClassDefinitionError 채택. Java 관측면이 ClassFormatError 1종 → ClassFormatError·UnsupportedClassVersionError·VerifyError·UnsupportedOperationException 4종.", + "java/lang/string.rs — upstream 의 decode_str/encode_str 중복 charset 표를 지우고 charset::Charset 으로 라우팅. 기본 charset 경로는 upstream 의미(미지원 시 UTF-8 폴백, JDK 규격), 명시 charset 경로는 UnsupportedEncodingException throw 유지.", + "java/lang/thread.rs — upstream 의 재작성 본문(monitor_enter/exit 로 감싼 정리 경로 + 예외 포맷 실패 처리)을 취하고 #[tracing::instrument] 자리에 PR #4 의 tracing::info_span!+.instrument(span) 를 다시 얹었다.", + "tests/test_class_format.rs — 메시지 문구 단정 3건 삭제(upstream 이 원인별 문구를 \"Invalid class file\" 로 평탄화), ClassFormatError 종류 단정 4건 유지. 4/4 통과.", + "STATE.md — 「다음」 ③-0(PR #8 처분) 항목을 «해소됨»으로 닫고 순서 재부여, ⑤ 운영 메모의 열린 PR/원격 브랜치 표를 2026-08-27 실측으로 교체, ① 절에 S3 실측 기록.", + "REPORT.md — [2026-08-27] 항목 append." + ], + "deploy": { + "sha": "", + "urls": [] + }, + "verification": "CI rust.yml 4종 전건 로컬 rc=0 — cargo fmt --all -- --check / cargo clippy --all -- -D warnings / cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings / cargo test --all. cargo test --all = 216 passed / 0 failed / 1 ignored (S1 169 → S2 191 → S3 216, 우리 테스트 유실 0). tests/test_class_format.rs 4/4. git grep 'tracing::instrument|tracing-attributes' 0건(§4-C 불변). 충돌 재측정은 git merge-tree --write-tree --name-only HEAD 822504b = 11파일. 우리 고유 줄 생존 대조: base af4f6f8 이후 우리가 추가한 .rs 321줄 중 부재 81줄이며 전건 의도한 해소(ParseError 기구 · thread.rs 구본문 · 완화한 문구 단정 3줄)임을 확인했다.", + "issues": [ + "게이트③에서 착지했다 — PR #16 · base 는 S2 착지 «전»에 main 으로 선제 재타깃됐다(부모 머지 시 자식 PR 자동 CLOSED 방지).", + "★착수 시 예측을 «절반» 틀렸다: 「S2 가 스쿼시로 들어가면 af4f6f8 조상이 끊긴다」로 적었는데, 실측은 ★upstream 조상(822504b·af4f6f8·1f356ae)은 «전건 무손상»이고 끊긴 것은 «origin/main 과의» 조상이었다(merge-base b3a4cf4 · 11ef501 이 조상 아님 · main 과 6충돌). ⇒ 복원은 필요했으나 대상이 «직전 upstream 컷»이 아니라 «main 의 S2 스쿼시커밋 11ef501» 이다. 게이트③이 `git merge -s ours --no-ff 11ef501`(트리 무변경 실측: `git diff df3b04a 11ef501` 빈 출력)로 복원해 충돌 0 으로 만들었다.", + "★다음 회차(S4~S7)에 그대로 재발한다 — 스택하지 않고 main 에서 따면 이 축은 애초에 생기지 않는다. S2·S3 가 스택한 것은 앞 회차가 미착지였기 때문이고, S4 착수 시점에는 S3 가 main 에 있으므로 스택할 이유가 없다." + ], + "proposals": [ + { + "title": "계획서 §5 의 「새 충돌」 예측을 «하한»으로 다시 적어라 — 앞 회차가 닫은 파일이 다시 충돌한다", + "plainSummary": "회차별로 «몇 개나 충돌할지» 미리 적어 둔 표가 있는데, 실제로는 그보다 더 나온다. 표를 믿고 회차 크기를 잡으면 매번 조금씩 넘친다.", + "userBenefit": "다음 회차(S4~S7)를 맡는 사람이 실제 크기에 맞는 시간을 잡는다 — 지금은 표가 «0 충돌»이라고 적어 둔 회차에서도 손이 갈 수 있다.", + "why": "S3 는 계획서 예측 +9 에 대해 실측 11 이었다. 초과 2건의 성격이 서로 다르다: ⑴AGENTS.md 는 계획서 작성(2026-08-16) 이후 우리가 만든 파일이라 표에 있을 수 없었다 ⑵★thread.rs 는 S1 이 이미 해소한 파일인데 822504b 가 같은 함수를 재작성해 «다시» 충돌했다. ⑵가 중요하다 — 계획서의 「새 충돌 = 해당 컷에서 «처음» 충돌하는 파일 수」라는 정의가 «한 번 닫히면 다시 안 열린다»를 암묵 전제하는데, 그 전제가 실측으로 깨졌다.", + "tradeoff": "⑴표에 「재충돌 가능」 열을 더하면 정확해지지만 컷마다 다시 재야 해서 계획서가 무거워진다. ⑵문장 한 줄로 「예측은 하한이다」만 적으면 공짜지만 회차 크기 산정에는 여전히 못 쓴다. ⑶현상 유지면 다음 사람이 같은 자리에서 같은 놀람을 겪는다(이미 S2 가 15→5 로 한 번 겪었다).", + "effort": "XS — docs/upstream-sync-approach.md §5 에 2~3줄.", + "target": "docs/upstream-sync-approach.md" + }, + { + "title": "classfile 진단 문구 손실을 되돌릴지 결정 — upstream ClassFileError 에 변형을 더하는 별 회차", + "plainSummary": "클래스파일이 깨졌을 때 «왜» 깨졌는지 알려 주던 메시지가 사라지고 «잘못된 클래스 파일»이라는 한 문장만 남았다.", + "userBenefit": "클래스파일이 거부될 때 원인(잘린 파일 / 매직 불일치 / 미지원 상수풀 태그)을 메시지에서 바로 읽는다 — 지금은 바이트를 직접 봐야 한다.", + "why": "계획서 §3-B 가 upstream 채택을 고른 근거는 «Java 관측면이 4배 세밀하다»였고 그 판단은 옳다. 하지만 치른 값이 실제로 청구됐다 — tests/test_class_format.rs 의 문구 단정 3건을 이번 회차가 «삭제»했다. 되살리려면 upstream classfile::ClassFileError 에 변형을 추가해야 하는데, 그것은 upstream(dlunch/RustJava) 발신이 필요해 이 머지 리니지 밖이다(계획서 §6 가 명시적으로 뺐다).", + "tradeoff": "⑴upstream PR 을 내면 정본이 고쳐지지만 CLAUDE.md 가 upstream 발신을 «티켓이 명시 허가할 때만» 으로 묶고 있어 별 승인이 필요하고 착지 시점을 우리가 못 정한다. ⑵우리 쪽에서 ClassFileError 를 다시 감싸면 포크 diff 가 늘어 다음 컷마다 충돌한다 — 이 리니지가 줄이려는 바로 그 비용이다. ⑶현상 유지면 진단은 계속 평문이고, 그 대신 포크 diff 는 0 이다.", + "effort": "M — upstream 발신 경로 확인 + 변형 추가 + 우리 쪽 테스트 복원.", + "target": "classfile/src/error.rs · tests/test_class_format.rs · docs/upstream-sync-approach.md §6" + } + ], + "resolvedIssues": [], + "adoptedProposals": [], + "declinedProposals": [] +} diff --git a/docs/worklog/2026-08-27-upstream-sync-s3.md b/docs/worklog/2026-08-27-upstream-sync-s3.md new file mode 100644 index 00000000..9921916c --- /dev/null +++ b/docs/worklog/2026-08-27-upstream-sync-s3.md @@ -0,0 +1,35 @@ +# S3 — upstream 컷 `822504b` 머지 (오류 분류 축) + +티켓 `rustjava-upstream-sync-s3`. 정본 = `docs/upstream-sync-approach.md` §5(7회차 분할 · 한 티켓 = 한 컷). + +## 무엇을 했나 +upstream `822504b`(#180 *Harden JVM runtime correctness*) **1커밋**을 머지하고 **충돌 11건**을 해소했다. +축은 계획서가 이름 붙인 「오류 분류 / PR #3」이다. + +| 파일 | 처분 | 이유 | +|---|---|---| +| `classfile/src/{class,constant_pool,error,lib}.rs` | upstream 채택 | 우리 `ParseError`(Rust 변형 5) → upstream `ClassFileError`. **Java 예외가 1종 → 4종**으로 세분화된다 | +| `jvm_rust/src/class_definition.rs` · `src/runtime.rs` · `test_utils/src/lib.rs` | upstream 채택 | 위의 종속. `verifier::verify(&class)` 가 클래스 정의 시점에 들어온다 | +| `java_runtime/src/classes/java/lang/string.rs` | ★양쪽 병합 | upstream 골격 + 우리 `charset::Charset` 라우팅. upstream 의 `decode_str`/`encode_str` **중복 표 삭제** | +| `java_runtime/tests/classes/java/lang/test_string.rs` | 합집합 | 양쪽이 같은 자리에 다른 테스트를 넣었다 | +| `java_runtime/src/classes/java/lang/thread.rs` | ★양쪽 병합 | upstream 본문 + PR #4 의 수동 span. `#[tracing::instrument]` 재유입 0 | +| `AGENTS.md` | 합집합 | 우리 「Round Worklog」 절 ↔ upstream 「Testing Boundaries」 절 | + +부수 1건 — `tests/test_class_format.rs`: **문구 단정 3건 삭제**(`"Truncated"`·`"tag 18"`·`"magic"`), +`ClassFormatError` **종류 단정은 유지**. 계획서 §4-A 가 예고한 「충돌 0으로 조용히 깨지는」 자리다. + +## 실측 +- 충돌 재측정 **11** — 계획서 예측 **+9** 에 2건 추가. ⑴`AGENTS.md`(계획서 이후 우리가 만든 절) + ⑵★`thread.rs` — **S1 이 이미 닫은 파일이 다시 충돌했다**(`822504b` 가 같은 함수를 재작성). +- 조상 무손상: `git merge-base HEAD upstream/main` = `af4f6f8` ⇒ S2 가 했던 `-s ours` 복원 **불필요**. + 대신 **S2 브랜치 위에 쌓았다**(PR #13 미착지 · `main` 기준이면 S2 충돌 5건 재현). +- CI `rust.yml` 4종 **전건 rc=0** · `cargo test --all` **216 passed / 0 failed / 1 ignored**(S2 191 → +25). +- `tests/test_class_format.rs` **4/4** · `git grep 'tracing::instrument\|tracing-attributes'` **0건**. +- 우리 고유 줄 생존 대조: base 이후 우리가 추가한 `.rs` **321줄** 중 부재 **81줄**, 전건 의도한 해소 + (`ParseError` 기구 · `thread.rs` 구본문 · 완화한 문구 단정 3줄). + +## 부수 — `STATE.md` 「다음」 절 갱신 (티켓 요구 0) +③-0 「`rustjava-pr8-claude-md-prune-disposition`」이 **이미 해소된 일**을 최우선으로 가리키고 있었다. +근거였던 두 사실이 모두 뒤집혔다: PR #8 은 `MERGED`(2026-08-18T19:26:08Z → `00bddf3`)이고 +`reports/rustjava-claude-md-prune.review.md` 는 **실재**한다(승계 `-fix` 리니지 포함). ⇒ 항목을 닫고 +순서를 재부여했으며, ⑤ 운영 메모의 「열린 PR 2건 · #8 좌초」 표도 현실(열린 PR = **#13 하나**)로 갈았다. diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index 8b72d5c2..d7b8f6ae 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -51,7 +51,10 @@ mod string_index_out_of_bounds_exception; mod system; mod thread; mod throwable; +mod unsatisfied_link_error; +mod unsupported_class_version_error; mod unsupported_operation_exception; +mod verify_error; mod virtual_machine_error; pub use self::{ @@ -70,5 +73,6 @@ pub use self::{ number_format_exception::NumberFormatException, object::Object, out_of_memory_error::OutOfMemoryError, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable, - unsupported_operation_exception::UnsupportedOperationException, virtual_machine_error::VirtualMachineError, + unsatisfied_link_error::UnsatisfiedLinkError, unsupported_class_version_error::UnsupportedClassVersionError, + unsupported_operation_exception::UnsupportedOperationException, verify_error::VerifyError, virtual_machine_error::VirtualMachineError, }; diff --git a/java_runtime/src/classes/java/lang/class_loader.rs b/java_runtime/src/classes/java/lang/class_loader.rs index e9ed3ae8..7fae556c 100644 --- a/java_runtime/src/classes/java/lang/class_loader.rs +++ b/java_runtime/src/classes/java/lang/class_loader.rs @@ -198,16 +198,14 @@ impl ClassLoader { } async fn find_class( - _: &Jvm, + jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, name: ClassInstanceRef, ) -> Result> { tracing::debug!("java.lang.ClassLoader::findClass({this:?}, {name:?})"); - // TODO raise ClassNotFoundException - - Ok(None.into()) + Err(jvm.exception("java/lang/ClassNotFoundException", "class not found").await) } async fn find_loaded_class( @@ -299,6 +297,14 @@ impl ClassLoader { ) -> Result> { tracing::debug!("java.lang.ClassLoader::defineClass({this:?}, {name:?}, {bytes:?}, {offset:?}, {length:?})"); + if bytes.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "class bytes").await); + } + let array_length = jvm.array_length(&bytes).await?; + if offset < 0 || length < 0 || (offset as usize).checked_add(length as usize).is_none_or(|end| end > array_length) { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "invalid class byte range").await); + } + let mut data = vec![0; length as usize]; jvm.array_raw_buffer(&bytes).await?.read(offset as _, &mut data)?; diff --git a/java_runtime/src/classes/java/lang/object.rs b/java_runtime/src/classes/java/lang/object.rs index 0d91b3e0..59539419 100644 --- a/java_runtime/src/classes/java/lang/object.rs +++ b/java_runtime/src/classes/java/lang/object.rs @@ -8,7 +8,7 @@ use alloc::{boxed::Box, format, vec}; use dyn_clone::clone_box; use java_class_proto::JavaMethodProto; use java_constants::MethodAccessFlags; -use jvm::{ClassInstance, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; +use jvm::{ClassInstance, ClassInstanceRef, Jvm, MonitorWaitTimeout, Result, runtime::JavaLangString}; use crate::{Runtime, RuntimeClassProto, RuntimeContext, SpawnCallback, classes::java::lang::String}; @@ -105,13 +105,13 @@ impl Object { } async fn clone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { - tracing::warn!("stub java.lang.Object::clone({this:?})"); + tracing::debug!("java.lang.Object::clone({this:?})"); if !jvm.is_instance(&**this, "java/lang/Cloneable") { return Err(jvm.exception("java/lang/CloneNotSupportedException", "Cannot clone this object").await); } - Ok(None.into()) + Ok(jvm.shallow_clone(&this)?.into()) } async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { @@ -131,7 +131,7 @@ impl Object { async fn notify(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Object::notify({this:?})"); - jvm.object_notify(&this, 1); + jvm.object_notify(&this, 1).await?; Ok(()) } @@ -139,7 +139,7 @@ impl Object { async fn notify_all(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Object::notifyAll({this:?})"); - jvm.object_notify(&this, usize::MAX); + jvm.object_notify(&this, usize::MAX).await?; Ok(()) } @@ -155,36 +155,39 @@ impl Object { async fn wait_long_int(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, millis: i64, nanos: i32) -> Result<()> { tracing::debug!("java.lang.Object::wait({this:?}, {millis:?}, {nanos:?})"); + if millis < 0 || !(0..=999_999).contains(&nanos) { + return Err(jvm.exception("java/lang/IllegalArgumentException", "invalid wait timeout").await); + } + struct TimeoutNotifier { - timeout: i64, - jvm: Jvm, - this: Box, + timeout: u64, + waiter: MonitorWaitTimeout, context: Box, } #[async_trait::async_trait] impl SpawnCallback for TimeoutNotifier { async fn call(&self) -> Result<()> { - self.context.sleep(Duration::from_millis(self.timeout as _)).await; - self.jvm.object_notify(&self.this, 1); // TODO this may wake an unrelated waiter + self.context.sleep(Duration::from_millis(self.timeout)).await; + self.waiter.clone().notify(); Ok(()) } } - let timeout = millis; // TODO nanos + let (waiter, timeout_notifier) = jvm.object_wait_prepare(&this).await?; + let timeout = millis as u64 + u64::from(nanos > 0); if timeout != 0 { context.spawn( jvm, Box::new(TimeoutNotifier { timeout, - jvm: jvm.clone(), - this: this.clone().into(), + waiter: timeout_notifier, context: clone_box(context), }), ); } - jvm.object_wait(&this).await?; + jvm.object_wait(waiter).await?; Ok(()) } diff --git a/java_runtime/src/classes/java/lang/string.rs b/java_runtime/src/classes/java/lang/string.rs index 9f68591e..4c7115eb 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -167,7 +167,10 @@ impl String { let bytes: Vec = jvm.load_array(&value, offset as _, count as _).await?; let charset = System::get_charset(jvm).await?; - let string = Charset::resolve(jvm, &charset).await?.decode(cast_slice(&bytes)); + let string = Charset::from_name(&charset).map_or_else( + || RustString::from_utf8_lossy(cast_slice(&bytes)).into_owned(), + |x| x.decode(cast_slice(&bytes)), + ); let utf16 = string.encode_utf16().collect::>(); @@ -281,7 +284,7 @@ impl String { let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; let charset = System::get_charset(jvm).await?; - let bytes = cast_vec(Charset::resolve(jvm, &charset).await?.encode(&string)); + let bytes = cast_vec(Charset::from_name(&charset).map_or_else(|| string.as_bytes().to_vec(), |x| x.encode(&string))); let mut byte_array = jvm.instantiate_array("B", bytes.len()).await?; jvm.array_raw_buffer_mut(&mut byte_array).await?.write(0, &bytes)?; @@ -414,15 +417,20 @@ impl String { async fn index_of_from(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, ch: i32, from_index: i32) -> Result { tracing::debug!("java.lang.String::indexOf({this:?}, {ch:?}, {from_index:?})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; + if !(0..=u16::MAX as i32).contains(&ch) { + return Ok(-1); + } - let index = this_string - .chars() - .skip(from_index as usize) - .position(|x| x as u32 == ch as u32) - .map(|x| x as i32 + from_index); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let from_index = from_index.max(0) as usize; + let index = chars + .get(from_index..) + .and_then(|chars| chars.iter().position(|&value| value == ch as u16)) + .map(|index| index + from_index); - Ok(index.unwrap_or(-1)) + Ok(index.map(|index| index as i32).unwrap_or(-1)) } async fn index_of_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, str: ClassInstanceRef) -> Result { @@ -440,30 +448,41 @@ impl String { ) -> Result { tracing::debug!("java.lang.String::indexOf({this:?}, {str:?}, {from_index})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - let str_string = JavaLangString::to_rust_string(jvm, &str.clone()).await?; + if str.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } - tracing::trace!("this_string: {this_string:?}"); - tracing::trace!("str_string: {str_string:?}"); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let pattern = jvm.get_field(&str, "value", "[C").await?; + let pattern_length = jvm.array_length(&pattern).await?; + let pattern: Vec = jvm.load_array(&pattern, 0, pattern_length).await?; + let from_index = (from_index.max(0) as usize).min(chars.len()); - let chars = this_string.chars().skip(from_index as usize).collect::>(); - let str_chars = str_string.chars().collect::>(); - let index = chars.windows(str_chars.len()).position(|x| x == str_chars).map(|x| x as i32 + from_index); + if pattern.is_empty() { + return Ok(from_index as i32); + } - Ok(index.unwrap_or(-1)) + let index = chars[from_index..] + .windows(pattern.len()) + .position(|window| window == pattern) + .map(|index| index + from_index); + + Ok(index.map(|index| index as i32).unwrap_or(-1)) } async fn last_index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, ch: i32) -> Result { tracing::debug!("java.lang.String::lastIndexOf({this:?}, {ch:?})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; + if !(0..=u16::MAX as i32).contains(&ch) { + return Ok(-1); + } - let index = this_string - .chars() - .collect::>() // TODO i think we don't need collect.. - .into_iter() - .rposition(|x| x as u32 == ch as u32) - .map(|x| x as i32); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let index = chars.iter().rposition(|&value| value == ch as u16).map(|index| index as i32); Ok(index.unwrap_or(-1)) } @@ -471,11 +490,18 @@ impl String { async fn trim(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.String::trim({this:?})"); - let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - - let trimmed = string.trim().to_string(); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let start = chars.iter().position(|&value| value > 0x20).unwrap_or(chars.len()); + let end = chars.iter().rposition(|&value| value > 0x20).map(|index| index + 1).unwrap_or(start); + if start == 0 && end == chars.len() { + return Ok(this); + } + let mut array = jvm.instantiate_array("C", end - start).await?; + jvm.store_array(&mut array, 0, chars[start..end].iter().copied()).await?; - Ok(JavaLangString::from_rust_string(jvm, &trimmed).await?.into()) // TODO buffer sharing + Ok(jvm.new_class("java/lang/String", "([C)V", (array,)).await?.into()) } async fn to_upper_case(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { @@ -503,14 +529,21 @@ impl String { ) -> Result { tracing::debug!("java.lang.String::startsWith({this:?}, {prefix:?}, {offset})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()) - .await? - .chars() - .skip(offset as usize) - .collect::(); - let prefix_string = JavaLangString::to_rust_string(jvm, &prefix.clone()).await?; + if prefix.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "prefix is null").await); + } + if offset < 0 { + return Ok(false); + } - Ok(this_string.starts_with(&prefix_string)) + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let prefix_value = jvm.get_field(&prefix, "value", "[C").await?; + let prefix_length = jvm.array_length(&prefix_value).await?; + let prefix: Vec = jvm.load_array(&prefix_value, 0, prefix_length).await?; + + Ok(chars.get(offset as usize..).is_some_and(|chars| chars.starts_with(&prefix))) } async fn init_empty(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { @@ -701,11 +734,16 @@ impl String { return Ok(-1); } - let this_string = JavaLangString::to_rust_string(jvm, &this).await?; - let chars: Vec = this_string.chars().collect(); + if !(0..=u16::MAX as i32).contains(&ch) { + return Ok(-1); + } + + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; let end = (from_index as usize + 1).min(chars.len()); - let index = chars[..end].iter().rposition(|&c| c as u32 == ch as u32).map(|x| x as i32); + let index = chars[..end].iter().rposition(|&value| value == ch as u16).map(|index| index as i32); Ok(index.unwrap_or(-1)) } diff --git a/java_runtime/src/classes/java/lang/system.rs b/java_runtime/src/classes/java/lang/system.rs index 7bc08cda..23aea2ef 100644 --- a/java_runtime/src/classes/java/lang/system.rs +++ b/java_runtime/src/classes/java/lang/system.rs @@ -152,9 +152,10 @@ impl System { Ok(value) } - async fn exit(_jvm: &Jvm, _: &mut RuntimeContext, status: i32) -> Result<()> { - tracing::warn!("stub java.lang.System::exit({status})"); + async fn exit(_jvm: &Jvm, context: &mut RuntimeContext, status: i32) -> Result<()> { + tracing::debug!("java.lang.System::exit({status})"); + context.exit(status); Ok(()) } diff --git a/java_runtime/src/classes/java/lang/thread.rs b/java_runtime/src/classes/java/lang/thread.rs index 83a85cd9..79b33bac 100644 --- a/java_runtime/src/classes/java/lang/thread.rs +++ b/java_runtime/src/classes/java/lang/thread.rs @@ -31,8 +31,8 @@ impl Thread { Self::init_with_runnable_and_name, Default::default(), ), - JavaMethodProto::new("start", "()V", Self::start, Default::default()), - JavaMethodProto::new("join", "()V", Self::join, Default::default()), + JavaMethodProto::new("start", "()V", Self::start, MethodAccessFlags::SYNCHRONIZED), + JavaMethodProto::new("join", "()V", Self::join, MethodAccessFlags::SYNCHRONIZED), JavaMethodProto::new("run", "()V", Self::run, Default::default()), JavaMethodProto::new("isAlive", "()Z", Self::is_alive, Default::default()), JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, Default::default()), @@ -209,40 +209,45 @@ impl Thread { let result: Result<()> = self.jvm.invoke_virtual(&self.this, "run", "()V", []).await; - if let Err(jvm::JavaError::JavaException(x)) = result { - let string_writer = self.jvm.new_class("java/io/StringWriter", "()V", ()).await.unwrap(); - let print_writer = self - .jvm - .new_class("java/io/PrintWriter", "(Ljava/io/Writer;)V", (string_writer.clone(),)) - .await - .unwrap(); - - let _: () = self - .jvm - .invoke_virtual(&x, "printStackTrace", "(Ljava/io/PrintWriter;)V", (print_writer,)) - .await - .unwrap(); - - let trace = self - .jvm - .invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", []) - .await - .unwrap(); - - tracing::error!( - "Uncaught exception in thread {}:\n{}", - self.thread_id, - JavaLangString::to_rust_string(&self.jvm, &trace).await.unwrap() - ); - } else { - result?; + if let Err(jvm::JavaError::JavaException(exception)) = &result { + let trace = async { + let string_writer = self.jvm.new_class("java/io/StringWriter", "()V", ()).await?; + let print_writer = self + .jvm + .new_class("java/io/PrintWriter", "(Ljava/io/Writer;)V", (string_writer.clone(),)) + .await?; + let _: () = self + .jvm + .invoke_virtual(exception, "printStackTrace", "(Ljava/io/PrintWriter;)V", (print_writer,)) + .await?; + let trace = self.jvm.invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", []).await?; + JavaLangString::to_rust_string(&self.jvm, &trace).await + } + .await; + + match trace { + Ok(trace) => tracing::error!("Uncaught exception in thread {}:\n{}", self.thread_id, trace), + Err(error) => tracing::error!(?error, "failed to format uncaught exception in thread {}", self.thread_id), + } } - self.jvm.detach_thread()?; - let mut this = self.this.clone(); - self.jvm.put_field(&mut this, "alive", "Z", false).await.unwrap(); - self.jvm.object_notify(&self.this, usize::MAX); + let cleanup = if let Err(error) = self.jvm.monitor_enter(&self.this).await { + Err(error) + } else { + let alive_result = self.jvm.put_field(&mut this, "alive", "Z", false).await; + let notify_result = if alive_result.is_ok() { + self.jvm.object_notify(&self.this, usize::MAX).await + } else { + Ok(()) + }; + let exit_result = self.jvm.monitor_exit(&self.this).await; + alive_result.and(notify_result).and(exit_result) + }; + let detach_result = self.jvm.detach_thread(); + + cleanup?; + detach_result?; Ok(()) } @@ -279,16 +284,15 @@ impl Thread { Ok(()) } - async fn join(jvm: &Jvm, _context: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + async fn join(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Thread::join({this:?})"); loop { - let listener = jvm.object_listen(&this); let alive: bool = jvm.get_field(&this, "alive", "Z").await?; if !alive { return Ok(()); } - listener.await; + let _: () = jvm.invoke_virtual(&this, "wait", "()V", ()).await?; } } diff --git a/java_runtime/src/classes/java/lang/unsatisfied_link_error.rs b/java_runtime/src/classes/java/lang/unsatisfied_link_error.rs new file mode 100644 index 00000000..9650a54c --- /dev/null +++ b/java_runtime/src/classes/java/lang/unsatisfied_link_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.UnsatisfiedLinkError +pub struct UnsatisfiedLinkError; + +impl UnsatisfiedLinkError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/UnsatisfiedLinkError", + parent_class: Some("java/lang/LinkageError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsatisfiedLinkError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsatisfiedLinkError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/LinkageError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/lang/unsupported_class_version_error.rs b/java_runtime/src/classes/java/lang/unsupported_class_version_error.rs new file mode 100644 index 00000000..18c354c1 --- /dev/null +++ b/java_runtime/src/classes/java/lang/unsupported_class_version_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.UnsupportedClassVersionError +pub struct UnsupportedClassVersionError; + +impl UnsupportedClassVersionError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/UnsupportedClassVersionError", + parent_class: Some("java/lang/ClassFormatError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsupportedClassVersionError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/ClassFormatError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsupportedClassVersionError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/ClassFormatError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/lang/verify_error.rs b/java_runtime/src/classes/java/lang/verify_error.rs new file mode 100644 index 00000000..b1424668 --- /dev/null +++ b/java_runtime/src/classes/java/lang/verify_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.VerifyError +pub struct VerifyError; + +impl VerifyError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/VerifyError", + parent_class: Some("java/lang/LinkageError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VerifyError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VerifyError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/LinkageError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 409819ac..6ae4be7a 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -90,6 +90,9 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Thread::as_proto(), crate::classes::java::lang::Throwable::as_proto(), crate::classes::java::lang::UnsupportedOperationException::as_proto(), + crate::classes::java::lang::UnsupportedClassVersionError::as_proto(), + crate::classes::java::lang::UnsatisfiedLinkError::as_proto(), + crate::classes::java::lang::VerifyError::as_proto(), crate::classes::java::lang::VirtualMachineError::as_proto(), crate::classes::java::net::JarURLConnection::as_proto(), crate::classes::java::net::MalformedURLException::as_proto(), diff --git a/java_runtime/src/runtime.rs b/java_runtime/src/runtime.rs index ae12d782..d653d008 100644 --- a/java_runtime/src/runtime.rs +++ b/java_runtime/src/runtime.rs @@ -19,6 +19,7 @@ pub trait Runtime: Sync + Send + DynClone { async fn sleep(&self, duration: Duration); async fn r#yield(&self); fn spawn(&self, jvm: &Jvm, callback: Box); + fn exit(&self, status: i32); fn now(&self) -> u64; // unix time in millis fn current_task_id(&self) -> u64; diff --git a/java_runtime/tests/classes/java/lang/mod.rs b/java_runtime/tests/classes/java/lang/mod.rs index b18634d2..5de9e808 100644 --- a/java_runtime/tests/classes/java/lang/mod.rs +++ b/java_runtime/tests/classes/java/lang/mod.rs @@ -13,5 +13,6 @@ mod test_object; mod test_short; mod test_string; mod test_string_buffer; +mod test_system; mod test_thread; mod test_throwable; diff --git a/java_runtime/tests/classes/java/lang/test_class.rs b/java_runtime/tests/classes/java/lang/test_class.rs index edcb6f9a..80aecb4d 100644 --- a/java_runtime/tests/classes/java/lang/test_class.rs +++ b/java_runtime/tests/classes/java/lang/test_class.rs @@ -1,6 +1,6 @@ -use java_runtime::classes::java::lang::{Class, String}; +use java_runtime::classes::java::lang::{Class, ClassLoader, String}; use jvm::{ - ClassInstanceRef, JavaError, Result, + Array, ClassInstanceRef, JavaError, Result, runtime::{JavaLangClass, JavaLangString}, }; @@ -244,3 +244,99 @@ async fn test_cldc_class_queries_and_new_instance() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_base_class_loader_find_class_throws() -> Result<()> { + let jvm = test_jvm().await?; + let loader = jvm.new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)).await?; + let name = JavaLangString::from_rust_string(&jvm, "missing.Type").await?; + + let result: Result> = jvm + .invoke_virtual(&loader, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;", (name,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("ClassLoader.findClass must throw ClassNotFoundException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassNotFoundException")); + + Ok(()) +} + +#[tokio::test] +async fn test_define_class_translates_parser_errors_to_java_errors() -> Result<()> { + let jvm = test_jvm().await?; + let loader: ClassInstanceRef = jvm + .new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)) + .await? + .into(); + let name: ClassInstanceRef = None.into(); + + let mut unsupported_version = include_bytes!("../../../../../test_data/Hello.class").to_vec(); + unsupported_version[6..8].copy_from_slice(&71u16.to_be_bytes()); + + let mut verification_error = include_bytes!("../../../../../test_data/MultiArray.class").to_vec(); + let multianewarray = [0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0xc5, 0x00, 0x07, 0x05]; + let multianewarray_offset = verification_error + .windows(multianewarray.len()) + .position(|window| window == multianewarray) + .expect("MultiArray fixture must contain the expected multianewarray instruction"); + verification_error[multianewarray_offset + multianewarray.len() - 1] = 6; + + for (data, expected_exception) in [ + (vec![0, 1, 2, 3], "java/lang/ClassFormatError"), + (unsupported_version, "java/lang/UnsupportedClassVersionError"), + (verification_error, "java/lang/VerifyError"), + ] { + let length = data.len() as i32; + let mut bytes = jvm.instantiate_array("B", data.len()).await?; + jvm.store_array(&mut bytes, 0, data.into_iter().map(|byte| byte as i8).collect::>()) + .await?; + + let result: Result> = jvm + .invoke_virtual( + &loader, + "defineClass", + "(Ljava/lang/String;[BII)Ljava/lang/Class;", + (name.clone(), bytes, 0, length), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("ClassLoader.defineClass must translate malformed class files to Java errors"); + }; + assert!(jvm.is_instance(&*exception, expected_exception)); + } + + Ok(()) +} + +#[tokio::test] +async fn test_define_class_validates_the_byte_range() -> Result<()> { + let jvm = test_jvm().await?; + let loader: ClassInstanceRef = jvm + .new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)) + .await? + .into(); + let name: ClassInstanceRef = None.into(); + let bytes: ClassInstanceRef> = jvm.instantiate_array("B", 4).await?.into(); + + for (bytes, offset, length, expected_exception) in [ + (bytes.clone(), -1, 1, "java/lang/IndexOutOfBoundsException"), + (bytes, 2, 3, "java/lang/IndexOutOfBoundsException"), + (ClassInstanceRef::new(None), 0, 0, "java/lang/NullPointerException"), + ] { + let result: Result> = jvm + .invoke_virtual( + &loader, + "defineClass", + "(Ljava/lang/String;[BII)Ljava/lang/Class;", + (name.clone(), bytes, offset, length), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("ClassLoader.defineClass must validate its byte range"); + }; + assert!(jvm.is_instance(&*exception, expected_exception)); + } + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs index ca95e7a5..1628ea0f 100644 --- a/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs +++ b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs @@ -11,6 +11,7 @@ async fn test_cldc11_exception_and_error_hierarchy() -> Result<()> { ("java/lang/IllegalMonitorStateException", "java/lang/RuntimeException"), ("java/lang/IllegalThreadStateException", "java/lang/IllegalArgumentException"), ("java/lang/InstantiationException", "java/lang/Exception"), + ("java/lang/VerifyError", "java/lang/LinkageError"), ("java/lang/VirtualMachineError", "java/lang/Error"), ("java/lang/OutOfMemoryError", "java/lang/VirtualMachineError"), ("java/io/InterruptedIOException", "java/io/IOException"), diff --git a/java_runtime/tests/classes/java/lang/test_object.rs b/java_runtime/tests/classes/java/lang/test_object.rs index ef3c7f33..0ed60931 100644 --- a/java_runtime/tests/classes/java/lang/test_object.rs +++ b/java_runtime/tests/classes/java/lang/test_object.rs @@ -4,13 +4,33 @@ use core::{ time::Duration, }; -use alloc::{boxed::Box, collections::btree_map::BTreeMap, sync::Arc}; +use alloc::{boxed::Box, collections::btree_map::BTreeMap, sync::Arc, vec}; -use java_runtime::{Runtime, SpawnCallback, classes::java::lang::Object}; -use jvm::{ClassInstanceRef, JavaError, Jvm, Result}; +use java_class_proto::JavaFieldProto; +use java_runtime::{Runtime, RuntimeClassProto, SpawnCallback, classes::java::lang::Object}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result}; +use jvm_rust::ClassDefinitionImpl; use test_utils::{TestRuntime, create_test_jvm}; +struct CloneableObject; + +impl CloneableObject { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "CloneableObject", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/lang/Cloneable"], + methods: vec![], + fields: vec![ + JavaFieldProto::new("value", "I", Default::default()), + JavaFieldProto::new("reference", "Ljava/lang/Object;", Default::default()), + ], + access_flags: Default::default(), + } + } +} + #[tokio::test] async fn test_wait() -> Result<()> { let runtime = TestRuntime::new(BTreeMap::new()); @@ -34,7 +54,9 @@ async fn test_wait() -> Result<()> { self.runtime.sleep(Duration::from_millis(100)).await; self.notified.store(true, Ordering::Relaxed); + self.jvm.monitor_enter(&self.target).await?; let _: () = self.jvm.invoke_virtual(&self.target, "notify", "()V", ()).await?; + self.jvm.monitor_exit(&self.target).await?; self.jvm.detach_thread()?; @@ -53,7 +75,9 @@ async fn test_wait() -> Result<()> { ); assert!(!notified.load(Ordering::Relaxed)); + jvm.monitor_enter(&object).await?; let _: () = jvm.invoke_virtual(&object, "wait", "()V", ()).await?; + jvm.monitor_exit(&object).await?; assert!(notified.load(Ordering::Relaxed)); Ok(()) @@ -63,46 +87,30 @@ async fn test_wait_timeout() -> Result<()> { let runtime = TestRuntime::new(BTreeMap::new()); let jvm = create_test_jvm(runtime.clone()).await?; - let notified = Arc::new(AtomicBool::new(false)); - let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + jvm.monitor_enter(&object).await?; + let _: () = jvm.invoke_virtual(&object, "wait", "(J)V", (100i64,)).await?; + jvm.monitor_exit(&object).await?; - struct Notifier { - jvm: Jvm, - notified: Arc, - runtime: TestRuntime, - target: ClassInstanceRef, - } - - #[async_trait::async_trait] - impl SpawnCallback for Notifier { - async fn call(&self) -> Result<()> { - self.jvm.attach_thread(None).await?; - - self.runtime.sleep(Duration::from_millis(1000)).await; - self.notified.store(true, Ordering::Relaxed); - let _: () = self.jvm.invoke_virtual(&self.target, "notify", "()V", ()).await?; + Ok(()) +} - self.jvm.detach_thread()?; +#[tokio::test] +async fn test_wait_and_notify_require_monitor_ownership() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime).await?; + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; - Ok(()) - } + for result in [ + jvm.invoke_virtual::<_, ()>(&object, "notify", "()V", ()).await, + jvm.invoke_virtual::<_, ()>(&object, "wait", "(J)V", (1i64,)).await, + ] { + let Err(JavaError::JavaException(exception)) = result else { + panic!("monitor ownership violation must throw IllegalMonitorStateException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalMonitorStateException")); } - runtime.spawn( - &jvm, - Box::new(Notifier { - jvm: jvm.clone(), - notified: notified.clone(), - runtime: runtime.clone(), - target: object.clone().into(), - }), - ); - - assert!(!notified.load(Ordering::Relaxed)); - let _: () = jvm.invoke_virtual(&object, "wait", "(J)V", (100i64,)).await?; - assert!(!notified.load(Ordering::Relaxed)); - Ok(()) } @@ -124,6 +132,57 @@ async fn test_clone_not_cloneable() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_clone_creates_shallow_object_and_array_copies() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + CloneableObject::as_proto(), + Box::new(runtime) as Box<_>, + )), + None, + ) + .await?; + + let mut original = jvm.instantiate_class("CloneableObject").await?; + let reference = jvm.new_class("java/lang/Object", "()V", ()).await?; + jvm.put_field(&mut original, "value", "I", 7i32).await?; + jvm.put_field(&mut original, "reference", "Ljava/lang/Object;", reference.clone()).await?; + + let mut cloned: ClassInstanceRef = jvm.invoke_virtual(&original, "clone", "()Ljava/lang/Object;", ()).await?; + assert_ne!(original.identity(), cloned.identity()); + assert_eq!(jvm.get_field::(&cloned, "value", "I").await?, 7); + let cloned_reference = jvm + .get_field::>(&cloned, "reference", "Ljava/lang/Object;") + .await?; + assert_eq!(reference.identity(), cloned_reference.identity()); + + jvm.put_field(&mut cloned, "value", "I", 9i32).await?; + assert_eq!(jvm.get_field::(&original, "value", "I").await?, 7); + + let mut array = jvm.instantiate_array("I", 2).await?; + jvm.store_array(&mut array, 0, [1i32, 2i32]).await?; + let mut cloned_array: ClassInstanceRef> = jvm.invoke_virtual(&array, "clone", "()Ljava/lang/Object;", ()).await?; + assert_ne!(array.identity(), cloned_array.identity()); + jvm.store_array(&mut cloned_array, 0, [9i32]).await?; + assert_eq!(jvm.load_array::(&array, 0, 2).await?, [1, 2]); + assert_eq!(jvm.load_array::(&cloned_array, 0, 2).await?, [9, 2]); + + let mut reference_array = jvm.instantiate_array("Ljava/lang/Object;", 1).await?; + jvm.store_array(&mut reference_array, 0, [reference.clone()]).await?; + let mut cloned_reference_array: ClassInstanceRef> = + jvm.invoke_virtual(&reference_array, "clone", "()Ljava/lang/Object;", ()).await?; + let cloned_element = jvm.load_array::>(&cloned_reference_array, 0, 1).await?; + assert_eq!(cloned_element[0].identity(), reference.identity()); + let replacement: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); + jvm.store_array(&mut cloned_reference_array, 0, [replacement]).await?; + let original_element = jvm.load_array::>(&reference_array, 0, 1).await?; + assert_eq!(original_element[0].identity(), reference.identity()); + + Ok(()) +} + #[tokio::test] async fn test_hash_code_is_stable_for_same_object() -> Result<()> { let runtime = TestRuntime::new(BTreeMap::new()); diff --git a/java_runtime/tests/classes/java/lang/test_string.rs b/java_runtime/tests/classes/java/lang/test_string.rs index 0f99ed46..9dcf4e2b 100644 --- a/java_runtime/tests/classes/java/lang/test_string.rs +++ b/java_runtime/tests/classes/java/lang/test_string.rs @@ -434,6 +434,39 @@ async fn test_get_bytes_unsupported_charset_throws() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_index_of_uses_utf16_indices_and_handles_empty_patterns() -> Result<()> { + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, "a😀b").await?; + let empty = JavaLangString::from_rust_string(&jvm, "").await?; + let emoji = JavaLangString::from_rust_string(&jvm, "😀").await?; + let tail = JavaLangString::from_rust_string(&jvm, "b").await?; + + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;)I", (emoji,)) + .await?, + 1 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;)I", (tail,)).await?, + 3 + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(II)I", (b'b' as i32, -10)).await?, 3); + assert_eq!(jvm.invoke_virtual::<_, i32>(&string, "lastIndexOf", "(I)I", (b'b' as i32,)).await?, 3); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;I)I", (empty.clone(), -10)) + .await?, + 0 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;I)I", (empty, 99)) + .await?, + 4 + ); + + Ok(()) +} + #[tokio::test] async fn test_new_string_unsupported_charset_throws() -> Result<()> { let jvm = test_jvm().await?; @@ -454,3 +487,43 @@ async fn test_new_string_unsupported_charset_throws() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_unknown_string_charset_throws_unsupported_encoding() -> Result<()> { + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, "value").await?; + let charset = JavaLangString::from_rust_string(&jvm, "not-a-charset").await?; + + let result: Result>> = jvm + .invoke_virtual(&string, "getBytes", "(Ljava/lang/String;)[B", (charset.clone(),)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown charset must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + let mut bytes = jvm.instantiate_array("B", 1).await?; + jvm.store_array(&mut bytes, 0, [b'a' as i8]).await?; + let result = jvm.new_class("java/lang/String", "([BLjava/lang/String;)V", (bytes, charset)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown constructor charset must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + Ok(()) +} + +#[tokio::test] +async fn test_trim_uses_java_control_character_boundary() -> Result<()> { + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, " \t\u{a0}value\u{a0}\n ").await?; + let trimmed: ClassInstanceRef = jvm.invoke_virtual(&string, "trim", "()Ljava/lang/String;", ()).await?; + + assert_eq!(JavaLangString::to_rust_string(&jvm, &trimmed).await?, "\u{a0}value\u{a0}"); + + let unchanged = JavaLangString::from_rust_string(&jvm, "value").await?; + let same: ClassInstanceRef = jvm.invoke_virtual(&unchanged, "trim", "()Ljava/lang/String;", ()).await?; + assert_eq!(unchanged.identity(), same.identity()); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_system.rs b/java_runtime/tests/classes/java/lang/test_system.rs new file mode 100644 index 00000000..78309ac4 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_system.rs @@ -0,0 +1,23 @@ +use alloc::collections::BTreeMap; + +use java_runtime::Runtime; +use jvm::Result; +use test_utils::{TestRuntime, create_test_jvm}; + +#[tokio::test] +async fn test_system_time_yield_and_exit_runtime_contract() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + + let before = runtime.now(); + let now: i64 = jvm.invoke_static("java/lang/System", "currentTimeMillis", "()J", ()).await?; + assert!(now >= before as i64); + + let _: () = jvm.invoke_static("java/lang/Thread", "yield", "()V", ()).await?; + let _: () = jvm.invoke_static("java/lang/System", "exit", "(I)V", (17,)).await?; + assert_eq!(runtime.exit_status(), Some(17)); + let _: () = jvm.invoke_static("java/lang/System", "exit", "(I)V", (i32::MIN,)).await?; + assert_eq!(runtime.exit_status(), Some(i32::MIN)); + + Ok(()) +} diff --git a/jvm/src/array_class_instance.rs b/jvm/src/array_class_instance.rs index baf07ef6..80056f94 100644 --- a/jvm/src/array_class_instance.rs +++ b/jvm/src/array_class_instance.rs @@ -6,6 +6,8 @@ use crate::{Result, class_definition::ClassDefinition, class_instance::ClassInst pub trait ArrayClassInstance: ClassInstance { fn class_definition(&self) -> Box; fn destroy(self: Box); + fn identity(&self) -> usize; + fn shallow_clone(&self) -> Result>; fn equals(&self, other: &dyn ClassInstance) -> Result; fn store(&mut self, offset: usize, values: Box<[JavaValue]>) -> Result<()>; fn load(&self, offset: usize, count: usize) -> Result>; @@ -20,6 +22,14 @@ impl ClassInstance for T { ArrayClassInstance::destroy(self) } + fn identity(&self) -> usize { + ArrayClassInstance::identity(self) + } + + fn shallow_clone(&self) -> Result> { + ArrayClassInstance::shallow_clone(self) + } + fn class_definition(&self) -> Box { ArrayClassInstance::class_definition(self) } diff --git a/jvm/src/class_instance.rs b/jvm/src/class_instance.rs index f6248043..f1f0a91e 100644 --- a/jvm/src/class_instance.rs +++ b/jvm/src/class_instance.rs @@ -13,6 +13,8 @@ use crate::{ArrayClassInstance, ClassDefinition, Field, Result, as_any::AsAny, v #[async_trait::async_trait] pub trait ClassInstance: Sync + Send + AsAny + Debug + DynHash + DynClone + 'static { fn destroy(self: Box); + fn identity(&self) -> usize; + fn shallow_clone(&self) -> Result>; fn class_definition(&self) -> Box; fn equals(&self, other: &dyn ClassInstance) -> Result; fn get_field(&self, field: &dyn Field) -> Result; diff --git a/jvm/src/class_loader.rs b/jvm/src/class_loader.rs index 7e3c096e..aa739938 100644 --- a/jvm/src/class_loader.rs +++ b/jvm/src/class_loader.rs @@ -1,6 +1,7 @@ use alloc::{boxed::Box, sync::Arc}; -use parking_lot::RwLock; +use event_listener::{Event, EventListener}; +use parking_lot::{Mutex, RwLock}; use crate::{ ClassDefinition, ClassInstance, Jvm, Result, @@ -15,11 +16,29 @@ pub(crate) enum InitState { Erroneous, } +pub(crate) enum InitializationAction { + Initialize, + Recursive, + Wait(EventListener), + Initialized, + Erroneous, +} + +struct ClassInitializationState { + status: InitState, + owner: Option, +} + +struct ClassInitialization { + state: Mutex, + completed: Event, +} + #[derive(Clone)] pub struct Class { pub definition: Box, java_class: Arc>>>, - init_state: Arc>, + initialization: Arc, } impl Class { @@ -27,16 +46,40 @@ impl Class { Self { definition, java_class: Arc::new(RwLock::new(java_class)), - init_state: Arc::new(RwLock::new(InitState::NotInitialized)), + initialization: Arc::new(ClassInitialization { + state: Mutex::new(ClassInitializationState { + status: InitState::NotInitialized, + owner: None, + }), + completed: Event::new(), + }), } } - pub(crate) fn init_state(&self) -> InitState { - *self.init_state.read() + pub(crate) fn initialization_action(&self, thread_id: u64) -> InitializationAction { + let listener = self.initialization.completed.listen(); + let mut state = self.initialization.state.lock(); + + match state.status { + InitState::NotInitialized => { + state.status = InitState::InProgress; + state.owner = Some(thread_id); + InitializationAction::Initialize + } + InitState::InProgress if state.owner == Some(thread_id) => InitializationAction::Recursive, + InitState::InProgress => InitializationAction::Wait(listener), + InitState::Initialized => InitializationAction::Initialized, + InitState::Erroneous => InitializationAction::Erroneous, + } } - pub(crate) fn set_init_state(&self, state: InitState) { - *self.init_state.write() = state; + pub(crate) fn finish_initialization(&self, status: InitState) { + { + let mut state = self.initialization.state.lock(); + state.status = status; + state.owner = None; + } + self.initialization.completed.notify(usize::MAX); } pub fn set_java_class(&self, java_class: Box) { @@ -73,9 +116,11 @@ impl ClassLoaderWrapper for BootstrapClassLoaderWrapper<'_> { async fn load_class(&self, jvm: &Jvm, name: &str) -> Result> { let definition = self.bootstrap_class_loader.load_class(jvm, name).await?; if let Some(definition) = definition { - let java_class = jvm.register_class(definition.clone(), None).await?; + let java_class = JavaLangClass::from_rust_class(jvm, definition.clone(), None).await?; + let class = Class::new(definition, Some(java_class)); + jvm.register_class_internal(class.clone(), Some(self)).await?; - Ok(Some(Class::new(definition, java_class))) + Ok(Some(class)) } else { Ok(None) } diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 89e97288..2daab7f6 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -3,14 +3,12 @@ use alloc::{borrow::ToOwned, boxed::Box, collections::BTreeMap, format, string::String, sync::Arc, vec::Vec}; use core::{ fmt::Debug, - hash::BuildHasher, iter, sync::atomic::{AtomicBool, Ordering}, }; use dyn_clone::clone_box; -use event_listener::{Event, EventListener}; -use hashbrown::{DefaultHashBuilder, HashSet}; +use hashbrown::HashSet; use parking_lot::RwLock; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; @@ -20,12 +18,15 @@ use crate::{ array_class_instance::{ArrayClassInstance, ArrayRawBuffer, ArrayRawBufferMut}, class_definition::ClassDefinition, class_instance::ClassInstance, - class_loader::{BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, InitState, JavaClassLoaderWrapper}, + class_loader::{ + BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, InitState, InitializationAction, JavaClassLoaderWrapper, + }, error::JavaError, field::Field, garbage_collector::determine_garbage, invoke_arg::InvokeArg, method::Method, + monitor::{Monitor, MonitorWait, MonitorWaitTimeout}, runtime::{JavaLangClass, JavaLangClassLoader, JavaLangString}, thread::JvmThread, r#type::JavaType, @@ -37,8 +38,7 @@ struct JvmInner { threads: RwLock>, all_objects: RwLock>>, string_pool: RwLock, Box>>, - monitors: RwLock>>, - monitor_hasher: DefaultHashBuilder, + monitors: RwLock>>, get_current_thread_id: Box u64 + Sync + Send>, bootstrap_class_loader: Box, bootstrapping: AtomicBool, @@ -62,7 +62,6 @@ impl Jvm { all_objects: RwLock::new(HashSet::new()), string_pool: RwLock::new(BTreeMap::new()), monitors: RwLock::new(BTreeMap::new()), - monitor_hasher: DefaultHashBuilder::default(), get_current_thread_id: Box::new(get_current_thread_id), bootstrap_class_loader: Box::new(bootstrap_class_loader), bootstrapping: AtomicBool::new(true), @@ -468,12 +467,28 @@ impl Jvm { pub fn destroy(&self, instance: Box) -> Result<()> { tracing::debug!("Destroy {}", instance.class_definition().name()); + self.inner.monitors.write().remove(&instance.identity()); self.inner.all_objects.write().remove(&instance); instance.destroy(); Ok(()) } + pub fn shallow_clone(&self, instance: &Box) -> Result> { + let cloned = instance.shallow_clone()?; + let thread_id = (self.inner.get_current_thread_id)(); + self.inner + .threads + .write() + .get_mut(&thread_id) + .unwrap() + .top_frame_mut() + .local_variables_mut() + .push(cloned.clone()); + self.inner.all_objects.write().insert(cloned.clone()); + Ok(cloned) + } + // JVMS 5.1 string interning: equal string literals (and String.intern results) share one instance pub async fn intern_string(&self, value: &str) -> Result> { let key = value.encode_utf16().collect::>(); @@ -515,19 +530,45 @@ impl Jvm { self.inner.classes.read().get(class_name).cloned() } - pub fn object_listen(&self, obj: &Box) -> EventListener { - self.get_or_create_monitor(obj).listen() + pub async fn monitor_enter(&self, obj: &Box) -> Result<()> { + let thread_id = (self.inner.get_current_thread_id)(); + self.get_or_create_monitor(obj).enter(thread_id).await; + Ok(()) + } + + pub async fn monitor_exit(&self, obj: &Box) -> Result<()> { + let thread_id = (self.inner.get_current_thread_id)(); + match self.get_or_create_monitor(obj).exit(thread_id) { + Ok(()) => Ok(()), + Err(_) => Err(self + .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") + .await), + } } - pub async fn object_wait(&self, obj: &Box) -> Result<()> { - self.object_listen(obj).await; + pub async fn object_wait_prepare(&self, obj: &Box) -> Result<(MonitorWait, MonitorWaitTimeout)> { + let thread_id = (self.inner.get_current_thread_id)(); + match self.get_or_create_monitor(obj).prepare_wait(thread_id) { + Ok(wait) => Ok(wait), + Err(_) => Err(self + .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") + .await), + } + } + pub async fn object_wait(&self, wait: MonitorWait) -> Result<()> { + wait.wait().await; Ok(()) } - pub fn object_notify(&self, obj: &Box, count: usize) { - let monitor = self.get_or_create_monitor(obj); - monitor.notify(count); + pub async fn object_notify(&self, obj: &Box, count: usize) -> Result<()> { + let thread_id = (self.inner.get_current_thread_id)(); + match self.get_or_create_monitor(obj).notify(thread_id, count) { + Ok(()) => Ok(()), + Err(_) => Err(self + .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") + .await), + } } #[allow(clippy::double_must_use)] // `#[must_use]` comes from the async_recursion macro expansion, not our code @@ -686,6 +727,11 @@ impl Jvm { pub async fn exception(&self, r#type: &str, message: &str) -> JavaError { tracing::info!("throwing java exception: {} {message}", r#type); + if !self.has_class(r#type) { + self.resolve_class_internal(r#type, Some(&BootstrapClassLoaderWrapper::new(&*self.inner.bootstrap_class_loader))) + .await + .unwrap(); + } let message_str = JavaLangString::from_rust_string(self, message).await.unwrap(); let instance = self.new_class(r#type, "(Ljava/lang/String;)V", (message_str,)).await.unwrap(); @@ -739,7 +785,7 @@ impl Jvm { Ok(garbage_count) } - async fn register_class_internal(&self, class: Class, class_loader_wrapper: Option<&dyn ClassLoaderWrapper>) -> Result<()> { + pub(crate) async fn register_class_internal(&self, class: Class, class_loader_wrapper: Option<&dyn ClassLoaderWrapper>) -> Result<()> { if !class.definition.name().starts_with('[') { // ensure superclass and superinterfaces are loaded if let Some(super_class) = class.definition.super_class_name() @@ -755,7 +801,7 @@ impl Jvm { } } - self.inner.classes.write().insert(class.definition.name().to_owned(), class.clone()); + self.inner.classes.write().entry(class.definition.name().to_owned()).or_insert(class); Ok(()) } @@ -767,39 +813,41 @@ impl Jvm { return Ok(()); } - match class.init_state() { - InitState::Initialized | InitState::InProgress => return Ok(()), - InitState::Erroneous => { - return Err(self - .exception( - "java/lang/NoClassDefFoundError", - &format!("Could not initialize class {}", class.definition.name()), - ) - .await); + let thread_id = (self.inner.get_current_thread_id)(); + loop { + match class.initialization_action(thread_id) { + InitializationAction::Initialize => break, + InitializationAction::Recursive | InitializationAction::Initialized => return Ok(()), + InitializationAction::Wait(listener) => listener.await, + InitializationAction::Erroneous => { + return Err(self + .exception( + "java/lang/NoClassDefFoundError", + &format!("Could not initialize class {}", class.definition.name()), + ) + .await); + } } - InitState::NotInitialized => {} } - class.set_init_state(InitState::InProgress); - if let Some(super_name) = class.definition.super_class_name() { // resolution failure is not an initialization failure, so initialization may be retried let super_class = match self.resolve_class(&super_name).await { Ok(x) => x, Err(err) => { - class.set_init_state(InitState::NotInitialized); + class.finish_initialization(InitState::NotInitialized); return Err(err); } }; if let Err(err) = self.ensure_initialized(&super_class).await { - class.set_init_state(InitState::Erroneous); + class.finish_initialization(InitState::Erroneous); return Err(err); } } if let Err(err) = class.definition.prepare(self).await { - class.set_init_state(InitState::Erroneous); + class.finish_initialization(InitState::Erroneous); return Err(err); } @@ -807,7 +855,7 @@ impl Jvm { tracing::debug!("Calling for {}", class.definition.name()); if let Err(err) = self.execute_method(class, None, &clinit, Box::new([])).await { - class.set_init_state(InitState::Erroneous); + class.finish_initialization(InitState::Erroneous); let JavaError::JavaException(exception) = &err; if self.is_instance(&**exception, "java/lang/Error") { @@ -823,7 +871,7 @@ impl Jvm { } } - class.set_init_state(InitState::Initialized); + class.finish_initialization(InitState::Initialized); Ok(()) } @@ -896,8 +944,8 @@ impl Jvm { } } - fn get_or_create_monitor(&self, obj: &Box) -> Arc { - let key = self.inner.monitor_hasher.hash_one(obj); + fn get_or_create_monitor(&self, obj: &Box) -> Arc { + let key = obj.identity(); let monitors = self.inner.monitors.read(); if let Some(monitor) = monitors.get(&key) { @@ -906,7 +954,7 @@ impl Jvm { drop(monitors); let mut monitors = self.inner.monitors.write(); - monitors.entry(key).or_insert_with(|| Arc::new(Event::new())).clone() + monitors.entry(key).or_insert_with(|| Arc::new(Monitor::new())).clone() } // JVMS 5.4.3.2 field resolution: search the class, then its superinterfaces, then its superclass. @@ -985,6 +1033,15 @@ impl Jvm { let thread_id = (self.inner.get_current_thread_id)(); let method_str = format!("{}{}", method.name(), method.descriptor()); + let synchronized_object = if method.access_flags().contains(MethodAccessFlags::SYNCHRONIZED) { + Some(class_instance.clone().unwrap_or_else(|| class.java_class())) + } else { + None + }; + if let Some(object) = &synchronized_object { + self.monitor_enter(object).await?; + } + self.inner .threads .write() @@ -998,6 +1055,15 @@ impl Jvm { self.inner.threads.write().get_mut(&thread_id).unwrap().pop_frame(); + if let Some(object) = &synchronized_object + && let Err(error) = self.monitor_exit(object).await + { + if result.is_ok() { + return Err(error); + } + tracing::error!(?error, "failed to release synchronized method monitor"); + } + result } } diff --git a/jvm/src/lib.rs b/jvm/src/lib.rs index 80ec54d8..9706e426 100644 --- a/jvm/src/lib.rs +++ b/jvm/src/lib.rs @@ -13,6 +13,7 @@ mod garbage_collector; mod invoke_arg; mod jvm; mod method; +mod monitor; mod thread; mod r#type; mod value; @@ -39,6 +40,7 @@ pub use self::{ field::Field, jvm::Jvm, method::Method, + monitor::{MonitorWait, MonitorWaitTimeout}, r#type::JavaType, value::{JavaChar, JavaValue}, }; diff --git a/jvm/src/monitor.rs b/jvm/src/monitor.rs new file mode 100644 index 00000000..4d859c59 --- /dev/null +++ b/jvm/src/monitor.rs @@ -0,0 +1,299 @@ +use alloc::{collections::VecDeque, sync::Arc}; + +use event_listener::{Event, EventListener}; +use parking_lot::Mutex; + +pub(crate) struct Monitor { + state: Mutex, + entry_event: Event, +} + +struct MonitorState { + owner: Option, + depth: usize, + next_waiter_id: u64, + waiters: VecDeque, +} + +struct MonitorWaiter { + id: u64, + event: Arc, +} + +pub struct MonitorWait { + monitor: Arc, + listener: EventListener, + depth: usize, + thread_id: u64, +} + +#[derive(Clone)] +pub struct MonitorWaitTimeout { + monitor: Arc, + waiter_id: u64, + event: Arc, +} + +#[derive(Debug)] +pub(crate) enum MonitorError { + NotOwner, +} + +impl Monitor { + pub(crate) fn new() -> Self { + Self { + state: Mutex::new(MonitorState { + owner: None, + depth: 0, + next_waiter_id: 0, + waiters: VecDeque::new(), + }), + entry_event: Event::new(), + } + } + + pub(crate) async fn enter(self: &Arc, thread_id: u64) { + loop { + let listener = self.entry_event.listen(); + { + let mut state = self.state.lock(); + match state.owner { + None => { + state.owner = Some(thread_id); + state.depth = 1; + return; + } + Some(owner) if owner == thread_id => { + state.depth += 1; + return; + } + Some(_) => {} + } + } + listener.await; + } + } + + pub(crate) fn exit(&self, thread_id: u64) -> core::result::Result<(), MonitorError> { + let released = { + let mut state = self.state.lock(); + if state.owner != Some(thread_id) { + return Err(MonitorError::NotOwner); + } + + state.depth -= 1; + if state.depth == 0 { + state.owner = None; + true + } else { + false + } + }; + + if released { + self.entry_event.notify(1); + } + Ok(()) + } + + pub(crate) fn prepare_wait(self: &Arc, thread_id: u64) -> core::result::Result<(MonitorWait, MonitorWaitTimeout), MonitorError> { + let event = Arc::new(Event::new()); + let listener = event.listen(); + + let (waiter_id, depth) = { + let mut state = self.state.lock(); + if state.owner != Some(thread_id) { + return Err(MonitorError::NotOwner); + } + + let depth = state.depth; + let waiter_id = state.next_waiter_id; + state.next_waiter_id = state.next_waiter_id.wrapping_add(1); + state.waiters.push_back(MonitorWaiter { + id: waiter_id, + event: event.clone(), + }); + state.owner = None; + state.depth = 0; + + (waiter_id, depth) + }; + + self.entry_event.notify(1); + + Ok(( + MonitorWait { + monitor: self.clone(), + listener, + depth, + thread_id, + }, + MonitorWaitTimeout { + monitor: self.clone(), + waiter_id, + event, + }, + )) + } + + pub(crate) fn notify(&self, thread_id: u64, count: usize) -> core::result::Result<(), MonitorError> { + let events = { + let mut state = self.state.lock(); + if state.owner != Some(thread_id) { + return Err(MonitorError::NotOwner); + } + + let count = count.min(state.waiters.len()); + (0..count) + .filter_map(|_| state.waiters.pop_front()) + .map(|waiter| waiter.event) + .collect::>() + }; + + for event in events { + event.notify(1); + } + Ok(()) + } +} + +impl MonitorWait { + pub(crate) async fn wait(self) { + self.listener.await; + self.monitor.enter(self.thread_id).await; + self.monitor.state.lock().depth = self.depth; + } +} + +impl MonitorWaitTimeout { + pub fn notify(self) { + let event = { + let mut state = self.monitor.state.lock(); + state + .waiters + .iter() + .position(|waiter| waiter.id == self.waiter_id) + .and_then(|position| state.waiters.remove(position)) + .map(|waiter| waiter.event) + }; + + if let Some(event) = event { + debug_assert!(Arc::ptr_eq(&event, &self.event)); + event.notify(1); + } + } +} + +#[cfg(test)] +mod tests { + use alloc::sync::Arc; + use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, + }; + + use super::Monitor; + + #[tokio::test] + async fn monitor_is_reentrant_and_excludes_other_threads() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + monitor.enter(1).await; + + let entered = Arc::new(AtomicBool::new(false)); + let contender = { + let monitor = monitor.clone(); + let entered = entered.clone(); + tokio::spawn(async move { + monitor.enter(2).await; + entered.store(true, Ordering::SeqCst); + monitor.exit(2).unwrap(); + }) + }; + + tokio::time::sleep(Duration::from_millis(10)).await; + assert!(!entered.load(Ordering::SeqCst)); + monitor.exit(1).unwrap(); + tokio::time::sleep(Duration::from_millis(10)).await; + assert!(!entered.load(Ordering::SeqCst)); + monitor.exit(1).unwrap(); + + tokio::time::timeout(Duration::from_secs(1), contender).await.unwrap().unwrap(); + assert!(entered.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn wait_releases_and_restores_the_full_reentrancy_depth() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + monitor.enter(1).await; + let (wait, _) = monitor.prepare_wait(1).unwrap(); + + monitor.enter(2).await; + monitor.notify(2, 1).unwrap(); + monitor.exit(2).unwrap(); + wait.wait().await; + + monitor.exit(1).unwrap(); + let entered = Arc::new(AtomicBool::new(false)); + let contender = { + let monitor = monitor.clone(); + let entered = entered.clone(); + tokio::spawn(async move { + monitor.enter(3).await; + entered.store(true, Ordering::SeqCst); + monitor.exit(3).unwrap(); + }) + }; + tokio::time::sleep(Duration::from_millis(10)).await; + assert!(!entered.load(Ordering::SeqCst)); + + monitor.exit(1).unwrap(); + tokio::time::timeout(Duration::from_secs(1), contender).await.unwrap().unwrap(); + assert!(entered.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn notify_one_and_notify_all_remove_the_expected_waiters() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + let (first_wait, _) = monitor.prepare_wait(1).unwrap(); + monitor.enter(2).await; + let (second_wait, _) = monitor.prepare_wait(2).unwrap(); + + monitor.enter(3).await; + monitor.notify(3, 1).unwrap(); + assert_eq!(monitor.state.lock().waiters.len(), 1); + monitor.exit(3).unwrap(); + first_wait.wait().await; + monitor.exit(1).unwrap(); + + monitor.enter(3).await; + monitor.notify(3, usize::MAX).unwrap(); + assert!(monitor.state.lock().waiters.is_empty()); + monitor.exit(3).unwrap(); + second_wait.wait().await; + monitor.exit(2).unwrap(); + } + + #[tokio::test] + async fn a_stale_timeout_cannot_consume_a_later_notification() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + let (first_wait, first_timeout) = monitor.prepare_wait(1).unwrap(); + first_timeout.clone().notify(); + first_wait.wait().await; + monitor.exit(1).unwrap(); + + monitor.enter(2).await; + let (second_wait, _) = monitor.prepare_wait(2).unwrap(); + first_timeout.notify(); + assert_eq!(monitor.state.lock().waiters.len(), 1); + + monitor.enter(3).await; + monitor.notify(3, 1).unwrap(); + monitor.exit(3).unwrap(); + second_wait.wait().await; + monitor.exit(2).unwrap(); + } +} diff --git a/jvm/src/type.rs b/jvm/src/type.rs index 5478529c..50c9fe10 100644 --- a/jvm/src/type.rs +++ b/jvm/src/type.rs @@ -8,7 +8,7 @@ use nom::{ IResult, Parser, bytes::complete::{take, take_until}, character::complete::anychar, - multi::many0, + error::{Error, ErrorKind}, sequence::terminated, }; @@ -49,7 +49,12 @@ impl JavaType { } pub fn parse(descriptor: &str) -> Self { - Self::parse_type(descriptor).unwrap().1 + Self::try_parse(descriptor).expect("invalid Java type descriptor") + } + + pub fn try_parse(descriptor: &str) -> Option { + let (remaining, r#type) = Self::parse_type(descriptor).ok()?; + if remaining.is_empty() { Some(r#type) } else { None } } // a CONSTANT_Class_info name (JVMS 4.4.1): a class binary name in internal form (java/lang/String) @@ -85,21 +90,39 @@ impl JavaType { 'D' => Ok((remaining, Self::Double)), 'L' => { let (remaining, class_name) = terminated(take_until(";"), take(1usize)).parse(remaining)?; + if class_name.is_empty() || class_name.contains(['.', '[', ';']) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } Ok((remaining, Self::Class(class_name.to_string()))) } '[' => { let (remaining, element_type) = Self::parse_type(remaining)?; + if matches!(element_type, Self::Void | Self::Method(_, _)) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } Ok((remaining, Self::Array(Box::new(element_type)))) } '(' => { let (remaining, params) = terminated(take_until(")"), take(1usize)).parse(remaining)?; - let param_types = many0(Self::parse_type).parse(params)?.1; + let mut param_types = Vec::new(); + let mut params = params; + while !params.is_empty() { + let (remaining_params, param_type) = Self::parse_type(params)?; + if remaining_params.len() >= params.len() || matches!(param_type, Self::Void | Self::Method(_, _)) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } + param_types.push(param_type); + params = remaining_params; + } let (remaining, return_type) = Self::parse_type(remaining)?; + if matches!(return_type, Self::Method(_, _)) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } Ok((remaining, Self::Method(param_types, Box::new(return_type)))) } - _ => panic!("Invalid type descriptor: {}", descriptor), + _ => Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))), } } } @@ -140,4 +163,14 @@ mod test { == JavaType::Array(Box::new(JavaType::Array(Box::new(JavaType::Class("java/lang/String".into()))))) ); } + + #[test] + fn test_try_parse_rejects_malformed_descriptors() { + assert!(JavaType::try_parse("").is_none()); + assert!(JavaType::try_parse("Igarbage").is_none()); + assert!(JavaType::try_parse("[V").is_none()); + assert!(JavaType::try_parse("(V)V").is_none()); + assert!(JavaType::try_parse("(I").is_none()); + assert!(JavaType::try_parse("L;").is_none()); + } } diff --git a/jvm_rust/src/array_class_instance.rs b/jvm_rust/src/array_class_instance.rs index eee85991..52bd8fd5 100644 --- a/jvm_rust/src/array_class_instance.rs +++ b/jvm_rust/src/array_class_instance.rs @@ -10,6 +10,7 @@ use jvm::{ArrayClassDefinition, ArrayClassInstance, ArrayRawBuffer, ArrayRawBuff use crate::array_class_definition::ArrayClassDefinitionImpl; +#[derive(Clone)] enum ArrayElements { Primitive(Vec), NonPrimitive(Vec), @@ -112,6 +113,21 @@ impl ArrayClassInstanceImpl { #[async_trait::async_trait] impl ArrayClassInstance for ArrayClassInstanceImpl { + fn identity(&self) -> usize { + Arc::as_ptr(&self.inner) as usize + } + + fn shallow_clone(&self) -> Result> { + Ok(Box::new(Self { + inner: Arc::new(ArrayClassInstanceInner { + class: self.inner.class.clone(), + length: self.inner.length, + element_type: self.inner.element_type.clone(), + elements: RwLock::new(self.inner.elements.read().clone()), + }), + })) + } + fn class_definition(&self) -> Box { self.inner.class.clone() } diff --git a/jvm_rust/src/class_definition.rs b/jvm_rust/src/class_definition.rs index aafed8e4..0e411238 100644 --- a/jvm_rust/src/class_definition.rs +++ b/jvm_rust/src/class_definition.rs @@ -12,12 +12,12 @@ use core::{ use parking_lot::RwLock; -use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference, ParseError}; +use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference}; use java_class_proto::JavaClassProto; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; use jvm::{ClassDefinition, ClassInstance, Field, JavaType, JavaValue, Jvm, Method, Result}; -use crate::{class_instance::ClassInstanceImpl, field::FieldImpl, method::MethodImpl}; +use crate::{ClassDefinitionError, class_instance::ClassInstanceImpl, field::FieldImpl, method::MethodImpl, verifier}; struct ClassDefinitionInner { name: String, @@ -96,8 +96,9 @@ impl ClassDefinitionImpl { ) } - pub fn from_classfile(data: &[u8]) -> core::result::Result { + pub fn from_classfile(data: &[u8]) -> core::result::Result { let class = ClassInfo::parse(data)?; + verifier::verify(&class)?; let mut constant_values = Vec::new(); let fields = class diff --git a/jvm_rust/src/class_instance.rs b/jvm_rust/src/class_instance.rs index 88e7eacd..d832142c 100644 --- a/jvm_rust/src/class_instance.rs +++ b/jvm_rust/src/class_instance.rs @@ -35,6 +35,19 @@ impl ClassInstanceImpl { impl ClassInstance for ClassInstanceImpl { fn destroy(self: Box) {} + fn identity(&self) -> usize { + Arc::as_ptr(&self.inner) as usize + } + + fn shallow_clone(&self) -> Result> { + Ok(Box::new(Self { + inner: Arc::new(ClassInstanceInner { + class: self.inner.class.clone(), + storage: RwLock::new(self.inner.storage.read().clone()), + }), + })) + } + fn class_definition(&self) -> Box { self.inner.class.clone() } diff --git a/jvm_rust/src/error.rs b/jvm_rust/src/error.rs new file mode 100644 index 00000000..b0e9996e --- /dev/null +++ b/jvm_rust/src/error.rs @@ -0,0 +1,14 @@ +use classfile::ClassFileError; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ClassDefinitionError { + ClassFile(ClassFileError), + Verification, + UnsupportedFeature(&'static str), +} + +impl From for ClassDefinitionError { + fn from(error: ClassFileError) -> Self { + Self::ClassFile(error) + } +} diff --git a/jvm_rust/src/interpreter.rs b/jvm_rust/src/interpreter.rs index 647398bd..6e17f869 100644 --- a/jvm_rust/src/interpreter.rs +++ b/jvm_rust/src/interpreter.rs @@ -858,12 +858,18 @@ impl Interpreter { return Ok(ExecuteNext::Jump((current_offset as i32 + *default) as u32)); } Opcode::Monitorenter => { - let stack_value = stack_frame.operand_stack.pop().unwrap(); - tracing::warn!("Unimplemented monitorenter{stack_value:?}"); + let object: Option> = stack_frame.operand_stack.pop().unwrap().into(); + let Some(object) = object else { + return Err(jvm.exception("java/lang/NullPointerException", "monitorenter on null").await); + }; + jvm.monitor_enter(&object).await?; } Opcode::Monitorexit => { - let stack_value = stack_frame.operand_stack.pop().unwrap(); - tracing::warn!("Unimplemented monitorexit{stack_value:?}"); + let object: Option> = stack_frame.operand_stack.pop().unwrap().into(); + let Some(object) = object else { + return Err(jvm.exception("java/lang/NullPointerException", "monitorexit on null").await); + }; + jvm.monitor_exit(&object).await?; } Opcode::Multianewarray(x, d) => { let mut dimensions: Vec = (0..*d).map(|_| stack_frame.operand_stack.pop().unwrap().into()).collect(); diff --git a/jvm_rust/src/lib.rs b/jvm_rust/src/lib.rs index 3c4c20f2..7fab6b66 100644 --- a/jvm_rust/src/lib.rs +++ b/jvm_rust/src/lib.rs @@ -5,14 +5,17 @@ mod array_class_definition; mod array_class_instance; mod class_definition; mod class_instance; +mod error; mod field; mod interpreter; mod method; mod stack_frame; +mod verifier; pub use self::{ array_class_definition::ArrayClassDefinitionImpl, class_definition::ClassDefinitionImpl, + error::ClassDefinitionError, field::FieldImpl, method::{MethodBody, MethodImpl}, }; diff --git a/jvm_rust/src/method.rs b/jvm_rust/src/method.rs index 7b2694a3..96a3ce98 100644 --- a/jvm_rust/src/method.rs +++ b/jvm_rust/src/method.rs @@ -133,7 +133,14 @@ impl Method for MethodImpl { } async fn run(&self, jvm: &Jvm, args: Box<[JavaValue]>) -> Result { - Ok(match &self.inner.body.as_ref().unwrap() { + let Some(body) = self.inner.body.as_ref() else { + if self.inner.access_flags.contains(MethodAccessFlags::NATIVE) { + return Err(jvm.exception("java/lang/UnsatisfiedLinkError", &self.inner.name).await); + } + return Err(jvm.exception("java/lang/AbstractMethodError", &self.inner.name).await); + }; + + Ok(match body { MethodBody::ByteCode(x) => { let r#type = JavaType::parse(&self.inner.descriptor); Interpreter::run(jvm, x, args, r#type.as_method().1).await? diff --git a/jvm_rust/src/verifier.rs b/jvm_rust/src/verifier.rs new file mode 100644 index 00000000..01509595 --- /dev/null +++ b/jvm_rust/src/verifier.rs @@ -0,0 +1,64 @@ +use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference, Opcode}; +use jvm::JavaType; + +use crate::ClassDefinitionError; + +pub(crate) fn verify(class: &ClassInfo) -> Result<(), ClassDefinitionError> { + for method in &class.methods { + for attribute in &method.attributes { + let AttributeInfo::Code(code) = attribute else { + continue; + }; + for opcode in code.code.values() { + match opcode { + Opcode::Multianewarray(ConstantPoolReference::Class(name), dimensions) => { + let Some(mut r#type) = JavaType::try_parse(name) else { + return Err(ClassDefinitionError::Verification); + }; + let mut available_dimensions = 0; + while let JavaType::Array(element) = r#type { + available_dimensions += 1; + r#type = *element; + } + if available_dimensions < *dimensions as usize { + return Err(ClassDefinitionError::Verification); + } + } + Opcode::Invokedynamic(_) => return Err(ClassDefinitionError::UnsupportedFeature("invokedynamic")), + _ => {} + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use classfile::{AttributeInfo, ClassInfo, Opcode}; + + use crate::{ClassDefinitionError, verifier::verify}; + + #[test] + fn rejects_multianewarray_dimensions_larger_than_the_array_type() { + let mut class = ClassInfo::parse(include_bytes!("../../test_data/MultiArray.class")).unwrap(); + let mut changed = false; + for method in &mut class.methods { + for attribute in &mut method.attributes { + let AttributeInfo::Code(code) = attribute else { + continue; + }; + for opcode in code.code.values_mut() { + if let Opcode::Multianewarray(_, dimensions) = opcode { + *dimensions = u8::MAX; + changed = true; + } + } + } + } + + assert!(changed); + assert_eq!(verify(&class), Err(ClassDefinitionError::Verification)); + } +} diff --git a/src/runtime.rs b/src/runtime.rs index c71d1b4e..79e45d39 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,6 +1,6 @@ mod io; -use alloc::{collections::BTreeMap, sync::Arc}; +use alloc::{collections::BTreeMap, format, sync::Arc}; use core::{ sync::atomic::{AtomicU32, AtomicU64, Ordering}, time::Duration, @@ -12,9 +12,10 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use classfile::ClassFileError; use java_runtime::{File, FileDescriptorId, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_runtime_class_proto}; use jvm::{ClassDefinition, Jvm}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl}; use self::io::{FileImpl, InputStreamFile, WriteStreamFile}; @@ -101,14 +102,20 @@ where tokio::spawn(async move { TASK_ID .scope(task_id, async move { - callback.call().await.unwrap(); + if let Err(error) = callback.call().await { + tracing::error!(?error, "spawned Java task failed"); + } }) .await; }); } + fn exit(&self, status: i32) { + std::process::exit(status); + } + fn now(&self) -> u64 { - SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_millis() as u64 + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO).as_millis() as u64 } fn current_task_id(&self) -> u64 { @@ -177,8 +184,23 @@ where async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { match ClassDefinitionImpl::from_classfile(data) { - Ok(class) => Ok(Box::new(class) as Box<_>), - Err(err) => Err(jvm.exception("java/lang/ClassFormatError", &err.to_string()).await), + Ok(class) => Ok(Box::new(class)), + Err(ClassDefinitionError::ClassFile(ClassFileError::InvalidFormat)) => { + Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await) + } + Err(ClassDefinitionError::ClassFile(ClassFileError::UnsupportedVersion(version))) => Err(jvm + .exception( + "java/lang/UnsupportedClassVersionError", + &format!("Unsupported class file version {version}"), + ) + .await), + Err(ClassDefinitionError::Verification) => Err(jvm.exception("java/lang/VerifyError", "Bytecode verification failed").await), + Err(ClassDefinitionError::UnsupportedFeature(feature)) => Err(jvm + .exception( + "java/lang/UnsupportedOperationException", + &format!("Unsupported class file feature: {feature}"), + ) + .await), } } diff --git a/test_data/Array.class b/test_data/Array.class index 288ed32a..6eb383b7 100644 Binary files a/test_data/Array.class and b/test_data/Array.class differ diff --git a/test_data/Array.txt b/test_data/Array.txt index b3e7d36f..b7a9bf71 100644 --- a/test_data/Array.txt +++ b/test_data/Array.txt @@ -12,3 +12,5 @@ test한글 10 10 10 +112344 +654321 diff --git a/test_data/ConcurrentClinit$Failing.class b/test_data/ConcurrentClinit$Failing.class new file mode 100644 index 00000000..795add5f Binary files /dev/null and b/test_data/ConcurrentClinit$Failing.class differ diff --git a/test_data/ConcurrentClinit$FailingReader.class b/test_data/ConcurrentClinit$FailingReader.class new file mode 100644 index 00000000..592b38ee Binary files /dev/null and b/test_data/ConcurrentClinit$FailingReader.class differ diff --git a/test_data/ConcurrentClinit$Successful.class b/test_data/ConcurrentClinit$Successful.class new file mode 100644 index 00000000..7a13368d Binary files /dev/null and b/test_data/ConcurrentClinit$Successful.class differ diff --git a/test_data/ConcurrentClinit$SuccessfulReader.class b/test_data/ConcurrentClinit$SuccessfulReader.class new file mode 100644 index 00000000..2d1d174a Binary files /dev/null and b/test_data/ConcurrentClinit$SuccessfulReader.class differ diff --git a/test_data/ConcurrentClinit.class b/test_data/ConcurrentClinit.class new file mode 100644 index 00000000..03eb810f Binary files /dev/null and b/test_data/ConcurrentClinit.class differ diff --git a/test_data/ConcurrentClinit.txt b/test_data/ConcurrentClinit.txt new file mode 100644 index 00000000..3bd25ba7 --- /dev/null +++ b/test_data/ConcurrentClinit.txt @@ -0,0 +1,6 @@ +42 +42 +1 +java.lang.ExceptionInInitializerError +java.lang.NoClassDefFoundError +java.lang.NoClassDefFoundError diff --git a/test_data/MonitorSemantics$1.class b/test_data/MonitorSemantics$1.class new file mode 100644 index 00000000..f3746ff4 Binary files /dev/null and b/test_data/MonitorSemantics$1.class differ diff --git a/test_data/MonitorSemantics$CounterWorker.class b/test_data/MonitorSemantics$CounterWorker.class new file mode 100644 index 00000000..e9702cd0 Binary files /dev/null and b/test_data/MonitorSemantics$CounterWorker.class differ diff --git a/test_data/MonitorSemantics$FailingWorker.class b/test_data/MonitorSemantics$FailingWorker.class new file mode 100644 index 00000000..c39937f7 Binary files /dev/null and b/test_data/MonitorSemantics$FailingWorker.class differ diff --git a/test_data/MonitorSemantics.class b/test_data/MonitorSemantics.class new file mode 100644 index 00000000..ea2b76b7 Binary files /dev/null and b/test_data/MonitorSemantics.class differ diff --git a/test_data/MonitorSemantics.txt b/test_data/MonitorSemantics.txt new file mode 100644 index 00000000..bc6298e8 --- /dev/null +++ b/test_data/MonitorSemantics.txt @@ -0,0 +1 @@ +110 diff --git a/test_data/NativeMethod.class b/test_data/NativeMethod.class new file mode 100644 index 00000000..2798b674 Binary files /dev/null and b/test_data/NativeMethod.class differ diff --git a/test_data/NativeMethod.txt b/test_data/NativeMethod.txt new file mode 100644 index 00000000..ad7ccf7a --- /dev/null +++ b/test_data/NativeMethod.txt @@ -0,0 +1 @@ +unsupported diff --git a/test_data/src/Array.java b/test_data/src/Array.java index 1c26b090..8038ff84 100644 --- a/test_data/src/Array.java +++ b/test_data/src/Array.java @@ -31,5 +31,10 @@ public static void main(String[] args) { System.out.println(char_array.length); System.out.println(boolean_array.length); System.out.println(string_array.length); + + int[] cloned_int_array = (int[]) int_array.clone(); + cloned_int_array[0] = 654321; + System.out.println(int_array[0]); + System.out.println(cloned_int_array[0]); } } diff --git a/test_data/src/ConcurrentClinit.java b/test_data/src/ConcurrentClinit.java new file mode 100644 index 00000000..e283a6ca --- /dev/null +++ b/test_data/src/ConcurrentClinit.java @@ -0,0 +1,120 @@ +public class ConcurrentClinit { + private static volatile boolean successfulOwnerStarted; + private static volatile boolean successfulWaiterStarted; + private static volatile boolean failingOwnerStarted; + private static volatile boolean failingWaiterStarted; + + private static class Successful { + static int initializationCount; + static int value; + + static { + initializationCount++; + successfulOwnerStarted = true; + while (!successfulWaiterStarted) { + Thread.yield(); + } + try { + Thread.sleep(10); + } catch (InterruptedException exception) { + throw new RuntimeException("interrupted"); + } + value = 42; + } + } + + private static class Failing { + static int value; + + static { + failingOwnerStarted = true; + while (!failingWaiterStarted) { + Thread.yield(); + } + try { + Thread.sleep(10); + } catch (InterruptedException exception) { + throw new RuntimeException("interrupted"); + } + if (failingOwnerStarted) { + throw new RuntimeException("initialization failed"); + } + } + } + + private static class SuccessfulReader implements Runnable { + private final boolean waiter; + int value; + + SuccessfulReader(boolean waiter) { + this.waiter = waiter; + } + + public void run() { + if (waiter) { + successfulWaiterStarted = true; + } + value = Successful.value; + } + } + + private static class FailingReader implements Runnable { + private final boolean waiter; + String errorClass; + + FailingReader(boolean waiter) { + this.waiter = waiter; + } + + public void run() { + if (waiter) { + failingWaiterStarted = true; + } + try { + int ignored = Failing.value; + } catch (Throwable throwable) { + errorClass = throwable.getClass().getName(); + } + } + } + + public static void main(String[] args) throws Exception { + SuccessfulReader successfulOwner = new SuccessfulReader(false); + Thread successfulOwnerThread = new Thread(successfulOwner); + successfulOwnerThread.start(); + while (!successfulOwnerStarted) { + Thread.yield(); + } + + SuccessfulReader successfulWaiter = new SuccessfulReader(true); + Thread successfulWaiterThread = new Thread(successfulWaiter); + successfulWaiterThread.start(); + successfulOwnerThread.join(); + successfulWaiterThread.join(); + + System.out.println(successfulOwner.value); + System.out.println(successfulWaiter.value); + System.out.println(Successful.initializationCount); + + FailingReader failingOwner = new FailingReader(false); + Thread failingOwnerThread = new Thread(failingOwner); + failingOwnerThread.start(); + while (!failingOwnerStarted) { + Thread.yield(); + } + + FailingReader failingWaiter = new FailingReader(true); + Thread failingWaiterThread = new Thread(failingWaiter); + failingWaiterThread.start(); + failingOwnerThread.join(); + failingWaiterThread.join(); + + System.out.println(failingOwner.errorClass); + System.out.println(failingWaiter.errorClass); + try { + int ignored = Failing.value; + } catch (Throwable throwable) { + System.out.println(throwable.getClass().getName()); + } + } +} diff --git a/test_data/src/MonitorSemantics.java b/test_data/src/MonitorSemantics.java new file mode 100644 index 00000000..9dc5451d --- /dev/null +++ b/test_data/src/MonitorSemantics.java @@ -0,0 +1,49 @@ +public class MonitorSemantics { + private static final Object LOCK = new Object(); + private static int counter; + + private static class CounterWorker implements Runnable { + public void run() { + for (int i = 0; i < 50; i++) { + synchronized (LOCK) { + int current = counter; + Thread.yield(); + counter = current + 1; + } + } + } + } + + private static class FailingWorker implements Runnable { + public void run() { + try { + failWhileSynchronized(); + } catch (RuntimeException expected) { + } + } + } + + private static synchronized void failWhileSynchronized() { + throw new RuntimeException("expected"); + } + + private static synchronized void addTen() { + counter += 10; + } + + public static void main(String[] args) throws Exception { + Thread first = new Thread(new CounterWorker()); + Thread second = new Thread(new CounterWorker()); + first.start(); + second.start(); + first.join(); + second.join(); + + Thread failing = new Thread(new FailingWorker()); + failing.start(); + failing.join(); + addTen(); + + System.out.println(counter); + } +} diff --git a/test_data/src/NativeMethod.java b/test_data/src/NativeMethod.java new file mode 100644 index 00000000..335a8d1d --- /dev/null +++ b/test_data/src/NativeMethod.java @@ -0,0 +1,11 @@ +public class NativeMethod { + private native void missing(); + + public static void main(String[] args) { + try { + new NativeMethod().missing(); + } catch (UnsatisfiedLinkError expected) { + System.out.println("unsupported"); + } + } +} diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml index 7e798257..76b5cc74 100644 --- a/test_utils/Cargo.toml +++ b/test_utils/Cargo.toml @@ -6,8 +6,10 @@ license.workspace = true [dependencies] async-trait = { workspace = true } +tracing = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "time"] } +classfile = { workspace = true } jvm = { workspace = true } jvm_rust = { workspace = true } java_runtime = { workspace = true } diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 9bba4dd8..7656aa89 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -1,9 +1,9 @@ extern crate alloc; -use alloc::{boxed::Box, collections::BTreeMap, string::String, sync::Arc, vec::Vec}; +use alloc::{boxed::Box, collections::BTreeMap, format, string::String, sync::Arc, vec::Vec}; use core::{ cmp::min, - sync::atomic::{AtomicU32, AtomicU64, Ordering}, + sync::atomic::{AtomicI64, AtomicU32, AtomicU64, Ordering}, time::Duration, }; use std::{ @@ -11,8 +11,9 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use classfile::ClassFileError; use jvm::{ClassDefinition, Jvm, Result}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl}; use java_runtime::{ File, FileDescriptorId, FileSize, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_bootstrap_class_loader, @@ -23,6 +24,7 @@ pub struct TestRuntime { filesystem: BTreeMap>, file_table: Arc>>>, next_fd: Arc, + exit_status: Arc, } impl Clone for TestRuntime { @@ -31,6 +33,7 @@ impl Clone for TestRuntime { filesystem: self.filesystem.clone(), file_table: self.file_table.clone(), next_fd: self.next_fd.clone(), + exit_status: self.exit_status.clone(), } } } @@ -41,9 +44,15 @@ impl TestRuntime { filesystem, file_table: Arc::new(Mutex::new(BTreeMap::new())), next_fd: Arc::new(AtomicU32::new(1)), + exit_status: Arc::new(AtomicI64::new(i64::MIN)), } } + pub fn exit_status(&self) -> Option { + let status = self.exit_status.load(Ordering::SeqCst); + (status != i64::MIN).then_some(status as i32) + } + fn register_file(&self, file: Box) -> FileDescriptorId { let fd = self.next_fd.fetch_add(1, Ordering::SeqCst); self.file_table.lock().unwrap().insert(fd, file); @@ -72,12 +81,18 @@ impl Runtime for TestRuntime { tokio::spawn(async move { TASK_ID .scope(task_id, async move { - callback.call().await.unwrap(); + if let Err(error) = callback.call().await { + tracing::error!(?error, "spawned Java test task failed"); + } }) .await; }); } + fn exit(&self, status: i32) { + self.exit_status.store(status as i64, Ordering::SeqCst); + } + fn now(&self) -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_millis() as u64 } @@ -148,8 +163,23 @@ impl Runtime for TestRuntime { async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { match ClassDefinitionImpl::from_classfile(data) { - Ok(class) => Ok(Box::new(class) as Box<_>), - Err(err) => Err(jvm.exception("java/lang/ClassFormatError", &err.to_string()).await), + Ok(class) => Ok(Box::new(class)), + Err(ClassDefinitionError::ClassFile(ClassFileError::InvalidFormat)) => { + Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await) + } + Err(ClassDefinitionError::ClassFile(ClassFileError::UnsupportedVersion(version))) => Err(jvm + .exception( + "java/lang/UnsupportedClassVersionError", + &format!("Unsupported class file version {version}"), + ) + .await), + Err(ClassDefinitionError::Verification) => Err(jvm.exception("java/lang/VerifyError", "Bytecode verification failed").await), + Err(ClassDefinitionError::UnsupportedFeature(feature)) => Err(jvm + .exception( + "java/lang/UnsupportedOperationException", + &format!("Unsupported class file feature: {feature}"), + ) + .await), } } diff --git a/tests/test_class_format.rs b/tests/test_class_format.rs index 27d3e6c0..c05e749e 100644 --- a/tests/test_class_format.rs +++ b/tests/test_class_format.rs @@ -28,13 +28,15 @@ fn hello_class() -> Vec { fs::read("test_data/Hello.class").unwrap() } +// Only the exception *kind* is asserted, not the message: upstream `ClassFileError` +// (cut 822504b) collapses every parse failure into a flat "Invalid class file", +// so per-cause wording is no longer available. Restoring it needs upstream variants. #[tokio::test] async fn test_truncated_class_raises_class_format_error() { let (dir, path) = fixture("TruncatedHello.class", &hello_class()[..60]); let err = run_class(&path, &[dir.as_path()], &[]).await.unwrap_err().to_string(); assert!(err.contains("java.lang.ClassFormatError"), "expected ClassFormatError, got: {err}"); - assert!(err.contains("Truncated"), "expected truncation cause in message, got: {err}"); } #[tokio::test] @@ -47,7 +49,6 @@ async fn test_unsupported_constant_pool_tag_raises_class_format_error() { let err = run_class(&path, &[dir.as_path()], &[]).await.unwrap_err().to_string(); assert!(err.contains("java.lang.ClassFormatError"), "expected ClassFormatError, got: {err}"); - assert!(err.contains("tag 18"), "expected offending tag in message, got: {err}"); } #[tokio::test] @@ -58,7 +59,6 @@ async fn test_bad_magic_raises_class_format_error() { let err = run_class(&path, &[dir.as_path()], &[]).await.unwrap_err().to_string(); assert!(err.contains("java.lang.ClassFormatError"), "expected ClassFormatError, got: {err}"); - assert!(err.contains("magic"), "expected magic mismatch cause in message, got: {err}"); } #[tokio::test]