mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
## Why The encoder/decoder fuzz targets only flagged crashes, unhandled exceptions and encoding-fidelity mismatches. They could not observe a decode that stays within the exception contract but consumes far more memory, CPU or stack than its input justifies, or that materialises values larger than the context's encoding limits. None of the resource-related findings of the codec audit would have been caught. ## What Adds four shared oracles to the fuzz harness (`fuzzing/Common/Fuzz/FuzzOracles*.cs`), applied by every host (libFuzzer, AFL, `--replay`, Tools playback and the NUnit replay): - **Allocation** — wraps the Binary/JSON/XML decode cores, the built-in-type targets, the PubSub UADP/JSON decoders and the UADP reassembler; fails when a decode allocates more than `64x` its input plus `8 MB`, whether it returns or throws. Uses the per-thread allocation counter (the AppDomain counter on .NET Framework). - **Time** — a per-input budget of `1 s + 100 ms/KB`, re-run twice and reported only if the fastest run is still over, so JIT/GC/CI noise does not trip it (`OPCUA_FUZZ_TIME_BUDGET_SCALE`, `0` disables). - **Stack** — every input runs on a worker thread with a `256 KB` stack, so recursion that comes close to the limit of a normal thread is caught in-process as a crash rather than near 200 levels. `StackTestcases` inputs replay through every target in a child process (`--fuzz-replay-all`), since such a crash cannot be caught. - **Decoded limits** — walks a decoded value for strings, identifiers, ByteStrings and arrays beyond the context's `MaxStringLength`/`MaxByteStringLength`/`MaxArrayLength`; matrices are additionally boxed the legacy way so a value no consumer can materialise is reported. The fuzz context uses `4096/8192/4096` so small inputs reach the limits. Supporting changes: - `ArrayOf<T>`/`MatrixOf<T>` are not `IEnumerable`, so the limit walker unpacks them via `ToArray`/`ToArrayOf` (otherwise it silently traversed nothing). - `Decimal` is registered in the fuzz context so the targets exercise `Decimal.Decode`. - **B1-2 bug fix**: `BinaryDecoder.ReadExtensionObject` dereferenced a null element for an XML body that is not an element; now rejected with `BadDecodingError`. Found by replaying the nightly crash corpus under the new exception contract; 5 corpus inputs are added as gating reproducers. - Reproducers for B1-1, T1-3, X1-1 and U1-3, built in process by documented builders (`ResourceReproducerTests`, `PubSubReproducerTests`) and checked in to the corpus, where every target replays them under the oracles. Each was verified by reverting the matching fix locally and confirming the guard fires (e.g. B1-1 then allocates ~21 MB from 481 input bytes). - `Fuzzing.md` / `CrashCorpus.md` document the oracles and the private-input log policy. ## Testing Green on net10.0 and net48: | Suite | net10.0 | net48 | |---|---|---| | `Opc.Ua.Encoders.Fuzz.Tests` | 8130 | 8130 | | `Opc.Ua.PubSub.Fuzz.Tests` | 149 | n/a (modern-only) | | `Opc.Ua.Fuzzing.Tests` | 154 | 154 | | `Opc.Ua.Types.Tests` (encoders) | 3698 | builds | Also verified: `Network`/`Certificates` fuzz test suites, all fuzz hosts and Tools projects build, the fuzz manifest validator passes, `dotnet format whitespace` is clean, and the full ~22k-input nightly crash corpus replays with no oracle false positives. Stacked on `romanett/encoder-decoder-spec-security-83c9f5`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
5.8 KiB
C#
133 lines
5.8 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Buffers.Binary;
|
|
using System.IO;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.PubSub.Diagnostics;
|
|
using Opc.Ua.PubSub.Encoding;
|
|
using Opc.Ua.PubSub.Encoding.Uadp;
|
|
|
|
namespace Opc.Ua.Fuzzing
|
|
{
|
|
/// <summary>
|
|
/// Curated reproducers under Assets/Repo, replayed with the resource oracles.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Fuzzing")]
|
|
public sealed class PubSubReproducerTests
|
|
{
|
|
/// <summary>
|
|
/// U1-1: a UADP discovery request whose array count claims about 2^31 elements
|
|
/// in a 9 byte frame made the decoder allocate gigabytes before reading them.
|
|
/// </summary>
|
|
private const string kDiscoveryCountReproducer = "crash-a37cbc149037fca2713b51983af60846703fce99";
|
|
|
|
[Test]
|
|
public void DiscoveryRequestCountReproducerIsSoftRejectedWithinTheAllocationBudget()
|
|
{
|
|
byte[] input = LoadReproducer(kDiscoveryCountReproducer);
|
|
PubSubNetworkMessageContext context = FuzzableCode.NewContext();
|
|
|
|
// DecodeUadp runs the decode under the allocation oracle, so a decoder that
|
|
// sizes the array from the count fails here with a ResourceBudgetException.
|
|
PubSubNetworkMessage decoded = FuzzableCode.DecodeUadp(input, context);
|
|
|
|
Assert.That(decoded, Is.Null);
|
|
Assert.That(
|
|
context.Diagnostics.Read(PubSubDiagnosticsCounterKind.ReceivedInvalidNetworkMessages),
|
|
Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void DiscoveryRequestCountReproducerPassesEveryTarget()
|
|
{
|
|
byte[] input = LoadReproducer(kDiscoveryCountReproducer);
|
|
|
|
Assert.DoesNotThrow(() => FuzzableCode.LibfuzzUadpNetworkMessageDecode(input));
|
|
Assert.DoesNotThrow(() => FuzzableCode.LibfuzzUadpChunkReassembly(input));
|
|
Assert.DoesNotThrow(() => FuzzableCode.LibfuzzPubSubJsonDecode(input));
|
|
}
|
|
|
|
/// <summary>
|
|
/// U1-3: a UADP chunk header advertising a huge TotalSize made the reassembler
|
|
/// preallocate that many bytes. The fix bounds TotalSize by MaxReassembledMessageSize
|
|
/// and charges memory per received chunk, so the chunk is rejected before allocation.
|
|
/// The reassembler probe runs under the allocation oracle in ExerciseUadpChunks.
|
|
/// </summary>
|
|
[Test]
|
|
public void OversizedChunkTotalSizeIsRejectedBeforeAllocation()
|
|
{
|
|
byte[] chunk = PubSubReproducerBuilders.BuildOversizedTotalSizeChunk();
|
|
PubSubNetworkMessageContext context = FuzzableCode.NewContext();
|
|
|
|
using (var reassembler = new UadpReassembler(context.TimeProvider))
|
|
{
|
|
bool completed = reassembler.TryAddChunk(
|
|
FuzzableCode.SeedPublisherId, 1, chunk, out ReadOnlyMemory<byte>? reassembled);
|
|
|
|
Assert.That(completed, Is.False);
|
|
Assert.That(reassembled, Is.Null);
|
|
Assert.That(reassembler.PendingCount, Is.Zero);
|
|
}
|
|
|
|
Assert.DoesNotThrow(() => FuzzableCode.LibfuzzUadpChunkReassembly(chunk));
|
|
}
|
|
|
|
private static byte[] LoadReproducer(string name)
|
|
{
|
|
return File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "Assets", "Repo", name));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builders for the PubSub resource reproducers, documenting the wire shape they produce.
|
|
/// </summary>
|
|
internal static class PubSubReproducerBuilders
|
|
{
|
|
/// <summary>
|
|
/// A single UADP chunk (Part 14 Table 159) whose TotalSize advertises about 1 GiB while
|
|
/// the chunk carries one payload byte.
|
|
/// </summary>
|
|
public static byte[] BuildOversizedTotalSizeChunk()
|
|
{
|
|
const uint totalSize = 0x40000000; // 1 GiB.
|
|
byte[] payload = [0x2A];
|
|
byte[] frame = new byte[UadpChunker.ChunkHeaderSize + payload.Length];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(frame.AsSpan(0), 1); // MessageSequenceNumber
|
|
BinaryPrimitives.WriteUInt32LittleEndian(frame.AsSpan(2), 0); // ChunkOffset
|
|
BinaryPrimitives.WriteUInt32LittleEndian(frame.AsSpan(6), totalSize);
|
|
BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(10), payload.Length); // ChunkData length
|
|
payload.CopyTo(frame.AsSpan(UadpChunker.ChunkHeaderSize));
|
|
return frame;
|
|
}
|
|
}
|
|
}
|