/*-
 * Copyright (c) 2013 Mikolaj Golub <trociny@FreeBSD.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__FBSDID("$FreeBSD: releng/9.2/lib/libprocstat/core.c 250870 2013-05-21 19:04:16Z trociny $");

#include <sys/param.h>
#include <sys/elf.h>
#include <sys/exec.h>
#include <sys/user.h>

#include <assert.h>
#include <err.h>
#include <fcntl.h>
#include <gelf.h>
#include <libelf.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "core.h"

#define PROCSTAT_CORE_MAGIC	0x012DADB8
struct procstat_core
{
	int		pc_magic;
	int		pc_fd;
	Elf		*pc_elf;
	GElf_Ehdr	pc_ehdr;
	GElf_Phdr	pc_phdr;
};

static bool	core_offset(struct procstat_core *core, off_t offset);
static bool	core_read(struct procstat_core *core, void *buf, size_t len);
static ssize_t	core_read_mem(struct procstat_core *core, void *buf,
    size_t len, vm_offset_t addr, bool readall);
static void	*get_args(struct procstat_core *core, vm_offset_t psstrings,
    enum psc_type type, void *buf, size_t *lenp);

struct procstat_core *
procstat_core_open(const char *filename)
{
	struct procstat_core *core;
	Elf *e;
	GElf_Ehdr ehdr;
	GElf_Phdr phdr;
	size_t nph;
	int fd, i;

	if (elf_version(EV_CURRENT) == EV_NONE) {
		warnx("ELF library too old");
		return (NULL);
	}
	fd = open(filename, O_RDONLY, 0);
	if (fd == -1) {
		warn("open(%s)", filename);
		return (NULL);
	}
	e = elf_begin(fd, ELF_C_READ, NULL);
	if (e == NULL) {
		warnx("elf_begin: %s", elf_errmsg(-1));
		goto fail;
	}
	if (elf_kind(e) != ELF_K_ELF) {
		warnx("%s is not an ELF object", filename);
		goto fail;
	}
	if (gelf_getehdr(e, &ehdr) == NULL) {
		warnx("gelf_getehdr: %s", elf_errmsg(-1));
		goto fail;
	}
	if (ehdr.e_type != ET_CORE) {
		warnx("%s is not a CORE file", filename);
		goto fail;
	}
	if (elf_getphnum(e, &nph) == 0) {
		warnx("program headers not found");
		goto fail;
	}
	for (i = 0; i < ehdr.e_phnum; i++) {
		if (gelf_getphdr(e, i, &phdr) != &phdr) {
			warnx("gelf_getphdr: %s", elf_errmsg(-1));
			goto fail;
		}
		if (phdr.p_type == PT_NOTE)
			break;
	}
	if (i == ehdr.e_phnum) {
		warnx("NOTE program header not found");
		goto fail;
	}
	core = malloc(sizeof(struct procstat_core));
	if (core == NULL) {
		warn("malloc(%zu)", sizeof(struct procstat_core));
		goto fail;
	}
	core->pc_magic = PROCSTAT_CORE_MAGIC;
	core->pc_fd = fd;
	core->pc_elf = e;
	core->pc_ehdr = ehdr;
	core->pc_phdr = phdr;

	return (core);
fail:
	if (e != NULL)
		elf_end(e);
	close(fd);

	return (NULL);
}

void
procstat_core_close(struct procstat_core *core)
{

	assert(core->pc_magic == PROCSTAT_CORE_MAGIC);

	elf_end(core->pc_elf);
	close(core->pc_fd);
	free(core);
}

void *
procstat_core_get(struct procstat_core *core, enum psc_type type, void *buf,
    size_t *lenp)
{
	Elf_Note nhdr;
	off_t offset, eoffset;
	vm_offset_t psstrings;
	void *freebuf;
	size_t len;
	u_int32_t n_type;
	int cstructsize, structsize;
	char nbuf[8];

	assert(core->pc_magic == PROCSTAT_CORE_MAGIC);

	switch(type) {
	case PSC_TYPE_PROC:
		n_type = NT_PROCSTAT_PROC;
		structsize = sizeof(struct kinfo_proc);
		break;
	case PSC_TYPE_FILES:
		n_type = NT_PROCSTAT_FILES;
		structsize = sizeof(struct kinfo_file);
		break;
	case PSC_TYPE_VMMAP:
		n_type = NT_PROCSTAT_VMMAP;
		structsize = sizeof(struct kinfo_vmentry);
		break;
	case PSC_TYPE_GROUPS:
		n_type = NT_PROCSTAT_GROUPS;
		structsize = sizeof(gid_t);
		break;
	case PSC_TYPE_UMASK:
		n_type = NT_PROCSTAT_UMASK;
		structsize = sizeof(u_short);
		break;
	case PSC_TYPE_RLIMIT:
		n_type = NT_PROCSTAT_RLIMIT;
		structsize = sizeof(struct rlimit) * RLIM_NLIMITS;
		break;
	case PSC_TYPE_OSREL:
		n_type = NT_PROCSTAT_OSREL;
		structsize = sizeof(int);
		break;
	case PSC_TYPE_PSSTRINGS:
	case PSC_TYPE_ARGV:
	case PSC_TYPE_ENVV:
		n_type = NT_PROCSTAT_PSSTRINGS;
		structsize = sizeof(vm_offset_t);
		break;
	case PSC_TYPE_AUXV:
		n_type = NT_PROCSTAT_AUXV;
		structsize = sizeof(Elf_Auxinfo);
		break;
	default:
		warnx("unknown core stat type: %d", type);
		return (NULL);
	}

	offset = core->pc_phdr.p_offset;
	eoffset = offset + core->pc_phdr.p_filesz;

	while (offset < eoffset) {
		if (!core_offset(core, offset))
			return (NULL);
		if (!core_read(core, &nhdr, sizeof(nhdr)))
			return (NULL);

		offset += sizeof(nhdr) +
		    roundup2(nhdr.n_namesz, sizeof(Elf32_Size)) +
		    roundup2(nhdr.n_descsz, sizeof(Elf32_Size));

		if (nhdr.n_namesz == 0 && nhdr.n_descsz == 0)
			break;
		if (nhdr.n_type != n_type)
			continue;
		if (nhdr.n_namesz != 8)
			continue;
		if (!core_read(core, nbuf, sizeof(nbuf)))
			return (NULL);
		if (strcmp(nbuf, "FreeBSD") != 0)
			continue;
		if (nhdr.n_descsz < sizeof(cstructsize)) {
			warnx("corrupted core file");
			return (NULL);
		}
		if (!core_read(core, &cstructsize, sizeof(cstructsize)))
			return (NULL);
		if (cstructsize != structsize) {
			warnx("version mismatch");
			return (NULL);
		}
		len = nhdr.n_descsz - sizeof(cstructsize);
		if (len == 0)
			return (NULL);
		if (buf != NULL) {
			len = MIN(len, *lenp);
			freebuf = NULL;
		} else {
			freebuf = buf = malloc(len);
			if (buf == NULL) {
				warn("malloc(%zu)", len);
				return (NULL);
			}
		}
		if (!core_read(core, buf, len)) {
			free(freebuf);
			return (NULL);
		}
		if (type == PSC_TYPE_ARGV || type == PSC_TYPE_ENVV) {
			if (len < sizeof(psstrings)) {
				free(freebuf);
				return (NULL);
			}
			psstrings = *(vm_offset_t *)buf;
			if (freebuf == NULL)
				len = *lenp;
			else
				buf = NULL;
			free(freebuf);
			buf = get_args(core, psstrings, type, buf, &len);
		}
		*lenp = len;
		return (buf);
        }

	return (NULL);
}

static bool
core_offset(struct procstat_core *core, off_t offset)
{

	assert(core->pc_magic == PROCSTAT_CORE_MAGIC);

	if (lseek(core->pc_fd, offset, SEEK_SET) == -1) {
		warn("core: lseek(%jd)", (intmax_t)offset);
		return (false);
	}
	return (true);
}

static bool
core_read(struct procstat_core *core, void *buf, size_t len)
{
	ssize_t n;

	assert(core->pc_magic == PROCSTAT_CORE_MAGIC);

	n = read(core->pc_fd, buf, len);
	if (n == -1) {
		warn("core: read");
		return (false);
	}
	if (n < (ssize_t)len) {
		warnx("core: short read");
		return (false);
	}
	return (true);
}

static ssize_t
core_read_mem(struct procstat_core *core, void *buf, size_t len,
    vm_offset_t addr, bool readall)
{
	GElf_Phdr phdr;
	off_t offset;
	int i;

	assert(core->pc_magic == PROCSTAT_CORE_MAGIC);

	for (i = 0; i < core->pc_ehdr.e_phnum; i++) {
		if (gelf_getphdr(core->pc_elf, i, &phdr) != &phdr) {
			warnx("gelf_getphdr: %s", elf_errmsg(-1));
			return (-1);
		}
		if (phdr.p_type != PT_LOAD)
			continue;
		if (addr < phdr.p_vaddr || addr > phdr.p_vaddr + phdr.p_memsz)
			continue;
		offset = phdr.p_offset + (addr - phdr.p_vaddr);
		if ((phdr.p_vaddr + phdr.p_memsz) - addr < len) {
			if (readall) {
				warnx("format error: "
				    "attempt to read out of segment");
				return (-1);
			}
			len = (phdr.p_vaddr + phdr.p_memsz) - addr;
		}
		if (!core_offset(core, offset))
			return (-1);
		if (!core_read(core, buf, len))
			return (-1);
		return (len);
	}
	warnx("format error: address %ju not found", (uintmax_t)addr);
	return (-1);
}

#define ARGS_CHUNK_SZ	256	/* Chunk size (bytes) for get_args operations. */

