Print this page
OS-4904 mdb can't print types from an object file with ctf
Reviewed by: Patrick Mooney <patrick.mooney@joyent.com>
OS-4341 libproc should also check the GNU build id in addition to the debug link
Reviewed by: Patrick Mooney <patrick.f.mooney@gmail.com>
Reviewed by: Joshua M. Clulow <jmc@joyent.com>
OS-4050 libproc reads .gnu_debuglink padding incorrectly
Reviewed by: Jerry Jelinek <jerry.jelinek@joyent.com>
OS-3780 libproc could know about .gnu_debuglink for remote symbol tables
Reviewed by: Jerry Jelinek <jerry.jelinek@joyent.com>

@@ -19,11 +19,11 @@
  * CDDL HEADER END
  */
 
 /*
  * Copyright (c) 1997, 2010, Oracle and/or its affiliates. All rights reserved.
- * Copyright (c) 2013, Joyent, Inc. All rights reserved.
+ * Copyright 2016 Joyent, Inc.
  * Copyright (c) 2013 by Delphix. All rights reserved.
  */
 
 #include <assert.h>
 #include <stdio.h>

@@ -41,10 +41,11 @@
 #include <limits.h>
 #include <libgen.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <sys/sysmacros.h>
+#include <sys/crc32.h>
 
 #include "libproc.h"
 #include "Pcontrol.h"
 #include "Putil.h"
 #include "Psymtab_machelf.h"

@@ -59,18 +60,35 @@
     uintptr_t);
 #ifdef _LP64
 static int read_ehdr64(struct ps_prochandle *, Elf64_Ehdr *, uint_t *,
     uintptr_t);
 #endif
+static uint32_t psym_crc32[] = { CRC32_TABLE };
 
 #define DATA_TYPES      \
         ((1 << STT_OBJECT) | (1 << STT_FUNC) | \
         (1 << STT_COMMON) | (1 << STT_TLS))
 #define IS_DATA_TYPE(tp)        (((1 << (tp)) & DATA_TYPES) != 0)
 
 #define MA_RWX  (MA_READ | MA_WRITE | MA_EXEC)
 
+/*
+ * Minimum and maximum length of a build-id that we'll accept. Generally it's a
+ * 20 byte SHA1 and it's expected that the first byte (which is two ascii
+ * characters) indicates a directory and the remaining bytes become the file
+ * name. Therefore, our minimum length is at least 2 bytes (one for the
+ * directory and one for the name) and the max is a bit over the minimum -- 64,
+ * just in case folks do something odd. The string length is three times the max
+ * length. This accounts for the fact that each byte is two characters, a null
+ * terminator, and the directory '/' character.
+ */
+#define MINBUILDID      2
+#define MAXBUILDID      64
+#define BUILDID_STRLEN  (3*MAXBUILDID)
+#define BUILDID_NAME    ".note.gnu.build-id"
+#define DBGLINK_NAME    ".gnu_debuglink"
+
 typedef enum {
         PRO_NATURAL,
         PRO_BYADDR,
         PRO_BYNAME
 } pr_order_t;

@@ -182,10 +200,11 @@
         list_link(fptr, &P->file_head);
         (void) strcpy(fptr->file_pname, mptr->map_pmap.pr_mapname);
         mptr->map_file = fptr;
         fptr->file_ref = 1;
         fptr->file_fd = -1;
+        fptr->file_dbgfile = -1;
         P->num_files++;
 
         /*
          * To figure out which map_info_t instances correspond to the mappings
          * for this load object we try to obtain the start and end address

@@ -272,10 +291,14 @@
                         (void) elf_end(fptr->file_elf);
                 if (fptr->file_elfmem != NULL)
                         free(fptr->file_elfmem);
                 if (fptr->file_fd >= 0)
                         (void) close(fptr->file_fd);
+                if (fptr->file_dbgelf)
+                        (void) elf_end(fptr->file_dbgelf);
+                if (fptr->file_dbgfile >= 0)
+                        (void) close(fptr->file_dbgfile);
                 if (fptr->file_ctfp) {
                         ctf_close(fptr->file_ctfp);
                         free(fptr->file_ctf_buf);
                 }
                 if (fptr->file_saddrs)

@@ -817,18 +840,32 @@
 
 ctf_file_t *
 Plmid_to_ctf(struct ps_prochandle *P, Lmid_t lmid, const char *name)
 {
         map_info_t *mptr;
-        file_info_t *fptr;
+        file_info_t *fptr = NULL;
 
         if (name == PR_OBJ_EVERY)
                 return (NULL);
 
+        /*
+         * While most idle files are all ELF objects, not all of them have
+         * mapping information available. There's nothing which would make
+         * sense to fake up for ET_REL. Instead, if we're being asked for their
+         * executable object and we know that the information is valid and they
+         * only have a single file, we jump straight to that file pointer.
+         */
+        if (P->state == PS_IDLE && name == PR_OBJ_EXEC && P->info_valid == 1 &&
+            P->num_files == 1 && P->mappings == NULL) {
+                fptr = list_next(&P->file_head);
+        }
+
+        if (fptr == NULL) {
         if ((mptr = object_name_to_map(P, lmid, name)) == NULL ||
             (fptr = mptr->map_file) == NULL)
                 return (NULL);
+        }
 
         return (Pbuild_file_ctf(P, fptr));
 }
 
 ctf_file_t *

@@ -1565,10 +1602,229 @@
 
         return (elf);
 }
 
 /*
+ * Try and find the file described by path in the file system and validate that
+ * it matches our CRC before we try and process it for symbol information. If we
+ * instead have an ELF data section, then that means we're checking a build-id
+ * section instead. In that case we just need to find and bcmp the corresponding
+ * section.
+ *
+ * Before we validate if it's a valid CRC or data section, we check to ensure
+ * that it's a normal file and not anything else.
+ */
+static boolean_t
+build_alt_debug(file_info_t *fptr, const char *path, uint32_t crc,
+    Elf_Data *data)
+{
+        int fd;
+        struct stat st;
+        Elf *elf;
+        Elf_Scn *scn;
+        GElf_Shdr symshdr, strshdr;
+        Elf_Data *symdata, *strdata;
+        boolean_t valid;
+        uint32_t c = -1U;
+
+        if ((fd = open(path, O_RDONLY)) < 0)
+                return (B_FALSE);
+
+        if (fstat(fd, &st) != 0) {
+                (void) close(fd);
+                return (B_FALSE);
+        }
+
+        if (S_ISREG(st.st_mode) == 0) {
+                (void) close(fd);
+                return (B_FALSE);
+        }
+
+        /*
+         * Only check the CRC if we've come here through a GNU debug link
+         * section as opposed to the build id. This is indicated by having the
+         * value of data be NULL.
+         */
+        if (data == NULL) {
+                for (;;) {
+                        char buf[4096];
+                        ssize_t ret = read(fd, buf, sizeof (buf));
+                        if (ret == -1) {
+                                if (ret == EINTR)
+                                        continue;
+                                (void) close(fd);
+                                return (B_FALSE);
+                        }
+                        if (ret == 0) {
+                                c = ~c;
+                                if (c != crc) {
+                                        dprintf("crc mismatch, found: 0x%x "
+                                            "expected 0x%x\n", c, crc);
+                                        (void) close(fd);
+                                        return (B_FALSE);
+                                }
+                                break;
+                        }
+                        CRC32(c, buf, ret, c, psym_crc32);
+                }
+        }
+
+        elf = elf_begin(fd, ELF_C_READ, NULL);
+        if (elf == NULL) {
+                (void) close(fd);
+                return (B_FALSE);
+        }
+
+        if (elf_kind(elf) != ELF_K_ELF) {
+                goto fail;
+        }
+
+        /*
+         * If we have a data section, that indicates we have a build-id which
+         * means we need to find the corresponding build-id section and compare
+         * it.
+         */
+        scn = NULL;
+        valid = B_FALSE;
+        for (scn = elf_nextscn(elf, scn); data != NULL && scn != NULL;
+            scn = elf_nextscn(elf, scn)) {
+                GElf_Shdr hdr;
+                Elf_Data *ntdata;
+
+                if (gelf_getshdr(scn, &hdr) == NULL)
+                        goto fail;
+
+                if (hdr.sh_type != SHT_NOTE)
+                        continue;
+
+                if ((ntdata = elf_getdata(scn, NULL)) == NULL)
+                        goto fail;
+
+                /*
+                 * First verify the data section sizes are equal, then the
+                 * section name. If that's all true, then we can just do a bcmp.
+                 */
+                if (data->d_size != ntdata->d_size)
+                        continue;
+
+                dprintf("found corresponding section in alternate file\n");
+                if (bcmp(ntdata->d_buf, data->d_buf, data->d_size) != 0)
+                        goto fail;
+
+                valid = B_TRUE;
+                break;
+        }
+        if (data != NULL && valid == B_FALSE) {
+                dprintf("failed to find a matching %s section in %s\n",
+                    BUILDID_NAME, path);
+                goto fail;
+        }
+
+
+        /*
+         * Do two passes, first see if we have a symbol header, then see if we
+         * can find the corresponding linked string table.
+         */
+        scn = NULL;
+        for (scn = elf_nextscn(elf, scn); scn != NULL;
+            scn = elf_nextscn(elf, scn)) {
+
+                if (gelf_getshdr(scn, &symshdr) == NULL)
+                        goto fail;
+
+                if (symshdr.sh_type != SHT_SYMTAB)
+                        continue;
+
+                if ((symdata = elf_getdata(scn, NULL)) == NULL)
+                        goto fail;
+
+                break;
+        }
+        if (scn == NULL)
+                goto fail;
+
+        if ((scn = elf_getscn(elf, symshdr.sh_link)) == NULL)
+                goto fail;
+
+        if (gelf_getshdr(scn, &strshdr) == NULL)
+                goto fail;
+
+        if ((strdata = elf_getdata(scn, NULL)) == NULL)
+                goto fail;
+
+        fptr->file_symtab.sym_data_pri = symdata;
+        fptr->file_symtab.sym_symn += symshdr.sh_size / symshdr.sh_entsize;
+        fptr->file_symtab.sym_strs = strdata->d_buf;
+        fptr->file_symtab.sym_strsz = strdata->d_size;
+        fptr->file_symtab.sym_hdr_pri = symshdr;
+        fptr->file_symtab.sym_strhdr = strshdr;
+
+        dprintf("successfully loaded additional debug symbols for %s from %s\n",
+            fptr->file_rname, path);
+
+        fptr->file_dbgfile = fd;
+        fptr->file_dbgelf = elf;
+        return (B_TRUE);
+fail:
+        (void) elf_end(elf);
+        (void) close(fd);
+        return (B_FALSE);
+}
+
+/*
+ * We're here because the object in question has no symbol information, that's a
+ * bit unfortunate. However, we've found that there's a .gnu_debuglink sitting
+ * around. By convention that means that given the current location of the
+ * object on disk, and the debug name that we found in the binary we need to
+ * search the following locations for a matching file.
+ *
+ * <dirname>/.debug/<debug-name>
+ * /usr/lib/debug/<dirname>/<debug-name>
+ *
+ * In the future, we should consider supporting looking in the prefix's
+ * lib/debug directory for a matching object or supporting an arbitrary user
+ * defined set of places to look.
+ */
+static void
+find_alt_debuglink(file_info_t *fptr, const char *name, uint32_t crc)
+{
+        boolean_t r;
+        char *dup = NULL, *path = NULL, *dname;
+
+        dprintf("find_alt_debug: looking for %s, crc 0x%x\n", name, crc);
+        if (fptr->file_rname == NULL) {
+                dprintf("find_alt_debug: encountered null file_rname\n");
+                return;
+        }
+
+        dup = strdup(fptr->file_rname);
+        if (dup == NULL)
+                return;
+
+        dname = dirname(dup);
+        if (asprintf(&path, "%s/.debug/%s", dname, name) != -1) {
+                dprintf("attempting to load alternate debug information "
+                    "from %s\n", path);
+                r = build_alt_debug(fptr, path, crc, NULL);
+                free(path);
+                if (r == B_TRUE)
+                        goto out;
+        }
+
+        if (asprintf(&path, "/usr/lib/debug/%s/%s", dname, name) != -1) {
+                dprintf("attempting to load alternate debug information "
+                    "from %s\n", path);
+                r = build_alt_debug(fptr, path, crc, NULL);
+                free(path);
+                if (r == B_TRUE)
+                        goto out;
+        }
+out:
+        free(dup);
+}
+
+/*
  * Build the symbol table for the given mapped file.
  */
 void
 Pbuild_file_symtab(struct ps_prochandle *P, file_info_t *fptr)
 {

@@ -1585,11 +1841,12 @@
 
         struct {
                 GElf_Shdr c_shdr;
                 Elf_Data *c_data;
                 const char *c_name;
-        } *cp, *cache = NULL, *dyn = NULL, *plt = NULL, *ctf = NULL;
+        } *cp, *cache = NULL, *dyn = NULL, *plt = NULL, *ctf = NULL,
+        *dbglink = NULL, *buildid = NULL;
 
         if (fptr->file_init)
                 return; /* We've already processed this file */
 
         /*

@@ -1811,14 +2068,158 @@
                                 dprintf("Bad sh_link %d for "
                                     "CTF\n", shp->sh_link);
                                 continue;
                         }
                         ctf = cp;
+                } else if (strcmp(cp->c_name, BUILDID_NAME) == 0) {
+                        dprintf("Found a %s section for %s\n", BUILDID_NAME,
+                            fptr->file_rname);
+                        /* The ElfXX_Nhdr is 32/64-bit neutral */
+                        if (cp->c_shdr.sh_type == SHT_NOTE &&
+                            cp->c_data->d_buf != NULL &&
+                            cp->c_data->d_size >= sizeof (Elf32_Nhdr)) {
+                                Elf32_Nhdr *hdr = cp->c_data->d_buf;
+                                if (hdr->n_type != 3)
+                                        continue;
+                                if (hdr->n_namesz != 4)
+                                        continue;
+                                if (hdr->n_descsz < MINBUILDID)
+                                        continue;
+                                /* Set a reasonable upper bound */
+                                if (hdr->n_descsz > MAXBUILDID) {
+                                        dprintf("Skipped %s as too large "
+                                            "(%ld)\n", BUILDID_NAME,
+                                            (unsigned long)hdr->n_descsz);
+                                        continue;
                 }
+
+                                if (cp->c_data->d_size < sizeof (hdr) +
+                                    hdr->n_namesz + hdr->n_descsz)
+                                        continue;
+                                buildid = cp;
         }
