fix(scripting): deny System.AppContext and other process-wide mutators in the script sandbox (lmxopcua#792)
v2-ci / unit-tests (push) Waiting to run

ForbiddenTypeAnalyzer now denies System.AppContext, System.Console, GCSettings, ProfileOptimization,
EncodingProvider/CodePagesEncodingProvider, TypeDescriptor and CryptoConfig as types, and the ambient
CultureInfo.{DefaultThreadCurrentCulture,DefaultThreadCurrentUICulture,CurrentCulture,CurrentUICulture}
and TimeZoneInfo.ClearCachedData members (member deny-list now covers properties and member-access reads).
Shadow-mode tests diverge via culture-sensitive string.Compare (worker is InvariantGlobalization) instead of
AppContext.BaseDirectory, with a premise test. Mutation-proved: 21 new cases fail with the analyzer reverted.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-10 07:48:39 -05:00
parent b1eedceacf
commit 0e96dd08a2
5 changed files with 161 additions and 27 deletions
@@ -1133,8 +1133,9 @@ other work on the box, 10 s phases):**
hangs, blocks its caller or throws never reaches the caller and is never a mismatch; MaxInFlight drop accounting;
inputs copied before `Mirror` returns.
- `ShadowModeTests` (real worker): eleven scripts over short / double / string / arrays / DateTime / bool / status /
a throw publish exactly what InProcess publishes and match; a deliberately divergent script (it reads
`AppContext.BaseDirectory`, so it answers differently in the worker) and a `decimal` result are counted against
a throw publish exactly what InProcess publishes and match; a deliberately divergent script (it does a culture-sensitive
`string.Compare("a", "B")`: the worker runs `InvariantGlobalization` so it compares ordinally and answers differently;
originally it read `AppContext.BaseDirectory`, which lmxopcua#792 then denied) and a `decimal` result are counted against
exactly those two scripts (`value`, `unsupported_type`). A runaway that only runs away in the worker is killed twice
and quarantined **there** while every published value stays the in-process one, no published evaluation waits more
than 250 ms, and its sibling on the killed shard (a victim, not compared) has no mismatch. Alarms: the emission
@@ -1146,8 +1147,10 @@ other work on the box, 10 s phases):**
VirtualTags 16, Calculation 84, ScriptWorker 160/4 skip, Host 275, Runtime 1002/3 skip, OpcUaServer.IntegrationTests
133. windev (Windows VM, scratch dir under `C:\build`): ScriptWorker suite 160 passed / 4 skipped, 0 warnings. The first
windev run caught a test bug (a `DateTime` seconds argument past 59 once the slower VM looped longer), fixed before the commit.
- Found while writing them: scripts can reach `System.AppContext` (`SetSwitch` / `SetData` / `GetData`) →
lmxopcua#792.
- Found while writing them: scripts could reach `System.AppContext` (`SetSwitch` / `SetData` / `GetData`) →
lmxopcua#792, fixed: `System.AppContext`, `System.Console`, `GCSettings`, `ProfileOptimization`, `EncodingProvider`,
`TypeDescriptor`, `CryptoConfig` and the ambient `CultureInfo` / `TimeZoneInfo.ClearCachedData` members are now denied
by `ForbiddenTypeAnalyzer` (in-process and in the worker, one analyzer).
**docker-dev drill** (image `otopcua-host:w34-shadow` built from this branch; `central-1`/`central-2` only; drill rows
`w34-*` inserted by SQL: VTs `w34mul` (HR5×3), `w34raw` (`var v = …HR5.Value; return v;` — compiled, returns the boxed