mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-07-27 19:09:50 -04:00
strset_add_str_mem() might reallocate the strset data buffer in order to
accommodate the provided string 's'. However, if 's' points to a string
already present in the buffer, it becomes dangling after the realloc.
This leads to a use-after-free when attempting to memcpy() the string
into the new buffer.
One scenario that triggers this problematic path is when resolve_btfids
attempts to patch kfunc prototypes using existing BTF parameter names:
| resolve_btfids: function bpf_list_push_back_impl already exists in BTF
| Segmentation fault (core dumped)
Compiling resolve_btfids with fsanitize=address generates a detailed
report of the UAF:
| =================================================================
| ERROR: AddressSanitizer: heap-use-after-free on address 0x7f4c4a500bd4
| ==1507892==ERROR: AddressSanitizer: heap-use-after-free on address 0x7f4c4a500bd4 at pc 0x55d25155a2a8 bp 0x7ffcef879060 sp 0x7ffcef878818
| READ of size 5 at 0x7f4c4a500bd4 thread T0
| #0 0x55d25155a2a7 in memcpy (tools/bpf/resolve_btfids/resolve_btfids+0xcf2a7)
| #1 0x55d2515d708e in strset__add_str tools/lib/bpf/strset.c:162:2
| #2 0x55d2515c730b in btf__add_str tools/lib/bpf/btf.c:2109:8
| #3 0x55d2515c9020 in btf__add_func_param tools/lib/bpf/btf.c:3108:14
| #4 0x55d25159f0b5 in process_kfunc_with_implicit_args tools/bpf/resolve_btfids/main.c:1196:9
| #5 0x55d25159e004 in btf2btf tools/bpf/resolve_btfids/main.c:1229:9
| #6 0x55d25159cee7 in main tools/bpf/resolve_btfids/main.c:1535:6
| #7 0x7f4c78e29f76 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
| #8 0x7f4c78e2a026 in __libc_start_main csu/../csu/libc-start.c:360:3
| #9 0x55d2514bb860 in _start (tools/bpf/resolve_btfids/resolve_btfids+0x30860)
|
| 0x7f4c4a500bd4 is located 13268 bytes inside of 2829000-byte region [0x7f4c4a4fd800,0x7f4c4a7b02c8)
| freed by thread T0 here:
| #0 0x55d25155b700 in realloc (tools/bpf/resolve_btfids/resolve_btfids+0xd0700)
| #1 0x55d2515c426c in libbpf_reallocarray tools/lib/bpf/./libbpf_internal.h:220:9
| #2 0x55d2515c426c in libbpf_add_mem tools/lib/bpf/btf.c:224:13
|
| previously allocated by thread T0 here:
| #0 0x55d25155b2e3 in malloc (tools/bpf/resolve_btfids/resolve_btfids+0xd02e3)
| #1 0x55d2515d6e7d in strset__new tools/lib/bpf/strset.c:58:20
While resolve_btfids could be refactored to avoid this call path, let's
instead fix this issue at the source in strset__add_str() and avoid
similar scenarios.
Let's check if set->strs_data was reallocated and whether 's' points to
an internal string within the old strset buffer. In such case, 's' is
reconstructed to point to the new buffer.
While already here, also fix strset__find_str() which suffers from the
same problem by factoring out the common operations into a new helper
function strset_str_append().
Fixes: 90d76d3ece ("libbpf: Extract internal set-of-strings datastructure APIs")
Suggested-by: Andrii Nakryiko <andrii@kernel.org>
Suggested-by: Mykyta Yatsenko <yatsenko@meta.com>
Signed-off-by: Carlos Llamas <cmllamas@google.com>
Signed-off-by: Andrii Nakryiko <andrii@kernel.org>
Link: https://lore.kernel.org/bpf/20260523162722.2718940-1-cmllamas@google.com
198 lines
4.8 KiB
C
198 lines
4.8 KiB
C
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
|
/* Copyright (c) 2021 Facebook */
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
#include <linux/err.h>
|
|
#include "hashmap.h"
|
|
#include "libbpf_internal.h"
|
|
#include "strset.h"
|
|
|
|
struct strset {
|
|
void *strs_data;
|
|
size_t strs_data_len;
|
|
size_t strs_data_cap;
|
|
size_t strs_data_max_len;
|
|
|
|
/* lookup index for each unique string in strings set */
|
|
struct hashmap *strs_hash;
|
|
};
|
|
|
|
static size_t strset_hash_fn(long key, void *ctx)
|
|
{
|
|
const struct strset *s = ctx;
|
|
const char *str = s->strs_data + key;
|
|
|
|
return str_hash(str);
|
|
}
|
|
|
|
static bool strset_equal_fn(long key1, long key2, void *ctx)
|
|
{
|
|
const struct strset *s = ctx;
|
|
const char *str1 = s->strs_data + key1;
|
|
const char *str2 = s->strs_data + key2;
|
|
|
|
return strcmp(str1, str2) == 0;
|
|
}
|
|
|
|
struct strset *strset__new(size_t max_data_sz, const char *init_data, size_t init_data_sz)
|
|
{
|
|
struct strset *set = calloc(1, sizeof(*set));
|
|
struct hashmap *hash;
|
|
int err = -ENOMEM;
|
|
|
|
if (!set)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
hash = hashmap__new(strset_hash_fn, strset_equal_fn, set);
|
|
if (IS_ERR(hash))
|
|
goto err_out;
|
|
|
|
set->strs_data_max_len = max_data_sz;
|
|
set->strs_hash = hash;
|
|
|
|
if (init_data) {
|
|
long off;
|
|
|
|
set->strs_data = malloc(init_data_sz);
|
|
if (!set->strs_data)
|
|
goto err_out;
|
|
|
|
memcpy(set->strs_data, init_data, init_data_sz);
|
|
set->strs_data_len = init_data_sz;
|
|
set->strs_data_cap = init_data_sz;
|
|
|
|
for (off = 0; off < set->strs_data_len; off += strlen(set->strs_data + off) + 1) {
|
|
/* hashmap__add() returns EEXIST if string with the same
|
|
* content already is in the hash map
|
|
*/
|
|
err = hashmap__add(hash, off, off);
|
|
if (err == -EEXIST)
|
|
continue; /* duplicate */
|
|
if (err)
|
|
goto err_out;
|
|
}
|
|
}
|
|
|
|
return set;
|
|
err_out:
|
|
strset__free(set);
|
|
return ERR_PTR(err);
|
|
}
|
|
|
|
void strset__free(struct strset *set)
|
|
{
|
|
if (IS_ERR_OR_NULL(set))
|
|
return;
|
|
|
|
hashmap__free(set->strs_hash);
|
|
free(set->strs_data);
|
|
free(set);
|
|
}
|
|
|
|
size_t strset__data_size(const struct strset *set)
|
|
{
|
|
return set->strs_data_len;
|
|
}
|
|
|
|
const char *strset__data(const struct strset *set)
|
|
{
|
|
return set->strs_data;
|
|
}
|
|
|
|
static void *strset_add_str_mem(struct strset *set, size_t add_sz)
|
|
{
|
|
return libbpf_add_mem(&set->strs_data, &set->strs_data_cap, 1,
|
|
set->strs_data_len, set->strs_data_max_len, add_sz);
|
|
}
|
|
|
|
static long strset_str_append(struct strset *set, const char *s)
|
|
{
|
|
uintptr_t old_data = (uintptr_t)set->strs_data;
|
|
size_t old_data_len = set->strs_data_len;
|
|
uintptr_t old_s = (uintptr_t)s;
|
|
long len = strlen(s) + 1;
|
|
void *p;
|
|
|
|
/*
|
|
* Hashmap keys are always offsets within set->strs_data, so to even
|
|
* look up some string from the "outside", we need to first append it
|
|
* at the end, so that it can be addressed with an offset. Luckily,
|
|
* until set->strs_data_len is incremented, that string is just a piece
|
|
* of garbage for the rest of the code, so no harm, no foul. On the
|
|
* other hand, if the string is unique, it's already appended and
|
|
* ready to be used, only a simple set->strs_data_len increment away.
|
|
*/
|
|
p = strset_add_str_mem(set, len);
|
|
if (!p)
|
|
return -ENOMEM;
|
|
|
|
/*
|
|
* The set->strs_data might have reallocated and if 's' pointed
|
|
* to an internal string within the old buffer, then it became
|
|
* dangling and needs to be reconstructed before the copy.
|
|
*/
|
|
if (old_data && old_data != (uintptr_t)set->strs_data &&
|
|
old_s >= old_data && old_s < old_data + old_data_len)
|
|
s = set->strs_data + (old_s - old_data);
|
|
|
|
memcpy(p, s, len);
|
|
|
|
return len;
|
|
}
|
|
|
|
/* Find string offset that corresponds to a given string *s*.
|
|
* Returns:
|
|
* - >0 offset into string data, if string is found;
|
|
* - -ENOENT, if string is not in the string data;
|
|
* - <0, on any other error.
|
|
*/
|
|
int strset__find_str(struct strset *set, const char *s)
|
|
{
|
|
long old_off, new_off, len;
|
|
|
|
len = strset_str_append(set, s);
|
|
if (len < 0)
|
|
return len;
|
|
|
|
new_off = set->strs_data_len;
|
|
|
|
if (hashmap__find(set->strs_hash, new_off, &old_off))
|
|
return old_off;
|
|
|
|
return -ENOENT;
|
|
}
|
|
|
|
/* Add a string s to the string data. If the string already exists, return its
|
|
* offset within string data.
|
|
* Returns:
|
|
* - > 0 offset into string data, on success;
|
|
* - < 0, on error.
|
|
*/
|
|
int strset__add_str(struct strset *set, const char *s)
|
|
{
|
|
long old_off, new_off, len;
|
|
int err;
|
|
|
|
len = strset_str_append(set, s);
|
|
if (len < 0)
|
|
return len;
|
|
|
|
new_off = set->strs_data_len;
|
|
|
|
/* Now attempt to add the string, but only if the string with the same
|
|
* contents doesn't exist already (HASHMAP_ADD strategy). If such
|
|
* string exists, we'll get its offset in old_off (that's old_key).
|
|
*/
|
|
err = hashmap__insert(set->strs_hash, new_off, new_off,
|
|
HASHMAP_ADD, &old_off, NULL);
|
|
if (err == -EEXIST)
|
|
return old_off; /* duplicated string, return existing offset */
|
|
if (err)
|
|
return err;
|
|
|
|
set->strs_data_len += len; /* new unique string, adjust data length */
|
|
return new_off;
|
|
}
|