libmicrohttpd

HTTP/1.x server C library (MHD 1.x, stable)
Log | Files | Refs | Submodules | README | LICENSE

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 */