diff --git a/changelogs/unreleased/fiber-custom-stack-leak.md b/changelogs/unreleased/fiber-custom-stack-leak.md new file mode 100644 index 0000000000000000000000000000000000000000..72c48b67d6035ed94608cbd08ab5f91231e6b535 --- /dev/null +++ b/changelogs/unreleased/fiber-custom-stack-leak.md @@ -0,0 +1,5 @@ +## bugfix/core + +* Fixed a bug due to which all fibers created with `fiber_attr_setstacksize()` + leaked until the thread exit. Their stacks also leaked except when + `fiber_set_joinable(..., true)` was used. diff --git a/src/lib/core/fiber.c b/src/lib/core/fiber.c index 3e19f131b7c37d3a7d1ada639517e57411508c74..08087cfe46586cba582f66f08b5b98f40d099db7 100644 --- a/src/lib/core/fiber.c +++ b/src/lib/core/fiber.c @@ -373,7 +373,14 @@ static void fiber_stack_recycle(struct fiber *fiber); static void -fiber_destroy(struct cord *cord, struct fiber *f); +fiber_delete(struct cord *cord, struct fiber *f); + +/** + * Try to delete a fiber right now or later if can't do now. The latter happens + * for self fiber - can't delete own stack. + */ +static void +cord_add_garbage(struct cord *cord, struct fiber *f); /** * Transfer control to callee fiber. @@ -903,7 +910,7 @@ fiber_recycle(struct fiber *fiber) if (!has_custom_stack) { rlist_move_entry(&cord()->dead, fiber, link); } else { - fiber_destroy(cord(), fiber); + cord_add_garbage(cord(), fiber); } } @@ -1222,6 +1229,7 @@ fiber_new_ex(const char *name, const struct fiber_attr *fiber_attr, struct cord *cord = cord(); struct fiber *fiber = NULL; assert(fiber_attr != NULL); + cord_collect_garbage(cord); /* Now we can not reuse fiber if custom attribute was set */ if (!(fiber_attr->flags & FIBER_CUSTOM_STACK) && @@ -1297,13 +1305,9 @@ fiber_new(const char *name, fiber_func f) * cord_destroy(). */ static void -fiber_destroy(struct cord *cord, struct fiber *f) +fiber_delete(struct cord *cord, struct fiber *f) { - if (f == fiber()) { - /** End of the application. */ - assert(cord == &main_cord); - return; - } + assert(f != cord->fiber); assert(f != &cord->sched); trigger_destroy(&f->on_yield); @@ -1315,18 +1319,27 @@ fiber_destroy(struct cord *cord, struct fiber *f) diag_destroy(&f->diag); if (f->name != f->inline_name) free(f->name); - f->name = NULL; + TRASH(f); + mempool_free(&cord->fiber_mempool, f); +} + +/** Delete all fibers in the given list so it becomes empty. */ +static void +cord_delete_fibers_in_list(struct cord *cord, struct rlist *list) +{ + while (!rlist_empty(list)) { + struct fiber *f = rlist_first_entry(list, struct fiber, link); + fiber_delete(cord, f); + } } void -fiber_destroy_all(struct cord *cord) +fiber_delete_all(struct cord *cord) { - while (!rlist_empty(&cord->alive)) - fiber_destroy(cord, rlist_first_entry(&cord->alive, - struct fiber, link)); - while (!rlist_empty(&cord->dead)) - fiber_destroy(cord, rlist_first_entry(&cord->dead, - struct fiber, link)); + cord_collect_garbage(cord); + cord_delete_fibers_in_list(cord, &cord->alive); + cord_delete_fibers_in_list(cord, &cord->dead); + cord_delete_fibers_in_list(cord, &cord->ready); } #if ENABLE_FIBER_TOP @@ -1454,6 +1467,7 @@ cord_create(struct cord *cord, const char *name) rlist_create(&cord->alive); rlist_create(&cord->ready); rlist_create(&cord->dead); + cord->garbage = NULL; cord->fiber_registry = mh_i64ptr_new(); /* sched fiber is not present in alive/ready/dead list. */ @@ -1498,6 +1512,27 @@ cord_create(struct cord *cord, const char *name) #endif } +void +cord_collect_garbage(struct cord *cord) +{ + struct fiber *garbage = cord->garbage; + if (garbage == NULL) + return; + fiber_delete(cord, garbage); + cord->garbage = NULL; +} + +static void +cord_add_garbage(struct cord *cord, struct fiber *f) +{ + cord_collect_garbage(cord); + assert(cord->garbage == NULL); + if (f != cord->fiber) + fiber_delete(cord, f); + else + cord->garbage = f; +} + void cord_destroy(struct cord *cord) { @@ -1506,7 +1541,7 @@ cord_destroy(struct cord *cord) ev_loop_destroy(cord->loop); /* Only clean up if initialized. */ if (cord->fiber_registry) { - fiber_destroy_all(cord); + fiber_delete_all(cord); mh_i64ptr_delete(cord->fiber_registry); } region_destroy(&cord->sched.gc); diff --git a/src/lib/core/fiber.h b/src/lib/core/fiber.h index 74d89ba135f19bcda48643b9f0a5c0ef22e4e07e..305255c9a602303eec4f3034940c46fe445a7b5f 100644 --- a/src/lib/core/fiber.h +++ b/src/lib/core/fiber.h @@ -698,6 +698,17 @@ struct cord { struct rlist ready; /** A cache of dead fibers for reuse */ struct rlist dead; + /** + * Latest dead fiber which couldn't be reused and waits for its + * deletion. A fiber can't be reused if it is somehow non-standard. For + * instance, has a special stack. + * A fiber can't be deleted if it is the current fiber - can't delete + * own stack safely. Then it schedules own deletion for later. The + * approach is very similar to pthread stacks deletion - pthread can't + * delete own stack, so they are saved and deleted later by a newer + * pthread or by some other dying pthread. Same here with fibers. + */ + struct fiber *garbage; /** A watcher to have a single async event for all ready fibers. * This technique is necessary to be able to suspend * a single fiber on a few watchers (for example, @@ -810,6 +821,16 @@ cord_name(struct cord *cord) bool cord_is_main(void); +/** + * Delete the latest garbage fiber which couldn't be deleted somewhy before. Can + * safely rely on the fiber being not the current one. Because if it was added + * here before, it means some previous fiber put itself here, then died + * immediately afterwards for good, and gave control to another fiber. It + * couldn't be scheduled again. + */ +void +cord_collect_garbage(struct cord *cord); + void fiber_init(int (*fiber_invoke)(fiber_func f, va_list ap)); diff --git a/test/unit/fiber.cc b/test/unit/fiber.cc index a1e3ded87b0e580f1a87a7484a4f0b48a8cfdca7..c0a46c6123c282e644bd3ffe28b457a60dba5579 100644 --- a/test/unit/fiber.cc +++ b/test/unit/fiber.cc @@ -11,6 +11,15 @@ static struct fiber_attr default_attr; static unsigned long page_size; #define PAGE_4K 4096 +/** Total count of allocated fibers in the cord. Including dead ones. */ +static int +fiber_count_total(void) +{ + size_t res = mempool_count(&cord()->fiber_mempool); + assert(res <= INT_MAX); + return (int)res; +} + static int noop_f(va_list ap) { @@ -153,6 +162,7 @@ fiber_stack_test() struct fiber *fiber; struct fiber_attr *fiber_attr; + struct slab_cache *slabc = &cord()->slabc; /* * Test a fiber with the default stack size. @@ -166,15 +176,22 @@ fiber_stack_test() /* * Test a fiber with a custom stack size. */ + int fiber_count = fiber_count_total(); + size_t used1 = slabc->allocated.stats.used; fiber_attr = fiber_attr_new(); fiber_attr_setstacksize(fiber_attr, default_attr.stack_size * 2); stack_expand_limit = default_attr.stack_size * 3 / 2; fiber = fiber_new_ex("test_stack", fiber_attr, test_stack_f); + fail_unless(fiber_count + 1 == fiber_count_total()); fiber_attr_delete(fiber_attr); if (fiber == NULL) diag_raise(); fiber_wakeup(fiber); fiber_sleep(0); + cord_collect_garbage(cord()); + fail_unless(fiber_count == fiber_count_total()); + size_t used2 = slabc->allocated.stats.used; + fail_unless(used2 == used1); note("big-stack fiber not crashed"); footer(); diff --git a/test/unit/fiber_stack.c b/test/unit/fiber_stack.c index 1525b5d0736f92b3933cc7c66bf75a8e259e8d6b..5ab1f335804835014adb5d44d03c07947657448a 100644 --- a/test/unit/fiber_stack.c +++ b/test/unit/fiber_stack.c @@ -6,6 +6,15 @@ static struct fiber_attr default_attr; +/** Total count of allocated fibers in the cord. Including dead ones. */ +static int +fiber_count_total(void) +{ + size_t res = mempool_count(&cord()->fiber_mempool); + assert(res <= INT_MAX); + return (int)res; +} + static int noop_f(va_list ap) { @@ -19,9 +28,10 @@ main_f(va_list ap) size_t used_before, used_after; struct errinj *inj; struct fiber *fiber; + int fiber_count = fiber_count_total(); header(); - plan(6); + plan(10); /* * Set non-default stack size to prevent reusing of an @@ -57,9 +67,15 @@ main_f(va_list ap) fiber = fiber_new_ex("test_madvise", fiber_attr, noop_f); inj->bparam = false; + ok(fiber_count_total() == fiber_count + 1, "allocated new"); ok(fiber != NULL, "madvise: non critical error on madvise hint"); ok(diag_get() != NULL, "madvise: diag is armed after error"); + fiber_wakeup(fiber); + fiber_sleep(0); + cord_collect_garbage(cord()); + ok(fiber_count_total() == fiber_count, "fiber is deleted"); + /* * Check if we leak on fiber destruction. * We will print an error and result get @@ -76,6 +92,7 @@ main_f(va_list ap) fiber = fiber_new_ex("test_madvise", fiber_attr, noop_f); ok(fiber != NULL, "fiber with custom stack"); + ok(fiber_count_total() == fiber_count + 1, "allocated new"); fiber_set_joinable(fiber, true); inj = errinj(ERRINJ_FIBER_MPROTECT, ERRINJ_INT); @@ -96,6 +113,9 @@ main_f(va_list ap) used_after = slabc->allocated.stats.used; ok(used_after > used_before, "expected leak detected"); + cord_collect_garbage(cord()); + ok(fiber_count_total() == fiber_count, "fiber is deleted"); + fiber_attr_delete(fiber_attr); footer(); diff --git a/test/unit/fiber_stack.result b/test/unit/fiber_stack.result index 278db4fc589412b3f7aeb36f660f27cba6a9e497..e6fca997697ca80b3c0b7315b6ccc0c44cc588ce 100644 --- a/test/unit/fiber_stack.result +++ b/test/unit/fiber_stack.result @@ -1,10 +1,14 @@ SystemError: fiber mprotect failed: Cannot allocate memory *** main_f *** -1..6 +1..10 ok 1 - mprotect: failed to setup fiber guard page ok 2 - mprotect: diag is armed after error -ok 3 - madvise: non critical error on madvise hint -ok 4 - madvise: diag is armed after error -ok 5 - fiber with custom stack -ok 6 - expected leak detected +ok 3 - allocated new +ok 4 - madvise: non critical error on madvise hint +ok 5 - madvise: diag is armed after error +ok 6 - fiber is deleted +ok 7 - fiber with custom stack +ok 8 - allocated new +ok 9 - expected leak detected +ok 10 - fiber is deleted *** main_f: done ***