Skip to content

Commit f8a98fe

Browse files
committed
Bounds-check data_len against buffer size
Previously, any data_len passed to GetVariable() would be accepted, as there exists only one single DATA_LIMIT. Now that buffer size is somewhat decoupled from variable size, check data_len as if it's untrusted. Fully decoupling buffer and variable size would require further work and potentially another version bump. Add a basic test for nr_pages in v2. Signed-off-by: Tu Dinh <ngoc-tu.dinh@vates.tech>
1 parent ad19535 commit f8a98fe

File tree

2 files changed

+121
-10
lines changed

2 files changed

+121
-10
lines changed

handler.c

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,43 @@ get_space_usage(void)
199199
return total;
200200
}
201201

202+
static UINTN
203+
get_buf_avail(UINT32 nr_pages, uint8_t *comm_buf, uint8_t *ptr)
204+
{
205+
ptrdiff_t offset = (ptrdiff_t)ptr - (ptrdiff_t)comm_buf;
206+
ptrdiff_t bufsize = (ptrdiff_t)nr_pages * PAGE_SIZE;
207+
208+
if (offset < 0 || offset > bufsize) {
209+
assert(0);
210+
return 0;
211+
}
212+
213+
return (UINTN)(bufsize - offset);
214+
}
215+
216+
static inline uint8_t *
217+
unserialize_data_checked(UINT32 nr_pages, uint8_t *comm_buf,
218+
uint8_t **ptr, UINTN *len, UINTN limit)
219+
{
220+
uint8_t *data;
221+
UINTN avail;
222+
223+
*len = unserialize_uintn(ptr);
224+
avail = get_buf_avail(nr_pages, comm_buf, *ptr);
225+
226+
if (*len > limit || *len == 0 || *len > avail)
227+
return NULL;
228+
229+
data = malloc(*len);
230+
if (!data)
231+
return NULL;
232+
233+
memcpy(data, *ptr, *len);
234+
*ptr += *len;
235+
236+
return data;
237+
}
238+
202239
/* A limited version of SetVariable for internal use. */
203240
EFI_STATUS
204241
internal_set_variable(const uint8_t *name, UINTN name_len, const EFI_GUID *guid,
@@ -277,19 +314,20 @@ internal_get_variable(const uint8_t *name, UINTN name_len, const EFI_GUID *guid,
277314
static void
278315
do_get_variable(uint8_t *comm_buf)
279316
{
280-
UINT32 version;
317+
UINT32 version, nr_pages;
281318
uint8_t *ptr, *name;
282319
EFI_GUID guid;
283-
UINTN name_len, data_len;
320+
UINTN name_len, data_len, data_avail;
284321
BOOLEAN at_runtime;
285322
struct efi_variable *l;
286323

287324
ptr = comm_buf;
288-
if (snoop_command(&ptr, &version, NULL, NULL) != EFI_SUCCESS) {
325+
if (snoop_command(&ptr, &version, &nr_pages, NULL) != EFI_SUCCESS) {
289326
assert(0);
290327
return;
291328
}
292-
name = unserialize_data(&ptr, &name_len, NAME_LIMIT);
329+
name = unserialize_data_checked(nr_pages, comm_buf,
330+
&ptr, &name_len, NAME_LIMIT);
293331
if (!name) {
294332
serialize_result(&comm_buf, name_len == 0 ? EFI_NOT_FOUND : EFI_DEVICE_ERROR);
295333
return;
@@ -299,6 +337,17 @@ do_get_variable(uint8_t *comm_buf)
299337
at_runtime = unserialize_boolean(&ptr);
300338

301339
ptr = comm_buf;
340+
data_avail = get_buf_avail(nr_pages, comm_buf, ptr) - sizeof(EFI_STATUS) -
341+
sizeof(UINT32);
342+
/*
343+
* DataSize is not firmware-controlled, and callers are not aware of comm
344+
* buffer size. Therefore, we can't return an error code even if DataSize is
345+
* larger than the actual available buffer size; all we can do is to limit
346+
* data_len accordingly.
347+
*/
348+
if (data_len > data_avail)
349+
data_len = data_avail;
350+
302351
l = var_list;
303352
while (l) {
304353
if (l->name_len == name_len &&
@@ -1592,7 +1641,7 @@ debug_all_variables(const struct efi_variable *l)
15921641
static void
15931642
do_set_variable(uint8_t *comm_buf)
15941643
{
1595-
UINT32 version;
1644+
UINT32 version, nr_pages;
15961645
UINTN name_len, data_len;
15971646
struct efi_variable *l, *prev = NULL;
15981647
uint8_t *ptr, *name, *data;
@@ -1604,17 +1653,19 @@ do_set_variable(uint8_t *comm_buf)
16041653
EFI_TIME timestamp;
16051654

16061655
ptr = comm_buf;
1607-
if (snoop_command(&ptr, &version, NULL, NULL) != EFI_SUCCESS) {
1656+
if (snoop_command(&ptr, &version, &nr_pages, NULL) != EFI_SUCCESS) {
16081657
assert(0);
16091658
return;
16101659
}
1611-
name = unserialize_data(&ptr, &name_len, NAME_LIMIT);
1660+
name = unserialize_data_checked(nr_pages, comm_buf,
1661+
&ptr, &name_len, NAME_LIMIT);
16121662
if (!name) {
16131663
serialize_result(&comm_buf, name_len == 0 ? EFI_INVALID_PARAMETER : EFI_DEVICE_ERROR);
16141664
return;
16151665
}
16161666
unserialize_guid(&ptr, &guid);
1617-
data = unserialize_data(&ptr, &data_len, data_limit(version));
1667+
data = unserialize_data_checked(nr_pages, comm_buf,
1668+
&ptr, &data_len, data_limit(version));
16181669
if (!data && data_len) {
16191670
serialize_result(&comm_buf,
16201671
data_len > data_limit(version)
@@ -1928,19 +1979,21 @@ do_set_variable(uint8_t *comm_buf)
19281979
static void
19291980
do_get_next_variable(uint8_t *comm_buf)
19301981
{
1982+
UINT32 nr_pages;
19311983
UINTN name_len, avail_len;
19321984
uint8_t *ptr, *name;
19331985
struct efi_variable *l;
19341986
EFI_GUID guid;
19351987
BOOLEAN at_runtime;
19361988

19371989
ptr = comm_buf;
1938-
if (snoop_command(&ptr, NULL, NULL, NULL) != EFI_SUCCESS) {
1990+
if (snoop_command(&ptr, NULL, &nr_pages, NULL) != EFI_SUCCESS) {
19391991
assert(0);
19401992
return;
19411993
}
19421994
avail_len = unserialize_uintn(&ptr);
1943-
name = unserialize_data(&ptr, &name_len, NAME_LIMIT);
1995+
name = unserialize_data_checked(nr_pages, comm_buf,
1996+
&ptr, &name_len, NAME_LIMIT);
19441997
if (!name && name_len) {
19451998
serialize_result(&comm_buf, EFI_DEVICE_ERROR);
19461999
return;

test.c

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,6 +1962,60 @@ MULTI_TEST(test_set_variable_mor_key)
19621962
data, 8, ATTR_BRNV, EFI_ACCESS_DENIED);
19631963
}
19641964

1965+
static void test_set_variable_v2_downgrade(void)
1966+
{
1967+
EFI_STATUS status;
1968+
uint8_t tmp[DATA_LIMIT_MAX] = {0};
1969+
uint8_t *ptr;
1970+
1971+
reset_test(2);
1972+
1973+
sv_ok(2, tname1, &tguid1, tmp, DATA_LIMIT_V2, ATTR_B);
1974+
1975+
reset_buf(SHMEM_PAGES_V1);
1976+
1977+
call_get_variable(1, tname1, &tguid1, DATA_LIMIT_V1, 1);
1978+
1979+
/* Whatever it does here, it shouldn't crash. */
1980+
1981+
ptr = buf;
1982+
status = unserialize_uintn(&ptr); /* status */
1983+
g_assert_cmpuint(status, !=, EFI_SUCCESS);
1984+
}
1985+
1986+
static void test_set_variable_v2_short_buffer(void)
1987+
{
1988+
UINT32 nr_pages;
1989+
EFI_STATUS status;
1990+
uint8_t tmp[DATA_LIMIT_V2] = {0};
1991+
uint8_t *ptr = buf;
1992+
1993+
reset_test(2);
1994+
1995+
sv_ok(2, tname1, &tguid1, tmp, DATA_LIMIT_V2, ATTR_B);
1996+
1997+
for (nr_pages = 1; nr_pages < SHMEM_PAGES_V2_MIN; nr_pages++) {
1998+
reset_buf(nr_pages);
1999+
call_get_variable(2, tname1, &tguid1, DATA_LIMIT_V2, 0);
2000+
ptr = buf;
2001+
status = unserialize_uintn(&ptr); /* status */
2002+
g_assert_cmpuint(status, ==, EFI_INVALID_PARAMETER);
2003+
}
2004+
/*
2005+
* TODO: For now all nr_pages values within range are valid for containing
2006+
* a variable of size DATA_LIMIT_V2. If DATA_LIMIT_V2 is increased, full
2007+
* serialization bounds check would be needed.
2008+
*/
2009+
for (nr_pages = SHMEM_PAGES_V2_MIN; nr_pages <= SHMEM_PAGES_V2_MAX;
2010+
nr_pages++) {
2011+
reset_buf(nr_pages);
2012+
call_get_variable(2, tname1, &tguid1, DATA_LIMIT_V2, 0);
2013+
ptr = buf;
2014+
status = unserialize_uintn(&ptr); /* status */
2015+
g_assert_cmpuint(status, ==, EFI_SUCCESS);
2016+
}
2017+
}
2018+
19652019
static void set_usermode(UINT32 version)
19662020
{
19672021
/* Move into user mode by enrolling Platform Key. */
@@ -2778,6 +2832,10 @@ int main(int argc, char **argv)
27782832
test_set_variable_mor);
27792833
ADD_MULTI_TEST("/test/set_variable/mor_key",
27802834
test_set_variable_mor_key);
2835+
g_test_add_func("/test/set_variable/v2_downgrade",
2836+
test_set_variable_v2_downgrade);
2837+
g_test_add_func("/test/set_variable/v2_short_buffer",
2838+
test_set_variable_v2_short_buffer);
27812839

27822840
ADD_MULTI_TEST("/test/secure_set_variable/use_bad_digest",
27832841
test_use_bad_digest);

0 commit comments

Comments
 (0)