Skip to content

feat(extractor): emit fresh-returning factory facts — D5.2 interprocedural leaks on real C##126

Merged
PhysShell merged 3 commits into
mainfrom
claude/agenda-2rufsj
Jun 26, 2026
Merged

feat(extractor): emit fresh-returning factory facts — D5.2 interprocedural leaks on real C##126
PhysShell merged 3 commits into
mainfrom
claude/agenda-2rufsj

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 26, 2026

Copy link
Copy Markdown
Owner

The bridge from machinery to real findings

D5.1b / D5.2 / the branch-scope fix built and unit-tested the interprocedural ownership core, but it was driven only by synthetic OwnIR facts — the Roslyn extractor didn't emit the shapes that trigger it, so it found nothing new on real C#. This PR closes that gap for the fresh-returning factory case (D5.2 / T1).

What the extractor now emits (--flow-locals)

  1. Factory side — a new'd IDisposable returned bare outside a try stays tracked and lowers to acquire …; return <var>. The core then classifies the method returnsOwned: fresh (the return <var> discharges it, so the factory itself stays silent). A return inside a try (finally edges not yet modelled), or a using/pool/owning-factory local, keeps its existing behaviour.
  2. Caller sidevar r = FirstPartyFactory() (a call to a source-visible method returning an owned IDisposable) emits a new call callee=… result=r op. The core mints the acquire only when it proves the callee fresh, so a non-fresh first-party call is never falsely owned.

IsFirstPartyDisposableFactory gates narrowly: source-declared (we can see the body to infer), non-void, return implements IDisposable, not dispose-optional. Overloads (non-unique names) resolve to unknown in the core → silent. Computed at the emission site, so no param-threading through the flow recursion.

Validation

  • Bridge side: already proven by the D5.2 unit tests; I additionally simulated the exact OwnIR this extractor emits and confirmed the core yields OWN001 @ factoryLeak with the factory + disposed caller silent.
  • C# side (no local dotnet — CI is the validator): new frontend/roslyn/samples/FactoryLeakSample.cs + a CI assertion in the --flow-locals step:
    • dropped factory result → OWN001 at the call site (interprocedural — beyond the flat, intra-procedural detectors)
    • disposed caller (factoryOk) and the factory's own local (made) → silent

This is the first slice where the accumulated D5 machinery surfaces a genuinely new ownership finding on real code. Next: run the oracle on a real external repo to see it in the wild.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added interprocedural detection for ownership-leaks involving factory-created IDisposable “fresh” return values, including validation of silent/disposed flows.
  • Bug Fixes
    • Improved call lowering robustness: unresolved/unneeded calls no longer cause failures or misleading findings, including correct handling when call results overwrite existing owned locals.
  • Documentation
    • Expanded the D5.2 “return-value door” note with fresh-return factory ownership-transfer and gating rules.
  • Tests
    • Added a new Roslyn factory leak sample and extended CI/bridge regressions to cover unknown-cal l targets and overwrite scenarios.

… interprocedural leaks)

Wire the Roslyn extractor (--flow-locals) to produce the OwnIR the D5.2
fresh-return machinery consumes, so first-party factory leaks are caught on real
C# (not just synthetic unit tests):

- A new'd IDisposable returned BARE outside a try stays tracked and lowers to
  acquire + return <var>, so the core classifies the method returnsOwned: fresh
  (the return discharges it -> the factory itself stays silent). A return inside a
  try, or a using/pool/owning-factory local, keeps its existing behaviour.
- var r = FirstPartyFactory() (a source-visible method returning an owned
  IDisposable) emits a new call callee=... result=r op. The core mints the acquire
  only when it proves the callee fresh, so a non-fresh first-party call is never
  falsely owned. IsFirstPartyDisposableFactory gates on source-declared, non-void,
  disposable, non-dispose-optional return; overloads resolve to unknown (silent).

Adds FactoryLeakSample.cs + a CI assertion: a dropped factory result leaks
interprocedurally (OWN001 at the call site), the disposed caller and the factory
stay silent. Bridge side already proven by the D5.2 unit tests; this closes the
extractor gap so the capability fires on real code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF
@coderabbitai

coderabbitai Bot commented Jun 26, 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: 7b3ddaf2-0ca3-4f26-8199-63a0409e5b15

📥 Commits

