Skip to content

Commit ce39224

Browse files
committed
Reduce chance of crashes in CTRL-C handler, rework handler example
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, but we can at least allocate the buffer outside of the signal handler itself - hopefully, this should reduce the frequency of crashes, if nothing else. It will still crash from time to time on Windows in particular, but since we're about to quit without cleanup, the loss of functionality is simited (ie the stack trace will not show and a crash dump will happen which the OS will notice). 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). eh skip signal handler in alloc-counting tests more alloc stats raises? fun with raises more tests reenable on windows we're about to terminate anyway
1 parent ff9cae8 commit ce39224

23 files changed

+84
-43
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: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2201,22 +2201,37 @@ when not defined(js):
22012201
when declared(initGC): initGC()
22022202

22032203
when notJSnotNims:
2204-
proc setControlCHook*(hook: proc () {.noconv.})
2204+
proc setControlCHook*(hook: proc () {.noconv.}) {.raises: [], gcsafe.}
22052205
## Allows you to override the behaviour of your application when CTRL+C
22062206
## is pressed. Only one such hook is supported.
2207-
## Example:
22082207
##
2209-
## ```nim
2208+
## The handler runs inside a C signal handler and comes with similar
2209+
## limitations.
2210+
##
2211+
## Allocating memory and interacting with most system calls, including using
2212+
## `echo`, `string`, `seq`, raising or catching exceptions etc is undefined
2213+
## behavior and will likely lead to application crashes.
2214+
##
2215+
## The OS may call the ctrl-c handler from any thread, including threads
2216+
## that were not created by Nim, such as happens on Windows.
2217+
##
2218+
## ## Example:
2219+
##
2220+
## ```nim
2221+
## var stop: Atomic[bool]
22102222
## proc ctrlc() {.noconv.} =
2211-
## echo "Ctrl+C fired!"
2212-
## # do clean up stuff
2213-
## quit()
2223+
## # Using atomics types is safe!
2224+
## stop.store(true)
22142225
##
22152226
## setControlCHook(ctrlc)
2227+
##
2228+
## while not stop.load():
2229+
## echo "Still running.."
2230+
## sleep(1000)
22162231
## ```
22172232

22182233
when not defined(noSignalHandler) and not defined(useNimRtl):
2219-
proc unsetControlCHook*()
2234+
proc unsetControlCHook*() {.raises: [], gcsafe.}
22202235
## Reverts a call to setControlCHook.
22212236

22222237
when hostOS != "standalone":

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: 34 additions & 11 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,19 @@ 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+
# TODO this allocation happens in the main thread while the CTRL-C handler
624+
# runs in any thread - since we're about to quit, hopefully nobody will
625+
# notice.
626+
# 32kb is hopefully enough to not cause further allocations, since
627+
# allocations are not well-defined in a signal handler.
628+
# Unfortunately, `showErrorMessage2` and `rawWriteStackTrace` may still
629+
# perform allocations on their own which will continue to cause crashes.
630+
# Fixing this requires changing the API which in turn requires a larger
631+
# refactor of the stack trace handling mechanism, including its support
632+
# for libbacktrace.
633+
var sigHandlerBuf = newStringOfCap(32*1024)
634+
635+
proc signalHandler(sign: cint) {.exportc: "signalHandler", noconv, raises: [].} =
623636
template processSignal(s, action: untyped) {.dirty.} =
624637
if s == SIGINT: action("SIGINT: Interrupted by Ctrl-C.\n")
625638
elif s == SIGSEGV:
@@ -641,26 +654,35 @@ when not defined(noSignalHandler) and not defined(useNimRtl):
641654
# print stack trace and quit
642655
when defined(memtracker):
643656
logPendingOps()
657+
# On windows, it is common that the signal handler is called from a non-Nim
658+
# thread and any allocation will (likely) cause a crash.
659+
# On other platforms, if memory needs to be allocated and the signal happens
660+
# during memory allocation, we'll alos (likely) see a crash and corrupt the
661+
# memory allocator - less frequently than on windows but still.
662+
# However, since we're about to go down anyway, YOLO.
663+
644664
when hasSomeStackTrace:
645665
when not usesDestructors: GC_disable()
646-
var buf = newStringOfCap(2000)
647-
rawWriteStackTrace(buf)
648-
processSignal(sign, buf.add) # nice hu? currying a la Nim :-)
649-
showErrorMessage2(buf)
666+
rawWriteStackTrace(sigHandlerBuf)
667+
processSignal(sign, sigHandlerBuf.add) # nice hu? currying a la Nim :-)
668+
showErrorMessage2(sigHandlerBuf)
650669
when not usesDestructors: GC_enable()
651670
else:
652671
var msg: cstring
653672
template asgn(y) =
654673
msg = y
655674
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?
675+
# showErrorMessage may allocate, which may cause a crash, and calls C
676+
# library functions which is undefined behavior, ie it may also crash.
677+
# Nevertheless, we sometimes manage to emit the message regardless which
678+
# pragmatically makes this attempt "useful enough".
679+
# See also https://en.cppreference.com/w/c/program/signal
659680
showErrorMessage(msg, msg.len)
660681

661682
when defined(posix):
662683
# reset the signal handler to OS default
663-
c_signal(sign, SIG_DFL)
684+
{.cast(raises: []).}: # Work around -d:laxEffects bugs
685+
discard c_signal(sign, SIG_DFL)
664686

665687
# re-raise the signal, which will arrive once this handler exit.
666688
# this lets the OS perform actions like core dumping and will
@@ -697,4 +719,5 @@ proc setControlCHook(hook: proc () {.noconv.}) =
697719
when not defined(noSignalHandler) and not defined(useNimRtl):
698720
proc unsetControlCHook() =
699721
# proc to unset a hook set by setControlCHook
700-
c_signal(SIGINT, signalHandler)
722+
{.gcsafe.}: # Work around -d:laxEffects bugs
723+
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

tests/arc/tasyncleak.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
discard """
22
outputsub: "(allocCount: 4011, deallocCount: 4009)"
3-
cmd: "nim c --gc:orc -d:nimAllocStats $file"
3+
cmd: "nim c --gc:orc -d:nimAllocStats -d:noSignalHandler $file"
44
"""
55

66
import asyncdispatch

tests/arc/tasyncleak2.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
discard """
22
output: "success"
3-
cmd: "nim c --gc:orc $file"
3+
cmd: "nim c --gc:orc -d:noSignalHandler $file"
44
"""
55

66
# issue #15076

tests/arc/tclosureiter.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
discard """
2-
cmd: '''nim c -d:nimAllocStats --gc:arc $file'''
2+
cmd: '''nim c -d:nimAllocStats -d:noSignalHandler --gc:arc $file'''
33
output: '''(allocCount: 102, deallocCount: 102)'''
44
"""
55

tests/arc/tdeepcopy.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
discard """
2-
cmd: "nim c --gc:arc --deepcopy:on $file"
2+
cmd: "nim c --gc:arc -d:noSignalHandler --deepcopy:on $file"
33
output: '''13 abc
44
13 abc
55
13 abc

tests/arc/tdestroy_in_loopcond.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
discard """
22
output: '''400 true'''
3-
cmd: "nim c --gc:orc $file"
3+
cmd: "nim c --gc:orc -d:noSignalHandler $file"
44
"""
55

66
type HeapQueue*[T] = object

0 commit comments

Comments
 (0)