Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions abi/snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -842,11 +842,21 @@
"name": "kernel_ioctl",
"signature": "(i32,i32,i64,i32) -> (i32)"
},
{
"kind": "func",
"name": "kernel_ipc_shm_lookup_mapping",
"signature": "(i64,i64) -> (i32)"
},
{
"kind": "func",
"name": "kernel_ipc_shm_read_chunk",
"signature": "(i32,i32,i64,i32) -> (i32)"
},
{
"kind": "func",
"name": "kernel_ipc_shm_record_mapping",
"signature": "(i64,i32,i32) -> (i32)"
},
{
"kind": "func",
"name": "kernel_ipc_shm_write_chunk",
Expand All @@ -862,6 +872,11 @@
"name": "kernel_ipc_shmdt",
"signature": "(i32) -> (i32)"
},
{
"kind": "func",
"name": "kernel_ipc_shmdt_addr",
"signature": "(i64) -> (i32)"
},
{
"kind": "func",
"name": "kernel_is_fd_nonblock",
Expand Down Expand Up @@ -1002,6 +1017,11 @@
"name": "kernel_pick_signal_target_tid",
"signature": "(i32,i32) -> (i32)"
},
{
"kind": "func",
"name": "kernel_pick_tcp_listener_target",
"signature": "(i32,i32,i64) -> (i32)"
},
{
"kind": "func",
"name": "kernel_pipe",
Expand Down Expand Up @@ -1462,6 +1482,11 @@
"name": "kernel_sysconf",
"signature": "(i32) -> (i64)"
},
{
"kind": "func",
"name": "kernel_take_process_timer_cleanup",
"signature": "(i32,i64,i32) -> (i32)"
},
{
"kind": "func",
"name": "kernel_tcgetattr",
Expand Down Expand Up @@ -2108,6 +2133,61 @@
"__channel_base",
"__tls_base"
],
"process_snapshot": {
"count_offset": 0,
"count_size": 4,
"record_fields": [
{
"name": "pid",
"offset": 0,
"size": 4,
"type": "u32"
},
{
"name": "ppid",
"offset": 4,
"size": 4,
"type": "u32"
},
{
"name": "uid",
"offset": 8,
"size": 4,
"type": "u32"
},
{
"name": "gid",
"offset": 12,
"size": 4,
"type": "u32"
},
{
"name": "vsizeBytes",
"offset": 16,
"size": 8,
"type": "u64"
},
{
"name": "state",
"offset": 24,
"size": 4,
"type": "u32_ascii"
},
{
"name": "commLen",
"offset": 28,
"size": 4,
"type": "u32"
},
{
"name": "cmdlineLen",
"offset": 32,
"size": 4,
"type": "u32"
}
],
"record_fixed_size": 36
},
"syscall_arg_descriptors": {
"1": [
{
Expand Down
2 changes: 1 addition & 1 deletion crates/kernel/src/wasm_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1790,7 +1790,7 @@ pub extern "C" fn kernel_enum_procs(out_ptr: *mut u8, out_len: u32) -> i32 {
// First pass: compute total bytes we need to write so we can fail fast
// on a too-small buffer rather than partial-writing. Skip zombies on
// the count too so the size estimate matches what we actually emit.
const HDR_BYTES: usize = 4 + 4 + 4 + 4 + 4 + 8 + 4 + 4 + 4; // 40 bytes per record
const HDR_BYTES: usize = wasm_posix_shared::process_snapshot::RECORD_FIXED_SIZE;
let mut need: usize = 4; // count u32
for pid in &pids {
let proc = match table.get(*pid) {
Expand Down
15 changes: 15 additions & 0 deletions crates/shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

pub mod host_abi;

pub mod process_snapshot {
pub const COUNT_OFFSET: usize = 0;
pub const COUNT_SIZE: usize = 4;

pub const RECORD_PID_OFFSET: usize = 0;
pub const RECORD_PPID_OFFSET: usize = 4;
pub const RECORD_UID_OFFSET: usize = 8;
pub const RECORD_GID_OFFSET: usize = 12;
pub const RECORD_VSIZE_BYTES_OFFSET: usize = 16;
pub const RECORD_STATE_OFFSET: usize = 24;
pub const RECORD_COMM_LEN_OFFSET: usize = 28;
pub const RECORD_CMDLINE_LEN_OFFSET: usize = 32;
pub const RECORD_FIXED_SIZE: usize = 36;
}

/// Kernel ABI version.
///
/// This number is baked into every compiled user program (wasm custom section
Expand Down
2 changes: 1 addition & 1 deletion docs/plans/2026-05-20-rust-owned-host-logic-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ path.
| In progress / stacked PR | IPC/resource cleanup in Rust | Rust `Process` now records `shmat` address -> segment metadata, inherits it through fork, clears it across exec setup, and detaches live mappings from `remove_process()`. TS still copies bytes between guest memory and kernel SysV segments because only the host can address guest `Memory`. | `remove_process()` owns IPC attachment cleanup; JS only handles guest-memory transfer and host primitive wake/schedule work. | SysV IPC and mqueue Rust tests plus host integration/e2e coverage for blocking and cleanup. |
| Planned | Readiness metadata improvements | Replace broad host inference with kernel-emitted readiness events for pipe/socket/poll/select cases where the kernel already knows state changes. | JS still owns timers/retry queues/`Atomics.waitAsync`, but readiness decisions are less inferred from syscall numbers. No extra Wasm round trip per syscall. | Pipe/socket/poll/select/ppoll/pselect tests; browser bridge smoke for affected wake paths; performance comparison before removing broad fallback logic. |
| Planned | VFS policy split | Keep backend I/O, OPFS/IndexedDB/fetch, Node `fs`, and lazy archive materialization in JS. Move permission and policy decisions into Rust where process uid/gid/umask/fd context is authoritative. | Guest-visible policy is enforced in Rust; host adapters only perform platform operations requested through a checked contract. | VFS unit tests, uid/gid/permission tests, host-fs metadata tests, default mount tests, Node/browser parity tests. |
| Planned | Procfs/process snapshot schema metadata | Generate binary process snapshot schema/constants consumed by TS UI decoding, or replace TS decoding with a Rust-exported stable formatter if that does not add hot-path cost. | TS no longer hand-decodes undocumented offsets for kernel process snapshot data. Procfs text formatting remains Rust-owned. | Rust procfs/process snapshot tests; generated ABI vitest; UI/kernel-host tests that consume snapshots. |
| Done / stacked PR | Procfs/process snapshot schema metadata | `crates/shared` owns the binary process snapshot layout, `dump-abi` publishes the schema in `abi/snapshot.json` and generated TS bindings, and `parseProcSnapshots` consumes those generated offsets and sizes. | TS no longer hand-decodes undocumented offsets for kernel process snapshot data. Procfs text formatting remains Rust-owned. | Rust procfs/process snapshot tests; generated ABI vitest; UI/kernel-host tests that consume snapshots. |

Deferral rule: if a chunk would move browser/Node primitives, add runtime JS
evaluation, or add a Wasm call to every syscall without removing meaningful
Expand Down
14 changes: 14 additions & 0 deletions host/src/generated/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ export const CH_SIG_HANDLER = 65564 as const;
export const CH_SIG_FLAGS = 65568 as const;
export const CH_SIG_OLD_MASK = 65576 as const;

export const PROC_SNAPSHOT_COUNT_OFFSET = 0 as const;
export const PROC_SNAPSHOT_COUNT_SIZE = 4 as const;
export const PROC_SNAPSHOT_RECORD_FIXED_SIZE = 36 as const;
export const PROC_SNAPSHOT_RECORD_FIELDS = {
pid: { offset: 0, size: 4, type: "u32" },
ppid: { offset: 4, size: 4, type: "u32" },
uid: { offset: 8, size: 4, type: "u32" },
gid: { offset: 12, size: 4, type: "u32" },
vsizeBytes: { offset: 16, size: 8, type: "u64" },
state: { offset: 24, size: 4, type: "u32_ascii" },
commLen: { offset: 28, size: 4, type: "u32" },
cmdlineLen: { offset: 32, size: 4, type: "u32" },
} as const;

export const STRUCT_SIZE_WASM_STAT = 88 as const;
export const STRUCT_SIZE_WASM_DIRENT = 16 as const;
export const STRUCT_SIZE_WASM_TIMESPEC = 16 as const;
Expand Down
32 changes: 20 additions & 12 deletions host/src/kernel-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ import {
CH_SYSCALL,
CH_TOTAL_SIZE,
HOST_INTERCEPTED_SYSCALLS,
PROC_SNAPSHOT_COUNT_OFFSET,
PROC_SNAPSHOT_COUNT_SIZE,
PROC_SNAPSHOT_RECORD_FIELDS,
PROC_SNAPSHOT_RECORD_FIXED_SIZE,
SYSCALL_ARGS,
type SyscallArgDesc,
} from "./generated/abi";
Expand Down Expand Up @@ -291,22 +295,26 @@ export interface ProcessSnapshot {
}

function parseProcSnapshots(mem: Uint8Array): ProcessSnapshot[] {
if (mem.byteLength < 4) return [];
if (mem.byteLength < PROC_SNAPSHOT_COUNT_SIZE) return [];
const dv = new DataView(mem.buffer, mem.byteOffset, mem.byteLength);
const count = dv.getUint32(0, true);
let off = 4;
const count = dv.getUint32(PROC_SNAPSHOT_COUNT_OFFSET, true);
let off = PROC_SNAPSHOT_COUNT_SIZE;
const out: ProcessSnapshot[] = [];
const dec = new TextDecoder("utf-8", { fatal: false });
for (let i = 0; i < count; i++) {
if (off + 36 > mem.byteLength) break;
const pid = dv.getUint32(off, true); off += 4;
const ppid = dv.getUint32(off, true); off += 4;
const uid = dv.getUint32(off, true); off += 4;
const gid = dv.getUint32(off, true); off += 4;
const vsizeBytes = Number(dv.getBigUint64(off, true)); off += 8;
const state = String.fromCharCode(dv.getUint32(off, true)) as ProcessSnapshot["state"]; off += 4;
const commLen = dv.getUint32(off, true); off += 4;
const cmdLen = dv.getUint32(off, true); off += 4;
if (off + PROC_SNAPSHOT_RECORD_FIXED_SIZE > mem.byteLength) break;
const record = off;
const pid = dv.getUint32(record + PROC_SNAPSHOT_RECORD_FIELDS.pid.offset, true);
const ppid = dv.getUint32(record + PROC_SNAPSHOT_RECORD_FIELDS.ppid.offset, true);
const uid = dv.getUint32(record + PROC_SNAPSHOT_RECORD_FIELDS.uid.offset, true);
const gid = dv.getUint32(record + PROC_SNAPSHOT_RECORD_FIELDS.gid.offset, true);
const vsizeBytes = Number(dv.getBigUint64(record + PROC_SNAPSHOT_RECORD_FIELDS.vsizeBytes.offset, true));
const state = String.fromCharCode(
dv.getUint32(record + PROC_SNAPSHOT_RECORD_FIELDS.state.offset, true),
) as ProcessSnapshot["state"];
const commLen = dv.getUint32(record + PROC_SNAPSHOT_RECORD_FIELDS.commLen.offset, true);
const cmdLen = dv.getUint32(record + PROC_SNAPSHOT_RECORD_FIELDS.cmdlineLen.offset, true);
off += PROC_SNAPSHOT_RECORD_FIXED_SIZE;
if (off + commLen + cmdLen > mem.byteLength) break;
const comm = dec.decode(mem.subarray(off, off + commLen));
off += commLen;
Expand Down
26 changes: 26 additions & 0 deletions host/test/generated-abi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import {
HOST_ADAPTER_VERSION,
HOST_ADAPTER_WORKER_FEATURES,
HOST_INTERCEPTED_SYSCALLS,
PROC_SNAPSHOT_COUNT_OFFSET,
PROC_SNAPSHOT_COUNT_SIZE,
PROC_SNAPSHOT_RECORD_FIELDS,
PROC_SNAPSHOT_RECORD_FIXED_SIZE,
STRUCT_SIZE_WASM_DIRENT,
STRUCT_SIZE_WASM_POLL_FD,
STRUCT_SIZE_WASM_STAT,
Expand Down Expand Up @@ -79,6 +83,12 @@ function hostAdapterManifestField(name: string): { offset: number; size: number
return { offset: field.offset, size: field.size };
}

function processSnapshotField(name: string): { offset: number; size: number; type: string } {
const field = snapshot.process_snapshot.record_fields.find((f: { name: string }) => f.name === name);
if (!field) throw new Error(`missing process_snapshot field ${name}`);
return { offset: field.offset, size: field.size, type: field.type };
}

describe("generated host ABI bindings", () => {
it("match the ABI version and channel layout snapshot", () => {
expect(ABI_VERSION).toBe(snapshot.abi_version);
Expand Down Expand Up @@ -166,4 +176,20 @@ describe("generated host ABI bindings", () => {
).toEqual(hostAdapterManifestField(fieldName));
}
});

it("match Rust-owned process snapshot schema metadata", () => {
expect(PROC_SNAPSHOT_COUNT_OFFSET).toBe(snapshot.process_snapshot.count_offset);
expect(PROC_SNAPSHOT_COUNT_SIZE).toBe(snapshot.process_snapshot.count_size);
expect(PROC_SNAPSHOT_RECORD_FIXED_SIZE).toBe(
snapshot.process_snapshot.record_fixed_size,
);

for (const fieldName of Object.keys(PROC_SNAPSHOT_RECORD_FIELDS)) {
expect(
PROC_SNAPSHOT_RECORD_FIELDS[
fieldName as keyof typeof PROC_SNAPSHOT_RECORD_FIELDS
],
).toEqual(processSnapshotField(fieldName));
}
});
});
Loading
Loading