LCOV - code coverage report
Current view: top level - utils - s2n_fork_detection.c (source / functions) Hit Total Coverage
Test: unit_test_coverage.info Lines: 112 134 83.6 %
Date: 2025-08-15 07:28:39 Functions: 15 15 100.0 %
Branches: 40 102 39.2 %

           Branch data     Line data    Source code
       1                 :            : /*
       2                 :            :  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
       3                 :            :  *
       4                 :            :  * Licensed under the Apache License, Version 2.0 (the "License").
       5                 :            :  * You may not use this file except in compliance with the License.
       6                 :            :  * A copy of the License is located at
       7                 :            :  *
       8                 :            :  *  http://aws.amazon.com/apache2.0
       9                 :            :  *
      10                 :            :  * or in the "license" file accompanying this file. This file is distributed
      11                 :            :  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
      12                 :            :  * express or implied. See the License for the specific language governing
      13                 :            :  * permissions and limitations under the License.
      14                 :            :  */
      15                 :            : 
      16                 :            : /* force the internal header to be included first, since it modifies _GNU_SOURCE/_POSIX_C_SOURCE */
      17                 :            : /* clang-format off */
      18                 :            : #include "utils/s2n_fork_detection_features.h"
      19                 :            : /* clang-format on */
      20                 :            : 
      21                 :            : #include "utils/s2n_fork_detection.h"
      22                 :            : 
      23                 :            : #include "error/s2n_errno.h"
      24                 :            : #include "utils/s2n_safety.h"
      25                 :            : 
      26                 :            : #if defined(S2N_MADVISE_SUPPORTED) && defined(MADV_WIPEONFORK)
      27                 :            :     #if (MADV_WIPEONFORK != 18)
      28                 :            :         #error "MADV_WIPEONFORK is not 18"
      29                 :            :     #endif
      30                 :            : #else /* defined(S2N_MADVISE_SUPPORTED) && defined(MADV_WIPEONFORK) */
      31                 :            :     #define MADV_WIPEONFORK 18
      32                 :            : #endif
      33                 :            : 
      34                 :            : /* Sometimes (for example, on FreeBSD) MAP_INHERIT_ZERO is called INHERIT_ZERO */
      35                 :            : #if !defined(MAP_INHERIT_ZERO) && defined(INHERIT_ZERO)
      36                 :            :     #define MAP_INHERIT_ZERO INHERIT_ZERO
      37                 :            : #endif
      38                 :            : 
      39                 :            : /* These variables are used to disable all fork detection mechanisms or at the
      40                 :            :  * individual level during testing.
      41                 :            :  */
      42                 :            : static bool ignore_wipeonfork_or_inherit_zero_method_for_testing = false;
      43                 :            : static bool ignore_pthread_atfork_method_for_testing = false;
      44                 :            : static bool ignore_fork_detection_for_testing = false;
      45                 :            : 
      46                 :    1225693 : #define S2N_FORK_EVENT    0
      47                 :        741 : #define S2N_NO_FORK_EVENT 1
      48                 :            : 
      49                 :            : struct FGN_STATE {
      50                 :            :     /* The current cached fork generation number for this process */
      51                 :            :     uint64_t current_fork_generation_number;
      52                 :            : 
      53                 :            :     /* Semaphore controlling access to the shared sentinel and signaling whether
      54                 :            :      * fork detection is enabled or not. We could use zero_on_fork_addr, but
      55                 :            :      * avoid overloading by using an explicit variable.
      56                 :            :      */
      57                 :            :     bool is_fork_detection_enabled;
      58                 :            : 
      59                 :            :     /* Sentinel that signals a fork event has occurred */
      60                 :            :     volatile char *zero_on_fork_addr;
      61                 :            : 
      62                 :            :     pthread_once_t fork_detection_once;
      63                 :            :     pthread_rwlock_t fork_detection_rw_lock;
      64                 :            : };
      65                 :            : 
      66                 :            : /* We only need a single statically initialised state. Note, the state is
      67                 :            :  * inherited by child processes.
      68                 :            :  */
      69                 :            : static struct FGN_STATE fgn_state = {
      70                 :            :     .current_fork_generation_number = 0,
      71                 :            :     .is_fork_detection_enabled = false,
      72                 :            :     .zero_on_fork_addr = NULL,
      73                 :            :     .fork_detection_once = PTHREAD_ONCE_INIT,
      74                 :            :     .fork_detection_rw_lock = PTHREAD_RWLOCK_INITIALIZER,
      75                 :            : };
      76                 :            : 
      77                 :            : /* Can currently never fail. See initialise_fork_detection_methods() for
      78                 :            :  * motivation.
      79                 :            :  */
      80                 :            : static inline S2N_RESULT s2n_initialise_wipeonfork_best_effort(void *addr, long page_size)
      81                 :        481 : {
      82                 :        481 : #if defined(S2N_MADVISE_SUPPORTED)
      83                 :            :     /* Return value ignored on purpose */
      84                 :        481 :     madvise(addr, (size_t) page_size, MADV_WIPEONFORK);
      85                 :        481 : #endif
      86                 :            : 
      87                 :        481 :     return S2N_RESULT_OK;
      88                 :        481 : }
      89                 :            : 
      90                 :            : static inline S2N_RESULT s2n_initialise_inherit_zero(void *addr, long page_size)
      91                 :        481 : {
      92                 :            : #if defined(S2N_MINHERIT_SUPPORTED) && defined(MAP_INHERIT_ZERO)
      93                 :            :     RESULT_ENSURE(minherit(addr, page_size, MAP_INHERIT_ZERO) == 0, S2N_ERR_FORK_DETECTION_INIT);
      94                 :            : #endif
      95                 :            : 
      96                 :        481 :     return S2N_RESULT_OK;
      97                 :        481 : }
      98                 :            : 
      99                 :            : static void s2n_pthread_atfork_on_fork(void)
     100                 :        186 : {
     101                 :            :     /* This zeroises the first byte of the memory page pointed to by
     102                 :            :      * *zero_on_fork_addr. This is the same byte used as fork event detection
     103                 :            :      * sentinel in s2n_get_fork_generation_number(). The same memory page, and in
     104                 :            :      * turn, the byte, is also the memory zeroised by the MADV_WIPEONFORK fork
     105                 :            :      * detection mechanism.
     106                 :            :      *
     107                 :            :      * Aquire locks to be on the safe side. We want to avoid the checks in
     108                 :            :      * s2n_get_fork_generation_number() getting executed before setting the sentinel
     109                 :            :      * flag. The write lock prevents any other thread from owning any other type
     110                 :            :      * of lock.
     111                 :            :      *
     112                 :            :      * pthread_atfork_on_fork() cannot return errors. Hence, there is no way to
     113                 :            :      * gracefully recover if [un]locking fails.
     114                 :            :      */
     115         [ -  + ]:        186 :     if (pthread_rwlock_wrlock(&fgn_state.fork_detection_rw_lock) != 0) {
     116                 :          0 :         printf("pthread_rwlock_wrlock() failed. Aborting.\n");
     117                 :          0 :         abort();
     118                 :          0 :     }
     119                 :            : 
     120         [ -  + ]:        186 :     if (fgn_state.zero_on_fork_addr == NULL) {
     121                 :          0 :         printf("fgn_state.zero_on_fork_addr is NULL. Aborting.\n");
     122                 :          0 :         abort();
     123                 :          0 :     }
     124                 :        186 :     *fgn_state.zero_on_fork_addr = 0;
     125                 :            : 
     126         [ -  + ]:        186 :     if (pthread_rwlock_unlock(&fgn_state.fork_detection_rw_lock) != 0) {
     127                 :          0 :         printf("pthread_rwlock_unlock() failed. Aborting.\n");
     128                 :          0 :         abort();
     129                 :          0 :     }
     130                 :        186 : }
     131                 :            : 
     132                 :            : static S2N_RESULT s2n_inititalise_pthread_atfork(void)
     133                 :        476 : {
     134                 :            :     /* Register the fork handler pthread_atfork_on_fork that is executed in the
     135                 :            :      * child process after a fork.
     136                 :            :      */
     137         [ +  - ]:        476 :     if (s2n_is_pthread_atfork_supported() == true) {
     138 [ -  + ][ #  # ]:        476 :         RESULT_ENSURE(pthread_atfork(NULL, NULL, s2n_pthread_atfork_on_fork) == 0, S2N_ERR_FORK_DETECTION_INIT);
     139                 :        476 :     }
     140                 :            : 
     141                 :        476 :     return S2N_RESULT_OK;
     142                 :        476 : }
     143                 :            : 
     144                 :            : static S2N_RESULT s2n_initialise_fork_detection_methods_try(void *addr, long page_size)
     145                 :        516 : {
     146         [ -  + ]:        516 :     RESULT_GUARD_PTR(addr);
     147                 :            : 
     148                 :            :     /* Some systems don't define MADV_WIPEONFORK in sys/mman.h but the kernel
     149                 :            :      * still supports the mechanism (AL2 being a prime example). Likely because
     150                 :            :      * glibc on the system is old. We might be able to include kernel header
     151                 :            :      * files directly, that define MADV_WIPEONFORK, conditioning on specific
     152                 :            :      * OS's. But it is a mess. A more reliable method is to probe the system, at
     153                 :            :      * run-time, whether madvise supports the MADV_WIPEONFORK advice. However,
     154                 :            :      * the method to probe for this feature is equivalent to actually attempting
     155                 :            :      * to initialise the MADV_WIPEONFORK fork detection. Compare with
     156                 :            :      * probe_madv_wipeonfork_support() (used for testing).
     157                 :            :      *
     158                 :            :      * Instead, we apply best-effort to initialise the MADV_WIPEONFORK fork
     159                 :            :      * detection and otherwise always require pthread_atfork to be initialised.
     160                 :            :      * We also currently always apply prediction resistance. So, this should be
     161                 :            :      * a safe default.
     162                 :            :      */
     163         [ +  + ]:        516 :     if (ignore_wipeonfork_or_inherit_zero_method_for_testing == false) {
     164         [ -  + ]:        481 :         RESULT_GUARD(s2n_initialise_wipeonfork_best_effort(addr, page_size));
     165                 :        481 :     }
     166                 :            : 
     167         [ +  + ]:        516 :     if (ignore_wipeonfork_or_inherit_zero_method_for_testing == false) {
     168         [ -  + ]:        481 :         RESULT_GUARD(s2n_initialise_inherit_zero(addr, page_size));
     169                 :        481 :     }
     170                 :            : 
     171         [ +  + ]:        516 :     if (ignore_pthread_atfork_method_for_testing == false) {
     172         [ -  + ]:        476 :         RESULT_GUARD(s2n_inititalise_pthread_atfork());
     173                 :        476 :     }
     174                 :            : 
     175                 :        516 :     fgn_state.zero_on_fork_addr = addr;
     176                 :        516 :     *fgn_state.zero_on_fork_addr = S2N_NO_FORK_EVENT;
     177                 :        516 :     fgn_state.is_fork_detection_enabled = true;
     178                 :            : 
     179                 :        516 :     return S2N_RESULT_OK;
     180                 :        516 : }
     181                 :            : 
     182                 :            : static S2N_RESULT s2n_setup_mapping(void **addr, long *page_size)
     183                 :        620 : {
     184                 :        620 :     *page_size = sysconf(_SC_PAGESIZE);
     185 [ -  + ][ #  # ]:        620 :     RESULT_ENSURE_GT(*page_size, 0);
     186                 :            : 
     187                 :        620 :     *addr = mmap(NULL, (size_t) *page_size, PROT_READ | PROT_WRITE,
     188                 :        620 :             MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
     189 [ -  + ][ #  # ]:        620 :     RESULT_ENSURE_NE(*addr, MAP_FAILED);
     190                 :            : 
     191                 :        620 :     return S2N_RESULT_OK;
     192                 :        620 : }
     193                 :            : 
     194                 :            : static void s2n_initialise_fork_detection_methods(void)
     195                 :        516 : {
     196                 :        516 :     void *addr = MAP_FAILED;
     197                 :        516 :     long page_size = 0;
     198                 :            : 
     199                 :            :     /* Only used to disable fork detection mechanisms during testing. */
     200 [ +  + ][ -  + ]:        516 :     if (ignore_wipeonfork_or_inherit_zero_method_for_testing == true && ignore_pthread_atfork_method_for_testing == true) {
     201                 :          0 :         ignore_fork_detection_for_testing = true;
     202                 :          0 :         return;
     203                 :          0 :     }
     204                 :            : 
     205         [ -  + ]:        516 :     if (s2n_result_is_error(s2n_setup_mapping(&addr, &page_size)) == true) {
     206                 :          0 :         return;
     207                 :          0 :     }
     208                 :            : 
     209                 :            :     /* Now we know that we have some memory mapped. Try to initialise fork
     210                 :            :      * detection methods. Unmap the memory if we fail for some reason.
     211                 :            :      */
     212         [ -  + ]:        516 :     if (s2n_result_is_error(s2n_initialise_fork_detection_methods_try(addr, page_size)) == true) {
     213                 :            :         /* No reason to verify return value of munmap() since we can't use that
     214                 :            :          * information for anything anyway. */
     215                 :          0 :         munmap(addr, (size_t) page_size);
     216                 :          0 :         addr = NULL;
     217                 :          0 :         fgn_state.zero_on_fork_addr = NULL;
     218                 :          0 :         fgn_state.is_fork_detection_enabled = false;
     219                 :          0 :     }
     220                 :        516 : }
     221                 :            : 
     222                 :            : /* s2n_get_fork_generation_number returns S2N_RESULT_OK on success and
     223                 :            :  * S2N_RESULT_ERROR otherwise.
     224                 :            :  *
     225                 :            :  * On success, returns the current fork generation number in
     226                 :            :  * return_fork_generation_number. Caller must synchronise access to
     227                 :            :  * return_fork_generation_number.
     228                 :            :  */
     229                 :            : S2N_RESULT s2n_get_fork_generation_number(uint64_t *return_fork_generation_number)
     230                 :    1225516 : {
     231 [ #  # ][ -  + ]:    1225516 :     RESULT_ENSURE(pthread_once(&fgn_state.fork_detection_once, s2n_initialise_fork_detection_methods) == 0, S2N_ERR_FORK_DETECTION_INIT);
     232                 :            : 
     233         [ -  + ]:    1225516 :     if (ignore_fork_detection_for_testing == true) {
     234                 :            :         /* Fork detection is meant to be disabled. Hence, return success.
     235                 :            :          * This should only happen during testing.
     236                 :            :          */
     237 [ #  # ][ #  # ]:          0 :         RESULT_ENSURE(s2n_in_unit_test(), S2N_ERR_NOT_IN_UNIT_TEST);
     238                 :          0 :         return S2N_RESULT_OK;
     239                 :          0 :     }
     240                 :            : 
     241 [ #  # ][ -  + ]:    1225516 :     RESULT_ENSURE(fgn_state.is_fork_detection_enabled == true, S2N_ERR_FORK_DETECTION_INIT);
     242                 :            : 
     243                 :            :     /* In most cases, we would not need to increment the fork generation number.
     244                 :            :      * So, it is cheaper, in the expected case, to take an optimistic read lock
     245                 :            :      * and later aquire a write lock if needed.
     246                 :            :      * Note that we set the returned fgn before checking for a fork event. We
     247                 :            :      * need to do this because thread execution might change between releasing
     248                 :            :      * the read lock and taking the write lock. In that time span, another
     249                 :            :      * thread can reset the fork event detection sentinel and we return from
     250                 :            :      * s2n_get_fork_generation_number() without setting the returned fgn
     251                 :            :      * appropriately.
     252                 :            :      */
     253 [ #  # ][ -  + ]:    1225516 :     RESULT_ENSURE(pthread_rwlock_rdlock(&fgn_state.fork_detection_rw_lock) == 0, S2N_ERR_RETRIEVE_FORK_GENERATION_NUMBER);
     254                 :    1225516 :     *return_fork_generation_number = fgn_state.current_fork_generation_number;
     255         [ +  + ]:    1225516 :     if (*fgn_state.zero_on_fork_addr != S2N_FORK_EVENT) {
     256                 :            :         /* No fork event detected. */
     257 [ -  + ][ #  # ]:    1225339 :         RESULT_ENSURE(pthread_rwlock_unlock(&fgn_state.fork_detection_rw_lock) == 0, S2N_ERR_RETRIEVE_FORK_GENERATION_NUMBER);
     258                 :    1225339 :         return S2N_RESULT_OK;
     259                 :    1225339 :     }
     260 [ #  # ][ -  + ]:        177 :     RESULT_ENSURE(pthread_rwlock_unlock(&fgn_state.fork_detection_rw_lock) == 0, S2N_ERR_RETRIEVE_FORK_GENERATION_NUMBER);
     261                 :            : 
     262                 :            :     /* We are mutating the process-global, cached fork generation number. Need
     263                 :            :      * to acquire the write lock for that. Set returned fgn before checking the
     264                 :            :      * if condition with the same reasons as above.
     265                 :            :      */
     266 [ -  + ][ #  # ]:        177 :     RESULT_ENSURE(pthread_rwlock_wrlock(&fgn_state.fork_detection_rw_lock) == 0, S2N_ERR_RETRIEVE_FORK_GENERATION_NUMBER);
     267                 :        177 :     *return_fork_generation_number = fgn_state.current_fork_generation_number;
     268         [ +  + ]:        225 :     if (*fgn_state.zero_on_fork_addr == S2N_FORK_EVENT) {
     269                 :            :         /* Fork event has been detected; reset sentinel, increment cached fork
     270                 :            :          * generation number (which is now "current" in this child process), and
     271                 :            :          * write incremented fork generation number to the output parameter.
     272                 :            :          */
     273                 :        225 :         *fgn_state.zero_on_fork_addr = S2N_NO_FORK_EVENT;
     274                 :        225 :         fgn_state.current_fork_generation_number = fgn_state.current_fork_generation_number + 1;
     275                 :        225 :         *return_fork_generation_number = fgn_state.current_fork_generation_number;
     276                 :        225 :     }
     277 [ -  + ][ #  # ]:        177 :     RESULT_ENSURE(pthread_rwlock_unlock(&fgn_state.fork_detection_rw_lock) == 0, S2N_ERR_RETRIEVE_FORK_GENERATION_NUMBER);
     278                 :            : 
     279                 :        177 :     return S2N_RESULT_OK;
     280                 :        177 : }
     281                 :            : 
     282                 :            : static void s2n_cleanup_cb_munmap(void **probe_addr)
     283                 :        104 : {
     284                 :        104 :     munmap(*probe_addr, (size_t) sysconf(_SC_PAGESIZE));
     285                 :        104 : }
     286                 :            : 
     287                 :            : /* Run-time probe checking whether the system supports the MADV_WIPEONFORK fork
     288                 :            :  * detection mechanism.
     289                 :            :  */
     290                 :            : static S2N_RESULT s2n_probe_madv_wipeonfork_support(void)
     291                 :        104 : {
     292                 :        104 :     bool result = false;
     293                 :            : 
     294                 :            :     /* It is not an error to call munmap on a range that does not contain any
     295                 :            :      * mapped pages.
     296                 :            :      */
     297                 :        104 :     DEFER_CLEANUP(void *probe_addr = MAP_FAILED, s2n_cleanup_cb_munmap);
     298                 :        104 :     long page_size = 0;
     299                 :            : 
     300         [ -  + ]:        104 :     RESULT_GUARD(s2n_setup_mapping(&probe_addr, &page_size));
     301                 :            : 
     302                 :        104 : #if defined(S2N_MADVISE_SUPPORTED)
     303                 :            :     /* Some versions of qemu (up to at least 5.0.0-rc4, see
     304                 :            :      * linux-user/syscall.c) ignore invalid advice arguments. Hence, we first
     305                 :            :      * verify that madvise() rejects advice arguments it doesn't know about.
     306                 :            :      */
     307 [ #  # ][ -  + ]:        104 :     RESULT_ENSURE_NE(madvise(probe_addr, (size_t) page_size, -1), 0);
     308 [ #  # ][ -  + ]:        104 :     RESULT_ENSURE_EQ(madvise(probe_addr, (size_t) page_size, MADV_WIPEONFORK), 0);
     309                 :            : 
     310                 :        104 :     result = true;
     311                 :        104 : #endif
     312                 :            : 
     313 [ #  # ][ -  + ]:        104 :     RESULT_ENSURE_EQ(result, true);
     314                 :            : 
     315                 :        104 :     return S2N_RESULT_OK;
     316                 :        104 : }
     317                 :            : 
     318                 :            : bool s2n_is_madv_wipeonfork_supported(void)
     319                 :        104 : {
     320                 :        104 :     return s2n_result_is_ok(s2n_probe_madv_wipeonfork_support());
     321                 :        104 : }
     322                 :            : 
     323                 :            : bool s2n_is_map_inherit_zero_supported(void)
     324                 :          2 : {
     325                 :            : #if defined(S2N_MINHERIT_SUPPORTED) && defined(MAP_INHERIT_ZERO)
     326                 :            :     return true;
     327                 :            : #else
     328                 :          2 :     return false;
     329                 :          2 : #endif
     330                 :          2 : }
     331                 :            : 
     332                 :            : bool s2n_is_pthread_atfork_supported(void)
     333                 :        511 : {
     334                 :            :     /*
     335                 :            :      * There is a bug in OpenBSD's libc which is triggered by
     336                 :            :      * multi-generational forking of multi-threaded processes which call
     337                 :            :      * pthread_atfork(3). Under these conditions, a grandchild process will
     338                 :            :      * deadlock when trying to fork a great-grandchild.
     339                 :            :      * https://marc.info/?l=openbsd-tech&m=167047636422884&w=2
     340                 :            :      */
     341                 :            : #if defined(__OpenBSD__)
     342                 :            :     return false;
     343                 :            : #else
     344                 :        511 :     return true;
     345                 :        511 : #endif
     346                 :        511 : }
     347                 :            : 
     348                 :            : /* Use for testing only */
     349                 :            : S2N_RESULT s2n_ignore_wipeonfork_and_inherit_zero_for_testing(void)
     350                 :         35 : {
     351 [ #  # ][ -  + ]:         35 :     RESULT_ENSURE(s2n_in_unit_test(), S2N_ERR_NOT_IN_UNIT_TEST);
     352                 :            : 
     353                 :         35 :     ignore_wipeonfork_or_inherit_zero_method_for_testing = true;
     354                 :            : 
     355                 :         35 :     return S2N_RESULT_OK;
     356                 :         35 : }
     357                 :            : 
     358                 :            : S2N_RESULT s2n_ignore_pthread_atfork_for_testing(void)
     359                 :         40 : {
     360 [ #  # ][ -  + ]:         40 :     RESULT_ENSURE(s2n_in_unit_test(), S2N_ERR_NOT_IN_UNIT_TEST);
     361                 :            : 
     362                 :         40 :     ignore_pthread_atfork_method_for_testing = true;
     363                 :            : 
     364                 :         40 :     return S2N_RESULT_OK;
     365                 :         40 : }

Generated by: LCOV version 1.14