Skip to content

Fix #3546: re-enable FuzzCrashAssets and harden BinaryDecoder#3884

Merged
marcschier merged 1 commit into
OPCFoundation:masterfrom
marcschier:fuzzing3546
Jun 15, 2026
Merged

Fix #3546: re-enable FuzzCrashAssets and harden BinaryDecoder#3884
marcschier merged 1 commit into
OPCFoundation:masterfrom
marcschier:fuzzing3546

Conversation

@marcschier

Copy link
Copy Markdown
Collaborator

Description

Two coordinated fixes that close issue #3546 and remove the CI gap that allowed the regression to slip past dotnet test:

  1. BinaryDecoder hardens matrix-dimension handling
    Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs — wraps the matrix-construction switch in ReadVariantValue (all 24 BuiltInType cases) and both ReadEncodeableMatrix<T> overloads in a try/catch that converts the deliberate ArgumentException thrown by MatrixOf<T>(values, dimensions) validation (overflow / negative / length-mismatch) into a ServiceResultException(BadDecodingError). Attacker-controlled wire dimensions now flow through the standard decoder rejection channel instead of escaping as an unhandled exception. The MatrixOf<T> ctor validation itself (security hardening at MatrixOf.cs:178-217) is unchanged.

  2. FuzzCrashAssets is now assertive
    Fuzzing/Common/Fuzz.Tests/FuzzTargetTestsBase.cs — replaces silent log-and-count with Assert.That(failures, Is.Empty, …listing failed assets). CI now breaks loud the moment a residual Assets/crash-* reproduces on any target. .github/agents/fuzz-tester.agent.md doc note rewritten to match.

The originally reported "DataTypeNode / VariableNode IsEqual non-determinism" symptom turned out to already be implicitly resolved by the modern Equals overhaul (ArrayOf<T> value-equality, ExtensionObject.Equals deep-compare, IEquatable<T> on ReferenceNode / RolePermissionType). The 8 new synthetic round-trip tests + 6 permanent corpus seeds confirm this end-to-end.

Regression coverage

  • Tests/Opc.Ua.Types.Tests/Encoders/BinaryDecoderTests.cs — 3 new tests lock in the BadDecodingError contract for malformed matrix dimensions (overflow, negative, length-mismatch).
  • Fuzzing/Opc.Ua.Encoders.Fuzz.Tests/Issue3546RoundTripTests.cs — 8 synthetic round-trip tests for populated DataTypeNode and VariableNode, exercising the implicated fields (non-empty References, RolePermissions, DataTypeDefinition ExtensionObject body, Value Variant carrying ExtensionObject, ArrayDimensions empty / null / non-empty).
  • Fuzzing/Opc.Ua.Encoders.Fuzz.Tools/Encoders.Testcases.cs + Fuzzing/Opc.Ua.Encoders.Fuzz.Corpus/Testcases.{Binary,Json,Xml}/DataTypeNodeMessage / VariableNodeMessage seed generators plus 6 generated corpus files. These feed FuzzGoodTestcases automatically on every test run.

Verification

Suite TFM Result
Opc.Ua.Encoders.Fuzz.Tests net10.0 4625 / 0
Opc.Ua.Encoders.Fuzz.Tests net48 4625 / 0
Opc.Ua.Types.Tests (BinaryDecoder / Matrix / Variant / EncodeableMatrixRoundTrip subset) net10.0 985 / 0

The single pre-existing Assets/crash-0483107a22caf892cff4f795c96ab14c581a8f55 no longer escapes any fuzz target — the matrix-dimensions fix turns it into a clean parser-rejected input.

Related Issues

Checklist

  • I have signed the CLA and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added all necessary documentation.
  • I have verified that my changes do not introduce (new) build or analyzer warnings.
  • I ran the affected tests locally (Opc.Ua.Encoders.Fuzz.Tests on net10.0 + net48, targeted Opc.Ua.Types.Tests subset on net10.0); full UA.slnx still pending CI.
  • I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings.
  • I have addressed all PR feedback received.

…dens matrix dimensions

