Skip to content

NullReferenceException in GeometryImpl.PathContainsCore during pointer-over hit testing (SceneInvalidated) under software rendering / RDP #21964

Description

@dnehl

Describe the bug

We are seeing repeated NullReferenceExceptions thrown from Avalonia.Skia.GeometryImpl.PathContainsCore during the pointer-over re-evaluation that PresentationSource.SceneInvalidated triggers after a composition update. No application code is on the stack — the exception surfaces on the UI thread via Dispatcher.UIThread.UnhandledException (and once additionally as an UnobservedTaskException wrapping the same exception).

This happened twice within two days for different users of a desktop LOB application, in both cases:

  • while the pointer was over a window containing several DataGrids,
  • in Remote Desktop (RDP) sessions with software rendering (no GPU),
  • once while the window was simply open, and once ~2 seconds after a window had just been closed.

The application survives when the exception is marked handled; hit testing works again on the next pointer move. It is not reproducible on demand — it appears to be a timing-dependent race that our GPU-rendering machines never hit.

Stack traces

Variant 1 (via SceneInvalidated, most common):

System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Skia.GeometryImpl.PathContainsCore(SKPath path, Point point)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositingRenderer.HitTestFirst(Point p, Visual root, Func`2 filter)
   at Avalonia.Controls.PresentationSource.SceneInvalidated(Object sender, SceneInvalidatedEventArgs sceneInvalidatedEventArgs)
   at Avalonia.Rendering.Composition.CompositingRenderer.<UpdateCore>b__28_1()
   at Avalonia.Threading.DispatcherOperation.InvokeCore()

Variant 2 (second incident, shallower — via Dispatcher.Send, ~2 s after a Window was closed):

System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Skia.GeometryImpl.PathContainsCore(SKPath path, Point point)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Rendering.Composition.CompositionTarget.HitTestFirstCore(CompositionVisual visual, Point parentPoint, Func`2 filter, Func`2 resultFilter)
   at Avalonia.Threading.Dispatcher.Send(SendOrPostCallback action, Object arg, Nullable`1 priority)

The first incident was also re-thrown by the finalizer thread as an UnobservedTaskException (AggregateException wrapping the identical inner NullReferenceException), so the same failure escaped through two channels.

Analysis

PathContainsCore already null-checks its parameter (identical in the 12.1.1 tag and current master):

private static bool PathContainsCore(SKPath? path, Point point)
{
    return path is not null && path.Contains((float)point.X, (float)point.Y);
}

So the NRE cannot come from a null SKPath reference. It must originate inside SKPath.Contains, i.e. SkiaApi.sk_path_contains(Handle, x, y) dereferencing an invalid/zeroed native handle — which the CLR then reports as a managed NullReferenceException attributed to the calling frame (a native access violation near address zero).

That points to a lifetime/threading race on the native SKPath:

  • GeometryImpl.FillContains reads FillPath without taking _lock (unlike StrokeContains, which locks around the PathCache), so the UI-thread hit test can use an SKPath that is concurrently being replaced/disposed.
  • Under software rendering the render timing differs substantially from GPU rendering, which would explain why we only see this on RDP/software-rendering sessions.

This looks like the same root cause as #13706 (AccessViolationException in SkiaApi.sk_path_contains via PathContainsCore while hit testing a PolylineGeometry, closed as not planned) — whether the fault surfaces as an AccessViolationException or a NullReferenceException just depends on the faulting address.

To Reproduce

Not reproducible on demand, unfortunately. Observed in production:

  1. Run an Avalonia desktop app in an RDP session with software rendering.
  2. Work in windows containing several DataGrids with frequent updates, mouse moving over the content.
  3. After minutes to hours, the exception fires from the hit-test path described above.

Expected behavior

Pointer-over hit testing never throws, even if a geometry's native path is being swapped/disposed concurrently.

Avalonia version

12.1.1

OS

Windows

Additional context

Environment

  • OS: Windows Server 2022 (10.0.20348), app running inside RDP sessions
  • .NET: 10.0.10
  • Rendering: software rendering (no GPU in the RDP session)
  • Occurrences: 2 incidents in 2 days across different users/machines of the same deployment; never observed on GPU-rendered local sessions

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions