summaryrefslogtreecommitdiff
path: root/test/always_memset.c
blob: 8a022ef8ef7525091bb7e932148c3be31acb946e (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
/* Copyright 2022 The ChromiumOS Authors
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/*
 * We enable optimization level 3 in the test/build.mk.
 * Running these tests without optimization is pointless, since the primary
 * purpose of the always_memset function is to evade compiler optimizations.
 * If optimization is disabled, the test_optimization_working test will fail.
 */

#include "common.h"
#include "cryptoc/util.h"
#include "test_util.h"

#include <string.h>

/* 256 bytes of stack is only safe enough for a memcpy. */
#define EXTRA_STACK_SIZE 256
#define UNIQUE_STRING "Hello World!"

/**
 * @brief Check basic memset behavior of always_memset.
 */
test_static int test_basic_functionality(void)
{
	char buf[256];

	for (size_t i = 0; i < sizeof(buf); i++) {
		buf[i] = (char)i;
	}

	always_memset(buf, 1, sizeof(buf));

	TEST_ASSERT_MEMSET(buf, 1, sizeof(buf));

	return EC_SUCCESS;
}

/**
 * @brief Builtin memset stand-in.
 *
 * The compiler doesn't see our EC memset as a function that can be optimized
 * out "with no side effect", so we present one here.
 */
test_static inline __attribute__((always_inline)) void
fake_builtin_memset(char *dest, char c, size_t len)
{
	for (size_t i = 0; i < len; i++)
		dest[i] = c;
}

/**
 * This function creates a contrived scenario where the compiler would choose
 * to optimize out the last memset due to no side effect.
 *
 * This methodology/setup has also been manually tested using the real builtin
 * memset with -O3 in a GNU/Linux environment using GCC-12.2.0 and clang-14.0.6.
 *
 * Notes for manually verifying:
 * - Run outright with normal memset. Ensure that you can still read the
 *   UNIQUE_STRING from p.
 * - Use GDB.
 * - Set break point at |exercise_memset| and run.
 * - Run "info locals" to check the state of |space| and |buf|.
 * - Run "p &space" and "p &buf". Ensure both are real stack addresses and
 *   that buf's address is lower than space's address.
 *   Ensure the size of |space|.
 * - Save the address of |space| for later by doing "set $addr = &space".
 * - Set a mem watch on |buf| by running "watch buf".
 * - Run "c" to continue the function. Ensure that the only part of the function
 *   that touches this variable is the initialization.
 * - If there seems to be something odd happening, open "layout split" and
 *   step instruction-by-instruction using "si".
 *   Note that memset will not be a function call and will not always
 *   look the same. It will probably be tightly integrated with other
 *   functionality.
 * - To check how much of the stack |space| was used by the caller,
 *   run "x/256xb $adr" or "x/64xw $adr" after the memcpy.
 *
 * Disable ASAN with the attribute because this function returns a pointer to
 * stack memory, which is normally very naughty, but is what we want to test
 * here.
 */
__attribute__((no_sanitize("address"))) test_static void
exercise_memset(char **p)
{
	/*
	 * Add extra stack space so that |buf| doesn't get trampled while the
	 * caller is processing the |p| we set (ex. printing and testing p).
	 *
	 * Without volatile, space will be optimized out.
	 */
	volatile char __unused space[EXTRA_STACK_SIZE] = {
		[0 ... EXTRA_STACK_SIZE - 1] = 's'
	};

	char buf[] = UNIQUE_STRING;
	*p = buf;

	/*
	 * Force access to |buf| to ensure that it is allocated and seen as
	 * used. Without casting to volatile, the access would be optimized out.
	 * We don't want to make |buf| itself volatile, since
	 * we want the compiler to optimize out the final memset.
	 */
	for (size_t i = 0; i < sizeof(buf); i++)
		(void)((volatile char *)buf)[i];

	/* Expect the following memset to be omitted during optimization. */
	fake_builtin_memset(buf, 0, sizeof(buf));
}

/**
 * Ensure that optimization is removing a trailing memset that it deems to have
 * no side-effect.
 */
test_static int test_optimization_working(void)
{
	char buf[sizeof(UNIQUE_STRING)];
	char *p;

	exercise_memset(&p);
	memcpy(buf, p, sizeof(buf));

	/*
	 * We expect that the compiler would have optimized out the final
	 * memset, thus we should still be able to see the UNIQUE_STRING in
	 * memory.
	 */
	TEST_ASSERT_ARRAY_EQ(buf, UNIQUE_STRING, sizeof(buf));

	return EC_SUCCESS;
}

/**
 * This function creates a contrived scenario where the compiler would choose
 * to optimize out the last memset due to no side effect.
 *
 * This function layout must remain identical to exercise_memset.
 *
 * Disable ASAN with the attribute because this function returns a pointer to
 * stack memory, which is normally very naughty, but is what we want to test
 * here.
 */
__attribute__((no_sanitize("address"))) test_static void
exercise_always_memset(char **p)
{
	/*
	 * Add extra stack space so that |buf| doesn't get trampled while the
	 * caller is processing the |p| we set (ex. printing and testing p).
	 *
	 * Without volatile, space will be optimized out.
	 */
	volatile char __unused space[EXTRA_STACK_SIZE] = {
		[0 ... EXTRA_STACK_SIZE - 1] = 's'
	};

	char buf[] = UNIQUE_STRING;
	*p = buf;

	/*
	 * Force access to |buf| to ensure that it is allocated and seen as
	 * used. Without casting to volatile, the access would be optimized out.
	 * We don't want to make |buf| itself volatile, since
	 * we want the compiler to optimize out the final memset.
	 */
	for (size_t i = 0; i < sizeof(buf); i++)
		(void)((volatile char *)buf)[i];

	/* Expect the following memset to NOT be omitted during optimization. */
	always_memset(buf, 0, sizeof(buf));
}

/**
 * Ensure that always_memset works when used in a scenario where a normal
 * memset would be removed.
 */
test_static int test_always_memset(void)
{
	char buf[sizeof(UNIQUE_STRING)];
	char *p;

	exercise_always_memset(&p);
	memcpy(buf, p, sizeof(buf));

	TEST_ASSERT_MEMSET(buf, 0, sizeof(buf));

	return EC_SUCCESS;
}

void run_test(int argc, const char **argv)
{
	test_reset();

	RUN_TEST(test_basic_functionality);
	RUN_TEST(test_optimization_working);
	RUN_TEST(test_always_memset);

	test_print_result();
}