Skip to content

Commit 11b7592

Browse files
authored
Fix uncatchable error inside a promise
Fixes: #810
1 parent 8132347 commit 11b7592

File tree

4 files changed

+194
-12
lines changed

4 files changed

+194
-12
lines changed

.github/workflows/ci.yml

+23
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ jobs:
169169
run: |
170170
./build/qjs -c examples/hello.js -o hello
171171
./hello
172+
173+
- name: test interrupt
174+
run: |
175+
./build/interrupt-test
172176
173177
windows-msvc:
174178
runs-on: windows-latest
@@ -198,6 +202,9 @@ jobs:
198202
run: |
199203
build\${{matrix.buildType}}\qjs.exe -c examples\hello.js -o hello.exe
200204
.\hello.exe
205+
- name: test interrupt
206+
run: |
207+
build\${{matrix.buildType}}\interrupt-test.exe
201208
- name: Set up Visual Studio shell
202209
uses: egor-tensin/vs-shell@v2
203210
with:
@@ -260,6 +267,9 @@ jobs:
260267
build\${{matrix.buildType}}\qjs.exe examples\test_point.js
261268
build\${{matrix.buildType}}\run-test262.exe -c tests.conf
262269
build\${{matrix.buildType}}\function_source.exe
270+
- name: test interrupt
271+
run: |
272+
build\${{matrix.buildType}}\interrupt-test.exe
263273
264274
windows-ninja:
265275
runs-on: windows-latest
@@ -289,6 +299,9 @@ jobs:
289299
build\qjs.exe examples\test_point.js
290300
build\run-test262.exe -c tests.conf
291301
build\function_source.exe
302+
- name: test interrupt
303+
run: |
304+
build\interrupt-test.exe
292305
293306
windows-sdk:
294307
runs-on: windows-latest
@@ -319,6 +332,9 @@ jobs:
319332
build\${{matrix.buildType}}\qjs.exe examples\test_point.js
320333
build\${{matrix.buildType}}\run-test262.exe -c tests.conf
321334
build\${{matrix.buildType}}\function_source.exe
335+
- name: test interrupt
336+
run: |
337+
build\${{matrix.buildType}}\interrupt-test.exe
322338
323339
windows-mingw:
324340
runs-on: windows-latest
@@ -369,6 +385,9 @@ jobs:
369385
run: |
370386
./build/qjs -c examples/hello.js -o hello.exe
371387
./hello
388+
- name: test interrupt
389+
run: |
390+
./build/interrupt-test
372391
windows-mingw-shared:
373392
runs-on: windows-latest
374393
defaults:
@@ -453,6 +472,10 @@ jobs:
453472
- name: test
454473
run: make test
455474

475+
- name: test interrupt
476+
run: |
477+
./build/interrupt-test
478+
456479
openbsd:
457480
runs-on: ubuntu-latest
458481
steps:

CMakeLists.txt

+9
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,15 @@ if(NOT EMSCRIPTEN)
298298
target_link_libraries(run-test262 qjs)
299299
endif()
300300

301+
# Interrupt test
302+
#
303+
304+
add_executable(interrupt-test
305+
interrupt-test.c
306+
)
307+
target_compile_definitions(interrupt-test PRIVATE ${qjs_defines})
308+
target_link_libraries(interrupt-test qjs)
309+
301310
# Unicode generator
302311
#
303312

interrupt-test.c

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#include <stdlib.h>
2+
#include "quickjs.h"
3+
4+
#define MAX_TIME 10
5+
6+
#define expect(condition) \
7+
do { \
8+
if (!(condition)) { \
9+
fprintf(stderr, "Failed: %s, file %s, line %d\n", \
10+
#condition, __FILE__, __LINE__); \
11+
exit(EXIT_FAILURE); \
12+
} \
13+
} while (0)
14+
15+
static int timeout_interrupt_handler(JSRuntime *rt, void *opaque)
16+
{
17+
int *time = (int *)opaque;
18+
if (*time <= MAX_TIME)
19+
*time += 1;
20+
return *time > MAX_TIME;
21+
}
22+
23+
static void sync_call(void)
24+
{
25+
const char *code =
26+
"(function() { \
27+
try { \
28+
while (true) {} \
29+
} catch (e) {} \
30+
})();";
31+
32+
JSRuntime *rt = JS_NewRuntime();
33+
JSContext *ctx = JS_NewContext(rt);
34+
int time = 0;
35+
JS_SetInterruptHandler(rt, timeout_interrupt_handler, &time);
36+
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_GLOBAL);
37+
expect(time > MAX_TIME);
38+
expect(JS_IsException(ret));
39+
JS_FreeValue(ctx, ret);
40+
expect(JS_HasException(ctx));
41+
JSValue e = JS_GetException(ctx);
42+
expect(JS_IsUncatchableError(ctx, e));
43+
JS_FreeValue(ctx, e);
44+
JS_FreeContext(ctx);
45+
JS_FreeRuntime(rt);
46+
}
47+
48+
static void async_call(void)
49+
{
50+
const char *code =
51+
"(async function() { \
52+
const loop = async () => { \
53+
await Promise.resolve(); \
54+
while (true) {} \
55+
}; \
56+
await loop().catch(() => {}); \
57+
})();";
58+
59+
JSRuntime *rt = JS_NewRuntime();
60+
JSContext *ctx = JS_NewContext(rt);
61+
int time = 0;
62+
JS_SetInterruptHandler(rt, timeout_interrupt_handler, &time);
63+
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_GLOBAL);
64+
expect(!JS_IsException(ret));
65+
JS_FreeValue(ctx, ret);
66+
expect(JS_IsJobPending(rt));
67+
int r = 0;
68+
while (JS_IsJobPending(rt)) {
69+
r = JS_ExecutePendingJob(rt, &ctx);
70+
}
71+
expect(time > MAX_TIME);
72+
expect(r == -1);
73+
expect(JS_HasException(ctx));
74+
JSValue e = JS_GetException(ctx);
75+
expect(JS_IsUncatchableError(ctx, e));
76+
JS_FreeValue(ctx, e);
77+
JS_FreeContext(ctx);
78+
JS_FreeRuntime(rt);
79+
}
80+
81+
static JSValue save_value(JSContext *ctx, JSValue this_val, int argc, JSValue *argv)
82+
{
83+
expect(argc == 1);
84+
JSValue *p = (JSValue *)JS_GetContextOpaque(ctx);
85+
*p = JS_DupValue(ctx, argv[0]);
86+
return JS_UNDEFINED;
87+
}
88+
89+
static void async_call_stack_overflow(void)
90+
{
91+
const char *code =
92+
"(async function() { \
93+
const f = () => f(); \
94+
try { \
95+
await Promise.resolve(); \
96+
f(); \
97+
} catch (e) { \
98+
save_value(e); \
99+
} \
100+
})();";
101+
102+
JSRuntime *rt = JS_NewRuntime();
103+
JSContext *ctx = JS_NewContext(rt);
104+
JS_SetMaxStackSize(rt, 128 * 1024);
105+
JS_UpdateStackTop(rt);
106+
JSValue value = JS_UNDEFINED;
107+
JS_SetContextOpaque(ctx, &value);
108+
JSValue global = JS_GetGlobalObject(ctx);
109+
JS_SetPropertyStr(ctx, global, "save_value", JS_NewCFunction(ctx, save_value, "save_value", 1));
110+
JS_FreeValue(ctx, global);
111+
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_GLOBAL);
112+
expect(!JS_IsException(ret));
113+
JS_FreeValue(ctx, ret);
114+
expect(JS_IsJobPending(rt));
115+
int r = 0;
116+
while (JS_IsJobPending(rt)) {
117+
r = JS_ExecutePendingJob(rt, &ctx);
118+
}
119+
expect(r == 1);
120+
expect(!JS_HasException(ctx));
121+
expect(JS_IsError(ctx, value)); /* StackOverflow should be caught */
122+
JS_FreeValue(ctx, value);
123+
JS_FreeContext(ctx);
124+
JS_FreeRuntime(rt);
125+
}
126+
127+
int main()
128+
{
129+
sync_call();
130+
async_call();
131+
async_call_stack_overflow();
132+
printf("interrupt-test passed\n");
133+
return 0;
134+
}

quickjs.c

+28-12
Original file line numberDiff line numberDiff line change
@@ -17989,20 +17989,31 @@ static int js_async_function_resolve_create(JSContext *ctx,
1798917989
return 0;
1799017990
}
1799117991

17992-
static void js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s)
17992+
static bool js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s)
1799317993
{
17994+
bool is_success = true;
1799417995
JSValue func_ret, ret2;
1799517996

1799617997
func_ret = async_func_resume(ctx, &s->func_state);
1799717998
if (JS_IsException(func_ret)) {
17998-
JSValue error;
1799917999
fail:
18000-
error = JS_GetException(ctx);
18001-
ret2 = JS_Call(ctx, s->resolving_funcs[1], JS_UNDEFINED,
18002-
1, &error);
18003-
JS_FreeValue(ctx, error);
18000+
if (unlikely(JS_IsUncatchableError(ctx, ctx->rt->current_exception))) {
18001+
is_success = false;
18002+
} else {
18003+
JSValue error = JS_GetException(ctx);
18004+
ret2 = JS_Call(ctx, s->resolving_funcs[1], JS_UNDEFINED, 1, &error);
18005+
JS_FreeValue(ctx, error);
18006+
resolved:
18007+
if (unlikely(JS_IsException(ret2))) {
18008+
if (JS_IsUncatchableError(ctx, ctx->rt->current_exception)) {
18009+
is_success = false;
18010+
} else {
18011+
abort(); /* BUG */
18012+
}
18013+
}
18014+
JS_FreeValue(ctx, ret2);
18015+
}
1800418016
js_async_function_terminate(ctx->rt, s);
18005-
JS_FreeValue(ctx, ret2); /* XXX: what to do if exception ? */
1800618017
} else {
1800718018
JSValue value;
1800818019
value = s->func_state.frame.cur_sp[-1];
@@ -18011,9 +18022,8 @@ static void js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s)
1801118022
/* function returned */
1801218023
ret2 = JS_Call(ctx, s->resolving_funcs[0], JS_UNDEFINED,
1801318024
1, &value);
18014-
JS_FreeValue(ctx, ret2); /* XXX: what to do if exception ? */
1801518025
JS_FreeValue(ctx, value);
18016-
js_async_function_terminate(ctx->rt, s);
18026+
goto resolved;
1801718027
} else {
1801818028
JSValue promise, resolving_funcs[2], resolving_funcs1[2];
1801918029
int i, res;
@@ -18044,6 +18054,7 @@ static void js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s)
1804418054
goto fail;
1804518055
}
1804618056
}
18057+
return is_success;
1804718058
}
1804818059

1804918060
static JSValue js_async_function_resolve_call(JSContext *ctx,
@@ -18068,7 +18079,8 @@ static JSValue js_async_function_resolve_call(JSContext *ctx,
1806818079
/* return value of await */
1806918080
s->func_state.frame.cur_sp[-1] = js_dup(arg);
1807018081
}
18071-
js_async_function_resume(ctx, s);
18082+
if (!js_async_function_resume(ctx, s))
18083+
return JS_EXCEPTION;
1807218084
return JS_UNDEFINED;
1807318085
}
1807418086

@@ -18100,7 +18112,8 @@ static JSValue js_async_function_call(JSContext *ctx, JSValue func_obj,
1810018112
}
1810118113
s->is_active = true;
1810218114

18103-
js_async_function_resume(ctx, s);
18115+
if (!js_async_function_resume(ctx, s))
18116+
goto fail;
1810418117

1810518118
js_async_function_free(ctx->rt, s);
1810618119

@@ -48620,8 +48633,11 @@ static JSValue promise_reaction_job(JSContext *ctx, int argc,
4862048633
res = JS_Call(ctx, handler, JS_UNDEFINED, 1, &arg);
4862148634
}
4862248635
is_reject = JS_IsException(res);
48623-
if (is_reject)
48636+
if (is_reject) {
48637+
if (unlikely(JS_IsUncatchableError(ctx, ctx->rt->current_exception)))
48638+
return JS_EXCEPTION;
4862448639
res = JS_GetException(ctx);
48640+
}
4862548641
func = argv[is_reject];
4862648642
/* as an extension, we support undefined as value to avoid
4862748643
creating a dummy promise in the 'await' implementation of async

0 commit comments

Comments
 (0)