Skip to content

Commit a6c7ff3

Browse files
authored
Fixes thread-safety issue in calculated field script evaluation (#185)
* Fixes thread-safety issue in calculated field script evaluation Root Cause: The Jint Engine is not thread-safe. When Parallel.ForEachAsync was used to add multiple employees concurrently, multiple threads called EnsureExpressionFunction, SetSource, and GetValue simultaneously on the same engine instance, corrupting its internal parser state and causing the NullReferenceException in Acornima.Tokenizer.ReadWord1(). * Fixes jint threading issues with locking Addresses potential race conditions in the Jint script service by ensuring proper locking around critical operations. This change ensures that all access to shared resources within the script service is synchronized, preventing data corruption and unexpected behavior in multithreaded scenarios.
1 parent 3bbf321 commit a6c7ff3

File tree

1 file changed

+74
-22
lines changed

1 file changed

+74
-22
lines changed

tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Configuration/Indexes/CalculatedIntegerFieldType.cs

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
using System;
1+
using System;
22
using System.Collections.Concurrent;
33
using System.Security.Cryptography;
44
using System.Text;
55
using System.Threading.Tasks;
6+
using Foundatio.AsyncEx;
67
using Foundatio.Serializer;
78
using Jint;
89
using Jint.Native;
@@ -20,25 +21,21 @@ public CalculatedIntegerFieldType(ScriptService scriptService)
2021
_scriptService = scriptService;
2122
}
2223

23-
public override Task<ProcessFieldValueResult> ProcessValueAsync<T>(T document, object value, CustomFieldDefinition fieldDefinition) where T : class
24+
public override async Task<ProcessFieldValueResult> ProcessValueAsync<T>(T document, object value, CustomFieldDefinition fieldDefinition) where T : class
2425
{
2526
if (!fieldDefinition.Data.TryGetValue("Expression", out object expression))
26-
return base.ProcessValueAsync(document, value, fieldDefinition);
27+
return await base.ProcessValueAsync(document, value, fieldDefinition);
2728

28-
string functionName = _scriptService.EnsureExpressionFunction(expression.ToString());
29-
_scriptService.SetSource(document);
30-
31-
var calculatedValue = _scriptService.GetValue(functionName);
29+
var calculatedValue = await _scriptService.EvaluateForSourceAsync(document, expression.ToString());
3230

3331
// TODO: Implement a consecutive errors counter that disables badly behaving expressions
34-
3532
if (calculatedValue.IsCancelled)
36-
return Task.FromResult(new ProcessFieldValueResult { Value = null });
33+
return new ProcessFieldValueResult { Value = null };
3734

38-
if (calculatedValue.Value is double doubleValue && Double.IsNaN(doubleValue))
39-
return Task.FromResult(new ProcessFieldValueResult { Value = null });
35+
if (calculatedValue.Value is Double.NaN)
36+
return new ProcessFieldValueResult { Value = null };
4037

41-
return Task.FromResult(new ProcessFieldValueResult { Value = calculatedValue.Value });
38+
return new ProcessFieldValueResult { Value = calculatedValue.Value };
4239
}
4340
}
4441

@@ -47,6 +44,7 @@ public class ScriptService
4744
private readonly ITextSerializer _serializer;
4845
private readonly ILogger<ScriptService> _logger;
4946
private readonly ConcurrentDictionary<string, string> _registeredExpressions = new();
47+
private readonly AsyncLock _lock = new();
5048

