Skip to content

fix(extractor): exempt process-host AppDomain event subscriptions from OWN014 (mined Npgsql)#88

Merged
PhysShell merged 3 commits into
mainfrom
claude/fp-appdomain-shutdown-events
Jun 23, 2026
Merged

fix(extractor): exempt process-host AppDomain event subscriptions from OWN014 (mined Npgsql)#88
PhysShell merged 3 commits into
mainfrom
claude/fp-appdomain-shutdown-events

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Mined from npgsql/npgsql

First fix from the Npgsql mining run (121 findings, OWN050=0). This is the cleanest, most universal FP.

The bug

PoolManager's static ctor:

static PoolManager()
{
    // When the appdomain gets unloaded (e.g. web app redeployment) attempt to nicely
    // close idle connectors to prevent errors in PostgreSQL logs (#491).
    AppDomain.CurrentDomain.DomainUnload += (_, _) => ClearAll();
    AppDomain.CurrentDomain.ProcessExit += (_, _) => ClearAll();
}

Both were reported as OWN014 region escapes ("promotes PoolManager to the source's lifetime"). But subscribing to a process-host AppDomain event is never a leak — ProcessExit/DomainUnload run at shutdown, UnhandledException/FirstChanceException on every unhandled throw, and the AppDomain is the process host. The handler is meant to live for the whole process; the "escape" is the intent.

The fix

IsProcessLifetimeAppDomainEvent exempts those four System.AppDomain events from OWN014, alongside the existing self-owned-source and static-handler exemptions. Scoped to the event's resolved declaring type — so a project's own AppDomain-named type isn't matched, and a non-AppDomain process-lived static event still escapes.

Regression guard

AppDomainShutdownSample.csShutdownCleanup (ProcessExit/DomainUnload/UnhandledException lambdas) stays silent; NonAppDomainSubscriber (a lambda on a non-AppDomain static event) still raises OWN014.

Npgsql triage (remaining, for follow-ups)

  • NpgsqlEventSource ×8PollingCounter/IncrementingPollingCounter fields in an : EventSource singleton ("fine to leave them around" per Npgsql); a process-lived-EventSource exemption (like App).
  • NpgsqlDataSource CTS — actually disposed, but via a local alias the field detector misses (alias-tracking frontier).
  • Honest/real: _setupMappingsSemaphore (deliberately finalizer-disposed) + ~100 benchmark/test NpgsqlCommand/Connection fields.

Independent of the still-unlanded #2/#3/#4 (subscription detector, not field/pool), so it bases cleanly on main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Reduced false positives in leak/region-escape reporting for subscriptions to AppDomain process-lifetime events when handlers follow the non-capturing exemption pattern.
  • Tests

    • Added/updated a Roslyn sample covering AppDomain shutdown/diagnostics-hook subscriptions, including both exempt (non-capturing) and non-exempt (capturing and non-AppDomain static event) scenarios.
  • CI / Validation

    • Updated the CI extractor/verification suite to run on the new sample and assert the expected OWN014 outcomes for each case.

{
public NonAppDomainSubscriber()
{
SomeBus.Pinged += (_, _) => Handle(); // static event, lambda, not AppDomain -> OWN014
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 88715ee4-df46-494c-ba16-d370b53fe277

📥 Commits

Reviewing files that changed from the base of the PR and between 685fbf6 and 63b0bc5.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci.yml

📝 Walkthrough

Walkthrough

Adds IsProcessLifetimeAppDomainEvent and HandlerRetainsNoInstance predicates to OwnSharp.Extractor/Program.cs that classify System.AppDomain events (ProcessExit, DomainUnload, UnhandledException, FirstChanceException) as process-lifetime exemptions when paired with non-capturing handlers, integrates these predicates into the += subscription non-leak filter, adds a sample file demonstrating both exempt and non-exempt patterns, and extends the CI workflow to include and verify those patterns.

Changes

AppDomain Process-Lifetime Event Subscription Exemption

Layer / File(s) Summary
AppDomain process-lifetime predicates and leak filter integration
frontend/roslyn/OwnSharp.Extractor/Program.cs
Adds IsProcessLifetimeAppDomainEvent and HandlerRetainsNoInstance helpers to classify System.AppDomain events and ensure handlers do not capture instance state. Extends the += non-leak condition to skip those events when paired with non-capturing handlers, alongside existing self-owned and static-handler exemptions.
Sample file demonstrating exempt and non-exempt patterns
frontend/roslyn/samples/AppDomainShutdownSample.cs
Introduces ShutdownCleanup (non-capturing AppDomain.CurrentDomain subscriptions), CapturingShutdownSubscriber (AppDomain subscription capturing instance state), SomeBus (static event), and NonAppDomainSubscriber (lambda on non-AppDomain static event) to cover both exempt and non-exempt subscription cases.
CI workflow updates and verification assertions
.github/workflows/ci.yml
Adds AppDomainShutdownSample.cs to the extractor input list and extends verification to assert ShutdownCleanup produces no OWN014 findings while CapturingShutdownSubscriber and NonAppDomainSubscriber still trigger OWN014, validating the exemption scope.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PhysShell/Own.NET#28: Both PRs modify event-subscription leak detection in Program.cs to distinguish capturing vs non-detachable handlers, applied here for AppDomain process-lifetime events and previously for lambda handler severity.
  • PhysShell/Own.NET#86: Both PRs modify Program.cs logic for exempting leak/region-escape reporting by analyzing whether event handlers are treated as non-capturing/static.

Poem

🐇 A bunny once feared every hook and every bind,
That AppDomain shutdowns would leave leaks behind.
But now with a predicate, scoped just right,
ProcessExit and DomainUnload slip past the light.
The bus still fires OWN014 when it captures state—
The exemption is narrow, as exemptions should be! 🌟

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: exempting AppDomain process-lifetime event subscriptions from OWN014 region-escape warnings, which aligns with the core modifications in all three files and resolves false positives in Npgsql analysis.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fp-appdomain-shutdown-events

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6eb172d7c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (!isTimer && (IsSelfOwnedSource(a.Left, ev, model, selfOwned)
|| IsStaticHandler(a.Right, model)))
|| IsStaticHandler(a.Right, model)
|| IsProcessLifetimeAppDomainEvent(ev)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve AppDomain instance-capture diagnostics

This exemption is applied before SubscriptionSourceKind can classify AppDomain.CurrentDomain as a process-lived/static source, so every handler on these AppDomain events is now dropped. That hides real region escapes such as a short-lived object doing AppDomain.CurrentDomain.ProcessExit += OnExit or a lambda that captures instance state; those delegates are retained until process/AppDomain shutdown and should still be OWN014 unless the handler/subscriber is proven non-capturing or process-lived. The mined Npgsql case is a lambda to static cleanup, but this check ignores the RHS and suppresses genuine instance captures too.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
frontend/roslyn/samples/AppDomainShutdownSample.cs (1)

16-19: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add FirstChanceException to the AppDomain exemption sample.

Line 16–19 currently exercise 3/4 exempted AppDomain events. Add FirstChanceException here so the sample fully covers the predicate surface and prevents silent drift.

Proposed patch
     public ShutdownCleanup()
     {
         AppDomain.CurrentDomain.ProcessExit += (_, _) => Cleanup();        // shutdown hook -> SILENT
         AppDomain.CurrentDomain.DomainUnload += (_, _) => Cleanup();       // shutdown hook -> SILENT
         AppDomain.CurrentDomain.UnhandledException += (_, _) => Cleanup(); // process diagnostics -> SILENT
+        AppDomain.CurrentDomain.FirstChanceException += (_, _) => Cleanup(); // diagnostics hook -> SILENT
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/roslyn/samples/AppDomainShutdownSample.cs` around lines 16 - 19, The
AppDomain exemption sample currently covers only three of four exempted
AppDomain events. Add a handler for the FirstChanceException event to complete
the sample coverage. After the UnhandledException event handler registration
(which subscribes to AppDomain.CurrentDomain.UnhandledException), add a new line
that registers a handler for AppDomain.CurrentDomain.FirstChanceException with
the same pattern: an event subscription with a lambda that calls Cleanup() and
includes the comment marker "// shutdown hook -> SILENT".
.github/workflows/ci.yml (1)

305-313: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Tighten the AppDomain regression assertion to be diagnostic-specific.

Line 309–313 should assert the absence of OWN014 for AppDomainShutdownSample.cs (not only absence of ShutdownCleanup text), and include the FirstChanceException silent path once added. That makes the guard less formatting-sensitive and fully aligned with the 4-event exemption.

Proposed patch
-          if echo "$out" | grep -q "ShutdownCleanup"; then
-            echo "FAIL: an AppDomain process-lifetime event subscription was wrongly reported as a region escape"; exit 1
-          fi
+          if echo "$out" | grep -qE "AppDomainShutdownSample\.cs:[0-9]+:.*\[OWN014\]"; then
+            echo "FAIL: an AppDomain process-lifetime event subscription was wrongly reported as OWN014"; exit 1
+          fi
           echo "$out" | grep -qE "NonAppDomainSubscriber.*OWN014|OWN014.*NonAppDomainSubscriber" \
             || { echo "FAIL: a lambda on a non-AppDomain static event must still raise OWN014 (exemption stays scoped)"; exit 1; }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 305 - 313, The current assertions in
lines 309-313 check for generic text patterns ("ShutdownCleanup" and
"NonAppDomainSubscriber") which are formatting-sensitive and not
diagnostic-specific enough. Replace these assertions to explicitly check for the
absence of the OWN014 diagnostic code for AppDomainShutdownSample.cs
specifically, and add an assertion for the FirstChanceException silent path when
that path is implemented. This will make the guard less formatting-sensitive and
properly validate that all 4 AppDomain event exemptions (ProcessExit,
DomainUnload, UnhandledException, and FirstChanceException) are correctly
silenced in the diagnostic output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 305-313: The current assertions in lines 309-313 check for generic
text patterns ("ShutdownCleanup" and "NonAppDomainSubscriber") which are
formatting-sensitive and not diagnostic-specific enough. Replace these
assertions to explicitly check for the absence of the OWN014 diagnostic code for
AppDomainShutdownSample.cs specifically, and add an assertion for the
FirstChanceException silent path when that path is implemented. This will make
the guard less formatting-sensitive and properly validate that all 4 AppDomain
event exemptions (ProcessExit, DomainUnload, UnhandledException, and
FirstChanceException) are correctly silenced in the diagnostic output.

In `@frontend/roslyn/samples/AppDomainShutdownSample.cs`:
- Around line 16-19: The AppDomain exemption sample currently covers only three
of four exempted AppDomain events. Add a handler for the FirstChanceException
event to complete the sample coverage. After the UnhandledException event
handler registration (which subscribes to
AppDomain.CurrentDomain.UnhandledException), add a new line that registers a
handler for AppDomain.CurrentDomain.FirstChanceException with the same pattern:
an event subscription with a lambda that calls Cleanup() and includes the
comment marker "// shutdown hook -> SILENT".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c96f460f-6faa-43a0-a6a1-c5702ac5ff55

📥 Commits

Reviewing files that changed from the base of the PR and between 78c234b and 6eb172d.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/AppDomainShutdownSample.cs

claude added 2 commits June 22, 2026 12:57
…m OWN014 (mined Npgsql)

Mining npgsql/npgsql surfaced this: PoolManager's static ctor subscribes
`AppDomain.CurrentDomain.{DomainUnload,ProcessExit} += (_,_) => ClearAll()` — a deliberate
"close idle connectors on appdomain unload (web-app redeployment)" hook (#491) — and the
detector reported it as an OWN014 region escape. But subscribing to a process-host AppDomain
event is never a leak: ProcessExit/DomainUnload run at shutdown, UnhandledException /
FirstChanceException on every unhandled throw, and the AppDomain IS the process host — the
handler is MEANT to live for the whole process. Promoting the subscriber to "the AppDomain's
lifetime" is the intent, not a leak.

IsProcessLifetimeAppDomainEvent exempts these four System.AppDomain events from the OWN014
region escape (alongside the self-owned-source and static-handler exemptions). Scoped to the
event's resolved declaring type, so a project's own `AppDomain`-named type is not matched, and a
non-AppDomain process-lived static event still escapes.

Regression sample AppDomainShutdownSample.cs: ShutdownCleanup (ProcessExit/DomainUnload/
UnhandledException lambdas) stays silent; NonAppDomainSubscriber (a lambda on a non-AppDomain
static event) still raises OWN014.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
…handlers (Codex P1)

Codex: the first cut keyed only off the EVENT (the AppDomain source), ignoring the handler —
so it also dropped a real region escape, e.g. `AppDomain.CurrentDomain.ProcessExit += (_,_) =>
_field++` or an instance-method handler, whose delegate target is the subscriber and is pinned
to the process until shutdown. The Npgsql case is safe only because its lambda is non-capturing
(a static `ClearAll()` call).

HandlerRetainsNoInstance now gates the exemption: a static method group (null target) or a
lambda that captures neither `this` (explicit, or implicit via an instance member) nor an
enclosing local/parameter. A capturing handler stays OWN014.

Sample: ShutdownCleanup gains the 4th event (FirstChanceException, CodeRabbit) and a new
CapturingShutdownSubscriber (`(_,_) => _count++`) that must STILL warn. CI: assert no OWN014
anywhere in the sample file (format-insensitive, CodeRabbit) for the exempt cases, and OWN014
for both the non-AppDomain and the capturing controls.

(Rebased onto current main — #86/#87 landed; resolved the ci.yml sample-list conflict.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@PhysShell PhysShell force-pushed the claude/fp-appdomain-shutdown-events branch from 6eb172d to 685fbf6 Compare June 22, 2026 12:59

public CapturingShutdownSubscriber()
{
AppDomain.CurrentDomain.ProcessExit += (_, _) => _count++; // captures `this` -> OWN014
…uring control shares the file)

The previous assertion failed CI: it asserted NO OWN014 anywhere in AppDomainShutdownSample.cs,
but CapturingShutdownSubscriber — added in the same file to prove the Codex non-capturing scope —
correctly DOES raise OWN014. Scope the silent check to ShutdownCleanup (the exempt class); the
capturing and non-AppDomain controls keep their own must-warn OWN014 assertions. Still
OWN014-specific (CodeRabbit), just per-class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@PhysShell PhysShell merged commit 19d35d9 into main Jun 23, 2026
22 checks passed
PhysShell pushed a commit that referenced this pull request Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants