Skip to content

Commit

Permalink
Fix uncatchable error inside a promise (#810)
Browse files Browse the repository at this point in the history
  • Loading branch information
laishere committed Jan 19, 2025
1 parent a884e2d commit 77332f2
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 12 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ jobs:
run: |
./build/qjs -c examples/hello.js -o hello
./hello
- name: test interrupt
run: |
./build/interrupt-test
windows-msvc:
runs-on: windows-latest
Expand Down Expand Up @@ -198,6 +202,9 @@ jobs:
run: |
build\${{matrix.buildType}}\qjs.exe -c examples\hello.js -o hello.exe
.\hello.exe
- name: test interrupt
run: |
build\${{matrix.buildType}}\interrupt-test.exe
- name: Set up Visual Studio shell
uses: egor-tensin/vs-shell@v2
with:
Expand Down Expand Up @@ -260,6 +267,9 @@ jobs:
build\${{matrix.buildType}}\qjs.exe examples\test_point.js
build\${{matrix.buildType}}\run-test262.exe -c tests.conf
build\${{matrix.buildType}}\function_source.exe
- name: test interrupt
run: |
build\${{matrix.buildType}}\interrupt-test.exe
windows-ninja:
runs-on: windows-latest
Expand Down Expand Up @@ -289,6 +299,9 @@ jobs:
build\qjs.exe examples\test_point.js
build\run-test262.exe -c tests.conf
build\function_source.exe
- name: test interrupt
run: |
build\interrupt-test.exe
windows-sdk:
runs-on: windows-latest
Expand Down Expand Up @@ -319,6 +332,9 @@ jobs:
build\${{matrix.buildType}}\qjs.exe examples\test_point.js
build\${{matrix.buildType}}\run-test262.exe -c tests.conf
build\${{matrix.buildType}}\function_source.exe
- name: test interrupt
run: |
build\${{matrix.buildType}}\interrupt-test.exe
windows-mingw:
runs-on: windows-latest
Expand Down Expand Up @@ -369,6 +385,9 @@ jobs:
run: |
./build/qjs -c examples/hello.js -o hello.exe
./hello
- name: test interrupt
run: |
./build/interrupt-test
windows-mingw-shared:
runs-on: windows-latest
defaults:
Expand Down Expand Up @@ -453,6 +472,10 @@ jobs:
- name: test
run: make test

- name: test interrupt
run: |
./build/interrupt-test
openbsd:
runs-on: ubuntu-latest
steps:
Expand Down
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,15 @@ if(NOT EMSCRIPTEN)
target_link_libraries(run-test262 qjs)
endif()

# Interrupt test
#

add_executable(interrupt-test
interrupt-test.c
)
target_compile_definitions(interrupt-test PRIVATE ${qjs_defines})
target_link_libraries(interrupt-test qjs)

# Unicode generator
#

Expand Down
134 changes: 134 additions & 0 deletions interrupt-test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#include <stdlib.h>
#include "quickjs.h"

#define MAX_TIME 10

#define expect(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "Failed: %s, file %s, line %d\n", \
#condition, __FILE__, __LINE__); \
exit(EXIT_FAILURE); \
} \
} while (0)

static int timeout_interrupt_handler(JSRuntime *rt, void *opaque)
{
int *time = (int *)opaque;
if (*time <= MAX_TIME)
*time += 1;
return *time > MAX_TIME;
}

static void sync_call(void)
{
const char *code =
"(function() { \
try { \
while (true) {} \
} catch (e) {} \
})();";

JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
int time = 0;
JS_SetInterruptHandler(rt, timeout_interrupt_handler, &time);
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_GLOBAL);
expect(time > MAX_TIME);
expect(JS_IsException(ret));
JS_FreeValue(ctx, ret);
expect(JS_HasException(ctx));
JSValue e = JS_GetException(ctx);
expect(JS_IsUncatchableError(ctx, e));
JS_FreeValue(ctx, e);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}

static void async_call(void)
{
const char *code =
"(async function() { \
const loop = async () => { \
await Promise.resolve(); \
while (true) {} \
}; \
await loop().catch(() => {}); \
})();";

JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
int time = 0;
JS_SetInterruptHandler(rt, timeout_interrupt_handler, &time);
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_GLOBAL);
expect(!JS_IsException(ret));
JS_FreeValue(ctx, ret);
expect(JS_IsJobPending(rt));
int r = 0;
while (JS_IsJobPending(rt)) {
r = JS_ExecutePendingJob(rt, &ctx);
}
expect(time > MAX_TIME);
expect(r == -1);
expect(JS_HasException(ctx));
JSValue e = JS_GetException(ctx);
expect(JS_IsUncatchableError(ctx, e));
JS_FreeValue(ctx, e);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}

