Skip to content

Commit

Permalink
libdrgn: debug_info: match vmlinux by version if build ID is not avai…
Browse files Browse the repository at this point in the history
…lable

Since the module API was introduced, Program.load_debug_info() and the
drgn CLI's -s option match strictly based on build IDs. This fails when
the build ID is not available, specifically in the case of Linux kernel
core dumps without a usable build ID in VMCOREINFO (old versions and a
few buggy stable versions).

Before the module API, Program.load_debug_info() and -s used any vmlinux
file given to them. This caused confusion when the wrong file was given,
so we don't want to bring that behavior back. Instead, let's look for a
vmlinux file matching the Linux version from VMCOREINFO.

Fixes #464.

Fixes: 4e83130 ("Introduce module and debug info finder APIs")
Signed-off-by: Omar Sandoval <osandov@osandov.com>
  • Loading branch information
osandov committed Jan 28, 2025
1 parent 191a4d5 commit d149324
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 2 deletions.
88 changes: 87 additions & 1 deletion libdrgn/debug_info.c
Original file line number Diff line number Diff line change
Expand Up @@ -4968,6 +4968,80 @@ load_debug_info_try_provided_supplementary_files(struct drgn_module *module,
DRGN_MODULE_FILE_WANT_SUPPLEMENTARY);
}

static struct drgn_error *
load_debug_info_try_provided_vmlinux(struct drgn_module *module,
struct load_debug_info_state *state)
{
struct drgn_error *err;
struct drgn_program *prog = module->prog;
bool logged_trying = false;
for (auto it = load_debug_info_provided_table_first(&state->provided);
it.entry;
it = load_debug_info_provided_table_next(it)) {
vector_for_each(load_debug_info_file_vector, file,
&it.entry->files) {
int r = elf_is_vmlinux(file->elf);
if (r < 0) {
drgn_log_debug(prog, "%s: %s", file->path,
elf_errmsg(-1));
}
if (r <= 0)
continue;

if (!logged_trying) {
drgn_module_try_files_log(module,
"(Linux version %s): trying provided files for",
prog->vmcoreinfo.osrelease);
logged_trying = true;
}

const char *release;
ssize_t release_len =
elf_vmlinux_release(file->elf, &release);
if (release_len < 0) {
drgn_log_debug(prog, "%s: %s", file->path,
elf_errmsg(-1));
continue;
} else if (release_len == 0) {
drgn_log_debug(prog, "%s: %s Linux version not found",
module->name, file->path);
continue;
}

if (strlen(prog->vmcoreinfo.osrelease) == release_len
&& memcmp(release, prog->vmcoreinfo.osrelease,
release_len) == 0) {
drgn_log_debug(prog, "%s: %s Linux version matches",
module->name, file->path);
} else {
drgn_log_debug(prog,
"%s: %s Linux version (%.*s) does not match",
module->name, file->path,
release_len > INT_MAX
? INT_MAX : (int)release_len,
release);
continue;
}

if (!it.entry->matched) {
state->unmatched_provided--;
it.entry->matched = true;
}

err = drgn_module_try_file_internal(module, file->path,
file->fd, true,
NULL);
file->fd = -1;
if (err)
return err;
if (module->loaded_file_status != DRGN_MODULE_FILE_WANT
&& module->debug_file_status != DRGN_MODULE_FILE_WANT)
break;
}
}
return NULL;
}

static struct drgn_error *
load_debug_info_try_provided_files(struct drgn_module *module,
struct load_debug_info_state *state)
Expand All @@ -4981,7 +5055,7 @@ load_debug_info_try_provided_files(struct drgn_module *module,
const void *build_id;
size_t build_id_len;
drgn_module_build_id(module, &build_id, &build_id_len);
if (build_id_len != 0) {
if (build_id_len > 0) {
// Look up the provided file even if we don't need it so that it
// counts as matched.
struct load_debug_info_provided *provided =
Expand All @@ -5006,6 +5080,18 @@ load_debug_info_try_provided_files(struct drgn_module *module,
return err;
}
}
} else if (module->prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL
&& drgn_module_kind(module) == DRGN_MODULE_MAIN) {
// Before Linux kernel commit 0935288c6e00 ("kdump: append
// kernel build-id string to VMCOREINFO") (in v5.9) and in a few
// broken stable versions (see
// ignore_broken_vmcoreinfo_build_id()), we can't get the
// vmlinux build ID from a kernel core dump. Fall back to
// checking every provided file for a vmlinux file with a
// matching version.
err = load_debug_info_try_provided_vmlinux(module, state);
if (err)
return err;
}
return NULL;
}
Expand Down
100 changes: 100 additions & 0 deletions libdrgn/elf_file.c
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ struct drgn_error *drgn_elf_file_create(struct drgn_module *module,
// We consider a file to be vmlinux if it has an
// .init.text section and is not relocatable
// (which excludes kernel modules).
// Keep this in sync with elf_is_vmlinux().
file->is_vmlinux = ehdr->e_type != ET_REL;
index = DRGN_SECTION_INDEX_NUM;
} else {
Expand Down Expand Up @@ -761,3 +762,102 @@ bool drgn_elf_file_address_range(struct drgn_elf_file *file,
end_ret);
}
}

