Skip to content

web_core: DataModel accepts unbounded list indices, amplifying the serialized client data model #2420

Description

@polina-c

web_core's DataModel accepts an arbitrarily large list index, such as /items/999999999. The write itself is cheap, because JavaScript arrays are sparse. The cost appears later, when that array is turned into JSON.

Measured

Against the built web_core DataModel:

set('/items/1000000', 'x')
  heap growth:       ~14 KB
  array length:       1000001
  serialized length:  5,000,002 chars

JSON.stringify expands every hole to null. At index 10⁹ that is roughly 5 GB of JSON from a 14 KB write.

Why it matters

The index does not have to come from the page's own code. It can come from the agent:

  1. The agent creates a surface with sendDataModel: true. That opts the surface into sending its data model along with anything the client sends back, so the agent can see form state.
  2. The agent sends updateDataModel with path: "/items/999999999". It looks ordinary and web_core accepts it. Cost so far: ~14 KB.
  3. Later the user does something that the client reports, for example tapping a Button with an action. Before sending, the client gathers its data model via MessageProcessor.getClientDataModel(), which reads dataModel.get('/') for every surface with sendDataModel enabled.
  4. The transport JSON-encodes that object into the a2uiClientDataModel metadata field of the outgoing message.
  5. The array reports length: 1000000000, so JSON.stringify writes null a billion times. The outgoing message is roughly 5 GB.

So one small, well-formed message from the agent makes the client build an enormous payload on its next turn. The client pays the cost, and the agent controls the index.

Why the two implementations differ

  • Dart lists are dense, so writing index N allocates N+1 slots. It already caps this: maxAutoVivifyIndex = 10000, enforced in DataModel.set at L86 and L125.
  • web_core arrays are sparse, so the same write costs nothing on the heap and DataModel.set has no cap.

Sparse arrays protect the heap, not the payload. Both implementations need a bound; they just need it for different resources.

Proposal

  1. Agree a shared upper bound on auto-vivified list indices. Dart's 10000 is arbitrary and worth choosing deliberately.
  2. Enforce it in web_core's DataModel.set, throwing A2uiDataError as it already does for other invalid segments.
  3. Promote the behaviour to conformance/core/data_model.yaml and drop the package-local tests.
  4. Check the other renderers and SDKs for the same gap.

Step 2 changes what input a shipped renderer accepts, so it needs a maintainer decision rather than being folded into a test change.

Repro

import {DataModel} from './dist/src/v0_9/state/data-model.js';

const m = new DataModel({items: ['a', 'b', 'c']});
m.set('/items/1000000', 'x');
console.log(m.get('/items').length);                 // 1000001
console.log(JSON.stringify(m.get('/items')).length); // 5000002

Context

Surfaced while migrating data-model.test.ts into the shared conformance suite in #2408. The exclusion is currently documented in the suite header and in the web_core test's doc comment; both should be removed once this is resolved.

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

    P2The team intends to work on this in the near future, or at a lower priority.type: bugSomething isn't working

    Type

    No type

    Projects

    • Status
      Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions