* [PATCH bpf-next v7 0/4] bpf: Add kfuncs for read-only string operations
@ 2025-06-23 13:47 Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 1/4] uaccess: Define pagefault lock guard Viktor Malik
` (3 more replies)
0 siblings, 4 replies; 9+ messages in thread
From: Viktor Malik @ 2025-06-23 13:47 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
Viktor Malik
String operations are commonly used in programming and BPF programs are
no exception. Since it is cumbersome to reimplement them over and over,
this series introduce kfuncs which provide the most common operations.
For now, we only limit ourselves to functions which do not copy memory
since these usually introduce undefined behaviour in case the
source/destination buffers overlap which would have to be prevented by
the verifier.
The kernel already contains implementations for all of these, however,
it is not possible to use them from BPF context. The main reason is that
the verifier is not able to check that it is safe to access the entire
string and that the string is null-terminated and the function won't
loop forever. Therefore, the operations are open-coded using
__get_kernel_nofault instead of plain dereference and bounded to at most
XATTR_SIZE_MAX characters to make them safe. That allows to skip all the
verfier checks for the passed-in strings as safety is ensured
dynamically.
All of the proposed functions return integers, even those that normally
(in the kernel or libc) return pointers into the strings. The reason is
that since the strings are generally treated as unsafe, the pointers
couldn't be dereferenced anyways. So, instead, we return an index to the
string and let user decide what to do with it. The integer APIs also
allow to return various error codes when unexpected situations happen
while processing the strings.
The series include both positive and negative tests using the kfuncs.
Changelog
---------
Changes in v7:
- Disable negative tests passing NULL and 0x1 to kfuncs on s390 as they
aren't relevant (see comment in string_kfuncs_failure1.c for details).
Changes in v6:
- Improve the third patch which allows to use macros in __retval in
selftests. The previous solution broke several tests.
Changes in v5:
- Make all kfuncs return integers (Andrii).
- Return -ERANGE when passing non-kernel pointers on arches with
non-overlapping address spaces (Alexei).
- Implement "unbounded" variants using the bounded ones (Andrii).
- Add more negative test cases.
Changes in v4 (all suggested by Andrii):
- Open-code all the kfuncs, not just the unbounded variants.
- Introduce `pagefault` lock guard to simplify the implementation
- Return appropriate error codes (-E2BIG and -EFAULT) on failures
- Const-ify all arguments and return values
- Add negative test-cases
Changes in v3:
- Open-code unbounded variants with __get_kernel_nofault instead of
dereference (suggested by Alexei).
- Use the __sz suffix for size parameters in bounded variants (suggested
by Eduard and Alexei).
- Make tests more compact (suggested by Eduard).
- Add benchmark.
Viktor Malik (4):
uaccess: Define pagefault lock guard
bpf: Add kfuncs for read-only string operations
selftests/bpf: Allow macros in __retval
selftests/bpf: Add tests for string kfuncs
include/linux/uaccess.h | 2 +
kernel/bpf/helpers.c | 389 ++++++++++++++++++
.../selftests/bpf/prog_tests/string_kfuncs.c | 63 +++
tools/testing/selftests/bpf/progs/bpf_misc.h | 14 +-
.../bpf/progs/string_kfuncs_failure1.c | 85 ++++
.../bpf/progs/string_kfuncs_failure2.c | 21 +
.../bpf/progs/string_kfuncs_success.c | 35 ++
.../bpf/progs/verifier_div_overflow.c | 4 +-
tools/testing/selftests/bpf/test_loader.c | 23 +-
9 files changed, 613 insertions(+), 23 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/string_kfuncs.c
create mode 100644 tools/testing/selftests/bpf/progs/string_kfuncs_failure1.c
create mode 100644 tools/testing/selftests/bpf/progs/string_kfuncs_failure2.c
create mode 100644 tools/testing/selftests/bpf/progs/string_kfuncs_success.c
--
2.49.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH bpf-next v7 1/4] uaccess: Define pagefault lock guard
2025-06-23 13:47 [PATCH bpf-next v7 0/4] bpf: Add kfuncs for read-only string operations Viktor Malik
@ 2025-06-23 13:48 ` Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations Viktor Malik
` (2 subsequent siblings)
3 siblings, 0 replies; 9+ messages in thread
From: Viktor Malik @ 2025-06-23 13:48 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
linux-kernel, Viktor Malik
Define a pagefault lock guard which allows to simplify functions that
need to disable page faults.
Signed-off-by: Viktor Malik <vmalik@redhat.com>
---
include/linux/uaccess.h | 2 ++
1 file changed, 2 insertions(+)
diff --git a/include/linux/uaccess.h b/include/linux/uaccess.h
index 7c06f4795670..1beb5b395d81 100644
--- a/include/linux/uaccess.h
+++ b/include/linux/uaccess.h
@@ -296,6 +296,8 @@ static inline bool pagefault_disabled(void)
*/
#define faulthandler_disabled() (pagefault_disabled() || in_atomic())
+DEFINE_LOCK_GUARD_0(pagefault, pagefault_disable(), pagefault_enable())
+
#ifndef CONFIG_ARCH_HAS_SUBPAGE_FAULTS
/**
--
2.49.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations
2025-06-23 13:47 [PATCH bpf-next v7 0/4] bpf: Add kfuncs for read-only string operations Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 1/4] uaccess: Define pagefault lock guard Viktor Malik
@ 2025-06-23 13:48 ` Viktor Malik
2025-06-23 16:42 ` Alexei Starovoitov
2025-06-23 21:29 ` Andrii Nakryiko
2025-06-23 13:48 ` [PATCH bpf-next v7 3/4] selftests/bpf: Allow macros in __retval Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 4/4] selftests/bpf: Add tests for string kfuncs Viktor Malik
3 siblings, 2 replies; 9+ messages in thread
From: Viktor Malik @ 2025-06-23 13:48 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
Viktor Malik
String operations are commonly used so this exposes the most common ones
to BPF programs. For now, we limit ourselves to operations which do not
copy memory around.
Unfortunately, most in-kernel implementations assume that strings are
%NUL-terminated, which is not necessarily true, and therefore we cannot
use them directly in the BPF context. Instead, we open-code them using
__get_kernel_nofault instead of plain dereference to make them safe and
limit the strings length to XATTR_SIZE_MAX to make sure the functions
terminate. When __get_kernel_nofault fails, functions return -EFAULT.
Similarly, when the size bound is reached, the functions return -E2BIG.
In addition, we return -ERANGE when the passed strings are outside of
the kernel address space.
Note that thanks to these dynamic safety checks, no other constraints
are put on the kfunc args (they are marked with the "__ign" suffix to
skip any verifier checks for them).
All of the functions return integers, including functions which normally
(in kernel or libc) return pointers to the strings. The reason is that
since the strings are generally treated as unsafe, the pointers couldn't
be dereferenced anyways. So, instead, we return an index to the string
and let user decide what to do with it. This also nicely fits with
returning various error codes when necessary (see above).
Suggested-by: Alexei Starovoitov <ast@kernel.org>
Signed-off-by: Viktor Malik <vmalik@redhat.com>
---
kernel/bpf/helpers.c | 389 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 389 insertions(+)
diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
index b71e428ad936..d32175dc89bd 100644
--- a/kernel/bpf/helpers.c
+++ b/kernel/bpf/helpers.c
@@ -24,6 +24,7 @@
#include <linux/bpf_mem_alloc.h>
#include <linux/kasan.h>
#include <linux/bpf_verifier.h>
+#include <linux/uaccess.h>
#include "../../lib/kstrtox.h"
@@ -3278,6 +3279,383 @@ __bpf_kfunc void __bpf_trap(void)
{
}
+/* Kfuncs for string operations.
+ *
+ * Since strings are not necessarily %NUL-terminated, we cannot directly call
+ * in-kernel implementations. Instead, we open-code the implementations using
+ * __get_kernel_nofault instead of plain dereference to make them safe.
+ */
+
+/**
+ * bpf_strcmp - Compare two strings
+ * @s1__ign: One string
+ * @s2__ign: Another string
+ *
+ * Return:
+ * * %0 - Strings are equal
+ * * %-1 - @s1__ign is smaller
+ * * %1 - @s2__ign is smaller
+ * * %-EFAULT - Cannot read one of the strings
+ * * %-E2BIG - One of strings is too large
+ * * %-ERANGE - One of strings is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strcmp(const char *s1__ign, const char *s2__ign)
+{
+ char c1, c2;
+ int i;
+
+ if (!copy_from_kernel_nofault_allowed(s1__ign, 1) ||
+ !copy_from_kernel_nofault_allowed(s2__ign, 1)) {
+ return -ERANGE;
+ }
+
+ guard(pagefault)();
+ for (i = 0; i < XATTR_SIZE_MAX; i++) {
+ __get_kernel_nofault(&c1, s1__ign, char, err_out);
+ __get_kernel_nofault(&c2, s2__ign, char, err_out);
+ if (c1 != c2)
+ return c1 < c2 ? -1 : 1;
+ if (c1 == '\0')
+ return 0;
+ s1__ign++;
+ s2__ign++;
+ }
+ return -E2BIG;
+err_out:
+ return -EFAULT;
+}
+
+/**
+ * bpf_strnchr - Find a character in a length limited string
+ * @s__ign: The string to be searched
+ * @count: The number of characters to be searched
+ * @c: The character to search for
+ *
+ * Note that the %NUL-terminator is considered part of the string, and can
+ * be searched for.
+ *
+ * Return:
+ * * >=0 - Index of the first occurrence of @c within @s__ign
+ * * %-1 - @c not found in the first @count characters of @s__ign
+ * * %-EFAULT - Cannot read @s__ign
+ * * %-E2BIG - @s__ign is too large
+ * * %-ERANGE - @s__ign is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strnchr(const char *s__ign, size_t count, char c)
+{
+ char sc;
+ int i;
+
+ if (!copy_from_kernel_nofault_allowed(s__ign, 1))
+ return -ERANGE;
+
+ guard(pagefault)();
+ for (i = 0; i < count && i < XATTR_SIZE_MAX; i++) {
+ __get_kernel_nofault(&sc, s__ign, char, err_out);
+ if (sc == c)
+ return i;
+ if (sc == '\0')
+ return -1;
+ s__ign++;
+ }
+ return i == XATTR_SIZE_MAX ? -E2BIG : -1;
+err_out:
+ return -EFAULT;
+}
+
+/**
+ * bpf_strchr - Find the first occurrence of a character in a string
+ * @s__ign: The string to be searched
+ * @c: The character to search for
+ *
+ * Note that the %NUL-terminator is considered part of the string, and can
+ * be searched for.
+ *
+ * Return:
+ * * >=0 - The index of the first occurrence of @c within @s__ign
+ * * -1 - @c not found in @s__ign
+ * * %-EFAULT - Cannot read @s__ign
+ * * %-E2BIG - @s__ign is too large
+ * * %-ERANGE - @s__ign is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strchr(const char *s__ign, char c)
+{
+ return bpf_strnchr(s__ign, XATTR_SIZE_MAX, c);
+}
+
+/**
+ * bpf_strchrnul - Find and return a character in a string, or end of string
+ * @s__ign: The string to be searched
+ * @c: The character to search for
+ *
+ * Return:
+ * * >=0 - Index of the first occurrence of @c within @s__ign or index of
+ * the null byte at the end of @s__ign when @c is not found
+ * * %-EFAULT - Cannot read @s__ign
+ * * %-E2BIG - @s__ign is too large
+ * * %-ERANGE - @s__ign is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strchrnul(const char *s__ign, char c)
+{
+ char sc;
+ int i;
+
+ if (!copy_from_kernel_nofault_allowed(s__ign, 1))
+ return -ERANGE;
+
+ guard(pagefault)();
+ for (i = 0; i < XATTR_SIZE_MAX; i++) {
+ __get_kernel_nofault(&sc, s__ign, char, err_out);
+ if (sc == '\0' || sc == c)
+ return i;
+ s__ign++;
+ }
+ return -E2BIG;
+err_out:
+ return -EFAULT;
+}
+
+/**
+ * bpf_strrchr - Find the last occurrence of a character in a string
+ * @s__ign: The string to be searched
+ * @c: The character to search for
+ *
+ * Return:
+ * * >=0 - Index of the last occurrence of @c within @s__ign
+ * * %-1 - @c not found in @s__ign
+ * * %-EFAULT - Cannot read @s__ign
+ * * %-E2BIG - @s__ign is too large
+ * * %-ERANGE - @s__ign is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strrchr(const char *s__ign, int c)
+{
+ char sc;
+ int i, last = -1;
+
+ if (!copy_from_kernel_nofault_allowed(s__ign, 1))
+ return -ERANGE;
+
+ guard(pagefault)();
+ for (i = 0; i < XATTR_SIZE_MAX; i++) {
+ __get_kernel_nofault(&sc, s__ign, char, err_out);
+ if (sc == '\0')
+ return last;
+ if (sc == c)
+ last = i;
+ s__ign++;
+ }
+ return -E2BIG;
+err_out:
+ return -EFAULT;
+}
+
+/**
+ * bpf_strlen - Calculate the length of a length-limited string
+ * @s__ign: The string
+ * @count: The maximum number of characters to count
+ *
+ * Return:
+ * * >=0 - The length of @s__ign
+ * * %-EFAULT - Cannot read @s__ign
+ * * %-E2BIG - @s__ign is too large
+ * * %-ERANGE - @s__ign is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strnlen(const char *s__ign, size_t count)
+{
+ char c;
+ int i;
+
+ if (!copy_from_kernel_nofault_allowed(s__ign, 1))
+ return -ERANGE;
+
+ guard(pagefault)();
+ for (i = 0; i < count && i < XATTR_SIZE_MAX; i++) {
+ __get_kernel_nofault(&c, s__ign, char, err_out);
+ if (c == '\0')
+ return i;
+ s__ign++;
+ }
+ return i == XATTR_SIZE_MAX ? -E2BIG : i;
+err_out:
+ return -EFAULT;
+}
+
+/**
+ * bpf_strlen - Calculate the length of a string
+ * @s__ign: The string
+ *
+ * Return:
+ * * >=0 - The length of @s__ign
+ * * %-EFAULT - Cannot read @s__ign
+ * * %-E2BIG - @s__ign is too large
+ * * %-ERANGE - @s__ign is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strlen(const char *s__ign)
+{
+ return bpf_strnlen(s__ign, XATTR_SIZE_MAX);
+}
+
+/**
+ * bpf_strspn - Calculate the length of the initial substring of @s__ign which
+ * only contains letters in @accept__ign
+ * @s__ign: The string to be searched
+ * @accept__ign: The string to search for
+ *
+ * Return:
+ * * >=0 - The length of the initial substring of @s__ign which only
+ * contains letters from @accept__ign
+ * * %-EFAULT - Cannot read one of the strings
+ * * %-E2BIG - One of the strings is too large
+ * * %-ERANGE - One of the strings is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strspn(const char *s__ign, const char *accept__ign)
+{
+ char cs, ca;
+ bool found;
+ int i, j;
+
+ if (!copy_from_kernel_nofault_allowed(s__ign, 1) ||
+ !copy_from_kernel_nofault_allowed(accept__ign, 1)) {
+ return -ERANGE;
+ }
+
+ guard(pagefault)();
+ for (i = 0; i < XATTR_SIZE_MAX; i++) {
+ __get_kernel_nofault(&cs, s__ign, char, err_out);
+ if (cs == '\0')
+ return i;
+ found = false;
+ for (j = 0; j < XATTR_SIZE_MAX; j++) {
+ __get_kernel_nofault(&ca, accept__ign + j, char, err_out);
+ if (cs == ca) {
+ found = true;
+ break;
+ }
+ if (ca == '\0')
+ break;
+ }
+ if (!found)
+ return i;
+ s__ign++;
+ }
+ return -E2BIG;
+err_out:
+ return -EFAULT;
+}
+
+/**
+ * strcspn - Calculate the length of the initial substring of @s__ign which
+ * does not contain letters in @reject__ign
+ * @s__ign: The string to be searched
+ * @reject__ign: The string to search for
+ *
+ * Return:
+ * * >=0 - The length of the initial substring of @s__ign which does not
+ * contain letters from @reject__ign
+ * * %-EFAULT - Cannot read one of the strings
+ * * %-E2BIG - One of the strings is too large
+ * * %-ERANGE - One of the strings is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strcspn(const char *s__ign, const char *reject__ign)
+{
+ char cs, cr;
+ bool found;
+ int i, j;
+
+ if (!copy_from_kernel_nofault_allowed(s__ign, 1) ||
+ !copy_from_kernel_nofault_allowed(reject__ign, 1)) {
+ return -ERANGE;
+ }
+
+ guard(pagefault)();
+ for (i = 0; i < XATTR_SIZE_MAX; i++) {
+ __get_kernel_nofault(&cs, s__ign, char, err_out);
+ if (cs == '\0')
+ return i;
+ found = false;
+ for (j = 0; j < XATTR_SIZE_MAX; j++) {
+ __get_kernel_nofault(&cr, reject__ign + j, char, err_out);
+ if (cs == cr) {
+ found = true;
+ break;
+ }
+ if (cr == '\0')
+ break;
+ }
+ if (found)
+ return i;
+ s__ign++;
+ }
+ return -E2BIG;
+err_out:
+ return -EFAULT;
+}
+
+/**
+ * bpf_strnstr - Find the first substring in a length-limited string
+ * @s1__ign: The string to be searched
+ * @s2__ign: The string to search for
+ * @len: the maximum number of characters to search
+ *
+ * Return:
+ * * >=0 - Index of the first character of the first occurrence of @s2__ign
+ * within the first @len characters of @s1__ign
+ * * %-1 - @s2__ign not found in the first @len characters of @s1__ign
+ * * %-EFAULT - Cannot read one of the strings
+ * * %-E2BIG - One of the strings is too large
+ * * %-ERANGE - One of the strings is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strnstr(const char *s1__ign, const char *s2__ign, size_t len)
+{
+ char c1, c2;
+ int i, j;
+
+ if (!copy_from_kernel_nofault_allowed(s1__ign, 1) ||
+ !copy_from_kernel_nofault_allowed(s2__ign, 1)) {
+ return -ERANGE;
+ }
+
+ guard(pagefault)();
+ for (i = 0; i < XATTR_SIZE_MAX; i++) {
+ for (j = 0; i + j < len && j < XATTR_SIZE_MAX; j++) {
+ __get_kernel_nofault(&c1, s1__ign + j, char, err_out);
+ __get_kernel_nofault(&c2, s2__ign + j, char, err_out);
+ if (c2 == '\0')
+ return i;
+ if (c1 == '\0')
+ return -1;
+ if (c1 != c2)
+ break;
+ }
+ if (j == XATTR_SIZE_MAX)
+ return -E2BIG;
+ if (i + j == len)
+ return -1;
+ s1__ign++;
+ }
+ return -E2BIG;
+err_out:
+ return -EFAULT;
+}
+
+/**
+ * bpf_strstr - Find the first substring in a string
+ * @s1__ign: The string to be searched
+ * @s2__ign: The string to search for
+ *
+ * Return:
+ * * >=0 - Index of the first character of the first occurrence of @s2__ign
+ * within @s1__ign
+ * * %-1 - @s2__ign is not a substring of @s1__ign
+ * * %-EFAULT - Cannot read one of the strings
+ * * %-E2BIG - One of the strings is too large
+ * * %-ERANGE - One of the strings is outside of kernel address space
+ */
+__bpf_kfunc int bpf_strstr(const char *s1__ign, const char *s2__ign)
+{
+ return bpf_strnstr(s1__ign, s2__ign, XATTR_SIZE_MAX);
+}
+
__bpf_kfunc_end_defs();
BTF_KFUNCS_START(generic_btf_ids)
@@ -3397,6 +3775,17 @@ BTF_ID_FLAGS(func, bpf_iter_dmabuf_next, KF_ITER_NEXT | KF_RET_NULL | KF_SLEEPAB
BTF_ID_FLAGS(func, bpf_iter_dmabuf_destroy, KF_ITER_DESTROY | KF_SLEEPABLE)
#endif
BTF_ID_FLAGS(func, __bpf_trap)
+BTF_ID_FLAGS(func, bpf_strcmp);
+BTF_ID_FLAGS(func, bpf_strchr);
+BTF_ID_FLAGS(func, bpf_strchrnul);
+BTF_ID_FLAGS(func, bpf_strnchr);
+BTF_ID_FLAGS(func, bpf_strrchr);
+BTF_ID_FLAGS(func, bpf_strlen);
+BTF_ID_FLAGS(func, bpf_strnlen);
+BTF_ID_FLAGS(func, bpf_strspn);
+BTF_ID_FLAGS(func, bpf_strcspn);
+BTF_ID_FLAGS(func, bpf_strstr);
+BTF_ID_FLAGS(func, bpf_strnstr);
BTF_KFUNCS_END(common_btf_ids)
static const struct btf_kfunc_id_set common_kfunc_set = {
--
2.49.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH bpf-next v7 3/4] selftests/bpf: Allow macros in __retval
2025-06-23 13:47 [PATCH bpf-next v7 0/4] bpf: Add kfuncs for read-only string operations Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 1/4] uaccess: Define pagefault lock guard Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations Viktor Malik
@ 2025-06-23 13:48 ` Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 4/4] selftests/bpf: Add tests for string kfuncs Viktor Malik
3 siblings, 0 replies; 9+ messages in thread
From: Viktor Malik @ 2025-06-23 13:48 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
Viktor Malik
Allow macro expansion for values passed to the `__retval` and
`__retval_unpriv` attributes. This is especially useful for testing
programs which return various error codes.
With this change, the code for parsing special literals can be made
simpler, as the literals are defined via macros. The only exception is
INT_MIN which expands to (-INT_MAX -1), which is not single number and
cannot be parsed by strtol. So, we instead use a prefixed literal
_INT_MIN in __retval and handle it separately (assign the expected
return to INT_MIN). Also, strtol cannot handle the "ll" suffix so change
the value of POINTER_VALUE from 0xcafe4all to 0xbadcafe.
Signed-off-by: Viktor Malik <vmalik@redhat.com>
---
tools/testing/selftests/bpf/progs/bpf_misc.h | 14 ++++++-----
.../bpf/progs/verifier_div_overflow.c | 4 ++--
tools/testing/selftests/bpf/test_loader.c | 23 +++++++------------
3 files changed, 18 insertions(+), 23 deletions(-)
diff --git a/tools/testing/selftests/bpf/progs/bpf_misc.h b/tools/testing/selftests/bpf/progs/bpf_misc.h
index a678463e972c..20dce508d8e0 100644
--- a/tools/testing/selftests/bpf/progs/bpf_misc.h
+++ b/tools/testing/selftests/bpf/progs/bpf_misc.h
@@ -83,9 +83,11 @@
* expect return value to match passed parameter:
* - a decimal number
* - a hexadecimal number, when starts from 0x
- * - literal INT_MIN
- * - literal POINTER_VALUE (see definition below)
- * - literal TEST_DATA_LEN (see definition below)
+ * - a macro which expands to one of the above
+ * - literal _INT_MIN (expands to INT_MIN)
+ * In addition, two special macros are defined below:
+ * - POINTER_VALUE
+ * - TEST_DATA_LEN
* __retval_unpriv Same, but load program in unprivileged mode.
*
* __description Text to be used instead of a program name for display
@@ -125,8 +127,8 @@
#define __success_unpriv __attribute__((btf_decl_tag("comment:test_expect_success_unpriv")))
#define __log_level(lvl) __attribute__((btf_decl_tag("comment:test_log_level="#lvl)))
#define __flag(flag) __attribute__((btf_decl_tag("comment:test_prog_flags="#flag)))
-#define __retval(val) __attribute__((btf_decl_tag("comment:test_retval="#val)))
-#define __retval_unpriv(val) __attribute__((btf_decl_tag("comment:test_retval_unpriv="#val)))
+#define __retval(val) __attribute__((btf_decl_tag("comment:test_retval="XSTR(val))))
+#define __retval_unpriv(val) __attribute__((btf_decl_tag("comment:test_retval_unpriv="XSTR(val))))
#define __auxiliary __attribute__((btf_decl_tag("comment:test_auxiliary")))
#define __auxiliary_unpriv __attribute__((btf_decl_tag("comment:test_auxiliary_unpriv")))
#define __btf_path(path) __attribute__((btf_decl_tag("comment:test_btf_path=" path)))
@@ -155,7 +157,7 @@
#define __imm_insn(name, expr) [name]"i"(*(long *)&(expr))
/* Magic constants used with __retval() */
-#define POINTER_VALUE 0xcafe4all
+#define POINTER_VALUE 0xbadcafe
#define TEST_DATA_LEN 64
#ifndef __used
diff --git a/tools/testing/selftests/bpf/progs/verifier_div_overflow.c b/tools/testing/selftests/bpf/progs/verifier_div_overflow.c
index 458984da804c..34e0c012ee76 100644
--- a/tools/testing/selftests/bpf/progs/verifier_div_overflow.c
+++ b/tools/testing/selftests/bpf/progs/verifier_div_overflow.c
@@ -77,7 +77,7 @@ l0_%=: exit; \
SEC("tc")
__description("MOD32 overflow, check 1")
-__success __retval(INT_MIN)
+__success __retval(_INT_MIN)
__naked void mod32_overflow_check_1(void)
{
asm volatile (" \
@@ -92,7 +92,7 @@ __naked void mod32_overflow_check_1(void)
SEC("tc")
__description("MOD32 overflow, check 2")
-__success __retval(INT_MIN)
+__success __retval(_INT_MIN)
__naked void mod32_overflow_check_2(void)
{
asm volatile (" \
diff --git a/tools/testing/selftests/bpf/test_loader.c b/tools/testing/selftests/bpf/test_loader.c
index 2c7e9729d5fe..d300326476f6 100644
--- a/tools/testing/selftests/bpf/test_loader.c
+++ b/tools/testing/selftests/bpf/test_loader.c
@@ -40,7 +40,7 @@
#define TEST_TAG_LOAD_MODE_PFX "comment:load_mode="
/* Warning: duplicated in bpf_misc.h */
-#define POINTER_VALUE 0xcafe4all
+#define POINTER_VALUE 0xbadcafe
#define TEST_DATA_LEN 64
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
@@ -318,20 +318,13 @@ static int parse_caps(const char *str, __u64 *val, const char *name)
static int parse_retval(const char *str, int *val, const char *name)
{
- struct {
- char *name;
- int val;
- } named_values[] = {
- { "INT_MIN" , INT_MIN },
- { "POINTER_VALUE", POINTER_VALUE },
- { "TEST_DATA_LEN", TEST_DATA_LEN },
- };
- int i;
-
- for (i = 0; i < ARRAY_SIZE(named_values); ++i) {
- if (strcmp(str, named_values[i].name) != 0)
- continue;
- *val = named_values[i].val;
+ /* INT_MIN is defined as (-INT_MAX -1), i.e. it doesn't expand to a
+ * single int and cannot be parsed with strtol, so we handle it
+ * separately here. In addition, it expands to different expressions in
+ * different compilers so we use a prefixed _INT_MIN instead.
+ */
+ if (strcmp(str, "_INT_MIN") == 0) {
+ *val = INT_MIN;
return 0;
}
--
2.49.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH bpf-next v7 4/4] selftests/bpf: Add tests for string kfuncs
2025-06-23 13:47 [PATCH bpf-next v7 0/4] bpf: Add kfuncs for read-only string operations Viktor Malik
` (2 preceding siblings ...)
2025-06-23 13:48 ` [PATCH bpf-next v7 3/4] selftests/bpf: Allow macros in __retval Viktor Malik
@ 2025-06-23 13:48 ` Viktor Malik
3 siblings, 0 replies; 9+ messages in thread
From: Viktor Malik @ 2025-06-23 13:48 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
Viktor Malik
Add both positive and negative tests cases using string kfuncs added in
the previous patches.
Positive tests check that the functions work as expected.
Negative tests pass various incorrect strings to the kfuncs and check
for the expected error codes:
-E2BIG when passing too long strings
-EFAULT when trying to read inaccessible kernel memory
-ERANGE when passing userspace pointers on arches with non-overlapping
address spaces
A majority of the tests use the RUN_TESTS helper which executes BPF
programs with BPF_PROG_TEST_RUN and check for the expected return value.
An exception to this are tests for long strings as we need to memset the
long string from userspace (at least I haven't found an ergonomic way to
memset it from a BPF program), which cannot be done using the RUN_TESTS
infrastructure.
Suggested-by: Eduard Zingerman <eddyz87@gmail.com>
Signed-off-by: Viktor Malik <vmalik@redhat.com>
---
.../selftests/bpf/prog_tests/string_kfuncs.c | 63 ++++++++++++++
.../bpf/progs/string_kfuncs_failure1.c | 85 +++++++++++++++++++
.../bpf/progs/string_kfuncs_failure2.c | 21 +++++
.../bpf/progs/string_kfuncs_success.c | 35 ++++++++
4 files changed, 204 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/string_kfuncs.c
create mode 100644 tools/testing/selftests/bpf/progs/string_kfuncs_failure1.c
create mode 100644 tools/testing/selftests/bpf/progs/string_kfuncs_failure2.c
create mode 100644 tools/testing/selftests/bpf/progs/string_kfuncs_success.c
diff --git a/tools/testing/selftests/bpf/prog_tests/string_kfuncs.c b/tools/testing/selftests/bpf/prog_tests/string_kfuncs.c
new file mode 100644
index 000000000000..39322f1649ea
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/string_kfuncs.c
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2025 Red Hat, Inc.*/
+#include <test_progs.h>
+#include "string_kfuncs_success.skel.h"
+#include "string_kfuncs_failure1.skel.h"
+#include "string_kfuncs_failure2.skel.h"
+#include <sys/mman.h>
+
+static const char * const string_kfuncs[] = {
+ "strcmp",
+ "strchr",
+ "strchrnul",
+ "strnchr",
+ "strrchr",
+ "strlen",
+ "strnlen",
+ "strspn",
+ "strcspn",
+ "strstr",
+ "strnstr",
+};
+
+void run_too_long_tests(void)
+{
+ struct string_kfuncs_failure2 *skel;
+ struct bpf_program *prog;
+ char test_name[256];
+ int err, i;
+
+ skel = string_kfuncs_failure2__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "string_kfuncs_failure2__open_and_load"))
+ return;
+
+ memset(skel->bss->long_str, 'a', sizeof(skel->bss->long_str));
+
+ for (i = 0; i < ARRAY_SIZE(string_kfuncs); i++) {
+ sprintf(test_name, "test_%s_too_long", string_kfuncs[i]);
+ if (!test__start_subtest(test_name))
+ continue;
+
+ prog = bpf_object__find_program_by_name(skel->obj, test_name);
+ if (!ASSERT_OK_PTR(prog, "bpf_object__find_program_by_name"))
+ goto cleanup;
+
+ LIBBPF_OPTS(bpf_test_run_opts, topts);
+ err = bpf_prog_test_run_opts(bpf_program__fd(prog), &topts);
+ if (!ASSERT_OK(err, "bpf_prog_test_run"))
+ goto cleanup;
+
+ ASSERT_EQ(topts.retval, -E2BIG, "reading too long string fails with -E2BIG");
+ }
+
+cleanup:
+ string_kfuncs_failure2__destroy(skel);
+}
+
+void test_string_kfuncs(void)
+{
+ RUN_TESTS(string_kfuncs_success);
+ RUN_TESTS(string_kfuncs_failure1);
+
+ run_too_long_tests();
+}
diff --git a/tools/testing/selftests/bpf/progs/string_kfuncs_failure1.c b/tools/testing/selftests/bpf/progs/string_kfuncs_failure1.c
new file mode 100644
index 000000000000..ed67328e837a
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/string_kfuncs_failure1.c
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2025 Red Hat, Inc.*/
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <linux/limits.h>
+#include "bpf_misc.h"
+#include "errno.h"
+
+char *user_ptr = (char *)1;
+char *invalid_kern_ptr = (char *)-1;
+
+/* When passing userspace pointers, the error code differs based on arch:
+ * -ERANGE on arches with non-overlapping address spaces
+ * -EFAULT on other arches
+ */
+#if defined(__TARGET_ARCH_arm) || defined(__TARGET_ARCH_loongarch) || \
+ defined(__TARGET_ARCH_powerpc) || defined(__TARGET_ARCH_x86)
+#define USER_PTR_ERR -ERANGE
+#else
+#define USER_PTR_ERR -EFAULT
+#endif
+
+/* On s390, __get_kernel_nofault (used in string kfuncs) returns 0 for NULL and
+ * user_ptr (instead of causing an exception) so the below two groups of tests
+ * are not applicable.
+ */
+#ifndef __TARGET_ARCH_s390
+
+/* Passing NULL to string kfuncs (treated as a userspace ptr) */
+SEC("syscall") __retval(USER_PTR_ERR) int test_strcmp_null1(void *ctx) { return bpf_strcmp(NULL, "hello"); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strcmp_null2(void *ctx) { return bpf_strcmp("hello", NULL); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strchr_null(void *ctx) { return bpf_strchr(NULL, 'a'); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strchrnul_null(void *ctx) { return bpf_strchrnul(NULL, 'a'); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strnchr_null(void *ctx) { return bpf_strnchr(NULL, 1, 'a'); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strrchr_null(void *ctx) { return bpf_strrchr(NULL, 'a'); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strlen_null(void *ctx) { return bpf_strlen(NULL); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strnlen_null(void *ctx) { return bpf_strnlen(NULL, 1); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strspn_null1(void *ctx) { return bpf_strspn(NULL, "hello"); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strspn_null2(void *ctx) { return bpf_strspn("hello", NULL); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strcspn_null1(void *ctx) { return bpf_strcspn(NULL, "hello"); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strcspn_null2(void *ctx) { return bpf_strcspn("hello", NULL); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strstr_null1(void *ctx) { return bpf_strstr(NULL, "hello"); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strstr_null2(void *ctx) { return bpf_strstr("hello", NULL); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strnstr_null1(void *ctx) { return bpf_strnstr(NULL, "hello", 1); }
+SEC("syscall") __retval(USER_PTR_ERR)int test_strnstr_null2(void *ctx) { return bpf_strnstr("hello", NULL, 1); }
+
+/* Passing userspace ptr to string kfuncs */
+SEC("syscall") __retval(USER_PTR_ERR) int test_strcmp_user_ptr1(void *ctx) { return bpf_strcmp(user_ptr, "hello"); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strcmp_user_ptr2(void *ctx) { return bpf_strcmp("hello", user_ptr); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strchr_user_ptr(void *ctx) { return bpf_strchr(user_ptr, 'a'); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strchrnul_user_ptr(void *ctx) { return bpf_strchrnul(user_ptr, 'a'); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strnchr_user_ptr(void *ctx) { return bpf_strnchr(user_ptr, 1, 'a'); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strrchr_user_ptr(void *ctx) { return bpf_strrchr(user_ptr, 'a'); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strlen_user_ptr(void *ctx) { return bpf_strlen(user_ptr); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strnlen_user_ptr(void *ctx) { return bpf_strnlen(user_ptr, 1); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strspn_user_ptr1(void *ctx) { return bpf_strspn(user_ptr, "hello"); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strspn_user_ptr2(void *ctx) { return bpf_strspn("hello", user_ptr); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strcspn_user_ptr1(void *ctx) { return bpf_strcspn(user_ptr, "hello"); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strcspn_user_ptr2(void *ctx) { return bpf_strcspn("hello", user_ptr); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strstr_user_ptr1(void *ctx) { return bpf_strstr(user_ptr, "hello"); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strstr_user_ptr2(void *ctx) { return bpf_strstr("hello", user_ptr); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strnstr_user_ptr1(void *ctx) { return bpf_strnstr(user_ptr, "hello", 1); }
+SEC("syscall") __retval(USER_PTR_ERR) int test_strnstr_user_ptr2(void *ctx) { return bpf_strnstr("hello", user_ptr, 1); }
+
+#endif /* __TARGET_ARCH_s390 */
+
+/* Passing invalid kernel ptr to string kfuncs should always return -EFAULT */
+SEC("syscall") __retval(-EFAULT) int test_strcmp_pagefault1(void *ctx) { return bpf_strcmp(invalid_kern_ptr, "hello"); }
+SEC("syscall") __retval(-EFAULT) int test_strcmp_pagefault2(void *ctx) { return bpf_strcmp("hello", invalid_kern_ptr); }
+SEC("syscall") __retval(-EFAULT) int test_strchr_pagefault(void *ctx) { return bpf_strchr(invalid_kern_ptr, 'a'); }
+SEC("syscall") __retval(-EFAULT) int test_strchrnul_pagefault(void *ctx) { return bpf_strchrnul(invalid_kern_ptr, 'a'); }
+SEC("syscall") __retval(-EFAULT) int test_strnchr_pagefault(void *ctx) { return bpf_strnchr(invalid_kern_ptr, 1, 'a'); }
+SEC("syscall") __retval(-EFAULT) int test_strrchr_pagefault(void *ctx) { return bpf_strrchr(invalid_kern_ptr, 'a'); }
+SEC("syscall") __retval(-EFAULT) int test_strlen_pagefault(void *ctx) { return bpf_strlen(invalid_kern_ptr); }
+SEC("syscall") __retval(-EFAULT) int test_strnlen_pagefault(void *ctx) { return bpf_strnlen(invalid_kern_ptr, 1); }
+SEC("syscall") __retval(-EFAULT) int test_strspn_pagefault1(void *ctx) { return bpf_strspn(invalid_kern_ptr, "hello"); }
+SEC("syscall") __retval(-EFAULT) int test_strspn_pagefault2(void *ctx) { return bpf_strspn("hello", invalid_kern_ptr); }
+SEC("syscall") __retval(-EFAULT) int test_strcspn_pagefault1(void *ctx) { return bpf_strcspn(invalid_kern_ptr, "hello"); }
+SEC("syscall") __retval(-EFAULT) int test_strcspn_pagefault2(void *ctx) { return bpf_strcspn("hello", invalid_kern_ptr); }
+SEC("syscall") __retval(-EFAULT) int test_strstr_pagefault1(void *ctx) { return bpf_strstr(invalid_kern_ptr, "hello"); }
+SEC("syscall") __retval(-EFAULT) int test_strstr_pagefault2(void *ctx) { return bpf_strstr("hello", invalid_kern_ptr); }
+SEC("syscall") __retval(-EFAULT) int test_strnstr_pagefault1(void *ctx) { return bpf_strnstr(invalid_kern_ptr, "hello", 1); }
+SEC("syscall") __retval(-EFAULT) int test_strnstr_pagefault2(void *ctx) { return bpf_strnstr("hello", invalid_kern_ptr, 1); }
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/string_kfuncs_failure2.c b/tools/testing/selftests/bpf/progs/string_kfuncs_failure2.c
new file mode 100644
index 000000000000..685d221d8aa0
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/string_kfuncs_failure2.c
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2025 Red Hat, Inc.*/
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <linux/limits.h>
+
+char long_str[XATTR_SIZE_MAX + 1];
+
+SEC("syscall") int test_strcmp_too_long(void *ctx) { return bpf_strcmp(long_str, long_str); }
+SEC("syscall") int test_strchr_too_long(void *ctx) { return bpf_strchr(long_str, 'b'); }
+SEC("syscall") int test_strchrnul_too_long(void *ctx) { return bpf_strchrnul(long_str, 'b'); }
+SEC("syscall") int test_strnchr_too_long(void *ctx) { return bpf_strnchr(long_str, sizeof(long_str), 'b'); }
+SEC("syscall") int test_strrchr_too_long(void *ctx) { return bpf_strrchr(long_str, 'b'); }
+SEC("syscall") int test_strlen_too_long(void *ctx) { return bpf_strlen(long_str); }
+SEC("syscall") int test_strnlen_too_long(void *ctx) { return bpf_strnlen(long_str, sizeof(long_str)); }
+SEC("syscall") int test_strspn_too_long(void *ctx) { return bpf_strspn(long_str, "a"); }
+SEC("syscall") int test_strcspn_too_long(void *ctx) { return bpf_strcspn(long_str, "b"); }
+SEC("syscall") int test_strstr_too_long(void *ctx) { return bpf_strstr(long_str, "hello"); }
+SEC("syscall") int test_strnstr_too_long(void *ctx) { return bpf_strnstr(long_str, "hello", sizeof(long_str)); }
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/string_kfuncs_success.c b/tools/testing/selftests/bpf/progs/string_kfuncs_success.c
new file mode 100644
index 000000000000..d0e94921e811
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/string_kfuncs_success.c
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2025 Red Hat, Inc.*/
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+
+char str[] = "hello world";
+
+#define __test(retval) SEC("syscall") __success __retval(retval)
+
+/* Functional tests */
+__test(0) int test_strcmp_eq(void *ctx) { return bpf_strcmp(str, "hello world"); }
+__test(1) int test_strcmp_neq(void *ctx) { return bpf_strcmp(str, "hello"); }
+__test(1) int test_strchr_found(void *ctx) { return bpf_strchr(str, 'e'); }
+__test(11) int test_strchr_null(void *ctx) { return bpf_strchr(str, '\0'); }
+__test(-1) int test_strchr_notfound(void *ctx) { return bpf_strchr(str, 'x'); }
+__test(1) int test_strchrnul_found(void *ctx) { return bpf_strchrnul(str, 'e'); }
+__test(11) int test_strchrnul_notfound(void *ctx) { return bpf_strchrnul(str, 'x'); }
+__test(1) int test_strnchr_found(void *ctx) { return bpf_strnchr(str, 5, 'e'); }
+__test(11) int test_strnchr_null(void *ctx) { return bpf_strnchr(str, 12, '\0'); }
+__test(-1) int test_strnchr_notfound(void *ctx) { return bpf_strnchr(str, 5, 'w'); }
+__test(9) int test_strrchr_found(void *ctx) { return bpf_strrchr(str, 'l'); }
+__test(-1) int test_strrchr_notfound(void *ctx) { return bpf_strrchr(str, 'x'); }
+__test(11) int test_strlen(void *ctx) { return bpf_strlen(str); }
+__test(11) int test_strnlen(void *ctx) { return bpf_strnlen(str, 12); }
+__test(5) int test_strspn(void *ctx) { return bpf_strspn(str, "ehlo"); }
+__test(2) int test_strcspn(void *ctx) { return bpf_strcspn(str, "lo"); }
+__test(6) int test_strstr_found(void *ctx) { return bpf_strstr(str, "world"); }
+__test(-1) int test_strstr_notfound(void *ctx) { return bpf_strstr(str, "hi"); }
+__test(0) int test_strstr_empty(void *ctx) { return bpf_strstr(str, ""); }
+__test(0) int test_strnstr_found(void *ctx) { return bpf_strnstr(str, "hello", 6); }
+__test(-1) int test_strnstr_notfound(void *ctx) { return bpf_strnstr(str, "hi", 10); }
+__test(0) int test_strnstr_empty(void *ctx) { return bpf_strnstr(str, "", 1); }
+
+char _license[] SEC("license") = "GPL";
--
2.49.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations
2025-06-23 13:48 ` [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations Viktor Malik
@ 2025-06-23 16:42 ` Alexei Starovoitov
2025-06-24 9:15 ` Viktor Malik
2025-06-23 21:29 ` Andrii Nakryiko
1 sibling, 1 reply; 9+ messages in thread
From: Alexei Starovoitov @ 2025-06-23 16:42 UTC (permalink / raw)
To: Viktor Malik
Cc: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa
On Mon, Jun 23, 2025 at 6:48 AM Viktor Malik <vmalik@redhat.com> wrote:
>
> +/* Kfuncs for string operations.
Pls use normal kernel style comments instead of old networking.
That's what we prefer for all new code.
> + *
> + * Since strings are not necessarily %NUL-terminated, we cannot directly call
> + * in-kernel implementations. Instead, we open-code the implementations using
> + * __get_kernel_nofault instead of plain dereference to make them safe.
> + */
> +
> +/**
> + * bpf_strcmp - Compare two strings
> + * @s1__ign: One string
> + * @s2__ign: Another string
> + *
> + * Return:
> + * * %0 - Strings are equal
> + * * %-1 - @s1__ign is smaller
> + * * %1 - @s2__ign is smaller
Here -1 and 1 return values are probably ok, since they match
traditional strcmp.
> + * * %-EFAULT - Cannot read one of the strings
> + * * %-E2BIG - One of strings is too large
> + * * %-ERANGE - One of strings is outside of kernel address space
> + */
...
> +/**
> + * bpf_strnchr - Find a character in a length limited string
> + * @s__ign: The string to be searched
> + * @count: The number of characters to be searched
> + * @c: The character to search for
> + *
> + * Note that the %NUL-terminator is considered part of the string, and can
> + * be searched for.
> + *
> + * Return:
> + * * >=0 - Index of the first occurrence of @c within @s__ign
> + * * %-1 - @c not found in the first @count characters of @s__ign
> + * * %-EFAULT - Cannot read @s__ign
> + * * %-E2BIG - @s__ign is too large
> + * * %-ERANGE - @s__ign is outside of kernel address space
> + */
but here and in a few other places returning -1 is effectively
returning -EPERM for "not found".
Which is odd.
Maybe let's return -ENOENT instead ?
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations
2025-06-23 13:48 ` [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations Viktor Malik
2025-06-23 16:42 ` Alexei Starovoitov
@ 2025-06-23 21:29 ` Andrii Nakryiko
2025-06-24 10:46 ` Viktor Malik
1 sibling, 1 reply; 9+ messages in thread
From: Andrii Nakryiko @ 2025-06-23 21:29 UTC (permalink / raw)
To: Viktor Malik
Cc: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa
On Mon, Jun 23, 2025 at 6:48 AM Viktor Malik <vmalik@redhat.com> wrote:
>
> String operations are commonly used so this exposes the most common ones
> to BPF programs. For now, we limit ourselves to operations which do not
> copy memory around.
>
> Unfortunately, most in-kernel implementations assume that strings are
> %NUL-terminated, which is not necessarily true, and therefore we cannot
> use them directly in the BPF context. Instead, we open-code them using
> __get_kernel_nofault instead of plain dereference to make them safe and
> limit the strings length to XATTR_SIZE_MAX to make sure the functions
> terminate. When __get_kernel_nofault fails, functions return -EFAULT.
> Similarly, when the size bound is reached, the functions return -E2BIG.
> In addition, we return -ERANGE when the passed strings are outside of
> the kernel address space.
>
> Note that thanks to these dynamic safety checks, no other constraints
> are put on the kfunc args (they are marked with the "__ign" suffix to
> skip any verifier checks for them).
>
> All of the functions return integers, including functions which normally
> (in kernel or libc) return pointers to the strings. The reason is that
> since the strings are generally treated as unsafe, the pointers couldn't
> be dereferenced anyways. So, instead, we return an index to the string
> and let user decide what to do with it. This also nicely fits with
> returning various error codes when necessary (see above).
>
> Suggested-by: Alexei Starovoitov <ast@kernel.org>
> Signed-off-by: Viktor Malik <vmalik@redhat.com>
> ---
> kernel/bpf/helpers.c | 389 +++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 389 insertions(+)
>
few more comments below, beside the -ENOENT issue Alexei mentioned earlier
[...]
> +/**
> + * bpf_strrchr - Find the last occurrence of a character in a string
> + * @s__ign: The string to be searched
> + * @c: The character to search for
> + *
> + * Return:
> + * * >=0 - Index of the last occurrence of @c within @s__ign
> + * * %-1 - @c not found in @s__ign
> + * * %-EFAULT - Cannot read @s__ign
> + * * %-E2BIG - @s__ign is too large
> + * * %-ERANGE - @s__ign is outside of kernel address space
> + */
> +__bpf_kfunc int bpf_strrchr(const char *s__ign, int c)
> +{
> + char sc;
> + int i, last = -1;
> +
> + if (!copy_from_kernel_nofault_allowed(s__ign, 1))
> + return -ERANGE;
> +
> + guard(pagefault)();
> + for (i = 0; i < XATTR_SIZE_MAX; i++) {
> + __get_kernel_nofault(&sc, s__ign, char, err_out);
> + if (sc == '\0')
> + return last;
> + if (sc == c)
> + last = i;
swap these two ifs, so that bpf_strrchr("blah", 0) will still return a
meaningful result (effectively become bpf_strlen(), of course)? That
should match strrchr's behavior in libc, if I'm reading this
correctly:
"The terminating NUL character is considered to be part of the string."
> + s__ign++;
> + }
> + return -E2BIG;
> +err_out:
> + return -EFAULT;
> +}
> +
[...]
> +/**
> + * bpf_strspn - Calculate the length of the initial substring of @s__ign which
> + * only contains letters in @accept__ign
> + * @s__ign: The string to be searched
> + * @accept__ign: The string to search for
> + *
> + * Return:
> + * * >=0 - The length of the initial substring of @s__ign which only
> + * contains letters from @accept__ign
> + * * %-EFAULT - Cannot read one of the strings
> + * * %-E2BIG - One of the strings is too large
> + * * %-ERANGE - One of the strings is outside of kernel address space
> + */
> +__bpf_kfunc int bpf_strspn(const char *s__ign, const char *accept__ign)
> +{
> + char cs, ca;
> + bool found;
> + int i, j;
> +
> + if (!copy_from_kernel_nofault_allowed(s__ign, 1) ||
> + !copy_from_kernel_nofault_allowed(accept__ign, 1)) {
> + return -ERANGE;
> + }
> +
> + guard(pagefault)();
> + for (i = 0; i < XATTR_SIZE_MAX; i++) {
> + __get_kernel_nofault(&cs, s__ign, char, err_out);
> + if (cs == '\0')
> + return i;
> + found = false;
> + for (j = 0; j < XATTR_SIZE_MAX; j++) {
> + __get_kernel_nofault(&ca, accept__ign + j, char, err_out);
> + if (cs == ca) {
> + found = true;
> + break;
> + }
> + if (ca == '\0')
> + break;
> + }
> + if (!found)
> + return i;
nit: you shouldn't need "found", just `ca == '\0'` would mean "not
found", I think?
so you'd have a succinct `if (cs == ca || ca == '\0') break;` in the
innermost loop
> + s__ign++;
> + }
> + return -E2BIG;
> +err_out:
> + return -EFAULT;
> +}
> +
[...]
> +/**
> + * bpf_strnstr - Find the first substring in a length-limited string
> + * @s1__ign: The string to be searched
> + * @s2__ign: The string to search for
> + * @len: the maximum number of characters to search
> + *
> + * Return:
> + * * >=0 - Index of the first character of the first occurrence of @s2__ign
> + * within the first @len characters of @s1__ign
> + * * %-1 - @s2__ign not found in the first @len characters of @s1__ign
> + * * %-EFAULT - Cannot read one of the strings
> + * * %-E2BIG - One of the strings is too large
> + * * %-ERANGE - One of the strings is outside of kernel address space
> + */
> +__bpf_kfunc int bpf_strnstr(const char *s1__ign, const char *s2__ign, size_t len)
> +{
> + char c1, c2;
> + int i, j;
> +
> + if (!copy_from_kernel_nofault_allowed(s1__ign, 1) ||
> + !copy_from_kernel_nofault_allowed(s2__ign, 1)) {
> + return -ERANGE;
> + }
> +
> + guard(pagefault)();
> + for (i = 0; i < XATTR_SIZE_MAX; i++) {
> + for (j = 0; i + j < len && j < XATTR_SIZE_MAX; j++) {
> + __get_kernel_nofault(&c1, s1__ign + j, char, err_out);
move this after you check `c2 == 0` below? why reading this character
if we are not going to compare it?
> + __get_kernel_nofault(&c2, s2__ign + j, char, err_out);
> + if (c2 == '\0')
> + return i;
> + if (c1 == '\0')
> + return -1;
> + if (c1 != c2)
> + break;
> + }
> + if (j == XATTR_SIZE_MAX)
> + return -E2BIG;
> + if (i + j == len)
> + return -1;
> + s1__ign++;
> + }
> + return -E2BIG;
> +err_out:
> + return -EFAULT;
> +}
> +
[...]
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations
2025-06-23 16:42 ` Alexei Starovoitov
@ 2025-06-24 9:15 ` Viktor Malik
0 siblings, 0 replies; 9+ messages in thread
From: Viktor Malik @ 2025-06-24 9:15 UTC (permalink / raw)
To: Alexei Starovoitov
Cc: bpf, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa
On 6/23/25 18:42, Alexei Starovoitov wrote:
> On Mon, Jun 23, 2025 at 6:48 AM Viktor Malik <vmalik@redhat.com> wrote:
>>
>> +/* Kfuncs for string operations.
>
> Pls use normal kernel style comments instead of old networking.
> That's what we prefer for all new code.
>
>> + *
>> + * Since strings are not necessarily %NUL-terminated, we cannot directly call
>> + * in-kernel implementations. Instead, we open-code the implementations using
>> + * __get_kernel_nofault instead of plain dereference to make them safe.
>> + */
>> +
>> +/**
>> + * bpf_strcmp - Compare two strings
>> + * @s1__ign: One string
>> + * @s2__ign: Another string
>> + *
>> + * Return:
>> + * * %0 - Strings are equal
>> + * * %-1 - @s1__ign is smaller
>> + * * %1 - @s2__ign is smaller
>
> Here -1 and 1 return values are probably ok, since they match
> traditional strcmp.
>
>> + * * %-EFAULT - Cannot read one of the strings
>> + * * %-E2BIG - One of strings is too large
>> + * * %-ERANGE - One of strings is outside of kernel address space
>> + */
>
> ...
>
>> +/**
>> + * bpf_strnchr - Find a character in a length limited string
>> + * @s__ign: The string to be searched
>> + * @count: The number of characters to be searched
>> + * @c: The character to search for
>> + *
>> + * Note that the %NUL-terminator is considered part of the string, and can
>> + * be searched for.
>> + *
>> + * Return:
>> + * * >=0 - Index of the first occurrence of @c within @s__ign
>> + * * %-1 - @c not found in the first @count characters of @s__ign
>> + * * %-EFAULT - Cannot read @s__ign
>> + * * %-E2BIG - @s__ign is too large
>> + * * %-ERANGE - @s__ign is outside of kernel address space
>> + */
>
> but here and in a few other places returning -1 is effectively
> returning -EPERM for "not found".
> Which is odd.
> Maybe let's return -ENOENT instead ?
Yes, that was my other alternative, I'll change it.
Thanks.
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations
2025-06-23 21:29 ` Andrii Nakryiko
@ 2025-06-24 10:46 ` Viktor Malik
0 siblings, 0 replies; 9+ messages in thread
From: Viktor Malik @ 2025-06-24 10:46 UTC (permalink / raw)
To: Andrii Nakryiko
Cc: bpf, Daniel Borkmann, Andrii Nakryiko, Martin KaFai Lau,
Eduard Zingerman, Song Liu, Yonghong Song, John Fastabend,
KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
Alexei Starovoitov
On 6/23/25 23:29, Andrii Nakryiko wrote:
> On Mon, Jun 23, 2025 at 6:48 AM Viktor Malik <vmalik@redhat.com> wrote:
>>
>> String operations are commonly used so this exposes the most common ones
>> to BPF programs. For now, we limit ourselves to operations which do not
>> copy memory around.
>>
>> Unfortunately, most in-kernel implementations assume that strings are
>> %NUL-terminated, which is not necessarily true, and therefore we cannot
>> use them directly in the BPF context. Instead, we open-code them using
>> __get_kernel_nofault instead of plain dereference to make them safe and
>> limit the strings length to XATTR_SIZE_MAX to make sure the functions
>> terminate. When __get_kernel_nofault fails, functions return -EFAULT.
>> Similarly, when the size bound is reached, the functions return -E2BIG.
>> In addition, we return -ERANGE when the passed strings are outside of
>> the kernel address space.
>>
>> Note that thanks to these dynamic safety checks, no other constraints
>> are put on the kfunc args (they are marked with the "__ign" suffix to
>> skip any verifier checks for them).
>>
>> All of the functions return integers, including functions which normally
>> (in kernel or libc) return pointers to the strings. The reason is that
>> since the strings are generally treated as unsafe, the pointers couldn't
>> be dereferenced anyways. So, instead, we return an index to the string
>> and let user decide what to do with it. This also nicely fits with
>> returning various error codes when necessary (see above).
>>
>> Suggested-by: Alexei Starovoitov <ast@kernel.org>
>> Signed-off-by: Viktor Malik <vmalik@redhat.com>
>> ---
>> kernel/bpf/helpers.c | 389 +++++++++++++++++++++++++++++++++++++++++++
>> 1 file changed, 389 insertions(+)
>>
>
> few more comments below, beside the -ENOENT issue Alexei mentioned earlier
>
> [...]
>
>> +/**
>> + * bpf_strrchr - Find the last occurrence of a character in a string
>> + * @s__ign: The string to be searched
>> + * @c: The character to search for
>> + *
>> + * Return:
>> + * * >=0 - Index of the last occurrence of @c within @s__ign
>> + * * %-1 - @c not found in @s__ign
>> + * * %-EFAULT - Cannot read @s__ign
>> + * * %-E2BIG - @s__ign is too large
>> + * * %-ERANGE - @s__ign is outside of kernel address space
>> + */
>> +__bpf_kfunc int bpf_strrchr(const char *s__ign, int c)
>> +{
>> + char sc;
>> + int i, last = -1;
>> +
>> + if (!copy_from_kernel_nofault_allowed(s__ign, 1))
>> + return -ERANGE;
>> +
>> + guard(pagefault)();
>> + for (i = 0; i < XATTR_SIZE_MAX; i++) {
>> + __get_kernel_nofault(&sc, s__ign, char, err_out);
>> + if (sc == '\0')
>> + return last;
>> + if (sc == c)
>> + last = i;
>
> swap these two ifs, so that bpf_strrchr("blah", 0) will still return a
> meaningful result (effectively become bpf_strlen(), of course)? That
> should match strrchr's behavior in libc, if I'm reading this
> correctly:
>
> "The terminating NUL character is considered to be part of the string."
Yeah, that's correct, nice catch! I'll add a test for the above case.
>
>
>> + s__ign++;
>> + }
>> + return -E2BIG;
>> +err_out:
>> + return -EFAULT;
>> +}
>> +
>
> [...]
>
>> +/**
>> + * bpf_strspn - Calculate the length of the initial substring of @s__ign which
>> + * only contains letters in @accept__ign
>> + * @s__ign: The string to be searched
>> + * @accept__ign: The string to search for
>> + *
>> + * Return:
>> + * * >=0 - The length of the initial substring of @s__ign which only
>> + * contains letters from @accept__ign
>> + * * %-EFAULT - Cannot read one of the strings
>> + * * %-E2BIG - One of the strings is too large
>> + * * %-ERANGE - One of the strings is outside of kernel address space
>> + */
>> +__bpf_kfunc int bpf_strspn(const char *s__ign, const char *accept__ign)
>> +{
>> + char cs, ca;
>> + bool found;
>> + int i, j;
>> +
>> + if (!copy_from_kernel_nofault_allowed(s__ign, 1) ||
>> + !copy_from_kernel_nofault_allowed(accept__ign, 1)) {
>> + return -ERANGE;
>> + }
>> +
>> + guard(pagefault)();
>> + for (i = 0; i < XATTR_SIZE_MAX; i++) {
>> + __get_kernel_nofault(&cs, s__ign, char, err_out);
>> + if (cs == '\0')
>> + return i;
>> + found = false;
>> + for (j = 0; j < XATTR_SIZE_MAX; j++) {
>> + __get_kernel_nofault(&ca, accept__ign + j, char, err_out);
>> + if (cs == ca) {
>> + found = true;
>> + break;
>> + }
>> + if (ca == '\0')
>> + break;
>> + }
>> + if (!found)
>> + return i;
>
> nit: you shouldn't need "found", just `ca == '\0'` would mean "not
> found", I think?
>
> so you'd have a succinct `if (cs == ca || ca == '\0') break;` in the
> innermost loop
That sounds about right. We'll just need to cover the case when
`j == XATTR_SIZE_MAX` and return -E2BIG in such a case (which should
have been done anyways here and currently is not).
Also, an analogical thing can be done in bpf_strcspn.
>
>
>> + s__ign++;
>> + }
>> + return -E2BIG;
>> +err_out:
>> + return -EFAULT;
>> +}
>> +
>
> [...]
>
>> +/**
>> + * bpf_strnstr - Find the first substring in a length-limited string
>> + * @s1__ign: The string to be searched
>> + * @s2__ign: The string to search for
>> + * @len: the maximum number of characters to search
>> + *
>> + * Return:
>> + * * >=0 - Index of the first character of the first occurrence of @s2__ign
>> + * within the first @len characters of @s1__ign
>> + * * %-1 - @s2__ign not found in the first @len characters of @s1__ign
>> + * * %-EFAULT - Cannot read one of the strings
>> + * * %-E2BIG - One of the strings is too large
>> + * * %-ERANGE - One of the strings is outside of kernel address space
>> + */
>> +__bpf_kfunc int bpf_strnstr(const char *s1__ign, const char *s2__ign, size_t len)
>> +{
>> + char c1, c2;
>> + int i, j;
>> +
>> + if (!copy_from_kernel_nofault_allowed(s1__ign, 1) ||
>> + !copy_from_kernel_nofault_allowed(s2__ign, 1)) {
>> + return -ERANGE;
>> + }
>> +
>> + guard(pagefault)();
>> + for (i = 0; i < XATTR_SIZE_MAX; i++) {
>> + for (j = 0; i + j < len && j < XATTR_SIZE_MAX; j++) {
>> + __get_kernel_nofault(&c1, s1__ign + j, char, err_out);
>
> move this after you check `c2 == 0` below? why reading this character
> if we are not going to compare it?
Good point, will move it.
>
>> + __get_kernel_nofault(&c2, s2__ign + j, char, err_out);
>> + if (c2 == '\0')
>> + return i;
>> + if (c1 == '\0')
>> + return -1;
>> + if (c1 != c2)
>> + break;
>> + }
>> + if (j == XATTR_SIZE_MAX)
>> + return -E2BIG;
>> + if (i + j == len)
>> + return -1;
>> + s1__ign++;
>> + }
>> + return -E2BIG;
>> +err_out:
>> + return -EFAULT;
>> +}
>> +
>
> [...]
Thanks for the reviews!
I'll wait a couple more days for potential reviews for the other patches
before sending the next revision.
Viktor
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2025-06-24 10:47 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-06-23 13:47 [PATCH bpf-next v7 0/4] bpf: Add kfuncs for read-only string operations Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 1/4] uaccess: Define pagefault lock guard Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 2/4] bpf: Add kfuncs for read-only string operations Viktor Malik
2025-06-23 16:42 ` Alexei Starovoitov
2025-06-24 9:15 ` Viktor Malik
2025-06-23 21:29 ` Andrii Nakryiko
2025-06-24 10:46 ` Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 3/4] selftests/bpf: Allow macros in __retval Viktor Malik
2025-06-23 13:48 ` [PATCH bpf-next v7 4/4] selftests/bpf: Add tests for string kfuncs Viktor Malik
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).