2e9160317f
R05:10 — the namespace deny-list covered System.Threading.Tasks but not System.Threading, so SemaphoreSlim / Monitor / SpinWait / Mutex / EventWaitHandle could park a script's pool thread past its budget (or touch a system-global kernel object); System.Type.GetType(string) / GetMethod* / InvokeMember and System.Runtime.CompilerServices.RuntimeHelpers resolved and reach a forbidden type without naming it syntactically. ForbiddenTypeAnalyzer now denies the whole System.Threading namespace with a type-granular allow-list (CancellationToken, Interlocked, Volatile — the sync-free primitives), adds a member deny-list for System.Type's reflection entry points (GetType/GetMethod(s)/InvokeMember/GetProperty(ies)/GetField(s)/ GetMember(s)/GetConstructor(s)/GetEvent(s)/GetDefaultMembers; instance object.GetType() stays allowed) and puts RuntimeHelpers on the type deny-list. Thread/ThreadPool/Timer leave the type list — the prefix rule reaches them. R05:11 — the allocation budget landed with E1's ScriptRunGuard (Scripting:MaxAllocBytesPerRun, default 64 MB): sampled around each run, breach quarantines. It is observation after the fact; a pre-emptive OOM guard still needs an out-of-process runner (follow-up issue in the lane report). The Monaco editor shares the analyzer, so DiagnoseTests pins every new form as an OTSCRIPT_FORBIDDEN marker and every allow-listed form as clean. Refs #734 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
71 lines
3.6 KiB
C#
71 lines
3.6 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis;
|
|
|
|
public sealed class DiagnoseTests
|
|
{
|
|
private static readonly ScriptAnalysisService Svc = new();
|
|
private static IReadOnlyList<DiagnosticMarker> Diag(string code) => Svc.Diagnose(new DiagnoseRequest(code)).Markers;
|
|
|
|
[Fact] public void Clean_script_has_no_markers()
|
|
=> Diag("""return (double)ctx.GetTag("A").Value * 2.0;""").ShouldBeEmpty();
|
|
|
|
[Fact] public void Roslyn_error_is_reported_on_user_line_1()
|
|
{
|
|
var m = Diag("return ctx.NoSuchMember();");
|
|
m.ShouldNotBeEmpty();
|
|
m.ShouldContain(x => x.Severity == 8);
|
|
m.First(x => x.Severity == 8).StartLineNumber.ShouldBe(1); // #line 1 → user coords (GetMappedLineSpan)
|
|
}
|
|
|
|
[Fact] public void Roslyn_error_maps_to_correct_user_line()
|
|
{
|
|
// error on the THIRD user line — proves mapping isn't off by the preamble height
|
|
var m = Diag("var a = 1;\nvar b = 2;\nreturn ctx.Nope();");
|
|
m.ShouldContain(x => x.Severity == 8 && x.StartLineNumber == 3);
|
|
}
|
|
|
|
[Fact] public void Forbidden_or_unresolved_dangerous_type_is_flagged()
|
|
=> Diag("""var c = new System.Net.Http.HttpClient(); return 0;""")
|
|
.ShouldContain(x => x.Severity == 8);
|
|
|
|
// Sandbox hardening parity (R05:10): the editor runs the SAME ForbiddenTypeAnalyzer the publish gate
|
|
// runs, so every newly-denied form must surface as an OTSCRIPT_FORBIDDEN marker here — editor accepts
|
|
// ⇔ publish accepts. One case per deny mechanism (namespace, member, type), plus the allow-list.
|
|
[Theory]
|
|
[InlineData("""new System.Threading.SemaphoreSlim(0).Wait(); return 0;""")] // System.Threading namespace
|
|
[InlineData("""System.Threading.SpinWait.SpinUntil(() => false); return 0;""")]
|
|
[InlineData("""var t = System.Type.GetType("System.IO.File"); return t is null ? 0 : 1;""")] // forbidden member
|
|
[InlineData("""return typeof(string).GetMethods().Length;""")]
|
|
[InlineData("""var o = System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(string)); return 0;""")] // forbidden type
|
|
public void Hardened_sandbox_forms_are_flagged_in_the_editor(string code)
|
|
=> Diag(code).ShouldContain(x => x.Code == "OTSCRIPT_FORBIDDEN" && x.Severity == 8);
|
|
|
|
[Theory]
|
|
[InlineData("""var t = System.Threading.CancellationToken.None; return t.IsCancellationRequested ? 1 : 0;""")]
|
|
[InlineData("""var x = 41; System.Threading.Interlocked.Increment(ref x); return x;""")]
|
|
[InlineData("""var x = 42; return System.Threading.Volatile.Read(ref x);""")]
|
|
[InlineData("""object o = 42; return o.GetType().IsValueType ? 1 : 0;""")]
|
|
public void Allow_listed_forms_are_not_flagged_in_the_editor(string code)
|
|
=> Diag(code).ShouldBeEmpty();
|
|
|
|
[Fact] public void Dynamic_tag_path_is_flagged()
|
|
=> Diag("""var p = "A"; return ctx.GetTag(p).Value;""")
|
|
.ShouldContain(x => x.Message.Contains("literal"));
|
|
|
|
[Fact] public void Missing_return_does_not_produce_a_phantom_preamble_marker()
|
|
=> Diag("var x = 1;").ShouldNotContain(m => m.Code == "CS0161");
|
|
|
|
[Fact] public void Empty_code_has_no_markers()
|
|
=> Diag("").ShouldBeEmpty();
|
|
|
|
[Fact] public void Crlf_source_maps_dynamic_path_to_the_correct_line()
|
|
{
|
|
// \r\n line endings; the dynamic-path rejection must land on user line 3, not be column-shifted.
|
|
var m = Diag("var a = 1;\r\nvar b = 2;\r\nreturn ctx.GetTag(b > 0 ? \"x\" : \"y\").Value;");
|
|
m.ShouldContain(x => x.Message.Contains("literal") && x.StartLineNumber == 3);
|
|
}
|
|
}
|