From 068fdb9517cba513ee7146be08af8acc33b07051 Mon Sep 17 00:00:00 2001 From: jun0 Date: Fri, 11 Sep 2026 20:17:28 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[rustjava-null-guard-audit-remaining-runtim?= =?UTF-8?q?e-entrypoints]=20fix:=20null=20=EA=B0=80=EB=93=9C=20=EA=B0=90?= =?UTF-8?q?=EC=82=AC=20=ED=9B=84=20java/io=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=A0=84=EC=86=A1=20=EB=B2=84=ED=8D=BC=2018=EA=B3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 감사: N 1,163 → M 58 → K 46(술어는 worklog 표가 정본 · sink 17개 한정). ★계측기를 선행 9곳으로 먼저 검증했고 초판 off-by-one 을 그때 잡았다(3/9 → 8/9). ★K 는 하한이다 — 절차간 deref 를 못 본다. 닫은 범위 = java/io read/write 버퍼 + String.getChars 18곳(JDK 가 NPE 를 명시하는 자리). 잔여 20건은 일괄 가드가 «틀리는» 자리를 포함해(URL 의 null handler 는 합법) 후속으로 남겼다. Co-Authored-By: Claude Fable 5 --- REPORT.md | 17 ++++ STATE.md | 16 +++ ...null-guard-audit-and-io-buffer-guards.json | 49 +++++++++ ...1-null-guard-audit-and-io-buffer-guards.md | 87 ++++++++++++++++ .../classes/java/io/buffered_output_stream.rs | 4 + .../src/classes/java/io/data_input_stream.rs | 4 + .../src/classes/java/io/data_output_stream.rs | 12 +++ .../src/classes/java/io/file_input_stream.rs | 4 + .../src/classes/java/io/file_output_stream.rs | 4 + .../classes/java/io/input_stream_reader.rs | 4 + .../classes/java/io/output_stream_writer.rs | 4 + .../src/classes/java/io/random_access_file.rs | 16 +++ .../src/classes/java/io/reader.rs | 4 + .../src/classes/java/io/writer.rs | 12 +++ .../src/classes/java/lang/string.rs | 4 + test-data/NullBufferGuards.class | Bin 0 -> 2412 bytes test-data/NullBufferGuards.txt | 20 ++++ test-data/src/NullBufferGuards.java | 96 ++++++++++++++++++ 18 files changed, 357 insertions(+) create mode 100644 docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json create mode 100644 docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md create mode 100644 test-data/NullBufferGuards.class create mode 100644 test-data/NullBufferGuards.txt create mode 100644 test-data/src/NullBufferGuards.java diff --git a/REPORT.md b/REPORT.md index 36a8d3ff..672381c4 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,5 +1,22 @@ # REPORT +## [2026-09-11] null 가드 «감사» — 세고, 그중 데이터 전송 버퍼 18곳을 닫았다 (rustjava-null-guard-audit-remaining-runtime-entrypoints) +- 무엇을: 런타임 전체에서 `ClassInstanceRef` 인자를 받아 **곧바로 역참조하는** 진입점을 **셌고**(N 1,163 → M 58 → K 46), + 그중 **`java/io` 데이터 전송 버퍼 + `String.getChars` 18곳**에 선행 회차와 **같은 모양**의 진입부 `is_null()` 가드를 넣었다. + 회귀 잠금 = 픽스처 `test-data/NullBufferGuards`(10케이스). 채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. +- 왜: 선행 회차가 닫은 것은 **손으로 열거한** 9곳이었고, ★그 열거 자신이 `arraycopy` 의 `dest` 를 빠뜨렸다. + ⇒ 이번엔 **열거가 아니라 술어로 셌다**. sink 는 `jvm.rs` 가 `&Box`/`impl AsClassInstance` 로 받는 **17개 메서드**다. +- 사용자 영향: 게스트가 스트림·Writer·`String.getChars` 에 **null 버퍼**를 넘겨도 에뮬레이터가 죽지 않고 `NullPointerException` 이 난다. + 정상 입력 동작 **불변**(가드 앞에서 패닉하던 입력만 예외로 바뀐다) · 예외 종류·메시지 형태는 선행 회차와 동일. +- 검증: ★**양방향** — 18곳 전건 되돌림 → `test_class` **FAILED**(`class_instance.rs:108`) · ★**단일 가드**(`get_chars(dst)`)만 빼도 **FAILED**(`:114` `DerefMut`) ↔ 복원 **ok**. + DoD 7종 rc=0 · `cargo test --all` **554/0/1**(계수 불변이 맞다). +- ★★**계측기를 먼저 검증했고, 초판이 틀렸다**: 선행 9곳을 정답지로 대니 **3/9** 만 잡혔다(근인 = `"\n"+src` 에 정규식을 돌리고 인덱스는 `src` 에 쓴 **off-by-one**). + 고쳐서 **8/9**. 남은 1건은 helper **안**에서 deref 하므로 ★**이 계측은 절차간을 못 보고, 따라서 K 는 «하한»이다**(검수자의 「899/560 상한」과 방향이 반대다). +- ★**잔여 20건을 «일괄 가드»로 닫지 마라** — `URL(context, spec, handler)` 는 JDK 규격상 **null handler 가 합법**이다. + ⇒ 후속의 본체는 「가드를 넣는 것」이 아니라 **「null 이 합법인지 규격으로 가르는 것」**이다. +- 후속 추천: `rustjava-null-guard-spec-triage-for-constructor-and-collection-params`(P3) — 잔여 20건을 **규격 기준으로 삼분** + (NPE 의무 / null 합법 / 도달 불가)하고 첫 갈래만 닫는다. + ## [2026-09-11] null 인자가 호스트를 죽이던 9경로에 가드 (rustjava-null-guard-string-init-and-arraycopy-p0) - 무엇을: `String.` 7개 오버로드와 `System.arraycopy`(`src`·`dest`)에 진입부 `is_null()` 가드를 넣어, null 을 넘겼을 때 **Rust 패닉(= 호스트 프로세스 abort)** 대신 **`NullPointerException`** 이 나게 했다. diff --git a/STATE.md b/STATE.md index 4a9beeae..c333001c 100644 --- a/STATE.md +++ b/STATE.md @@ -4,6 +4,22 @@ (없음 — 2026-09-11 실측: 진행 티켓 0 · 열린 PR 0. ※「열린 PR 0」은 ★**이 PR(#35) 착지 시점 기준**이다 — 회신 시점에는 #35 자신이 열려 있다) ## 완료 +- [rustjava-null-guard-audit-remaining-runtime-entrypoints] ★★**런타임 전체를 «세고»(감사) 그중 «데이터 전송 버퍼» 18곳에 가드를 넣었다.** + 채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. 픽스처 `test-data/NullBufferGuards` 10케이스. + ★★**감사 표(측정 2026-09-11 · 트리 `origin/main` `6da7d66f`)**: **N 1,163**(`this`·`_` 제외 `ClassInstanceRef` 인자) + → **M 58**(deref 강제 sink 에 미가드 도달) → **K 46**(그중 `as_proto` 등재 = 게스트 도달 가능) ⇒ 이 회차가 **18** 을 닫아 **K 28 → 실측 20**. + ★**판정 술어를 적어 둔다**(「보아하니」 금지): sink = `jvm/src/jvm.rs` 가 **`&Box`/`impl AsClassInstance`** 로 받는 **17개 메서드** + (`array_length`·`load_array`·`store_array`·`get_field`·`put_field`·`invoke_virtual`·`invoke_special`·`monitor_*` …) — 그 인자에 `&p`/`&mut p` 로 넘기면 **Deref/DerefMut 가 강제**된다. + ★★**계측기를 «먼저 검증»했다 — 그리고 초판이 틀렸다.** 선행 회차가 닫은 **9곳**을 정답지로 삼아 가드 전/후 트리에 같은 자를 댔는데 + 초판은 **3/9** 만 되찾았다(근인 = 정규식을 `"\n"+src` 에 돌리고 인덱스를 `src` 에 적용한 **off-by-one** — 괄호 스캔이 한 칸 밀렸다). + 고친 뒤 **8/9**. ★**남은 1건(`init_with_string`)은 `Self::value_range` «안»에서 deref 한다** ⇒ ★★**이 계측은 절차간(interprocedural)을 못 본다 — 그래서 K 는 «하한»이다.** + ★**검수자의 「899 중 560 · 상한값」과 방향이 반대다**: 그쪽은 fn 단위 크루드 스캔(상한), 이쪽은 sink 한정 + 절차내(하한). + ★★**세 번째 패닉 impl 을 찾았다** — `Deref`(:108)·`DerefMut`(:114) 외에 **`AsClassInstance::as_class_instance`**(`as_deref().unwrap()`)가 있다(monitor 계열이 그 경로다). + ★**범위를 «좁게» 골랐다 — 「전부 고쳐라」가 아니다**: `java/io` **데이터 전송**(read/write 버퍼) + `String.getChars` **18곳**. + ★★**생성자류를 «일부러» 뺐다 — 일괄 가드가 «틀리는» 자리가 실재한다**: `URL(context, spec, handler)` 는 JDK 규격상 **handler == null 이 «합법»**(기본 핸들러를 쓰라는 뜻)이다. + ⇒ ★**잔여 20건은 «가드를 넣을지»가 아니라 «null 이 합법인지»를 규격으로 먼저 가려야 한다** — 후속의 본체가 그것이다. + ★개악 대조 **양방향**: 18곳 전건 되돌림 → `test_class` **FAILED**(`:108`) · ★**단일 가드**(`get_chars(dst)`)만 제거해도 **FAILED**(`:114` `DerefMut`) ↔ 복원 **ok**. + ★`cargo test --all` **554 / 0 / 1**(증감 0 이 맞다 — `test_class` 는 픽스처를 한 함수가 순회한다) · DoD 7종 rc=0. - [rustjava-null-guard-string-init-and-arraycopy-p0] ★★**null 인자 8경로의 «호스트 abort» 를 `NullPointerException` 으로 바꿨다.** 채택 제안 `2026-09-11-s5-duplicate-issuance-stale-next-close#p0`. ★**가드 9곳**(`string.rs` 7 · `system.rs` 2) · 픽스처 `test-data/NullArgGuards` 9케이스. ★**제안은 「8경로」라 했는데 실측은 «9곳»이다** — `System.arraycopy` 가 `src`·`dest` **둘 다** 뚫려 있었고(제안·STATE ③-2 는 `src` 만 셌다) 그 둘은 다른 인자다. diff --git a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json new file mode 100644 index 00000000..571e3520 --- /dev/null +++ b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json @@ -0,0 +1,49 @@ +{ + "schema": 1, + "date": "2026-09-11", + "title": "null 가드 감사 — N 1163 / M 58 / K 46 을 세고 java/io 데이터 전송 버퍼 18곳을 닫았다", + "services": ["RustJava"], + "taskId": "rustjava-null-guard-audit-remaining-runtime-entrypoints", + "summary": "선행 회차는 손으로 «열거한» 9곳을 닫았고 그 열거 자신이 arraycopy 의 dest 를 빠뜨렸다. 그래서 이번엔 술어로 셌다 — N 1,163(this·_ 제외 ClassInstanceRef 파라미터) → M 58(deref 강제 sink 에 미가드 도달) → K 46(as_proto 등재 = 게스트 도달 가능). sink 는 jvm/src/jvm.rs 가 &Box/impl AsClassInstance 로 받는 17개 메서드로 한정했다 — 초판은 「&p 가 나오면 deref」로 세어 과대계상했다(&ClassInstanceRef 를 받는 헬퍼는 deref 하지 않는다). ★계측기를 «먼저» 검증했고 초판이 틀렸다: 선행 9곳을 정답지로 대니 3/9 만 잡혔고 근인은 정규식을 \"\\n\"+src 에 돌리고 인덱스를 src 에 적용한 off-by-one 이었다(괄호 스캔이 한 칸 밀려 파라미터 문자열이 본문을 삼켰다). 고친 뒤 8/9. 남은 1건(init_with_string)은 helper 안에서 deref 하므로 ★이 계측은 절차간을 못 보고 따라서 K 는 «하한»이다 — 검수자가 선행 회차에 적은 「899 중 560 · 상한값」과 방향이 반대이므로 두 수를 같은 축으로 비교하지 마라. 이 회차가 닫은 범위는 java/io 데이터 전송 버퍼 + String.getChars 18곳이고, 잔여 20건은 «일괄 가드가 틀리는» 자리를 포함하므로(URL(context,spec,handler) 는 JDK 규격상 null handler 가 합법) 후속으로 남겼다.", + "changes": [ + "rustjava-runtime/src/classes/java/io/{buffered_output_stream,data_input_stream,data_output_stream,file_input_stream,file_output_stream,input_stream_reader,output_stream_writer,random_access_file,reader,writer}.rs — 진입부 is_null() 가드 17곳(삽입만 · 삭제 0)", + "rustjava-runtime/src/classes/java/lang/string.rs — get_chars(dst) 가드 1곳(삽입만 · 삭제 0)", + "test-data/src/NullBufferGuards.java + .class + test-data/NullBufferGuards.txt — 10케이스 회귀 픽스처", + "STATE.md · REPORT.md — 감사 표·술어·범위 근거·잔여 목록" + ], + "verification": [ + "정상 → green: cargo test --test test_class ok · cargo fmt --check rc=0 · clippy stable/beta/wasm32 rc=0 · cargo test --all 554 passed / 0 failed / 1 ignored · check-worklog-json.py rc=0 · check-dod-ci-parity.py rc=0", + "개악 → red ⑴: 18곳 전건 되돌림 → test_class FAILED, panicked at jvm/src/class_instance.rs:108 (Deref)", + "개악 → red ⑵: 단일 가드(String.getChars 의 dst)만 제거 → test_class FAILED, panicked at jvm/src/class_instance.rs:114 (DerefMut) ⇒ 픽스처가 케이스별로 문다", + "계측기 검증: 가드 전(eaad8e9c)/후 트리 차집합이 선행 9곳 중 8건과 일치 · 새로 생긴 것 0", + "픽스처 형식: javac --release 8 ⇒ major 52 · invokedynamic/CP 태그 15~18 0건" + ], + "issues": [ + "★픽스처 커버리지는 18 중 12 다 — FileInputStream.read · FileOutputStream.write · RandomAccessFile 4종은 실파일 핸들이 있어야 인스턴스가 생겨서 덮지 못했다(작업 디렉터리에 파일을 만드는 것을 피했다). 가드는 들어갔으나 픽스처가 잠그지 않는다 ⇒ 임시 파일 픽스처는 다음 회차 몫이다.", + "★이 계측은 절차간 deref 와 섀도잉을 못 본다. 전자는 위음성(init_with_string 을 놓쳤다), 후자는 위양성(url.rs 의 handler 는 섀도잉돼 무시되는데 잡혔다) ⇒ N·M·K 를 «정확한 수»가 아니라 «하한 + 알려진 오탐 1건»으로 읽어라.", + "★부수 발견 — 패닉 impl 은 둘이 아니라 셋이다: Deref(:108) · DerefMut(:114) · AsClassInstance::as_class_instance(as_deref().unwrap()). 셋째는 monitor_enter/monitor_exit/object_wait_prepare/object_notify 경로다.", + "감사 스크립트는 커밋하지 않았다 — CI 에 배선되지 않은 검사기는 낡는다(이 저장소의 기존 판정). 술어는 worklog .md 의 표가 정본이다." + ], + "proposals": [ + { + "title": "rustjava-null-guard-spec-triage-for-constructor-and-collection-params", + "plainSummary": "남은 20곳을 «가드를 넣을 자리»로 보지 말고, JDK 규격 기준으로 세 갈래(NPE 의무 / null 이 합법 / 애초에 도달 불가)로 가른 뒤 첫 갈래만 닫는다.", + "userBenefit": "null 이 합법인 API(예: URL 의 handler)에 잘못된 예외를 심지 않으면서, 진짜로 죽는 자리만 막는다.", + "why": "잔여 20건은 대부분 생성자·컬렉션 내부 파라미터라 «null 이 오면 NPE» 가 자명하지 않다. URL(context, spec, handler) 는 JDK 가 null handler 를 명시적으로 허용하므로 일괄 가드는 규격 위반이 된다. ⇒ 세는 것은 끝났고 다음은 «가르는 것»이다.", + "tradeoff": "규격 확인에 시간이 든다(자동화 불가 — Javadoc 판단). 그리고 삼분 결과 첫 갈래가 작으면 이 후속의 코드 산출이 거의 없을 수 있다 — 그래도 «가른 기록»이 남는 것이 값이다.", + "effort": "M(규격 삼분이 대부분 · 코드는 그 뒤)", + "target": "RustJava rustjava-runtime (java/net · java/util · java/util/zip · java/io 생성자류 20곳)" + }, + { + "title": "rustjava-null-guard-fixture-for-file-backed-io", + "plainSummary": "임시 파일을 쓰는 픽스처를 세워, 이번에 가드는 넣었으나 테스트가 잠그지 않는 6곳(FileInputStream·FileOutputStream·RandomAccessFile)을 회귀로 묶는다.", + "userBenefit": "그 6곳의 가드가 나중에 지워져도 CI 가 말한다 — 지금은 조용히 사라질 수 있다.", + "why": "이 회차의 픽스처는 18곳 중 12곳만 덮는다. 파일 핸들이 필요한 6곳은 인스턴스를 만들 수 없어 빠졌고, 그 사실이 이 워크로그에만 적혀 있다(기계는 모른다).", + "tradeoff": "테스트가 임시 파일을 만들고 지워야 한다 — 기존 픽스처 관례(순수 계산)에서 벗어나고, 실패 시 잔여 파일이 남을 수 있다.", + "effort": "S", + "target": "RustJava test-data (FileInputStream·FileOutputStream·RandomAccessFile 6곳)" + } + ], + "adoptedProposals": ["2026-09-11-null-guard-string-init-and-arraycopy#p0"], + "declinedProposals": [] +} diff --git a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md new file mode 100644 index 00000000..1d902e24 --- /dev/null +++ b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md @@ -0,0 +1,87 @@ +# 2026-09-11 — null 가드 감사(세기) + `java/io` 버퍼 18곳 가드 + +채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. + +선행 회차는 **손으로 열거한** 9곳을 닫았고, ★그 열거 자신이 `arraycopy` 의 `dest` 를 빠뜨렸다. +그래서 이번엔 **술어로 셌다**. + +## 감사 — 수와 술어 (측정 2026-09-11 · 트리 `origin/main` `6da7d66f`) + +| 축 | 값 | 술어 | +|---|---|---| +| **N** | **1,163** | `rustjava-runtime/src/**` 의 fn 파라미터 중 타입이 `ClassInstanceRef<…>` 인 것(★`this` 와 `_` 접두는 제외) | +| **M** | **58** | N 중, 그 파라미터가 **deref 강제 sink** 에 `&p`/`&mut p`(또는 `p.as_class_instance()`)로 도달하고 ★**그 «앞»에 `p.is_null()` 가드가 없는** 것 | +| **K** | **46** | M 중, 그 fn 이 같은 파일의 `JavaMethodProto::new(… Self::fn …)` 에 등재 = **게스트 바이트코드가 부를 수 있다** ⇒ null 이 «도달 가능» | + +**deref 강제 sink = 17개**(`jvm/src/jvm.rs` 의 시그니처로 확정 — `&Box` · `&mut Box<…>` · `impl AsClassInstance`): +`array_element_type` `array_length` `array_raw_buffer` `array_raw_buffer_mut` `get_field` `interrupt_java_thread` +`invoke_special` `invoke_virtual` `is_java_thread_interrupted` `load_array` `monitor_enter` `monitor_exit` +`object_notify` `object_wait_prepare` `put_field` `shallow_clone` `store_array` + +★**왜 sink 를 한정했는가**: 초판은 「`&p` 가 나오면 deref」로 셌는데 그것은 **과대계상**이다 — +`Self::value_range(jvm, &value)` 처럼 **`&ClassInstanceRef` 를 받는** 헬퍼는 deref 하지 «않는다». +한정 전 220 → 한정 후 152 → (계측기 버그 수정 후) **58**. + +## ★계측기를 «먼저» 검증했다 — 그리고 초판이 틀렸다 + +정답지 = 선행 회차가 닫은 **9곳**. 가드 «전»(`eaad8e9c`)·«후» 트리에 같은 자를 대고 차집합을 봤다. + +- **초판: 3/9** — 근인은 **off-by-one** 이다. 정규식을 `"\n" + src` 에 돌려 놓고 인덱스는 `src` 에 적용해 + 괄호 스캔이 **한 칸 밀렸고**, 시그니처의 `)` 에서 멈추지 못해 파라미터 문자열이 본문까지 삼켰다. +- **수정 후: 8/9** · 새로 생긴 것 **0**. +- ★**남은 1건 = `init_with_string(value)`** — 그 fn 은 `Self::value_range(jvm, &value)` **안에서** deref 한다. + ⇒ ★★**이 계측은 절차간(interprocedural)을 못 본다. 따라서 N·M·K 는 «하한»이다.** + ※검수자가 선행 회차에 적은 「899 중 560 · **상한값**」과 **방향이 반대**다 — 그쪽은 fn 단위 크루드 스캔이고 이쪽은 sink 한정 + 절차내다. **두 수를 같은 축으로 비교하지 마라.** + +## 부수 발견 — 패닉 impl 은 «둘»이 아니라 «셋»이다 + +`jvm/src/class_instance.rs` 에 `Deref`(`as_ref().unwrap()`) · `DerefMut`(`as_mut().unwrap()`) 말고 +★**`AsClassInstance::as_class_instance`(`as_deref().unwrap()`)** 가 있다 — `monitor_enter`/`monitor_exit`/`object_wait_prepare`/`object_notify` 가 그 경로다. + +## 이 회차가 «닫은» 선 — `java/io` 데이터 전송 + `String.getChars` (18곳) + +`BufferedOutputStream.write(byte[],int,int)` · `DataInputStream.readFully(byte[])` · +`DataOutputStream.{writeBytes,writeChars,writeUTF}(String)` · `FileInputStream.read(byte[])` · +`FileOutputStream.write(byte[],int,int)` · `InputStreamReader.read(char[])` · `OutputStreamWriter.write(char[])` · +`RandomAccessFile.{read(byte[]),read(byte[],int,int),write(byte[]),write(byte[],int,int)}` · +`Reader.read(char[])` · `Writer.{write(char[]),write(String),write(String,int,int)}` · `String.getChars(int,int,char[],int)` + +★**선의 근거**: 전부 **JDK 규격이 NPE 를 명시**하는 «데이터를 옮기는 버퍼»이고, 게스트가 가장 흔히 null 을 넘기는 자리다. +★**가드 형태는 선행 회차 그대로**(진입부 `is_null()` → `jvm.exception("java/lang/NullPointerException", " is null")`) — 새 형태를 만들지 않았다. + +## ★남긴 20건 — 「가드를 넣어라」가 «아니다» + +``` +java/io read_utf_from_input(input) · init(file)×2 · init_with_file(file) +java/net init(url) · init_with_context_spec_handler(handler) · set_url(url) · open_connection(url) +java/util init(list)×4 · on_access(map) · init(map) · init(pattern) · matches(pattern) · split_with_limit(input) +java/util/zip init_with_zip_entry(zip_entry) · get_input_stream(entry) · init(file) +``` +★★**일괄 가드는 «틀린다»** — `URL(URL context, String spec, URLStreamHandler handler)` 는 JDK 규격상 +**`handler == null` 이 합법**이다(기본 핸들러를 쓰라는 뜻). 실제로 이 repo 의 구현도 그 파라미터를 **섀도잉해 무시**한다 +(⇒ 그 1건은 이 계측의 **오탐**이기도 하다 — 이름 기반 절차내 스캔은 섀도잉을 못 본다). +⇒ **후속의 본체는 「규격으로 삼분하는 것」**(NPE 의무 / null 합법 / 도달 불가)이고, 첫 갈래만 닫는다. + +## 검증 — 양방향 + +- **정상 → green**: `cargo test --test test_class` **ok** · DoD 7종 rc=0 · `cargo test --all` **554/0/1** + (★계수 불변이 맞다 — `test_class` 는 `test-data/` 를 **한 테스트 함수**가 순회한다). +- **개악 → red**: ⑴18곳 **전건** 되돌림 → **FAILED** `class_instance.rs:108`(`Deref`) + ⑵★**단일 가드**(`String.getChars` 의 `dst`)만 제거 → **FAILED** `:114`(`DerefMut`) ⇒ 픽스처가 **케이스별로** 문다. + +## 픽스처 커버리지 — ★**18 중 12만 덮는다**(숨기지 않는다) + +`NullBufferGuards` 10케이스가 무는 것은 `BufferedOutputStream`·`DataInputStream`·`DataOutputStream`×3· +`InputStreamReader`·`OutputStreamWriter`·`Writer`×2·`String.getChars` 다. +★**덮지 «못한» 6곳** = `FileInputStream.read` · `FileOutputStream.write` · `RandomAccessFile` 4종 — +**실파일 핸들이 있어야 인스턴스가 생기고**, 테스트가 작업 디렉터리에 파일을 만드는 것을 피했다. +⇒ ★**그 6곳은 «가드는 들어갔으나 픽스처가 잠그지 않았다»** — 다음 회차가 임시 파일 픽스처를 세우면 닫힌다. + +## 재현 — 감사를 다시 돌리는 법 + +이 회차는 **감사 스크립트를 커밋하지 않았다**(CI 에 배선되지 않은 검사기는 낡는다 — 이 저장소가 이미 그렇게 판정했다). +술어는 위 표가 정본이고, 재현은 **sink 17개 + 「첫 sink 도달 «앞»에 `is_null()` 이 있는가」** 를 그대로 구현하면 된다. +★**다시 돌릴 때는 반드시 «정답지 검증»을 먼저 하라** — 이 회차가 그것으로 자기 버그를 잡았다. + +## 제안 +- p0: 잔여 20건의 **규격 삼분**(NPE 의무 / null 합법 / 도달 불가) 후 첫 갈래만 닫기. diff --git a/rustjava-runtime/src/classes/java/io/buffered_output_stream.rs b/rustjava-runtime/src/classes/java/io/buffered_output_stream.rs index 59d0dd89..eff785a0 100644 --- a/rustjava-runtime/src/classes/java/io/buffered_output_stream.rs +++ b/rustjava-runtime/src/classes/java/io/buffered_output_stream.rs @@ -117,6 +117,10 @@ impl BufferedOutputStream { ) -> Result<()> { tracing::debug!("java.io.BufferedOutputStream::write({this:?}, {bytes:?}, {offset}, {length})"); + if bytes.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "bytes is null").await); + } + let source_length = jvm.array_length(&bytes).await? as i32; if offset < 0 || length < 0 || offset > source_length - length { return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); diff --git a/rustjava-runtime/src/classes/java/io/data_input_stream.rs b/rustjava-runtime/src/classes/java/io/data_input_stream.rs index 6c7186d0..a1ab2e7b 100644 --- a/rustjava-runtime/src/classes/java/io/data_input_stream.rs +++ b/rustjava-runtime/src/classes/java/io/data_input_stream.rs @@ -248,6 +248,10 @@ impl DataInputStream { async fn read_fully(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, b: ClassInstanceRef>) -> Result<()> { tracing::debug!("java.io.DataInputStream::readFully({this:?}, {b:?})"); + if b.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "b is null").await); + } + let length = jvm.array_length(&b).await?; let _: () = jvm diff --git a/rustjava-runtime/src/classes/java/io/data_output_stream.rs b/rustjava-runtime/src/classes/java/io/data_output_stream.rs index 9c4a2bfd..5b75dafd 100644 --- a/rustjava-runtime/src/classes/java/io/data_output_stream.rs +++ b/rustjava-runtime/src/classes/java/io/data_output_stream.rs @@ -190,6 +190,10 @@ impl DataOutputStream { async fn write_bytes(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.DataOutputStream::writeBytes({this:?}, {s:?})"); + if s.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "s is null").await); + } + let chars: ClassInstanceRef> = jvm.invoke_virtual(&s, "java/lang/String", "toCharArray", "()[C", ()).await?; let length = jvm.array_length(&chars).await?; let chars: Vec = jvm.load_array(&chars, 0, length).await?; @@ -203,6 +207,10 @@ impl DataOutputStream { async fn write_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.DataOutputStream::writeChars({this:?}, {s:?})"); + if s.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "s is null").await); + } + let chars: ClassInstanceRef> = jvm.invoke_virtual(&s, "java/lang/String", "toCharArray", "()[C", ()).await?; let length = jvm.array_length(&chars).await?; let chars: Vec = jvm.load_array(&chars, 0, length).await?; @@ -222,6 +230,10 @@ impl DataOutputStream { async fn write_utf(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.DataOutputStream::writeUTF({this:?}, {s:?})"); + if s.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "s is null").await); + } + let chars: ClassInstanceRef> = jvm.invoke_virtual(&s, "java/lang/String", "toCharArray", "()[C", ()).await?; let length = jvm.array_length(&chars).await?; let chars: Vec = jvm.load_array(&chars, 0, length).await?; diff --git a/rustjava-runtime/src/classes/java/io/file_input_stream.rs b/rustjava-runtime/src/classes/java/io/file_input_stream.rs index 9ed873df..d24ab3d8 100644 --- a/rustjava-runtime/src/classes/java/io/file_input_stream.rs +++ b/rustjava-runtime/src/classes/java/io/file_input_stream.rs @@ -114,6 +114,10 @@ impl FileInputStream { ) -> Result { tracing::debug!("java.io.FileInputStream::read({this:?}, {buf:?}, {offset:?}, {length:?})"); + if buf.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buf is null").await); + } + let fd = jvm.get_field(&this, "fd", "Ljava/io/FileDescriptor;").await?; let mut rust_file = FileDescriptor::file(jvm, context, fd).await?; diff --git a/rustjava-runtime/src/classes/java/io/file_output_stream.rs b/rustjava-runtime/src/classes/java/io/file_output_stream.rs index e72d02c0..2f1d7872 100644 --- a/rustjava-runtime/src/classes/java/io/file_output_stream.rs +++ b/rustjava-runtime/src/classes/java/io/file_output_stream.rs @@ -107,6 +107,10 @@ impl FileOutputStream { ) -> Result<()> { tracing::debug!("java.io.FileOutputStream::write({this:?}, {buffer:?}, {offset:?}, {length:?})"); + if buffer.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } + let fd = jvm.get_field(&this, "fd", "Ljava/io/FileDescriptor;").await?; let mut file = FileDescriptor::file(jvm, context, fd).await?; diff --git a/rustjava-runtime/src/classes/java/io/input_stream_reader.rs b/rustjava-runtime/src/classes/java/io/input_stream_reader.rs index 8c8b93e9..b6876db8 100644 --- a/rustjava-runtime/src/classes/java/io/input_stream_reader.rs +++ b/rustjava-runtime/src/classes/java/io/input_stream_reader.rs @@ -110,6 +110,10 @@ impl InputStreamReader { ) -> Result { tracing::debug!("java.io.InputStreamReader::read({this:?}, {buf:?}, {offset:?}, {length:?})"); + if buf.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buf is null").await); + } + let destination_length = jvm.array_length(&buf).await? as i32; if offset < 0 || length < 0 || offset > destination_length - length { return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); diff --git a/rustjava-runtime/src/classes/java/io/output_stream_writer.rs b/rustjava-runtime/src/classes/java/io/output_stream_writer.rs index d6e2434d..321771d5 100644 --- a/rustjava-runtime/src/classes/java/io/output_stream_writer.rs +++ b/rustjava-runtime/src/classes/java/io/output_stream_writer.rs @@ -104,6 +104,10 @@ impl OutputStreamWriter { ) -> Result<()> { tracing::debug!("java.io.OutputStreamWriter::write({this:?}, {chars:?}, {off}, {len})"); + if chars.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "chars is null").await); + } + let array_length = jvm.array_length(&chars).await? as i32; if off < 0 || len < 0 || off > array_length - len { return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); diff --git a/rustjava-runtime/src/classes/java/io/random_access_file.rs b/rustjava-runtime/src/classes/java/io/random_access_file.rs index 59ab137e..068fc02a 100644 --- a/rustjava-runtime/src/classes/java/io/random_access_file.rs +++ b/rustjava-runtime/src/classes/java/io/random_access_file.rs @@ -119,6 +119,10 @@ impl RandomAccessFile { async fn read(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, buf: ClassInstanceRef>) -> Result { tracing::debug!("java.io.RandomAccessFile::read({this:?}, {buf:?})"); + if buf.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buf is null").await); + } + let length = jvm.array_length(&buf).await?; let read = jvm .invoke_virtual(&this, "java/io/RandomAccessFile", "read", "([BII)I", (buf, 0, length as i32)) @@ -137,6 +141,10 @@ impl RandomAccessFile { ) -> Result { tracing::debug!("java.io.RandomAccessFile::read({this:?}, {buf:?}, {offset:?}, {length:?})"); + if buf.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buf is null").await); + } + let fd = jvm.get_field(&this, "fd", "Ljava/io/FileDescriptor;").await?; let mut rust_file = FileDescriptor::file(jvm, context, fd).await?; @@ -153,6 +161,10 @@ impl RandomAccessFile { async fn write(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, buf: ClassInstanceRef>) -> Result<()> { tracing::debug!("java.io.RandomAccessFile::write({this:?}, {buf:?})"); + if buf.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buf is null").await); + } + let length = jvm.array_length(&buf).await?; let _: () = jvm .invoke_virtual(&this, "java/io/RandomAccessFile", "write", "([BII)V", (buf, 0, length as i32)) @@ -171,6 +183,10 @@ impl RandomAccessFile { ) -> Result<()> { tracing::debug!("java.io.RandomAccessFile::write({this:?}, {buf:?}, {offset:?}, {length:?})"); + if buf.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buf is null").await); + } + let fd = jvm.get_field(&this, "fd", "Ljava/io/FileDescriptor;").await?; let mut rust_file = FileDescriptor::file(jvm, context, fd).await?; diff --git a/rustjava-runtime/src/classes/java/io/reader.rs b/rustjava-runtime/src/classes/java/io/reader.rs index 4aff769c..e27e7dd5 100644 --- a/rustjava-runtime/src/classes/java/io/reader.rs +++ b/rustjava-runtime/src/classes/java/io/reader.rs @@ -74,6 +74,10 @@ impl Reader { async fn read(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, buf: ClassInstanceRef>) -> Result { tracing::debug!("java.io.Reader::read({this:?}, {buf:?})"); + if buf.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buf is null").await); + } + let len = jvm.array_length(&buf).await? as i32; let result = jvm.invoke_virtual(&this, "java/io/Reader", "read", "([CII)I", (buf, 0, len)).await?; diff --git a/rustjava-runtime/src/classes/java/io/writer.rs b/rustjava-runtime/src/classes/java/io/writer.rs index 8590ff63..cd765273 100644 --- a/rustjava-runtime/src/classes/java/io/writer.rs +++ b/rustjava-runtime/src/classes/java/io/writer.rs @@ -99,6 +99,10 @@ impl Writer { async fn write_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, chars: ClassInstanceRef>) -> Result<()> { tracing::debug!("java.io.Writer::write({this:?}, {chars:?})"); + if chars.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "chars is null").await); + } + let length = jvm.array_length(&chars).await? as i32; jvm.invoke_virtual(&this, "java/io/Writer", "write", "([CII)V", (chars, 0, length)).await } @@ -106,6 +110,10 @@ impl Writer { async fn write_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.Writer::write_string({this:?}, {string:?})"); + if string.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "string is null").await); + } + let chars: ClassInstanceRef> = jvm.invoke_virtual(&string, "java/lang/String", "toCharArray", "()[C", ()).await?; let length = jvm.array_length(&chars).await?; @@ -126,6 +134,10 @@ impl Writer { ) -> Result<()> { tracing::debug!("java.io.Writer::write({this:?}, {string:?}, {off}, {len})"); + if string.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "string is null").await); + } + let chars: ClassInstanceRef> = jvm.invoke_virtual(&string, "java/lang/String", "toCharArray", "()[C", ()).await?; jvm.invoke_virtual(&this, "java/io/Writer", "write", "([CII)V", (chars, off, len)).await } diff --git a/rustjava-runtime/src/classes/java/lang/string.rs b/rustjava-runtime/src/classes/java/lang/string.rs index cbcd6495..68f502d7 100644 --- a/rustjava-runtime/src/classes/java/lang/string.rs +++ b/rustjava-runtime/src/classes/java/lang/string.rs @@ -599,6 +599,10 @@ impl String { ) -> Result<()> { tracing::debug!("java.lang.String::getChars({this:?}, {src_begin}, {src_end}, {dst:?}, {dst_begin})"); + if dst.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "dst is null").await); + } + let (value, offset, count) = Self::value_range(jvm, &this).await?; if src_begin < 0 || src_begin > src_end || src_end as usize > count { return Err(jvm diff --git a/test-data/NullBufferGuards.class b/test-data/NullBufferGuards.class new file mode 100644 index 0000000000000000000000000000000000000000..6c8959d216011fe54bf58e7202c883c9d8bc91ea GIT binary patch literal 2412 zcmZ`*&sP&y6#g!tGfd+TQ1BOM{Mm_M5T(`v{vv=z0jYr?4PYl>4AGF#VWQG@VUKp# z?(DLQEGope&4x@Waztz5e8dh(8)as`{aGr5d=UQlg@;u>^8vj@uL+j<&AbT#2_Oc|v_dod$Me zkA}Sg>_d}aM;W66ZB9{Sb7VD(v|eoJX(pHMQt{aD_p=PUfo24ip9ckdN;Viezd+hi zsKWsq!BIxT(@SnfB^nLIb-c}J%+KTt+38>|?*Ydd`p zQudsi$>(&m2#l0fNYA*zq2WFqCj^IjE!Xm0M40BL`+0hy6rxb4hBm>jm4Li~HxNd< za_E#`_nJdKi=IDWbOi8@y4~7U0v+!PjwpM6_lx(Va%tWy239F}kJkMP{3i^YMNkdh z0i08pXLup=Oz5~!ks~r=Icprsz@-3s)(m*hb@Y~k)Tlsu)e;Fs zb&LoO`X!F3G|Np72sbdQLOF_Ym3<$StTX2(nDXl^u3gR4`r`CzL7Dha znP}3HWQ!7?4GSy1#w**^Cq%{a1t(?qXB2nCn>xel z4iQ9-4O{)Gku4j7Ij)*Hi`qH91bNPd;3JOeF;L5ogLxhyv^v3KH3Ym^hBm&0+JR*l z>HA?Qvy&~q4kSGDuCZK4n3nBY8%&+wS@S9pf+Isbw` z@fYWpc*UzXrA|y~;oL1TiE~cNLwUsci9D6>h^zAQ(b4Q#Z#$-0-VXdk)vlf8)r*9@CUIg3MBz+q}RMm0z9Iga8>Pt|dJgBEHFQ(Y^5 zzzO__lT>*MzvDEuo}tXmT_M47zX7Qh9%8*`2aC_h&a6+k;fR7FK| Date: Fri, 11 Sep 2026 22:53:32 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[rustjava-null-guard-audit-remaining-runtim?= =?UTF-8?q?e-entrypoints-fix]=20fix:=20=EA=B0=90=EC=82=AC=20=EC=88=98=20?= =?UTF-8?q?=EC=A0=95=EC=A0=95(=ED=8A=B8=EB=A6=AC=20=ED=98=BC=EC=9E=85)=20?= =?UTF-8?q?=C2=B7=20=ED=94=BD=EC=8A=A4=EC=B2=98=202=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80=20=C2=B7=20getChars=20=EC=84=A0?= =?UTF-8?q?=ED=9B=84=20=EB=B3=B5=EC=9B=90=20=C2=B7=20=EA=B0=90=EC=82=AC=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F1: K 46 은 base(6da7d66f)가 아니라 eaad8e9c 트리의 수였다(라벨만 base). 46−18=28≠20 이 증상. ⇒ base 1163/50/38 · head 1163/32/20 · Δ18 로 4곳 동시 정정하고 «트리 병기» 형태로 바꿨다. F2: 픽스처 커버리지는 그 시점 10/18 이었다(Reader/Writer base 구현에 닿지 못했다). ⇒ 중첩 클래스로 2케이스 추가 ⇒ 진짜 12/18. 개악 ⑶⑷가 도달을 증명한다. F4: getChars 가드를 «범위 검사 뒤»로 옮겼다 — JDK 는 잘못된 범위가 null dst 를 이긴다. 선후 잠금 케이스 추가(옛 위치로 되돌리면 red). F5: scripts/audit-null-guards.py 커밋 — CI 미배선(잠금이 아니라 감사). ★가드 자체는 무접촉(계약 2). string.rs 의 5+/4- 는 «이동»이라 삭제행이 있는 것이 맞다. Co-Authored-By: Claude Fable 5 --- REPORT.md | 16 +- STATE.md | 23 ++- ...null-guard-audit-and-io-buffer-guards.json | 43 ++++-- ...1-null-guard-audit-and-io-buffer-guards.md | 63 ++++++-- .../src/classes/java/lang/string.rs | 9 +- scripts/audit-null-guards.py | 143 ++++++++++++++++++ test-data/NullBufferGuards$BareReader.class | Bin 0 -> 385 bytes test-data/NullBufferGuards$BareWriter.class | Bin 0 -> 432 bytes test-data/NullBufferGuards.class | Bin 2412 -> 2999 bytes test-data/NullBufferGuards.txt | 6 + test-data/src/NullBufferGuards.java | 53 +++++++ 11 files changed, 314 insertions(+), 42 deletions(-) create mode 100644 scripts/audit-null-guards.py create mode 100644 test-data/NullBufferGuards$BareReader.class create mode 100644 test-data/NullBufferGuards$BareWriter.class diff --git a/REPORT.md b/REPORT.md index 672381c4..68a9c225 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,17 +1,25 @@ # REPORT ## [2026-09-11] null 가드 «감사» — 세고, 그중 데이터 전송 버퍼 18곳을 닫았다 (rustjava-null-guard-audit-remaining-runtime-entrypoints) -- 무엇을: 런타임 전체에서 `ClassInstanceRef` 인자를 받아 **곧바로 역참조하는** 진입점을 **셌고**(N 1,163 → M 58 → K 46), +- 무엇을: 런타임 전체에서 `ClassInstanceRef` 인자를 받아 **곧바로 역참조하는** 진입점을 **셌고** + (★**트리 병기** — base `6da7d66f`: **N 1,163 · M 50 · K 38** → head `068fdb95`: **M 32 · K 20** · **ΔK 18**), 그중 **`java/io` 데이터 전송 버퍼 + `String.getChars` 18곳**에 선행 회차와 **같은 모양**의 진입부 `is_null()` 가드를 넣었다. - 회귀 잠금 = 픽스처 `test-data/NullBufferGuards`(10케이스). 채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. + 회귀 잠금 = 픽스처 `test-data/NullBufferGuards`(**13케이스** · 가드 **12/18** 커버). 채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. - 왜: 선행 회차가 닫은 것은 **손으로 열거한** 9곳이었고, ★그 열거 자신이 `arraycopy` 의 `dest` 를 빠뜨렸다. ⇒ 이번엔 **열거가 아니라 술어로 셌다**. sink 는 `jvm.rs` 가 `&Box`/`impl AsClassInstance` 로 받는 **17개 메서드**다. - 사용자 영향: 게스트가 스트림·Writer·`String.getChars` 에 **null 버퍼**를 넘겨도 에뮬레이터가 죽지 않고 `NullPointerException` 이 난다. 정상 입력 동작 **불변**(가드 앞에서 패닉하던 입력만 예외로 바뀐다) · 예외 종류·메시지 형태는 선행 회차와 동일. -- 검증: ★**양방향** — 18곳 전건 되돌림 → `test_class` **FAILED**(`class_instance.rs:108`) · ★**단일 가드**(`get_chars(dst)`)만 빼도 **FAILED**(`:114` `DerefMut`) ↔ 복원 **ok**. - DoD 7종 rc=0 · `cargo test --all` **554/0/1**(계수 불변이 맞다). +- 검증: ★**양방향 · 개악 5종** — ⑴18곳 전건 ⑵`get_chars(dst)` 단독 ⑶`reader.rs read` 단독 ⑷`writer.rs write_chars` 단독 + ⑸**`getChars` 가드를 옛 위치로 되돌림** → **전건 FAILED** ↔ 복원 전건 **ok**. DoD 7종 rc=0 · `cargo test --all` **554/0/1**(★전체 실행 · 계수 불변이 맞다). - ★★**계측기를 먼저 검증했고, 초판이 틀렸다**: 선행 9곳을 정답지로 대니 **3/9** 만 잡혔다(근인 = `"\n"+src` 에 정규식을 돌리고 인덱스는 `src` 에 쓴 **off-by-one**). 고쳐서 **8/9**. 남은 1건은 helper **안**에서 deref 하므로 ★**이 계측은 절차간을 못 보고, 따라서 K 는 «하한»이다**(검수자의 「899/560 상한」과 방향이 반대다). +- ★★**[게이트② 정정 3건] 초판이 낸 수 중 «둘»이 틀렸고, 둘 다 «수를 섞은» 형태다.** + ⑴**K 46** 은 base 가 아니라 **`eaad8e9c`**(두 회차 전) 트리의 수였다 — 라벨만 `6da7d66f` 였고, `46 − 18 = 28 ≠ 20` 이 그 증상이었다. + ⇒ ★**base·head 를 나란히 적는 형태로 바꿨다.** ⑵**픽스처 12/18** 도 그 시점엔 **10/18** 이었다(`Reader`/`Writer` 의 base 구현에 닿지 못했다) + ⇒ ★**검수 권고대로 2케이스를 «추가»해 실제로 12/18 로 만들었다**(중첩 클래스로 base 구현 도달 · 개악 ⑶⑷가 증명). + ⑶`String.getChars` 가드가 **JDK 의 예외 선후를 뒤집고 있었다** ⇒ **범위 검사 뒤로 옮기고** 선후 잠금 케이스를 넣었다. +- ★**감사 스크립트를 커밋했다**(`scripts/audit-null-guards.py` · ★CI 미배선 = **잠금이 아니라 감사**) — 초판의 「배선 없는 검사기는 낡는다」를 뒤집는다. + ★**근거는 이 회차 자신이다**: 작성자가 자기 수를 교차검증할 수단이 없어 ⑴이 났다. - ★**잔여 20건을 «일괄 가드»로 닫지 마라** — `URL(context, spec, handler)` 는 JDK 규격상 **null handler 가 합법**이다. ⇒ 후속의 본체는 「가드를 넣는 것」이 아니라 **「null 이 합법인지 규격으로 가르는 것」**이다. - 후속 추천: `rustjava-null-guard-spec-triage-for-constructor-and-collection-params`(P3) — 잔여 20건을 **규격 기준으로 삼분** diff --git a/STATE.md b/STATE.md index c333001c..88703c7b 100644 --- a/STATE.md +++ b/STATE.md @@ -5,9 +5,12 @@ ## 완료 - [rustjava-null-guard-audit-remaining-runtime-entrypoints] ★★**런타임 전체를 «세고»(감사) 그중 «데이터 전송 버퍼» 18곳에 가드를 넣었다.** - 채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. 픽스처 `test-data/NullBufferGuards` 10케이스. - ★★**감사 표(측정 2026-09-11 · 트리 `origin/main` `6da7d66f`)**: **N 1,163**(`this`·`_` 제외 `ClassInstanceRef` 인자) - → **M 58**(deref 강제 sink 에 미가드 도달) → **K 46**(그중 `as_proto` 등재 = 게스트 도달 가능) ⇒ 이 회차가 **18** 을 닫아 **K 28 → 실측 20**. + 채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. 픽스처 `test-data/NullBufferGuards` **13케이스**(가드 **12/18** 커버). + ★★**감사 표 — 측정 2026-09-11 · ★트리 병기**(`bin/audit-null-guards` 가 아니라 **`scripts/audit-null-guards.py`** 가 정본): + **base `6da7d66f`** = **N 1,163 · M 50 · K 38** → **head `068fdb95`** = **N 1,163 · M 32 · K 20** ⇒ **ΔM = ΔK = 18** = 이 회차의 가드 수(자기정합). + ★★**[게이트② F1 정정] 초판이 적은 「M 58 · K 46」은 «다른 트리»의 수다** — **`eaad8e9c`**(= 선행 9곳 회차 «이전» main)에서 재고 + 라벨만 `6da7d66f` 로 달았다. ⇒ ★**`46 − 18 = 28 ≠ 20`** 이라는 산술 모순이 그 혼입의 증상이었고, 검수자가 그것으로 잡았다. + ★**이 저장소가 반복해 규탄한 「base 를 병기하지 않으면 수가 섞인다」의 교과서적 재현**이다 — ★그래서 이제 **두 트리를 나란히** 적는다. ★**판정 술어를 적어 둔다**(「보아하니」 금지): sink = `jvm/src/jvm.rs` 가 **`&Box`/`impl AsClassInstance`** 로 받는 **17개 메서드** (`array_length`·`load_array`·`store_array`·`get_field`·`put_field`·`invoke_virtual`·`invoke_special`·`monitor_*` …) — 그 인자에 `&p`/`&mut p` 로 넘기면 **Deref/DerefMut 가 강제**된다. ★★**계측기를 «먼저 검증»했다 — 그리고 초판이 틀렸다.** 선행 회차가 닫은 **9곳**을 정답지로 삼아 가드 전/후 트리에 같은 자를 댔는데 @@ -18,8 +21,18 @@ ★**범위를 «좁게» 골랐다 — 「전부 고쳐라」가 아니다**: `java/io` **데이터 전송**(read/write 버퍼) + `String.getChars` **18곳**. ★★**생성자류를 «일부러» 뺐다 — 일괄 가드가 «틀리는» 자리가 실재한다**: `URL(context, spec, handler)` 는 JDK 규격상 **handler == null 이 «합법»**(기본 핸들러를 쓰라는 뜻)이다. ⇒ ★**잔여 20건은 «가드를 넣을지»가 아니라 «null 이 합법인지»를 규격으로 먼저 가려야 한다** — 후속의 본체가 그것이다. - ★개악 대조 **양방향**: 18곳 전건 되돌림 → `test_class` **FAILED**(`:108`) · ★**단일 가드**(`get_chars(dst)`)만 제거해도 **FAILED**(`:114` `DerefMut`) ↔ 복원 **ok**. - ★`cargo test --all` **554 / 0 / 1**(증감 0 이 맞다 — `test_class` 는 픽스처를 한 함수가 순회한다) · DoD 7종 rc=0. + ★개악 대조 **양방향 · 4종**: ⑴18곳 전건 되돌림 → `test_class` **FAILED** ⑵**단일 가드**(`get_chars(dst)`)만 제거 → **FAILED** + ⑶`reader.rs read(buf)` 만 · ⑷`writer.rs write_chars(chars)` 만 제거 → **각각 FAILED** ↔ 복원 전건 **ok**. + ★★**[게이트② F2 정정] 초판의 「픽스처 18 중 12 커버」는 «그 시점엔 거짓»이었다 — 실측 10** (`Reader.read(char[])`·`Writer.write(char[])` 는 + `InputStreamReader`/`OutputStreamWriter` 가 오버라이드해 **base 구현에 닿지 못했다**). ⇒ ★**검수 권고를 채택해 그 2케이스를 «추가»했다** — + `Reader`/`Writer` 를 상속하되 추상 `(char[],int,int)` 만 구현하는 **중첩 클래스**(`$` 가 든 이름은 하버스가 건너뛴다)로 base 구현에 도달시켰고, + ⑶⑷ 개악이 그 도달을 **실행으로 증명**한다. ⇒ ★**지금은 진짜로 12/18** 이고 **미커버 6**(파일 핸들 필요분). + ★★**[게이트② F4] `String.getChars` 가드를 «범위 검사 뒤»로 옮겼다 — «선택»이다.** + JDK 는 범위를 먼저 보므로 «잘못된 범위 + null dst» 면 **SIOOBE 가 이긴다**. 초판 위치는 그 선후를 **NPE 로 뒤집었다**(계약 4 「동작 의미를 바꾸지 마라」 위반). + ★픽스처에 **선후 잠금 케이스**를 넣었고, 가드를 옛 위치로 되돌리면 그 케이스가 **red** 다(개악 ⑸). + ★`cargo test --all` **554 / 0 / 1**(★**전체 실행** · 증감 0 이 맞다 — `test_class` 는 픽스처를 한 함수가 순회한다) · DoD 7종 rc=0. + ★★**[게이트② F5] 감사 스크립트를 «커밋했다» — 초판의 「배선 없는 검사기는 낡는다」 판단을 뒤집는다.** + 근거: ★**작성자가 자기 수를 교차검증할 수단이 없어 F1 이 났다.** `scripts/audit-null-guards.py`(★CI 미배선 = **잠금이 아니라 감사**). - [rustjava-null-guard-string-init-and-arraycopy-p0] ★★**null 인자 8경로의 «호스트 abort» 를 `NullPointerException` 으로 바꿨다.** 채택 제안 `2026-09-11-s5-duplicate-issuance-stale-next-close#p0`. ★**가드 9곳**(`string.rs` 7 · `system.rs` 2) · 픽스처 `test-data/NullArgGuards` 9케이스. ★**제안은 「8경로」라 했는데 실측은 «9곳»이다** — `System.arraycopy` 가 `src`·`dest` **둘 다** 뚫려 있었고(제안·STATE ③-2 는 `src` 만 셌다) 그 둘은 다른 인자다. diff --git a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json index 571e3520..4665ffd4 100644 --- a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json +++ b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json @@ -1,28 +1,37 @@ { "schema": 1, "date": "2026-09-11", - "title": "null 가드 감사 — N 1163 / M 58 / K 46 을 세고 java/io 데이터 전송 버퍼 18곳을 닫았다", - "services": ["RustJava"], + "title": "null 가드 감사 — base 6da7d66f 에서 N 1163 / M 50 / K 38 을 세고 java/io 데이터 전송 버퍼 18곳을 닫았다", + "services": [ + "RustJava" + ], "taskId": "rustjava-null-guard-audit-remaining-runtime-entrypoints", - "summary": "선행 회차는 손으로 «열거한» 9곳을 닫았고 그 열거 자신이 arraycopy 의 dest 를 빠뜨렸다. 그래서 이번엔 술어로 셌다 — N 1,163(this·_ 제외 ClassInstanceRef 파라미터) → M 58(deref 강제 sink 에 미가드 도달) → K 46(as_proto 등재 = 게스트 도달 가능). sink 는 jvm/src/jvm.rs 가 &Box/impl AsClassInstance 로 받는 17개 메서드로 한정했다 — 초판은 「&p 가 나오면 deref」로 세어 과대계상했다(&ClassInstanceRef 를 받는 헬퍼는 deref 하지 않는다). ★계측기를 «먼저» 검증했고 초판이 틀렸다: 선행 9곳을 정답지로 대니 3/9 만 잡혔고 근인은 정규식을 \"\\n\"+src 에 돌리고 인덱스를 src 에 적용한 off-by-one 이었다(괄호 스캔이 한 칸 밀려 파라미터 문자열이 본문을 삼켰다). 고친 뒤 8/9. 남은 1건(init_with_string)은 helper 안에서 deref 하므로 ★이 계측은 절차간을 못 보고 따라서 K 는 «하한»이다 — 검수자가 선행 회차에 적은 「899 중 560 · 상한값」과 방향이 반대이므로 두 수를 같은 축으로 비교하지 마라. 이 회차가 닫은 범위는 java/io 데이터 전송 버퍼 + String.getChars 18곳이고, 잔여 20건은 «일괄 가드가 틀리는» 자리를 포함하므로(URL(context,spec,handler) 는 JDK 규격상 null handler 가 합법) 후속으로 남겼다.", + "summary": "선행 회차는 손으로 «열거한» 9곳을 닫았고 그 열거 자신이 arraycopy 의 dest 를 빠뜨렸다. 그래서 이번엔 술어로 셌다. ★수는 «트리와 함께» 읽어라 — base 6da7d66f: N 1,163 · M 50 · K 38 → head 068fdb95: N 1,163 · M 32 · K 20 ⇒ ΔM = ΔK = 18 = 이 회차의 가드 수(자기정합). sink 는 jvm/src/jvm.rs 가 &Box/impl AsClassInstance 로 받는 17개 메서드로 한정했다 — 「&p 가 나오면 deref」로 세면 &ClassInstanceRef 를 받는 헬퍼까지 물어 과대계상한다. ★[게이트² F1 정정] 초판이 적은 「M 58 · K 46」은 eaad8e9c(선행 9곳 회차 «이전» main)의 수인데 라벨만 6da7d66f 였다 — 46 − 18 = 28 ≠ 20 이라는 산술 모순이 그 혼입의 증상이었고 검수자가 그것으로 잡았다. 이 저장소가 반복해 규탄한 「base 를 병기하지 않으면 수가 섞인다」의 교과서적 재현이다. ★계측기를 먼저 검증했고 초판이 틀렸다: 선행 9곳을 정답지로 대니 3/9 만 잡혔고 근인은 off-by-one 이었다(고친 뒤 8/9). 남은 1건(init_with_string)은 helper 안에서 deref 하므로 이 계측은 절차간을 못 보고 따라서 K 는 «하한»이다 — 검수자가 선행 회차에 적은 「899 중 560 · 상한값」과 방향이 반대이므로 두 수를 같은 축으로 비교하지 마라. 닫은 범위는 java/io 데이터 전송 버퍼 + String.getChars 18곳이고, 잔여 20건은 «일괄 가드가 틀리는» 자리를 포함하므로(URL(context,spec,handler) 는 JDK 규격상 null handler 가 합법) 후속으로 남겼다.", "changes": [ - "rustjava-runtime/src/classes/java/io/{buffered_output_stream,data_input_stream,data_output_stream,file_input_stream,file_output_stream,input_stream_reader,output_stream_writer,random_access_file,reader,writer}.rs — 진입부 is_null() 가드 17곳(삽입만 · 삭제 0)", - "rustjava-runtime/src/classes/java/lang/string.rs — get_chars(dst) 가드 1곳(삽입만 · 삭제 0)", - "test-data/src/NullBufferGuards.java + .class + test-data/NullBufferGuards.txt — 10케이스 회귀 픽스처", - "STATE.md · REPORT.md — 감사 표·술어·범위 근거·잔여 목록" + "rustjava-runtime/src/classes/java/io/{buffered_output_stream,data_input_stream,data_output_stream,file_input_stream,file_output_stream,input_stream_reader,output_stream_writer,random_access_file,reader,writer}.rs — 진입부 is_null() 가드 17곳", + "rustjava-runtime/src/classes/java/lang/string.rs — get_chars(dst) 가드 1곳 · ★[F4] 위치를 «범위 검사 뒤»로(JDK 의 예외 선후 보존)", + "test-data/src/NullBufferGuards.java + .class(중첩 $BareReader/$BareWriter 포함) + .txt — 13케이스(가드 12/18 커버)", + "★scripts/audit-null-guards.py — 감사 스크립트 신설(★CI 미배선 = 잠금이 아니라 감사)", + "STATE.md · REPORT.md — 감사 표(트리 병기)·술어·범위 근거·잔여 목록" ], "verification": [ - "정상 → green: cargo test --test test_class ok · cargo fmt --check rc=0 · clippy stable/beta/wasm32 rc=0 · cargo test --all 554 passed / 0 failed / 1 ignored · check-worklog-json.py rc=0 · check-dod-ci-parity.py rc=0", - "개악 → red ⑴: 18곳 전건 되돌림 → test_class FAILED, panicked at jvm/src/class_instance.rs:108 (Deref)", - "개악 → red ⑵: 단일 가드(String.getChars 의 dst)만 제거 → test_class FAILED, panicked at jvm/src/class_instance.rs:114 (DerefMut) ⇒ 픽스처가 케이스별로 문다", - "계측기 검증: 가드 전(eaad8e9c)/후 트리 차집합이 선행 9곳 중 8건과 일치 · 새로 생긴 것 0", + "정상 → green: cargo test --test test_class ok · cargo fmt --check rc=0 · clippy stable/beta/wasm32 rc=0 · cargo test --all 554 passed / 0 failed / 1 ignored(★전체 실행) · check-worklog-json.py rc=0 · check-dod-ci-parity.py rc=0", + "개악 → red ⑴: 18곳 전건 되돌림 → test_class FAILED @ jvm/src/class_instance.rs:108 (Deref)", + "개악 → red ⑵: String.getChars 의 dst 가드만 제거 → FAILED @ :114 (DerefMut)", + "★개악 → red ⑶: reader.rs read(buf) 가드«만» 제거 → FAILED — 새 케이스 Reader.read(char[]) 가 base 구현에 실제로 도달함을 증명", + "★개악 → red ⑷: writer.rs write_chars(chars) 가드«만» 제거 → FAILED — 새 케이스 Writer.write(char[]) 동일 증명", + "★개악 → red ⑸: getChars 가드를 «범위 검사 앞»(초판 위치)으로 되돌림 → 선후 잠금 케이스가 caught NPE 를 내어 FAILED ⇒ F4 의 이동이 하중을 진다", + "계측기 검증: base(6da7d66f)/head 차집합이 이 회차 가드 18건과 일치 · 선행 회차 정답지로는 8/9(나머지 1건은 절차간이라 원리적 위음성)", "픽스처 형식: javac --release 8 ⇒ major 52 · invokedynamic/CP 태그 15~18 0건" ], "issues": [ - "★픽스처 커버리지는 18 중 12 다 — FileInputStream.read · FileOutputStream.write · RandomAccessFile 4종은 실파일 핸들이 있어야 인스턴스가 생겨서 덮지 못했다(작업 디렉터리에 파일을 만드는 것을 피했다). 가드는 들어갔으나 픽스처가 잠그지 않는다 ⇒ 임시 파일 픽스처는 다음 회차 몫이다.", - "★이 계측은 절차간 deref 와 섀도잉을 못 본다. 전자는 위음성(init_with_string 을 놓쳤다), 후자는 위양성(url.rs 의 handler 는 섀도잉돼 무시되는데 잡혔다) ⇒ N·M·K 를 «정확한 수»가 아니라 «하한 + 알려진 오탐 1건»으로 읽어라.", - "★부수 발견 — 패닉 impl 은 둘이 아니라 셋이다: Deref(:108) · DerefMut(:114) · AsClassInstance::as_class_instance(as_deref().unwrap()). 셋째는 monitor_enter/monitor_exit/object_wait_prepare/object_notify 경로다.", - "감사 스크립트는 커밋하지 않았다 — CI 에 배선되지 않은 검사기는 낡는다(이 저장소의 기존 판정). 술어는 worklog .md 의 표가 정본이다." + "★[게이트² F1] 초판이 K 46 을 적었는데 그것은 eaad8e9c 트리의 수였다(라벨은 6da7d66f). 46 − 18 = 28 ≠ 20 이 증상. ⇒ 정정했고 이제 base·head 를 나란히 적는다.", + "★[게이트² F2] 초판의 「픽스처 18 중 12 커버」는 그 시점엔 거짓이고 실측 10 이었다 — InputStreamReader/OutputStreamWriter 가 오버라이드해 Reader/Writer base 구현에 닿지 못했다. ⇒ 검수 권고를 채택해 중첩 클래스(BareReader/BareWriter)로 2케이스를 추가했고 지금은 진짜로 12/18 이다.", + "★[게이트² F4] String.getChars 가드가 JDK 의 예외 선후를 뒤집고 있었다(잘못된 범위 + null dst 면 JDK 는 SIOOBE). ⇒ 범위 검사 뒤로 «옮겼고» 선후 잠금 케이스를 넣었다 — issues 기재가 아니라 이동을 선택했다.", + "★[게이트² F5] 감사 스크립트를 커밋했다 — 초판의 「배선 없는 검사기는 낡는다」를 뒤집는다. 근거는 이 회차 자신이다: 작성자가 자기 수를 교차검증할 수단이 없어 F1 이 났고 검수자는 술어를 산문에서 재구현해야 했다(그래서 N 1,163↔1,130 · M 50↔52 가 갈렸다 — ★판단을 이끄는 K 는 38/20 으로 정확 일치).", + "★픽스처 커버리지는 여전히 18 중 12 다 — FileInputStream.read · FileOutputStream.write · RandomAccessFile 4종은 실파일 핸들이 필요해 못 덮었다. 가드는 들어갔으나 픽스처가 잠그지 않는다 ⇒ proposals[1].", + "★이 계측은 절차간 deref 와 섀도잉을 못 본다. 전자는 위음성(init_with_string), 후자는 위양성(url.rs 의 handler) ⇒ N·M·K 를 «정확한 수»가 아니라 «하한 + 알려진 오탐 1건»으로 읽어라.", + "★부수 발견 — 패닉 impl 은 둘이 아니라 셋이다: Deref · DerefMut · AsClassInstance::as_class_instance(as_deref().unwrap()). 셋째는 monitor_* 경로다." ], "proposals": [ { @@ -44,6 +53,8 @@ "target": "RustJava test-data (FileInputStream·FileOutputStream·RandomAccessFile 6곳)" } ], - "adoptedProposals": ["2026-09-11-null-guard-string-init-and-arraycopy#p0"], + "adoptedProposals": [ + "2026-09-11-null-guard-string-init-and-arraycopy#p0" + ], "declinedProposals": [] } diff --git a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md index 1d902e24..0b9e25fd 100644 --- a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md +++ b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md @@ -7,11 +7,24 @@ ## 감사 — 수와 술어 (측정 2026-09-11 · 트리 `origin/main` `6da7d66f`) -| 축 | 값 | 술어 | -|---|---|---| -| **N** | **1,163** | `rustjava-runtime/src/**` 의 fn 파라미터 중 타입이 `ClassInstanceRef<…>` 인 것(★`this` 와 `_` 접두는 제외) | -| **M** | **58** | N 중, 그 파라미터가 **deref 강제 sink** 에 `&p`/`&mut p`(또는 `p.as_class_instance()`)로 도달하고 ★**그 «앞»에 `p.is_null()` 가드가 없는** 것 | -| **K** | **46** | M 중, 그 fn 이 같은 파일의 `JavaMethodProto::new(… Self::fn …)` 에 등재 = **게스트 바이트코드가 부를 수 있다** ⇒ null 이 «도달 가능» | +★★**[게이트② F1 정정] 수는 «트리와 함께» 적는다 — 초판이 두 트리를 섞었다.** +정본 = **`scripts/audit-null-guards.py`**(이 회차가 커밋했다). 재현: `python3 scripts/audit-null-guards.py []`. + +| 트리 | N | M | K | +|---|---|---|---| +| **base `6da7d66f`**(= 이 PR 의 `merge-base`) | 1,163 | **50** | ★**38** | +| **head `068fdb95`** | 1,163 | **32** | **20** | +| **Δ** | 0 | **18** | ★**18** = 이 회차의 가드 수(자기정합) | + +술어: **N** = `rustjava-runtime/src/**` 의 fn 파라미터 중 타입이 `ClassInstanceRef<…>`(★`this`·`_` 접두 제외) · +**M** = N 중 그 파라미터가 **deref 강제 sink** 에 `&p`/`&mut p`(또는 `p.as_class_instance()`)로 도달하고 ★**그 «앞»에 `p.is_null()` 가 없는** 것 · +**K** = M 중 그 fn 이 같은 파일 `JavaMethodProto::new(… Self::fn …)` 에 등재 = **게스트가 부를 수 있다**. + +★★**초판의 「M 58 · K 46」은 «다른 트리»의 수다** — **`eaad8e9c`**(선행 9곳 회차 «이전» main)에서 재고 라벨만 `6da7d66f` 로 달았다. +⇒ ★**`46 − 18 = 28 ≠ 20`** 이라는 산술 모순이 그 혼입의 «증상»이었고, 검수자가 술어를 재구현해 그것으로 잡았다. +★**이 저장소가 반복해 규탄한 「base 를 병기하지 않으면 수가 섞인다」의 교과서적 재현이다.** +※검수 재구현치는 N **1,130** · M **52/34**(K 는 **38/20 으로 정확 일치**) — ★**판단을 이끄는 K 가 두 구현에서 같다.** +N·M 의 차는 구현 세부(파라미터 계수 방식)이고, ★**이제 스크립트가 커밋돼 있으므로 «어느 쪽이 맞나»는 돌려서 답한다.** **deref 강제 sink = 17개**(`jvm/src/jvm.rs` 의 시그니처로 확정 — `&Box` · `&mut Box<…>` · `impl AsClassInstance`): `array_element_type` `array_length` `array_raw_buffer` `array_raw_buffer_mut` `get_field` `interrupt_java_thread` @@ -69,19 +82,43 @@ java/util/zip init_with_zip_entry(zip_entry) · get_input_stream(entry) · init( - **개악 → red**: ⑴18곳 **전건** 되돌림 → **FAILED** `class_instance.rs:108`(`Deref`) ⑵★**단일 가드**(`String.getChars` 의 `dst`)만 제거 → **FAILED** `:114`(`DerefMut`) ⇒ 픽스처가 **케이스별로** 문다. -## 픽스처 커버리지 — ★**18 중 12만 덮는다**(숨기지 않는다) +## 픽스처 커버리지 — ★**13케이스가 가드 12/18 을 문다** (F2 정정 + 권고 채택) + +★★**[게이트② F2] 초판의 「18 중 12」는 «그 시점엔 거짓»이었다 — 실측 10.** +근인: `new InputStreamReader(...).read(char[])` · `new OutputStreamWriter(...).write(char[])` 는 +그 **서브클래스가 오버라이드**하므로 `Reader`/`Writer` 의 **base 구현에 닿지 못한다** ⇒ 그 2곳은 «덮은 줄 알았지만» 안 덮였다. + +⇒ ★**검수 권고를 채택해 2케이스를 «추가»했다**: `Reader`/`Writer` 를 상속하되 **추상 `(char[],int,int)` 만** 구현하는 +**중첩 클래스**(`NullBufferGuards$BareReader`·`$BareWriter` — ★하버스가 `$` 든 이름을 건너뛰므로 별 `.txt` 가 필요 없다)로 base 구현에 도달시켰다. +★**도달을 «실행»으로 증명했다** — `reader.rs read(buf)` 만, `writer.rs write_chars(chars)` 만 각각 제거하면 **각각 red** 다. + +**덮는 12** = `BufferedOutputStream.write` · `DataInputStream.readFully` · `DataOutputStream`×3 · +`InputStreamReader.read` · `OutputStreamWriter.write` · `Writer.write(String)` · `Writer.write(String,int,int)` · +★`Reader.read(char[])` · ★`Writer.write(char[])` · `String.getChars` +(+13번째 케이스는 `getChars` 의 **예외 선후 잠금** — 같은 가드의 «위치»를 문다). -`NullBufferGuards` 10케이스가 무는 것은 `BufferedOutputStream`·`DataInputStream`·`DataOutputStream`×3· -`InputStreamReader`·`OutputStreamWriter`·`Writer`×2·`String.getChars` 다. ★**덮지 «못한» 6곳** = `FileInputStream.read` · `FileOutputStream.write` · `RandomAccessFile` 4종 — **실파일 핸들이 있어야 인스턴스가 생기고**, 테스트가 작업 디렉터리에 파일을 만드는 것을 피했다. -⇒ ★**그 6곳은 «가드는 들어갔으나 픽스처가 잠그지 않았다»** — 다음 회차가 임시 파일 픽스처를 세우면 닫힌다. +⇒ ★**그 6곳은 «가드는 들어갔으나 픽스처가 잠그지 않는다»** — 다음 회차가 임시 파일 픽스처를 세우면 닫힌다(`proposals[1]`). + +## ★F4 — `String.getChars` 가드의 «위치»를 고쳤다 (선택했다) -## 재현 — 감사를 다시 돌리는 법 +JDK `getChars` 는 **범위를 먼저** 검사하므로 «잘못된 범위 + null dst» 면 **`StringIndexOutOfBoundsException` 이 이긴다**. +초판은 가드를 범위 검사 «앞»에 둬 그 선후를 **NPE 로 뒤집었다** — 티켓 계약 4(「동작 의미를 바꾸지 마라」)의 그 축이다. +⇒ ★**옮겼다**(`issues` 에 적는 쪽이 아니라). 그리고 ★**선후 잠금 케이스를 픽스처에 넣었다** — 옛 위치로 되돌리면 그 케이스가 **red** 다. -이 회차는 **감사 스크립트를 커밋하지 않았다**(CI 에 배선되지 않은 검사기는 낡는다 — 이 저장소가 이미 그렇게 판정했다). -술어는 위 표가 정본이고, 재현은 **sink 17개 + 「첫 sink 도달 «앞»에 `is_null()` 이 있는가」** 를 그대로 구현하면 된다. -★**다시 돌릴 때는 반드시 «정답지 검증»을 먼저 하라** — 이 회차가 그것으로 자기 버그를 잡았다. +## 재현 — ★**스크립트를 커밋했다**(F5 · 초판 판단을 뒤집는다) + +```sh +python3 scripts/audit-null-guards.py # 현재 트리 +git worktree add /tmp/t && python3 scripts/audit-null-guards.py /tmp/t/rustjava-runtime/src +``` +★★**초판은 「CI 에 배선되지 않은 검사기는 낡는다」며 커밋하지 «않았다». 그 판단을 뒤집는다 — 근거는 이 회차 자신이다**: +★**작성자가 자기 수를 교차검증할 수단이 없어 F1(두 트리 혼입)이 났고**, 검수자는 **술어를 prose 에서 재구현**해야 했다(그래서 N·M 이 갈렸다). +⇒ **실행 가능한 정의 하나가 산문보다 싸다.** +★**단 «잠금이 아니라 감사»다** — CI 에 배선하지 «않았다». K 가 커도 아무것도 red 가 되지 않는다(이 스크립트는 수를 보고할 뿐이다). +★**다시 돌릴 때는 반드시 «정답지 검증»을 먼저 하라**(스크립트 docstring 에도 박았다) — 이 회차가 그것으로 자기 off-by-one 을 잡았다. ## 제안 - p0: 잔여 20건의 **규격 삼분**(NPE 의무 / null 합법 / 도달 불가) 후 첫 갈래만 닫기. +- p1: **파일 기반 IO 픽스처** — 가드는 있는데 잠그지 않는 6곳. diff --git a/rustjava-runtime/src/classes/java/lang/string.rs b/rustjava-runtime/src/classes/java/lang/string.rs index 68f502d7..0dce204c 100644 --- a/rustjava-runtime/src/classes/java/lang/string.rs +++ b/rustjava-runtime/src/classes/java/lang/string.rs @@ -599,10 +599,6 @@ impl String { ) -> Result<()> { tracing::debug!("java.lang.String::getChars({this:?}, {src_begin}, {src_end}, {dst:?}, {dst_begin})"); - if dst.is_null() { - return Err(jvm.exception("java/lang/NullPointerException", "dst is null").await); - } - let (value, offset, count) = Self::value_range(jvm, &this).await?; if src_begin < 0 || src_begin > src_end || src_end as usize > count { return Err(jvm @@ -613,6 +609,11 @@ impl String { .await); } + // the range checks run first, matching JDK getChars: a bad range wins over a null dst + if dst.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "dst is null").await); + } + let chars: Vec = jvm .load_array(&value, offset + src_begin as usize, (src_end - src_begin) as usize) .await?; diff --git a/scripts/audit-null-guards.py b/scripts/audit-null-guards.py new file mode 100644 index 00000000..48f58c46 --- /dev/null +++ b/scripts/audit-null-guards.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Count runtime entry points where a null `ClassInstanceRef` argument would abort the host. + +★This is an AUDIT, not a lock. It is deliberately NOT wired into CI: it reports numbers, + it does not gate. Nothing fails because this script prints a large K. + +Why it is committed at all (2026-09-11, gate2 F5): the round that first produced these +numbers could not cross-check its own arithmetic, and shipped a K measured on the WRONG +TREE. One executable definition of the predicate is cheaper than re-deriving it from prose. + + python3 scripts/audit-null-guards.py [] [--json ] + +To measure another commit, materialise it and point at that tree — the script never +consults git, so the tree you pass is the tree you measure: + + git worktree add /tmp/t + python3 scripts/audit-null-guards.py /tmp/t/rustjava-runtime/src + +Predicate +--------- +N fn parameters typed `ClassInstanceRef<…>`, excluding `this` and `_`-prefixed. +M of N, the parameter reaches a DEREF-FORCING SINK as `&p` / `&mut p` + (or `p.as_class_instance()`) with no `p.is_null()` earlier in the body. +K of M, the fn is registered via `JavaMethodProto::new(…, Self::fn, …)` in the same + file, i.e. guest bytecode can call it, i.e. null can actually arrive. + +SINKS are the `Jvm` methods that take `&Box`, `&mut Box<…>` or +`impl AsClassInstance` — passing a `ClassInstanceRef` there forces Deref/DerefMut/ +AsClassInstance, all three of which are `.unwrap()` on an Option (see +`jvm/src/class_instance.rs`). That unwrap is the host abort. + +Known limits — K is a LOWER bound, and not a clean one: + * intraprocedural only. A fn that derefs inside a helper is invisible (this is why + `String::init_with_string` was missed: it derefs inside `Self::value_range`). + * name-based, so shadowing yields false positives (`java/net/url.rs` flags a `handler` + parameter that the body shadows and ignores). + * `null can arrive` is approximated by `registered in as_proto`; it does not ask + whether the JDK spec actually permits null there (for `URL(context, spec, handler)` + a null handler is LEGAL, so a guard would be wrong). + +★When you re-run this, validate it against a known answer key BEFORE trusting the output: + measure the tree before and after a round that added guards, and check the set difference + equals exactly that round's guards. That check is what caught an off-by-one here once. +""" + +import argparse +import collections +import json +import pathlib +import re +import sys + +SINKS = ( + "array_element_type", "array_length", "array_raw_buffer", "array_raw_buffer_mut", + "get_field", "interrupt_java_thread", "invoke_special", "invoke_virtual", + "is_java_thread_interrupted", "load_array", "monitor_enter", "monitor_exit", + "object_notify", "object_wait_prepare", "put_field", "shallow_clone", "store_array", +) + +FN = re.compile(r"(?:^|\n)\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)\s*\(") +PARAM = re.compile(r"([A-Za-z0-9_]+)\s*:\s*(?:&\s*)?(?:mut\s+)?ClassInstanceRef\s*<") +PROTO = re.compile(r'JavaMethodProto::new\(\s*"[^"]*"\s*,\s*"[^"]*"\s*,\s*Self::([A-Za-z0-9_]+)') +SINK_CALL = r"\.\s*(?:" + "|".join(SINKS) + r")\s*\(\s*&\s*(?:mut\s+)?\**\s*" + + +def _balanced(text, start, open_ch, close_ch): + """Index of the closer matching the opener at `start`, or -1.""" + depth = 0 + for i in range(start, len(text)): + if text[i] == open_ch: + depth += 1 + elif text[i] == close_ch: + depth -= 1 + if depth == 0: + return i + return -1 + + +def split_fns(text): + """Yield (name, params_src, body_src). All indices address `text` itself.""" + for m in FN.finditer(text): + paren = m.end() - 1 + close = _balanced(text, paren, "(", ")") + if close < 0: + continue + brace = text.find("{", close) + if brace < 0: + continue + end = _balanced(text, brace, "{", "}") + if end < 0: + continue + yield m.group(1), text[paren + 1:close], text[brace:end] + + +def audit(root): + rows, n = [], 0 + for path in sorted(pathlib.Path(root).rglob("*.rs")): + text = path.read_text(encoding="utf-8", errors="replace") + protos = set(PROTO.findall(text)) + for name, params, body in split_fns(text): + for pm in PARAM.finditer(params): + p = pm.group(1) + if p == "this" or p.startswith("_"): + continue + n += 1 + hits = [m.start() for m in re.finditer(SINK_CALL + re.escape(p) + r"\b", body)] + hits += [m.start() for m in + re.finditer(re.escape(p) + r"\s*\.\s*as_class_instance\s*\(", body)] + if not hits: + continue + guard = re.search(r"\b" + re.escape(p) + r"\s*\.\s*is_null\s*\(\s*\)", body) + if guard and guard.start() < min(hits): + continue + rows.append({"file": str(path), "fn": name, "param": p, + "guest_reachable": name in protos}) + return n, rows + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("root", nargs="?", default="rustjava-runtime/src") + ap.add_argument("--json", dest="out") + args = ap.parse_args() + + n, rows = audit(args.root) + k_rows = [r for r in rows if r["guest_reachable"]] + print(f"tree: {args.root}") + print(f"N (ClassInstanceRef params, excl. this/_) = {n}") + print(f"M (unguarded, reaches a deref-forcing sink) = {len(rows)}") + print(f"K (of M, registered in as_proto) = {len(k_rows)}") + print() + by = collections.defaultdict(list) + for r in k_rows: + by[r["file"].split("/classes/")[-1]].append(f'{r["fn"]}({r["param"]})') + for f in sorted(by): + print(f" {f}: " + ", ".join(sorted(by[f]))) + if args.out: + json.dump(rows, open(args.out, "w"), indent=1) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test-data/NullBufferGuards$BareReader.class b/test-data/NullBufferGuards$BareReader.class new file mode 100644 index 0000000000000000000000000000000000000000..f83afb0cf880557b25debe1949c190a3e27b6077 GIT binary patch literal 385 zcmZ8cO-sW-5Pg%R-5O&*)E-m>4<1?%_TsU4Xg~=_4=P?fZJRAbVgeiEukzwa@Zb;d zM@iobevpNEZ+7O*W9IYg{R6-mc0+jZ6@myYv<2M<{j4WhIk`4EHCE7`W<~aNF7QX= zJB5zmU|#3>tX{2*y{xrOtK*rr=0_jIWoiVytE@2d`f*|GtzP7;1eT5hHM)TF3m;eRKuqEsar@8B}HLuoa?GGmBO~5-&E4+ZxI|p@iO;(25*C4gV`Zj zT;=2q^2&r`MArs-#7#s*C*3CpWY2pc{?`ro+xoTK`|eWFv>S4Rt}~!bVSpWU E)^h?wVE_OC literal 0 HcmV?d00001 diff --git a/test-data/NullBufferGuards$BareWriter.class b/test-data/NullBufferGuards$BareWriter.class new file mode 100644 index 0000000000000000000000000000000000000000..42a287bfd1e40beda29c8ba508c285d1c27c6677 GIT binary patch literal 432 zcmaivyGjE=6o&tkY%+0OlXwYuK}cbdq%ciNvB-k5AZe6XSWS{y92s|ECZo?~C8Y2H zd?@iup+ZtPa4wwr{&V^KT73W*p%bHmsz8*WhB~47BwyrEm&1Fjm&y|A7rM~POG35R zp9(YxClhbX*yp*j*IwF%I~z-@e)uRY7mCoj(S@4$=b5s1a%Q+vdkq~4qW6$yS%1ov z+<5oMac)Y-Kk;qp?Oa{yfOpoXoQKT_NmdlfrbaraoWMSzy-sqczm3p^;1k1(*!V~g zwm3WGNTD)#2fcB^GWOvcIAp(#gndAd*n)TiJ7!?#w`0J9cmoTgn*U>6-m}H}3fkD^ M7_&@Z4+m&`1ErHk;{X5v literal 0 HcmV?d00001 diff --git a/test-data/NullBufferGuards.class b/test-data/NullBufferGuards.class index 6c8959d216011fe54bf58e7202c883c9d8bc91ea..da5ac5b3b554be70a0df68a6170bea1a90b21221 100644 GIT binary patch delta 1025 zcmZ{iO>9(U6o#L3XQne9OR1wmlP&}sOf&|6g2pyV6t|jaVqz0q7~{ePi7OK~B%tTcXmsP|ocDXb^PQaY ze(!II<+br|Zh!tI(8a|o@k`xy!ghC7 zEH~$^>$Y6GULL1FCeQf?}Xc1DuQ8+ z#TjL~i5X`7pTldaFE1i4fNE&7VXX7G?ES3Pa26ldssd#QvbArraWFG#jRIKXbL5BB|5r2C|3eb%XW9*uY)1)5b=P9c&_{4tt14>13Op zdU=HBlo{f2MwA`pF>E$-fGy0jm1AsI4YsKYPgOa~)4WL!Z?S{-*vb3)`~#lhQ~LOV ze!k{ez9-EO>|%uhe$v*zlHm^><`!Ai^aT+HrI8`IlVLF#ktBO%Bcsy6i_(Q9TN#&r za*`o0gV>T~LiVcb^HlfC3?(_rq|B?g7jWbZhhz~?-li;DgWNat{qmCWbCCN3ZI58PtGVdRKRnZ R6N(Dzzw`P#j^+!*{sJpn^!)$; delta 517 zcmW-dOH7kd5QV?f_U}u4RTN(}jS98}jY>5}K!_D%tq`?ZALS8D8t{P+K&5JeMBKsZ z$E7AN2$JZc7$Gsyz3xnO>CzobT?$Uk%{_B&X6DR2vskkf34dMw_!&s?q#S7g5=WxMx^NL&(k$!`5P@-U1FjE0}+u3`{E)8SpytE^uR>wUN37 zYG$eGtnj4qGGVtE%8t$G4RPzQXfD%gxSAkq*uXlr5g}ph0GrHZHHX+p3!7R+|slOn}YwDR}A8%>k9eeq}em-$j|@T-G#K^^$^Si1wKOF3r(xyX|Gf{HLj2vA=i&+;>}7rM46T T#DhGvjL&vHYWI Date: Sat, 12 Sep 2026 00:49:52 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[rustjava-null-guard-audit-remaining-runtim?= =?UTF-8?q?e-entrypoints-fix2]=20fix:=20=EA=B0=90=EC=82=AC=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EB=88=88=EB=A8=BC=20=EA=B5=AC?= =?UTF-8?q?=EA=B0=84=20=C2=B7=20=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80=20?= =?UTF-8?q?=EA=B7=BC=EC=9D=B8=20=EC=97=AD=EC=A0=84=20=EC=A0=95=EC=A0=95=20?= =?UTF-8?q?=C2=B7=203=EC=9D=B8=EC=9E=90=20=EC=BC=80=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EC=8B=A4=EC=A0=9C=20=EC=BB=A4=EB=B2=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R1: 근인 서술이 뒤집혀 있었다 — InputStreamReader 가 «오버라이드해서»가 아니라 «오버라이드가 없어서» 1인자 호출이 Reader::read([C) 로 해소된다(등재 서술자 실측: InputStreamReader 는 ([CII)I 만). ⇒ 전 회차가 더한 중첩 클래스 2케이스는 «이미 덮인 가드»를 또 덮었다. 지웠다. 대신 3인자 호출(read([CII) · write([CII))로 그 두 가드를 실제로 덮었다. ★커버리지를 «추론»하지 않고 «측정»했다 — 18곳 단일 개악 2회전 ⇒ 12/18 · 미커버 6(전부 파일 핸들). R2: 커밋한 스크립트가 M 을 2 낮게 셌다 — fn 정규식이 제네릭·pub(crate) 선언 34개를 못 봤다. 고친 뒤 세 트리 × 세 독립 구현 전건 일치(1,175 / 60·52·34 / 46·38·20). ★K 가 무사했던 것은 운이다(눈먼 2건이 우연히 as_proto 미등재) — docstring 에 박았다. ★「내 수가 맞다」던 전 회차 문안을 철회한다. R3: 잔여 20건 분해 오기 정정(도구 출력 기준). ★가드 자체와 K(46/38/20) 무접촉(계약 2). Co-Authored-By: Claude Fable 5 --- REPORT.md | 18 +++- STATE.md | 23 +++-- ...null-guard-audit-and-io-buffer-guards.json | 37 ++++--- ...1-null-guard-audit-and-io-buffer-guards.md | 91 +++++++++++------- scripts/audit-null-guards.py | 65 ++++++++++++- test-data/NullBufferGuards$BareReader.class | Bin 385 -> 0 bytes test-data/NullBufferGuards$BareWriter.class | Bin 432 -> 0 bytes test-data/NullBufferGuards.class | Bin 2999 -> 2952 bytes test-data/NullBufferGuards.txt | 8 +- test-data/src/NullBufferGuards.java | 37 ++----- 10 files changed, 176 insertions(+), 103 deletions(-) delete mode 100644 test-data/NullBufferGuards$BareReader.class delete mode 100644 test-data/NullBufferGuards$BareWriter.class diff --git a/REPORT.md b/REPORT.md index 68a9c225..70728be2 100644 --- a/REPORT.md +++ b/REPORT.md @@ -2,24 +2,32 @@ ## [2026-09-11] null 가드 «감사» — 세고, 그중 데이터 전송 버퍼 18곳을 닫았다 (rustjava-null-guard-audit-remaining-runtime-entrypoints) - 무엇을: 런타임 전체에서 `ClassInstanceRef` 인자를 받아 **곧바로 역참조하는** 진입점을 **셌고** - (★**트리 병기** — base `6da7d66f`: **N 1,163 · M 50 · K 38** → head `068fdb95`: **M 32 · K 20** · **ΔK 18**), + (★**트리 병기 · 재측 2026-09-12** — base `6da7d66f`: **N 1,175 · M 52 · K 38** → head: **M 34 · K 20** · **ΔK 18**), 그중 **`java/io` 데이터 전송 버퍼 + `String.getChars` 18곳**에 선행 회차와 **같은 모양**의 진입부 `is_null()` 가드를 넣었다. 회귀 잠금 = 픽스처 `test-data/NullBufferGuards`(**13케이스** · 가드 **12/18** 커버). 채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. - 왜: 선행 회차가 닫은 것은 **손으로 열거한** 9곳이었고, ★그 열거 자신이 `arraycopy` 의 `dest` 를 빠뜨렸다. ⇒ 이번엔 **열거가 아니라 술어로 셌다**. sink 는 `jvm.rs` 가 `&Box`/`impl AsClassInstance` 로 받는 **17개 메서드**다. - 사용자 영향: 게스트가 스트림·Writer·`String.getChars` 에 **null 버퍼**를 넘겨도 에뮬레이터가 죽지 않고 `NullPointerException` 이 난다. 정상 입력 동작 **불변**(가드 앞에서 패닉하던 입력만 예외로 바뀐다) · 예외 종류·메시지 형태는 선행 회차와 동일. -- 검증: ★**양방향 · 개악 5종** — ⑴18곳 전건 ⑵`get_chars(dst)` 단독 ⑶`reader.rs read` 단독 ⑷`writer.rs write_chars` 단독 - ⑸**`getChars` 가드를 옛 위치로 되돌림** → **전건 FAILED** ↔ 복원 전건 **ok**. DoD 7종 rc=0 · `cargo test --all` **554/0/1**(★전체 실행 · 계수 불변이 맞다). +- 검증: ★★**커버리지를 «추론»하지 않고 «측정»했다 — 가드 18곳을 «하나씩» 빼고 각각 빌드·실행**(2×18 회차). + ⇒ **12 곳은 red(덮인다) · 6 곳은 green(★안 덮인다 — 전부 파일 핸들 필요분)**. 그 밖에 `getChars` **가드 위치** 원복 개악도 red. + DoD 7종 rc=0 · `cargo test --all` **554/0/1**(★전체 실행 · 계수 불변이 맞다). - ★★**계측기를 먼저 검증했고, 초판이 틀렸다**: 선행 9곳을 정답지로 대니 **3/9** 만 잡혔다(근인 = `"\n"+src` 에 정규식을 돌리고 인덱스는 `src` 에 쓴 **off-by-one**). 고쳐서 **8/9**. 남은 1건은 helper **안**에서 deref 하므로 ★**이 계측은 절차간을 못 보고, 따라서 K 는 «하한»이다**(검수자의 「899/560 상한」과 방향이 반대다). - ★★**[게이트② 정정 3건] 초판이 낸 수 중 «둘»이 틀렸고, 둘 다 «수를 섞은» 형태다.** ⑴**K 46** 은 base 가 아니라 **`eaad8e9c`**(두 회차 전) 트리의 수였다 — 라벨만 `6da7d66f` 였고, `46 − 18 = 28 ≠ 20` 이 그 증상이었다. - ⇒ ★**base·head 를 나란히 적는 형태로 바꿨다.** ⑵**픽스처 12/18** 도 그 시점엔 **10/18** 이었다(`Reader`/`Writer` 의 base 구현에 닿지 못했다) - ⇒ ★**검수 권고대로 2케이스를 «추가»해 실제로 12/18 로 만들었다**(중첩 클래스로 base 구현 도달 · 개악 ⑶⑷가 증명). + ⇒ ★**base·head 를 나란히 적는 형태로 바꿨다.** ⑵**픽스처 12/18** 도 그 시점엔 **10/18** 이었고, + ★**내가 적은 근인이 «뒤집혀» 있었다** — `InputStreamReader` 가 오버라이드해서가 «아니라» **오버라이드가 없어서** `Reader::read([C)` 로 해소된다 + (등재 서술자로 확인: `InputStreamReader` 는 `([CII)I` 만, `Reader` 가 `([C)I` 를 등재한다). + ⇒ 그때 더한 중첩 클래스 2케이스는 **이미 덮인 가드를 또 덮었다** ⇒ **지웠고**, 대신 **3인자 호출**로 그 두 가드를 실제로 덮었다. ⑶`String.getChars` 가드가 **JDK 의 예외 선후를 뒤집고 있었다** ⇒ **범위 검사 뒤로 옮기고** 선후 잠금 케이스를 넣었다. - ★**감사 스크립트를 커밋했다**(`scripts/audit-null-guards.py` · ★CI 미배선 = **잠금이 아니라 감사**) — 초판의 「배선 없는 검사기는 낡는다」를 뒤집는다. ★**근거는 이 회차 자신이다**: 작성자가 자기 수를 교차검증할 수단이 없어 ⑴이 났다. +- ★★**[게이트② R2] 그런데 그 커밋된 스크립트가 M 을 «2 낮게» 셌다** — `fn` 정규식이 **제네릭·`pub(crate)` 선언 34개를 못 봤다**. + ★고쳤고 **세 트리 × 세 독립 구현이 전건 일치**한다(N **1,175** / M **60·52·34** / K **46·38·20**). + ★★**「내 수가 맞고 검수 구현이 세부에서 다르다」던 전 회차 문안을 «철회»한다 — 틀린 것은 내 쪽이었다.** + ★★**K 가 무사했던 것은 «설계가 아니라 운»이다** — 눈먼 fn 2건이 우연히 `as_proto` 미등재였을 뿐이고, + 등재된 제네릭 진입점이 **하나만** 있었어도 K 가 조용히 낮아져 ★**이 감사가 「닫혔다」고 답했을 것**이다. ⇒ 그 눈먼 구간을 docstring 에 박았다. - ★**잔여 20건을 «일괄 가드»로 닫지 마라** — `URL(context, spec, handler)` 는 JDK 규격상 **null handler 가 합법**이다. ⇒ 후속의 본체는 「가드를 넣는 것」이 아니라 **「null 이 합법인지 규격으로 가르는 것」**이다. - 후속 추천: `rustjava-null-guard-spec-triage-for-constructor-and-collection-params`(P3) — 잔여 20건을 **규격 기준으로 삼분** diff --git a/STATE.md b/STATE.md index 88703c7b..a78e80ab 100644 --- a/STATE.md +++ b/STATE.md @@ -5,9 +5,15 @@ ## 완료 - [rustjava-null-guard-audit-remaining-runtime-entrypoints] ★★**런타임 전체를 «세고»(감사) 그중 «데이터 전송 버퍼» 18곳에 가드를 넣었다.** - 채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. 픽스처 `test-data/NullBufferGuards` **13케이스**(가드 **12/18** 커버). - ★★**감사 표 — 측정 2026-09-11 · ★트리 병기**(`bin/audit-null-guards` 가 아니라 **`scripts/audit-null-guards.py`** 가 정본): - **base `6da7d66f`** = **N 1,163 · M 50 · K 38** → **head `068fdb95`** = **N 1,163 · M 32 · K 20** ⇒ **ΔM = ΔK = 18** = 이 회차의 가드 수(자기정합). + 채택 제안 `2026-09-11-null-guard-string-init-and-arraycopy#p0`. 픽스처 `test-data/NullBufferGuards` **13케이스**(가드 **12/18** 커버 — ★**18곳 전건 단일 가드 개악으로 «측정»**). + ★★**감사 표 — 재측 2026-09-12 · ★트리 병기**(정본 = `scripts/audit-null-guards.py`): + **base `6da7d66f`** = **N 1,175 · M 52 · K 38** → **head** = **N 1,175 · M 34 · K 20** ⇒ **ΔM = ΔK = 18**(자기정합). + ★★**[게이트② R2 정정] 커밋한 스크립트가 M 을 «2 낮게» 셌다**(50/32 로 신고) — `fn` 정규식이 이름 뒤 `(` 를 즉시 요구하고 + 접두를 `pub `/`async ` 로만 받아 ★**제네릭 fn·`pub(crate)`/`pub(super)` 선언 34개가 통째로 안 보였다**. + ★고친 뒤 **세 트리 × 세 독립 구현이 전건 일치**한다(아래). + ★★**그리고 K 가 무사했던 것은 «설계가 아니라 운»이다** — 눈먼 fn 2건이 **우연히** `as_proto` 미등재였을 뿐이고, + 제네릭·`pub(crate)` 진입점이 **하나라도** 등재됐으면 K 가 **조용히** 낮아져 ★**이 감사가 「닫혔다」고 답했을 것**이다. + ⇒ 그 «눈먼 구간»을 스크립트 docstring 에 박았다. ★★**[게이트② F1 정정] 초판이 적은 「M 58 · K 46」은 «다른 트리»의 수다** — **`eaad8e9c`**(= 선행 9곳 회차 «이전» main)에서 재고 라벨만 `6da7d66f` 로 달았다. ⇒ ★**`46 − 18 = 28 ≠ 20`** 이라는 산술 모순이 그 혼입의 증상이었고, 검수자가 그것으로 잡았다. ★**이 저장소가 반복해 규탄한 「base 를 병기하지 않으면 수가 섞인다」의 교과서적 재현**이다 — ★그래서 이제 **두 트리를 나란히** 적는다. @@ -23,10 +29,13 @@ ⇒ ★**잔여 20건은 «가드를 넣을지»가 아니라 «null 이 합법인지»를 규격으로 먼저 가려야 한다** — 후속의 본체가 그것이다. ★개악 대조 **양방향 · 4종**: ⑴18곳 전건 되돌림 → `test_class` **FAILED** ⑵**단일 가드**(`get_chars(dst)`)만 제거 → **FAILED** ⑶`reader.rs read(buf)` 만 · ⑷`writer.rs write_chars(chars)` 만 제거 → **각각 FAILED** ↔ 복원 전건 **ok**. - ★★**[게이트② F2 정정] 초판의 「픽스처 18 중 12 커버」는 «그 시점엔 거짓»이었다 — 실측 10** (`Reader.read(char[])`·`Writer.write(char[])` 는 - `InputStreamReader`/`OutputStreamWriter` 가 오버라이드해 **base 구현에 닿지 못했다**). ⇒ ★**검수 권고를 채택해 그 2케이스를 «추가»했다** — - `Reader`/`Writer` 를 상속하되 추상 `(char[],int,int)` 만 구현하는 **중첩 클래스**(`$` 가 든 이름은 하버스가 건너뛴다)로 base 구현에 도달시켰고, - ⑶⑷ 개악이 그 도달을 **실행으로 증명**한다. ⇒ ★**지금은 진짜로 12/18** 이고 **미커버 6**(파일 핸들 필요분). + ★★**[게이트② R1 정정 — 근인 서술이 «뒤집혀» 있었다]** 종전 문안은 「`InputStreamReader` 가 **오버라이드해서** base 에 닿지 못한다」였는데 + ★**정반대다 — 오버라이드가 «없어서» base 에 닿는다.** 실측: `InputStreamReader` 는 `read` 를 **`([CII)I` 로만** 등재하고 + `Reader` 가 **`([C)I`** 를 등재한다(`OutputStreamWriter`/`Writer` 도 같은 형상) ⇒ 1인자 호출은 **`Reader::read`** 로 해소된다. + ⇒ ★**그래서 그때 더한 중첩 클래스 2케이스는 «이미 덮인 가드»를 또 덮었고 커버리지는 10/18 그대로였다**(검수 지적이 옳다) — **그 2케이스를 «지웠다».** + ★**대신 «미등재 서술자»를 겨눴다** — `read(char[],int,int)`·`write(char[],int,int)` **3인자 호출**이 그 두 가드에 닿는 유일한 경로다. + ★★**판정 축은 «클래스 이름»이 아니라 «서술자 + 등재 위치»다**(검수가 준 그 축을 그대로 썼다). + ★★**그리고 이번엔 «추론»이 아니라 «18곳 전건 단일 가드 개악»으로 쟀다** ⇒ ★**12/18 · 미커버 6**(전부 파일 핸들 필요분 · 목록은 worklog). ★★**[게이트② F4] `String.getChars` 가드를 «범위 검사 뒤»로 옮겼다 — «선택»이다.** JDK 는 범위를 먼저 보므로 «잘못된 범위 + null dst» 면 **SIOOBE 가 이긴다**. 초판 위치는 그 선후를 **NPE 로 뒤집었다**(계약 4 「동작 의미를 바꾸지 마라」 위반). ★픽스처에 **선후 잠금 케이스**를 넣었고, 가드를 옛 위치로 되돌리면 그 케이스가 **red** 다(개악 ⑸). diff --git a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json index 4665ffd4..de70b862 100644 --- a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json +++ b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json @@ -1,37 +1,36 @@ { "schema": 1, "date": "2026-09-11", - "title": "null 가드 감사 — base 6da7d66f 에서 N 1163 / M 50 / K 38 을 세고 java/io 데이터 전송 버퍼 18곳을 닫았다", + "title": "null 가드 감사 — base 6da7d66f 에서 N 1175 / M 52 / K 38 을 세고 java/io 데이터 전송 버퍼 18곳을 닫았다", "services": [ "RustJava" ], "taskId": "rustjava-null-guard-audit-remaining-runtime-entrypoints", - "summary": "선행 회차는 손으로 «열거한» 9곳을 닫았고 그 열거 자신이 arraycopy 의 dest 를 빠뜨렸다. 그래서 이번엔 술어로 셌다. ★수는 «트리와 함께» 읽어라 — base 6da7d66f: N 1,163 · M 50 · K 38 → head 068fdb95: N 1,163 · M 32 · K 20 ⇒ ΔM = ΔK = 18 = 이 회차의 가드 수(자기정합). sink 는 jvm/src/jvm.rs 가 &Box/impl AsClassInstance 로 받는 17개 메서드로 한정했다 — 「&p 가 나오면 deref」로 세면 &ClassInstanceRef 를 받는 헬퍼까지 물어 과대계상한다. ★[게이트² F1 정정] 초판이 적은 「M 58 · K 46」은 eaad8e9c(선행 9곳 회차 «이전» main)의 수인데 라벨만 6da7d66f 였다 — 46 − 18 = 28 ≠ 20 이라는 산술 모순이 그 혼입의 증상이었고 검수자가 그것으로 잡았다. 이 저장소가 반복해 규탄한 「base 를 병기하지 않으면 수가 섞인다」의 교과서적 재현이다. ★계측기를 먼저 검증했고 초판이 틀렸다: 선행 9곳을 정답지로 대니 3/9 만 잡혔고 근인은 off-by-one 이었다(고친 뒤 8/9). 남은 1건(init_with_string)은 helper 안에서 deref 하므로 이 계측은 절차간을 못 보고 따라서 K 는 «하한»이다 — 검수자가 선행 회차에 적은 「899 중 560 · 상한값」과 방향이 반대이므로 두 수를 같은 축으로 비교하지 마라. 닫은 범위는 java/io 데이터 전송 버퍼 + String.getChars 18곳이고, 잔여 20건은 «일괄 가드가 틀리는» 자리를 포함하므로(URL(context,spec,handler) 는 JDK 규격상 null handler 가 합법) 후속으로 남겼다.", + "summary": "선행 회차는 손으로 «열거한» 9곳을 닫았고 그 열거 자신이 arraycopy 의 dest 를 빠뜨렸다. 그래서 이번엔 술어로 셌다. ★수는 «트리와 함께» 읽어라(재측 2026-09-12) — eaad8e9c: 1,175/60/46 · base 6da7d66f: 1,175/52/38 · head: 1,175/34/20 ⇒ ΔM = ΔK = 18 = 이 회차의 가드 수. sink 는 jvm/src/jvm.rs 가 &Box/impl AsClassInstance 로 받는 17개 메서드로 한정했다. ★[게이트² F1] 초판이 적은 K 46 은 eaad8e9c 트리의 수인데 라벨만 base 였다 — 46−18=28≠20 이 증상. ★[게이트² R2] 그 다음 회차가 커밋한 스크립트는 M 을 2 낮게 셌다 — fn 정규식이 이름 뒤 ( 를 즉시 요구하고 접두를 pub/async 로만 받아 제네릭 fn 과 pub(crate)/pub(super) 선언 34개를 통째로 못 봤다(놓친 2건 = arrays.rs sort_primitive(array) · timer_task_queue.rs add(task)). 고친 뒤 세 트리 × 세 독립 구현이 전건 일치한다. ★그리고 K 가 흔들리지 않은 것은 설계가 아니라 «운»이다 — 그 2건이 우연히 as_proto 미등재였을 뿐이고, 등재된 제네릭 진입점이 하나만 있었어도 K 가 조용히 낮아져 이 감사가 「닫혔다」고 답했을 것이다. ★[게이트² R1] 커버리지 근인 서술도 뒤집혀 있었다 — InputStreamReader 가 오버라이드해서가 아니라 «오버라이드가 없어서» 1인자 호출이 Reader::read([C) 로 해소된다(등재 서술자 실측). 그래서 전 회차가 더한 중첩 클래스 2케이스는 이미 덮인 가드를 또 덮었고 커버리지는 10/18 그대로였다 ⇒ 그 2케이스를 지우고 3인자 호출로 그 두 가드를 실제로 덮었다. ★이번엔 추론하지 않고 가드 18곳을 하나씩 빼고 각각 빌드·실행해 측정했다: 12/18 · 미커버 6(전부 파일 핸들 필요분). 닫은 범위는 java/io 데이터 전송 버퍼 + String.getChars 18곳이고, 잔여 20건은 일괄 가드가 틀리는 자리를 포함하므로(URL(context,spec,handler) 는 JDK 규격상 null handler 가 합법) 후속으로 남겼다.", "changes": [ "rustjava-runtime/src/classes/java/io/{buffered_output_stream,data_input_stream,data_output_stream,file_input_stream,file_output_stream,input_stream_reader,output_stream_writer,random_access_file,reader,writer}.rs — 진입부 is_null() 가드 17곳", - "rustjava-runtime/src/classes/java/lang/string.rs — get_chars(dst) 가드 1곳 · ★[F4] 위치를 «범위 검사 뒤»로(JDK 의 예외 선후 보존)", - "test-data/src/NullBufferGuards.java + .class(중첩 $BareReader/$BareWriter 포함) + .txt — 13케이스(가드 12/18 커버)", - "★scripts/audit-null-guards.py — 감사 스크립트 신설(★CI 미배선 = 잠금이 아니라 감사)", - "STATE.md · REPORT.md — 감사 표(트리 병기)·술어·범위 근거·잔여 목록" + "rustjava-runtime/src/classes/java/lang/string.rs — get_chars(dst) 가드 1곳 · [F4] 위치를 «범위 검사 뒤»로(JDK 의 예외 선후 보존)", + "test-data/src/NullBufferGuards.java + .class + .txt — 13케이스(가드 12/18 커버) · ★[R1] 중첩 $BareReader/$BareWriter 와 그 2케이스 제거 · 3인자 호출 2케이스 추가 · 1인자 케이스 라벨 정정", + "scripts/audit-null-guards.py — 감사 스크립트(★CI 미배선 = 잠금이 아니라 감사) · ★[R2] fn 스캐너가 제네릭·모든 가시성을 받도록 수정 + 파라미터 깊이0 분할 + «눈먼 구간» docstring", + "STATE.md · REPORT.md — 감사 표(트리 병기)·술어·범위 근거·잔여 목록·정정 기록" ], "verification": [ "정상 → green: cargo test --test test_class ok · cargo fmt --check rc=0 · clippy stable/beta/wasm32 rc=0 · cargo test --all 554 passed / 0 failed / 1 ignored(★전체 실행) · check-worklog-json.py rc=0 · check-dod-ci-parity.py rc=0", - "개악 → red ⑴: 18곳 전건 되돌림 → test_class FAILED @ jvm/src/class_instance.rs:108 (Deref)", - "개악 → red ⑵: String.getChars 의 dst 가드만 제거 → FAILED @ :114 (DerefMut)", - "★개악 → red ⑶: reader.rs read(buf) 가드«만» 제거 → FAILED — 새 케이스 Reader.read(char[]) 가 base 구현에 실제로 도달함을 증명", - "★개악 → red ⑷: writer.rs write_chars(chars) 가드«만» 제거 → FAILED — 새 케이스 Writer.write(char[]) 동일 증명", - "★개악 → red ⑸: getChars 가드를 «범위 검사 앞»(초판 위치)으로 되돌림 → 선후 잠금 케이스가 caught NPE 를 내어 FAILED ⇒ F4 의 이동이 하중을 진다", - "계측기 검증: base(6da7d66f)/head 차집합이 이 회차 가드 18건과 일치 · 선행 회차 정답지로는 8/9(나머지 1건은 절차간이라 원리적 위음성)", + "★커버리지 측정(추론 아님): 가드 18곳을 하나씩 제거하고 각각 빌드·실행 → 12곳 red(덮인다) · 6곳 green(file_input_stream · file_output_stream · random_access_file×4)", + "★3인자 케이스가 실제로 무는 것을 확인: 수정 전 input_stream_reader.read(buf)·output_stream_writer.write(chars) 는 단일 제거해도 green 이었고, 3인자 케이스 추가 후 둘 다 red 로 바뀌었다", + "개악 → red: 18곳 전건 되돌림 → FAILED @ class_instance.rs:108 · getChars 가드 위치 원복 → 선후 잠금 케이스가 FAILED", + "★감사 스크립트 3트리 재측(수정 후): eaad8e9c 1,175/60/46 · 6da7d66f 1,175/52/38 · head 1,175/34/20 — 검수 독립 구현과 전건 일치", "픽스처 형식: javac --release 8 ⇒ major 52 · invokedynamic/CP 태그 15~18 0건" ], "issues": [ - "★[게이트² F1] 초판이 K 46 을 적었는데 그것은 eaad8e9c 트리의 수였다(라벨은 6da7d66f). 46 − 18 = 28 ≠ 20 이 증상. ⇒ 정정했고 이제 base·head 를 나란히 적는다.", - "★[게이트² F2] 초판의 「픽스처 18 중 12 커버」는 그 시점엔 거짓이고 실측 10 이었다 — InputStreamReader/OutputStreamWriter 가 오버라이드해 Reader/Writer base 구현에 닿지 못했다. ⇒ 검수 권고를 채택해 중첩 클래스(BareReader/BareWriter)로 2케이스를 추가했고 지금은 진짜로 12/18 이다.", - "★[게이트² F4] String.getChars 가드가 JDK 의 예외 선후를 뒤집고 있었다(잘못된 범위 + null dst 면 JDK 는 SIOOBE). ⇒ 범위 검사 뒤로 «옮겼고» 선후 잠금 케이스를 넣었다 — issues 기재가 아니라 이동을 선택했다.", - "★[게이트² F5] 감사 스크립트를 커밋했다 — 초판의 「배선 없는 검사기는 낡는다」를 뒤집는다. 근거는 이 회차 자신이다: 작성자가 자기 수를 교차검증할 수단이 없어 F1 이 났고 검수자는 술어를 산문에서 재구현해야 했다(그래서 N 1,163↔1,130 · M 50↔52 가 갈렸다 — ★판단을 이끄는 K 는 38/20 으로 정확 일치).", - "★픽스처 커버리지는 여전히 18 중 12 다 — FileInputStream.read · FileOutputStream.write · RandomAccessFile 4종은 실파일 핸들이 필요해 못 덮었다. 가드는 들어갔으나 픽스처가 잠그지 않는다 ⇒ proposals[1].", - "★이 계측은 절차간 deref 와 섀도잉을 못 본다. 전자는 위음성(init_with_string), 후자는 위양성(url.rs 의 handler) ⇒ N·M·K 를 «정확한 수»가 아니라 «하한 + 알려진 오탐 1건»으로 읽어라.", - "★부수 발견 — 패닉 impl 은 둘이 아니라 셋이다: Deref · DerefMut · AsClassInstance::as_class_instance(as_deref().unwrap()). 셋째는 monitor_* 경로다." + "★[게이트² R1] 커버리지 근인 서술이 뒤집혀 있었다 — 오버라이드가 «있어서»가 아니라 «없어서» base 로 해소된다. 판정 축은 클래스 이름이 아니라 «서술자 + 등재 위치»다. 전 회차가 더한 2케이스는 중복이었고 제거했다.", + "★[게이트² R2] 커밋한 감사 스크립트가 M 을 2 낮게 셌다(제네릭·pub(crate) 눈먼 구간 34선언). 고쳤고 세 구현이 일치한다. ★전 회차의 「내 수가 맞고 검수는 구현 세부 차」라는 취지의 문안을 철회한다 — 틀린 것은 내 쪽이었다.", + "★[게이트² R2] K 가 무사했던 것은 «운»이다 — 눈먼 fn 2건이 우연히 as_proto 미등재였다. 등재된 제네릭 진입점이 하나만 있었어도 K 가 조용히 낮아져 감사가 「닫혔다」고 답했을 것이고, 그것이 감사 도구의 «침묵 실패»다. 눈먼 구간을 docstring 에 박았다.", + "★[게이트² R3] 잔여 20건 분해가 오기였다(open_connection 을 java/net 으로, java/util/regex 3건을 java/util 에 합산). 도구 출력 기준 = java/io 4 · java/net 3 · java/util 6 · java/util/regex 3 · java/util/zip 3 · org/rustjava/net 1.", + "★[게이트² F4] String.getChars 가드가 JDK 의 예외 선후를 뒤집고 있었다 ⇒ 범위 검사 뒤로 옮기고 선후 잠금 케이스를 넣었다.", + "★픽스처 커버리지는 12/18 이고 미커버 6 은 전부 실파일 핸들이 필요하다 — 가드는 들어갔으나 테스트가 잠그지 않는다 ⇒ proposals[1].", + "★이 계측은 절차간 deref 와 섀도잉을 못 본다. 전자는 위음성(init_with_string), 후자는 위양성(url.rs 의 handler) ⇒ N·M·K 를 «하한 + 알려진 오탐 1건»으로 읽어라.", + "★부수 발견 — 패닉 impl 은 둘이 아니라 셋이다: Deref · DerefMut · AsClassInstance::as_class_instance. 셋째는 monitor_* 경로다." ], "proposals": [ { diff --git a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md index 0b9e25fd..dcd45930 100644 --- a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md +++ b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.md @@ -5,26 +5,38 @@ 선행 회차는 **손으로 열거한** 9곳을 닫았고, ★그 열거 자신이 `arraycopy` 의 `dest` 를 빠뜨렸다. 그래서 이번엔 **술어로 셌다**. -## 감사 — 수와 술어 (측정 2026-09-11 · 트리 `origin/main` `6da7d66f`) +## 감사 — 수와 술어 (★**재측 2026-09-12** · 트리별 병기) ★★**[게이트② F1 정정] 수는 «트리와 함께» 적는다 — 초판이 두 트리를 섞었다.** 정본 = **`scripts/audit-null-guards.py`**(이 회차가 커밋했다). 재현: `python3 scripts/audit-null-guards.py []`. | 트리 | N | M | K | |---|---|---|---| -| **base `6da7d66f`**(= 이 PR 의 `merge-base`) | 1,163 | **50** | ★**38** | -| **head `068fdb95`** | 1,163 | **32** | **20** | -| **Δ** | 0 | **18** | ★**18** = 이 회차의 가드 수(자기정합) | +| `eaad8e9c`(두 회차 전 · ★초판이 잘못 인용한 트리) | 1,175 | 60 | 46 | +| **base `6da7d66f`**(= 이 PR 의 `merge-base`) | **1,175** | **52** | ★**38** | +| **head** | **1,175** | **34** | **20** | +| **Δ**(base→head) | 0 | **18** | ★**18** = 이 회차의 가드 수(자기정합) | + +★★**[게이트² R2] 이 표의 M 은 «한 번 더» 틀렸었다 — 커밋한 스크립트가 2 낮게 셌다**(50/32 로 신고). +근인은 `fn` 정규식 한 줄이다: 이름 뒤 `(` 를 **즉시** 요구하고 접두를 `pub `/`async ` 로만 받아 +★**제네릭 fn(`fn sort_primitive(`)과 `pub(crate)`/`pub(super)` 선언 34개가 통째로 안 보였다** +(놓친 2건 = `java/util/arrays.rs sort_primitive(array)` · `java/util/timer_task_queue.rs add(task)`). +★고친 뒤 **세 트리 × 세 독립 구현(커밋본·검수 재구현·전 검수)이 전건 일치**한다. +★★**그리고 K 가 흔들리지 않은 것은 «설계가 아니라 운»이다** — 그 2건이 **우연히** `as_proto` 미등재였을 뿐이고, +제네릭·`pub(crate)` 진입점이 **하나라도** 등재됐으면 ★**K 가 조용히 낮아져 이 감사가 「닫혔다」고 답했을 것**이다. +⇒ 그 «눈먼 구간»을 스크립트 docstring 에 박았다(무엇을 못 보는지). 술어: **N** = `rustjava-runtime/src/**` 의 fn 파라미터 중 타입이 `ClassInstanceRef<…>`(★`this`·`_` 접두 제외) · **M** = N 중 그 파라미터가 **deref 강제 sink** 에 `&p`/`&mut p`(또는 `p.as_class_instance()`)로 도달하고 ★**그 «앞»에 `p.is_null()` 가 없는** 것 · **K** = M 중 그 fn 이 같은 파일 `JavaMethodProto::new(… Self::fn …)` 에 등재 = **게스트가 부를 수 있다**. -★★**초판의 「M 58 · K 46」은 «다른 트리»의 수다** — **`eaad8e9c`**(선행 9곳 회차 «이전» main)에서 재고 라벨만 `6da7d66f` 로 달았다. +★★**초판의 「M 58 · K 46」은 «다른 트리»의 수다** — **`eaad8e9c`**(선행 9곳 회차 «이전» main)에서 재고 라벨만 `6da7d66f` 로 달았다 +(★그 트리의 «옳은» 수는 **60/46** 이다 — 58 은 위 R2 의 undercount 가 겹친 값이다). ⇒ ★**`46 − 18 = 28 ≠ 20`** 이라는 산술 모순이 그 혼입의 «증상»이었고, 검수자가 술어를 재구현해 그것으로 잡았다. ★**이 저장소가 반복해 규탄한 「base 를 병기하지 않으면 수가 섞인다」의 교과서적 재현이다.** -※검수 재구현치는 N **1,130** · M **52/34**(K 는 **38/20 으로 정확 일치**) — ★**판단을 이끄는 K 가 두 구현에서 같다.** -N·M 의 차는 구현 세부(파라미터 계수 방식)이고, ★**이제 스크립트가 커밋돼 있으므로 «어느 쪽이 맞나»는 돌려서 답한다.** +★★**[철회] 전 회차가 「N·M 의 차는 구현 세부이고 내 쪽이 정본」이라는 뜻으로 적은 문안을 «철회»한다 — 틀린 것은 내 쪽이었다.** +검수 재구현(N 1,175 · M 60/52/34)이 옳았고, 커밋본을 고치니 **그 값에 정확히 수렴**했다. +※그보다 앞선 검수의 N **1,130** 은 또 다른 구현이고, M/K 는 **52·34 / 38·20 으로 이미 일치**했다. **deref 강제 sink = 17개**(`jvm/src/jvm.rs` 의 시그니처로 확정 — `&Box` · `&mut Box<…>` · `impl AsClassInstance`): `array_element_type` `array_length` `array_raw_buffer` `array_raw_buffer_mut` `get_field` `interrupt_java_thread` @@ -33,7 +45,7 @@ N·M 의 차는 구현 세부(파라미터 계수 방식)이고, ★**이제 스 ★**왜 sink 를 한정했는가**: 초판은 「`&p` 가 나오면 deref」로 셌는데 그것은 **과대계상**이다 — `Self::value_range(jvm, &value)` 처럼 **`&ClassInstanceRef` 를 받는** 헬퍼는 deref 하지 «않는다». -한정 전 220 → 한정 후 152 → (계측기 버그 수정 후) **58**. +한정 전 220 → 한정 후 152 → (off-by-one 수정 후) 58 → ★(**R2 의 `fn` 눈먼 구간까지 수정 후**) **60**. ※전부 `eaad8e9c` 기준. ## ★계측기를 «먼저» 검증했다 — 그리고 초판이 틀렸다 @@ -65,11 +77,17 @@ N·M 의 차는 구현 세부(파라미터 계수 방식)이고, ★**이제 스 ## ★남긴 20건 — 「가드를 넣어라」가 «아니다» ``` -java/io read_utf_from_input(input) · init(file)×2 · init_with_file(file) -java/net init(url) · init_with_context_spec_handler(handler) · set_url(url) · open_connection(url) -java/util init(list)×4 · on_access(map) · init(map) · init(pattern) · matches(pattern) · split_with_limit(input) -java/util/zip init_with_zip_entry(zip_entry) · get_input_stream(entry) · init(file) +java/io 4 read_utf_from_input(input) · init(file) · init_with_append(file) · init_with_file(file) +java/net 3 init(url) · init_with_context_spec_handler(handler) · set_url(url) +java/util 6 init(list)×4 [abstract_list · array_list_itr · linked_list_itr · vector_itr] + · on_access(map) · init(map) +java/util/regex 3 init(pattern) · matches(pattern) · split_with_limit(input) +java/util/zip 3 init_with_zip_entry(zip_entry) · get_input_stream(entry) · init(file) +org/rustjava/net 1 open_connection(url) + ── 합계 20 ``` +★★**[게이트² R3 정정] 전 회차의 분해는 오기였다** — `open_connection` 을 `java/net` 으로 세고 `java/util/regex` 3건을 `java/util` 에 합쳐 +「4·8·3·1」로 적었다. 위가 도구 출력 그대로다. ★★**일괄 가드는 «틀린다»** — `URL(URL context, String spec, URLStreamHandler handler)` 는 JDK 규격상 **`handler == null` 이 합법**이다(기본 핸들러를 쓰라는 뜻). 실제로 이 repo 의 구현도 그 파라미터를 **섀도잉해 무시**한다 (⇒ 그 1건은 이 계측의 **오탐**이기도 하다 — 이름 기반 절차내 스캔은 섀도잉을 못 본다). @@ -79,27 +97,34 @@ java/util/zip init_with_zip_entry(zip_entry) · get_input_stream(entry) · init( - **정상 → green**: `cargo test --test test_class` **ok** · DoD 7종 rc=0 · `cargo test --all` **554/0/1** (★계수 불변이 맞다 — `test_class` 는 `test-data/` 를 **한 테스트 함수**가 순회한다). -- **개악 → red**: ⑴18곳 **전건** 되돌림 → **FAILED** `class_instance.rs:108`(`Deref`) - ⑵★**단일 가드**(`String.getChars` 의 `dst`)만 제거 → **FAILED** `:114`(`DerefMut`) ⇒ 픽스처가 **케이스별로** 문다. - -## 픽스처 커버리지 — ★**13케이스가 가드 12/18 을 문다** (F2 정정 + 권고 채택) - -★★**[게이트② F2] 초판의 「18 중 12」는 «그 시점엔 거짓»이었다 — 실측 10.** -근인: `new InputStreamReader(...).read(char[])` · `new OutputStreamWriter(...).write(char[])` 는 -그 **서브클래스가 오버라이드**하므로 `Reader`/`Writer` 의 **base 구현에 닿지 못한다** ⇒ 그 2곳은 «덮은 줄 알았지만» 안 덮였다. - -⇒ ★**검수 권고를 채택해 2케이스를 «추가»했다**: `Reader`/`Writer` 를 상속하되 **추상 `(char[],int,int)` 만** 구현하는 -**중첩 클래스**(`NullBufferGuards$BareReader`·`$BareWriter` — ★하버스가 `$` 든 이름을 건너뛰므로 별 `.txt` 가 필요 없다)로 base 구현에 도달시켰다. -★**도달을 «실행»으로 증명했다** — `reader.rs read(buf)` 만, `writer.rs write_chars(chars)` 만 각각 제거하면 **각각 red** 다. - -**덮는 12** = `BufferedOutputStream.write` · `DataInputStream.readFully` · `DataOutputStream`×3 · -`InputStreamReader.read` · `OutputStreamWriter.write` · `Writer.write(String)` · `Writer.write(String,int,int)` · -★`Reader.read(char[])` · ★`Writer.write(char[])` · `String.getChars` -(+13번째 케이스는 `getChars` 의 **예외 선후 잠금** — 같은 가드의 «위치»를 문다). - -★**덮지 «못한» 6곳** = `FileInputStream.read` · `FileOutputStream.write` · `RandomAccessFile` 4종 — -**실파일 핸들이 있어야 인스턴스가 생기고**, 테스트가 작업 디렉터리에 파일을 만드는 것을 피했다. -⇒ ★**그 6곳은 «가드는 들어갔으나 픽스처가 잠그지 않는다»** — 다음 회차가 임시 파일 픽스처를 세우면 닫힌다(`proposals[1]`). +- **개악 → red**: ★★**커버리지를 «추론»하지 않고 «측정»했다 — 18곳을 «하나씩» 빼고 각각 빌드·실행했다**(2회전 = 36빌드). + ⇒ **12곳 red(덮인다) · 6곳 green(안 덮인다)**. 그 밖에 `getChars` **가드 위치** 원복 개악도 red. + ★**이 측정이 R1 을 잡는 «지문»이다** — 이미 덮인 가드를 또 덮으면 red 가 «안» 나므로, 중복 케이스가 즉시 드러난다. + +## 픽스처 커버리지 — ★**측정값 12/18 · 미커버 6** (게이트² R1 정정) + +★★**[R1] 전 회차의 근인 서술이 «뒤집혀» 있었다 — 그 문장이 다음 사람을 오도한다.** +종전 문안: 「`InputStreamReader` 가 **오버라이드하므로** `Reader` base 에 닿지 못한다」 ⇒ ★**정반대다.** +**등재 서술자 실측**: `InputStreamReader` 는 `read` 를 **`([CII)I` 로만** 등재하고 `Reader` 가 **`([C)I`** 를 등재한다 +(`OutputStreamWriter` `write([CII)V` ↔ `Writer` `write([C)V` 도 같은 형상). +⇒ ★**오버라이드가 «없어서» 1인자 호출이 `Reader::read([C)` 로 해소된다.** + +⇒ 그래서 전 회차가 더한 **중첩 클래스 2케이스는 «이미 덮인 가드»를 또 덮었고** 커버리지는 **10/18 그대로**였다(검수 지적이 옳다). +★**그 2케이스와 `$BareReader`/`$BareWriter` 를 지웠다.** +★**대신 «그 가드가 실제로 사는 서술자»를 겨눴다** — `read(char[],int,int)` · `write(char[],int,int)` **3인자 호출**. +★★**판정 축은 «클래스 이름»이 아니라 «서술자 + 등재 위치»다**(검수가 준 축 그대로). + +★★**그리고 이번엔 «추론»하지 않고 «쟀다» — 가드 18곳을 하나씩 빼고 각각 빌드·실행**: + +| | 가드 | 판정 | +|---|---|---| +| **덮는다(12)** | `buffered_output_stream.write_bytes` · `data_input_stream.read_fully` · `data_output_stream`×3 · ★`input_stream_reader.read` · ★`output_stream_writer.write` · `reader.read` · `writer.write_chars` · `writer.write_string` · `writer.write_string_offset` · `string.get_chars` | 단일 제거 → **red** | +| ★**미커버(6)** | `file_input_stream.read_array` · `file_output_stream.write_bytes_offset` · `random_access_file`×4 | 단일 제거 → **green** | + +★**미커버 6 은 전부 «실파일 핸들이 있어야 인스턴스가 생기는» 것**이고, 테스트가 작업 디렉터리에 파일을 만드는 것을 피했다. +⇒ ★**「가드는 들어갔으나 픽스처가 잠그지 않는다」** — `proposals[1]` 이 진다. +※★전 회차가 「reader/writer 단일 개악이 red 였으니 새 케이스가 문다」고 적은 것은 **논증이 안 된다** — +그 red 는 **기존 `InputStreamReader` 케이스만으로도** 났다. ★**한 가드를 두 케이스가 덮으면 단일 개악은 그 둘을 구별하지 못한다.** ## ★F4 — `String.getChars` 가드의 «위치»를 고쳤다 (선택했다) diff --git a/scripts/audit-null-guards.py b/scripts/audit-null-guards.py index 48f58c46..1204b604 100644 --- a/scripts/audit-null-guards.py +++ b/scripts/audit-null-guards.py @@ -29,6 +29,20 @@ AsClassInstance, all three of which are `.unwrap()` on an Option (see `jvm/src/class_instance.rs`). That unwrap is the host abort. +Blind spots this scanner HAS HAD (2026-09-12, gate2 R2) — read before trusting a count: + * The first committed version demanded `(` immediately after the function name and only + accepted `pub `/`async ` prefixes. That hid 34 declarations repo-wide — every generic + fn (`fn sort_primitive(`) and every `pub(crate)`/`pub(super)` fn — and undercounted + M by 2. Both are handled now (FN + the balanced `<…>` scan in split_fns). + * ★Why that mattered more than the 2: K survived only by luck. The two hidden functions + happened not to be registered in as_proto. Had ONE generic or `pub(crate)` entry point + been registered, K would have dropped SILENTLY and this audit would have answered + "closed" while the hole stayed open. A miscount here is not a rounding error; it is the + audit lying in the safe direction. + * Parameters are split on depth-0 commas and the type is anchored at the parameter's + start, so a `ClassInstanceRef` nested inside another parameter's type is not counted + as its own parameter. + Known limits — K is a LOWER bound, and not a clean one: * intraprocedural only. A fn that derefs inside a helper is invisible (this is why `String::init_with_string` was missed: it derefs inside `Self::value_range`). @@ -57,8 +71,21 @@ "object_notify", "object_wait_prepare", "put_field", "shallow_clone", "store_array", ) -FN = re.compile(r"(?:^|\n)\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)\s*\(") -PARAM = re.compile(r"([A-Za-z0-9_]+)\s*:\s*(?:&\s*)?(?:mut\s+)?ClassInstanceRef\s*<") +# Matches `fn ` under any visibility/qualifier. Generics are NOT matched here: +# `fn sort_primitive(` needs a balanced `<…>` scan, which split_fns does. An earlier +# version demanded `(` immediately after the name and only accepted `pub `/`async ` as +# prefixes; that silently hid 34 declarations repo-wide (generic fns and `pub(crate)`/ +# `pub(super)`), undercounting M by 2. See "blind spots" in the module docstring. +FN = re.compile( + r"(?:^|\n)\s*" + r"(?:pub\s*(?:\([^)]*\)\s*)?)?" # pub, pub(crate), pub(super), pub(in …) + r"(?:default\s+)?(?:const\s+)?(?:async\s+)?(?:unsafe\s+)?" + r"(?:extern\s+\"[^\"]*\"\s+)?" + r"fn\s+([A-Za-z0-9_]+)" +) +# Anchored at the START of a parameter, so a `ClassInstanceRef` nested inside another +# parameter's type (a closure bound, say) is not miscounted as its own parameter. +PARAM = re.compile(r"^(?:mut\s+)?([A-Za-z0-9_]+)\s*:\s*(?:&\s*)?(?:mut\s+)?ClassInstanceRef\s*<") PROTO = re.compile(r'JavaMethodProto::new\(\s*"[^"]*"\s*,\s*"[^"]*"\s*,\s*Self::([A-Za-z0-9_]+)') SINK_CALL = r"\.\s*(?:" + "|".join(SINKS) + r")\s*\(\s*&\s*(?:mut\s+)?\**\s*" @@ -79,7 +106,19 @@ def _balanced(text, start, open_ch, close_ch): def split_fns(text): """Yield (name, params_src, body_src). All indices address `text` itself.""" for m in FN.finditer(text): - paren = m.end() - 1 + i = m.end() + while i < len(text) and text[i].isspace(): + i += 1 + if i < len(text) and text[i] == "<": # generic parameter list + i = _balanced(text, i, "<", ">") + if i < 0: + continue + i += 1 + while i < len(text) and text[i].isspace(): + i += 1 + if i >= len(text) or text[i] != "(": + continue + paren = i close = _balanced(text, paren, "(", ")") if close < 0: continue @@ -92,13 +131,31 @@ def split_fns(text): yield m.group(1), text[paren + 1:close], text[brace:end] +def split_params(params): + """Split a parameter list on depth-0 commas, ignoring nested <>, (), [].""" + out, depth, start = [], 0, 0 + for i, ch in enumerate(params): + if ch in "<([": + depth += 1 + elif ch in ">)]": + depth -= 1 + elif ch == "," and depth == 0: + out.append(params[start:i]) + start = i + 1 + out.append(params[start:]) + return out + + def audit(root): rows, n = [], 0 for path in sorted(pathlib.Path(root).rglob("*.rs")): text = path.read_text(encoding="utf-8", errors="replace") protos = set(PROTO.findall(text)) for name, params, body in split_fns(text): - for pm in PARAM.finditer(params): + for raw in split_params(params): + pm = PARAM.match(raw.strip()) + if not pm: + continue p = pm.group(1) if p == "this" or p.startswith("_"): continue diff --git a/test-data/NullBufferGuards$BareReader.class b/test-data/NullBufferGuards$BareReader.class deleted file mode 100644 index f83afb0cf880557b25debe1949c190a3e27b6077..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 385 zcmZ8cO-sW-5Pg%R-5O&*)E-m>4<1?%_TsU4Xg~=_4=P?fZJRAbVgeiEukzwa@Zb;d zM@iobevpNEZ+7O*W9IYg{R6-mc0+jZ6@myYv<2M<{j4WhIk`4EHCE7`W<~aNF7QX= zJB5zmU|#3>tX{2*y{xrOtK*rr=0_jIWoiVytE@2d`f*|GtzP7;1eT5hHM)TF3m;eRKuqEsar@8B}HLuoa?GGmBO~5-&E4+ZxI|p@iO;(25*C4gV`Zj zT;=2q^2&r`MArs-#7#s*C*3CpWY2pc{?`ro+xoTK`|eWFv>S4Rt}~!bVSpWU E)^h?wVE_OC diff --git a/test-data/NullBufferGuards$BareWriter.class b/test-data/NullBufferGuards$BareWriter.class deleted file mode 100644 index 42a287bfd1e40beda29c8ba508c285d1c27c6677..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 432 zcmaivyGjE=6o&tkY%+0OlXwYuK}cbdq%ciNvB-k5AZe6XSWS{y92s|ECZo?~C8Y2H zd?@iup+ZtPa4wwr{&V^KT73W*p%bHmsz8*WhB~47BwyrEm&1Fjm&y|A7rM~POG35R zp9(YxClhbX*yp*j*IwF%I~z-@e)uRY7mCoj(S@4$=b5s1a%Q+vdkq~4qW6$yS%1ov z+<5oMac)Y-Kk;qp?Oa{yfOpoXoQKT_NmdlfrbaraoWMSzy-sqczm3p^;1k1(*!V~g zwm3WGNTD)#2fcB^GWOvcIAp(#gndAd*n)TiJ7!?#w`0J9cmoTgn*U>6-m}H}3fkD^ M7_&@Z4+m&`1ErHk;{X5v diff --git a/test-data/NullBufferGuards.class b/test-data/NullBufferGuards.class index da5ac5b3b554be70a0df68a6170bea1a90b21221..fb0ea04ddb29f1f48c9a29bb1e646dcee1e56f03 100644 GIT binary patch delta 908 zcmY+CTTEP46o$Wb4#Ny1v;~V`DKM1+GsB2-Y10C|q?D!u0?KeHa;ekl2*^b`J;SXc zv{0ojeL8L5jSt3n3mBQirs69;X`%^<_N{#|>7zWNu7RY{ob0{U{@1_OUgzH{k+YQJ z{^{@Ue*mgkUU9dZet%mud^s8`kE!uS2E(zQKA&e|DC`N2jmNE!rTFMB#hx{r0)ejB zkfrp=r1>AeHEumm*d^J2n(I<(Uo`U%2ZqM1eX9p(FHg^WlzJ}_u`Lc!M7GYwAW?7tlS@{t`%EtXsMbZTug9zMiSdDz(f{QN&6jg;Tr8zQ_)=%HFIJMLr|9CFy@)hti^R zoANtec_!Q1x7Wxtii{Fwc5EeH?MeHTHrln%3|#EtMfQvb~Ilyvx5i77fNOS*2WZ*b>dVf(*- delta 965 zcmZ{i%}-Qk6vlt&4)eh%wxR|MF-%jnGZn!`{6JA#sFDc+77#)J^$HgT9Y?)0BYvPG z1(jAo<=Q{MxM-TzHq;aniK0tQT$o5=7hQDGg|wO%;c<)h%@4;mCwe##Y*QqGnu80mf3CDrM5!2D`wj5oslhm z`h=qTbUa>_?&-1YnzU(mCs$UPwv{ap62s$$Jpqmg%UbQels4&i;e#$mKh`B%A0!cU z+azcUkaWKGMj~m!Fb@Xlq<1{8%u?nq74mIJN*ng9^XD0aSr-c$z^B6w^eHvMiU9ku8y0@q@~S*?_cvS$qq$` z^B8Iz^U;eB;d{+iD2k%@%PC|+i7=$mJ%$D3GRy_NqNO=~c*)WG71Q|JrpVnreP_9U zRPth)9FhmrO>(%^G5v-4@kt7%NvM7-s~>meJX_`bl*cQcNlv&*@#(DcY1h2;C!gf6 zR^?|3>s@}%sm!%9v+|sRL)#wI#c?PMbs6&ztJr>JC*9s1yH`sD45@(n*xo#7_m@(VTm zrq^$?i@%6+k6IqFo2NAJj5=ni=O1nTA5FZ{VcyWH2JRK1MRIAA#k5O^4hgeg*3c;> z?2~d#siaG4>6RvXr3Fh`>5=`qr@b7Olf>mbnWHkK+c-=@u5es#kd#}blimSbAyZOX-klzId#nA=aLg%P_g5HpKzev4IO?4YF)w)T9~67^P_&p X73GGSH>Q}cET)ThRX--7c#XjS8tmC` diff --git a/test-data/NullBufferGuards.txt b/test-data/NullBufferGuards.txt index 4a49a19a..c60b679d 100644 --- a/test-data/NullBufferGuards.txt +++ b/test-data/NullBufferGuards.txt @@ -8,17 +8,17 @@ DataOutputStream.writeChars(String): caught NPE DataOutputStream.writeUTF(String): caught NPE -InputStreamReader.read(char[]): +Reader.read(char[]) via InputStreamReader: caught NPE -OutputStreamWriter.write(char[]): +Writer.write(char[]) via OutputStreamWriter: caught NPE Writer.write(String): caught NPE Writer.write(String,int,int): caught NPE -Reader.read(char[]): +InputStreamReader.read(char[],int,int): caught NPE -Writer.write(char[]): +OutputStreamWriter.write(char[],int,int): caught NPE String.getChars(int,int,char[],int): caught NPE diff --git a/test-data/src/NullBufferGuards.java b/test-data/src/NullBufferGuards.java index d4720a37..744a2129 100644 --- a/test-data/src/NullBufferGuards.java +++ b/test-data/src/NullBufferGuards.java @@ -5,33 +5,8 @@ import java.io.DataOutputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; class NullBufferGuards { - // Reader/Writer only expose the array overloads through their own base implementation, - // so a subclass that overrides just the abstract (char[],int,int) form reaches them. - // Nested on purpose: the fixture harness skips class files whose name contains '$'. - static class BareReader extends Reader { - public int read(char[] cbuf, int off, int len) { - return -1; - } - - public void close() { - } - } - - static class BareWriter extends Writer { - public void write(char[] cbuf, int off, int len) { - } - - public void flush() { - } - - public void close() { - } - } - public static void main(String[] args) throws Exception { // Each case passes a null buffer to a java.io data-transfer method. The JDK spec // mandates NullPointerException; the runtime used to dereference and abort the host. @@ -78,7 +53,7 @@ public static void main(String[] args) throws Exception { System.out.println("caught NPE"); } - System.out.println("InputStreamReader.read(char[]):"); + System.out.println("Reader.read(char[]) via InputStreamReader:"); try { new InputStreamReader(source).read((char[]) null); System.out.println("should not reach"); @@ -86,7 +61,7 @@ public static void main(String[] args) throws Exception { System.out.println("caught NPE"); } - System.out.println("OutputStreamWriter.write(char[]):"); + System.out.println("Writer.write(char[]) via OutputStreamWriter:"); try { new OutputStreamWriter(sink).write((char[]) null); System.out.println("should not reach"); @@ -110,17 +85,17 @@ public static void main(String[] args) throws Exception { System.out.println("caught NPE"); } - System.out.println("Reader.read(char[]):"); + System.out.println("InputStreamReader.read(char[],int,int):"); try { - new BareReader().read((char[]) null); + new InputStreamReader(source).read(null, 0, 0); System.out.println("should not reach"); } catch (NullPointerException e) { System.out.println("caught NPE"); } - System.out.println("Writer.write(char[]):"); + System.out.println("OutputStreamWriter.write(char[],int,int):"); try { - new BareWriter().write((char[]) null); + new OutputStreamWriter(sink).write((char[]) null, 0, 0); System.out.println("should not reach"); } catch (NullPointerException e) { System.out.println("caught NPE"); From d57ff6b04ef20e4d82970310521fa584015f167e Mon Sep 17 00:00:00 2001 From: jun0 Date: Sat, 12 Sep 2026 01:54:14 +0900 Subject: [PATCH 4/4] docs: record gate3 execution in the ledger [rustjava-null-guard-audit-remaining-runtime-entrypoints] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STATE.md 완료 항목에 게이트③ 집행 블록(PR #37 · --merge · upstream-sync 등재 repo) + worklog .json issues 에 집행 기록 · 검수 minor 3건 중 «중첩 타입 파라미터 현재 0건» 측정 기록. 코드(.rs) 변경 0. ★태그는 «기반 티켓 id» 다 — 머지 티켓 id(-merge)를 달지 않았다(계약 2-b⒜·4-D). Co-Authored-By: Claude Fable 5 --- STATE.md | 9 +++++++++ ...2026-09-11-null-guard-audit-and-io-buffer-guards.json | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/STATE.md b/STATE.md index a78e80ab..f2e56988 100644 --- a/STATE.md +++ b/STATE.md @@ -40,6 +40,15 @@ JDK 는 범위를 먼저 보므로 «잘못된 범위 + null dst» 면 **SIOOBE 가 이긴다**. 초판 위치는 그 선후를 **NPE 로 뒤집었다**(계약 4 「동작 의미를 바꾸지 마라」 위반). ★픽스처에 **선후 잠금 케이스**를 넣었고, 가드를 옛 위치로 되돌리면 그 케이스가 **red** 다(개악 ⑸). ★`cargo test --all` **554 / 0 / 1**(★**전체 실행** · 증감 0 이 맞다 — `test_class` 는 픽스처를 한 함수가 순회한다) · DoD 7종 rc=0. + ★★**게이트③ 착지 — PR #37 · `--merge`**(등재 repo · 스쿼시는 부모 2개를 1개로 접어 계보를 지운다). + 게이트② **3회차 만에 approve**(초판 → `-fix` → `-fix2`) · 핀 `77dec927` **불이동**(동봉 전 실측) · `ci-presence` **rc=0 CI_GREEN** · + 자식 PR **0건** · ★**배포 워크플로 0개 ⇒ 배포 0**. + ★★**반려 셋이 전부 «수»였다** — F1(두 트리 혼입 · K 46↔38) · R2(스크립트가 M 을 2 낮게) · R1(커버리지 근인 역전 · 10↔12). + ⇒ ★**이 리니지가 남긴 규율: 「감사 회차는 «계측기»부터 정답지로 검증하라」**(그 검증이 off-by-one 과 눈먼 구간을 각각 잡았다). + ★**검수자가 «독립 재구현 + 행 단위 대칭차 0» 로 세 트리를 재현**했고, `da3727c`(수정 전)에서 두 가드가 **green** 임을 대조군으로 확인했다. + ★**검수 minor 3건은 이 회차가 «고치지 않았다»**(계약 7 — 총괄 승계 판정): ⒜`.gitignore` 에 `__pycache__` 부재 + ⒝새 `PARAM` 앵커가 `Option>` 같은 **중첩 타입 파라미터를 세지 않는다** — ★**검수 실측 «현재 0건»**(기록해 둔다 · 다음 회차가 다시 재지 않도록) + ⒞감사 스크립트 CI 미배선(= 선언된 선택이지 결함 아님). ★★**[게이트② F5] 감사 스크립트를 «커밋했다» — 초판의 「배선 없는 검사기는 낡는다」 판단을 뒤집는다.** 근거: ★**작성자가 자기 수를 교차검증할 수단이 없어 F1 이 났다.** `scripts/audit-null-guards.py`(★CI 미배선 = **잠금이 아니라 감사**). - [rustjava-null-guard-string-init-and-arraycopy-p0] ★★**null 인자 8경로의 «호스트 abort» 를 `NullPointerException` 으로 바꿨다.** diff --git a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json index de70b862..83d84fc9 100644 --- a/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json +++ b/docs/worklog/2026-09-11-null-guard-audit-and-io-buffer-guards.json @@ -23,6 +23,7 @@ "픽스처 형식: javac --release 8 ⇒ major 52 · invokedynamic/CP 태그 15~18 0건" ], "issues": [ + "★[게이트③ 집행] PR #37 을 --merge 로 착지시켰다(upstream-sync 등재 repo — 스쿼시는 부모 2개를 1개로 접어 계보를 지운다). 게이트② 3회차 만에 approve(초판 → -fix → -fix2) · 핀 77dec927 불이동(원장 동봉 «전» 실측) · ci-presence rc=0 CI_GREEN · 자식 PR 0건 · 배포 워크플로 0개 ⇒ 배포 0.", "★[게이트² R1] 커버리지 근인 서술이 뒤집혀 있었다 — 오버라이드가 «있어서»가 아니라 «없어서» base 로 해소된다. 판정 축은 클래스 이름이 아니라 «서술자 + 등재 위치»다. 전 회차가 더한 2케이스는 중복이었고 제거했다.", "★[게이트² R2] 커밋한 감사 스크립트가 M 을 2 낮게 셌다(제네릭·pub(crate) 눈먼 구간 34선언). 고쳤고 세 구현이 일치한다. ★전 회차의 「내 수가 맞고 검수는 구현 세부 차」라는 취지의 문안을 철회한다 — 틀린 것은 내 쪽이었다.", "★[게이트² R2] K 가 무사했던 것은 «운»이다 — 눈먼 fn 2건이 우연히 as_proto 미등재였다. 등재된 제네릭 진입점이 하나만 있었어도 K 가 조용히 낮아져 감사가 「닫혔다」고 답했을 것이고, 그것이 감사 도구의 «침묵 실패»다. 눈먼 구간을 docstring 에 박았다.", @@ -30,7 +31,9 @@ "★[게이트² F4] String.getChars 가드가 JDK 의 예외 선후를 뒤집고 있었다 ⇒ 범위 검사 뒤로 옮기고 선후 잠금 케이스를 넣었다.", "★픽스처 커버리지는 12/18 이고 미커버 6 은 전부 실파일 핸들이 필요하다 — 가드는 들어갔으나 테스트가 잠그지 않는다 ⇒ proposals[1].", "★이 계측은 절차간 deref 와 섀도잉을 못 본다. 전자는 위음성(init_with_string), 후자는 위양성(url.rs 의 handler) ⇒ N·M·K 를 «하한 + 알려진 오탐 1건»으로 읽어라.", - "★부수 발견 — 패닉 impl 은 둘이 아니라 셋이다: Deref · DerefMut · AsClassInstance::as_class_instance. 셋째는 monitor_* 경로다." + "★부수 발견 — 패닉 impl 은 둘이 아니라 셋이다: Deref · DerefMut · AsClassInstance::as_class_instance. 셋째는 monitor_* 경로다.", + "★[검수 minor · 기록] 새 PARAM 앵커는 Option> 같은 중첩 타입 파라미터를 세지 않는다 — 검수 실측 «현재 0건»이라 무해하다. ★그러나 이것도 «조용히 낮아지는» 형태이므로 다음 회차가 재지 않아도 되게 여기 적어 둔다.", + "★[검수 minor · 범위 밖] .gitignore 에 __pycache__ 항목이 없다 — 이 회차는 손으로 잡아 뺐고 커밋에는 0건이지만 방어가 «사람의 주의»뿐이다. 후속 티켓 감." ], "proposals": [ {