2023-04-17 19:51:35

by Espen Grindhaug

[permalink] [raw]
Subject: [PATCH] libbpf: Improve version handling when attaching uprobe

This change fixes the handling of versions in elf_find_func_offset.
In the previous implementation, we incorrectly assumed that the
version information would be present in the string found in the
string table.

We now look up the correct version string in the version symbol
table before constructing the full name and then comparing.

This patch adds support for both name@version and name@@version to
match output of the various elf parsers.

Signed-off-by: Espen Grindhaug <[email protected]>
---
tools/lib/bpf/libbpf.c | 148 +++++++++++++++++++++++++++++++++++------
1 file changed, 129 insertions(+), 19 deletions(-)

diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index 49cd304ae3bc..ef5e11ce6241 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -10620,31 +10620,94 @@ static int perf_event_uprobe_open_legacy(const char *probe_name, bool retprobe,
}

/* Return next ELF section of sh_type after scn, or first of that type if scn is NULL. */
-static Elf_Scn *elf_find_next_scn_by_type(Elf *elf, int sh_type, Elf_Scn *scn)
+static Elf_Scn *elf_find_next_scn_by_type(Elf *elf, int sh_type, Elf_Scn *scn, size_t *idx)
{
while ((scn = elf_nextscn(elf, scn)) != NULL) {
GElf_Shdr sh;

if (!gelf_getshdr(scn, &sh))
continue;
- if (sh.sh_type == sh_type)
+ if (sh.sh_type == sh_type) {
+ if (idx)
+ *idx = sh.sh_link;
return scn;
+ }
+ }
+ return NULL;
+}
+
+static Elf_Data *elf_find_data_by_type(Elf *elf, int sh_type, size_t *idx)
+{
+ Elf_Scn *scn = elf_find_next_scn_by_type(elf, sh_type, NULL, idx);
+
+ if (scn)
+ return elf_getdata(scn, NULL);
+
+ return NULL;
+}
+
+static Elf64_Verdef *elf_verdef_by_offset(Elf_Data *data, size_t offset)
+{
+ if (offset + sizeof(Elf64_Verdef) > data->d_size)
+ return NULL;
+
+ return (Elf64_Verdef *)((char *) data->d_buf + offset);
+}
+
+static Elf64_Versym *elf_versym_by_idx(Elf_Data *data, size_t idx)
+{
+ if (idx >= data->d_size / sizeof(Elf64_Versym))
+ return NULL;
+
+ return (Elf64_Versym *)(data->d_buf + idx * sizeof(Elf64_Versym));
+}
+
+static Elf64_Verdaux *elf_verdaux_by_offset(Elf_Data *data, size_t offset)
+{
+ if (offset + sizeof(Elf64_Verdaux) > data->d_size)
+ return NULL;
+
+ return (Elf64_Verdaux *)((char *) data->d_buf + offset);
+}
+
+#define ELF_VERSYM_HIDDEN 0x8000
+#define ELF_VERSYM_IDX_MASK 0x7fff
+
+static Elf64_Verdaux *elf_get_verdaux_by_versym(Elf_Data *verdef_data, Elf64_Versym *versym)
+{
+ size_t offset = 0;
+
+ while (offset + sizeof(Elf64_Verdef) <= verdef_data->d_size) {
+ Elf64_Verdef *verdef = elf_verdef_by_offset(verdef_data, offset);
+
+ if (!verdef)
+ break;
+
+ if (verdef->vd_ndx == (*versym & ELF_VERSYM_IDX_MASK))
+ return elf_verdaux_by_offset(verdef_data, offset + verdef->vd_aux);
+
+ if (verdef->vd_next == 0)
+ break;
+
+ offset += verdef->vd_next;
}
return NULL;
}

