From 34265a767b419137dab4f04fee708e8dee912650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Talpalaru?= Date: Tue, 23 Nov 2021 22:38:03 +0100 Subject: [PATCH] nimRawSetjmp: support Windows Using `_setjmp()` directly is required to avoid some rare (but very annoying) exception-related stack corruption leading to segfaults on Windows, with Mingw-w64 and SEH. More details: https://github.com/status-im/nimbus-eth2/issues/3121 Also add "nimBuiltinSetjmp" - mostly for benchmarking. --- compiler/ccgstmts.nim | 13 +++- doc/nimc.rst | 14 ++++ lib/system/ansi_c.nim | 37 +++++++-- tests/exception/texceptions.nim | 6 +- tests/exception/texceptions2.nim | 130 +++++++++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 tests/exception/texceptions2.nim diff --git a/compiler/ccgstmts.nim b/compiler/ccgstmts.nim index 559849f0de2e2..7fecc14752919 100644 --- a/compiler/ccgstmts.nim +++ b/compiler/ccgstmts.nim @@ -1356,8 +1356,19 @@ proc genTrySetjmp(p: BProc, t: PNode, d: var TLoc) = linefmt(p, cpsStmts, "$1.status = setjmp($1.context);$n", [safePoint]) elif isDefined(p.config, "nimSigSetjmp"): linefmt(p, cpsStmts, "$1.status = sigsetjmp($1.context, 0);$n", [safePoint]) + elif isDefined(p.config, "nimBuiltinSetjmp"): + linefmt(p, cpsStmts, "$1.status = __builtin_setjmp($1.context);$n", [safePoint]) elif isDefined(p.config, "nimRawSetjmp"): - linefmt(p, cpsStmts, "$1.status = _setjmp($1.context);$n", [safePoint]) + if isDefined(p.config, "mswindows"): + # The Windows `_setjmp()` takes two arguments, with the second being an + # undocumented buffer used by the SEH mechanism for stack unwinding. + # Mingw-w64 has been trying to get it right for years, but it's still + # prone to stack corruption during unwinding, so we disable that by setting + # it to NULL. + # More details: https://github.com/status-im/nimbus-eth2/issues/3121 + linefmt(p, cpsStmts, "$1.status = _setjmp($1.context, 0);$n", [safePoint]) + else: + linefmt(p, cpsStmts, "$1.status = _setjmp($1.context);$n", [safePoint]) else: linefmt(p, cpsStmts, "$1.status = setjmp($1.context);$n", [safePoint]) lineCg(p, cpsStmts, "if ($1.status == 0) {$n", [safePoint]) diff --git a/doc/nimc.rst b/doc/nimc.rst index 6be4c204ebf74..9f163cf2515e1 100644 --- a/doc/nimc.rst +++ b/doc/nimc.rst @@ -165,6 +165,20 @@ ignored too. `--define:FOO`:option: and `--define:foo`:option: are identical. Compile-time symbols starting with the `nim` prefix are reserved for the implementation and should not be used elsewhere. +========================== ============================================ +Name Description +========================== ============================================ +nimStdSetjmp Use the standard `setjmp()/longjmp()` library + functions for setjmp-based exceptions. This is + the default on most platforms. +nimSigSetjmp Use `sigsetjmp()/siglongjmp()` for setjmp-based exceptions. +nimRawSetjmp Use `_setjmp()/_longjmp()` on POSIX and `_setjmp()/longjmp()` + on Windows, for setjmp-based exceptions. It's the default on + BSDs and BSD-like platforms, where it's significantly faster + than the standard functions. +nimBuiltinSetjmp Use `__builtin_setjmp()/__builtin_longjmp()` for setjmp-based + exceptions. This will not work if an exception is being thrown + and caught inside the same procedure. Useful for benchmarking. Configuration files ------------------- diff --git a/lib/system/ansi_c.nim b/lib/system/ansi_c.nim index 259d36633db25..4c094011be803 100644 --- a/lib/system/ansi_c.nim +++ b/lib/system/ansi_c.nim @@ -31,7 +31,10 @@ proc c_abort*() {. importc: "abort", header: "", noSideEffect, noreturn.} -when defined(linux) and defined(amd64): +when defined(nimBuiltinSetjmp): + type + C_JmpBuf* = array[5, int] +elif defined(linux) and defined(amd64): type C_JmpBuf* {.importc: "jmp_buf", header: "", bycopy.} = object abi: array[200 div sizeof(clong), clong] @@ -92,18 +95,40 @@ when defined(macosx): elif defined(haiku): const SIGBUS* = cint(30) -when defined(nimSigSetjmp) and not defined(nimStdSetjmp): +# "nimRawSetjmp" is defined by default for certain platforms, so we need the +# "nimStdSetjmp" escape hatch with it. +when defined(nimSigSetjmp): proc c_longjmp*(jmpb: C_JmpBuf, retval: cint) {. header: "", importc: "siglongjmp".} - template c_setjmp*(jmpb: C_JmpBuf): cint = + proc c_setjmp*(jmpb: C_JmpBuf): cint = proc c_sigsetjmp(jmpb: C_JmpBuf, savemask: cint): cint {. header: "", importc: "sigsetjmp".} c_sigsetjmp(jmpb, 0) -elif defined(nimRawSetjmp) and not defined(nimStdSetjmp): +elif defined(nimBuiltinSetjmp): proc c_longjmp*(jmpb: C_JmpBuf, retval: cint) {. - header: "", importc: "_longjmp".} + importc: "__builtin_longjmp", nodecl.} proc c_setjmp*(jmpb: C_JmpBuf): cint {. - header: "", importc: "_setjmp".} + importc: "__builtin_setjmp", nodecl.} +elif defined(nimRawSetjmp) and not defined(nimStdSetjmp): + when defined(windows): + # No `_longjmp()` on Windows. + proc c_longjmp*(jmpb: C_JmpBuf, retval: cint) {. + header: "", importc: "longjmp".} + # The Windows `_setjmp()` takes two arguments, with the second being an + # undocumented buffer used by the SEH mechanism for stack unwinding. + # Mingw-w64 has been trying to get it right for years, but it's still + # prone to stack corruption during unwinding, so we disable that by setting + # it to NULL. + # More details: https://github.com/status-im/nimbus-eth2/issues/3121 + proc c_setjmp*(jmpb: C_JmpBuf): cint = + proc c_setjmp_win(jmpb: C_JmpBuf, ctx: pointer): cint {. + header: "", importc: "_setjmp".} + c_setjmp_win(jmpb, nil) + else: + proc c_longjmp*(jmpb: C_JmpBuf, retval: cint) {. + header: "", importc: "_longjmp".} + proc c_setjmp*(jmpb: C_JmpBuf): cint {. + header: "", importc: "_setjmp".} else: proc c_longjmp*(jmpb: C_JmpBuf, retval: cint) {. header: "", importc: "longjmp".} diff --git a/tests/exception/texceptions.nim b/tests/exception/texceptions.nim index adee5d1d5b8a9..62d24c9341cc2 100644 --- a/tests/exception/texceptions.nim +++ b/tests/exception/texceptions.nim @@ -1,4 +1,6 @@ discard """ + disabled: "windows" # no sigsetjmp() there + matrix: "-d:nimStdSetjmp; -d:nimSigSetjmp; -d:nimRawSetjmp; -d:nimBuiltinSetjmp" output: ''' BEFORE @@ -17,7 +19,7 @@ FINALLY echo "" -proc no_expcetion = +proc no_exception = try: echo "BEFORE" @@ -28,7 +30,7 @@ proc no_expcetion = finally: echo "FINALLY" -try: no_expcetion() +try: no_exception() except: echo "RECOVER" echo "" diff --git a/tests/exception/texceptions2.nim b/tests/exception/texceptions2.nim new file mode 100644 index 0000000000000..97fd856a02708 --- /dev/null +++ b/tests/exception/texceptions2.nim @@ -0,0 +1,130 @@ +discard """ + disabled: "posix" # already covered by texceptions.nim + matrix: "-d:nimStdSetjmp; -d:nimRawSetjmp; -d:nimBuiltinSetjmp" + output: ''' + +BEFORE +FINALLY + +BEFORE +EXCEPT +FINALLY +RECOVER + +BEFORE +EXCEPT: IOError: hi +FINALLY +''' +""" + +echo "" + +proc no_exception = + try: + echo "BEFORE" + + except: + echo "EXCEPT" + raise + + finally: + echo "FINALLY" + +try: no_exception() +except: echo "RECOVER" + +echo "" + +proc reraise_in_except = + try: + echo "BEFORE" + raise newException(IOError, "") + + except IOError: + echo "EXCEPT" + raise + + finally: + echo "FINALLY" + +try: reraise_in_except() +except: echo "RECOVER" + +echo "" + +proc return_in_except = + try: + echo "BEFORE" + raise newException(IOError, "hi") + + except: + echo "EXCEPT: ", getCurrentException().name, ": ", getCurrentExceptionMsg() + return + + finally: + echo "FINALLY" + +try: return_in_except() +except: echo "RECOVER" + +block: #10417 + proc moo() {.noreturn.} = discard + + let bar = + try: + 1 + except: + moo() + + doAssert(bar == 1) + +# Make sure the VM handles the exceptions correctly +block: + proc fun1(): seq[int] = + try: + try: + raise newException(ValueError, "xx") + except: + doAssert("xx" == getCurrentExceptionMsg()) + raise newException(KeyError, "yy") + except: + doAssert("yy" == getCurrentExceptionMsg()) + result.add(1212) + try: + try: + raise newException(AssertionDefect, "a") + finally: + result.add(42) + except AssertionDefect: + result.add(99) + finally: + result.add(10) + result.add(4) + result.add(0) + try: + result.add(1) + except KeyError: + result.add(-1) + except ValueError: + result.add(-1) + except IndexDefect: + result.add(2) + except: + result.add(3) + + try: + try: + result.add(1) + return + except: + result.add(-1) + finally: + result.add(2) + except KeyError: + doAssert(false) + finally: + result.add(3) + + let x1 = fun1() + const x2 = fun1() + doAssert(x1 == x2)