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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ jobs:
python audit/aggregate/score.py --selftest
python audit/aggregate/report.py --selftest
python audit/static/run_static.py --selftest
python audit/runtime/ingest.py --selftest

tests:
name: tests (py${{ matrix.python-version }})
Expand Down
24 changes: 16 additions & 8 deletions audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ audit/
inject/ # OwnAudit.Directory.Build.props/.targets (analyzer injection, gated)
taxonomy/
categories.yml # rule-id -> category knowledge base (Plan.md §2/§3.4)
runtime/ # runtime layer (Plan.md §4) — see runtime/README.md
ingest.py # leak-harness JSON -> SARIF -> the unified pipeline (PURE PYTHON, CI-gated)
scenarios/ # declarative leak-harness scenarios (+ schema)
LeakHarness/ # C# harness (FlaUI + procdump + ClrMD), Windows/build-required, not CI-gated
config/profiles/
desktop-wpf.yml # which packs / severity floor for the net472 WPF target
requirements.txt # PyYAML (audit-scoped)
Expand Down Expand Up @@ -113,11 +117,15 @@ python audit/static/run_static.py --selftest # full pipeline end-to-end on fix

## Status

- **Done:** static build-free runners, normalization + taxonomy (incl. the OWN001
`[resource:]` split, OWN014 region-escape labelling, and OWN050 routed to the
coverage ledger), DevExpress baseline-suppress, cross-tool agreement scoring, the
pain heatmap, **all four renderers (markdown / json / merged SARIF / HTML)**, the
analyzer-injection props/targets, and selftests.
- **Deferred:** the runtime layer (FlaUI + ClrMD leak-harness, duplicate-immutable
detector); the AI-reviewer layer; feeding confirmed findings back into the
OwnLang corpus.
- **Static (Phase 1) — done:** build-free runners, normalization + taxonomy (incl.
the OWN001 `[resource:]` split, OWN014 region-escape labelling, and OWN050 routed
to the coverage ledger), DevExpress baseline-suppress, cross-tool agreement
scoring, the pain heatmap, **all four renderers (markdown / json / merged SARIF /
HTML)**, the analyzer-injection props/targets, and selftests.
- **Runtime (Phase 2) — started:** the runtime→pipeline bridge (`runtime/ingest.py`,
CI-gated), the leak-harness scenario schema + one scenario, runtime rule mappings
in the taxonomy (categories 2/3/4/11), and the C# leak-harness skeleton. See
`runtime/README.md`.
- **Deferred:** the ClrMD duplicate-immutable detector and PropertyChanged-storm
profiler; the AI-reviewer layer; feeding confirmed findings back into the OwnLang
corpus.
98 changes: 98 additions & 0 deletions audit/runtime/LeakHarness/HeapCounter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Diagnostics.Runtime;

namespace OwnNet.Audit.Runtime
{
/// <summary>
/// Snapshots the TARGET process heap and counts live instances of suspect types.
/// The harness drives the app out-of-process (FlaUI), so the only way to read the
/// target's managed heap is a full dump (procdump) + ClrMD — dotnet-gcdump /
/// dotnet-counters do not attach to .NET Framework, only CoreCLR (Plan.md §4).
/// </summary>
internal sealed class HeapCounter
{
private readonly string _procdump;
private readonly string _scratch;

public HeapCounter(string procdumpPath, string scratchDir)
{
_procdump = procdumpPath;
_scratch = scratchDir;
Directory.CreateDirectory(_scratch);
}

/// <summary>
/// Full-dump the process and return { type -> live instance count } for the
/// requested types. A full dump captures the heap as the GC last left it, so
/// request a GC in the target (SematixTrace) before calling this.
/// </summary>
public Dictionary<string, int> CountLiveInstances(int pid, IEnumerable<string> types)
{
var wanted = new HashSet<string>(types);
var counts = new Dictionary<string, int>();
foreach (var t in wanted)
{
counts[t] = 0;
}

var dump = Path.Combine(_scratch, $"target-{pid}-{Stopwatch.GetTimestamp()}.dmp");
RunProcdump(pid, dump);
try
{
using var dataTarget = DataTarget.LoadDump(dump);
var clr = dataTarget.ClrVersions.FirstOrDefault()
?? throw new InvalidOperationException(
$"dump for pid {pid} contains no CLR — is the target a managed process?");
using var runtime = clr.CreateRuntime();
foreach (var obj in runtime.Heap.EnumerateObjects())
{
var name = obj.Type?.Name;
if (name != null && wanted.Contains(name))
{
counts[name]++;
}
}
}
finally
{
File.Delete(dump); // dumps are large; the counts are the artifact, not the dump
}
return counts;
}

private void RunProcdump(int pid, string dumpPath)
{
// -ma = full dump (managed heap included), -accepteula for unattended runs.
var psi = new ProcessStartInfo(_procdump, $"-accepteula -ma {pid} \"{dumpPath}\"")
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
using var p = Process.Start(psi)!;
// Drain both pipes asynchronously BEFORE waiting: if procdump fills a pipe
// buffer while the harness blocks in WaitForExit(), both deadlock. Bound the
// wait so a stuck procdump can't hang the harness forever, and check the
// exit code, not just whether a file appeared.
var stdout = p.StandardOutput.ReadToEndAsync();
var stderr = p.StandardError.ReadToEndAsync();
if (!p.WaitForExit(120_000))
{
try { p.Kill(); } catch { /* best effort */ }
throw new IOException($"procdump timed out (>120s) for pid {pid}");
}
Task.WaitAll(stdout, stderr);
if (p.ExitCode != 0 || !File.Exists(dumpPath))
{
throw new IOException(
$"procdump failed for pid {pid} (exit {p.ExitCode}): {stderr.Result}");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
27 changes: 27 additions & 0 deletions audit/runtime/LeakHarness/LeakHarness.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
Own.NET Audit — leak-harness (Plan.md §4.1). Build-required, WINDOWS-ONLY:
needs a running target, procdump (Sysinternals) and ClrMD. This project is NOT
part of the Linux CI gate — the Python ingest bridge (audit/runtime/ingest.py)
is what CI tests. net472 to match the legacy target's runtime so ClrMD reads its
heap and FlaUI drives its WPF/WinForms UI.
-->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net472</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<AssemblyName>LeakHarness</AssemblyName>
<RootNamespace>OwnNet.Audit.Runtime</RootNamespace>
<Platforms>x64</Platforms>
<!-- <Platforms> only declares availability; pin the build/run bitness so ClrMD
reads the full dump and FlaUI inspects the target at a predictable arch. -->
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FlaUI.UIA3" Version="4.0.0" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.512801" />
<PackageReference Include="YamlDotNet" Version="15.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
Loading
Loading