Skip to content

Commit 951a6a2

Browse files
committed
feat: add OWN053 orphaned-awaitable advisory
1 parent 5323dd4 commit 951a6a2

37 files changed

Lines changed: 2780 additions & 74 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
ownership-semantics-lab H-29 -> OWN053 promotion: the fixture re-run on the PROMOTED build.
2+
3+
Build: frontend/roslyn/OwnSharp.Extractor at the promotion commit (net8.0, Release,
4+
--no-incremental), the research flag OWEN_H29 removed, the rule always on.
5+
Command (cwd = the Own.NET checkout, fixture = corpus/ownership-lab/h29/fx/Orphan.cs
6+
at its scratchpad path, references = the H-29 falsifier's Npgsql build output):
7+
dotnet ownsharp-extract.dll --flow-locals <fx>/Orphan.cs --ref-dir <falsifier>/bin/Release/net8.0 -o promo-u.facts.json
8+
python -m ownlang ownir promo-u.facts.json > promo-u.py.txt
9+
own-cli ownir promo-u.facts.json > promo-u.rust.txt
10+
11+
promo-u.facts.json the promoted build's facts
12+
promo-u.py.txt / promo-u.rust.txt both engines: identical (parity), 5 advisory OWN053
13+
at Orphan.cs 11 / 12 / 21 / 22 / 23 (O01, O02, O11,
14+
O12, O13 -- exactly the five frozen primary sites),
15+
0 findings, exit code 0; the eleven twins silent
16+
fixture-diff-prototype-vs-promoted.txt diff against rule/on-u.facts.json (the OWEN_H29=1
17+
prototype build): the ONLY difference is the `file`
18+
of the five entries, now written through the same
19+
cwd-relative helper every other record uses
20+
(prototype: the raw syntax-tree path); sites, lines,
21+
columns, callees, families, result types unchanged
22+
build.txt the build log's error / warning / elapsed lines
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/home/user/Own.NET/frontend/roslyn/OwnSharp.Extractor/Program.cs(7028,36): warning CS8620: 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. [/home/user/Own.NET/frontend/roslyn/OwnSharp.Extractor/OwnSharp.Extractor.csproj]
2+
/home/user/Own.NET/frontend/roslyn/OwnSharp.Extractor/Program.cs(3071,78): warning CS8602: Dereference of a possibly null reference. [/home/user/Own.NET/frontend/roslyn/OwnSharp.Extractor/OwnSharp.Extractor.csproj]
3+
/home/user/Own.NET/frontend/roslyn/OwnSharp.Extractor/Program.cs(7028,36): warning CS8620: 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. [/home/user/Own.NET/frontend/roslyn/OwnSharp.Extractor/OwnSharp.Extractor.csproj]
4+
/home/user/Own.NET/frontend/roslyn/OwnSharp.Extractor/Program.cs(3071,78): warning CS8602: Dereference of a possibly null reference. [/home/user/Own.NET/frontend/roslyn/OwnSharp.Extractor/OwnSharp.Extractor.csproj]
5+
2 Warning(s)
6+
0 Error(s)
7+
Time Elapsed 00:00:08.20
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
106c106
2+
< "file": "/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
3+
---
4+
> "file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
5+
116c116
6+
< "file": "/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
7+
---
8+
> "file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
9+
126c126
10+
< "file": "/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
11+
---
12+
> "file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
13+
136c136
14+
< "file": "/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
15+
---
16+
> "file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
17+
146c146
18+
< "file": "/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
19+
---
20+
> "file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
{
2+
"ownir_version": 0,
3+
"module": "Extracted",
4+
"components": [],
5+
"services": [],
6+
"functions": [
7+
{
8+
"name": "Orphan.O06_discard",
9+
"file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
10+
"sig": "Npgsql.NpgsqlConnection,System.Threading.CancellationToken",
11+
"params": [
12+
{
13+
"name": "conn",
14+
"line": 16,
15+
"ordinal": 0
16+
}
17+
],
18+
"body": [
19+
{
20+
"op": "use",
21+
"var": "conn",
22+
"line": 16
23+
}
24+
]
25+
},
26+
{
27+
"name": "Orphan.O07_statement",
28+
"file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
29+
"sig": "Npgsql.NpgsqlConnection,System.Threading.CancellationToken",
30+
"params": [
31+
{
32+
"name": "conn",
33+
"line": 17,
34+
"ordinal": 0
35+
}
36+
],
37+
"body": [
38+
{
39+
"op": "use",
40+
"var": "conn",
41+
"line": 17
42+
}
43+
]
44+
},
45+
{
46+
"name": "Orphan.O12_in_try",
47+
"file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
48+
"sig": "Npgsql.NpgsqlConnection,System.Threading.CancellationToken",
49+
"params": [
50+
{
51+
"name": "conn",
52+
"line": 22,
53+
"ordinal": 0
54+
}
55+
],
56+
"body": [
57+
{
58+
"op": "if",
59+
"line": 22,
60+
"then": [
61+
{
62+
"op": "use",
63+
"var": "conn",
64+
"line": 22
65+
},
66+
{
67+
"op": "return",
68+
"var": null,
69+
"line": 22
70+
}
71+
],
72+
"else": []
73+
},
74+
{
75+
"op": "if",
76+
"line": 22,
77+
"then": [
78+
{
79+
"op": "use",
80+
"var": "conn",
81+
"line": 22
82+
},
83+
{
84+
"op": "return",
85+
"var": null,
86+
"line": 22
87+
}
88+
],
89+
"else": []
90+
},
91+
{
92+
"op": "use",
93+
"var": "conn",
94+
"line": 22
95+
}
96+
]
97+
}
98+
],
99+
"stats": {
100+
"methods_with_local": 15,
101+
"methods_flow_analysed": 3,
102+
"methods_skipped_unmodelled": 12
103+
},
104+
"orphaned_awaitables": [
105+
{
106+
"file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
107+
"line": 11,
108+
"column": 87,
109+
"method": "Orphan.O01_B_orphan",
110+
"local": "tx",
111+
"callee": "Npgsql.NpgsqlConnection.BeginTransactionAsync/1",
112+
"family": "A_owned_result",
113+
"result_type": "Npgsql.NpgsqlTransaction"
114+
},
115+
{
116+
"file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
117+
"line": 12,
118+
"column": 61,
119+
"method": "Orphan.O02_A_orphan",
120+
"local": "r",
121+
"callee": "Npgsql.NpgsqlCommand.ExecuteReaderAsync/1",
122+
"family": "A_owned_result",
123+
"result_type": "Npgsql.NpgsqlDataReader"
124+
},
125+
{
126+
"file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
127+
"line": 21,
128+
"column": 95,
129+
"method": "Orphan.O11_B_configureawait",
130+
"local": "tx",
131+
"callee": "Npgsql.NpgsqlConnection.BeginTransactionAsync/1",
132+
"family": "A_owned_result",
133+
"result_type": "Npgsql.NpgsqlTransaction"
134+
},
135+
{
136+
"file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
137+
"line": 22,
138+
"column": 91,
139+
"method": "Orphan.O12_in_try",
140+
"local": "tx",
141+
"callee": "Npgsql.NpgsqlConnection.BeginTransactionAsync/1",
142+
"family": "A_owned_result",
143+
"result_type": "Npgsql.NpgsqlTransaction"
144+
},
145+
{
146+
"file": "../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs",
147+
"line": 23,
148+
"column": 62,
149+
"method": "Orphan.O13_B_sync_commit",
150+
"local": "c",
151+
"callee": "Npgsql.NpgsqlTransaction.CommitAsync/1",
152+
"family": "B_protocol_lifecycle",
153+
"result_type": null
154+
}
155+
]
156+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:11: warning: [OWN053] orphaned awaitable: 'tx' = Npgsql.NpgsqlConnection.BeginTransactionAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (its result Npgsql.NpgsqlTransaction is never released), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
2+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:12: warning: [OWN053] orphaned awaitable: 'r' = Npgsql.NpgsqlCommand.ExecuteReaderAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (its result Npgsql.NpgsqlDataReader is never released), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
3+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:21: warning: [OWN053] orphaned awaitable: 'tx' = Npgsql.NpgsqlConnection.BeginTransactionAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (its result Npgsql.NpgsqlTransaction is never released), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
4+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:22: warning: [OWN053] orphaned awaitable: 'tx' = Npgsql.NpgsqlConnection.BeginTransactionAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (its result Npgsql.NpgsqlTransaction is never released), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
5+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:23: warning: [OWN053] orphaned awaitable: 'c' = Npgsql.NpgsqlTransaction.CommitAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (there is no result to release), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
6+
7+
0 findings, 5 advisory (OWN053).
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:11: warning: [OWN053] orphaned awaitable: 'tx' = Npgsql.NpgsqlConnection.BeginTransactionAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (its result Npgsql.NpgsqlTransaction is never released), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
2+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:12: warning: [OWN053] orphaned awaitable: 'r' = Npgsql.NpgsqlCommand.ExecuteReaderAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (its result Npgsql.NpgsqlDataReader is never released), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
3+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:21: warning: [OWN053] orphaned awaitable: 'tx' = Npgsql.NpgsqlConnection.BeginTransactionAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (its result Npgsql.NpgsqlTransaction is never released), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
4+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:22: warning: [OWN053] orphaned awaitable: 'tx' = Npgsql.NpgsqlConnection.BeginTransactionAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (its result Npgsql.NpgsqlTransaction is never released), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
5+
../../../tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad/lab/h29/fx/Orphan.cs:23: warning: [OWN053] orphaned awaitable: 'c' = Npgsql.NpgsqlTransaction.CommitAsync/1(...) is obtained and lost -- never awaited, returned, stored or otherwise observed; the operation still runs (there is no result to release), its failure is lost, and a transaction / connection lifecycle call leaves the connection in a state nobody can finish. Await it and keep the result, return or store it where it is observed, or express fire-and-forget explicitly [resource: orphaned awaitable]
6+
7+
0 findings, 5 advisory (OWN053).
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Case study: the orphaned awaitable (OWN053) and Wolverine's `ScheduleRetryAsync`
2+
3+
**What the analyzer saw.** Scanning JasperFx/wolverine (a frozen benchmark
4+
consumer, main at `7ee3df90` / `c20fb0cc`) the OwnIR extractor found exactly one
5+
production local in 772 project/TFM units that is initialised by an un-awaited,
6+
effectful awaitable and never referenced again:
7+
8+
```csharp
9+
// src/Persistence/Wolverine.Postgresql/Transport/PostgresqlQueueSender.cs
10+
try
11+
{
12+
var tx = conn.BeginTransactionAsync(cancellationToken); // never awaited, never read
13+
await scheduleMessageAsync(envelope, cancellationToken, conn);
14+
}
15+
finally
16+
{
17+
await conn.CloseAsync();
18+
}
19+
```
20+
21+
**Why it matters (measured, not assumed).** Against Npgsql 10.0.3 and PostgreSQL 16:
22+
23+
- `BeginTransactionAsync` completes synchronously (`IsCompleted == true` right
24+
after the call) and changes the connection state inline, whether or not the
25+
returned `ValueTask<NpgsqlTransaction>` is ever awaited.
26+
- Every later command on the connection runs inside that transaction; a second
27+
`BeginTransactionAsync` throws "a transaction is already in progress".
28+
- Nobody holds the transaction, so nothing commits or disposes it; closing the
29+
connection rolls the work back. A probe that inserts after the orphaned call
30+
and closes the connection loses the insert.
31+
32+
**Why the message is not lost today.** Driving the real `PostgresqlQueueSender`
33+
with an incoming row present gives `incoming = 0, scheduled = 1` after the call:
34+
the command right before the `try` is one autocommitted batch (`delete from
35+
incoming ...; insert into scheduled ... on conflict do update`) that already moves
36+
the message. The write inside the orphaned transaction is a redundant second upsert
37+
whose rollback is invisible. The defect is real — an unobserved acquisition, a
38+
redundant rolled-back write, and a latent trap for any write placed after the
39+
orphan — but its current impact is robustness, not data loss. The first
40+
hand-written "exact shape" probe omitted that batch and overstated the impact;
41+
the correction is recorded (erratum 5 in the research paperwork) and is the
42+
reason the upstream report describes the measured behaviour only.
43+
44+
**Why nothing else reported it.** CA2012, VSTHRD110, MA0134 and CS4014 are silent
45+
by design: assigning the task to a local counts as "observing it later". CS0219 is
46+
not issued for a method-call result. IDE0059 reports the pattern in plain and
47+
async shapes but goes silent inside `try` / `finally` — exactly this site's shape.
48+
49+
**What Own.NET does differently.** OWN053 looks at the *subsequent life of the
50+
acquired protocol object*, not at the statement form: an awaitable whose result or
51+
completion carries an obligation (an owned result, or a connection / transaction
52+
lifecycle call) that is obtained and then never awaited, returned, stored, passed
53+
or otherwise observed. It is deliberately narrow — discards and bare statements
54+
belong to other rules, non-effectful awaitables are out of scope, and new
55+
lifecycle names enter only with a runtime witness — and it offers no automatic
56+
fix: only the author knows whether the call should be awaited, kept, or made an
57+
explicit fire-and-forget.
58+
59+
**Evidence trail.** Preregistration before any code, an exact Npgsql runtime
60+
falsifier, the analyzer-overlap measurement, an 8-consumer census (one orphan),
61+
and an OFF/ON scan of the prototype over 766 units with Python/Rust parity and
62+
byte-identical OFF facts; the records live in the Own.NET-paperwork repository
63+
(`paper-eval/h29/`), the probes and drivers in `corpus/ownership-lab/h29/`.

0 commit comments

Comments
 (0)