ExecutionConstraintTests.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. using Jint.Native.Function;
  2. using Jint.Runtime;
  3. namespace Jint.Tests.Runtime;
  4. public class ExecutionConstraintTests
  5. {
  6. [Fact]
  7. public void ShouldThrowStatementCountOverflow()
  8. {
  9. Assert.Throws<StatementsCountOverflowException>(
  10. () => new Engine(cfg => cfg.MaxStatements(100)).Evaluate("while(true);")
  11. );
  12. }
  13. [Fact]
  14. public void ShouldThrowMemoryLimitExceeded()
  15. {
  16. Assert.Throws<MemoryLimitExceededException>(
  17. () => new Engine(cfg => cfg.LimitMemory(2048)).Evaluate("a=[]; while(true){ a.push(0); }")
  18. );
  19. }
  20. [Fact]
  21. public void ShouldThrowTimeout()
  22. {
  23. Assert.Throws<TimeoutException>(
  24. () => new Engine(cfg => cfg.TimeoutInterval(new TimeSpan(0, 0, 0, 0, 500))).Evaluate("while(true);")
  25. );
  26. }
  27. [Fact]
  28. public void ShouldThrowExecutionCanceled()
  29. {
  30. Assert.Throws<ExecutionCanceledException>(
  31. () =>
  32. {
  33. using (var cts = new CancellationTokenSource())
  34. using (var waitHandle = new ManualResetEvent(false))
  35. using (var cancellationTokenSet = new ManualResetEvent(initialState: false))
  36. {
  37. var engine = new Engine(cfg => cfg.CancellationToken(cts.Token));
  38. /*
  39. * To ensure that the action "threadPoolAction" has actually been called by the ThreadPool
  40. * (which can happen very late, depending on the ThreadPool usage, etc.), the
  41. * "cancellationTokenSet" event will be an indicator for that.
  42. *
  43. * 1. The "cancellationTokenSet" will be set after the "cts" has been cancelled.
  44. * 2. Within the JS, the "test" code will only start as soon as the "cancellationTokenSet"
  45. * event will be set.
  46. * 3. The cancellationToken and its source has not been used on purpose for this test to
  47. * not mix "test infrastructure" and "test code".
  48. * 4. To verify that this test still works under heavy load, you can add
  49. * a "Thread.Sleep(10000)" call anywhere within the "threadPoolAction" action.
  50. */
  51. WaitCallback threadPoolAction = _ =>
  52. {
  53. waitHandle.WaitOne();
  54. cts.Cancel();
  55. cancellationTokenSet.Set();
  56. };
  57. ThreadPool.QueueUserWorkItem(threadPoolAction);
  58. engine.SetValue("waitHandle", waitHandle);
  59. engine.SetValue("waitForTokenToBeSet", new Action(() => cancellationTokenSet.WaitOne()));
  60. engine.SetValue("mustNotBeCalled", new Action(() =>
  61. throw new InvalidOperationException("A cancellation of the script execution has been requested, but the script did continue to run.")));
  62. engine.Evaluate(@"
  63. function sleep(millisecondsTimeout) {
  64. var totalMilliseconds = new Date().getTime() + millisecondsTimeout;
  65. while (new Date() < totalMilliseconds) { }
  66. }
  67. sleep(100);
  68. waitHandle.Set();
  69. waitForTokenToBeSet();
  70. // Now it is ensured that the cancellationToken has been cancelled.
  71. // The following JS code execution should get aborted by the engine.
  72. sleep(1000);
  73. sleep(1000);
  74. sleep(1000);
  75. sleep(1000);
  76. sleep(1000);
  77. mustNotBeCalled();
  78. ");
  79. }
  80. }
  81. );
  82. }
  83. [Fact]
  84. public void CanDiscardRecursion()
  85. {
  86. var script = @"var factorial = function(n) {
  87. if (n>1) {
  88. return n * factorial(n - 1);
  89. }
  90. };
  91. var result = factorial(500);
  92. ";
  93. Assert.Throws<RecursionDepthOverflowException>(
  94. () => new Engine(cfg => cfg.LimitRecursion()).Execute(script)
  95. );
  96. }
  97. [Fact]
  98. public void ShouldDiscardHiddenRecursion()
  99. {
  100. var script = @"var renamedFunc;
  101. var exec = function(callback) {
  102. renamedFunc = callback;
  103. callback();
  104. };
  105. var result = exec(function() {
  106. renamedFunc();
  107. });
  108. ";
  109. Assert.Throws<RecursionDepthOverflowException>(
  110. () => new Engine(cfg => cfg.LimitRecursion()).Execute(script)
  111. );
  112. }
  113. [Fact]
  114. public void ShouldRecognizeAndDiscardChainedRecursion()
  115. {
  116. var script = @" var funcRoot, funcA, funcB, funcC, funcD;
  117. var funcRoot = function() {
  118. funcA();
  119. };
  120. var funcA = function() {
  121. funcB();
  122. };
  123. var funcB = function() {
  124. funcC();
  125. };
  126. var funcC = function() {
  127. funcD();
  128. };
  129. var funcD = function() {
  130. funcRoot();
  131. };
  132. funcRoot();
  133. ";
  134. Assert.Throws<RecursionDepthOverflowException>(
  135. () => new Engine(cfg => cfg.LimitRecursion()).Execute(script)
  136. );
  137. }
  138. [Fact]
  139. public void ShouldProvideCallChainWhenDiscardRecursion()
  140. {
  141. var script = @" var funcRoot, funcA, funcB, funcC, funcD;
  142. var funcRoot = function() {
  143. funcA();
  144. };
  145. var funcA = function() {
  146. funcB();
  147. };
  148. var funcB = function() {
  149. funcC();
  150. };
  151. var funcC = function() {
  152. funcD();
  153. };
  154. var funcD = function() {
  155. funcRoot();
  156. };
  157. funcRoot();
  158. ";
  159. RecursionDepthOverflowException exception = null;
  160. try
  161. {
  162. new Engine(cfg => cfg.LimitRecursion()).Execute(script);
  163. }
  164. catch (RecursionDepthOverflowException ex)
  165. {
  166. exception = ex;
  167. }
  168. Assert.NotNull(exception);
  169. Assert.Equal("funcRoot->funcA->funcB->funcC->funcD", exception.CallChain);
  170. Assert.Equal("funcRoot", exception.CallExpressionReference);
  171. }
  172. [Fact]
  173. public void ShouldAllowShallowRecursion()
  174. {
  175. var script = @"var factorial = function(n) {
  176. if (n>1) {
  177. return n * factorial(n - 1);
  178. }
  179. };
  180. var result = factorial(8);
  181. ";
  182. new Engine(cfg => cfg.LimitRecursion(20)).Execute(script);
  183. }
  184. [Fact]
  185. public void ShouldDiscardDeepRecursion()
  186. {
  187. var script = @"var factorial = function(n) {
  188. if (n>1) {
  189. return n * factorial(n - 1);
  190. }
  191. };
  192. var result = factorial(38);
  193. ";
  194. Assert.Throws<RecursionDepthOverflowException>(
  195. () => new Engine(cfg => cfg.LimitRecursion(20)).Execute(script)
  196. );
  197. }
  198. [Fact]
  199. public void ShouldAllowRecursionLimitWithoutReferencedName()
  200. {
  201. const string input = @"(function () {
  202. var factorial = function(n) {
  203. if (n>1) {
  204. return n * factorial(n - 1);
  205. }
  206. };
  207. var result = factorial(38);
  208. })();
  209. ";
  210. var engine = new Engine(o => o.LimitRecursion(20));
  211. Assert.Throws<RecursionDepthOverflowException>(() => engine.Execute(input));
  212. }
  213. [Fact]
  214. public void ShouldLimitRecursionWithAllFunctionInstances()
  215. {
  216. var engine = new Engine(cfg =>
  217. {
  218. // Limit recursion to 5 invocations
  219. cfg.LimitRecursion(5);
  220. cfg.Strict();
  221. });
  222. var ex = Assert.Throws<RecursionDepthOverflowException>(() => engine.Evaluate(@"
  223. var myarr = new Array(5000);
  224. for (var i = 0; i < myarr.length; i++) {
  225. myarr[i] = function(i) {
  226. myarr[i + 1](i + 1);
  227. }
  228. }
  229. myarr[0](0);
  230. "));
  231. }
  232. [Fact]
  233. public void ShouldLimitRecursionWithGetters()
  234. {
  235. const string code = @"var obj = { get test() { return this.test + '2'; } }; obj.test;";
  236. var engine = new Engine(cfg => cfg.LimitRecursion(10));
  237. Assert.Throws<RecursionDepthOverflowException>(() => engine.Evaluate(code));
  238. }
  239. [Fact]
  240. public void ShouldLimitArraySizeForConcat()
  241. {
  242. var engine = new Engine(o => o.MaxStatements(1_000).MaxArraySize(1_000_000));
  243. Assert.Throws<MemoryLimitExceededException>(() => engine.Evaluate("for (let a = [1, 2, 3];; a = a.concat(a)) ;"));
  244. }
  245. [Fact]
  246. public void ShouldLimitArraySizeForFill()
  247. {
  248. var engine = new Engine(o => o.MaxStatements(1_000).MaxArraySize(1_000_000));
  249. Assert.Throws<MemoryLimitExceededException>(() => engine.Evaluate("var arr = Array(1000000000).fill(new Array(1000000000));"));
  250. }
  251. [Fact]
  252. public void ShouldLimitArraySizeForJoin()
  253. {
  254. var engine = new Engine(o => o.MaxStatements(1_000).MaxArraySize(1_000_000));
  255. Assert.Throws<MemoryLimitExceededException>(() => engine.Evaluate("new Array(2147483647).join('*')"));
  256. }
  257. [Fact]
  258. public void ShouldConsiderConstraintsWhenCallingInvoke()
  259. {
  260. var engine = new Engine(options =>
  261. {
  262. options.TimeoutInterval(TimeSpan.FromMilliseconds(100));
  263. });
  264. var myApi = new MyApi();
  265. engine.SetValue("myApi", myApi);
  266. engine.Execute("myApi.addEventListener('DataReceived', (data) => { myApi.log(data) })");
  267. var dataReceivedCallbacks = myApi.Callbacks.Where(kvp => kvp.Key == "DataReceived");
  268. foreach (var callback in dataReceivedCallbacks)
  269. {
  270. engine.Invoke(callback.Value, "Data Received #1");
  271. Thread.Sleep(101);
  272. engine.Invoke(callback.Value, "Data Received #2");
  273. }
  274. }
  275. [Fact]
  276. public void ResetConstraints()
  277. {
  278. void ExecuteAction(Engine engine) => engine.Execute("recursion()");
  279. void InvokeAction(Engine engine) => engine.Invoke("recursion");
  280. List<int> expected = [6, 6, 6, 6, 6];
  281. Assert.Equal(expected, RunLoop(CreateEngine(), ExecuteAction));
  282. Assert.Equal(expected, RunLoop(CreateEngine(), InvokeAction));
  283. var e1 = CreateEngine();
  284. Assert.Equal(expected, RunLoop(e1, ExecuteAction));
  285. Assert.Equal(expected, RunLoop(e1, InvokeAction));
  286. var e2 = CreateEngine();
  287. Assert.Equal(expected, RunLoop(e2, InvokeAction));
  288. Assert.Equal(expected, RunLoop(e2, ExecuteAction));
  289. var e3 = CreateEngine();
  290. Assert.Equal(expected, RunLoop(e3, InvokeAction));
  291. Assert.Equal(expected, RunLoop(e3, ExecuteAction));
  292. Assert.Equal(expected, RunLoop(e3, InvokeAction));
  293. var e4 = CreateEngine();
  294. Assert.Equal(expected, RunLoop(e4, InvokeAction));
  295. Assert.Equal(expected, RunLoop(e4, InvokeAction));
  296. }
  297. private static Engine CreateEngine()
  298. {
  299. Engine engine = new(options => options.LimitRecursion(5));
  300. return engine.Execute("""
  301. var num = 0;
  302. function recursion() {
  303. num++;
  304. recursion(num);
  305. }
  306. """);
  307. }
  308. private static List<int> RunLoop(Engine engine, Action<Engine> engineAction)
  309. {
  310. List<int> results = new();
  311. for (var i = 0; i < 5; i++)
  312. {
  313. try
  314. {
  315. engine.SetValue("num", 0);
  316. engineAction.Invoke(engine);
  317. }
  318. catch (RecursionDepthOverflowException)
  319. {
  320. results.Add((int) engine.GetValue("num").AsNumber());
  321. }
  322. }
  323. return results;
  324. }
  325. private class MyApi
  326. {
  327. public readonly Dictionary<string, ScriptFunction> Callbacks = new Dictionary<string, ScriptFunction>();
  328. public void AddEventListener(string eventName, ScriptFunction callback)
  329. {
  330. Callbacks.Add(eventName, callback);
  331. }
  332. public void Log(string logMessage)
  333. {
  334. Console.WriteLine(logMessage);
  335. }
  336. }
  337. }