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