5149
public ScriptService(ITextSerializer jsonSerializer, ILogger<ScriptService> logger)
5250
{
@@ -57,20 +55,50 @@ public ScriptService(ITextSerializer jsonSerializer, ILogger<ScriptService> logg
5755

5856
public Engine Engine { get; }
5957

58+
/// <summary>
59+
/// Thread-safe method that atomically evaluates an expression for a given source document.
60+
/// The Jint Engine is not thread-safe, so this method uses a lock to ensure only one evaluation occurs at a time.
61+
/// </summary>
62+
public async Task<ScriptValueResult> EvaluateForSourceAsync<T>(T source, string expression) where T : class
63+
{
64+
using (await _lock.LockAsync().ConfigureAwait(false))
65+
{
66+
string functionName = EnsureExpressionFunctionInternal(expression);
67+
SetSourceInternal(source);
68+
return GetValueInternal(functionName);
69+
}
70+
}
71+
6072
public string EnsureExpressionFunction(string expression)
6173
{
62-
if (_registeredExpressions.TryGetValue(expression, out var functionName))
74+
using (_lock.Lock())
75+
{
76+
return EnsureExpressionFunctionInternal(expression);
77+
}
78+
}
79+
80+
private string EnsureExpressionFunctionInternal(string expression)
81+
{
82+
if (_registeredExpressions.TryGetValue(expression, out string functionName))
6383
return functionName;
6484

6585
functionName = "_" + ComputeSha256Hash(expression);
66-
RegisterFunction(functionName, expression);
86+
RegisterFunctionInternal(functionName, expression);
6787

6888
_registeredExpressions.TryAdd(expression, functionName);
6989

7090
return functionName;
7191
}
7292

7393
public void RegisterFunction(string name, string body)
94+
{
95+
using (_lock.Lock())
96+
{
97+
RegisterFunctionInternal(name, body);
98+
}
99+
}
100+
101+
private void RegisterFunctionInternal(string name, string body)
74102
{
75103
if (String.IsNullOrEmpty(name))
76104
throw new ArgumentNullException(nameof(name));
@@ -83,7 +111,7 @@ public void RegisterFunction(string name, string body)
83111

84112
body = body.Trim();
85113
string script;
86-
if (body.StartsWith("{"))
114+
if (body.StartsWith('{'))
87115
{
88116
script = $"function {name}() {body}";
89117
}
@@ -100,15 +128,32 @@ public void RegisterFunction(string name, string body)
100128
}
101129

102130
public void SetSource(object source)
131+
{
132+
using (_lock.Lock())
133+
{
134+
SetSourceInternal(source);
135+
}
136+
}
137+
138+
private void SetSourceInternal(object source)
103139
{
104140
string json = _serializer.SerializeToString(source);
105-
if (json == null)
141+
if (json is null)
106142
json = "null";
143+
107144
string script = $"var source = {json};";
108145
Engine.Execute(script);
109146
}
110147

111148
public ScriptValueResult GetValue(string functionName)
149+
{
150+
using (_lock.Lock())
151+
{
152+
return GetValueInternal(functionName);
153+
}
154+
}
155+
156+
private ScriptValueResult GetValueInternal(string functionName)
112157
{
113158
string script = $"{functionName}()";
114159
try
@@ -126,6 +171,14 @@ public ScriptValueResult GetValue(string functionName)
126171
}
127172

128173
public object ExecuteExpression(string expression)
174+
{
175+
using (_lock.Lock())
176+
{
177+
return ExecuteExpressionInternal(expression);
178+
}
179+
}
180+
181+
private object ExecuteExpressionInternal(string expression)
129182
{
130183
if (String.IsNullOrEmpty(expression))
131184
throw new ArgumentNullException(nameof(expression));
@@ -140,7 +193,7 @@ public object ExecuteExpression(string expression)
140193
}
141194
}
142195

143-
private bool IsValidJavaScriptIdentifier(string identifier)
196+
private static bool IsValidJavaScriptIdentifier(string identifier)
144197
{
145198
if (String.IsNullOrEmpty(identifier))
146199
return false;
@@ -152,15 +205,14 @@ private bool IsValidJavaScriptIdentifier(string identifier)
152205
return Array.TrueForAll(identifier.ToCharArray(), c => Char.IsLetterOrDigit(c) || c == '_');
153206
}
154207

155-
private string ComputeSha256Hash(string value)
208+
private static string ComputeSha256Hash(string value)
156209
{
157210
using var sha256Hash = SHA256.Create();
158-
159211
byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(value));
160212

161213
var builder = new StringBuilder();
162-
for (int i = 0; i < bytes.Length; i++)
163-
builder.Append(bytes[i].ToString("x2"));
214+
foreach (byte b in bytes)
215+
builder.Append(b.ToString("x2"));
164216

165217
return builder.ToString();
166218
}
@@ -196,7 +248,7 @@ public ScriptValueResult(object value, bool isCancelled = false)
196248
public object Value { get; }
197249
public bool IsCancelled { get; }
198250

199-
public static ScriptValueResult Cancelled = new ScriptValueResult(null, true);
251+
public static readonly ScriptValueResult Cancelled = new(null, true);
200252
}
201253

202254
public class JintEnumConverter : IObjectConverter

0 commit comments

Comments
 (0)