From 5fd83771641d15c418f747bd343ba6738d3875f7 Mon Sep 17 00:00:00 2001 From: Cameron Katri Date: Sun, 9 May 2021 14:20:58 -0400 Subject: Import macOS userland adv_cmds-176 basic_cmds-55 bootstrap_cmds-116.100.1 developer_cmds-66 diskdev_cmds-667.40.1 doc_cmds-53.60.1 file_cmds-321.40.3 mail_cmds-35 misc_cmds-34 network_cmds-606.40.1 patch_cmds-17 remote_cmds-63 shell_cmds-216.60.1 system_cmds-880.60.2 text_cmds-106 --- system_cmds/gcore.tproj/convert.c | 1117 ++++++++++++++++++++++ system_cmds/gcore.tproj/convert.h | 24 + system_cmds/gcore.tproj/corefile.c | 852 +++++++++++++++++ system_cmds/gcore.tproj/corefile.h | 71 ++ system_cmds/gcore.tproj/dyld.c | 314 ++++++ system_cmds/gcore.tproj/dyld.h | 35 + system_cmds/gcore.tproj/dyld_shared_cache.c | 108 +++ system_cmds/gcore.tproj/dyld_shared_cache.h | 36 + system_cmds/gcore.tproj/gcore-entitlements.plist | 8 + system_cmds/gcore.tproj/gcore-internal.1 | 201 ++++ system_cmds/gcore.tproj/gcore.1 | 105 ++ system_cmds/gcore.tproj/loader_additions.h | 103 ++ system_cmds/gcore.tproj/main.c | 863 +++++++++++++++++ system_cmds/gcore.tproj/options.h | 62 ++ system_cmds/gcore.tproj/region.h | 133 +++ system_cmds/gcore.tproj/sparse.c | 497 ++++++++++ system_cmds/gcore.tproj/sparse.h | 72 ++ system_cmds/gcore.tproj/threads.c | 81 ++ system_cmds/gcore.tproj/threads.h | 14 + system_cmds/gcore.tproj/utils.c | 421 ++++++++ system_cmds/gcore.tproj/utils.h | 42 + system_cmds/gcore.tproj/vanilla.c | 911 ++++++++++++++++++ system_cmds/gcore.tproj/vanilla.h | 17 + system_cmds/gcore.tproj/vm.c | 493 ++++++++++ system_cmds/gcore.tproj/vm.h | 47 + 25 files changed, 6627 insertions(+) create mode 100644 system_cmds/gcore.tproj/convert.c create mode 100644 system_cmds/gcore.tproj/convert.h create mode 100644 system_cmds/gcore.tproj/corefile.c create mode 100644 system_cmds/gcore.tproj/corefile.h create mode 100644 system_cmds/gcore.tproj/dyld.c create mode 100644 system_cmds/gcore.tproj/dyld.h create mode 100644 system_cmds/gcore.tproj/dyld_shared_cache.c create mode 100644 system_cmds/gcore.tproj/dyld_shared_cache.h create mode 100644 system_cmds/gcore.tproj/gcore-entitlements.plist create mode 100644 system_cmds/gcore.tproj/gcore-internal.1 create mode 100644 system_cmds/gcore.tproj/gcore.1 create mode 100644 system_cmds/gcore.tproj/loader_additions.h create mode 100644 system_cmds/gcore.tproj/main.c create mode 100644 system_cmds/gcore.tproj/options.h create mode 100644 system_cmds/gcore.tproj/region.h create mode 100644 system_cmds/gcore.tproj/sparse.c create mode 100644 system_cmds/gcore.tproj/sparse.h create mode 100644 system_cmds/gcore.tproj/threads.c create mode 100644 system_cmds/gcore.tproj/threads.h create mode 100644 system_cmds/gcore.tproj/utils.c create mode 100644 system_cmds/gcore.tproj/utils.h create mode 100644 system_cmds/gcore.tproj/vanilla.c create mode 100644 system_cmds/gcore.tproj/vanilla.h create mode 100644 system_cmds/gcore.tproj/vm.c create mode 100644 system_cmds/gcore.tproj/vm.h (limited to 'system_cmds/gcore.tproj') diff --git a/system_cmds/gcore.tproj/convert.c b/system_cmds/gcore.tproj/convert.c new file mode 100644 index 0000000..b945733 --- /dev/null +++ b/system_cmds/gcore.tproj/convert.c @@ -0,0 +1,1117 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "convert.h" +#include "corefile.h" +#include "vanilla.h" +#include "threads.h" +#include "vm.h" +#include "dyld_shared_cache.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_GCORE_MAP) || defined(CONFIG_GCORE_CONV) || defined(CONFIG_GCORE_FREF) + +static const void * +mmapfile(int fd, off_t off, off_t *filesize) +{ + struct stat st; + if (-1 == fstat(fd, &st)) + errc(EX_OSERR, errno, "can't stat input file"); + + const size_t size = (size_t)(st.st_size - off); + if ((off_t)size != (st.st_size - off)) + errc(EX_OSERR, EOVERFLOW, "input file too large?"); + + const void *addr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, off); + if ((void *)-1 == addr) + errc(EX_OSERR, errno, "can't mmap input file"); + *filesize = st.st_size; + return addr; +} + +static void +walkcore( + const native_mach_header_t *mh, + void (^coreinfo)(const struct proto_coreinfo_command *), + void (^frefdata)(const struct proto_fileref_command *), + void (^coredata)(const struct proto_coredata_command *), + void (^segdata)(const native_segment_command_t *), + void (^thrdata)(const struct thread_command *)) +{ + const struct load_command *lc = (const void *)(mh + 1); + for (unsigned i = 0; i < mh->ncmds; i++) { + switch (lc->cmd) { + case proto_LC_COREINFO: + if (coreinfo) + coreinfo((const void *)lc); + break; + case proto_LC_FILEREF: + if (frefdata) + frefdata((const void *)lc); + break; + case proto_LC_COREDATA: + if (coredata) + coredata((const void *)lc); + break; + case NATIVE_LC_SEGMENT: + if (segdata) + segdata((const void *)lc); + break; + case LC_THREAD: + if (thrdata) + thrdata((const void *)lc); + break; + default: + break; + } + if (NULL == (lc = next_lc(lc))) + break; + } +} + +#endif + +#ifdef CONFIG_GCORE_FREF + +int +gcore_fref(int fd) +{ + off_t filesize; + const void *corebase = mmapfile(fd, 0, &filesize); + + close(fd); + struct flist { + STAILQ_ENTRY(flist) f_linkage; + const char *f_nm; + unsigned long f_nmhash; + }; + STAILQ_HEAD(flisthead, flist) __flh, *flh = &__flh; + STAILQ_INIT(flh); + + walkcore(corebase, NULL, ^(const struct proto_fileref_command *fc) { + const char *nm = fc->filename.offset + (const char *)fc; + const unsigned long nmhash = simple_namehash(nm); + struct flist *f; + STAILQ_FOREACH(f, flh, f_linkage) { + if (nmhash == f->f_nmhash && 0 == strcmp(f->f_nm, nm)) + return; /* skip duplicates */ + } + struct flist *nf = calloc(1, sizeof (*nf)); + nf->f_nm = nm; + nf->f_nmhash = nmhash; + STAILQ_INSERT_TAIL(flh, nf, f_linkage); + }, NULL, NULL, NULL); + + struct flist *f, *tf; + STAILQ_FOREACH_SAFE(f, flh, f_linkage, tf) { + printf("%s\n", f->f_nm); + free(f); + f = NULL; + } + + munmap((void *)corebase, (size_t)filesize); + return 0; +} + +#endif /* CONFIG_GCORE_FREF */ + +#ifdef CONFIG_GCORE_MAP + +/* + * A pale imitation of vmmap, but for core files + */ +int +gcore_map(int fd) +{ + off_t filesize; + const void *corebase = mmapfile(fd, 0, &filesize); + + __block int coreversion = 0; + + walkcore(corebase, ^(const struct proto_coreinfo_command *ci) { + coreversion = ci->version; + }, NULL, NULL, NULL, NULL); + + if (0 == coreversion) { + const char titlfmt[] = "%16s-%-16s [%7s] %3s/%3s\n"; + const char *segcfmt = "%016llx-%016llx [%7s] %3s/%3s\n"; + + printf(titlfmt, "start ", " end", "vsize", "prt", "max"); + walkcore(corebase, NULL, NULL, NULL, ^(const native_segment_command_t *sc) { + hsize_str_t vstr; + printf(segcfmt, (mach_vm_offset_t)sc->vmaddr, (mach_vm_offset_t)sc->vmaddr + sc->vmsize, str_hsize(vstr, sc->vmsize), str_prot(sc->initprot), str_prot(sc->maxprot)); + }, NULL); + } else { + const char titlfmt[] = "%-23s %16s-%-16s [%7s] %3s/%3s %6s %4s %-14s\n"; + const char *freffmt = "%-23s %016llx-%016llx [%7s] %3s/%3s %6s %4s %-14s @%lld\n"; + const char *datafmt = "%-23s %016llx-%016llx [%7s] %3s/%3s %6s %4s %-14s\n"; + + printf(titlfmt, "region type", "start ", " end", "vsize", "prt", "max", "shrmod", "purge", "region detail"); + walkcore(corebase, NULL, ^(const struct proto_fileref_command *fc) { + const char *nm = fc->filename.offset + (const char *)fc; + tag_str_t tstr; + hsize_str_t vstr; + printf(freffmt, str_tag(tstr, fc->tag, fc->share_mode, fc->prot, fc->extp), + fc->vmaddr, fc->vmaddr + fc->vmsize, + str_hsize(vstr, fc->vmsize), str_prot(fc->prot), + str_prot(fc->maxprot), str_shared(fc->share_mode), + str_purgable(fc->purgable, fc->share_mode), nm, fc->fileoff); + }, ^(const struct proto_coredata_command *cc) { + tag_str_t tstr; + hsize_str_t vstr; + printf(datafmt, str_tag(tstr, cc->tag, cc->share_mode, cc->prot, cc->extp), + cc->vmaddr, cc->vmaddr + cc->vmsize, + str_hsize(vstr, cc->vmsize), str_prot(cc->prot), + str_prot(cc->maxprot), str_shared(cc->share_mode), + str_purgable(cc->purgable, cc->share_mode), + cc->vmsize && 0 == cc->filesize ? "(zfod)" : ""); + }, ^(const native_segment_command_t *sc) { + hsize_str_t vstr; + printf(datafmt, "", (mach_vm_offset_t)sc->vmaddr, + (mach_vm_offset_t)sc->vmaddr + sc->vmsize, + str_hsize(vstr, sc->vmsize), str_prot(sc->initprot), + str_prot(sc->maxprot), "", "", + sc->vmsize && 0 == sc->filesize ? "(zfod)" : ""); + }, NULL); + } + close(fd); + munmap((void *)corebase, (size_t)filesize); + return 0; +} + +#endif + +#ifdef CONFIG_GCORE_CONV + +/* + * Convert an input core file into an "old" format core file + * (a) convert all fileref segments into regular segments + * (b) uncompress anything we find compressed. + * This should be equivalent to a copy for an "old" format core file. + */ + +static int +machocmp(const native_mach_header_t *tmh, const native_mach_header_t *mh, const struct proto_fileref_command *fr) +{ + if (tmh->magic == mh->magic) { + const struct load_command *lc = (const void *)(tmh + 1); + for (unsigned i = 0; i < tmh->ncmds; i++) { + if (LC_UUID == lc->cmd && lc->cmdsize >= sizeof (struct uuid_command)) { + const struct uuid_command *uc = (const void *)lc; + return uuid_compare(uc->uuid, fr->id); + } + if (NULL == (lc = next_lc(lc))) + break; + } + } + return -1; +} + +static int +fat_machocmp(const struct fat_header *fh, const native_mach_header_t *mh, const struct proto_fileref_command *fr, off_t *reloff) +{ + const uint32_t (^get32)(uint32_t); + + if (FAT_MAGIC == fh->magic) { + get32 = ^(uint32_t val) { + return val; + }; + } else { + get32 = ^(uint32_t val) { + uint32_t result = 0; + for (unsigned i = 0; i < sizeof (uint32_t); i++) + ((uint8_t *)&result)[i] = ((uint8_t *)&val)[3-i]; + return result; + }; + } + + assert(FAT_MAGIC == get32(fh->magic)); + assert(kFREF_ID_UUID == FREF_ID_TYPE(fr->flags) && !uuid_is_null(fr->id)); + + const struct fat_arch *fa = (const struct fat_arch *)(fh + 1); + uint32_t narch = get32(fh->nfat_arch); + for (unsigned n = 0; n < narch; n++, fa++) { + const native_mach_header_t *tmh = (const void *)(((const char *)fh) + get32(fa->offset)); + if (tmh->magic == mh->magic && 0 == machocmp(tmh, mh, fr)) { + *reloff = get32(fa->offset); + return 0; + } + } + return -1; +} + +struct output_info { + int oi_fd; + off_t oi_foffset; + bool oi_nocache; +}; + +static struct convstats { + int64_t copied; + int64_t added; + int64_t compressed; + int64_t uncompressed; +} cstat, *cstats = &cstat; + +/* + * A fileref segment references a read-only file that contains pages from + * the image. The file may be a Mach binary or dylib identified with a uuid. + */ +static int +convert_fileref_with_file(const char *filename, const native_mach_header_t *inmh, const struct proto_fileref_command *infr, const struct vm_range *invr, struct load_command *lc, struct output_info *oi) +{ + assert(invr->addr == infr->vmaddr && invr->size == infr->vmsize); + + struct stat st; + const int rfd = open(filename, O_RDONLY); + if (-1 == rfd || -1 == fstat(rfd, &st)) { + warnc(errno, "%s: open", filename); + return EX_IOERR; + } + const size_t rlen = (size_t)st.st_size; + void *raddr = mmap(NULL, rlen, PROT_READ, MAP_PRIVATE, rfd, 0); + if ((void *)-1 == raddr) { + warnc(errno, "%s: mmap", filename); + close(rfd); + return EX_IOERR; + } + close(rfd); + + off_t fatoff = 0; /* for FAT objects */ + int ecode = EX_DATAERR; + + switch (FREF_ID_TYPE(infr->flags)) { + case kFREF_ID_UUID: { + /* file should be a mach binary: check that uuid matches */ + const uint32_t magic = *(uint32_t *)raddr; + switch (magic) { + case FAT_MAGIC: + case FAT_CIGAM: + if (0 == fat_machocmp(raddr, inmh, infr, &fatoff)) + ecode = 0; + break; + case NATIVE_MH_MAGIC: + if (0 == machocmp(raddr, inmh, infr)) + ecode = 0; + break; + default: { + /* + * Maybe this is the shared cache? + */ + uuid_t uu; + if (get_uuid_from_shared_cache_mapping(raddr, rlen, uu) && uuid_compare(uu, infr->id) == 0) + ecode = 0; + break; + } + } + break; + } + case kFREF_ID_MTIMESPEC_LE: + /* file should have the same mtime */ + if (0 == memcmp(&st.st_mtimespec, infr->id, sizeof (infr->id))) + ecode = 0; + break; + case kFREF_ID_NONE: + /* file has no uniquifier, copy it anyway */ + break; + } + + if (0 != ecode) { + munmap(raddr, rlen); + warnx("%s doesn't match corefile content", filename); + return ecode; + } + + const off_t fileoff = fatoff + infr->fileoff; + const void *start = (const char *)raddr + fileoff; + const size_t len = (size_t)infr->filesize; + void *zaddr = NULL; + size_t zlen = 0; + + if (fileoff + (off_t)infr->filesize > (off_t)rlen) { + /* + * the file content needed (as described on machine with + * larger pagesize) extends beyond the end of the mapped + * file using our smaller pagesize. Zero pad it. + */ + const size_t pagesize_host = 1ul << pageshift_host; + void *endaddr = (caddr_t)raddr + roundup(rlen, pagesize_host); + zlen = (size_t)(fileoff + infr->filesize - rlen); + zaddr = mmap(endaddr, zlen, PROT_READ, MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0); + if ((void *)-1 == zaddr) { + hsize_str_t hstr; + warnc(errno, "cannot zero-pad %s mapping for %s", str_hsize(hstr, zlen),filename); + munmap(raddr, rlen); + return EX_IOERR; + } + } + + if (-1 == madvise((void *)start, len, MADV_SEQUENTIAL)) + warnc(errno, "%s: madvise", filename); + + const int error = bounded_pwrite(oi->oi_fd, start, len, oi->oi_foffset, &oi->oi_nocache, NULL); + + if (zlen) { + if (-1 == munmap(zaddr, zlen)) + warnc(errno, "%s: munmap zero pad", filename); + } + if (-1 == munmap(raddr, rlen)) + warnc(errno, "%s: munmap", filename); + if (error) { + warnc(error, "while copying %s to core file", filename); + return EX_IOERR; + } + + const struct file_range fr = { + .off = oi->oi_foffset, + .size = infr->filesize, + }; + make_native_segment_command(lc, invr, &fr, infr->maxprot, infr->prot); + oi->oi_foffset += fr.size; + cstats->added += infr->filesize; + return 0; +} + +/* + * expanduser tries to expand the leading '~' (if there is any) in the given + * path and returns a copy of the expanded path; it returns NULL on failures. + * The caller is responsible for freeing the returned string. + */ +static char * +expanduser(const char *path) +{ + if (path == NULL) { + return NULL; + } + if (path[0] != '~') { + /* + * For consistency, still dup the string so that the caller always + * needs to free the string. + */ + return strdup(path); + } + + char *expanded = NULL; + glob_t globbuf = {}; + if (OPTIONS_DEBUG(opt, 1)) { + printf("Expanding %s\n", path); + } + int rc = glob(path, GLOB_TILDE, NULL, &globbuf); + if (rc == 0) { + if (OPTIONS_DEBUG(opt, 3)) { + printf("expanduser - gl_pathc: %zu\n", globbuf.gl_pathc); + for (size_t i = 0; i < globbuf.gl_pathc; i++) { + printf("expanduser - gl_pathv[%zu]: %s\n", i, globbuf.gl_pathv[i]); + } + } + if (globbuf.gl_pathc == 1) { + expanded = strdup(globbuf.gl_pathv[0]); + if (OPTIONS_DEBUG(opt, 1)) { + printf("Expanded path: %s\n", expanded); + } + } + globfree(&globbuf); + } + + return expanded; +} + +#define RESPONSE_BUFF_SIZE (2048) + +/* + * read_response dynamically allocates buffer for reading bytes from the given + * fd. Upon success, this function sets response to point to the buffer and + * returns bytes being read; otherwise, it returns -1. The caller is + * responsible for freeing the response buffer. + */ +static ssize_t +read_response(int fd, char **response) +{ + if (response == NULL || *response) { + warnx("Invalid response buffer pointer"); + return -1; + } + + ssize_t bytes_read = 0; + size_t buff_size = RESPONSE_BUFF_SIZE; + + if (OPTIONS_DEBUG(opt, 3)) { + printf("Allocating response buffer (%zu)\n", buff_size); + } + char *buff = malloc(buff_size); + if (buff == NULL) { + warn("Failed to allocate response buffer (%zu)", buff_size); + return -1; + } + + size_t total = 0; + bool failed = false; + + do { + bytes_read = read(fd, buff + total, buff_size - total); + if (bytes_read == -1) { + if (errno == EINTR) { + continue; + } + failed = true; + break; + } + + total += (size_t)bytes_read; + if (total == buff_size) { + size_t new_buff_size = buff_size * 2; + if (OPTIONS_DEBUG(opt, 3)) { + printf("Reallocating response buffer (%zu)\n", new_buff_size); + } + char *new_buff = realloc(buff, new_buff_size); + if (new_buff == NULL) { + warn("Failed to reallocate response buffer (%zu)", new_buff_size); + failed = true; + break; + } + buff_size = new_buff_size; + buff = new_buff; + } + } while (bytes_read != 0); + + if (failed) { + if (buff != NULL) { + free(buff); + } + return -1; + } + + assert(total < buff_size); + buff[total] = '\0'; + *response = buff; + + return (ssize_t)total; +} + +#define WAITPID_WTO_SIGALRM (100) /* alternative for SIGALRM for kevent timeout */ +#define WAITPID_WTO_SIGERR (101) /* sig for error when waiting for pid */ + +/* + * waitpid_with_timeout returns true if the process exits successfully within + * timeout; otherwise, it returns false along with setting exitstatus and + * signal_no if the pointers are given. + */ +static bool +waitpid_with_timeout(pid_t pid, int *exitstatus, int *signal_no, int timeout) +{ + int status; + int kq = -1; + + if (timeout > 0) { + kq = kqueue(); + struct kevent64_s event = { + .ident = (uint64_t)pid, + .filter = EVFILT_PROC, + .flags = EV_ADD | EV_ONESHOT, + .fflags = NOTE_EXIT + }; + struct timespec tmout = { + .tv_sec = timeout + }; + int ret = kevent64(kq, &event, 1, &event, 1, 0, &tmout); + int kevent64_errno = errno; + + close(kq); + if (ret == 0) { /* timeout */ + if (exitstatus) { + *exitstatus = 0; + } + if (signal_no) { + *signal_no = WAITPID_WTO_SIGALRM; + } + return false; + } + + if (ret == -1) { + warnx("kevent64(): errno=%d (%s)\n", kevent64_errno, strerror(kevent64_errno)); + goto waitpid_error; + } + + if (event.flags == EV_ERROR && event.data != ESRCH) { + warnx("event.data (%lld) is not ESRCH when event.flags is EV_ERROR\n", event.data); + goto waitpid_error; + } + + if (event.ident != (uint64_t)pid) { + warnx("event.ident is %lld (should be pid %d)\n", event.ident, pid); + goto waitpid_error; + } + + if (event.filter != EVFILT_PROC) { + warnx("event.filter (%d) is not EVFILT_PROC\n", event.filter); + goto waitpid_error; + } + } + + while (waitpid(pid, &status, 0) < 0) { + if (errno == EINTR) { + continue; + } + warnx("waitpid(): errno=%d (%s)\n", errno, strerror(errno)); + goto waitpid_error; + } + if (WIFEXITED(status)) { + if (exitstatus) { + *exitstatus = WEXITSTATUS(status); + } + if (signal_no) { + *signal_no = 0; + } + return WEXITSTATUS(status) == 0; + } + if (WIFSIGNALED(status)) { + if (exitstatus) { + *exitstatus = 0; + } + if (signal_no) { + *signal_no = WTERMSIG(status); + } + return false; + } + +waitpid_error: + if (exitstatus) *exitstatus = 0; + if (signal_no) *signal_no = WAITPID_WTO_SIGERR; + return false; +} + +#define DSYMFORUUID_PATH "/usr/local/bin/dsymForUUID" + +/* + * exec_dsymForUUID spawns dsymForUUID to query dsym UUID info and responds the + * result plist. Upon success, this function sets response point to the buffer + * and returns bytes being read; otherwise, it returns -1. The caller is + * responsible for freeing the response buffer. + */ +static ssize_t +exec_dsymForUUID(uuid_string_t id, char **response) +{ + int pipe_fds[2] = {-1, -1}; + bool file_actions_inited = false; + ssize_t bytes_read = -1; + int rc; + + rc = pipe(pipe_fds); + if (rc == -1) { + goto cleanup; + } + + posix_spawn_file_actions_t file_actions; + rc = posix_spawn_file_actions_init(&file_actions); + if (rc) { + goto cleanup; + } + file_actions_inited = true; + + rc = posix_spawn_file_actions_addclose(&file_actions, pipe_fds[0]); + if (rc) { + goto cleanup; + } + + rc = posix_spawn_file_actions_adddup2(&file_actions, pipe_fds[1], STDOUT_FILENO); + if (rc) { + goto cleanup; + } + + rc = posix_spawn_file_actions_addclose(&file_actions, pipe_fds[1]); + if (rc) { + goto cleanup; + } + + char *command[] = {DSYMFORUUID_PATH, id, NULL}; + pid_t child; + rc = posix_spawn(&child, command[0], &file_actions, NULL, command, NULL); + if (rc) { + goto cleanup; + } + + close(pipe_fds[1]); + pipe_fds[1] = -1; + + bytes_read = read_response(pipe_fds[0], response); + + waitpid_with_timeout(child, NULL, NULL, 3); + +cleanup: + if (pipe_fds[1] != -1) { + close(pipe_fds[1]); + } + if (pipe_fds[0] != -1) { + close(pipe_fds[0]); + } + if (file_actions_inited) { + posix_spawn_file_actions_destroy(&file_actions); + } + + return bytes_read; +} + +/* + * get_symbol_rich_executable_path_via_dsymForUUID spawns dsymForUUID to query + * dsym uuid info for the given uuid and returns the string of + * DBGSymbolRichExecutable; otherwise, it returns NULL on failures. The caller + * is responsible for freeing the returned string. + */ +static char * +get_symbol_rich_executable_path_via_dsymForUUID(const uuid_t uuid) +{ + char *response; + ssize_t size; + uuid_string_t uuid_str; + xpc_object_t plist = NULL; + xpc_object_t uuid_info = NULL; + xpc_object_t exec_path = NULL; + char *expanded_exec_path = NULL; + + uuid_unparse_upper(uuid, uuid_str); + + size = exec_dsymForUUID(uuid_str, &response); + if (size <= 0) { + goto cleanup; + } + + if (OPTIONS_DEBUG(opt, 3)) { + printf("dsymForUUID response:\n%s\n", response); + } + + plist = xpc_create_from_plist(response, (size_t)size); + if (plist == NULL) { + goto cleanup; + } + if (xpc_get_type(plist) != XPC_TYPE_DICTIONARY) { + goto cleanup; + } + + uuid_info = xpc_dictionary_get_value(plist, uuid_str); + if (uuid_info == NULL) { + goto cleanup; + } + if (xpc_get_type(uuid_info) != XPC_TYPE_DICTIONARY) { + goto cleanup; + } + + exec_path = xpc_dictionary_get_value(uuid_info, "DBGSymbolRichExecutable"); + if (exec_path == NULL) { + goto cleanup; + } + if (xpc_get_type(exec_path) != XPC_TYPE_STRING) { + goto cleanup; + } + + expanded_exec_path = expanduser(xpc_string_get_string_ptr(exec_path)); + +cleanup: + if (plist) { + xpc_release(plist); + } + if (response) { + free(response); + } + + return expanded_exec_path; +} + +/* + * bind the file reference into the output core file. + * filename optionally prefixed with names from a ':'-separated PATH variable + */ +static int +convert_fileref(const char *path, bool zf, const native_mach_header_t *inmh, const struct proto_fileref_command *infr, struct load_command *lc, struct output_info *oi) +{ + const char *nm = infr->filename.offset + (const char *)infr; + uuid_string_t uustr; + const struct vm_range invr = { + .addr = infr->vmaddr, + .size = infr->vmsize, + }; + + if (opt->verbose) { + hsize_str_t hstr; + printvr(&invr, "adding %s from '%s'", + str_hsize(hstr, (off_t)infr->filesize), nm); + switch (FREF_ID_TYPE(infr->flags)) { + case kFREF_ID_NONE: + break; + case kFREF_ID_UUID: + uuid_unparse_lower(infr->id, uustr); + printf(" (%s)", uustr); + break; + case kFREF_ID_MTIMESPEC_LE: { + struct timespec mts; + struct tm tm; + char tbuf[4 + 2 + 2 + 2 + 2 + 1 + 2 + 1]; /* touch -t */ + memcpy(&mts, &infr->id, sizeof (mts)); + localtime_r(&mts.tv_sec, &tm); + strftime(tbuf, sizeof (tbuf), "%Y%m%d%H%M.%S", &tm); + printf(" (%s)", tbuf); + } break; + } + printf("\n"); + } + + int ecode = 0; + if (opt->dsymforuuid && (FREF_ID_TYPE(infr->flags) == kFREF_ID_UUID)) { + /* Try to use dsymForUUID to get the symbol-rich executable */ + char *symrich_filepath = get_symbol_rich_executable_path_via_dsymForUUID(infr->id); + if (symrich_filepath) { + if (opt->verbose) { + printf("\tTrying %s from dsymForUUID\n", symrich_filepath); + } + ecode = convert_fileref_with_file(symrich_filepath, inmh, infr, &invr, lc, oi); + free(symrich_filepath); + if (ecode == 0) { + return (ecode); + } + warnx("Failed to convert fileref with dsymForUUID. Fall back to local paths"); + } + } + + const size_t pathsize = path ? strlen(path) : 0; + ecode = EX_DATAERR; + if (0 == pathsize) + ecode = convert_fileref_with_file(nm, inmh, infr, &invr, lc, oi); + else { + /* search the : separated path (-L) for possible matches */ + char *pathcopy = strdup(path); + char *searchpath = pathcopy; + const char *token; + + while ((token = strsep(&searchpath, ":")) != NULL) { + const size_t buflen = strlen(token) + 1 + strlen(nm) + 1; + char *buf = malloc(buflen); + snprintf(buf, buflen, "%s%s%s", token, '/' == nm[0] ? "" : "/", nm); + if (opt->verbose) + printf("\tTrying '%s'", buf); + if (0 == access(buf, R_OK)) { + if (opt->verbose) + printf("\n"); + ecode = convert_fileref_with_file(buf, inmh, infr, &invr, lc, oi); + if (0 == ecode) { + free(buf); + break; + } + } else if (opt->verbose) + printf(": %s.\n", + 0 == access(buf, F_OK) ? "Unreadable" : "Not present"); + free(buf); + } + free(pathcopy); + } + + if (0 != ecode && zf) { + /* + * Failed to find the file reference. If this was a fileref that uses + * a file metadata tagging method (e.g. mtime), allow the user to subsitute a + * zfod region: assumes that it's better to have something to debug + * vs. nothing. UUID-tagged filerefs are Mach-O tags, and are + * assumed to be never substitutable. + */ + switch (FREF_ID_TYPE(infr->flags)) { + case kFREF_ID_NONE: + case kFREF_ID_MTIMESPEC_LE: { // weak tagging, allow zfod substitution + const struct file_range outfr = { + .off = oi->oi_foffset, + .size = 0, + }; + if (opt->verbose) + printf("\tWARNING: no file matched. Missing content is now zfod\n"); + else + printvr(&invr, "WARNING: missing content (%s) now zfod\n", nm); + make_native_segment_command(lc, &invr, &outfr, infr->maxprot, infr->prot); + ecode = 0; + } break; + default: + break; + } + } + + return (ecode); +} + +static int +segment_uncompflags(unsigned algnum, compression_algorithm *ca) +{ + switch (algnum) { + case kCOMP_LZ4: + *ca = COMPRESSION_LZ4; + break; + case kCOMP_ZLIB: + *ca = COMPRESSION_ZLIB; + break; + case kCOMP_LZMA: + *ca = COMPRESSION_LZMA; + break; + case kCOMP_LZFSE: + *ca = COMPRESSION_LZFSE; + break; + default: + warnx("unknown compression flavor %d", algnum); + return EX_DATAERR; + } + return 0; +} + +static int +convert_region(const void *inbase, const struct vm_range *invr, const struct file_range *infr, const vm_prot_t prot, const vm_prot_t maxprot, const int flavor, struct load_command *lc, struct output_info *oi) +{ + int ecode = 0; + + if (F_SIZE(infr)) { + void *input = (const caddr_t)inbase + F_OFF(infr); + void *buf; + + if (0 == flavor) { + buf = input; + if (opt->verbose) { + hsize_str_t hstr; + printvr(invr, "copying %s\n", str_hsize(hstr, F_SIZE(infr))); + } + } else { + compression_algorithm ca; + + if (0 != (ecode = segment_uncompflags(flavor, &ca))) + return ecode; + if (opt->verbose) { + hsize_str_t hstr1, hstr2; + printvr(invr, "uncompressing %s to %s\n", + str_hsize(hstr1, F_SIZE(infr)), str_hsize(hstr2, V_SIZE(invr))); + } + const size_t buflen = V_SIZEOF(invr); + buf = malloc(buflen); + const size_t dstsize = compression_decode_buffer(buf, buflen, input, (size_t)F_SIZE(infr), NULL, ca); + if (buflen != dstsize) { + warnx("failed to uncompress segment"); + free(buf); + return EX_DATAERR; + } + cstats->compressed += F_SIZE(infr); + } + const int error = bounded_pwrite(oi->oi_fd, buf, V_SIZEOF(invr), oi->oi_foffset, &oi->oi_nocache, NULL); + if (error) { + warnc(error, "failed to write data to core file"); + ecode = EX_IOERR; + } + if (buf != input) + free(buf); + if (ecode) + return ecode; + + const struct file_range outfr = { + .off = oi->oi_foffset, + .size = V_SIZE(invr), + }; + make_native_segment_command(lc, invr, &outfr, maxprot, prot); + oi->oi_foffset += outfr.size; + + if (0 == flavor) + cstats->copied += outfr.size; + else + cstats->uncompressed += outfr.size; + } else { + if (opt->verbose) { + hsize_str_t hstr; + printvr(invr, "%s remains zfod\n", str_hsize(hstr, V_SIZE(invr))); + } + const struct file_range outfr = { + .off = oi->oi_foffset, + .size = 0, + }; + make_native_segment_command(lc, invr, &outfr, maxprot, prot); + } + return ecode; +} + +static int +convert_coredata(const void *inbase, const native_mach_header_t *__unused inmh, const struct proto_coredata_command *cc, struct load_command *lc, struct output_info *oi) +{ + const struct vm_range vr = { + .addr = cc->vmaddr, + .size = cc->vmsize, + }; + const struct file_range fr = { + .off = cc->fileoff, + .size = cc->filesize, + }; + return convert_region(inbase, &vr, &fr, cc->prot, cc->maxprot, COMP_ALG_TYPE(cc->flags), lc, oi); +} + +static int +convert_segment(const void *inbase, const native_mach_header_t *__unused inmh, const native_segment_command_t *sc, struct load_command *lc, struct output_info *oi) +{ + const struct vm_range vr = { + .addr = sc->vmaddr, + .size = sc->vmsize, + }; + const struct file_range fr = { + .off = sc->fileoff, + .size = sc->filesize, + }; + return convert_region(inbase, &vr, &fr, sc->initprot, sc->maxprot, 0, lc, oi); +} + +/* pass-through - content is all in the header */ + +static int +convert_thread(struct thread_command *dst, const struct thread_command *src) +{ + assert(LC_THREAD == src->cmd); + memcpy(dst, src, src->cmdsize); + cstats->copied += src->cmdsize; + return 0; +} + +int +gcore_conv(int infd, const char *searchpath, bool zf, int fd) +{ + off_t filesize; + const void *corebase = mmapfile(infd, 0, &filesize); + close(infd); + /* + * Check to see if the input file is "sane" as far as we're concerned. + * XXX Note that this -won't- necessarily work for other ISAs than + * our own! + */ + const native_mach_header_t *inmh = corebase; + validate_core_header(inmh, filesize); + + /* + * The sparse file may have created many more segments, but there's no + * attempt to change their numbers here. Just count all the segment + * types needed to figure out the size of the output file header. + * + * (Size assertions to be deleted once data structures stable!) + */ + __block size_t headersize = sizeof (native_mach_header_t); + __block unsigned pageshift_target = pageshift_host; + + walkcore(inmh, ^(const struct proto_coreinfo_command *ci) { + assert(sizeof (*ci) == ci->cmdsize); + if (opt->verbose) + printf("Converting version %d core file to pre-versioned format\n", ci->version); + if (0 < ci->pageshift && ci->pageshift < 31) + pageshift_target = ci->pageshift; + else if (CPU_TYPE_ARM64 == inmh->cputype) + pageshift_target = 14; // compatibility hack, should go soon + }, ^(const struct proto_fileref_command *__unused fc) { + const char *nm = fc->filename.offset + (const char *)fc; + size_t nmlen = strlen(nm) + 1; + size_t cmdsize = sizeof (*fc) + roundup(nmlen, sizeof (long)); + assert(cmdsize == fc->cmdsize); + + headersize += sizeof (native_segment_command_t); + }, ^(const struct proto_coredata_command *__unused cc) { + assert(sizeof (*cc) == cc->cmdsize); + headersize += sizeof (native_segment_command_t); + }, ^(const native_segment_command_t *sc) { + headersize += sc->cmdsize; + }, ^(const struct thread_command *tc) { + headersize += tc->cmdsize; + }); + + void *header = calloc(1, headersize); + if (NULL == header) + errx(EX_OSERR, "out of memory for header"); + + native_mach_header_t *mh = memcpy(header, inmh, sizeof (*mh)); + mh->ncmds = 0; + mh->sizeofcmds = 0; + + assert(0 < pageshift_target && pageshift_target < 31); + const vm_offset_t pagesize_target = ((vm_offset_t)1 << pageshift_target); + const vm_offset_t pagemask_target = pagesize_target - 1; + + const struct load_command *inlc = (const void *)(inmh + 1); + struct load_command *lc = (void *)(mh + 1); + int ecode = 0; + + struct output_info oi = { + .oi_fd = fd, + .oi_foffset = ((vm_offset_t)headersize + pagemask_target) & ~pagemask_target, + .oi_nocache = false, + }; + + for (unsigned i = 0; i < inmh->ncmds; i++) { + switch (inlc->cmd) { + case proto_LC_FILEREF: + ecode = convert_fileref(searchpath, zf, inmh, (const void *)inlc, lc, &oi); + break; + case proto_LC_COREDATA: + ecode = convert_coredata(corebase, inmh, (const void *)inlc, lc, &oi); + break; + case NATIVE_LC_SEGMENT: + ecode = convert_segment(corebase, inmh, (const void *)inlc, lc, &oi); + break; + case LC_THREAD: + ecode = convert_thread((void *)lc, (const void *)inlc); + break; + default: + if (OPTIONS_DEBUG(opt, 1)) + printf("discarding load command %d\n", inlc->cmd); + break; + } + if (0 != ecode) + break; + if (NATIVE_LC_SEGMENT == lc->cmd || LC_THREAD == lc->cmd) { + mach_header_inc_ncmds(mh, 1); + mach_header_inc_sizeofcmds(mh, lc->cmdsize); + lc = (void *)next_lc(lc); + } + if (NULL == (inlc = next_lc(inlc))) + break; + } + + /* + * Even if we've encountered an error, try and write out the header + */ + if (0 != bounded_pwrite(fd, header, headersize, 0, &oi.oi_nocache, NULL)) + ecode = EX_IOERR; + if (0 == ecode && sizeof (*mh) + mh->sizeofcmds != headersize) + ecode = EX_SOFTWARE; + validate_core_header(mh, oi.oi_foffset); + if (ecode) + warnx("failed to write new core file correctly"); + else if (opt->verbose) { + hsize_str_t hstr; + printf("Conversion complete: %s copied", str_hsize(hstr, cstats->copied)); + const int64_t delta = cstats->uncompressed - cstats->compressed; + if (delta > 0) + printf(", %s uncompressed", str_hsize(hstr, delta)); + const int64_t added = cstats->added + ((int)mh->sizeofcmds - (int)inmh->sizeofcmds); + if (added > 0) + printf(", %s added", str_hsize(hstr, added)); + printf("\n"); + } + free(header); + munmap((void *)corebase, (size_t)filesize); + return ecode; +} +#endif diff --git a/system_cmds/gcore.tproj/convert.h b/system_cmds/gcore.tproj/convert.h new file mode 100644 index 0000000..03854b8 --- /dev/null +++ b/system_cmds/gcore.tproj/convert.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "options.h" + +#include + +#ifndef _CONVERT_H +#define _CONVERT_H + +#ifdef CONFIG_GCORE_FREF +extern int gcore_fref(int); +#endif + +#ifdef CONFIG_GCORE_MAP +extern int gcore_map(int); +#endif + +#ifdef CONFIG_GCORE_CONV +extern int gcore_conv(int, const char *, bool, int); +#endif + +#endif /* _CONVERT_H */ diff --git a/system_cmds/gcore.tproj/corefile.c b/system_cmds/gcore.tproj/corefile.c new file mode 100644 index 0000000..b1e4421 --- /dev/null +++ b/system_cmds/gcore.tproj/corefile.c @@ -0,0 +1,852 @@ +/* + * Copyright (c) 2016-2018 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "corefile.h" +#include "sparse.h" +#include "utils.h" +#include "vm.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +native_mach_header_t * +make_corefile_mach_header(void *data) +{ + native_mach_header_t *mh = data; + mh->magic = NATIVE_MH_MAGIC; + mh->filetype = MH_CORE; +#if defined(__LP64__) + const int is64 = 1; +#else + const int is64 = 0; +#endif +#if defined(__i386__) || defined(__x86_64__) + mh->cputype = is64 ? CPU_TYPE_X86_64 : CPU_TYPE_I386; + mh->cpusubtype = is64 ? CPU_SUBTYPE_X86_64_ALL : CPU_SUBTYPE_I386_ALL; +#elif defined(__arm__) || defined(__arm64__) + mh->cputype = is64 ? CPU_TYPE_ARM64 : CPU_TYPE_ARM; + mh->cpusubtype = is64 ? CPU_SUBTYPE_ARM64_ALL : CPU_SUBTYPE_ARM_ALL; +#else +#error undefined +#endif + return mh; +} + +struct proto_coreinfo_command * +make_coreinfo_command(native_mach_header_t *mh, void *data, const uuid_t aoutid, uint64_t address, uint64_t dyninfo) +{ + struct proto_coreinfo_command *cc = data; + cc->cmd = proto_LC_COREINFO; + cc->cmdsize = sizeof (*cc); + cc->version = 1; + cc->type = proto_CORETYPE_USER; + cc->pageshift = (uint16_t)pageshift_host; + cc->address = address; + uuid_copy(cc->uuid, aoutid); + cc->dyninfo = dyninfo; + mach_header_inc_ncmds(mh, 1); + mach_header_inc_sizeofcmds(mh, cc->cmdsize); + return cc; +} + +native_segment_command_t * +make_native_segment_command(void *data, const struct vm_range *vr, const struct file_range *fr, vm_prot_t maxprot, vm_prot_t initprot) +{ + native_segment_command_t *sc = data; + sc->cmd = NATIVE_LC_SEGMENT; + sc->cmdsize = sizeof (*sc); + assert(V_SIZE(vr)); + sc->vmaddr = (unsigned long)V_ADDR(vr); + sc->vmsize = (unsigned long)V_SIZE(vr); + sc->fileoff = (unsigned long)F_OFF(fr); + sc->filesize = (unsigned long)F_SIZE(fr); + sc->maxprot = maxprot; + sc->initprot = initprot; + sc->nsects = 0; + sc->flags = 0; + return sc; +} + +static struct proto_coredata_command * +make_coredata_command(void *data, const struct vm_range *vr, const struct file_range *fr, const vm_region_submap_info_data_64_t *info, unsigned comptype, unsigned purgable) +{ + struct proto_coredata_command *cc = data; + cc->cmd = proto_LC_COREDATA; + cc->cmdsize = sizeof (*cc); + assert(V_SIZE(vr)); + cc->vmaddr = V_ADDR(vr); + cc->vmsize = V_SIZE(vr); + cc->fileoff = F_OFF(fr); + cc->filesize = F_SIZE(fr); + cc->maxprot = info->max_protection; + cc->prot = info->protection; + cc->flags = COMP_MAKE_FLAGS(comptype); + cc->share_mode = info->share_mode; + assert(purgable <= UINT8_MAX); + cc->purgable = (uint8_t)purgable; + assert(info->user_tag <= UINT8_MAX); + cc->tag = (uint8_t)info->user_tag; + cc->extp = info->external_pager; + return cc; +} + +static size_t +sizeof_segment_command(void) { + return opt->extended ? + sizeof (struct proto_coredata_command) : sizeof (native_segment_command_t); +} + +static struct load_command * +make_segment_command(void *data, const struct vm_range *vr, const struct file_range *fr, const vm_region_submap_info_data_64_t *info, unsigned comptype, int purgable) +{ + if (opt->extended) + make_coredata_command(data, vr, fr, info, comptype, purgable); + else + make_native_segment_command(data, vr, fr, info->max_protection, info->protection); + return data; +} + +/* + * Increment the mach-o header data when we succeed + */ +static void +commit_load_command(struct write_segment_data *wsd, const struct load_command *lc) +{ + wsd->wsd_lc = (caddr_t)lc + lc->cmdsize; + native_mach_header_t *mh = wsd->wsd_mh; + mach_header_inc_ncmds(mh, 1); + mach_header_inc_sizeofcmds(mh, lc->cmdsize); +} + +#pragma mark -- Regions written as "file references" -- + +static size_t +cmdsize_fileref_command(const char *nm) +{ + size_t cmdsize = sizeof (struct proto_fileref_command); + size_t len; + if (0 != (len = strlen(nm))) { + len++; // NUL-terminated for mmap sanity + cmdsize += roundup(len, sizeof (long)); + } + return cmdsize; +} + +static void +size_fileref_subregion(const struct subregion *s, struct size_core *sc) +{ + assert(S_LIBENT(s)); + + size_t cmdsize = cmdsize_fileref_command(S_PATHNAME(s)); + sc->headersize += cmdsize; + sc->count++; + sc->memsize += S_SIZE(s); +} + +static void +size_fileref_region(const struct region *r, struct size_core *sc) +{ + assert(0 == r->r_nsubregions); + assert(!r->r_inzfodregion); + + size_t cmdsize = cmdsize_fileref_command(r->r_fileref->fr_pathname); + sc->headersize += cmdsize; + sc->count++; + sc->memsize += R_SIZE(r); +} + +static struct proto_fileref_command * +make_fileref_command(void *data, const char *pathname, const uuid_t uuid, + const struct vm_range *vr, const struct file_range *fr, + const vm_region_submap_info_data_64_t *info, unsigned purgable) +{ + struct proto_fileref_command *fc = data; + size_t len; + + fc->cmd = proto_LC_FILEREF; + fc->cmdsize = sizeof (*fc); + if (0 != (len = strlen(pathname))) { + /* + * Strings live immediately after the + * command, and are included in the cmdsize + */ + fc->filename.offset = sizeof (*fc); + void *s = fc + 1; + strlcpy(s, pathname, ++len); // NUL-terminated for mmap sanity + fc->cmdsize += roundup(len, sizeof (long)); + assert(cmdsize_fileref_command(pathname) == fc->cmdsize); + } + + /* + * A file reference allows different kinds of identifiers for + * the reference to be reconstructed. + */ + assert(info->external_pager); + + if (!uuid_is_null(uuid)) { + uuid_copy(fc->id, uuid); + fc->flags = FREF_MAKE_FLAGS(kFREF_ID_UUID); + } else { + struct stat st; + if (-1 != stat(pathname, &st) && 0 != st.st_mtimespec.tv_sec) { + /* "little-endian format timespec structure" */ + struct timespec ts = st.st_mtimespec; + ts.tv_nsec = 0; // allow touch(1) to fix things + memset(fc->id, 0, sizeof(fc->id)); + memcpy(fc->id, &ts, sizeof(ts)); + fc->flags = FREF_MAKE_FLAGS(kFREF_ID_MTIMESPEC_LE); + } else + fc->flags = FREF_MAKE_FLAGS(kFREF_ID_NONE); + } + + fc->vmaddr = V_ADDR(vr); + assert(V_SIZE(vr)); + fc->vmsize = V_SIZE(vr); + + assert(F_OFF(fr) >= 0); + fc->fileoff = F_OFF(fr); + fc->filesize = F_SIZE(fr); + + assert(info->max_protection & VM_PROT_READ); + fc->maxprot = info->max_protection; + fc->prot = info->protection; + + fc->share_mode = info->share_mode; + assert(purgable <= UINT8_MAX); + fc->purgable = (uint8_t)purgable; + assert(info->user_tag <= UINT8_MAX); + fc->tag = (uint8_t)info->user_tag; + fc->extp = info->external_pager; + return fc; +} + +/* + * It's almost always more efficient to write out a reference to the + * data than write out the data itself. + */ +static walk_return_t +write_fileref_subregion(const struct region *r, const struct subregion *s, struct write_segment_data *wsd) +{ + assert(S_LIBENT(s)); + if (OPTIONS_DEBUG(opt, 1) && !issubregiontype(s, SEG_TEXT) && !issubregiontype(s, SEG_LINKEDIT)) + printf("%s: unusual segment type %s from %s\n", __func__, S_MACHO_TYPE(s), S_FILENAME(s)); + assert((r->r_info.max_protection & VM_PROT_READ) == VM_PROT_READ); + assert((r->r_info.protection & VM_PROT_WRITE) == 0); + + const struct libent *le = S_LIBENT(s); + const struct file_range fr = { + .off = S_MACHO_FILEOFF(s), + .size = S_SIZE(s), + }; + const struct proto_fileref_command *fc = make_fileref_command(wsd->wsd_lc, le->le_pathname, le->le_uuid, S_RANGE(s), &fr, &r->r_info, r->r_purgable); + + commit_load_command(wsd, (const void *)fc); + if (OPTIONS_DEBUG(opt, 3)) { + hsize_str_t hstr; + printr(r, "ref '%s' %s (vm %llx-%llx, file offset %lld for %s)\n", S_FILENAME(s), S_MACHO_TYPE(s), (uint64_t)fc->vmaddr, (uint64_t)fc->vmaddr + fc->vmsize, (int64_t)fc->fileoff, str_hsize(hstr, fc->filesize)); + } + return WALK_CONTINUE; +} + +/* + * Note that we may be asked to write reference segments whose protections + * are rw- -- this -should- be ok as we don't convert the region to a file + * reference unless we know it hasn't been modified. + */ +static walk_return_t +write_fileref_region(const struct region *r, struct write_segment_data *wsd) +{ + assert(0 == r->r_nsubregions); + assert(r->r_info.user_tag != VM_MEMORY_IOKIT); + assert((r->r_info.max_protection & VM_PROT_READ) == VM_PROT_READ); + assert(!r->r_inzfodregion); + + const struct libent *le = r->r_fileref->fr_libent; + const char *pathname = r->r_fileref->fr_pathname; + const struct file_range fr = { + .off = r->r_fileref->fr_offset, + .size = R_SIZE(r), + }; + const struct proto_fileref_command *fc = make_fileref_command(wsd->wsd_lc, pathname, le ? le->le_uuid : UUID_NULL, R_RANGE(r), &fr, &r->r_info, r->r_purgable); + + commit_load_command(wsd, (const void *)fc); + if (OPTIONS_DEBUG(opt, 3)) { + hsize_str_t hstr; + printr(r, "ref '%s' %s (vm %llx-%llx, file offset %lld for %s)\n", pathname, "(type?)", (uint64_t)fc->vmaddr, (uint64_t)fc->vmaddr + fc->vmsize, (int64_t)fc->fileoff, str_hsize(hstr, fc->filesize)); + } + return WALK_CONTINUE; +} + +const struct regionop fileref_ops = { + print_memory_region, + write_fileref_region, + del_fileref_region, +}; + + +#pragma mark -- ZFOD segments written only to the header -- + +static void +size_zfod_region(const struct region *r, struct size_core *sc) +{ + assert(0 == r->r_nsubregions); + assert(r->r_inzfodregion); + sc->headersize += sizeof_segment_command(); + sc->count++; + sc->memsize += R_SIZE(r); +} + +static walk_return_t +write_zfod_region(const struct region *r, struct write_segment_data *wsd) +{ + assert(r->r_info.user_tag != VM_MEMORY_IOKIT); + assert((r->r_info.max_protection & VM_PROT_READ) == VM_PROT_READ); + + const struct file_range fr = { + .off = wsd->wsd_foffset, + .size = 0, + }; + make_segment_command(wsd->wsd_lc, R_RANGE(r), &fr, &r->r_info, 0, VM_PURGABLE_EMPTY); + commit_load_command(wsd, wsd->wsd_lc); + return WALK_CONTINUE; +} + +const struct regionop zfod_ops = { + print_memory_region, + write_zfod_region, + del_zfod_region, +}; + +#pragma mark -- Regions containing data -- + +static walk_return_t +pwrite_memory(struct write_segment_data *wsd, const void *addr, size_t size, const struct vm_range *vr) +{ + assert(size); + + ssize_t nwritten; + const int error = bounded_pwrite(wsd->wsd_fd, addr, size, wsd->wsd_foffset, &wsd->wsd_nocache, &nwritten); + + if (error || OPTIONS_DEBUG(opt, 3)) { + hsize_str_t hsz; + printvr(vr, "writing %ld bytes at offset %lld -> ", size, wsd->wsd_foffset); + if (error) + printf("err #%d - %s ", error, strerror(error)); + else { + printf("%s ", str_hsize(hsz, nwritten)); + if (size != (size_t)nwritten) + printf("[%zd - incomplete write!] ", nwritten); + else if (size != V_SIZE(vr)) + printf("(%s in memory) ", + str_hsize(hsz, V_SIZE(vr))); + } + printf("\n"); + } + + walk_return_t step = WALK_CONTINUE; + switch (error) { + case 0: + if (size != (size_t)nwritten) + step = WALK_ERROR; + else { + wsd->wsd_foffset += nwritten; + wsd->wsd_nwritten += nwritten; + } + break; + case EFAULT: // transient mapping failure? + break; + default: // EROFS, ENOSPC, EFBIG etc. */ + step = WALK_ERROR; + break; + } + return step; +} + + +/* + * Write a contiguous range of memory into the core file. + * Apply compression, and chunk if necessary. + */ +static int +segment_compflags(compression_algorithm ca, unsigned *algnum) +{ + switch (ca) { + case COMPRESSION_LZ4: + *algnum = kCOMP_LZ4; + break; + case COMPRESSION_ZLIB: + *algnum = kCOMP_ZLIB; + break; + case COMPRESSION_LZMA: + *algnum = kCOMP_LZMA; + break; + case COMPRESSION_LZFSE: + *algnum = kCOMP_LZFSE; + break; + default: + err(EX_SOFTWARE, "unsupported compression algorithm %x", ca); + } + return 0; +} + +static bool +is_file_mapped_shared(const struct region *r) +{ + if (r->r_info.external_pager) + switch (r->r_info.share_mode) { + case SM_TRUESHARED: // sm=shm + case SM_SHARED: // sm=ali + case SM_SHARED_ALIASED: // sm=s/a + return true; + default: + break; + } + return false; +} + +static walk_return_t +map_memory_range(struct write_segment_data *wsd, const struct region *r, const struct vm_range *vr, struct vm_range *dp) +{ + if (r->r_incommregion) { + /* + * Special case: for commpage access, copy from our own address space. + */ + V_SETADDR(dp, 0); + V_SETSIZE(dp, V_SIZE(vr)); + + kern_return_t kr = mach_vm_allocate(mach_task_self(), &dp->addr, dp->size, VM_FLAGS_ANYWHERE); + if (KERN_SUCCESS != kr || 0 == dp->addr) { + err_mach(kr, r, "mach_vm_allocate c %llx-%llx", V_ADDR(vr), V_ENDADDR(vr)); + print_one_memory_region(r); + return WALK_ERROR; + } + if (OPTIONS_DEBUG(opt, 3)) + printr(r, "copying from self %llx-%llx\n", V_ADDR(vr), V_ENDADDR(vr)); + memcpy((void *)dp->addr, (const void *)V_ADDR(vr), V_SIZE(vr)); + return WALK_CONTINUE; + } + + if (!r->r_insharedregion && 0 == (r->r_info.protection & VM_PROT_READ)) { + assert(0 != (r->r_info.max_protection & VM_PROT_READ)); // simple_region_optimization() + + /* + * Special case: region that doesn't currently have read permission. + * (e.g. --x/r-x permissions with tag 64 - JS JIT generated code + * from com.apple.WebKit.WebContent) + */ + const mach_vm_offset_t pagesize_host = 1u << pageshift_host; + if (OPTIONS_DEBUG(opt, 3)) + printr(r, "unreadable (%s/%s), remap with read permission\n", + str_prot(r->r_info.protection), str_prot(r->r_info.max_protection)); + V_SETADDR(dp, 0); + V_SETSIZE(dp, V_SIZE(vr)); + vm_prot_t cprot, mprot; + kern_return_t kr = mach_vm_remap(mach_task_self(), &dp->addr, V_SIZE(dp), pagesize_host - 1, true, wsd->wsd_task, V_ADDR(vr), true, &cprot, &mprot, VM_INHERIT_NONE); + if (KERN_SUCCESS != kr) { + err_mach(kr, r, "mach_vm_remap() %llx-%llx", V_ADDR(vr), V_ENDADDR(vr)); + return WALK_ERROR; + } + assert(r->r_info.protection == cprot && r->r_info.max_protection == mprot); + kr = mach_vm_protect(mach_task_self(), V_ADDR(dp), V_SIZE(dp), false, VM_PROT_READ); + if (KERN_SUCCESS != kr) { + err_mach(kr, r, "mach_vm_protect() %llx-%llx", V_ADDR(vr), V_ENDADDR(vr)); + mach_vm_deallocate(mach_task_self(), V_ADDR(dp), V_SIZE(dp)); + return WALK_ERROR; + } + return WALK_CONTINUE; + } + + /* + * Most segments with data are read here + */ + vm_offset_t data32 = 0; + mach_msg_type_number_t data32_count; + kern_return_t kr = mach_vm_read(wsd->wsd_task, V_ADDR(vr), V_SIZE(vr), &data32, &data32_count); + switch (kr) { + case KERN_SUCCESS: + V_SETADDR(dp, data32); + V_SETSIZE(dp, data32_count); + break; + case KERN_INVALID_ADDRESS: + if (!r->r_insharedregion && + (VM_MEMORY_SKYWALK == r->r_info.user_tag || is_file_mapped_shared(r))) { + if (OPTIONS_DEBUG(opt, 1)) { + /* not necessarily an error: mitigation below */ + tag_str_t tstr; + printr(r, "mach_vm_read() failed (%s) -- substituting zeroed region\n", str_tagr(tstr, r)); + if (OPTIONS_DEBUG(opt, 2)) + print_one_memory_region(r); + } + V_SETSIZE(dp, V_SIZE(vr)); + kr = mach_vm_allocate(mach_task_self(), &dp->addr, V_SIZE(dp), VM_FLAGS_ANYWHERE); + if (KERN_SUCCESS != kr || 0 == V_ADDR(dp)) + err_mach(kr, r, "mach_vm_allocate() z %llx-%llx", V_ADDR(vr), V_ENDADDR(vr)); + break; + } + /*FALLTHROUGH*/ + default: + err_mach(kr, r, "mach_vm_read() %llx-%llx", V_ADDR(vr), V_SIZE(vr)); + if (OPTIONS_DEBUG(opt, 1)) + print_one_memory_region(r); + break; + } + if (kr != KERN_SUCCESS) { + V_SETADDR(dp, 0); + return WALK_ERROR; + } + + /* + * Sometimes (e.g. searchd) we may not be able to fetch all the pages + * from the underlying mapped file, in which case replace those pages + * with zfod pages (at least they compress efficiently) rather than + * taking a SIGBUS when compressing them. + * + * XXX Perhaps we should just catch the SIGBUS, and if the faulting address + * is in the right range, substitute zfod pages and rerun region compression? + * Complex though, because the compression code may be multithreaded. + */ + if (!r->r_insharedregion && is_file_mapped_shared(r)) { + const mach_vm_offset_t pagesize_host = 1u << pageshift_host; + + if (r->r_info.pages_resident * pagesize_host == V_SIZE(dp)) + return WALK_CONTINUE; // all pages resident, so skip .. + + if (OPTIONS_DEBUG(opt, 2)) + printr(r, "probing %llu pages in mapped-shared file\n", V_SIZE(dp) / pagesize_host); + + kr = KERN_SUCCESS; + for (mach_vm_offset_t a = V_ADDR(dp); a < V_ENDADDR(dp); a += pagesize_host) { + + mach_msg_type_number_t pCount = VM_PAGE_INFO_BASIC_COUNT; + vm_page_info_basic_data_t pInfo; + + kr = mach_vm_page_info(mach_task_self(), a, VM_PAGE_INFO_BASIC, (vm_page_info_t)&pInfo, &pCount); + if (KERN_SUCCESS != kr) { + err_mach(kr, NULL, "mach_vm_page_info() at %llx", a); + break; + } + /* If the VM has the page somewhere, assume we can bring it back */ + if (pInfo.disposition & (VM_PAGE_QUERY_PAGE_PRESENT | VM_PAGE_QUERY_PAGE_REF | VM_PAGE_QUERY_PAGE_DIRTY)) + continue; + + /* Force the page to be fetched to see if it faults */ + mach_vm_size_t tsize = pagesize_host; + void *tmp = valloc((size_t)tsize); + const mach_vm_address_t vtmp = (mach_vm_address_t)tmp; + + switch (kr = mach_vm_read_overwrite(mach_task_self(), a, tsize, vtmp, &tsize)) { + case KERN_SUCCESS: + break; + case KERN_INVALID_ADDRESS: { + /* Content can't be found: replace it and the rest of the region with zero-fill pages */ + if (OPTIONS_DEBUG(opt, 2)) { + printr(r, "mach_vm_read_overwrite() failed after %llu pages -- substituting zfod\n", (a - V_ADDR(dp)) / pagesize_host); + print_one_memory_region(r); + } + mach_vm_address_t va = a; + kr = mach_vm_allocate(mach_task_self(), &va, V_ENDADDR(dp) - va, VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE); + if (KERN_SUCCESS != kr) { + err_mach(kr, r, "mach_vm_allocate() %llx", a); + } else { + assert(a == va); + a = V_ENDADDR(dp); // no need to look any further + } + break; + } + default: + err_mach(kr, r, "mach_vm_overwrite() %llx", a); + break; + } + free(tmp); + if (KERN_SUCCESS != kr) + break; + } + if (KERN_SUCCESS != kr) { + kr = mach_vm_deallocate(mach_task_self(), V_ADDR(dp), V_SIZE(dp)); + if (KERN_SUCCESS != kr && OPTIONS_DEBUG(opt, 1)) + err_mach(kr, r, "mach_vm_deallocate() pre %llx-%llx", V_ADDR(dp), V_ENDADDR(dp)); + V_SETADDR(dp, 0); + return WALK_ERROR; + } + } + + return WALK_CONTINUE; +} + +static walk_return_t +write_memory_range(struct write_segment_data *wsd, const struct region *r, mach_vm_offset_t vmaddr, mach_vm_offset_t vmsize) +{ + assert(R_ADDR(r) <= vmaddr && R_ENDADDR(r) >= vmaddr + vmsize); + + mach_vm_offset_t resid = vmsize; + walk_return_t step = WALK_CONTINUE; + + do { + vmsize = resid; + + /* + * Since some regions can be inconveniently large, + * chop them into multiple chunks as we compress them. + * (mach_vm_read has 32-bit limitations too). + */ + vmsize = vmsize > INT32_MAX ? INT32_MAX : vmsize; + if (opt->chunksize > 0 && vmsize > opt->chunksize) + vmsize = opt->chunksize; + assert(vmsize <= INT32_MAX); + + const struct vm_range vr = { + .addr = vmaddr, + .size = vmsize, + }; + struct vm_range d, *dp = &d; + + step = map_memory_range(wsd, r, &vr, dp); + if (WALK_CONTINUE != step) + break; + assert(0 != V_ADDR(dp) && 0 != V_SIZE(dp)); + const void *srcaddr = (const void *)V_ADDR(dp); + + mach_vm_behavior_set(mach_task_self(), V_ADDR(dp), V_SIZE(dp), VM_BEHAVIOR_SEQUENTIAL); + + void *dstbuf = NULL; + unsigned algorithm = 0; + size_t filesize; + + if (opt->extended) { + dstbuf = malloc(V_SIZEOF(dp)); + if (dstbuf) { + filesize = compression_encode_buffer(dstbuf, V_SIZEOF(dp), srcaddr, V_SIZEOF(dp), NULL, opt->calgorithm); + if (filesize > 0 && filesize < V_SIZEOF(dp)) { + srcaddr = dstbuf; /* the data source is now heap, compressed */ + mach_vm_deallocate(mach_task_self(), V_ADDR(dp), V_SIZE(dp)); + V_SETADDR(dp, 0); + if (segment_compflags(opt->calgorithm, &algorithm) != 0) { + free(dstbuf); + mach_vm_deallocate(mach_task_self(), V_ADDR(dp), V_SIZE(dp)); + V_SETADDR(dp, 0); + step = WALK_ERROR; + break; + } + } else { + free(dstbuf); + dstbuf = NULL; + filesize = V_SIZEOF(dp); + } + } else + filesize = V_SIZEOF(dp); + assert(filesize <= V_SIZEOF(dp)); + } else + filesize = V_SIZEOF(dp); + + assert(filesize); + + const struct file_range fr = { + .off = wsd->wsd_foffset, + .size = filesize, + }; + make_segment_command(wsd->wsd_lc, &vr, &fr, &r->r_info, algorithm, r->r_purgable); + step = pwrite_memory(wsd, srcaddr, filesize, &vr); + if (dstbuf) + free(dstbuf); + if (V_ADDR(dp)) { + kern_return_t kr = mach_vm_deallocate(mach_task_self(), V_ADDR(dp), V_SIZE(dp)); + if (KERN_SUCCESS != kr && OPTIONS_DEBUG(opt, 1)) + err_mach(kr, r, "mach_vm_deallocate() post %llx-%llx", V_ADDR(dp), V_SIZE(dp)); + } + + if (WALK_ERROR == step) + break; + commit_load_command(wsd, wsd->wsd_lc); + resid -= vmsize; + vmaddr += vmsize; + } while (resid); + + return step; +} + +#ifdef RDAR_23744374 +/* + * Sigh. This is a workaround. + * Find the vmsize as if the VM system manages ranges in host pagesize units + * rather than application pagesize units. + */ +static mach_vm_size_t +getvmsize_host(const task_t task, const struct region *r) +{ + mach_vm_size_t vmsize_host = R_SIZE(r); + + if (pageshift_host != pageshift_app) { + is_actual_size(task, r, &vmsize_host); + if (OPTIONS_DEBUG(opt, 1) && R_SIZE(r) != vmsize_host) + printr(r, "(region size tweak: was %llx, is %llx)\n", R_SIZE(r), vmsize_host); + } + return vmsize_host; +} +#else +static __inline mach_vm_size_t +getvmsize_host(__unused const task_t task, const struct region *r) +{ + return R_SIZE(r); +} +#endif + +static walk_return_t +write_sparse_region(const struct region *r, struct write_segment_data *wsd) +{ + assert(r->r_nsubregions); + assert(!r->r_inzfodregion); + assert(NULL == r->r_fileref); + + const mach_vm_size_t vmsize_host = getvmsize_host(wsd->wsd_task, r); + walk_return_t step = WALK_CONTINUE; + + for (unsigned i = 0; i < r->r_nsubregions; i++) { + const struct subregion *s = r->r_subregions[i]; + + if (s->s_isuuidref) + step = write_fileref_subregion(r, s, wsd); + else { + /* Write this one out as real data */ + mach_vm_size_t vmsize = S_SIZE(s); + if (R_SIZE(r) != vmsize_host) { + if (S_ADDR(s) + vmsize > R_ADDR(r) + vmsize_host) { + vmsize = R_ADDR(r) + vmsize_host - S_ADDR(s); + if (OPTIONS_DEBUG(opt, 3)) + printr(r, "(subregion size tweak: was %llx, is %llx)\n", + S_SIZE(s), vmsize); + } + } + step = write_memory_range(wsd, r, S_ADDR(s), vmsize); + } + if (WALK_ERROR == step) + break; + } + return step; +} + +static walk_return_t +write_vanilla_region(const struct region *r, struct write_segment_data *wsd) +{ + assert(0 == r->r_nsubregions); + assert(!r->r_inzfodregion); + assert(NULL == r->r_fileref); + + const mach_vm_size_t vmsize_host = getvmsize_host(wsd->wsd_task, r); + return write_memory_range(wsd, r, R_ADDR(r), vmsize_host); +} + +walk_return_t +region_write_memory(struct region *r, void *arg) +{ + assert(r->r_info.user_tag != VM_MEMORY_IOKIT); // elided in walk_regions() + assert((r->r_info.max_protection & VM_PROT_READ) == VM_PROT_READ); + return ROP_WRITE(r, arg); +} + +/* + * Handles the cases where segments are broken into chunks i.e. when + * writing compressed segments. + */ +static unsigned long +count_memory_range(mach_vm_offset_t vmsize) +{ + unsigned long count; + if (opt->chunksize) { + count = (size_t)vmsize / opt->chunksize; + if (vmsize != (mach_vm_offset_t)count * opt->chunksize) + count++; + } else + count = 1; + return count; +} + +/* + * A sparse region is likely a writable data segment described by + * native_segment_command_t somewhere in the address space. + */ +static void +size_sparse_subregion(const struct subregion *s, struct size_core *sc) +{ + const unsigned long count = count_memory_range(S_SIZE(s)); + sc->headersize += sizeof_segment_command() * count; + sc->count += count; + sc->memsize += S_SIZE(s); +} + +static void +size_sparse_region(const struct region *r, struct size_core *sc_sparse, struct size_core *sc_fileref) +{ + assert(0 != r->r_nsubregions); + + unsigned long entry_total = sc_sparse->count + sc_fileref->count; + for (unsigned i = 0; i < r->r_nsubregions; i++) { + const struct subregion *s = r->r_subregions[i]; + if (s->s_isuuidref) + size_fileref_subregion(s, sc_fileref); + else + size_sparse_subregion(s, sc_sparse); + } + if (OPTIONS_DEBUG(opt, 3)) { + /* caused by compression breaking a large region into chunks */ + entry_total = (sc_fileref->count + sc_sparse->count) - entry_total; + if (entry_total > r->r_nsubregions) + printr(r, "range contains %u subregions requires %lu segment commands\n", + r->r_nsubregions, entry_total); + } +} + +const struct regionop sparse_ops = { + print_memory_region, + write_sparse_region, + del_sparse_region, +}; + +static void +size_vanilla_region(const struct region *r, struct size_core *sc) +{ + assert(0 == r->r_nsubregions); + + const unsigned long count = count_memory_range(R_SIZE(r)); + sc->headersize += sizeof_segment_command() * count; + sc->count += count; + sc->memsize += R_SIZE(r); + + if (OPTIONS_DEBUG(opt, 3) && count != 1) + printr(r, "range with 1 region, but requires %lu segment commands\n", count); +} + +const struct regionop vanilla_ops = { + print_memory_region, + write_vanilla_region, + del_vanilla_region, +}; + +walk_return_t +region_size_memory(struct region *r, void *arg) +{ + struct size_segment_data *ssd = arg; + + if (&zfod_ops == r->r_op) + size_zfod_region(r, &ssd->ssd_zfod); + else if (&fileref_ops == r->r_op) + size_fileref_region(r, &ssd->ssd_fileref); + else if (&sparse_ops == r->r_op) + size_sparse_region(r, &ssd->ssd_sparse, &ssd->ssd_fileref); + else if (&vanilla_ops == r->r_op) + size_vanilla_region(r, &ssd->ssd_vanilla); + else + errx(EX_SOFTWARE, "%s: bad op", __func__); + + return WALK_CONTINUE; +} diff --git a/system_cmds/gcore.tproj/corefile.h b/system_cmds/gcore.tproj/corefile.h new file mode 100644 index 0000000..2bdc43e --- /dev/null +++ b/system_cmds/gcore.tproj/corefile.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "loader_additions.h" +#include "dyld_shared_cache.h" +#include "region.h" + +#include +#include +#include +#include + +#ifndef _COREFILE_H +#define _COREFILE_H + +#if defined(__LP64__) +typedef struct mach_header_64 native_mach_header_t; +typedef struct segment_command_64 native_segment_command_t; +#define NATIVE_MH_MAGIC MH_MAGIC_64 +#define NATIVE_LC_SEGMENT LC_SEGMENT_64 +#else +typedef struct mach_header native_mach_header_t; +typedef struct segment_command native_segment_command_t; +#define NATIVE_MH_MAGIC MH_MAGIC +#define NATIVE_LC_SEGMENT LC_SEGMENT +#endif + +static __inline const struct load_command *next_lc(const struct load_command *lc) { + if (lc->cmdsize && (lc->cmdsize & 3) == 0) + return (const void *)((caddr_t)lc + lc->cmdsize); + return NULL; +} + +extern native_segment_command_t *make_native_segment_command(void *, const struct vm_range *, const struct file_range *, vm_prot_t, vm_prot_t); + +extern native_mach_header_t *make_corefile_mach_header(void *); +extern struct proto_coreinfo_command *make_coreinfo_command(native_mach_header_t *, void *, const uuid_t, uint64_t, uint64_t); + +static __inline void mach_header_inc_ncmds(native_mach_header_t *mh, uint32_t inc) { + mh->ncmds += inc; +} + +static __inline void mach_header_inc_sizeofcmds(native_mach_header_t *mh, uint32_t inc) { + mh->sizeofcmds += inc; +} + +struct size_core { + unsigned long count; /* number-of-objects */ + size_t headersize; /* size in mach header */ + mach_vm_offset_t memsize; /* size in memory */ +}; + +struct size_segment_data { + struct size_core ssd_vanilla; /* full segments with data */ + struct size_core ssd_sparse; /* sparse segments with data */ + struct size_core ssd_fileref; /* full & sparse segments with uuid file references */ + struct size_core ssd_zfod; /* full segments with zfod pages */ +}; + +struct write_segment_data { + task_t wsd_task; + native_mach_header_t *wsd_mh; + void *wsd_lc; + int wsd_fd; + bool wsd_nocache; + off_t wsd_foffset; + off_t wsd_nwritten; +}; + +#endif /* _COREFILE_H */ diff --git a/system_cmds/gcore.tproj/dyld.c b/system_cmds/gcore.tproj/dyld.c new file mode 100644 index 0000000..92aeac1 --- /dev/null +++ b/system_cmds/gcore.tproj/dyld.c @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "dyld.h" +#include "utils.h" +#include "corefile.h" +#include "vm.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * WARNING WARNING WARNING + * + * Do not trust any of the data from the target task. + * + * A broken program may have damaged it, or a malicious + * program may have deliberately constructed something to + * cause us harm. + */ + +static const char warn_dyld_info[] = "dyld information is incomplete or damaged"; + +dyld_process_info +get_task_dyld_info(const task_t task) +{ + kern_return_t kret; + dyld_process_info dpi = _dyld_process_info_create(task, 0, &kret); + if (NULL == dpi) { + err_mach(kret, NULL, "_dlyd_process_info_create"); + } else { + dyld_process_state_info stateInfo; + + _dyld_process_info_get_state(dpi, &stateInfo); + switch (stateInfo.dyldState) { + case dyld_process_state_not_started: + warnx("%s: dyld state %d", warn_dyld_info, stateInfo.dyldState); + _dyld_process_info_release(dpi); + dpi = NULL; + break; + default: + break; + } + } + return dpi; +} + +/* + * Get the shared cache UUID iff it's in use and is the system one + */ +bool +get_sc_uuid(dyld_process_info dpi, uuid_t uu) +{ + dyld_process_cache_info cacheInfo; + + _dyld_process_info_get_cache(dpi, &cacheInfo); + if (!cacheInfo.noCache && !cacheInfo.privateCache) { + uuid_copy(uu, cacheInfo.cacheUUID); + return true; + } + return false; +} + +void +free_task_dyld_info(dyld_process_info dpi) +{ + _dyld_process_info_release(dpi); +} + +/* + * This routine collects both the Mach-O header and the commands + * "below" it, assuming they're in contiguous memory. + */ +static native_mach_header_t * +copy_dyld_image_mh(task_t task, mach_vm_address_t targetmh, const char *path) +{ + vm_offset_t mhaddr = 0; + mach_msg_type_number_t mhlen = sizeof (native_mach_header_t); + + for (int attempts = 0; attempts < 2; attempts++) { + + const kern_return_t ret = mach_vm_read(task, targetmh, mhlen, &mhaddr, &mhlen); + if (KERN_SUCCESS != ret) { + err_mach(ret, NULL, "mach_vm_read() at 0x%llx for image %s\n", targetmh, path); + mhaddr = 0; + break; + } + const native_mach_header_t *mh = (void *)mhaddr; + if (mhlen < mh->sizeofcmds + sizeof (*mh)) { + const mach_msg_type_number_t newmhlen = sizeof (*mh) + mh->sizeofcmds; + mach_vm_deallocate(mach_task_self(), mhaddr, mhlen); + mhlen = newmhlen; + } else + break; + } + + native_mach_header_t *result = NULL; + + if (mhaddr) { + if (NULL != (result = malloc(mhlen))) + memcpy(result, (void *)mhaddr, mhlen); + mach_vm_deallocate(mach_task_self(), mhaddr, mhlen); + } + return result; +} + +/* + * This table (list) describes libraries and the executable in the address space + */ +struct liblist { + STAILQ_ENTRY(liblist) ll_linkage; + unsigned long ll_namehash; + struct libent ll_entry; +}; +static STAILQ_HEAD(, liblist) libhead = STAILQ_HEAD_INITIALIZER(libhead); + +static const struct libent * +libent_lookup_bypathname_withhash(const char *nm, const unsigned long hash) +{ + struct liblist *ll; + STAILQ_FOREACH(ll, &libhead, ll_linkage) { + if (hash != ll->ll_namehash) + continue; + struct libent *le = &ll->ll_entry; + if (strcmp(nm, le->le_pathname) == 0) + return le; + } + return NULL; +} + +const struct libent * +libent_lookup_byuuid(const uuid_t uuid) +{ + struct liblist *ll; + STAILQ_FOREACH(ll, &libhead, ll_linkage) { + struct libent *le = &ll->ll_entry; + if (uuid_compare(uuid, le->le_uuid) == 0) + return le; + } + return NULL; +} + +const struct libent * +libent_lookup_first_bytype(uint32_t mhtype) +{ + struct liblist *ll; + STAILQ_FOREACH(ll, &libhead, ll_linkage) { + struct libent *le = &ll->ll_entry; + if (mhtype == le->le_mh->filetype) + return le; + } + return NULL; +} + +const struct libent * +libent_insert(const char *rawnm, const uuid_t uuid, uint64_t mhaddr, const native_mach_header_t *mh, const struct vm_range *vr, mach_vm_offset_t objoff) +{ + const struct libent *le = libent_lookup_byuuid(uuid); + if (NULL != le) + return le; // disallow multiple names for the same uuid + + char *nm = realpath(rawnm, NULL); + if (NULL == nm) + nm = strdup(rawnm); + const unsigned long nmhash = simple_namehash(nm); + le = libent_lookup_bypathname_withhash(nm, nmhash); + if (NULL != le) { + free(nm); + return le; + } + + if (OPTIONS_DEBUG(opt, 3)) { + uuid_string_t uustr; + uuid_unparse_lower(uuid, uustr); + printf("[adding <'%s', %s, 0x%llx, %p", nm, uustr, mhaddr, mh); + if (vr) + printf(" (%llx-%llx)", V_ADDR(vr), V_ENDADDR(vr)); + printf(">]\n"); + } + struct liblist *ll = malloc(sizeof (*ll)); + ll->ll_namehash = nmhash; + ll->ll_entry.le_pathname = nm; + ll->ll_entry.le_filename = strrchr(ll->ll_entry.le_pathname, '/'); + if (NULL == ll->ll_entry.le_filename) + ll->ll_entry.le_filename = ll->ll_entry.le_pathname; + else + ll->ll_entry.le_filename++; + uuid_copy(ll->ll_entry.le_uuid, uuid); + ll->ll_entry.le_mhaddr = mhaddr; + ll->ll_entry.le_mh = mh; + if (vr) + ll->ll_entry.le_vr = *vr; + else { + V_SETADDR(&ll->ll_entry.le_vr, MACH_VM_MAX_ADDRESS); + V_SETSIZE(&ll->ll_entry.le_vr, 0); + } + ll->ll_entry.le_objoff = objoff; + STAILQ_INSERT_HEAD(&libhead, ll, ll_linkage); + + return &ll->ll_entry; +} + +bool +libent_build_nametable(task_t task, dyld_process_info dpi) +{ + __block bool valid = true; + + _dyld_process_info_for_each_image(dpi, ^(uint64_t mhaddr, const uuid_t uuid, const char *path) { + if (valid) { + native_mach_header_t *mh = copy_dyld_image_mh(task, mhaddr, path); + if (mh) { + /* + * Validate the rest of the mach information in the header before attempting optimizations + */ + const size_t mhlen = sizeof (*mh) + mh->sizeofcmds; + const struct load_command *lc = (const void *)(mh + 1); + struct vm_range vr = { + .addr = MACH_VM_MAX_ADDRESS, + .size = 0 + }; + mach_vm_offset_t objoff = MACH_VM_MAX_ADDRESS; + + for (unsigned n = 0; n < mh->ncmds; n++) { + if (((uintptr_t)lc & 0x3) != 0 || + (uintptr_t)lc < (uintptr_t)mh || (uintptr_t)lc > (uintptr_t)mh + mhlen) { + warnx("%s, %d", warn_dyld_info, __LINE__); + valid = false; + break; + } + switch (lc->cmd) { + case NATIVE_LC_SEGMENT: { + const native_segment_command_t *sc = (const void *)lc; + + char scsegname[17]; + strlcpy(scsegname, sc->segname, sizeof (scsegname)); + + if (0 == sc->vmaddr && + strcmp(scsegname, SEG_PAGEZERO) == 0) + break; + + /* + * -Depends- on finding a __TEXT segment first + * which implicitly maps the mach header too + */ + + if (MACH_VM_MAX_ADDRESS == objoff) { + if (strcmp(scsegname, SEG_TEXT) == 0) { + objoff = mhaddr - sc->vmaddr; + V_SETADDR(&vr, mhaddr); + V_SETSIZE(&vr, sc->vmsize); + } else { + printf("%s: expected %s segment, found %s\n", path, SEG_TEXT, scsegname); + valid = false; + break; + } + } + + mach_vm_offset_t lo = sc->vmaddr + objoff; + mach_vm_offset_t hi = lo + sc->vmsize; + + if (V_SIZE(&vr)) { + if (lo < V_ADDR(&vr)) { + mach_vm_offset_t newsize = V_SIZE(&vr) + (V_ADDR(&vr) - lo); + V_SETSIZE(&vr, newsize); + V_SETADDR(&vr, lo); + } + if (hi > V_ENDADDR(&vr)) { + V_SETSIZE(&vr, (hi - V_ADDR(&vr))); + } + } else { + V_SETADDR(&vr, lo); + V_SETSIZE(&vr, hi - lo); + } + assert(lo >= V_ADDR(&vr) && hi <= V_ENDADDR(&vr)); + } break; +#if defined(RDAR_28040018) + case LC_ID_DYLINKER: + if (MH_DYLINKER == mh->filetype) { + /* workaround: the API doesn't always return the right name */ + const struct dylinker_command *dc = (const void *)lc; + path = dc->name.offset + (const char *)dc; + } + break; +#endif + default: + break; + } + if (NULL == (lc = next_lc(lc))) + break; + } + if (valid) + (void) libent_insert(path, uuid, mhaddr, mh, &vr, objoff); + } + } + }); + if (OPTIONS_DEBUG(opt, 3)) + printf("nametable %sconstructed\n", valid ? "" : "NOT "); + return valid; +} diff --git a/system_cmds/gcore.tproj/dyld.h b/system_cmds/gcore.tproj/dyld.h new file mode 100644 index 0000000..02ded2a --- /dev/null +++ b/system_cmds/gcore.tproj/dyld.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "corefile.h" +#include "utils.h" + +#include +#include +#include + +#ifndef _DYLD_H +#define _DYLD_H + +struct libent { + const char *le_filename; // (points into le_pathname!) + char *le_pathname; + uuid_t le_uuid; + uint64_t le_mhaddr; // address in target process + const native_mach_header_t *le_mh; // copy mapped into this address space + struct vm_range le_vr; // vmaddr, vmsize bounds in target process + mach_vm_offset_t le_objoff; // offset from le_mhaddr to first __TEXT seg +}; + +extern const struct libent *libent_lookup_byuuid(const uuid_t); +extern const struct libent *libent_lookup_first_bytype(uint32_t); +extern const struct libent *libent_insert(const char *, const uuid_t, uint64_t, const native_mach_header_t *, const struct vm_range *, mach_vm_offset_t); +extern bool libent_build_nametable(task_t, dyld_process_info); + +extern dyld_process_info get_task_dyld_info(task_t); +extern bool get_sc_uuid(dyld_process_info, uuid_t); +extern void free_task_dyld_info(dyld_process_info); + +#endif /* _DYLD_H */ diff --git a/system_cmds/gcore.tproj/dyld_shared_cache.c b/system_cmds/gcore.tproj/dyld_shared_cache.c new file mode 100644 index 0000000..0e21163 --- /dev/null +++ b/system_cmds/gcore.tproj/dyld_shared_cache.c @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "dyld_shared_cache.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const size_t dyld_cache_header_size = sizeof (struct copied_dyld_cache_header); + +/* + * The shared cache must both contain the magic ID and + * match the uuid we discovered via dyld's information. + * Assumes that the dyld_cache_header grows in a binary compatible fashion. + */ +bool +get_uuid_from_shared_cache_mapping(const void *addr, size_t size, uuid_t out) +{ + const struct copied_dyld_cache_header *ch = addr; + if (size < sizeof (*ch)) + return false; + static const char prefix[] = "dyld_v"; + if (strncmp(ch->magic, prefix, strlen(prefix)) != 0) + return false; + uuid_copy(out, ch->uuid); + return true; +} + +/* + * Look in the known places to see if we can find this one .. + */ +char * +shared_cache_filename(const uuid_t uu) +{ + assert(!uuid_is_null(uu)); + static char *sc_argv[] = { +#if TARGET_OS_OSX + "/System/Library/dyld", +#elif TARGET_OS_IPHONE + "/System/Library/Caches/com.apple.dyld", +#else +#error undefined +#endif + NULL + }; + char *nm = NULL; + FTS *fts = fts_open(sc_argv, FTS_NOCHDIR | FTS_LOGICAL | FTS_XDEV, NULL); + if (NULL != fts) { + FTSENT *fe; + while (NULL != (fe = fts_read(fts))) { + if ((fe->fts_info & FTS_F) == 0 || + (fe->fts_info & FTS_ERR) != 0) + continue; + + static const char prefix[] = "dyld_shared_cache_"; + if (strncmp(fe->fts_name, prefix, strlen(prefix)) != 0) + continue; + + if (fe->fts_statp->st_size < (off_t)dyld_cache_header_size) + continue; + + int d = open(fe->fts_accpath, O_RDONLY); + if (-1 == d) { + if (OPTIONS_DEBUG(opt, 1)) + printf("%s: cannot open - %s\n", fe->fts_accpath, strerror(errno)); + continue; + } + void *addr = mmap(NULL, dyld_cache_header_size, PROT_READ, MAP_PRIVATE, d, 0); + close(d); + if ((void *)-1 == addr) { + if (OPTIONS_DEBUG(opt, 1)) + printf("%s: cannot mmap - %s\n", fe->fts_accpath, strerror(errno)); + continue; + } + uuid_t scuuid; + uuid_clear(scuuid); + if (get_uuid_from_shared_cache_mapping(addr, dyld_cache_header_size, scuuid)) { + if (uuid_compare(uu, scuuid) == 0) + nm = strdup(fe->fts_accpath); + else if (OPTIONS_DEBUG(opt, 3)) { + uuid_string_t scstr; + uuid_unparse_lower(scuuid, scstr); + printf("%s: shared cache mismatch (%s)\n", fe->fts_accpath, scstr); + } + } + munmap(addr, dyld_cache_header_size); + if (NULL != nm) + break; + } + } + if (fts) + fts_close(fts); + return nm; +} diff --git a/system_cmds/gcore.tproj/dyld_shared_cache.h b/system_cmds/gcore.tproj/dyld_shared_cache.h new file mode 100644 index 0000000..1fe0fac --- /dev/null +++ b/system_cmds/gcore.tproj/dyld_shared_cache.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2015 Apple Inc. All rights reserved. + */ + +#include +#include +#include +#include + +#ifndef _DYLD_SHARED_CACHE_H +#define _DYLD_SHARED_CACHE_H + +/* + * Guilty knowledge of dyld shared cache internals, used to verify shared cache + */ +struct copied_dyld_cache_header { + char magic[16]; // e.g. "dyld_v0 i386" + uint32_t mappingOffset; // file offset to first dyld_cache_mapping_info + uint32_t mappingCount; // number of dyld_cache_mapping_info entries + uint32_t imagesOffset; // file offset to first dyld_cache_image_info + uint32_t imagesCount; // number of dyld_cache_image_info entries + uint64_t dyldBaseAddress; // base address of dyld when cache was built + uint64_t codeSignatureOffset; // file offset of code signature blob + uint64_t codeSignatureSize; // size of code signature blob (zero means to end of file) + uint64_t slideInfoOffset; // file offset of kernel slid info + uint64_t slideInfoSize; // size of kernel slid info + uint64_t localSymbolsOffset; // file offset of where local symbols are stored + uint64_t localSymbolsSize; // size of local symbols information + uint8_t uuid[16]; // unique value for each shared cache file + uint64_t cacheType; // 1 for development, 0 for optimized +}; + +extern bool get_uuid_from_shared_cache_mapping(const void *, size_t, uuid_t); +extern char *shared_cache_filename(const uuid_t); + +#endif /* _DYLD_SHARED_CACHE_H */ diff --git a/system_cmds/gcore.tproj/gcore-entitlements.plist b/system_cmds/gcore.tproj/gcore-entitlements.plist new file mode 100644 index 0000000..39c14ef --- /dev/null +++ b/system_cmds/gcore.tproj/gcore-entitlements.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.debugger.root + + + diff --git a/system_cmds/gcore.tproj/gcore-internal.1 b/system_cmds/gcore.tproj/gcore-internal.1 new file mode 100644 index 0000000..923f3e9 --- /dev/null +++ b/system_cmds/gcore.tproj/gcore-internal.1 @@ -0,0 +1,201 @@ +.Dd 9/29/16 +.Dt gcore-internal 1 +.Os Darwin +.Sh NAME +.Nm gcore +.Nd get core images of running processes and corpses +.Sh SYNOPSIS +.Nm +.Op Fl x +.Op Fl F +.Op Fl C +.Op Fl Z Ar compopts +.Op Fl t Ar threshold +.Op Fl d +.Ar args ... +.Nm +.Sy conv +.Op Fl L Ar searchpath +.Op Fl z +.Op Fl s +.Op Fl v +.Op Fl d +.Ar incore outcore +.Nm +.Sy map +.Ar corefile +.Nm +.Sy fref +.Ar corefile +.Sh DESCRIPTION +For an introduction to this command and its options, see +.Xr gcore 1 . +This page describes various experimental capabilities +of the +.Nm +command intended, for the moment, for internal use only. +.Pp +The following set of additional flags are available: +.Bl -tag -width Fl +.It Fl x +Create extended (compact) core files. With this flag, +.Nm +elides empty and unmapped regions from the dump, and uses +metadata from the VM system and +.Xr dyld 1 +to minimize the size of the dump, writing compressed versions of +the active regions of the address space into the dump file. +.Nm +also records file references to the various files mapped into the +address space, potentially including the shared cache, to +avoid duplicating content already present on the filesystem. +Taken together, these techniques can lead to very significant +space savings for the core file, particularly for smaller programs. +.It Fl F +Normally when +.Fl x +is specified, +.Nm +makes conservative assumptions about which files should be +incorporated into the dump as file references so that the +full core file can be recreated later. This flag attempts to make +.Em every +mapped file into a file reference. While this can occasionally +be useful for applications that map many files into their address space, +it may be +.Em extremely +difficult to recreate the process image as a result. +Use cautiously! +.El +.Pp +The remaining options are more relevant to the +.Nm +maintainers: +.Bl -tag -width Fl +.It Fl C +Forcibly generate a corpse for the process, even if the process is suspended. +.It Fl Z Ar compopts +Specify compression options e.g. algorithm and chunksize. +.It Fl t Ar threshold +Set the threshold at which I/O caching is disabled to +.Ar threshold +KiBytes. +.It Fl d +Enable debugging of the +.Nm +command. +.El +.Pp +If the +.Ar pid +value is specified as 0, +.Nm +assumes it has been passed a corpse port by its parent; +if so it will generate a core dump for that corpse. The +.Fl c +flag may not be used in this case, as the process context may no longer exist. +.Pp +The +.Nm +command supports several sub-commands that can be +used with extended core files created using the +.Fl x +flag. These are: +.Bl -tag -width frefs +.\" -compact -offset indent +.Pp +.It Sy conv +Copy and convert a core file to the "pre-coreinfo" format +compatible with +.Xr lldb(1) . +This operation reads the input core file dereferencing any file +references it contains by copying the content +and decompressing any compressed data into the output core file. +This conversion usually makes the core file substantially larger. +.Pp +By default, files to be dereferenced must be accessible on the +local filesystem by the same relative paths as they were originally recorded +when the dump was taken. +Files that are Mach-O objects containing UUIDs are required to match +the UUIDs recorded at the time the core dump was taken. +Files are otherwise only checked for matching modification times, and +thus can easily be "forged" using +.Xr touch 1 . +.Pp +Several flags can be used with the conversion: +.Pp +.Bl -tag -width Fl +.It Fl L Ar searchpath +When processing file references, +look for the pathnames in the directories specified in +.Ar searchpath . +These should be specified as a colon-separated +list of base directories which will be prepended to each pathname in turn +for each file reference. +.It Fl z +During conversion, if any mapped file +identified by modification time +cannot be located, substitute zeroed memory. +.It Fl s +When processing file references, +try looking for the pathname through +.Xr dsymForUUID 1 +before searching locally. +.It Fl v +Report progress on the conversion as it proceeds. +.It Fl d +Enable debugging of the +.Sy conv +subcommand. +.El +.It Sy map +Print a representation of the address space contained in the core file. +.Pp +.It Sy frefs +Print a list of files corresponding to the file references +in the core file. +Can be used to capture the set of files needed to bind the file references +into the core file at a later time. +.El +.Sh BUGS +.Pp +When using the +.Fl x +flag, +.Nm +will likely incorporate a reference to the shared cache into +.Ar corefile +including the UUID of that cache. +On some platforms, the cache is created when the release is built +and since it resides on a read-only root filesystem it should +generally be easy to retrieve. +However on the desktop, the lifecycle of this cache is managed locally +e.g. with +.Xr update_dyld_shared_cache 1 . +When this cache is recreated it is given a new UUID, the directory +entry for the old cache is removed, and the same filename +is used for the new cache. +Thus when the last named copy of the shared cache is removed from the +filesystem, extended core files that contain references to that cache +can no longer be converted. +.Pp +When using the +.Fl x +flag, +.Nm +may be unable to locate the currently mapped shared cache in the filesystem. +In this case +.Nm +does not generate any file references to the content of the +shared cache; it just copies as much of the content +as is needed from the memory image into the core file. +This may lead to substantially larger core files than expected. +.Pp +It would be nice if +.Xr lldb 1 +could examine extended core files directly i.e. without the conversion step. +.Pp +.Sh SEE ALSO +.Xr dyld 1 , +.Xr update_dyld_shared_cache 1 , +.Xr dsymForUUID 1 diff --git a/system_cmds/gcore.tproj/gcore.1 b/system_cmds/gcore.tproj/gcore.1 new file mode 100644 index 0000000..48b360d --- /dev/null +++ b/system_cmds/gcore.tproj/gcore.1 @@ -0,0 +1,105 @@ +.Dd 2/10/16 +.Dt gcore 1 +.Os Darwin +.Sh NAME +.Nm gcore +.Nd get core images of running processes +.Sh SYNOPSIS +.Nm +.Op Fl s +.Op Fl v +.Op Fl b Ar size +.Op Fl o Ar path | Fl c Ar pathformat +.Ar pid +.Sh DESCRIPTION +The +.Nm gcore +program creates a core file image of the process specified by +.Ar pid . +The resulting core file can be used with a debugger, e.g. +.Xr lldb(1) , +to examine the state of the process. +.Pp +The following options are available: +.Bl -tag -width Fl +.It Fl s +Suspend the process while the core file is captured. +.It Fl v +Report progress on the dump as it proceeds. +.It Fl b Ar size +Limit the size of the core file to +.Ar size +MiBytes. +.El +.Pp +The following options control the name of the core file: +.Bl -tag -width flag +.It Fl o Ar path +Write the core file to +.Ar path . +.It Fl c Ar pathformat +Write the core file to +.Ar pathformat . +The +.Ar pathformat +string is treated as a pathname that may contain various special +characters which cause the interpolation of strings representing +specific attributes of the process into the name. +.Pp +Each special character is introduced by the +.Cm % +character. The format characters and their meanings are: +.Bl -tag -width Fl +.It Cm N +The name of the program being dumped, as reported by +.Xr ps 1 . +.It Cm U +The uid of the process being dumped, converted to a string. +.It Cm P +The pid of the process being dumped, converted to a string. +.It Cm T +The time when the core file was taken, converted to ISO 8601 format. +.It Cm % +Output a percent character. +.El +.El +.Pp +The default file name used by +.Nm gcore +is +.Ar %N-%P-%T . +By default, the core file will be written to a directory whose +name is determined from the +.Ar kern.corefile +MIB. This can be printed or modified using +.Xr sysctl 8 . +.Pp +The directory where the core file is to be written must be +accessible to the owner of the target process. +.Pp +.Nm gcore +will not overwrite an existing file, +nor will it create missing directories in the path. +.Sh EXIT_STATUS +.Ex -std +.Pp +.Sh FILES +.Bl -tag -width "/cores/%N-%P-%T plus" -compact +.It Pa /cores/%N-%P-%T +default pathname for the corefile. +.El +.Sh BUGS +With the +.Fl b +flag, +.Nm gcore +writes out as much data as it can up to the specified limit, +even if that results in an incomplete core image. +Such a partial core dump may confuse subsequent +programs that attempt to parse the contents of such files. +.Sh SEE ALSO +.Xr lldb 1 , +.Xr core 5 , +.Xr Mach-O 5 , +.Xr sysctl 8 , +.Xr sudo 8 . diff --git a/system_cmds/gcore.tproj/loader_additions.h b/system_cmds/gcore.tproj/loader_additions.h new file mode 100644 index 0000000..16cf5b5 --- /dev/null +++ b/system_cmds/gcore.tproj/loader_additions.h @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2015 Apple Inc. All rights reserved. + */ + +#include + +#ifndef _LOADER_ADDITIONS_H +#define _LOADER_ADDITIONS_H + +/* + * Something like this should end up in + */ + +#define proto_LC_COREINFO 0x140 /* unofficial value!! */ + +#define proto_CORETYPE_KERNEL 1 +#define proto_CORETYPE_USER 2 +#define proto_CORETYPE_IBOOT 3 + +struct proto_coreinfo_command { + uint32_t cmd; /* LC_COREINFO */ + uint32_t cmdsize; /* total size of this command */ + uint32_t version; /* currently 1 */ + uint16_t type; /* CORETYPE_KERNEL, CORETYPE_USER etc. */ + uint16_t pageshift; /* log2 host pagesize */ + /* content & interpretation depends on 'type' field */ + uint64_t address; /* load address of "main binary" */ + uint8_t uuid[16]; /* uuid of "main binary" */ + uint64_t dyninfo; /* dynamic modules info */ +}; + +#define proto_LC_FILEREF 0x141 /* unofficial value! */ + +#define FREF_ID_SHIFT 0 +#define FREF_ID_MASK 0x7 /* up to 8 flavors */ + +typedef enum { + kFREF_ID_NONE = 0, /* file has no available verifying ID */ + kFREF_ID_UUID = 1, /* file has an associated UUID */ + kFREF_ID_MTIMESPEC_LE = 2, /* file has a specific mtime */ + /* one day: file has a computed hash? */ +} fref_type_t; + +#define FREF_ID_TYPE(f) ((fref_type_t)(((f) >> FREF_ID_SHIFT) & FREF_ID_MASK)) +#define FREF_MAKE_FLAGS(t) (((t) & FREF_ID_MASK) << FREF_ID_SHIFT) + +struct proto_fileref_command { + uint32_t cmd; /* LC_FILEREF */ + uint32_t cmdsize; + union lc_str filename; /* filename these bits come from */ + uint8_t id[16]; /* uuid, size or hash etc. */ + uint64_t vmaddr; /* memory address of this segment */ + uint64_t vmsize; /* memory size of this segment */ + uint64_t fileoff; /* file offset of this segment */ + uint64_t filesize; /* amount to map from the file */ + vm_prot_t maxprot; /* maximum VM protection */ + vm_prot_t prot; /* current VM protection */ + uint32_t flags; + uint8_t share_mode; /* SM_COW etc. */ + uint8_t purgable; /* VM_PURGABLE_NONVOLATILE etc. */ + uint8_t tag; /* VM_MEMORY_MALLOC etc. */ + uint8_t extp; /* external pager */ +}; + +#define proto_LC_COREDATA 0x142 /* unofficial value! */ + +/* + * These are flag bits for the segment_command 'flags' field. + */ + +#define COMP_ALG_MASK 0x7 +/* carve out 3 bits for an enum i.e. allow for 7 flavors */ +#define COMP_ALG_SHIFT 4 /* (bottom 4 bits taken) */ + +/* zero -> no compression */ +typedef enum { + kCOMP_NONE = 0, + kCOMP_LZ4 = 1, /* 0x100 */ + kCOMP_ZLIB = 2, /* 0x205 */ + kCOMP_LZMA = 3, /* 0x306 */ + kCOMP_LZFSE = 4, /* 0x801 */ +} compression_flavor_t; + +#define COMP_ALG_TYPE(f) ((compression_flavor_t)((f) >> COMP_ALG_SHIFT) & COMP_ALG_MASK) +#define COMP_MAKE_FLAGS(t) (((t) & COMP_ALG_MASK) << COMP_ALG_SHIFT) + +struct proto_coredata_command { + uint32_t cmd; /* LC_COREDATA */ + uint32_t cmdsize; + uint64_t vmaddr; /* memory address of this segment */ + uint64_t vmsize; /* memory size of this segment */ + uint64_t fileoff; /* file offset of this segment */ + uint64_t filesize; /* amount to map from the file */ + vm_prot_t maxprot; /* maximum VM protection */ + vm_prot_t prot; /* current VM protection */ + uint32_t flags; + uint8_t share_mode; /* SM_COW etc. */ + uint8_t purgable; /* VM_PURGABLE_NONVOLATILE etc. */ + uint8_t tag; /* VM_MEMORY_MALLOC etc. */ + uint8_t extp; /* external pager */ +}; + +#endif /* _LOADER_ADDITIONS_H */ diff --git a/system_cmds/gcore.tproj/main.c b/system_cmds/gcore.tproj/main.c new file mode 100644 index 0000000..abefa14 --- /dev/null +++ b/system_cmds/gcore.tproj/main.c @@ -0,0 +1,863 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "utils.h" +#include "corefile.h" +#include "vanilla.h" +#include "sparse.h" +#include "convert.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static char * +kern_corefile(void) +{ + char *(^sysc_string)(const char *name) = ^(const char *name) { + char *p = NULL; + size_t len = 0; + + if (-1 == sysctlbyname(name, NULL, &len, NULL, 0)) { + warnc(errno, "sysctl: %s", name); + } else if (0 != len) { + p = malloc(len); + if (-1 == sysctlbyname(name, p, &len, NULL, 0)) { + warnc(errno, "sysctl: %s", name); + free(p); + p = NULL; + } + } + return p; + }; + + char *s = sysc_string("kern.corefile"); + if (NULL == s) + s = strdup("/cores/core.%P"); + return s; +} + +static const struct proc_bsdinfo * +get_bsdinfo(pid_t pid) +{ + if (0 == pid) + return NULL; + struct proc_bsdinfo *pbi = calloc(1, sizeof (*pbi)); + if (0 != proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, pbi, sizeof (*pbi))) + return pbi; + free(pbi); + return NULL; +} + +static char * +format_gcore_name(const char *fmt, pid_t pid, uid_t uid, const char *nm) +{ + __block size_t resid = MAXPATHLEN; + __block char *p = calloc(1, resid); + char *out = p; + + int (^outchar)(int c) = ^(int c) { + if (resid > 1) { + *p++ = (char)c; + resid--; + return 1; + } else + return 0; + }; + + ptrdiff_t (^outstr)(const char *str) = ^(const char *str) { + const char *s = str; + while (*s && 0 != outchar(*s++)) + ; + return s - str; + }; + + ptrdiff_t (^outint)(int value) = ^(int value) { + char id[11]; + snprintf(id, sizeof (id), "%u", value); + return outstr(id); + }; + + ptrdiff_t (^outtstamp)(void) = ^(void) { + time_t now; + time(&now); + struct tm tm; + gmtime_r(&now, &tm); + char tstamp[50]; + strftime(tstamp, sizeof (tstamp), "%Y%m%dT%H%M%SZ", &tm); + return outstr(tstamp); + }; + + int c; + + for (int i = 0; resid > 0; i++) + switch (c = fmt[i]) { + default: + outchar(c); + break; + case '%': + i++; + switch (c = fmt[i]) { + case '%': + outchar(c); + break; + case 'P': + outint(pid); + break; + case 'U': + outint(uid); + break; + case 'N': + outstr(nm); + break; + case 'T': + outtstamp(); // ISO 8601 format + break; + default: + if (isprint(c)) + err(EX_DATAERR, "unknown format char: %%%c", c); + else if (c != 0) + err(EX_DATAERR, "bad format char %%\\%03o", c); + else + err(EX_DATAERR, "bad format specifier"); + } + break; + case 0: + outchar(c); + goto done; + } +done: + return out; +} + +static char * +make_gcore_path(char **corefmtp, pid_t pid, uid_t uid, const char *nm) +{ + char *corefmt = *corefmtp; + if (NULL == corefmt) { + const char defcore[] = "%N-%P-%T"; + if (NULL == (corefmt = kern_corefile())) + corefmt = strdup(defcore); + else { + // use the same directory as kern.corefile + char *p = strrchr(corefmt, '/'); + if (NULL != p) { + *p = '\0'; + size_t len = strlen(corefmt) + strlen(defcore) + 2; + char *buf = malloc(len); + snprintf(buf, len, "%s/%s", corefmt, defcore); + free(corefmt); + corefmt = buf; + } + if (OPTIONS_DEBUG(opt, 3)) + printf("corefmt '%s'\n", corefmt); + } + } + char *path = format_gcore_name(corefmt, pid, uid, nm); + free(corefmt); + *corefmtp = NULL; + return path; +} + +static bool proc_same_data_model(const struct proc_bsdinfo *pbi) { +#if defined(__LP64__) + return (pbi->pbi_flags & PROC_FLAG_LP64) != 0; +#else + return (pbi->pbi_flags & PROC_FLAG_LP64) == 0; +#endif +} + +static bool task_same_data_model(const task_flags_info_data_t *tfid) { +#if defined(__LP64__) + return (tfid->flags & TF_LP64) != 0; +#else + return (tfid->flags & TF_LP64) == 0; +#endif +} + +/* + * Change credentials for writing out the file + */ +static void +change_credentials(gid_t uid, uid_t gid) +{ + if ((getgid() != gid && -1 == setgid(gid)) || + (getuid() != uid && -1 == setuid(uid))) + errc(EX_NOPERM, errno, "insufficient privilege"); + if (uid != getuid() || gid != getgid()) + err(EX_OSERR, "wrong credentials"); +} + +static int +openout(const char *corefname, char **coretname, struct stat *st) +{ + const int tfd = open(corefname, O_WRONLY); + if (-1 == tfd) { + if (ENOENT == errno) { + /* + * Arrange for a core file to appear "atomically": write the data + * to the file + ".tmp" suffix, then fchmod and rename it into + * place once the dump completes successfully. + */ + const size_t nmlen = strlen(corefname) + 4 + 1; + char *tnm = malloc(nmlen); + snprintf(tnm, nmlen, "%s.tmp", corefname); + const int fd = open(tnm, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (-1 == fd || -1 == fstat(fd, st)) + errc(EX_CANTCREAT, errno, "%s", tnm); + if (!S_ISREG(st->st_mode) || 1 != st->st_nlink) + errx(EX_CANTCREAT, "%s: invalid attributes", tnm); + *coretname = tnm; + return fd; + } else + errc(EX_CANTCREAT, errno, "%s", corefname); + } else if (-1 == fstat(tfd, st)) { + close(tfd); + errx(EX_CANTCREAT, "%s: fstat", corefname); + } else if (S_ISBLK(st->st_mode) || S_ISCHR(st->st_mode)) { + /* + * Write dump to a device, no rename! + */ + *coretname = NULL; + return tfd; + } else { + close(tfd); + errc(EX_CANTCREAT, EEXIST, "%s", corefname); + } +} + +static int +closeout(int fd, int ecode, char *corefname, char *coretname, const struct stat *st) +{ + if (0 != ecode && !opt->preserve && S_ISREG(st->st_mode)) + ftruncate(fd, 0); // limit large file clutter + if (0 == ecode && S_ISREG(st->st_mode)) + fchmod(fd, 0400); // protect core files + if (-1 == close(fd)) { + warnc(errno, "%s: close", coretname ? coretname : corefname); + ecode = EX_OSERR; + } + if (NULL != coretname) { + if (0 == ecode && -1 == rename(coretname, corefname)) { + warnc(errno, "cannot rename %s to %s", coretname, corefname); + ecode = EX_NOPERM; + } + free(coretname); + } + if (corefname) + free(corefname); + return ecode; +} + +const char *pgm; +const struct options *opt; + +static const size_t oneK = 1024; +static const size_t oneM = oneK * oneK; + +#define LARGEST_CHUNKSIZE INT32_MAX +#define DEFAULT_COMPRESSION_CHUNKSIZE (16 * oneM) +#define DEFAULT_NC_THRESHOLD (17 * oneK) + +static struct options options = { + .corpsify = 0, + .suspend = 0, + .preserve = 0, + .verbose = 0, +#ifdef CONFIG_DEBUG + .debug = 0, +#endif + .extended = 0, + .sizebound = 0, + .chunksize = 0, + .calgorithm = COMPRESSION_LZFSE, + .ncthresh = DEFAULT_NC_THRESHOLD, + .dsymforuuid = 0, +}; + +static int +gcore_main(int argc, char *const *argv) +{ +#define ZOPT_ALG (0) +#define ZOPT_CHSIZE (ZOPT_ALG + 1) + + static char *const zoptkeys[] = { + [ZOPT_ALG] = "algorithm", + [ZOPT_CHSIZE] = "chunksize", + NULL + }; + + err_set_exit_b(^(int eval) { + if (EX_USAGE == eval) { + fprintf(stderr, + "usage:\t%s [-s] [-v] [[-o file] | [-c pathfmt ]] [-b size] " +#if DEBUG +#ifdef CONFIG_DEBUG + "[-d] " +#endif + "[-x] [-C] " + "[-Z compression-options] " + "[-t size] " + "[-F] " +#endif + "pid\n", pgm); +#if DEBUG + fprintf(stderr, "where compression-options:\n"); + const char zvalfmt[] = "\t%s=%s\t\t%s\n"; + fprintf(stderr, zvalfmt, zoptkeys[ZOPT_ALG], "alg", + "set compression algorithm"); + fprintf(stderr, zvalfmt, zoptkeys[ZOPT_CHSIZE], "size", + "set compression chunksize, Mib"); +#endif + } + }); + + char *corefmt = NULL; + char *corefname = NULL; + + int c; + char *sopts, *value; + + while ((c = getopt(argc, argv, "vdsxCFZ:o:c:b:t:")) != -1) { + switch (c) { + + /* + * documented options + */ + case 's': /* FreeBSD compat: stop while gathering */ + options.suspend = 1; + break; + case 'o': /* Linux (& SunOS) compat: basic name */ + corefname = strdup(optarg); + break; + case 'c': /* FreeBSD compat: basic name */ + /* (also allows pattern-based naming) */ + corefmt = strdup(optarg); + break; + + case 'b': /* bound the size of the core file */ + if (NULL != optarg) { + off_t bsize = atoi(optarg) * oneM; + if (bsize > 0) + options.sizebound = bsize; + else + errx(EX_USAGE, "invalid bound"); + } else + errx(EX_USAGE, "no bound specified"); + break; + case 'v': /* verbose output */ + options.verbose++; + break; + + /* + * dev and debugging help + */ +#ifdef CONFIG_DEBUG + case 'd': /* debugging */ + options.debug++; + options.verbose++; + options.preserve++; + break; +#endif + /* + * Remaining options are experimental and/or + * affect the content of the core file + */ + case 'x': /* write extended format (small) core files */ + options.extended++; + options.chunksize = DEFAULT_COMPRESSION_CHUNKSIZE; + break; + case 'C': /* forcibly corpsify rather than suspend */ + options.corpsify++; + break; + case 'Z': /* control compression options */ + /* + * Only LZFSE and LZ4 seem practical. + * (Default to LZ4 compression when the + * process is suspended, LZFSE when corpsed?) + */ + if (0 == options.extended) + errx(EX_USAGE, "illegal flag combination"); + sopts = optarg; + while (*sopts) { + size_t chsize; + + switch (getsubopt(&sopts, zoptkeys, &value)) { + case ZOPT_ALG: /* change the algorithm */ + if (NULL == value) + errx(EX_USAGE, "missing algorithm for " + "%s suboption", + zoptkeys[ZOPT_ALG]); + if (strcmp(value, "lz4") == 0) + options.calgorithm = COMPRESSION_LZ4; + else if (strcmp(value, "zlib") == 0) + options.calgorithm = COMPRESSION_ZLIB; + else if (strcmp(value, "lzma") == 0) + options.calgorithm = COMPRESSION_LZMA; + else if (strcmp(value, "lzfse") == 0) + options.calgorithm = COMPRESSION_LZFSE; + else + errx(EX_USAGE, "unknown algorithm '%s'" + " for %s suboption", + value, zoptkeys[ZOPT_ALG]); + break; + case ZOPT_CHSIZE: /* set the chunksize */ + if (NULL == value) + errx(EX_USAGE, "no value specified for " + "%s suboption", + zoptkeys[ZOPT_CHSIZE]); + if ((chsize = atoi(value)) < 1) + errx(EX_USAGE, "chunksize %lu too small", chsize); + if (chsize > (LARGEST_CHUNKSIZE / oneM)) + errx(EX_USAGE, "chunksize %lu too large", chsize); + options.chunksize = chsize * oneM; + break; + default: + if (suboptarg) + errx(EX_USAGE, "illegal suboption '%s'", + suboptarg); + else + errx(EX_USAGE, "missing suboption"); + } + } + break; + case 't': /* set the F_NOCACHE threshold */ + if (NULL != optarg) { + size_t tsize = atoi(optarg) * oneK; + if (tsize > 0) + options.ncthresh = tsize; + else + errx(EX_USAGE, "invalid nc threshold"); + } else + errx(EX_USAGE, "no threshold specified"); + break; + case 'F': /* maximize filerefs */ + options.allfilerefs++; + break; + default: + errx(EX_USAGE, "unknown flag"); + } + } + + if (optind == argc) + errx(EX_USAGE, "no pid specified"); + if (optind < argc-1) + errx(EX_USAGE, "too many arguments"); + + opt = &options; + if (NULL != corefname && NULL != corefmt) + errx(EX_USAGE, "specify only one of -o and -c"); + if (!opt->extended && opt->allfilerefs) + errx(EX_USAGE, "unknown flag"); + + setpageshift(); + + if (opt->ncthresh < ((vm_offset_t)1 << pageshift_host)) + errx(EX_USAGE, "threshold %lu less than host pagesize", opt->ncthresh); + + const pid_t apid = atoi(argv[optind]); + pid_t pid = apid; + mach_port_t corpse = MACH_PORT_NULL; + kern_return_t ret; + + if (0 == apid) { + /* look for corpse - dead or alive */ + mach_port_array_t parray = NULL; + mach_msg_type_number_t pcount = 0; + ret = mach_ports_lookup(mach_task_self(), &parray, &pcount); + if (KERN_SUCCESS == ret && pcount > 0) { + task_t tcorpse = parray[0]; + mig_deallocate((vm_address_t)parray, pcount * sizeof (*parray)); + pid_t tpid = 0; + ret = pid_for_task(tcorpse, &tpid); + if (KERN_SUCCESS == ret && tpid != getpid()) { + corpse = tcorpse; + pid = tpid; + } + } + } + + if (pid < 1 || getpid() == pid) + errx(EX_DATAERR, "invalid pid: %d", pid); + + if (0 == apid && MACH_PORT_NULL == corpse) + errx(EX_DATAERR, "missing or bad corpse from parent"); + + task_t task = TASK_NULL; + const struct proc_bsdinfo *pbi = NULL; + const int rc = kill(pid, 0); + + if (rc == 0) { + /* process or corpse that may respond to signals */ + pbi = get_bsdinfo(pid); + } + + if (rc == 0 && pbi != NULL) { + /* process or corpse that responds to signals */ + + /* make our data model match the data model of the target */ + if (-1 == reexec_to_match_lp64ness(pbi->pbi_flags & PROC_FLAG_LP64)) + errc(1, errno, "cannot match data model of %d", pid); + + if (!proc_same_data_model(pbi)) + errx(EX_OSERR, "cannot match data model of %d", pid); + + if (pbi->pbi_ruid != pbi->pbi_svuid || + pbi->pbi_rgid != pbi->pbi_svgid) + errx(EX_NOPERM, "pid %d - not dumping a set-id process", pid); + + if (NULL == corefname) + corefname = make_gcore_path(&corefmt, pbi->pbi_pid, pbi->pbi_uid, pbi->pbi_name[0] ? pbi->pbi_name : pbi->pbi_comm); + + if (MACH_PORT_NULL == corpse) { + ret = task_for_pid(mach_task_self(), pid, &task); + if (KERN_SUCCESS != ret) { + if (KERN_FAILURE == ret) + errx(EX_NOPERM, "insufficient privilege"); + else + errx(EX_NOPERM, "task_for_pid: %s", mach_error_string(ret)); + } + } + + /* + * Have either the corpse port or the task port so adopt the + * credentials of the target process, *before* opening the + * core file, and analyzing the address space. + * + * If we are unable to match the target credentials, bail out. + */ + change_credentials(pbi->pbi_uid, pbi->pbi_gid); + } else { + if (MACH_PORT_NULL == corpse) { + if (rc == 0) { + errx(EX_OSERR, "cannot get process info for %d", pid); + } + switch (errno) { + case ESRCH: + errc(EX_DATAERR, errno, "no process with pid %d", pid); + default: + errc(EX_DATAERR, errno, "pid %d", pid); + } + } + /* a corpse with no live process backing it */ + + assert(0 == apid && TASK_NULL == task); + + task_flags_info_data_t tfid; + mach_msg_type_number_t count = TASK_FLAGS_INFO_COUNT; + ret = task_info(corpse, TASK_FLAGS_INFO, (task_info_t)&tfid, &count); + if (KERN_SUCCESS != ret) + err_mach(ret, NULL, "task_info"); + if (!task_same_data_model(&tfid)) + errx(EX_OSERR, "data model mismatch for target corpse"); + + if (opt->suspend || opt->corpsify) + errx(EX_USAGE, "cannot use -s or -C option with a corpse"); + if (NULL != corefmt) + errx(EX_USAGE, "cannot use -c with a corpse"); + if (NULL == corefname) + corefname = make_gcore_path(&corefmt, pid, -2, "corpse"); + + /* + * Only have a corpse, thus no process credentials. + * Switch to nobody. + */ + change_credentials(-2, -2); + } + + struct stat cst; + char *coretname = NULL; + const int fd = openout(corefname, &coretname, &cst); + + if (opt->verbose) { + printf("Dumping core "); + if (OPTIONS_DEBUG(opt, 1)) { + printf("(%s", opt->extended ? "extended" : "vanilla"); + if (0 != opt->sizebound) { + hsize_str_t hstr; + printf(", <= %s", str_hsize(hstr, opt->sizebound)); + } + printf(") "); + } + printf("for pid %d to %s\n", pid, corefname); + } + + int ecode; + + if (MACH_PORT_NULL == corpse) { + assert(TASK_NULL != task); + + /* + * The "traditional" way to capture a consistent core dump is to + * suspend the process while examining it and writing it out. + * Yet suspending a large process for a long time can have + * unpleasant side-effects. Alternatively dumping from the live + * process can lead to an inconsistent state in the core file. + * + * Instead we can ask xnu to create a 'corpse' - the process is transiently + * suspended while a COW snapshot of the address space is constructed + * in the kernel and dump from that. This vastly reduces the suspend + * time, but it is more resource hungry and thus may fail. + * + * The -s flag (opt->suspend) causes a task_suspend/task_resume + * The -C flag (opt->corpse) causes a corpse be taken, exiting if that fails. + * Both flags can be specified, in which case corpse errors are ignored. + * + * With no flags, we imitate traditional behavior though more + * efficiently: we try to take a corpse-based dump, in the event that + * fails, dump the live process. + */ + + int trycorpse = 1; /* default: use corpses */ + int badcorpse_is_fatal = 1; /* default: failure to create is an error */ + + if (!opt->suspend && !opt->corpsify) { + /* try a corpse dump, else dump the live process */ + badcorpse_is_fatal = 0; + } else if (opt->suspend) { + trycorpse = opt->corpsify; + /* if suspended anyway, ignore corpse-creation errors */ + badcorpse_is_fatal = 0; + } + + if (opt->suspend) + task_suspend(task); + + if (trycorpse) { + /* + * Create a corpse from the image before dumping it + */ + ret = task_generate_corpse(task, &corpse); + switch (ret) { + case KERN_SUCCESS: + if (OPTIONS_DEBUG(opt, 1)) + printf("Corpse generated on port %x, task %x\n", + corpse, task); + ecode = coredump(corpse, fd, pbi); + mach_port_deallocate(mach_task_self(), corpse); + break; + default: + if (badcorpse_is_fatal || opt->verbose) { + warnx("failed to snapshot pid %d: %s\n", + pid, mach_error_string(ret)); + if (badcorpse_is_fatal) { + ecode = KERN_RESOURCE_SHORTAGE == ret ? EX_TEMPFAIL : EX_OSERR; + goto out; + } + } + ecode = coredump(task, fd, pbi); + break; + } + } else { + /* + * Examine the task directly + */ + ecode = coredump(task, fd, pbi); + } + + out: + if (opt->suspend) + task_resume(task); + } else { + /* + * Handed a corpse by our parent. + */ + ecode = coredump(corpse, fd, pbi); + mach_port_deallocate(mach_task_self(), corpse); + } + + ecode = closeout(fd, ecode, corefname, coretname, &cst); + if (ecode) + errx(ecode, "failed to dump core for pid %d", pid); + return 0; +} + +#if defined(CONFIG_GCORE_FREF) || defined(CONFIG_GCORE_MAP) || defined(GCONFIG_GCORE_CONV) + +static int +getcorefd(const char *infile) +{ + const int fd = open(infile, O_RDONLY | O_CLOEXEC); + if (-1 == fd) + errc(EX_DATAERR, errno, "cannot open %s", infile); + + struct mach_header mh; + if (-1 == pread(fd, &mh, sizeof (mh), 0)) + errc(EX_OSERR, errno, "cannot read mach header from %s", infile); + + static const char cant_match_data_model[] = "cannot match the data model of %s"; + + if (-1 == reexec_to_match_lp64ness(MH_MAGIC_64 == mh.magic)) + errc(1, errno, cant_match_data_model, infile); + + if (NATIVE_MH_MAGIC != mh.magic) + errx(EX_OSERR, cant_match_data_model, infile); + if (MH_CORE != mh.filetype) + errx(EX_DATAERR, "%s is not a mach core file", infile); + return fd; +} + +#endif + +#ifdef CONFIG_GCORE_FREF + +static int +gcore_fref_main(int argc, char *argv[]) +{ + err_set_exit_b(^(int eval) { + if (EX_USAGE == eval) { + fprintf(stderr, "usage:\t%s %s corefile\n", pgm, argv[1]); + } + }); + if (2 == argc) + errx(EX_USAGE, "no input corefile"); + if (argc > 3) + errx(EX_USAGE, "too many arguments"); + opt = &options; + return gcore_fref(getcorefd(argv[2])); +} + +#endif /* CONFIG_GCORE_FREF */ + +#ifdef CONFIG_GCORE_MAP + +static int +gcore_map_main(int argc, char *argv[]) +{ + err_set_exit_b(^(int eval) { + if (EX_USAGE == eval) { + fprintf(stderr, "usage:\t%s %s corefile\n", pgm, argv[1]); + } + }); + if (2 == argc) + errx(EX_USAGE, "no input corefile"); + if (argc > 3) + errx(EX_USAGE, "too many arguments"); + opt = &options; + return gcore_map(getcorefd(argv[2])); +} + +#endif + +#ifdef CONFIG_GCORE_CONV + +static int +gcore_conv_main(int argc, char *argv[]) +{ + err_set_exit_b(^(int eval) { + if (EX_USAGE == eval) + fprintf(stderr, + "usage:\t%s %s [-v] [-L searchpath] [-z] [-s] incore outcore\n", pgm, argv[1]); + }); + + char *searchpath = NULL; + bool zf = false; + + int c; + optind = 2; + while ((c = getopt(argc, argv, "dzvL:s")) != -1) { + switch (c) { + /* + * likely documented options + */ + case 'L': + searchpath = strdup(optarg); + break; + case 'z': + zf = true; + break; + case 'v': + options.verbose++; + break; + case 's': + options.dsymforuuid++; + break; + /* + * dev and debugging help + */ +#ifdef CONFIG_DEBUG + case 'd': + options.debug++; + options.verbose++; + options.preserve++; + break; +#endif + default: + errx(EX_USAGE, "unknown flag"); + } + } + if (optind == argc) + errx(EX_USAGE, "no input corefile"); + if (optind == argc - 1) + errx(EX_USAGE, "no output corefile"); + if (optind < argc - 2) + errx(EX_USAGE, "too many arguments"); + + const char *incore = argv[optind]; + char *corefname = strdup(argv[optind+1]); + + opt = &options; + + setpageshift(); + + if (opt->ncthresh < ((vm_offset_t)1 << pageshift_host)) + errx(EX_USAGE, "threshold %lu less than host pagesize", opt->ncthresh); + + const int infd = getcorefd(incore); + struct stat cst; + char *coretname = NULL; + const int fd = openout(corefname, &coretname, &cst); + int ecode = gcore_conv(infd, searchpath, zf, fd); + ecode = closeout(fd, ecode, corefname, coretname, &cst); + if (ecode) + errx(ecode, "failed to convert core file successfully"); + return 0; +} +#endif + +int +main(int argc, char *argv[]) +{ + if (NULL == (pgm = strrchr(*argv, '/'))) + pgm = *argv; + else + pgm++; +#ifdef CONFIG_GCORE_FREF + if (argc > 1 && 0 == strcmp(argv[1], "fref")) { + return gcore_fref_main(argc, argv); + } +#endif +#ifdef CONFIG_GCORE_MAP + if (argc > 1 && 0 == strcmp(argv[1], "map")) { + return gcore_map_main(argc, argv); + } +#endif +#ifdef CONFIG_GCORE_CONV + if (argc > 1 && 0 == strcmp(argv[1], "conv")) { + return gcore_conv_main(argc, argv); + } +#endif + return gcore_main(argc, argv); +} diff --git a/system_cmds/gcore.tproj/options.h b/system_cmds/gcore.tproj/options.h new file mode 100644 index 0000000..71481fa --- /dev/null +++ b/system_cmds/gcore.tproj/options.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include +#include + +#include + +#ifndef _OPTIONS_H +#define _OPTIONS_H + +#if defined(__arm__) || defined(__arm64__) +#define RDAR_23744374 1 /* 'true' while not fixed i.e. enable workarounds */ +#define RDAR_28040018 1 /* 'true' while not fixed i.e. enable workarounds */ +#endif + +//#define CONFIG_SUBMAP 1 /* include submaps (debugging output) */ +#define CONFIG_GCORE_MAP 1 /* support 'gcore map' */ +#define CONFIG_GCORE_CONV 1 /* support 'gcore conv' - new -> old core files */ +#define CONFIG_GCORE_FREF 1 /* support 'gcore fref' - referenced file list */ +#define CONFIG_DEBUG 1 /* support '-d' option */ + +#ifdef NDEBUG +#define poison(a, p, s) /* do nothing */ +#else +#define poison(a, p, s) memset(a, p, s) /* scribble on dying memory */ +#endif + +struct options { + int corpsify; // make a corpse to dump from + int suspend; // suspend while dumping + int preserve; // preserve the core file, even if there are errors + int verbose; // be chatty +#ifdef CONFIG_DEBUG + int debug; // internal debugging: options accumulate. noisy. +#endif + int extended; // avoid writing out ro mapped files, compress regions + off_t sizebound; // maximum size of the dump + size_t chunksize; // max size of a compressed subregion + compression_algorithm calgorithm; // algorithm in use + size_t ncthresh; // F_NOCACHE enabled *above* this value + int allfilerefs; // if set, every mapped file on the root fs is a fileref + int dsymforuuid; // Try dsysForUUID to retrieve symbol-rich executable +}; + +extern const struct options *opt; + +/* + * == 0 - not verbose + * >= 1 - verbose plus chatty + * >= 2 - tabular summaries + * >= 3 - all + */ + +#ifdef CONFIG_DEBUG +#define OPTIONS_DEBUG(opt, lvl) ((opt)->debug && (opt)->debug >= (lvl)) +#else +#define OPTIONS_DEBUG(opt, lvl) 0 +#endif + +#endif /* _OPTIONS_H */ diff --git a/system_cmds/gcore.tproj/region.h b/system_cmds/gcore.tproj/region.h new file mode 100644 index 0000000..77853b2 --- /dev/null +++ b/system_cmds/gcore.tproj/region.h @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include +#include +#include +#include + +#ifndef _REGION_H +#define _REGION_H + +/* + * A range of virtual memory + */ +struct vm_range { + mach_vm_offset_t addr; + mach_vm_offset_t size; +}; + +#define _V_ADDR(g) ((g)->addr) +#define _V_SIZE(g) ((g)->size) +#define V_SETADDR(g, a) ((g)->addr = (a)) +#define V_SETSIZE(g, z) ((g)->size = (z)) +#define V_ENDADDR(g) (_V_ADDR(g) + _V_SIZE(g)) + +static __inline const mach_vm_offset_t V_ADDR(const struct vm_range *vr) { + return _V_ADDR(vr); +} +static __inline const mach_vm_offset_t V_SIZE(const struct vm_range *vr) { + return _V_SIZE(vr); +} +static __inline const size_t V_SIZEOF(const struct vm_range *vr) { + assert((typeof (vr->size))(size_t)_V_SIZE(vr) == _V_SIZE(vr)); + return (size_t)_V_SIZE(vr); +} + +/* + * A range of offsets into a file + */ +struct file_range { + off_t off; + off_t size; +}; + +#define F_OFF(f) ((f)->off) +#define F_SIZE(f) ((f)->size) + +struct regionop; +struct subregion; + +struct region { + STAILQ_ENTRY(region) r_linkage; + + struct vm_range r_range; + +#define R_RANGE(r) (&(r)->r_range) +#define _R_ADDR(r) _V_ADDR(R_RANGE(r)) +#define _R_SIZE(r) _V_SIZE(R_RANGE(r)) +#define R_SIZEOF(r) V_SIZEOF(R_RANGE(r)) +#define R_SETADDR(r, a) V_SETADDR(R_RANGE(r), (a)) +#define R_SETSIZE(r, z) V_SETSIZE(R_RANGE(r), (z)) +#define R_ENDADDR(r) (_R_ADDR(r) + _R_SIZE(r)) + + vm_region_submap_info_data_64_t r_info; + vm_page_info_basic_data_t r_pageinfo; + + int r_purgable; + +#ifdef CONFIG_SUBMAP + int r_depth; +#endif + boolean_t r_insharedregion, r_inzfodregion, r_incommregion; + + /* + * This field may be non-NULL if the region is a read-only part + * of a mapped file (i.e. the shared cache) and thus + * doesn't need to be copied. + */ + struct { + const struct libent *fr_libent; + const char *fr_pathname; + off_t fr_offset; + } *r_fileref; + + /* + * These (optional) fields are filled in after we parse the information + * about the dylibs we've mapped, as provided by dyld. + */ + struct subregion **r_subregions; + unsigned r_nsubregions; + + const struct regionop *r_op; +}; + +static __inline const mach_vm_offset_t R_ADDR(const struct region *r) { + return _R_ADDR(r); +} +static __inline const mach_vm_offset_t R_SIZE(const struct region *r) { + return _R_SIZE(r); +} + +/* + * Describes the disposition of the region after a walker returns + */ +typedef enum { + WALK_CONTINUE, // press on .. + WALK_DELETE_REGION, // discard this region, then continue + WALK_TERMINATE, // early termination, no error + WALK_ERROR, // early termination, error +} walk_return_t; + +struct size_core; +struct write_segment_data; + +typedef walk_return_t walk_region_cbfn_t(struct region *, void *); + +struct regionop { + void (*rop_print)(const struct region *); + walk_return_t (*rop_write)(const struct region *, struct write_segment_data *); + void (*rop_delete)(struct region *); +}; + +#define ROP_PRINT(r) (((r)->r_op->rop_print)(r)) +#define ROP_WRITE(r, w) (((r)->r_op->rop_write)(r, w)) +#define ROP_DELETE(r) (((r)->r_op->rop_delete)(r)) + +extern const struct regionop vanilla_ops, sparse_ops, zfod_ops; +extern const struct regionop fileref_ops; + +struct regionhead; + +#endif /* _REGION_H */ diff --git a/system_cmds/gcore.tproj/sparse.c b/system_cmds/gcore.tproj/sparse.c new file mode 100644 index 0000000..c62b9f3 --- /dev/null +++ b/system_cmds/gcore.tproj/sparse.c @@ -0,0 +1,497 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "vm.h" +#include "region.h" +#include "utils.h" +#include "dyld.h" +#include "threads.h" +#include "sparse.h" +#include "vanilla.h" +#include "corefile.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static struct subregion * +new_subregion( + const mach_vm_offset_t vmaddr, + const mach_vm_offset_t vmsize, + const native_segment_command_t *sc, + const struct libent *le) +{ + struct subregion *s = malloc(sizeof (*s)); + + assert(vmaddr != 0 && vmsize != 0); + assert(vmaddr < vmaddr + vmsize); + s->s_segcmd = *sc; + + S_SETADDR(s, vmaddr); + S_SETSIZE(s, vmsize); + + s->s_libent = le; + s->s_isuuidref = false; + return s; +} + +static void +del_subregion(struct subregion *s) +{ + poison(s, 0xfacefac1, sizeof (*s)); + free(s); +} + +static walk_return_t +clean_subregions(struct region *r) +{ + if (r->r_nsubregions) { + assert(r->r_subregions); + for (unsigned i = 0; i < r->r_nsubregions; i++) + del_subregion(r->r_subregions[i]); + poison(r->r_subregions, 0xfac1fac1, sizeof (r->r_subregions[0]) * r->r_nsubregions); + free(r->r_subregions); + r->r_nsubregions = 0; + r->r_subregions = NULL; + } else { + assert(NULL == r->r_subregions); + } + return WALK_CONTINUE; +} + +void +del_sparse_region(struct region *r) +{ + clean_subregions(r); + poison(r, 0xcafecaff, sizeof (*r)); + free(r); +} + +#define NULLsc ((native_segment_command_t *)0) + +static bool +issamesubregiontype(const struct subregion *s0, const struct subregion *s1) { + return 0 == strncmp(S_MACHO_TYPE(s0), S_MACHO_TYPE(s1), sizeof (NULLsc->segname)); +} + +bool +issubregiontype(const struct subregion *s, const char *sctype) { + return 0 == strncmp(S_MACHO_TYPE(s), sctype, sizeof (NULLsc->segname)); +} + +static void +elide_subregion(struct region *r, unsigned ind) +{ + del_subregion(r->r_subregions[ind]); + for (unsigned j = ind; j < r->r_nsubregions - 1; j++) + r->r_subregions[j] = r->r_subregions[j+1]; + assert(r->r_nsubregions != 0); + r->r_subregions[--r->r_nsubregions] = NULL; +} + +struct subregionlist { + STAILQ_ENTRY(subregionlist) srl_linkage; + struct subregion *srl_s; +}; +typedef STAILQ_HEAD(, subregionlist) subregionlisthead_t; + +static walk_return_t +add_subregions_for_libent( + subregionlisthead_t *srlh, + const struct region *r, + const native_mach_header_t *mh, + const mach_vm_offset_t __unused mh_taddr, // address in target + const struct libent *le) +{ + const struct load_command *lc = (const void *)(mh + 1); + mach_vm_offset_t objoff = le->le_objoff; + for (unsigned n = 0; n < mh->ncmds; n++) { + + const native_segment_command_t *sc; + + switch (lc->cmd) { + case NATIVE_LC_SEGMENT: + sc = (const void *)lc; + + if (0 == sc->vmaddr && strcmp(sc->segname, SEG_PAGEZERO) == 0) + break; + mach_vm_offset_t lo = sc->vmaddr + objoff; + mach_vm_offset_t hi = lo + sc->vmsize; + + /* Eliminate non-overlapping sections first */ + + if (R_ENDADDR(r) - 1 < lo) + break; + if (hi - 1 < R_ADDR(r)) + break; + + /* + * Some part of this segment is in the region. + * Trim the edges in the case where we span regions. + */ + if (lo < R_ADDR(r)) + lo = R_ADDR(r); + if (hi > R_ENDADDR(r)) + hi = R_ENDADDR(r); + + struct subregionlist *srl = calloc(1, sizeof (*srl)); + struct subregion *s = new_subregion(lo, hi - lo, sc, le); + assert(sc->fileoff >= 0); + srl->srl_s = s; + STAILQ_INSERT_HEAD(srlh, srl, srl_linkage); + + if (OPTIONS_DEBUG(opt, 2)) { + hsize_str_t hstr; + printr(r, "subregion %llx-%llx %7s %12s\t%s [%s off %lu for %lu nsects %u flags %x]\n", + S_ADDR(s), S_ENDADDR(s), + str_hsize(hstr, S_SIZE(s)), + sc->segname, + S_FILENAME(s), + str_prot(sc->initprot), + (unsigned long)sc->fileoff, + (unsigned long)sc->filesize, + sc->nsects, sc->flags); + } + break; + default: + break; + } + if (lc->cmdsize) + lc = (const void *)((caddr_t)lc + lc->cmdsize); + else + break; + } + return WALK_CONTINUE; +} + +/* + * Because we aggregate information from multiple sources, there may + * be duplicate subregions. Eliminate them here. + * + * Note that the each library in the shared cache points + * separately at a single, unified (large!) __LINKEDIT section; these + * get removed here too. + * + * Assumes the subregion array is sorted by address! + */ +static void +eliminate_duplicate_subregions(struct region *r) +{ + unsigned i = 1; + while (i < r->r_nsubregions) { + struct subregion *s0 = r->r_subregions[i-1]; + struct subregion *s1 = r->r_subregions[i]; + + if (S_ADDR(s0) != S_ADDR(s1) || S_SIZE(s0) != S_SIZE(s1)) { + i++; + continue; + } + if (memcmp(&s0->s_segcmd, &s1->s_segcmd, sizeof (s0->s_segcmd)) != 0) { + i++; + continue; + } + if (OPTIONS_DEBUG(opt, 3)) + printr(r, "eliding duplicate %s subregion (%llx-%llx) file %s\n", + S_MACHO_TYPE(s1), S_ADDR(s1), S_ENDADDR(s1), S_FILENAME(s1)); + /* If the duplicate subregions aren't mapping the same file (?), forget the name */ + if (s0->s_libent != s1->s_libent) + s0->s_libent = s1->s_libent = NULL; + elide_subregion(r, i); + } +} + +/* + * See if any of the dyld information we have can better describe this + * region of the target address space. + */ +walk_return_t +decorate_memory_region(struct region *r, void *arg) +{ + if (r->r_inzfodregion || r->r_incommregion) + return WALK_CONTINUE; + + const dyld_process_info dpi = arg; + + __block walk_return_t retval = WALK_CONTINUE; + __block subregionlisthead_t srlhead = STAILQ_HEAD_INITIALIZER(srlhead); + + _dyld_process_info_for_each_image(dpi, ^(uint64_t __unused mhaddr, const uuid_t uuid, __unused const char *path) { + if (WALK_CONTINUE == retval) { + const struct libent *le = libent_lookup_byuuid(uuid); + assert(le->le_mhaddr == mhaddr); + bool shouldskip = false; + if (V_SIZE(&le->le_vr)) + shouldskip = (R_ENDADDR(r) < V_ADDR(&le->le_vr) || + R_ADDR(r) > V_ENDADDR(&le->le_vr)); + if (!shouldskip) + retval = add_subregions_for_libent(&srlhead, r, le->le_mh, le->le_mhaddr, le); + } + }); + if (WALK_CONTINUE != retval) + goto done; + + /* + * Take the unsorted list of subregions, if any, + * and hang a sorted array of ranges on the region structure. + */ + if (!STAILQ_EMPTY(&srlhead)) { + struct subregionlist *srl; + STAILQ_FOREACH(srl, &srlhead, srl_linkage) { + r->r_nsubregions++; + } + assert(r->r_nsubregions); + + r->r_subregions = calloc(r->r_nsubregions, sizeof (void *)); + unsigned i = 0; + STAILQ_FOREACH(srl, &srlhead, srl_linkage) { + r->r_subregions[i++] = srl->srl_s; + } + qsort_b(r->r_subregions, r->r_nsubregions, sizeof (void *), + ^(const void *a, const void *b) { + const struct subregion *lhs = *(struct subregion **)a; + const struct subregion *rhs = *(struct subregion **)b; + if (S_ADDR(lhs) > S_ADDR(rhs)) + return 1; + if (S_ADDR(lhs) < S_ADDR(rhs)) + return -1; + return 0; + }); + + eliminate_duplicate_subregions(r); + + if (r->r_info.external_pager) { + /* + * Only very specific segment types get to be filerefs + */ + for (i = 0; i < r->r_nsubregions; i++) { + struct subregion *s = r->r_subregions[i]; + /* + * Anything marked writable is trivially disqualified; we're + * going to copy it anyway. + */ + if (s->s_segcmd.initprot & VM_PROT_WRITE) + continue; + + /* __TEXT and __LINKEDIT are our real targets */ + if (!issubregiontype(s, SEG_TEXT) && !issubregiontype(s, SEG_LINKEDIT) && !issubregiontype(s, "__UNICODE")) { + if (OPTIONS_DEBUG(opt, 3)) { + hsize_str_t hstr; + printvr(S_RANGE(s), "skipping read-only %s segment %s\n", S_MACHO_TYPE(s), str_hsize(hstr, S_SIZE(s))); + } + continue; + } + if (r->r_insharedregion) { + /* + * Part of the shared region: things get more complicated. + */ + if (r->r_fileref) { + /* + * There's a file reference here for the whole region. + * For __TEXT subregions, we could, in principle (though + * see below) generate references to the individual + * dylibs that dyld reports in the region. If the + * debugger could then use the __LINKEDIT info in the + * file, then we'd be done. But as long as the dump + * includes __LINKEDIT sections, we're going to + * end up generating a file reference to the combined + * __LINKEDIT section in the shared cache anyway, so + * we might as well do that for the __TEXT regions as + * well. + */ + s->s_libent = r->r_fileref->fr_libent; + s->s_isuuidref = true; + } else { + /* + * If we get here, it's likely that the shared cache + * name can't be found e.g. update_dyld_shared_cache(1). + * For __TEXT subregions, we could generate refs to + * the individual dylibs, but note that the mach header + * and segment commands in memory are still pointing + * into the shared cache so any act of reconstruction + * is fiendishly complex. So copy it. + */ + assert(!s->s_isuuidref); + } + } else { + /* Just a regular dylib? */ + if (s->s_libent) + s->s_isuuidref = true; + } + } + } + } + assert(WALK_CONTINUE == retval); + +done: + if (!STAILQ_EMPTY(&srlhead)) { + struct subregionlist *srl, *trl; + STAILQ_FOREACH_SAFE(srl, &srlhead, srl_linkage, trl) { + free(srl); + } + } + return retval; +} + +/* + * Strip region of all decoration + * + * Invoked (on every region!) after an error during the initial + * 'decoration' phase to discard potentially incomplete information. + */ +walk_return_t +undecorate_memory_region(struct region *r, __unused void *arg) +{ + assert(&sparse_ops != r->r_op); + return r->r_nsubregions ? clean_subregions(r) : WALK_CONTINUE; +} + +/* + * This optimization occurs -after- the vanilla_region_optimizations(), + * and -after- we've tagged zfod and first-pass fileref's. + */ +walk_return_t +sparse_region_optimization(struct region *r, __unused void *arg) +{ + assert(&sparse_ops != r->r_op); + + if (r->r_inzfodregion) { + /* + * Pure zfod region: almost certainly a more compact + * representation - keep it that way. + */ + if (OPTIONS_DEBUG(opt, 3)) + printr(r, "retaining zfod region\n"); + assert(&zfod_ops == r->r_op); + return clean_subregions(r); + } + + if (r->r_insharedregion && 0 == r->r_nsubregions) { + /* + * A segment in the shared region needs to be + * identified with an LC_SEGMENT that dyld claims, + * otherwise (we assert) it's not useful to the dump. + */ + if (OPTIONS_DEBUG(opt, 2)) { + hsize_str_t hstr; + printr(r, "not referenced in dyld info => " + "eliding %s range in shared region\n", + str_hsize(hstr, R_SIZE(r))); + } + if (0 == r->r_info.pages_dirtied && 0 == r->r_info.pages_swapped_out) + return WALK_DELETE_REGION; + if (OPTIONS_DEBUG(opt, 2)) { + hsize_str_t hstr; + printr(r, "dirty pages, but not referenced in dyld info => " + "NOT eliding %s range in shared region\n", + str_hsize(hstr, R_SIZE(r))); + } + } + + if (r->r_fileref) { + /* + * Already have a fileref for the whole region: already + * a more compact representation - keep it that way. + */ + if (OPTIONS_DEBUG(opt, 3)) + printr(r, "retaining fileref region\n"); + assert(&fileref_ops == r->r_op); + return clean_subregions(r); + } + + if (r->r_nsubregions > 1) { + /* + * Merge adjacent or identical subregions that have no file reference + * (Reducing the number of subregions reduces header overhead and + * improves compressability) + */ + unsigned i = 1; + while (i < r->r_nsubregions) { + struct subregion *s0 = r->r_subregions[i-1]; + struct subregion *s1 = r->r_subregions[i]; + + if (s0->s_isuuidref) { + i++; + continue; /* => destined to be a fileref */ + } + if (!issamesubregiontype(s0, s1)) { + i++; + continue; /* merge-able subregions must have same "type" */ + } + + if (S_ENDADDR(s0) == S_ADDR(s1)) { + /* directly adjacent subregions */ + if (OPTIONS_DEBUG(opt, 2)) + printr(r, "merging subregions (%llx-%llx + %llx-%llx) -- adjacent\n", + S_ADDR(s0), S_ENDADDR(s0), S_ADDR(s1), S_ENDADDR(s1)); + S_SETSIZE(s0, S_ENDADDR(s1) - S_ADDR(s0)); + elide_subregion(r, i); + continue; + } + + const mach_vm_size_t pfn[2] = { + S_ADDR(s0) >> pageshift_host, + S_ADDR(s1) >> pageshift_host + }; + const mach_vm_size_t endpfn[2] = { + (S_ENDADDR(s0) - 1) >> pageshift_host, + (S_ENDADDR(s1) - 1) >> pageshift_host + }; + + if (pfn[0] == pfn[1] && pfn[0] == endpfn[0] && pfn[0] == endpfn[1]) { + /* two small subregions share a host page */ + if (OPTIONS_DEBUG(opt, 2)) + printr(r, "merging subregions (%llx-%llx + %llx-%llx) -- same page\n", + S_ADDR(s0), S_ENDADDR(s0), S_ADDR(s1), S_ENDADDR(s1)); + S_SETSIZE(s0, S_ENDADDR(s1) - S_ADDR(s0)); + elide_subregion(r, i); + continue; + } + + if (pfn[1] == 1 + endpfn[0]) { + /* subregions are pagewise-adjacent: bigger chunks to compress */ + if (OPTIONS_DEBUG(opt, 2)) + printr(r, "merging subregions (%llx-%llx + %llx-%llx) -- adjacent pages\n", + S_ADDR(s0), S_ENDADDR(s0), S_ADDR(s1), S_ENDADDR(s1)); + S_SETSIZE(s0, S_ENDADDR(s1) - S_ADDR(s0)); + elide_subregion(r, i); + continue; + } + + i++; /* this isn't the subregion we're looking for */ + } + } + + if (1 == r->r_nsubregions) { + struct subregion *s = r->r_subregions[0]; + if (!s->s_isuuidref && + R_ADDR(r) == S_ADDR(s) && R_ENDADDR(r) == S_ENDADDR(s)) { + if (OPTIONS_DEBUG(opt, 3)) + printr(r, "subregion (%llx-%llx) reverts to region\n", + S_ADDR(s), S_ENDADDR(s)); + return clean_subregions(r); + } + } + + if (r->r_nsubregions) + r->r_op = &sparse_ops; + + return WALK_CONTINUE; +} diff --git a/system_cmds/gcore.tproj/sparse.h b/system_cmds/gcore.tproj/sparse.h new file mode 100644 index 0000000..cf920ff --- /dev/null +++ b/system_cmds/gcore.tproj/sparse.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "region.h" +#include "dyld.h" + +#ifndef _SPARSE_H +#define _SPARSE_H + +struct subregion { + struct vm_range s_range; + native_segment_command_t s_segcmd; + const struct libent *s_libent; + bool s_isuuidref; +}; + +#define S_RANGE(s) (&(s)->s_range) + +static __inline void S_SETADDR(struct subregion *s, mach_vm_offset_t a) { + V_SETADDR(S_RANGE(s), a); +} + +static __inline void S_SETSIZE(struct subregion *s, mach_vm_offset_t sz) { + V_SETSIZE(S_RANGE(s), sz); +} + +static __inline const mach_vm_offset_t S_ADDR(const struct subregion *s) { + return V_ADDR(S_RANGE(s)); +} + +static __inline const mach_vm_offset_t S_SIZE(const struct subregion *s) { + return V_SIZE(S_RANGE(s)); +} + +static __inline const mach_vm_offset_t S_ENDADDR(const struct subregion *s) { + return S_ADDR(s) + S_SIZE(s); +} + +static __inline const char *S_MACHO_TYPE(const struct subregion *s) { + return s->s_segcmd.segname; +} + +static __inline off_t S_MACHO_FILEOFF(const struct subregion *s) { + return s->s_segcmd.fileoff; +} + +static __inline off_t S_MACHO_FILESIZE(const struct subregion *s) { + return s->s_segcmd.filesize; +} + +static __inline const struct libent *S_LIBENT(const struct subregion *s) { + return s->s_libent; +} + +static __inline const char *S_PATHNAME(const struct subregion *s) { + const struct libent *le = S_LIBENT(s); + return le ? le->le_pathname : "(unknown)"; +} + +static __inline const char *S_FILENAME(const struct subregion *s) { + const struct libent *le = S_LIBENT(s); + return le ? le->le_filename : S_PATHNAME(s); +} + +extern bool issubregiontype(const struct subregion *, const char *); + +extern walk_region_cbfn_t decorate_memory_region; +extern walk_region_cbfn_t undecorate_memory_region; +extern walk_region_cbfn_t sparse_region_optimization; + +#endif /* _SPARSE_H */ diff --git a/system_cmds/gcore.tproj/threads.c b/system_cmds/gcore.tproj/threads.c new file mode 100644 index 0000000..b1b3d6f --- /dev/null +++ b/system_cmds/gcore.tproj/threads.c @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2015 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "utils.h" +#include "threads.h" +#include "corefile.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + int flavor; + mach_msg_type_number_t count; +} threadflavor_t; + +static threadflavor_t thread_flavor[] = { +#if defined(__i386__) || defined(__x86_64__) + { x86_THREAD_STATE, x86_THREAD_STATE_COUNT }, + { x86_FLOAT_STATE, x86_FLOAT_STATE_COUNT }, + { x86_EXCEPTION_STATE, x86_EXCEPTION_STATE_COUNT }, +#elif defined(__arm__) + { ARM_THREAD_STATE, ARM_THREAD_STATE_COUNT }, + { ARM_VFP_STATE, ARM_VFP_STATE_COUNT }, + { ARM_EXCEPTION_STATE, ARM_EXCEPTION_STATE_COUNT }, +#elif defined(__arm64__) + { ARM_THREAD_STATE64, ARM_THREAD_STATE64_COUNT }, + /* ARM64_TODO: NEON? */ + { ARM_EXCEPTION_STATE64, ARM_EXCEPTION_STATE64_COUNT }, +#else +#error architecture not supported +#endif +}; + +static const int nthread_flavors = sizeof (thread_flavor) / sizeof (thread_flavor[0]); + +size_t +sizeof_LC_THREAD() +{ + size_t cmdsize = sizeof (struct thread_command); + for (int i = 0; i < nthread_flavors; i++) { + cmdsize += sizeof (thread_flavor[i]) + + thread_flavor[i].count * sizeof (int); + } + return cmdsize; +} + +void +dump_thread_state(native_mach_header_t *mh, struct thread_command *tc, mach_port_t thread) +{ + tc->cmd = LC_THREAD; + tc->cmdsize = (uint32_t) sizeof_LC_THREAD(); + + uint32_t *wbuf = (void *)(tc + 1); + + for (int f = 0; f < nthread_flavors; f++) { + + memcpy(wbuf, &thread_flavor[f], sizeof (thread_flavor[f])); + wbuf += sizeof (thread_flavor[f]) / sizeof (*wbuf); + + const kern_return_t kr = thread_get_state(thread, thread_flavor[f].flavor, (thread_state_t)wbuf, &thread_flavor[f].count); + if (KERN_SUCCESS != kr) { + err_mach(kr, NULL, "getting flavor %d of thread", + thread_flavor[f].flavor); + bzero(wbuf, thread_flavor[f].count * sizeof (int)); + } + + wbuf += thread_flavor[f].count; + } + assert((ptrdiff_t)tc->cmdsize == ((caddr_t)wbuf - (caddr_t)tc)); + + mach_header_inc_ncmds(mh, 1); + mach_header_inc_sizeofcmds(mh, tc->cmdsize); +} diff --git a/system_cmds/gcore.tproj/threads.h b/system_cmds/gcore.tproj/threads.h new file mode 100644 index 0000000..c5605ce --- /dev/null +++ b/system_cmds/gcore.tproj/threads.h @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2015 Apple Inc. All rights reserved. + */ + +#include "corefile.h" +#include + +#ifndef _THREADS_H +#define _THREADS_H + +extern size_t sizeof_LC_THREAD(void); +extern void dump_thread_state(native_mach_header_t *, struct thread_command *, mach_port_t); + +#endif /* _THREADS_H */ diff --git a/system_cmds/gcore.tproj/utils.c b/system_cmds/gcore.tproj/utils.c new file mode 100644 index 0000000..f0edcf8 --- /dev/null +++ b/system_cmds/gcore.tproj/utils.c @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "utils.h" +#include "region.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +void +err_mach(kern_return_t kr, const struct region *r, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + if (0 != kr) + printf("%s: ", pgm); + if (NULL != r) + printf("%016llx-%016llx ", R_ADDR(r), R_ENDADDR(r)); + vprintf(fmt, ap); + va_end(ap); + + if (0 != kr) { + printf(": failed: %s (0x%x)", mach_error_string(kr), kr); + switch (err_get_system(kr)) { + case err_get_system(err_mach_ipc): + /* 0x10000000 == (4 << 26) */ + printf(" => fatal\n"); + exit(127); + default: + putchar('\n'); + break; + } + } else + putchar('\n'); +} + +static void +vprintvr(const struct vm_range *vr, const char *restrict fmt, va_list ap) +{ + if (NULL != vr) + printf("%016llx-%016llx ", V_ADDR(vr), V_ENDADDR(vr)); + vprintf(fmt, ap); +} + +void +printvr(const struct vm_range *vr, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vprintvr(vr, fmt, ap); + va_end(ap); +} + +void +printr(const struct region *r, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vprintvr(R_RANGE(r), fmt, ap); + va_end(ap); +} + +/* + * Print power-of-1024 sizes in human-readable form + */ +const char * +str_hsize(hsize_str_t hstr, uint64_t size) +{ + humanize_number(hstr, sizeof (hsize_str_t) - 1, size, "", + HN_AUTOSCALE, HN_B | HN_NOSPACE | HN_DECIMAL | HN_IEC_PREFIXES); + return hstr; +} + +/* + * Print VM protections in human-readable form + */ +const char * +str_prot(const vm_prot_t prot) +{ + static const char *pstr[] = { + [0] = "---", + [VM_PROT_READ] = "r--", + [VM_PROT_WRITE] = "-w-", + [VM_PROT_READ|VM_PROT_WRITE] = "rw-", + [VM_PROT_EXECUTE] = "--x", + [VM_PROT_READ|VM_PROT_EXECUTE] = "r-x", + [VM_PROT_WRITE|VM_PROT_EXECUTE] = "-wx", + [VM_PROT_READ|VM_PROT_WRITE|VM_PROT_EXECUTE] = "rwx" + }; + return pstr[prot & 7]; +} + +// c.f. VMUVMRegion.m + +const char * +str_shared(int sm) +{ + static const char *sstr[] = { + [0] = " ", + [SM_COW] = "sm=cow", + [SM_PRIVATE] = "sm=prv", + [SM_EMPTY] = "sm=nul", + [SM_SHARED] = "sm=ali", + [SM_TRUESHARED] = "sm=shm", + [SM_PRIVATE_ALIASED] = "sm=zer", + [SM_SHARED_ALIASED] = "sm=s/a", + [SM_LARGE_PAGE] = "sm=lpg", + }; + if ((unsigned)sm < sizeof (sstr) / sizeof (sstr[0])) + return sstr[sm]; + return "sm=???"; +} + +const char * +str_purgable(int pu, int sm) +{ + if (SM_EMPTY == sm) + return " "; + static const char *pstr[] = { + [VM_PURGABLE_NONVOLATILE] = "p=n", + [VM_PURGABLE_VOLATILE] = "p=v", + [VM_PURGABLE_EMPTY] = "p=e", + [VM_PURGABLE_DENY] = " ", + }; + if ((unsigned)pu < sizeof (pstr) / sizeof (pstr[0])) + return pstr[pu]; + return "p=?"; +} + +/* + * c.f. VMURegionTypeDescriptionForTagShareProtAndPager. + */ +const char * +str_tag(tag_str_t tstr, int tag, int share_mode, vm_prot_t curprot, int external_pager) +{ + const char *rtype; + + switch (tag) { + case 0: + if (external_pager) + rtype = "mapped file"; + else if (SM_TRUESHARED == share_mode) + rtype = "shared memory"; + else + rtype = "VM_allocate"; + break; + case VM_MEMORY_MALLOC: + if (VM_PROT_NONE == curprot) + rtype = "MALLOC guard page"; + else if (SM_EMPTY == share_mode) + rtype = "MALLOC"; + else + rtype = "MALLOC metadata"; + break; + case VM_MEMORY_STACK: + if (VM_PROT_NONE == curprot) + rtype = "Stack guard"; + else + rtype = "Stack"; + break; +#if defined(CONFIG_DEBUG) || defined(CONFIG_GCORE_MAP) + case VM_MEMORY_MALLOC_SMALL: + rtype = "MALLOC_SMALL"; + break; + case VM_MEMORY_MALLOC_LARGE: + rtype = "MALLOC_LARGE"; + break; + case VM_MEMORY_MALLOC_HUGE: + rtype = "MALLOC_HUGE"; + break; + case VM_MEMORY_SBRK: + rtype = "SBRK"; + break; + case VM_MEMORY_REALLOC: + rtype = "MALLOC_REALLOC"; + break; + case VM_MEMORY_MALLOC_TINY: + rtype = "MALLOC_TINY"; + break; + case VM_MEMORY_MALLOC_LARGE_REUSABLE: + rtype = "MALLOC_LARGE_REUSABLE"; + break; + case VM_MEMORY_MALLOC_LARGE_REUSED: + rtype = "MALLOC_LARGE"; + break; + case VM_MEMORY_ANALYSIS_TOOL: + rtype = "Performance tool data"; + break; + case VM_MEMORY_MALLOC_NANO: + rtype = "MALLOC_NANO"; + break; + case VM_MEMORY_MACH_MSG: + rtype = "Mach message"; + break; + case VM_MEMORY_IOKIT: + rtype = "IOKit"; + break; + case VM_MEMORY_GUARD: + rtype = "Guard"; + break; + case VM_MEMORY_SHARED_PMAP: + rtype = "shared pmap"; + break; + case VM_MEMORY_DYLIB: + rtype = "dylib"; + break; + case VM_MEMORY_OBJC_DISPATCHERS: + rtype = "ObjC dispatching code"; + break; + case VM_MEMORY_UNSHARED_PMAP: + rtype = "unshared pmap"; + break; + case VM_MEMORY_APPKIT: + rtype = "AppKit"; + break; + case VM_MEMORY_FOUNDATION: + rtype = "Foundation"; + break; + case VM_MEMORY_COREGRAPHICS: + rtype = "CoreGraphics"; + break; + case VM_MEMORY_CORESERVICES: + rtype = "CoreServices"; + break; + case VM_MEMORY_JAVA: + rtype = "Java"; + break; + case VM_MEMORY_COREDATA: + rtype = "CoreData"; + break; + case VM_MEMORY_COREDATA_OBJECTIDS: + rtype = "CoreData Object IDs"; + break; + case VM_MEMORY_ATS: + rtype = "ATS (font support)"; + break; + case VM_MEMORY_LAYERKIT: + rtype = "CoreAnimation"; + break; + case VM_MEMORY_CGIMAGE: + rtype = "CG image"; + break; + case VM_MEMORY_TCMALLOC: + rtype = "WebKit Malloc"; + break; + case VM_MEMORY_COREGRAPHICS_DATA: + rtype = "CG raster data"; + break; + case VM_MEMORY_COREGRAPHICS_SHARED: + rtype = "CG shared images"; + break; + case VM_MEMORY_COREGRAPHICS_FRAMEBUFFERS: + rtype = "CG framebuffers"; + break; + case VM_MEMORY_COREGRAPHICS_BACKINGSTORES: + rtype = "CG backingstores"; + break; + case VM_MEMORY_DYLD: + rtype = "dyld private memory"; + break; + case VM_MEMORY_DYLD_MALLOC: + rtype = "dyld malloc memory"; + break; + case VM_MEMORY_SQLITE: + rtype = "SQlite page cache"; + break; + case VM_MEMORY_JAVASCRIPT_CORE: + rtype = "WebAssembly memory"; + break; + case VM_MEMORY_JAVASCRIPT_JIT_EXECUTABLE_ALLOCATOR: + rtype = "JS JIT generated code"; + break; + case VM_MEMORY_JAVASCRIPT_JIT_REGISTER_FILE: + rtype = "JS VM register file"; + break; + case VM_MEMORY_GLSL: + rtype = "OpenGL GLSL"; + break; + case VM_MEMORY_OPENCL: + rtype = "OpenCL"; + break; + case VM_MEMORY_COREIMAGE: + rtype = "CoreImage"; + break; + case VM_MEMORY_WEBCORE_PURGEABLE_BUFFERS: + rtype = "WebCore purgable data"; + break; + case VM_MEMORY_IMAGEIO: + rtype = "Image IO"; + break; + case VM_MEMORY_COREPROFILE: + rtype = "CoreProfile"; + break; + case VM_MEMORY_ASSETSD: + rtype = "Assets Library"; + break; + case VM_MEMORY_OS_ALLOC_ONCE: + rtype = "OS Alloc Once"; + break; + case VM_MEMORY_LIBDISPATCH: + rtype = "Dispatch continuations"; + break; + case VM_MEMORY_ACCELERATE: + rtype = "Accelerate framework"; + break; + case VM_MEMORY_COREUI: + rtype = "CoreUI image data"; + break; + case VM_MEMORY_COREUIFILE: + rtype = "CoreUI image file"; + break; + case VM_MEMORY_GENEALOGY: + rtype = "Activity Tracing"; + break; + case VM_MEMORY_RAWCAMERA: + rtype = "RawCamera"; + break; + case VM_MEMORY_CORPSEINFO: + rtype = "Process Corpse Info"; + break; + case VM_MEMORY_ASL: + rtype = "Apple System Log"; + break; + case VM_MEMORY_SWIFT_RUNTIME: + rtype = "Swift runtime"; + break; + case VM_MEMORY_SWIFT_METADATA: + rtype = "Swift metadata"; + break; + case VM_MEMORY_DHMM: + rtype = "DHMM"; + break; + case VM_MEMORY_SCENEKIT: + rtype = "SceneKit"; + break; + case VM_MEMORY_SKYWALK: + rtype = "Skywalk Networking"; + break; +#endif + default: + rtype = NULL; + break; + } + if (rtype) + snprintf(tstr, sizeof (tag_str_t), "%s", rtype); + else + snprintf(tstr, sizeof (tag_str_t), "tag #%d", tag); + return tstr; +} + +const char * +str_tagr(tag_str_t tstr, const struct region *r) { + return str_tag(tstr, r->r_info.user_tag, r->r_info.share_mode, r->r_info.protection, r->r_info.external_pager); +} + +/* + * Put two strings together separated by a '+' sign + * If the string gets too long, then add an ellipsis and + * stop concatenating it. + */ +char * +strconcat(const char *s0, const char *s1, size_t maxlen) +{ + const char ellipsis[] = "..."; + const char junction[] = ", "; + const size_t s0len = strlen(s0); + size_t nmlen = s0len + strlen(s1) + strlen(junction) + 1; + if (maxlen > strlen(ellipsis) && nmlen > maxlen) { + if (strcmp(s0 + s0len - strlen(ellipsis), ellipsis) == 0) + return strdup(s0); + s1 = ellipsis; + nmlen = s0len + strlen(s1) + strlen(junction) + 1; + } + char *p = malloc(nmlen); + if (p) { + strlcpy(p, s0, nmlen); + strlcat(p, junction, nmlen); + strlcat(p, s1, nmlen); + } + return p; +} + +unsigned long +simple_namehash(const char *nm) +{ + unsigned long result = 5381; + int c; + while (0 != (c = *nm++)) + result = (result * 33) ^ c; + return result; /* modified djb2 */ +} + +int +bounded_pwrite(int fd, const void *addr, size_t size, off_t off, bool *nocache, ssize_t *nwrittenp) +{ + if (opt->sizebound && off + (off_t)size > opt->sizebound) + return EFBIG; + + bool oldnocache = *nocache; + if (size >= opt->ncthresh && !oldnocache) + *nocache = 0 == fcntl(fd, F_NOCACHE, 1); + else if (size < opt->ncthresh && oldnocache) + *nocache = 0 != fcntl(fd, F_NOCACHE, 0); + if (OPTIONS_DEBUG(opt, 3) && oldnocache ^ *nocache) + printf("F_NOCACHE now %sabled on fd %d\n", *nocache ? "en" : "dis", fd); + + const ssize_t nwritten = pwrite(fd, addr, size, off); + if (-1 == nwritten) + return errno; + if (nwrittenp) + *nwrittenp = nwritten; + return 0; +} diff --git a/system_cmds/gcore.tproj/utils.h b/system_cmds/gcore.tproj/utils.h new file mode 100644 index 0000000..37eda58 --- /dev/null +++ b/system_cmds/gcore.tproj/utils.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef _UTILS_H +#define _UTILS_H + +extern const char *pgm; + +struct vm_range; +struct region; + +extern void err_mach(kern_return_t, const struct region *, const char *, ...) __printflike(3, 4); +extern void printvr(const struct vm_range *, const char *, ...) __printflike(2, 3); +extern void printr(const struct region *, const char *, ...) __printflike(2, 3); + +typedef char hsize_str_t[7]; /* e.g. 1008Mib */ + +extern const char *str_hsize(hsize_str_t hstr, uint64_t); +extern const char *str_prot(vm_prot_t); +extern const char *str_shared(int); +extern const char *str_purgable(int, int); + +typedef char tag_str_t[24]; + +extern const char *str_tag(tag_str_t, int, int, vm_prot_t, int); +extern const char *str_tagr(tag_str_t, const struct region *); + +extern char *strconcat(const char *, const char *, size_t); +extern unsigned long simple_namehash(const char *); +extern int bounded_pwrite(int, const void *, size_t, off_t, bool *, ssize_t *); + +#endif /* _UTILS_H */ diff --git a/system_cmds/gcore.tproj/vanilla.c b/system_cmds/gcore.tproj/vanilla.c new file mode 100644 index 0000000..2253bff --- /dev/null +++ b/system_cmds/gcore.tproj/vanilla.c @@ -0,0 +1,911 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "vm.h" +#include "region.h" +#include "utils.h" +#include "dyld.h" +#include "threads.h" +#include "vanilla.h" +#include "sparse.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* + * (Another optimization to consider is merging adjacent regions with + * the same properties.) + */ + +static walk_return_t +simple_region_optimization(struct region *r, __unused void *arg) +{ + assert(0 != R_SIZE(r)); + + /* + * Elide unreadable regions + */ + if ((r->r_info.max_protection & VM_PROT_READ) != VM_PROT_READ) { + if (OPTIONS_DEBUG(opt, 2)) + printr(r, "eliding unreadable region\n"); + return WALK_DELETE_REGION; + } +#ifdef CONFIG_SUBMAP + /* + * Elide submaps (here for debugging purposes?) + */ + if (r->r_info.is_submap) { + if (OPTIONS_DEBUG(opt)) + printr(r, "eliding submap\n"); + return WALK_DELETE_REGION; + } +#endif + /* + * Elide guard regions + */ + if (r->r_info.protection == VM_PROT_NONE && + (VM_MEMORY_STACK == r->r_info.user_tag || + VM_MEMORY_MALLOC == r->r_info.user_tag)) { + if (OPTIONS_DEBUG(opt, 2)) { + hsize_str_t hstr; + tag_str_t tstr; + printr(r, "elide %s - %s\n", str_hsize(hstr, R_SIZE(r)), str_tagr(tstr, r)); + } + return WALK_DELETE_REGION; + } + + /* + * Regions full of clean zfod data e.g. VM_MEMORY_MALLOC_LARGE can be recorded as zfod + */ + if (r->r_info.share_mode == SM_PRIVATE && + 0 == r->r_info.external_pager && + 0 == r->r_info.pages_dirtied) { + if (OPTIONS_DEBUG(opt, 2)) { + hsize_str_t hstr; + tag_str_t tstr; + printr(r, "convert to zfod %s - %s\n", str_hsize(hstr, R_SIZE(r)), str_tagr(tstr, r)); + } + r->r_inzfodregion = true; + r->r_op = &zfod_ops; + } + + return WALK_CONTINUE; +} + +/* + * (Paranoid validation + debugging assistance.) + */ +void +validate_core_header(const native_mach_header_t *mh, off_t corefilesize) +{ + assert(NATIVE_MH_MAGIC == mh->magic); + assert(MH_CORE == mh->filetype); + + if (OPTIONS_DEBUG(opt, 2)) + printf("%s: core file: mh %p ncmds %u sizeofcmds %u\n", + __func__, mh, mh->ncmds, mh->sizeofcmds); + + unsigned sizeofcmds = 0; + off_t corefilemaxoff = 0; + const struct load_command *lc = (const void *)(mh + 1); + for (unsigned i = 0; i < mh->ncmds; i++) { + + if ((uintptr_t)lc < (uintptr_t)mh || + (uintptr_t)lc > (uintptr_t)mh + mh->sizeofcmds) { + warnx("load command %p outside mach header range [%p, 0x%lx]?", + lc, mh, (uintptr_t)mh + mh->sizeofcmds); + abort(); + } + if (OPTIONS_DEBUG(opt, 2)) + printf("lc %p cmd %3u size %3u ", lc, lc->cmd, lc->cmdsize); + sizeofcmds += lc->cmdsize; + + switch (lc->cmd) { + case NATIVE_LC_SEGMENT: { + const native_segment_command_t *sc = (const void *)lc; + if (OPTIONS_DEBUG(opt, 2)) { + printf("%8s: mem %llx-%llx file %lld-%lld %s/%s nsect %u flags %x\n", + "SEGMENT", + (mach_vm_offset_t)sc->vmaddr, + (mach_vm_offset_t)sc->vmaddr + sc->vmsize, + (off_t)sc->fileoff, + (off_t)sc->fileoff + (off_t)sc->filesize, + str_prot(sc->initprot), str_prot(sc->maxprot), + sc->nsects, sc->flags); + } + if ((off_t)sc->fileoff < mh->sizeofcmds || + (off_t)sc->filesize < 0) { + warnx("bad segment command"); + abort(); + } + const off_t endoff = (off_t)sc->fileoff + (off_t)sc->filesize; + if ((off_t)sc->fileoff > corefilesize || endoff > corefilesize) { + /* + * We may have run out of space to write the data + */ + warnx("segment command points beyond end of file"); + } + corefilemaxoff = MAX(corefilemaxoff, endoff); + break; + } + case proto_LC_COREINFO: { + const struct proto_coreinfo_command *cic = (const void *)lc; + if (OPTIONS_DEBUG(opt, 2)) { + uuid_string_t uustr; + uuid_unparse_lower(cic->uuid, uustr); + printf("%8s: version %d type %d uuid %s addr %llx dyninfo %llx\n", + "COREINFO", cic->version, cic->type, uustr, cic->address, cic->dyninfo); + } + if (cic->version < 1 || + cic->type != proto_CORETYPE_USER) { + warnx("bad coreinfo command"); + abort(); + } + break; + } + case proto_LC_FILEREF: { + const struct proto_fileref_command *frc = (const void *)lc; + const char *nm = frc->filename.offset + (char *)lc; + if (OPTIONS_DEBUG(opt, 2)) { + printf("%8s: mem %llx-%llx file %lld-%lld %s/%s '%s'\n", + "FREF", + frc->vmaddr, frc->vmaddr + frc->vmsize, + (off_t)frc->fileoff, + (off_t)frc->fileoff + (off_t)frc->filesize, + str_prot(frc->prot), str_prot(frc->maxprot), nm); + } + switch (FREF_ID_TYPE(frc->flags)) { + case kFREF_ID_UUID: + case kFREF_ID_MTIMESPEC_LE: + case kFREF_ID_NONE: + break; + default: + warnx("unknown fref id type: flags %x", frc->flags); + abort(); + } + if (nm <= (caddr_t)lc || + nm > (caddr_t)lc + lc->cmdsize || + (off_t)frc->fileoff < 0 || (off_t)frc->filesize < 0) { + warnx("bad fileref command"); + abort(); + } + break; + } + case proto_LC_COREDATA: { + const struct proto_coredata_command *cc = (const void *)lc; + if (OPTIONS_DEBUG(opt, 2)) { + printf("%8s: mem %llx-%llx file %lld-%lld %s/%s flags %x\n", + "COREDATA", + cc->vmaddr, cc->vmaddr + cc->vmsize, + (off_t)cc->fileoff, + (off_t)cc->fileoff + (off_t)cc->filesize, + str_prot(cc->prot), str_prot(cc->maxprot), cc->flags); + } + if ((off_t)cc->fileoff < mh->sizeofcmds || + (off_t)cc->filesize < 0) { + warnx("bad COREDATA command"); + abort(); + } + const off_t endoff = (off_t)cc->fileoff + (off_t)cc->filesize; + if ((off_t)cc->fileoff > corefilesize || endoff > corefilesize) { + /* + * We may have run out of space to write the data + */ + warnx("segment command points beyond end of file"); + } + corefilemaxoff = MAX(corefilemaxoff, endoff); + break; + } + case LC_THREAD: { + const struct thread_command *tc = (const void *)lc; + if (OPTIONS_DEBUG(opt, 2)) + printf("%8s:\n", "THREAD"); + uint32_t *wbuf = (void *)(tc + 1); + do { + const uint32_t flavor = *wbuf++; + const uint32_t count = *wbuf++; + + if (OPTIONS_DEBUG(opt, 2)) { + printf(" flavor %u count %u\n", flavor, count); + if (count) { + bool nl = false; + for (unsigned k = 0; k < count; k++) { + if (0 == (k & 7)) + printf(" [%3u] ", k); + printf("%08x ", *wbuf++); + if (7 == (k & 7)) { + printf("\n"); + nl = true; + } else + nl = false; + } + if (!nl) + printf("\n"); + } + } else + wbuf += count; + + if (!VALID_THREAD_STATE_FLAVOR(flavor)) { + warnx("bad thread state flavor"); + abort(); + } + } while ((caddr_t) wbuf < (caddr_t)tc + tc->cmdsize); + break; + } + default: + warnx("unknown cmd %u in header", lc->cmd); + abort(); + } + if (lc->cmdsize) + lc = (const void *)((caddr_t)lc + lc->cmdsize); + else + break; + } + if (corefilemaxoff < corefilesize) + warnx("unused data after corefile offset %lld", corefilemaxoff); + if (sizeofcmds != mh->sizeofcmds) { + warnx("inconsistent mach header %u vs. %u", sizeofcmds, mh->sizeofcmds); + abort(); + } +} + +/* + * The vanilla Mach-O core file consists of: + * + * - A Mach-O header of type MH_CORE + * + * A set of load commands of the following types: + * + * - LC_SEGMENT{,_64} pointing at memory content in the file, + * each chunk consisting of a contiguous region. Regions may be zfod + * (no file content present). + * + * - proto_LC_COREDATA pointing at memory content in the file, + * each chunk consisting of a contiguous region. Regions may be zfod + * (no file content present) or content may be compressed (experimental) + * + * - proto_LC_COREINFO (experimental), pointing at dyld (10.12 onwards) + * + * - proto_LC_FILEREF (experimental) pointing at memory + * content to be mapped in from another uuid-tagged file at various offsets + * + * - LC_THREAD commands with state for each thread + * + * These load commands are followed by the relevant contents of memory, + * pointed to by the various commands. + */ + +int +coredump_write( + const task_t task, + const int fd, + struct regionhead *rhead, + const uuid_t aout_uuid, + mach_vm_offset_t aout_load_addr, + mach_vm_offset_t dyld_aii_addr) +{ + struct size_segment_data ssda; + bzero(&ssda, sizeof (ssda)); + + if (walk_region_list(rhead, region_size_memory, &ssda) < 0) { + warnx(0, "cannot count segments"); + return EX_OSERR; + } + + unsigned thread_count = 0; + mach_port_t *threads = NULL; + kern_return_t ret = task_threads(task, &threads, &thread_count); + if (KERN_SUCCESS != ret || thread_count < 1) { + err_mach(ret, NULL, "cannot retrieve threads"); + thread_count = 0; + } + + if (OPTIONS_DEBUG(opt, 3)) { + print_memory_region_header(); + walk_region_list(rhead, region_print_memory, NULL); + printf("\nmach header %lu\n", sizeof (native_mach_header_t)); + printf("threadcount %u threadsize %lu\n", thread_count, thread_count * sizeof_LC_THREAD()); + printf("fileref %lu %lu %llu\n", ssda.ssd_fileref.count, ssda.ssd_fileref.headersize, ssda.ssd_fileref.memsize); + printf("zfod %lu %lu %llu\n", ssda.ssd_zfod.count, ssda.ssd_zfod.headersize, ssda.ssd_zfod.memsize); + printf("vanilla %lu %lu %llu\n", ssda.ssd_vanilla.count, ssda.ssd_vanilla.headersize, ssda.ssd_vanilla.memsize); + printf("sparse %lu %lu %llu\n", ssda.ssd_sparse.count, ssda.ssd_sparse.headersize, ssda.ssd_sparse.memsize); + } + + size_t headersize = sizeof (native_mach_header_t) + + thread_count * sizeof_LC_THREAD() + + ssda.ssd_fileref.headersize + + ssda.ssd_zfod.headersize + + ssda.ssd_vanilla.headersize + + ssda.ssd_sparse.headersize; + if (opt->extended) + headersize += sizeof (struct proto_coreinfo_command); + + void *header = calloc(1, headersize); + if (NULL == header) + errx(EX_OSERR, "out of memory for header"); + + native_mach_header_t *mh = make_corefile_mach_header(header); + struct load_command *lc = (void *)(mh + 1); + + if (opt->extended) { + const struct proto_coreinfo_command *cc = + make_coreinfo_command(mh, lc, aout_uuid, aout_load_addr, dyld_aii_addr); + lc = (void *)((caddr_t)cc + cc->cmdsize); + } + + if (opt->verbose) { + const unsigned long fileref_count = ssda.ssd_fileref.count; + const unsigned long segment_count = fileref_count + + ssda.ssd_zfod.count + ssda.ssd_vanilla.count + ssda.ssd_sparse.count; + printf("Writing %lu segments", segment_count); + if (0 != fileref_count) + printf(" (including %lu file reference%s (%lu bytes))", + fileref_count, 1 == fileref_count ? "" : "s", + ssda.ssd_fileref.headersize); + printf("\n"); + } + + mach_vm_offset_t pagesize = ((mach_vm_offset_t)1 << pageshift_host); + mach_vm_offset_t pagemask = pagesize - 1; + + struct write_segment_data wsda = { + .wsd_task = task, + .wsd_mh = mh, + .wsd_lc = lc, + .wsd_fd = fd, + .wsd_nocache = false, + .wsd_foffset = ((mach_vm_offset_t)headersize + pagemask) & ~pagemask, + .wsd_nwritten = 0, + }; + + int ecode = 0; + if (0 != walk_region_list(rhead, region_write_memory, &wsda)) + ecode = EX_IOERR; + + del_region_list(rhead); + + struct thread_command *tc = (void *)wsda.wsd_lc; + + for (unsigned t = 0; t < thread_count; t++) { + dump_thread_state(mh, tc, threads[t]); + mach_port_deallocate(mach_task_self(), threads[t]); + tc = (void *)((caddr_t)tc + tc->cmdsize); + } + + /* + * Even if we've run out of space, try our best to + * write out the header. + */ + if (0 != bounded_pwrite(fd, header, headersize, 0, &wsda.wsd_nocache, NULL)) + ecode = EX_IOERR; + if (0 == ecode && headersize != sizeof (*mh) + mh->sizeofcmds) + ecode = EX_SOFTWARE; + if (0 == ecode) + wsda.wsd_nwritten += headersize; + + validate_core_header(mh, wsda.wsd_foffset); + + if (ecode) + warnx("failed to write core file correctly"); + else if (opt->verbose) { + hsize_str_t hsz; + printf("Wrote %s to corefile ", str_hsize(hsz, wsda.wsd_nwritten)); + printf("(memory image %s", str_hsize(hsz, ssda.ssd_vanilla.memsize)); + if (ssda.ssd_sparse.memsize) + printf("+%s", str_hsize(hsz, ssda.ssd_sparse.memsize)); + if (ssda.ssd_fileref.memsize) + printf(", referenced %s", str_hsize(hsz, ssda.ssd_fileref.memsize)); + if (ssda.ssd_zfod.memsize) + printf(", zfod %s", str_hsize(hsz, ssda.ssd_zfod.memsize)); + printf(")\n"); + } + free(header); + return ecode; +} + +static void +addfileref(struct region *r, const struct libent *le, const char *nm) +{ + r->r_fileref = calloc(1, sizeof (*r->r_fileref)); + if (r->r_fileref) { + if (le) { + assert(NULL == nm); + r->r_fileref->fr_libent = le; + r->r_fileref->fr_pathname = le->le_pathname; + } else { + assert(NULL == le); + r->r_fileref->fr_pathname = strdup(nm); + } + r->r_fileref->fr_offset = r->r_pageinfo.offset; + r->r_op = &fileref_ops; + } +} + +/* + * Once all the info about the shared cache (filerefs) and the information from + * dyld (filerefs and subregions), take one last look for mappings + * of filesystem content to convert to additional filerefs. + * + * By default we are pessimistic: read-only mappings on read-only root. + */ +static walk_return_t +label_mapped_files(struct region *r, void *arg) +{ + const struct proc_bsdinfo *pbi = arg; + + if (r->r_fileref || r->r_insharedregion || r->r_incommregion || r->r_inzfodregion) + return WALK_CONTINUE; + if (r->r_nsubregions) + return WALK_CONTINUE; + if (!r->r_info.external_pager) + return WALK_CONTINUE; + if (!opt->allfilerefs) { + /* must be mapped without write permission */ + if (0 != (r->r_info.protection & VM_PROT_WRITE)) + return WALK_CONTINUE; + } + + char pathbuf[MAXPATHLEN+1]; + pathbuf[0] = '\0'; + int len = proc_regionfilename(pbi->pbi_pid, R_ADDR(r), pathbuf, sizeof (pathbuf)-1); + if (len <= 0 || len > MAXPATHLEN) + return WALK_CONTINUE; + pathbuf[len] = 0; + +#if 0 + /* + * On the desktop, only refer to files beginning with particular + * prefixes to increase the likelihood that we'll be able to + * find the content later on. + * + * XXX Not practical with a writable root, but helpful for testing. + */ + static const char *white[] = { + "/System", + "/Library", + "/usr", + }; + const unsigned nwhite = sizeof (white) / sizeof (white[0]); + bool skip = true; + for (unsigned i = 0; skip && i < nwhite; i++) + skip = 0 != strncmp(white[i], pathbuf, strlen(white[i])); + if (skip) { + if (OPTIONS_DEBUG(opt, 3)) + printf("\t(%s not included)\n", pathbuf); + return WALK_CONTINUE; + } + static const char *black[] = { + "/System/Library/Caches", + "/Library/Caches", + "/usr/local", + }; + const unsigned nblack = sizeof (black) / sizeof (black[0]); + for (unsigned i = 0; !skip && i < nblack; i++) + skip = 0 == strncmp(black[i], pathbuf, strlen(black[i])); + if (skip) { + if (OPTIONS_DEBUG(opt, 3)) + printf("\t(%s excluded)\n", pathbuf); + return WALK_CONTINUE; + } +#endif + + struct statfs stfs; + if (-1 == statfs(pathbuf, &stfs)) { + switch (errno) { + case EACCES: + case EPERM: + case ENOENT: + break; + default: + warnc(errno, "statfs: %s", pathbuf); + break; + } + return WALK_CONTINUE; + } + + do { + if (OPTIONS_DEBUG(opt, 2)) + printr(r, "found mapped file %s\n", pathbuf); + if (!opt->allfilerefs) { + if ((stfs.f_flags & MNT_ROOTFS) != MNT_ROOTFS) + break; // must be on the root filesystem + if ((stfs.f_flags & MNT_RDONLY) != MNT_RDONLY) + break; // must be on a read-only filesystem + } + if (OPTIONS_DEBUG(opt, 2)) + print_memory_region(r); + addfileref(r, NULL, pathbuf); + } while (0); + + return WALK_CONTINUE; +} + +int +coredump(task_t task, int fd, const struct proc_bsdinfo *__unused pbi) +{ + /* this is the shared cache id, if any */ + uuid_t sc_uuid; + uuid_clear(sc_uuid); + + dyld_process_info dpi = NULL; + if (opt->extended) { + dpi = get_task_dyld_info(task); + if (dpi) { + get_sc_uuid(dpi, sc_uuid); + } + } + + /* this group is for LC_COREINFO */ + mach_vm_offset_t dyld_addr = 0; // all_image_infos -or- dyld mach header + mach_vm_offset_t aout_load_addr = 0; + uuid_t aout_uuid; + uuid_clear(aout_uuid); + + /* + * Walk the address space + */ + int ecode = 0; + struct regionhead *rhead = coredump_prepare(task, sc_uuid); + if (NULL == rhead) { + ecode = EX_OSERR; + goto done; + } + + if (OPTIONS_DEBUG(opt, 1)) + printf("Optimizing dump content\n"); + walk_region_list(rhead, simple_region_optimization, NULL); + + if (dpi) { + /* + * Snapshot dyld's info .. + */ + if (!libent_build_nametable(task, dpi)) + warnx("error parsing dyld data => ignored"); + else { + /* + * Find the a.out load address and uuid, and the dyld mach header for the coreinfo + */ + const struct libent *le; + if (NULL != (le = libent_lookup_first_bytype(MH_EXECUTE))) { + aout_load_addr = le->le_mhaddr; + uuid_copy(aout_uuid, le->le_uuid); + } + if (NULL != (le = libent_lookup_first_bytype(MH_DYLINKER))) { + dyld_addr = le->le_mhaddr; + } + + /* + * Use dyld's view of what's being used in the address + * space to shrink the dump. + */ + if (OPTIONS_DEBUG(opt, 1)) + printf("Decorating dump with dyld-derived data\n"); + if (0 == walk_region_list(rhead, decorate_memory_region, (void *)dpi)) { + if (OPTIONS_DEBUG(opt, 1)) + printf("Sparse dump optimization(s)\n"); + walk_region_list(rhead, sparse_region_optimization, NULL); + } else { + walk_region_list(rhead, undecorate_memory_region, NULL); + warnx("error parsing dyld data => ignored"); + } + } + free_task_dyld_info(dpi); + } + + /* + * Hunt for any memory mapped files that we can include by reference + * Depending on whether the bsd part of the task is still present + * we might be able to determine filenames of other regions mapping + * them here - this allows fonts, images, and other read-only content + * to be converted into file references, further reducing the size + * of the dump. + * + * NOTE: Even though the corpse snapshots the VM, the filesystem is + * not correspondingly snapshotted and thus may mutate while the dump + * proceeds - so be pessimistic about inclusion. + */ + if (opt->extended && NULL != pbi) { + if (OPTIONS_DEBUG(opt, 1)) + printf("Mapped file optimization\n"); + walk_region_list(rhead, label_mapped_files, (void *)pbi); + } + + if (OPTIONS_DEBUG(opt, 1)) + printf("Optimization(s) done\n"); + +done: + if (0 == ecode) + ecode = coredump_write(task, fd, rhead, aout_uuid, aout_load_addr, dyld_addr); + return ecode; +} + +struct find_shared_cache_args { + task_t fsc_task; + vm_object_id_t fsc_object_id; + vm32_object_id_t fsc_region_object_id; + uuid_t fsc_uuid; + const struct libent *fsc_le; + int fsc_fd; +}; + +/* + * This is "find the objid of the first shared cache" in the shared region. + */ +static walk_return_t +find_shared_cache(struct region *r, void *arg) +{ + struct find_shared_cache_args *fsc = arg; + + if (!r->r_insharedregion) + return WALK_CONTINUE; /* wrong address range! */ + if (0 != r->r_info.user_tag) + return WALK_CONTINUE; /* must be tag zero */ + if ((VM_PROT_READ | VM_PROT_EXECUTE) != r->r_info.protection || + r->r_info.protection != r->r_info.max_protection) + return WALK_CONTINUE; /* must be r-x / r-x */ + if (r->r_pageinfo.offset != 0) + return WALK_CONTINUE; /* must map beginning of file */ + + if (OPTIONS_DEBUG(opt, 1)) { + hsize_str_t hstr; + printr(r, "examining %s shared cache candidate\n", str_hsize(hstr, R_SIZE(r))); + } + + struct copied_dyld_cache_header *ch; + mach_msg_type_number_t chlen = sizeof (*ch); + kern_return_t ret = mach_vm_read(fsc->fsc_task, R_ADDR(r), sizeof (*ch), (vm_offset_t *)&ch, &chlen); + + if (KERN_SUCCESS != ret) { + err_mach(ret, NULL, "mach_vm_read() candidate shared region header"); + return WALK_CONTINUE; + } + + uuid_t scuuid; + if (get_uuid_from_shared_cache_mapping(ch, chlen, scuuid) && + uuid_compare(scuuid, fsc->fsc_uuid) == 0) { + if (OPTIONS_DEBUG(opt, 1)) { + uuid_string_t uustr; + uuid_unparse_lower(fsc->fsc_uuid, uustr); + printr(r, "found shared cache %s here\n", uustr); + } + if (!r->r_info.external_pager) { + if (OPTIONS_DEBUG(opt, 1)) + printf("Hmm. Found shared cache magic# + uuid, but not externally paged?\n"); +#if 0 + return WALK_CONTINUE; /* should be "paged" from a file */ +#endif + } + // This is the ID associated with the first page of the mapping + fsc->fsc_object_id = r->r_pageinfo.object_id; + // This is the ID associated with the region + fsc->fsc_region_object_id = r->r_info.object_id; + } + mach_vm_deallocate(mach_task_self(), (vm_offset_t)ch, chlen); + if (fsc->fsc_object_id) { + if (OPTIONS_DEBUG(opt, 3)) { + uuid_string_t uu; + uuid_unparse_lower(fsc->fsc_uuid, uu); + printf("Shared cache objid %llx uuid %s\n", + fsc->fsc_object_id, uu); + } + return WALK_TERMINATE; + } + return WALK_CONTINUE; +} + +static bool +compare_region_with_shared_cache(const struct region *r, struct find_shared_cache_args *fsc) +{ + struct stat st; + if (-1 == fstat(fsc->fsc_fd, &st)) { + if (OPTIONS_DEBUG(opt, 1)) + printr(r, "cannot fstat %s - %s\n", + fsc->fsc_le->le_filename, strerror(errno)); + return false; + } + void *file = mmap(NULL, R_SIZEOF(r), PROT_READ, MAP_PRIVATE, fsc->fsc_fd, r->r_pageinfo.offset); + if ((void *)-1L == file) { + if (OPTIONS_DEBUG(opt, 1)) + printr(r, "mmap %s - %s\n", fsc->fsc_le->le_filename, strerror(errno)); + return false; + } + madvise(file, R_SIZEOF(r), MADV_SEQUENTIAL); + + vm_offset_t data = 0; + mach_msg_type_number_t data_count; + const kern_return_t kr = mach_vm_read(fsc->fsc_task, R_ADDR(r), R_SIZE(r), &data, &data_count); + + if (KERN_SUCCESS != kr || data_count < R_SIZE(r)) { + err_mach(kr, r, "mach_vm_read()"); + munmap(file, R_SIZEOF(r)); + return false; + } + + mach_vm_size_t cmpsize = data_count; + +#ifdef RDAR_23744374 + /* + * Now we have the corresponding regions mapped, we should be + * able to compare them. There's just one last twist that relates + * to heterogenous pagesize systems: rdar://23744374 + */ + if (st.st_size < (off_t)(r->r_pageinfo.offset + cmpsize) && + pageshift_host < pageshift_app) { + /* + * Looks like we're about to map close to the end of the object. + * Check what's really mapped there and reduce the size accordingly. + */ + if (!is_actual_size(fsc->fsc_task, r, &cmpsize)) { + if (OPTIONS_DEBUG(opt, 3)) + printr(r, "narrowing the comparison (%llu " + "-> %llu)\n", R_SIZE(r), cmpsize); + } + } +#endif + + mach_vm_behavior_set(mach_task_self(), data, data_count, VM_BEHAVIOR_SEQUENTIAL); + + const bool thesame = memcmp(file, (void *)data, (size_t)cmpsize) == 0; +#if 0 + if (!thesame) { + int diffcount = 0; + int samecount = 0; + const char *f = file; + const char *d = (void *)data; + for (mach_vm_size_t off = 0; off < cmpsize; off += 4096) { + if (memcmp(f, d, 4096) != 0) { + diffcount++; + } else samecount++; + f += 4096; + d += 4096; + } + if (diffcount) + printr(r, "%d of %d pages different\n", diffcount, diffcount + samecount); + } +#endif + mach_vm_deallocate(mach_task_self(), data, data_count); + munmap(file, R_SIZEOF(r)); + + if (!thesame && OPTIONS_DEBUG(opt, 3)) + printr(r, "mapped file (%s) region is modified\n", fsc->fsc_le->le_filename); + return thesame; +} + +static walk_return_t +label_shared_cache(struct region *r, void *arg) +{ + struct find_shared_cache_args *fsc = arg; + + if (!r->r_insharedregion) + return WALK_CONTINUE; + if (!r->r_info.external_pager) + return WALK_CONTINUE; + if (r->r_pageinfo.object_id != fsc->fsc_object_id) { + /* wrong object, or first page already modified */ + return WALK_CONTINUE; + } + if (((r->r_info.protection | r->r_info.max_protection) & VM_PROT_WRITE) != 0) { + /* potentially writable, but was it written? */ + if (0 != r->r_info.pages_dirtied) + return WALK_CONTINUE; + if (0 != r->r_info.pages_swapped_out) + return WALK_CONTINUE; + if (0 != r->r_info.pages_resident && !r->r_info.external_pager) + return WALK_CONTINUE; + if (OPTIONS_DEBUG(opt, 1)) + printr(r, "verifying shared cache content against memory image\n"); + if (!compare_region_with_shared_cache(r, fsc)) { + /* bits don't match */ + if (OPTIONS_DEBUG(opt, 1)) + printr(r, "hmm .. mismatch: using memory image\n"); + return WALK_CONTINUE; + } + } + + /* + * This mapped file segment will be represented as a reference + * to the file, rather than as a copy of the mapped file. + */ + addfileref(r, libent_lookup_byuuid(fsc->fsc_uuid), NULL); + return WALK_CONTINUE; +} + +struct regionhead * +coredump_prepare(task_t task, uuid_t sc_uuid) +{ + struct regionhead *rhead = build_region_list(task); + + if (OPTIONS_DEBUG(opt, 2)) { + printf("Region list built\n"); + print_memory_region_header(); + walk_region_list(rhead, region_print_memory, NULL); + } + + if (uuid_is_null(sc_uuid)) + return rhead; + + /* + * Name the shared cache, if we can + */ + char *nm = shared_cache_filename(sc_uuid); + const struct libent *le; + + if (NULL != nm) + le = libent_insert(nm, sc_uuid, 0, NULL, NULL, 0); + else { + libent_insert("(anonymous shared cache)", sc_uuid, 0, NULL, NULL, 0); + if (opt->verbose){ + printf("Warning: cannot name the shared cache "); + if (OPTIONS_DEBUG(opt, 1)) { + uuid_string_t uustr; + uuid_unparse_lower(sc_uuid, uustr); + printf("(%s) ", uustr); + } + printf("- dump may be large!\n"); + } + return rhead; + } + + if (opt->extended) { + /* + * See if we can replace entire regions with references to the shared cache + * by looking at the VM meta-data about those regions. + */ + if (OPTIONS_DEBUG(opt, 1)) { + uuid_string_t uustr; + uuid_unparse_lower(sc_uuid, uustr); + printf("Searching for shared cache with uuid %s\n", uustr); + } + + /* + * Identify the regions mapping the shared cache by comparing the UUID via + * dyld with the UUID of likely-looking mappings in the right address range + */ + struct find_shared_cache_args fsca; + bzero(&fsca, sizeof (fsca)); + fsca.fsc_task = task; + uuid_copy(fsca.fsc_uuid, sc_uuid); + fsca.fsc_fd = -1; + + walk_region_list(rhead, find_shared_cache, &fsca); + + if (0 == fsca.fsc_object_id) { + printf("Cannot identify the shared cache region(s) => ignored\n"); + } else { + if (opt->verbose) + printf("Referenced %s\n", nm); + fsca.fsc_le = le; + fsca.fsc_fd = open(fsca.fsc_le->le_pathname, O_RDONLY); + if (-1 == fsca.fsc_fd) + errc(EX_SOFTWARE, errno, "open %s", fsca.fsc_le->le_pathname); + else { + walk_region_list(rhead, label_shared_cache, &fsca); + close(fsca.fsc_fd); + } + free(nm); + } + } + + return rhead; +} diff --git a/system_cmds/gcore.tproj/vanilla.h b/system_cmds/gcore.tproj/vanilla.h new file mode 100644 index 0000000..721c653 --- /dev/null +++ b/system_cmds/gcore.tproj/vanilla.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "vm.h" + +#ifndef _VANILLA_H +#define _VANILLA_H + +struct proc_bsdinfo; + +extern void validate_core_header(const native_mach_header_t *, off_t); +extern int coredump(task_t, int, const struct proc_bsdinfo *); +extern int coredump_write(task_t, int, struct regionhead *, const uuid_t, mach_vm_offset_t, mach_vm_offset_t); +extern struct regionhead *coredump_prepare(task_t, uuid_t); + +#endif /* _VANILLA_H */ diff --git a/system_cmds/gcore.tproj/vm.c b/system_cmds/gcore.tproj/vm.c new file mode 100644 index 0000000..22b0efe --- /dev/null +++ b/system_cmds/gcore.tproj/vm.c @@ -0,0 +1,493 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include "options.h" +#include "vm.h" +#include "utils.h" +#include "region.h" +#include "sparse.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * There should be better APIs to describe the shared region + * For now, some hackery. + */ + +#include + +static __inline boolean_t +in_shared_region(mach_vm_address_t addr) +{ + const mach_vm_address_t base = SHARED_REGION_BASE; + const mach_vm_address_t size = SHARED_REGION_SIZE; + return addr >= base && addr < (base + size); +} + +/* + * On both x64 and arm, there's a globallly-shared + * read-only page at _COMM_PAGE_START_ADDRESS + * which low-level library routines reference. + * + * On x64, somewhere randomly chosen between _COMM_PAGE_TEXT_ADDRESS + * and the top of the user address space, there's the + * pre-emption-free-zone read-execute page. + */ + +#include + +static __inline boolean_t +in_comm_region(const mach_vm_address_t addr, const vm_region_submap_info_data_64_t *info) +{ + return addr >= _COMM_PAGE_START_ADDRESS && + SM_TRUESHARED == info->share_mode && + VM_INHERIT_SHARE == info->inheritance && + !info->external_pager && (info->max_protection & VM_PROT_WRITE) == 0; +} + +static __inline boolean_t +in_zfod_region(const vm_region_submap_info_data_64_t *info) +{ + return info->share_mode == SM_EMPTY && !info->is_submap && + 0 == info->object_id && !info->external_pager && + 0 == info->pages_dirtied + info->pages_resident + info->pages_swapped_out; +} + +static struct region * +new_region(mach_vm_offset_t vmaddr, mach_vm_size_t vmsize, const vm_region_submap_info_data_64_t *infop) +{ + struct region *r = calloc(1, sizeof (*r)); + assert(vmaddr != 0 && vmsize != 0); + R_SETADDR(r, vmaddr); + R_SETSIZE(r, vmsize); + r->r_info = *infop; + r->r_purgable = VM_PURGABLE_DENY; + r->r_insharedregion = in_shared_region(vmaddr); + r->r_incommregion = in_comm_region(vmaddr, &r->r_info); + r->r_inzfodregion = in_zfod_region(&r->r_info); + + if (r->r_inzfodregion) + r->r_op = &zfod_ops; + else + r->r_op = &vanilla_ops; + return r; +} + +void +del_fileref_region(struct region *r) +{ + assert(&fileref_ops == r->r_op); + /* r->r_fileref->fr_libent is a reference into the name table */ + poison(r->r_fileref, 0xdeadbee9, sizeof (*r->r_fileref)); + free(r->r_fileref); + poison(r, 0xdeadbeeb, sizeof (*r)); + free(r); +} + +void +del_zfod_region(struct region *r) +{ + assert(&zfod_ops == r->r_op); + assert(r->r_inzfodregion && 0 == r->r_nsubregions); + assert(NULL == r->r_fileref); + poison(r, 0xdeadbeed, sizeof (*r)); + free(r); +} + +void +del_vanilla_region(struct region *r) +{ + assert(&vanilla_ops == r->r_op); + assert(!r->r_inzfodregion && 0 == r->r_nsubregions); + assert(NULL == r->r_fileref); + poison(r, 0xdeadbeef, sizeof (*r)); + free(r); +} + +/* + * "does any part of this address range match the tag?" + */ +int +is_tagged(task_t task, mach_vm_offset_t addr, mach_vm_offset_t size, unsigned tag) +{ + mach_vm_offset_t vm_addr = addr; + mach_vm_offset_t vm_size = 0; + natural_t depth = 0; + size_t pgsize = (1u << pageshift_host); + + do { + mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64; + vm_region_submap_info_data_64_t info; + + kern_return_t ret = mach_vm_region_recurse(task, &vm_addr, &vm_size, &depth, (vm_region_recurse_info_t)&info, &count); + + if (KERN_FAILURE == ret) { + err_mach(ret, NULL, "error inspecting task at %llx", vm_addr); + return -1; + } else if (KERN_INVALID_ADDRESS == ret) { + err_mach(ret, NULL, "invalid address at %llx", vm_addr); + return -1; + } else if (KERN_SUCCESS != ret) { + err_mach(ret, NULL, "error inspecting task at %llx", vm_addr); + return -1; + } + if (info.is_submap) { + depth++; + continue; + } + if (info.user_tag == tag) + return 1; + if (vm_addr + vm_size > addr + size) + return 0; + vm_addr += pgsize; + } while (1); +} + +STAILQ_HEAD(regionhead, region); + +/* + * XXX Need something like mach_vm_shared_region_recurse() + * to properly identify the shared region address ranges as + * we go. + */ + +static int +walk_regions(task_t task, struct regionhead *rhead) +{ + mach_vm_offset_t vm_addr = MACH_VM_MIN_ADDRESS; + natural_t depth = 0; + + if (OPTIONS_DEBUG(opt, 3)) { + printf("Building raw region list\n"); + print_memory_region_header(); + } + while (1) { + vm_region_submap_info_data_64_t info; + mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64; + mach_vm_size_t vm_size; + + kern_return_t ret = mach_vm_region_recurse(task, &vm_addr, &vm_size, &depth, (vm_region_recurse_info_t)&info, &count); + + if (KERN_FAILURE == ret) { + err_mach(ret, NULL, "error inspecting task at %llx", vm_addr); + goto bad; + } else if (KERN_INVALID_ADDRESS == ret) { + break; /* loop termination */ + } else if (KERN_SUCCESS != ret) { + err_mach(ret, NULL, "error inspecting task at %llx", vm_addr); + goto bad; + } + + if (OPTIONS_DEBUG(opt, 3)) { + struct region *d = new_region(vm_addr, vm_size, &info); + ROP_PRINT(d); + ROP_DELETE(d); + } + + if (info.is_submap) { +#ifdef CONFIG_SUBMAP + /* We also want to see submaps -- for debugging purposes. */ + struct region *r = new_region(vm_addr, vm_size, &info); + r->r_depth = depth; + STAILQ_INSERT_TAIL(rhead, r, r_linkage); +#endif + depth++; + continue; + } + + if (VM_MEMORY_IOKIT == info.user_tag) { + vm_addr += vm_size; + continue; // ignore immediately: IO memory has side-effects + } + + struct region *r = new_region(vm_addr, vm_size, &info); +#ifdef CONFIG_SUBMAP + r->r_depth = depth; +#endif + /* grab the page info of the first page in the mapping */ + + mach_msg_type_number_t pageinfoCount = VM_PAGE_INFO_BASIC_COUNT; + ret = mach_vm_page_info(task, R_ADDR(r), VM_PAGE_INFO_BASIC, (vm_page_info_t)&r->r_pageinfo, &pageinfoCount); + if (KERN_SUCCESS != ret) + err_mach(ret, r, "getting pageinfo at %llx", R_ADDR(r)); + + /* record the purgability */ + + ret = mach_vm_purgable_control(task, vm_addr, VM_PURGABLE_GET_STATE, &r->r_purgable); + if (KERN_SUCCESS != ret) + r->r_purgable = VM_PURGABLE_DENY; + + STAILQ_INSERT_TAIL(rhead, r, r_linkage); + + vm_addr += vm_size; + } + + return 0; +bad: + return EX_OSERR; +} + +void +del_region_list(struct regionhead *rhead) +{ + struct region *r, *t; + + STAILQ_FOREACH_SAFE(r, rhead, r_linkage, t) { + STAILQ_REMOVE(rhead, r, region, r_linkage); + ROP_DELETE(r); + } + free(rhead); +} + +struct regionhead * +build_region_list(task_t task) +{ + struct regionhead *rhead = malloc(sizeof (*rhead)); + STAILQ_INIT(rhead); + if (0 != walk_regions(task, rhead)) { + del_region_list(rhead); + return NULL; + } + return rhead; +} + +int +walk_region_list(struct regionhead *rhead, walk_region_cbfn_t cbfn, void *arg) +{ + struct region *r, *t; + + STAILQ_FOREACH_SAFE(r, rhead, r_linkage, t) { + switch (cbfn(r, arg)) { + case WALK_CONTINUE: + break; + case WALK_DELETE_REGION: + STAILQ_REMOVE(rhead, r, region, r_linkage); + ROP_DELETE(r); + break; + case WALK_TERMINATE: + goto done; + case WALK_ERROR: + return -1; + } + } +done: + return 0; +} + +int pageshift_host; +int pageshift_app; + +void +setpageshift(void) +{ + if (0 == pageshift_host) { + vm_size_t hps = 0; + kern_return_t ret = host_page_size(MACH_PORT_NULL, &hps); + if (KERN_SUCCESS != ret || hps == 0) + err_mach(ret, NULL, "host page size"); + int pshift = 0; + while (((vm_offset_t)1 << pshift) != hps) + pshift++; + pageshift_host = pshift; + } + if (OPTIONS_DEBUG(opt, 3)) + printf("host page size: %lu\n", 1ul << pageshift_host); + + if (0 == pageshift_app) { + size_t psz = getpagesize(); + int pshift = 0; + while ((1ul << pshift) != psz) + pshift++; + pageshift_app = pshift; + } + if (OPTIONS_DEBUG(opt, 3) && pageshift_app != pageshift_host) + printf("app page size: %lu\n", 1ul << pageshift_app); +} + +void +print_memory_region_header(void) +{ + printf("%-33s %c %-7s %-7s %8s %16s ", + "Address Range", 'S', "Size", "Cur/Max", "Obj32", "FirstPgObjectID"); + printf("%9s %-3s %-11s %5s ", + "Offset", "Tag", "Mode", "Refc"); +#ifdef CONFIG_SUBMAP + printf("%5s ", "Depth"); +#endif + printf("%5s %5s %5s %3s ", + "Res", "SNP", "Dirty", "Pgr"); + printf("\n"); +} + +static __inline char +region_type(const struct region *r) +{ + if (r->r_fileref) + return 'f'; + if (r->r_inzfodregion) + return 'z'; + if (r->r_incommregion) + return 'c'; + if (r->r_insharedregion) + return 's'; + return ' '; +} + +void +print_memory_region(const struct region *r) +{ + hsize_str_t hstr; + tag_str_t tstr; + + printf("%016llx-%016llx %c %-7s %s/%s %8x %16llx ", + R_ADDR(r), R_ENDADDR(r), region_type(r), + str_hsize(hstr, R_SIZE(r)), + str_prot(r->r_info.protection), + str_prot(r->r_info.max_protection), + r->r_info.object_id, r->r_pageinfo.object_id + ); + + printf("%9lld %3d %-11s %5u ", + r->r_info.external_pager ? + r->r_pageinfo.offset : r->r_info.offset, + r->r_info.user_tag, + str_shared(r->r_info.share_mode), + r->r_info.ref_count + ); +#ifdef CONFIG_SUBMAP + printf("%5u ", r->r_depth); +#endif + + if (!r->r_info.is_submap) { + printf("%5u %5u %5u %3s ", + r->r_info.pages_resident, + r->r_info.pages_shared_now_private, + r->r_info.pages_dirtied, + r->r_info.external_pager ? "ext" : ""); + if (r->r_fileref) + printf("\n %s at %lld ", + r->r_fileref->fr_pathname, + r->r_fileref->fr_offset); + else + printf("%s", str_tagr(tstr, r)); + printf("\n"); + if (r->r_nsubregions) { + printf(" %-33s %7s %12s\t%s\n", + "Address Range", "Size", "Type(s)", "Filename(s)"); + for (unsigned i = 0; i < r->r_nsubregions; i++) { + struct subregion *s = r->r_subregions[i]; + printf(" %016llx-%016llx %7s %12s\t%s\n", + S_ADDR(s), S_ENDADDR(s), + str_hsize(hstr, S_SIZE(s)), + S_MACHO_TYPE(s), + S_FILENAME(s)); + } + } + } else { + printf("%5s %5s %5s %3s %s\n", "", "", "", "", str_tagr(tstr, r)); + } +} + +walk_return_t +region_print_memory(struct region *r, __unused void *arg) +{ + ROP_PRINT(r); + return WALK_CONTINUE; +} + +void +print_one_memory_region(const struct region *r) +{ + print_memory_region_header(); + ROP_PRINT(r); +} + +#ifdef RDAR_23744374 +/* + * The reported size of a mapping to a file object gleaned from + * mach_vm_region_recurse() can exceed the underlying size of the file. + * If we attempt to write out the full reported size, we find that we + * error (EFAULT) or if we compress it, we die with the SIGBUS. + * + * See rdar://23744374 + * + * Figure out what the "non-faulting" size of the object is to + * *host* page size resolution. + */ +bool +is_actual_size(const task_t task, const struct region *r, mach_vm_size_t *hostvmsize) +{ + if (!r->r_info.external_pager || + (r->r_info.max_protection & VM_PROT_READ) == VM_PROT_NONE) + return true; + + const size_t pagesize_host = 1ul << pageshift_host; + const unsigned filepages = r->r_info.pages_resident + + r->r_info.pages_swapped_out; + + if (pagesize_host * filepages == R_SIZE(r)) + return true; + + /* + * Verify that the last couple of host-pagesize pages + * of a file backed mapping are actually pageable in the + * underlying object by walking backwards from the end + * of the application-pagesize mapping. + */ + *hostvmsize = R_SIZE(r); + + const long npagemax = 1ul << (pageshift_app - pageshift_host); + for (long npage = 0; npage < npagemax; npage++) { + + const mach_vm_address_t taddress = + R_ENDADDR(r) - pagesize_host * (npage + 1); + if (taddress < R_ADDR(r) || taddress >= R_ENDADDR(r)) + break; + + mach_msg_type_number_t pCount = VM_PAGE_INFO_BASIC_COUNT; + vm_page_info_basic_data_t pInfo; + + kern_return_t ret = mach_vm_page_info(task, taddress, VM_PAGE_INFO_BASIC, (vm_page_info_t)&pInfo, &pCount); + if (KERN_SUCCESS != ret) { + err_mach(ret, NULL, "getting pageinfo at %llx", taddress); + break; /* bail */ + } + + /* + * If this page has been in memory before, assume it can + * be brought back again + */ + if (pInfo.disposition & (VM_PAGE_QUERY_PAGE_PRESENT | VM_PAGE_QUERY_PAGE_REF | VM_PAGE_QUERY_PAGE_DIRTY | VM_PAGE_QUERY_PAGE_PAGED_OUT)) + continue; + + /* + * Force the page to be fetched to see if it faults + */ + mach_vm_size_t tsize = 1ul << pageshift_host; + void *tmp = valloc((size_t)tsize); + const mach_vm_address_t vtmp = (mach_vm_address_t)tmp; + + switch (ret = mach_vm_read_overwrite(task, + taddress, tsize, vtmp, &tsize)) { + case KERN_INVALID_ADDRESS: + *hostvmsize = taddress - R_ADDR(r); + break; + case KERN_SUCCESS: + break; + default: + err_mach(ret, NULL, "mach_vm_overwrite()"); + break; + } + free(tmp); + } + return R_SIZE(r) == *hostvmsize; +} +#endif diff --git a/system_cmds/gcore.tproj/vm.h b/system_cmds/gcore.tproj/vm.h new file mode 100644 index 0000000..b912782 --- /dev/null +++ b/system_cmds/gcore.tproj/vm.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016 Apple Inc. All rights reserved. + */ + +#include +#include +#include +#include +#include + +#include "corefile.h" +#include "region.h" + +#ifndef _VM_H +#define _VM_H + +extern void setpageshift(void); +extern int pageshift_host; +extern int pageshift_app; + +struct region; +struct regionhead; + +extern void del_fileref_region(struct region *); +extern void del_zfod_region(struct region *); +extern void del_sparse_region(struct region *); +extern void del_vanilla_region(struct region *); + +extern struct regionhead *build_region_list(task_t); +extern int walk_region_list(struct regionhead *, walk_region_cbfn_t, void *); +extern void del_region_list(struct regionhead *); + +extern void print_memory_region_header(void); +extern void print_memory_region(const struct region *); +extern void print_one_memory_region(const struct region *); + +extern walk_region_cbfn_t region_print_memory; +extern walk_region_cbfn_t region_write_memory; +extern walk_region_cbfn_t region_size_memory; + +extern int is_tagged(task_t, mach_vm_offset_t, mach_vm_offset_t, unsigned); + +#ifdef RDAR_23744374 +extern bool is_actual_size(const task_t, const struct region *, mach_vm_size_t *); +#endif + +#endif /* _VM_H */ -- cgit v1.2.3-56-ge451