Reviewing files that changed from the base of the PR and between 099e73c and fb3d5be.

📒 Files selected for processing (3)
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • ownlang/ownir.py
  • tests/test_ownir.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_ownir.py
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • ownlang/ownir.py

📝 Walkthrough

Walkthrough

The PR adds first-party disposable-factory handling to the Roslyn extractor’s flow-locals pipeline, updates OwnIR call lowering and diagnostics, and adds a sample, CI verification, tests, and documentation for the ownership-transfer behavior.

Changes

First-party factory ownership flow

Layer / File(s) Summary
Factory detection and candidates
frontend/roslyn/OwnSharp.Extractor/Program.cs
Adds first-party disposable-factory detection, flow-function naming, and matching locals in --flow-locals candidate discovery.
Lowering and fresh return handling
frontend/roslyn/OwnSharp.Extractor/Program.cs
Lowers first-party factory initializers as call ops with result, emits return var for tracked bare returns, and keeps bare fresh returns tracked outside try bodies.
OwnIR call lowering and diagnostics
ownlang/ownir.py
Recognizes sink extern calls, drops unresolvable calls, clears overwritten result bindings before fresh-result acquisition, and skips OWN040 mapping failures.
Sample, CI, tests, and notes
frontend/roslyn/samples/FactoryLeakSample.cs, .github/workflows/ci.yml, docs/notes/d5-ownership-transfer.md, tests/test_ownir.py
Adds the factory-leak sample, extends CI coverage and assertions for leak and silence cases, records the emitted ownership-transfer facts in the D5.2 note, and adds regressions for unknown callees and overwrite behavior.

Sequence Diagram(s)

sequenceDiagram
  participant FactoryLeakSample.cs
  participant OwnSharp.Extractor
  participant ownlang/ownir.py
  participant .github/workflows/ci.yml
  FactoryLeakSample.cs->>OwnSharp.Extractor: run `--flow-locals`
  OwnSharp.Extractor->>ownlang/ownir.py: emit `call` and `return` facts
  .github/workflows/ci.yml->>ownlang/ownir.py: assert `OWN001` on `factoryLeak`
  .github/workflows/ci.yml->>ownlang/ownir.py: assert silence for `factoryOk` and `made`
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PhysShell/Own.NET#116: Matches the same fresh-return and caller-side ownership inference path for factory-returned values.
  • PhysShell/Own.NET#54: Also extends --flow-locals factory-result tracking and acquisition handling in Program.cs.
  • PhysShell/Own.NET#113: Also changes ownlang/ownir.py call lowering around ownership-sink extern handling.

Poem

I hopped by the stream with a snip and a snap,
Fresh little returns went back on the map.
One leak got a nudge,
The quiet ones didn’t budge—
Then I munched my carrot and took a nap.

🚥 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 accurately summarizes the main change: extractor support for fresh-returning factory facts and interprocedural leak validation on real C#.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/agenda-2rufsj

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

@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: 5291fa9f8c

ℹ️ 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".

return false; // void, or not first-party (no visible body to infer `fresh` from)
if (!ImplementsIDisposable(m.ReturnType) || IsDisposeOptional(m.ReturnType))
return false;
callee = $"{m.ContainingType.Name}.{m.Name}";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use fully qualified factory summary keys

Because this call key drops the namespace and containing-type chain, it can alias a different source method. In a repo with A.StreamFactory.Make() returning a cached Stream (no local candidates, so it emits no functions[] entry) and B.StreamFactory.Make() returning a new stream, a call to A.StreamFactory.Make() is lowered as StreamFactory.Make; the solver can then use B's fresh summary and report a leak on a non-owned singleton result. Use the same fully qualified symbol key for both summaries and calls, or require an exact emitted function, to avoid false OWN001s.

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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 825-831: Tighten the D5.2 CI assertion so it verifies the OWN001
finding is anchored to the sample call site, not just any match containing
'factoryLeak'. Update the grep/check in the ci workflow to require the
diagnostic text for FactoryLeakSample.cs and the caller location emitted by the
interprocedural analysis, using the existing D5.2 sample/OWN001 assertion block
as the reference point.

