LCOV - code coverage report
Current view: top level - src - oomfill.c (source / functions) Hit Total Coverage
Test: liboom.info Lines: 113 120 94.2 %
Date: 2024-12-21 15:44:04 Functions: 12 12 100.0 %

          Line data    Source code
       1             : /**   @file  oomfill.c
       2             :  *   @brief  Library safely (rlimited) consumming RAM to trigger OOMs (only on POSIX systems)
       3             :  *  @author  François Cerbelle (Fanfan), francois@cerbelle.net
       4             :  *
       5             :  *  @internal
       6             :  *       Created:  21/06/2024
       7             :  *      Revision:  none
       8             :  * Last modified:  2024-12-11 12:28
       9             :  *      Compiler:  gcc
      10             :  *  Organization:  Cerbelle.net
      11             :  *     Copyright:  Copyright (c) 2024, François Cerbelle
      12             :  *
      13             :  *  This source code is released for free distribution under the terms of the
      14             :  *  GNU General Public License as published by the Free Software Foundation.
      15             :  */
      16             : 
      17             : #include "liboom/oomfill.h"                         /* OOM simulation */
      18             : 
      19             : #include <assert.h>
      20             : #include <stdio.h>                              /* printf */
      21             : #include <stdlib.h>                             /* abort() */
      22             : #include <errno.h>                              /* errno */
      23             : #include <sys/resource.h>                       /* setrlimit/getrlimit */
      24             : #include <string.h>                             /* strerror() */
      25             : #include <alloca.h>                             /* alloca() */
      26             : #include <unistd.h>                             /* sysconf() */
      27             : 
      28             : /** Maximum number of fragmented blocks to allocate
      29             :  *
      30             :  * I never reached more than 350 on my systems.
      31             :  * The value needs to be less than UINT_MAX
      32             :  * */
      33             : #define RAMBLOCKS_MAX 1000
      34             : 
      35             : /** RAM eating pointer
      36             :  *
      37             :  * Used to store the huge memory block used to fill the available memory */
      38             : static void* _oomblocks[RAMBLOCKS_MAX] = {0};
      39             : 
      40             : /** getrlimit helper with abort on fail */
      41        8018 : static int checked_getrlimit(int resource, struct rlimit *rlim) {
      42             :     /* Get current limit values */
      43        8018 :     if (getrlimit(resource, rlim) != 0) {
      44             :         /* Can occur, thus not ignored, but impossible to trigger for gcov/lcov */
      45           0 :         fprintf (stderr,"%s:%d getrlimit() failed with errno=%d %s\n",
      46           0 :                  __FILE__,__LINE__, errno,strerror(errno));
      47           0 :         abort();
      48             :     }
      49        8018 :     return 0;
      50             : }
      51             : 
      52             : /** Disabled fill function
      53             :  *
      54             :  * Used as oomfill_fill when disabled. Thus, the same test can be surrounded by
      55             :  * fill/free, but the actual RAM pressure will or will not be activated.
      56             :  *
      57             :  * \sa oomfill_fill
      58             :  * \sa oomfill_free
      59             :  * */
      60           3 : static size_t oomfill_fill_preinit(const size_t minHeap, const size_t minStack) {
      61             :     (void)minHeap;
      62             :     (void)minStack;
      63           3 :     assert(NULL==_oomblocks[0]);
      64           3 :     return 0;
      65             : }
      66             : 
      67             : /** Starts an almost OOM single and simple test
      68             :  *
      69             :  * This function is only enabled after a call to oomfill_enable and will have no
      70             :  * effect otherwise. Its goal is to completely fill the RAM until the very last
      71             :  * bytes, to keep only between minHeap and maxHeap bytes available in the heap
      72             :  * and minStack bytes in the stack.
      73             :  *
      74             :  * If oomfill_enable was not invoked beforehand, this function will simply
      75             :  * return without any RAM consumption.
      76             :  *
      77             :  * It should be called immediately before the test and should be reverted with
      78             :  * oomfill_free immediately after. It can create so much pressure on the
      79             :  * available memory that a simple printf could fail.
      80             :  *
      81             :  * It is designed to fail if invoked twice as this is very probably a mistake in
      82             :  * the test code. Should you need to change the RAM filling values, first call
      83             :  * oomfill_free and reapply oomfill_fill. This ensure a really wanted behavior
      84             :  * and not a mistake in your test code.
      85             :  *
      86             :  * \param [in] minHeap
      87             :  * \param [in] minStack
      88             :  *
      89             :  * \return Returns the allocated size to fill the memory
      90             :  *
      91             :  * \sa oomfill_enable
      92             :  * \sa oomfill_disable
      93             :  * \sa oomfill_fill
      94             :  * \sa oomfill_free
      95             :  * */
      96             : size_t (*oomfill_fill)(const size_t minHeap, const size_t minStack)=oomfill_fill_preinit;
      97             : 
      98             : /** Disabled free function
      99             :  *
     100             :  * Used as oomfill_free when disabled. Thus, the same test can be surrounded by
     101             :  * fill/free, but the actual RAM pressure will or will not be activated.
     102             :  *
     103             :  * \sa oomfill_free
     104             :  * \sa oomfill_fill
     105             :  * */
     106           6 : static void oomfill_free_preinit() {
     107           6 :     assert(NULL==_oomblocks[0]);
     108           6 : }
     109             : 
     110             : /** Ends a single simple OOM test
     111             :  *
     112             :  * If this function is invoked between an oomfill_enable and an oomfill_disable
     113             :  * invocations, it deallocates the RAM allocated by the oomfill_fill function. It
     114             :  * should be called immediately after the single and simple test because the RAM
     115             :  * pressure can even make a printf to fail.
     116             :  *
     117             :  * If called without a prior oomfill_enable invocation, this function simply
     118             :  * returns without any action.
     119             :  *
     120             :  * This function is designed to fail and abort the process if invocated whereas
     121             :  * there is no current RAM allocated, when called twice, for example. This helps
     122             :  * to avoid mistakes in the test scenario.
     123             :  *
     124             :  * */
     125             : void (*oomfill_free)()=oomfill_free_preinit;
     126             : 
     127             : /** Disabled oomfill_enable function
     128             :  *
     129             :  * Used as oomfill_enable before oomfill_config was invoked, which should never
     130             :  * happen and will fail to detect mistakes in the test code.
     131             :  *
     132             :  * \sa oomfill_enable
     133             :  * \sa oomfill_disable
     134             :  * */
     135           8 : static size_t oomfill_enable_preinit(const size_t softlimit) {
     136             :     (void)softlimit;
     137             : 
     138             :     /* Should not be called without configuration */
     139           8 :     fprintf(stderr,"%s:%d oomfill_enable called without oomfill_config before\n",__FILE__,__LINE__);
     140           8 :     abort();
     141             : }
     142             : 
     143             : /** Starts a new oomfill environment or reconfigure the soft limit
     144             :  *
     145             :  * This function can only be invoked after at least a first call to
     146             :  * oomfill_config to initialize the oomfill helpers environment.  If invocated
     147             :  * before, it will abort the current process to help detecting mistakes in the
     148             :  * test code.
     149             :  *
     150             :  * It will (soft)limit the current process and his children to the provided
     151             :  * value in bytes. Then, it will enable the fill/free oomfill helper functions
     152             :  * which are disabled otherwise.
     153             :  *
     154             :  * It is designed to be invoked at the begining of a test case to apply for all
     155             :  * tests in this testcase.
     156             :  *
     157             :  * In case of any unexpected behavior, it should abort the current process.
     158             :  *
     159             :  * \param [in] softlimit Soft limit to set in bytes. It has to be less than the
     160             :  * hardlimit otherwise it will fail and abort. If set to 0, it will apply the
     161             :  * same value as the hardlimit.
     162             :  *
     163             :  * \return Returns the actually set value as a softlimit, either the requested
     164             :  * value or the hardlimit in case of 0 requested.
     165             :  *
     166             :  * \sa oomfill_enable
     167             :  * \sa oomfill_disable
     168             :  * \sa oomfill_fill
     169             :  * \sa oomfill_free
     170             :  * */
     171             : size_t (*oomfill_enable)(const size_t softlimit)=oomfill_enable_preinit;
     172             : 
     173             : /** Disabled oomfill_disable function
     174             :  *
     175             :  * Used as oomfill_disable before oomfill_config was invoked, which should never
     176             :  * happen and will fail to detect mistakes in the test code.
     177             :  *
     178             :  * \sa oomfill_enable
     179             :  * \sa oomfill_disable
     180             :  * */
     181           2 : static size_t oomfill_disable_preinit() {
     182             :     /* Should not be called before configuration */
     183           2 :     fprintf(stderr,"%s:%d oomfill_disable called without oomfill_config before\n",__FILE__,__LINE__);
     184           2 :     abort();
     185             : }
     186             : 
     187             : /** Stops the current oomfill
     188             :  *
     189             :  * This function can be invoked any time after the oomfill_config initialized
     190             :  * the environment. If invocated before, it will abort the current process to
     191             :  * help detecting mistakes in the test code.
     192             :  *
     193             :  * This function disables the fill/free oomfill functions and free the allocated
     194             :  * RAM before reverting the soft limit to the hard limit value.
     195             :  *
     196             :  * It is designed to be invoked t the end of a test case.
     197             :  *
     198             :  * Despite it could be invoked twice or without a prior call to
     199             :  * oomfill_enable, it is illegal and not allowed to help detecting
     200             :  * mistakes in the test code.
     201             :  *
     202             :  * \return Returns the applied soft limit, which should be the same as the hard
     203             :  * limit.
     204             :  *
     205             :  * \sa oomfill_enable
     206             :  * \sa oomfill_disable
     207             :  * \sa oomfill_fill
     208             :  * \sa oomfill_free
     209             :  * */
     210             : size_t (*oomfill_disable)()=oomfill_disable_preinit;
     211             : 
     212             : /** Finds biggest available RAM block and allocate it
     213             :  *
     214             :  * This functions tries to find and allocate the biggest available memory block
     215             :  * by dichotomy (Olog2 complexity) between 0 and rlim_cur soft limit. It returns
     216             :  * the pointer.
     217             :  *
     218             :  * \param [in,out] The allocated pointer if successfull, NULL otherwise
     219             :  *
     220             :  * \return The size of the allocated block, 0 if not block was allocated.
     221             :  *
     222             :  * */
     223        6486 : static size_t oomfill_getbiggestblock(void** p_ramblock) {
     224             :     /* This function could receive an optimized "max" value and return the final
     225             :      * "max" value to the caller. So, it could be used as the next invocation
     226             :      * starting "max" value. This would save few loop iterations, at the cost of
     227             :      * extra stack usage, I chose to not pass this value as a parameter to avoid
     228             :      * consuming stack.
     229             :      * The function starts to search between 0..rlim_cur which has very little
     230             :      * impact given the Olog2 complexity. It could be optimized and start to
     231             :      * search between 0..sysconf(AVPHYSPAGE*PAGESIZE), assuming that sysconf is
     232             :      * faster than few loop iterations. */
     233             : 
     234             :     /* Use static to avoid stack allocation/free */
     235             :     static size_t max,cur;
     236             :     static struct rlimit limit;
     237             : 
     238        6486 :     assert(NULL!=p_ramblock);
     239        6486 :     assert(NULL==*p_ramblock);
     240             : 
     241             :     /* Get the current limits */
     242        6486 :     checked_getrlimit(RLIMIT_AS, &limit);
     243        6486 :     max = limit.rlim_cur;
     244             : 
     245             :     /* Restart the whole process if cur can not be allocated at the end */
     246       12972 :     while ((max>0)&&(NULL==*p_ramblock)) {
     247             :         static size_t min;
     248             :         /* Iterate quickly (Olog2) to converge to the biggest available RAM block */
     249        6486 :         min = 0;
     250      188317 :         while (max>min) {
     251      181831 :             cur = min+(max-min)/2; /* To avoid overflow */
     252      181831 :             if (NULL==(*p_ramblock = malloc(cur))) {
     253      149984 :                 max = cur;
     254             :             } else {
     255       31847 :                 min = cur+1;
     256       31847 :                 free(*p_ramblock);
     257             :             }
     258             :         }
     259        6486 :         cur -= 1;
     260        6486 :         *p_ramblock=malloc(cur);
     261             :     }
     262             : 
     263        6486 :     return cur;
     264             : }
     265             : 
     266             : /** Enabled fill function
     267             :  *
     268             :  * Used as oomfill_fill when enabled.
     269             :  *
     270             :  * \sa oomfill_fill
     271             :  * \sa oomfill_free
     272             :  * */
     273         364 : static size_t oomfill_fill_postinit(const size_t minHeap, const size_t minStack) {
     274             :     unsigned int l_numblock;
     275             :     size_t l_sum;
     276             :     void* volatile l_reservedheap;
     277             :     void* l_reservedstack;
     278             : 
     279             :     /* Probably a mistake in the test code, do not accept despite we could */
     280         364 :     if (NULL!=_oomblocks[0]) {
     281             :         /* Dirty hack to free some RAM and allow abort to SIGABRT */
     282          30 :         free(_oomblocks[0]);
     283          30 :         fprintf (stderr,"%s:%d oomblocks are already allocated\n", __FILE__,__LINE__);
     284          30 :         abort();
     285             :     }
     286             : 
     287             :     /* Reserve/Protect stack bytes which will be auto freed at return */
     288             :     /* A failure means a stackoverflow, which is not recoverable and will abort
     289             :      * the process anyway. */
     290         334 :     if (0<minStack)
     291         334 :         l_reservedstack=alloca(minStack);
     292             :     (void)l_reservedstack;
     293             : 
     294             :     /* Reserve/Protect heap bytes which will be released before return */
     295         334 :     l_reservedheap=NULL;
     296         334 :     if (0<minHeap)
     297         327 :         if (NULL==(l_reservedheap=malloc(minHeap))) {
     298             :             /* This will fail if minHeap is higher than rlimit */
     299           2 :             fprintf(stderr,"%s:%d Failed to reserve minheap bytes\n",__FILE__,__LINE__);
     300           2 :             abort();
     301             :         }
     302             : 
     303             :     /* Find and allocate the biggest available RAM blocks until no mre RAM
     304             :      * available or all _oomblocks are allocated */
     305         332 :     l_numblock=0;
     306         332 :     l_sum = 0;
     307         332 :     l_sum += oomfill_getbiggestblock(&(_oomblocks[l_numblock++]));
     308        6486 :     while ((NULL!=_oomblocks[l_numblock-1])&&((RAMBLOCKS_MAX-1)>l_numblock))
     309        6154 :         l_sum += oomfill_getbiggestblock(&(_oomblocks[l_numblock++]));
     310             :     /* Either already NULL, which stopped the while loop or reached the last
     311             :      * slot in the table, which stopped the while loop and the slot has to be
     312             :      * set to NULL */
     313         332 :     _oomblocks[l_numblock]=NULL;
     314             : 
     315             :     /* There can be less than 4 bytes available at this point !!! */
     316             : 
     317             :     /* Make the protected heap bytes available again */
     318         332 :     if (NULL!=l_reservedheap)
     319         325 :         free(l_reservedheap);
     320             : 
     321         332 :     return l_sum;
     322             : }
     323             : 
     324             : /** Enabled free function
     325             :  *
     326             :  * Used as oomfill_free when enabled
     327             :  *
     328             :  * \sa oomfill_free
     329             :  * \sa oomfill_fill
     330             :  * */
     331         306 : static void oomfill_free_postinit() {
     332             :     unsigned int l_i;
     333             : 
     334             :     /* Despite we could manage, abort to help detecting mistakes in test code */
     335         306 :     if (NULL==_oomblocks[0]) {
     336           4 :         fprintf(stderr,"%s:%d no blocks to free in oomfill_free.\n",__FILE__,__LINE__);
     337           4 :         abort();
     338             :     }
     339             : 
     340             :     /* Actually free allocated blocks and set their pointer to NULL */
     341         302 :     l_i = 0;
     342        6218 :     while ((l_i<(RAMBLOCKS_MAX-1))&&(_oomblocks[l_i])) {
     343        5916 :         free(_oomblocks[l_i]);
     344        5916 :         _oomblocks[l_i] = NULL;
     345        5916 :         l_i+=1;
     346             :     }
     347         302 : }
     348             : 
     349             : /** Enabled oomfill_enable function
     350             :  *
     351             :  * Used as oomfill_enable after oomfill_config was invoked
     352             :  *
     353             :  * \sa oomfill_enable
     354             :  * \sa oomfill_disable
     355             :  * */
     356         753 : static size_t oomfill_enable_postinit(const size_t softlimit) {
     357             :     struct rlimit limit;
     358         753 :     size_t l_softlimit = softlimit;
     359             : 
     360             :     /* Probably a bug in oomfill */
     361         753 :     assert(NULL==_oomblocks[0]);
     362             : 
     363             :     /* Get current limit values */
     364         753 :     checked_getrlimit(RLIMIT_AS, &limit);
     365             : 
     366             :     /* Check the requested value */
     367         753 :     if (0==l_softlimit) {
     368             :         /* Defaults to available physical RAM if requested 0 */
     369         326 :         l_softlimit = limit.rlim_max;
     370             :     };
     371             : 
     372             :     /* Soft limit available RAM */
     373         753 :     limit.rlim_cur = l_softlimit;
     374             : 
     375             :     /* Abort if setrlimit fails to avoid RAM bombing */
     376         753 :     if (setrlimit(RLIMIT_AS, &limit) != 0) {
     377           4 :         fprintf (stderr,"%s:%d setrlimit(cur=%lu, max=%lu) with errno=%d %s\n",
     378             :                  __FILE__,__LINE__,
     379           2 :                  (unsigned long)limit.rlim_cur, (unsigned long)limit.rlim_max,
     380           2 :                  errno,strerror(errno));
     381           2 :         abort();
     382             :     }
     383             : 
     384             :     /* Activate fill/free functions */
     385         751 :     oomfill_fill = oomfill_fill_postinit;
     386         751 :     oomfill_free = oomfill_free_postinit;
     387             : 
     388             :     /* Return the actual current soft limit */
     389         751 :     return limit.rlim_cur;
     390             : }
     391             : 
     392             : /** Enabled oomfill_disable function
     393             :  *
     394             :  * Used as oomfill_disable after oomfill_config was invoked
     395             :  *
     396             :  * \sa oomfill_enable
     397             :  * \sa oomfill_disable
     398             :  * */
     399         329 : static size_t oomfill_disable_postinit() {
     400             :     struct rlimit limit;
     401             : 
     402         329 :     if (NULL!=_oomblocks[0]) {
     403           0 :         fprintf(stderr,"%s:%d RAM still allocated while calling oomfill_disable\n",__FILE__,__LINE__);
     404           0 :         abort();
     405             :     }
     406             : 
     407             :     /* Do not allow calling disable if not enabled to detect test mistakes */
     408         329 :     if ((oomfill_fill!=oomfill_fill_postinit)||(oomfill_free!=oomfill_free_postinit)) {
     409           6 :         fprintf(stderr,"%s:%d Impossible to disable oomfill if not previously enabled\n",__FILE__,__LINE__);
     410           6 :         abort();
     411             :     }
     412             : 
     413             :     /* Reset the soft limit to hard limit */
     414         323 :     oomfill_enable_postinit(0);
     415             : 
     416             :     /* Get current limit values */
     417         323 :     checked_getrlimit(RLIMIT_AS, &limit);
     418             : 
     419             :     /* Restore disabled functors */
     420         323 :     oomfill_fill = oomfill_fill_preinit;
     421         323 :     oomfill_free = oomfill_free_preinit;
     422             : 
     423             :     /* Return the actual current soft limit, which is equal to hard limit */
     424         323 :     return limit.rlim_cur;
     425             : }
     426             : 
     427             : /** Sets the oomfill helpers hard rlimit and enables the oomfill helper features
     428             :  *
     429             :  * This function needs to be invoqued BEFORE any other helper from the
     430             :  * framework. It will configure an hard RAM limit for the current process and
     431             :  * each of his children. Then, it will enable the other oomfill helper
     432             :  * functions.
     433             :  *
     434             :  * It refuses to set a limit over the actually installed physical RAM to limit
     435             :  * pushing RAM pages to the swap. If the requested value is zero, it will
     436             :  * default to the actually installed physical RAM.
     437             :  *
     438             :  * If anything goes wrong or did not behaves as expected, it abort the process
     439             :  * to avoid RAM bombing and swapping.
     440             :  *
     441             :  * Despite it was designed to be invoked only once, from the main parent
     442             :  * process, it can be invoked several times as long as the hardlimit parameter
     443             :  * is always less than the previous call, otherwise it will fail and abort the
     444             :  * process.
     445             :  *
     446             :  * \param [in] hardlimit the size in bytes to limit the process to.
     447             :  *
     448             :  * \return Returns the actual configured size. It should be the same
     449             :  * value as the hardlimit parameter, otherwise something went wrong, it was not
     450             :  * detected and the process was not aborted (which is a bug to report)
     451             :  *
     452             :  * \sa oomfill_enable
     453             :  * \sa oomfill_disable
     454             :  * \sa oomfill_fill
     455             :  * \sa oomfill_free
     456             :  * */
     457         454 : size_t oomfill_config(const size_t hardlimit) {
     458             :     struct rlimit limit;
     459             :     size_t l_avail;
     460         454 :     size_t l_hardlimit = hardlimit;
     461             : 
     462             :     /* Probably a test implementation mistake */
     463         454 :     if (NULL!=_oomblocks[0]) {
     464           0 :         fprintf(stderr,"%s:%d Calling oomfill_config with allocated RAM blocks is not allowed.\n",
     465             :                 __FILE__,__LINE__);
     466           0 :         abort();
     467             :     }
     468             : 
     469             :     /* Find *installed* physical RAM, 0 in case of failure */
     470         454 :     l_avail = (sysconf(_SC_PHYS_PAGES) * sysconf(_SC_PAGESIZE));
     471             : 
     472             :     /* Get current limit values */
     473         454 :     checked_getrlimit(RLIMIT_AS, &limit);
     474         454 :     if (0==l_hardlimit) {
     475             :         /* Defaults to available physical RAM or already set rlimit
     476             :          * if 0 requested */
     477           3 :         l_hardlimit=(limit.rlim_max<l_avail?limit.rlim_max:l_avail);
     478         451 :     } else if (l_hardlimit>l_avail) {
     479             :         /* Fails if request is over available physical RAM to avoir swapping */
     480           2 :         fprintf(stderr,"%s:%d Requesing a limit %lu bigger than *installed* RAM %lu is not allowed.\n",
     481             :                 __FILE__,__LINE__,
     482             :                 (unsigned long)l_hardlimit,(unsigned long)l_avail);
     483           2 :         abort();
     484             :     }
     485             : 
     486             :     /* Hard limit available RAM to hardlimit globally with no way back */
     487         452 :     limit.rlim_cur = l_hardlimit;
     488         452 :     limit.rlim_max = l_hardlimit;
     489             : 
     490             :     /* Abort if setrlimit fails to avoid RAM bombing */
     491         452 :     if (setrlimit(RLIMIT_AS, &limit) != 0) {
     492           4 :         fprintf (stderr,"%s:%d setrlimit(cur=%lu, max=%lu) with errno=%d %s\n",
     493           2 :                  __FILE__,__LINE__, (unsigned long)limit.rlim_cur,
     494           2 :                  (unsigned long)limit.rlim_max, errno,strerror(errno));
     495             :         /* Get current limit values */
     496           2 :         checked_getrlimit(RLIMIT_AS, &limit);
     497           2 :         fprintf (stderr,"%s:%d getrlimit() is cur=%lu, max=%lu\n",
     498           2 :                  __FILE__,__LINE__, (unsigned long)limit.rlim_cur,
     499           2 :                  (unsigned long)limit.rlim_max);
     500           2 :         abort();
     501             :     }
     502             : 
     503             :     /* Activate enable/disable functions */
     504         450 :     oomfill_enable = oomfill_enable_postinit;
     505         450 :     oomfill_disable = oomfill_disable_postinit;
     506             : 
     507             :     /* Return the configured limit hard=soft */
     508         450 :     return limit.rlim_max;
     509             : }
     510             : 
     511          71 : bool oomfill_enabled() {
     512          71 :     return (oomfill_fill==oomfill_fill_postinit?true:false);
     513             : }
     514             : /* vim: set tw=80: */

Generated by: LCOV version 1.16