Issue OPCFoundation#3546 reported that the libfuzz CI pipeline (which feeds a zip of
corpus inputs sequentially) caught a DataTypeNode/VariableNode round-trip
failure that the local NUnit `EncoderTests` runner did not. The local
miss had TWO root causes:

  1. `Fuzz.Tests/FuzzTargetTestsBase.FuzzCrashAssets` silently swallowed
     every exception — counter was incremented but never asserted. So a
     residual `Assets/crash-*` could sit in the tree without breaking
     `dotnet test`.
  2. The single crash asset on disk reproduces an `ArgumentException` from
     `MatrixOf<T>(values, dimensions)` (deliberate security validation
     for overflow / negative / length-mismatch dimensions) escaping
     `BinaryDecoder.ReadVariantValue` and `ReadEncodeableMatrix<T>`. The
     fuzz target only swallows `ServiceResultException(BadDecodingError /
     BadEncodingLimitsExceeded)`, so the unhandled `ArgumentException`
     surfaced as a "crash" in the pipeline.

Both are fixed here.

### Production fix
`Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs`:
  * Wrap the variant-matrix construction switch in `ReadVariantValue`
    (covers all 24 BuiltInType cases) in a try/catch that converts
    `ArgumentException` from `MatrixOf<T>` validation into a
    `ServiceResultException(BadDecodingError)`. Attacker-controlled wire
    dimensions are now rejected through the standard decoder channel.
  * Same treatment for both `ReadEncodeableMatrix<T>` overloads which
    call `ArrayOf<T>.ToMatrix(dimensions)`.

The fix preserves the deliberate `MatrixOf<T>` ctor validation (the
security hardening at `MatrixOf.cs:178-217`); it only routes that
rejection through the same channel as the rest of the decoder.

### Regression coverage
`Tests/Opc.Ua.Types.Tests/Encoders/BinaryDecoderTests.cs`:
  * `ReadMatrixWithOverflowingDimensionsThrowsBadDecodingError`
  * `ReadMatrixWithNegativeDimensionThrowsBadDecodingError`
  * `ReadMatrixWithLengthMismatchThrowsBadDecodingError`

`Fuzzing/Opc.Ua.Encoders.Fuzz.Tests/Issue3546RoundTripTests.cs`:
  * 8 synthetic round-trip tests exercising populated `DataTypeNode` and
    `VariableNode` through `FuzzableCode.FuzzBinaryEncoderIndempotentCore`
    (the exact 3rd-gen IsEqual gate that originally failed in CI). All
    pass, confirming that the modern `Equals` overhaul (`ArrayOf<T>`,
    `ExtensionObject.Equals`, IEquatable on `ReferenceNode` /
    `RolePermissionType`) has implicitly resolved the IsEqual
    non-determinism that OPCFoundation#3546 was filed for.

### Fuzz harness
`Fuzzing/Common/Fuzz.Tests/FuzzTargetTestsBase.cs`:
  * `FuzzCrashAssets` now collects every per-asset failure and finishes
    with `Assert.That(failures, Is.Empty, ...)`. CI breaks loud when a
    residual crash asset reproduces — the exact gap that originally let
    OPCFoundation#3546 slip past `dotnet test`.

`.github/agents/fuzz-tester.agent.md`: rewrote the "does not currently
Assert" note to reflect the new assertive behaviour and link the gap
closure to OPCFoundation#3546.

### Permanent corpus seeds
`Fuzzing/Opc.Ua.Encoders.Fuzz.Tools/Encoders.Testcases.cs`: added
`DataTypeNodeMessage` and `VariableNodeMessage` message-encoder methods
that populate the fields originally implicated (non-empty References,
RolePermissions, DataTypeDefinition with an `ExtensionObject` body for
`DataTypeNode`; Value `Variant` carrying an `ExtensionObject`,
non-empty ArrayDimensions/References/RolePermissions for `VariableNode`).

`Fuzzing/Opc.Ua.Encoders.Fuzz.Corpus/Testcases.{Binary,Json,Xml}/`:
6 new seed files (`datatypenodemessage.{bin,json,xml}` and
`variablenodemessage.{bin,json,xml}`) generated by the tool. These flow
automatically into `FuzzGoodTestcases` so the round-trip is exercised
through every fuzz target on every test run.

### Verification (net10.0 + net48)
* Encoders.Fuzz.Tests net10.0: 4625 / 0 (baseline 4131 + 6 new seeds *
  ~81 targets effectively across the matrix + 8 synthetic + 3 other
  new tests)
* Encoders.Fuzz.Tests net48:   4625 / 0
* Types.Tests (BinaryDecoder/Matrix/Variant/EncodeableMatrixRoundTrip
  subset) net10.0: 985 / 0 (baseline 982 + 3 new regression tests).

The single pre-existing `Assets/crash-0483107a...` no longer escapes any
fuzz target — the BinaryDecoder fix above turns it into a clean
parser-rejected input.
@marcschier marcschier changed the title Fix #3546: assertive FuzzCrashAssets + BinaryDecoder hardens matrix dimensions Fix #3546: re-enable FuzzCrashAssets and harden BinaryDecoder Jun 15, 2026
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.81132% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.75%. Comparing base (03ed5c8) to head (e415266).

