Skip to content

Commit eb5c372

Browse files
marcosqlbiclaude
andauthored
Stop over-releasing the LiveView frame surface and bump to 1.0.3 (#71)
1.0.0, 1.0.1 and 1.0.2 all crash the Store build a few seconds after a LiveView starts presenting, with the same `AccessViolationException` (`0xC0000005`) from `IObjectReference.Finalize` -> `Marshal.Release` -> `GC.RunFinalizers`. Both previous fixes moved WinRT wrappers between apartments and neither changed the symptom, because this was never a threading problem. It is a use-after-free. The finalizer in the stack is the victim: it releases a pointer whose reference count has already reached zero. `GetTexture` released the frame surface twice, and has done so since LiveView was added, which is why neither of the two fixes since touched it: - `IObjectReference.GetRef()` hands out one owned reference (+1). - `ComObject.As<T>(nint)` takes ownership of the pointer it is given: its temporary wrapper releases it, including when the `QueryInterface` throws (-1). - The `finally` released it a third time (-1). Both deltas were measured against the shipped `SharpGen.Runtime` and `WinRT.Runtime` assemblies rather than inferred from the API names. `TryPresent` calls this once per presented frame, the frame pool holds two buffers and recycles them, so at 15 fps the recycled surfaces run out of references within seconds. The next release of the freed pointer faults, and whichever holder gets there last raises it — in the reported crash, the finalizer. That matches the repro: create a LiveView, then move it or just wait. Pair `GetRef` with `ComObject.As` and release nothing else. Net zero per frame. Because `As` consumes the pointer even on a failed `QueryInterface`, dropping the `try`/`finally` is safe on the error path too. The over-release is present in every build. What varies is whether it faults — how many references the capture pipeline itself holds on the recycled surfaces depends on the GPU driver, and whether the freed block is reused before the final release depends on the process. The unpackaged builds are very likely corrupting memory too and getting away with it, so the packaged-only correlation that shaped 1.0.1 and 1.0.2 was a red herring. The `WinRtThreading` and DispatcherQueue work from 1.0.1 and 1.0.2 stays: it is independently correct and harmless. `VersionPrefix` is 1.0.3 so this can go to the Store (identity `1.0.3.0`). Verified locally: `dotnet build Whiteboard.sln -c Release` is clean, Core smoke tests pass. The reference arithmetic is machine-independent, but the crash was only ever observed on a packaged install, so confirmation is still 1.0.3 packaged on a machine that crashed on 1.0.2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent ff5bfba commit eb5c372

3 files changed

Lines changed: 14 additions & 11 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
scripts/build-installer.ps1 both read it from here, so releasing is a reviewed change
66
to this line rather than an edit in a pipeline variable group.
77
-->
8-
<VersionPrefix>1.0.2</VersionPrefix>
8+
<VersionPrefix>1.0.3</VersionPrefix>
99
<LangVersion>latest</LangVersion>
1010
<Nullable>enable</Nullable>
1111
<ImplicitUsings>enable</ImplicitUsings>

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The delivery chain works end to end: a merge to `main` builds, signs, and publis
1616
pre-release to GitHub Releases, and one approval promotes that same build to a release.
1717
<https://whiteboard.sqlbi.com> reads its download links from the release manifest
1818
deployed beside it and needs no edit per release. The current product version is `VersionPrefix` in `Directory.Build.props`
19-
(1.0.2). Identity version for the Store package is `VersionPrefix.0` (`1.0.2.0`).
19+
(1.0.3). Identity version for the Store package is `VersionPrefix.0` (`1.0.3.0`).
2020

2121
Declaring that number is decision 20 in [docs/decisions.md](docs/decisions.md). What 1.0
2222
was waiting on shipped during 0.9.x: Preferences, `.wimport`, Explorer and VS Code

src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -455,18 +455,21 @@ private void ReleaseCaptureItem()
455455
_captureItem = null;
456456
}
457457

458+
/// <summary>
459+
/// Borrows the frame's DXGI texture. Reference counting here is the whole
460+
/// point: <c>GetRef</c> hands out one owned reference, and
461+
/// <c>ComObject.As</c> takes ownership of the pointer it is given — its
462+
/// temporary wrapper Releases it, including when the QueryInterface throws.
463+
/// Those two pair exactly. Releasing <c>inspectable</c> again dropped the
464+
/// frame pool's recycled surface by one reference on every presented frame,
465+
/// and the finalizer that later Released the freed pointer is the
466+
/// AccessViolationException that closes the app.
467+
/// </summary>
458468
private static ID3D11Texture2D GetTexture(IDirect3DSurface surface)
459469
{
460470
nint inspectable = ((IWinRTObject)surface).NativeObject.GetRef();
461-
try
462-
{
463-
using IDirect3DDxgiInterfaceAccess access = ComObject.As<IDirect3DDxgiInterfaceAccess>(inspectable);
464-
return access.GetInterface<ID3D11Texture2D>();
465-
}
466-
finally
467-
{
468-
Marshal.Release(inspectable);
469-
}
471+
using IDirect3DDxgiInterfaceAccess access = ComObject.As<IDirect3DDxgiInterfaceAccess>(inspectable);
472+
return access.GetInterface<ID3D11Texture2D>();
470473
}
471474

472475
private static WinRtDirect3DDevice CreateWinRtDevice(IDXGIDevice dxgiDevice)

0 commit comments

Comments
 (0)