Files
romanett 594eb7a245 [master378] Bound nested array preallocation, XML depth and inbound PubSub metadata (#4603)
Minimal backports to the 1.5.378 line of three findings from the codec
audit behind #4599 / #4602 (B1-1, X1-1, P1-1). All three can be reached
from untrusted input. The changes are kept small, and the performance of
legitimate messages is unchanged.

## Changes

**B1-1: nested array preallocation (`BinaryDecoder`)**
Variant, DataValue, ExtensionObject and encodeable arrays were allocated
at the full length prefix before any element was read. Arrays nested in
the first element of their parent all see the same remaining message, so
a chain of them multiplied the allocation: about 1 KB of input in
`RequestHeader.AdditionalHeader` preallocated about 200 MB before
authentication.
- These arrays now use the #4602 budget. An array is allocated at full
length only while all such arrays still being read take at most 8 bytes
per remaining message byte. Otherwise it starts at 16 KB and grows as
elements are read.
- The reservation is released in a `finally`.
- Leaf arrays (numbers, strings, NodeIds, …) are unchanged and stay
bounded by MaxArrayLength.

**X1-1: XML element depth (`XmlDecoder`, `BinaryDecoder`,
`JsonDecoder`)**
The depth of XmlElement values and XML ExtensionObject bodies was
unbounded, and the `InnerXml` setter and `CloneNode(true)` recurse once
per level. A known-type XML body of about 200 KB inside a binary message
terminated the process with a stack overflow.
- Before any network XML is loaded into a DOM, it is now checked with a
streaming reader. XML nested deeper than `MaxEncodingNestingLevels`
(default 200, the same limit master uses) is rejected with
`BadEncodingLimitsExceeded`.
- Master's node-by-node copier is not ported.

**P1-1: inbound DataSetMetaData (`UaPubSubConnection`)**
Received metadata is not authenticated, yet it replaced the metadata of
every reader with a matching DataSetWriterId, whichever publisher sent
it.
- It now only updates readers whose PublisherId filter accepts the
sender, using the same rule as data messages. A reader without a
PublisherId still accepts any publisher.
- Master's registry cap is not needed, because 378 has no registry.

## Verification (net10.0 unless noted)

| Check | Result |
|---|---|
| Nested Variant-array chain, 995 bytes | 199.2 MB → 3.3 MB allocated |
| Known-type XML body 30 000 levels deep inside binary | process crash →
`BadEncodingLimitsExceeded` |
| `Opc.Ua.Types.Tests` (incl. new `DecoderNestingLimitTests`) | 327 /
327; new tests also 9 / 9 on net48 |
| `Opc.Ua.Encoders.Fuzz.Tests` (crash/timeout/slow corpus) | 150 / 150 |
| `Opc.Ua.Core.Tests` (Encoders + Types) | 6 327 passed, 0 failed |
| `Opc.Ua.PubSub.Tests` (without broker-dependent MQTT/UDP) + new
`MetaDataPublisherFilterTests` | 617 + 4 passed, 13 skipped |
| Types / Core / PubSub build, all TFMs | 0 warnings |

**Performance vs. db39629d2:**
- Test cases: decoding `Variant[100000]`, `Variant(Variant[])`,
`DataValue[10000]`, `ReadValueId[20000]` and `ExtensionObject[20000]`.
- Allocations are byte-identical to the baseline.
- Median times are within run-to-run noise over two interleaved rounds.
- The only new cost is one extra streaming parse for XmlElement values.

## Notes for review
- If a deployment raises `MaxEncodingNestingLevels` well above the
default, the allowed XML depth rises with it. Master behaves the same
way.
- No public API changes. The XML depth helper is internal to
`Opc.Ua.Types`, and `JsonDecoder` in `Opc.Ua.Core` has a private copy.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
2026-10-01 16:01:22 +03:00
..