udev-util: bound leading whitespace skip in udev_replace_whitespace

The function documents that at most 'len' bytes are read from 'str',
but the leading whitespace skip used strspn(), which is bounded only
by a non-whitespace byte or a NUL. ata_id passes the space padded,
non-NUL-terminated ATA IDENTIFY fields, so an all-blank model reads
past the buffer. Use strnspn() to cap the skip to 'len'.
This commit is contained in:
Syed Mohammed Nayyar
2026-06-29 11:32:14 +05:30
parent 0af6caff42
commit 6b2cb3abeb
2 changed files with 42 additions and 1 deletions

View File

@@ -315,7 +315,9 @@ size_t udev_replace_whitespace(const char *str, char *to, size_t len) {
*
* Note that only 'len' characters are read from 'str'. */
i = strspn(str, WHITESPACE);
/* Skip leading whitespace, but read at most 'len' bytes: 'str' is not necessarily NUL
* terminated within 'len' (e.g. the space padded ATA IDENTIFY fields ata_id passes in). */
i = strnspn(str, WHITESPACE, len);
for (j = 0; j < len && i < len && str[i] != '\0'; i++) {
if (isspace(str[i])) {

View File

@@ -57,4 +57,43 @@ TEST(udev_replace_whitespace) {
test_udev_replace_whitespace_one_len(" hoge hoge ", 0, "");
}
static void test_udev_replace_whitespace_not_terminated_one(const char *str, size_t len, const char *expected) {
_cleanup_free_ char *buf = NULL, *result = NULL;
int r;
/* Copy exactly 'len' bytes with no trailing NUL, so any read past 'len' is caught by the
* sanitizers. */
assert_se(buf = memdup(str, len));
assert_se(result = new(char, len + 1));
r = udev_replace_whitespace(buf, result, len);
assert_se((size_t) r == strlen(expected));
ASSERT_STREQ(result, expected);
}
TEST(udev_replace_whitespace_not_nul_terminated) {
/* 'str' is not NUL terminated within 'len' and consists entirely of whitespace, like a
* space padded, non-terminated ATA IDENTIFY model/serial field. Only 'len' bytes may be
* read from it; otherwise this reads off the end of the buffer (caught by the sanitizers). */
for (size_t len = 1; len <= 64; len++) {
_cleanup_free_ char *str = NULL, *result = NULL;
assert_se(str = new(char, len));
memset(str, ' ', len);
assert_se(result = new(char, len + 1));
assert_se(udev_replace_whitespace(str, result, len) == 0);
ASSERT_STREQ(result, "");
}
/* Leading whitespace followed by content, also not NUL terminated within 'len': exercises
* the handoff from the bounded leading skip into the copy loop on a non-terminated buffer. */
test_udev_replace_whitespace_not_terminated_one(" hoge", 7, "hoge");
test_udev_replace_whitespace_not_terminated_one(" hoge hoge", 13, "hoge_hoge");
test_udev_replace_whitespace_not_terminated_one(" hoge hoge ", 16, "hoge_hoge");
test_udev_replace_whitespace_not_terminated_one(" hoge hoge ", 7, "hog");
test_udev_replace_whitespace_not_terminated_one(" x", 2, "x");
}
DEFINE_TEST_MAIN(LOG_INFO);