• meiravgri's avatar
    Fix async safety in signal handlers (#12658) · 2e854bcc
    meiravgri authored
    see discussion from after https://github.com/redis/redis/pull/12453 was
    merged
    ----
    This PR replaces signals that are not considered async-signal-safe
    (AS-safe) with safe calls.
    
    #### **1. serverLog() and serverLogFromHandler()**
    `serverLog` uses unsafe calls. It was decided that we will **avoid**
    `serverLog` calls by the signal handlers when:
    * The signal is not fatal, such as SIGALRM. In these cases, we prefer
    using `serverLogFromHandler` which is the safe version of `serverLog`.
    Note they have different prompts:
    `serverLog`: `62220:M 26 Oct 2023 14:39:04.526 # <msg>`
    `serverLogFromHandler`: `62220:signal-handler (1698331136) <msg>`
    * The code was added recently. Calls to `serverLog` by the signal
    handler have been there ever since Redis exists and it hasn't caused
    problems so far. To avoid regression, from now we should use
    `serverLogFromHandler`
    
    #### **2. `snprintf` `fgets` and `strtoul`(base = 16) -------->
    `_safe_snprintf`, `fgets_async_signal_safe`, `string_to_hex`**
    The safe version of `snprintf` was taken from
    [here](https://github.com/twitter/twemcache/blob/8cfc4ca5e76ed936bd3786c8cc43ed47e7778c08/src/mc_util.c#L754)
    
    #### **3. fopen(), fgets(), fclose() --------> open(), read(), close()**
    
    #### **4. opendir(), readdir(), closedir() --------> open(),
    syscall(SYS_getdents64), close()**
    
    #### **5. Threads_mngr sync mechanisms**
    * waiting for the thread to generate stack trace: semaphore -------->
    busy-wait
    * `globals_rw_lock` was removed: as we are not using malloc and the
    semaphore anymore we don't need to protect `ThreadsManager_cleanups`.
    
    #### **6. Stacktraces buffer**
    The initial problem was that we were not able to safely call malloc
    within the signal handler.
    To solve that we created a buffer on the stack of `writeStacktraces` and
    saved it in a global pointer, assuming that under normal circumstances,
    the function `writeStacktraces` would complete before any thread
    attempted to write to it. However, **if threads lag behind, they might
    access this global pointer after it no longer belongs to the
    `writeStacktraces` stack, potentially corrupting memory.**
    To address this, various solutions were discussed
    [here](https://github.com/redis/redis/pull/12658#discussion_r1390442896)
    Eventually, we decided to **create a pipe** at server startup that will
    remain valid as long as the process is alive.
    We chose this solution due to its minimal memory usage, and since
    `write()` and `read()` are atomic operations. It ensures that stack
    traces from different threads won't mix.
    
    **The stacktraces collection process is now as  follows:**
    * Cleaning the pipe to eliminate writes of late threads from previous
    runs.
    * Each thread writes to the pipe its stacktrace
    * Waiting for all the threads to mark completion or until a timeout (2
    sec) is reached
    * Reading from the pipe to print the stacktraces.
    
    #### **7. Changes that were considered and eventually were dropped**
    * replace watchdog timer with a POSIX timer: 
    according to [settimer man](https://linux.die.net/man/2/setitimer)
    
    > POSIX.1-2008 marks getitimer() and setitimer() obsolete, recommending
    the use of the POSIX timers API
    ([timer_gettime](https://linux.die.net/man/2/timer_gettime)(2),
    [timer_settime](https://linux.die.net/man/2/timer_settime)(2), etc.)
    instead.
    
    However, although it is supposed to conform to POSIX std, POSIX timers
    API is not supported on Mac.
    You can take a look here at the Linux implementation:
    
    [here](https://github.com/redis/redis/commit/c7562ee13546e504977372fdf40d33c3f86775a5
    
    )
    To avoid messing up the code, and uncertainty regarding compatibility,
    it was decided to drop it for now.
    
    * avoid using sds (uses malloc) in logConfigDebugInfo
    It was considered to print config info instead of using sds, however
    apparently, `logConfigDebugInfo` does more than just print the sds, so
    it was decided this fix is out of this issue scope.
    
    #### **8. fix Signal mask check**
    The check `signum & sig_mask` intended to indicate whether the signal is
    blocked by the thread was incorrect. Actually, the bit position in the
    signal mask corresponds to the signal number. We fixed this by changing
    the condition to: `sig_mask & (1L << (sig_num - 1))`
    
    #### **9. Unrelated changes**
    both `fork.tcl `and `util.tcl` implemented a function called
    `count_log_message` expecting different parameters. This caused
    confusion when trying to run daily tests with additional test parameters
    to run a specific test.
    The `count_log_message` in `fork.tcl` was removed and the calls were
    replaced with calls to `count_log_message` located in `util.tcl`
    
    ---------
    Co-authored-by: default avatarOzan Tezcan <ozantezcan@gmail.com>
    Co-authored-by: default avatarOran Agra <oran@redislabs.com>
    2e854bcc
server.c 289 KB