Skip to content

Commit 41ce86b

Browse files
authored
Remove Nim signal handler for SIGINT (#25169)
Inside a signal handler, you cannot allocate memory because the signal handler, being implemented with a C [`signal`](https://en.cppreference.com/w/c/program/signal) call, can be called _during_ a memory allocation - when that happens, the CTRL-C handler causes a segfault and/or other inconsistent state. Similarly, the call can happen from a non-nim thread or inside a C library function call etc, most of which do not support reentrancy and therefore cannot be called _from_ a signal handler. The stack trace facility used in the default handler is unfortunately beyond fixing without more significant refactoring since it uses garbage-collected types in its API and implementation. As an alternative to #25110, this PR removes the most problematic signal handler, namely the one for SIGINT (ctrl-c) - SIGINT is special because it's meant to cause a regular shutdown of the application and crashes during SIGINT handling are both confusing and, if turned into SIGSEGV, have downstream effects like core dumps and OS crash reports. The signal handlers for the various crash scenarios remain as-is - they may too cause their own crashes but we're already going down in a bad way, so the harm is more limited - in particular, crashing during a crash handler corrupts `core`/crash dumps. Users wanting to keep their core files pristine should continue to use `-d:noSignalHandler` - this is usually the better option for production applications since they carry more detail than the Nim stack trace that gets printed. Finally, the example of a ctrl-c handler performs the same mistake of calling `echo` which is not well-defined - replace it with an example that is mostly correct (except maybe for the lack of `volatile` for the `stop` variable).
1 parent 51a9ada commit 41ce86b

File tree

5 files changed

+59
-20
lines changed

5 files changed

+59
-20
lines changed

lib/pure/cgi.nim

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,14 @@ Content-Type: text/html
289289
proc writeErrorMessage*(data: string) =
290290
## Tries to reset browser state and writes `data` to stdout in
291291
## <plaintext> tag.
292-
resetForStacktrace()
293-
# We use <plaintext> here, instead of escaping, so stacktrace can
294-
# be understood by human looking at source.
295-
stdout.write("<plaintext>\n")
296-
stdout.write(data)
292+
try:
293+
resetForStacktrace()
294+
# We use <plaintext> here, instead of escaping, so stacktrace can
295+
# be understood by human looking at source.
296+
stdout.write("<plaintext>\n")
297+
stdout.write(data)
298+
except IOError as exc:
299+
discard # Too bad..
297300

298301
proc setStackTraceStdout*() =
299302
## Makes Nim output stacktraces to stdout, instead of server log.

lib/system.nim

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,18 +2194,33 @@ when not defined(js):
21942194
when declared(initGC): initGC()
21952195

21962196
when notJSnotNims:
2197-
proc setControlCHook*(hook: proc () {.noconv.})
2197+
proc setControlCHook*(hook: proc () {.noconv.}) {.raises: [], gcsafe.}
21982198
## Allows you to override the behaviour of your application when CTRL+C
21992199
## is pressed. Only one such hook is supported.
2200-
## Example:
22012200
##
2202-
## ```nim
2201+
## The handler runs inside a C signal handler and comes with similar
2202+
## limitations.
2203+
##
2204+
## Allocating memory and interacting with most system calls, including using
2205+
## `echo`, `string`, `seq`, raising or catching exceptions etc is undefined
2206+
## behavior and will likely lead to application crashes.
2207+
##
2208+
## The OS may call the ctrl-c handler from any thread, including threads
2209+
## that were not created by Nim, such as happens on Windows.
2210+
##
2211+
## ## Example:
2212+
##
2213+
## ```nim
2214+
## var stop: Atomic[bool]
22032215
## proc ctrlc() {.noconv.} =
2204-
## echo "Ctrl+C fired!"
2205-
## # do clean up stuff
2206-
## quit()
2216+
## # Using atomics types is safe!
2217+
## stop.store(true)
22072218
##
22082219
## setControlCHook(ctrlc)
2220+
##
2221+
## while not stop.load():
2222+
## echo "Still running.."
2223+
## sleep(1000)
22092224
## ```
22102225

22112226
when not defined(noSignalHandler) and not defined(useNimRtl):

lib/system/ansi_c.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# and definitions of Ansi C types in Nim syntax
1212
# All symbols are prefixed with 'c_' to avoid ambiguities
1313

14-
{.push hints:off, stack_trace: off, profiler: off.}
14+
{.push hints:off, stack_trace: off, profiler: off, raises: [].}
1515

1616
proc c_memchr*(s: pointer, c: cint, n: csize_t): pointer {.
1717
importc: "memchr", header: "<string.h>".}

lib/system/excpt.nim

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const noStacktraceAvailable = "No stack traceback available\n"
1717

1818
var
1919
errorMessageWriter*: (proc(msg: string) {.tags: [WriteIOEffect], benign,
20-
nimcall.})
20+
nimcall, raises: [].})
2121
## Function that will be called
2222
## instead of `stdmsg.write` when printing stacktrace.
2323
## Unstable API.
@@ -58,6 +58,7 @@ proc showErrorMessage(data: cstring, length: int) {.gcsafe, raises: [].} =
5858
writeToStdErr(data, length)
5959

