Merge vphoned spawn proxy

This commit is contained in:
Lakr
2026-09-25 14:44:53 +09:00
5 changed files with 228 additions and 17 deletions
+17 -2
View File
@@ -220,8 +220,23 @@ message.
## Connection failure behavior
A dropped HTTP or WebSocket connection closes only that request channel. The
guest launchd plist keeps vphoned alive and restarts it if the daemon itself
exits. Host socket writes use `F_SETNOSIGPIPE`, so a guest disconnect becomes
guest launchd plist starts a small vphoned proxy. It uses `posix_spawn` to
start the same signed executable with `--io`, then waits for and reaps that
worker. The worker owns VSOCK 1338 and 1339 and all API state. The proxy
restarts an unexpectedly exited worker with bounded backoff, and forwards
shutdown to it. A pipe makes the worker exit if launchd kills the proxy, so
the old worker cannot retain the ports after launchd starts a replacement.
The proxy never initializes NIO, IcliKit, or the camera server under its
6 MB per-process Jetsam limit. A successful `agent.apply_update` worker exit
makes the proxy exit so launchd can restart the updated cached binary. If a
cached worker fails before binding, the bundled-binary fallback remains in
effect. On a 26.6.2 VM, the proxy's physical footprint stayed near 1.4 MB
through 30 health requests and six app listings; the worker served those
requests without a PID change. Killing the proxy caused the worker to leave
and launchd to start one new proxy/worker pair. The worker's Jetsam snapshot
reported no per-process limit.
Host socket writes use `F_SETNOSIGPIPE`, so a guest disconnect becomes
an ordinary error instead of terminating `vphone-vm`. The host HTTP client
also times out stalled reads and writes. Camera frames use a duplicated
descriptor for each in-flight send; the original descriptor remains owned by
+7 -1
View File
@@ -5,7 +5,13 @@ import NIOPosix
import NIOWebSocket
import VphonedNative
vp_native_bootstrap_cached_binary()
let mode = vp_native_process_mode()
if mode != 1 {
guard mode == 0 else { exit(64) }
vp_native_bootstrap_cached_binary()
exit(vp_native_run_proxy())
}
guard vp_native_watch_proxy() == 0 else { exit(1) }
vp_vcam_start()
GuestIrisinInstaller.refreshBootstrapOnStartup()
@@ -1,10 +1,18 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __OBJC__
#import <Foundation/Foundation.h>
#endif
/// Sign all executable code in an extracted app. Returns a malloc-owned error or NULL.
char *vp_sign_app_for_install(const char *appPath, const char *certificatePath);
void vp_native_bootstrap_cached_binary(void);
void vp_native_confirm_cached_binary(void);
/// 0 = launchd proxy, 1 = --io worker, -1 = invalid arguments.
int vp_native_process_mode(void);
int vp_native_run_proxy(void);
int vp_native_watch_proxy(void);
void vp_vcam_start(void);
typedef struct {
+42 -14
View File
@@ -1,31 +1,59 @@
#import "Include/VphonedNative.h"
#import <CommonCrypto/CommonDigest.h>
#include <CommonCrypto/CommonDigest.h>
#include <errno.h>
#include <mach-o/dyld.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static const char *cache = "/var/root/Library/Caches/vphoned";
static const char *marker = "/var/root/Library/Caches/vphoned.api-v2";
static const char *pending = "/var/root/Library/Caches/vphoned.api-v2.pending";
void vp_native_bootstrap_cached_binary(void) {
// A cached update gets one attempt to bind. If it fails, launchd restarts
// the bundled daemon, which remains the fallback.
if (access(cache, X_OK) != 0 || access(marker, R_OK) != 0) return;
NSData *binary = [NSData dataWithContentsOfFile:@(cache) options:NSDataReadingMappedIfSafe error:nil];
NSString *expected = [NSString stringWithContentsOfFile:@(marker) encoding:NSUTF8StringEncoding error:nil];
if (!binary || expected.length != CC_SHA256_DIGEST_LENGTH * 2) return;
static bool cached_binary_matches_marker(void) {
FILE *image = fopen(cache, "rb");
if (!image) return false;
CC_SHA256_CTX context;
CC_SHA256_Init(&context);
unsigned char buffer[64 * 1024];
size_t count;
while ((count = fread(buffer, 1, sizeof(buffer), image)) > 0)
CC_SHA256_Update(&context, buffer, (CC_LONG)count);
bool complete = !ferror(image);
fclose(image);
if (!complete) return false;
FILE *record = fopen(marker, "rb");
if (!record) return false;
char expected[65];
count = fread(expected, 1, sizeof(expected), record);
fclose(record);
if (count != 64) return false;
expected[64] = '\0';
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(binary.bytes, (CC_LONG)binary.length, digest);
NSMutableString *actual = [NSMutableString string];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) [actual appendFormat:@"%02x", digest[i]];
if (![actual isEqualToString:expected]) return;
CC_SHA256_Final(digest, &context);
static const char hex[] = "0123456789abcdef";
char actual[65];
for (int index = 0; index < CC_SHA256_DIGEST_LENGTH; index++) {
actual[index * 2] = hex[digest[index] >> 4];
actual[index * 2 + 1] = hex[digest[index] & 15];
}
actual[64] = '\0';
return strcmp(actual, expected) == 0;
}
void vp_native_bootstrap_cached_binary(void) {
// Keep the launchd process small: hash the cache in bounded chunks.
if (access(cache, X_OK) != 0 || !cached_binary_matches_marker()) return;
char current[4096];
uint32_t size = sizeof(current);
if (_NSGetExecutablePath(current, &size) != 0 || strcmp(current, cache) == 0) return;
if (rename(marker, pending) != 0) return;
char *const arguments[] = {(char *)cache, NULL};
execv(cache, arguments);
NSLog(@"vphoned: cached binary launch failed: %s", strerror(errno));
fprintf(stderr, "vphoned proxy: cached binary exec failed: %s\n", strerror(errno));
_exit(1);
}
void vp_native_confirm_cached_binary(void) {
@@ -33,5 +61,5 @@ void vp_native_confirm_cached_binary(void) {
uint32_t size = sizeof(current);
if (_NSGetExecutablePath(current, &size) != 0 || strcmp(current, cache) != 0) return;
if (rename(pending, marker) != 0)
NSLog(@"vphoned: could not confirm cached binary: %s", strerror(errno));
fprintf(stderr, "vphoned: could not confirm cached binary: %s\n", strerror(errno));
}
+154
View File
@@ -0,0 +1,154 @@
#include "Include/VphonedNative.h"
#include <crt_externs.h>
#include <errno.h>
#include <limits.h>
#include <mach-o/dyld.h>
#include <pthread.h>
#include <signal.h>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
extern char **environ;
static volatile sig_atomic_t stopping = 0;
static const char *pending_update = "/var/root/Library/Caches/vphoned.api-v2.pending";
static void request_stop(int signal_number) {
stopping = signal_number;
}
static int worker_fd(void) {
int argc = *_NSGetArgc();
char **argv = *_NSGetArgv();
if (argc != 3 || strcmp(argv[1], "--io") != 0) return -1;
char *end = NULL;
long value = strtol(argv[2], &end, 10);
if (!end || *end != '\0' || value < 3 || value > INT_MAX) return -1;
return (int)value;
}
int vp_native_process_mode(void) {
int argc = *_NSGetArgc();
if (argc == 1) return 0;
return worker_fd() >= 0 ? 1 : -1;
}
static void *watch_proxy(void *context) {
int fd = (int)(intptr_t)context;
char byte;
while (read(fd, &byte, 1) < 0 && errno == EINTR) {}
// Only the proxy owns the write end. Its death must not leave an
// unaccounted worker holding the VSOCK ports open.
_exit(0);
}
int vp_native_watch_proxy(void) {
int fd = worker_fd();
if (fd < 0) return -1;
pthread_attr_t attributes;
if (pthread_attr_init(&attributes) != 0) return -1;
int result = pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED);
if (result == 0) result = pthread_attr_setstacksize(&attributes, 64 * 1024);
pthread_t thread;
if (result == 0) result = pthread_create(&thread, &attributes, watch_proxy, (void *)(intptr_t)fd);
pthread_attr_destroy(&attributes);
return result == 0 ? 0 : -1;
}
static void pause_before_retry(unsigned seconds) {
struct timespec remaining = {.tv_sec = seconds, .tv_nsec = 0};
while (!stopping && nanosleep(&remaining, &remaining) < 0 && errno == EINTR) {}
}
static void stop_worker(pid_t pid, int write_fd) {
close(write_fd);
kill(pid, SIGTERM);
for (int tries = 0; tries < 30; tries++) {
pid_t waited = waitpid(pid, NULL, WNOHANG);
if (waited == pid || (waited < 0 && errno == ECHILD)) return;
struct timespec delay = {.tv_sec = 0, .tv_nsec = 100000000};
nanosleep(&delay, NULL);
}
kill(pid, SIGKILL);
while (waitpid(pid, NULL, 0) < 0 && errno == EINTR) {}
}
int vp_native_run_proxy(void) {
char executable[4096];
uint32_t size = sizeof(executable);
if (_NSGetExecutablePath(executable, &size) != 0) return 1;
struct sigaction action = {.sa_handler = request_stop};
sigemptyset(&action.sa_mask);
sigaction(SIGTERM, &action, NULL);
sigaction(SIGINT, &action, NULL);
unsigned retry_delay = 1;
while (!stopping) {
int pipe_fds[2];
if (pipe(pipe_fds) != 0) return 1;
posix_spawn_file_actions_t file_actions;
int error = posix_spawn_file_actions_init(&file_actions);
if (error != 0) {
close(pipe_fds[0]);
close(pipe_fds[1]);
return 1;
}
error = posix_spawn_file_actions_addclose(&file_actions, pipe_fds[1]);
if (error != 0) {
posix_spawn_file_actions_destroy(&file_actions);
close(pipe_fds[0]);
close(pipe_fds[1]);
return 1;
}
char fd_argument[24];
snprintf(fd_argument, sizeof(fd_argument), "%d", pipe_fds[0]);
char *arguments[] = {executable, "--io", fd_argument, NULL};
pid_t child = -1;
error = posix_spawn(&child, executable, &file_actions, NULL, arguments, environ);
posix_spawn_file_actions_destroy(&file_actions);
close(pipe_fds[0]);
if (error != 0) {
fprintf(stderr, "vphoned proxy: posix_spawn failed: %s\n", strerror(error));
close(pipe_fds[1]);
pause_before_retry(retry_delay);
} else {
if (stopping) {
stop_worker(child, pipe_fds[1]);
break;
}
int status = 0;
pid_t waited;
do {
waited = waitpid(child, &status, 0);
} while (waited < 0 && errno == EINTR && !stopping);
if (stopping) {
if (waited == child) close(pipe_fds[1]);
else stop_worker(child, pipe_fds[1]);
break;
}
close(pipe_fds[1]);
if (waited == child && WIFEXITED(status) && WEXITSTATUS(status) == 0) {
// agent.apply_update exits the worker after installing the
// cached binary. launchd restarts us into that new image.
return 0;
}
if (access(pending_update, F_OK) == 0) {
// A cached worker failed before binding. The next launchd
// start falls back to the bundled binary.
return 1;
}
fprintf(stderr, "vphoned proxy: worker exited; retrying\n");
pause_before_retry(retry_delay);
}
if (retry_delay < 10) retry_delay *= 2;
}
return 0;
}