// Keep this in sync with drgn_elf_file_create().
int elf_is_vmlinux(Elf *elf)
{
GElf_Ehdr ehdr_mem, *ehdr = gelf_getehdr(elf, &ehdr_mem);
if (!ehdr)
return -1;

if (ehdr->e_type == ET_REL)
return 0;

size_t shstrndx;
if (elf_getshdrstrndx(elf, &shstrndx))
return -1;
Elf_Scn *scn = NULL;
while ((scn = elf_nextscn(elf, scn))) {
GElf_Shdr shdr_mem, *shdr = gelf_getshdr(scn, &shdr_mem);
if (!shdr)
return -1;

if (shdr->sh_type != SHT_PROGBITS)
continue;

const char *scnname = elf_strptr(elf, shstrndx, shdr->sh_name);
if (!scnname)
return -1;

if (strcmp(scnname, ".init.text") == 0)
return 1;
}
return 0;
}

ssize_t elf_vmlinux_release(Elf *elf, const char **ret)
{
Elf_Scn *scn = NULL;
while ((scn = elf_nextscn(elf, scn))) {
GElf_Shdr shdr_mem, *shdr = gelf_getshdr(scn, &shdr_mem);
if (!shdr)
return -1;

if (shdr->sh_type != SHT_SYMTAB || shdr->sh_entsize == 0)
continue;

Elf_Data *data = elf_getdata(scn, NULL);
if (!data)
return -1;

size_t num_syms = shdr->sh_size / shdr->sh_entsize;
for (size_t i = 0; i < num_syms; i++) {
GElf_Sym sym_mem, *sym = gelf_getsym(data, i, &sym_mem);
if (!sym)
return -1;

static const char prefix[] = "Linux version ";

if (GELF_ST_TYPE(sym->st_info) != STT_OBJECT
|| GELF_ST_BIND(sym->st_info) != STB_GLOBAL
|| sym->st_size < sizeof(prefix) - 1)
continue;

const char *name = elf_strptr(elf, shdr->sh_link,
sym->st_name);
if (!name)
return -1;
if (strcmp(name, "linux_banner") != 0)
continue;

GElf_Shdr sym_shdr_mem, *sym_shdr =
gelf_getshdr(elf_getscn(elf, sym->st_shndx),
&sym_shdr_mem);
if (!sym_shdr)
return -1;

int64_t offset = sym_shdr->sh_offset
+ sym->st_value - sym_shdr->sh_addr;
Elf_Data *banner_data =
elf_getdata_rawchunk(elf, offset, sym->st_size,
ELF_T_BYTE);
if (!banner_data)
return -1;

if (memcmp(banner_data->d_buf, prefix,
sizeof(prefix) - 1) != 0)
return 0;

const char *release = (const char *)banner_data->d_buf
+ (sizeof(prefix) - 1);
const char *space =
memchr(release, ' ',
banner_data->d_size - (sizeof(prefix) - 1));
if (!space)
return 0;
*ret = release;
return space - release;
}
}
return 0;
}
15 changes: 15 additions & 0 deletions libdrgn/elf_file.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,21 @@ drgn_elf_file_section_buffer_read(struct drgn_elf_file_section_buffer *buffer,
bool drgn_elf_file_address_range(struct drgn_elf_file *file,
uint64_t *start_ret, uint64_t *end_ret);

/**
* Return whether an ELF file is a vmlinux file.
*
* @return > 0 if the file is vmlinux, 0 if it is not, < 0 on libelf error.
*/
int elf_is_vmlinux(Elf *elf);

/**
* Get the Linux release from a vmlinux file.
*
* @param[out] ret Returned release.
* @return Length of @p ret on success, 0 if not found, < 0 on libelf error.
*/
ssize_t elf_vmlinux_release(Elf *elf, const char **ret);

/** @} */

#endif /* DRGN_ELF_FILE_H */
15 changes: 14 additions & 1 deletion tests/linux_kernel/test_debug_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import os

from drgn import Program, RelocatableModule
from drgn import MainModule, Program, RelocatableModule
from drgn.helpers.linux.module import find_module
from tests import modifyenv
from tests.linux_kernel import LinuxKernelTestCase, skip_unless_have_test_kmod
Expand All @@ -20,6 +20,19 @@ def iter_proc_modules():
yield tokens[0], int(tokens[5], 16)


class TestLoadDebugInfo(LinuxKernelTestCase):
def test_no_build_id(self):
prog = Program()
prog.set_kernel()
for module, _ in self.prog.loaded_modules():
if isinstance(module, MainModule):
module.build_id = None
break
else:
self.fail("main module not found")
prog.load_debug_info([self.prog.main_module().debug_file_path])


class TestModule(LinuxKernelTestCase):
def test_loaded_modules(self):
expected = [("kernel", None), *iter_proc_modules()]
Expand Down

0 comments on commit d149324

Please sign in to comment.