/* SPDX-License-Identifier: GPL-2.0 */
/* Helpers shared by the binfmt_misc selftests. */
#ifndef __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H
#define __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H

#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <link.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define BINFMT_DIR	"/proc/sys/fs/binfmt_misc"
#define BINFMT_REG	BINFMT_DIR "/register"

/* comm holds 15 usable chars; a read of /proc/self/comm appends a newline. */
#define TASK_COMM_LEN	16

/* The canonical payload argv: run_payload() passes it, the payloads assert it. */
#define PAYLOAD_ARGV0	"payload-argv0"
#define PAYLOAD_ARG1	"argone"
#define PAYLOAD_ARG2	"argtwo"

/* Marker the loader tests poke into the payload's e_ident padding. */
#define LOADER_MARKER	"LDRTST"

/* Exit status run_payload() reports when the exec was refused as unhandled. */
#define RUN_ENOEXEC	42

static inline int copy_file(const char *src, const char *dst)
{
	char buf[4096];
	int in, out;
	ssize_t n;

	in = open(src, O_RDONLY);
	if (in < 0)
		return -1;
	/* The tests share /tmp, so never write through a name they don't own. */
	unlink(dst);
	out = open(dst, O_WRONLY | O_CREAT | O_EXCL, 0755);
	if (out < 0) {
		close(in);
		return -1;
	}
	while ((n = read(in, buf, sizeof(buf))) > 0) {
		if (write(out, buf, n) != n) {
			close(in);
			close(out);
			return -1;
		}
	}
	close(in);
	close(out);
	return n < 0 ? -1 : 0;
}

/* Write @rule to the register file, preserving the write's errno. */
static inline int write_reg(const char *rule)
{
	int fd, saved;
	ssize_t n;

	fd = open(BINFMT_REG, O_WRONLY);
	if (fd < 0)
		return -1;
	n = write(fd, rule, strlen(rule));
	saved = errno;
	close(fd);
	errno = saved;
	return n < 0 ? -1 : 0;
}

static inline void unregister(const char *name)
{
	char path[PATH_MAX];
	int fd;

	snprintf(path, sizeof(path), BINFMT_DIR "/%s", name);
	fd = open(path, O_WRONLY);
	if (fd >= 0) {
		if (write(fd, "-1", 2) < 0)
			; /* best effort */
		close(fd);
	}
}

/* Write @line to @entry's file, reporting the errno it was refused with. */
static inline int entry_command(const char *entry, const char *line)
{
	char path[PATH_MAX];
	int fd, retval = 0;
	size_t len = strlen(line);

	snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
	fd = open(path, O_WRONLY | O_CLOEXEC);
	if (fd < 0)
		return -errno;
	if (write(fd, line, len) != (ssize_t)len)
		retval = -errno;
	close(fd);
	return retval;
}

/* Does @entry's file report @line? */
static inline bool entry_shows(const char *entry, const char *line)
{
	char path[PATH_MAX], buf[PATH_MAX];
	bool found = false;
	FILE *fp;

	snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
	fp = fopen(path, "r");
	if (!fp)
		return false;
	while (fgets(buf, sizeof(buf), fp)) {
		buf[strcspn(buf, "\n")] = '\0';
		if (!strcmp(buf, line)) {
			found = true;
			break;
		}
	}
	fclose(fp);
	return found;
}

/* Mount binfmt_misc unless it already is, and report whether it is usable. */
static inline bool binfmt_misc_available(void)
{
	if (access(BINFMT_REG, F_OK) < 0)
		mount("binfmt_misc", BINFMT_DIR, "binfmt_misc", 0, NULL);
	return access(BINFMT_REG, F_OK) == 0;
}

/* Absolute path of @name in the directory this test was built into. */
static inline int artifact_path(char *out, size_t sz, const char *name)
{
	char exe[PATH_MAX];
	ssize_t n;

	n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
	if (n < 0)
		return -1;
	exe[n] = '\0';
	if ((size_t)snprintf(out, sz, "%s/%s", dirname(exe), name) >= sz)
		return -1;
	return 0;
}

/* Probe kernel support for a registration flag with a throwaway entry. */
static inline bool binfmt_flag_supported(char flag)
{
	char rule[64];

	snprintf(rule, sizeof(rule), ":bm_flag_probe:E::bmprobe::/bin/true:%c",
		 flag);
	if (write_reg(rule))
		return false;
	unregister("bm_flag_probe");
	return true;
}

/*
 * Run @path with the canonical payload argv and return its exit status, or
 * RUN_ENOEXEC when the exec itself was refused as unhandled.
 */
static inline int run_payload(const char *path)
{
	int status;
	pid_t pid;

	pid = fork();
	if (pid == 0) {
		execl(path, PAYLOAD_ARGV0, PAYLOAD_ARG1, PAYLOAD_ARG2,
		      (char *)NULL);
		_exit(errno == ENOEXEC ? RUN_ENOEXEC : 126);
	}
	if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
		return -1;
	return WEXITSTATUS(status);
}

/* Does the exe link name @path? */
static inline bool exe_is(const char *path)
{
	char exe[PATH_MAX], real[PATH_MAX];
	ssize_t n;

	n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
	if (n <= 0 || !realpath(path, real))
		return false;
	exe[n] = '\0';
	return !strcmp(exe, real);
}

/* Is comm @name truncated to what a comm can hold? */
static inline bool comm_is(const char *name)
{
	char comm[TASK_COMM_LEN + 2], expect[TASK_COMM_LEN];
	ssize_t n;
	int fd;

	fd = open("/proc/self/comm", O_RDONLY);
	if (fd < 0)
		return false;
	n = read(fd, comm, sizeof(comm) - 1);
	close(fd);
	if (n <= 0)
		return false;
	if (comm[n - 1] == '\n')
		n--;
	comm[n] = '\0';
	snprintf(expect, sizeof(expect), "%s", name);
	return !strcmp(comm, expect);
}

/* Opening @path for writing has to fail with ETXTBSY. */
static inline bool write_denied(const char *path)
{
	int fd = open(path, O_WRONLY);

	if (fd >= 0) {
		close(fd);
		return false;
	}
	return errno == ETXTBSY;
}

static inline int patch_file(const char *path, off_t off, const void *data, size_t len)
{
	ssize_t n;
	int fd;

	fd = open(path, O_WRONLY);
	if (fd < 0)
		return -1;
	n = pwrite(fd, data, len, off);
	close(fd);
	return n == (ssize_t)len ? 0 : -1;
}

/* start_code and end_code are the 26th and 27th fields of /proc/pid/stat. */
static inline int stat_codes(pid_t pid, unsigned long *start_code,
			     unsigned long *end_code)
{
	char buf[4096], path[64], *p;
	ssize_t n;
	int fd, i;

	snprintf(path, sizeof(path), "/proc/%d/stat", pid);
	fd = open(path, O_RDONLY);
	if (fd < 0)
		return -1;
	n = read(fd, buf, sizeof(buf) - 1);
	close(fd);
	if (n <= 0)
		return -1;
	buf[n] = '\0';

	/* Skip "pid (comm)", then start_code is the 24th field after it. */
	p = strrchr(buf, ')');
	if (!p)
		return -1;
	p++;
	for (i = 0; i < 23; i++) {
		p = strchr(p + 1, ' ');
		if (!p)
			return -1;
	}
	if (sscanf(p, " %lu %lu", start_code, end_code) != 2)
		return -1;
	return 0;
}

/* Find the system loader through our own PT_INTERP. */
static inline int find_loader(char *out, size_t sz)
{
	ElfW(Ehdr) eh;
	ElfW(Phdr) ph;
	int fd, i, ret = -1;

	fd = open("/proc/self/exe", O_RDONLY);
	if (fd < 0)
		return -1;
	if (pread(fd, &eh, sizeof(eh), 0) != sizeof(eh))
		goto out;
	for (i = 0; i < eh.e_phnum; i++) {
		if (pread(fd, &ph, sizeof(ph),
			  eh.e_phoff + i * eh.e_phentsize) != sizeof(ph))
			goto out;
		if (ph.p_type != PT_INTERP)
			continue;
		if (!ph.p_filesz || ph.p_filesz > sz)
			goto out;
		if (pread(fd, out, ph.p_filesz, ph.p_offset) !=
		    (ssize_t)ph.p_filesz)
			goto out;
		out[ph.p_filesz - 1] = '\0';
		ret = 0;
		break;
	}
out:
	close(fd);
	return ret;
}

#endif /* __SELFTESTS_EXEC_BINFMT_MISC_COMMON_H */
