Files
archy/core/runtime-proxy.c

180 lines
7.7 KiB
C

/* Static, unprivileged command relay for the protected manager's executable
* namespace. Never executes a program locally or interprets shell text.
* The separately credentialed runtime owner receives argv/environment and
* exactly stdin/stdout/stderr descriptors. No manager identity key is opened.
*/
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
extern char **environ;
#define SOCKET_PATH "/run/archipelago-managed/runtime.sock"
#define MAX_PACKET 65536U
#define MAGIC 0x41524352U
#ifndef RUNTIME_RESULT_MS
#define RUNTIME_RESULT_MS 1810000U
#endif
static unsigned char packet[MAX_PACKET];
static size_t used;
static int control_fd = -1;
static _Noreturn void fail(void) {
/* Standard streams are delegated aliases and may be full blocking pipes.
* Revoke first and never wait for stdio diagnostics/flushing on failure. */
if (control_fd >= 0) close(control_fd);
explicit_bzero(packet, sizeof(packet));
_exit(125);
}
static uint64_t now_ms(void) {
struct timespec now;
if (clock_gettime(CLOCK_BOOTTIME, &now) || now.tv_sec < 0 || now.tv_nsec < 0 ||
now.tv_nsec >= 1000000000L || (uint64_t)now.tv_sec > UINT64_MAX / 1000U) fail();
return (uint64_t)now.tv_sec * 1000U + (uint64_t)now.tv_nsec / 1000000U;
}
static void number(uint32_t value) {
if (used > MAX_PACKET - 4) fail();
value = htonl(value);
memcpy(packet + used, &value, 4); used += 4;
}
static void string(const char *value) {
size_t size = strnlen(value, MAX_PACKET);
if (size == MAX_PACKET || used > MAX_PACKET - 4 || size > MAX_PACKET - used - 4) fail();
number((uint32_t)size);
memcpy(packet + used, value, size); used += size;
}
static void ready(int fd, short events, uint64_t end) {
for (;;) {
uint64_t now = now_ms();
if (now >= end) fail();
uint64_t left = end - now;
struct pollfd wait = {.fd = fd, .events = events};
int result = poll(&wait, 1, (int)(left > 1000U ? 1000U : left));
if (now_ms() >= end) fail();
if (result < 0 && errno == EINTR) continue;
if (result == 0) continue;
if (result < 0 || !(wait.revents & events) || (wait.revents & (POLLERR | POLLNVAL))) fail();
return;
}
}
static void send_rest(int fd, const unsigned char *bytes, size_t size, uint64_t end) {
while (size) {
ready(fd, POLLOUT, end);
ssize_t count = send(fd, bytes, size, MSG_DONTWAIT | MSG_NOSIGNAL);
if (count < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
if (count <= 0) fail();
bytes += count; size -= (size_t)count;
}
}
static void private_endpoint(void) {
struct stat meta;
const char *parents[] = {"/", "/run", "/run/archipelago-managed"};
for (size_t i = 0; i < sizeof(parents) / sizeof(parents[0]); i++) {
if (lstat(parents[i], &meta) || !S_ISDIR(meta.st_mode) || meta.st_uid != 0 || (meta.st_mode & 0022)) fail();
}
if (lstat(SOCKET_PATH, &meta) || !S_ISSOCK(meta.st_mode) || meta.st_uid != 0 || (meta.st_mode & 0007)) fail();
}
int main(int argc, char **argv) {
if (argc < 1 || !argv || !argv[0]) fail();
if (getuid() == 0 || geteuid() != getuid() || getegid() != getgid() ||
prctl(PR_SET_DUMPABLE, 0, 0, 0, 0)) fail();
for (int fd = 0; fd <= 2; fd++) if (fcntl(fd, F_GETFD) < 0) fail();
signal(SIGPIPE, SIG_IGN);
private_endpoint();
const char *name = strrchr(argv[0], '/'); name = name ? name + 1 : argv[0];
int start = 0;
if (!strcmp(name, "archipelago-runtime-proxy")) {
if (argc < 3 || strcmp(argv[1], "--")) fail();
start = 2;
}
if (argc - start < 1 || argc - start > 256) fail();
size_t envc = 0;
while (environ[envc]) { if (++envc > 128) fail(); }
char cwd[4096];
if (!getcwd(cwd, sizeof(cwd))) fail();
number(MAGIC); number(1); number((uint32_t)(argc - start)); number((uint32_t)envc);
string(cwd);
for (int i = start; i < argc; i++) string(argv[i]);
for (size_t i = 0; i < envc; i++) string(environ[i]);
uint64_t admission_end = now_ms() + 2000U;
int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
if (fd < 0) fail();
control_fd = fd;
struct sockaddr_un address = {.sun_family = AF_UNIX};
memcpy(address.sun_path, SOCKET_PATH, sizeof(SOCKET_PATH));
if (connect(fd, (struct sockaddr *)&address, sizeof(address))) {
if (errno != EINPROGRESS) fail();
ready(fd, POLLOUT, admission_end);
int error = 0; socklen_t length = sizeof(error);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) || error) fail();
}
if (now_ms() >= admission_end) fail();
struct ucred peer; socklen_t peer_size = sizeof(peer);
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &peer, &peer_size) || peer_size != sizeof(peer) ||
peer.uid != 0 || peer.gid != 0 || peer.pid <= 1) fail();
uint32_t size = htonl((uint32_t)used);
struct iovec io[] = {{&size, sizeof(size)}, {packet, used}};
union { struct cmsghdr alignment; char bytes[CMSG_SPACE(3 * sizeof(int))]; } control;
memset(&control, 0, sizeof(control));
struct msghdr message = {.msg_iov = io, .msg_iovlen = 2, .msg_control = control.bytes,
.msg_controllen = sizeof(control.bytes)};
struct cmsghdr *header = CMSG_FIRSTHDR(&message);
header->cmsg_level = SOL_SOCKET; header->cmsg_type = SCM_RIGHTS;
header->cmsg_len = CMSG_LEN(3 * sizeof(int));
int streams[] = {0, 1, 2}; memcpy(CMSG_DATA(header), streams, sizeof(streams));
ssize_t count;
for (;;) {
ready(fd, POLLOUT, admission_end);
count = sendmsg(fd, &message, MSG_DONTWAIT | MSG_NOSIGNAL);
if (count < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
break;
}
if (count <= 0 || (size_t)count > used + 4) fail();
if ((size_t)count < 4) {
send_rest(fd, ((unsigned char *)&size) + count, 4 - (size_t)count, admission_end);
send_rest(fd, packet, used, admission_end);
} else {
send_rest(fd, packet + ((size_t)count - 4), used - ((size_t)count - 4), admission_end);
}
explicit_bzero(packet, sizeof(packet));
if (now_ms() >= admission_end) fail();
/* The owner cancels on connection loss. This prototype client budget is
* separate from future interactive-session/lifecycle policy. Output travels
* through delegated FDs, never an unbounded broker-owned capture buffer. */
uint32_t result[3]; size_t received = 0;
uint64_t result_end = now_ms() + RUNTIME_RESULT_MS;
while (received < sizeof(result)) {
ready(fd, POLLIN, result_end);
ssize_t n = recv(fd, ((unsigned char *)result) + received, sizeof(result) - received, MSG_DONTWAIT);
if (n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
if (n <= 0) fail();
received += (size_t)n;
}
if (now_ms() >= result_end) fail();
close(fd); control_fd = -1;
if (ntohl(result[0]) != MAGIC) fail();
uint32_t kind = ntohl(result[1]), value = ntohl(result[2]);
if (kind == 0 && value <= 255) return (int)value;
if (kind == 1 && value > 0 && value < NSIG && value != SIGSTOP && value != SIGCONT &&
value != SIGTSTP && value != SIGTTIN && value != SIGTTOU) {
if (value != SIGKILL && signal((int)value, SIG_DFL) == SIG_ERR) fail();
sigset_t unblocked;
if (sigemptyset(&unblocked) || sigaddset(&unblocked, (int)value) ||
sigprocmask(SIG_UNBLOCK, &unblocked, NULL) || raise((int)value)) fail();
}
fail();
}