Use the word-at-a-time character finder to skip haystack regions which cannot begin a match. For case-insensitive searches, check both ASCII case variants of the first needle byte.
At each candidate, use a direct fault-safe word matcher. When both strings have the same relative alignment, skip equal words and inspect locally loaded bytes only for mismatches and case folding. Keep needle-first access, fall back at the same byte after a word-load fault, and retain the extra needle-byte check at the search limit so fault and boundary behavior remain unchanged. Assisted-by: Codex:gpt-5.6-sol Signed-off-by: Leon Hwang <[email protected]> --- kernel/bpf/helpers.c | 142 +++++++++++++++++++++++++++++++++---------- 1 file changed, 111 insertions(+), 31 deletions(-) diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c index af29acc90245..93d5fa409cd7 100644 --- a/kernel/bpf/helpers.c +++ b/kernel/bpf/helpers.c @@ -4281,11 +4281,90 @@ __bpf_kfunc int bpf_strcspn(const char *s__ign, const char *reject__ign) return __bpf_strspn(s__ign, reject__ign, true); } -static int __bpf_strnstr(const char *s1, const char *s2, size_t len, - bool ignore_case) +#define bpf_str_match_byte(byte1, byte2, ignore_case) \ +do { \ + char __c1 = (byte1); \ + char __c2 = (byte2); \ + \ + if (ignore_case) { \ + __c1 = tolower(__c1); \ + __c2 = tolower(__c2); \ + } \ + if (__c1 == '\0') \ + return -ENOENT; \ + if (__c1 != __c2) \ + return 0; \ +} while (0) + +static __always_inline int +bpf_str_match_at(const char *s1, const char *s2, size_t limit, bool ignore_case) { - char c1, c2; - int i, j; + const struct word_at_a_time constants = WORD_AT_A_TIME_CONSTANTS; + unsigned char byte1, byte2, *bytes1, *bytes2; + unsigned long word1, word2, data; + size_t pos = 0, word_end, i; + + if (IS_ENABLED(CONFIG_KMSAN) || + !IS_ALIGNED((unsigned long)s1 ^ (unsigned long)s2, sizeof(word1))) + goto byte_at_a_time; + + while (pos < limit && !IS_ALIGNED((unsigned long)(s1 + pos), sizeof(word1))) { + __get_kernel_nofault(&byte2, s2 + pos, unsigned char, err_out); + if (byte2 == '\0') + return 1; + __get_kernel_nofault(&byte1, s1 + pos, unsigned char, err_out); + bpf_str_match_byte(byte1, byte2, ignore_case); + pos++; + } + + word_end = pos + round_down(limit - pos, sizeof(word1)); + while (pos < word_end) { + __get_kernel_nofault(&word2, s2 + pos, unsigned long, byte_at_a_time); + if (has_zero(word2, &data, &constants)) + goto byte_at_a_time; + __get_kernel_nofault(&word1, s1 + pos, unsigned long, byte_at_a_time); + if (word1 == word2) { + pos += sizeof(word1); + continue; + } + + bytes1 = (unsigned char *)&word1; + bytes2 = (unsigned char *)&word2; + for (i = 0; i < sizeof(word1); i++) + bpf_str_match_byte(bytes1[i], bytes2[i], ignore_case); + pos += sizeof(word1); + } + +byte_at_a_time: + for (; pos < limit; pos++) { + __get_kernel_nofault(&byte2, s2 + pos, unsigned char, err_out); + if (byte2 == '\0') + return 1; + __get_kernel_nofault(&byte1, s1 + pos, unsigned char, err_out); + bpf_str_match_byte(byte1, byte2, ignore_case); + } + + if (limit == XATTR_SIZE_MAX) + return -E2BIG; + + /* + * Read one extra byte from s2 to cover the case when it is a suffix + * of the first limit bytes of s1. + */ + __get_kernel_nofault(&byte2, s2 + limit, unsigned char, err_out); + return byte2 == '\0' ? 1 : -ENOENT; + +err_out: + return -EFAULT; +} + +#undef bpf_str_match_byte + +static int __bpf_strnstr(const char *s1, const char *s2, size_t len, bool ignore_case) +{ + unsigned char first, candidate; + size_t pos = 0, limit; + int ret, offset; if (!copy_from_kernel_nofault_allowed(s1, 1) || !copy_from_kernel_nofault_allowed(s2, 1)) { @@ -4293,37 +4372,38 @@ static int __bpf_strnstr(const char *s1, const char *s2, size_t len, } guard(pagefault)(); - for (i = 0; i < XATTR_SIZE_MAX; i++) { - for (j = 0; i + j <= len && j < XATTR_SIZE_MAX; j++) { - __get_kernel_nofault(&c2, s2 + j, char, err_out); - if (c2 == '\0') - return i; - /* - * We allow reading an extra byte from s2 (note the - * `i + j <= len` above) to cover the case when s2 is - * a suffix of the first len chars of s1. - */ - if (i + j == len) - break; - __get_kernel_nofault(&c1, s1 + j, char, err_out); - - if (ignore_case) { - c1 = tolower(c1); - c2 = tolower(c2); - } + __get_kernel_nofault(&first, s2, unsigned char, err_out); + if (first == '\0') + return 0; - if (c1 == '\0') - return -ENOENT; - if (c1 != c2) - break; + while (pos < XATTR_SIZE_MAX && pos < len) { + limit = min_t(size_t, len - pos, XATTR_SIZE_MAX - pos); + offset = bpf_str_find(s1 + pos, limit, first, ignore_case, true); + if (offset < 0) { + if (offset == -ENOENT && limit == XATTR_SIZE_MAX - pos) + return -E2BIG; + return offset; } - if (j == XATTR_SIZE_MAX) - return -E2BIG; - if (i + j == len) + pos += offset; + + /* + * bpf_str_find() also returns the position of a terminating + * NUL. Distinguish it from a first-character match. + */ + __get_kernel_nofault(&candidate, s1 + pos, unsigned char, err_out); + if (candidate == '\0') return -ENOENT; - s1++; + + limit = min_t(size_t, len - pos, XATTR_SIZE_MAX); + ret = bpf_str_match_at(s1 + pos, s2, limit, ignore_case); + if (ret > 0) + return pos; + if (ret < 0) + return ret; + pos++; } - return -E2BIG; + return pos == XATTR_SIZE_MAX ? -E2BIG : -ENOENT; + err_out: return -EFAULT; } -- 2.55.0

