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 "Platform.h"
30#if !ARDUINO_EMULATOR_WINDOWS
31#include <unistd.h>
32#else
33#include <atomic>
34#include <condition_variable>
35#include <cstdlib>
36#endif
37
38#undef INADDR_NONE
39
40// Generic signal handler utility
41//
42// dispatch() used to run the registered handlers (std::map lookup,
43// std::function calls) and call exit(0) directly from inside the real OS
44// signal handler. None of that is async-signal-safe: exit() in particular
45// is explicitly documented as unsafe to call from a signal handler, since
46// it runs atexit()/static-destructor cleanup - including destroying the
47// very std::map dispatch() itself was reading. In a multi-threaded process
48// (any sketch using audio/video/network libraries alongside this) a second
49// signal delivered to another thread while the first is mid-exit() re-enters
50// dispatch() and reads/frees that map concurrently - a genuine
51// heap-use-after-free, reproducible under AddressSanitizer.
52//
53// Fixed with the standard self-pipe trick: the real signal handler only
54// does the one thing that's actually async-signal-safe here - write() one
55// byte to a pipe - and a dedicated background thread, running in ordinary
56// (non-signal) context, blocks reading that pipe and does the unsafe work
57// (map lookup, invoking handlers, exit()) with no signal-safety
58// restrictions and no reentrancy risk.
59//
60// That move alone isn't enough, though: any sketch with other threads
61// still running at signal time (e.g. an audio callback thread, a decoder
62// thread) still races exit()'s normal C++ shutdown, which runs every
63// static/global object's destructor on the reaper thread while those
64// other threads may still be mid-call on the very same objects - observed
65// as "pure virtual method called" (a virtual call landing on an object
66// whose vtable was already torn down mid-destruction). The registered
67// HandlerFunc callbacks above are the sketch's actual cleanup and still
68// run first, in full; what follows uses _exit(), which ends the process
69// immediately without invoking any other static destructor or atexit
70// handler, so it can't race with them.
72 public:
73 using HandlerFunc = std::function<void(int)>;
74
75 static void registerHandler(int signum, HandlerFunc handler) {
76 auto& vec = getHandlers()[signum];
77 vec.push_back(handler);
78 std::signal(signum, SignalHandler::dispatch);
79 ensureReaperThread();
80 }
81
82 private:
83 static std::map<int, std::vector<HandlerFunc>>& getHandlers() {
84 static std::map<int, std::vector<HandlerFunc>> handlers;
85 return handlers;
86 }
87
88#if ARDUINO_EMULATOR_WINDOWS
89 static std::atomic<int>& pendingSignal() { static std::atomic<int> value{0}; return value; }
90 static std::condition_variable& condition() { static std::condition_variable value; return value; }
91 static std::mutex& conditionMutex() { static std::mutex value; return value; }
92#endif
93
94 // Async-signal-safe: writes one byte and returns. No map access, no
95 // std::function calls, no exit() - all of that is deferred to the
96 // reaper thread, well outside signal-handler context.
97 static void dispatch(int signum) {
98#if ARDUINO_EMULATOR_WINDOWS
99 pendingSignal() = signum;
100 condition().notify_one();
101#else
102 char sig = (char)signum;
103 ssize_t n = write(pipeWriteFd(), &sig, 1);
104 (void)n; // nothing safe to do with a failed write() from a handler
105#endif
106 }
107
108 static int& pipeWriteFd() {
109 static int fd = -1;
110 return fd;
111 }
112 static int& pipeReadFd() {
113 static int fd = -1;
114 return fd;
115 }
116
117 // Starts the reaper thread at most once, the first time any signal is
118 // registered - safe to call from registerHandler() every time.
119 static void ensureReaperThread() {
120 static std::once_flag started;
121 std::call_once(started, [] {
122#if ARDUINO_EMULATOR_WINDOWS
123 std::thread([] {
124 std::unique_lock<std::mutex> lock(conditionMutex());
125 condition().wait(lock, [] { return pendingSignal() != 0; });
126 int signum = pendingSignal();
127 auto& handlers = getHandlers();
128 auto it = handlers.find(signum);
129 if (it != handlers.end()) {
130 for (auto& func : it->second) func(signum);
131 }
132 std::_Exit(0);
133 }).detach();
134#else
135 int fds[2];
136 pipe(fds);
137 pipeReadFd() = fds[0];
138 pipeWriteFd() = fds[1];
139 std::thread([] {
140 char sig;
141 while (read(pipeReadFd(), &sig, 1) == 1) {
142 int signum = (int)(unsigned char)sig;
143 auto& handlers = getHandlers();
144 auto it = handlers.find(signum);
145 if (it != handlers.end()) {
146 for (auto& func : it->second) {
147 func(signum);
148 }
149 }
150 // not exit(): see the class comment - avoids racing other still
151 // -running threads against this thread's C++ static destructors.
152 _exit(0);
153 }
154 }).detach();
155#endif
156 });
157 }
158};
Definition SignalHandler.h:71