/* ========================================================================
* 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.Diagnostics;
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Threading;
namespace Opc.Ua.Fuzzing
{
///
/// The resource a reports.
///
public enum ResourceFindingKind
{
///
/// A decode allocated far more memory than its input can justify.
///
Allocation,
///
/// An input took longer than the time budget of its size, confirmed by re-runs.
///
Time,
///
/// A decoded value exceeded an encoding limit of the message context.
///
Limit
}
///
/// Raised by a fuzz oracle when an input stays within the exception contract but abuses a
/// resource: memory, CPU time, or an encoding limit the decoder should have enforced.
///
/// These are resource-abuse findings. Unlike an
/// they are never tolerated: a decoder must bound what an arbitrary input can cost.
///
///
public sealed class ResourceBudgetException : InvalidOperationException
{
///
/// Creates the exception for a finding of the given kind.
///
public ResourceBudgetException(ResourceFindingKind kind, string message, Exception? innerException = null)
: base(message, innerException)
{
Kind = kind;
}
///
/// Creates the exception with a default message.
///
public ResourceBudgetException()
: base("Resource budget exceeded.")
{
}
///
/// Creates the exception with the supplied message.
///
public ResourceBudgetException(string message)
: base(message)
{
}
///
/// Creates the exception with the supplied message and inner exception.
///
public ResourceBudgetException(string message, Exception innerException)
: base(message, innerException)
{
}
///
/// The resource that was abused.
///
public ResourceFindingKind Kind { get; }
}
///
/// Oracles that turn resource abuse into fuzzing findings.
///
/// Crash and exception oracles alone never flag an input that makes a decoder allocate
/// gigabytes, recurse close to the stack limit or run for seconds, as long as it finally
/// returns or throws the documented exception. These oracles make such inputs fail:
///
///
/// runs a target on a thread with a small stack, so recursion
/// that would come close to the limit of a 1 MB thread overflows and is reported as a crash,
/// and checks the time budget of the input.
/// fails a decode that allocates more than
/// times its input plus .
///
///
public static partial class FuzzOracles
{
///
/// The stack of the thread that runs a fuzz target. A quarter of the 1 MB default of a
/// Windows thread: a decoder that is not guarded against deep nesting overflows it at a
/// depth random mutation can reach, instead of only past 200 levels.
///
public const int WorkerStackSize = 256 * 1024;
///
/// Bytes a decode may allocate per input byte.
///
public const long AllocationFactor = 64;
///
/// Bytes a decode may allocate regardless of its input: readers, buffers, pools and
/// the bounded preallocation of nested arrays.
///
public const long AllocationAllowance = 8L * 1024 * 1024;
///
/// Time budget of an input in milliseconds, before scaling, regardless of its size.
///
public const int TimeBudgetBaseMilliseconds = 1000;
///
/// Time budget in milliseconds per KB of input, before scaling. Generous for linear
/// work; an input that takes longer does quadratic or worse work in its size.
///
public const int TimeBudgetMillisecondsPerKilobyte = 100;
///
/// Re-runs of an input over its time budget. Only the fastest run counts, so JIT,
/// garbage collection and a noisy CI neighbour are not reported.
///
public const int TimeConfirmationRuns = 2;
///
/// Scales the time budget. 0 disables the time oracle.
///
public const string TimeBudgetScaleVariable = "OPCUA_FUZZ_TIME_BUDGET_SCALE";
///
/// Overrides the worker stack in KB. 0 runs targets on the calling thread.
///
public const string WorkerStackVariable = "OPCUA_FUZZ_STACK_KB";
#if NETFRAMEWORK
static FuzzOracles()
{
// The only allocation counter of .NET Framework; it cannot be switched off again.
AppDomain.MonitoringIsEnabled = true;
}
#endif
///
/// Whether the allocation oracle can measure on this runtime.
///
public static bool IsAllocationMeasurementSupported
{
get
{
#if NETFRAMEWORK
return AppDomain.MonitoringIsEnabled;
#else
return true;
#endif
}
}
///
/// The time budget of an input of the given size, 0 when the time oracle is disabled.
///
public static long GetTimeBudgetMilliseconds(long inputLength)
{
double scale = s_timeBudgetScale.Value;
if (scale <= 0)
{
return 0;
}
double budget = TimeBudgetBaseMilliseconds +
(Math.Max(0, inputLength) * (double)TimeBudgetMillisecondsPerKilobyte / 1024);
return (long)Math.Ceiling(budget * scale);
}
///
/// The allocation budget of a decode of the given input size.
///
public static long GetAllocationBudget(long inputLength)
{
return AllocationAllowance + (Math.Max(0, inputLength) * AllocationFactor);
}
///
/// Runs a fuzz target on a thread with a stack and fails
/// with if it is over the time budget of its
/// input in the fastest of additional runs.
/// Exceptions of the target propagate unchanged.
///
///
public static void RunTarget(string target, long inputLength, Action run)
{
RunTarget(target, inputLength, run, GetTimeBudgetMilliseconds(inputLength));
}
///
/// Runs a fuzz target like with an
/// explicit time budget in milliseconds; 0 disables the time oracle.
///
///
public static void RunTarget(string target, long inputLength, Action run, long budget)
{
if (run == null)
{
throw new ArgumentNullException(nameof(run));
}
long fastest = RunOnWorker(run);
if (budget <= 0 || fastest <= budget)
{
return;
}
long first = fastest;
for (int ii = 0; ii < TimeConfirmationRuns && fastest > budget; ii++)
{
fastest = Math.Min(fastest, RunOnWorker(run));
}
if (fastest > budget)
{
throw new ResourceBudgetException(
ResourceFindingKind.Time,
string.Format(
CultureInfo.InvariantCulture,
"Time budget exceeded by target '{0}': {1} ms (first run {2} ms, fastest of {3}) " +
"for {4} input bytes, budget {5} ms.",
target,
fastest,
first,
TimeConfirmationRuns + 1,
inputLength,
budget));
}
}
///
/// Runs a decode and fails with when it
/// allocated more than the budget of its input, whether it returned or threw. A decode
/// that throws over budget reports the finding with the decode exception as the inner
/// exception.
///
///
public static T MeasureAllocation(string operation, long inputLength, Func decode)
{
if (decode == null)
{
throw new ArgumentNullException(nameof(decode));
}
long before = GetAllocatedBytes();
T result;
try
{
result = decode();
}
catch (Exception ex) when (ex is not ResourceBudgetException)
{
CheckAllocation(operation, inputLength, before, ex);
throw;
}
CheckAllocation(operation, inputLength, before, null);
return result;
}
///
/// Runs a decode under .
///
public static void MeasureAllocation(string operation, long inputLength, Action decode)
{
if (decode == null)
{
throw new ArgumentNullException(nameof(decode));
}
_ = MeasureAllocation