Hi,
The motivation for introducing bpf_strncmp() helper comes from
two aspects:
(1) clang doesn't always replace strncmp() automatically
(and don't known why)
In tracing program, sometimes we need to using a home-made
strncmp() to check whether or not the file name is expected.
(2) the performance of home-made strncmp is not so good
As shown in the benchmark of patch #2, the performance of
bpf_strncmp helper is 80% better than home-made strncmp under
x86-64, and 600% better under arm64 thanks to its arch-optimized
strncmp().
But i'm concernt about whether the API of bpf_strncmp() is OK.
Now the first argument must be a read-only null-terminated
string, it is enough for our file-name comparsion case because
the target file name is const and read-only, but may be not
usable for comparsion of two strings stored in writable-maps.
Any comments are welcome.
Regards,
Tao
Hou Tao (2):
bpf: add bpf_strncmp helper
selftests/bpf: add benchmark bpf_strcmp
include/linux/bpf.h | 1 +
include/uapi/linux/bpf.h | 11 ++
kernel/bpf/helpers.c | 14 +++
kernel/trace/bpf_trace.c | 2 +
tools/include/uapi/linux/bpf.h | 11 ++
.../bpf/prog_tests/test_strncmp_helper.c | 75 ++++++++++++
.../selftests/bpf/progs/strncmp_helper.c | 109 ++++++++++++++++++
7 files changed, 223 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/test_strncmp_helper.c
create mode 100644 tools/testing/selftests/bpf/progs/strncmp_helper.c
--
2.29.2
The helper compares two strings: one string is a null-terminated
read-only string, and another one has const max storage size. And
it can be used to compare file name in tracing or LSM program.
We don't check whether or not s2 in bpf_strncmp() is null-terminated,
because its content may be changed by malicous program, and we only
ensure the memory accessed is bounded by s2_sz.
Signed-off-by: Hou Tao <redacted>
---
include/linux/bpf.h | 1 +
include/uapi/linux/bpf.h | 11 +++++++++++
kernel/bpf/helpers.c | 14 ++++++++++++++
kernel/trace/bpf_trace.c | 2 ++
tools/include/uapi/linux/bpf.h | 11 +++++++++++
5 files changed, 39 insertions(+)
@@ -4938,6 +4938,16 @@ union bpf_attr {***-ENOENT**ifsymbolisnotfound.****-EPERM**ifcallerdoesnothavepermissiontoobtainkerneladdress.+*+*longbpf_strncmp(constchar*s1,constchar*s2,u32s2_sz)+*Description+*Dostrncmp()between**s1**and**s2**.**s1**mustbea+*read-onlystring.**s2_sz**isthemaximumstoragesizeof+***s2**.+*Return+*Returnanintegerlessthan,equalto,orgreaterthanzero+*ifthefirst**s2_sz**bytesof**s2**isfoundtobe+*lessthan,tomatch,orbegreaterthan**s1**.*/#define __BPF_FUNC_MAPPER(FN) \FN(unspec),\
@@ -5120,6 +5130,7 @@ union bpf_attr {FN(trace_vprintk),\FN(skc_to_unix_sock),\FN(kallsyms_lookup_name),\+FN(strncmp),\/* *//* integer value in 'imm' field of BPF_CALL instruction selects which helper
@@ -4938,6 +4938,16 @@ union bpf_attr {***-ENOENT**ifsymbolisnotfound.****-EPERM**ifcallerdoesnothavepermissiontoobtainkerneladdress.+*+*longbpf_strncmp(constchar*s1,constchar*s2,u32s2_sz)+*Description+*Dostrncmp()between**s1**and**s2**.**s1**mustbea+*read-onlystring.**s2_sz**isthemaximumstoragesizeof+***s2**.+*Return+*Returnanintegerlessthan,equalto,orgreaterthanzero+*ifthefirst**s2_sz**bytesof**s2**isfoundtobe+*lessthan,tomatch,orbegreaterthan**s1**.*/#define __BPF_FUNC_MAPPER(FN) \FN(unspec),\
@@ -5120,6 +5130,7 @@ union bpf_attr {FN(trace_vprintk),\FN(skc_to_unix_sock),\FN(kallsyms_lookup_name),\+FN(strncmp),\/* *//* integer value in 'imm' field of BPF_CALL instruction selects which helper
The benchmark runs a loop 5000 times. In the loop it reads the file name
from kprobe argument into stack by using bpf_probe_read_kernel_str(),
and compares the file name with a target character or string.
Three cases are compared: only compare one character, compare the whole
string by a home-made strncmp() and compare the whole string by
bpf_strcmp().
The following is the result:
x86-64 host:
one character: 2613499 ns
whole str by strncmp: 2920348 ns
whole str by helper: 2779332 ns
arm64 host:
one character: 3898867 ns
whole str by strncmp: 4396787 ns
whole str by helper: 3968113 ns
Compared with home-made strncmp, the performance of bpf_strncmp helper
improves 80% under x86-64 and 600% under arm64. The big performance win
on arm64 may comes from its arch-optimized strncmp().
Signed-off-by: Hou Tao <redacted>
---
.../bpf/prog_tests/test_strncmp_helper.c | 75 ++++++++++++
.../selftests/bpf/progs/strncmp_helper.c | 109 ++++++++++++++++++
2 files changed, 184 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/test_strncmp_helper.c
create mode 100644 tools/testing/selftests/bpf/progs/strncmp_helper.c
On Sat, Nov 06, 2021 at 09:28:22PM +0800, Hou Tao wrote:
The benchmark runs a loop 5000 times. In the loop it reads the file name
from kprobe argument into stack by using bpf_probe_read_kernel_str(),
and compares the file name with a target character or string.
Three cases are compared: only compare one character, compare the whole
string by a home-made strncmp() and compare the whole string by
bpf_strcmp().
The following is the result:
x86-64 host:
one character: 2613499 ns
whole str by strncmp: 2920348 ns
whole str by helper: 2779332 ns
arm64 host:
one character: 3898867 ns
whole str by strncmp: 4396787 ns
whole str by helper: 3968113 ns
Compared with home-made strncmp, the performance of bpf_strncmp helper
improves 80% under x86-64 and 600% under arm64. The big performance win
on arm64 may comes from its arch-optimized strncmp().
80% and 600% improvement?!
I don't understand how this math works.
Why one char is barely different in total nsec than the whole string?
The string shouldn't miscompare on the first char as far as I understand the test.
On Sat, Nov 06, 2021 at 09:28:21PM +0800, Hou Tao wrote:
The helper compares two strings: one string is a null-terminated
read-only string, and another one has const max storage size. And
it can be used to compare file name in tracing or LSM program.
We don't check whether or not s2 in bpf_strncmp() is null-terminated,
because its content may be changed by malicous program, and we only
ensure the memory accessed is bounded by s2_sz.
I think "malicous" adjective is unnecessary and misleading.
It's also misspelled.
Just mention that 2nd argument doesn't have to be null terminated.
why tracing only?
Should probably be in bpf_base_func_proto.
I was thinking whether the proto could be:
long bpf_strncmp(const char *s1, u32 s1_sz, const char *s2)
but I think your version is better though having const string as 1st arg
is a bit odd in normal C.
Would it make sense to add bpf_memchr as well while we are at it?
And
static inline bpf_strnlen(const char *s, u32 sz)
{
return bpf_memchr(s, sz, 0);
}
to bpf_helpers.h ?
On Sat, Nov 6, 2021 at 12:26 PM Alexei Starovoitov
[off-list ref] wrote:
On Sat, Nov 06, 2021 at 09:28:21PM +0800, Hou Tao wrote:
quoted
The helper compares two strings: one string is a null-terminated
read-only string, and another one has const max storage size. And
it can be used to compare file name in tracing or LSM program.
We don't check whether or not s2 in bpf_strncmp() is null-terminated,
because its content may be changed by malicous program, and we only
ensure the memory accessed is bounded by s2_sz.
I think "malicous" adjective is unnecessary and misleading.
It's also misspelled.
Just mention that 2nd argument doesn't have to be null terminated.
why tracing only?
Should probably be in bpf_base_func_proto.
I was thinking whether the proto could be:
long bpf_strncmp(const char *s1, u32 s1_sz, const char *s2)
but I think your version is better though having const string as 1st arg
is a bit odd in normal C.
Why do you think it's better? This is equivalent to `123 == x` if it
was integer comparison, so it feels like bpf_strncmp(s, sz, "blah") is
indeed more natural. No big deal, just curious what's better about it.
Would it make sense to add bpf_memchr as well while we are at it?
And
static inline bpf_strnlen(const char *s, u32 sz)
{
return bpf_memchr(s, sz, 0);
}
to bpf_helpers.h ?
On Sat, Nov 6, 2021 at 1:07 PM Andrii Nakryiko
[off-list ref] wrote:
On Sat, Nov 6, 2021 at 12:26 PM Alexei Starovoitov
[off-list ref] wrote:
quoted
On Sat, Nov 06, 2021 at 09:28:21PM +0800, Hou Tao wrote:
quoted
The helper compares two strings: one string is a null-terminated
read-only string, and another one has const max storage size. And
it can be used to compare file name in tracing or LSM program.
We don't check whether or not s2 in bpf_strncmp() is null-terminated,
because its content may be changed by malicous program, and we only
ensure the memory accessed is bounded by s2_sz.
I think "malicous" adjective is unnecessary and misleading.
It's also misspelled.
Just mention that 2nd argument doesn't have to be null terminated.
why tracing only?
Should probably be in bpf_base_func_proto.
I was thinking whether the proto could be:
long bpf_strncmp(const char *s1, u32 s1_sz, const char *s2)
but I think your version is better though having const string as 1st arg
is a bit odd in normal C.
Why do you think it's better? This is equivalent to `123 == x` if it
was integer comparison, so it feels like bpf_strncmp(s, sz, "blah") is
indeed more natural. No big deal, just curious what's better about it.
Only that helper implementation has two less register moves.
which makes it 51%/49% win for me.
Hi,
On 11/7/2021 3:26 AM, Alexei Starovoitov wrote:
On Sat, Nov 06, 2021 at 09:28:21PM +0800, Hou Tao wrote:
quoted
The helper compares two strings: one string is a null-terminated
read-only string, and another one has const max storage size. And
it can be used to compare file name in tracing or LSM program.
We don't check whether or not s2 in bpf_strncmp() is null-terminated,
because its content may be changed by malicous program, and we only
ensure the memory accessed is bounded by s2_sz.
I think "malicous" adjective is unnecessary and misleading.
It's also misspelled.
Just mention that 2nd argument doesn't have to be null terminated.
why tracing only?
Should probably be in bpf_base_func_proto.
Because in our use case, bpf_strncmp() is only used by tracing program, but moving
it to bpf_base_func_proto() incurs no harm, so will do it.
I was thinking whether the proto could be:
long bpf_strncmp(const char *s1, u32 s1_sz, const char *s2)
but I think your version is better though having const string as 1st arg
is a bit odd in normal C.
Would it make sense to add bpf_memchr as well while we are at it?
And
static inline bpf_strnlen(const char *s, u32 sz)
{
return bpf_memchr(s, sz, 0);
}
to bpf_helpers.h ?
.
It is OK to add it, although I don't have a use case for it.
HI,
On 11/7/2021 2:43 AM, Alexei Starovoitov wrote:
On Sat, Nov 06, 2021 at 09:28:22PM +0800, Hou Tao wrote:
quoted
The benchmark runs a loop 5000 times. In the loop it reads the file name
from kprobe argument into stack by using bpf_probe_read_kernel_str(),
and compares the file name with a target character or string.
Three cases are compared: only compare one character, compare the whole
string by a home-made strncmp() and compare the whole string by
bpf_strcmp().
The following is the result:
x86-64 host:
one character: 2613499 ns
whole str by strncmp: 2920348 ns
whole str by helper: 2779332 ns
arm64 host:
one character: 3898867 ns
whole str by strncmp: 4396787 ns
whole str by helper: 3968113 ns
Compared with home-made strncmp, the performance of bpf_strncmp helper
improves 80% under x86-64 and 600% under arm64. The big performance win
on arm64 may comes from its arch-optimized strncmp().
80% and 600% improvement?!
I don't understand how this math works.
Why one char is barely different in total nsec than the whole string?
The string shouldn't miscompare on the first char as far as I understand the test.
Because the result of "one character" includes the overhead of process filtering and
string read.
My bad, I should explain the tests results in more details.
Three tests are exercised:
(1) one character
Filter unexpected caller by bpf_get_current_pid_tgid()
Use bpf_probe_read_kernel_str() to read the file name into 64-bytes sized-buffer
in stack
Only compare the first character of file name
(2) whole str by strncmp
Filter unexpected caller by bpf_get_current_pid_tgid()
Use bpf_probe_read_kernel_str() to read the file name into 64-bytes sized-buffer
in stack
Compare by using home-made strncmp(): the compared two strings are the same, so
the whole string is compared
(3) whole str by helper
Filter unexpected caller by bpf_get_current_pid_tgid()
Use bpf_probe_read_kernel_str() to read the file name into 64-bytes sized-buffer
in stack
Compare by using bpf_strncmp: the compared two strings are the same, so
the whole string is compared
Now "(1) one character" is used to calculate the overhead of process filtering and
string read. So under x86-64, the overhead of strncmp() is
total time of whole str by strncmp test - total time of no character test =
306849 ns.
The overhead of bpf_strncmp() is:
total time of whole str by helper test - total time of no character test =
165833 ns
So the performance win is about (306849 / 165833 ) * 100 - 100 = ~85%
And the win under arm64 is about (497920 / 69246) * 100 - 100 = ~600%
Hi,
On 11/7/2021 4:32 AM, Alexei Starovoitov wrote:
On Sat, Nov 6, 2021 at 1:07 PM Andrii Nakryiko
[off-list ref] wrote:
snip
quoted
quoted
I was thinking whether the proto could be:
long bpf_strncmp(const char *s1, u32 s1_sz, const char *s2)
but I think your version is better though having const string as 1st arg
is a bit odd in normal C.
Why do you think it's better? This is equivalent to `123 == x` if it
was integer comparison, so it feels like bpf_strncmp(s, sz, "blah") is
indeed more natural. No big deal, just curious what's better about it.
Only that helper implementation has two less register moves.
which makes it 51%/49% win for me.
.
I agree with Andrii that bpf_strncmp(s, sz, "blah") is more nature. I can run
some simple benchmarks to show whether or not the difference matters.
Regards,
Tao.
On Mon, Nov 8, 2021 at 6:05 AM Hou Tao [off-list ref] wrote:
HI,
On 11/7/2021 2:43 AM, Alexei Starovoitov wrote:
quoted
On Sat, Nov 06, 2021 at 09:28:22PM +0800, Hou Tao wrote:
quoted
The benchmark runs a loop 5000 times. In the loop it reads the file name
from kprobe argument into stack by using bpf_probe_read_kernel_str(),
and compares the file name with a target character or string.
Three cases are compared: only compare one character, compare the whole
string by a home-made strncmp() and compare the whole string by
bpf_strcmp().
The following is the result:
x86-64 host:
one character: 2613499 ns
whole str by strncmp: 2920348 ns
whole str by helper: 2779332 ns
arm64 host:
one character: 3898867 ns
whole str by strncmp: 4396787 ns
whole str by helper: 3968113 ns
Compared with home-made strncmp, the performance of bpf_strncmp helper
improves 80% under x86-64 and 600% under arm64. The big performance win
on arm64 may comes from its arch-optimized strncmp().
80% and 600% improvement?!
I don't understand how this math works.
Why one char is barely different in total nsec than the whole string?
The string shouldn't miscompare on the first char as far as I understand the test.
Because the result of "one character" includes the overhead of process filtering and
string read.
My bad, I should explain the tests results in more details.
Maybe use bench framework for your benchmark? It allows to setup the
benchmark and collect measurements in a more structured way. Check
some existing benchmarks under benchs/ in selftests/bpf directory.
To actually test just bpf_strncmp() don't add
bpf_probe_read_kernel_str() into the loop logic, set your data in
global variable and just search it. This will give you more accurate
microbenchmark data.
Three tests are exercised:
(1) one character
Filter unexpected caller by bpf_get_current_pid_tgid()
Use bpf_probe_read_kernel_str() to read the file name into 64-bytes sized-buffer
in stack
Only compare the first character of file name
(2) whole str by strncmp
Filter unexpected caller by bpf_get_current_pid_tgid()
Use bpf_probe_read_kernel_str() to read the file name into 64-bytes sized-buffer
in stack
Compare by using home-made strncmp(): the compared two strings are the same, so
the whole string is compared
(3) whole str by helper
Filter unexpected caller by bpf_get_current_pid_tgid()
Use bpf_probe_read_kernel_str() to read the file name into 64-bytes sized-buffer
in stack
Compare by using bpf_strncmp: the compared two strings are the same, so
the whole string is compared
Now "(1) one character" is used to calculate the overhead of process filtering and
string read. So under x86-64, the overhead of strncmp() is
total time of whole str by strncmp test - total time of no character test =
306849 ns.
The overhead of bpf_strncmp() is:
total time of whole str by helper test - total time of no character test =
165833 ns
So the performance win is about (306849 / 165833 ) * 100 - 100 = ~85%
And the win under arm64 is about (497920 / 69246) * 100 - 100 = ~600%