PyLite runs scripts that the host did not write, so its threat model is part of its contract. Every
guarantee below names the tests and the adversarial probes that hold it. tools/run-gates.ps1 runs all of
them, and the test report of each release shows that run on the release commit.
TESTING.md says how to repeat it.
- The attacker is the script author. They choose the source text and whatever the host passes in as input. They can call every builtin and module the engine ships or the host registers, and they will try to exhaust time, memory, stack, threads and output, to reach past the engine, or to make it fail in a way the host cannot handle.
- What is protected is the host process: its CPU, its memory and its threads, its stability (no crash, no hang), and everything the host does not hand to the script (files, network, processes, the environment, the .NET runtime).
- The boundary is the set of functions and modules the host registers. Those run with the host's privileges; see what is not guaranteed.
Test classes are under tests/PyLite.Tests/; probes are the files of tools/PyLite.Stress/bombs, grouped
by prefix in its README.
| guarantee | held by |
|---|---|
No files, sockets, processes or environment. The engine ships no module that reaches them, an import of anything not registered fails, and eval, exec, compile, open, input, globals and locals do not exist. The modules are listed in docs/modules.md. |
Builtins/BuiltinsInventoryTests (the builtin surface, pinned), Builtins/HostFunctionTableTests, Runtime/ModuleRegistryTests, Evaluator/EvaluatorPhase5Tests (unknown imports) |
No reach into .NET. Script values are the engine's own value model. Introspection does not lead out of it: __subclasses__, __globals__, __code__, __builtins__, __import__ and the format-string attribute walk find nothing to climb, and no attribute or call yields a CLR object, type or method. |
Hosting/IntrospectionEscapeTests |
Every run is bounded by ResourceLimits: steps, wall-clock deadline, allocated bytes, call depth, data depth, string and collection size, integer size, output bytes, regex time, sleep time. Crossing a limit ends the run with a BudgetExceeded result naming the limit; the script cannot catch it. |
Runtime/BudgetTests, Runtime/ResourceLimitsTests, Hosting/ResourceBoundTests (bombs of every kind end near their deadline, no thread leaked), the Stress/ classes, examples/limits with Hosting/LimitExamplesTests (one script per limit), every probe |
| The deadline holds inside long operations too: big-int arithmetic, regex, sort, string building, parsing, compression. | Hosting/ResourceBoundTests, Values/BigOpsTests, probes h, r, l |
No stack overflow. Deep recursion ends the run on MaxCallDepth; deep data raises RecursionError at MaxDataDepth; the C# stack is probed on every recursive descent, at compile time as well, so a script cannot take the process down with StackOverflowException. |
Runtime/StackGuardTests, Syntax/CompileStackSafetyTests, Syntax/SyntaxLimitsTests, Stress/ValueModelStressTests, probes d and l1-l4 |
No engine fault from script content. Whatever the script does, the host receives a typed result (RuntimeError, SyntaxError, BudgetExceeded, HostError), never an exception. |
Hosting/HostBoundaryTests (a hostile corpus through Run and TryCompile), Hosting/RunnerThreadTests, Runtime/OverflowNetTests, Modules/ArgumentFaultTests, Modules/ModuleBoundaryTests, the Fuzz category |
| The memory of a stopped run is given back. | Hosting/ResourceBoundTests (AbortedMemoryBombGraphIsCollected), Hosting/MemoryHygieneTests |
Concurrency is bounded. At most MaxConcurrentRuns runs at once (8 by default, 32 at most), and the MaxAllocBytes of the running runs together stay under GlobalMemoryCeilingBytes (1 GB by default). A run past either is refused at once (BudgetExceeded, MaxConcurrentRuns or GlobalMemoryCeiling), never queued. |
Hosting/ScriptEngineGateTests, Hosting/RunGateTests, Hosting/EngineLifecycleTests (twenty callers on four slots, three runs filling the ceiling) |
- Script code always stops at a budget check.
Runreturns at aboutDeadlineMs, plus a terminating grace of 250 ms in whichfinallyblocks may run but cannot un-fail the run. A run cancelled throughRunRequest.CancellationTokenstops the same way: its runner thread exits and no script code runs afterRunreturns (Hosting/EngineLifecycleTests,ACancelledRunLeavesNothingRunning). - A host function that blocks is the one thing that can keep a run's thread past deadline + grace +
5 s.
Runthen returns aHostError, the runner thread is leaked (its gate slot and its memory stay held until the host function returns), the engine raises aThreadLeakedevent, and onceCircuitBreakerLeakedThreadsthreads (4 by default) are leaked, the circuit breaker opens: everyRunis refused until they finish. Held byHosting/RunGateWatchdogTestsandHosting/RunnerThreadTests, and end to end throughRunbyHosting/EngineLifecycleTests(ALeakedThreadHoldsItsSlotUntilItReturns). - A long big-int operation (a multiply, division, gcd, power,
pow(b, e, m)or decimal conversion that would take more than about 10 ms) runs on a helper thread while the run's thread polls the deadline. A stopped run returns on time; that one call then finishes on its own: seconds of CPU at the defaultMaxIntBitsof 1,000,000 bits, about 16 times that at its 4,000,000-bit hard cap. The helper holds no gate slot. Held byValues/BigOpsTestsand probesli-lm.
- A host function or module the host registers runs with the host's privileges. Everything the host hands
to a script is the host's responsibility; PyLite fences those calls so that an exception in them becomes
a
HostErrorresult, nothing more, and a host function that blocks is the leaked-thread case above. - The CPU a stopped run's last big-int call still uses, as above.
- Timing side channels and resource-usage inference are out of scope.
- The default limits are generous. A host that runs untrusted scripts should set its own
ResourceLimitsper run and a ceiling per engine inEngineOptions; the engine's hard caps (deadline 300 s, 2·10⁹ steps, 1 GB allocated, call depth 512, data depth 10,000, int size 4,000,000 bits, 32 concurrent runs) apply on top of both. - Denial of service against the host by a flood of runs is the host's concern: the engine bounds each run and refuses runs past its gate, but it does not throttle callers.
Please report security issues privately through GitHub's "Report a vulnerability" form on this repository (Security → Advisories) rather than as a public issue. Include a script that reproduces the behaviour and the limits it ran under. We aim to acknowledge reports within five working days.