Files
Snapintosh/tools/test-console-link.c
Acelogic 1510d8c6ce Snapintosh public release
An EL2 monitor/hypervisor and toolchain that boots macOS 27 on the ASUS A16
(Snapdragon X2 Elite), with an Adreno Metal plug-in and IOGPU kext, and
air2ir3, an AIR to Mesa ir3 shader compiler.

MIT, with GPL-2.0 components listed in LICENSE and THIRD_PARTY_NOTICES.md.
No Apple, Qualcomm, ASUS or Microsoft binaries are included.
2026-10-21 21:26:36 -04:00

298 lines
14 KiB
C

/* SPDX-License-Identifier: MIT */
/* Offline stand-in for the two-way console (docs/GUEST-SHELL-DESIGN-20261016.md):
* q1n1's real console and stop handlers (../q1n1/platform/uefi/call-service.c)
* behind a localhost UDP socket, served only during polls the way the monitor
* polls q1n1, with the monitor's real console link (compat/console-link.h) and
* DockChannel receive model (shadow/dockchannel.c) behind them. The "guest"
* reads the UART exactly as XNU's serial_keyboard_poll does (RX_COUNT, then
* RX_8, every 16 ms) and runs XNU's "Automatic NMI" matcher over what it gets.
* Optionally a real shell sits on a pty standing in for /dev/console: what the
* guest receives is typed into it, and what it prints is the console.
*
* Not a unit test of these pieces: tools/test-console-link.py runs
* tools/guest-console.py against this, with loss, duplication and reordering
* injected here, and compares the streams byte for byte. One reply leaves per
* poll, as q1n1's side path keeps one frame in flight; later ones are dropped.
*
* clang -std=gnu11 -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all \
* -Wall -Wextra -Werror -I../q1n1/platform/uefi tools/test-console-link.c \
* shadow/dockchannel.c ../q1n1/platform/uefi/call-service.c -o build/test-console-link
*
* Usage: test-console-link --session HEX [--port N] [--poll-ms N] [--duration S]
* [--linger S] [--noise FILE --noise-rate BYTES [--quiet S] [--noise-lines]] [--shell COMMAND]
* [--loss P] [--reply-loss P] [--dup P] [--reorder P] [--seed N]
* [--produced FILE] [--delivered FILE] [--result FILE]
* --noise-lines adds noise only as whole lines and only after a newline, so
* it never splits a line of the shell's. --quiet stops the noise S seconds
* before the end, so a tool can catch up
* before the call ends. Prints "port N" once it is listening. */
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <util.h>
#include "call-service.h"
#include "../compat/console-link.h"
#define RING 65536u /* CONSOLE_TAIL_MAX */
#define GUEST_POLL_NS 16000000ull /* serial_keyboard_poll's 16 ms */
#define REPLY_CAPACITY 1440u /* NCM_SIDE_REPLY_MAX */
_Static_assert(sizeof(struct console_link_view) == sizeof(struct q1n1_console_view),
"monitor and q1n1 views agree");
_Static_assert(CONSOLE_LINK_VIEW_MAGIC == Q1N1_CONSOLE_VIEW_MAGIC, "view magic");
_Static_assert(CONSOLE_LINK_STAGING >= Q1N1_CONSOLE_IN_MAX, "one request's input fits staging");
static struct {
uint64_t requests, dropped_requests, dropped_replies, duplicated, reordered;
uint64_t one_in_flight, guest_reads, guest_bytes, nmi_entries, shell_bytes, noise_bytes;
} h;
static double now_s(void)
{
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec + t.tv_nsec / 1e9;
}
static uint64_t now_ns(void) { return (uint64_t)(now_s() * 1e9); }
static int chance(double p) { return p > 0 && (double)random() / 2147483647.0 < p; }
static uint8_t ring[RING];
static uint64_t produced;
static FILE *produced_file, *delivered_file;
static void console_put(uint8_t byte)
{
ring[produced % RING] = byte;
produced++;
if (produced_file) fputc(byte, produced_file);
}
int main(int argc, char **argv)
{
uint64_t session = 0;
unsigned port = 0, poll_ms = 20, noise_rate = 0, seed = 1, noise_lines = 0;
double duration = 10, linger = 1, quiet = 0, loss = 0, reply_loss = 0, dup = 0, reorder = 0;
const char *noise_path = NULL, *shell = NULL, *result_path = NULL;
for (int n = 1; n < argc; n++) {
const char *a = argv[n], *v = n + 1 < argc ? argv[n + 1] : NULL;
if (!strcmp(a, "--noise-lines")) { noise_lines = 1; continue; }
if (!v) { fprintf(stderr, "%s needs a value\n", a); return 1; }
n++;
if (!strcmp(a, "--session")) session = strtoull(v, NULL, 16);
else if (!strcmp(a, "--port")) port = (unsigned)atoi(v);
else if (!strcmp(a, "--poll-ms")) poll_ms = (unsigned)atoi(v);
else if (!strcmp(a, "--duration")) duration = atof(v);
else if (!strcmp(a, "--linger")) linger = atof(v);
else if (!strcmp(a, "--quiet")) quiet = atof(v);
else if (!strcmp(a, "--noise")) noise_path = v;
else if (!strcmp(a, "--noise-rate")) noise_rate = (unsigned)atoi(v);
else if (!strcmp(a, "--shell")) shell = v;
else if (!strcmp(a, "--loss")) loss = atof(v);
else if (!strcmp(a, "--reply-loss")) reply_loss = atof(v);
else if (!strcmp(a, "--dup")) dup = atof(v);
else if (!strcmp(a, "--reorder")) reorder = atof(v);
else if (!strcmp(a, "--seed")) seed = (unsigned)atoi(v);
else if (!strcmp(a, "--produced")) produced_file = fopen(v, "wb");
else if (!strcmp(a, "--delivered")) delivered_file = fopen(v, "wb");
else if (!strcmp(a, "--result")) result_path = v;
else { fprintf(stderr, "unknown %s\n", a); return 1; }
}
if (!session || !poll_ms) { fprintf(stderr, "--session HEX (nonzero) and --poll-ms\n"); return 1; }
srandom(seed);
FILE *noise = noise_path ? fopen(noise_path, "rb") : NULL;
if (noise_path && !noise) { perror(noise_path); return 1; }
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in me = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port),
.sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
socklen_t me_len = sizeof me;
if (sock < 0 || bind(sock, (struct sockaddr *)&me, sizeof me) ||
getsockname(sock, (struct sockaddr *)&me, &me_len) || fcntl(sock, F_SETFL, O_NONBLOCK)) {
perror("socket");
return 1;
}
int master = -1;
pid_t child = -1;
if (shell) {
child = forkpty(&master, NULL, NULL, NULL);
if (child < 0) { perror("forkpty"); return 1; }
if (!child) {
setenv("TERM", "vt100", 1);
execl("/bin/sh", "sh", "-c", shell, (char *)NULL);
_exit(127);
}
fcntl(master, F_SETFL, O_NONBLOCK);
}
printf("port %u\n", ntohs(me.sin_port));
fflush(stdout);
static struct q1n1_call_state state;
static struct console_link link;
static struct dockchannel uart;
q1n1_call_init(&state);
q1n1_call_arm(&state, session);
static uint8_t held[REPLY_CAPACITY];
uint32_t held_length = 0;
struct sockaddr_in held_to;
uint64_t nmi_match = 0;
double start = now_s(), next_poll = start;
uint64_t next_guest = now_ns();
int armed = 1;
for (;;) {
double t = now_s();
if (armed && t - start > duration) {
q1n1_call_disarm(&state);
armed = 0;
}
if (!armed && t - start > duration + linger) break;
/* The shell's output is console output, as the guest's TX is. */
if (master >= 0) {
uint8_t buffer[4096];
ssize_t got;
while ((got = read(master, buffer, sizeof buffer)) > 0)
for (ssize_t n = 0; n < got; n++) { console_put(buffer[n]); h.shell_bytes++; }
}
/* The guest: serial_keyboard_poll every 16 ms, a byte at a time. */
if (now_ns() >= next_guest) {
next_guest += GUEST_POLL_NS;
for (;;) {
uint64_t count = 0, data = 0;
h.guest_reads++;
if (dc_read_rx(&uart, armed ? &link.rx : NULL, DC_WINDOW_DATA,
DC_BLOCK + DC_DATA_RX_COUNT, &count) != DC_OK) return 2;
if (!(count & 0x7f)) break;
h.guest_reads++;
if (dc_read_rx(&uart, &link.rx, DC_WINDOW_DATA, DC_BLOCK + DC_DATA_RX8, &data) != DC_OK)
return 2;
uint8_t c = (uint8_t)(data >> 8);
if ((data & 0xff) != 1) return 3;
h.guest_bytes++;
/* XNU's own matcher: advance on a match, zero on anything else. */
nmi_match = c == (uint8_t)CONSOLE_LINK_NMI[nmi_match] ? nmi_match + 1 : 0;
if (nmi_match == CONSOLE_LINK_NMI_LENGTH) { h.nmi_entries++; nmi_match = 0; }
if (delivered_file) fputc(c, delivered_file);
if (master >= 0) { ssize_t w = write(master, &c, 1); (void)w; }
}
}
if (t < next_poll) {
usleep(1000);
continue;
}
next_poll += poll_ms / 1000.0;
/* Kernel and launchd logging, at a fixed rate per poll. */
if (noise && armed && t - start < duration - quiet &&
(!noise_lines || !produced || ring[(produced - 1) % RING] == '\n'))
for (unsigned n = 0; n < noise_rate || noise_lines; n++) {
int c = fgetc(noise);
if (c == EOF) break;
console_put((uint8_t)c);
h.noise_bytes++;
if (noise_lines && c == '\n' && n + 1 >= noise_rate) break;
}
/* One POLL, as main.c runs it: the view only for its duration. */
struct q1n1_console_view *view = NULL;
if (armed) {
console_link_prepare(&link, ring, RING, produced);
view = (struct q1n1_console_view *)&link.view;
}
if (state.polls != UINT64_MAX) state.polls++;
if (view && view->magic == Q1N1_CONSOLE_VIEW_MAGIC) view->polls_seen++;
int sent = 0;
uint64_t before = now_ns();
for (;;) {
uint8_t datagram[2048], reply[REPLY_CAPACITY];
struct sockaddr_in from;
socklen_t from_len = sizeof from;
ssize_t got = recvfrom(sock, datagram, sizeof datagram, 0, (struct sockaddr *)&from, &from_len);
if (got < 0) break;
h.requests++;
if (chance(loss)) { h.dropped_requests++; continue; }
uint32_t n = q1n1_console_request(datagram, (uint32_t)got)
? q1n1_console_datagram(&state, view, datagram, (uint32_t)got, reply, sizeof reply)
: q1n1_stop_datagram(&state, datagram, (uint32_t)got, before, reply);
if (!n) continue;
if (sent) { h.one_in_flight++; continue; }
if (chance(reply_loss)) { h.dropped_replies++; sent = 1; continue; }
if (!held_length && chance(reorder)) {
memcpy(held, reply, n);
held_length = n;
held_to = from;
h.reordered++;
sent = 1;
continue;
}
sendto(sock, reply, n, 0, (struct sockaddr *)&from, from_len);
if (chance(dup)) { sendto(sock, reply, n, 0, (struct sockaddr *)&from, from_len); h.duplicated++; }
sent = 1;
if (held_length) {
/* The earlier reply, now behind a later one. */
sendto(sock, held, held_length, 0, (struct sockaddr *)&held_to, sizeof held_to);
held_length = 0;
}
}
if (armed) {
console_link_account(&link, now_ns() - before);
console_link_drain(&link);
}
}
if (child > 0) { kill(child, SIGKILL); waitpid(child, NULL, 0); }
if (produced_file) fclose(produced_file);
if (delivered_file) fclose(delivered_file);
FILE *out = result_path ? fopen(result_path, "w") : stdout;
if (!out) { perror(result_path); return 1; }
const struct console_link_view *v = &link.view;
fprintf(out, "{\n \"produced\": %llu, \"noise_bytes\": %llu, \"shell_bytes\": %llu,\n",
(unsigned long long)produced, (unsigned long long)h.noise_bytes,
(unsigned long long)h.shell_bytes);
fprintf(out, " \"guest_bytes\": %llu, \"guest_reads\": %llu, \"nmi_entries\": %llu,\n",
(unsigned long long)h.guest_bytes, (unsigned long long)h.guest_reads,
(unsigned long long)h.nmi_entries);
fprintf(out, " \"requests\": %llu, \"dropped_requests\": %llu, \"dropped_replies\": %llu, "
"\"duplicated\": %llu, \"reordered\": %llu, \"one_in_flight\": %llu,\n",
(unsigned long long)h.requests, (unsigned long long)h.dropped_requests,
(unsigned long long)h.dropped_replies, (unsigned long long)h.duplicated,
(unsigned long long)h.reordered, (unsigned long long)h.one_in_flight);
fprintf(out, " \"view\": {\"in_held\": %llu, \"owner_changes\": %llu, \"requests\": %llu, "
"\"refused\": %llu, \"in_duplicate\": %llu, \"in_gaps\": %llu, \"in_overflow\": %llu, "
"\"out_served\": %llu, \"out_gaps\": %llu, \"polls_seen\": %llu},\n",
(unsigned long long)v->in_held, (unsigned long long)v->owner_changes,
(unsigned long long)v->requests, (unsigned long long)v->refused,
(unsigned long long)v->in_duplicate, (unsigned long long)v->in_gaps,
(unsigned long long)v->in_overflow, (unsigned long long)v->out_served,
(unsigned long long)v->out_gaps, (unsigned long long)v->polls_seen);
fprintf(out, " \"link\": {\"polls\": %llu, \"staged\": %llu, \"delivered\": %llu, "
"\"nmi_dropped\": %llu, \"rx_refused\": %llu, \"rx_pushed\": %llu, \"rx_popped\": %llu, "
"\"rx_waiting\": %u},\n",
(unsigned long long)link.polls, (unsigned long long)link.staged,
(unsigned long long)link.delivered, (unsigned long long)link.nmi_dropped,
(unsigned long long)link.rx_refused, (unsigned long long)link.rx.pushed,
(unsigned long long)link.rx.popped, link.rx.count);
fprintf(out, " \"q1n1\": {\"polls\": %llu, \"datagrams\": %llu, \"malformed\": %llu, "
"\"refused\": %llu, \"last_status\": %lld}\n}\n",
(unsigned long long)state.polls, (unsigned long long)state.datagrams,
(unsigned long long)state.malformed, (unsigned long long)state.refused,
(long long)(int64_t)state.last_status);
if (out != stdout) fclose(out);
return 0;
}