Skip to content

Commit

Permalink
nimRawSetjmp: support Windows
Browse files Browse the repository at this point in the history
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: status-im/nimbus-eth2#3121

Also add "nimBuiltinSetjmp" - mostly for benchmarking.
  • Loading branch information
stefantalpalaru committed Nov 28, 2021
1 parent 5933aec commit 34265a7
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 9 deletions.
13 changes: 12 additions & 1 deletion compiler/ccgstmts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
14 changes: 14 additions & 0 deletions doc/nimc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------------
Expand Down
37 changes: 31 additions & 6 deletions lib/system/ansi_c.nim
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ proc c_abort*() {.
importc: "abort", header: "<stdlib.h>", 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: "<setjmp.h>", bycopy.} = object
abi: array[200 div sizeof(clong), clong]
Expand Down Expand Up @@ -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: "<setjmp.h>", 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: "<setjmp.h>", 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: "<setjmp.h>", importc: "_longjmp".}
importc: "__builtin_longjmp", nodecl.}
proc c_setjmp*(jmpb: C_JmpBuf): cint {.
header: "<setjmp.h>", 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: "<setjmp.h>", 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: "<setjmp.h>", importc: "_setjmp".}
c_setjmp_win(jmpb, nil)
else:
proc c_longjmp*(jmpb: C_JmpBuf, retval: cint) {.
header: "<setjmp.h>", importc: "_longjmp".}
proc c_setjmp*(jmpb: C_JmpBuf): cint {.
header: "<setjmp.h>", importc: "_setjmp".}
else:
proc c_longjmp*(jmpb: C_JmpBuf, retval: cint) {.
header: "<setjmp.h>", importc: "longjmp".}
Expand Down
6 changes: 4 additions & 2 deletions tests/exception/texceptions.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
discard """
disabled: "windows" # no sigsetjmp() there
matrix: "-d:nimStdSetjmp; -d:nimSigSetjmp; -d:nimRawSetjmp; -d:nimBuiltinSetjmp"
output: '''
BEFORE
Expand All @@ -17,7 +19,7 @@ FINALLY

echo ""

proc no_expcetion =
proc no_exception =
try:
echo "BEFORE"

Expand All @@ -28,7 +30,7 @@ proc no_expcetion =
finally:
echo "FINALLY"

try: no_expcetion()
try: no_exception()
except: echo "RECOVER"

echo ""
Expand Down
130 changes: 130 additions & 0 deletions tests/exception/texceptions2.nim
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 34265a7

Please sign in to comment.