Skip to content

[Bug] Keyed ItemsView history replacement exposes WinUI invalid-anchor crash #1213

Description

This content was largely generated by AI. AI makes mistakes.

Reactor ItemsView history replacement exposes WinUI's invalid bring-into-view anchor crash

Summary

Replacing a keyed Reactor ItemsView history while an
ItemsView.StartBringItemIntoView request is pending can terminate the process
with E_INVALIDARG (0x80070057, surfaced as 0xC000027B).

This is a Reactor-based reproduction of the native WinUI bug tracked in
microsoft/microsoft-ui-xaml#11865.
Unlike the native minimal reproduction, the application never collapses a
container
. A diagnostic run showed Reactor doing that during recycling:
ElementFactory<T>.RecycleElement -> TryParkForPool -> ParkOrphan.

The purpose of this report is to document the confirmed Reactor/WinUI interaction
and request investigation of a safe integration behavior/regression test. It does
not establish that Reactor, rather than WinUI, must own the eventual fix.

Related application investigation:
openclaw/openclaw-windows-node#1167.
OpenClaw's application-side workaround is
openclaw/openclaw-windows-node#1407 (fix(chat): work around WinUI session-switch crash).
That workaround avoids index-based bring navigation. It is not included in this
sample.

Reproduction

The complete sample is included below: one Program.cs and a small project
file. It starts with ReactorApp.Run<ChatRepro>() and is
unpackaged, with no application XAML, custom titlebar, window sizing, icons,
assets, or packaging setup.

  1. Save the two files below in an empty directory. On Windows ARM64, build and
    launch the project:

    winapp run .\ReactorItemsViewRepro.csproj --arch arm64 -p Platform=ARM64 --debug-output
  2. Wait for the synthetic session history and Switch session button to appear.

  3. Click Switch session once.

Actual: the process crashes during subsequent native layout with a stowed
E_INVALIDARG. In the confirmed runs, the native bring call returned and the
button handler requested the new Reactor state before the crash.

Expected: the old pending navigation should be canceled, superseded, or
otherwise handled safely when its row is recycled. Replacing the history should
not terminate the process.

The first button activation reproduced the crash in two out of two fresh,
uninstrumented launches of the minimal unpackaged sample
, using UI Automation
Invoke. The earlier packaged prototype also crashed on its first click in three
out of three launches, including a physical-pointer click. These are observed
success rates, not guarantees across machines.

What the sample models

  • One Reactor root and one persistent keyed ItemsView.
  • Two immutable synthetic histories with 77 variable-height text rows each.
  • Stable, unique keys within each history; different keys between sessions.
  • ItemContainer row roots with .WithKey(row.Key).
  • StackLayout, no selection, and no item invocation.
  • Non-animated, bottom-aligned ItemsView.StartBringItemIntoView.
  • A UseState session change, causing Reactor to reconcile the history.

These are the relevant list shapes used by OpenClaw before its workaround.
OpenClaw embeds its root in ReactorHostControl; the simplified sample uses
ReactorApp.Run instead. The embedded host and packaging are not necessary to
trigger the bug.

Timing reduction: the button deliberately starts a bring request for the
current history's tail, then immediately calls the state setter for the other
history. This makes the pending-navigation/history-replacement overlap reliable.
OpenClaw reaches overlapping operations through asynchronous history updates and
queued tail requests; this sample does not reproduce its entire scheduling
pipeline.

There is no explicit UpdateLayout, application-written visibility change,
custom element factory, manual ItemsSource mutation, reflection, artificial
exception, timer, network, Markdown renderer, or OpenClaw dependency. The requested
index is 76 and the current history contains 77 items when the native call starts.

Program.cs

using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using static Microsoft.UI.Reactor.Factories;
using NativeItemsView = Microsoft.UI.Xaml.Controls.ItemsView;

ReactorApp.Run<ChatRepro>();

public sealed class ChatRepro : Component
{
    private sealed record Message(string Key, string Text);

