From 9565ee1b54e576f78ce77795dc952e0f23805292 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Wed, 4 Nov 2026 23:17:35 +0100 Subject: [PATCH] fix: reject lambda signatures beyond the closure parameter limit A lambda with 65,535 explicit parameters plus FEC's hidden closure parameter exceeds the runtime method-signature limit, but compilation can currently return an unusable delegate. Reject the excessive count before allocating its signature array. The check is confined to the existing unpooled large-signature branch: zero-argument and small pooled signatures retain their current paths. No new traversal or context metadata. Focused Standard/Light regressions pass on .NET 8, .NET 10 and .NET Framework 4.7.2. Full solution and legacy suites pass. Modern full runners retain only the reproduced upstream nullable MultiplyChecked parity failure. Working benchmark controls retain allocation counts and emitted IL. --- .../FastExpressionCompiler.cs | 7 +++- .../Program.cs | 2 + .../Program.cs | 2 + .../ExcessiveArityRegressionTests.cs | 41 +++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 test/FastExpressionCompiler.UnitTests/ExcessiveArityRegressionTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index b0e957416..ab5d562e8 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -482,8 +482,11 @@ internal static Type[] RentPooledOrNewClosureTypeToParamTypes(ParamExprs paramEx var count = paramExprs.GetParamCount(); if (count == 0) return _closureAsASingleParamType; - - var pooledOrNew = count < 8 ? Interlocked.Exchange(ref _paramTypesPoolWithElem0OfLength1[count], null) ?? new Type[count + 1] : new Type[count + 1]; + // The hidden closure argument also counts toward the CLI signature limit. + // The pooled small-signature path is already below that limit. + var pooledOrNew = count < 8 ? Interlocked.Exchange(ref _paramTypesPoolWithElem0OfLength1[count], null) ?? new Type[count + 1] : + count < ushort.MaxValue ? new Type[count + 1] : + throw new InvalidProgramException("The lambda and its closure exceed the maximum method parameter count."); pooledOrNew[0] = typeof(ArrayClosure); for (var i = 0; i < count; i++) { diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index 7bdfc33bb..15adaa36a 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -34,6 +34,7 @@ static void RunLightExpressionTests(object state) var justLightTestsStopwatch = Stopwatch.StartNew(); var t = (LightExpression.TestRun)state; + t.Run(new LightExpression.UnitTests.ExcessiveArityRegressionTests()); t.Run(new LightExpression.UnitTests.NestedStructFieldAddressRegressionTests()); t.Run(new LightExpression.IssueTests.Issue183_NullableDecimal()); t.Run(new LightExpression.IssueTests.Issue398_Optimize_Switch_with_OpCodes_Switch()); @@ -60,6 +61,7 @@ static void RunLightExpressionTests(object state) var fecTestsStopwatch = Stopwatch.StartNew(); var fecTests = new TestRun(); + fecTests.Run(new ExcessiveArityRegressionTests()); fecTests.Run(new NestedStructFieldAddressRegressionTests()); fecTests.Run(new Issue183_NullableDecimal()); fecTests.Run(new Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index ca07a0cad..85e80bd71 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -63,6 +63,7 @@ public static void Main(string[] args) var ltTask = Task.Run(() => { var ltStopwatch = Stopwatch.StartNew(); + lt.Run(new LightExpression.UnitTests.ExcessiveArityRegressionTests()); #if NET8_0_OR_GREATER lt.Run(new LightExpression.UnitTests.NestedStructFieldAddressRegressionTests()); lt.Run(new LightExpression.IssueTests.Issue490_Regression_in_compiling_lambdas_with_ref_struct_parameters()); @@ -86,6 +87,7 @@ public static void Main(string[] args) }); var stStopwatch = Stopwatch.StartNew(); + st.Run(new ExcessiveArityRegressionTests()); st.Run(new NestedStructFieldAddressRegressionTests()); st.Run(new Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); st.Run(new Issue561_NonBooleanEqualityOperator()); diff --git a/test/FastExpressionCompiler.UnitTests/ExcessiveArityRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/ExcessiveArityRegressionTests.cs new file mode 100644 index 000000000..793450033 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/ExcessiveArityRegressionTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using S = System.Linq.Expressions.Expression; +#if LIGHT_EXPRESSION +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +namespace FastExpressionCompiler.UnitTests; +#endif + +public struct ExcessiveArityRegressionTests : ITestX +{ + public void Run(TestRun t) + { + Unrepresentable_method_signatures_are_rejected_during_compilation(t); + Large_supported_signatures_keep_argument_indices(t); + } + + public void Unrepresentable_method_signatures_are_rejected_during_compilation(TestContext t) + { + var parameters = Enumerable.Range(0, ushort.MaxValue).Select(_ => S.Parameter(typeof(int))).ToArray(); + var system = S.Lambda(parameters[parameters.Length - 1], parameters); + var expression = system.FromSysExpression(); + t.Throws(() => expression.CompileFast(true)); + t.Throws(() => expression.CompileFast(true, CompilerFlags.ThrowOnNotSupportedExpression)); + t.Throws(() => system.Compile()); + } + + public void Large_supported_signatures_keep_argument_indices(TestContext t) + { + foreach (var count in new[] { 255, 256, 300 }) + { + var parameters = Enumerable.Range(0, count).Select(_ => S.Parameter(typeof(int))).ToArray(); + var system = S.Lambda(S.Add(parameters[0], parameters[count - 1]), parameters); + var fast = system.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + var arguments = Enumerable.Range(0, count).Select(i => (object)i).ToArray(); + t.AreEqual(count - 1, fast.DynamicInvoke(arguments)); + t.AreEqual(system.Compile().DynamicInvoke(arguments), fast.DynamicInvoke(arguments)); + } + } +}