Skip to content

Commit 9c67719

Browse files
committed
Add support for isolated execution context
* introduce IScriptExecutor to narrow down interface * add engine factories and refine interfaces * make constraint disabling more generic, allow options to build constraints * support disabling constrains when initializing engine pool, fix statement counting * add test cases to show behavior * shape engine pool api
1 parent 978d319 commit 9c67719

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1297
-92
lines changed

Jint.Benchmark/EnginePoolBenchmark.cs

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using BenchmarkDotNet.Attributes;
5+
using Jint.Pooling;
6+
7+
namespace Jint.Benchmark
8+
{
9+
[MemoryDiagnoser]
10+
public class EnginePoolBenchmark
11+
{
12+
private static readonly Options options = new Options().Strict();
13+
14+
private static readonly List<string> files = new List<string>
15+
{
16+
"dromaeo-3d-cube",
17+
"dromaeo-core-eval",
18+
"dromaeo-object-array",
19+
"dromaeo-object-regexp",
20+
"dromaeo-object-string",
21+
"dromaeo-string-base64"
22+
};
23+
24+
private IsolatedEngineFactory factory;
25+
private Engine _engine;
26+
27+
[GlobalSetup]
28+
public void Setup()
29+
{
30+
factory = new IsolatedEngineFactory(options, InitializeEngine);
31+
32+
_engine = new Engine(options);
33+
InitializeEngine(_engine);
34+
}
35+
36+
/// <summary>
37+
/// Test case where we discard the pool and data.
38+
/// </summary>
39+
[Benchmark]
40+
public void RunUsingPooledIsolated()
41+
{
42+
using var engine = factory.Build();
43+
RunScript(engine);
44+
}
45+
46+
/// <summary>
47+
/// Using one engine that isn't able to clean up.
48+
/// </summary>
49+
[Benchmark]
50+
public void RunUsingDirtiedEngineInstance()
51+
{
52+
RunScript(_engine);
53+
}
54+
55+
/// <summary>
56+
/// Test case to do always the manual labour.
57+
/// </summary>
58+
[Benchmark]
59+
public void RunUsingNewEngineInstance()
60+
{
61+
var freshEngine = new Engine(options);
62+
InitializeEngine(freshEngine);
63+
RunScript(freshEngine);
64+
}
65+
66+
private static void RunScript(IScriptExecutor engine)
67+
{
68+
for (int i = 5; i < 20; ++i)
69+
{
70+
engine.Evaluate($"Init({i});");
71+
}
72+
}
73+
74+
private static void InitializeEngine(IEngine engine)
75+
{
76+
engine.SetValue("log", new Action<object>(Console.WriteLine));
77+
engine.SetValue("assert", new Action<bool>(b => { }));
78+
engine.SetValue("name", 123);
79+
engine.Evaluate(@"
80+
var startTest = function () { };
81+
var test = function (name, fn) { fn(); };
82+
var endTest = function () { };
83+
var prep = function (fn) { fn(); };
84+
");
85+
86+
foreach (var fileName in files)
87+
{
88+
var content = File.ReadAllText($"Scripts/dromaeo/{fileName}.js");
89+
engine.Evaluate(content);
90+
}
91+
}
92+
}
93+
}

Jint.Tests/Jint.Tests.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<AssemblyOriginatorKeyFile>..\Jint\Jint.snk</AssemblyOriginatorKeyFile>
66
<SignAssembly>true</SignAssembly>
77
<IsPackable>false</IsPackable>
8+
<LangVersion>latest</LangVersion>
89
</PropertyGroup>
910
<ItemGroup>
1011
<EmbeddedResource Include="Runtime\Scripts\*.*;Parser\Scripts\*.*" />
@@ -16,6 +17,7 @@
1617
<Reference Include="Microsoft.CSharp" Condition=" '$(TargetFramework)' == 'net461' " />
1718
</ItemGroup>
1819
<ItemGroup>
20+
<PackageReference Include="FluentAssertions" Version="5.10.3" />
1921
<PackageReference Include="Flurl.Http.Signed" Version="3.0.0" />
2022
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
2123
<PackageReference Include="MongoDB.Bson.signed" Version="2.11.2" />

Jint.Tests/Runtime/EnginePoolTests.cs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using FluentAssertions;
2+
using Jint.Pooling;
3+
using Jint.Runtime;
4+
using Xunit;
5+
6+
namespace Jint.Tests.Runtime
7+
{
8+
public class EnginePoolTests
9+
{
10+
[Fact]
11+
public void LimitsShouldNotBeActiveDuringInitialization()
12+
{
13+
var options = new Options()
14+
.Strict()
15+
.MaxStatements(2);
16+
17+
// we cannot run three statements with this configuration
18+
const string script = "Math.max(1, 2); Math.max(1, 2); Math.max(1, 2);";
19+
20+
var pool = new IsolatedEngineFactory(options, e =>
21+
{
22+
// unless we are initializing the engine
23+
e.Evaluate(script);
24+
});
25+
26+
using var engine = pool.Build();
27+
28+
// no longer allowed for many statements
29+
var ex = Assert.Throws<StatementsCountOverflowException>(() => engine.Evaluate(script));
30+
ex.Message.Should().Be("The maximum number of statements executed has reached allowed limit (2).");
31+
32+
// allowed for less statements
33+
engine.Evaluate("Math.max(1, 2); Math.max(1, 2);");
34+
}
35+
}
36+
}
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using FluentAssertions;
3+
using Jint.Native;
4+
using Jint.Runtime;
5+
using Xunit;
6+
7+
namespace Jint.Tests.Runtime
8+
{
9+
public class IsolatedContextTests
10+
{
11+
[Fact]
12+
public void IsolatedContextCanBeUsedAndDisposed()
13+
{
14+
var engineFactory = new DefaultEngineFactory(new Options().Strict());
15+
var engine = engineFactory.Build();
16+
engine.SetValue("assert", new Action<bool>(Assert.True));
17+
18+
// Set outer variable in global scope
19+
engine.SetValue("outer", 123);
20+
engine.SetValue("outerClrArray", new[] { "a", "b", "c" });
21+
engine.Evaluate("var outerJsArray = [ 'a', 'b', 'c' ];");
22+
engine.Evaluate("assert(outer === 123)");
23+
engine.Evaluate("outer").ToObject().Should().Be(123);
24+
25+
// outer scope functions and trusted, they can break things, so be aware
26+
engine.Evaluate("function outerBreaker() { var m = Math.max; delete Math.max; assert(typeof Math.max === 'undefined'); Math.max = m; }");
27+
28+
// Enter new execution context
29+
using (engine.ActivateIsolatedContext())
30+
{
31+
// Can see global scope
32+
engine.Evaluate("assert(outer === 123)");
33+
34+
// Can modify global scope
35+
engine.Evaluate("outer = 321");
36+
engine.Evaluate("outer").ToObject().Should().Be(321);
37+
38+
// Create variable in new context
39+
engine.Evaluate("var inner = 456");
40+
engine.Evaluate("assert(inner === 456)");
41+
42+
engine.Evaluate("function innerBreaker() { Math.max = null; }");
43+
44+
engine.Evaluate("var m = Math.max; outerBreaker();");
45+
engine.Evaluate("Math.max").Should().BeAssignableTo<ICallable>();
46+
engine.Evaluate("m === Math.max").AsBoolean().Should().BeTrue();
47+
48+
// cannot break anything
49+
Assert.Throws<NotSupportedException>(() => engine.Evaluate("innerBreaker();"));
50+
Assert.Throws<NotSupportedException>(() => engine.Evaluate("Math.max = null;"));
51+
Assert.Throws<NotSupportedException>(() => engine.Evaluate("outerJsArray[0] = null;"));
52+
Assert.Throws<NotSupportedException>(() => engine.Evaluate("outerClrArray[0] = null;"));
53+
54+
// but we can redeclare the whole math thing and make it "better" in this context
55+
engine.Evaluate("var Math = ({});");
56+
engine.Evaluate("Math.max = () => 42;");
57+
58+
engine.Evaluate("outerJsArray = [ 'c' ];");
59+
engine.Evaluate("outerJsArray[0]").AsString().Should().Be("c");
60+
61+
var maxInner = engine.Evaluate("Math.max(1, 2);").AsNumber();
62+
maxInner.Should().Be(42);
63+
}
64+
65+
// The new variable is no longer visible
66+
engine.Evaluate("assert(typeof inner === 'undefined')");
67+
68+
// The new variable is not in the global scope
69+
var ex = Assert.Throws<JavaScriptException>(() => engine.Evaluate("inner"));
70+
ex.Message.Should().Be("inner is not defined");
71+
72+
var max = engine.Evaluate("Math.max");
73+
max.ToObject().Should().NotBeNull();
74+
75+
// and we should again get reasonable results from max
76+
var maxOuter = engine.Evaluate("Math.max(1, 2);").AsNumber();
77+
maxOuter.Should().Be(2);
78+
79+
// array back to itself
80+
engine.Evaluate("outerJsArray[0]").AsString().Should().Be("a");
81+
82+
// but can again break things
83+
engine.Evaluate("Math.max = null;");
84+
engine.Evaluate("Math.max").ToObject().Should().BeNull();
85+
86+
engine.Evaluate("outerBreaker();");
87+
engine.Evaluate("Math.max").ToObject().Should().BeNull();
88+
}
89+
}
90+
}

Jint/Constraints/ConstraintsOptionsExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static Options LimitMemory(this Options options, long memoryLimit)
2121
{
2222
options.WithoutConstraint(x => x is MemoryLimit);
2323

24-
if (memoryLimit > 0 && memoryLimit < int.MaxValue)
24+
if (memoryLimit > 0 && memoryLimit < long.MaxValue)
2525
{
2626
options.Constraint(new MemoryLimit(memoryLimit));
2727
}

Jint/Constraints/MaxStatements.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ public MaxStatements(int maxStatements)
1414

1515
public void Check()
1616
{
17-
if (_maxStatements > 0 && _statementsCount++ > _maxStatements)
17+
if (_maxStatements > 0 && _statementsCount >= _maxStatements)
1818
{
19-
ExceptionHelper.ThrowStatementsCountOverflowException();
19+
ExceptionHelper.ThrowStatementsCountOverflowException(_statementsCount, _maxStatements);
2020
}
21+
22+
_statementsCount++;
2123
}
2224

2325
public void Reset()

Jint/Constraints/MemoryLimit.cs

+16-13
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,31 @@ static MemoryLimit()
2121

2222
public MemoryLimit(long memoryLimit)
2323
{
24+
if (GetAllocatedBytesForCurrentThread is null)
25+
{
26+
ExceptionHelper.ThrowPlatformNotSupportedException("The current platform doesn't support MemoryLimit.");
27+
}
28+
if (memoryLimit <= 0)
29+
{
30+
ExceptionHelper.ThrowArgumentException("Memory limit must be positive, non-zero value");
31+
}
2432
_memoryLimit = memoryLimit;
2533
}
2634

2735
public void Check()
2836
{
29-
if (_memoryLimit > 0)
37+
var memoryUsage = GetAllocatedBytesForCurrentThread() - _initialMemoryUsage;
38+
if (memoryUsage > _memoryLimit)
3039
{
31-
if (GetAllocatedBytesForCurrentThread != null)
32-
{
33-
var memoryUsage = GetAllocatedBytesForCurrentThread() - _initialMemoryUsage;
34-
if (memoryUsage > _memoryLimit)
35-
{
36-
ExceptionHelper.ThrowMemoryLimitExceededException($"Script has allocated {memoryUsage} but is limited to {_memoryLimit}");
37-
}
38-
}
39-
else
40-
{
41-
ExceptionHelper.ThrowPlatformNotSupportedException("The current platform doesn't support MemoryLimit.");
42-
}
40+
ThrowMemoryLimitExceededException(memoryUsage);
4341
}
4442
}
4543

44+
private void ThrowMemoryLimitExceededException(long memoryUsage)
45+
{
46+
throw new MemoryLimitExceededException($"Script has allocated {memoryUsage} but is limited to {_memoryLimit}");
47+
}
48+
4649
public void Reset()
4750
{
4851
if (GetAllocatedBytesForCurrentThread != null)

Jint/DefaultEngineFactory.cs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#nullable enable
2+
3+
using System;
4+
5+
namespace Jint
6+
{
7+
/// <summary>
8+
/// Default engine factory for creating <see cref="IEngine" /> instances.
9+
/// </summary>
10+
public sealed class DefaultEngineFactory : EngineFactory<IEngine>
11+
{
12+
/// <summary>
13+
/// Creates a new instance default engine factory.
14+
/// </summary>
15+
/// <param name="options">Options to use for each new Engine instance.</param>
16+
/// <param name="initialize">Actions to run against the engine to create initial state, any execution constraints are not honored during this step!</param>
17+
public DefaultEngineFactory(Options options, Action<IEngine>? initialize = null) : base(options, initialize)
18+
{
19+
}
20+
21+
public override IEngine Build()
22+
{
23+
return CreateAndInitializeEngine();
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)