Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,8 @@ jobs:
dotnet run --project frontend/roslyn/OwnSharp.Extractor -- \
frontend/roslyn/samples/FlowLocalsSample.cs \
frontend/roslyn/samples/MemoryOwnerEscapeSample.cs \
frontend/roslyn/samples/FactoryLeakSample.cs --flow-locals -o "$RUNNER_TEMP/flow.json"
frontend/roslyn/samples/FactoryLeakSample.cs \
frontend/roslyn/samples/OverloadSigSample.cs --flow-locals -o "$RUNNER_TEMP/flow.json"
out=$(python -m ownlang ownir "$RUNNER_TEMP/flow.json" || true)
echo "$out"
echo "$out" | grep -q "OWN002" || { echo "FAIL: expected OWN002 (use-after-dispose)"; exit 1; }
Expand Down Expand Up @@ -920,6 +921,39 @@ jobs:
for ok in factoryOk made; do
if echo "$out" | grep -q "'$ok'"; then echo "FAIL: D5.2 silent case '$ok' was reported"; exit 1; fi
done
# Interprocedural stage 2 (spec/OwnIR.md §5.1): per-overload signature keys.
# SigOverloads.Open is overloaded — the (string) overload is a fresh factory,
# the (FileStream,bool) one returns its parameter — so the name-merged returns
# DISAGREE (no fresh claim). The `sig` stamped on both the functions[] records
# and the call op resolves the fresh overload's OWN summary, so the dropped
# stream in Drop surfaces as OWN001 at the call — the recall the merge lost.
# Structural check (CodeRabbit): pin BOTH sides of the edge exactly — the two
# overload records' sigs AND the Drop call op's callee/sig/result — so a
# missing or unmatched CALL-side sig fails here by itself, not only via the
# downstream finding.
python3 - "$RUNNER_TEMP/flow.json" <<'PY'
import json, sys
facts = json.load(open(sys.argv[1]))
fns = facts.get("functions", [])
sigs = sorted(str(f.get("sig")) for f in fns
if f.get("name") == "SigOverloads.Open")
assert sigs == ["System.IO.FileStream,System.Boolean", "System.String"], \
f"overload record sigs wrong: {sigs}"
calls = [op for f in fns if f.get("name") == "SigOverloads.Drop"
for op in f.get("body", []) if op.get("op") == "call"]
assert any(c.get("callee") == "SigOverloads.Open"
and c.get("sig") == "System.String"
and c.get("result") == "dropped" for c in calls), \
f"Drop call op missing the (string) overload sig: {calls}"
print("OK: stage-2 sig stamped on both sides of the Drop edge")
PY
echo "$out" | grep -qE "OverloadSigSample\.cs:[0-9]+:.*\[OWN001\].*'dropped' is never disposed" \
|| { echo "FAIL: expected the sig-resolved fresh-overload leak (OWN001 on 'dropped')"; exit 1; }
# the factory itself (transfers out via return), the non-fresh overload's
# balanced probe, and the fresh local it returns all stay silent.
for ok in opened probe; do
if echo "$out" | grep -q "'$ok'"; then echo "FAIL: stage-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: |
Expand Down
17 changes: 17 additions & 0 deletions docs/notes/interprocedural-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,23 @@ consume-сводку всех остальных, а overload-неоднозна
одной арности с разными типами при варианте `arity` остаются слитыми — потому
предпочтителен сразу `signature`, чтобы не делать двух миграций.

> **Статус: сделано (вариант `signature` сразу, минуя `arity`).** Формат и
> fallback-правила — `spec/OwnIR.md` §5.1 + `spec/ownir.schema.json`. Мост:
> `_build_skeletons` эмитит per-overload скелет `name(sig)` РЯДОМ со слитым
> по имени (fallback для рёбер без `sig`); `_mos_lookup` — единая резолюция
> (sig-ключ → точное имя → каноническое) для канала, kill-sites, OWN051,
> fresh-минта и параметров самого метода. Extractor штампует `sig` из одного
> и того же `IMethodSymbol` на `functions[]`-записи и `call`-опе (generic-
> аргументы стёрты до backtick-арности — коллизия просто сливает группу,
> никогда не «мимо»). Паритетный дамп несёт оба словаря ключей; e2e-якорь —
> `OverloadSigSample.cs` (fresh-перегрузка возвращает OWN001, который merge
> терял). Тест-матрица §6.1 ТЗ — в `tests/test_ownir.py` (stage 2 блок).
> Пересмотр Tier B-исключений: `Process.Start` / `new StreamReader(stream)`
> остаются вне таблицы — их неоднозначность лежит на BCL-стороне (static-vs-
> instance, adopt-vs-factory), где `functions[]`-записей нет и per-overload
> summary не возникает; sig-ключёванная Tier B-таблица теперь ВОЗМОЖНА
> (у `call`-опа есть `sig`), но это отдельный аддитивный срез.

