Skip to content

Commit 59dc886

Browse files
authored
nvpair: Check for un-terminated strings in packed nvlist
Add additional checks to verify a packed string or string array nvpair is terminated. Or more specifically, verify doing a strlen() on the prospective string does not overrun the packed nvlist buffer. Also add additional checks in the libzfs_input_checks test case to verify un-terminated strings, and add in a nvlist ioctl payload fuzz test for good measure. Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov> Signed-off-by: Tony Hutter <hutter2@llnl.gov> Closes #18604
1 parent 4bc8c39 commit 59dc886

3 files changed

Lines changed: 207 additions & 17 deletions

File tree

module/nvpair/nvpair.c

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@
135135
#define NVP_SIZE_CALC(name_len, data_len) \
136136
(NV_ALIGN((sizeof (nvpair_t)) + name_len) + NV_ALIGN(data_len))
137137

138-
static int i_get_value_size(data_type_t type, const void *data, uint_t nelem);
138+
static int i_get_value_size(data_type_t type, const void *data, uint_t nelem,
139+
size_t max_size);
139140
static int nvlist_add_common(nvlist_t *nvl, const char *name, data_type_t type,
140141
uint_t nelem, const void *data);
141142

@@ -810,8 +811,10 @@ i_validate_nvpair(nvpair_t *nvp)
810811
* verify nvp_type, nvp_value_elem, and also possibly
811812
* verify string values and get the value size.
812813
*/
813-
size2 = i_get_value_size(type, NVP_VALUE(nvp), NVP_NELEM(nvp));
814814
size1 = nvp->nvp_size - NVP_VALOFF(nvp);
815+
size2 = i_get_value_size(type, NVP_VALUE(nvp), NVP_NELEM(nvp),
816+
size1);
817+
815818
if (size2 < 0 || size1 != NV_ALIGN(size2))
816819
return (EFAULT);
817820