Files with missing lines Patch % Lines
Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs 69.81% 15 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (69.81%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #3884   +/-   ##
=======================================
  Coverage   72.75%   72.75%           
=======================================
  Files         913      913           
  Lines      148286   148305   +19     
  Branches    25663    25663           
=======================================
+ Hits       107879   107901   +22     
- Misses      31053    31055    +2     
+ Partials     9354     9349    -5     
Files with missing lines Coverage Δ
Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs 88.81% <69.81%> (-1.07%) ⬇️

... and 19 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@marcschier
marcschier marked this pull request as ready for review June 15, 2026 09:51

@koepalex koepalex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@marcschier
marcschier merged commit 1c312e5 into OPCFoundation:master Jun 15, 2026
192 of 194 checks passed
@marcschier
marcschier deleted the fuzzing3546 branch June 15, 2026 13:55
marcschier added a commit that referenced this pull request Jun 16, 2026
…aryDecoder hardening)

Merged two upstream commits:
- d8a95b4 dotnet format sweep (whitespace + IDE + RCS) (#3879)
- 1c312e5 Fix #3546: re-enable FuzzCrashAssets and harden BinaryDecoder (#3884)

Conflict resolution:
- CapturingMessageSocket.cs + CapturingMessageSocketFactoryTests.cs +
  CapturingMessageSocketTests.cs: master's format sweep modified these
  files that this branch DELETED (the IMessageSocket abstraction was
  replaced with CapturingByteTransport / IUaSCByteTransport). Kept them
  deleted.
- PcapServiceCollectionExtensions.cs: kept HEAD's ITransportBindingRegistry
  + ITransportBindingConfigurator wiring over master's removed
  TransportBindings.Channels static path (2 hunks: xmldoc + the
  AddTransportBindingRegistry/configurator install).
- ClientChannelManager.cs: kept HEAD's DefaultTransportBindingRegistry
  xmldoc over master's stale TransportBindings.Channels reference.
- PcapOptionsDiagnosticsToolsGateTests.cs + PcapServiceCollectionExtensionsTests.cs:
  dropped master's SetUp/TearDown that snapshot/restore the removed
  static TransportBindings.Channels; kept HEAD's per-IServiceProvider
  registry assertions.

Post-merge format fixes (master's sweep promoted RCS rules to error and
this branch's newer files were not part of that sweep):
- HttpsTransportChannel.cs: RCS0009 blank line before the MediaType
  property doc comment.
- SharedKestrelHostTests.cs: RCS1078 use string.Empty instead of empty
  literal.

Verified: dotnet build UA.slnx -c Debug -p:CustomTestTarget=net10.0
clean; 386/386 Opc.Ua.Bindings.Pcap.Tests pass (incl. the
MaxBytesCapStopsAcceptingFramesAfterLimit test after a clean rebuild).
marcschier added a commit to marcschier/UA-.NETStandard that referenced this pull request Jul 9, 2026
Brings in 4 upstream commits: OPCFoundation#3879 (dotnet format sweep — whitespace +
IDE + RCS), OPCFoundation#3884 (re-enable FuzzCrashAssets + harden BinaryDecoder),
OPCFoundation#3846 (port session activation race fix from master378), and OPCFoundation#3745
(split MigrationGuide.md, add What's New in 2.0, rewrite Profiles.md).

No textual merge conflicts. The format sweep (OPCFoundation#3879) tightened the
repo-root Roslynator analyzer configuration, which newly flagged 27
pre-existing UaLens style sites across 14 files (RCS1078 use
string.Empty, RCS0031 enum member on own line, RCS1089 use ++
operator, RCS1006/RCS1061 merge if/else, RCS1031 remove unnecessary
braces, RCS1051 remove parentheses, RCS0061 remove blank line between
switch sections). Auto-fixed via `dotnet format analyzers` (matching
how upstream applied the sweep) plus one manual RCS0061 fix in
HistorianPlugin that surfaced after the brace removal.

UaLens build clean: 0 errors. The 16 remaining warnings are upstream
NuGet TFM notices (Microsoft.Extensions.* 10.6.0 not supporting
net48/net472 via Opc.Ua.Gds.Client.Common), unrelated to UaLens.

Co-authored-by: Copilot <[email protected]>
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.

Follow up task: DataTypeNode and VariableNode Fuzztest failure

3 participants