## 6. Этап 3 — дожать T1/T4: `aliasOf:i` через return, out/ref (2–3 недели)

Самый большой прикладной выигрыш из оставшихся — «step 2 remainder» D5.4:
Expand Down
12 changes: 12 additions & 0 deletions docs/notes/interprocedural-tz.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,18 @@ Python-стороне — тот же ход, что уже сделан для
перегрузок; `_merge_skeletons` вызывается только для реально одноключевых
групп; паритетный дамп сводок расширен ключом.

> **Статус: сделано.** Спека — `spec/OwnIR.md` §5.1 (+ schema). Ключ и
> fallback — как выше; одно уточнение против буквы DoD: слитый-по-имени
> скелет НЕ исчез — он эмитится рядом с per-overload ключами как цель для
> рёбер без `sig` (иначе fallback-правилу не во что резолвиться), а
> `_merge_skeletons` для sig-групп вызывается только на реально одноключевых
> (обычно одиночных) группах. Тест-матрица этого раздела — полностью в
> `tests/test_ownir.py` (включая global::-qualified `sig`, смешанных
> продюсеров и fresh-регресс fallback'а). Tier B: пересмотрено, записано в
> roadmap §5 — существующие исключения остаются (неоднозначность BCL-стороны,
> не перегрузок первопартийного множества), sig-ключёванная таблица —
> возможный аддитивный follow-up.

### 6.2 Этап 3 — `aliasOf:i` через return (первый срез)

- **Деривация:** `ReturnSkeleton("aliasOf", i)` ⇔ ровно один возвращаемый var
Expand Down
75 changes: 64 additions & 11 deletions frontend/roslyn/OwnSharp.Extractor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1162,9 +1162,11 @@
// `callee` matches the `functions[]` key `{TypeName}.{MethodName}`. A null/extern symbol,
// a void/non-disposable return, or a dispose-optional return is rejected (no claim); an
// overload (non-unique name) resolves to `unknown` in the core and is silently safe.
static bool IsFirstPartyDisposableFactory(ExpressionSyntax? expr, SemanticModel model, out string callee)
static bool IsFirstPartyDisposableFactory(ExpressionSyntax? expr, SemanticModel model,
out string callee, out string sig)
{
callee = "";
sig = "";
if (expr is not InvocationExpressionSyntax inv)
return false;
if (model.GetSymbolInfo(inv).Symbol is not IMethodSymbol m)
Expand All @@ -1176,8 +1178,10 @@
// Fully-qualified key (namespace + containing-type chain) so the call resolves to the
// RIGHT summary: two `StreamFactory.Make` in different namespaces must not alias, or a
// call to a non-fresh one could pick up a fresh one's summary and fabricate OWN001
// (Codex). Must match the `functions[]` name built by `FlowFunctionName`.
// (Codex). Must match the `functions[]` name built by `FlowFunctionName`; the stage-2
// `sig` narrows the same call to the resolved OVERLOAD's summary (spec §5.1).
callee = $"{m.ContainingType.ToDisplayString()}.{m.Name}";
sig = CanonicalSig(m);
Comment thread
PhysShell marked this conversation as resolved.
return true;
}

Expand All @@ -1191,6 +1195,40 @@
? $"{ms.ContainingType.ToDisplayString()}.{ms.Name}"
: $"{fallbackType}.{MethodName(method)}";

// Interprocedural stage 2 (spec/OwnIR.md §5.1): the canonical per-overload signature key —
// the method's parameter TYPES, fully-qualified, comma-separated, no spaces, generic arity
// via backtick (type arguments erased, so two same-arity `List<T>` overloads share a sig —
// a collision merely merges those overloads' summaries, never mis-resolves), `global::`
// stripped, "" for zero parameters. Both sides of an edge (the `functions[]` record and
// the `call` op) derive it from the SAME resolved IMethodSymbol, so they agree by
// construction; the bridge falls back to the name-merged summary whenever either side
// lacks or mismatches it.
// Canonicalize through the DECLARED definition (Codex P2 on #217): a call site
// resolves the REDUCED form of an extension method (no `this` receiver in
// Parameters) and/or a CONSTRUCTED generic (type args substituted), while the
// `functions[]` record is stamped from the declaration — `ReducedFrom` restores
// the receiver parameter, `OriginalDefinition` restores the open type params, so
// both sides of the edge always produce the same string.
static string CanonicalSig(IMethodSymbol m) =>
string.Join(",", (m.ReducedFrom ?? m).OriginalDefinition
.Parameters.Select(p => CanonicalTypeName(p.Type)));

static string CanonicalTypeName(ITypeSymbol t) => t switch
{
IArrayTypeSymbol a => CanonicalTypeName(a.ElementType)
+ "[" + new string(',', a.Rank - 1) + "]",
IPointerTypeSymbol p => CanonicalTypeName(p.PointedAtType) + "*",
ITypeParameterSymbol tp => tp.Name, // erased: `Wrap<T>(T x)` keys as "T"
// MetadataName carries the backtick arity (List`1) and drops nullable
// annotations (`string?` == `string` — C# overloads cannot differ there).
INamedTypeSymbol n when n.ContainingType is { } outer =>
CanonicalTypeName(outer) + "+" + n.MetadataName,
INamedTypeSymbol n => n.ContainingNamespace is { IsGlobalNamespace: false } ns
? $"{ns.ToDisplayString()}.{n.MetadataName}"
: n.MetadataName,
_ => t.ToDisplayString().Replace("global::", ""), // dynamic / error types
};

// P-005 D5.4 (T4 wrap/adopt): the set of OWNING fields a first-party type disposes
// UNCONDITIONALLY in its `Dispose()` — i.e. a `_f.Dispose()` / `_f?.Dispose()` /
// `this._f.Dispose()` that is a TOP-LEVEL statement of the Dispose body (not nested in an
Expand Down Expand Up @@ -1592,7 +1630,8 @@
// acquire); the core mints the acquire only if it proves the callee returns
// `fresh`, so a non-fresh first-party call is never falsely owned.
else if (tracked.Contains(v.Identifier.Text)
&& IsFirstPartyDisposableFactory(v.Initializer?.Value, model, out var fpCallee))
&& IsFirstPartyDisposableFactory(v.Initializer?.Value, model,
out var fpCallee, out var fpSig))
{
// Preserve the call's TRACKED identifier args (CodeRabbit) so the core
// can apply the callee's per-argument ownership effects (consume/borrow)
Expand All @@ -1611,7 +1650,8 @@
.Where(tracked.Contains)
.ToArray()
: Array.Empty<string>();
nodes.Add(new { op = "call", callee = fpCallee, args = fpArgs,
nodes.Add(new { op = "call", callee = fpCallee, sig = fpSig,
args = fpArgs,
result = v.Identifier.Text, line = LineOf(v) });
}
// POOL005: a full-length view in the initializer — `var copy = buf.AsSpan().ToArray();`
Expand Down Expand Up @@ -3509,7 +3549,7 @@
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)
.Where(p => p.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
.ToList();
var refNames = new HashSet<string>(tpa.Select(Path.GetFileName), StringComparer.OrdinalIgnoreCase);

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / P-014 Tier B — external reference resolution (--ref-dir)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / ownsharp CLI (gate A) — clean install -> check -> findings (ubuntu-latest)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check repo scan (github + msbuild) + composite action

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / ownsharp CLI (gate A) — clean install -> check -> findings (ubuntu-latest)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check SARIF -> GitHub code scanning (dog-food)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / P-014 Tier B — external reference resolution (--ref-dir)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check SARIF -> GitHub code scanning (dog-food)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / C# leak extractor (Roslyn) -> OwnIR -> core

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / C# leak extractor (Roslyn) -> OwnIR -> core

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / own-check repo scan (github + msbuild) + composite action

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / ownsharp CLI (gate A) — clean install -> check -> findings (windows-latest)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.

Check warning on line 3552 in frontend/roslyn/OwnSharp.Extractor/Program.cs

View workflow job for this annotation

GitHub Actions / ownsharp CLI (gate A) — clean install -> check -> findings (windows-latest)

Argument of type 'IEnumerable<string?>' cannot be used for parameter 'collection' of type 'IEnumerable<string>' in 'HashSet<string>.HashSet(IEnumerable<string> collection, IEqualityComparer<string>? comparer)' due to differences in the nullability of reference types.
var references = tpa.Select(p => (MetadataReference)MetadataReference.CreateFromFile(p)).ToList();
// P-004 WPF profile: widen the reference set with assemblies named by the
// OWN_EXTRA_REF_DIRS env var (colon-separated dirs) — e.g. the WindowsDesktop ref
Expand Down Expand Up @@ -4428,7 +4468,8 @@
candidates.Add(v.Identifier.Text);
else if (IsMemoryPoolRent(v.Initializer?.Value, model)) // MemoryPool<T> IMemoryOwner (Dispose-released, NOT a poolBuffer)
candidates.Add(v.Identifier.Text);
else if (IsFirstPartyDisposableFactory(v.Initializer?.Value, model, out _))
else if (IsFirstPartyDisposableFactory(v.Initializer?.Value, model,
out _, out _))
// P-005 D5.2: `var r = FirstPartyFactory()` — a candidate acquire
// IFF the core proves the callee returns `fresh` (it emits a `call`
// op, not an `acquire`; the core decides). Checked last so `new` /
Expand Down Expand Up @@ -4568,12 +4609,24 @@
continue;
}
statMethodsAnalysed++;
flowFunctions.Add(new
{
name = FlowFunctionName(method, cls.Identifier.Text, model),
file,
body = fbody,
});
// Stage 2 (spec §5.1): stamp the record's per-overload `sig` when the
// symbol resolves, so an overloaded method gets its own summary beside
// the name-merge; an unresolved symbol omits the field (the bridge then
// keeps the merged-fallback behaviour — degraded, never mis-keyed).
flowFunctions.Add(model.GetDeclaredSymbol(method) is IMethodSymbol msym
? new
{
name = $"{msym.ContainingType.ToDisplayString()}.{msym.Name}",
file,
sig = CanonicalSig(msym),
body = fbody,
}
: (object)new
{
name = FlowFunctionName(method, cls.Identifier.Text, model),
file,
body = fbody,
});
}

