Skip to content

fix: add element count limit per collection to prevent resource exhau…#9373

Open
karan68 wants to merge 1 commit intomicrosoft:mainfrom
karan68:fix/element-collection-count-limit
Open

fix: add element count limit per collection to prevent resource exhau…#9373
karan68 wants to merge 1 commit intomicrosoft:mainfrom
karan68:fix/element-collection-count-limit

Conversation

@karan68
Copy link
Copy Markdown

@karan68 karan68 commented May 8, 2026

Summary
Adds a maximum element count limit (200) per collection in the shared C++ object model parser. Collections exceeding the limit are truncated with a parse warning.

Problem
GetElementCollection, GetElementCollectionOfSingleType, and GetActionCollection in ParseUtil.h/ParseUtil.cpp loop over every item in a JSON array with no upper bound. Additionally, elements.reserve(elementArray.size()) allocates memory for the full array size upfront.
A card with 100,000 TextBlocks in the body creates 100,000 parsed C++ objects. When rendered, each becomes a XAML control — exhausting memory and freezing the UI thread.

Verified locally: A standalone C++ binary linked against the ObjectModel library confirmed:

+--------------------------+-------------+-----------------------------------+----------------------------------+
| Collection               | Input count | Before fix                        | After fix                        |
+--------------------------+-------------+-----------------------------------+----------------------------------+
| Body (TextBlock)         | 100,000     | 100,000 parsed (853ms)            | 200 parsed (3ms)                 |
| Actions (OpenUrl)        | 100,000     | 100,000 parsed (952ms)            | 200 parsed (4ms)                 |
| Body (TextBlock)         | 10          | 10 parsed                         | 10 parsed (unchanged)            |
| Body (TextBlock)         | 200         | 200 parsed                        | 200 parsed (at limit, unchanged) |
+--------------------------+-------------+-----------------------------------+----------------------------------+

This affects all renderers using the shared C++ model (UWP, WinUI3, Android, iOS). The parser currently provides no API for hosts to set a collection size limit —> even hosts that want to cap elements have no way to do so.

Changes
4 files changed, 120 insertions, 3 deletions:

+-----------------------------------------------------------+----------------------------------------------------------------------------------------------+
| File                                                      | Change                                                                                       |
+-----------------------------------------------------------+----------------------------------------------------------------------------------------------+
| ParseContext.h                                            | Added c_maxElementsPerCollection = 200 as a static constexpr                                 |
| ParseUtil.h                                               | Added limit check + warning in GetElementCollection and GetElementCollectionOfSingleType;    |
|                                                           | capped reserve()                                                                             |
| ParseUtil.cpp                                             | Added limit check + warning in GetActionCollection; capped reserve()                         |
| AdaptiveCardsSharedModelUnitTest/ObjectModelTest.cpp      | Added 4 test methods: within-limit, at-limit, body-exceeds-limit, actions-exceed-limit      |
+-----------------------------------------------------------+----------------------------------------------------------------------------------------------+

How it works
All three collection parsing functions now check before each item:

const size_t maxElements = static_cast<size_t>(ParseContext::c_maxElementsPerCollection);
elements.reserve(std::min(elemSize, maxElements));  // cap upfront allocation too

for (auto& curJsonValue : elementArray)
{
    if (elements.size() >= maxElements)
    {
        context.warnings.emplace_back(std::make_shared<AdaptiveCardParseWarning>(
            WarningStatusCode::CustomWarning,
            "Maximum number of elements in a collection exceeded; remaining items were dropped"));
        break;
    }
    // ... parse item
}

Scope
Covers all 19 call sites through the 3 guarded functions:

  • Card body, actions, Container items, Column items, ColumnSet columns
  • Carousel pages, ChoiceSet choices, FactSet facts, ImageSet images
  • Table rows, TableRow cells, RichTextBlock inlines
  • Media sources, caption sources, auth buttons, toggle visibility targets
  • ActionSet actions

Behavior

┌────────────────────────────────────┬───────────────────────────────────────────┐
│ Scenario                           │ Result                                    │
├────────────────────────────────────┼───────────────────────────────────────────┤
│ Card with 10 body elements         │ Parses all 10 (unchanged)                 │
│ Card with 200 body elements        │ Parses all 200 (at limit, unchanged)      │
│ Card with 250 body elements        │ Parses 200 + emits warning                │
│ Card with 100,000 body elements    │ Parses 200 + emits warning                │
│ Card with 250 actions              │ Parses 200 + emits warning                │
└────────────────────────────────────┴───────────────────────────────────────────┘

No breaking changes. Real-world Adaptive Cards rarely have more than 20-30 elements in a single collection. The limit of 200 is very generous.

Testing

4 unit tests added to ObjectModelTest.cpp:

  • ElementCollection_WithinLimit_ParsesFully — 10 elements, all parsed
  • ElementCollection_AtLimit_ParsesFully — 200 elements, all parsed
  • ElementCollection_ExceedsLimit_CappedWithWarning — 250 elements, capped at 200 + warning
  • ActionCollection_ExceedsLimit_CappedWithWarning — 250 actions, capped at 200 + warning
  • Standalone repro binary verified body and action capping across 10 to 100,000 items.

Context
This is a defense-in-depth hardening for scenarios where AdaptiveCards renders untrusted content (e.g., Windows Widgets accepting 3rd-party card payloads). The parser was originally designed for trusted-source cards, but the threat model has expanded. There is currently no existing API or config for hosts to limit collection sizes.

…stion

GetElementCollection, GetElementCollectionOfSingleType, and
GetActionCollection loop all items in a JSON array with no cap.
A card with 100,000 elements creates 100,000 parsed objects,
leading to memory exhaustion and UI thread freeze when rendered.

Adds c_maxElementsPerCollection (200) to ParseContext. Collections
exceeding the limit are truncated with a parse warning. The
reserve() call is also capped to prevent upfront over-allocation.

Covers all 19 call sites: body, actions, columns, choices,
facts, images, table rows/cells, carousel pages, inlines, etc.
@karan68 karan68 requested review from a team, SatwikKrSharma and dipeshmsft May 8, 2026 15:18
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