static JSValue save_value(JSContext *ctx, JSValue this_val, int argc, JSValue *argv)
{
expect(argc == 1);
JSValue *p = (JSValue *)JS_GetContextOpaque(ctx);
*p = JS_DupValue(ctx, argv[0]);
return JS_UNDEFINED;
}

static void async_call_stack_overflow(void)
{
const char *code =
"(async function() { \
const f = () => f(); \
try { \
await Promise.resolve(); \
f(); \
} catch (e) { \
save_value(e); \
} \
})();";

JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
JS_SetMaxStackSize(rt, 128 * 1024);
JS_UpdateStackTop(rt);
JSValue value = JS_UNDEFINED;
JS_SetContextOpaque(ctx, &value);
JSValue global = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global, "save_value", JS_NewCFunction(ctx, save_value, "save_value", 1));
JS_FreeValue(ctx, global);
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_GLOBAL);
expect(!JS_IsException(ret));
JS_FreeValue(ctx, ret);
expect(JS_IsJobPending(rt));
int r = 0;
while (JS_IsJobPending(rt)) {
r = JS_ExecutePendingJob(rt, &ctx);
}
expect(r == 1);
expect(!JS_HasException(ctx));
expect(JS_IsError(ctx, value)); /* StackOverflow should be caught */
JS_FreeValue(ctx, value);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}

int main()
{
sync_call();
async_call();
async_call_stack_overflow();
printf("interrupt-test passed\n");
return 0;
}
40 changes: 28 additions & 12 deletions quickjs.c
Original file line number Diff line number Diff line change
Expand Up @@ -18009,20 +18009,31 @@ static int js_async_function_resolve_create(JSContext *ctx,
return 0;
}

static void js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s)
static bool js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s)
{
bool is_success = true;
JSValue func_ret, ret2;

func_ret = async_func_resume(ctx, &s->func_state);
if (JS_IsException(func_ret)) {
JSValue error;
fail:
error = JS_GetException(ctx);
ret2 = JS_Call(ctx, s->resolving_funcs[1], JS_UNDEFINED,
1, &error);
JS_FreeValue(ctx, error);
if (unlikely(JS_IsUncatchableError(ctx, ctx->rt->current_exception))) {
is_success = false;
} else {
JSValue error = JS_GetException(ctx);
ret2 = JS_Call(ctx, s->resolving_funcs[1], JS_UNDEFINED, 1, &error);
JS_FreeValue(ctx, error);
resolved:
if (unlikely(JS_IsException(ret2))) {
if (JS_IsUncatchableError(ctx, ctx->rt->current_exception)) {
is_success = false;
} else {
abort(); /* BUG */
}
}
JS_FreeValue(ctx, ret2);
}
js_async_function_terminate(ctx->rt, s);
JS_FreeValue(ctx, ret2); /* XXX: what to do if exception ? */
} else {
JSValue value;
value = s->func_state.frame.cur_sp[-1];
Expand All @@ -18031,9 +18042,8 @@ static void js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s)
/* function returned */
ret2 = JS_Call(ctx, s->resolving_funcs[0], JS_UNDEFINED,
1, &value);
JS_FreeValue(ctx, ret2); /* XXX: what to do if exception ? */
JS_FreeValue(ctx, value);
js_async_function_terminate(ctx->rt, s);
goto resolved;
} else {
JSValue promise, resolving_funcs[2], resolving_funcs1[2];
int i, res;
Expand Down Expand Up @@ -18064,6 +18074,7 @@ static void js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s)
goto fail;
}
}
return is_success;
}

static JSValue js_async_function_resolve_call(JSContext *ctx,
Expand All @@ -18088,7 +18099,8 @@ static JSValue js_async_function_resolve_call(JSContext *ctx,
/* return value of await */
s->func_state.frame.cur_sp[-1] = js_dup(arg);
}
js_async_function_resume(ctx, s);
if (!js_async_function_resume(ctx, s))
return JS_EXCEPTION;
return JS_UNDEFINED;
}

Expand Down Expand Up @@ -18120,7 +18132,8 @@ static JSValue js_async_function_call(JSContext *ctx, JSValue func_obj,
}
s->is_active = true;

js_async_function_resume(ctx, s);
if (!js_async_function_resume(ctx, s))
goto fail;

js_async_function_free(ctx->rt, s);

Expand Down Expand Up @@ -48640,8 +48653,11 @@ static JSValue promise_reaction_job(JSContext *ctx, int argc,
res = JS_Call(ctx, handler, JS_UNDEFINED, 1, &arg);
}
is_reject = JS_IsException(res);
if (is_reject)
if (is_reject) {
if (unlikely(JS_IsUncatchableError(ctx, ctx->rt->current_exception)))
return JS_EXCEPTION;
res = JS_GetException(ctx);
}
func = argv[is_reject];
/* as an extension, we support undefined as value to avoid
creating a dummy promise in the 'await' implementation of async
Expand Down

0 comments on commit 77332f2

Please sign in to comment.