|
| 1 | +# Real-world mining run — milestone 1 (WPF leak spike) |
| 2 | + |
| 3 | +This is the write-up of the first end-to-end run of the analyser over **real, |
| 4 | +unmodified OSS C#**, the `ROADMAP.md` milestone 1: *"find 1–3 real |
| 5 | +subscription/timer leaks in real code (P-004)."* It is the honest record of what |
| 6 | +the tool actually surfaces on real code — signal, precision, and the next gap the |
| 7 | +run revealed. |
| 8 | + |
| 9 | +## How it ran (the loop) |
| 10 | + |
| 11 | +The Roslyn extractor needs a .NET SDK, which the dev sandbox does not have, and |
| 12 | +the supported on-demand path (`mine.yml`, `workflow_dispatch`) could not be |
| 13 | +triggered from the automation token. So the run used a small **push-triggered** |
| 14 | +miner (`.github/workflows/mine-on-push.yml`) driven by a sentinel file |
| 15 | +(`corpus/mine-target.txt`): bump the target, and CI clones the repo, runs |
| 16 | +`scripts/mine.sh` (extractor → OwnIR → core, no per-repo build), and echoes the |
| 17 | +report to the job log. The findings were then triaged by reading the flagged |
| 18 | +`file:line` in a local clone of the target. |
| 19 | + |
| 20 | +> `mine-on-push.yml` + `corpus/mine-target.txt` are **dev-loop scaffolding** |
| 21 | +> (dev-branch only) — they exist because the token can't dispatch `mine.yml`. |
| 22 | +> They should not be merged to `main`; the supported path stays `mine.yml`. |
| 23 | +
|
| 24 | +## What it found |
| 25 | + |
| 26 | +| Repo (commit) | findings | triage | |
| 27 | +|---|---|---| |
| 28 | +| `DapperLib/Dapper` @72a54c4 | 1 × OWN001, 1 × OWN050 | TP: `BenchmarkBase._connection` — an undisposed `SqlConnection` field (benchmark project). | |
| 29 | +| `JoshClose/CsvHelper` @33970e5 | 43 × OWN001 | TP: undisposed `StreamReader/Writer/CsvDataReader` **locals in tests**; every `using`-scoped local was correctly skipped. | |
| 30 | +| `NickeManarin/ScreenToGif` @27a49c3 (WPF profile off) | 8 × OWN001, 210 × OWN050 | **flagship** below + a likely-benign `App`→`AppDomain.UnhandledException` (process-lived subscriber). | |
| 31 | +| `NickeManarin/ScreenToGif` @27a49c3 (WPF profile **on**) | 123 × OWN001, 37 × OWN050 | unlock works (OWN050 ↓), but exposes the self-owned-control precision gap below. | |
| 32 | + |
| 33 | +**Precision.** Every finding triaged by hand was a *real* undisposed/undetached |
| 34 | +resource — no false positives from `using` (the extractor models it as release), |
| 35 | +and the severity tiering behaved as designed. The findings cluster where |
| 36 | +disposal discipline is intentionally lax (test/benchmark code) — real, but mostly |
| 37 | +low-severity in practice. Disciplined shipping libraries came up clean, which is |
| 38 | +the *precision* result the methodology wants to see. |
| 39 | + |
| 40 | +## The flagship finding (milestone 1 ✔) |
| 41 | + |
| 42 | +`ScreenToGif/Windows/Other/VideoSource.xaml.cs:50-83` — a WPF `Window` subscribes |
| 43 | +**four inline lambdas to its view-model's custom events in `Window_Loaded` and |
| 44 | +never detaches them** (`Window_Closing` does no `-=`). This is the canonical |
| 45 | +view↔view-model lifetime shape that generic IDisposable/CA analyzers miss; it |
| 46 | +resolves **without** the WPF reference pack because the events are the app's own |
| 47 | +types. The extractor rates it **warning** (the source `_viewModel` is injected, so |
| 48 | +its lifetime can't be proven) and notes the lambdas have no `-=` handle — the |
| 49 | +honest verdict (it may be a collectable view↔vm cycle, but the |
| 50 | +duplicate-handler-on-reload bug is real). Captured as a regression: |
| 51 | +`corpus/real-world/screentogif-loaded-subscription/`. |
| 52 | + |
| 53 | +## The WPF reference unlock (and the gap it revealed) |
| 54 | + |
| 55 | +The flagship detectors went blind on framework events because the extractor only |
| 56 | +loaded the runtime's trusted-platform assemblies — `Button.Click`, |
| 57 | +`DispatcherTimer.Tick`, etc. fell out as OWN050. The extractor now also loads |
| 58 | +`*.dll` from each dir in the **`OWN_EXTRA_REF_DIRS`** env var (deduped by simple |
| 59 | +name against the TPA); the miner materializes the WindowsDesktop ref pack on Linux |
| 60 | +(a `net8.0-windows`/`UseWPF` stub restores it via `EnableWindowsTargeting` → 47 ref |
| 61 | +dlls) and points the var at it. On ScreenToGif this drove **OWN050 210 → 37**. |
| 62 | + |
| 63 | +The change is **off by default** (`OWN_EXTRA_REF_DIRS` unset → unchanged |
| 64 | +behaviour; the whole existing suite is the guard), so it is a safe, opt-in |
| 65 | +capability add. |
| 66 | + |
| 67 | +But unlocking framework events also jumped **OWN001 8 → 123**, and the new |
| 68 | +findings are dominated by **self-owned controls**: `_thumbBottomLeft.DragDelta +=` |
| 69 | +(a `Thumb` the adorner constructs via `BuildCorner(ref _thumb, …)`), |
| 70 | +`_upButton.Click +=` (a template part fetched with `GetTemplateChild`), etc. A |
| 71 | +class subscribing to a control it *owns* is a collectable cycle — not a leak — but |
| 72 | +the existing self-owned exemption only recognises fields built by a direct |
| 73 | +`field = new …`, so indirect construction (`ref`/`out` params) and template parts |
| 74 | +slip through. **Next step:** extend the self-owned exemption to those two shapes |
| 75 | +before the WPF profile is on by default. This is textbook bug-driven expansion — |
| 76 | +the real-world run defined the next unit of work. |
| 77 | + |
| 78 | +## Reproduce |
| 79 | + |
| 80 | +Point `mine.yml` (Actions → *mine (corpus)* → Run workflow) at a target; for the |
| 81 | +WPF profile, set `OWN_EXTRA_REF_DIRS` to a WindowsDesktop `ref/net8.0` dir (the |
| 82 | +miner shows how to materialize it). Read the report in the run summary / artifact. |
0 commit comments