if (subs.Count > 0)
Expand Down
35 changes: 35 additions & 0 deletions frontend/roslyn/samples/OverloadSigSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.IO;

// Interprocedural stage 2 (spec/OwnIR.md §5.1) — per-overload signature keys.
//
// `Open` is OVERLOADED: the `(string)` overload is a fresh factory (constructs
// and returns a new FileStream), the `(FileStream, bool)` overload returns its
// own parameter (not fresh). Pre-stage-2 both merged into one name-keyed summary
// whose returns DISAGREE -> no fresh claim -> the dropped result in `Drop` leaked
// invisibly. With `sig` stamped on the `functions[]` records and the `call` op,
// the call resolves the `(string)` overload's own summary (`fresh`), so the
// dropped stream surfaces as OWN001 at the call site.

public static class SigOverloads
{
public static FileStream Open(string path)
{
var opened = new FileStream(path, FileMode.Open); // fresh factory
return opened;
}

public static FileStream Open(FileStream existing, bool flush)
{
var probe = new MemoryStream(); // a tracked local, so this overload emits a record
probe.Dispose();
if (flush)
existing.Flush();
return existing; // returns a PARAMETER -> not fresh
}

public static void Drop(string path)
{
var dropped = Open(path); // sig'd call -> the fresh overload's contract

Check warning

Code scanning / Own.NET

owned resource not released on all paths (possible leak) Warning

IDisposable local 'dropped' is never disposed (leak) [resource: disposable]
dropped.Flush(); // used but never disposed: OWN001 (the leak stage 2 restores)
}
}
11 changes: 7 additions & 4 deletions ownlang/ownership.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,13 @@ class ReturnSkeleton:

@dataclass(frozen=True)
class MethodSkeleton:
key: str # method identity the call graph resolves on. NOT a full signature key: the
# extractor names a callee `{Type}.{Method}` with no parameter signature, so
# same-name overloads share a key and are merged conservatively before solving
# (`_merge_skeletons` in ownir.py — note's open question 2, partially addressed).
key: str # method identity the call graph resolves on — an opaque string to this
# solver. The bridge (ownir.py) keys a method `{Type}.{Method}` and, since
# interprocedural stage 2, ALSO emits one `{Type}.{Method}(sig)` skeleton per
# overload whose record carries a canonical parameter-type `sig`; a forward
# edge targets the per-overload key when its call op carries a matching `sig`,
# else the bare-name key (same-name overloads merged conservatively there —
# `_merge_skeletons`, the stage-2 fallback).
params: tuple[ParamSkeleton, ...] = ()
ret: ReturnSkeleton = field(default_factory=ReturnSkeleton)
file: str = "?"
Expand Down
Loading
Loading