diff --git a/include/chaiscript/chaiscript_defines.hpp b/include/chaiscript/chaiscript_defines.hpp index 530fb419..a5d8c6df 100644 --- a/include/chaiscript/chaiscript_defines.hpp +++ b/include/chaiscript/chaiscript_defines.hpp @@ -75,6 +75,14 @@ static_assert(_MSC_FULL_VER >= 190024210, "Visual C++ 2015 Update 3 or later req #define CHAISCRIPT_DEBUG false #endif +// Upper bound on the depth of nested ChaiScript function calls. Hitting it +// causes the dispatcher to throw chaiscript::exception::stack_overflow_error +// instead of letting the native call stack overflow. Defining the macro on +// the command line overrides the default. +#ifndef CHAISCRIPT_MAX_CALL_DEPTH +#define CHAISCRIPT_MAX_CALL_DEPTH 256 +#endif + #include #include #include @@ -88,6 +96,9 @@ namespace chaiscript { constexpr static const char *compiler_name = CHAISCRIPT_COMPILER_NAME; constexpr static const bool debug_build = CHAISCRIPT_DEBUG; + constexpr static const int max_call_depth = CHAISCRIPT_MAX_CALL_DEPTH; + static_assert(max_call_depth > 0, "CHAISCRIPT_MAX_CALL_DEPTH must be a positive integer"); + template inline std::shared_ptr make_shared(Arg &&...arg) { #ifdef CHAISCRIPT_USE_STD_MAKE_SHARED diff --git a/include/chaiscript/dispatchkit/dispatchkit.hpp b/include/chaiscript/dispatchkit/dispatchkit.hpp index 2d1eff7e..f38147c0 100644 --- a/include/chaiscript/dispatchkit/dispatchkit.hpp +++ b/include/chaiscript/dispatchkit/dispatchkit.hpp @@ -121,6 +121,19 @@ namespace chaiscript { global_non_const(const global_non_const &) = default; ~global_non_const() noexcept override = default; }; + + /// Exception thrown when the ChaiScript call stack exceeds + /// chaiscript::max_call_depth, signalling runaway recursion before the + /// native stack can overflow. + class stack_overflow_error : public std::runtime_error { + public: + stack_overflow_error() noexcept + : std::runtime_error("Maximum call stack depth exceeded") { + } + + stack_overflow_error(const stack_overflow_error &) = default; + ~stack_overflow_error() noexcept override = default; + }; } // namespace exception /// \brief Holds a collection of ChaiScript settings which can be applied to the ChaiScript runtime. @@ -1010,6 +1023,10 @@ namespace chaiscript { void save_function_params(const Function_Params &t_params) { save_function_params(*m_stack_holder, t_params); } void new_function_call(Stack_Holder &t_s, Type_Conversions::Conversion_Saves &t_saves) { + if (t_s.call_depth >= max_call_depth) { + throw chaiscript::exception::stack_overflow_error{}; + } + if (t_s.call_depth == 0) { m_conversions.enable_conversion_saves(t_saves, true); } diff --git a/unittests/recursion_depth_protection.chai b/unittests/recursion_depth_protection.chai new file mode 100644 index 00000000..082854ff --- /dev/null +++ b/unittests/recursion_depth_protection.chai @@ -0,0 +1,39 @@ +// Regression test for issue #633: Stack-overflow due to infinite recursion +// in user-defined operator (string interpolation). +// +// Before the fix the recursive `/=` invocation triggered unbounded native +// recursion in the evaluator and crashed the host process with a SIGSEGV. +// The engine now bounds call_depth and throws a catchable exception +// instead of letting the native stack overflow. + +def string::`/=`(double d) { + this = "${this/= 2}/=${d}"; + return this; +} + +var s = "o World" +var caught = false +var message = "" + +try { + s /= 2 + // unreachable: the recursive operator must abort with an exception + assert_true(false) +} catch (e) { + caught = true + message = e.what() +} + +assert_true(caught) + +// The reported error must mention the call-stack overflow so users can +// distinguish it from an arbitrary script-level error. +assert_true(find(message, "call stack") != -1) + +// A bounded recursion that stays below the limit must keep working. +def count_down(n) { + if (n <= 0) { return 0 } + return count_down(n - 1) + 1 +} + +assert_equal(50, count_down(50))