From 223d66a41455033e77e8c2cbb8170eaf0217b954 Mon Sep 17 00:00:00 2001 From: Neels Hofmeyr Date: Sat, 23 Mar 2019 23:38:43 +0100 Subject: add fsm_dealloc_test.c Despite efforts to properly handle "GONE" events and entering a ST_DESTROYING only once, so far this test runs straight into a heap use-after-free. With current fsm.c, it is hard to resolve the situation with the objects named "other" also causing deallocations besides the FSM instance parent/child relations. For illustration, add an "expected" test output file fsm_dealloc_test.err, making this pass will follow in a subsequent patch. Change-Id: If801907c541bca9f524c9e5fd22ac280ca16979a --- tests/fsm/fsm_dealloc_test.c | 476 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 tests/fsm/fsm_dealloc_test.c (limited to 'tests/fsm/fsm_dealloc_test.c') diff --git a/tests/fsm/fsm_dealloc_test.c b/tests/fsm/fsm_dealloc_test.c new file mode 100644 index 00000000..5a493ad2 --- /dev/null +++ b/tests/fsm/fsm_dealloc_test.c @@ -0,0 +1,476 @@ +/* Scenarios of parent/child FSM instances cleaning up and deallocating from various triggers. */ + +#include + +#include +#include +#include +#include + +enum event { + EV_DESTROY, + EV_CHILD_GONE, + EV_OTHER_GONE, +}; + +static const struct value_string test_fsm_event_names[] = { + OSMO_VALUE_STRING(EV_DESTROY), + OSMO_VALUE_STRING(EV_CHILD_GONE), + OSMO_VALUE_STRING(EV_OTHER_GONE), + {} +}; + +enum state { + ST_ALIVE, + ST_DESTROYING, +}; + +enum objname { + root = 0, + branch0, + twig0a, + twig0b, + branch1, + twig1a, + twig1b, + + other, + scene_size +}; + +struct scene { + struct obj *o[scene_size]; + + /* The use count is actually just to help tracking what functions have not exited yet */ + struct osmo_use_count use_count; +}; + +int use_cb(struct osmo_use_count_entry *use_count_entry, int32_t old_use_count, const char *file, int line) +{ + char buf[128]; + LOGP(DLGLOBAL, LOGL_DEBUG, "%s\n", osmo_use_count_name_buf(buf, sizeof(buf), use_count_entry->use_count)); + return 0; +} + +/* References to related actual objects that are tied to FSM instances. */ +struct obj { + struct osmo_fsm_inst *fi; + struct scene *s; + struct obj *parent; + struct obj *child[2]; + struct obj *other[3]; +}; + +static void scene_forget_obj(struct scene *s, struct obj *obj) +{ + int i; + for (i = 0; i < ARRAY_SIZE(obj->s->o); i++) { + if (obj->s->o[i] != obj) + continue; + LOGPFSML(obj->fi, LOGL_DEBUG, "scene forgets %s\n", obj->fi->id); + obj->s->o[i] = NULL; + } +} + +struct scene *g_scene = NULL; + +#define GET() \ + char *token = talloc_asprintf(g_scene, "%s.%s()", obj->fi->id, __func__); \ + osmo_use_count_get_put(&g_scene->use_count, token, 1) + +#define PUT() osmo_use_count_get_put(&g_scene->use_count, token, -1) + +void alive_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + LOGPFSML(fi, LOGL_DEBUG, "%s()\n", __func__); +} + +/* Remove obj->other[*] reference, return true if found and removed, false if not. */ +bool other_gone(struct obj *obj, struct obj *other) +{ + int i; + GET(); + for (i = 0; i < ARRAY_SIZE(obj->other); i++) { + if (obj->other[i] == other) { + obj->other[i] = NULL; + LOGPFSML(obj->fi, LOGL_DEBUG, "EV_OTHER_GONE: Dropped reference %s.other[%d] = %s\n", obj->fi->id, i, + other->fi->id); + PUT(); + return true; + } + } + PUT(); + return false; +} + +/* Remove obj->child[*] reference, return true if more children remain after this, false if all are gone */ +bool child_gone(struct obj *obj, struct obj *child) +{ + int i; + bool found; + if (!child) { + LOGPFSML(obj->fi, LOGL_DEBUG, "EV_CHILD_GONE with NULL data, must be a parent_term event. Ignore.\n"); + return true; + } + GET(); + found = false; + for (i = 0; i < ARRAY_SIZE(obj->child); i++) { + if (obj->child[i] == child) { + obj->child[i] = NULL; + LOGPFSML(obj->fi, LOGL_DEBUG, "EV_CHILD_GONE: Dropped reference %s.child[%d] = %s\n", obj->fi->id, i, + child->fi->id); + found = true; + } + } + if (!found) + LOGPFSML(obj->fi, LOGL_ERROR, "EV_CHILD_GONE: cannot find child %s\n", + child && child->fi ? child->fi->id : "(null)"); + + /* Any children left? */ + for (i = 0; i < ARRAY_SIZE(obj->child); i++) { + if (obj->child[i]) { + LOGPFSML(obj->fi, LOGL_DEBUG, "still exists: child[%d]\n", i); + PUT(); + return true; + } + } + LOGPFSML(obj->fi, LOGL_DEBUG, "No more children\n"); + PUT(); + return false; +} + +void alive(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct obj *obj = fi->priv; + GET(); + LOGPFSML(fi, LOGL_DEBUG, "%s(%s)\n", __func__, osmo_fsm_event_name(fi->fsm, event)); + switch (event) { + case EV_OTHER_GONE: + if (other_gone(obj, data)) { + /* Something this object depends on is gone, trigger deallocation */ + osmo_fsm_inst_state_chg(fi, ST_DESTROYING, 0, 0); + } + break; + + case EV_CHILD_GONE: + if (!child_gone(obj, data)) { + /* All children are gone. Deallocate. */ + osmo_fsm_inst_state_chg(fi, ST_DESTROYING, 0, 0); + } + break; + + case EV_DESTROY: + osmo_fsm_inst_state_chg(fi, ST_DESTROYING, 0, 0); + break; + + default: + OSMO_ASSERT(false); + } + PUT(); +} + +void destroying_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct obj *obj = fi->priv; + GET(); + LOGPFSML(fi, LOGL_DEBUG, "%s() from %s\n", __func__, osmo_fsm_state_name(fi->fsm, prev_state)); + osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, 0); + PUT(); +} + +void destroying(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct obj *obj = fi->priv; + GET(); + LOGPFSML(fi, LOGL_DEBUG, "%s(%s)\n", __func__, osmo_fsm_event_name(fi->fsm, event)); + switch (event) { + case EV_OTHER_GONE: + other_gone(obj, data); + break; + + case EV_CHILD_GONE: + child_gone(obj, data); + break; + + case EV_DESTROY: + LOGPFSML(fi, LOGL_DEBUG, "already destroying\n"); + break; + + default: + OSMO_ASSERT(false); + } + PUT(); +} + +#define S(x) (1 << (x)) + +static const struct osmo_fsm_state test_fsm_states[] = { + [ST_ALIVE] = { + .name = "alive", + .in_event_mask = 0 + | S(EV_CHILD_GONE) + | S(EV_OTHER_GONE) + | S(EV_DESTROY) + , + .out_state_mask = 0 + | S(ST_ALIVE) + | S(ST_DESTROYING) + , + .onenter = alive_onenter, + .action = alive, + }, + [ST_DESTROYING] = { + .name = "destroying", + .in_event_mask = 0 + | S(EV_CHILD_GONE) + | S(EV_OTHER_GONE) + | S(EV_DESTROY) + , + .out_state_mask = 0 + | S(ST_DESTROYING) + , + .onenter = destroying_onenter, + .action = destroying, + }, +}; + +void cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause) +{ + struct obj *obj = fi->priv; + int i; + GET(); + LOGPFSML(fi, LOGL_DEBUG, "%s()\n", __func__); + + /* Remove from the scene overview for this test */ + scene_forget_obj(obj->s, obj); + + /* Signal "other" objects */ + for (i = 0; i < ARRAY_SIZE(obj->other); i++) { + struct obj *other = obj->other[i]; + if (!other) + continue; + LOGPFSML(fi, LOGL_DEBUG, "removing reference %s.other[%d] -> %s\n", + obj->fi->id, i, other->fi->id); + obj->other[i] = NULL; + osmo_fsm_inst_dispatch(other->fi, EV_OTHER_GONE, obj); + } + + if (obj->parent) + osmo_fsm_inst_dispatch(obj->parent->fi, EV_CHILD_GONE, obj); + + /* children are handled by fsm.c: term event / osmo_fsm_inst_term_children() */ + LOGPFSML(fi, LOGL_DEBUG, "%s() done\n", __func__); + PUT(); +} + +int timer_cb(struct osmo_fsm_inst *fi) +{ + LOGPFSML(fi, LOGL_DEBUG, "%s()\n", __func__); + return 1; +} + +void pre_term(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause) +{ + LOGPFSML(fi, LOGL_DEBUG, "%s()\n", __func__); +} + +struct osmo_fsm test_fsm = { + .name = "test", + .states = test_fsm_states, + .num_states = ARRAY_SIZE(test_fsm_states), + .cleanup = cleanup, + .timer_cb = timer_cb, + .event_names = test_fsm_event_names, + .pre_term = pre_term, + .log_subsys = DLGLOBAL, +}; + +void *ctx = NULL; + +static struct obj *obj_alloc(struct scene *s, struct obj *parent, const char *id) { + struct osmo_fsm_inst *fi; + struct obj *obj; + if (!parent) { + fi = osmo_fsm_inst_alloc(&test_fsm, s, NULL, LOGL_DEBUG, id); + OSMO_ASSERT(fi); + } else { + fi = osmo_fsm_inst_alloc_child(&test_fsm, parent->fi, EV_CHILD_GONE); + OSMO_ASSERT(fi); + osmo_fsm_inst_update_id(fi, id); + } + + obj = talloc_zero(fi, struct obj); + fi->priv = obj; + *obj = (struct obj){ + .fi = fi, + .s = s, + .parent = parent, + }; + + if (parent) { + int i; + for (i = 0; i < ARRAY_SIZE(parent->child); i++) { + if (parent->child[i]) + continue; + parent->child[i] = obj; + break; + } + } + + return obj; +}; + +void obj_add_other(struct obj *a, struct obj *b) +{ + int i; + for (i = 0; i < ARRAY_SIZE(a->other); i++) { + if (a->other[i]) + i++; + a->other[i] = b; + LOGPFSML(a->fi, LOGL_DEBUG, "%s.other[%d] = %s\n", a->fi->id, i, b->fi->id); + return; + } +} + +void obj_set_other(struct obj *a, struct obj *b) +{ + obj_add_other(a, b); + obj_add_other(b, a); +} + +static struct scene *scene_alloc() +{ + struct scene *s = talloc_zero(ctx, struct scene); + s->use_count.talloc_object = s; + s->use_count.use_cb = use_cb; + + LOGP(DLGLOBAL, LOGL_DEBUG, "%s()\n", __func__); + + /* + s->o[root] = obj_alloc(s, NULL, "root"); + */ + + s->o[branch0] = obj_alloc(s, s->o[root], "_branch0"); + + s->o[twig0a] = obj_alloc(s, s->o[branch0], "__twig0a"); + + /* + s->o[twig0b] = obj_alloc(s, s->o[branch0], "__twig0b"); + + s->o[branch1] = obj_alloc(s, s->o[root], "_branch1"); + s->o[twig1a] = obj_alloc(s, s->o[branch1], "__twig1a"); + s->o[twig1b] = obj_alloc(s, s->o[branch1], "__twig1b"); + */ + + s->o[other] = obj_alloc(s, NULL, "other"); + + obj_set_other(s->o[branch0], s->o[other]); + obj_set_other(s->o[twig0a], s->o[other]); + + return s; +} + +static int scene_dump(struct scene *s) +{ + int i; + int got = 0; + for (i = 0; i < ARRAY_SIZE(s->o); i++) { + if (!s->o[i]) + continue; + LOGP(DLGLOBAL, LOGL_DEBUG, " %s\n", s->o[i]->fi->id); + got++; + } + return got; +} + +static void scene_clean(struct scene *s) +{ + int i; + for (i = 0; i < ARRAY_SIZE(s->o); i++) { + if (!s->o[i]) + continue; + osmo_fsm_inst_term(s->o[i]->fi, OSMO_FSM_TERM_ERROR, 0); + s->o[i] = NULL; + } + talloc_free(s); +} + +void obj_destroy(struct obj *obj) +{ + osmo_fsm_inst_dispatch(obj->fi, EV_DESTROY, NULL); +} + +void obj_term(struct obj *obj) +{ + osmo_fsm_inst_term(obj->fi, OSMO_FSM_TERM_REGULAR, NULL); +} + +void test_dealloc(enum objname trigger, bool by_destroy_event) +{ + struct scene *s = scene_alloc(); + const char *label = by_destroy_event ? "destroy-event" : "term"; + int remain; + g_scene = s; + if (!s->o[trigger]) { + LOGP(DLGLOBAL, LOGL_DEBUG, "--- Test disabled: object %d was not created. Cleaning up.\n", + trigger); + scene_clean(s); + return; + } + LOGP(DLGLOBAL, LOGL_DEBUG, "------ before %s cascade, got:\n", label); + scene_dump(s); + LOGP(DLGLOBAL, LOGL_DEBUG, "---\n"); + LOGP(DLGLOBAL, LOGL_DEBUG, "--- %s at %s\n", label, s->o[trigger]->fi->id); + + if (by_destroy_event) + obj_destroy(s->o[trigger]); + else + obj_term(s->o[trigger]); + + LOGP(DLGLOBAL, LOGL_DEBUG, "--- after %s cascade:\n", label); + remain = scene_dump(s); + if (remain) { + LOGP(DLGLOBAL, LOGL_DEBUG, "--- %d objects remain. cleaning up\n", remain); + } else + LOGP(DLGLOBAL, LOGL_DEBUG, "--- all deallocated.\n"); + scene_clean(s); +} + +int main(void) +{ + enum objname trigger; + size_t ctx_blocks; + size_t ctx_size; + int by_destroy_event; + + ctx = talloc_named_const(NULL, 0, "main"); + osmo_init_logging2(ctx, NULL); + + log_set_print_filename(osmo_stderr_target, 0); + log_set_print_level(osmo_stderr_target, 1); + log_set_print_category(osmo_stderr_target, 1); + log_set_print_category_hex(osmo_stderr_target, 0); + log_set_use_color(osmo_stderr_target, 0); + osmo_fsm_log_addr(false); + + log_set_category_filter(osmo_stderr_target, DLGLOBAL, 1, LOGL_DEBUG); + + osmo_fsm_register(&test_fsm); + + ctx_blocks = talloc_total_blocks(ctx); + ctx_size = talloc_total_size(ctx); + + for (trigger = 0; trigger < scene_size; trigger++) { + for (by_destroy_event = 0; by_destroy_event < 2; by_destroy_event++) { + test_dealloc(trigger, (bool)by_destroy_event); + if (ctx_blocks != talloc_total_blocks(ctx) + || ctx_size != talloc_total_size(ctx)) { + talloc_report_full(ctx, stderr); + OSMO_ASSERT(false); + } + } + } + + talloc_free(ctx); + return 0; +} -- cgit v1.2.3