6060
proc showErrorMessage2(data: string) {.inline.} =
61+
# TODO showErrorMessage will turn it back to a string when a hook is set (!)
6162
showErrorMessage(data.cstring, data.len)
6263

6364
proc chckIndx(i, a, b: int): int {.inline, compilerproc, benign.}
@@ -619,7 +620,7 @@ when not defined(noSignalHandler) and not defined(useNimRtl):
619620
type Sighandler = proc (a: cint) {.noconv, benign.}
620621
# xxx factor with ansi_c.CSighandlerT, posix.Sighandler
621622

622-
proc signalHandler(sign: cint) {.exportc: "signalHandler", noconv.} =
623+
proc signalHandler(sign: cint) {.exportc: "signalHandler", noconv, raises: [].} =
623624
template processSignal(s, action: untyped) {.dirty.} =
624625
if s == SIGINT: action("SIGINT: Interrupted by Ctrl-C.\n")
625626
elif s == SIGSEGV:
@@ -641,6 +642,22 @@ when not defined(noSignalHandler) and not defined(useNimRtl):
641642
# print stack trace and quit
642643
when defined(memtracker):
643644
logPendingOps()
645+
# On windows, it is common that the signal handler is called from a non-Nim
646+
# thread and any allocation will (likely) cause a crash. Since we're about
647+
# to quit, we can try setting up the GC - the correct course of action is to
648+
# not use the GC at all in signal handlers but that requires redesigning
649+
# the stack trace mechanism
650+
when defined(windows):
651+
when declared(initStackBottom):
652+
initStackBottom()
653+
when declared(initGC):
654+
initGC()
655+
656+
# On other platforms, if memory needs to be allocated and the signal happens
657+
# during memory allocation, we'll also (likely) see a crash and corrupt the
658+
# memory allocator - less frequently than on windows but still.
659+
# However, since we're about to go down anyway, YOLO.
660+
644661
when hasSomeStackTrace:
645662
when not usesDestructors: GC_disable()
646663
var buf = newStringOfCap(2000)
@@ -653,14 +670,17 @@ when not defined(noSignalHandler) and not defined(useNimRtl):
653670
template asgn(y) =
654671
msg = y
655672
processSignal(sign, asgn)
656-
# xxx use string for msg instead of cstring, and here use showErrorMessage2(msg)
657-
# unless there's a good reason to use cstring in signal handler to avoid
658-
# using gc?
673+
# showErrorMessage may allocate, which may cause a crash, and calls C
674+
# library functions which is undefined behavior, ie it may also crash.
675+
# Nevertheless, we sometimes manage to emit the message regardless which
676+
# pragmatically makes this attempt "useful enough".
677+
# See also https://en.cppreference.com/w/c/program/signal
659678
showErrorMessage(msg, msg.len)
660679

661680
when defined(posix):
662681
# reset the signal handler to OS default
663-
c_signal(sign, SIG_DFL)
682+
{.cast(raises: []).}: # Work around -d:laxEffects bugs
683+
discard c_signal(sign, SIG_DFL)
664684

665685
# re-raise the signal, which will arrive once this handler exit.
666686
# this lets the OS perform actions like core dumping and will
@@ -697,4 +717,5 @@ proc setControlCHook(hook: proc () {.noconv.}) =
697717
when not defined(noSignalHandler) and not defined(useNimRtl):
698718
proc unsetControlCHook() =
699719
# proc to unset a hook set by setControlCHook
700-
c_signal(SIGINT, signalHandler)
720+
{.gcsafe.}: # Work around -d:laxEffects bugs
721+
discard c_signal(SIGINT, signalHandler)

lib/system/memtracker.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type
3535
count*: int
3636
disabled: bool
3737
data*: array[400, LogEntry]
38-
TrackLogger* = proc (log: TrackLog) {.nimcall, tags: [], gcsafe.}
38+
TrackLogger* = proc (log: TrackLog) {.nimcall, tags: [], gcsafe, raises: [].}
3939

4040
var
4141
gLog*: TrackLog

0 commit comments

Comments
 (0)