Skip to content

Commit 14d6d86

Browse files
author
Alexei Starovoitov
committed
Merge branch 'Fix bpf_probe_read_user_str() overcopying'
Daniel Xu says: ==================== 6ae08ae ("bpf: Add probe_read_{user, kernel} and probe_read_{user, kernel}_str helpers") introduced a subtle bug where bpf_probe_read_user_str() would potentially copy a few extra bytes after the NUL terminator. This issue is particularly nefarious when strings are used as map keys, as seemingly identical strings can occupy multiple entries in a map. This patchset fixes the issue and introduces a selftest to prevent future regressions. v6 -> v7: * Add comments v5 -> v6: * zero-pad up to sizeof(unsigned long) after NUL v4 -> v5: * don't read potentially uninitialized memory v3 -> v4: * directly pass userspace pointer to prog * test more strings of different length v2 -> v3: * set pid filter before attaching prog in selftest * use long instead of int as bpf_probe_read_user_str() retval * style changes v1 -> v2: * add Fixes: tag * add selftest ==================== Signed-off-by: Alexei Starovoitov <ast@kernel.org>
2 parents 1fd6cee + c8a36ae commit 14d6d86

File tree

4 files changed

+123
-2
lines changed

4 files changed

+123
-2
lines changed

kernel/trace/bpf_trace.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ bpf_probe_read_user_str_common(void *dst, u32 size,
181181
{
182182
int ret;
183183

184+
/*
185+
* NB: We rely on strncpy_from_user() not copying junk past the NUL
186+
* terminator into `dst`.
187+
*
188+
* strncpy_from_user() does long-sized strides in the fast path. If the
189+
* strncpy does not mask out the bytes after the NUL in `unsafe_ptr`,
190+
* then there could be junk after the NUL in `dst`. If user takes `dst`
191+
* and keys a hash map with it, then semantically identical strings can
192+
* occupy multiple entries in the map.
193+
*/
184194
ret = strncpy_from_user_nofault(dst, unsafe_ptr, size);
185195
if (unlikely(ret < 0))
186196
memset(dst, 0, size);

lib/strncpy_from_user.c

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,32 @@ static inline long do_strncpy_from_user(char *dst, const char __user *src,
3535
goto byte_at_a_time;
3636

3737
while (max >= sizeof(unsigned long)) {
38-
unsigned long c, data;
38+
unsigned long c, data, mask;
3939

4040
/* Fall back to byte-at-a-time if we get a page fault */
4141
unsafe_get_user(c, (unsigned long __user *)(src+res), byte_at_a_time);
4242

43-
*(unsigned long *)(dst+res) = c;
43+
/*
44+
* Note that we mask out the bytes following the NUL. This is
45+
* important to do because string oblivious code may read past
46+
* the NUL. For those routines, we don't want to give them
47+
* potentially random bytes after the NUL in `src`.
48+
*
49+
* One example of such code is BPF map keys. BPF treats map keys
50+
* as an opaque set of bytes. Without the post-NUL mask, any BPF
51+
* maps keyed by strings returned from strncpy_from_user() may
52+
* have multiple entries for semantically identical strings.
53+
*/
4454
if (has_zero(c, &data, &constants)) {
4555
data = prep_zero_mask(c, data, &constants);
4656
data = create_zero_mask(data);
57+
mask = zero_bytemask(data);
58+
*(unsigned long *)(dst+res) = c & mask;
4759
return res + find_zero(data);
4860
}
61+
62+
*(unsigned long *)(dst+res) = c;
63+
4964
res += sizeof(unsigned long);
5065
max -= sizeof(unsigned long);
5166
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// SPDX-License-Identifier: GPL-2.0
2+
#include <test_progs.h>
3+
#include "test_probe_read_user_str.skel.h"
4+
5+
static const char str1[] = "mestring";
6+
static const char str2[] = "mestringalittlebigger";
7+
static const char str3[] = "mestringblubblubblubblubblub";
8+
9+
static int test_one_str(struct test_probe_read_user_str *skel, const char *str,
10+
size_t len)
11+
{
12+
int err, duration = 0;
13+
char buf[256];
14+
15+
/* Ensure bytes after string are ones */
16+
memset(buf, 1, sizeof(buf));
17+
memcpy(buf, str, len);
18+
19+
/* Give prog our userspace pointer */
20+
skel->bss->user_ptr = buf;
21+
22+
/* Trigger tracepoint */
23+
usleep(1);
24+
25+
/* Did helper fail? */
26+
if (CHECK(skel->bss->ret < 0, "prog_ret", "prog returned: %ld\n",
27+
skel->bss->ret))
28+
return 1;
29+
30+
/* Check that string was copied correctly */
31+
err = memcmp(skel->bss->buf, str, len);
32+
if (CHECK(err, "memcmp", "prog copied wrong string"))
33+
return 1;
34+
35+
/* Now check that no extra trailing bytes were copied */
36+
memset(buf, 0, sizeof(buf));
37+
err = memcmp(skel->bss->buf + len, buf, sizeof(buf) - len);
38+
if (CHECK(err, "memcmp", "trailing bytes were not stripped"))
39+
return 1;
40+
41+
return 0;
42+
}
43+
44+
void test_probe_read_user_str(void)
45+
{
46+
struct test_probe_read_user_str *skel;
47+
int err, duration = 0;
48+
49+
skel = test_probe_read_user_str__open_and_load();
50+
if (CHECK(!skel, "test_probe_read_user_str__open_and_load",
51+
"skeleton open and load failed\n"))
52+
return;
53+
54+
/* Give pid to bpf prog so it doesn't read from anyone else */
55+
skel->bss->pid = getpid();
56+
57+
err = test_probe_read_user_str__attach(skel);
58+
if (CHECK(err, "test_probe_read_user_str__attach",
59+
"skeleton attach failed: %d\n", err))
60+
goto out;
61+
62+
if (test_one_str(skel, str1, sizeof(str1)))
63+
goto out;
64+
if (test_one_str(skel, str2, sizeof(str2)))
65+
goto out;
66+
if (test_one_str(skel, str3, sizeof(str3)))
67+
goto out;
68+
69+
out:
70+
test_probe_read_user_str__destroy(skel);
71+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: GPL-2.0
2+
3+
#include <linux/bpf.h>
4+
#include <bpf/bpf_helpers.h>
5+
#include <bpf/bpf_tracing.h>
6+
7+
#include <sys/types.h>
8+
9+
pid_t pid = 0;
10+
long ret = 0;
11+
void *user_ptr = 0;
12+
char buf[256] = {};
13+
14+
SEC("tracepoint/syscalls/sys_enter_nanosleep")
15+
int on_write(void *ctx)
16+
{
17+
if (pid != (bpf_get_current_pid_tgid() >> 32))
18+
return 0;
19+
20+
ret = bpf_probe_read_user_str(buf, sizeof(buf), user_ptr);
21+
22+
return 0;
23+
}
24+
25+
char _license[] SEC("license") = "GPL";

0 commit comments

Comments
 (0)