From 6c142c4aee0b1968d8a19f822c8aae1345f971f7 Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Fri, 10 Jul 2026 07:38:50 -0400 Subject: [PATCH] portal: add HYPER_OP_OSI_PROC_ALL bulk process walk + denormalize pid into syscall_event New op HYPER_OP_OSI_PROC_ALL walks for_each_process under rcu_read_lock and returns a slim osi_proc_node {pid, ppid, create_time, uid/gid/euid/egid, comm[16]} per user process in one paginated, RCU-consistent transaction. The host was otherwise issuing one HYPER_OP_OSI_PROC per pid to build a process tree -- 1+N round-trips (the portal region is a single page) and a snapshot that can tear between the per-pid reads. comm comes from task->comm (kernel memory only) and !mm tasks (kthreads) are skipped, so it never touches userspace and is safe against exiting/stopped contexts. Auto-wired via the portal_op_list X-macro (enum + dispatch + prototype). Also denormalize current->pid and current->start_time into struct syscall_event, populated at fire time. Host-side syscall consumers (e.g. process-tree exit tracking) can then identify the task straight off the event with zero extra round-trips and without reading the dying task's memory. Compiles for 4.10 + 6.13 across armel/arm64/x86_64. osi_proc_node layout and the new syscall_event fields verified identical in the emitted DWARF on both kernel versions, and exercised live in an armel/6.13 guest. --- src/hooks/syscalls_hc.c | 4 ++ src/hooks/syscalls_hc.h | 5 ++ src/portal/portal_op_list.h | 1 + src/portal/portal_osi.c | 93 +++++++++++++++++++++++++++++++++++++ src/portal/portal_types.h | 19 ++++++++ 5 files changed, 122 insertions(+) diff --git a/src/hooks/syscalls_hc.c b/src/hooks/syscalls_hc.c index 4f899cd..3193d98 100644 --- a/src/hooks/syscalls_hc.c +++ b/src/hooks/syscalls_hc.c @@ -200,6 +200,10 @@ static inline void fill_handler(struct syscall_event *args, int argc, const unsi } args->task = current; + // Denormalize identity so host-side hooks (e.g. process-tree exit tracking) + // can attribute the event without a separate OSI_PROC portal round-trip. + args->pid = task_pid_nr(current); + args->create_time = current->start_time; args->retval = 0; // Initialize to 0, will be set by hypercall regs = task_pt_regs(current); diff --git a/src/hooks/syscalls_hc.h b/src/hooks/syscalls_hc.h index d48b264..f13eba9 100644 --- a/src/hooks/syscalls_hc.h +++ b/src/hooks/syscalls_hc.h @@ -86,6 +86,11 @@ struct syscall_event { struct task_struct *task; /* Task pointer */ struct pt_regs *regs; /* Pointer to current registers */ long retval; /* Return value */ + uint64_t pid; /* current->pid, denormalized so the host + * can identify the task without a + * separate OSI_PROC round-trip */ + uint64_t create_time; /* current->start_time (process identity, + * stable across execve) */ u32 argc; /* Number of arguments */ bool skip_syscall; /* Flag to skip syscall execution */ char syscall_name[SYSCALL_NAME_MAX_LEN]; /* Name of syscall (embedded in structure) */ diff --git a/src/portal/portal_op_list.h b/src/portal/portal_op_list.h index 1087142..6e8e450 100644 --- a/src/portal/portal_op_list.h +++ b/src/portal/portal_op_list.h @@ -7,6 +7,7 @@ X(exec, EXEC) \ X(osi_proc, OSI_PROC) \ X(osi_proc_handles, OSI_PROC_HANDLES) \ + X(osi_proc_all, OSI_PROC_ALL) \ X(osi_mappings, OSI_MAPPINGS) \ X(osi_proc_mem, OSI_PROC_MEM) \ X(osi_proc_exe, OSI_PROC_EXE) \ diff --git a/src/portal/portal_osi.c b/src/portal/portal_osi.c index 1f68b30..4b31416 100644 --- a/src/portal/portal_osi.c +++ b/src/portal/portal_osi.c @@ -268,6 +268,99 @@ void handle_op_osi_proc_handles(portal_region *mem_region) mem_region->header.op = (HYPER_RESP_READ_OK); } +/* + * handle_op_osi_proc_all: kernel-side whole-process-tree walk. + * + * Returns a slim osi_proc_node (pid, ppid, create_time, ids, inline comm) for + * every user process in a single RCU-consistent pass, instead of the host + * issuing one HYPER_OP_OSI_PROC per pid. This turns a tree snapshot from 1+N + * portal round-trips into ceil(N / nodes-per-page) and, because the walk holds + * rcu_read_lock() throughout, yields a single coherent snapshot with no + * host-side tear between per-pid reads. + * + * Response layout: [osi_result_header][osi_proc_node * result_count]. + * Records are fixed-size with comm inlined (no string pool), so ~72 fit a page. + * + * Pagination: mem_region->header.addr carries the number of user processes the + * host has already consumed (skip); total_count is the true count so the host + * knows when to stop. comm comes from task->comm (kernel memory) only -- like + * HYPER_OP_OSI_PROC this never touches userspace, so it is safe against + * exiting/mm-less contexts. Kernel threads (!mm) are skipped (that is slice 2). + */ +void handle_op_osi_proc_all(portal_region *mem_region) +{ + struct task_struct *task; + int count = 0; /* nodes filled into this page */ + int total_count = 0; /* all user procs (for host pagination) */ + int skip = 0; /* procs already consumed by the host */ + size_t max_nodes; + char *data_buf = PORTAL_DATA(mem_region); + struct osi_result_header *header = (struct osi_result_header *)data_buf; + struct osi_proc_node *node_arr; + + skip = (int)(mem_region->header.addr); + if (skip < 0) { + skip = 0; + } + + igloo_debug_osi("igloo: Handling HYPER_OP_OSI_PROC_ALL skip=%d\n", skip); + + max_nodes = (CHUNK_SIZE - sizeof(struct osi_result_header)) / + sizeof(struct osi_proc_node); + if (max_nodes == 0) { + max_nodes = 1; + } + + header->result_count = 0; + header->total_count = 0; + node_arr = (struct osi_proc_node *)(data_buf + sizeof(struct osi_result_header)); + + rcu_read_lock(); + for_each_process(task) { + struct mm_struct *mm = task->mm; + struct osi_proc_node *node; + size_t name_len; + int ord; + + if (!mm) { + continue; /* user processes only; kthreads are slice 2 */ + } + + ord = total_count; /* 0-based ordinal among user procs */ + total_count++; + + if (ord < skip || (size_t)count >= max_nodes) { + continue; /* already delivered, or this page is full */ + } + + node = &node_arr[count]; + node->pid = (task->pid); + node->ppid = (task->real_parent ? task->real_parent->pid : 0); + node->create_time = (task->start_time); + node->uid = (task->cred->uid.val); + node->gid = (task->cred->gid.val); + node->euid = (task->cred->euid.val); + node->egid = (task->cred->egid.val); + + name_len = strnlen(task->comm, sizeof(node->comm) - 1); + memcpy(node->comm, task->comm, name_len); + node->comm[name_len] = '\0'; + + count++; + } + rcu_read_unlock(); + + header->result_count = (count); + header->total_count = (total_count); + + igloo_debug_osi("igloo: OSI_PROC_ALL returning %d procs (skip=%d total=%d)\n", + count, skip, total_count); + + mem_region->header.size = (sizeof(struct osi_result_header) + + (count * sizeof(struct osi_proc_node))); + mem_region->header.op = (HYPER_RESP_READ_OK); +} + void handle_op_osi_proc_exe(portal_region *mem_region) { struct task_struct *task; diff --git a/src/portal/portal_types.h b/src/portal/portal_types.h index e396538..6e154b9 100644 --- a/src/portal/portal_types.h +++ b/src/portal/portal_types.h @@ -92,6 +92,25 @@ struct osi_proc { uint64_t start_time; }; +/* + * Slim per-process record for the bulk process-tree walk + * (handle_op_osi_proc_all). Purpose-built for slice-1 tree()/get(): identity + * + genealogy + ids only, with comm inlined (no string pool) so records are + * fixed-size and densely packed (~72 per page vs ~18 for full osi_proc). + * TASK_COMM_LEN is 16; keep the field literal so the host ABI is stable + * regardless of the guest kernel's TASK_COMM_LEN. + */ +struct osi_proc_node { + uint64_t pid; + uint64_t ppid; + uint64_t create_time; // task->start_time; stable across execve (identity) + uint32_t uid; + uint32_t gid; + uint32_t euid; + uint32_t egid; + char comm[16]; // NUL-padded task->comm (truncated to 15 + NUL) +}; + // File descriptor entry structure for handle_op_read_fds struct osi_fd_entry { uint64_t fd; // File descriptor number