summaryrefslogtreecommitdiffstats
path: root/src/lib/test-data-stack.c
blob: c38da60fd0f4485e75d36afc3e782026dbd2b80d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
/* 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
}