diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f2098de..3c3a3407 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,11 @@ under [1.0.0](#100---2026-09-20) before upgrading.
### Fixed
+- Bound protected-operation catalogue JSON nesting to 64 containers and preserve exact diagnostic locations for
+ CR-only files and Unicode line separators. Generated parser, numeric/string conversion and native operation tests
+ exercise the relevant input contracts.
+- Make the Native AOT validation tool refuse out-of-range PE export addresses as malformed data instead of throwing
+ an arithmetic overflow; generated byte-input tests cover the reader without loading the images.
- Keep Engine API spec diagnostics within the source line when a contract field has an empty value.
- Lua operation admission rejects a host-provided state with a negative stack top and checks that runtime stamp
verification preserves that top. Refusal releases the operation admission without resetting or retrying the state.
diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/Catalog/ProtectedOperationCatalogParser.cs b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/Catalog/ProtectedOperationCatalogParser.cs
index 1a4f4cf0..07b41654 100644
--- a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/Catalog/ProtectedOperationCatalogParser.cs
+++ b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/Catalog/ProtectedOperationCatalogParser.cs
@@ -13,6 +13,7 @@ internal static class ProtectedOperationCatalogParser
{
private const string FileName = "protected-operations.json";
private const string ExpectedCatalogId = "cheatengine-sdk-lua-protected-operations";
+ private const int MaximumJsonDepth = 64;
public static bool IsCatalogFile(string path)
{
@@ -530,6 +531,7 @@ private sealed class JsonReader
private readonly string _path;
private readonly string _text;
private int _position;
+ private int _depth;
public JsonReader(string path, string text)
{
@@ -576,8 +578,7 @@ private bool TryParseValue(out JsonValue value, out CatalogDiagnostic? diagnosti
return _text[_position] switch
{
- '{' => TryParseObject(out value, out diagnostic),
- '[' => TryParseArray(out value, out diagnostic),
+ '{' or '[' => TryParseContainer(out value, out diagnostic),
'"' => TryParseStringValue(out value, out diagnostic),
't' => TryParseLiteral("true", true, out value, out diagnostic),
'f' => TryParseLiteral("false", false, out value, out diagnostic),
@@ -588,6 +589,21 @@ private bool TryParseValue(out JsonValue value, out CatalogDiagnostic? diagnosti
};
}
+ private bool TryParseContainer(out JsonValue value, out CatalogDiagnostic? diagnostic)
+ {
+ if (_depth >= MaximumJsonDepth)
+ {
+ return Fail(out value, out diagnostic, "JSON nesting exceeds the maximum depth of 64.");
+ }
+
+ _depth++;
+ bool success = _text[_position] == '{'
+ ? TryParseObject(out value, out diagnostic)
+ : TryParseArray(out value, out diagnostic);
+ _depth--;
+ return success;
+ }
+
private bool TryParseObject(out JsonValue value, out CatalogDiagnostic? diagnostic)
{
int start = _position++;
@@ -975,12 +991,14 @@ private LinePosition GetLinePosition(int position)
int character = 0;
for (int i = 0; i < position; i++)
{
- if (_text[i] == '\n')
+ char characterAtPosition = _text[i];
+ if (characterAtPosition is '\n' or '\u0085' or '\u2028' or '\u2029' ||
+ (characterAtPosition == '\r' && (i + 1 == _text.Length || _text[i + 1] != '\n')))
{
line++;
character = 0;
}
- else if (_text[i] != '\r')
+ else
{
character++;
}
diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/README.md b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/README.md
index 6b4f7cc4..b30d25db 100644
--- a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/README.md
+++ b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/README.md
@@ -25,6 +25,10 @@ variables, the clock, current culture or the network. Invalid JSON, duplicate op
or an incompatible bridge declaration gives a localized `CESDK4001` error on the additional file and emits nothing.
Two matching catalogue files give `CESDK4002` on both files and emit nothing.
+JSON may contain at most 64 nested object/array containers. Deeper input receives a localized `CESDK4001` error
+before further recursion. Diagnostic locations follow Roslyn's source-text line boundaries, including CR-only files
+and Unicode line separators within strings.
+
## Testing
```powershell
diff --git a/tests/CheatEngine.SDK.Lua.FailureProbe/Program.cs b/tests/CheatEngine.SDK.Lua.FailureProbe/Program.cs
index 6fad5749..73e79ad4 100644
--- a/tests/CheatEngine.SDK.Lua.FailureProbe/Program.cs
+++ b/tests/CheatEngine.SDK.Lua.FailureProbe/Program.cs
@@ -17,6 +17,8 @@ internal static unsafe class Program
private const string GeneratedFunctionAllocationMode = "--generated-function-allocation";
+ private const string FuzzProtectedOperationsMode = "--fuzz-protected-operations";
+
private const int CheckStackGrowthSlots = 4096;
// Exact C11 CHEATENGINE_SDK_NO_ERROR sentinel. The internal production alias is LuaProtectedApi.NoErrorStatus.
@@ -39,7 +41,7 @@ public static int Main(string[] arguments)
if (!HasValidArguments(arguments))
{
return Fail(
- "expected the Lua DLL path, optionally followed by --checkstack-growth or --generated-function-allocation");
+ "expected the Lua DLL path, optionally followed by --checkstack-growth, --generated-function-allocation, or --fuzz-protected-operations");
}
nint module = 0;
@@ -97,7 +99,8 @@ private static bool HasValidArguments(string[] arguments)
return arguments.Length is >= 1 and <= 2 &&
(arguments.Length != 2 ||
string.Equals(arguments[1], CheckStackGrowthMode, StringComparison.Ordinal) ||
- string.Equals(arguments[1], GeneratedFunctionAllocationMode, StringComparison.Ordinal));
+ string.Equals(arguments[1], GeneratedFunctionAllocationMode, StringComparison.Ordinal) ||
+ string.Equals(arguments[1], FuzzProtectedOperationsMode, StringComparison.Ordinal));
}
private static int RunRequestedProbe(LuaState state, nint module, string[] arguments)
@@ -106,7 +109,9 @@ private static int RunRequestedProbe(LuaState state, nint module, string[] argum
{
return string.Equals(arguments[1], CheckStackGrowthMode, StringComparison.Ordinal)
? RunCheckStackGrowthProbe(state)
- : RunGeneratedFunctionAllocationProbe(state);
+ : string.Equals(arguments[1], GeneratedFunctionAllocationMode, StringComparison.Ordinal)
+ ? RunGeneratedFunctionAllocationProbe(state)
+ : ProtectedOperationFuzzProbe.Run(state);
}
return RunProbe(state, module);
@@ -1093,7 +1098,7 @@ private static void PopulateProtectedExports(nint module, nint* exports)
exports[19] = NativeLibrary.GetExport(module, "luaL_unref");
}
- private static LuaStatus CaptureMemoryException(Action operation)
+ internal static LuaStatus CaptureMemoryException(Action operation)
{
Volatile.Write(ref s_rejectAllocations, 1);
try
diff --git a/tests/CheatEngine.SDK.Lua.FailureProbe/ProtectedOperationFuzzProbe.cs b/tests/CheatEngine.SDK.Lua.FailureProbe/ProtectedOperationFuzzProbe.cs
new file mode 100644
index 00000000..9d8288c3
--- /dev/null
+++ b/tests/CheatEngine.SDK.Lua.FailureProbe/ProtectedOperationFuzzProbe.cs
@@ -0,0 +1,245 @@
+using System.Globalization;
+
+using CheatEngine.SDK.Lua.Calls;
+using CheatEngine.SDK.Lua.State;
+
+namespace CheatEngine.SDK.Lua.FailureProbe;
+
+/// Deterministic, process-isolated sequences for protected Lua operations and their recovery contracts.
+internal static class ProtectedOperationFuzzProbe
+{
+ private const uint Seed = 0xD1CE_BA5E;
+ private const int SequenceCount = 2000;
+ private const int MaximumBufferLength = 1024;
+ private const int MaximumCapacity = 64;
+ private const int OperationsPerSequence = 10;
+ private const long Sentinel = 0x51A7_E11A_600D_0001;
+
+ public static int Run(LuaState state)
+ {
+ RandomSource random = new(Seed);
+ int observedApiCalls = 0;
+ for (int iteration = 0; iteration < SequenceCount; iteration++)
+ {
+ int stringLength = BoundaryOrRandom(ref random, iteration, MaximumBufferLength);
+ int tableLength = BoundaryOrRandom(ref random, iteration, MaximumCapacity);
+ int capacity = BoundaryOrRandom(ref random, iteration, MaximumCapacity);
+ nuint userdataSize = (nuint) BoundaryOrRandom(ref random, iteration, MaximumBufferLength);
+ byte[] stringBytes = random.NextBytes(stringLength);
+ byte[] tableBytes = random.NextBytes(tableLength);
+ long key = random.NextInt32(MaximumCapacity) + 1;
+ long value = random.NextInt64();
+
+ if (AssertEmpty(state, iteration, "sequence-start") != 0 ||
+ PushBytes(state, stringBytes, iteration, ref observedApiCalls) != 0 ||
+ CreateTable(state, capacity, iteration, ref observedApiCalls) != 0 ||
+ PushByteTable(state, tableBytes, iteration, ref observedApiCalls) != 0 ||
+ CreateUserdata(state, userdataSize, iteration, ref observedApiCalls) != 0 ||
+ MutateTable(state, key, value, iteration, ref observedApiCalls) != 0 ||
+ RejectInvalidTable(state, key, iteration, ref observedApiCalls) != 0 ||
+ RejectAllocation(state, capacity, iteration, ref observedApiCalls) != 0 ||
+ Recover(state, iteration, ref observedApiCalls) != 0 ||
+ AssertEmpty(state, iteration, "sequence-end") != 0)
+ {
+ return 1;
+ }
+ }
+
+ if (observedApiCalls != SequenceCount * OperationsPerSequence)
+ {
+ return Fail(SequenceCount, "call-budget", "did not execute every protected API partition");
+ }
+
+ Console.WriteLine(string.Create(CultureInfo.InvariantCulture,
+ $"PASS protected fuzz seed=0x{Seed:X8} sequences={SequenceCount} protected-api-calls={observedApiCalls} buffers<={MaximumBufferLength} capacities<={MaximumCapacity} allocator-refusals={SequenceCount}"));
+ return 0;
+ }
+
+ private static int BoundaryOrRandom(ref RandomSource random, int iteration, int maximum)
+ {
+ return (iteration % 3) switch
+ {
+ 0 => 0,
+ 1 => maximum,
+ _ => random.NextInt32(maximum + 1)
+ };
+ }
+
+ private static int PushBytes(LuaState state, byte[] bytes, int iteration, ref int observedApiCalls)
+ {
+ LuaStatus status = state.TryPushString(bytes);
+ observedApiCalls++;
+ if (!status.IsOk || state.Top != 1 || !state.TryReadUtf8(1, out ReadOnlySpan actual) ||
+ !actual.SequenceEqual(bytes))
+ {
+ return Fail(iteration, "TryPushString", "did not leave the exact successful string as its only stack value");
+ }
+
+ state.Pop(1);
+ return AssertEmpty(state, iteration, "TryPushString-cleanup");
+ }
+
+ private static int CreateTable(LuaState state, int capacity, int iteration, ref int observedApiCalls)
+ {
+ state.CreateTable(capacity, capacity);
+ observedApiCalls++;
+ if (state.Top != 1 || !state.IsTable(1))
+ {
+ return Fail(iteration, "CreateTable", "did not leave one table on the stack");
+ }
+
+ state.Pop(1);
+ return AssertEmpty(state, iteration, "CreateTable-cleanup");
+ }
+
+ private static int PushByteTable(LuaState state, byte[] bytes, int iteration, ref int observedApiCalls)
+ {
+ state.PushByteTable(bytes);
+ observedApiCalls++;
+ if (state.Top != 1 || !state.IsTable(1) || state.RawLength(1) != (nuint) bytes.Length)
+ {
+ return Fail(iteration, "PushByteTable", "did not leave a table with the requested sequence length");
+ }
+
+ for (int i = 0; i < bytes.Length; i++)
+ {
+ if (state.RawGetIndex(1, i + 1) != LuaType.Number || !state.TryReadInteger(-1, out long value) ||
+ value != bytes[i])
+ {
+ return Fail(iteration, "PushByteTable", "table entry did not preserve its source byte");
+ }
+
+ state.Pop(1);
+ }
+
+ state.Pop(1);
+ return AssertEmpty(state, iteration, "PushByteTable-cleanup");
+ }
+
+ private static int CreateUserdata(LuaState state, nuint size, int iteration, ref int observedApiCalls)
+ {
+ nint block = state.NewUserdata(size);
+ observedApiCalls++;
+ if (block == 0 || state.Top != 1 || !state.IsUserdata(1) || state.RawLength(1) != size)
+ {
+ return Fail(iteration, "NewUserdata", "did not leave one correctly sized userdata block");
+ }
+
+ state.Pop(1);
+ return AssertEmpty(state, iteration, "NewUserdata-cleanup");
+ }
+
+ private static int MutateTable(LuaState state, long key, long value, int iteration, ref int observedApiCalls)
+ {
+ state.CreateTable(0, 1);
+ observedApiCalls++;
+ state.PushInteger(value);
+ LuaStatus setStatus = state.TrySetIndex(1, key);
+ observedApiCalls++;
+ if (!setStatus.IsOk || state.Top != 1 || !state.IsTable(1))
+ {
+ return Fail(iteration, "TrySetIndex", "did not consume the value and preserve the table");
+ }
+
+ LuaStatus getStatus = state.TryGetIndex(1, key);
+ observedApiCalls++;
+ if (!getStatus.IsOk || state.Top != 2 || !state.TryReadInteger(2, out long actual) || actual != value)
+ {
+ return Fail(iteration, "TryGetIndex", "did not return the value written through the protected setter");
+ }
+
+ state.Pop(2);
+ return AssertEmpty(state, iteration, "table-mutation-cleanup");
+ }
+
+ private static int RejectInvalidTable(LuaState state, long key, int iteration, ref int observedApiCalls)
+ {
+ state.PushInteger(Sentinel);
+ LuaStatus status = state.TryGetIndex(1, key);
+ observedApiCalls++;
+ if (status != LuaStatus.RuntimeError || state.Top != 2 || !state.TryReadInteger(1, out long sentinel) ||
+ sentinel != Sentinel || !state.TryReadString(2, out string? error) || string.IsNullOrWhiteSpace(error))
+ {
+ return Fail(iteration, "TryGetIndex-invalid-table", "did not return one runtime error above the unchanged sentinel");
+ }
+
+ state.Pop(2);
+ return AssertEmpty(state, iteration, "invalid-table-cleanup");
+ }
+
+ private static int RejectAllocation(LuaState state, int capacity, int iteration, ref int observedApiCalls)
+ {
+ state.PushInteger(Sentinel);
+ LuaStatus status = Program.CaptureMemoryException(() => state.CreateTable(capacity, capacity));
+ observedApiCalls++;
+ if (status != LuaStatus.MemoryError || state.Top != 1 || !state.TryReadInteger(1, out long sentinel) ||
+ sentinel != Sentinel)
+ {
+ return Fail(iteration, "CreateTable-allocator-refusal", "did not return memory error while restoring the sentinel");
+ }
+
+ state.Pop(1);
+ return AssertEmpty(state, iteration, "allocator-refusal-cleanup");
+ }
+
+ private static int Recover(LuaState state, int iteration, ref int observedApiCalls)
+ {
+ LuaStatus status = state.TryExecute("return 6 * 7"u8, 1, "=protected-fuzz-recovery"u8);
+ observedApiCalls++;
+ if (!status.IsOk || state.Top != 1 || !state.TryReadInteger(1, out long value) || value != 42)
+ {
+ return Fail(iteration, "recovery", "the state did not execute the fixed protected recovery call");
+ }
+
+ state.Pop(1);
+ return 0;
+ }
+
+ private static int AssertEmpty(LuaState state, int iteration, string operation)
+ {
+ return state.Top == 0 ? 0 : Fail(iteration, operation, "did not completely clean up the Lua stack");
+ }
+
+ private static int Fail(int iteration, string operation, string detail)
+ {
+ Console.Error.WriteLine(string.Create(CultureInfo.InvariantCulture,
+ $"FAIL protected fuzz seed=0x{Seed:X8} iteration={iteration} operation={operation}: {detail}"));
+ return 1;
+ }
+
+ private struct RandomSource(uint state)
+ {
+ private uint _state = state;
+
+ public int NextInt32(int exclusiveMaximum)
+ {
+ return (int) (NextUInt32() % (uint) exclusiveMaximum);
+ }
+
+ public long NextInt64()
+ {
+ return unchecked(((long) NextUInt32() << 32) | NextUInt32());
+ }
+
+ public byte[] NextBytes(int length)
+ {
+ byte[] result = new byte[length];
+ for (int i = 0; i < result.Length; i++)
+ {
+ result[i] = (byte) NextUInt32();
+ }
+
+ return result;
+ }
+
+ private uint NextUInt32()
+ {
+ uint value = _state;
+ value ^= value << 13;
+ value ^= value >> 17;
+ value ^= value << 5;
+ _state = value;
+ return value;
+ }
+ }
+}
diff --git a/tests/CheatEngine.SDK.Lua.FailureProbe/README.md b/tests/CheatEngine.SDK.Lua.FailureProbe/README.md
index 4f75a6cb..b497eed2 100644
--- a/tests/CheatEngine.SDK.Lua.FailureProbe/README.md
+++ b/tests/CheatEngine.SDK.Lua.FailureProbe/README.md
@@ -27,3 +27,14 @@ time until that direct check returns `0`, then calls the bridge wire export dire
`PushBytes` operation. The bridge must return its `NoErrorStatus` sentinel without changing the full stack; the child
then restores the stack and verifies recovery. The mode stays out of process because a regression that lets a native
non-local exit cross the managed call frame must never terminate the main test process.
+
+Its `--fuzz-protected-operations` mode runs 2,000 deterministic sequences from the printed `0xD1CEBA5E` seed and
+prints 20,000 exercised top-level protected API calls. Each
+sequence keeps buffers at or below 1 KiB and table capacities at or below 64 while checking protected byte strings,
+tables, byte tables, userdata, indexed table mutations, deliberate indexing of a non-table, allocator refusal and a
+fixed recovery call. Every operation checks its values and documented stack shape, uses a lower sentinel where an
+error is expected, and ends with an empty stack. Sequence positions cycle through zero, maximum and seeded-random
+input sizes, so every run includes empty and boundary buffers, capacities and userdata sizes. The mode intentionally
+does not create callbacks, nested protected
+callback chains, executable Lua source or pointer/function-pointer inputs; those paths already have dedicated
+crash-boundary tests and are unsuitable for generated data here.
diff --git a/tests/CheatEngine.SDK.Lua.Tests/CheatEngine.SDK.Lua.Tests.csproj b/tests/CheatEngine.SDK.Lua.Tests/CheatEngine.SDK.Lua.Tests.csproj
index 5232da23..12241869 100644
--- a/tests/CheatEngine.SDK.Lua.Tests/CheatEngine.SDK.Lua.Tests.csproj
+++ b/tests/CheatEngine.SDK.Lua.Tests/CheatEngine.SDK.Lua.Tests.csproj
@@ -14,6 +14,11 @@
+
+
+
+
+
Generated-input checks for the lossless Lua integer conversion contract.
+[Trait("Category", "NativeLua")]
+public sealed class LuaIntegerFuzzTests
+{
+ private const long FloatExactLimit = 1L << 53;
+ private static readonly Config s_configuration = Config.QuickThrowOnFailure.WithMaxTest(1000).WithEndSize(128);
+
+ [Fact]
+ public void Generated_integer_numerals_match_an_independent_arithmetic_oracle()
+ {
+ LuaTest.RequireNativeLua();
+ Prop.ForAll(values =>
+ {
+ string numeral = BuildNumeral(Bound(values, 128));
+ byte[] utf8 = Encoding.UTF8.GetBytes(numeral);
+ (bool expectedAccepted, long expectedValue) = TryParseIntegerNumeral(utf8);
+
+ using NativeLuaState state = new(false);
+ LuaState L = LuaTest.View(state);
+ L.PushString(utf8);
+
+ Assert.Equal(expectedAccepted, Int64Marshaller.TryRead(L, 1, out long actualValue));
+ Assert.Equal(expectedAccepted ? expectedValue : 0L, actualValue);
+ Assert.Equal(LuaType.String, L.TypeOf(1));
+ Assert.Equal(1, L.Top);
+ Assert.True(L.TryReadUtf8(1, out ReadOnlySpan stored));
+ Assert.True(stored.SequenceEqual(utf8));
+ return true;
+ }).Check(s_configuration);
+ }
+
+ [Fact]
+ public void Generated_integer_and_float_values_keep_exactness_and_never_change_the_slot()
+ {
+ LuaTest.RequireNativeLua();
+ Prop.ForAll(value =>
+ {
+ long exact = value % (FloatExactLimit - 1);
+ double fractional = (value % 1_000_000) + 0.5;
+
+ using NativeLuaState state = new(false);
+ LuaState L = LuaTest.View(state);
+ L.PushInteger(value);
+ L.PushNumber(exact);
+ L.PushNumber(fractional);
+ L.PushNumber(FloatExactLimit);
+ L.PushNumber(-FloatExactLimit);
+
+ Assert.True(Int64Marshaller.TryRead(L, 1, out long integerRead));
+ Assert.Equal(value, integerRead);
+ Assert.True(Int64Marshaller.TryRead(L, 2, out long exactRead));
+ Assert.Equal(exact, exactRead);
+ Assert.False(Int64Marshaller.TryRead(L, 3, out long fractionalRead));
+ Assert.Equal(0, fractionalRead);
+ Assert.False(Int64Marshaller.TryRead(L, 4, out long positiveBoundaryRead));
+ Assert.Equal(0, positiveBoundaryRead);
+ Assert.False(Int64Marshaller.TryRead(L, 5, out long negativeBoundaryRead));
+ Assert.Equal(0, negativeBoundaryRead);
+ Assert.True(L.IsInteger(1));
+ Assert.False(L.IsInteger(2));
+ Assert.False(L.IsInteger(3));
+ Assert.False(L.IsInteger(4));
+ Assert.False(L.IsInteger(5));
+ Assert.Equal(5, L.Top);
+ return true;
+ }).Check(s_configuration);
+ }
+
+ [Theory]
+ [InlineData("0xFFFFFFFFFFFFFFFF", -1L)]
+ [InlineData("-0xFFFFFFFFFFFFFFFF", 1L)]
+ [InlineData("0x000000000000000000001", 1L)]
+ [InlineData("-9223372036854775808", long.MinValue)]
+ [InlineData("9223372036854775807", long.MaxValue)]
+ public void Integer_numeral_boundaries_preserve_lua_signed_wrap_policy(string numeral, long expected)
+ {
+ LuaTest.RequireNativeLua();
+ using NativeLuaState state = new(false);
+ LuaState L = LuaTest.View(state);
+ L.PushString(numeral.AsSpan());
+
+ Assert.True(Int64Marshaller.TryRead(L, 1, out long actual));
+ Assert.Equal(expected, actual);
+ Assert.Equal(LuaType.String, L.TypeOf(1));
+ Assert.Equal(1, L.Top);
+ }
+
+ private static byte[] Bound(byte[] values, int maximumLength)
+ {
+ return values.Length <= maximumLength ? values : values[..maximumLength];
+ }
+
+ private static string BuildNumeral(byte[] values)
+ {
+ byte selector = At(values, 0);
+ string spaces = BuildSpaces(values, 1);
+ return (selector % 8) switch
+ {
+ 0 => spaces + unchecked((long) ReadUInt64(values, 8)).ToString(CultureInfo.InvariantCulture) + spaces,
+ 1 => BuildHexadecimal(values, spaces),
+ 2 => spaces + "9223372036854775808" + spaces,
+ 3 => spaces + "0x10000000000000000" + spaces,
+ 4 => spaces + "12.5" + spaces,
+ 5 => spaces + "1e3" + spaces,
+ 6 => spaces + "42\0" + Convert.ToHexString(Bound(values, 8)),
+ _ => "\u00A0" + spaces + "17" + spaces
+ };
+ }
+
+ private static string BuildHexadecimal(byte[] values, string spaces)
+ {
+ const string Hex = "0123456789abcdef";
+ int digits = (At(values, 2) % 16) + 1;
+ int leadingZeros = At(values, 3) % 33;
+ StringBuilder text = new(spaces);
+ if ((At(values, 4) & 1) != 0)
+ {
+ text.Append('-');
+ }
+
+ text.Append((At(values, 5) & 1) == 0 ? "0x" : "0X");
+ text.Append('0', leadingZeros);
+ for (int i = 0; i < digits; i++)
+ {
+ text.Append(Hex[At(values, i + 6) & 0x0F]);
+ }
+
+ return text.Append(spaces).ToString();
+ }
+
+ private static string BuildSpaces(byte[] values, int offset)
+ {
+ ReadOnlySpan whitespace = [' ', '\t', '\n', '\v', '\f', '\r'];
+ StringBuilder text = new();
+ for (int i = 0; i < At(values, offset) % 5; i++)
+ {
+ text.Append(whitespace[At(values, offset + i + 1) % whitespace.Length]);
+ }
+
+ return text.ToString();
+ }
+
+ private static (bool Accepted, long Value) TryParseIntegerNumeral(ReadOnlySpan utf8)
+ {
+ foreach (byte utf8Byte in utf8)
+ {
+ if (utf8Byte > 0x7F)
+ {
+ return (false, 0);
+ }
+ }
+
+ string text = Encoding.ASCII.GetString(utf8).Trim(' ', '\t', '\n', '\v', '\f', '\r');
+ if (text.Length == 0)
+ {
+ return (false, 0);
+ }
+
+ bool negative = text[0] == '-';
+ int signLength = text[0] is '-' or '+' ? 1 : 0;
+ string unsigned = text[signLength..];
+ if (unsigned.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+ {
+ string digits = unsigned[2..];
+ if (!IsDigits(digits, hexadecimal: true))
+ {
+ return (false, 0);
+ }
+
+ string significant = digits.TrimStart('0');
+ if (significant.Length > 16 || !ulong.TryParse(digits, NumberStyles.AllowHexSpecifier,
+ CultureInfo.InvariantCulture, out ulong bits))
+ {
+ return (false, 0);
+ }
+
+ return (true, negative ? unchecked((long) (0UL - bits)) : unchecked((long) bits));
+ }
+
+ if (!IsDigits(unsigned, hexadecimal: false) ||
+ !BigInteger.TryParse(text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out BigInteger value) ||
+ value < long.MinValue || value > long.MaxValue)
+ {
+ return (false, 0);
+ }
+
+ return (true, (long) value);
+ }
+
+ private static bool IsDigits(string text, bool hexadecimal)
+ {
+ foreach (char character in text)
+ {
+ if (hexadecimal ? !Uri.IsHexDigit(character) : character is < '0' or > '9')
+ {
+ return false;
+ }
+ }
+
+ return text.Length > 0;
+ }
+
+ private static byte At(byte[] values, int index) => values.Length == 0 ? (byte) 0 : values[index % values.Length];
+
+ private static ulong ReadUInt64(byte[] values, int offset)
+ {
+ ulong result = 0;
+ for (int i = 0; i < sizeof(ulong); i++)
+ {
+ result |= (ulong) At(values, offset + i) << (i * 8);
+ }
+
+ return result;
+ }
+}
diff --git a/tests/CheatEngine.SDK.Lua.Tests/Protected/NativeFuzzProcessTests.cs b/tests/CheatEngine.SDK.Lua.Tests/Protected/NativeFuzzProcessTests.cs
new file mode 100644
index 00000000..889dc3cd
--- /dev/null
+++ b/tests/CheatEngine.SDK.Lua.Tests/Protected/NativeFuzzProcessTests.cs
@@ -0,0 +1,87 @@
+using System.Diagnostics;
+using System.Globalization;
+
+using CheatEngine.SDK.Tests.Shared.NativeLua;
+
+namespace CheatEngine.SDK.Lua.Tests.Protected;
+
+/// Runs bounded native protected-operation fuzzing out of process so native regressions cannot stop MTP.
+[Trait("Category", "NativeLua")]
+public sealed class NativeFuzzProcessTests
+{
+ [Fact]
+ public async Task Protected_operation_fuzz_sequences_preserve_results_stack_and_recovery()
+ {
+ Assert.SkipUnless(NativeLuaLibrary.IsAvailable, NativeLuaLibrary.UnavailableReason);
+ string baseDirectory = AppContext.BaseDirectory;
+ string probe = Path.Combine(baseDirectory, "CheatEngine.SDK.Lua.FailureProbe.dll");
+ Assert.True(File.Exists(probe), $"Failure probe was not copied to '{probe}'.");
+
+ ProcessStartInfo start = new("dotnet")
+ {
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+ start.ArgumentList.Add("exec");
+ start.ArgumentList.Add("--runtimeconfig");
+ start.ArgumentList.Add(Path.Combine(baseDirectory, "CheatEngine.SDK.Lua.Tests.runtimeconfig.json"));
+ start.ArgumentList.Add("--depsfile");
+ start.ArgumentList.Add(Path.Combine(baseDirectory, "CheatEngine.SDK.Lua.Tests.deps.json"));
+ start.ArgumentList.Add(probe);
+ start.ArgumentList.Add(NativeLuaLibrary.LibraryPath!);
+ start.ArgumentList.Add("--fuzz-protected-operations");
+
+ using Process? process = Process.Start(start);
+ Assert.NotNull(process);
+ CancellationToken cancellationToken = TestContext.Current.CancellationToken;
+ // Pipe lifetime follows the owned process: cancellation kills it, then drains both streams before disposal.
+ Task output = process.StandardOutput.ReadToEndAsync(CancellationToken.None);
+ Task error = process.StandardError.ReadToEndAsync(CancellationToken.None);
+ using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ timeout.CancelAfter(TimeSpan.FromSeconds(60));
+ try
+ {
+ await process.WaitForExitAsync(timeout.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ await StopProcessAsync(process);
+ string[] captured = await Task.WhenAll(output, error);
+ if (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+
+ Assert.Fail($"The protected-operation fuzz probe did not exit within 60 seconds.\nstdout:\n{captured[0]}\nstderr:\n{captured[1]}");
+ }
+
+ string standardOutput = await output;
+ string standardError = await error;
+ Assert.True(process.ExitCode == 0,
+ string.Create(CultureInfo.InvariantCulture,
+ $"Probe exit code: {process.ExitCode}{Environment.NewLine}stdout:{Environment.NewLine}{standardOutput}{Environment.NewLine}stderr:{Environment.NewLine}{standardError}"));
+ Assert.Contains("PASS protected fuzz seed=0xD1CEBA5E sequences=2000 protected-api-calls=20000", standardOutput,
+ StringComparison.Ordinal);
+ Assert.Contains("buffers<=1024 capacities<=64 allocator-refusals=2000", standardOutput,
+ StringComparison.Ordinal);
+ }
+
+ private static async Task StopProcessAsync(Process process)
+ {
+ if (!process.HasExited)
+ {
+ try
+ {
+ process.Kill(true);
+ }
+ catch (InvalidOperationException) when (process.HasExited)
+ {
+ // It exited between the status check and the kill request; drain it below.
+ }
+ }
+
+ await process.WaitForExitAsync(CancellationToken.None);
+ }
+}
diff --git a/tests/CheatEngine.SDK.Lua.Tests/README.md b/tests/CheatEngine.SDK.Lua.Tests/README.md
index 8f25b06b..08489903 100644
--- a/tests/CheatEngine.SDK.Lua.Tests/README.md
+++ b/tests/CheatEngine.SDK.Lua.Tests/README.md
@@ -18,6 +18,7 @@ error unwind a managed frame or allocates on a hot path. Only a real interpreter
|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Stack and protected operations | `LuaFrame` restores the stack top on every exit path; raising metamethods, syntax errors and host-object pusher exits become statuses, never a Lua unwind |
| Marshallers | Values round trip exactly, a value that does not fit is refused instead of truncated, and no integer or address passes through a lossy `double` (Q21) |
+| Generated input fuzzing | Bounded FsCheck properties exercise integer numeral grammar, number precision, raw Lua bytes, UTF-16 replacement encoding and stack/pool string boundaries |
| Optional values and status | `LuaOptional` keeps omitted, `nil` and a value apart; `LuaCallSupport` pushes, reads and copies optional and variadic values; status enums start at `Unknown = 0` |
| References and runtime | Every `LuaRuntime.Attach` advances the epoch, and a `LuaRef` from an earlier epoch is never pushed |
| Callbacks | Test thunks follow the SDK rule (static, cdecl, catch-all), state travels in the upvalue, and `Detach` drains admitted invocations before neutralizing callbacks |
@@ -66,6 +67,9 @@ dotnet test --project tests/CheatEngine.SDK.Lua.Tests --filter-trait "Category=N
`StringBindingTests`).
- Integer and address marshallers keep every bit of an integer and refuse a float at or above 2^53, a float numeral
and an out-of-range numeral (`MarshallerRoundTripTests`, traited `Qualification=Q21`).
+- Generated inputs retain Lua's exact integer-string and float conversion policy, raw bytes are copied only when a
+ destination fits, and UTF-16 text follows the platform UTF-8 replacement policy (`LuaIntegerFuzzTests`,
+ `LuaStringFuzzTests`, `Utf8ScratchFuzzTests`; 1,000 cases per property).
- `LuaOptional` and the optional and variadic call helpers keep omitted, `nil` and values distinct, allocate
nothing, and report a missing result or an exceeded capacity (`LuaOptionalTests`, `LuaCallSupportOptionalTests`).
- A default status is `Unknown`, never success, and every status value is pinned (`LuaOperationStatusTests`,
diff --git a/tests/CheatEngine.SDK.Lua.Tests/State/LuaStringFuzzTests.cs b/tests/CheatEngine.SDK.Lua.Tests/State/LuaStringFuzzTests.cs
new file mode 100644
index 00000000..d8c9574c
--- /dev/null
+++ b/tests/CheatEngine.SDK.Lua.Tests/State/LuaStringFuzzTests.cs
@@ -0,0 +1,98 @@
+using System.Text;
+
+using CheatEngine.SDK.Lua.State;
+using CheatEngine.SDK.Lua.Tests.Support;
+using CheatEngine.SDK.Tests.Shared.NativeLua;
+
+using FsCheck;
+using FsCheck.Fluent;
+
+namespace CheatEngine.SDK.Lua.Tests.State;
+
+/// Generated byte and UTF-16 strings prove that Lua string access preserves its documented contracts.
+[Trait("Category", "NativeLua")]
+public sealed class LuaStringFuzzTests
+{
+ private static readonly Config s_configuration = Config.QuickThrowOnFailure.WithMaxTest(1000).WithEndSize(128);
+
+ [Fact]
+ public void Arbitrary_raw_bytes_round_trip_and_undersized_copies_preserve_the_destination()
+ {
+ LuaTest.RequireNativeLua();
+ Prop.ForAll(values =>
+ {
+ byte[] payload = Bound(values, 512);
+ using NativeLuaState state = new(false);
+ LuaState L = LuaTest.View(state);
+ L.PushString(payload);
+
+ Assert.Equal((nuint) payload.Length, L.RawLength(1));
+ Assert.True(L.TryReadUtf8(1, out ReadOnlySpan read));
+ Assert.True(read.SequenceEqual(payload));
+ byte[] exact = new byte[payload.Length];
+ Assert.True(L.TryCopyUtf8(1, exact, out int written));
+ Assert.Equal(payload.Length, written);
+ Assert.Equal(payload, exact);
+ if (payload.Length > 0)
+ {
+ byte[] undersized = new byte[payload.Length - 1];
+ Array.Fill(undersized, (byte) 0xA5);
+ Assert.False(L.TryCopyUtf8(1, undersized, out int refusedWritten));
+ Assert.Equal(0, refusedWritten);
+ Assert.All(undersized, static value => Assert.Equal(0xA5, value));
+ }
+
+ Assert.Equal(LuaType.String, L.TypeOf(1));
+ Assert.Equal(1, L.Top);
+ return true;
+ }).Check(s_configuration);
+ }
+
+ [Fact]
+ public void Arbitrary_utf16_uses_the_standard_utf8_replacement_oracle()
+ {
+ LuaTest.RequireNativeLua();
+ Prop.ForAll(values =>
+ {
+ char[] characters = Array.ConvertAll(values[..Math.Min(values.Length, 128)], static value => (char) value);
+ string text = new(characters);
+ byte[] expectedUtf8 = Encoding.UTF8.GetBytes(text);
+ string expectedText = Encoding.UTF8.GetString(expectedUtf8);
+
+ using NativeLuaState state = new(false);
+ LuaState L = LuaTest.View(state);
+ L.PushString(text.AsSpan());
+
+ Assert.True(L.TryReadUtf8(1, out ReadOnlySpan actualUtf8));
+ Assert.True(actualUtf8.SequenceEqual(expectedUtf8));
+ Assert.True(L.TryReadString(1, out string? actualText));
+ Assert.Equal(expectedText, actualText);
+ Assert.Equal((nuint) expectedUtf8.Length, L.RawLength(1));
+ Assert.Equal(LuaType.String, L.TypeOf(1));
+ Assert.Equal(1, L.Top);
+ return true;
+ }).Check(s_configuration);
+ }
+
+ [Fact]
+ public void Embedded_nuls_and_malformed_utf8_keep_length_and_decode_with_the_platform_oracle()
+ {
+ LuaTest.RequireNativeLua();
+ byte[] payload = [0x61, 0x00, 0xC0, 0xAF, 0xFF, 0x62];
+ using NativeLuaState state = new(false);
+ LuaState L = LuaTest.View(state);
+ L.PushString(payload);
+
+ Assert.True(L.TryReadUtf8(1, out ReadOnlySpan bytes));
+ Assert.True(bytes.SequenceEqual(payload));
+ Assert.True(L.TryReadString(1, out string? text));
+ Assert.Equal(Encoding.UTF8.GetString(payload), text);
+ Assert.Equal((nuint) payload.Length, L.RawLength(1));
+ Assert.Equal(1, L.Top);
+ }
+
+ private static byte[] Bound(byte[] values, int maximumLength)
+ {
+ return values.Length <= maximumLength ? values : values[..maximumLength];
+ }
+}
diff --git a/tests/CheatEngine.SDK.Lua.Tests/Text/Utf8ScratchFuzzTests.cs b/tests/CheatEngine.SDK.Lua.Tests/Text/Utf8ScratchFuzzTests.cs
new file mode 100644
index 00000000..e6262207
--- /dev/null
+++ b/tests/CheatEngine.SDK.Lua.Tests/Text/Utf8ScratchFuzzTests.cs
@@ -0,0 +1,64 @@
+using System.Text;
+
+using CheatEngine.SDK.Lua.Text;
+
+using FsCheck;
+using FsCheck.Fluent;
+
+namespace CheatEngine.SDK.Lua.Tests.Text;
+
+/// Generated UTF-16 checks for the stack-first UTF-8 scratch buffer.
+public sealed class Utf8ScratchFuzzTests
+{
+ private static readonly Config s_configuration = Config.QuickThrowOnFailure.WithMaxTest(1000).WithEndSize(128);
+
+ [Fact]
+ public void Generated_utf16_matches_encoding_utf8_across_stack_and_pool_capacity()
+ {
+ Prop.ForAll(values =>
+ {
+ string generated = new(Array.ConvertAll(values[..Math.Min(values.Length, 128)], static value => (char) value));
+ string text = (values.Length == 0 || (values[0] & 1) == 0) ? generated : generated + new string('x', 513);
+ byte[] expected = Encoding.UTF8.GetBytes(text);
+ Span scratch = stackalloc byte[Utf8Scratch.StackBufferSize];
+ using Utf8Scratch utf8 = Utf8Scratch.Encode(text, scratch);
+
+ Assert.Equal(expected.Length > Utf8Scratch.StackBufferSize, utf8.IsPooled);
+ Assert.True(utf8.Bytes.SequenceEqual(expected));
+ return true;
+ }).Check(s_configuration);
+ }
+
+ [Theory]
+ [InlineData(511, false)]
+ [InlineData(512, false)]
+ [InlineData(513, true)]
+ public void Ascii_at_the_stack_buffer_cutover_uses_the_documented_storage(int byteCount, bool expectedPooled)
+ {
+ string text = new('x', byteCount);
+ AssertEncodingAndStorage(text, expectedPooled);
+ }
+
+ [Theory]
+ [InlineData(510, false)]
+ [InlineData(512, false)]
+ [InlineData(514, true)]
+ public void Multibyte_utf8_at_the_stack_buffer_cutover_uses_the_documented_storage(int byteCount,
+ bool expectedPooled)
+ {
+ Assert.Equal(0, byteCount % 2);
+ string text = new('\u00E9', byteCount / 2);
+ AssertEncodingAndStorage(text, expectedPooled);
+ }
+
+ private static void AssertEncodingAndStorage(string text, bool expectedPooled)
+ {
+ byte[] expected = Encoding.UTF8.GetBytes(text);
+ Span scratch = stackalloc byte[Utf8Scratch.StackBufferSize];
+ using Utf8Scratch utf8 = Utf8Scratch.Encode(text, scratch);
+
+ Assert.Equal(expectedPooled, utf8.IsPooled);
+ Assert.Equal(expected.Length, utf8.Bytes.Length);
+ Assert.True(utf8.Bytes.SequenceEqual(expected));
+ }
+}
diff --git a/tests/CheatEngine.SDK.Lua.Tests/packages.lock.json b/tests/CheatEngine.SDK.Lua.Tests/packages.lock.json
index 8db749f6..b24b2429 100644
--- a/tests/CheatEngine.SDK.Lua.Tests/packages.lock.json
+++ b/tests/CheatEngine.SDK.Lua.Tests/packages.lock.json
@@ -2,6 +2,15 @@
"version": 2,
"dependencies": {
"net10.0": {
+ "FsCheck": {
+ "type": "Direct",
+ "requested": "[3.4.0, )",
+ "resolved": "3.4.0",
+ "contentHash": "wkWR1DORrKd46W6MEhmS4muo/0kgrtUV/AGW1ohMWemy7WukcRH8T3JjkDpn/daOHk0yVqf0CBvScU17Ab//PA==",
+ "dependencies": {
+ "FSharp.Core": "5.0.2"
+ }
+ },
"Meziantou.Analyzer": {
"type": "Direct",
"requested": "[3.0.290, )",
@@ -75,6 +84,11 @@
"xunit.v3.core.mtp-v2": "[4.0.1]"
}
},
+ "FSharp.Core": {
+ "type": "Transitive",
+ "resolved": "5.0.2",
+ "contentHash": "DpcajqENgb3VXaHw1FKfVS+TWwEzck1PCajqLwBAVrtd1lfxd7T8JISVZBdV1mX9+2g48+tIgpNaVJWObdMrBw=="
+ },
"Microsoft.ApplicationInsights": {
"type": "Transitive",
"resolved": "2.23.0",
diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests.csproj b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests.csproj
index 150c8cee..69db7801 100644
--- a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests.csproj
+++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests.csproj
@@ -1,5 +1,9 @@
+
+
+
+