mhd_panic_tripwire.h (16531B)
1 /* 2 This file is part of libmicrohttpd 3 Copyright (C) 2026 Christian Grothoff 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. 17 If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 /** 21 * @file microhttpd/mhd_panic_tripwire.h 22 * @brief Test-only tripwire that turns a fatal libmicrohttpd invariant 23 * failure into a loud, greppable, machine-classifiable test error. 24 * @author Christian Grothoff 25 * 26 * Rationale (TESTING.md, proposal P5): no network input must ever be able to 27 * reach #MHD_PANIC() or a failing mhd_assert(). When it does, the test suite 28 * should say so in a way that a CI job or the option-matrix driver can pick 29 * out mechanically, instead of leaving behind an anonymous SIGABRT that is 30 * indistinguishable from an ordinary test failure. 31 * 32 * USAGE 33 * ----- 34 * Add exactly one line to a test program: 35 * 36 * #include "mhd_panic_tripwire.h" 37 * 38 * Nothing else is needed: on GCC/clang the header installs itself from a 39 * `__attribute__((constructor))`, which runs after the libmicrohttpd shared 40 * object has run its own initialiser (and therefore after MHD_init() has 41 * installed the stock panic handler), so the tripwire wins. On toolchains 42 * without constructor support, call mhd_panic_tripwire_install() as the first 43 * statement of main() instead. 44 * 45 * WHAT IS INTERCEPTED 46 * ------------------- 47 * 1. #MHD_PANIC() - via the public MHD_set_panic_func() hook. The reference 48 * to MHD_set_panic_func() is *weak*, so this header can also be included 49 * by the unit tests that compile a couple of library objects directly and 50 * do not link libmicrohttpd at all; there the hook is simply skipped. 51 * 2. abort() - via a SIGABRT handler. This matters because in this code base 52 * the overwhelming majority of "fatal invariant reached from network 53 * input" sites are mhd_assert(), which goes to assert(3)/abort(3) and 54 * never passes through mhd_panic(). Set MHD_TEST_PANIC_TRIPWIRE_ABRT=0 55 * to intercept #MHD_PANIC() only. 56 * 57 * WHY THE HANDLER MUST NOT RETURN, AND WHY IT DOES NOT longjmp() 58 * -------------------------------------------------------------- 59 * #MHD_PANIC(msg) expands to `mhd_panic (...); BUILTIN_NOT_REACHED;`, i.e. 60 * __builtin_unreachable(). A panic handler that returns therefore drops the 61 * caller straight into undefined behaviour - the compiler has already been 62 * told that the following code is dead and may have deleted it. So the 63 * handler has to terminate the process. 64 * 65 * longjmp() back into main() is not an option either: MHD_PANIC() is reached 66 * from library code that may be running on a daemon worker thread or on a 67 * thread-per-connection thread, and a longjmp() across threads is undefined. 68 * Even on the right thread it would leave the daemon's mutexes locked and its 69 * connection lists half-updated, so any "recording" done afterwards would run 70 * in a process whose state is already corrupt. We therefore report and 71 * terminate rather than recover. 72 * 73 * TERMINATION STATUS 74 * ------------------ 75 * The default is _exit(99). The automake parallel test harness classifies 76 * exit status 99 as a hard ERROR rather than an ordinary FAIL, which is 77 * exactly the "this is not a normal test failure, a fatal invariant was 78 * reached" signal wanted here. Note that this also applies to tests listed 79 * in XFAIL_TESTS - a test that is *expected* to trip an assertion (such as 80 * src/microhttpd/test_known_bugs.c) must therefore NOT include this header. 81 * 82 * ENVIRONMENT VARIABLES 83 * --------------------- 84 * @c MHD_TEST_PANIC_TRIPWIRE 85 * - unset, or any unrecognised value: enabled, terminate with _exit(99). 86 * - @c 0 / @c off / @c no / @c disable: completely disabled; the stock 87 * libmicrohttpd behaviour (mhd_panic_std(), plain abort()) is left in 88 * place. Use this when a debugger or an external tool wants the 89 * original behaviour. 90 * - @c abort: print the marker and the stack trace, then re-raise SIGABRT 91 * with the default disposition so that a core dump is produced. 92 * - @c exit:N (0 <= N <= 255): print the marker and the stack trace, then 93 * terminate with status N instead of 99. 94 * 95 * @c MHD_TEST_PANIC_TRIPWIRE_ABRT 96 * - @c 0 / @c off / @c no / @c disable: do not install the SIGABRT handler; 97 * only #MHD_PANIC() is intercepted. Failing mhd_assert()s then abort as 98 * usual. 99 * - unset or anything else: the SIGABRT handler is installed. 100 * 101 * OUTPUT FORMAT 102 * ------------- 103 * A single greppable marker line is written to stderr, followed (where 104 * available) by a stack trace and a final status line: 105 * 106 * MHD-PANIC-TRIPWIRE: kind=MHD_PANIC file=connection.c line=1234 \ 107 * reason=Data offset exceeds limit. 108 * MHD-PANIC-TRIPWIRE-FRAME: ... 109 * MHD-PANIC-TRIPWIRE: terminating with exit status 99 110 * 111 * Grep for #MHD_PANIC_TRIPWIRE_MARKER ("MHD-PANIC-TRIPWIRE") to classify a 112 * failure. Everything is emitted with write(2) and without allocating, so 113 * the same code path is usable from the SIGABRT handler. 114 */ 115 116 #ifndef MHD_PANIC_TRIPWIRE_H 117 #define MHD_PANIC_TRIPWIRE_H 1 118 119 #include <stdlib.h> 120 #include <string.h> 121 #include <signal.h> 122 #ifdef _WIN32 123 #include <io.h> 124 #else /* ! _WIN32 */ 125 #include <unistd.h> 126 #endif /* ! _WIN32 */ 127 128 /** 129 * The distinctive, greppable marker that prefixes every line printed by the 130 * tripwire. 131 */ 132 #define MHD_PANIC_TRIPWIRE_MARKER "MHD-PANIC-TRIPWIRE" 133 134 /** 135 * The exit status used by default. The automake test harness reports this 136 * as a hard ERROR instead of a FAIL. 137 */ 138 #define MHD_PANIC_TRIPWIRE_EXIT_STATUS 99 139 140 /* Detect a usable backtrace() implementation. This must not depend on 141 configure, because this header is also used from test directories that do 142 not run any dedicated check for it. Degrade silently when absent. */ 143 #if ! defined(MHD_PANIC_TRIPWIRE_BACKTRACE_) 144 # if defined(__has_include ) 145 # if __has_include (<execinfo.h>) 146 # define MHD_PANIC_TRIPWIRE_BACKTRACE_ 1 147 # endif /* __has_include(<execinfo.h>) */ 148 # elif defined(HAVE_EXECINFO_H) || defined(__GLIBC__) 149 # define MHD_PANIC_TRIPWIRE_BACKTRACE_ 1 150 # endif /* HAVE_EXECINFO_H || __GLIBC__ */ 151 #endif /* ! MHD_PANIC_TRIPWIRE_BACKTRACE_ */ 152 #ifdef MHD_PANIC_TRIPWIRE_BACKTRACE_ 153 #include <execinfo.h> 154 #endif /* MHD_PANIC_TRIPWIRE_BACKTRACE_ */ 155 156 /* Weak references let this header be included by the unit tests that do not 157 link libmicrohttpd (test_str*, test_md5, test_sha*, test_auth_parse, ...). 158 Without weak-symbol support the MHD_PANIC() hook is simply not installed 159 and only the abort() hook remains. */ 160 #if defined(__GNUC__) && defined(__ELF__) && ! defined(_WIN32) 161 # define MHD_PANIC_TRIPWIRE_WEAK_ __attribute__ ((weak)) 162 #endif /* __GNUC__ && __ELF__ && ! _WIN32 */ 163 164 #ifdef MHD_PANIC_TRIPWIRE_WEAK_ 165 /* Deliberately declared here instead of pulling in <microhttpd.h>: the 166 signature is identical to MHD_PanicCallback, so this declaration is 167 compatible with the one in <microhttpd.h> when that header is also 168 included, and it keeps this file usable in translation units that never 169 see the public header. */ 170 extern MHD_PANIC_TRIPWIRE_WEAK_ void 171 MHD_set_panic_func (void (*cb)(void *cls, 172 const char *file, 173 unsigned int line, 174 const char *reason), 175 void *cls); 176 177 #endif /* MHD_PANIC_TRIPWIRE_WEAK_ */ 178 179 #if defined(__GNUC__) || defined(__clang__) 180 # define MHD_PANIC_TRIPWIRE_UNUSED_ __attribute__ ((unused)) 181 # define MHD_PANIC_TRIPWIRE_NORETURN_ __attribute__ ((noreturn)) 182 #else /* ! __GNUC__ */ 183 # define MHD_PANIC_TRIPWIRE_UNUSED_ 184 # define MHD_PANIC_TRIPWIRE_NORETURN_ 185 #endif /* ! __GNUC__ */ 186 187 /** 188 * Selected action, resolved once by the installer so that the handlers 189 * themselves never have to call getenv(). 190 * 0: terminate with #mhd_panic_tripwire_status_; 1: re-raise SIGABRT. 191 */ 192 static volatile sig_atomic_t mhd_panic_tripwire_reraise_ = 0; 193 194 /** 195 * The exit status to use when #mhd_panic_tripwire_reraise_ is zero. 196 */ 197 static volatile sig_atomic_t mhd_panic_tripwire_status_ = 198 MHD_PANIC_TRIPWIRE_EXIT_STATUS; 199 200 /** 201 * Set once a report is in progress, so that a second thread tripping at the 202 * same time does not interleave its output with the first one. 203 */ 204 static volatile sig_atomic_t mhd_panic_tripwire_busy_ = 0; 205 206 207 /** 208 * Write a string literal to stderr. 209 * 210 * @param str a string literal 211 */ 212 #define MHD_PANIC_TRIPWIRE_WRS_(str) \ 213 mhd_panic_tripwire_wr_ (str, sizeof(str) - 1) 214 215 216 /** 217 * Write a buffer to stderr. Async-signal-safe: no locking, no allocation, 218 * no stdio. 219 * 220 * @param buf the bytes to write 221 * @param len the number of bytes to write 222 */ 223 static void 224 mhd_panic_tripwire_wr_ (const char *buf, 225 size_t len) 226 { 227 size_t off = 0; 228 229 while (off < len) 230 { 231 #ifdef _WIN32 232 int res = _write (2, buf + off, (unsigned int) (len - off)); 233 #else /* ! _WIN32 */ 234 ssize_t res = write (STDERR_FILENO, buf + off, len - off); 235 #endif /* ! _WIN32 */ 236 if (0 >= res) 237 return; /* Give up rather than spin. */ 238 off += (size_t) res; 239 } 240 } 241 242 243 /** 244 * Write a NUL-terminated string to stderr, replacing every control character 245 * by a space so that the marker always stays on a single line. 246 * 247 * @param str the string to write, may be NULL 248 */ 249 static void 250 mhd_panic_tripwire_wr_clean_ (const char *str) 251 { 252 char chunk[256]; 253 size_t used = 0; 254 255 if (NULL == str) 256 { 257 MHD_PANIC_TRIPWIRE_WRS_ ("(null)"); 258 return; 259 } 260 while (0 != *str) 261 { 262 const unsigned char chr = (unsigned char) *str++; 263 264 chunk[used++] = (0x20 > chr) ? ' ' : (char) chr; 265 if (sizeof(chunk) == used) 266 { 267 mhd_panic_tripwire_wr_ (chunk, used); 268 used = 0; 269 } 270 } 271 if (0 != used) 272 mhd_panic_tripwire_wr_ (chunk, used); 273 } 274 275 276 /** 277 * Write an unsigned number in decimal to stderr. 278 * 279 * @param val the value to print 280 */ 281 static void 282 mhd_panic_tripwire_wr_num_ (unsigned long val) 283 { 284 char buf[24]; 285 size_t pos = sizeof(buf); 286 287 do 288 { 289 buf[--pos] = (char) ('0' + (val % 10)); 290 val /= 10; 291 } while (0 != val); 292 mhd_panic_tripwire_wr_ (buf + pos, sizeof(buf) - pos); 293 } 294 295 296 /** 297 * Print the stack trace of the calling thread, if the platform provides one. 298 * Silently does nothing otherwise. 299 */ 300 static void 301 mhd_panic_tripwire_backtrace_ (void) 302 { 303 #ifdef MHD_PANIC_TRIPWIRE_BACKTRACE_ 304 void *frames[64]; 305 int num; 306 307 num = backtrace (frames, (int) (sizeof(frames) / sizeof(frames[0]))); 308 if (0 >= num) 309 return; 310 MHD_PANIC_TRIPWIRE_WRS_ (MHD_PANIC_TRIPWIRE_MARKER "-FRAMES: "); 311 mhd_panic_tripwire_wr_num_ ((unsigned long) num); 312 MHD_PANIC_TRIPWIRE_WRS_ (" frames follow\n"); 313 /* backtrace_symbols_fd() does not allocate, unlike backtrace_symbols(). */ 314 backtrace_symbols_fd (frames, num, 2); 315 #endif /* MHD_PANIC_TRIPWIRE_BACKTRACE_ */ 316 } 317 318 319 /** 320 * Print the marker line and the stack trace, then terminate the process. 321 * Never returns; see the file comment for why recovering is not an option. 322 * 323 * @param kind a short token naming what was intercepted 324 * @param file the source file of the failure, may be NULL 325 * @param line the source line of the failure 326 * @param reason the human-readable reason, may be NULL 327 */ 328 MHD_PANIC_TRIPWIRE_NORETURN_ static void 329 mhd_panic_tripwire_report_ (const char *kind, 330 const char *file, 331 unsigned int line, 332 const char *reason) 333 { 334 if (0 == mhd_panic_tripwire_busy_) 335 { 336 mhd_panic_tripwire_busy_ = 1; 337 MHD_PANIC_TRIPWIRE_WRS_ ("\n" MHD_PANIC_TRIPWIRE_MARKER ": kind="); 338 mhd_panic_tripwire_wr_clean_ (kind); 339 MHD_PANIC_TRIPWIRE_WRS_ (" file="); 340 mhd_panic_tripwire_wr_clean_ ((NULL != file) ? file : "(unknown)"); 341 MHD_PANIC_TRIPWIRE_WRS_ (" line="); 342 mhd_panic_tripwire_wr_num_ ((unsigned long) line); 343 MHD_PANIC_TRIPWIRE_WRS_ (" reason="); 344 mhd_panic_tripwire_wr_clean_ ((NULL != reason) ? reason : "(none)"); 345 MHD_PANIC_TRIPWIRE_WRS_ ("\n"); 346 mhd_panic_tripwire_backtrace_ (); 347 if (0 == mhd_panic_tripwire_reraise_) 348 { 349 MHD_PANIC_TRIPWIRE_WRS_ (MHD_PANIC_TRIPWIRE_MARKER 350 ": terminating with exit status "); 351 mhd_panic_tripwire_wr_num_ ((unsigned long) mhd_panic_tripwire_status_); 352 MHD_PANIC_TRIPWIRE_WRS_ ("\n"); 353 } 354 else 355 MHD_PANIC_TRIPWIRE_WRS_ (MHD_PANIC_TRIPWIRE_MARKER 356 ": re-raising SIGABRT for a core dump\n"); 357 } 358 if (0 != mhd_panic_tripwire_reraise_) 359 { 360 signal (SIGABRT, SIG_DFL); 361 raise (SIGABRT); 362 } 363 _exit ((int) mhd_panic_tripwire_status_); 364 } 365 366 367 /** 368 * The #MHD_PanicCallback installed by the tripwire. 369 * 370 * @param cls unused 371 * @param file the name of the file with the problem 372 * @param line the line number with the problem 373 * @param reason the error message with details 374 */ 375 static void 376 mhd_panic_tripwire_panic_cb_ (void *cls, 377 const char *file, 378 unsigned int line, 379 const char *reason) 380 { 381 (void) cls; /* Mute compiler warning. */ 382 mhd_panic_tripwire_report_ ("MHD_PANIC", 383 file, 384 line, 385 reason); 386 } 387 388 389 /** 390 * The SIGABRT handler installed by the tripwire. Catches a failing 391 * mhd_assert() (which goes through assert(3) and abort(3)) and any other 392 * abort(), for example from the C library's own consistency checks. 393 * 394 * @param sig the signal number, always SIGABRT 395 */ 396 static void 397 mhd_panic_tripwire_abrt_cb_ (int sig) 398 { 399 (void) sig; /* Mute compiler warning. */ 400 mhd_panic_tripwire_report_ ("abort", 401 NULL, 402 0, 403 "abort() reached; see the line above for the " 404 "failed assertion, if any"); 405 } 406 407 408 /** 409 * Test whether an environment variable value means "off". 410 * 411 * @param val the value to test, may be NULL 412 * @return non-zero if @a val explicitly disables a feature 413 */ 414 static int 415 mhd_panic_tripwire_is_off_ (const char *val) 416 { 417 if (NULL == val) 418 return 0; 419 return (0 == strcmp (val, "0")) || 420 (0 == strcmp (val, "off")) || 421 (0 == strcmp (val, "no")) || 422 (0 == strcmp (val, "disable")); 423 } 424 425 426 /** 427 * Install the panic tripwire. Called automatically from a constructor on 428 * GCC-compatible toolchains; call it explicitly as the first statement of 429 * main() on any other toolchain. Idempotent. 430 */ 431 MHD_PANIC_TRIPWIRE_UNUSED_ static void 432 mhd_panic_tripwire_install (void) 433 { 434 const char *mode = getenv ("MHD_TEST_PANIC_TRIPWIRE"); 435 436 if (mhd_panic_tripwire_is_off_ (mode)) 437 return; /* Leave the stock behaviour completely untouched. */ 438 if ((NULL != mode) && (0 == strcmp (mode, "abort"))) 439 mhd_panic_tripwire_reraise_ = 1; 440 else if ((NULL != mode) && (0 == strncmp (mode, "exit:", 5))) 441 { 442 const long status = strtol (mode + 5, NULL, 10); 443 444 if ((0 <= status) && (255 >= status)) 445 mhd_panic_tripwire_status_ = (sig_atomic_t) status; 446 } 447 #ifdef MHD_PANIC_TRIPWIRE_WEAK_ 448 if (NULL != MHD_set_panic_func) 449 MHD_set_panic_func (&mhd_panic_tripwire_panic_cb_, 450 NULL); 451 #else /* ! MHD_PANIC_TRIPWIRE_WEAK_ */ 452 MHD_set_panic_func (&mhd_panic_tripwire_panic_cb_, 453 NULL); 454 #endif /* ! MHD_PANIC_TRIPWIRE_WEAK_ */ 455 if (! mhd_panic_tripwire_is_off_ (getenv ("MHD_TEST_PANIC_TRIPWIRE_ABRT"))) 456 signal (SIGABRT, 457 &mhd_panic_tripwire_abrt_cb_); 458 } 459 460 461 #if defined(__GNUC__) || defined(__clang__) 462 /** 463 * Self-installer, so that adding the tripwire to a test program is a single 464 * #include line. Runs after the libmicrohttpd initialiser (the library is a 465 * dependency of the test executable and is therefore initialised first), so 466 * MHD_init()'s call to MHD_set_panic_func(NULL, NULL) cannot undo this. 467 */ 468 __attribute__ ((constructor)) static void 469 mhd_panic_tripwire_ctor_ (void) 470 { 471 mhd_panic_tripwire_install (); 472 } 473 474 475 #endif /* __GNUC__ || __clang__ */ 476 477 #endif /* ! MHD_PANIC_TRIPWIRE_H */