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
63 changes: 63 additions & 0 deletions crates/kernel/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ pub struct ShmMapping {
pub size: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostTimerCleanup {
pub cancel_alarm: bool,
pub posix_timer_ids: Vec<usize>,
}

/// Per-thread state within a process.
#[derive(Debug, Clone)]
pub struct ThreadInfo {
Expand Down Expand Up @@ -597,6 +603,27 @@ impl Process {
Some(self.shm_mappings.swap_remove(idx))
}

/// Return host timer handles that should be cancelled for this process and
/// clear the Rust timer state that made them live.
pub fn take_host_timer_cleanup(&mut self) -> HostTimerCleanup {
let cancel_alarm = self.alarm_deadline_ns != 0 || self.alarm_interval_ns != 0;
self.alarm_deadline_ns = 0;
self.alarm_interval_ns = 0;

let mut posix_timer_ids = Vec::new();
for (timer_id, slot) in self.posix_timers.iter_mut().enumerate() {
if slot.is_some() {
posix_timer_ids.push(timer_id);
*slot = None;
}
}

HostTimerCleanup {
cancel_alarm,
posix_timer_ids,
}
}

/// True if `tid` names the process's main thread. The main thread's TID
/// equals the process PID (Linux convention) and is not tracked in
/// [`Process::threads`]; per-thread signal state for the main thread lives
Expand Down Expand Up @@ -1000,6 +1027,42 @@ mod tests {
assert_eq!(proc.shm_mapping_at(0x20000), None);
}

#[test]
fn host_timer_cleanup_drains_alarm_and_posix_timer_state() {
let mut proc = Process::new(1);
proc.alarm_deadline_ns = 10;
proc.alarm_interval_ns = 5;
proc.posix_timers.push(Some(PosixTimerState {
clock_id: 0,
sigev_signo: 14,
sigev_value: 0,
interval_sec: 0,
interval_nsec: 0,
value_sec: 1,
value_nsec: 0,
overrun: 0,
}));
proc.posix_timers.push(None);
proc.posix_timers.push(Some(PosixTimerState {
clock_id: 0,
sigev_signo: 15,
sigev_value: 0,
interval_sec: 1,
interval_nsec: 0,
value_sec: 1,
value_nsec: 0,
overrun: 0,
}));

let cleanup = proc.take_host_timer_cleanup();

assert!(cleanup.cancel_alarm);
assert_eq!(cleanup.posix_timer_ids, alloc::vec![0, 2]);
assert_eq!(proc.alarm_deadline_ns, 0);
assert_eq!(proc.alarm_interval_ns, 0);
assert!(proc.posix_timers.iter().all(|slot| slot.is_none()));
}

#[test]
fn spawn_child_basic_inherits_cwd_and_returns_pid() {
use crate::process_table::ProcessTable;
Expand Down
40 changes: 40 additions & 0 deletions crates/kernel/src/wasm_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9091,6 +9091,46 @@ pub extern "C" fn kernel_timer_delete(timerid: i32) -> i32 {
0
}

/// Drain the process-owned timer cleanup list for host-side timer handles.
///
/// Writes `{ u32 cancel_alarm, u32 posix_count, u32 timer_ids[posix_count] }`
/// to `out_ptr`, clears the Rust timer state, and returns `posix_count`.
#[unsafe(no_mangle)]
pub extern "C" fn kernel_take_process_timer_cleanup(
pid: u32,
out_ptr: *mut u8,
max_timer_ids: u32,
) -> i32 {
if out_ptr.is_null() {
return -(Errno::EFAULT as i32);
}

let table = unsafe { &mut *PROCESS_TABLE.0.get() };
let Some(proc) = table.get_mut(pid) else {
return -(Errno::ESRCH as i32);
};

let timer_count = proc
.posix_timers
.iter()
.filter(|slot| slot.is_some())
.count();
if timer_count > max_timer_ids as usize {
return -(Errno::EINVAL as i32);
}

let cleanup = proc.take_host_timer_cleanup();
let out_len = 8 + cleanup.posix_timer_ids.len() * 4;
let out = unsafe { core::slice::from_raw_parts_mut(out_ptr, out_len) };
out[0..4].copy_from_slice(&(cleanup.cancel_alarm as u32).to_le_bytes());
out[4..8].copy_from_slice(&(cleanup.posix_timer_ids.len() as u32).to_le_bytes());
for (idx, timer_id) in cleanup.posix_timer_ids.iter().enumerate() {
let offset = 8 + idx * 4;
out[offset..offset + 4].copy_from_slice(&(*timer_id as u32).to_le_bytes());
}
cleanup.posix_timer_ids.len() as i32
}

/// Called by the host when a repeating POSIX timer fires to increment the overrun counter.
/// This is used for timer_getoverrun() support.
#[unsafe(no_mangle)]
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 @@ -114,7 +114,7 @@ path.
| Done / PR #534 | Rust-owned syscall marshalling descriptors | `crates/shared::host_abi` owns simple pointer-argument descriptors; `dump-abi` generates `SYSCALL_ARGS`; TS host keeps memory copies but reads generated descriptors. | The old TS `SYSCALL_ARGS` table and syscall-number size switches are gone. `poll`/`ppoll`, SysV message prefix, `semop`, and `msgrcv` copy-back adjustments are metadata fields. Nested-pointer syscalls (`readv`/`writev`/preadv/pwritev) stay on dedicated TS paths. | Shared unit tests for descriptor ordering/high-risk sizes/nested-pointer exclusion; xtask ABI tests; `bash scripts/check-abi-version.sh`; generated ABI vitest; host build; kernel lib tests. |
| Done / PR #534 follow-up | Extended host-visible syscall numbers and names | Add Rust/shared metadata for ABI-visible syscall numbers still hardcoded in host TS but not currently in `shared::Syscall`, such as `getrandom`, `clone`, `futex`, `ppoll`, `pselect6`, epoll, `exit_group`, `waitid`, `msync`, preadv/pwritev, mqueue, SysV IPC, `sched_yield`, `fallocate`, timers, and `thread_cancel`. Generate TS bindings, logging names, and snapshot coverage. | Host TS no longer defines literal syscall numbers for this set, and syscall trace names are generated from Rust-owned metadata. Existing `HOST_INTERCEPTED_SYSCALLS` remains separate for fork/exec/spawn because those are caught before normal dispatch. Public behavior unchanged. | Rust metadata uniqueness tests; xtask compatibility tests; `bash scripts/check-abi-version.sh update` + check; generated ABI vitest; host build; kernel lib tests. |
| Done / stacked PR | Rust-defined host adapter manifest | Add a compact Rust-defined manifest describing ABI version, required host adapter protocol version, required/optional exports, worker protocol features, and channel metadata. JS validates it during kernel boot. | Boot fails earlier with clear errors when the host/kernel contract is incompatible. No Worker creation or Wasm instantiation moves out of JS. | Rust manifest serialization tests; ABI snapshot check; vitest boot validation cases; Node/browser worker-entry smoke if boot code changes. |
| In progress / stacked PR | Process lifecycle cleanup consolidation | Rust `ProcessTable` now owns parent lookup, wait-target matching, wait-status derivation, host-crash zombie marking, authorized child reaping, thread-exit clear-tid metadata, SysV shared-memory attachment metadata, and host-bridged TCP listener target policy. TS keeps blocked waiter queues, Worker/memory cleanup, platform timers, process-memory writes/futex wakeups, and the actual TCP server objects because those are host primitives. Remaining audit: thread channel/Worker allocation and free-list lifecycle plus host timer cancellation. | Kernel owns process lifecycle invariants that do not require Worker identity; JS owns Worker termination, memory objects, crash observation, and platform callbacks. | ProcessTable unit tests; fork/exec/spawn/clone/wait tests; crash/trap tests; browser parity smoke when worker entries change. |
| In progress / stacked PR | Process lifecycle cleanup consolidation | Rust `ProcessTable` now owns parent lookup, wait-target matching, wait-status derivation, host-crash zombie marking, authorized child reaping, thread-exit clear-tid metadata, SysV shared-memory attachment metadata, host-bridged TCP listener target policy, and process-owned host timer cleanup metadata. TS keeps blocked waiter queues, Worker/memory cleanup, platform timer handles, process-memory writes/futex wakeups, and the actual TCP server objects because those are host primitives. Remaining audit: thread channel/Worker allocation and free-list lifecycle. | Kernel owns process lifecycle invariants that do not require Worker identity; JS owns Worker termination, memory objects, crash observation, and platform callbacks. | ProcessTable unit tests; fork/exec/spawn/clone/wait tests; crash/trap tests; browser parity smoke when worker entries change. |
| 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. |
Expand Down
68 changes: 54 additions & 14 deletions host/src/kernel-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,20 +1397,7 @@ export class CentralizedKernelWorker {
this.processes.delete(pid);
this.stdinFinite.delete(pid);
this.stdinBuffers.delete(pid);
// Cancel any pending alarm timer for this process
const alarmTimer = this.alarmTimers.get(pid);
if (alarmTimer) {
clearTimeout(alarmTimer);
this.alarmTimers.delete(pid);
}
// Cancel any pending posix timers for this process
for (const [key, entry] of this.posixTimers) {
if (key.startsWith(`${pid}:`)) {
clearTimeout(entry.timeout);
if (entry.interval) clearInterval(entry.interval);
this.posixTimers.delete(key);
}
}
this.cancelProcessHostTimers(pid);
// Cancel any pending sleep timer for this process
const sleepTimer = this.pendingSleeps.get(pid);
if (sleepTimer) {
Expand All @@ -1431,6 +1418,59 @@ export class CentralizedKernelWorker {
this.hostReaped.delete(pid);
}

private cancelProcessHostTimers(pid: number): void {
const cleanup = this.takeRustProcessTimerCleanup(pid);
if (cleanup !== undefined) {
if (cleanup.cancelAlarm) {
const alarmTimer = this.alarmTimers.get(pid);
if (alarmTimer) clearTimeout(alarmTimer);
this.alarmTimers.delete(pid);
}
for (const timerId of cleanup.posixTimerIds) {
const key = `${pid}:${timerId}`;
const entry = this.posixTimers.get(key);
if (entry) {
clearTimeout(entry.timeout);
if (entry.interval) clearInterval(entry.interval);
this.posixTimers.delete(key);
}
}
return;
}

const alarmTimer = this.alarmTimers.get(pid);
if (alarmTimer) {
clearTimeout(alarmTimer);
this.alarmTimers.delete(pid);
}
for (const [key, entry] of this.posixTimers) {
if (key.startsWith(`${pid}:`)) {
clearTimeout(entry.timeout);
if (entry.interval) clearInterval(entry.interval);
this.posixTimers.delete(key);
}
}
}

private takeRustProcessTimerCleanup(pid: number): { cancelAlarm: boolean; posixTimerIds: number[] } | undefined {
const takeCleanup = this.kernelInstance!.exports.kernel_take_process_timer_cleanup as
((pid: number, outPtr: bigint, maxTimerIds: number) => number) | undefined;
if (!takeCleanup) return undefined;

const maxTimerIds = Math.floor((SCRATCH_SIZE - 8) / 4);
const result = takeCleanup(pid, BigInt(this.scratchOffset), maxTimerIds);
if (result < 0) return undefined;

const view = new DataView(this.kernelMemory!.buffer, this.scratchOffset);
const cancelAlarm = view.getUint32(0, true) !== 0;
const count = view.getUint32(4, true);
const posixTimerIds: number[] = [];
for (let i = 0; i < count; i++) {
posixTimerIds.push(view.getUint32(8 + i * 4, true));
}
return { cancelAlarm, posixTimerIds };
}

/**
* Run kernel-side exec setup: close CLOEXEC fds, reset signal handlers.
* Returns 0 on success, negative errno on failure.
Expand Down
41 changes: 41 additions & 0 deletions host/test/process-wait-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,47 @@ describe("Rust-owned process wait lifecycle", () => {
expect(worker.pickListenerTarget(8080)).toEqual({ pid: 44, fd: 7 });
expect(pickTarget).toHaveBeenCalledWith(8080, 0, BigInt(worker.scratchOffset));
});

it("host timer cancellation follows Rust-owned cleanup metadata", () => {
vi.useFakeTimers();
try {
const kernelMemory = createSharedMemory();
const takeCleanup = vi.fn((_pid: number, outPtr: bigint, _maxTimerIds: number) => {
const view = new DataView(kernelMemory.buffer);
view.setUint32(Number(outPtr), 1, true);
view.setUint32(Number(outPtr) + 4, 2, true);
view.setUint32(Number(outPtr) + 8, 4, true);
view.setUint32(Number(outPtr) + 12, 8, true);
return 2;
});
const worker = createWorkerHarness({
kernel_take_process_timer_cleanup: takeCleanup,
});
worker.kernelMemory = kernelMemory;
worker.alarmTimers = new Map([
[11, setTimeout(() => {}, 1000)],
[12, setTimeout(() => {}, 1000)],
]);
worker.posixTimers = new Map([
["11:4", { timeout: setTimeout(() => {}, 1000) }],
["11:8", { timeout: setTimeout(() => {}, 1000), interval: setInterval(() => {}, 1000) }],
["11:9", { timeout: setTimeout(() => {}, 1000) }],
["12:4", { timeout: setTimeout(() => {}, 1000) }],
]);

worker.cancelProcessHostTimers(11);

expect(takeCleanup).toHaveBeenCalledWith(11, BigInt(worker.scratchOffset), expect.any(Number));
expect(worker.alarmTimers.has(11)).toBe(false);
expect(worker.alarmTimers.has(12)).toBe(true);
expect(worker.posixTimers.has("11:4")).toBe(false);
expect(worker.posixTimers.has("11:8")).toBe(false);
expect(worker.posixTimers.has("11:9")).toBe(true);
expect(worker.posixTimers.has("12:4")).toBe(true);
} finally {
vi.useRealTimers();
}
});
});

function createWorkerHarness(exports: Record<string, unknown>): any {
Expand Down
Loading