Skip to content

Commit 0039ef5

Browse files
committed
ownership-semantics-lab: probe seams, fixtures, the API-surface tool and the discovery pipeline (research-only)
Research seams behind opt-in flags, byte-identical off (NC1: 137 of 137 sealed corpus documents identical; the pinned resource-effects control suites pass with the flags off): OWEN_LAB_NULLINIT (H-01: a null-initialised local assigned once from an acquire-shaped expression is a candidate), OWEN_LAB_NULLGUARD (H-19: a not-null guard on an unwritten, un-aliased local whose branch names only that local is lowered bare; narrowed after the G-V4 control), OWEN_LAB_THROWEXIT (H-20: throw exits carry `exit: "throw"`; both engines stop reading them as bare value returns). The research key loader accepts `assembly: {name, mvid}` pins (H-09; mismatches counted as shadow) and the WITNESSED provenance (H-10). The JSON writer's MaxDepth is raised (deeply nested else chains crashed the extractor on Newtonsoft.Json). OwnSharp.ApiList lists an assembly's public resource-bearing surface. Fixtures with observed outputs for F0/F1/F2/F3, the Z3 probe, the MVID falsifier inputs, the witness generator and the discovery scripts. Records: Own.NET-paperwork paper-eval/ownership-lab/probes-v1.json. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Am9eQwzNfbugH72eVKetC2
1 parent 37f0b02 commit 0039ef5

36 files changed

Lines changed: 1143 additions & 19 deletions

‎corpus/ownership-lab/README.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# ownership-semantics-lab (research/ownership-semantics-lab-v1, EXPLORATORY)
2+
3+
Research-only fixtures and tooling of the ownership-semantics-lab track. Nothing here is part of a
4+
production test or build; every analyzer change on this branch sits behind an opt-in environment
5+
flag (`OWEN_LAB_NULLINIT`, `OWEN_LAB_NULLGUARD`, `OWEN_LAB_THROWEXIT`) and is byte-identical off.
6+
Records: Own.NET-paperwork `paper-eval/ownership-lab/` (taxonomy, prior-art map, hypothesis register,
7+
probes, discovery protocol and results).
8+
9+
| dir | what |
10+
|---|---|
11+
| `adoption-probe/` | the four fixture variants of the RLC# held-out wrapper (classification probe) |
12+
| `depid/` | LibA/LibB/LibC/App: the runtime-effective dependency identity falsifier |
13+
| `h01/` | F0 (null-guard control), F1 (H-01 null-initialised locals), F2 (H-19 twins) with observed outputs |
14+
| `h09/` | the MVID-pinned row falsifier inputs (consumer, keys) |
15+
| `h16/` | the Z3 symbolic probe and the complementary-guard fixture |
16+
| `h20/` | F3 (throw-exit versus bare return) with observed outputs off/on |
17+
| `witness/` | the runtime-witness generator (`gen.py`), its ten rows and results |
18+
| `discovery/` | the discovery-experiment pipeline scripts (acquire, API surface, fetch, derive, consumers, scan) |
19+
20+
Tools: `frontend/roslyn/OwnSharp.AsmId` (assembly identity facets), `frontend/roslyn/OwnSharp.ApiList`
21+
(public resource-bearing API surface). Scratch paths inside the scripts point at the session
22+
container and are recorded as such.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""discovery S5: derive BODY_PROVED rows from the fetched source files of one library through the research body arms
2+
(E1 fresh returns, E2 receiver releases, E3 mixed returns) and pin them to the deployed assembly's MVID.
3+
usage: derive.py <PackageId> (reads libs/<id>/acquire.json and libs/<id>/src/**.cs)"""
4+
import sys, os, json, subprocess, glob, re
5+
S='/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad'; R='/home/user/Own.NET'
6+
pid=sys.argv[1]; d=f'{S}/lab/disc/libs/{pid}'; acq=json.load(open(f'{d}/acquire.json'))
7+
src=sorted(glob.glob(f'{d}/src/**/*.cs', recursive=True))
8+
env={**os.environ,'PATH':'/root/.dotnet:'+os.environ['PATH'],'DOTNET_NOLOGO':'1','DOTNET_CLI_TELEMETRY_OPTOUT':'1','PYTHONPATH':R,
9+
'OWEN_RE_BODY':'1','OWEN_RE_MINTED_RETURN':'1','OWEN_RE_MIXED_RETURN':'1','OWEN_RE_DUMP_EFFECTS':f'{d}/dump.json'}
10+
for k in ('OWEN_RE_ORACLE','OWEN_P037X_RELATIONAL','OWEN_LAB_NULLINIT','OWEN_LAB_NULLGUARD'): env.pop(k,None)
11+
refdir=os.path.dirname(acq['main_assembly']) if acq.get('main_assembly') else None
12+
argv=['dotnet',f'{R}/frontend/roslyn/OwnSharp.Extractor/bin/Release/net8.0/ownsharp-extract.dll','--flow-locals',*src,'-o',f'{d}/facts.json']
13+
if refdir: argv+=['--ref-dir',refdir]
14+
r=subprocess.run(argv,cwd=R,capture_output=True,text=True,env=env)
15+
open(f'{d}/derive.err','w').write(r.stderr)
16+
summ=subprocess.run(['python3','-m','ownlang','summaries',f'{d}/facts.json'],cwd=R,capture_output=True,text=True,env=env).stdout
17+
try: summaries=json.loads(summ)['summaries']
18+
except Exception: summaries=[]
19+
api=json.load(open(f'{d}/api.json')) if os.path.exists(f'{d}/api.json') else {'factories':[],'release_name_candidates':[],'types':[]}
20+
public_callables={f['callable'] for f in api['factories']} | {r['callable'] for r in api['release_name_candidates']} | {f"{t['type']}.Dispose" for t in api['types']}
21+
public_types={t['type'] for t in api['types']}
22+
mvid=(acq.get('identity') or {}).get('mvid'); asm=acq['package']
23+
rows=[]; seen=set()
24+
for s in summaries:
25+
if s.get('returns',{}).get('owned')=='fresh':
26+
m=s['method'].split('(')[0]; sig=s.get('sig')
27+
# method summaries are keyed Namespace.Type.Method[(sig)]; keep only PUBLIC factories of the API surface;
28+
# explicit interface implementations (Type.IFace.Method) never match a public callable
29+
if m in public_callables and (m,'fresh') not in seen:
30+
seen.add((m,'fresh')); rows.append({'callable':m,'effect':'return_fresh_owned','provenance':'BODY_PROVED','assembly':{'name':asm,'mvid':mvid},'derived_from':f"{acq.get('repository_url')}@{acq.get('repository_commit')} via E1/E3 ({s.get('source')})"})
31+
dump=json.load(open(f'{d}/dump.json')) if os.path.exists(f'{d}/dump.json') else {}
32+
for e in dump.get('receiver_release',[]):
33+
if e.get('releases') and e['callable'] in public_callables and not e['callable'].endswith('.Dispose') and (e['callable'],e.get('arity')) not in seen:
34+
seen.add((e['callable'],e.get('arity'))); rows.append({'callable':e['callable'],'effect':'receiver_terminal_release','arity':e.get('arity'),'provenance':'BODY_PROVED','assembly':{'name':asm,'mvid':mvid},'derived_from':f"{acq.get('repository_url')}@{acq.get('repository_commit')} via E2"})
35+
census=re.search(r're-body: (.*)',r.stderr); stats=json.load(open(f'{d}/facts.json')).get('stats') if os.path.exists(f'{d}/facts.json') else None
36+
key={'schema':'own.net/re-oracle/v1','label':f'discovery BODY_PROVED rows for {pid} {acq["version"]} (pinned to MVID {mvid})','entries':rows}
37+
json.dump(key,open(f'{d}/rows-bodyproved.json','w'),indent=1)
38+
fresh_all=[s['method'] for s in summaries if s.get('returns',{}).get('owned')=='fresh']
39+
fresh_public_missing=[s['method'] for s in summaries if s.get('returns',{}).get('owned')=='fresh' and s['method'].split('(')[0] not in public_callables]
40+
rel_all=[e['callable'] for e in dump.get('receiver_release',[]) if e.get('releases')]
41+
json.dump({'source_files':len(src),'extractor_stats':stats,'re_body_census':census.group(1) if census else None,'fresh_summaries_all':fresh_all,'receiver_release_all':rel_all,'fresh_not_public_surface':fresh_public_missing,'instance_methods_evaluated':dump.get('instance_methods_evaluated'),'public_rows':len(rows)},open(f'{d}/derive-summary.json','w'),indent=1)
42+
print(json.dumps({'package':pid,'files':len(src),'stats':stats,'census':census.group(1) if census else None,'fresh_all':len(fresh_all),'release_all':len(rel_all),'public_rows':len(rows)}))
43+
for x in rows: print(' ROW', x['effect'], x['callable'], x.get('arity',''))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""discovery S1: acquire the exact binary (nupkg from the NuGet flat container), its nuspec repository pin, the
2+
highest-TFM lib assembly and its identity (MVID/sha256). usage: fetch.py <PackageId> <version>"""
3+
import sys, os, json, hashlib, zipfile, urllib.request, re, subprocess
4+
S='/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad'; R='/home/user/Own.NET'
5+
pid, ver = sys.argv[1], sys.argv[2]
6+
d=f'{S}/lab/disc/libs/{pid}'; os.makedirs(d, exist_ok=True)
7+
nupkg=f'{d}/{pid.lower()}.{ver}.nupkg'
8+
if not os.path.exists(nupkg):
9+
urllib.request.urlretrieve(f'https://api.nuget.org/v3-flatcontainer/{pid.lower()}/{ver}/{pid.lower()}.{ver}.nupkg', nupkg)
10+
sha=hashlib.sha256(open(nupkg,'rb').read()).hexdigest()
11+
z=zipfile.ZipFile(nupkg)
12+
spec=[n for n in z.namelist() if n.endswith('.nuspec')][0]; xml=z.read(spec).decode('utf-8','replace')
13+
repo=re.search(r'<repository[^>]*url="([^"]+)"[^>]*commit="([^"]+)"', xml) or re.search(r'<repository[^>]*commit="([^"]+)"[^>]*url="([^"]+)"', xml)
14+
url=commit=None
15+
if repo:
16+
a,b=repo.group(1),repo.group(2); (url,commit)=(a,b) if a.startswith('http') else (b,a)
17+
tfms=['net8.0','net9.0','net10.0','net7.0','net6.0','netstandard2.1','netstandard2.0','net462','net461','net45']
18+
libs=[n for n in z.namelist() if n.startswith('lib/') and n.endswith('.dll')]
19+
chosen=None
20+
for t in tfms:
21+
c=[n for n in libs if n.split('/')[1]==t]
22+
if c: chosen=c; break
23+
if chosen is None and libs: chosen=libs
24+
out=[]
25+
if chosen:
26+
for n in chosen:
27+
p=f'{d}/lib/{n.split("/")[1]}/{os.path.basename(n)}'; os.makedirs(os.path.dirname(p), exist_ok=True); open(p,'wb').write(z.read(n)); out.append(p)
28+
main=[p for p in out if os.path.basename(p).lower()==f'{pid.lower()}.dll'] or out[:1]
29+
ident=None
30+
if main:
31+
r=subprocess.run(['dotnet',f'{R}/frontend/roslyn/OwnSharp.AsmId/bin/Release/net8.0/ownsharp-asmid.dll',main[0]],capture_output=True,text=True,env={**os.environ,'PATH':'/root/.dotnet:'+os.environ['PATH'],'DOTNET_NOLOGO':'1','DOTNET_CLI_TELEMETRY_OPTOUT':'1'})
32+
try: ident=json.loads(r.stdout.strip().splitlines()[-1])
33+
except Exception: ident={'error':r.stderr[-300:]}
34+
rec={'package':pid,'version':ver,'nupkg_sha256':sha,'repository_url':url,'repository_commit':commit,'lib_files':out,'main_assembly':main[0] if main else None,'identity':ident,'xmldoc':[n for n in z.namelist() if n.endswith('.xml') and n.startswith('lib/')][:3]}
35+
json.dump(rec, open(f'{d}/acquire.json','w'), indent=1); print(json.dumps({k:rec[k] for k in ('package','version','repository_url','repository_commit','main_assembly')}), 'mvid=', (ident or {}).get('mvid'))
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""discovery S8 (acquire): fetch consumer source files at a commit into libs/<pkg>/consumers/<tag>/ (cap 40)."""
2+
import sys, os, json, hashlib, urllib.request
3+
S='/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad'
4+
pid, tag, repo, commit, paths = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5:][:40]
5+
d=f'{S}/lab/disc/libs/{pid}/consumers/{tag}'; os.makedirs(d, exist_ok=True); rec=[]
6+
for p in paths:
7+
url=f'https://raw.githubusercontent.com/{repo}/{commit}/{p}'; out=f'{d}/{p.replace("/","__")}'
8+
try:
9+
with urllib.request.urlopen(url, timeout=60) as r: data=r.read()
10+
open(out,'wb').write(data); rec.append({'path':p,'sha256':hashlib.sha256(data).hexdigest(),'bytes':len(data)})
11+
except Exception as e: rec.append({'path':p,'error':str(e)[:80]})
12+
json.dump({'package':pid,'consumer':tag,'repo':repo,'commit':commit,'files':rec}, open(f'{S}/lab/disc/libs/{pid}/s8-consumers-{tag}.json','w'), indent=1)
13+
print(pid, tag, 'fetched', len([r for r in rec if 'sha256' in r]), 'of', len(rec), 'errors:', [r['path'] for r in rec if 'error' in r])
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""discovery S4: fetch the resource-bearing source files at the nuspec-pinned commit from raw.githubusercontent.com.
2+
usage: fetchsrc.py <PackageId> <owner/repo> <commit> <path> [<path> ...] (cap 25 files; digests recorded)"""
3+
import sys, os, json, hashlib, urllib.request
4+
S='/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad'
5+
pid, repo, commit, paths = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4:][:25]
6+
d=f'{S}/lab/disc/libs/{pid}/src'; os.makedirs(d, exist_ok=True); rec=[]
7+
for p in paths:
8+
url=f'https://raw.githubusercontent.com/{repo}/{commit}/{p}'; out=f'{d}/{p.replace("/","__")}'
9+
try:
10+
with urllib.request.urlopen(url, timeout=60) as r: data=r.read()
11+
open(out,'wb').write(data); rec.append({'path':p,'sha256':hashlib.sha256(data).hexdigest(),'bytes':len(data)})
12+
except Exception as e:
13+
rec.append({'path':p,'error':str(e)[:80]})
14+
json.dump({'package':pid,'repo':repo,'commit':commit,'files':rec}, open(f'{S}/lab/disc/libs/{pid}/s4-source.json','w'), indent=1)
15+
ok=[r for r in rec if 'sha256' in r]; print(pid, 'fetched', len(ok), 'of', len(rec), 'errors:', [r['path'] for r in rec if 'error' in r])
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
# discovery S1+S2 for a list of "Package version" pairs
3+
export PATH=/root/.dotnet:$PATH DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
4+
S=/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad; cd $S/lab/disc
5+
while read -r pid ver; do
6+
[ -z "$pid" ] && continue
7+
python3 fetch.py "$pid" "$ver" 2>&1 | tail -1
8+
main=$(python3 -c "import json;print(json.load(open('libs/$pid/acquire.json')).get('main_assembly') or '')")
9+
if [ -n "$main" ]; then dotnet /home/user/Own.NET/frontend/roslyn/OwnSharp.ApiList/bin/Release/net8.0/ownsharp-apilist.dll "$main" > libs/$pid/api.json 2> libs/$pid/api.err
10+
python3 -c "import json;a=json.load(open('libs/$pid/api.json'));print(' API $pid: types',a['disposable_types'],'factories',len(a['factories']),'adopting ctors',len(a['adopting_ctors']),'release names',len(a['release_name_candidates']))" || echo " API $pid: FAILED $(head -c 200 libs/$pid/api.err)"
11+
else echo " $pid: no main assembly"; fi
12+
done <<'LIST'
13+
MySqlConnector 2.6.2
14+
StackExchange.Redis 3.3.1
15+
RabbitMQ.Client 7.2.2
16+
MQTTnet 5.2.0.1603
17+
SSH.NET 2026.0.0
18+
MailKit 4.18.1
19+
SharpZipLib 1.4.2
20+
SharpCompress 1.0.0
21+
SixLabors.ImageSharp 4.1.2
22+
BouncyCastle.Cryptography 2.7.0
23+
NSec.Cryptography 26.4.0
24+
SkiaSharp 4.153.1
25+
LibGit2Sharp 0.32.0
26+
CsvHelper 33.1.0
27+
Newtonsoft.Json 13.0.4
28+
LIST
29+
echo S12_DONE
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""discovery S8 (scan): OFF (no key) versus KEY (applied rows) over a consumer set; findings diffed per file (Rust engine,
2+
guarded off); python==rust checked. usage: scan.py <pkg> <consumer-tag> [applied-rows.json]"""
3+
import sys, os, json, subprocess, glob, re, hashlib
4+
S='/tmp/claude-0/-home-user/8a9da608-aa27-5449-8306-1fae9426f9b9/scratchpad'; R='/home/user/Own.NET'
5+
pid, tag = sys.argv[1], sys.argv[2]; L=f'{S}/lab/disc/libs/{pid}'
6+
key = sys.argv[3] if len(sys.argv)>3 else f'{L}/rows-applied.json'
7+
acq=json.load(open(f'{L}/acquire.json')); refdir=os.path.dirname(acq['main_assembly'])
8+
files=sorted(glob.glob(f'{L}/consumers/{tag}/*.cs'))
9+
DLL=f'{R}/frontend/roslyn/OwnSharp.Extractor/bin/Release/net8.0/ownsharp-extract.dll'; OWN=f'{R}/rust/target/release/own-cli'
10+
FIND=re.compile(r'([^/\s]+\.cs):(\d+): (\w+): \[(OWN\d+)\] (.*)')
11+
env={**os.environ,'PATH':'/root/.dotnet:'+os.environ['PATH'],'DOTNET_NOLOGO':'1','DOTNET_CLI_TELEMETRY_OPTOUT':'1','PYTHONPATH':R}
12+
for k in ('OWEN_RE_ORACLE','OWEN_RE_BODY','OWEN_RE_MINTED_RETURN','OWEN_RE_MIXED_RETURN','OWEN_P037X_RELATIONAL','OWEN_P037X_GUARDED','OWEN_LAB_NULLINIT','OWEN_LAB_NULLGUARD'): env.pop(k,None)
13+
def run(arm, e):
14+
out=f'{L}/s8-{tag}.{arm}.facts.json'
15+
r=subprocess.run(['dotnet',DLL,'--flow-locals',*files,'--ref-dir',refdir,'-o',out],cwd=R,capture_output=True,text=True,env=e)
16+
census=re.findall(r'(re-oracle[^\n]*)',r.stderr)
17+
v=subprocess.run([OWN,'ownir',out,'--format','human','--severity','warning','--verbosity','verbose'],cwd=R,capture_output=True,text=True,env={**e,'OWEN_P037X_GUARDED':'0'}).stdout
18+
p=subprocess.run(['python3','-m','ownlang','ownir',out,'--format','human','--severity','warning','--verbosity','verbose'],cwd=R,capture_output=True,text=True,env=e).stdout
19+
f=sorted(set((m.group(1),int(m.group(2)),m.group(4),m.group(5)) for m in FIND.finditer(v)))
20+
fp=sorted(set((m.group(1),int(m.group(2)),m.group(4),m.group(5)) for m in FIND.finditer(p)))
21+
return {'findings':f,'python_equals_rust':f==fp,'census':census,'stats':json.load(open(out)).get('stats') if os.path.exists(out) else None}
22+
off=run('off',env); key_=run('key',{**env,'OWEN_RE_ORACLE':key})
23+
new=[x for x in key_['findings'] if x not in off['findings']]; lost=[x for x in off['findings'] if x not in key_['findings']]
24+
res={'package':pid,'consumer':tag,'files':len(files),'key':key,'key_sha256':hashlib.sha256(open(key,'rb').read()).hexdigest(),'off':off,'key_arm':key_,'new_findings':new,'lost_findings':lost}
25+
json.dump(res,open(f'{L}/s8-scan-{tag}.json','w'),indent=1)
26+
print(json.dumps({'package':pid,'consumer':tag,'files':len(files),'off_findings':len(off['findings']),'key_findings':len(key_['findings']),'new':len(new),'lost':len(lost),'py==rs':off['python_equals_rust'] and key_['python_equals_rust'],'census':key_['census'],'stats':key_['stats']}))
27+
for x in new: print(' NEW', x)
28+
for x in lost: print(' LOST', x)

‎corpus/ownership-lab/h01/F0.cs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using System.IO;
3+
public static class F0
4+
{
5+
public static void NullGuardRelease()
6+
{
7+
var x = new MemoryStream();
8+
x.WriteByte(1);
9+
if (x != null) x.Dispose(); // control: a null guard on a tracked local must not read as 'may not release'
10+
}
11+
public static void NullConditionalRelease()
12+
{
13+
var x = new MemoryStream();
14+
x.WriteByte(1);
15+
x?.Dispose();
16+
}
17+
public static void TryFinallyNullGuard()
18+
{
19+
var x = new MemoryStream();
20+
try { x.WriteByte(1); }
21+
finally { if (x != null) x.Dispose(); }
22+
}
23+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
F0.guard2.facts.json: ok — no subscription leaks found
2+
3+
0 findings.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
F0.cs:7: warning: [OWN001] IDisposable local 'x' may not be disposed on every path (leak) [resource: disposable]
2+
F0.cs:19: warning: [OWN001] IDisposable local 'x' may not be disposed on every path (leak) [resource: disposable]
3+
4+
2 findings.

0 commit comments

Comments
 (0)