From 70923d26680c6e4c7b6c754563cc194bc33573fb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 19:19:06 +0000 Subject: [PATCH 1/8] Add rust/README.md; link it from the root README layout map Documents the P-022 Rust core workspace: crate status, build/test commands, and a real test-case table pulled from the parity fixtures (tests/fixtures/syntax_parity.json, tests/fixtures/ownir/) and the latest passing CI run of the rust-core job. --- README.md | 2 ++ rust/README.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 rust/README.md diff --git a/README.md b/README.md index 903b7bd1..8be6baaa 100644 --- a/README.md +++ b/README.md @@ -823,6 +823,8 @@ ownlang/ test_spec.py # conformance: every spec/ rule fires on an example test_ownir.py # the OwnIR bridge: C# facts -> core -> OWN001 at the C# site frontend/roslyn/ # the C# extractor (Roslyn, CI-only) + .cs samples (P-001) + rust/ # the Rust core migration (P-022): own-ir + own-syntax so far, + # oracle-gated against this Python core — see rust/README.md pyproject.toml # gate: ruff + mypy --strict (see below) ``` diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 00000000..d53dea4f --- /dev/null +++ b/rust/README.md @@ -0,0 +1,98 @@ +# The Rust core workspace (P-022) + +The strangler-fig port of the Python core (`ownlang/`) to Rust, one crate at a +time. Full plan, crate DAG, and rationale: +[`docs/proposals/P-022-rust-core-migration.md`](../docs/proposals/P-022-rust-core-migration.md) +(revised per +[`docs/notes/p022-review-notes.md`](../docs/notes/p022-review-notes.md)). + +**Python stays authoritative.** Nothing here replaces `python -m ownlang` yet. +Each crate lands *behind a differential ratchet*: it must reproduce the +Python core byte-for-byte (error text, AST shape, OwnIR round-trip) on the +existing fixture corpus before the next crate is added. If Rust and Python +ever disagree, Python wins until the divergence is a deliberate, justified +change. + +## Status + +Two of eight planned crates exist. The rest are design-only (see the crate +topology in P-022) — populated bottom-up, oracle-gated, in this order: + +| Crate | Status | What it is | +|---|---|---| +| `own-ir` | **done** (step 1) | The OwnIR fact contract (`serde` types + schema-version gate) and the span/location leaf. Port of `ownlang/ownir.py`'s schema, not its ~2000 lines of bridge logic (that's `own-bridge`, later). | +| `own-syntax` | **done** (step 2) | Lexer + recursive-descent parser + AST. Port of `ownlang/{lexer,parser,ast_nodes}.py`, with a **byte-identical error-text** contract against Python. | +| `own-cfg` | not started | AST → CFG lowering. | +| `own-analysis` | not started | The worklist/lattice solver: ownership, lifetime, effect, DI. | +| `own-diagnostics` | not started | `Diagnostic`/`Evidence` types + text/SARIF rendering. | +| `own-codegen` | not started | C# emission (`emit_*` templates), verdict-independent. | +| `own-bridge` | not started | The OwnIR bridge: facts → core AST, interprocedural MOS inference. | +| `own-cli` | not started | The binary; `own-oracle` is the dev-only differential harness alongside it. | + +## Build & test + +```bash +cd rust +cargo fmt --check +cargo clippy --all-targets # workspace lints are the gate — see Cargo.toml +cargo test +``` + +Same three commands the CI job `rust (fmt + clippy + tests)` runs +(`.github/workflows/ci.yml`) on every push. Latest run on `main`: 23 tests, +0 failures, ~1s (`own-ir`: 10, `own-syntax`: 12 unit + 1 integration +covering 24 fixture cases). + +`unsafe_code = "forbid"` workspace-wide, `clippy::pedantic`/`nursery` warn, +`unwrap_used`/`indexing_slicing`/`arithmetic_side_effects`/`panic` deny — see +the workspace `[lints]` in [`Cargo.toml`](Cargo.toml) and the "ratchet" +section of P-022 for why (and where it's allowed to be loosened, with a +justification comment, never by reflex). + +## Test cases: what "parity" actually checks + +Both crates are pinned by fixtures the **Python side generates and owns** — +Rust only replays them (`tests/test_syntax_fixtures.py --write` regenerates +`tests/fixtures/syntax_parity.json`; a stale fixture fails Python's own test +first). This is deliberate: Python is the oracle, Rust proves it agrees. + +### `own-syntax` — byte-identical error text, or a matching AST digest + +`tests/parity.rs` replays every case in `tests/fixtures/syntax_parity.json` +(24 today) through the Rust parser and asserts either the exact Python error +string or an equivalent structural digest. A sample of what's actually in +there: + +| Case | Input (abridged) | Expected | +|---|---|---| +| `unexpected_char` | `@...` | `1:1: unexpected character '@'` | +| `unterminated_string` | `"...` (no closing quote) | `1:33: unterminated string literal` | +| `rejected_keyword_top_level` | `for ...` | `'for' is out of scope for the MVP — for/loop-style iteration and async are deliberately unsupported ('while' is supported; see README, 'Where it cheats')` | +| `subscribe_not_self` | `subscribe foo to bus;` | `expected 'self' after 'subscribe' (got IDENT 'foo')` | +| `subscribe_not_to` | `subscribe self from bus;` | `expected 'to' in 'subscribe self to ' (got IDENT 'from')` | +| `buffer_positional_after_named` | `Buffer.stack(1, max = 2, 3)` | `only the leading size may be positional in a buffer intent; later arguments must be named` | +| `unicode_idents` | `module м { fn f(х: int) {} }` | accepted; digest matches Python's | +| `full_module` | a resource + 2 externs + 2 fns, one with a `while` | digest `m=Demo r=2 e=4 f=2 p=1 l=2 fns=[setup/2/16,empty/0/1] conds=[n < 10|n]` | + +The point of the digest cases isn't the string itself — it's that Rust and +Python parsed the **same shape** (resource/extern/fn counts, statement +counts including into nested `if`/`while` bodies, and every condition's raw +token text) out of the same source. + +### `own-ir` — every OwnIR fixture round-trips value-for-value + +`tests/roundtrip.rs` reads every `*.json` under `tests/fixtures/ownir/` (21 +files — the same fixtures `tests/test_ownir.py` uses on the Python side: +`subscribe`, `di`, `pool`, `flow_while`, `protocol_isloaded_violation`, …), +parses it with `OwnIr::from_json`, re-serializes it, and asserts the output +equals the input **exactly** — typed fields and unrecognized `extra` fields +alike, so a newer frontend's additive field survives a Rust round-trip +untouched. Plus schema-gate unit tests: `version_gate_rejects_future_schema`, +`absent_version_means_v0`, `bool_is_not_an_integer` (JSON `true` isn't an +`int` here, unlike Python), `additive_unknown_fields_are_preserved`. + +## Why a `rust/` subtree and not a sibling repo + +Monorepo, for this phase: the oracle and the fixture corpus are one `git` +away, no submodule/pinned-SHA ceremony. Revisit once Rust is authoritative +and Python is reference-only — see P-022 "Open questions". From 328898b2883104dffea06ae1e45035230d7ed05c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 19:31:57 +0000 Subject: [PATCH 2/8] Pin all GitHub Actions to commit SHAs (closes README item #7) Every `uses:` in ci.yml, mine.yml, mine-on-push.yml, oracle.yml, and pr-issue-validation.yml now references a commit SHA with a `# vN` comment instead of a mutable tag, resolved via `git ls-remote` against each action's upstream repo. Updates the two "Where it cheats" entries (README.md, README.ru.md) that tracked this as deferred. --- .github/workflows/ci.yml | 64 ++++++++++++----------- .github/workflows/mine-on-push.yml | 8 +-- .github/workflows/mine.yml | 8 +-- .github/workflows/oracle.yml | 14 ++--- .github/workflows/pr-issue-validation.yml | 4 +- README.md | 8 +-- README.ru.md | 10 ++-- 7 files changed, 59 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8608ebc4..754630bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,10 @@ name: CI # Least privilege: every job only reads the repo (no job pushes or needs write). -# Action SHA-pinning / persist-credentials hardening is deliberately deferred to -# a Dependabot/hardening pass — see README "Where it cheats" item #7. +# Every `uses:` is pinned to a commit SHA (with a `# vN` comment for the human- +# readable version) — see README "Where it cheats" item #7. `persist-credentials: +# false` is a separate, still-open hardening item (no job pushes or has secrets, +# so the exposure is checkout-token-lifetime only). permissions: contents: read @@ -20,8 +22,8 @@ jobs: name: lint (ruff + mypy --strict) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.11" - name: Install linters @@ -56,8 +58,8 @@ jobs: run: working-directory: rust steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable, 2026-07-09 with: components: rustfmt, clippy - name: cargo fmt --check @@ -75,8 +77,8 @@ jobs: name: audit aggregation selftests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.11" - name: Install audit deps (PyYAML, audit-scoped) @@ -102,10 +104,10 @@ jobs: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ matrix.python-version }} @@ -121,8 +123,8 @@ jobs: name: extended codegen fuzz runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - name: Property fuzz (50k draws, rotating seed) @@ -136,11 +138,11 @@ jobs: name: golden C# compiles & runs (.NET) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: "8.0.x" - name: Check the emitted method is still in sync with the golden host @@ -158,11 +160,11 @@ jobs: name: C# leak extractor (Roslyn) -> OwnIR -> core runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: "8.0.x" - name: Extract OwnIR facts from sample C# @@ -1076,8 +1078,8 @@ jobs: name: OwnTS (React useEffect) -> OwnIR -> core runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - name: Pin the spike (leaky=3xOWN001+EFF001, clean=0, showcase=2xEFF001) @@ -1198,11 +1200,11 @@ jobs: name: own-check repo scan (github + msbuild) + composite action runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: "8.0.x" - name: GitHub-annotation format over the sample tree (directory walk) @@ -1301,7 +1303,7 @@ jobs: contents: read security-events: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Own.NET leak check (SARIF surface) id: own uses: ./ @@ -1317,7 +1319,7 @@ jobs: test -s "$f" || { echo "FAIL: sarif-file '$f' is missing or empty"; exit 1; } echo "OK: action wrote $(wc -c < "$f") bytes to $f" - name: Upload to GitHub code scanning - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 with: sarif_file: ${{ steps.own.outputs.sarif-file }} category: own-net-samples @@ -1332,11 +1334,11 @@ jobs: name: P-014 Tier B — external reference resolution (--ref-dir) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: "8.0.x" - name: Materialize a third-party reference (CommunityToolkit.Mvvm 8.2.2, pinned) @@ -1395,11 +1397,11 @@ jobs: name: corpus benchmark (real C# recall + specificity) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: "8.0.x" # Some corpus cases subscribe to framework events (WPF Window, Microsoft.Win32 diff --git a/.github/workflows/mine-on-push.yml b/.github/workflows/mine-on-push.yml index 6cc68dd1..8cb68420 100644 --- a/.github/workflows/mine-on-push.yml +++ b/.github/workflows/mine-on-push.yml @@ -23,11 +23,11 @@ jobs: name: mine (sentinel) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: "8.0.x" - name: Materialize WPF reference assemblies (WPF profile) @@ -94,7 +94,7 @@ jobs: fi - name: Upload the report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: mine-report path: | diff --git a/.github/workflows/mine.yml b/.github/workflows/mine.yml index 343937be..22d8c6bf 100644 --- a/.github/workflows/mine.yml +++ b/.github/workflows/mine.yml @@ -30,11 +30,11 @@ jobs: name: mine ${{ inputs.repo }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: "8.0.x" - name: Mine the target @@ -65,7 +65,7 @@ jobs: fi - name: Upload the report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: mine-report path: | diff --git a/.github/workflows/oracle.yml b/.github/workflows/oracle.yml index 0a290055..19251acd 100644 --- a/.github/workflows/oracle.yml +++ b/.github/workflows/oracle.yml @@ -57,11 +57,11 @@ jobs: name: oracle ${{ inputs.repo }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: "8.0.x" @@ -182,7 +182,7 @@ jobs: # default code-scanning (security) suite — so request security-and-quality, # else CodeQL silently contributes zero. Comparator filters to the leak family. - name: CodeQL init - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 continue-on-error: true with: languages: csharp @@ -190,7 +190,7 @@ jobs: source-root: target queries: security-and-quality - name: CodeQL analyze - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 continue-on-error: true with: category: ownnet-oracle @@ -243,7 +243,7 @@ jobs: fi - name: Run Infer# if: env.BUILD_OK == '1' - uses: microsoft/infersharpaction@v1.5 + uses: microsoft/infersharpaction@b749060de518f410f92c87d37d2366e5e9d7c5fc # v1.5 continue-on-error: true with: binary-path: _bin @@ -281,7 +281,7 @@ jobs: fi - name: Upload the report and raw outputs if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: oracle-report path: | diff --git a/.github/workflows/pr-issue-validation.yml b/.github/workflows/pr-issue-validation.yml index b93108b0..4cebfcf2 100644 --- a/.github/workflows/pr-issue-validation.yml +++ b/.github/workflows/pr-issue-validation.yml @@ -32,8 +32,8 @@ jobs: name: validate contribution format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.11" diff --git a/README.md b/README.md index 8be6baaa..a17ab85a 100644 --- a/README.md +++ b/README.md @@ -701,10 +701,10 @@ This is a PoC. The list of holes is deliberately explicit. 6. **Shadowing is forbidden** (OWN031). Rust allows it; for the PoC the ban is simpler. -7. **CI actions are not pinned by commit SHA** (`actions/checkout@v4` and so on on tags, - without `persist-credentials: false`) — SAST (zizmor) flags this. Deliberately - deferred: SHA pinning is a repo-wide policy run by Dependabot / a separate hardening - pass, not a single PR; the jobs are only checkout + running tests, with no push and no +7. ~~CI actions are not pinned by commit SHA~~ — **fixed**: every `uses:` in + `ci.yml`/`mine.yml`/`mine-on-push.yml`/`oracle.yml`/`pr-issue-validation.yml` is now a + commit SHA with a `# vN` comment. `persist-credentials: false` is still open — SAST + (zizmor) flags it, but the jobs are checkout + running tests, with no push and no secrets, so the exposure is minimal. --- diff --git a/README.ru.md b/README.ru.md index 08520cba..652c7d5b 100644 --- a/README.ru.md +++ b/README.ru.md @@ -684,11 +684,11 @@ OWN010 в новой схеме занят «maybe-move».) 6. **Запрещено shadowing** (OWN031). Rust разрешает; для PoC запрет проще. -7. **CI-экшены не запинены по commit-SHA** (`actions/checkout@v4` и пр. на тегах, - без `persist-credentials: false`) — SAST (zizmor) это флагует. Сознательно - отложено: SHA-пиннинг — repo-wide политика, которую ведёт Dependabot / отдельный - hardening-проход, а не один PR; джобы только checkout + прогон тестов, без push - и без секретов, так что экспозиция минимальна. +7. ~~CI-экшены не запинены по commit-SHA~~ — **исправлено**: каждый `uses:` в + `ci.yml`/`mine.yml`/`mine-on-push.yml`/`oracle.yml`/`pr-issue-validation.yml` теперь + commit-SHA с комментарием `# vN`. `persist-credentials: false` пока не сделано — SAST + (zizmor) это флагует, но джобы только checkout + прогон тестов, без push и без + секретов, так что экспозиция минимальна. --- From 4290a0b3a0e6c5feab689d87acd3304639a2036f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 19:40:22 +0000 Subject: [PATCH 3/8] Add a 20-second landing to README, three case studies, and an FP/suppression page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README.md/README.ru.md: replace the "OwnLang — PoC" research-framed opening with a landing (pitch verbatim from ROADMAP.md, slogans from alpha-readiness.md, a 6-line Action quickstart, a local one-liner, one real bad/fixed example from ScreenToGif, and a "why not Sonar/CodeQL" link). The prior opening moves down as the lead-in to the research-depth section instead of being deleted. docs/case-studies/: three pages built from the real-world mining run (docs/notes/real-world-mining.md) and the cross-tool oracle (docs/notes/oracle.md) — the ScreenToGif VideoSource view/view-model leak, the two SystemEvents leaks, and the Dispose-class fixture where Own.NET, CodeQL, and Infer# all agree. docs/suppression-and-fp-policy.md: consolidates the FP policy (honest OWN050 skips, the "no FP from using" precision record) with the [OwnIgnore]/project-config suppression design (P-004/P-015), honestly marking both as designed/drafted rather than implemented. --- README.md | 101 ++++++++++++++-- README.ru.md | 106 +++++++++++++++-- .../dispose-agreement-with-codeql.md | 89 ++++++++++++++ docs/case-studies/screentogif-systemevents.md | 90 ++++++++++++++ docs/case-studies/screentogif-videosource.md | 112 ++++++++++++++++++ docs/suppression-and-fp-policy.md | 86 ++++++++++++++ 6 files changed, 564 insertions(+), 20 deletions(-) create mode 100644 docs/case-studies/dispose-agreement-with-codeql.md create mode 100644 docs/case-studies/screentogif-systemevents.md create mode 100644 docs/case-studies/screentogif-videosource.md create mode 100644 docs/suppression-and-fp-policy.md diff --git a/README.md b/README.md index a17ab85a..136e4af7 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,99 @@ **English** · [Русский](README.ru.md) -# OwnLang — PoC +# Own.NET -A working prototype of what the design documents were about: a small -ownership language with strict Rust-style ownership discipline that compiles to -C#. This is the **front half** of the whole idea — exactly the layer document №2 -advised building first (annotations/subset → analyzer → IR), and deliberately -**before** a Boogie/Dafny/F\* backend. +> Own.NET finds lifetime/resource bugs that C# cannot express: WPF/event +> leaks, missing `Dispose`, DI lifetime mismatch, and pooled-buffer misuse. -Not "Rust for C#". More honestly: +*Find leaks before the profiler.* GC collects unreachable objects; Own finds +objects that should have become unreachable. `event +=` is acquire, `-=` is +release. -> A static ownership checker for a small resource subset, with flow-sensitive -> analysis, a loans/permissions model, a strict call boundary, and code -> generation to C#. +## Run it in CI — 6 lines + +```yaml +- uses: actions/checkout@v4 +- uses: PhysShell/own.net@main + with: + format: github # inline PR annotations; use "sarif" for the Security tab + fail-on-finding: "true" +``` + +## Or point it at a repo you already have + +```bash +git clone https://github.com/PhysShell/Own.NET && cd Own.NET +scripts/own-check.sh --format human -- /path/to/your/csharp/repo +``` + +Needs Python 3.11+ and the .NET SDK on `PATH` — nothing to build, nothing to +`pip install` (there's no packaged CLI yet; see +[`docs/notes/alpha-readiness.md`](docs/notes/alpha-readiness.md) gate **A**). + +## One it actually found + +A real, unmodified file from [`ScreenToGif`](https://github.com/NickeManarin/ScreenToGif) — +a `Window` subscribes to a **static, process-lifetime** event and never +unsubscribes, so the window can never be collected: + +```csharp +// bad — GraphicsConfigurationDialog.xaml.cs:35 +SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; +// ...never `-=`'d + +// fixed +SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; +Closed += (_, _) => SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; +``` + +```text +GraphicsConfigurationDialog.xaml.cs:35: error: [OWN001] event + 'SystemEvents.DisplaySettingsChanged' is subscribed (handler + 'SystemEvents_DisplaySettingsChanged') but never unsubscribed — the source keeps + 'GraphicsConfigurationDialog' alive (leak) [resource: subscription token] +``` + +No `IDisposable` involved, nothing "not disposed" — a defect class Dispose/RAII +checkers (CA2213, CodeQL's `cs/local-not-disposed`, …) have no query for. Three +more real finds, one where Own.NET's verdict lines up with CodeQL/Infer# and one +consolidated suppression/false-positive policy: + +- [`docs/case-studies/screentogif-videosource.md`](docs/case-studies/screentogif-videosource.md) — the flagship find, a view→view-model handler leak +- [`docs/case-studies/screentogif-systemevents.md`](docs/case-studies/screentogif-systemevents.md) — the pair above, in full +- [`docs/case-studies/dispose-agreement-with-codeql.md`](docs/case-studies/dispose-agreement-with-codeql.md) — where Own.NET, CodeQL, and Infer# agree +- [`docs/suppression-and-fp-policy.md`](docs/suppression-and-fp-policy.md) — suppressing a finding, and the false-positive policy + +## Why not Sonar / CodeQL / Semgrep? + +Because they already own "find bugs/vulnerabilities," backed by sales teams — +that is a fight lost to marketing budget, not merit. Own.NET's niche is +narrower and doesn't overlap where it doesn't have to: a **resource / lifetime / +effect contract checker** — who holds whom, who must release, which resource +outlives which. Full positioning, including the "same model, many skins" case +for treating WPF leaks, DI captive dependencies, and pooled-buffer misuse as one +underlying bug class: +[`docs/ROADMAP.md` — Positioning against the competition](docs/ROADMAP.md#positioning-against-the-competition-not-another-sast). + +--- + +Everything below this line is the research-depth documentation: the analysis +model, the ownership core, codegen, and how this maps to the original design +proposals. Start here if you're evaluating the engine itself, contributing, or +just curious how "GC finds unreachable objects; Own finds objects that should +have become unreachable" is actually implemented. + +`ownlang/` (the Python core described below) began as a working prototype of +the design documents' idea: a small ownership language with strict Rust-style +ownership discipline that compiles to C#. Not "Rust for C#" — more honestly, **a +static ownership checker for a small resource subset**, with flow-sensitive +analysis, a loans/permissions model, a strict call boundary, and code +generation to C#. This is the **front half** of the whole idea — exactly the +layer document №2 advised building first (annotations/subset → analyzer → IR), +and deliberately **before** a Boogie/Dafny/F\* backend. It is also, today, the +reference implementation the real C# extractor (`frontend/roslyn/`) and the +Rust port (`rust/`) are held to parity against — see +[`corpus/wpf/`](corpus/wpf/) and the case studies above for what it looks like +pointed at real code, and everything from here down for how it works. This revision is a rework after review. What changed: an explicit **loans + permissions** model (the owner stays `Owned`, borrows are separate diff --git a/README.ru.md b/README.ru.md index 652c7d5b..e4c62907 100644 --- a/README.ru.md +++ b/README.ru.md @@ -1,18 +1,104 @@ [English](README.md) · **Русский** -# OwnLang — PoC +# Own.NET -Рабочий прототип того, о чём шла речь в твоих документах: маленький -ownership-язык со строгой дисциплиной владения в духе Rust, который -компилируется в C#. Это **передняя половина** всей задумки — ровно тот слой, -который документ №2 советовал строить первым (annotations/subset → analyzer → -IR), и сознательно **до** backend'а на Boogie/Dafny/F\*. +> Own.NET находит баги времени жизни/ресурсов, которые C# не может выразить: +> WPF/event-лики, забытый `Dispose`, рассинхрон DI lifetime и неправильное +> использование pooled-буферов. -Не «Rust для C#». Честнее так: +*Находи лики до профайлера.* GC собирает недостижимые объекты; Own находит +объекты, которые должны были стать недостижимыми. `event +=` — это acquire, +`-=` — это release. -> Статический ownership-checker для маленького ресурсного подмножества, -> с flow-sensitive анализом, моделью loans/permissions, строгой границей вызовов -> и кодогенерацией в C#. +## Запустить в CI — 6 строк + +```yaml +- uses: actions/checkout@v4 +- uses: PhysShell/own.net@main + with: + format: github # инлайн-аннотации в PR; "sarif" — для вкладки Security + fail-on-finding: "true" +``` + +## Или локально, на репозитории, который уже есть + +```bash +git clone https://github.com/PhysShell/Own.NET && cd Own.NET +scripts/own-check.sh --format human -- /путь/к/вашему/csharp/репозиторию +``` + +Нужны Python 3.11+ и .NET SDK в `PATH` — ничего собирать, ничего ставить через +`pip install` (упакованного CLI пока нет; см. +[`docs/notes/alpha-readiness.md`](docs/notes/alpha-readiness.md), gate **A**). + +## Один реальный баг, который он нашёл + +Настоящий, неизменённый файл из +[`ScreenToGif`](https://github.com/NickeManarin/ScreenToGif) — `Window` +подписывается на **статическое, process-lifetime** событие и никогда не +отписывается, поэтому окно никогда не будет собрано GC: + +```csharp +// bad — GraphicsConfigurationDialog.xaml.cs:35 +SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; +// ...ни разу не `-=` + +// fixed +SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; +Closed += (_, _) => SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; +``` + +```text +GraphicsConfigurationDialog.xaml.cs:35: error: [OWN001] event + 'SystemEvents.DisplaySettingsChanged' is subscribed (handler + 'SystemEvents_DisplaySettingsChanged') but never unsubscribed — the source keeps + 'GraphicsConfigurationDialog' alive (leak) [resource: subscription token] +``` + +Никакого `IDisposable`, нечему быть «not disposed» — класс дефектов, для +которого у Dispose/RAII-чекеров (CA2213, CodeQL `cs/local-not-disposed`, …) +попросту нет запроса. Ещё три реальные находки, одна — где вердикт Own.NET +совпадает с CodeQL/Infer#, и одна консолидированная страница про +suppression/false-positive: + +- [`docs/case-studies/screentogif-videosource.md`](docs/case-studies/screentogif-videosource.md) — флагманская находка, лик handler'а view→view-model +- [`docs/case-studies/screentogif-systemevents.md`](docs/case-studies/screentogif-systemevents.md) — пара выше, целиком +- [`docs/case-studies/dispose-agreement-with-codeql.md`](docs/case-studies/dispose-agreement-with-codeql.md) — где Own.NET, CodeQL и Infer# сходятся +- [`docs/suppression-and-fp-policy.md`](docs/suppression-and-fp-policy.md) — как подавить находку, и политика false positive + +## Почему не Sonar / CodeQL / Semgrep? + +Потому что они уже владеют «находим баги/уязвимости», за спиной — sales-команды: +это бой, проигранный маркетинговому бюджету, а не по существу. Ниша Own.NET +у́же и не пересекается там, где не обязана: **чекер контрактов +ресурсов/времени жизни/эффектов** — кто кем владеет, кто обязан release, +какой ресурс переживает какой. Полное позиционирование, включая кейс «одна +модель, много обличий» для WPF-ликов, DI captive dependencies и неправильного +использования pooled-буферов как одного класса багов: +[`docs/ROADMAP.md` — Positioning against the competition](docs/ROADMAP.md#positioning-against-the-competition-not-another-sast) +(на английском). + +--- + +Всё, что ниже этой черты — исследовательская документация: модель анализа, +ownership-ядро, кодогенерация и как это соотносится с исходными +design-документами. Начните отсюда, если оцениваете сам движок, собираетесь +контрибьютить или просто любопытно, как «GC находит недостижимые объекты; Own +находит объекты, которые должны были стать недостижимыми» устроено на самом +деле. + +`ownlang/` (Python-ядро, описанное ниже) начинался как рабочий прототип идеи +из design-документов: маленький ownership-язык со строгой дисциплиной владения +в духе Rust, который компилируется в C#. Не «Rust для C#» — честнее: **статический +ownership-checker для маленького ресурсного подмножества**, с flow-sensitive +анализом, моделью loans/permissions, строгой границей вызовов и кодогенерацией +в C#. Это **передняя половина** всей задумки — ровно тот слой, который +документ №2 советовал строить первым (annotations/subset → analyzer → IR), и +сознательно **до** backend'а на Boogie/Dafny/F\*. Сегодня это ещё и эталонная +реализация, с которой на паритет держат реальный C#-экстрактор +(`frontend/roslyn/`) и Rust-порт (`rust/`) — см. [`corpus/wpf/`](corpus/wpf/) и +кейсы выше, как это выглядит на реальном коде, и всё, что ниже — как это +устроено внутри. Эта ревизия — переработка по ревью. Что изменилось: явная модель **loans + permissions** (владелец остаётся `Owned`, borrow'ы — отдельные факты), diff --git a/docs/case-studies/dispose-agreement-with-codeql.md b/docs/case-studies/dispose-agreement-with-codeql.md new file mode 100644 index 00000000..65729ddf --- /dev/null +++ b/docs/case-studies/dispose-agreement-with-codeql.md @@ -0,0 +1,89 @@ +# Case study: where Own.NET agrees with CodeQL (and Infer#) + +The other two case studies ([`VideoSource`](screentogif-videosource.md), +[`SystemEvents`](screentogif-systemevents.md)) are about the defect class Own.NET +finds that Dispose/RAII checkers structurally cannot. This one is the other +half of an honest positioning: proof that on the class those checkers *do* +cover, Own.NET's verdict lines up with theirs — not just argued, but run +side-by-side through the cross-tool oracle +([`docs/notes/oracle.md`](../notes/oracle.md)) and pinned as a regression fixture. + +ScreenToGif itself is WPF and doesn't `dotnet build` on the Linux oracle +runner, so Infer# can't run against it there. The fixture below +(`corpus/fixtures/systemevents-console/`) is a small `net8.0` console program, +Linux-buildable, that reproduces the same leak classes so all three tools — +Own.NET, CodeQL, **and** Infer# — run over identical code. + +## Bad + +```csharp +private static void LeakAFile() +{ + var stream = new FileStream("scratch.bin", FileMode.Create); + stream.WriteByte(0x42); + // ...no Dispose()/using -> resource leak +} +``` + +A local `FileStream`, never disposed, never wrapped in `using`. The plainest +possible Dispose/RAII leak — deliberately plain, because its job in this +fixture is to be the **control**: if all three tools don't flag it, the +comparison itself is broken, not informative. + +## Fixed + +```csharp +private static void LeakAFile() +{ + using var stream = new FileStream("scratch.bin", FileMode.Create); + stream.WriteByte(0x42); +} +``` + +## What others miss (here: nothing — that's the point) + +| Finding (`Program.cs`) | class | Own.NET | CodeQL | Infer# | +|---|---|:-:|:-:|:-:| +| `:43` `new FileStream(…)` never disposed | Dispose/RAII (control) | ✓ | ✓ | ✓ | +| `:54` undisposed local inside a `try`-method | Dispose/RAII (`try`-lowering) | ✓ | ✓ | ✓ | +| `:77` `Dispose()` in `try`, skipped on the throw path | dispose-on-throw (exception-edge) | ✓ | ✓ | ✓ | +| `:20` `SystemEvents.DisplaySettingsChanged +=`, never `-=` | subscription | ✓ | — | — | + +The first three rows are **Agree** across all three tools — CodeQL via +`cs/local-not-disposed` (and, for the third row, `cs/dispose-not-called-on-throw`), +Infer# via Pulse. The fourth row is the same differentiation the other two case +studies make: Own.NET flags the subscription leak, and **neither CodeQL nor +Infer# has an equivalent query** — not "they missed it," they don't model the +defect class at all. + +The middle two rows are not filler: they mark recall Own.NET didn't always +have. Before `try`/`finally` lowering, a method containing a `try` was skipped +entirely, so row `:54` used to be oracle-only (CodeQL/Infer# caught it, Own.NET +didn't). Before the exception-edge model, a resource disposed *somewhere* in +the method looked balanced, so row `:77` — disposed on the normal path, leaked +on the exceptional one — was also invisible to Own.NET. Both are now closed and +both land in the CI-pinned `Agree` bucket, matching CodeQL's dedicated +`cs/dispose-not-called-on-throw` query on the third row specifically. + +## How Own reports it + +```text +Program.cs:43: error: [OWN001] 'stream' is owned but not released at end of + function (leaks on at least one path) +Program.cs:54: error: [OWN001] 'tried' is owned but not released at end of + function (leaks on at least one path) +Program.cs:77: error: [OWN001] 'onThrow' is owned but not released on the + exceptional path (disposed on the normal path only) +Program.cs:20: error: [OWN001] event 'SystemEvents.DisplaySettingsChanged' is + subscribed (handler 'OnDisplayChanged') but never unsubscribed — the source + keeps 'DisplayWatcher' alive (leak) [resource: subscription token] +``` + +Same core, same `OWN001` code, for two structurally different defect classes +(a plain leaked handle vs. an unreleased event subscription) — the `[resource: +...]` tag is what a later profile/front-end would use to phrase them +differently, not a second checker. + +Fixture: `corpus/fixtures/systemevents-console/` (`Program.cs` + its own +[README](../../corpus/fixtures/systemevents-console/README.md) with the full +expected 2×2), exercised by `oracle.yml`'s local-fixture mode. diff --git a/docs/case-studies/screentogif-systemevents.md b/docs/case-studies/screentogif-systemevents.md new file mode 100644 index 00000000..19834884 --- /dev/null +++ b/docs/case-studies/screentogif-systemevents.md @@ -0,0 +1,90 @@ +# Case study: subscribing to a process, not a window (ScreenToGif) + +**Target:** [`NickeManarin/ScreenToGif`](https://github.com/NickeManarin/ScreenToGif) +@ `27a49c3` — the same pattern independently, twice: +`ScreenToGif/Windows/Other/GraphicsConfigurationDialog.xaml.cs:35` and +`ScreenToGif/Windows/Other/Troubleshoot.xaml.cs:27`. Found by mining after +turning on the WPF profile +([`docs/notes/real-world-mining.md`](../notes/real-world-mining.md)). + +## Bad + +```csharp +public partial class GraphicsConfigurationDialog : Window +{ + public GraphicsConfigurationDialog() + { + InitializeComponent(); + SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; + // ...never `-=`'d + } + + private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) { } +} +``` + +`Microsoft.Win32.SystemEvents` is a **static class** — its events live for the +entire process, not for any window. Subscribing a dialog's method-group handler +to it hands the static event a strong reference to the dialog, and nothing +ever gives it back. The dialog can be closed a hundred times over; the process +keeps every instance alive until it exits. This is the textbook `SystemEvents` +leak the .NET docs carry an explicit warning about — and it occurs twice in +this codebase, independently, in two unrelated dialogs. + +## Fixed + +Unsubscribe when the window is done — here, on `Closed`: + +```csharp +public GraphicsConfigurationDialog() +{ + InitializeComponent(); + SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; + Closed += OnClosed; +} + +private void OnClosed(object sender, EventArgs e) +{ + SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; + Closed -= OnClosed; +} +``` + +Breaking the static source's hold is enough — the dialog goes back to being +collectable exactly when it should. + +## What others miss + +Same story as the [`VideoSource` case](screentogif-videosource.md): nothing here +is ever "not disposed," so `IDisposableAnalyzers`/`CA2213`/CodeQL's +`cs/local-not-disposed` have no defect to find — the cross-tool run +([`docs/notes/oracle.md`](../notes/oracle.md)) confirms CodeQL flags neither +site on this commit; its query set doesn't have "event subscribed to a static +source, never unsubscribed." What makes this pair worth its own write-up next +to `VideoSource`, rather than folding into it, is the **severity**: here Own.NET +does not hedge. + +## How Own reports it + +Real extractor output (P-001), WPF reference pack **on** (needed to resolve +`SystemEvents` as a framework type): + +```text +GraphicsConfigurationDialog.xaml.cs:35: error: [OWN001] event + 'SystemEvents.DisplaySettingsChanged' is subscribed (handler + 'SystemEvents_DisplaySettingsChanged') but never unsubscribed — the source keeps + 'GraphicsConfigurationDialog' alive (leak) [resource: subscription token] +``` + +**Why error, not warning — the tiering.** The `VideoSource` finding next door is +a *warning* because its subscription source is an injected field of unknown +lifetime — Own.NET can't prove it outlives the window. Here the source is +`static`: it provably outlives every window in the process, so the P-004 +severity tiering classifies it accordingly and the same shape (subscribe, +never unsubscribe) escalates to a hard **error**, not a hedge. The extractor +draws that line from the source's *provable* lifetime, not from a blanket rule +about events — the two case studies are the same defect class reported at two +different confidence levels, on purpose. + +Regression-locked as `corpus/real-world/screentogif-systemevents-leak/` +(`before.cs`/`after.cs`, pinned by `tests/test_corpus.py`). diff --git a/docs/case-studies/screentogif-videosource.md b/docs/case-studies/screentogif-videosource.md new file mode 100644 index 00000000..5da2f359 --- /dev/null +++ b/docs/case-studies/screentogif-videosource.md @@ -0,0 +1,112 @@ +# Case study: a view that outlives its close button (ScreenToGif) + +**Target:** [`NickeManarin/ScreenToGif`](https://github.com/NickeManarin/ScreenToGif) +@ `27a49c3`, `ScreenToGif/Windows/Other/VideoSource.xaml.cs:50-83`. Found by the +first real-world mining run +([`docs/notes/real-world-mining.md`](../notes/real-world-mining.md), milestone 1) +— unmodified, un-cherry-picked OSS code, not a constructed example. + +## Bad + +A `Window` reads its view-model out of `DataContext`, then wires four inline +lambdas to the view-model's custom events inside `Window_Loaded`: + +```csharp +public partial class VideoSource : Window +{ + private readonly VideoSourceViewModel _viewModel; + + public VideoSource() + { + InitializeComponent(); + _viewModel = DataContext as VideoSourceViewModel; + } + + private void Window_Loaded(object sender, RoutedEventArgs e) + { + _viewModel.ShowErrorRequested += (_, args) => StatusBand.Error(args?.ToString()); + _viewModel.HideErrorRequested += (_, _) => StatusBand.Hide(); + _viewModel.CloseRequested += (_, _) => DialogResult = true; + // ...never unsubscribed + } + + // Present in the real file, but it does NOT detach the handlers above. + private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { } +} +``` + +`Window_Closing` exists — it just doesn't undo the subscriptions. Two distinct +bugs fall out of that one omission: the lambdas capture `this`, so the +view-model holds a strong reference to the window for as long as the +view-model itself is reachable; and because WPF's `Loaded` can fire more than +once (an element re-added to the visual tree re-raises it), the handlers can +**stack**, so `ShowErrorRequested` fires the same status-band update twice, then +three times, once per reload. + +## Fixed + +Give each subscription a named handler — the thing an inline lambda doesn't +have — and detach it where `Window_Closing` already runs: + +```csharp +private void Window_Loaded(object sender, RoutedEventArgs e) +{ + _viewModel.ShowErrorRequested += OnShowError; + _viewModel.HideErrorRequested += OnHideError; + _viewModel.CloseRequested += OnClose; +} + +private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) +{ + _viewModel.ShowErrorRequested -= OnShowError; + _viewModel.HideErrorRequested -= OnHideError; + _viewModel.CloseRequested -= OnClose; +} + +private void OnShowError(object sender, EventArgs args) => StatusBand.Error(args?.ToString()); +private void OnHideError(object sender, EventArgs e) => StatusBand.Hide(); +private void OnClose(object sender, EventArgs e) => DialogResult = true; +``` + +No behavior change, no new fields — the fix is entirely "have a handle to +unsubscribe with, and use it in the close path that was already there." + +## What others miss + +This is a **view ↔ view-model lifetime** shape: an `IDisposable`/dispose-not-called +analyzer has nothing to check here, because neither side of the subscription +implements `IDisposable` and nothing is ever "not disposed" — the leak is a +plain C# event, the kind `CA2213`/`IDisposableAnalyzers`/CodeQL's +`cs/local-not-disposed` don't model at all. Cross-checked against CodeQL on the +same commit ([`docs/notes/oracle.md`](../notes/oracle.md)): its findings on +ScreenToGif are entirely the Dispose/RAII class (`OpenFileDialog`, `Pen`, +`Bitmap`, …); it flags none of the four `VideoSource` subscriptions, because its +query set has no "event subscribed, never unsubscribed" rule. Own.NET and CodeQL +are complementary here, not redundant — see the +[Dispose-agreement case study](dispose-agreement-with-codeql.md) for where they +*do* overlap. + +## How Own reports it + +Real extractor output (P-001), WPF reference pack off — these events are the +app's own types, so they resolve without it: + +```text +VideoSource.xaml.cs:50: warning: [OWN001] event '_viewModel.ShowErrorRequested' is + subscribed (handler '(_, args) => ...') but never unsubscribed; its source is an + injected dependency whose lifetime is unknown, so it may outlive and keep + 'VideoSource' alive (possible leak — and being an inline lambda it has no '-=' + handle, so it could never be detached) [resource: subscription token] +``` + +**Why warning, not error — the honest part.** `_viewModel` is an injected field; +Own.NET's source tiering can't *prove* it outlives the window (in fact, as the +window's own `DataContext`, the two likely share a lifetime — a collectable +cycle, not necessarily a leaked one). So the verdict is "possible leak," not a +hard error — the duplicate-handler-on-reload bug is real regardless, and the +diagnostic says so explicitly (an inline lambda has no `-=` handle, full stop). +Contrast the [SystemEvents case study](screentogif-systemevents.md), where the +subscription source is `static` and the same shape escalates to a hard error. + +Regression-locked as `corpus/real-world/screentogif-loaded-subscription/` +(`before.cs`/`after.cs`, pinned by `tests/test_corpus.py`). diff --git a/docs/suppression-and-fp-policy.md b/docs/suppression-and-fp-policy.md new file mode 100644 index 00000000..ae52431b --- /dev/null +++ b/docs/suppression-and-fp-policy.md @@ -0,0 +1,86 @@ +# Suppression & false-positive policy + +One consolidated, user-facing answer to "I got a finding I don't agree with — +what do I do?" This page doesn't introduce anything new; it collects what's +already designed (P-004), drafted (P-015), and observed in practice +(`docs/notes/real-world-mining.md`, `docs/notes/oracle.md`) into one place. + +## The policy: a false positive is worse than a miss + +This is the project's prime directive for shipped tooling +([`docs/notes/strictness-and-fitness.md`](notes/strictness-and-fitness.md)) — +and it drives a concrete design choice: when the C# extractor can't *prove* a +fact (an external type it has no reference for, a construct it doesn't model +yet), it emits an honest **`OWN050`** ("unresolved" / "skipped"), never a +guessed leak. Silence-by-default, not confidence-by-default. + +This is why **`using` never produces a false positive**: the extractor models +`using` as a release, full stop — it is not a heuristic that occasionally +misses. Every real-world mining run to date confirms the policy holds in +practice, not just on paper: triaging by hand across `Dapper`, `CsvHelper`, and +`ScreenToGif` turned up zero false positives from `using`-scoped locals +(`real-world-mining.md`), and the cross-tool oracle runs against `Dapper` and +`App-vNext/Polly` both closed at **`own-only 0`** — nothing Own.NET flagged +turned out to be wrong (`oracle.md`). Where the extractor *did* have a +precision gap (self-owned WPF controls built via `ref`/`out` construction, +template parts from `GetTemplateChild`/`FindName`), it was fixed, not +suppressed — see the self-owned-control fix in `real-world-mining.md`. + +**A narrower, separate point about the core.** The `.own` DSL's ownership +dataflow (`ownlang/analysis.py`) is *intentionally conservative* in the +Rust-borrow-checker sense: it proves soundness over an idealized closed-world +program, and would rather reject a technically-fine `.own` program (a +"maybe"-tier `OWN009`/`OWN010`) than silently accept an unsound one — see the +README's ["An important turn on false positives"](../README.md#an-important-turn-on-false-positives). +That is a *different axis* from the policy above: it is about the core +prover's soundness on a small formal language, not about the real-C# extractor's +UX. The two are compatible, not in tension — the extractor's honest-skip +(`OWN050`) is exactly what lets the ambiguous, can't-prove-it-either-way case in +real C# stay silent instead of forcing the core's conservative "maybe" tier to +fire on unprovable input. + +## What you can do about a finding today + +| Lever | Status | Scope | +|---|---|---| +| `--severity warning` | **works today** (P-013) | Global: downgrades every error-tier finding for that run to advisory. Per-run, not per-finding — an escape hatch for "show me everything, but don't fail the build yet," not a way to silence one specific site. | +| `--fail-on-finding` (off) | **works today** (P-013, the GitHub Action's default input) | Global: findings still print/annotate, but the process/step exit code stays 0. | +| `[OwnIgnore("reason")]` | **designed, not implemented** (P-004) | Inline, per-site suppression attribute — the intended fine-grained escape hatch for a specific subscription/field the checker can't see enough context to clear. Referenced across P-001/P-004/P-010/P-014/P-017 as the standing design; there is no code behind it yet. If you need this today, the honest answer is: you don't have it — file the case so it informs the implementation. | +| Project-wide config (`.ownrc`/`own.toml`) | **draft, not implemented** (P-015) | Per-check-category enable/disable + severity + per-path overrides (e.g. relax a category under `tests/`). Stub status — format (TOML vs INI vs JSON) and enforcement point are still open questions in the proposal. | +| `corpus/oracle-fp-baseline.txt` | **exists, but not a user-facing suppression tool** | An allowlist the *oracle comparator* (`scripts/oracle_compare.py`, a dev/maintainer tool) uses to keep already-triaged false positives out of the `own-only` bucket on re-runs. It doesn't change what `own-check`/the Action reports — it only keeps the oracle's own triage queue from re-showing confirmed noise. | + +So today, honestly: there is no way to suppress **one specific finding** in +your own repo. The two escape hatches for that (`[OwnIgnore]`, project config) +are designed and drafted respectively, not shipped. What you have is a global +severity dial and the extractor's own honest-skip behavior, which is why the +precision bar above matters as much as the (currently thin) suppression +surface — the fewer false positives reach you, the less suppression UX has to +carry. + +## The designed shape (so you know what's coming) + +Precedence, once both land (P-015's draft order): + +``` +CLI flag > inline [OwnIgnore] > config file > built-in default +``` + +`[OwnIgnore("reason")]` (P-004) is a per-site attribute — the *reason* string +is mandatory by design, so a suppression is a documented decision, not a +silent one. Project config (P-015) is the per-category, project-wide +counterpart — "treat subscriptions as warnings, keep disposables as errors, +skip pool checks under `tests/`" — discovered by walking up from the scanned +path, the same convention as `.editorconfig`/`ruff.toml`. Both are consumed +**core-side** ([P-013](proposals/P-013-distribution-surface.md)'s "one +checker" rule): the extractor may skip emitting a fact for a disabled category +as an optimization, but the core is the sole authority on what a finding says, +so config can never become a second, disagreeing checker. + +## Reporting a false positive + +If you hit one in real code: that is exactly the signal the project runs on. +Reduce it to a minimal repro if you can, and it becomes either a precision fix +(the self-owned-control fix above is the template) or a documented, deliberate +by-design skip recorded in +[`docs/notes/field-notes-patterns.md`](notes/field-notes-patterns.md) — the +living map of what Own.NET has seen and why it stays silent on it. From bd935b174a4ae38970886e1d33d27b09c19e17e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 19:55:21 +0000 Subject: [PATCH 4/8] Add C#-native bad/ok pairs for 7 of the 12 gallery cases (task E tail) examples/gallery/cs/ mirrors the .own gallery in real, compilable C#, run through the actual Roslyn extractor -> OwnIR -> core pipeline instead of the toy DSL's own dataflow. Wired into CI as a new step in the "C# leak extractor (Roslyn) -> OwnIR -> core" job, asserting the exact OWN0xx code on each .bad.cs and silence on each .ok.cs. Only 7 of the 12 .own cases have a real C# detector today (01, 02, 03, 07, 10, 11, plus the clean 00). The other 5 (04/05/06/08/09) depend on move/borrow/stack-buffer/unknown-call concepts that only exist in the `.own` DSL's AST -- ownlang/ownir.py never constructs them, so no real C# fact can reach them. Documented honestly in examples/gallery/cs/README.md with citations, rather than shipping .cs files that don't actually trip their claimed code. Also fixes the root README's gallery table, which was missing rows 10/11, and updates alpha-readiness.md's gate E status. --- .github/workflows/ci.yml | 26 ++++++++++ README.md | 8 ++++ README.ru.md | 8 ++++ docs/notes/alpha-readiness.md | 2 +- examples/gallery/cs/00_ok_clean.cs | 15 ++++++ .../gallery/cs/01_leak_on_error_path.bad.cs | 19 ++++++++ .../gallery/cs/01_leak_on_error_path.ok.cs | 17 +++++++ .../gallery/cs/02_use_after_release.bad.cs | 17 +++++++ .../gallery/cs/02_use_after_release.ok.cs | 14 ++++++ examples/gallery/cs/03_double_release.bad.cs | 16 +++++++ examples/gallery/cs/03_double_release.ok.cs | 13 +++++ .../gallery/cs/07_use_after_handoff.bad.cs | 26 ++++++++++ .../gallery/cs/07_use_after_handoff.ok.cs | 22 +++++++++ examples/gallery/cs/10_leak_in_loop.bad.cs | 18 +++++++ examples/gallery/cs/10_leak_in_loop.ok.cs | 19 ++++++++ .../gallery/cs/11_overspan_full_view.bad.cs | 31 ++++++++++++ .../gallery/cs/11_overspan_full_view.ok.cs | 27 +++++++++++ examples/gallery/cs/README.md | 48 +++++++++++++++++++ 18 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 examples/gallery/cs/00_ok_clean.cs create mode 100644 examples/gallery/cs/01_leak_on_error_path.bad.cs create mode 100644 examples/gallery/cs/01_leak_on_error_path.ok.cs create mode 100644 examples/gallery/cs/02_use_after_release.bad.cs create mode 100644 examples/gallery/cs/02_use_after_release.ok.cs create mode 100644 examples/gallery/cs/03_double_release.bad.cs create mode 100644 examples/gallery/cs/03_double_release.ok.cs create mode 100644 examples/gallery/cs/07_use_after_handoff.bad.cs create mode 100644 examples/gallery/cs/07_use_after_handoff.ok.cs create mode 100644 examples/gallery/cs/10_leak_in_loop.bad.cs create mode 100644 examples/gallery/cs/10_leak_in_loop.ok.cs create mode 100644 examples/gallery/cs/11_overspan_full_view.bad.cs create mode 100644 examples/gallery/cs/11_overspan_full_view.ok.cs create mode 100644 examples/gallery/cs/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 754630bd..03e4d6cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -866,6 +866,32 @@ jobs: if echo "$out" | grep -q "'$ok'"; then echo "FAIL: D5.2 silent case '$ok' was reported"; exit 1; fi done echo "OK: flow-sensitive OWN001/002/003 on real C# (path-sensitive, loops via while/foreach/for, try/finally sequential, never-vs-every-path wording, dispose-optional exempt, beyond flat)" + - name: Gallery C#-native bad/ok pairs (examples/gallery/cs/) + run: | + # C#-native mirror of examples/gallery/*.own, run through the real extractor -> + # OwnIR -> core (not the toy .own DSL's own dataflow) — see + # examples/gallery/cs/README.md for the 7-of-12 mapping and why the remaining 5 + # (move/borrow/stack-buffer/unknown-call) have no real C# detector yet. + dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \ + examples/gallery/cs --flow-locals -o "$RUNNER_TEMP/gallery.json" + out=$(python -m ownlang ownir "$RUNNER_TEMP/gallery.json" || true) + echo "$out" + echo "$out" | grep -qE "01_leak_on_error_path\.bad\.cs:[0-9]+:.*\[OWN001\].*'galleryLeakOnError'" \ + || { echo "FAIL: expected OWN001 on galleryLeakOnError"; exit 1; } + echo "$out" | grep -qE "02_use_after_release\.bad\.cs:[0-9]+:.*\[OWN002\].*'galleryUseAfterRelease'" \ + || { echo "FAIL: expected OWN002 on galleryUseAfterRelease"; exit 1; } + echo "$out" | grep -qE "03_double_release\.bad\.cs:[0-9]+:.*\[OWN003\].*'galleryDoubleRelease'" \ + || { echo "FAIL: expected OWN003 on galleryDoubleRelease"; exit 1; } + echo "$out" | grep -qE "07_use_after_handoff\.bad\.cs:[0-9]+:.*\[OWN002\].*'galleryHandoff'" \ + || { echo "FAIL: expected OWN002 on galleryHandoff (use after handoff)"; exit 1; } + echo "$out" | grep -qE "10_leak_in_loop\.bad\.cs:[0-9]+:.*\[OWN001\].*'galleryLoopLeak'" \ + || { echo "FAIL: expected OWN001 on galleryLoopLeak"; exit 1; } + echo "$out" | grep -qE "11_overspan_full_view\.bad\.cs:[0-9]+:.*\[OWN025\].*'galleryOverspanBuf'" \ + || { echo "FAIL: expected OWN025 on galleryOverspanBuf"; exit 1; } + for ok in galleryClean galleryLeakOnErrorOk galleryUseAfterReleaseOk galleryDoubleReleaseOk galleryHandoffOk galleryLoopLeakOk galleryOverspanOkBuf; do + if echo "$out" | grep -q "'$ok'"; then echo "FAIL: clean gallery case '$ok' was reported"; exit 1; fi + done + echo "OK: examples/gallery/cs/ bad/ok pairs match the .own gallery's codes 1:1 on the real extractor pipeline" - name: P-005 D5.4 T4 wrap/adopt (--flow-locals) run: | # The extractor recognises a first-party wrapper that ADOPTS a disposable arg into an diff --git a/README.md b/README.md index 136e4af7..a4a471f8 100644 --- a/README.md +++ b/README.md @@ -162,10 +162,18 @@ python tests/test_gallery.py | `07_use_after_handoff` | **OWN002** | touched the buffer after a call took it | | `08_stack_buffer_escapes` | **OWN015** | returned a `Span` over a `stackalloc` (dangling) | | `09_untracked_call` | **OWN040** | ownership "laundered" through an opaque call | +| `10_leak_in_loop` | **OWN001** | a resource acquired every loop iteration, never released | +| `11_overspan_full_view` | **OWN025** | a full-length `buf.AsSpan()` reading past the rented length | `00_ok_clean` — a clean happy path (rent → view → return) that lowers into exception-safe `ArrayPool` Rent/Return. +[`examples/gallery/cs/`](examples/gallery/cs/) mirrors 7 of these 12 cases in real, +compilable C#, run through the actual Roslyn extractor → OwnIR → core pipeline +(not the `.own` DSL's own dataflow) and verified in CI. The other 5 (move/borrow/ +stack-buffer/unknown-call) are DSL-only concepts with no real C# detector yet — +see that directory's README for exactly why. + `check` prints the error rustc-style — `file:line:col`, the source line itself, and a caret under the offending name: diff --git a/README.ru.md b/README.ru.md index e4c62907..0c8d38b0 100644 --- a/README.ru.md +++ b/README.ru.md @@ -167,10 +167,18 @@ python tests/test_gallery.py | `07_use_after_handoff` | **OWN002** | тронул буфер после того, как его забрал вызов | | `08_stack_buffer_escapes` | **OWN015** | вернул `Span` над `stackalloc` (dangling) | | `09_untracked_call` | **OWN040** | владение «отмыли» через непрозрачный вызов | +| `10_leak_in_loop` | **OWN001** | ресурс, который забирают на каждой итерации цикла и никогда не освобождают | +| `11_overspan_full_view` | **OWN025** | полноразмерный `buf.AsSpan()`, читающий за пределы rented-длины | `00_ok_clean` — чистый happy-path (rent → view → return), лоуэрится в exception-safe `ArrayPool` Rent/Return. +[`examples/gallery/cs/`](examples/gallery/cs/) — 7 из этих 12 кейсов в виде настоящего, +компилируемого C#, прогнанного через реальный пайплайн Roslyn-экстрактор → OwnIR → ядро +(не через dataflow самого `.own`-DSL), проверяется в CI. Остальные 5 (move/borrow/ +stack-buffer/unknown-call) — концепции, существующие только в DSL, для них пока нет +реального C#-детектора — почему именно, см. README той директории. + `check` печатает ошибку в стиле rustc — `file:line:col`, сама строка исходника и каретка под виновным именем: diff --git a/docs/notes/alpha-readiness.md b/docs/notes/alpha-readiness.md index 186ccf15..9770b61b 100644 --- a/docs/notes/alpha-readiness.md +++ b/docs/notes/alpha-readiness.md @@ -40,7 +40,7 @@ The bar for "showable": a person can reproduce the wow in ~3 minutes | **B** | GitHub Action | ✅ **built** — `action.yml`: `path`/`severity`/`format` (`github` / `msbuild` / `human` / `sarif`), purple shield branding. Matches the "stupidly simple YAML" bar. | Publish to Marketplace; pin the 6-line usage in the README. | | **C** | SARIF / PR annotations | ✅ **built** — SARIF 2.1.0 + GitHub annotations + reachability/evidence (P-015). | — | | **D** | 5 core diagnostics | ✅ **built, well past** — OWN001/002/003, OWN014, DI001–005, POOL001–005, WPF001–005 (catalog). The comment's `SUB001/SUB002/TMR001/DISP001/DI001` all exist *semantically*; the `SUB/TMR/DISP` catalog rename is the deferred consolidation item, not new work. | (naming only) land the catalog rename with the OwnIR-v1/profile-label work. | -| **E** | 10 bad/ok examples | ✅ **built** — 12 test-pinned gallery cases (`examples/gallery/`, incl. `00_ok_clean`) + extractor samples. | Add `.cs`-native (not `.own`) bad/ok pairs for the C# audience. | +| **E** | 10 bad/ok examples | ✅ **built** — 12 test-pinned gallery cases (`examples/gallery/`, incl. `00_ok_clean`) + extractor samples. `.cs`-native bad/ok pairs now exist for 7/12 (`examples/gallery/cs/`, verified through the real extractor in CI). | The remaining 5 (`04`/`05`/`06`/`08`/`09`) need move/borrow/stack-buffer/unknown-call detectors on the C# side — recorded in `examples/gallery/cs/README.md`, not yet scheduled. | | **F** | 3 real-world case studies | ◑ **partial** — one honest mining write-up (`real-world-mining.md`: Dapper, CsvHelper, **ScreenToGif** flagship `VideoSource` + two `SystemEvents` leaks, all TPs, clean on disciplined libs) + a 20-case `corpus/real-world/`. Raw material for 3 studies exists; the *packaged* studies don't. | Write 3 `bad → fixed → what others miss → how Own reports it` studies from existing finds. | | **G** | suppression + false-positive policy | ◑ **partial** — `[OwnIgnore("reason")]` designed (P-004), project-wide config is P-015 (draft); precision behaviour is strong & documented ("no FP from `using`"). | One consolidated user-facing page: suppression mechanism + explicit FP policy. | diff --git a/examples/gallery/cs/00_ok_clean.cs b/examples/gallery/cs/00_ok_clean.cs new file mode 100644 index 00000000..adb51176 --- /dev/null +++ b/examples/gallery/cs/00_ok_clean.cs @@ -0,0 +1,15 @@ +using System.IO; + +namespace GalleryCs; + +// 00_ok_clean — C#-native mirror of examples/gallery/00_ok_clean.own: +// acquire, use, release on every path. No finding expected. +public class OkClean +{ + public void Process() + { + var galleryClean = new MemoryStream(); + galleryClean.WriteByte(1); + galleryClean.Dispose(); + } +} diff --git a/examples/gallery/cs/01_leak_on_error_path.bad.cs b/examples/gallery/cs/01_leak_on_error_path.bad.cs new file mode 100644 index 00000000..a17d663c --- /dev/null +++ b/examples/gallery/cs/01_leak_on_error_path.bad.cs @@ -0,0 +1,19 @@ +using System.IO; + +namespace GalleryCs; + +// 01_leak_on_error_path (bad) — C#-native mirror of +// examples/gallery/01_leak_on_error_path.own: OWN001, a leak on one path. +// Real C#: Dispose() runs in the happy branch but is forgotten on the early-out. +public class LeakOnErrorPath +{ + public void Handle(bool flag) + { + var galleryLeakOnError = new MemoryStream(); + if (flag) + { + galleryLeakOnError.Dispose(); // released here ... + } + // ...but on the else path it's never closed -> leak + } +} diff --git a/examples/gallery/cs/01_leak_on_error_path.ok.cs b/examples/gallery/cs/01_leak_on_error_path.ok.cs new file mode 100644 index 00000000..f850c402 --- /dev/null +++ b/examples/gallery/cs/01_leak_on_error_path.ok.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace GalleryCs; + +// 01_leak_on_error_path (fixed) — disposed on every path -> silent. +public class LeakOnErrorPathOk +{ + public void Handle(bool flag) + { + var galleryLeakOnErrorOk = new MemoryStream(); + if (flag) + { + galleryLeakOnErrorOk.WriteByte(1); + } + galleryLeakOnErrorOk.Dispose(); + } +} diff --git a/examples/gallery/cs/02_use_after_release.bad.cs b/examples/gallery/cs/02_use_after_release.bad.cs new file mode 100644 index 00000000..ec8f6705 --- /dev/null +++ b/examples/gallery/cs/02_use_after_release.bad.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace GalleryCs; + +// 02_use_after_release (bad) — C#-native mirror of +// examples/gallery/02_use_after_release.own: OWN002, use after release (definite). +// Real C#: touching a stream after Dispose() -> ObjectDisposedException at runtime. +public class UseAfterRelease +{ + public void Run() + { + var galleryUseAfterRelease = new MemoryStream(); + galleryUseAfterRelease.WriteByte(1); + galleryUseAfterRelease.Dispose(); + galleryUseAfterRelease.WriteByte(2); // used after Dispose() -> OWN002 + } +} diff --git a/examples/gallery/cs/02_use_after_release.ok.cs b/examples/gallery/cs/02_use_after_release.ok.cs new file mode 100644 index 00000000..2da1d338 --- /dev/null +++ b/examples/gallery/cs/02_use_after_release.ok.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace GalleryCs; + +// 02_use_after_release (fixed) — no touch after Dispose() -> silent. +public class UseAfterReleaseOk +{ + public void Run() + { + var galleryUseAfterReleaseOk = new MemoryStream(); + galleryUseAfterReleaseOk.WriteByte(1); + galleryUseAfterReleaseOk.Dispose(); + } +} diff --git a/examples/gallery/cs/03_double_release.bad.cs b/examples/gallery/cs/03_double_release.bad.cs new file mode 100644 index 00000000..f033201a --- /dev/null +++ b/examples/gallery/cs/03_double_release.bad.cs @@ -0,0 +1,16 @@ +using System.IO; + +namespace GalleryCs; + +// 03_double_release (bad) — C#-native mirror of +// examples/gallery/03_double_release.own: OWN003, double release. +// Real C#: Dispose() called twice (and the type isn't idempotent about it). +public class DoubleRelease +{ + public void Run() + { + var galleryDoubleRelease = new MemoryStream(); + galleryDoubleRelease.Dispose(); + galleryDoubleRelease.Dispose(); + } +} diff --git a/examples/gallery/cs/03_double_release.ok.cs b/examples/gallery/cs/03_double_release.ok.cs new file mode 100644 index 00000000..8921b6cf --- /dev/null +++ b/examples/gallery/cs/03_double_release.ok.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace GalleryCs; + +// 03_double_release (fixed) — disposed exactly once -> silent. +public class DoubleReleaseOk +{ + public void Run() + { + var galleryDoubleReleaseOk = new MemoryStream(); + galleryDoubleReleaseOk.Dispose(); + } +} diff --git a/examples/gallery/cs/07_use_after_handoff.bad.cs b/examples/gallery/cs/07_use_after_handoff.bad.cs new file mode 100644 index 00000000..345f59d4 --- /dev/null +++ b/examples/gallery/cs/07_use_after_handoff.bad.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; + +namespace GalleryCs; + +// 07_use_after_handoff (bad) — C#-native mirror of +// examples/gallery/07_use_after_handoff.own: OWN002, use after ownership was +// consumed by a callee. Real C#: a method takes ownership (it will Dispose), +// then the caller touches the value again. Reduced from the same pattern as +// corpus/real-world/ownership-handoff-use/. +public static class UseAfterHandoff +{ + // Consumer: takes ownership of `sink` and closes it. + public static void Consume(Stream sink) + { + sink.CopyTo(Stream.Null); + sink.Dispose(); // Consume owns and closes it + } + + public static long Run(string path) + { + var galleryHandoff = File.OpenRead(path); + Consume(galleryHandoff); // ownership moves to Consume + return galleryHandoff.Length; // ...but we used it afterwards -> OWN002 + } +} diff --git a/examples/gallery/cs/07_use_after_handoff.ok.cs b/examples/gallery/cs/07_use_after_handoff.ok.cs new file mode 100644 index 00000000..e5edc093 --- /dev/null +++ b/examples/gallery/cs/07_use_after_handoff.ok.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace GalleryCs; + +// 07_use_after_handoff (fixed) — read what's needed BEFORE handing ownership +// off, then never touch the stream again -> silent. +public static class UseAfterHandoffOk +{ + public static void Consume(Stream sink) + { + sink.CopyTo(Stream.Null); + sink.Dispose(); + } + + public static long Run(string path) + { + var galleryHandoffOk = File.OpenRead(path); + long len = galleryHandoffOk.Length; // read first ... + Consume(galleryHandoffOk); // ... then move ownership last + return len; + } +} diff --git a/examples/gallery/cs/10_leak_in_loop.bad.cs b/examples/gallery/cs/10_leak_in_loop.bad.cs new file mode 100644 index 00000000..c1b6e140 --- /dev/null +++ b/examples/gallery/cs/10_leak_in_loop.bad.cs @@ -0,0 +1,18 @@ +using System.IO; + +namespace GalleryCs; + +// 10_leak_in_loop (bad) — C#-native mirror of examples/gallery/10_leak_in_loop.own: +// OWN001, a resource acquired every iteration but never released — leaks each pass. +public class LeakInLoop +{ + public void Drain(int n) + { + while (n > 0) + { + var galleryLoopLeak = new MemoryStream(); // opened every iteration ... + galleryLoopLeak.WriteByte(1); + n = n - 1; + } // ...never closed -> leak + } +} diff --git a/examples/gallery/cs/10_leak_in_loop.ok.cs b/examples/gallery/cs/10_leak_in_loop.ok.cs new file mode 100644 index 00000000..1ff0a7d4 --- /dev/null +++ b/examples/gallery/cs/10_leak_in_loop.ok.cs @@ -0,0 +1,19 @@ +using System.IO; + +namespace GalleryCs; + +// 10_leak_in_loop (fixed) — disposed within the same iteration it was +// acquired in -> balanced on every pass -> silent. +public class LeakInLoopOk +{ + public void Drain(int n) + { + while (n > 0) + { + var galleryLoopLeakOk = new MemoryStream(); + galleryLoopLeakOk.WriteByte(1); + galleryLoopLeakOk.Dispose(); + n = n - 1; + } + } +} diff --git a/examples/gallery/cs/11_overspan_full_view.bad.cs b/examples/gallery/cs/11_overspan_full_view.bad.cs new file mode 100644 index 00000000..b9e81ffe --- /dev/null +++ b/examples/gallery/cs/11_overspan_full_view.bad.cs @@ -0,0 +1,31 @@ +using System; +using System.Buffers; + +namespace GalleryCs; + +// 11_overspan_full_view (bad) — C#-native mirror of +// examples/gallery/11_overspan_full_view.own: OWN025, a full-length view of a +// pooled buffer reaching past its logical (rented) length. Real C#: +// ArrayPool.Rent(n) returns an OVERSIZED array (Length >= n); an unbounded +// buf.AsSpan() reads the stale [n, Length) tail a previous renter left behind. +// Reduced from the same pattern as corpus/real-world/arraypool-fullspan-overread/. +public static class OverspanFullView +{ + public static void Frame(int n) + { + byte[] galleryOverspanBuf = ArrayPool.Shared.Rent(n); + Fill(galleryOverspanBuf, n); + Emit(galleryOverspanBuf.AsSpan()); // full-length view -> OWN025 + ArrayPool.Shared.Return(galleryOverspanBuf); + } + + private static void Fill(byte[] b, int n) + { + for (int i = 0; i < n; i++) + { + b[i] = (byte)i; + } + } + + private static void Emit(ReadOnlySpan data) { } +} diff --git a/examples/gallery/cs/11_overspan_full_view.ok.cs b/examples/gallery/cs/11_overspan_full_view.ok.cs new file mode 100644 index 00000000..5b0ce68c --- /dev/null +++ b/examples/gallery/cs/11_overspan_full_view.ok.cs @@ -0,0 +1,27 @@ +using System; +using System.Buffers; + +namespace GalleryCs; + +// 11_overspan_full_view (fixed) — the view is BOUNDED to the logical length, +// buf.AsSpan(0, n); the oversized [n, Length) tail is never read -> silent. +public static class OverspanFullViewOk +{ + public static void Frame(int n) + { + byte[] galleryOverspanOkBuf = ArrayPool.Shared.Rent(n); + Fill(galleryOverspanOkBuf, n); + Emit(galleryOverspanOkBuf.AsSpan(0, n)); // bounded view: only the logical [0, n) + ArrayPool.Shared.Return(galleryOverspanOkBuf); + } + + private static void Fill(byte[] b, int n) + { + for (int i = 0; i < n; i++) + { + b[i] = (byte)i; + } + } + + private static void Emit(ReadOnlySpan data) { } +} diff --git a/examples/gallery/cs/README.md b/examples/gallery/cs/README.md new file mode 100644 index 00000000..c3127383 --- /dev/null +++ b/examples/gallery/cs/README.md @@ -0,0 +1,48 @@ +# The gallery, in real C# + +`examples/gallery/*.own` demonstrates each diagnostic through the abstract +`.own` DSL's generic `resource { acquire / release }` model. This directory is +the same gallery, in **real, compilable C#**, run through the actual pipeline +(`frontend/roslyn/OwnSharp.Extractor` → OwnIR → `python -m ownlang ownir`) — +not the toy language's own dataflow. + +Each pair is `_.bad.cs` (trips the code) / `_.ok.cs` (the +fix, silent). `00_ok_clean.cs` has no bad variant, matching the `.own` file it +mirrors. + +| Pair | Code | `.own` original | +|---|---|---| +| `00_ok_clean.cs` | (clean) | `examples/gallery/00_ok_clean.own` | +| `01_leak_on_error_path` | OWN001 | `examples/gallery/01_leak_on_error_path.own` | +| `02_use_after_release` | OWN002 | `examples/gallery/02_use_after_release.own` | +| `03_double_release` | OWN003 | `examples/gallery/03_double_release.own` | +| `07_use_after_handoff` | OWN002 | `examples/gallery/07_use_after_handoff.own` | +| `10_leak_in_loop` | OWN001 | `examples/gallery/10_leak_in_loop.own` | +| `11_overspan_full_view` | OWN025 | `examples/gallery/11_overspan_full_view.own` | + +Verified in CI, not just "should compile": the `C# leak extractor (Roslyn) -> +OwnIR -> core` job (`.github/workflows/ci.yml`, step "Gallery C#-native bad/ok +pairs (examples/gallery/cs/)") runs every file above through the real +extractor and asserts the exact code on each `.bad.cs` and silence on each +`.ok.cs` and on `00_ok_clean.cs`. + +## Why only 7 of the 12 `.own` cases have a real C# pair + +Five `.own` gallery cases exercise a concept that exists in the abstract DSL's +ownership/loans model but has **no real detector on the C# side today** — this +isn't a missing example, it's a missing capability, and faking a `.cs` file +that doesn't actually trip the claimed code through the real pipeline would be +exactly the "probably compiles" dishonesty this gallery exists to avoid: + +| `.own` case | Code | Why there's no real-C# pair yet | +|---|---|---| +| `04_use_after_move` | OWN005 | Needs `move`. The extractor/bridge (`ownlang/ownir.py`) never constructs a `Move` AST node — it isn't in the OwnIR flow-op vocabulary (`_FLOW_OPS`) at all, so no C# fact can reach it. | +| `05_dispose_while_view_live` | OWN008 | Needs a `borrow_mut ... as view { release ...; use view; }` conflict. `BorrowBlock`/`BorrowKind` are DSL-only AST nodes `ownir.py` never builds; the real-world analogue (return an `ArrayPool` array while a `Span` view is outstanding) lowers as a plain use-of-owner and produces **OWN002**, not OWN008. | +| `06_exclusive_while_shared` | OWN006 | Same `BorrowBlock` dependency as above — OwnIR-lowered functions never declare a `borrow`/`borrow_mut` resource member, so the core's shared-vs-exclusive lattice path can't be entered from real C#. | +| `08_stack_buffer_escapes` | OWN015 | Needs a `stack`-backed `BufferIntent` escaping via `return`. There is no `stackalloc` detector in the extractor and no buffer-kind field in any OwnIR-lowered acquire. | +| `09_untracked_call` | OWN040 | The core *can* raise OWN040, but the extractor only ever lowers **resolvable** first-party calls — an unresolvable one is dropped before it reaches OwnIR — and `check_facts` explicitly filters OWN040 out as a "synthetic-call artifact, never a real C# bug" (`ownlang/ownir.py`, belt-and-suspenders). By design, not a gap to close. | + +The first four are real recall gaps (interprocedural move/borrow/stack-region +tracking through real C# — see the README's "Where it cheats" item 1 on field +escape for the same family of hole). If/when they close, the matching `.bad.cs`/ +`.ok.cs` pair belongs here, verified the same way as the seven above. From 6b08e38045a28ea6defeaf8da849ed84fcff545c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 01:22:02 +0000 Subject: [PATCH 5/8] Address Codex review: pin rust-toolchain from master, fix fail-on-finding default dtolnay/rust-toolchain's convenience branches (stable/beta/nightly) get rewritten over time, so a commit pinned there can become unreachable; the upstream guidance is to pin a `master` commit and pass the channel via an explicit `toolchain:` input instead. docs/suppression-and-fp-policy.md had the Action's fail-on-finding default backwards: action.yml sets it to "true" (fails on a finding by default), not "false" -- the CLI (own-check.sh) is the one that defaults to off. Corrected and clarified the CLI-vs-Action distinction. --- .github/workflows/ci.yml | 3 ++- docs/suppression-and-fp-policy.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03e4d6cf..6280bb81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,8 +59,9 @@ jobs: working-directory: rust steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable, 2026-07-09 + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master, 2026-07-10 with: + toolchain: stable components: rustfmt, clippy - name: cargo fmt --check run: cargo fmt --check diff --git a/docs/suppression-and-fp-policy.md b/docs/suppression-and-fp-policy.md index ae52431b..ac32870d 100644 --- a/docs/suppression-and-fp-policy.md +++ b/docs/suppression-and-fp-policy.md @@ -44,7 +44,7 @@ fire on unprovable input. | Lever | Status | Scope | |---|---|---| | `--severity warning` | **works today** (P-013) | Global: downgrades every error-tier finding for that run to advisory. Per-run, not per-finding — an escape hatch for "show me everything, but don't fail the build yet," not a way to silence one specific site. | -| `--fail-on-finding` (off) | **works today** (P-013, the GitHub Action's default input) | Global: findings still print/annotate, but the process/step exit code stays 0. | +| `--fail-on-finding` set to off | **works today** (P-013) | Global: findings still print/annotate, but the process/step exit code stays 0. The CLI (`own-check.sh`) is off by default — you must pass the flag to make findings fail the shell. The GitHub Action inverts that for safety: its `fail-on-finding` input defaults to `"true"` (fails the step on a finding), so to get the "annotate but don't fail" behavior in CI you must explicitly set `fail-on-finding: "false"`. | | `[OwnIgnore("reason")]` | **designed, not implemented** (P-004) | Inline, per-site suppression attribute — the intended fine-grained escape hatch for a specific subscription/field the checker can't see enough context to clear. Referenced across P-001/P-004/P-010/P-014/P-017 as the standing design; there is no code behind it yet. If you need this today, the honest answer is: you don't have it — file the case so it informs the implementation. | | Project-wide config (`.ownrc`/`own.toml`) | **draft, not implemented** (P-015) | Per-check-category enable/disable + severity + per-path overrides (e.g. relax a category under `tests/`). Stub status — format (TOML vs INI vs JSON) and enforcement point are still open questions in the proposal. | | `corpus/oracle-fp-baseline.txt` | **exists, but not a user-facing suppression tool** | An allowlist the *oracle comparator* (`scripts/oracle_compare.py`, a dev/maintainer tool) uses to keep already-triaged false positives out of the `own-only` bucket on re-runs. It doesn't change what `own-check`/the Action reports — it only keeps the oracle's own triage queue from re-showing confirmed noise. | From 0270e13bb1e4bd039bd4e92ed4e7efb7f46d57ef Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 01:27:42 +0000 Subject: [PATCH 6/8] Address CodeRabbit review comments - ci.yml: qualify the "every uses:" claim (local uses: ./ composite refs aren't pinnable external deps); the new gallery step now captures ownir's real exit code and fails loudly on a hard error (rc>=2) instead of swallowing it with `|| true`, and adds a filename-based silence check on top of the variable-name one so an unanticipated finding in a clean fixture can't slip through. - README.md/README.ru.md: pin actions/checkout in the quickstart snippet (it was the one unpinned reference left after the SHA-pinning pass); the landing's own copy-paste example should match what it preaches. - docs/case-studies/screentogif-videosource.md: the prose said "four" subscriptions but the shown code (matching the pinned corpus/real-world/screentogif-loaded-subscription fixture) has three -- fixed to three throughout; noted that the fix closes the leak but isn't by itself idempotent against Window_Loaded firing twice before one Closing. - docs/notes/alpha-readiness.md: rows F/G and the "front door" section still described the case studies/suppression page/landing README as missing after this PR added them -- updated to reflect what's actually built now. - docs/suppression-and-fp-policy.md: added a language tag to the precedence code fence (markdownlint MD040). - rust/README.md: escaped a bare `|` in a table cell that was being parsed as an extra column (markdownlint MD056). --- .github/workflows/ci.yml | 32 ++++++++++++++--- README.md | 2 +- README.ru.md | 2 +- docs/case-studies/screentogif-videosource.md | 11 ++++-- docs/notes/alpha-readiness.md | 37 +++++++++++--------- docs/suppression-and-fp-policy.md | 2 +- rust/README.md | 2 +- 7 files changed, 59 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6280bb81..ed6bfeb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,12 @@ name: CI # Least privilege: every job only reads the repo (no job pushes or needs write). -# Every `uses:` is pinned to a commit SHA (with a `# vN` comment for the human- -# readable version) — see README "Where it cheats" item #7. `persist-credentials: -# false` is a separate, still-open hardening item (no job pushes or has secrets, -# so the exposure is checkout-token-lifetime only). +# Every third-party `uses:` is pinned to a commit SHA (with a `# vN` comment for +# the human-readable version) — see README "Where it cheats" item #7. (The local +# `uses: ./` composite-action references are this repo's own action, not a +# pinnable external dependency.) `persist-credentials: false` is a separate, +# still-open hardening item (no job pushes or has secrets, so the exposure is +# checkout-token-lifetime only). permissions: contents: read @@ -875,8 +877,17 @@ jobs: # (move/borrow/stack-buffer/unknown-call) have no real C# detector yet. dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \ examples/gallery/cs --flow-locals -o "$RUNNER_TEMP/gallery.json" - out=$(python -m ownlang ownir "$RUNNER_TEMP/gallery.json" || true) + set +e + out=$(python -m ownlang ownir "$RUNNER_TEMP/gallery.json") + rc=$? + set -e echo "$out" + # own-check's contract: 0 clean, 1 findings (the expected outcome here — the + # .bad.cs files are SUPPOSED to trip a finding), >=2 a hard error (bad facts / + # drifted contract) that must fail loudly, not be swallowed as "no findings". + if [ "$rc" -ge 2 ]; then + echo "FAIL: ownir hard error (rc=$rc) — bad OwnIR facts or a drifted contract"; exit 1 + fi echo "$out" | grep -qE "01_leak_on_error_path\.bad\.cs:[0-9]+:.*\[OWN001\].*'galleryLeakOnError'" \ || { echo "FAIL: expected OWN001 on galleryLeakOnError"; exit 1; } echo "$out" | grep -qE "02_use_after_release\.bad\.cs:[0-9]+:.*\[OWN002\].*'galleryUseAfterRelease'" \ @@ -892,6 +903,17 @@ jobs: for ok in galleryClean galleryLeakOnErrorOk galleryUseAfterReleaseOk galleryDoubleReleaseOk galleryHandoffOk galleryLoopLeakOk galleryOverspanOkBuf; do if echo "$out" | grep -q "'$ok'"; then echo "FAIL: clean gallery case '$ok' was reported"; exit 1; fi done + # Stronger silence check: the ok/clean fixture FILES themselves must never + # appear as a flagged location, not just the variable names we happened to + # anticipate above (an unexpected finding on some other identifier in one of + # these files would otherwise slip through the name-only loop). + for f in 00_ok_clean.cs 01_leak_on_error_path.ok.cs 02_use_after_release.ok.cs \ + 03_double_release.ok.cs 07_use_after_handoff.ok.cs 10_leak_in_loop.ok.cs \ + 11_overspan_full_view.ok.cs; do + if echo "$out" | grep -q "$f:"; then + echo "FAIL: clean fixture '$f' was flagged"; exit 1 + fi + done echo "OK: examples/gallery/cs/ bad/ok pairs match the .own gallery's codes 1:1 on the real extractor pipeline" - name: P-005 D5.4 T4 wrap/adopt (--flow-locals) run: | diff --git a/README.md b/README.md index a4a471f8..bde59454 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ release. ## Run it in CI — 6 lines ```yaml -- uses: actions/checkout@v4 +- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: PhysShell/own.net@main with: format: github # inline PR annotations; use "sarif" for the Security tab diff --git a/README.ru.md b/README.ru.md index 0c8d38b0..e38d6cb8 100644 --- a/README.ru.md +++ b/README.ru.md @@ -13,7 +13,7 @@ ## Запустить в CI — 6 строк ```yaml -- uses: actions/checkout@v4 +- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: PhysShell/own.net@main with: format: github # инлайн-аннотации в PR; "sarif" — для вкладки Security diff --git a/docs/case-studies/screentogif-videosource.md b/docs/case-studies/screentogif-videosource.md index 5da2f359..d65bdb7d 100644 --- a/docs/case-studies/screentogif-videosource.md +++ b/docs/case-studies/screentogif-videosource.md @@ -8,7 +8,7 @@ first real-world mining run ## Bad -A `Window` reads its view-model out of `DataContext`, then wires four inline +A `Window` reads its view-model out of `DataContext`, then wires three inline lambdas to the view-model's custom events inside `Window_Loaded`: ```csharp @@ -69,7 +69,12 @@ private void OnClose(object sender, EventArgs e) => DialogResult = true; ``` No behavior change, no new fields — the fix is entirely "have a handle to -unsubscribe with, and use it in the close path that was already there." +unsubscribe with, and use it in the close path that was already there." This +closes the leak (the window is now collectible after `Closing`); it does not +by itself make `Window_Loaded` idempotent against firing twice before a single +`Closing` — that residual double-subscribe edge case is the same one flagged +above, and would need a detach-before-attach guard (or a one-time-subscribe +flag) to close fully. ## What others miss @@ -80,7 +85,7 @@ plain C# event, the kind `CA2213`/`IDisposableAnalyzers`/CodeQL's `cs/local-not-disposed` don't model at all. Cross-checked against CodeQL on the same commit ([`docs/notes/oracle.md`](../notes/oracle.md)): its findings on ScreenToGif are entirely the Dispose/RAII class (`OpenFileDialog`, `Pen`, -`Bitmap`, …); it flags none of the four `VideoSource` subscriptions, because its +`Bitmap`, …); it flags none of the three `VideoSource` subscriptions, because its query set has no "event subscribed, never unsubscribed" rule. Own.NET and CodeQL are complementary here, not redundant — see the [Dispose-agreement case study](dispose-agreement-with-codeql.md) for where they diff --git a/docs/notes/alpha-readiness.md b/docs/notes/alpha-readiness.md index 9770b61b..71c6079d 100644 --- a/docs/notes/alpha-readiness.md +++ b/docs/notes/alpha-readiness.md @@ -41,28 +41,30 @@ The bar for "showable": a person can reproduce the wow in ~3 minutes | **C** | SARIF / PR annotations | ✅ **built** — SARIF 2.1.0 + GitHub annotations + reachability/evidence (P-015). | — | | **D** | 5 core diagnostics | ✅ **built, well past** — OWN001/002/003, OWN014, DI001–005, POOL001–005, WPF001–005 (catalog). The comment's `SUB001/SUB002/TMR001/DISP001/DI001` all exist *semantically*; the `SUB/TMR/DISP` catalog rename is the deferred consolidation item, not new work. | (naming only) land the catalog rename with the OwnIR-v1/profile-label work. | | **E** | 10 bad/ok examples | ✅ **built** — 12 test-pinned gallery cases (`examples/gallery/`, incl. `00_ok_clean`) + extractor samples. `.cs`-native bad/ok pairs now exist for 7/12 (`examples/gallery/cs/`, verified through the real extractor in CI). | The remaining 5 (`04`/`05`/`06`/`08`/`09`) need move/borrow/stack-buffer/unknown-call detectors on the C# side — recorded in `examples/gallery/cs/README.md`, not yet scheduled. | -| **F** | 3 real-world case studies | ◑ **partial** — one honest mining write-up (`real-world-mining.md`: Dapper, CsvHelper, **ScreenToGif** flagship `VideoSource` + two `SystemEvents` leaks, all TPs, clean on disciplined libs) + a 20-case `corpus/real-world/`. Raw material for 3 studies exists; the *packaged* studies don't. | Write 3 `bad → fixed → what others miss → how Own reports it` studies from existing finds. | -| **G** | suppression + false-positive policy | ◑ **partial** — `[OwnIgnore("reason")]` designed (P-004), project-wide config is P-015 (draft); precision behaviour is strong & documented ("no FP from `using`"). | One consolidated user-facing page: suppression mechanism + explicit FP policy. | +| **F** | 3 real-world case studies | ✅ **built** — `docs/case-studies/`: `screentogif-videosource.md` (flagship view→view-model handler leak), `screentogif-systemevents.md` (two independent `SystemEvents` leaks), `dispose-agreement-with-codeql.md` (the Dispose/RAII class where Own.NET agrees with CodeQL/Infer#), all `bad → fixed → what others miss → how Own reports it`, linked from the README. | The wider proof (20–50 OSS repos, days 31–60 below) is still open — these three are the *packaged* studies, not the full real-world sweep. | +| **G** | suppression + false-positive policy | ✅ **built** — `docs/suppression-and-fp-policy.md` consolidates the FP policy (`OWN050` honest-skip, "no FP from `using`") with the suppression mechanisms, honestly marking `[OwnIgnore]` (P-004) as designed-not-implemented and project config (P-015) as draft-not-implemented — today's only working lever is `--severity`/`--fail-on-finding`. | — | -**Plus the front door (not in A–G but the real blocker):** `README.md` is now -bilingual (English default + a `README.ru.md` variant), but still `# OwnLang — PoC` — -deep, research-framed, no wedge landing. There is **no 20-second landing / -copy-paste install / Action quickstart** at the top. Per the comment's own -open-source-path list (README-in-20s → copy-paste → bad/ok → Action → SARIF → -suppression → "why not Sonar/CodeQL"), this is the highest-leverage missing piece. +**Plus the front door (not in A–G but the real blocker):** ✅ **built** — +`README.md`/`README.ru.md` now open with a 20-second landing (verbatim pitch, +slogans, a 6-line Action quickstart, a local one-liner, one real bad/fixed +example, a "why not Sonar/CodeQL" link) instead of `# OwnLang — PoC`; the prior +research-framed opening moved down rather than being deleted. Per the comment's +own open-source-path list (README-in-20s → copy-paste → bad/ok → Action → SARIF → +suppression → "why not Sonar/CodeQL"), every step of that path now exists. ## Honest verdict -**The engine is past alpha on *capability* (D/E strong, B/C built). The gap to -"showable" is *packaging and presentation*, not analysis power:** +**The engine is past alpha on *capability* (D/E strong, B/C built). F/G and the +front door have since closed too — the remaining packaging gap is narrower:** -1. a single `ownsharp check MyApp.sln` tool (**A**); -2. a wedge landing README + copy-paste quickstart (front door); -3. three packaged case studies from finds we already have (**F**); -4. one consolidated suppression / false-positive page (**G**). +1. a single `ownsharp check MyApp.sln` tool (**A**) — still open; +2. ~~a wedge landing README + copy-paste quickstart (front door)~~ — **done**; +3. ~~three packaged case studies from finds we already have (**F**)~~ — **done**; +4. ~~one consolidated suppression / false-positive page (**G**)~~ — **done**. None of those is research; all are the difference between "interesting PoC" and -"people install it." That ordering *is* the day 1–30 milestone. +"people install it." **A** is now the one item standing between here and the +day 1–30 milestone. ## The 20% rule (other stacks) @@ -83,8 +85,9 @@ until the .NET alpha above is delicious. Do not let the spike exceed 20%. ## 90-day shape (sequencing, not a schedule) -- **Days 1–30 — make the .NET alpha tasty:** close A, the README front door, F, G - (B/C/D/E already done). Suppression UX + bad/ok corpus polish. +- **Days 1–30 — make the .NET alpha tasty:** close A (B/C/D/E/F/G and the README + front door already done). Suppression UX + bad/ok corpus polish continue as + bug-driven follow-ups, not a blocking gate. - **Days 31–60 — real-world proof:** run over 20–50 OSS .NET/WPF/Avalonia/WinForms repos; table of findings / confirmed / FP / unsupported; 2 case studies; compare with CodeQL / NetAnalyzers / Infer# where possible (the oracle, `docs/notes/oracle.md`). diff --git a/docs/suppression-and-fp-policy.md b/docs/suppression-and-fp-policy.md index ac32870d..096fdd75 100644 --- a/docs/suppression-and-fp-policy.md +++ b/docs/suppression-and-fp-policy.md @@ -61,7 +61,7 @@ carry. Precedence, once both land (P-015's draft order): -``` +```text CLI flag > inline [OwnIgnore] > config file > built-in default ``` diff --git a/rust/README.md b/rust/README.md index d53dea4f..20a8372c 100644 --- a/rust/README.md +++ b/rust/README.md @@ -72,7 +72,7 @@ there: | `subscribe_not_to` | `subscribe self from bus;` | `expected 'to' in 'subscribe self to ' (got IDENT 'from')` | | `buffer_positional_after_named` | `Buffer.stack(1, max = 2, 3)` | `only the leading size may be positional in a buffer intent; later arguments must be named` | | `unicode_idents` | `module м { fn f(х: int) {} }` | accepted; digest matches Python's | -| `full_module` | a resource + 2 externs + 2 fns, one with a `while` | digest `m=Demo r=2 e=4 f=2 p=1 l=2 fns=[setup/2/16,empty/0/1] conds=[n < 10|n]` | +| `full_module` | a resource + 2 externs + 2 fns, one with a `while` | digest `m=Demo r=2 e=4 f=2 p=1 l=2 fns=[setup/2/16,empty/0/1] conds=[n < 10\|n]` | The point of the digest cases isn't the string itself — it's that Rust and Python parsed the **same shape** (resource/extern/fn counts, statement From 77fd29186e79fce8b77c4a3cf924b7d81a666a00 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 01:32:04 +0000 Subject: [PATCH 7/8] docs: restore the four-subscription truth in the VideoSource study; disambiguate OWN001 vs OWN014 in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review fix in 0270e13 resolved the count mismatch by lowering the prose to three, but the ground truth is four subscriptions (real-world-mining.md pins VideoSource.xaml.cs:50/67/75/83; the corpus reduction's notes.md names the omitted event, ShowWarningRequested) — add the fourth event to both examples instead. Also: annotate the quickstart's own.net@main as a deliberate pre-release choice (no tags yet), and state that the OWN001 SystemEvents error and the OWN014 region escape are two pinned modelings of one shape, not one finding changing codes (both corpus-pinned). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LsWw4Ay8KLTHFom1HvRu3U --- README.md | 10 ++++++++-- README.ru.md | 9 +++++++-- docs/case-studies/screentogif-videosource.md | 11 ++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bde59454..b88ce07f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ release. ```yaml - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -- uses: PhysShell/own.net@main +- uses: PhysShell/own.net@main # pre-release: no tagged release yet — pin a commit SHA for reproducibility with: format: github # inline PR annotations; use "sarif" for the Security tab fail-on-finding: "true" @@ -246,7 +246,13 @@ but a *region escape*: the extractor lowers it to a tokenless `capture` fact, an **the same core** produces **OWN014** (the object is promoted to process lifetime; a matching `-=` clears the finding) — the WPF escape as a profile of the general region model, not a separate detector (P-004 WPF005; sample -`StaticEventEscapeViewModel`). An injected source (unknown lifetime) stays an +`StaticEventEscapeViewModel`). These are two pinned modelings of the same +underlying shape, not one finding changing codes: the quickstart's +`GraphicsConfigurationDialog` verdict near the top of this README is the +token-tier **OWN001** error (pinned in +`corpus/real-world/screentogif-systemevents-leak`), while the region lowering +of the same static-source pattern is pinned as **OWN014** in +`corpus/wpf/systemevents-region-escape`. An injected source (unknown lifetime) stays an OWN001 warning — the subscription profile's deliberate down-tier (OWN001 is otherwise an error) — until ownership modelling can prove its lifetime. diff --git a/README.ru.md b/README.ru.md index e38d6cb8..25b9020d 100644 --- a/README.ru.md +++ b/README.ru.md @@ -14,7 +14,7 @@ ```yaml - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -- uses: PhysShell/own.net@main +- uses: PhysShell/own.net@main # пре-релиз: тегов ещё нет — для воспроизводимости пиньте commit SHA with: format: github # инлайн-аннотации в PR; "sarif" — для вкладки Security fail-on-finding: "true" @@ -250,7 +250,12 @@ CustomerViewModel.cs:9: error: [OWN001] event 'bus.CustomerChanged' is subscribe `capture`-факт, и **то же ядро** выдаёт **OWN014** (объект промотится в process-lifetime; парный `-=` снимает находку) — WPF-escape как профиль общей region-модели, а не отдельный детектор (P-004 WPF005; сэмпл -`StaticEventEscapeViewModel`). Источник-инъекция (неизвестное время жизни) +`StaticEventEscapeViewModel`). Это две запиненные модели одной и той же +формы, а не одна находка, меняющая код: вердикт для +`GraphicsConfigurationDialog` в начале этого README — токен-уровневая ошибка +**OWN001** (запинена в `corpus/real-world/screentogif-systemevents-leak`), +а region-понижение того же статического паттерна запинено как **OWN014** в +`corpus/wpf/systemevents-region-escape`. Источник-инъекция (неизвестное время жизни) остаётся OWN001-warning'ом — это осознанный down-tier профиля подписок (по умолчанию OWN001 — ошибка), — пока ownership-моделирование не докажет его время жизни. Ядро одно (не второй чекер на C#): экстрактор только производит факты. dotnet diff --git a/docs/case-studies/screentogif-videosource.md b/docs/case-studies/screentogif-videosource.md index d65bdb7d..f6e24476 100644 --- a/docs/case-studies/screentogif-videosource.md +++ b/docs/case-studies/screentogif-videosource.md @@ -8,8 +8,9 @@ first real-world mining run ## Bad -A `Window` reads its view-model out of `DataContext`, then wires three inline -lambdas to the view-model's custom events inside `Window_Loaded`: +A `Window` reads its view-model out of `DataContext`, then wires four inline +lambdas to the view-model's custom events inside `Window_Loaded` (real file, +lines 50/67/75/83): ```csharp public partial class VideoSource : Window @@ -26,6 +27,7 @@ public partial class VideoSource : Window { _viewModel.ShowErrorRequested += (_, args) => StatusBand.Error(args?.ToString()); _viewModel.HideErrorRequested += (_, _) => StatusBand.Hide(); + _viewModel.ShowWarningRequested += (_, args) => StatusBand.Warning(args?.ToString()); _viewModel.CloseRequested += (_, _) => DialogResult = true; // ...never unsubscribed } @@ -53,6 +55,7 @@ private void Window_Loaded(object sender, RoutedEventArgs e) { _viewModel.ShowErrorRequested += OnShowError; _viewModel.HideErrorRequested += OnHideError; + _viewModel.ShowWarningRequested += OnShowWarning; _viewModel.CloseRequested += OnClose; } @@ -60,11 +63,13 @@ private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs { _viewModel.ShowErrorRequested -= OnShowError; _viewModel.HideErrorRequested -= OnHideError; + _viewModel.ShowWarningRequested -= OnShowWarning; _viewModel.CloseRequested -= OnClose; } private void OnShowError(object sender, EventArgs args) => StatusBand.Error(args?.ToString()); private void OnHideError(object sender, EventArgs e) => StatusBand.Hide(); +private void OnShowWarning(object sender, EventArgs args) => StatusBand.Warning(args?.ToString()); private void OnClose(object sender, EventArgs e) => DialogResult = true; ``` @@ -85,7 +90,7 @@ plain C# event, the kind `CA2213`/`IDisposableAnalyzers`/CodeQL's `cs/local-not-disposed` don't model at all. Cross-checked against CodeQL on the same commit ([`docs/notes/oracle.md`](../notes/oracle.md)): its findings on ScreenToGif are entirely the Dispose/RAII class (`OpenFileDialog`, `Pen`, -`Bitmap`, …); it flags none of the three `VideoSource` subscriptions, because its +`Bitmap`, …); it flags none of the four `VideoSource` subscriptions, because its query set has no "event subscribed, never unsubscribed" rule. Own.NET and CodeQL are complementary here, not redundant — see the [Dispose-agreement case study](dispose-agreement-with-codeql.md) for where they From 74a0ef49c04b117d73c29c15be1ebe76907c6682 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 01:36:12 +0000 Subject: [PATCH 8/8] docs: note the corpus entry is a three-event reduction of the four-subscription original MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconciles the two review resolutions: the study's examples mirror the real file (four subscriptions, 77fd291), while the cited corpus regression is a deliberate three-event reduction — say so instead of letting the two snippet sets silently disagree. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LsWw4Ay8KLTHFom1HvRu3U --- docs/case-studies/screentogif-videosource.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/case-studies/screentogif-videosource.md b/docs/case-studies/screentogif-videosource.md index f6e24476..961dfbf8 100644 --- a/docs/case-studies/screentogif-videosource.md +++ b/docs/case-studies/screentogif-videosource.md @@ -119,4 +119,7 @@ Contrast the [SystemEvents case study](screentogif-systemevents.md), where the subscription source is `static` and the same shape escalates to a hard error. Regression-locked as `corpus/real-world/screentogif-loaded-subscription/` -(`before.cs`/`after.cs`, pinned by `tests/test_corpus.py`). +(`before.cs`/`after.cs`, pinned by `tests/test_corpus.py`) — the corpus entry is +a deliberate three-event *reduction* of the four-subscription original (it drops +`ShowWarningRequested`; the pattern is identical), so its snippets are one line +shorter than the real-file examples above.