    private static readonly Message[][] Histories = Enumerable.Range(0, 2)
        .Select(session => Enumerable.Range(0, 77).Select(index => new Message(
            $"session:{session}|message:{index}",
            $"Session {session + 1}, message {index + 1}\n"
                + string.Concat(Enumerable.Repeat(
                    "Synthetic chat text. This row has a different measured height.\n",
                    index % 7 + 1)))).ToArray()).ToArray();

    private NativeItemsView? _itemsView;

    public override Element Render()
    {
        var (session, setSession) = UseState(0);
        var rows = Histories[session];
        var items = ItemsView(
            rows,
            static row => row.Key,
            (row, _) => ItemContainer(
                TextBlock(row.Text).TextWrapping(TextWrapping.Wrap))
                .WithKey(row.Key)) with
        {
            LayoutKind = ItemsViewLayoutKind.StackLayout,
            SelectionMode = ItemsViewSelectionMode.None,
            IsItemInvokedEnabled = false,
        };

        return Grid(
            [GridSize.Star()],
            [GridSize.Auto, GridSize.Star()],
            Button("Switch session", () =>
            {
                if (_itemsView is not { IsLoaded: true } view)
                    throw new InvalidOperationException("The timeline is not loaded.");

                // Put the old history's tail request in flight, then replace the
                // keyed history through Reactor. No visibility or layout mutation.
                view.StartBringItemIntoView(rows.Length - 1, new BringIntoViewOptions
                {
                    AnimationDesired = false,
                    VerticalAlignmentRatio = 1.0,
                });
                setSession(1 - session);
            }).Grid(row: 0),
            items.Set(view => _itemsView = view)
                .Grid(row: 1));
    }
}

ReactorItemsViewRepro.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
    <Platforms>x64;ARM64</Platforms>
    <RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString().ToLowerInvariant())</RuntimeIdentifier>
    <UseWinUI>true</UseWinUI>
    <WindowsPackageType>None</WindowsPackageType>
    <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.UI.Reactor" Version="0.1.0-preview.15" />
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="2.4.0" />
  </ItemGroup>
</Project>

Observed Reactor transition

In a diagnostic build of the earlier packaged prototype, a read-only
visibility-property observer was registered on the current tail container after
StartBringItemIntoView returned. It reported:

REPRO: bring session=1, index=76
REPRO: target before reconcile: Visible
REPRO: requested session=2
REPRO: target visibility changed to Collapsed

The captured managed stack identified this path, shown in caller-to-callee order:

ReactorHostControl.Render
  Reconciler.BindItemsViewErasedKeyedItems
    Reconciler.ApplyItemsViewErasedKeyedDiffOrFallback
      KeyedListDiff.Apply / ApplyCore / ApplyGeneral
        KeyedListDiff.Bailout
          ReactorListState.Reset
            ObservableCollection.OnCollectionChanged
              [WinUI recycling]
                ElementFactory<T>.RecycleElement
                  TryParkForPool
                    ParkOrphan
                      UIElement.Visibility = Collapsed

The disjoint session keys take the keyed-diff reset path in this reproduction.
The observer neither changed visibility nor caught the failure. It was removed
before the confirmation runs; it is not required or present in the delivered
unpackaged sample.

Relevant Reactor source, pinned to the tested package's repository commit:

The factory comments explain that parking collapsed containers prevents ghost
rows while WinUI keeps recycled elements parented. Simply deleting that collapse
is therefore not proposed as a safe fix.

Native failure and comparison with OpenClaw

The minimal unpackaged sample dump contains 0x80070057 in its stowed
exceptions. Matching Microsoft symbol-server PDBs resolve the original throw to:

ScrollingAnchorRequestedEventArgs::AnchorElement
  ScrollingAnchorRequestedEventArgs.cpp:43
  Microsoft.UI.Xaml.Controls.dll + 0x325E48

ItemsView::OnScrollViewAnchorRequested
  ItemsView.cpp:972
  Microsoft.UI.Xaml.Controls.dll + 0x360B04