+                } else if (strcmp(cp->c_name, DBGLINK_NAME) == 0) {
+                        dprintf("found %s section for %s\n", DBGLINK_NAME,
+                            fptr->file_rname);
+                        /*
+                         * Let's make sure of a few things before we do this.
+                         */
+                        if (cp->c_shdr.sh_type == SHT_PROGBITS &&
+                            cp->c_data->d_buf != NULL &&
+                            cp->c_data->d_size) {
+                                dbglink = cp;
+                        }
+                }
+        }
 
         /*
+         * If we haven't found any symbol table information and we have found
+         * either a .note.gnu.build-id or a .gnu_debuglink, it's time to try and
+         * figure out where we might find this. Originally, GNU used the
+         * .gnu_debuglink solely, but then they added a .note.gnu.build-id. The
+         * build-id is some size, usually 16 or 20 bytes, often a SHA1 sum of
+         * the old, but not present file. All that you have to do to compare
+         * things is see if the sections are less, in theory saving you from
+         * doing lots of expensive I/O.
+         *
+         * For the .note.gnu.build-id, we're going to check a few things before
+         * using it, first that the name is 4 bytes, and is GNU and that the
+         * type is 3, which they say is the build-id identifier.
+         *
+         * To verify that the elf data for the .gnu_debuglink seems somewhat
+         * sane, eg. the elf data should be a string, so we want to verify we
+         * have a null-terminator.
+         */
+        if (fptr->file_symtab.sym_data_pri == NULL && buildid != NULL) {
+                int i, bo;
+                uint8_t *dp;
+                char buf[BUILDID_STRLEN], *path;
+                Elf32_Nhdr *hdr = buildid->c_data->d_buf;
+
+                /*
+                 * This was checked for validity when assigning the buildid
+                 * variable.
+                 */
+                bzero(buf, sizeof (buf));
+                dp = (uint8_t *)((uintptr_t)hdr + sizeof (*hdr) +
+                    hdr->n_namesz);
+                for (i = 0, bo = 0; i < hdr->n_descsz; i++, bo += 2, dp++) {
+                        assert(sizeof (buf) - bo > 0);
+
+                        /*
+                         * Recall that the build-id is structured as a series of
+                         * bytes. However, the first two characters are supposed
+                         * to represent a directory. Hence, once we reach offset
+                         * two, we insert a '/' character.
+                         */
+                        if (bo == 2) {
+                                buf[bo] = '/';
+                                bo++;
+                        }
+                        (void) snprintf(buf + bo, sizeof (buf) - bo, "%2x",
+                            *dp);
+                }
+
+                if (asprintf(&path, "/usr/lib/debug/.build-id/%s.debug",
+                    buf) != -1) {
+                        boolean_t r;
+                        dprintf("attempting to find build id alternate debug "
+                            "file at %s\n", path);
+                        r = build_alt_debug(fptr, path, 0, buildid->c_data);
+                        dprintf("attempt %s\n", r == B_TRUE ?
+                            "succeeded" : "failed");
+                        free(path);
+                } else {
+                        dprintf("failed to construct build id path: %s\n",
+                            strerror(errno));
+                }
+        }
+
+        if (fptr->file_symtab.sym_data_pri == NULL && dbglink != NULL) {
+                char *c = dbglink->c_data->d_buf;
+                size_t i;
+                boolean_t found = B_FALSE;
+                Elf_Data *ed = dbglink->c_data;
+                uint32_t crc;
+
+                for (i = 0; i < ed->d_size; i++) {
+                        if (c[i] == '\0') {
+                                uintptr_t off;
+                                dprintf("got .gnu_debuglink terminator at "
+                                    "offset %lu\n", (unsigned long)i);
+                                /*
+                                 * After the null terminator, there should be
+                                 * padding, followed by a 4 byte CRC of the
+                                 * file. If we don't see this, we're going to
+                                 * assume this is bogus.
+                                 */
+                                if ((i % sizeof (uint32_t)) == 0) {
+                                        i += 4;
+                                } else {
+                                        i += sizeof (uint32_t) -
+                                            (i % sizeof (uint32_t));
+                                }
+                                if (i + sizeof (uint32_t) ==
+                                    dbglink->c_data->d_size) {
+                                        found = B_TRUE;
+                                        off = (uintptr_t)ed->d_buf + i;
+                                        crc = *(uint32_t *)off;
+                                } else {
+                                        dprintf(".gnu_debuglink size mismatch, "
+                                            "expected: %lu, found: %lu\n",
+                                            (unsigned long)i,
+                                            (unsigned long)ed->d_size);
+                                }
+                                break;
+                        }
+                }
+
+                if (found == B_TRUE)
+                        find_alt_debuglink(fptr, dbglink->c_data->d_buf, crc);
+        }
+
+        /*
          * At this point, we've found all the symbol tables we're ever going
          * to find: the ones in the loop above and possibly the symtab that
          * was included in the core file. Before we perform any lookups, we
          * create sorted versions to optimize for lookups.
          */

@@ -1941,11 +2342,17 @@
         if (fptr->file_elfmem != NULL) {
                 free(fptr->file_elfmem);
                 fptr->file_elfmem = NULL;
         }
         (void) close(fptr->file_fd);
+        if (fptr->file_dbgelf != NULL)
+                (void) elf_end(fptr->file_dbgelf);
+        fptr->file_dbgelf = NULL;
+        if (fptr->file_dbgfile >= 0)
+                (void) close(fptr->file_dbgfile);
         fptr->file_fd = -1;
+        fptr->file_dbgfile = -1;
 }
 
 /*
  * Given a process virtual address, return the map_info_t containing it.
  * If none found, return NULL.