// SPDX-License-Identifier: LicenseRef-SYMBOT-Proprietary
// Copyright (c) 2026 SYMBOT LTD (trading as SYM.BOT). All rights reserved.
//
// xmesh-sandbox: the one Linux launcher the runtime starts its git and a mission's CHECK commands through
// (docs/DESIGN-git-bounded-read.md B2, B7). It bounds itself and then execs the program, so the bounds hold for the
// program and everything it starts: no namespace, no privilege.
//
//   xmesh-sandbox -s FD [RULE DEV:INO PATH]... -- PROGRAM [ARG]...
//
// Each RULE allows at PATH and below it: -r reading; -x reading and executing; -w reading and writing; -wx reading,
// writing and executing. -R and -X are -r and -x for a path of the doer's (a repository's objects, a check's
// workspace), named apart only for the report below. Executing is granted only where a rule says so (security
// re-review N3): for git, its binary, its exec path and the loader; for a check, its scratch copy and the toolchain;
// never a repository's objects or the runtime's private directory. DEV:INO is the identity (st_dev, st_ino, in decimal)
// the runtime recorded for PATH when it validated it. Nothing else is allowed: no other file, no TCP bind or connect,
// and on kernels that have it (Landlock ABI 6) no abstract unix socket or signal outside the sandbox.
//
// THE RULE IS MADE ON THE INODE THE RUNTIME VALIDATED, NOT ON WHATEVER THE PATH NAMES NOW. A rule path is often in a
// repository a doer can write while the runtime works (a repository's objects directory, a check's workspace). The
// kernel sample this replaces opened each path with open(O_PATH), which follows symbolic links, so a doer who swapped a
// component of the path for a link between the runtime's check and the launch had the rule granted on the link's
// target: a directory outside the repository (security review F1). Here each path is opened with openat2() and
// RESOLVE_NO_SYMLINKS, so no component of it may be a symbolic link, and the inode opened must be the one the runtime
// recorded (a real directory renamed into the place is refused too). The rule is then added on that open descriptor.
// Landlock ties a rule to the inode, so a swap after this point changes nothing: an access through a link to elsewhere
// reaches an inode no rule covers, and is refused.
//
// Before the exec it also installs a seccomp filter that denies socket(2) for every address family (Landlock ABI 4
// bounds TCP only, so a pathname unix socket or a UDP datagram would otherwise reach a daemon's IPC socket or the
// network) and denies io_uring, whose IORING_OP_SOCKET makes a socket without socket(2). socketpair(2) is allowed for
// one shape only, a unix stream pair (what node's child-process pipes and the test runners use), and refused for a
// datagram or seqpacket pair (security re-review N4). A call made through another syscall ABI is killed: the i386
// ABI on x86_64 (socket through socketcall, by numbers this filter does not name) and the x32 ABI (the same numbers
// with __X32_SYSCALL_BIT set, security review F3).
//
// It needs Landlock ABI 3 (Linux 6.2): ABI 1 and 2 cannot bound truncate(2), so a sandboxed program could truncate any
// file of the runtime's user, and the profile's "writes: this directory only" would not hold.
//
// It reports on FD, a pipe the runtime reads, never on the program's stdout or stderr: every refusal is one line there,
// "<sentinel> host: <why>" or "<sentinel> workspace: <why>", and exit 125, so a launch that did not happen never reads
// as one of the program's own answers; once the bounds are in force it writes "ok" there, marks FD close-on-exec and
// execs. The program therefore never holds FD, and nothing it prints can pass for the launcher's report (a check's
// command could otherwise print the sentinel to turn its own failure into "not run"). An exec that fails writes its
// refusal after the "ok". WHOSE REASON A REFUSAL IS (security re-review N1): only an identity or link refusal on a
// path of the doer's (-R, -X: it is not the directory the runtime checked, or no longer reachable without a link) is
// the workspace's; every other refusal (the kernel's Landlock too old, a limit, memory, a system path that changed, an
// exec that failed) is the host's, which says nothing of the work.
//
//   xmesh-sandbox --print-filter ARCH
//
// prints the seccomp program it installs on ARCH (x86_64 or aarch64), one instruction per line, and exits: the tests
// check the program for an architecture the test host cannot run (x32 needs an x86_64 kernel built with it).
//
//   xmesh-sandbox --abi
//
// prints this kernel's Landlock ABI and exits: the runtime's probe requires signal scoping where the ABI has it (6).
//
//   xmesh-sandbox --socketpair-probe
//
// tries a unix stream, datagram and seqpacket socketpair and prints what each returned: the runtime's probe runs it as
// the program under the sandbox, to measure that only the stream pair is allowed.

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <linux/filter.h>
#include <linux/openat2.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>

#define SENTINEL "xmesh-git-sandbox: "

/* The kernel's numbers, as include/uapi/linux/landlock.h and the unified syscall table give them; defined here so the
 * launcher builds against older uapi headers too (the values are the kernel's ABI and do not change). */
#ifndef __NR_openat2
#define __NR_openat2 437
#endif
#ifndef __NR_landlock_create_ruleset
#define __NR_landlock_create_ruleset 444
#endif
#ifndef __NR_landlock_add_rule
#define __NR_landlock_add_rule 445
#endif
#ifndef __NR_landlock_restrict_self
#define __NR_landlock_restrict_self 446
#endif
#define XM_CREATE_RULESET_VERSION (1U << 0)
#define XM_RULE_PATH_BENEATH 1

#define FS_EXECUTE (1ULL << 0)
#define FS_WRITE_FILE (1ULL << 1)
#define FS_READ_FILE (1ULL << 2)
#define FS_READ_DIR (1ULL << 3)
#define FS_REMOVE_DIR (1ULL << 4)
#define FS_REMOVE_FILE (1ULL << 5)
#define FS_MAKE_CHAR (1ULL << 6)
#define FS_MAKE_DIR (1ULL << 7)
#define FS_MAKE_REG (1ULL << 8)
#define FS_MAKE_SOCK (1ULL << 9)
#define FS_MAKE_FIFO (1ULL << 10)
#define FS_MAKE_BLOCK (1ULL << 11)
#define FS_MAKE_SYM (1ULL << 12)
#define FS_REFER (1ULL << 13)      /* ABI 2 */
#define FS_TRUNCATE (1ULL << 14)   /* ABI 3 */
#define FS_IOCTL_DEV (1ULL << 15)  /* ABI 5 */
#define NET_BIND_TCP (1ULL << 0)   /* ABI 4 */
#define NET_CONNECT_TCP (1ULL << 1)
#define SCOPE_ABSTRACT_UNIX (1ULL << 0) /* ABI 6 */
#define SCOPE_SIGNAL (1ULL << 1)

#define MIN_ABI 3

/* The rights a rule can grant on a file (the rest apply to directories only). */
#define FS_ON_FILE (FS_EXECUTE | FS_WRITE_FILE | FS_READ_FILE | FS_TRUNCATE | FS_IOCTL_DEV)
#define FS_READ (FS_READ_FILE | FS_READ_DIR)
#define FS_WRITE (FS_WRITE_FILE | FS_REMOVE_DIR | FS_REMOVE_FILE | FS_MAKE_CHAR | FS_MAKE_DIR | FS_MAKE_REG | \
	FS_MAKE_SOCK | FS_MAKE_FIFO | FS_MAKE_BLOCK | FS_MAKE_SYM | FS_REFER | FS_TRUNCATE | FS_IOCTL_DEV)

struct ruleset_attr { uint64_t handled_access_fs, handled_access_net, scoped; };
struct path_beneath_attr { uint64_t allowed_access; int32_t parent_fd; } __attribute__((packed));

/* ── the seccomp program ─────────────────────────────────────────────────────────────────────────────────────── */

#define X32_SYSCALL_BIT 0x40000000U
#define AUDIT_X86_64 0xC000003EU
#define AUDIT_AARCH64 0xC00000B7U

/* One architecture's numbers: its audit arch, socket(2), and whether it has the x32 ABI beside its own. io_uring's
 * three calls are 425, 426 and 427 on every architecture (the unified table). */
struct arch { const char *name; uint32_t audit, nr_socket, nr_socketpair; int x32; };
static const struct arch ARCHES[] = {
	{ "x86_64", AUDIT_X86_64, 41, 53, 1 },
	{ "aarch64", AUDIT_AARCH64, 198, 199, 0 },
};
/* socketpair's one allowed shape: AF_UNIX, SOCK_STREAM, with SOCK_CLOEXEC and SOCK_NONBLOCK masked off the type. The
 * values are the generic ones x86_64 and aarch64 share; the filter reads the low word of each argument, which is the
 * int the kernel takes, at its little-endian offset. */
#define XM_AF_UNIX 1U
#define XM_SOCK_STREAM 1U
#define XM_TYPE_FLAGS 0x80800U /* SOCK_CLOEXEC | SOCK_NONBLOCK */
_Static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__, "the filter reads an argument's low word at its offset");
#define NR_IO_URING_SETUP 425
#define NR_IO_URING_REGISTER 427

#if defined(__x86_64__) && !defined(__ILP32__)
#define NATIVE "x86_64"
_Static_assert(__NR_socket == 41 && __NR_socketpair == 53, "ARCHES names this architecture's socket(2) and socketpair(2)");
#elif defined(__aarch64__)
#define NATIVE "aarch64"
_Static_assert(__NR_socket == 198 && __NR_socketpair == 199, "ARCHES names this architecture's socket(2) and socketpair(2)");
#else
#error "xmesh-sandbox: this architecture's seccomp numbers are not in ARCHES"
#endif
#ifdef __NR_io_uring_setup
_Static_assert(__NR_io_uring_setup == NR_IO_URING_SETUP && __NR_io_uring_register == NR_IO_URING_REGISTER, "io_uring's numbers");
#endif

#define MAX_FILTER 24

/* The filter for `a`, into `out`; returns its length.
 *   arch is not a's own                 → kill the process (another ABI: i386 on x86_64)
 *   nr has __X32_SYSCALL_BIT (x86_64)   → kill the process (the x32 ABI)
 *   socket(2)                           → EACCES, for every address family
 *   socketpair(2)                       → allowed for AF_UNIX + SOCK_STREAM only, else EACCES
 *   io_uring_setup, _enter, _register   → ENOSYS (no io_uring: its IORING_OP_SOCKET would make a socket)
 *   anything else                       → allowed */
static int build_filter(const struct arch *a, struct sock_filter *out)
{
	int n = 0;
	out[n++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch));
	out[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, a->audit, 1, 0);
	out[n++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS);
	out[n++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr));
	if (a->x32) {
		out[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, X32_SYSCALL_BIT, 0, 1);
		out[n++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS);
	}
	out[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, a->nr_socket, 0, 1);
	out[n++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA));
	/* socketpair: a unix stream pair, and nothing else (the seven instructions after this one) */
	out[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, a->nr_socketpair, 0, 7);
	out[n++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0]));
	out[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, XM_AF_UNIX, 0, 4);
	out[n++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[1]));
	out[n++] = (struct sock_filter)BPF_STMT(BPF_ALU | BPF_AND | BPF_K, ~XM_TYPE_FLAGS);
	out[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, XM_SOCK_STREAM, 0, 1);
	out[n++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
	out[n++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA));
	out[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, NR_IO_URING_SETUP, 0, 2);
	out[n++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, NR_IO_URING_REGISTER, 1, 0);
	out[n++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (ENOSYS & SECCOMP_RET_DATA));
	out[n++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
	return n;
}

static const struct arch *arch_named(const char *name)
{
	for (size_t i = 0; i < sizeof(ARCHES) / sizeof(ARCHES[0]); i++)
		if (!strcmp(ARCHES[i].name, name)) return &ARCHES[i];
	return NULL;
}

/* ── refusing ───────────────────────────────────────────────────────────────────────────────────────────────── */

/* Where the launcher reports: -s FD, else stderr (an operator trying it by hand). */
static int status_fd = 2;

/* A refusal, reported as `whose` reason: "host" or "workspace" (see the head of this file). */
__attribute__((format(printf, 2, 3)))
static int refuse_as(const char *whose, const char *fmt, ...)
{
	char line[1024];
	va_list ap;
	va_start(ap, fmt);
	int n = vsnprintf(line, sizeof(line), fmt, ap);
	va_end(ap);
	if (n < 0) n = 0;
	if ((size_t)n >= sizeof(line)) n = (int)sizeof(line) - 1;
	dprintf(status_fd, SENTINEL "%s: %.*s\n", whose, n, line);
	return 125;
}
#define refuse(...) refuse_as("host", __VA_ARGS__)

/* ── the rules ──────────────────────────────────────────────────────────────────────────────────────────────── */

/* A rule path the runtime gave: absolute, and with no "." or ".." component and no empty one, so that it names one
 * place without resolving anything but its components. */
static int plain_path(const char *p)
{
	if (p[0] != '/') return 0;
	for (const char *c = p + 1; ; ) {
		const char *end = strchr(c, '/');
		size_t len = end ? (size_t)(end - c) : strlen(c);
		if (len == 0 && (end || c != p + 1)) return 0;   /* "//" or a trailing "/" (but "/" itself is a path) */
		if ((len == 1 && c[0] == '.') || (len == 2 && c[0] == '.' && c[1] == '.')) return 0;
		if (!end) return 1;
		c = end + 1;
	}
}

static int parse_id(const char *s, uint64_t *dev, uint64_t *ino)
{
	char *end;
	if (*s < '0' || *s > '9') return 0;
	errno = 0;
	unsigned long long d = strtoull(s, &end, 10);
	if (errno || *end != ':' || end[1] < '0' || end[1] > '9') return 0;
	unsigned long long i = strtoull(end + 1, &end, 10);
	if (errno || *end) return 0;
	*dev = d;
	*ino = i;
	return 1;
}

/* The rule path's inode, opened without resolving a symbolic link anywhere in the path (nor a /proc magic link). */
static int open_rule(const char *path)
{
	struct open_how how = { .flags = O_PATH | O_CLOEXEC, .resolve = RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS };
	return (int)syscall(__NR_openat2, AT_FDCWD, path, &how, sizeof(how));
}

/* Whether the inode opened is the one the runtime recorded. */
static int same_inode(const struct stat *st, uint64_t dev, uint64_t ino)
{
	return (uint64_t)st->st_dev == dev && (uint64_t)st->st_ino == ino;
}

/* The rights a rule kind grants (-r, -x, -w, -wx, and -R, -X for a path of the doer's), or 0 for no such kind. */
static uint64_t kind_access(const char *kind)
{
	if (!strcmp(kind, "-r") || !strcmp(kind, "-R")) return FS_READ;
	if (!strcmp(kind, "-x") || !strcmp(kind, "-X")) return FS_READ | FS_EXECUTE;
	if (!strcmp(kind, "-w")) return FS_READ | FS_WRITE;
	if (!strcmp(kind, "-wx")) return FS_READ | FS_WRITE | FS_EXECUTE;
	return 0;
}

static int add_rule(int ruleset, const char *kind, const char *path, const char *id, uint64_t handled)
{
	uint64_t dev, ino, access = kind_access(kind);
	/* the doer's path: its identity or link refusals are the workspace's; everything else is the host's */
	const char *mine = (kind[1] == 'R' || kind[1] == 'X') ? "workspace" : "host";
	if (!parse_id(id, &dev, &ino)) return refuse("the identity given for %s is not DEV:INO (%s)", path, id);
	if (!plain_path(path)) return refuse("a rule path must be absolute, with no . or .. component: %s", path);
	int fd = open_rule(path);
	if (fd < 0) {
		int e = errno;
		const char *whose = (e == ELOOP || e == ENOENT || e == ENOTDIR || e == EXDEV) ? mine : "host";
		return refuse_as(whose, "%s cannot be opened without following a link: %s", path, strerror(e));
	}
	struct stat st;
	if (fstat(fd, &st)) { close(fd); return refuse("%s cannot be read: %s", path, strerror(errno)); }
	if (!same_inode(&st, dev, ino)) { close(fd); return refuse_as(mine, "%s is not the directory or file the runtime checked", path); }
	if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode) && !S_ISCHR(st.st_mode)) {
		close(fd);
		return refuse_as(mine, "%s is neither a directory, a file nor a character device", path);
	}
	struct path_beneath_attr rule = { .allowed_access = access & handled, .parent_fd = fd };
	if (!S_ISDIR(st.st_mode)) rule.allowed_access &= FS_ON_FILE;
	if (syscall(__NR_landlock_add_rule, ruleset, XM_RULE_PATH_BENEATH, &rule, 0)) {
		int e = errno;
		close(fd);
		return refuse("the rule for %s could not be added: %s", path, strerror(e));
	}
	close(fd);
	return 0;
}

int main(int argc, char *argv[])
{
	if (argc == 2 && !strcmp(argv[1], "--abi")) {
		int abi = (int)syscall(__NR_landlock_create_ruleset, NULL, 0, XM_CREATE_RULESET_VERSION);
		if (abi < 0) return refuse("Landlock is not available here: %s", strerror(errno));
		printf("%d\n", abi);
		return 0;
	}
	if (argc == 2 && !strcmp(argv[1], "--socketpair-probe")) {
		static const struct { const char *name; int type; } shapes[] = {
			{ "stream", SOCK_STREAM | SOCK_CLOEXEC }, { "dgram", SOCK_DGRAM }, { "seqpacket", SOCK_SEQPACKET },
		};
		for (size_t i = 0; i < sizeof(shapes) / sizeof(shapes[0]); i++) {
			int sv[2];
			if (socketpair(AF_UNIX, shapes[i].type, 0, sv)) printf("%s refused %s\n", shapes[i].name, strerror(errno));
			else { printf("%s allowed\n", shapes[i].name); close(sv[0]); close(sv[1]); }
		}
		return 0;
	}
	if (argc == 3 && !strcmp(argv[1], "--print-filter")) {
		const struct arch *a = arch_named(argv[2]);
		if (!a) return refuse("no seccomp program for %s", argv[2]);
		struct sock_filter f[MAX_FILTER];
		int n = build_filter(a, f);
		for (int i = 0; i < n; i++) printf("%u %u %u %u\n", f[i].code, f[i].jt, f[i].jf, f[i].k);
		return 0;
	}

	/* -s FD, the rules, then "--", then the program */
	int first = 1;
	if (argc > 2 && !strcmp(argv[1], "-s")) {
		char *end;
		long fd = strtol(argv[2], &end, 10);
		if (*argv[2] < '0' || *argv[2] > '9' || *end || fd < 0 || fd > 1023 || fcntl((int)fd, F_GETFD) < 0) return refuse("-s names no open descriptor: %s", argv[2]);
		status_fd = (int)fd;
		first = 3;
	}
	int prog = -1;
	for (int i = first; i < argc; ) {
		if (!strcmp(argv[i], "--")) { prog = i + 1; break; }
		if (!kind_access(argv[i]) || i + 2 >= argc) return refuse("usage: xmesh-sandbox -s FD [-r|-x|-w|-wx|-R|-X DEV:INO PATH]... -- PROGRAM [ARG]...");
		i += 3;
	}
	if (prog < 0 || prog >= argc) return refuse("no program to run");
	if (argv[prog][0] != '/') return refuse("the program must be named by an absolute path: %s", argv[prog]);

	int abi = (int)syscall(__NR_landlock_create_ruleset, NULL, 0, XM_CREATE_RULESET_VERSION);
	if (abi < 0) return refuse("Landlock is not available here: %s", strerror(errno));
	if (abi < MIN_ABI) {
		return refuse("this kernel's Landlock is ABI %d; ABI %d (Linux 6.2) is the first that bounds truncate(2)", abi, MIN_ABI);
	}
	/* every right is handled, execute included: Landlock allows a right it does not handle everywhere, so execute is
	 * denied except where a rule grants it (-x, -X, -wx) only because it is in this set */
	struct ruleset_attr attr = { .handled_access_fs = FS_READ | FS_WRITE | FS_EXECUTE };
	size_t size = sizeof(attr.handled_access_fs);
	if (abi < 5) attr.handled_access_fs &= ~FS_IOCTL_DEV;
	if (abi >= 4) { attr.handled_access_net = NET_BIND_TCP | NET_CONNECT_TCP; size = 2 * sizeof(uint64_t); }
	if (abi >= 6) { attr.scoped = SCOPE_ABSTRACT_UNIX | SCOPE_SIGNAL; size = sizeof(attr); }
	int ruleset = (int)syscall(__NR_landlock_create_ruleset, &attr, size, 0);
	if (ruleset < 0) return refuse("the Landlock ruleset could not be made: %s", strerror(errno));

	for (int i = first; i < prog - 1; i += 3) {
		int r = add_rule(ruleset, argv[i], argv[i + 2], argv[i + 1], attr.handled_access_fs);
		if (r) { close(ruleset); return r; }
	}

	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { close(ruleset); return refuse("no_new_privs could not be set: %s", strerror(errno)); }
	if (syscall(__NR_landlock_restrict_self, ruleset, 0)) { close(ruleset); return refuse("the Landlock ruleset could not be enforced: %s", strerror(errno)); }
	close(ruleset);

	struct sock_filter f[MAX_FILTER];
	struct sock_fprog fprog = { .len = (unsigned short)build_filter(arch_named(NATIVE), f), .filter = f };
	if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &fprog, 0, 0)) return refuse("the socket filter could not be installed: %s", strerror(errno));

	if (status_fd != 2 && fcntl(status_fd, F_SETFD, FD_CLOEXEC)) return refuse("the report descriptor could not be closed for the program: %s", strerror(errno));
	if (write(status_fd, "ok\n", 3) != 3) return refuse("the report descriptor could not be written: %s", strerror(errno));
	execv(argv[prog], &argv[prog]);
	return refuse("could not execute %s: %s", argv[prog], strerror(errno));
}
