arduino-emulator
Loading...
Searching...
No Matches
SignalHandler.h
1/*
2 SignalHandler.h
3 Copyright (c) 2025 Phil Schatzmann. All right reserved.
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public
16 License along with this library; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18*/
19
20#pragma once
21#include <signal.h>
22#include <csignal>
23#include <functional>
24#include <vector>
25#include <map>
26#include <algorithm>
27#include <mutex>
28#include <thread>
29#include <unistd.h>
30
31#undef INADDR_NONE
32
33// Generic signal handler utility
34//
35// dispatch() used to run the registered handlers (std::map lookup,
36// std::function calls) and call exit(0) directly from inside the real OS
37// signal handler. None of that is async-signal-safe: exit() in particular
38// is explicitly documented as unsafe to call from a signal handler, since
39// it runs atexit()/static-destructor cleanup - including destroying the
40// very std::map dispatch() itself was reading. In a multi-threaded process
41// (any sketch using audio/video/network libraries alongside this) a second
42// signal delivered to another thread while the first is mid-exit() re-enters
43// dispatch() and reads/frees that map concurrently - a genuine
44// heap-use-after-free, reproducible under AddressSanitizer.
45//
46// Fixed with the standard self-pipe trick: the real signal handler only
47// does the one thing that's actually async-signal-safe here - write() one
48// byte to a pipe - and a dedicated background thread, running in ordinary
49// (non-signal) context, blocks reading that pipe and does the unsafe work
50// (map lookup, invoking handlers, exit()) with no signal-safety
51// restrictions and no reentrancy risk.
52//
53// That move alone isn't enough, though: any sketch with other threads
54// still running at signal time (e.g. an audio callback thread, a decoder
55// thread) still races exit()'s normal C++ shutdown, which runs every
56// static/global object's destructor on the reaper thread while those
57// other threads may still be mid-call on the very same objects - observed
58// as "pure virtual method called" (a virtual call landing on an object
59// whose vtable was already torn down mid-destruction). The registered
60// HandlerFunc callbacks above are the sketch's actual cleanup and still
61// run first, in full; what follows uses _exit(), which ends the process
62// immediately without invoking any other static destructor or atexit
63// handler, so it can't race with them.
65 public:
66 using HandlerFunc = std::function<void(int)>;
67
68 static void registerHandler(int signum, HandlerFunc handler) {
69 auto& vec = getHandlers()[signum];
70 vec.push_back(handler);
71 std::signal(signum, SignalHandler::dispatch);
72 ensureReaperThread();
73 }
74
75 private:
76 static std::map<int, std::vector<HandlerFunc>>& getHandlers() {
77 static std::map<int, std::vector<HandlerFunc>> handlers;
78 return handlers;
79 }
80
81 // Async-signal-safe: writes one byte and returns. No map access, no
82 // std::function calls, no exit() - all of that is deferred to the
83 // reaper thread, well outside signal-handler context.
84 static void dispatch(int signum) {
85 char sig = (char)signum;
86 ssize_t n = write(pipeWriteFd(), &sig, 1);
87 (void)n; // nothing safe to do with a failed write() from a handler
88 }
89
90 static int& pipeWriteFd() {
91 static int fd = -1;
92 return fd;
93 }
94 static int& pipeReadFd() {
95 static int fd = -1;
96 return fd;
97 }
98
99 // Starts the reaper thread at most once, the first time any signal is
100 // registered - safe to call from registerHandler() every time.
101 static void ensureReaperThread() {
102 static std::once_flag started;
103 std::call_once(started, [] {
104 int fds[2];
105 pipe(fds);
106 pipeReadFd() = fds[0];
107 pipeWriteFd() = fds[1];
108 std::thread([] {
109 char sig;
110 while (read(pipeReadFd(), &sig, 1) == 1) {
111 int signum = (int)(unsigned char)sig;
112 auto& handlers = getHandlers();
113 auto it = handlers.find(signum);
114 if (it != handlers.end()) {
115 for (auto& func : it->second) {
116 func(signum);
117 }
118 }
119 // not exit(): see the class comment - avoids racing other still
120 // -running threads against this thread's C++ static destructors.
121 _exit(0);
122 }
123 }).detach();
124 });
125 }
126};
Definition SignalHandler.h:64