Repository navigation
fix: execution guards stop runaway recursion and endless loops (C# + Python) - #7
Merged
Merged
Conversation
C#: the Roslyn adapter compiles guard checks into snippets, full-source code and includes: a sampled EnsureSufficientExecutionStack check at every method / local function / accessor / block-lambda entry (Runtime "Recursion too deep" instead of a process-killing stack overflow), a local countdown per loop iteration and a check before every goto (Timeout once timeoutMs is spent, also for synchronous code on the main thread), and a filter on every catch so a stop is not swallowed. Inserted code sits on its own lines behind #line directives: the agent's lines and columns are unchanged. CodeRunnerOptions.DefaultTimeoutMs (120 s) applies when a request has no timeoutMs; ExecutionGuards turns the guards off. Python: a watchdog raises SnippetTimeout in the snippet's thread when timeoutMs (or set_default_timeout_ms, 120 s) is spent; it is disarmed before run() returns. Also: full-source mode gets the request's usings and auto-import, plus CE0003 when names stay unresolved; Unity analyzer warnings UAC0005/0007/0020 are suppressed where the engine-agnostic core needs those APIs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…SON int fields Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C#: field/property initializers run the call check (a first guard field per class); async expression bodies return a value only for generic task types (aliases, UniTask); a finished run lifts the time budget for callbacks it left behind, a timed-out one keeps its stop and is not reused on Mono; full source starting with #define stays verbatim; only catch clauses that can catch a stop get the filter; #line file names are written verbatim. Python: the snippet thread never takes a lock while a stop can be pending (on 3.14 a stop delivered after acquiring one left it held): stop() clears a flag, waits for the timer thread when it may have fired, then drops a pending stop; ctypes is bound at import; each run raises its own SnippetTimeout subclass; a lowered recursion limit no longer fails a successful run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…test limits .NET 8 on Linux took minutes to unwind a recursion stopped near the end of an 8 MB stack (CI hang): the guard also stops once a snippet used MaxStackBytes (1 MiB default) on a thread. Python 3.10 never delivers the stop in a loop whose body is only a try statement: that case is skipped there and documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, docs Thread statics cost ~60 ns per sampled check on a collectible context: the base is recorded in Arm (same thread, right before the invocation) and compared only on that thread; other threads keep the runtime's margin. Contract: allowance wording, the 3.10 try/with loop limit, README; ICSharpCompiler note on unsafe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 28, 2026
Merged
IvanMurzak
added a commit
that referenced
this pull request
Oct 30, 2026
* chore: release 0.4.0 (execution guards, sliced waits) Version 0.3.1 -> 0.4.0 in dotnet/Directory.Build.props, the README snippet and the Python module; the contract's Unreleased changelog entry becomes v0.4.0 (title v0.4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * simplify: no changes Report-only review: the bump covers every hard-coded version; the blank line before v0.3.1 is pre-existing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * code review: no findings Report-only reviewer (high): every version reference is 0.4.0, the v0.4.0 changelog matches #7-#11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes two Code-Exec robustness findings from wave-5 testing. A snippet with deep recursion used to end the Godot editor with a stack overflow, and a synchronous endless loop froze the editor forever. This PR does not release anything or bump the version.
Execution guards (C#, spec §3.6)
The Roslyn adapter (
GuardWriter) inserts guard checks into the snippet, full-source code and includes. The guard type is compiled into the snippet's own assembly, so the snippet never binds to a copy of Core.CodeRunnerOptions.MaxStackBytes, 1 MiB by default, on the snippet's thread) and againstEnsureSufficientExecutionStack. The allowance exists because .NET 8 on Linux took minutes to unwind an exception thrown near the end of an 8 MB stack. Close to the limit the result is aRuntimeerror,System.InsufficientExecutionStackException: Recursion too deep: ..., not a stack overflow that kills the process. This works on the host's main thread.gotoa shared one; calls count down too. They read the clock after 1, 2, 4 … up to 256 iterations, and on every iteration while iterations are slow. OncetimeoutMsis spent the run stops withTimeout(System.TimeoutException: The snippet ran longer than its time budget of N ms (timeoutMs) and was stopped.), with the snippet's line in the error.Exception,SystemException, or the stop's own type) get awhen (Running())filter, so a catch-all inside the loop cannot swallow the stop.#linedirectives plus padding, so diagnostics and stack frames keep the agent's line and column.CodeRunnerOptions.DefaultTimeoutMs(120 000) is used when a request leavestimeoutMsat 0.ExecutionGuards(defaulttrue) turns the guards off. A negativetimeoutMsisInvalidRequest.Python time budget (spec §4)
timeoutMsis spent (orset_default_timeout_ms, default 120 s), a watchdog thread raisesSnippetTimeout, aBaseException, in the snippet's thread withPyThreadState_SetAsyncExc, and raises it again every 100 ms while the snippet catches it. The result iserrorKindTimeout.RecursionError. One limit is documented: a snippet that raisessys.setrecursionlimitcan still overflow the C stack on 3.11 and older through C-level recursion (reprorjson.dumpsof a deeply nested list).Also
usingsand auto-import. The host's default usings are still not added, because adding them could make working code ambiguous. A newCE0003diagnostic says so and names the unresolved names.Unity.Analyzers.Common.dll.Overhead
Measured on .NET 8 (snippet code is compiled in Debug); the baseline is the same loop without guards:
forloopforeachover aListwith a sumfib(32), non-collectible host (Mono-like)fib(32), collectibleAssemblyLoadContextEach call pays more on a collectible load context because statics there are slow to reach.
Python watchdog: about 65 µs per run (one timer thread started per run). A 3M-iteration loop showed no measurable per-bytecode cost (+2%, within noise) on 3.11 and 3.14.
Python 3.10 limit: a loop whose body begins with
tryorwithnever receives the stop on 3.10 (the interpreter does not check for pending exceptions there). This is documented, and the test skips that case on 3.10. Unreal uses 3.11, where it works.Tests
GuardTestscover recursion in each guarded form, endless loops of each kind including catch-alls and goto, async, full-source and include code, kept coordinates and kept meaning. Without the guards the recursion tests crash the test host with a stack overflow and the loop tests hang (checked locally).endless-loop-timeoutanddeep-recursionfor both runtimes, andfull-source-auto-import.TimeBudgetTests.pack.shplus the package consumer on Roslyn 3.8 and 5.9.Quality stages:
simplify:andcode review:commits (two report-only reviewers; their findings were fixed in thecode review:commit).🤖 Generated with Claude Code