Merge branch 'Fix bpf_probe_read_user_str() overcopying'

Daniel Xu says:

====================

6ae08ae3de ("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>
This commit is contained in:
Alexei Starovoitov 2020-11-19 10:45:55 -08:00
commit 14d6d86c21
4 changed files with 123 additions and 2 deletions

View File

@ -181,6 +181,16 @@ bpf_probe_read_user_str_common(void *dst, u32 size,
{
int ret;
/*
* NB: We rely on strncpy_from_user() not copying junk past the NUL
* terminator into `dst`.
*
* strncpy_from_user() does long-sized strides in the fast path. If the
* strncpy does not mask out the bytes after the NUL in `unsafe_ptr`,
* then there could be junk after the NUL in `dst`. If user takes `dst`
* and keys a hash map with it, then semantically identical strings can
* occupy multiple entries in the map.
*/
ret = strncpy_from_user_nofault(dst, unsafe_ptr, size);
if (unlikely(ret < 0))
memset(dst, 0, size);

View File

@ -35,17 +35,32 @@ static inline long do_strncpy_from_user(char *dst, const char __user *src,
goto byte_at_a_time;
while (max >= sizeof(unsigned long)) {
unsigned long c, data;
unsigned long c, data, mask;
/* Fall back to byte-at-a-time if we get a page fault */
unsafe_get_user(c, (unsigned long __user *)(src+res), byte_at_a_time);
*(unsigned long *)(dst+res) = c;
/*
* Note that we mask out the bytes following the NUL. This is
* important to do because string oblivious code may read past
* the NUL. For those routines, we don't want to give them
* potentially random bytes after the NUL in `src`.
*
* One example of such code is BPF map keys. BPF treats map keys
* as an opaque set of bytes. Without the post-NUL mask, any BPF
* maps keyed by strings returned from strncpy_from_user() may
* have multiple entries for semantically identical strings.
*/
if (has_zero(c, &data, &constants)) {
data = prep_zero_mask(c, data, &constants);
data = create_zero_mask(data);
mask = zero_bytemask(data);
*(unsigned long *)(dst+res) = c & mask;
return res + find_zero(data);
}
*(unsigned long *)(dst+res) = c;
res += sizeof(unsigned long);
max -= sizeof(unsigned long);
}

View File

@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-2.0
#include <test_progs.h>
#include "test_probe_read_user_str.skel.h"
static const char str1[] = "mestring";
static const char str2[] = "mestringalittlebigger";
static const char str3[] = "mestringblubblubblubblubblub";
static int test_one_str(struct test_probe_read_user_str *skel, const char *str,
size_t len)
{
int err, duration = 0;
char buf[256];
/* Ensure bytes after string are ones */
memset(buf, 1, sizeof(buf));
memcpy(buf, str, len);
/* Give prog our userspace pointer */
skel->bss->user_ptr = buf;
/* Trigger tracepoint */
usleep(1);
/* Did helper fail? */
if (CHECK(skel->bss->ret < 0, "prog_ret", "prog returned: %ld\n",
skel->bss->ret))
return 1;
/* Check that string was copied correctly */
err = memcmp(skel->bss->buf, str, len);
if (CHECK(err, "memcmp", "prog copied wrong string"))
return 1;
/* Now check that no extra trailing bytes were copied */
memset(buf, 0, sizeof(buf));
err = memcmp(skel->bss->buf + len, buf, sizeof(buf) - len);
if (CHECK(err, "memcmp", "trailing bytes were not stripped"))
return 1;
return 0;
}
void test_probe_read_user_str(void)
{
struct test_probe_read_user_str *skel;
int err, duration = 0;
skel = test_probe_read_user_str__open_and_load();
if (CHECK(!skel, "test_probe_read_user_str__open_and_load",
"skeleton open and load failed\n"))
return;
/* Give pid to bpf prog so it doesn't read from anyone else */
skel->bss->pid = getpid();
err = test_probe_read_user_str__attach(skel);
if (CHECK(err, "test_probe_read_user_str__attach",
"skeleton attach failed: %d\n", err))
goto out;
if (test_one_str(skel, str1, sizeof(str1)))
goto out;
if (test_one_str(skel, str2, sizeof(str2)))
goto out;
if (test_one_str(skel, str3, sizeof(str3)))
goto out;
out:
test_probe_read_user_str__destroy(skel);
}

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <sys/types.h>
pid_t pid = 0;
long ret = 0;
void *user_ptr = 0;
char buf[256] = {};
SEC("tracepoint/syscalls/sys_enter_nanosleep")
int on_write(void *ctx)
{
if (pid != (bpf_get_current_pid_tgid() >> 32))
return 0;
ret = bpf_probe_read_user_str(buf, sizeof(buf), user_ptr);
return 0;
}
char _license[] SEC("license") = "GPL";