From: Ahmad Fatoum <[email protected]> On shutdown, we want to run the devm_ actions, but not free managed memory one allocation at a time, as the kernel reclaims it anyway.
Let's add talloc_run_destructors(), which runs and drops destructors in talloc_free() order, but keeps all chunks. It only takes chunks without parent, which is all devres needs, as a destructor could otherwise free the chunk along with its parent while it's being walked. Assisted-by: Claude:opus-5.5 Signed-off-by: Ahmad Fatoum <[email protected]> --- include/talloc.h | 1 + lib/talloc.c | 52 +++++++++++++++++++++++++++++ test/self/talloc.c | 83 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) diff --git a/include/talloc.h b/include/talloc.h index 806921898b6c..a1a65267b2e3 100644 --- a/include/talloc.h +++ b/include/talloc.h @@ -31,6 +31,7 @@ void *talloc_realloc_size(const void *parent, void *mem, size_t size); void talloc_free(void *mem); int talloc_add_destructor(const void *parent, void (*fn)(void *), void *data); +void talloc_run_destructors(void *mem); char *talloc_strdup(const void *parent, const char *str); const char *talloc_strdup_const(const void *parent, const char *str); diff --git a/lib/talloc.c b/lib/talloc.c index 97787b2d6503..f5c414e82ae7 100644 --- a/lib/talloc.c +++ b/lib/talloc.c @@ -320,6 +320,58 @@ int talloc_add_destructor(const void *parent, void (*fn)(void *), void *data) } EXPORT_SYMBOL(talloc_add_destructor); +/* + * Like __talloc_free(), but children are parked in @kept instead of being + * freed. @hdr and @kept are marked, so destructors can't free them. + */ +static void talloc_run_child_destructors(struct talloc *hdr) +{ + struct talloc kept = {}; + + kept.next = &kept; + hdr->next = hdr; + + while (hdr->child) { + struct talloc *child = hdr->child; + + if (is_destructor(child)) { + __talloc_free(child); + continue; + } + + mem_set_parent(child, NULL); + talloc_run_child_destructors(child); + mem_set_parent(child, &kept); + } + + while (kept.child) + mem_set_parent(kept.child, hdr); +} + +/** + * talloc_run_destructors() - Run destructors without freeing any memory + * + * @mem: pointer to previously talloc'ed memory chunk without parent. + * + * Runs the destructors of @mem and its descendants in the order + * talloc_free() would and drops them, but keeps all chunks. + */ +void talloc_run_destructors(void *mem) +{ + struct talloc *hdr = mem2hdr(mem); + + if (ZERO_OR_NULL_PTR(mem) || is_freeing(hdr)) + return; + + /* A destructor could free the parent and @mem along with it */ + if (WARN_ON(!is_root(hdr))) + return; + + talloc_run_child_destructors(hdr); + hdr->next = NULL; +} +EXPORT_SYMBOL(talloc_run_destructors); + /** * talloc_free_const() - call talloc_free, unless read/only memory * diff --git a/test/self/talloc.c b/test/self/talloc.c index 85fc74336f43..12aef4c879ee 100644 --- a/test/self/talloc.c +++ b/test/self/talloc.c @@ -172,6 +172,13 @@ static void other_free_destructor(void *data) talloc_free(other_to_free); } +static void other_parent_free_destructor(void *data) +{ + log_destructor(data); + /* Must do nothing, other_to_free is parked while we run */ + talloc_free(talloc_parent(other_to_free)); +} + /* Chunk holding @c, with a destructor that is passed the chunk */ static void *talloc_char(const void *parent, char c, void (*fn)(void *)) { @@ -265,6 +272,81 @@ static void talloc_destructor_tests(void) talloc_free(root); } +static void talloc_run_destructors_tests(void) +{ + void *root, *a, *b, *c, *d, *x; + + root = talloc_new(NULL); + + /* Same order as talloc_free(), but everything stays */ + reset_destructor_log(); + a = talloc_char(root, 'a', log_destructor); + b = talloc_char(a, 'b', log_destructor); + selftest_check(talloc_add_destructor(a, log_destructor, "1") == 0); + c = talloc_char(a, 'c', log_destructor); + d = talloc_char(c, 'd', log_destructor); + if (!a || !b || !c || !d) + return; + + talloc_run_destructors(root); + selftest_check(!strcmp(destructor_log, "dc1ba")); + selftest_check(talloc_parent(a) == root); + selftest_check(talloc_parent(b) == a && talloc_parent(c) == a); + selftest_check(talloc_parent(d) == c && *(char *)d == 'd'); + selftest_check(talloc_total_blocks(a) == 3); + + /* Destructors that ran are gone, new ones run */ + selftest_check(talloc_add_destructor(c, log_destructor, "2") == 0); + talloc_run_destructors(root); + talloc_free(a); + selftest_check(!strcmp(destructor_log, "dc1ba2")); + + /* Chunks being walked can't be freed from destructors */ + reset_destructor_log(); + a = talloc_char(root, 'a', log_destructor); + b = talloc_char(a, 'b', self_free_destructor); + c = talloc_char(b, 'c', other_free_destructor); + other_to_free = a; + talloc_run_destructors(root); + selftest_check(!strcmp(destructor_log, "cba")); + selftest_check(talloc_parent(c) == b && talloc_parent(b) == a); + selftest_check(talloc_parent(a) == root); + talloc_free(a); + + /* Nor the parent of a chunk that was walked already */ + reset_destructor_log(); + a = talloc_char(root, 'a', log_destructor); + c = talloc_char(a, 'c', other_parent_free_destructor); + other_to_free = talloc_char(a, 'b', log_destructor); + talloc_run_destructors(root); + selftest_check(!strcmp(destructor_log, "bca")); + selftest_check(talloc_parent(other_to_free) == a && talloc_parent(c) == a); + talloc_free(a); + + /* Other chunks can, even siblings that are still to be walked */ + reset_destructor_log(); + a = talloc_char(root, 'a', log_destructor); + other_to_free = talloc_char(a, 'b', log_destructor); + c = talloc_char(a, 'c', other_free_destructor); + talloc_run_destructors(root); + selftest_check(!strcmp(destructor_log, "cba")); + selftest_check(talloc_total_blocks(a) == 2 && *(char *)c == 'c'); + talloc_free(a); + + selftest_check(talloc_total_blocks(root) == 1); + talloc_free(root); + + /* @mem can't be freed from its destructors and is usable after */ + reset_destructor_log(); + x = talloc_char(NULL, 'x', self_free_destructor); + talloc_run_destructors(x); + selftest_check(!strcmp(destructor_log, "x")); + selftest_check(*(char *)x == 'x' && !talloc_parent(x)); + selftest_check(talloc_add_destructor(x, log_destructor, "X") == 0); + talloc_free(x); + selftest_check(!strcmp(destructor_log, "xX")); +} + static void test_talloc(void) { talloc_basic_tests(); @@ -272,5 +354,6 @@ static void test_talloc(void) talloc_stress_tests(); talloc_corner_cases(); talloc_destructor_tests(); + talloc_run_destructors_tests(); } bselftest(core, test_talloc); -- 2.47.3