In `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 786-789: The emitted call op for tracked first-party disposable
factories is dropping caller arguments by using Array.Empty<string>(), which
loses ownership/borrow tracking for values like stream or owner. Update the
tracked branch in Program.cs where the anonymous call node is added so it
preserves the initializer call’s arguments instead of clearing them, using the
same argument extraction path already used for normal first-party calls and the
fpCallee returned by IsFirstPartyDisposableFactory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 809f28bf-93c9-4ee9-acb9-e1f3e719debb

📥 Commits

Reviewing files that changed from the base of the PR and between 1b82c24 and 5291fa9.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • docs/notes/d5-ownership-transfer.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/FactoryLeakSample.cs

Comment thread .github/workflows/ci.yml Outdated
Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs
…ll args (CI fix + Codex/CodeRabbit)

The extractor's new call ops surfaced calls to callees not in functions[] (BCL /
extension methods like GetRequiredService), which crashed check_facts with
OWN040. Plus three review items:

- (CI crash) Gate the Call emission on a RESOLVABLE callee (in the MOS or a sink
  extern); an unknown callee is dropped (no claim), never lowered to a Call that
  raises OWN040. Add OWN040 to the bridge-artifact skip list belt-and-suspenders.
  Regression test: a call to an unknown callee makes no claim and does not crash.
- (Codex P2) Fully-qualify the factory summary key on BOTH sides
  ({Namespace.Type}.{Method} via FlowFunctionName + ContainingType.ToDisplayString)
  so two same-named factories across namespaces never alias into a false OWN001.
- (CodeRabbit Major) Preserve the call's tracked identifier args instead of
  Array.Empty, so the core can apply the callee's per-argument effects.
- (CodeRabbit Minor) Anchor the CI assertion to FactoryLeakSample.cs:<line> at the
  call site, not just any match of 'factoryLeak'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF
// Drops the fresh factory result without disposing -> OWN001 at the call site.
public static void Leaks()
{
var factoryLeak = StreamFactory.Make();

@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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 807-813: The argument collection in OwnSharp.Extractor’s
initializer handling is still using the raw invocation argument order and
ignores named argument mapping, which can misalign positional ownership effects.
Update the logic around the `fpInv.ArgumentList.Arguments` processing to
normalize `NameColon`/named arguments before building `fpArgs`, preserving the
actual parameter order expected by the bridge. Use the `fpInv` and `fpArgs`
extraction path in `Program.cs` to locate the fix.

In `@ownlang/ownir.py`:
- Around line 1538-1545: The `Call` handling in `ownlang/ownir.py` needs to
invalidate any existing ownership binding for `result` before deciding whether a
fresh acquire can be minted. In the `lower_call` path where `summ` may be
unknown and the `Call` can be dropped, make sure `localmap[result]` is cleared
or killed first so overwriting a tracked local does not leave the old handle
alive. Update the logic around `result`, `summ`, `localmap`, and the `Call`
emission check so that a reused local cannot retain its previous ownership state
when no new summary-driven acquire is created.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 999ed7bc-f58c-4fbc-829b-0843ff9a2984

📥 Commits

Reviewing files that changed from the base of the PR and between 5291fa9 and 099e73c.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • ownlang/ownir.py
  • tests/test_ownir.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci.yml

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs
Comment thread ownlang/ownir.py
… call args (CodeRabbit)

- (Major, bridge) When a `call` op's result reuses a tracked local, drop the
  stale localmap binding before any optional fresh acquire. Otherwise
  `acquire x; x = Unknown(); release x` read as clean while the original x
  actually leaked (reference lost). A hoisted local keeps its single outer-scope
  handle. Regression test: the overwritten original now leaks OWN001.
- (Minor, extractor) Emit only POSITIONAL identifier args for the factory call op;
  a named arg (`Wrap(second: s2, first: s1)`) would mis-attribute under the
  bridge's positional effect application, so named args are dropped (under-claim,
  never mis-align).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF
@PhysShell
PhysShell merged commit 1e8bb8b into main Jun 26, 2026
26 checks passed
PhysShell pushed a commit that referenced this pull request Jun 26, 2026
…l detection on real code

Re-point the oracle push-trigger to this dev branch and set the target to Polly's
product source (src/). With the D5.2 extractor wiring merged (#126), own-check
(--flow-locals) now emits fresh-returning-factory facts, so this run probes
whether the new interprocedural capability surfaces anything new on real Polly
code, and how it buckets vs Infer# / CodeQL. Dev-branch only; revert before any
merge to main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF
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