diff --git a/Makefile b/Makefile index b305be1d..2b4573b6 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,7 @@ ifdef SYSTEMROOT # Windows # Skip fancy (linenoise) else CCOPTS_SHARED += -DDUK_CMDLINE_FANCY +#CCOPTS_SHARED += -DDUK_CMDLINE_PTHREAD_STACK_CHECK endif CCOPTS_SHARED += -DDUK_CMDLINE_ALLOC_LOGGING CCOPTS_SHARED += -DDUK_CMDLINE_ALLOC_TORTURE @@ -191,6 +192,7 @@ CCOPTS_DUKLOW += -DDUK_ALLOC_POOL_TRACK_WASTE # quite fast, but not free so dis ifdef SYSTEMROOT # Windows CCLIBS = -lm -lws2_32 else +#CCLIBS = -lm -lpthread CCLIBS = -lm endif diff --git a/RELEASES.rst b/RELEASES.rst index 0bc43bb5..4f37bcf8 100644 --- a/RELEASES.rst +++ b/RELEASES.rst @@ -3410,6 +3410,11 @@ Planned * Remove arguments.caller for strict argument objects to match revised ES2017 behavior (GH-2009) +* Add DUK_USE_NATIVE_STACK_CHECK() macro config option (disabled by default) + for a platform specific stack space check in recursive and stack heavy + code paths; this is more accurate than the default fixed recursion limit + (GH-1995) + * When using Proxy wrapping in console extra, don't return a fake NOP function for console.toJSON to avoid confusing JX serialization of the console object (GH-2052, GH-2054, GH-2055) diff --git a/config/config-options/DUK_USE_NATIVE_STACK_CHECK.yaml b/config/config-options/DUK_USE_NATIVE_STACK_CHECK.yaml new file mode 100644 index 00000000..24774c87 --- /dev/null +++ b/config/config-options/DUK_USE_NATIVE_STACK_CHECK.yaml @@ -0,0 +1,22 @@ +define: DUK_USE_NATIVE_STACK_CHECK +introduced: 2.4.0 +default: false +tags: + - portability + - execution +description: > + Provide a macro hook to check for available native stack space for the + currently executing native thread. The macro must evaluate to zero if + there is enough stack space available and non-zero otherwise; a RangeError + will then be thrown. + + The definition of "enough space" depends on the target platform and the + compiler because the size of native stack frames cannot be easily known + in advance. As a relatively safe estimate, one can check for 8kB of + available stack. + + Duktape doesn't call this macro for every internal native call. The macro + is called in code paths that are involved in potentially unlimited + recursion (such as making Ecmascript/native function calls, invoking + getters and Proxy traps, and resolving Proxy chains) and code paths + requiring a lot of stack space temporarily. diff --git a/examples/cmdline/duk_cmdline.c b/examples/cmdline/duk_cmdline.c index fe9f8ebf..0c4c74d0 100644 --- a/examples/cmdline/duk_cmdline.c +++ b/examples/cmdline/duk_cmdline.c @@ -1,7 +1,8 @@ /* * Command line execution tool. Useful for test cases and manual testing. + * Also demonstrates some basic integration techniques. * - * Optional features: + * Optional features include: * * - To enable print()/alert() bindings, define DUK_CMDLINE_PRINTALERT_SUPPORT * and add extras/print-alert/duk_print_alert.c to compilation. @@ -46,6 +47,11 @@ #endif #endif +#if defined(DUK_CMDLINE_PTHREAD_STACK_CHECK) +#define _GNU_SOURCE +#include +#endif + #include #include #include @@ -1563,3 +1569,47 @@ int main(int argc, char *argv[]) { fflush(stderr); exit(1); } + +/* Example of how a native stack check can be implemented in a platform + * specific manner for DUK_USE_NATIVE_STACK_CHECK(). This example is for + * (Linux) pthreads, and rejects further native recursion if less than + * 16kB stack is left (conservative). + */ +#if defined(DUK_CMDLINE_PTHREAD_STACK_CHECK) +int duk_cmdline_stack_check(void) { + pthread_attr_t attr; + void *stackaddr; + size_t stacksize; + char *ptr; + char *ptr_base; + ptrdiff_t remain; + + (void) pthread_getattr_np(pthread_self(), &attr); + (void) pthread_attr_getstack(&attr, &stackaddr, &stacksize); + ptr = (char *) &stacksize; /* Rough estimate of current stack pointer. */ + ptr_base = (char *) stackaddr; + remain = ptr - ptr_base; + + /* HIGH ADDR ----- stackaddr + stacksize + * | + * | stack growth direction + * v -- ptr, approximate used size + * + * LOW ADDR ----- ptr_base, end of stack (lowest address) + */ + +#if 0 + fprintf(stderr, "STACK CHECK: stackaddr=%p, stacksize=%ld, ptr=%p, remain=%ld\n", + stackaddr, (long) stacksize, (void *) ptr, (long) remain); + fflush(stderr); +#endif + if (remain < 16384) { + return 1; + } + return 0; /* 0: no error, != 0: throw RangeError */ +} +#else +int duk_cmdline_stack_check(void) { + return 0; +} +#endif diff --git a/src-input/duk_js.h b/src-input/duk_js.h index 5ad32488..db2d72ea 100644 --- a/src-input/duk_js.h +++ b/src-input/duk_js.h @@ -99,6 +99,7 @@ DUK_INTERNAL_DECL void duk_js_push_closure(duk_hthread *thr, duk_bool_t add_auto_proto); /* call handling */ +DUK_INTERNAL_DECL void duk_native_stack_check(duk_hthread *thr); DUK_INTERNAL_DECL duk_int_t duk_handle_call_unprotected(duk_hthread *thr, duk_idx_t idx_func, duk_small_uint_t call_flags); DUK_INTERNAL_DECL duk_int_t duk_handle_call_unprotected_nargs(duk_hthread *thr, duk_idx_t nargs, duk_small_uint_t call_flags); DUK_INTERNAL_DECL duk_int_t duk_handle_safe_call(duk_hthread *thr, duk_safe_call_function func, void *udata, duk_idx_t num_stack_args, duk_idx_t num_stack_res); diff --git a/src-input/duk_js_call.c b/src-input/duk_js_call.c index b1789993..f96e5f50 100644 --- a/src-input/duk_js_call.c +++ b/src-input/duk_js_call.c @@ -35,6 +35,17 @@ * Limit check helpers. */ +/* Check native stack space if DUK_USE_NATIVE_STACK_CHECK() defined. */ +DUK_INTERNAL void duk_native_stack_check(duk_hthread *thr) { +#if defined(DUK_USE_NATIVE_STACK_CHECK) + if (DUK_USE_NATIVE_STACK_CHECK() != 0) { + DUK_ERROR_RANGE(thr, DUK_STR_NATIVE_STACK_LIMIT); + } +#else + DUK_UNREF(thr); +#endif +} + /* Allow headroom for calls during error augmentation (see GH-191). * We allow space for 10 additional recursions, with one extra * for, e.g. a print() call at the deepest level, and an extra @@ -59,7 +70,7 @@ DUK_LOCAL DUK_NOINLINE void duk__call_c_recursion_limit_check_slowpath(duk_hthre #endif DUK_D(DUK_DPRINT("call prevented because C recursion limit reached")); - DUK_ERROR_RANGE(thr, DUK_STR_C_CALLSTACK_LIMIT); + DUK_ERROR_RANGE(thr, DUK_STR_NATIVE_STACK_LIMIT); DUK_WO_NORETURN(return;); } @@ -67,6 +78,8 @@ DUK_LOCAL DUK_ALWAYS_INLINE void duk__call_c_recursion_limit_check(duk_hthread * DUK_ASSERT(thr->heap->call_recursion_depth >= 0); DUK_ASSERT(thr->heap->call_recursion_depth <= thr->heap->call_recursion_limit); + duk_native_stack_check(thr); + /* This check is forcibly inlined because it's very cheap and almost * always passes. The slow path is forcibly noinline. */ diff --git a/src-input/duk_numconv.c b/src-input/duk_numconv.c index 5d03f6e0..014a1e61 100644 --- a/src-input/duk_numconv.c +++ b/src-input/duk_numconv.c @@ -1537,7 +1537,7 @@ DUK_LOCAL void duk__dragon4_ctx_to_double(duk__numconv_stringify_ctx *nc_ctx, du * Output: [ string ] */ -DUK_INTERNAL void duk_numconv_stringify(duk_hthread *thr, duk_small_int_t radix, duk_small_int_t digits, duk_small_uint_t flags) { +DUK_LOCAL DUK_NOINLINE void duk__numconv_stringify_raw(duk_hthread *thr, duk_small_int_t radix, duk_small_int_t digits, duk_small_uint_t flags) { duk_double_t x; duk_small_int_t c; duk_small_int_t neg; @@ -1730,6 +1730,11 @@ DUK_INTERNAL void duk_numconv_stringify(duk_hthread *thr, duk_small_int_t radix, duk__dragon4_convert_and_push(nc_ctx, thr, radix, digits, flags, neg); } +DUK_INTERNAL void duk_numconv_stringify(duk_hthread *thr, duk_small_int_t radix, duk_small_int_t digits, duk_small_uint_t flags) { + duk_native_stack_check(thr); + duk__numconv_stringify_raw(thr, radix, digits, flags); +} + /* * Exposed string-to-number API * @@ -1740,7 +1745,7 @@ DUK_INTERNAL void duk_numconv_stringify(duk_hthread *thr, duk_small_int_t radix, * fails due to an internal error, an InternalError is thrown. */ -DUK_INTERNAL void duk_numconv_parse(duk_hthread *thr, duk_small_int_t radix, duk_small_uint_t flags) { +DUK_LOCAL DUK_NOINLINE void duk__numconv_parse_raw(duk_hthread *thr, duk_small_int_t radix, duk_small_uint_t flags) { duk__numconv_stringify_ctx nc_ctx_alloc; /* large context; around 2kB now */ duk__numconv_stringify_ctx *nc_ctx = &nc_ctx_alloc; duk_double_t res; @@ -2268,3 +2273,8 @@ DUK_INTERNAL void duk_numconv_parse(duk_hthread *thr, duk_small_int_t radix, duk DUK_ERROR_RANGE(thr, "exponent too large"); DUK_WO_NORETURN(return;); } + +DUK_INTERNAL void duk_numconv_parse(duk_hthread *thr, duk_small_int_t radix, duk_small_uint_t flags) { + duk_native_stack_check(thr); + duk__numconv_parse_raw(thr, radix, flags); +} diff --git a/src-input/duk_regexp_compiler.c b/src-input/duk_regexp_compiler.c index 93e7f611..303836d4 100644 --- a/src-input/duk_regexp_compiler.c +++ b/src-input/duk_regexp_compiler.c @@ -522,6 +522,7 @@ DUK_LOCAL void duk__parse_disjunction(duk_re_compiler_ctx *re_ctx, duk_bool_t ex DUK_ASSERT(out_atom_info != NULL); + duk_native_stack_check(re_ctx->thr); if (re_ctx->recursion_depth >= re_ctx->recursion_limit) { DUK_ERROR_RANGE(re_ctx->thr, DUK_STR_REGEXP_COMPILER_RECURSION_LIMIT); DUK_WO_NORETURN(return;); diff --git a/src-input/duk_regexp_executor.c b/src-input/duk_regexp_executor.c index 35e2d87d..0add3dd2 100644 --- a/src-input/duk_regexp_executor.c +++ b/src-input/duk_regexp_executor.c @@ -146,6 +146,7 @@ DUK_LOCAL duk_codepoint_t duk__inp_get_prev_cp(duk_re_matcher_ctx *re_ctx, const */ DUK_LOCAL const duk_uint8_t *duk__match_regexp(duk_re_matcher_ctx *re_ctx, const duk_uint8_t *pc, const duk_uint8_t *sp) { + duk_native_stack_check(re_ctx->thr); if (re_ctx->recursion_depth >= re_ctx->recursion_limit) { DUK_ERROR_RANGE(re_ctx->thr, DUK_STR_REGEXP_EXECUTOR_RECURSION_LIMIT); DUK_WO_NORETURN(return NULL;); diff --git a/src-input/duk_strings.h b/src-input/duk_strings.h index 53e63f58..194c389a 100644 --- a/src-input/duk_strings.h +++ b/src-input/duk_strings.h @@ -153,7 +153,7 @@ #define DUK_STR_CALLSTACK_LIMIT "callstack limit" #define DUK_STR_PROTOTYPE_CHAIN_LIMIT "prototype chain limit" #define DUK_STR_BOUND_CHAIN_LIMIT "function call bound chain limit" -#define DUK_STR_C_CALLSTACK_LIMIT "C call stack depth limit" +#define DUK_STR_NATIVE_STACK_LIMIT "C stack depth limit" #define DUK_STR_COMPILER_RECURSION_LIMIT "compiler recursion limit" #define DUK_STR_BYTECODE_LIMIT "bytecode limit" #define DUK_STR_REG_LIMIT "register limit" diff --git a/tests/ecmascript/test-dev-cont-native-reclimit.js b/tests/ecmascript/test-dev-cont-native-reclimit.js index 92015408..448f274c 100644 --- a/tests/ecmascript/test-dev-cont-native-reclimit.js +++ b/tests/ecmascript/test-dev-cont-native-reclimit.js @@ -9,7 +9,7 @@ ---*/ /*=== -RangeError: C call stack depth limit +RangeError: C stack depth limit still here ===*/ diff --git a/util/makeduk_base.yaml b/util/makeduk_base.yaml index a015a589..5495725f 100644 --- a/util/makeduk_base.yaml +++ b/util/makeduk_base.yaml @@ -11,6 +11,11 @@ DUK_USE_FATAL_HANDLER: verbatim: "#define DUK_USE_FATAL_HANDLER(udata,msg) do { const char *fatal_msg = (msg); fprintf(stderr, \"*** FATAL ERROR: %s\\n\", fatal_msg ? fatal_msg : \"no message\"); fflush(stderr); *((volatile unsigned int *) 0) = (unsigned int) 0xdeadbeefUL; abort(); } while(0)" DUK_USE_SELF_TESTS: true +#DUK_USE_NATIVE_STACK_CHECK: +# verbatim: "int duk_cmdline_stack_check(void);\n#define DUK_USE_NATIVE_STACK_CHECK() duk_cmdline_stack_check()" +#DUK_USE_NATIVE_CALL_RECLIMIT: 10000000 +#DUK_USE_CALLSTACK_LIMIT: 10000000 + #DUK_USE_ASSERTIONS: true #DUK_USE_GC_TORTURE: true #DUK_USE_SHUFFLE_TORTURE: true