Skip to content

A recursion deeper than the native stack kills the interpreter with SIGSEGV: options stack bounds only the simulated stack and no diagnostic fires #4110

Description

@lookibed

daslang 0.6.4 at master 237df6024, Linux x86_64 (WSL2, kernel 6.6), gcc 11.4, Release.

A recursion that outgrows the native stack kills the process with SIGSEGV in the interpreter. options stack sizes the simulated stack only; the interpreter's callOrFastcall recurses on the C++ stack with no depth guard, so with a large options stack the simulated-stack check never fires and the native stack overflows first. The same program under -jit runs (LLVM turns the self tail call into a loop), so the crash depends on the tier.

Reproduction

deep.das:

options gen2
options stack = 4194304
require strings

def count_down(n : int; acc : int) : int {
    if (n == 0) {
        return acc
    }
    return count_down(n - 1, acc + 1)
}

[export]
def main() {
    let args = get_command_line_arguments()
    let depth = to_int(args[length(args) - 1])
    print("depth={depth} result={count_down(depth, 0)}\n")
}
$ daslang deep.das -- 100000
depth=100000 result=100000
$ daslang deep.das -- 1000000
CRASH: SIGSEGV (Segmentation fault) (signal 11) at address 0x7ffd8f63aff8
Stack trace:
  [ 0] [0x12]
  [ 1] das::Context::~Context()
$ echo $?
139
$ daslang -jit deep.das -- 10000000
depth=10000000 result=10000000

With the default options stack the same depth reports stack overflow while calling count_down and exits cleanly - the simulated stack is exhausted before the native one. The crash is what happens once the simulated stack is made large enough to no longer be the limit.

Expected

Recursion depth is bounded by a diagnostic on every tier: stack overflow while calling <fn> (the message the simulated-stack check already produces), recoverable like any other runtime error, whatever options stack says. A bare SIGSEGV from a program that compiled is not a diagnostic. If the native depth is meant to be the program's own responsibility, the reference for options stack should say that it bounds only the simulated stack.

Test

Drops into tests/language/; on master the interpreter and -jit runs of the file both die with SIGSEGV instead of reporting.

options gen2
options stack = 4194304
require dastest/testing_boost

def count_down(n : int; acc : int) : int {
    if (n == 0) {
        return acc
    }
    return count_down(n - 1, acc + 1)
}

[test]
def test_deep_recursion_is_a_diagnostic_not_a_crash(t : T?) {
    var overflowed = false
    var result = 0
    try {
        result = count_down(1000000, 0)
    } recover {
        overflowed = true
    }
    t |> success(overflowed || result == 1000000, "either the call completes or the runtime reports a stack overflow")
}
$ daslang dastest/dastest.das -- --test deep_recursion.das
Segmentation fault (exit 139)
$ daslang dastest/dastest.das -jit -- --test deep_recursion.das
CRASH: SIGSEGV (Segmentation fault) (signal 11) at address 0x7ffffcfccff8

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions