|
1 | | -# WPF subscription used after Dispose |
| 1 | +# WPF field-use-after-dispose reached INDIRECTLY (through a helper) |
2 | 2 |
|
3 | | -**Pattern:** a ViewModel unsubscribes / disposes its subscription on close, but a |
4 | | -callback that was already queued on the dispatcher still runs and touches the |
5 | | -disposed, subscription-backed state. In real code this is an |
6 | | -`ObjectDisposedException` or a read of torn state — the use-after-dispose cousin |
| 3 | +**Pattern:** a ViewModel owns an `IDisposable` (here a `SqlConnection`) and subscribes a handler to |
| 4 | +an event source. On teardown `Dispose()` disposes the connection (and the subscription token), but a |
| 5 | +callback already queued on the dispatcher still runs after `Dispose()` and reaches the disposed |
| 6 | +connection **indirectly** — the handler calls a private `Refresh()` helper that reads `_conn`. In |
| 7 | +real code this is an `ObjectDisposedException` or a read of torn state — the use-after-dispose cousin |
7 | 8 | of the zombie-ViewModel leak. |
8 | 9 |
|
9 | | -**What the checker says:** using a resource after its `release` (Dispose) is the |
10 | | -generic **OWN002** (use after release), carrying the resource-kind tag: |
| 10 | +**What's new — the extractor catches the one hop.** The Roslyn extractor's field-mediated |
| 11 | +use-after-dispose pass (under `--flow-locals`) already caught a **direct** `_field.Member` read in a |
| 12 | +live handler (`field-use-after-dispose`). This slice chases a **single hop**: a subscribed handler |
| 13 | +that calls a **private same-class helper** (`Refresh()` / `this.Refresh()`) which itself |
| 14 | +*unguardedly* reads a disposed field — with no `if (_disposed) return;` guard before the call — is |
| 15 | +lowered to a synthetic `acquire`/`release`/`use` flow → **OWN002**, via the existing OwnIR bridge |
| 16 | +(no new diagnostic). On the real C# the `corpus-benchmark` job scores `before.cs` as caught and |
| 17 | +`after.cs` (the guarded fix) as silent. |
11 | 18 |
|
12 | | -```text |
13 | | -$ python -m ownlang check corpus/wpf/handler-use-after-dispose/case.own |
14 | | -case.own:16:9: error: [OWN002] use 'sub' after it was released |
15 | | - [resource: subscription token] |
16 | | - 16 | use sub; |
17 | | - ^ |
18 | | -``` |
| 19 | +**Precision (why it stays low-FP).** One hop only — a deeper chain stays an honest miss. The helper |
| 20 | +must be a **private instance** method (not a public/virtual member with a broader contract); both the |
| 21 | +handler (before the call) and the helper must lack a disposed-guard; and the field read is a |
| 22 | +**direct** `this`-owned `_field.Member`. The guard exclusion is the canonical fix, so the guarded |
| 23 | +`after.cs` is silent. |
19 | 24 |
|
20 | | -**Honesty / scope.** `case.own` is a *hand reduction* of the C# pattern, not |
21 | | -direct C# extractor output (the C# extractor in P-001 is narrow — event |
22 | | -subscriptions only). It shows the ownership |
23 | | -*logic* maps onto the real bug; it does not model the dispatcher queue or |
24 | | -exception flow. `before.cs` / `after.cs` are representative, not a verbatim copy |
25 | | -of one PR. |
| 25 | +**Honesty / scope.** This catches the **direct** read (`field-use-after-dispose`) and now the |
| 26 | +**one-hop indirect** read (this case). A two-plus-hop chain, or a read through a field/property |
| 27 | +indirection, remains an honest extractor miss — the `case.own` reduction still fires OWN002, showing |
| 28 | +the ownership logic maps onto the real bug. `before.cs` / `after.cs` are representative of the bug |
| 29 | +and its fix. |
| 30 | + |
| 31 | +Reference: [P-007](../../../docs/proposals/P-007-arraypool-span.md); the direct twin is |
| 32 | +`field-use-after-dispose`; the late-callback framing matches `zombie-viewmodel`. |
0 commit comments