180 lines
7.7 KiB
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();
|
|
}
|