Skip to content

Fix Carousel layout crashes from missing null/bounds checks in AdaptiveCarouselRenderer#9375

Open
karan68 wants to merge 1 commit into
microsoft:mainfrom
karan68:fix/carousel-layout-crash-null-checks
Open

Fix Carousel layout crashes from missing null/bounds checks in AdaptiveCarouselRenderer#9375
karan68 wants to merge 1 commit into
microsoft:mainfrom
karan68:fix/carousel-layout-crash-null-checks

Conversation

@karan68
Copy link
Copy Markdown

@karan68 karan68 commented May 12, 2026

Summary

Fixes multiple crash paths in AdaptiveCarouselRenderer that cause STOWED_EXCEPTION (0x88000fa8) during XAML layout pass when rendering Carousel elements.

Affected apps: WidgetBoard (77.6%), lockapp (3.9%)
Affected SDK: AdaptiveCards.Rendering.Uwp 3.2.x, AdaptiveCards.Rendering.WinUI3 2.2.3-beta

Root Cause

AdaptiveCarouselRenderer.cpp has several unguarded code paths that throw during XAML layout/rendering:

Bug 1 — SelectionChanged handler crashes with SelectedIndex = -1

When carouselUI.Items().Append() is called during rendering, the FlipView fires SelectionChanged before any item is selected (SelectedIndex() == -1). The handler calls pipsPager.SelectedPageIndex(-1) which throws InvalidOperationException because PipsPager does not accept negative indices.

Bug 2 — SetFlipViewMaxHeight calls GetAt() without bounds check

flipView.SelectedIndex() can return -1 (no selection). GetAt(-1) throws E_BOUNDS. This function is called from a LayoutUpdated event handler — outside the try-catch in Render() — so the exception propagates unhandled and becomes a stowed exception that crashes the host process.

Bug 3 — SetFlipViewMaxHeight calls .Measure() on unchecked try_as result

try_as<FrameworkElement>() can return nullptr. The code calls .Measure() on the result without a null check → null pointer dereference.

Bug 4 — try_as<FrameworkElement>() chained directly into .LayoutUpdated()

In the render loop, carouselPageUI.try_as<FrameworkElement>().LayoutUpdated(...) chains the method call without checking if try_as returned null.

Bug 5 — InitialPage index used without bounds validation

carousel.InitialPage().GetUInt32() is passed directly to SelectedIndex() without checking it's within Items().Size().

Fix

  • SelectionChanged handler: Early return when SelectedIndex() < 0
  • SetFlipViewMaxHeight: Validate selectIndex >= 0 && < Items().Size() before GetAt(). Null-check try_as result before .Measure().
  • Render loop: Store try_as result and guard .LayoutUpdated() call with null check.
  • InitialPage: Validate index against carouselUI.Items().Size() before setting.

Repro

  1. Build and run source/uwp/winui3/SimpleVisualizer/SimpleVisualizer.sln
  2. Click "Show Adaptive Card" —> loads ClipchampCarousel.json
  3. Before fix: System.InvalidOperationException: 'Operation is not valid due to the current state of the object.' during RenderAdaptiveCard()
  4. After fix: Carousel renders and displays without crash

Files Changed

  • source/uwp/SharedRenderer/lib/AdaptiveCarouselRenderer.cpp —> All 5 fixes
  • source/uwp/winui3/SimpleVisualizer/ClipchampCarousel.json —> Test template (Clipchamp-style Carousel with empty Container)
  • source/uwp/winui3/SimpleVisualizer/MainWindow.xaml.cs —> Use AppContext.BaseDirectory instead of Package.Current.InstalledLocation.Path
image

Fix multiple crash paths in AdaptiveCarouselRenderer that cause
STOWED_EXCEPTION (0x88000fa8) during XAML layout pass when rendering
Carousel elements. Watson bucket: 39c24f3b-446a-5167-cd1a-fbaacb663624
(7,923 hits, primarily WidgetBoard 77.6%).

Fixes:
1. SelectionChanged handler: FlipView fires SelectionChanged with
   SelectedIndex=-1 during Items().Append(). Calling
   pipsPager.SelectedPageIndex(-1) throws InvalidOperationException.
   Added early return when val < 0.

2. SetFlipViewMaxHeight: GetAt(selectIndex) called without validating
   selectIndex bounds. SelectedIndex() can be -1 when no item is
   selected. Added bounds check before GetAt().

3. SetFlipViewMaxHeight: try_as<FrameworkElement>() result used
   without null check before calling .Measure(). Added null guard.

4. Render loop: try_as<FrameworkElement>() chained directly into
   .LayoutUpdated() without null check. Stored result and guarded.

5. InitialPage: GetUInt32() used as SelectedIndex without validating
   against Items().Size(). Added bounds check.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants