/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */ #include "test-lib.h" #include "lib-event-private.h" #include "event-filter.h" #include "data-stack.h" static int ds_grow_event_count = 0; static bool test_ds_grow_event_callback(struct event *event, enum event_callback_type type, struct failure_context *ctx, const char *fmt ATTR_UNUSED, va_list args ATTR_UNUSED) { const struct event_field *field; if (type != EVENT_CALLBACK_TYPE_SEND) return TRUE; ds_grow_event_count++; test_assert(ctx->type == LOG_TYPE_DEBUG); field = event_find_field_nonrecursive(event, "alloc_size"); test_assert(field != NULL && field->value_type == EVENT_FIELD_VALUE_TYPE_INTMAX && field->value.intmax >= 1024 * (5 + 100)); field = event_find_field_nonrecursive(event, "used_size"); test_assert(field != NULL && field->value_type == EVENT_FIELD_VALUE_TYPE_INTMAX && field->value.intmax >= 1024 * (5 + 100)); field = event_find_field_nonrecursive(event, "last_alloc_size"); test_assert(field != NULL && field->value_type == EVENT_FIELD_VALUE_TYPE_INTMAX && field->value.intmax >= 1024 * 100); field = event_find_field_nonrecursive(event, "frame_marker"); test_assert(field != NULL && field->value_type == EVENT_FIELD_VALUE_TYPE_STR && strstr(field->value.str, "data-stack.c") != NULL); return TRUE; } static void test_ds_grow_event(void) { const char *error; test_begin("data-stack grow event"); event_register_callback(test_ds_grow_event_callback); i_assert(event_get_global_debug_log_filter() == NULL); struct event_filter *filter = event_filter_create(); test_assert(event_filter_parse("event=data_stack_grow", filter, &error) == 0); event_set_global_debug_log_filter(filter); event_filter_unref(&filter); /* make sure the test won't fail due to earlier data stack allocations. */ data_stack_free_unused(); T_BEGIN { (void)t_malloc0(1024*5); test_assert(ds_grow_event_count == 0); (void)t_malloc0(1024*100); test_assert(ds_grow_event_count == 1); } T_END; event_unset_global_debug_log_filter(); event_unregister_callback(test_ds_grow_event_callback); test_end(); } static void test_ds_get_used_size(void) { test_begin("data-stack data_stack_get_used_size()"); size_t size1 = data_stack_get_used_size(); (void)t_malloc0(500); size_t size2 = data_stack_get_used_size(); test_assert(size1 + 500 <= size2); T_BEGIN { (void)t_malloc0(300); size_t sub_size1 = data_stack_get_used_size(); T_BEGIN { (void)t_malloc0(300); } T_END; test_assert_cmp(sub_size1, ==, data_stack_get_used_size()); } T_END; test_assert_cmp(size2, ==, data_stack_get_used_size()); test_end(); } static void test_ds_get_bytes_available(void) { test_begin("data-stack t_get_bytes_available()"); for (unsigned int i = 0; i < 32; i++) { size_t orig_avail = t_get_bytes_available(); size_t avail1; T_BEGIN { if (i > 0) t_malloc_no0(i); avail1 = t_get_bytes_available(); t_malloc_no0(avail1); test_assert_idx(t_get_bytes_available() == 0, i); t_malloc_no0(1); test_assert_idx(t_get_bytes_available() > 0, i); } T_END; T_BEGIN { if (i > 0) t_malloc_no0(i); size_t avail2 = t_get_bytes_available(); test_assert_idx(avail1 == avail2, i); t_malloc_no0(avail2 + 1); test_assert_idx(t_get_bytes_available() > 0, i); } T_END; test_assert_idx(t_get_bytes_available() == orig_avail, i); } test_end(); } static void ATTR_FORMAT(2, 0) test_ds_growing_debug(const struct failure_context *ctx ATTR_UNUSED, const char *format, va_list args) { ds_grow_event_count++; (void)t_strdup_vprintf(format, args); } static void test_ds_grow_in_event(void) { size_t i, alloc1 = 8096; unsigned char *buf; const char *error; test_begin("data-stack grow in event"); struct event_filter *filter = event_filter_create(); event_set_global_debug_log_filter(filter); test_assert(event_filter_parse("event=data_stack_grow", filter, &error) == 0); event_filter_unref(&filter); i_set_debug_handler(test_ds_growing_debug); buf = t_buffer_get(alloc1); for (i = 0; i < alloc1; i++) buf[i] = i & 0xff; test_assert(ds_grow_event_count == 0); buf = t_buffer_reget(buf, 65536); test_assert(ds_grow_event_count == 1); for (i = 0; i < alloc1; i++) { if (buf[i] != (unsigned char)i) break; } test_assert(i == alloc1); i_set_debug_handler(default_error_handler); event_unset_global_debug_log_filter(); test_end(); } static void test_ds_buffers(void) { test_begin("data-stack buffer growth"); T_BEGIN { size_t i; unsigned char *p; size_t left = t_get_bytes_available(); while (left < 10000) { t_malloc_no0(left+1); /* force a new block */ left = t_get_bytes_available(); } left -= 64; /* make room for the sentry if DEBUG */ p = t_buffer_get(1); p[0] = 1; for (i = 2; i <= left; i++) { /* grow it */ unsigned char *p2 = t_buffer_get(i); test_assert_idx(p == p2, i); p[i-1] = i & 0xff; test_assert_idx(p[i-2] == (unsigned char)(i-1), i); } /* now fix it permanently */ t_buffer_alloc_last_full(); test_assert(t_get_bytes_available() < 64 + MEM_ALIGN(1)); } T_END; test_end(); test_begin("data-stack buffer interruption"); T_BEGIN { void *b = t_buffer_get(1000); void *a = t_malloc_no0(1); void *b2 = t_buffer_get(1001); test_assert(a == b); /* expected, not guaranteed */ test_assert(b2 != b); } T_END; test_end(); test_begin("data-stack buffer with reallocs"); T_BEGIN { size_t bigleft = t_get_bytes_available(); size_t i; /* with DEBUG: the stack frame allocation takes 96 bytes and malloc takes extra 40 bytes + alignment, so don't let "i" be too high. */ for (i = 1; i < bigleft-96-40-16; i += i_rand_limit(32)) T_BEGIN { unsigned char *p, *p2; size_t left; t_malloc_no0(i); left = t_get_bytes_available(); /* The most useful idx for the assert is 'left' */ test_assert_idx(left <= bigleft-i, left); p = t_buffer_get(left/2); p[0] = 'Z'; p[left/2 - 1] = 'Z'; p2 = t_buffer_get(left + left/2); test_assert_idx(p != p2, left); test_assert_idx(p[0] == 'Z', left); test_assert_idx(p[left/2 -1] == 'Z', left); } T_END; } T_END; test_end(); } static void test_ds_realloc() { test_begin("data-stack realloc"); T_BEGIN { size_t i; unsigned char *p; size_t left = t_get_bytes_available(); while (left < 10000) { t_malloc_no0(left+1); /* force a new block */ left = t_get_bytes_available(); } left -= 64; /* make room for the sentry if DEBUG */ p = t_malloc_no0(1); p[0] = 1; for (i = 2; i <= left; i++) { /* grow it */ test_assert_idx(t_try_realloc(p, i), i); p[i-1] = i & 0xff; test_assert_idx(p[i-2] == (unsigned char)(i-1), i); } test_assert(t_get_bytes_available() < 64 + MEM_ALIGN(1)); } T_END; test_end(); } static void test_ds_recurse(int depth, int number, size_t size) { int i; char **ps; char tag[2] = { depth+1, '\0' }; int try_fails = 0; data_stack_frame_t t_id = t_push_named("test_ds_recurse[%i]", depth); ps = t_buffer_get(sizeof(char *) * number); i_assert(ps != NULL); t_buffer_alloc(sizeof(char *) * number); for (i = 0; i < number; i++) { ps[i] = t_malloc_no0(size/2); bool re = t_try_realloc(ps[i], size); i_assert(ps[i] != NULL); if (!re) { try_fails++; ps[i] = t_malloc_no0(size); } /* drop our own canaries */ memset(ps[i], tag[0], size); ps[i][size-2] = 0; } /* Do not expect a high failure rate from t_try_realloc */ test_assert_idx(try_fails <= number / 20, depth); /* Now recurse... */ if(depth>0) test_ds_recurse(depth-1, number, size); /* Test our canaries are still intact */ for (i = 0; i < number; i++) { test_assert_idx(strspn(ps[i], tag) == size - 2, i); test_assert_idx(ps[i][size-1] == tag[0], i); } test_assert_idx(t_pop(&t_id), depth); } static void test_ds_recursive(void) { int count = 20, depth = 80; int i; test_begin("data-stack recursive"); size_t init_size = data_stack_get_used_size(); for(i = 0; i < count; i++) T_BEGIN { int number=i_rand_limit(100)+50; int size=i_rand_limit(100)+50; test_ds_recurse(depth, number, size); } T_END; test_assert_cmp(init_size, ==, data_stack_get_used_size()); test_end(); } static void test_ds_pass_str(void) { data_stack_frame_t frames[32*2 + 1]; /* BLOCK_FRAME_COUNT*2 + 1 */ const char *strings[N_ELEMENTS(frames)]; test_begin("data-stack pass string"); for (unsigned int frame = 0; frame < N_ELEMENTS(frames); frame++) { frames[frame] = t_push("test"); if (frame % 10 == 5) { /* increase block counts */ (void)t_malloc_no0(1024*30); (void)t_malloc_no0(1024*30); } strings[frame] = t_strdup_printf("frame %d", frame); for (unsigned int i = 0; i <= frame; i++) { test_assert_idx(data_stack_frame_contains(&frames[frame], strings[i]) == (i == frame), frame * 100 + i); } } const char *last_str = strings[N_ELEMENTS(frames)-1]; for (unsigned int frame = N_ELEMENTS(frames); frame > 0; ) { frame--; test_assert(t_pop_pass_str(&frames[frame], &last_str)); } test_assert_strcmp(last_str, "frame 64"); /* make sure the pass_condition works properly */ const char *error, *orig_error, *orig2_error; T_BEGIN { (void)t_strdup("qwertyuiop"); error = orig_error = t_strdup("123456"); } T_END_PASS_STR_IF(TRUE, &error); orig2_error = orig_error; T_BEGIN { (void)t_strdup("abcdefghijklmnopqrstuvwxyz"); } T_END_PASS_STR_IF(FALSE, &orig2_error); /* orig_error and orig2_error both point to freed data stack frame */ test_assert(orig_error == orig2_error); /* the passed error is still valid though */ test_assert_strcmp(error, "123456"); test_end(); } void test_data_stack(void) { void (*tests[])(void) = { test_ds_grow_event, test_ds_get_used_size, test_ds_get_bytes_available, test_ds_grow_in_event, test_ds_buffers, test_ds_realloc, test_ds_recursive, test_ds_pass_str, }; for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) { ds_grow_event_count = 0; data_stack_free_unused(); T_BEGIN { tests[i](); } T_END; } } enum fatal_test_state fatal_data_stack(unsigned int stage) { #ifdef DEBUG #define NONEXISTENT_STACK_FRAME_ID (data_stack_frame_t)999999999 /* If we abort, then we'll be left with a dangling t_push() keep a record of our temporary stack id, so we can clean up. */ static data_stack_frame_t t_id = NONEXISTENT_STACK_FRAME_ID; static unsigned char *undo_ptr = NULL; static unsigned char undo_data; static bool things_are_messed_up = FALSE; if (stage != 0) { /* Presume that we need to clean up from the prior test: undo the evil write, then we will be able to t_pop cleanly, and finally we can end the test stanza. */ if (things_are_messed_up || undo_ptr == NULL) return FATAL_TEST_ABORT; /* abort, things are messed up with t_pop */ *undo_ptr = undo_data; undo_ptr = NULL; /* t_pop mustn't abort, that would cause recursion */ things_are_messed_up = TRUE; if (t_id != NONEXISTENT_STACK_FRAME_ID && !t_pop(&t_id)) return FATAL_TEST_ABORT; /* abort, things are messed up with us */ things_are_messed_up = FALSE; t_id = NONEXISTENT_STACK_FRAME_ID; test_end(); } switch(stage) { case 0: { unsigned char *p; test_begin("fatal data-stack underrun"); t_id = t_push_named("fatal_data_stack underrun"); size_t left = t_get_bytes_available(); p = t_malloc_no0(left-80); /* will fit */ p = t_malloc_no0(100); /* won't fit, will get new block */ int seek = 0; /* Seek back for the canary, don't assume endianness */ while(seek > -60 && ((p[seek+1] != 0xDB) || ((p[seek] != 0xBA || p[seek+2] != 0xAD) && (p[seek+2] != 0xBA || p[seek] != 0xAD)))) seek--; if (seek <= -60) return FATAL_TEST_ABORT; /* abort, couldn't find header */ undo_ptr = p + seek; undo_data = *undo_ptr; *undo_ptr = '*'; /* t_malloc_no0 will panic block header corruption */ test_expect_fatal_string("Corrupted data stack canary"); (void)t_malloc_no0(10); return FATAL_TEST_FAILURE; } case 1: case 2: { test_begin(stage == 1 ? "fatal t_malloc_no0 overrun near" : "fatal t_malloc_no0 overrun far"); t_id = t_push_named(stage == 1 ? "fatal t_malloc_no0 overrun first" : "fatal t_malloc_no0 overrun far"); unsigned char *p = t_malloc_no0(10); undo_ptr = p + 10 + (stage == 1 ? 0 : 8*4-1); /* presumes sentry size */ undo_data = *undo_ptr; *undo_ptr = '*'; /* t_pop will now fail */ test_expect_fatal_string("buffer overflow"); (void)t_pop(&t_id); t_id = NONEXISTENT_STACK_FRAME_ID; /* We're FUBAR, mustn't pop next entry */ return FATAL_TEST_FAILURE; } case 3: case 4: { test_begin(stage == 3 ? "fatal t_buffer_get overrun near" : "fatal t_buffer_get overrun far"); t_id = t_push_named(stage == 3 ? "fatal t_buffer overrun near" : "fatal t_buffer_get overrun far"); unsigned char *p = t_buffer_get(10); undo_ptr = p + 10 + (stage == 3 ? 0 : 8*4-1); undo_data = *undo_ptr; *undo_ptr = '*'; /* t_pop will now fail */ test_expect_fatal_string("buffer overflow"); (void)t_pop(&t_id); t_id = NONEXISTENT_STACK_FRAME_ID; /* We're FUBAR, mustn't pop next entry */ return FATAL_TEST_FAILURE; } default: things_are_messed_up = TRUE; return FATAL_TEST_FINISHED; } #else return stage == 0 ? FATAL_TEST_FINISHED : FATAL_TEST_ABORT; #endif }