static void *
get_args(struct procstat_core *core, vm_offset_t psstrings, enum psc_type type,
     void *args, size_t *lenp)
{
	struct ps_strings pss;
	void *freeargs;
	vm_offset_t addr;
	char **argv, *p;
	size_t chunksz, done, len, nchr, size;
	ssize_t n;
	u_int i, nstr;

	assert(type == PSC_TYPE_ARGV || type == PSC_TYPE_ENVV);

	if (core_read_mem(core, &pss, sizeof(pss), psstrings, true) == -1)
		return (NULL);
	if (type == PSC_TYPE_ARGV) {
		addr = (vm_offset_t)pss.ps_argvstr;
		nstr = pss.ps_nargvstr;
	} else /* type == PSC_TYPE_ENVV */ {
		addr = (vm_offset_t)pss.ps_envstr;
		nstr = pss.ps_nenvstr;
	}
	if (addr == 0 || nstr == 0)
		return (NULL);
	if (nstr > ARG_MAX) {
		warnx("format error");
		return (NULL);
	}
	size = nstr * sizeof(char *);
	argv = malloc(size);
	if (argv == NULL) {
		warn("malloc(%zu)", size);
		return (NULL);
	}
	done = 0;
	freeargs = NULL;
	if (core_read_mem(core, argv, size, addr, true) == -1)
		goto fail;
	if (args != NULL) {
		nchr = MIN(ARG_MAX, *lenp);
	} else {
		nchr = ARG_MAX;
		freeargs = args = malloc(nchr);
		if (args == NULL) {
			warn("malloc(%zu)", nchr);
			goto fail;
		}
	}
	p = args;
	for (i = 0; ; i++) {
		if (i == nstr)
			goto done;
		/*
		 * The program may have scribbled into its argv array, e.g. to
		 * remove some arguments.  If that has happened, break out
		 * before trying to read from NULL.
		 */
		if (argv[i] == NULL)
			goto done;
		for (addr = (vm_offset_t)argv[i]; ; addr += chunksz) {
			chunksz = MIN(ARGS_CHUNK_SZ, nchr - 1 - done);
			if (chunksz <= 0)
				goto done;
			n = core_read_mem(core, p, chunksz, addr, false);
			if (n == -1)
				goto fail;
			len = strnlen(p, chunksz);
			p += len;
			done += len;
			if (len != chunksz)
				break;
		}
		*p++ = '\0';
		done++;
	}
fail:
	free(freeargs);
	args = NULL;
done:
	*lenp = done;
	free(argv);
	return (args);
}