/* Find offset of function name in the provided ELF object. "binary_path" is
* the path to the ELF binary represented by "elf", and only used for error
- * reporting matters. "name" matches symbol name or name@@LIB for library
- * functions.
+ * reporting matters. "name" matches symbol name, name@LIB or name@@LIB for
+ * library functions.
*/
static long elf_find_func_offset(Elf *elf, const char *binary_path, const char *name)
{
int i, sh_types[2] = { SHT_DYNSYM, SHT_SYMTAB };
bool is_shared_lib, is_name_qualified;
long ret = -ENOENT;
- size_t name_len;
GElf_Ehdr ehdr;
+ Elf_Data *versym_data = NULL;
+ Elf_Data *verdef_data = NULL;
+ size_t verdef_stridx = 0;

if (!gelf_getehdr(elf, &ehdr)) {
pr_warn("elf: failed to get ehdr from %s: %s\n", binary_path, elf_errmsg(-1));
@@ -10654,9 +10717,12 @@ static long elf_find_func_offset(Elf *elf, const char *binary_path, const char *
/* for shared lib case, we do not need to calculate relative offset */
is_shared_lib = ehdr.e_type == ET_DYN;

- name_len = strlen(name);
- /* Does name specify "@@LIB"? */
- is_name_qualified = strstr(name, "@@") != NULL;
+ /* Does name specify "@@LIB" or "@LIB"? */
+ is_name_qualified = strstr(name, "@") != NULL;
+
+ /* Extract version definition and version symbol table */
+ versym_data = elf_find_data_by_type(elf, SHT_GNU_versym, NULL);
+ verdef_data = elf_find_data_by_type(elf, SHT_GNU_verdef, &verdef_stridx);

/* Search SHT_DYNSYM, SHT_SYMTAB for symbol. This search order is used because if
* a binary is stripped, it may only have SHT_DYNSYM, and a fully-statically
@@ -10671,10 +10737,10 @@ static long elf_find_func_offset(Elf *elf, const char *binary_path, const char *
const char *sname;
GElf_Shdr sh;

- scn = elf_find_next_scn_by_type(elf, sh_types[i], NULL);
+ scn = elf_find_next_scn_by_type(elf, sh_types[i], NULL, NULL);
if (!scn) {
pr_debug("elf: failed to find symbol table ELF sections in '%s'\n",
- binary_path);
+ binary_path);
continue;
}
if (!gelf_getshdr(scn, &sh))
@@ -10705,16 +10771,60 @@ static long elf_find_func_offset(Elf *elf, const char *binary_path, const char *
if (!sname)
continue;

- curr_bind = GELF_ST_BIND(sym.st_info);
+ if (is_name_qualified) {
+ Elf64_Versym *versym;
+ Elf64_Verdaux *verdaux;
+ int res;
+ char full_name[256];

- /* User can specify func, func@@LIB or func@@LIB_VERSION. */
- if (strncmp(sname, name, name_len) != 0)
- continue;
- /* ...but we don't want a search for "foo" to match 'foo2" also, so any
- * additional characters in sname should be of the form "@@LIB".
- */
- if (!is_name_qualified && sname[name_len] != '\0' && sname[name_len] != '@')
- continue;
+ /* check that name at least starts with sname before building
+ * the full name
+ */
+ if (strncmp(name, sname, strlen(sname)) != 0)
+ continue;
+
+ if (!versym_data || !verdef_data) {
+ pr_warn("elf: failed to find version definition or version symbol table in '%s'\n",
+ binary_path);
+ break;
+ }
+
+ versym = elf_versym_by_idx(versym_data, idx);
+ if (!versym) {
+ pr_warn("elf: failed to lookup versym for '%s' in '%s'\n",
+ sname, binary_path);
+ continue;
+ }
+
+ verdaux = elf_get_verdaux_by_versym(verdef_data, versym);
+ if (!verdaux) {
+ pr_warn("elf: failed to lookup verdaux for '%s' in '%s'\n",
+ sname, binary_path);
+ continue;
+ }
+
+ res = snprintf(full_name, sizeof(full_name),
+ (*versym & ELF_VERSYM_HIDDEN) ? "%s@%s" :
+ "%s@@%s",
+ sname,
+ elf_strptr(elf, verdef_stridx,
+ verdaux->vda_name));
+
+ if (res < 0 || res >= sizeof(full_name)) {
+ pr_warn("elf: failed to build full name for '%s' in '%s'\n",
+ sname, binary_path);
+ continue;
+ }
+
+ if (strcmp(full_name, name) != 0)
+ continue;
+ } else {
+ /* If name is not qualified, we want to match the symbol name */
+ if (strcmp(sname, name) != 0)
+ continue;
+ }
+
+ curr_bind = GELF_ST_BIND(sym.st_info);

if (ret >= 0) {
/* handle multiple matches */
--
2.34.1


2023-04-20 00:49:17

by Andrii Nakryiko

[permalink] [raw]
Subject: Re: [PATCH] libbpf: Improve version handling when attaching uprobe

cc Alan, can you please take a look, as you've worked on this code previously

On Mon, Apr 17, 2023 at 12:34 PM Espen Grindhaug
<[email protected]> wrote:
>
> This change fixes the handling of versions in elf_find_func_offset.
> In the previous implementation, we incorrectly assumed that the
> version information would be present in the string found in the
> string table.
>
> We now look up the correct version string in the version symbol
> table before constructing the full name and then comparing.
>
> This patch adds support for both name@version and name@@version to
> match output of the various elf parsers.
>
> Signed-off-by: Espen Grindhaug <[email protected]>
> ---

Espen, can you please also add a test to exercise this logic? We can
probably reuse urandom_read_lib and make sure we have versioned
symbols in it, and then try to attach to specific version or
something.

> tools/lib/bpf/libbpf.c | 148 +++++++++++++++++++++++++++++++++++------
> 1 file changed, 129 insertions(+), 19 deletions(-)
>
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index 49cd304ae3bc..ef5e11ce6241 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -10620,31 +10620,94 @@ static int perf_event_uprobe_open_legacy(const char *probe_name, bool retprobe,
> }
>
> /* Return next ELF section of sh_type after scn, or first of that type if scn is NULL. */
> -static Elf_Scn *elf_find_next_scn_by_type(Elf *elf, int sh_type, Elf_Scn *scn)
> +static Elf_Scn *elf_find_next_scn_by_type(Elf *elf, int sh_type, Elf_Scn *scn, size_t *idx)
> {
> while ((scn = elf_nextscn(elf, scn)) != NULL) {
> GElf_Shdr sh;
>
> if (!gelf_getshdr(scn, &sh))
> continue;
> - if (sh.sh_type == sh_type)
> + if (sh.sh_type == sh_type) {
> + if (idx)
> + *idx = sh.sh_link;
> return scn;
> + }
> + }
> + return NULL;
> +}
> +
> +static Elf_Data *elf_find_data_by_type(Elf *elf, int sh_type, size_t *idx)
> +{
> + Elf_Scn *scn = elf_find_next_scn_by_type(elf, sh_type, NULL, idx);
> +
> + if (scn)
> + return elf_getdata(scn, NULL);
> +
> + return NULL;
> +}
> +
> +static Elf64_Verdef *elf_verdef_by_offset(Elf_Data *data, size_t offset)
> +{
> + if (offset + sizeof(Elf64_Verdef) > data->d_size)
> + return NULL;
> +
> + return (Elf64_Verdef *)((char *) data->d_buf + offset);
> +}
> +
> +static Elf64_Versym *elf_versym_by_idx(Elf_Data *data, size_t idx)
> +{
> + if (idx >= data->d_size / sizeof(Elf64_Versym))
> + return NULL;
> +
> + return (Elf64_Versym *)(data->d_buf + idx * sizeof(Elf64_Versym));
> +}
> +
> +static Elf64_Verdaux *elf_verdaux_by_offset(Elf_Data *data, size_t offset)
> +{
> + if (offset + sizeof(Elf64_Verdaux) > data->d_size)
> + return NULL;
> +
> + return (Elf64_Verdaux *)((char *) data->d_buf + offset);
> +}
> +
> +#define ELF_VERSYM_HIDDEN 0x8000
> +#define ELF_VERSYM_IDX_MASK 0x7fff
> +
> +static Elf64_Verdaux *elf_get_verdaux_by_versym(Elf_Data *verdef_data, Elf64_Versym *versym)
> +{
> + size_t offset = 0;
> +
> + while (offset + sizeof(Elf64_Verdef) <= verdef_data->d_size) {
> + Elf64_Verdef *verdef = elf_verdef_by_offset(verdef_data, offset);
> +
> + if (!verdef)
> + break;
> +
> + if (verdef->vd_ndx == (*versym & ELF_VERSYM_IDX_MASK))
> + return elf_verdaux_by_offset(verdef_data, offset + verdef->vd_aux);
> +
> + if (verdef->vd_next == 0)
> + break;
> +
> + offset += verdef->vd_next;
> }
> return NULL;
> }
>
> /* Find offset of function name in the provided ELF object. "binary_path" is
> * the path to the ELF binary represented by "elf", and only used for error
> - * reporting matters. "name" matches symbol name or name@@LIB for library
> - * functions.
> + * reporting matters. "name" matches symbol name, name@LIB or name@@LIB for
> + * library functions.
> */
> static long elf_find_func_offset(Elf *elf, const char *binary_path, const char *name)
> {
> int i, sh_types[2] = { SHT_DYNSYM, SHT_SYMTAB };
> bool is_shared_lib, is_name_qualified;
> long ret = -ENOENT;
> - size_t name_len;
> GElf_Ehdr ehdr;
> + Elf_Data *versym_data = NULL;
> + Elf_Data *verdef_data = NULL;
> + size_t verdef_stridx = 0;
>
> if (!gelf_getehdr(elf, &ehdr)) {
> pr_warn("elf: failed to get ehdr from %s: %s\n", binary_path, elf_errmsg(-1));
> @@ -10654,9 +10717,12 @@ static long elf_find_func_offset(Elf *elf, const char *binary_path, const char *
> /* for shared lib case, we do not need to calculate relative offset */
> is_shared_lib = ehdr.e_type == ET_DYN;
>
> - name_len = strlen(name);
> - /* Does name specify "@@LIB"? */
> - is_name_qualified = strstr(name, "@@") != NULL;
> + /* Does name specify "@@LIB" or "@LIB"? */
> + is_name_qualified = strstr(name, "@") != NULL;
> +
> + /* Extract version definition and version symbol table */
> + versym_data = elf_find_data_by_type(elf, SHT_GNU_versym, NULL);
> + verdef_data = elf_find_data_by_type(elf, SHT_GNU_verdef, &verdef_stridx);
>
> /* Search SHT_DYNSYM, SHT_SYMTAB for symbol. This search order is used because if
> * a binary is stripped, it may only have SHT_DYNSYM, and a fully-statically
> @@ -10671,10 +10737,10 @@ static long elf_find_func_offset(Elf *elf, const char *binary_path, const char *
> const char *sname;
> GElf_Shdr sh;
>
> - scn = elf_find_next_scn_by_type(elf, sh_types[i], NULL);
> + scn = elf_find_next_scn_by_type(elf, sh_types[i], NULL, NULL);
> if (!scn) {
> pr_debug("elf: failed to find symbol table ELF sections in '%s'\n",
> - binary_path);
> + binary_path);
> continue;
> }
> if (!gelf_getshdr(scn, &sh))
> @@ -10705,16 +10771,60 @@ static long elf_find_func_offset(Elf *elf, const char *binary_path, const char *
> if (!sname)
> continue;
>
> - curr_bind = GELF_ST_BIND(sym.st_info);
> + if (is_name_qualified) {
> + Elf64_Versym *versym;
> + Elf64_Verdaux *verdaux;
> + int res;
> + char full_name[256];
>
> - /* User can specify func, func@@LIB or func@@LIB_VERSION. */
> - if (strncmp(sname, name, name_len) != 0)
> - continue;
> - /* ...but we don't want a search for "foo" to match 'foo2" also, so any
> - * additional characters in sname should be of the form "@@LIB".
> - */
> - if (!is_name_qualified && sname[name_len] != '\0' && sname[name_len] != '@')
> - continue;
> + /* check that name at least starts with sname before building
> + * the full name
> + */
> + if (strncmp(name, sname, strlen(sname)) != 0)
> + continue;
> +
> + if (!versym_data || !verdef_data) {
> + pr_warn("elf: failed to find version definition or version symbol table in '%s'\n",
> + binary_path);
> + break;
> + }
> +
> + versym = elf_versym_by_idx(versym_data, idx);
> + if (!versym) {
> + pr_warn("elf: failed to lookup versym for '%s' in '%s'\n",
> + sname, binary_path);
> + continue;
> + }
> +
> + verdaux = elf_get_verdaux_by_versym(verdef_data, versym);
> + if (!verdaux) {
> + pr_warn("elf: failed to lookup verdaux for '%s' in '%s'\n",
> + sname, binary_path);
> + continue;
> + }
> +
> + res = snprintf(full_name, sizeof(full_name),
> + (*versym & ELF_VERSYM_HIDDEN) ? "%s@%s" :
> + "%s@@%s",
> + sname,
> + elf_strptr(elf, verdef_stridx,
> + verdaux->vda_name));
> +
> + if (res < 0 || res >= sizeof(full_name)) {
> + pr_warn("elf: failed to build full name for '%s' in '%s'\n",
> + sname, binary_path);
> + continue;
> + }
> +
> + if (strcmp(full_name, name) != 0)
> + continue;
> + } else {
> + /* If name is not qualified, we want to match the symbol name */
> + if (strcmp(sname, name) != 0)
> + continue;
> + }
> +
> + curr_bind = GELF_ST_BIND(sym.st_info);
>
> if (ret >= 0) {
> /* handle multiple matches */
> --
> 2.34.1
>