mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-02 08:04:32 +08:00
A repo-wide security review found issues in the root CFW install, the Launchpad helper, VM bundle handling and the guest boundary. This fixes them and applies swift format. - cfw install mounts guest volumes nosuid,nodev,nobrowse in a root-only temp folder and does every guest read and write through an open folder handle, never following links. BuildManifest cryptex paths must stay in the restore folder, and only a private copy is attached. - Root no longer chowns or chmods the whole VM folder after an install. The shared walk skips hard links, symlinks, special files and other volumes. - Every Launchpad helper action needs administrator authorization. Only the user who started a CFW install can cancel it. The helper refuses setuid, hard-linked, special or escaping-symlink entries in a bundle. - Manifest file names must be single names in the bundle and point to regular files. vm import refuses links that leave the bundle. - Guest file names from the file browser, QuickLook, drag-out and crash logs are validated and written exclusively, without overwriting, and are quarantined. - The guest HTTP client no longer traps on a bare Content-Length, caps bodies and enforces a per-request deadline. - The --api-listen proxy needs a per-launch token. vphoned refuses browser-origin and non-local Host requests. - vphoned stops following links when it sets up Irisin and the camera files. Thanks to fresh-fx59 for reporting the guest file name, HTTP parsing, cfw install and manifest path issues in #469. Co-authored-by: Aleksey Aksenov <5788874+fresh-fx59@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
396 lines
12 KiB
Objective-C
396 lines
12 KiB
Objective-C
/*
|
|
* vphoned_vcam — vsock 1338 -> shared mmap frame publisher.
|
|
*
|
|
* Wire protocol (matches host VPhoneCameraServer.swift):
|
|
* uint32 LE total_payload_length
|
|
* uint32 LE header_json_length
|
|
* bytes JSON header { w, h, bpr, fmt, ts }
|
|
* bytes raw pixel data (width*height aligned by bpr)
|
|
*
|
|
* On each successful receive, the frame is written into the shm file
|
|
* with a seq-counter discipline and a notify_post() fires so any
|
|
* libvcamcaptured-mapped reader can pick it up immediately.
|
|
*/
|
|
|
|
#import "vphoned_vcam.h"
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <notify.h>
|
|
#include <pthread.h>
|
|
#include <pwd.h>
|
|
#include <stdarg.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/vsock.h>
|
|
#include <unistd.h>
|
|
|
|
static pthread_once_t s_start_once = PTHREAD_ONCE_INIT;
|
|
static uint8_t *s_shm_base = NULL;
|
|
static int s_notify_token = -1;
|
|
/* Opened once, relative to the camera directory, by open_shm(). Until then
|
|
* log lines go to NSLog. */
|
|
static _Atomic int s_log_fd = -1;
|
|
|
|
__attribute__((format(printf, 1, 2)))
|
|
static void vvc_logf(const char *fmt, ...) {
|
|
char message[1024];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(message, sizeof(message) - 1, fmt, ap);
|
|
va_end(ap);
|
|
int fd = atomic_load(&s_log_fd);
|
|
if (fd < 0) {
|
|
NSLog(@"%s", message);
|
|
return;
|
|
}
|
|
size_t length = strlen(message);
|
|
message[length++] = '\n';
|
|
/* One write per line keeps O_APPEND lines whole across threads. */
|
|
(void)write(fd, message, length);
|
|
}
|
|
|
|
// MARK: - Root-owned files in a mobile-owned directory
|
|
|
|
static bool is_private_root_file(int fd) {
|
|
struct stat info;
|
|
return fstat(fd, &info) == 0 && S_ISREG(info.st_mode) && info.st_uid == 0 &&
|
|
info.st_nlink == 1;
|
|
}
|
|
|
|
/* mobile owns the camera directory, so any entry in it may be a symlink, a
|
|
* hard link to a root-only file, a FIFO, or a file mobile created. Open it
|
|
* relative to the directory without following links, and accept only a
|
|
* regular root-owned file with a single name. Anything else is removed and
|
|
* created again exclusively. O_NONBLOCK keeps a FIFO from blocking the open. */
|
|
static int open_root_file(int directory_fd, const char *path, int access) {
|
|
const char *name = strrchr(path, '/') ? strrchr(path, '/') + 1 : path;
|
|
int flags = access | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK;
|
|
int fd = openat(directory_fd, name, flags | O_CREAT, 0644);
|
|
if (fd < 0 || !is_private_root_file(fd)) {
|
|
if (fd >= 0) close(fd);
|
|
if (unlinkat(directory_fd, name, 0) < 0 && errno != ENOENT) return -1;
|
|
fd = openat(directory_fd, name, flags | O_CREAT | O_EXCL, 0644);
|
|
if (fd < 0) return -1;
|
|
if (!is_private_root_file(fd)) {
|
|
close(fd);
|
|
errno = EPERM;
|
|
return -1;
|
|
}
|
|
}
|
|
int status = fcntl(fd, F_GETFL);
|
|
if (status < 0 || fcntl(fd, F_SETFL, status & ~O_NONBLOCK) < 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
static void open_log(int directory_fd) {
|
|
if (atomic_load(&s_log_fd) >= 0) return;
|
|
int fd = open_root_file(directory_fd, VPHONE_VCAM_DAEMON_LOG_PATH, O_WRONLY | O_APPEND);
|
|
if (fd < 0) {
|
|
vvc_logf("vphoned_vcam: open log failed: %s", strerror(errno));
|
|
return;
|
|
}
|
|
fchmod(fd, 0644);
|
|
int expected = -1;
|
|
if (!atomic_compare_exchange_strong(&s_log_fd, &expected, fd)) close(fd);
|
|
}
|
|
|
|
// MARK: - Shared frame memory
|
|
|
|
/* Opens the camera directory without following a link in its last two
|
|
* components. /var/mobile belongs to mobile, so Media could be replaced by a
|
|
* symlink; the parent is opened with O_NOFOLLOW and the leaf with openat. */
|
|
static int open_camera_directory(uid_t owner, gid_t group) {
|
|
NSString *directory = [NSString stringWithUTF8String:VPHONE_VCAM_DIRECTORY];
|
|
NSString *parent = directory.stringByDeletingLastPathComponent;
|
|
const char *leaf = directory.lastPathComponent.fileSystemRepresentation;
|
|
int parent_fd = open(parent.fileSystemRepresentation, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
if (parent_fd < 0) {
|
|
vvc_logf("vphoned_vcam: open(%s) failed: %s", parent.UTF8String, strerror(errno));
|
|
return -1;
|
|
}
|
|
if (mkdirat(parent_fd, leaf, 0755) < 0 && errno != EEXIST) {
|
|
vvc_logf("vphoned_vcam: create(%s) failed: %s", directory.UTF8String, strerror(errno));
|
|
close(parent_fd);
|
|
return -1;
|
|
}
|
|
int directory_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
close(parent_fd);
|
|
if (directory_fd < 0) {
|
|
vvc_logf("vphoned_vcam: open directory failed: %s", strerror(errno));
|
|
return -1;
|
|
}
|
|
if (fchown(directory_fd, owner, group) < 0) {
|
|
vvc_logf("vphoned_vcam: chown directory failed: %s", strerror(errno));
|
|
close(directory_fd);
|
|
return -1;
|
|
}
|
|
if (fchmod(directory_fd, 0755) < 0) {
|
|
vvc_logf("vphoned_vcam: chmod directory failed: %s", strerror(errno));
|
|
close(directory_fd);
|
|
return -1;
|
|
}
|
|
return directory_fd;
|
|
}
|
|
|
|
static int open_shm(void) {
|
|
/* Wait for mobile's home before creating its media directory. */
|
|
struct passwd *mobile_user = getpwnam("mobile");
|
|
struct stat mobile_home;
|
|
if (!mobile_user || stat("/var/mobile", &mobile_home) < 0 ||
|
|
!S_ISDIR(mobile_home.st_mode) || mobile_home.st_uid != mobile_user->pw_uid) {
|
|
vvc_logf("vphoned_vcam: mobile home is not ready");
|
|
return -1;
|
|
}
|
|
int directory_fd = open_camera_directory(mobile_user->pw_uid, mobile_user->pw_gid);
|
|
if (directory_fd < 0) return -1;
|
|
open_log(directory_fd);
|
|
/* Truncate to total size each fresh open so a stale half-written file
|
|
* from a previous boot doesn't confuse readers. */
|
|
int fd = open_root_file(directory_fd, VPHONE_VCAM_SHM_PATH, O_RDWR);
|
|
close(directory_fd);
|
|
if (fd < 0) {
|
|
vvc_logf("vphoned_vcam: open(%s) failed: %s",
|
|
VPHONE_VCAM_SHM_PATH,
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
if (ftruncate(fd, VPHONE_VCAM_SHM_TOTAL_SIZE) < 0) {
|
|
vvc_logf("vphoned_vcam: ftruncate failed: %s", strerror(errno));
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
/* Make sure other processes can map this file read-only. */
|
|
fchmod(fd, 0644);
|
|
void *base = mmap(
|
|
NULL,
|
|
VPHONE_VCAM_SHM_TOTAL_SIZE,
|
|
PROT_READ | PROT_WRITE,
|
|
MAP_SHARED,
|
|
fd,
|
|
0);
|
|
close(fd);
|
|
if (base == MAP_FAILED) {
|
|
vvc_logf("vphoned_vcam: mmap failed: %s", strerror(errno));
|
|
return -1;
|
|
}
|
|
/* Zero the header on first init so seq starts at 0. */
|
|
memset(base, 0, VPHONE_VCAM_SHM_HEADER_SIZE);
|
|
s_shm_base = (uint8_t *)base;
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t read_full(int fd, void *buf, size_t n) {
|
|
uint8_t *p = (uint8_t *)buf;
|
|
size_t got = 0;
|
|
while (got < n) {
|
|
ssize_t r = read(fd, p + got, n - got);
|
|
if (r > 0) { got += (size_t)r; continue; }
|
|
if (r == 0) return 0;
|
|
if (errno == EINTR) continue;
|
|
return -1;
|
|
}
|
|
return (ssize_t)got;
|
|
}
|
|
|
|
static void publish_frame(uint32_t w,
|
|
uint32_t h,
|
|
uint32_t bpr,
|
|
uint32_t fmt,
|
|
uint64_t ts_ns,
|
|
const uint8_t *pixels,
|
|
size_t pixel_len) {
|
|
if (!s_shm_base) return;
|
|
if (pixel_len > VPHONE_VCAM_SHM_MAX_PIXELS) {
|
|
vvc_logf("vphoned_vcam: frame too large: %zu", pixel_len);
|
|
return;
|
|
}
|
|
vphone_vcam_shm_header_t *hdr = (vphone_vcam_shm_header_t *)s_shm_base;
|
|
uint8_t *dst = s_shm_base + VPHONE_VCAM_SHM_HEADER_SIZE;
|
|
|
|
uint64_t prev_seq = atomic_load_explicit(
|
|
(_Atomic uint64_t *)&hdr->seq, memory_order_acquire);
|
|
/* Mark write in progress (odd seq). */
|
|
uint64_t writing_seq = (prev_seq | 1ull) + 2ull;
|
|
atomic_store_explicit(
|
|
(_Atomic uint64_t *)&hdr->seq,
|
|
writing_seq,
|
|
memory_order_release);
|
|
|
|
hdr->width = w;
|
|
hdr->height = h;
|
|
hdr->bytes_per_row = bpr;
|
|
hdr->pixel_format = fmt;
|
|
hdr->timestamp_ns = ts_ns;
|
|
hdr->frame_index += 1;
|
|
hdr->pixels_length = (uint32_t)pixel_len;
|
|
memcpy(dst, pixels, pixel_len);
|
|
|
|
/* Mark write done (even seq). */
|
|
atomic_store_explicit(
|
|
(_Atomic uint64_t *)&hdr->seq,
|
|
writing_seq + 1ull,
|
|
memory_order_release);
|
|
|
|
if (s_notify_token >= 0) {
|
|
notify_post(VPHONE_VCAM_NOTIFY_NAME);
|
|
}
|
|
}
|
|
|
|
static void handle_client(int fd) {
|
|
vvc_logf("vphoned_vcam: client connected fd=%d", fd);
|
|
uint64_t frames = 0;
|
|
for (;;) {
|
|
uint32_t total_len = 0, header_len = 0;
|
|
if (read_full(fd, &total_len, 4) <= 0) break;
|
|
if (read_full(fd, &header_len, 4) <= 0) break;
|
|
if (total_len < 4 || header_len > total_len - 4 ||
|
|
total_len > VPHONE_VCAM_SHM_MAX_PIXELS + 4096) {
|
|
vvc_logf("vphoned_vcam: framing error total=%u header=%u",
|
|
total_len,
|
|
header_len);
|
|
break;
|
|
}
|
|
uint8_t *header_buf = (uint8_t *)malloc(header_len);
|
|
if (!header_buf) break;
|
|
if (read_full(fd, header_buf, header_len) <= 0) {
|
|
free(header_buf);
|
|
break;
|
|
}
|
|
size_t pixel_len = (size_t)total_len - 4 - header_len;
|
|
uint8_t *pixel_buf = (uint8_t *)malloc(pixel_len);
|
|
if (!pixel_buf) { free(header_buf); break; }
|
|
if (read_full(fd, pixel_buf, pixel_len) <= 0) {
|
|
free(header_buf);
|
|
free(pixel_buf);
|
|
break;
|
|
}
|
|
NSData *hd = [NSData dataWithBytesNoCopy:header_buf
|
|
length:header_len
|
|
freeWhenDone:NO];
|
|
NSError *jerr = nil;
|
|
NSDictionary *hdict = [NSJSONSerialization JSONObjectWithData:hd
|
|
options:0
|
|
error:&jerr];
|
|
uint32_t w = (uint32_t)[hdict[@"w"] unsignedIntValue];
|
|
uint32_t h = (uint32_t)[hdict[@"h"] unsignedIntValue];
|
|
uint32_t bpr = (uint32_t)[hdict[@"bpr"] unsignedIntValue];
|
|
uint32_t fmt = (uint32_t)[hdict[@"fmt"] unsignedIntValue];
|
|
uint64_t ts = (uint64_t)[hdict[@"ts"] unsignedLongLongValue];
|
|
free(header_buf);
|
|
|
|
if (!hdict || jerr || w == 0 || h == 0 || bpr == 0 ||
|
|
pixel_len < (size_t)bpr * h) {
|
|
vvc_logf("vphoned_vcam: invalid frame w=%u h=%u bpr=%u pixel_len=%zu jerr=%s",
|
|
w,
|
|
h,
|
|
bpr,
|
|
pixel_len,
|
|
jerr ? jerr.localizedDescription.UTF8String : "(none)");
|
|
free(pixel_buf);
|
|
break;
|
|
}
|
|
publish_frame(w, h, bpr, fmt, ts, pixel_buf, pixel_len);
|
|
free(pixel_buf);
|
|
|
|
frames++;
|
|
if ((frames & 29) == 1) {
|
|
vvc_logf("vphoned_vcam: published frame #%llu w=%u h=%u bpr=%u",
|
|
(unsigned long long)frames,
|
|
w,
|
|
h,
|
|
bpr);
|
|
}
|
|
}
|
|
vvc_logf("vphoned_vcam: client disconnected (%llu frames)",
|
|
(unsigned long long)frames);
|
|
close(fd);
|
|
}
|
|
|
|
static void *listener_thread(__unused void *unused) {
|
|
for (;;) {
|
|
int ready;
|
|
@autoreleasepool {
|
|
ready = open_shm();
|
|
}
|
|
if (ready == 0) break;
|
|
sleep(3);
|
|
}
|
|
|
|
/* Register the notify name so notify_post() actually delivers. */
|
|
if (notify_register_check(VPHONE_VCAM_NOTIFY_NAME,
|
|
&s_notify_token) != NOTIFY_STATUS_OK) {
|
|
s_notify_token = -1;
|
|
}
|
|
|
|
int srv = socket(AF_VSOCK, SOCK_STREAM, 0);
|
|
if (srv < 0) {
|
|
vvc_logf("vphoned_vcam: socket(AF_VSOCK) failed: %s", strerror(errno));
|
|
return NULL;
|
|
}
|
|
int one = 1;
|
|
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
|
|
|
struct sockaddr_vm addr = {
|
|
.svm_len = sizeof(addr),
|
|
.svm_family = AF_VSOCK,
|
|
.svm_port = VPHONED_VCAM_VSOCK_PORT,
|
|
.svm_cid = VMADDR_CID_ANY,
|
|
};
|
|
if (bind(srv, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
|
vvc_logf("vphoned_vcam: bind(%d) failed: %s",
|
|
VPHONED_VCAM_VSOCK_PORT,
|
|
strerror(errno));
|
|
close(srv);
|
|
return NULL;
|
|
}
|
|
if (listen(srv, 2) < 0) {
|
|
vvc_logf("vphoned_vcam: listen failed: %s", strerror(errno));
|
|
close(srv);
|
|
return NULL;
|
|
}
|
|
vvc_logf("vphoned_vcam: listening on vsock %d, shm=%s",
|
|
VPHONED_VCAM_VSOCK_PORT,
|
|
VPHONE_VCAM_SHM_PATH);
|
|
|
|
for (;;) {
|
|
int fd = accept(srv, NULL, NULL);
|
|
if (fd < 0) {
|
|
if (errno == EINTR) continue;
|
|
vvc_logf("vphoned_vcam: accept failed: %s", strerror(errno));
|
|
sleep(1);
|
|
continue;
|
|
}
|
|
@autoreleasepool {
|
|
handle_client(fd);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void start_listener_once(void) {
|
|
pthread_t thr;
|
|
pthread_attr_t attr;
|
|
pthread_attr_init(&attr);
|
|
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
|
int rc = pthread_create(&thr, &attr, listener_thread, NULL);
|
|
pthread_attr_destroy(&attr);
|
|
if (rc != 0) {
|
|
vvc_logf("vphoned_vcam: pthread_create failed: %d", rc);
|
|
}
|
|
}
|
|
|
|
void vp_vcam_start(void) {
|
|
vvc_logf("vphoned_vcam: vp_vcam_start called (pid=%d)", getpid());
|
|
pthread_once(&s_start_once, start_listener_once);
|
|
}
|