#ifndef UTSM_IPC_PROTO_H #define UTSM_IPC_PROTO_H /* * UTSM ↔ Linux IPC Protocol — shared between both kernels. * * This header is included by: * - UTSM kernel (CODE/UTSM/vmm/ipc_shm.c, hypercall.c) * - Linux kernel module (CODE/linux/patches/utsm-ipc.patch → drivers/utsm/) * * Communication channels: * 1. Shared memory ring buffer (async messages, bidirectional) * 2. VMCALL hypercall (sync request/response) * 3. IPI notification (UTSM → Linux via guest IRQ injection) * * Shared memory layout (at GPA 0x04000000, 1MB total, packed struct): * 0x04000000: utsm_ipc_shm_header (64 bytes) * 0x04000040: utsm_ipc_ring utsm_to_linux (UTSM writes, Linux reads, 16400 B) * 0x04004050: utsm_ipc_ring linux_to_utsm (Linux writes, UTSM reads, 16400 B) * 0x04008060: payload pool (UTSM_IPC_PAYLOAD_SIZE bytes, ends exactly at +1MB) */ /* Kernel-space uses kernel types, user-space uses . * UTSM kernel defines __UTSM_KERNEL__, Linux kernel auto-defines __KERNEL__. */ #if defined(__KERNEL__) #include /* Linux kernel uses __u32/__u64/__u8; map to stdint names for shared structs */ typedef __u8 uint8_t; typedef __u32 uint32_t; typedef __u64 uint64_t; #elif defined(__UTSM_KERNEL__) #include #else #include #endif /* ===== Constants ===== */ #define UTSM_IPC_MAGIC 0x5554534D48430000ULL /* "UTSMHC\00\00" */ #define UTSM_IPC_VERSION 1 #define UTSM_IPC_SHM_GPA 0x04000000ULL #define UTSM_IPC_SHM_SIZE (1024 * 1024) /* 1MB */ #define UTSM_IPC_QUEUE_DEPTH 64 /* messages per ring */ #define UTSM_IPC_MSG_DATA_SIZE 240 /* inline data per message */ /* Derived region sizes (packed layout, both kernels must agree): * message = 16-byte header + inline data * ring = 16-byte header + QUEUE_DEPTH messages * SHM = 64-byte header + 2 rings + payload pool = exactly 1MB */ #define UTSM_IPC_MSG_SIZE (16 + UTSM_IPC_MSG_DATA_SIZE) /* 256 */ #define UTSM_IPC_RING_SIZE (16 + UTSM_IPC_QUEUE_DEPTH * UTSM_IPC_MSG_SIZE) /* 16400 */ #define UTSM_IPC_PAYLOAD_SIZE (UTSM_IPC_SHM_SIZE - 64 - 2 * UTSM_IPC_RING_SIZE) /* 1015712 */ /* ===== Hypercall operations ===== */ enum utsm_hcall_op { UTSM_HCALL_PING = 0x0001, /* Simple ping (returns PONG) */ UTSM_HCALL_HELLO = 0x0002, /* Hello handshake */ UTSM_HCALL_PARK = 0x0003, /* Park vCPU back to DSK (not HLT) */ UTSM_HCALL_SHM_INFO = 0x0010, /* Query shared memory GPA + size */ UTSM_HCALL_CAP_VALIDATE = 0x0020, /* Capability validation */ UTSM_HCALL_UTRW_READ = 0x0030, /* Sealed memory read (UTRW) */ UTSM_HCALL_UTRW_WRITE = 0x0031, /* Sealed memory write (UTRW) */ UTSM_HCALL_DRR_CHECKPOINT= 0x0040, /* Trigger DRR checkpoint */ UTSM_HCALL_CONSOLE_WRITE = 0x0050, /* Write to UTSM serial console */ UTSM_HCALL_CONSOLE_READ = 0x0051, /* Read from UTSM serial console */ /* Phase 3 file transfer: Linux guest ↔ UTSM FAT32 disk (payload_pool as * the bulk data buffer; see "Linux ↔ UTSM FAT32 hypercalls" below) */ UTSM_HCALL_FILE_READ = 0x0060, /* Read UTSM FAT32 root file into payload_pool */ UTSM_HCALL_FILE_WRITE = 0x0061, /* Write payload_pool content to UTSM FAT32 root file */ /* VSCode integration Phase 1: query graphics surface pool (GPA + size). * The surface pool is a separate DMA region EPT-mapped into the Linux * guest at LINUX_GUEST_SURFACE_GPA. Linux mmaps it via /dev/utsm and * exposes it to X server as the scanout framebuffer (Phase 3 virtio-gpu * shadow buffer lives here; guest writes → host reads for blit). */ UTSM_HCALL_SURFACE_INFO = 0x0070, /* Query graphics surface pool GPA + size */ }; /* Hypercall return codes */ #define UTSM_HCALL_OK 0 #define UTSM_HCALL_INVAL (-1) #define UTSM_HCALL_PERM (-2) #define UTSM_HCALL_NOMEM (-3) #define UTSM_HCALL_NOSYS (-4) #define UTSM_HCALL_NOENT (-5) /* file not found */ #define UTSM_HCALL_IO (-6) /* disk / generic I/O error */ /* ===== Message types ===== */ enum utsm_ipc_msg_type { UTSM_MSG_NONE = 0, UTSM_MSG_HELLO = 1, /* Initial handshake */ UTSM_MSG_HELLO_ACK= 2, /* Handshake acknowledgment */ UTSM_MSG_PING = 3, /* Ping request */ UTSM_MSG_PONG = 4, /* Pong response */ UTSM_MSG_DATA = 5, /* Generic data message */ UTSM_MSG_ACK = 6, /* Acknowledgment */ UTSM_MSG_NACK = 7, /* Negative acknowledgment */ UTSM_MSG_SHUTDOWN = 8, /* Shutdown notification */ /* Linux compat exec protocol (UTSM → Linux → UTSM) */ UTSM_MSG_EXEC_REQUEST = 16, /* UTSM→Linux: exec a program (payload=ipc_exec_request) */ UTSM_MSG_EXEC_STDOUT = 17, /* Linux→UTSM: stdout chunk (payload=raw bytes) */ UTSM_MSG_EXEC_STDERR = 18, /* Linux→UTSM: stderr chunk (payload=raw bytes) */ UTSM_MSG_EXEC_EXIT = 19, /* Linux→UTSM: process exited (payload=ipc_exec_exit) */ UTSM_MSG_EXEC_READY = 20, /* Linux→UTSM: daemon ready to accept exec requests */ /* Linux compat file transfer protocol (payload via payload_pool) */ UTSM_MSG_FILE_LIST_REQUEST = 24, /* UTSM→Linux: list directory (payload=ipc_file_request) */ UTSM_MSG_FILE_READ_REQUEST = 25, /* UTSM→Linux: read file chunk (payload=ipc_file_request) */ UTSM_MSG_FILE_RESPONSE = 26, /* Linux→UTSM: result header (payload=ipc_file_response), * bulk data already in payload_pool */ UTSM_MSG_FILE_WRITE_REQUEST= 27, /* UTSM→Linux: write file chunk (payload=ipc_file_request), * bulk data pre-staged in payload_pool by UTSM; * result via UTSM_MSG_FILE_RESPONSE */ }; /* ===== Linux compat exec payload structures ===== * * Flow: * 1. UTSM writes UTSM_MSG_EXEC_REQUEST into utsm_to_linux ring, * then calls linux_resume() to wake the parked Linux guest. * 2. Linux daemon reads the request, fork+exec's the program, * streams stdout/stderr back via UTSM_MSG_EXEC_STDOUT/STDERR. * 3. Linux daemon writes UTSM_MSG_EXEC_EXIT with exit code, then HLTs (park). * 4. UTSM reads the responses from linux_to_utsm ring, returns to caller. * * All strings are NUL-terminated. argv_blob contains argv[0..argc-1] each * NUL-terminated, concatenated. path is the program path to execute. */ #define UTSM_EXEC_PATH_MAX 128 #define UTSM_EXEC_ARGV_MAX 96 /* total argv blob size */ #define UTSM_EXEC_MAX_ARGS 16 /* VSCode Phase 5: exec flags (ipc_exec_request.flags) */ #define UTSM_EXEC_FLAG_ASYNC 0x00000001u /* daemon fork+setsid+detach, replies * EXEC_EXIT(0) immediately after * spawn; no stdout capture, no wait. * Used to launch X server / VSCode * without blocking the UTSM side. */ struct ipc_exec_request { uint32_t argc; /* number of arguments */ uint32_t flags; /* UTSM_EXEC_FLAG_* (0 = sync) */ char path[UTSM_EXEC_PATH_MAX]; /* program path (NUL-terminated) */ char argv_blob[UTSM_EXEC_ARGV_MAX]; /* argv strings, each NUL-terminated */ } __attribute__((packed)); struct ipc_exec_exit { uint32_t exit_code; /* process exit status */ uint32_t flags; /* UTSM_EXEC_EXIT_F_* (0 = 旧式 8B) */ uint32_t stdout_len; /* payload_pool 内有效字节数 */ uint32_t stdout_total; /* 截断前总输出字节数(诊断) */ } __attribute__((packed)); /* ipc_exec_exit.flags — 大输出/超时扩展(P3 补完)。 * 旧 daemon 只发 8B(exit_code+reserved=0),UTSM 按 data_len>=16 且 * flags&UTSM_EXEC_EXIT_F_POOL 判定新协议;旧式输出仍走 EXEC_STDOUT 内联。 */ #define UTSM_EXEC_EXIT_F_POOL 0x00000001u /* stdout 经 payload_pool 传输(offset 0) */ #define UTSM_EXEC_EXIT_F_TRUNC 0x00000002u /* 输出超 pool 容量被截断 */ #define UTSM_EXEC_EXIT_F_TIMEOUT 0x00000004u /* 子进程超时被 SIGKILL(exit_code=124) */ /* 同步 exec daemon 侧子进程超时(秒):park-and-resume 模型下 UTSM 在 * linux_resume() 期间不运行,无法宿主侧看门狗,必须 daemon alarm 兜底, * 否则 guest 内长命令(sleep infinity 等)会永久冻结整系统。 */ #define UTSM_EXEC_TIMEOUT_SEC 15u /* ===== Linux compat file transfer payload structures ===== * * Flow (synchronous with park-and-resume, same as exec): * 1. UTSM writes UTSM_MSG_FILE_LIST_REQUEST / FILE_READ_REQUEST into the * utsm_to_linux ring, then linux_resume() wakes the daemon. * 2. Linux daemon performs opendir/readdir or open/pread, writes the bulk * result data DIRECTLY into payload_pool at req.pool_offset (via * UTSM_IOCTL_WRITE_POOL), then enqueues UTSM_MSG_FILE_RESPONSE carrying * ipc_file_response (status + data_len + total_size), then HLTs. * 3. UTSM drains the ring, reads the response header, then copies * resp.data_len bytes out of payload_pool + req.pool_offset. * * Write flow (UTSM → Linux, UTSM_MSG_FILE_WRITE_REQUEST): * 1. UTSM stages the data into payload_pool at req.pool_offset (offset 0, * exclusive use — the model is synchronous), sets req.pool_capacity to * the staged byte count and req.file_offset to the write position. * 2. Linux daemon reads req.pool_capacity bytes out of payload_pool (via * UTSM_IOCTL_READ_POOL) and pwrite()s them to req.path at * req.file_offset. Convention: file_offset == 0 opens with O_TRUNC * (a push always replaces the file); offset > 0 writes in place. * The file is created with mode 0644 if missing. * 3. Daemon replies UTSM_MSG_FILE_RESPONSE with data_len = bytes written * and total_size = resulting file size. * * payload_pool is ~1MB (UTSM_IPC_PAYLOAD_SIZE), so a single round trip can * move up to that much data. For larger files the caller loops with * increasing file_offset. Directory listings are formatted as plain text, * one entry per line ("name" or "name/" for directories). * * All strings are NUL-terminated. */ #define UTSM_FILE_PATH_MAX 200 /* Status codes for ipc_file_response.status */ #define UTSM_FILE_OK 0 #define UTSM_FILE_ERR_NOENT 1 /* path does not exist */ #define UTSM_FILE_ERR_IO 2 /* generic I/O error */ #define UTSM_FILE_ERR_NOTDIR 3 /* LIST on a non-directory */ #define UTSM_FILE_ERR_ISDIR 4 /* READ/WRITE on a directory */ #define UTSM_FILE_ERR_PERM 5 /* permission denied */ #define UTSM_FILE_ERR_INVAL 6 /* malformed request */ #define UTSM_FILE_ERR_NOSPC 7 /* no space left on device / file too large */ struct ipc_file_request { uint32_t pool_offset; /* payload_pool offset of bulk data */ uint32_t pool_capacity; /* LIST/READ: max bytes daemon may write; * WRITE: staged data byte count */ uint64_t file_offset; /* READ: read offset; WRITE: write offset * (0 also implies O_TRUNC); LIST: 0 */ char path[UTSM_FILE_PATH_MAX]; /* absolute Linux path (NUL-terminated) */ } __attribute__((packed)); struct ipc_file_response { uint32_t status; /* UTSM_FILE_* */ uint32_t data_len; /* LIST/READ: bytes written into payload_pool; * WRITE: bytes written to the file */ uint64_t total_size; /* READ: total file size; * WRITE: resulting file size; LIST: 0 */ } __attribute__((packed)); /* ===== Linux ↔ UTSM FAT32 hypercalls (UTSM_HCALL_FILE_READ/WRITE) ===== * * These give the Linux guest direct access to files on the UTSM-side FAT32 * system disk (root directory, 8.3-convertible names, whole-file semantics, * max 256KB per file). payload_pool at offset 0 is the bulk data buffer. * * UTSM_HCALL_FILE_READ: a0 = GPA of NUL-terminated file name (max 63 chars) * a1 = file offset (u64) * a2 = max bytes to stage (clamped to pool capacity) * UTSM reads the file via its block/FAT32 path, stages * payload_pool[0..n) = file[a1 .. a1+n), and returns in RAX: * low 20 bits = n (bytes staged; pool capacity < 1MB so 20 bits fit) * high bits = total file size (RAX >> 20) * Negative RAX = UTSM_HCALL_* error (INVAL bad name, NOENT missing, * NOMEM pool unavailable, IO disk error). n == 0 with a1 >= size means EOF. * * UTSM_HCALL_FILE_WRITE: a0 = GPA of NUL-terminated file name (max 63 chars) * a1 = reserved (0) * a2 = byte count staged at payload_pool[0] * Whole-file replace: the staged bytes become the new file content * (create or replace). Returns bytes written (== a2) or a negative * UTSM_HCALL_* error. * * Concurrency note: payload_pool is a single-writer channel; the guest must * not run these hypercalls concurrently with a ring-based file transfer. */ /* ===== Ring buffer structures ===== */ /* Single IPC message (256 bytes, cache-line aligned) */ struct utsm_ipc_msg { uint32_t type; /* enum utsm_ipc_msg_type */ uint32_t seq; /* Sequence number (monotonic) */ uint32_t data_len; /* Actual bytes in data[] */ uint32_t flags; /* Reserved for future use */ uint8_t data[UTSM_IPC_MSG_DATA_SIZE]; /* Inline payload */ } __attribute__((packed)); /* Ring buffer: single-producer, single-consumer queue. * * Producer writes at tail, then increments tail (with memory barrier). * Consumer reads at head, then increments head (with memory barrier). * * Empty when head == tail. * Full when (tail + 1) % capacity == head. */ struct utsm_ipc_ring { volatile uint32_t head; /* Consumer updates (Linux or UTSM) */ volatile uint32_t tail; /* Producer updates (UTSM or Linux) */ uint32_t capacity; /* Max messages (UTSM_IPC_QUEUE_DEPTH) */ uint32_t reserved; struct utsm_ipc_msg messages[UTSM_IPC_QUEUE_DEPTH]; } __attribute__((packed)); /* Shared memory header */ struct utsm_ipc_shm_header { uint64_t magic; /* UTSM_IPC_MAGIC */ uint32_t version; /* UTSM_IPC_VERSION */ uint32_t header_size; /* sizeof(struct utsm_ipc_shm_header) */ uint64_t shm_phys; /* Physical address of SHM region */ uint64_t shm_size; /* Total SHM size */ volatile uint32_t utsm_ready; /* UTSM sets to 1 when initialized */ volatile uint32_t linux_ready; /* Linux sets to 1 when initialized */ uint8_t reserved[24]; } __attribute__((packed)); /* Full shared memory region */ struct utsm_ipc_shm { struct utsm_ipc_shm_header header; struct utsm_ipc_ring utsm_to_linux; /* UTSM writes, Linux reads */ struct utsm_ipc_ring linux_to_utsm; /* Linux writes, UTSM reads */ uint8_t payload_pool[UTSM_IPC_PAYLOAD_SIZE]; } __attribute__((packed)); /* Compile-time guard: the packed SHM layout must fit in the 1MB region * (UTSM allocates exactly UTSM_IPC_SHM_SIZE bytes and EPT-maps only that). */ _Static_assert(sizeof(struct utsm_ipc_shm) <= UTSM_IPC_SHM_SIZE, "utsm_ipc_shm exceeds the 1MB shared memory region"); /* ===== Ring buffer operations (inline, usable by both kernels) ===== */ /* Memory barriers — adapted for each kernel's environment. * UTSM: use compiler barriers (__asm__ volatile("" ::: "memory")) * Linux: use smp_store_release / smp_load_acquire * * For simplicity, we use full memory barriers here. */ static inline void utsm_ipc_mb(void) { __asm__ volatile("mfence" ::: "memory"); } /* Initialize a ring buffer */ static inline void utsm_ipc_ring_init(struct utsm_ipc_ring *ring) { ring->head = 0; ring->tail = 0; ring->capacity = UTSM_IPC_QUEUE_DEPTH; ring->reserved = 0; utsm_ipc_mb(); } /* Try to enqueue a message. Returns 0 on success, -1 if full. */ static inline int utsm_ipc_ring_push(struct utsm_ipc_ring *ring, const struct utsm_ipc_msg *msg) { uint32_t tail = ring->tail; uint32_t next_tail = (tail + 1) % ring->capacity; if (next_tail == ring->head) { return -1; /* Full */ } ring->messages[tail] = *msg; utsm_ipc_mb(); /* Ensure message is visible before updating tail */ ring->tail = next_tail; return 0; } /* Try to dequeue a message. Returns 0 on success, -1 if empty. */ static inline int utsm_ipc_ring_pop(struct utsm_ipc_ring *ring, struct utsm_ipc_msg *msg) { uint32_t head = ring->head; if (head == ring->tail) { return -1; /* Empty */ } *msg = ring->messages[head]; utsm_ipc_mb(); /* Ensure we've read the message before updating head */ ring->head = (head + 1) % ring->capacity; return 0; } /* Check if ring is empty */ static inline int utsm_ipc_ring_empty(const struct utsm_ipc_ring *ring) { return ring->head == ring->tail; } /* Check if ring is full */ static inline int utsm_ipc_ring_full(const struct utsm_ipc_ring *ring) { uint32_t next_tail = (ring->tail + 1) % ring->capacity; return next_tail == ring->head; } /* ===== Hypercall ABI (Linux → UTSM) ===== * * Linux calls VMCALL with: * RAX = UTSM_IPC_MAGIC | op * RDI = arg0 * RSI = arg1 * RDX = arg2 * * UTSM handles the VMCALL VM-Exit and returns: * RAX = return value (UTSM_HCALL_OK or error) * * The magic in RAX distinguishes UTSM hypercalls from other VMCALL users. * * Define UTSM_NO_INLINE_HCALL before including this header if you provide * your own hypercall wrapper implementations (e.g. Linux utsm_hcall.c). */ #define UTSM_HCALL_MAGIC 0x5554534D48430000ULL /* "UTSMHC\00\00" */ #ifndef UTSM_NO_INLINE_HCALL static inline long utsm_hcall(uint64_t op, uint64_t a0, uint64_t a1, uint64_t a2) { long ret; __asm__ volatile("vmcall" : "=a"(ret) : "a"(UTSM_HCALL_MAGIC | op), "D"(a0), "S"(a1), "d"(a2) : "memory"); return ret; } /* Convenience hypercall wrappers */ static inline long utsm_hcall_ping(void) { return utsm_hcall(UTSM_HCALL_PING, 0, 0, 0); } static inline long utsm_hcall_hello(void) { return utsm_hcall(UTSM_HCALL_HELLO, 0, 0, 0); } static inline long utsm_hcall_shm_info(uint64_t *gpa_out, uint64_t *size_out) { uint64_t gpa = 0, size = 0; long ret = utsm_hcall(UTSM_HCALL_SHM_INFO, (uint64_t)&gpa, (uint64_t)&size, 0); if (ret == 0) { if (gpa_out) *gpa_out = gpa; if (size_out) *size_out = size; } return ret; } /* VSCode integration Phase 1: query graphics surface pool. * Returns the surface pool GPA (visible inside the Linux guest) and size. * Linux user-space mmaps /dev/utsm at the returned GPA to obtain a writable * pointer that the host can scan for blitting to the Deshab framebuffer. */ static inline long utsm_hcall_surface_info(uint64_t *gpa_out, uint64_t *size_out) { uint64_t gpa = 0, size = 0; long ret = utsm_hcall(UTSM_HCALL_SURFACE_INFO, (uint64_t)&gpa, (uint64_t)&size, 0); if (ret == 0) { if (gpa_out) *gpa_out = gpa; if (size_out) *size_out = size; } return ret; } static inline long utsm_hcall_console_write(const char *buf, uint64_t len) { return utsm_hcall(UTSM_HCALL_CONSOLE_WRITE, (uint64_t)buf, len, 0); } #endif /* UTSM_NO_INLINE_HCALL */ #endif /* UTSM_IPC_PROTO_H */