/*
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * Copyright (C) 2013 Paul Woegerer <paul_woegerer@mentor.com>
 * Copyright (C) 2015 Antoine Busque <abusque@efficios.com>
 * Copyright (C) 2016 Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
 */

#define _LGPL_SOURCE
#include <link.h>
#include <limits.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>

#include "common/elf.h"
#include "common/macros.h"
#include "lttng-tracer-core.h"
#include "lttng-ust-statedump.h"
#include "common/jhash.h"
#include "common/getenv.h"
#include "lib/lttng-ust/events.h"

#define LTTNG_UST_TRACEPOINT_HIDDEN_DEFINITION
#define LTTNG_UST_TRACEPOINT_PROVIDER_HIDDEN_DEFINITION

#define LTTNG_UST_TRACEPOINT_DEFINE
#include "ust_lib.h"				/* Only define. */

#define LTTNG_UST_TRACEPOINT_CREATE_PROBES
#define LTTNG_UST_TP_SESSION_CHECK
#include "lttng-ust-statedump-provider.h"	/* Define and create probes. */

struct dl_iterate_data {
	int exec_found;
	bool first;
	bool cancel;
};

struct bin_info_data {
	void *base_addr_ptr;
	char resolved_path[PATH_MAX];
	char *dbg_file;
	uint8_t *build_id;
	uint64_t memsz;
	size_t build_id_len;
	int vdso;
	uint32_t crc;
	uint8_t is_pic;
	uint8_t has_build_id;
	uint8_t has_debug_link;
};

struct lttng_ust_dl_node {
	struct bin_info_data bin_data;
	struct cds_hlist_node node;
	bool traced;
	bool marked;
};

#define UST_DL_STATE_HASH_BITS	8
#define UST_DL_STATE_TABLE_SIZE	(1 << UST_DL_STATE_HASH_BITS)
static struct cds_hlist_head dl_state_table[UST_DL_STATE_TABLE_SIZE];

typedef void (*tracepoint_cb)(struct lttng_ust_session *session, void *priv);

static
struct lttng_ust_dl_node *alloc_dl_node(const struct bin_info_data *bin_data)
{
	struct lttng_ust_dl_node *e;

	e = zmalloc(sizeof(struct lttng_ust_dl_node));
	if (!e)
		return NULL;
	if (bin_data->dbg_file) {
		e->bin_data.dbg_file = strdup(bin_data->dbg_file);
		if (!e->bin_data.dbg_file)
			goto error;
	}
	if (bin_data->build_id) {
		e->bin_data.build_id = zmalloc(bin_data->build_id_len);
		if (!e->bin_data.build_id)
			goto error;
		memcpy(e->bin_data.build_id, bin_data->build_id,
				bin_data->build_id_len);
	}
	e->bin_data.base_addr_ptr = bin_data->base_addr_ptr;
	memcpy(e->bin_data.resolved_path, bin_data->resolved_path, PATH_MAX);
	e->bin_data.memsz = bin_data->memsz;
	e->bin_data.build_id_len = bin_data->build_id_len;
	e->bin_data.vdso = bin_data->vdso;
	e->bin_data.crc = bin_data->crc;
	e->bin_data.is_pic = bin_data->is_pic;
	e->bin_data.has_build_id = bin_data->has_build_id;
	e->bin_data.has_debug_link = bin_data->has_debug_link;
	return e;

error:
	free(e->bin_data.build_id);
	free(e->bin_data.dbg_file);
	free(e);
	return NULL;
}

static
void free_dl_node(struct lttng_ust_dl_node *e)
{
	free(e->bin_data.build_id);
	free(e->bin_data.dbg_file);
	free(e);
}

/* Return 0 if same, nonzero if not. */
static
int compare_bin_data(const struct bin_info_data *a,
		const struct bin_info_data *b)
{
	if (a->base_addr_ptr != b->base_addr_ptr)
		return -1;
	if (strcmp(a->resolved_path, b->resolved_path) != 0)
		return -1;
	if (a->dbg_file && !b->dbg_file)
		return -1;
	if (!a->dbg_file && b->dbg_file)
		return -1;
	if (a->dbg_file && strcmp(a->dbg_file, b->dbg_file) != 0)
		return -1;
	if (a->build_id && !b->build_id)
		return -1;
	if (!a->build_id && b->build_id)
		return -1;
	if (a->build_id_len != b->build_id_len)
		return -1;
	if (a->build_id &&
			memcmp(a->build_id, b->build_id, a->build_id_len) != 0)
		return -1;
	if (a->memsz != b->memsz)
		return -1;
	if (a->vdso != b->vdso)
		return -1;
	if (a->crc != b->crc)
		return -1;
	if (a->is_pic != b->is_pic)
		return -1;
	if (a->has_build_id != b->has_build_id)
		return -1;
	if (a->has_debug_link != b->has_debug_link)
		return -1;
	return 0;
}

static
struct lttng_ust_dl_node *find_or_create_dl_node(struct bin_info_data *bin_data)
{
	struct cds_hlist_head *head;
	struct lttng_ust_dl_node *e;
	unsigned int hash;
	bool found = false;

	hash = jhash(&bin_data->base_addr_ptr,
		sizeof(bin_data->base_addr_ptr), 0);
	head = &dl_state_table[hash & (UST_DL_STATE_TABLE_SIZE - 1)];
	cds_hlist_for_each_entry_2(e, head, node) {
		if (compare_bin_data(&e->bin_data, bin_data) != 0)
			continue;
		found = true;
		break;
	}
	if (!found) {
		/* Create */
		e = alloc_dl_node(bin_data);
		if (!e)
			return NULL;
		cds_hlist_add_head(&e->node, head);
	}
	return e;
}

static
void remove_dl_node(struct lttng_ust_dl_node *e)
{
	cds_hlist_del(&e->node);
}

/*
 * Trace statedump event into all sessions owned by the caller thread
 * for which statedump is pending.
 */
static
void trace_statedump_event(tracepoint_cb tp_cb, void *owner, void *priv)
{
	struct cds_list_head *sessionsp;
	struct lttng_ust_session_private *session_priv;

	sessionsp = lttng_get_sessions();
	cds_list_for_each_entry(session_priv, sessionsp, node) {
		if (session_priv->owner != owner)
			continue;
		if (!session_priv->statedump_pending)
			continue;
		tp_cb(session_priv->pub, priv);
	}
}

static
void trace_bin_info_cb(struct lttng_ust_session *session, void *priv)
{
	struct bin_info_data *bin_data = (struct bin_info_data *) priv;

	lttng_ust_tracepoint(lttng_ust_statedump, bin_info,
		session, bin_data->base_addr_ptr,
		bin_data->resolved_path, bin_data->memsz,
		bin_data->is_pic, bin_data->has_build_id,
		bin_data->has_debug_link);
}

static
void trace_build_id_cb(struct lttng_ust_session *session, void *priv)
{
	struct bin_info_data *bin_data = (struct bin_info_data *) priv;

	lttng_ust_tracepoint(lttng_ust_statedump, build_id,
		session, bin_data->base_addr_ptr,
		bin_data->build_id, bin_data->build_id_len);
}

static
void trace_debug_link_cb(struct lttng_ust_session *session, void *priv)
{
	struct bin_info_data *bin_data = (struct bin_info_data *) priv;

	lttng_ust_tracepoint(lttng_ust_statedump, debug_link,
		session, bin_data->base_addr_ptr,
		bin_data->dbg_file, bin_data->crc);
}

static
void procname_cb(struct lttng_ust_session *session, void *priv)
{
	char *procname = (char *) priv;
	lttng_ust_tracepoint(lttng_ust_statedump, procname, session, procname);
}

static
void trace_start_cb(struct lttng_ust_session *session, void *priv __attribute__((unused)))
{
	lttng_ust_tracepoint(lttng_ust_statedump, start, session);
}

static
void trace_end_cb(struct lttng_ust_session *session, void *priv __attribute__((unused)))
{
	lttng_ust_tracepoint(lttng_ust_statedump, end, session);
}

static
int get_elf_info(struct bin_info_data *bin_data)
{
	struct lttng_ust_elf *elf;
	int ret = 0, found;

	elf = lttng_ust_elf_create(bin_data->resolved_path);
	if (!elf) {
		ret = -1;
		goto end;
	}

	ret = lttng_ust_elf_get_memsz(elf, &bin_data->memsz);
	if (ret) {
		goto end;
	}

	found = 0;
	ret = lttng_ust_elf_get_build_id(elf, &bin_data->build_id,
					&bin_data->build_id_len,
					&found);
	if (ret) {
		goto end;
	}
	bin_data->has_build_id = !!found;
	found = 0;
	ret = lttng_ust_elf_get_debug_link(elf, &bin_data->dbg_file,
					&bin_data->crc,
					&found);
	if (ret) {
		goto end;
	}
	bin_data->has_debug_link = !!found;

	bin_data->is_pic = lttng_ust_elf_is_pic(elf);

end:
	lttng_ust_elf_destroy(elf);
	return ret;
}

static
void trace_baddr(struct bin_info_data *bin_data, void *owner)
{
	trace_statedump_event(trace_bin_info_cb, owner, bin_data);

	if (bin_data->has_build_id)
		trace_statedump_event(trace_build_id_cb, owner, bin_data);

	if (bin_data->has_debug_link)
		trace_statedump_event(trace_debug_link_cb, owner, bin_data);
}

static
int extract_baddr(struct bin_info_data *bin_data)
{
	int ret = 0;
	struct lttng_ust_dl_node *e;

	if (!bin_data->vdso) {
		ret = get_elf_info(bin_data);
		if (ret) {
			goto end;
		}
	} else {
		bin_data->memsz = 0;
		bin_data->has_build_id = 0;
		bin_data->has_debug_link = 0;
	}

	e = find_or_create_dl_node(bin_data);
	if (!e) {
		ret = -1;
		goto end;
	}
	e->marked = true;
end:
	free(bin_data->build_id);
	bin_data->build_id = NULL;
	free(bin_data->dbg_file);
	bin_data->dbg_file = NULL;
	return ret;
}

static
void trace_statedump_start(void *owner)
{
	trace_statedump_event(trace_start_cb, owner, NULL);
}

static
void trace_statedump_end(void *owner)
{
	trace_statedump_event(trace_end_cb, owner, NULL);
}

static
void iter_begin(struct dl_iterate_data *data)
{
	unsigned int i;

	/*
	 * UST lock nests within dynamic loader lock.
	 *
	 * Hold this lock across handling of the module listing to
	 * protect memory allocation at early process start, due to
	 * interactions with libc-wrapper lttng malloc instrumentation.
	 */
	if (ust_lock()) {
		data->cancel = true;
		return;
	}

	/* Ensure all entries are unmarked. */
	for (i = 0; i < UST_DL_STATE_TABLE_SIZE; i++) {
		struct cds_hlist_head *head;
		struct lttng_ust_dl_node *e;

		head = &dl_state_table[i];
		cds_hlist_for_each_entry_2(e, head, node)
			assert(!e->marked);
	}
}

static
void trace_lib_load(const struct bin_info_data *bin_data, void *ip)
{
	lttng_ust_tracepoint(lttng_ust_lib, load,
		ip, bin_data->base_addr_ptr, bin_data->resolved_path,
		bin_data->memsz, bin_data->has_build_id,
		bin_data->has_debug_link);

	if (bin_data->has_build_id) {
		lttng_ust_tracepoint(lttng_ust_lib, build_id,
			ip, bin_data->base_addr_ptr, bin_data->build_id,
			bin_data->build_id_len);
	}

	if (bin_data->has_debug_link) {
		lttng_ust_tracepoint(lttng_ust_lib, debug_link,
			ip, bin_data->base_addr_ptr, bin_data->dbg_file,
			bin_data->crc);
	}
}

static
void trace_lib_unload(const struct bin_info_data *bin_data, void *ip)
{
	lttng_ust_tracepoint(lttng_ust_lib, unload, ip, bin_data->base_addr_ptr);
}

static
void iter_end(struct dl_iterate_data *data, void *ip)
{
	unsigned int i;

	if (data->cancel)
		goto end;
	/*
	 * Iterate on hash table.
	 * For each marked, traced, do nothing.
	 * For each marked, not traced, trace lib open event. traced = true.
	 * For each unmarked, traced, trace lib close event. remove node.
	 * For each unmarked, not traced, remove node.
	 */
	for (i = 0; i < UST_DL_STATE_TABLE_SIZE; i++) {
		struct cds_hlist_head *head;
		struct lttng_ust_dl_node *e, *tmp;

		head = &dl_state_table[i];
		cds_hlist_for_each_entry_safe_2(e, tmp, head, node) {
			if (e->marked) {
				if (!e->traced) {
					trace_lib_load(&e->bin_data, ip);
					e->traced = true;
				}
				e->marked = false;
			} else {
				if (e->traced)
					trace_lib_unload(&e->bin_data, ip);
				remove_dl_node(e);
				free_dl_node(e);
			}
		}
	}
end:
	ust_unlock();
}

static
int extract_bin_info_events(struct dl_phdr_info *info, size_t size __attribute__((unused)), void *_data)
{
	int j, ret = 0;
	struct dl_iterate_data *data = _data;

	if (data->first) {
		iter_begin(data);
		data->first = false;
	}

	if (data->cancel)
		goto end;

	for (j = 0; j < info->dlpi_phnum; j++) {
		struct bin_info_data bin_data;

		if (info->dlpi_phdr[j].p_type != PT_LOAD)
			continue;

		memset(&bin_data, 0, sizeof(bin_data));

		/* Calculate virtual memory address of the loadable segment */
		bin_data.base_addr_ptr = (void *) info->dlpi_addr +
			info->dlpi_phdr[j].p_vaddr;

		if ((info->dlpi_name == NULL || info->dlpi_name[0] == 0)) {
			/*
			 * Only the first phdr without a dlpi_name
			 * encountered is considered as the program
			 * executable. The rest are vdsos.
			 */
			if (!data->exec_found) {
				ssize_t path_len;
				data->exec_found = 1;

				/*
				 * Use /proc/self/exe to resolve the
				 * executable's full path.
				 */
				path_len = readlink("/proc/self/exe",
						    bin_data.resolved_path,
						    PATH_MAX - 1);
				if (path_len <= 0)
					break;

				bin_data.resolved_path[path_len] = '\0';
				bin_data.vdso = 0;
			} else {
				snprintf(bin_data.resolved_path,
					PATH_MAX - 1, "[vdso]");
				bin_data.vdso = 1;
			}
		} else {
			/*
			 * For regular dl_phdr_info entries check if
			 * the path to the binary really exists. If not,
			 * treat as vdso and use dlpi_name as 'path'.
			 */
			if (!realpath(info->dlpi_name,
					bin_data.resolved_path)) {
				snprintf(bin_data.resolved_path,
					PATH_MAX - 1, "[%s]",
					info->dlpi_name);
				bin_data.vdso = 1;
			} else {
				bin_data.vdso = 0;
			}
		}

		ret = extract_baddr(&bin_data);
		break;
	}
end:
	return ret;
}

static
void ust_dl_table_statedump(void *owner)
{
	unsigned int i;

	if (ust_lock())
		goto end;

	/* Statedump each traced table entry into session for owner. */
	for (i = 0; i < UST_DL_STATE_TABLE_SIZE; i++) {
		struct cds_hlist_head *head;
		struct lttng_ust_dl_node *e;

		head = &dl_state_table[i];
		cds_hlist_for_each_entry_2(e, head, node) {
			if (e->traced)
				trace_baddr(&e->bin_data, owner);
		}
	}

end:
	ust_unlock();
}

void lttng_ust_dl_update(void *ip)
{
	struct dl_iterate_data data;

	if (lttng_ust_getenv("LTTNG_UST_WITHOUT_BADDR_STATEDUMP"))
		return;

	/*
	 * Force the allocation of lttng-ust TLS variables when called from
	 * dlopen/dlclose instrumentation.
	 */
	lttng_ust_alloc_tls();

	data.exec_found = 0;
	data.first = true;
	data.cancel = false;
	/*
	 * Iterate through the list of currently loaded shared objects and
	 * generate tables entries for loadable segments using
	 * extract_bin_info_events.
	 * Removed libraries are detected by mark-and-sweep: marking is
	 * done in the iteration over libraries, and sweeping is
	 * performed by iter_end().
	 */
	dl_iterate_phdr(extract_bin_info_events, &data);
	if (data.first)
		iter_begin(&data);
	iter_end(&data, ip);
}

/*
 * Generate a statedump of base addresses of all shared objects loaded
 * by the traced application, as well as for the application's
 * executable itself.
 */
static
int do_baddr_statedump(void *owner)
{
	if (lttng_ust_getenv("LTTNG_UST_WITHOUT_BADDR_STATEDUMP"))
		return 0;
	lttng_ust_dl_update(LTTNG_UST_CALLER_IP());
	ust_dl_table_statedump(owner);
	return 0;
}

static
int do_procname_statedump(void *owner)
{
	if (lttng_ust_getenv("LTTNG_UST_WITHOUT_PROCNAME_STATEDUMP"))
		return 0;

	trace_statedump_event(procname_cb, owner, lttng_ust_sockinfo_get_procname(owner));
	return 0;
}

/*
 * Generate a statedump of a given traced application. A statedump is
 * delimited by start and end events. For a given (process, session)
 * pair, begin/end events are serialized and will match. However, in a
 * session, statedumps from different processes may be
 * interleaved. The vpid context should be used to identify which
 * events belong to which process.
 *
 * Grab the ust_lock outside of the RCU read-side lock because we
 * perform synchronize_rcu with the ust_lock held, which can trigger
 * deadlocks otherwise.
 */
int do_lttng_ust_statedump(void *owner)
{
	ust_lock_nocheck();
	trace_statedump_start(owner);
	ust_unlock();

	do_procname_statedump(owner);
	do_baddr_statedump(owner);

	ust_lock_nocheck();
	trace_statedump_end(owner);
	ust_unlock();

	return 0;
}

void lttng_ust_statedump_init(void)
{
	lttng_ust__tracepoints__init();
	lttng_ust__tracepoints__ptrs_init();
	lttng_ust__events_init__lttng_ust_statedump();
	lttng_ust_dl_update(LTTNG_UST_CALLER_IP());
}

static
void ust_dl_state_destroy(void)
{
	unsigned int i;

	for (i = 0; i < UST_DL_STATE_TABLE_SIZE; i++) {
		struct cds_hlist_head *head;
		struct lttng_ust_dl_node *e, *tmp;

		head = &dl_state_table[i];
		cds_hlist_for_each_entry_safe_2(e, tmp, head, node)
			free_dl_node(e);
		CDS_INIT_HLIST_HEAD(head);
	}
}

void lttng_ust_statedump_destroy(void)
{
	lttng_ust__events_exit__lttng_ust_statedump();
	lttng_ust__tracepoints__ptrs_destroy();
	lttng_ust__tracepoints__destroy();
	ust_dl_state_destroy();
}
