using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// Rewrites the synthesized script wrapper so that the script body and every lambda, anonymous method and local /// function in it starts with ScriptStackGuard.Check(); (lmxopcua#779). Any recursion that runs script code — /// direct, mutual, through a self-referencing delegate, through an anonymous self-application, or through a BCL /// call that invokes a script lambda — therefore passes a check on every script frame, and fails with a catchable /// instead of an uncatchable /// that would terminate the server. /// /// /// /// Why a runtime check and not an analyzer rule rejecting recursion. Recursion cannot be decided from /// syntax: Func<object,int> g = s => ((Func<object,int>)s)(s); g(g); recurses without /// ever naming itself, and a delegate can reach itself through a collection or a captured variable. A rule /// would reject the obvious shapes and pass the rest. The check also leaves legitimate bounded recursion /// (a short tree walk) working. /// /// /// Semantics preserved. An expression-bodied lambda or local function becomes a block — { Check(); /// return e; }, or { Check(); e; } when the bound delegate/method returns void. The void-or-not /// decision is read from the semantic model of the already-validated compilation, i.e. from the overload the /// compiler actually picked, so the block form is compatible with the same delegate type and nothing /// re-binds differently. Lambdas converted to an expression tree (Expression<T>) are left alone: /// a block body is not legal there and an expression tree does not execute script frames. Async lambdas and /// local functions are left alone (the sandbox denies System.Threading.Tasks, so a script cannot /// produce one that compiles). /// /// /// Runs after , so the analyzer only ever sees the user's code; the /// inserted call names a type in Core.Scripting.Abstractions, which the sandbox already references. /// /// internal static class ScriptStackGuardInjector { private const string CheckCall = "global::ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptStackGuard.Check()"; /// Returns with a stack check at the entry of every script-declared body. /// The compiled (error-free, analyzer-clean) wrapper compilation. /// The wrapper syntax tree inside . /// The rewritten tree, with the original parse options. public static SyntaxTree Inject(CSharpCompilation compilation, SyntaxTree tree) { var model = compilation.GetSemanticModel(tree); var newRoot = new Rewriter(model).Visit(tree.GetRoot()); return tree.WithRootAndOptions(newRoot, tree.Options); } private static StatementSyntax CheckStatement() => ExpressionStatement(ParseExpression(CheckCall)); private static BlockSyntax Prepend(BlockSyntax block) => block.WithStatements(block.Statements.Insert(0, CheckStatement())); private static BlockSyntax ToBlock(ExpressionSyntax body, bool returnsVoid) => Block( CheckStatement(), returnsVoid ? ExpressionStatement(body) : ReturnStatement(body)); private sealed class Rewriter(SemanticModel model) : CSharpSyntaxRewriter { public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) { // The only method in the wrapper is the synthesized CompiledScript.Run (ScriptEvaluator enforces that). var visited = (MethodDeclarationSyntax)base.VisitMethodDeclaration(node)!; return visited.Body is { } body ? visited.WithBody(Prepend(body)) : visited; } public override SyntaxNode? VisitLocalFunctionStatement(LocalFunctionStatementSyntax node) { var symbol = model.GetDeclaredSymbol(node) as IMethodSymbol; var visited = (LocalFunctionStatementSyntax)base.VisitLocalFunctionStatement(node)!; if (symbol is null || symbol.IsAsync) return visited; if (visited.Body is { } body) return visited.WithBody(Prepend(body)); if (visited.ExpressionBody is { } arrow) { return visited .WithExpressionBody(null) .WithSemicolonToken(default) .WithBody(ToBlock(arrow.Expression, symbol.ReturnsVoid)); } return visited; } public override SyntaxNode? VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node) => Guard(node, (AnonymousFunctionExpressionSyntax)base.VisitSimpleLambdaExpression(node)!); public override SyntaxNode? VisitParenthesizedLambdaExpression(ParenthesizedLambdaExpressionSyntax node) => Guard(node, (AnonymousFunctionExpressionSyntax)base.VisitParenthesizedLambdaExpression(node)!); public override SyntaxNode? VisitAnonymousMethodExpression(AnonymousMethodExpressionSyntax node) => Guard(node, (AnonymousFunctionExpressionSyntax)base.VisitAnonymousMethodExpression(node)!); /// The node in the bound tree (semantic queries must use it). /// The same node with its descendants already rewritten. private SyntaxNode Guard(AnonymousFunctionExpressionSyntax original, AnonymousFunctionExpressionSyntax visited) { if (original.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword)) return visited; if (IsExpressionTree(original)) return visited; if (model.GetSymbolInfo(original).Symbol is not IMethodSymbol symbol) return visited; if (visited.Block is { } block) return visited.WithBlock(Prepend(block)).WithExpressionBody(null); if (visited.ExpressionBody is { } expression) return visited.WithExpressionBody(null).WithBlock(ToBlock(expression, symbol.ReturnsVoid)); return visited; } private bool IsExpressionTree(AnonymousFunctionExpressionSyntax lambda) { var converted = model.GetTypeInfo(lambda).ConvertedType; return converted is INamedTypeSymbol { Name: "Expression", IsGenericType: true } named && named.ContainingNamespace?.ToDisplayString() == "System.Linq.Expressions"; } } }