udev-util: bound leading whitespace skip in udev_replace_whitespace (#42757)

udev_replace_whitespace() is documented to read at most 'len' bytes from
'str':
- the strspn() skip of leading whitespace stops at a non-space byte or
NUL, not at 'len'
- ata_id passes the space padded, non-NUL-terminated ATA IDENTIFY
model/serial/fw fields
- an all-blank field reads off the end of the 512-byte hd_driveid stack
struct
Capped the skip to 'len'; existing outputs are unchanged. Added a
regression test.
This commit is contained in:
Yu Watanabe
2026-07-17 01:16:07 +09:00
committed by GitHub
5 changed files with 80 additions and 1 deletions
+17
View File
@@ -1428,6 +1428,23 @@ size_t strspn_from_end(const char *str, const char *accept) {
return n;
}
size_t strnspn(const char *str, const char *accept, size_t n) {
size_t i;
/* Like strspn(), but reads at most 'n' bytes from 'str'. Returns the length of the initial
* run of 'str' (capped at 'n') that consists solely of bytes found in 'accept'. Stops at a
* NUL byte too. Unlike strspn() this is safe on a buffer that is not NUL terminated within
* 'n' bytes. */
assert(str || n == 0);
assert(accept);
for (i = 0; i < n && str[i] != '\0' && strchr(accept, str[i]); i++)
;
return i;
}
char* strdupspn(const char *a, const char *accept) {
if (isempty(a) || isempty(accept))
return strdup("");
+1
View File
@@ -286,6 +286,7 @@ typedef enum MakeCStringMode {
int make_cstring(const void *s, size_t n, MakeCStringMode mode, char **ret);
size_t strspn_from_end(const char *str, const char *accept) _pure_;
size_t strnspn(const char *str, const char *accept, size_t n) _pure_ _nonnull_if_nonzero_(1, 3);
char* strdupspn(const char *a, const char *accept);
char* strdupcspn(const char *a, const char *reject);
+3 -1
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])) {
+20
View File
@@ -1288,6 +1288,26 @@ TEST(strspn_from_end) {
assert_se(strspn_from_end("aaa12aa34", DIGITS) == 2);
}
TEST(strnspn) {
assert_se(strnspn(NULL, WHITESPACE, 0) == 0);
assert_se(strnspn("", WHITESPACE, 0) == 0);
assert_se(strnspn("hoge", WHITESPACE, 0) == 0);
assert_se(strnspn("hoge", WHITESPACE, 4) == 0);
assert_se(strnspn(" hoge", WHITESPACE, 7) == 3);
assert_se(strnspn(" hoge", WHITESPACE, 2) == 2);
assert_se(strnspn(" ", WHITESPACE, 4) == 4);
/* 'str' is all whitespace and not NUL terminated within 'n': at most 'n' bytes may be read,
* otherwise this reads off the end of the buffer (caught by the sanitizers). */
for (size_t n = 1; n <= 64; n++) {
_cleanup_free_ char *str = NULL;
assert_se(str = new(char, n));
memset(str, ' ', n);
assert_se(strnspn(str, WHITESPACE, n) == n);
}
}
TEST(streq_skip_trailing_chars) {
/* NULL is WHITESPACE by default */
assert_se(streq_skip_trailing_chars("foo bar", "foo bar", NULL));
+39
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);