ScrollPresenter::EnsureAnchorElementSelection
  ScrollPresenterAnchoring.cpp:367
  Microsoft.UI.Xaml.Controls.dll + 0x3287E0

The minimal unpackaged run also includes:

FlowLayoutAlgorithm::Measure
  -> ViewportManagerWithPlatformFeatures::SuggestedAnchor
    -> ScrollPresenter::AnchorElement
      -> EnsureAnchorElementSelection
        -> ItemsView::OnScrollViewAnchorRequested
          -> ScrollingAnchorRequestedEventArgs::AnchorElement

The first 24 Controls-module frames in the minimal unpackaged dump, compared by module-relative address,
match the original OpenClaw crash's original-throw context. The module PDB
identities also match:

PDB GUID plus age
Microsoft.ui.xaml.pdb 1AA39E9A93038419472E12ABB014892F1
Microsoft.UI.Xaml.Controls.pdb EF5A6F388B15033C45BBC2D6F489BE2B1

An earlier prototype run entered anchor selection through
ScrollPresenter::ArrangeOverride instead. Both reach the same anchor setter;
the minimal unpackaged run matches OpenClaw's measure-path entry. This is not
just a match on the generic top-level 0xC000027B crash code.

Control experiment and validation

Experiment Result
Clean/build minimal unpackaged ARM64 Debug sample Passed, 0 warnings, 0 errors
Minimal unpackaged sample, two fresh launches First button activation crashed in both; native dump confirmed the anchor setter and OpenClaw measure path
Earlier packaged prototype, three fresh launches First click crashed in all three
Temporary visibility observer in the earlier prototype Reactor collapsed the pending tail target, then the process crashed
Earlier prototype with only the native bring request omitted from the button's behavior 20 session switches completed; all 20 selected-session transitions verified through UI Automation; process remained responsive

The earlier prototype's no-bring experiment is evidence that keyed replacement alone was insufficient
to trigger this failure in that run. It is not a general stress-safety claim for
Reactor. There is no no-bring mode or workaround UI in the delivered sample.

The project, screenshots, diagnostic logs, and dumps were generated locally with
synthetic data. No private chat data was used. Full dumps are not needed to run
the reproduction.

Environment

Tested September 14, 2026:

Component Version
OS Windows 11, build 10.0.26200, ARM64
Configuration Unpackaged Reactor application, ARM64 Debug
.NET SDK 10.0.400
Target framework net10.0-windows10.0.26100.0
Microsoft.UI.Reactor 0.1.0-preview.15
Reactor package source commit f17ce2dd12083e7e8709883c7c4ccd04c556810d
Microsoft.WindowsAppSDK 2.4.0
Native Xaml and Controls file version 3.2.3.2608
Native source revision aa188bfdf74608810e8e57228db6dd137f4e74e4
WinApp CLI used for launch/input/crash capture 0.6.3-prerelease.36

The NuGet versions are intentionally pinned to match the investigated OpenClaw
build, not selected as recommendations for new applications. The inspected
Reactor checkout was at d9ee0d9b; its relevant factory and ItemsView binding
files were unchanged from the package commit above.

Final Program.cs SHA-256:
D8ED327C3704B56153D9791C932DD5F60D4BF3290BB8EB538D20AD95F88F31F7.

Scope and requested investigation

This confirms that normal Reactor recycling can expose the already-reported
WinUI invalid-anchor failure without an application explicitly hiding a row.
It does not prove an independent Reactor defect, test a newer source-built
Reactor version, establish behavior on x64/Release, or resolve the separate
sustained-hang report in OpenClaw.

Please investigate how pending index-based bring requests should interact with
Reactor's keyed history replacement and collapsed-container recycling, and add a
real-WinUI regression test for this combination. Coordinate the ownership of the
fix with microsoft/microsoft-ui-xaml#11865 rather than assuming that suppressing a
synchronous managed exception or leaving parked containers visible is sufficient.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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