ExecutionConstraintTests.cs 13 KB

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