@@ -1002,12 +1005,21 @@ nvlist_remove_nvpair(nvlist_t *nvl, nvpair_t *nvp)
10021005
* DATA_TYPE_STRING and
10031006
* DATA_TYPE_STRING_ARRAY
10041007
* Is data == NULL then the size of the string(s) is excluded.
1008+
*
1009+
* If 'max_size' is non-zero, then don't look beyond 'max_size' number of
1010+
* bytes when calculating a value size. Note that 'max_size' should include
1011+
* the NULL terminator byte when calculating string size. If 'max_size' is 0,
1012+
* it is ignored.
10051013
*/
10061014
static int
1007-
i_get_value_size(data_type_t type, const void *data, uint_t nelem)
1015+
i_get_value_size(data_type_t type, const void *data, uint_t nelem,
1016+
size_t max_size)
10081017
{
10091018
uint64_t value_sz;
10101019

1020+
if (max_size == 0)
1021+
max_size = INT32_MAX;
1022+
10111023
if (i_validate_type_nelem(type, nelem) != 0)
10121024
return (-1);
10131025

@@ -1052,10 +1064,15 @@ i_get_value_size(data_type_t type, const void *data, uint_t nelem)
10521064
break;
10531065
#endif
10541066
case DATA_TYPE_STRING:
1055-
if (data == NULL)
1067+
if (data == NULL) {
10561068
value_sz = 0;
1057-
else
1058-
value_sz = strlen(data) + 1;
1069+
} else {
1070+
value_sz = strnlen(data, max_size);
1071+
if (value_sz >= max_size) {
1072+
return (-1); /* string not terminated */
1073+
}
1074+
value_sz += 1;
1075+
}
10591076
break;
10601077
case DATA_TYPE_BOOLEAN_ARRAY:
10611078
value_sz = (uint64_t)nelem * sizeof (boolean_t);
@@ -1089,16 +1106,23 @@ i_get_value_size(data_type_t type, const void *data, uint_t nelem)
10891106
break;
10901107
case DATA_TYPE_STRING_ARRAY:
10911108
value_sz = (uint64_t)nelem * sizeof (uint64_t);
1092-
10931109
if (data != NULL) {
10941110
char *const *strs = data;
10951111
uint_t i;
1112+
size_t newsize;
10961113

10971114
/* no alignment requirement for strings */
10981115
for (i = 0; i < nelem; i++) {
10991116
if (strs[i] == NULL)
11001117
return (-1);
1101-
value_sz += strlen(strs[i]) + 1;
1118+
1119+
newsize = strnlen(strs[i], max_size);
1120+
1121+
if (newsize == max_size)
1122+
return (-1); /* not terminated */
1123+
1124+
value_sz += newsize + 1; /* +1 for NULL */
1125+
max_size -= newsize + 1;
11021126
}
11031127
}
11041128
break;
@@ -1163,7 +1187,7 @@ nvlist_add_common(nvlist_t *nvl, const char *name,
11631187
* In case of data types DATA_TYPE_STRING and DATA_TYPE_STRING_ARRAY
11641188
* is the size of the string(s) included.
11651189
*/
1166-
if ((value_sz = i_get_value_size(type, data, nelem)) < 0)
1190+
if ((value_sz = i_get_value_size(type, data, nelem, 0)) < 0)
11671191
return (EINVAL);
11681192

11691193
if (i_validate_nvpair_value(type, nelem, data) != 0)
@@ -1588,7 +1612,7 @@ nvpair_value_common(const nvpair_t *nvp, data_type_t type, uint_t *nelem,
15881612
#endif
15891613
if (data == NULL)
15901614
return (EINVAL);
1591-
if ((value_sz = i_get_value_size(type, NULL, 1)) < 0)
1615+
if ((value_sz = i_get_value_size(type, NULL, 1, 0)) < 0)
15921616
return (EINVAL);
15931617
memcpy(data, NVP_VALUE(nvp), (size_t)value_sz);
15941618
if (nelem != NULL)
@@ -3019,7 +3043,8 @@ nvs_native_nvp_op(nvstream_t *nvs, nvpair_t *nvp)
30193043
* In case of data types DATA_TYPE_STRING and DATA_TYPE_STRING_ARRAY
30203044
* is the size of the string(s) excluded.
30213045
*/
3022-
if ((value_sz = i_get_value_size(type, NULL, NVP_NELEM(nvp))) < 0)
3046+
if ((value_sz = i_get_value_size(type, NULL, NVP_NELEM(nvp),
3047+
NVP_SIZE(nvp))) < 0)
30233048
return (EFAULT);
30243049

30253050
if (NVP_SIZE_CALC(nvp->nvp_name_sz, value_sz) > nvp->nvp_size)
@@ -3333,7 +3358,7 @@ nvs_xdr_nvp_op(nvstream_t *nvs, nvpair_t *nvp)
33333358
* In case of data types DATA_TYPE_STRING and DATA_TYPE_STRING_ARRAY
33343359
* is the size of the string(s) excluded.
33353360
*/
3336-
if ((value_sz = i_get_value_size(type, NULL, nelem)) < 0)
3361+
if ((value_sz = i_get_value_size(type, NULL, nelem, NVP_SIZE(nvp)) < 0))
33373362
return (EFAULT);
33383363

33393364
/* if there is no data to extract then return */

module/zfs/zfs_ioctl.c

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4126,7 +4126,6 @@ static int
41264126
zfs_ioc_log_history(const char *unused, nvlist_t *innvl, nvlist_t *outnvl)
41274127
{
41284128
(void) unused, (void) outnvl;
4129-
const char *message;
41304129
char *poolname;
41314130
spa_t *spa;
41324131
int error;
@@ -4147,7 +4146,7 @@ zfs_ioc_log_history(const char *unused, nvlist_t *innvl, nvlist_t *outnvl)
41474146
if (error != 0)
41484147
return (error);
41494148

4150-
message = fnvlist_lookup_string(innvl, "message");
4149+
const char *message = fnvlist_lookup_string(innvl, "message");
41514150

41524151
if (spa_version(spa) < SPA_VERSION_ZPOOL_HISTORY) {
41534152
spa_close(spa, FTAG);

tests/zfs-tests/cmd/libzfs_input_check.c

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ static const zfs_ioc_t ioc_skip[] = {
8585
ZFS_IOC_DSOBJ_TO_DSNAME,
8686
ZFS_IOC_OBJ_TO_PATH,
8787
ZFS_IOC_POOL_SET_PROPS,
88-
ZFS_IOC_POOL_GET_PROPS,
8988
ZFS_IOC_SET_FSACL,
9089
ZFS_IOC_GET_FSACL,
9190
ZFS_IOC_SHARE,
@@ -125,11 +124,136 @@ static const zfs_ioc_t ioc_skip[] = {
125124
lzc_ioctl_test(ioc, name, req, opt, err, wild); \
126125
} while (0)
127126

127+
#define IOC_INPUT_TEST_INJECT(ioc, name, innvl) \
128+
do { \
129+
active_test = __func__ + 5; \
130+
lzc_ioctl_run_impl(ioc, name, innvl, 0, B_TRUE); \
131+
} while (0)
132+
133+
/*
134+
* Given a zfs_cmd_t containing an already packed nvlist in zc->zc_nvlist_src,
135+
* and its original innvl, look in innvl for the last string nvpair, or last
136+
* string array nvpair, and remove the string terminator. The idea is to
137+
* corrupt the nvlist string value so that anyone doing a strlen() on it will
138+
* read past the end of the packed nvlist buffer and trigger a crash.
139+
*/
140+
static void
141+
do_bad_string(zfs_cmd_t *zc, nvlist_t *innvl)
142+
{
143+
nvpair_t *elem = NULL;
144+
nvpair_t *lastseen = NULL;
145+
const char *str = NULL;
146+
const char **arr;
147+
uint_t n;
148+
char *off;
149+
char *packed;
150+
uint64_t size, off_size;
151+
152+
while ((elem = nvlist_next_nvpair(innvl, elem)) != NULL) {
153+
if ((nvpair_type(elem) == DATA_TYPE_STRING) ||
154+
(nvpair_type(elem) == DATA_TYPE_STRING_ARRAY))
155+
lastseen = elem;
156+
}
157+
158+
if (lastseen == NULL)
159+
return; /* No strings */
160+
161+
/*
162+
* Lookup either the last string, or the last string in the last
163+
* string array in the nvlist. We will use this to corrupt from the
164+
* string to the end of the nvlist buffer. Any attempts to strlen this
165+
* string should run pass the end of the packed buffer.
166+
*/
167+
if (nvpair_value_string(lastseen, &str) != 0) {
168+
if (nvpair_value_string_array(lastseen, &arr, &n) == 0)
169+
str = arr[n-1];
170+
}
171+
172+
/*
173+
* We now have the last string. Corrupt everything from the NULL
174+
* terminator byte for the last string to the end of the packed nvlist
175+
* buffer.
176+
*/
177+
packed = (char *)zc->zc_nvlist_src;
178+
size = zc->zc_nvlist_src_size;
179+
180+
off = memmem(packed, size, str, strlen(str));
181+
off_size = strlen(str);
182+
183+
memset(&off[off_size - 1], '!', (packed + size) -
184+
(&off[off_size - 1]));
185+
186+
}
187+
188+
/*
189+
* For each byte in the packed nvlist list in zc, corrupt a single byte, then
190+
* try doing the ioctl. This tests how well the kernel handles fuzzed nvlists.
191+
*
192+
* NOTE - make sure you are doing this with a "safe" ioctl! You don't want to
193+
* run this on an ioctl that can potentially corrupt data (like a zpool create).
194+
*/
195+
static void
196+
do_fuzz(int zfs_fd, zfs_ioc_t ioc, zfs_cmd_t *zc)
197+
{
198+
uint64_t size;
199+
uint64_t i;
200+
unsigned char old = 0;
201+
unsigned char *pos;
202+
zfs_cmd_t orig_zc = *zc;
203+
204+
pos = (unsigned char *) zc->zc_nvlist_src;
205+
size = zc->zc_nvlist_src_size;
206+
207+
/*
208+
* Fuzz each byte in the packed nvlist, one byte at a time, and do the
209+
* ioctl. If the kernel doesn't crash, then the test passed.
210+
*/
211+
for (i = 0; i < size; i++) {
212+
/* Restore the previously corrupted byte */
213+
if (i > 0)
214+
pos[i-1] = old;
215+
216+
old = pos[i];
217+
218+
/* Corrupt the new byte */
219+
pos[i]++;
220+
221+
/*
222+
* Do the ioctl and ignore the return code. We just want to
223+
* see if the kernel panics.
224+
*/
225+
lzc_ioctl_fd(zfs_fd, ioc, zc);
226+
227+
/*
228+
* Restore 'zc' with original fields since the ioctl may
229+
* have modified them.
230+
*/
231+
*zc = orig_zc;
232+
}
233+
/* Restore last byte */
234+
if (i > 0)
235+
pos[i - 1] = old;
236+
237+
/*
238+
* Try fuzzing the packed nvlist size field. Test it with one byte
239+
* bigger and one byte smaller than the current value.
240+
*/
241+
zc->zc_nvlist_src_size--;
242+
lzc_ioctl_fd(zfs_fd, ioc, zc);
243+
244+
zc->zc_nvlist_src_size += 2;
245+
lzc_ioctl_fd(zfs_fd, ioc, zc);
246+
247+
/* Restore to normal */
248+
zc->zc_nvlist_src_size -= 1;
249+
}
250+
128251
/*
129252
* run a zfs ioctl command, verify expected results and log failures
130253
*/
131254
static void
132-
lzc_ioctl_run(zfs_ioc_t ioc, const char *name, nvlist_t *innvl, int expected)
255+
lzc_ioctl_run_impl(zfs_ioc_t ioc, const char *name, nvlist_t *innvl,
256+
int expected, boolean_t do_corrupt)
133257
{
134258
zfs_cmd_t zc = {"\0"};
135259
char *packed = NULL;
@@ -160,10 +284,30 @@ lzc_ioctl_run(zfs_ioc_t ioc, const char *name, nvlist_t *innvl, int expected)
160284
zc.zc_nvlist_dst_size = MAX(size * 2, 128 * 1024);
161285
zc.zc_nvlist_dst = (uint64_t)(uintptr_t)malloc(zc.zc_nvlist_dst_size);
162286

287+
if (do_corrupt) {
288+
/*
289+
* Try changing bytes in the packed nvlist to see if it will
290+
* panic the kernel when you do the ioctl.
291+
*/
292+
do_fuzz(zfs_fd, ioc, &zc);
293+
294+
/*
295+
* Corrupt the last string in the packed nvlist so it has no
296+
* NULL terminator.
297+
*/
298+
do_bad_string(&zc, innvl);
299+
300+
}
301+
163302
if (lzc_ioctl_fd(zfs_fd, ioc, &zc) != 0)
164303
error = errno;
165304

166-
if (error != expected) {
305+
/*
306+
* If we're corrupting the nvlist we don't care about the specific
307+
* error code that gets returned, as it could be one of many. We only
308+
* care if it panics the kernel.
309+
*/
310+
if (!do_corrupt && error != expected) {
167311
unexpected_failures = B_TRUE;
168312
(void) fprintf(stderr, "%s: Unexpected result with %s, "
169313
"error %d (expecting %d)\n",
@@ -174,6 +318,12 @@ lzc_ioctl_run(zfs_ioc_t ioc, const char *name, nvlist_t *innvl, int expected)
174318
free((void *)(uintptr_t)zc.zc_nvlist_dst);
175319
}
176320

321+
static void
322+
lzc_ioctl_run(zfs_ioc_t ioc, const char *name, nvlist_t *innvl, int expected)
323+
{
324+
return (lzc_ioctl_run_impl(ioc, name, innvl, expected, B_FALSE));
325+
}
326+
177327
/*
178328
* Test each ioc for the following ioctl input errors:
179329
* ZFS_ERR_IOC_ARG_UNAVAIL an input argument is not supported by kernel
@@ -310,6 +460,7 @@ test_log_history(const char *pool)
310460
fnvlist_add_string(required, "message", "input check");
311461

312462
IOC_INPUT_TEST(ZFS_IOC_LOG_HISTORY, pool, required, NULL, 0);
463+
IOC_INPUT_TEST_INJECT(ZFS_IOC_LOG_HISTORY, pool, required);
313464

314465
nvlist_free(required);
315466
}
@@ -791,6 +942,20 @@ test_set_bootenv(const char *pool)
791942
nvlist_free(required);
792943
}
793944

945+
static void
946+
test_zpool_get(const char *pool)
947+
{
948+
const char *strs[] = {ZPOOL_DEDUPCACHED_PROP_NAME};
949+
nvlist_t *optional = fnvlist_alloc();
950+
951+
fnvlist_add_string_array(optional, ZPOOL_GET_PROPS_NAMES, strs, 1);
952+
953+
IOC_INPUT_TEST(ZFS_IOC_POOL_GET_PROPS, pool, NULL, optional, 0);
954+
IOC_INPUT_TEST_INJECT(ZFS_IOC_POOL_GET_PROPS, pool, optional);
955+
956+
nvlist_free(optional);
957+
}
958+
794959
static void
795960
zfs_ioc_input_tests(const char *pool)
796961
{
@@ -885,6 +1050,7 @@ zfs_ioc_input_tests(const char *pool)
8851050

8861051
test_scrub(pool);
8871052

1053+
test_zpool_get(pool);
8881054
/*
8891055
* cleanup
8901056
*/

0 commit comments

Comments
 (0)