1288 lines
65 KiB
Zig
1288 lines
65 KiB
Zig
//! User-space processes: loading a user ELF and running it in ring 3. danos is a
|
|
//! microkernel, so this only ever loads *user* binaries — there is no kernel-space
|
|
//! loader; in-kernel code is linked into the kernel image, not loaded here.
|
|
//!
|
|
//! Two entry points:
|
|
//! - `spawnProcess` loads a user ELF (`/system/services/init`, and later servers/drivers)
|
|
//! into a fresh address space and schedules it as a real preemptive ring-3
|
|
//! process on its own page tables. This is the production path.
|
|
//! - `run` executes a raw code blob (the user-pf isolation test program) on the
|
|
//! *current* kernel context via the borrowed-thread path — a minimal probe of
|
|
//! the ring-transition mechanisms, kept for that test.
|
|
//! Both map frames user-accessible with W^X (code RO+X, data RW+NX); the program
|
|
//! talks to the kernel only through the system_call instruction (or the int 0x80
|
|
//! gate). The shared handler is installed once by `init`.
|
|
//!
|
|
//! Borrowed-path caveat (`run` only): it publishes TSS.rsp0 on the *current*
|
|
//! core and uses a single global unwind slot (`user_saved_rsp` in isr.s), so the
|
|
//! caller must disable preemption and only one core may be inside it at a time.
|
|
//! Real processes (`spawnProcess`) have none of these limits — the scheduler
|
|
//! maintains rsp0/CR3 per switch.
|
|
|
|
const std = @import("std");
|
|
const elf = std.elf;
|
|
const boot_handoff = @import("boot-handoff");
|
|
const abi = @import("abi");
|
|
const device_abi = @import("device-abi");
|
|
const parameters = @import("parameters");
|
|
const architecture = @import("architecture");
|
|
const pmm = @import("pmm.zig");
|
|
const scheduler = @import("scheduler.zig");
|
|
const sync = @import("sync.zig");
|
|
const ipc = @import("ipc-synchronous.zig");
|
|
const devices_broker = @import("devices-broker.zig");
|
|
const irq = @import("irq.zig");
|
|
const initial_ramdisk = @import("initial-ramdisk");
|
|
const log = @import("log.zig");
|
|
|
|
const page_size = abi.page_size;
|
|
const SystemCall = abi.SystemCall;
|
|
|
|
/// User virtual addresses. PML4 index 224 — a user-exclusive region, far from
|
|
/// the identity map (low indices) and the vmm test address (index 128), so
|
|
/// setting the U/S bit on its intermediate tables widens no kernel mapping.
|
|
/// An ELF image may occupy [code_virtual, stack_virtual); the stack sits above.
|
|
pub const code_virtual: u64 = 0x0000_7000_0000_0000;
|
|
|
|
/// The stack region, above the image. The page at `stack_virtual` is **never
|
|
/// mapped** — it is the guard page: a process that overflows its stack walks into
|
|
/// it and faults (killing only that process) rather than silently corrupting the
|
|
/// top of its own image. The stack proper is `parameters.user_stack_pages` pages
|
|
/// at [stack_base_virtual, stack_top_virtual), RW + NX, with the System V entry
|
|
/// block (argc/argv) at the very top.
|
|
pub const stack_virtual: u64 = 0x0000_7000_0020_0000; // guard page (unmapped)
|
|
pub const stack_base_virtual: u64 = stack_virtual + page_size;
|
|
pub const stack_top_virtual: u64 = stack_base_virtual + parameters.user_stack_pages * page_size;
|
|
|
|
/// The mmap grant arena: where `mmap` hands out fresh user pages, above the image
|
|
/// and stack but still inside PML4[224] (so no kernel mapping is widened). Each
|
|
/// process bump-allocates from `heap_arena_base` upward via `Task.heap_next`; a
|
|
/// 1 GiB window is far more than any user heap needs today.
|
|
pub const heap_arena_base: u64 = 0x0000_7000_1000_0000;
|
|
pub const heap_arena_end: u64 = heap_arena_base + (1 << 30);
|
|
|
|
/// End of the user (low) canonical half. Any legitimate user pointer is below it;
|
|
/// used to bound the addresses a system_call will dereference on the caller's behalf.
|
|
pub const user_half_end: u64 = 0x0000_8000_0000_0000;
|
|
|
|
/// The MMIO-grant arena: where `mmio_map` places device windows, in PML4[226] —
|
|
/// a user-exclusive region distinct from code/stack/heap (PML4[224]), so mapping
|
|
/// device pages user-accessible widens no kernel mapping. Per-process cursor in
|
|
/// `Task.device_map_next`.
|
|
pub const device_arena_base: u64 = 0x0000_7100_0000_0000;
|
|
pub const device_arena_end: u64 = device_arena_base + (4 << 30);
|
|
|
|
/// The DMA arena: where `dma_alloc` places coherent DMA buffers, in PML4[228] — a
|
|
/// user-exclusive region distinct from the MMIO arena. Unlike MMIO grants these back
|
|
/// real RAM (contiguous frames), so they are reclaimed on teardown. Per-process cursor
|
|
/// in `Task.dma_map_next`.
|
|
pub const dma_arena_base: u64 = 0x0000_7200_0000_0000;
|
|
pub const dma_arena_end: u64 = dma_arena_base + (256 << 20); // 256 MiB per process
|
|
|
|
/// Largest single `mmap` grant, in pages (1 MiB). The user heap grows in small
|
|
/// chunks, so this bound is generous; it also caps the frame scratch array below.
|
|
const maximum_mmap_pages = 256;
|
|
|
|
/// Ceiling on a process's argv entries, including argv[0]. Arguments are spawn
|
|
/// parameters ("you are the driver for device 12"), not bulk data — IPC carries
|
|
/// that — so the bound is small and everything fits the single stack page.
|
|
pub const maximum_arguments = 8;
|
|
|
|
/// Ceiling on the `system_spawn` extra-arguments blob (argv[1..], NUL-separated).
|
|
pub const maximum_argument_bytes = 256;
|
|
|
|
/// Auxiliary-vector entry types (System V AMD64 process entry). Only what the
|
|
/// kernel emits today; a C runtime scans the vector until the null terminator.
|
|
const auxiliary_vector_null: u64 = 0; // AT_NULL — end of the vector
|
|
const auxiliary_vector_page_size: u64 = 6; // AT_PAGESZ
|
|
|
|
// The hand-assembled user program blob (isr.s, .rodata) — the isolation probe.
|
|
const pf_start = @extern([*]const u8, .{ .name = "user_pf_start" });
|
|
const pf_end = @extern([*]const u8, .{ .name = "user_pf_end" });
|
|
|
|
/// The isolation-proof program: reads a kernel-only page, must #PF.
|
|
pub fn pfBlob() []const u8 {
|
|
return pf_start[0 .. @intFromPtr(pf_end) - @intFromPtr(pf_start)];
|
|
}
|
|
|
|
/// What debug_write syscalls produced (accumulated), and the exit system_call's code.
|
|
pub var write_buffer: [256]u8 = undefined;
|
|
pub var write_len: usize = 0;
|
|
pub var write_from_user: bool = false;
|
|
pub var write_count: u64 = 0; // total write syscalls served (for the heartbeat tests)
|
|
pub var exit_code: u64 = 0;
|
|
|
|
/// The initial-ramdisk image, recorded at boot so `system_spawn` can find bundled
|
|
/// binaries by name. Null until `setInitialRamdisk` runs; `system_spawn` then fails
|
|
/// cleanly rather than reaching into unset memory.
|
|
var ramdisk_image: ?[]const u8 = null;
|
|
|
|
/// Record the initial-ramdisk image (the kernel already holds it from the boot
|
|
/// handoff) so a user-space supervisor can `system_spawn` binaries out of it.
|
|
pub fn setInitialRamdisk(image: []const u8) void {
|
|
ramdisk_image = image;
|
|
}
|
|
|
|
/// The system_call surface, dispatched on the saved system_call number (`abi.SystemCall`).
|
|
/// This is the microkernel-minimal set — memory + scheduling only; file/device
|
|
/// I/O will arrive as IPC to user-space servers (docs/syscall.md). The result is
|
|
/// written back into the trap frame, since the entry paths restore user registers
|
|
/// from it. One handler serves both the system_call/sysret and int-0x80 entry paths.
|
|
///
|
|
/// Install it once at boot (before any user code runs) via `init`. Also registers
|
|
/// the scheduler's kill hooks: the scheduler sits below this layer, so finishing a
|
|
/// deferred process_kill (IRQ bindings, IPC handles, the exit notification) is
|
|
/// called back up into here from the tick (see scheduler.reapKillPendingLocked).
|
|
pub fn init() void {
|
|
architecture.setSystemCallHandler(system_call);
|
|
scheduler.terminate_current_hook = terminateCurrentLocked;
|
|
scheduler.reap_task_hook = reapTaskLocked;
|
|
scheduler.timer_tick_hook = timerSweepLocked;
|
|
}
|
|
|
|
/// Return -1 (as an unsigned bit pattern) in the system_call result register.
|
|
fn fail(state: *architecture.CpuState) void {
|
|
architecture.setSystemCallResult(state, @bitCast(@as(i64, -1)));
|
|
}
|
|
|
|
fn system_call(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
const user = t.aspace != 0;
|
|
if (user) {
|
|
// A condemned process (process_kill caught it running) dies at its next
|
|
// kernel entry — before it can spawn, claim, or message anything else.
|
|
if (t.kill_pending) terminateCurrent();
|
|
// Mark the span of this call so the timer tick never tears the task down
|
|
// in the middle of a kernel operation (scheduler.reapKillPendingLocked).
|
|
t.in_system_call = true;
|
|
}
|
|
defer if (user) {
|
|
t.in_system_call = false;
|
|
};
|
|
switch (@as(SystemCall, @enumFromInt(architecture.systemCallNumber(state)))) {
|
|
.exit => {
|
|
exit_code = architecture.systemCallArg(state, 0);
|
|
// A scheduled process tears down fully (terminateCurrent); a borrowed
|
|
// test thread unwinds back to the kernel that entered it.
|
|
if (scheduler.currentIsUserProcess()) {
|
|
scheduler.current().exit_reason = .exited;
|
|
terminateCurrent();
|
|
} else architecture.userExit();
|
|
},
|
|
.yield => {
|
|
scheduler.yield();
|
|
architecture.setSystemCallResult(state, 0);
|
|
},
|
|
.sleep => {
|
|
scheduler.sleep(architecture.systemCallArg(state, 0));
|
|
architecture.setSystemCallResult(state, 0);
|
|
},
|
|
.debug_write => systemDebugWrite(state),
|
|
.mmap => systemMmap(state),
|
|
.munmap => systemMunmap(state),
|
|
.create_ipc_endpoint => systemCreateIpcEndpoint(state),
|
|
.ipc_register => systemIpcRegister(state),
|
|
.ipc_lookup => systemIpcLookup(state),
|
|
.ipc_call => systemIpcCall(state),
|
|
.ipc_reply_wait => systemIpcReplyWait(state),
|
|
.ipc_send => systemIpcSend(state),
|
|
.device_enumerate => systemDeviceEnumerate(state),
|
|
.device_claim => systemDeviceClaim(state),
|
|
.mmio_map => systemMmioMap(state),
|
|
.irq_bind => systemIrqBind(state),
|
|
.irq_ack => systemIrqAck(state),
|
|
.device_register => systemDeviceRegister(state),
|
|
.system_spawn => systemSpawn(state),
|
|
.dma_alloc => systemDmaAlloc(state),
|
|
.dma_free => systemDmaFree(state),
|
|
.msi_bind => systemMsiBind(state),
|
|
.io_read => systemIoRead(state),
|
|
.io_write => systemIoWrite(state),
|
|
.clock => systemClock(state),
|
|
.process_enumerate => systemProcessEnumerate(state),
|
|
.process_kill => systemProcessKill(state),
|
|
.process_exit_reason => systemProcessExitReason(state),
|
|
.process_subscribe => systemProcessSubscribe(state),
|
|
.signal_bind => systemSignalBind(state),
|
|
.process_signal => systemProcessSignal(state),
|
|
.timer_bind => systemTimerBind(state),
|
|
_ => fail(state),
|
|
}
|
|
}
|
|
|
|
/// Return `-errno` in the system_call result register.
|
|
fn failErr(state: *architecture.CpuState, errno: i64) void {
|
|
architecture.setSystemCallResult(state, @bitCast(-errno));
|
|
}
|
|
|
|
/// create_ipc_endpoint() -> handle: allocate an endpoint and install it in the
|
|
/// caller's handle table.
|
|
fn systemCreateIpcEndpoint(state: *architecture.CpuState) void {
|
|
const endpoint = ipc.createIpcEndpoint() orelse return failErr(state, ipc.ENOMEM);
|
|
const h = ipc.installHandle(scheduler.current(), endpoint);
|
|
if (h < 0) {
|
|
ipc.dropRef(endpoint);
|
|
return failErr(state, ipc.ENOSPC);
|
|
}
|
|
architecture.setSystemCallResult(state, @intCast(h));
|
|
}
|
|
|
|
/// ipc_register(service_id, handle): publish the caller's endpoint under a
|
|
/// well-known id so other processes can find it.
|
|
fn systemIpcRegister(state: *architecture.CpuState) void {
|
|
const id: u32 = @truncate(architecture.systemCallArg(state, 0));
|
|
const endpoint = ipc.resolveHandle(scheduler.current(), architecture.systemCallArg(state, 1)) orelse return failErr(state, ipc.EBADF);
|
|
architecture.setSystemCallResult(state, @bitCast(ipc.register(id, endpoint)));
|
|
}
|
|
|
|
/// ipc_lookup(service_id) -> handle: find a published endpoint and install a
|
|
/// handle to it in the caller.
|
|
fn systemIpcLookup(state: *architecture.CpuState) void {
|
|
const id: u32 = @truncate(architecture.systemCallArg(state, 0));
|
|
const endpoint = ipc.lookup(id) orelse return failErr(state, ipc.ENOENT);
|
|
const h = ipc.installHandle(scheduler.current(), endpoint);
|
|
if (h < 0) {
|
|
ipc.dropRef(endpoint);
|
|
return failErr(state, ipc.ENOSPC);
|
|
}
|
|
architecture.setSystemCallResult(state, @intCast(h));
|
|
}
|
|
|
|
/// ipc_call(handle, message_ptr, message_len, reply_ptr, reply_cap) -> reply_len.
|
|
/// Blocks until the server replies; the trap frame lives on this task's kernel
|
|
/// stack, so it survives the block and receives the result on resume.
|
|
fn systemIpcCall(state: *architecture.CpuState) void {
|
|
const endpoint = ipc.resolveHandle(scheduler.current(), architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
|
var received_cap: u64 = abi.no_cap;
|
|
const r = ipc.call(endpoint, architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), architecture.systemCallArg(state, 3), architecture.systemCallArg(state, 4), architecture.systemCallArg(state, 5), &received_cap);
|
|
architecture.setSystemCallResult(state, @bitCast(r));
|
|
architecture.setSystemCallResult3(state, received_cap);
|
|
}
|
|
|
|
/// ipc_reply_wait(handle, reply_ptr, reply_len, receive_ptr, receive_cap) -> receive_len,
|
|
/// with the sender's badge in the secondary result register (rdx).
|
|
fn systemIpcReplyWait(state: *architecture.CpuState) void {
|
|
const endpoint = ipc.resolveHandle(scheduler.current(), architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
|
var badge: u64 = 0;
|
|
var received_cap: u64 = abi.no_cap;
|
|
const r = ipc.replyWait(endpoint, architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), architecture.systemCallArg(state, 3), architecture.systemCallArg(state, 4), architecture.systemCallArg(state, 5), &badge, &received_cap);
|
|
architecture.setSystemCallResult(state, @bitCast(r));
|
|
architecture.setSystemCallResult2(state, badge);
|
|
architecture.setSystemCallResult3(state, received_cap);
|
|
}
|
|
|
|
/// ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an
|
|
/// endpoint's async queue and wake a receiver, without blocking the caller. The async
|
|
/// counterpart of ipc_call — for broadcasts (the input service) where a rendezvous would
|
|
/// let one dead subscriber hang the sender. Delivered through ipc_reply_wait as a
|
|
/// buffered message (badge carries notify_message_bit and the caller's task id).
|
|
fn systemIpcSend(state: *architecture.CpuState) void {
|
|
const me = scheduler.current();
|
|
const endpoint = ipc.resolveHandle(me, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
|
const r = ipc.send(endpoint, me.aspace, architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), me.id);
|
|
architecture.setSystemCallResult(state, @bitCast(r));
|
|
}
|
|
|
|
/// device_enumerate(buffer, maximum) -> total: snapshot the device table into the caller's
|
|
/// buffer (up to `maximum` entries), returning the total device count.
|
|
fn systemDeviceEnumerate(state: *architecture.CpuState) void {
|
|
const buffer_ptr = architecture.systemCallArg(state, 0);
|
|
const maximum = architecture.systemCallArg(state, 1);
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0 or buffer_ptr >= user_half_end) return fail(state);
|
|
const sz = @sizeOf(device_abi.DeviceDescriptor);
|
|
const cap = @min(maximum, (user_half_end - buffer_ptr) / sz); // clamp to the user half
|
|
const out: [*]device_abi.DeviceDescriptor = @ptrFromInt(buffer_ptr);
|
|
architecture.setSystemCallResult(state, devices_broker.enumerate(out[0..@intCast(cap)]));
|
|
}
|
|
|
|
/// device_claim(id) -> 0/-1: take exclusive ownership of a device for this process.
|
|
fn systemDeviceClaim(state: *architecture.CpuState) void {
|
|
if (devices_broker.claim(architecture.systemCallArg(state, 0), scheduler.current().id))
|
|
architecture.setSystemCallResult(state, 0)
|
|
else
|
|
fail(state);
|
|
}
|
|
|
|
/// mmio_map(device_id, resource_index) -> vaddr: map a claimed device's MMIO window into
|
|
/// this address space (strong-uncacheable) and return the register base address.
|
|
/// The claim is the capability — a process can only map hardware it owns.
|
|
fn systemMmioMap(state: *architecture.CpuState) void {
|
|
const device_id = architecture.systemCallArg(state, 0);
|
|
const resource_index = architecture.systemCallArg(state, 1);
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const owner = devices_broker.ownerOf(device_id) orelse return fail(state);
|
|
if (owner != t.id) return fail(state); // not claimed by this process
|
|
const r = devices_broker.resourceOf(device_id, resource_index) orelse return fail(state);
|
|
if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) return fail(state);
|
|
|
|
if (t.device_map_next == 0) t.device_map_next = device_arena_base;
|
|
const first = r.start & ~@as(u64, page_size - 1);
|
|
const last = (r.start + r.len - 1) & ~@as(u64, page_size - 1);
|
|
const pages = (last - first) / page_size + 1;
|
|
const base_v = t.device_map_next;
|
|
if (base_v + pages * page_size > device_arena_end) return fail(state);
|
|
|
|
architecture.mapUserDeviceInto(t.aspace, base_v, r.start, r.len);
|
|
t.device_map_next = base_v + pages * page_size;
|
|
architecture.setSystemCallResult(state, base_v + (r.start & (page_size - 1))); // register base
|
|
}
|
|
|
|
/// Resolve a port-I/O access against the caller's claims. The device must be claimed by
|
|
/// `t`, `resource_index` must name one of its `io_port` resources, and the access
|
|
/// `[offset, offset+width)` must fall wholly inside it. Returns the absolute 16-bit
|
|
/// port, or null if the capability check fails. The claim plus the discovered `io_port`
|
|
/// resource are the capability — exactly like `mmio_map` for memory, so a driver can
|
|
/// only touch the ports its device actually owns, never a raw `in`/`out` to anywhere.
|
|
pub fn resolveIoPort(t: *scheduler.Task, device_id: u64, resource_index: u64, offset: u64, width: u64) ?u16 {
|
|
if (width != 1 and width != 2 and width != 4) return null;
|
|
const owner = devices_broker.ownerOf(device_id) orelse return null;
|
|
if (owner != t.id) return null; // not claimed by this process
|
|
const r = devices_broker.resourceOf(device_id, resource_index) orelse return null;
|
|
if (r.kind != @intFromEnum(device_abi.ResourceKind.io_port)) return null;
|
|
if (offset + width > r.len) return null; // access escapes the claimed port range
|
|
const port = r.start + offset;
|
|
if (port + width > 0x1_0000) return null; // I/O ports are 16-bit
|
|
return @intCast(port);
|
|
}
|
|
|
|
/// io_read(device_id, resource_index, offset, width) -> value: read `width` bytes (1/2/4)
|
|
/// from a port in a claimed device's `io_port` resource. Ring 3 has no direct `in`/`out`
|
|
/// (no TSS I/O bitmap, IOPL never raised), so a legacy driver (PS/2, 16550 UART) reaches
|
|
/// its ports through this claim-gated call — low-rate hardware, so a syscall per access
|
|
/// is fine. See docs/drivers.md.
|
|
fn systemIoRead(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const width = architecture.systemCallArg(state, 3);
|
|
const port = resolveIoPort(t, architecture.systemCallArg(state, 0), architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), width) orelse return fail(state);
|
|
architecture.setSystemCallResult(state, architecture.pioRead(@intCast(width), port));
|
|
}
|
|
|
|
/// io_write(device_id, resource_index, offset, width, value) -> 0: write `value` (low
|
|
/// `width` bytes) to a port in a claimed device's `io_port` resource. Same capability
|
|
/// gate as `io_read`.
|
|
fn systemIoWrite(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const width = architecture.systemCallArg(state, 3);
|
|
const port = resolveIoPort(t, architecture.systemCallArg(state, 0), architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), width) orelse return fail(state);
|
|
architecture.pioWrite(@intCast(width), port, @intCast(architecture.systemCallArg(state, 4)));
|
|
architecture.setSystemCallResult(state, 0);
|
|
}
|
|
|
|
/// dma_alloc(len, flags) -> vaddr (rax), paddr (rdx): grant `len` bytes (rounded up to
|
|
/// whole pages) of DMA-capable memory — physically contiguous, zeroed, pinned, and
|
|
/// strong-uncacheable (coherent) — mapping it into the caller's DMA arena and handing
|
|
/// back both the virtual address to touch and the physical address to program into the
|
|
/// device. This is what `sysMmap` can't do: mmap frames are scattered, cacheable, and
|
|
/// their physical address is never disclosed. `dma_below_4g` caps the physical address
|
|
/// for legacy engines; `dma_write_combining` is accepted but falls back to coherent
|
|
/// until PAT is programmed. See docs/driver-model.md (M14).
|
|
fn systemDmaAlloc(state: *architecture.CpuState) void {
|
|
const len = architecture.systemCallArg(state, 0);
|
|
const flags = architecture.systemCallArg(state, 1);
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0 or len == 0) return fail(state);
|
|
|
|
const pages: usize = @intCast((len + page_size - 1) / page_size);
|
|
const max_phys: u64 = if (flags & abi.dma_below_4g != 0) (@as(u64, 4) << 30) else ~@as(u64, 0);
|
|
const phys = pmm.allocContiguous(pages, max_phys) orelse return fail(state);
|
|
|
|
if (t.dma_map_next == 0) t.dma_map_next = dma_arena_base;
|
|
const base_v = t.dma_map_next;
|
|
if (base_v + pages * page_size > dma_arena_end) {
|
|
for (0..pages) |i| pmm.free(phys + i * page_size); // arena exhausted; give the frames back
|
|
return fail(state);
|
|
}
|
|
|
|
// Zero through the physmap (the frames aren't mapped in the caller yet), then map.
|
|
const kernel_view: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(phys));
|
|
@memset(kernel_view[0 .. pages * page_size], 0);
|
|
architecture.mapUserDmaInto(t.aspace, base_v, phys, pages * page_size);
|
|
|
|
t.dma_map_next = base_v + pages * page_size;
|
|
architecture.setSystemCallResult(state, base_v); // virtual address for the CPU
|
|
architecture.setSystemCallResult2(state, phys); // physical address for the device
|
|
}
|
|
|
|
/// dma_free(vaddr, len) -> 0: release a prior `dma_alloc`. Bounded to the DMA arena so
|
|
/// it can never unmap-and-free the caller's stack, heap, or an MMIO grant; only pages
|
|
/// actually mapped are freed (an unmapped hole is skipped). Teardown also reclaims any
|
|
/// DMA pages left mapped at exit (they carry no `device_grant`, so `freeSubtree` frees
|
|
/// them as ordinary RAM), so a driver that just dies leaks nothing.
|
|
fn systemDmaFree(state: *architecture.CpuState) void {
|
|
const base_v = architecture.systemCallArg(state, 0);
|
|
const len = architecture.systemCallArg(state, 1);
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const pages: usize = @intCast((len + page_size - 1) / page_size);
|
|
if (base_v < dma_arena_base or base_v + pages * page_size > dma_arena_end) return fail(state);
|
|
|
|
for (0..pages) |i| {
|
|
const va = base_v + i * page_size;
|
|
if (architecture.translate(t.aspace, va)) |phys| {
|
|
architecture.unmapUserPageInto(t.aspace, va);
|
|
pmm.free(phys);
|
|
}
|
|
}
|
|
architecture.setSystemCallResult(state, 0);
|
|
}
|
|
|
|
/// device_register(parent_id, descriptor_ptr) -> id: publish a child device below a device
|
|
/// this process has claimed. The bus-driver primitive: a process that owns a bus
|
|
/// enumerates it and hands each device it finds to the table, where a class driver
|
|
/// can claim it.
|
|
///
|
|
/// The kernel copies the descriptor into a kernel local *once* (via the same
|
|
/// physmap-walking path as IPC, so an unmapped user page fails the call rather than
|
|
/// faulting the kernel), then validates and uses that copy — no second read of user
|
|
/// memory, so nothing it checked can change under it. It refuses any child resource
|
|
/// that escapes the parent's windows: a descriptor is a licence to map physical
|
|
/// memory, so a bus may only subdivide what it already holds. `id`/`parent` in the
|
|
/// supplied descriptor are ignored.
|
|
fn systemDeviceRegister(state: *architecture.CpuState) void {
|
|
const parent_id = architecture.systemCallArg(state, 0);
|
|
const descriptor_ptr = architecture.systemCallArg(state, 1);
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
|
|
var descriptor: device_abi.DeviceDescriptor = undefined;
|
|
if (!ipc.copyFromUser(t.aspace, descriptor_ptr, std.mem.asBytes(&descriptor))) return fail(state);
|
|
|
|
const id = devices_broker.register(parent_id, t.id, &descriptor) catch return fail(state);
|
|
architecture.setSystemCallResult(state, id);
|
|
}
|
|
|
|
/// system_spawn(name_ptr, name_len, arguments_ptr, arguments_len, exit_endpoint)
|
|
/// -> the child's process id on success, -1 on failure. Load the binary bundled in
|
|
/// the initial-ramdisk under `name` as a fresh ring-3 process. `name` becomes the
|
|
/// child's argv[0] (and its task name, so a fault report can say which binary
|
|
/// died); `arguments` is an optional NUL-separated blob that becomes argv[1..] —
|
|
/// how a supervisor parameterises what it starts ("you are the driver for device
|
|
/// 12"). 0/0 means no extra arguments. This is the mechanism a user-space
|
|
/// supervisor (the device manager) uses to start a driver it matched: discovery
|
|
/// and policy stay in user space, the kernel only spawns.
|
|
///
|
|
/// The caller is recorded as the child's **supervisor** — the sole holder of the
|
|
/// right to `process_kill` it (docs/process-management.md). `exit_endpoint` (a
|
|
/// handle, or `abi.no_cap` for none) names an endpoint of the caller's to notify
|
|
/// when the child ends, any way it ends — the IRQ-as-IPC pattern reused as the
|
|
/// microkernel's SIGCHLD.
|
|
///
|
|
/// Spawning itself is still ungated — any process may spawn any bundled binary; a
|
|
/// spawn capability belongs here once the model grows one (docs/driver-model.md).
|
|
/// Both buffers are bounds-checked into the user half exactly like `debug_write`,
|
|
/// and an unknown name or a load failure returns -1.
|
|
fn systemSpawn(state: *architecture.CpuState) void {
|
|
const ptr = architecture.systemCallArg(state, 0);
|
|
const len = architecture.systemCallArg(state, 1);
|
|
const arguments_ptr = architecture.systemCallArg(state, 2);
|
|
const arguments_len = architecture.systemCallArg(state, 3);
|
|
const exit_handle = architecture.systemCallArg(state, 4);
|
|
const t = scheduler.current();
|
|
if (len == 0 or len > scheduler.maximum_task_name or ptr >= user_half_end or ptr + len > user_half_end) return fail(state);
|
|
if (arguments_len > maximum_argument_bytes) return fail(state);
|
|
if (arguments_len != 0 and (arguments_ptr >= user_half_end or arguments_ptr + arguments_len > user_half_end)) return fail(state);
|
|
const exit_endpoint: ?*ipc.Endpoint = if (exit_handle == abi.no_cap)
|
|
null
|
|
else
|
|
ipc.resolveHandle(t, exit_handle) orelse return failErr(state, ipc.EBADF);
|
|
const image = ramdisk_image orelse return fail(state);
|
|
const rd = initial_ramdisk.Reader.init(image) orelse return fail(state);
|
|
|
|
const name = @as([*]const u8, @ptrFromInt(ptr))[0..len];
|
|
var argv: [maximum_arguments][]const u8 = undefined;
|
|
argv[0] = name;
|
|
var argc: usize = 1;
|
|
if (arguments_len != 0) {
|
|
const blob = @as([*]const u8, @ptrFromInt(arguments_ptr))[0..arguments_len];
|
|
var pieces = std.mem.tokenizeScalar(u8, blob, 0);
|
|
while (pieces.next()) |piece| {
|
|
if (argc == maximum_arguments) return fail(state);
|
|
argv[argc] = piece;
|
|
argc += 1;
|
|
}
|
|
}
|
|
|
|
var i: u32 = 0;
|
|
while (i < rd.count) : (i += 1) {
|
|
const item = rd.entry(i) orelse continue;
|
|
if (!std.mem.eql(u8, item.name, name)) continue;
|
|
const child = spawnProcessSupervised(item.blob, 4, argv[0..argc], t.id, exit_endpoint) catch return fail(state);
|
|
architecture.setSystemCallResult(state, child);
|
|
return;
|
|
}
|
|
fail(state); // no bundled binary by that name
|
|
}
|
|
|
|
/// process_enumerate(buffer, maximum) -> total: snapshot the task table into the
|
|
/// caller's buffer (up to `maximum` `abi.ProcessDescriptor` entries), returning
|
|
/// the total live-task count — the exact shape of `device_enumerate`, so a `ps`
|
|
/// is a user program over a snapshot, not a kernel service. Read-only and
|
|
/// ungated: what is running is not a secret between cooperating bring-up
|
|
/// processes.
|
|
fn systemProcessEnumerate(state: *architecture.CpuState) void {
|
|
const buffer_ptr = architecture.systemCallArg(state, 0);
|
|
const maximum = architecture.systemCallArg(state, 1);
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0 or buffer_ptr >= user_half_end) return fail(state);
|
|
const sz = @sizeOf(abi.ProcessDescriptor);
|
|
const cap = @min(maximum, (user_half_end - buffer_ptr) / sz); // clamp to the user half
|
|
const out: [*]abi.ProcessDescriptor = @ptrFromInt(buffer_ptr);
|
|
architecture.setSystemCallResult(state, scheduler.enumerate(out[0..@intCast(cap)]));
|
|
}
|
|
|
|
/// process_kill(id) -> 0 / -ESRCH / -EPERM: end the process `id`. Only its
|
|
/// supervisor — the process that spawned it — may do so; the supervision link is
|
|
/// the kill capability, so no user/permission model is needed and a stray id
|
|
/// cannot be a weapon (ids are never reused, so a stale one just misses).
|
|
fn systemProcessKill(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const id = architecture.systemCallArg(state, 0);
|
|
if (id > std.math.maxInt(u32)) return failErr(state, ipc.ESRCH);
|
|
const r = killProcess(t.id, @intCast(id));
|
|
architecture.setSystemCallResult(state, @bitCast(r));
|
|
}
|
|
|
|
/// Processes killed by a CPU fault rather than a clean exit. Evidence for the
|
|
/// fault-recovery test, and a health signal a supervisor can consult later.
|
|
pub var fault_kill_count: u64 = 0;
|
|
|
|
/// Release everything a dying task holds and tell its supervisor — the shared
|
|
/// half of every path out of a process: clean exit, fault kill, and process_kill
|
|
/// (both the immediate reap and the deferred tick-time terminate). The order
|
|
/// matters:
|
|
/// - IRQ bindings are dropped before the handle table closes: dropping the last
|
|
/// endpoint reference destroys the Endpoint, and a still-bound GSI would have an
|
|
/// ISR call notifyFromIsr on freed memory the next time the device fired.
|
|
/// `releaseOwner` also leaves the line masked, so a dead driver's device goes
|
|
/// quiet rather than storming. (It drops MSI vectors by the same owner sweep.)
|
|
/// - Device claims are released with the IRQ bindings, so a restarted driver can
|
|
/// claim the same hardware again — the cleanup half of process-lifecycle.md's
|
|
/// iron rule 1. Claims hold no pointers, so ordering is free; they go here so
|
|
/// the exit notification (below, last) observes a fully-released child.
|
|
/// - A client this task still owes a reply to (it died between receive and reply)
|
|
/// is failed with -EPEER rather than left blocked forever — a dead server must
|
|
/// not hang its callers.
|
|
/// - The task is unlinked from wherever IPC parked it (an endpoint's sender FIFO,
|
|
/// a receive wait queue, or a server's owed-reply slot) *before* the handles
|
|
/// close, so nothing ever dequeues a dangling pointer. These are no-ops for a
|
|
/// running task ending itself; they matter when process_kill reaps a blocked one.
|
|
/// - The exit notification is posted last, once the process can no longer act, so
|
|
/// a supervisor that receives it observes a fully-released child. The endpoint
|
|
/// reference taken at spawn is dropped with it.
|
|
/// Precondition: the big kernel lock is held.
|
|
fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
|
recordExitLocked(t);
|
|
irq.releaseOwner(t.id);
|
|
devices_broker.releaseAllOwnedBy(t.id);
|
|
// The dying task's signal endpoint and one-shot timers go with it.
|
|
if (t.signal_endpoint) |raw| {
|
|
ipc.dropRef(@ptrCast(@alignCast(raw)));
|
|
t.signal_endpoint = null;
|
|
}
|
|
t.pending_signals = 0;
|
|
for (&one_shot_timers) |*slot| {
|
|
if (slot.*) |timer| {
|
|
if (timer.owner == t.id) {
|
|
ipc.dropRef(timer.endpoint);
|
|
slot.* = null;
|
|
}
|
|
}
|
|
}
|
|
// A dead subscriber's own subscriptions go first: it must not hear about
|
|
// itself, and the slots' endpoint references drop with it.
|
|
for (&exit_subscribers) |*slot| {
|
|
if (slot.*) |subscriber| {
|
|
if (subscriber.owner == t.id) {
|
|
ipc.dropRef(subscriber.endpoint);
|
|
slot.* = null;
|
|
}
|
|
}
|
|
}
|
|
if (t.ipc_client) |client| {
|
|
t.ipc_client = null;
|
|
client.ipc_status = -ipc.EPEER;
|
|
scheduler.readyLocked(client); // its blocked `call` now returns the error
|
|
}
|
|
ipc.abandonSenderLocked(t);
|
|
scheduler.removeFromWaitQueueLocked(t);
|
|
scheduler.forgetIpcClientLocked(t);
|
|
ipc.closeHandles(t);
|
|
// Publish the exit to every subscriber (docs/process-lifecycle.md): the same
|
|
// badge encoding as the supervisor's notification, and equally late, so a
|
|
// subscriber also observes a fully-released child.
|
|
for (&exit_subscribers) |*slot| {
|
|
if (slot.*) |subscriber| ipc.notifyLocked(subscriber.endpoint, abi.notify_exit_bit | t.id);
|
|
}
|
|
if (t.exit_endpoint) |raw| {
|
|
const endpoint: *ipc.Endpoint = @ptrCast(@alignCast(raw));
|
|
t.exit_endpoint = null;
|
|
ipc.notifyLocked(endpoint, abi.notify_exit_bit | t.id);
|
|
ipc.dropRef(endpoint);
|
|
}
|
|
}
|
|
|
|
/// Tear down the current user process and reschedule; never returns. Shared by the
|
|
/// exit system call and the fault path (`killCurrentProcess`). See
|
|
/// `releaseTaskResourcesLocked` for what is released, and in what order.
|
|
pub fn terminateCurrent() noreturn {
|
|
_ = sync.enter(); // handed off through the exit switch, released by the resumed task
|
|
terminateCurrentLocked();
|
|
}
|
|
|
|
/// The body of `terminateCurrent` for a caller that already holds the big kernel
|
|
/// lock — the scheduler's tick calls this (via `terminate_current_hook`) to finish
|
|
/// a deferred process_kill on its own core's current task. Never returns; the
|
|
/// tick's abandoned interrupt frame is fine (the LAPIC was acknowledged before the
|
|
/// tick hook ran), exactly as on the fault path.
|
|
fn terminateCurrentLocked() noreturn {
|
|
releaseTaskResourcesLocked(scheduler.current());
|
|
scheduler.exitUserLocked();
|
|
}
|
|
|
|
/// Reap a condemned task that is NOT running on any core (ready or blocked — and
|
|
/// it cannot start running: state changes need the lock we hold). The other half
|
|
/// of a deferred process_kill, called by the scheduler's tick (via
|
|
/// `reap_task_hook`) and directly by `killProcess` for targets caught off-CPU.
|
|
/// Precondition: the big kernel lock is held.
|
|
fn reapTaskLocked(t: *scheduler.Task) void {
|
|
releaseTaskResourcesLocked(t);
|
|
scheduler.removeFromReadyQueueLocked(t); // no-op unless it was ready in a queue
|
|
scheduler.destroyTaskLocked(t);
|
|
}
|
|
|
|
/// Kill process `target_id` on behalf of `caller_id` — the kernel half of the
|
|
/// process_kill system call. Returns 0, -ESRCH (no such live process — kernel
|
|
/// tasks are not killable processes and stale ids miss, since ids are never
|
|
/// reused), or -EPERM (the caller is not the target's supervisor).
|
|
///
|
|
/// A target that is ready or blocked is reaped on the spot. One that is running
|
|
/// on another core cannot be torn down mid-instruction, so it is condemned
|
|
/// (`kill_pending`) and dies at its next system_call entry, block, or timer tick
|
|
/// — like a Unix signal, delivery is prompt but asynchronous. Either way the
|
|
/// call returns 0: the kill is accepted and irrevocable.
|
|
pub fn killProcess(caller_id: u32, target_id: u32) i64 {
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
const target = scheduler.taskByIdLocked(target_id) orelse return -ipc.ESRCH;
|
|
if (target.aspace == 0) return -ipc.ESRCH; // kernel tasks are not processes
|
|
if (target.supervisor != caller_id) return -ipc.EPERM;
|
|
target.exit_reason = .killed;
|
|
if (target.state == .running) {
|
|
target.kill_pending = true;
|
|
} else {
|
|
reapTaskLocked(target);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Kill the current user process in response to a CPU fault it raised in ring 3.
|
|
/// The fault is confined to the process — the kernel trapped it on the task's own
|
|
/// kernel stack and is intact — so everything the process held is reclaimed and the
|
|
/// core reschedules. The system keeps running; only the faulting process dies
|
|
/// (docs/resilience.md: fault -> kill -> continue). `reason` is the fault class
|
|
/// (from the vector), recorded for the supervisor's `process_exit_reason`.
|
|
pub fn killCurrentProcess(reason: abi.ExitReason) noreturn {
|
|
scheduler.current().exit_reason = reason;
|
|
fault_kill_count += 1;
|
|
terminateCurrent();
|
|
}
|
|
|
|
/// The bounded record of recent deaths, for `process_exit_reason`: ids are never
|
|
/// reused, so a ring keyed by id is enough — a record evicted by wraparound reads
|
|
/// as -ESRCH, the same as an id that never lived, which a supervisor treats as
|
|
/// "too late to ask". Written under the big kernel lock by the reap.
|
|
const exit_record_capacity = 64;
|
|
const ExitRecord = struct { id: u32 = 0, supervisor: u32 = 0, reason: abi.ExitReason = .exited, valid: bool = false };
|
|
var exit_records: [exit_record_capacity]ExitRecord = .{ExitRecord{}} ** exit_record_capacity;
|
|
var exit_record_next: usize = 0;
|
|
|
|
/// Record a dying task's (id, supervisor, reason) — called by the reap before the
|
|
/// exit notification is posted, so a supervisor that hears the notification can
|
|
/// always still query the reason. Precondition: the big kernel lock is held.
|
|
fn recordExitLocked(t: *scheduler.Task) void {
|
|
exit_records[exit_record_next] = .{ .id = t.id, .supervisor = t.supervisor, .reason = t.exit_reason, .valid = true };
|
|
exit_record_next = (exit_record_next + 1) % exit_record_capacity;
|
|
}
|
|
|
|
/// How dead process `id` ended, for `caller` — the kernel half of the
|
|
/// process_exit_reason system call. Returns the ExitReason value, -ESRCH (never
|
|
/// lived, still alive, or evicted from the ring), or -EPERM (the caller was not
|
|
/// its supervisor — the same authority gate as process_kill).
|
|
pub fn exitReasonOf(caller_id: u32, target_id: u32) i64 {
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
for (&exit_records) |*record| {
|
|
if (record.valid and record.id == target_id) {
|
|
if (record.supervisor != caller_id) return -ipc.EPERM;
|
|
return @intFromEnum(record.reason);
|
|
}
|
|
}
|
|
return -ipc.ESRCH;
|
|
}
|
|
|
|
/// The published exit events' subscribers (docs/process-lifecycle.md "Who learns
|
|
/// of a death"): stateful services — the VFS's file handles, input's
|
|
/// subscriptions — that must release what a dead client held and cannot learn it
|
|
/// any other way (a client that simply never calls again looks like silence).
|
|
/// Bounded like every kernel table; each entry holds its own endpoint reference.
|
|
const exit_subscriber_capacity = 8;
|
|
const ExitSubscriber = struct { endpoint: *ipc.Endpoint, owner: u32 };
|
|
var exit_subscribers: [exit_subscriber_capacity]?ExitSubscriber = .{null} ** exit_subscriber_capacity;
|
|
|
|
/// process_subscribe(endpoint): subscribe the caller's endpoint to published exit
|
|
/// events. Ungated, like process_enumerate — what is running (and dying) is not a
|
|
/// secret between cooperating processes. -ENOSPC when the table is full.
|
|
fn systemProcessSubscribe(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
for (&exit_subscribers) |*slot| {
|
|
if (slot.* == null) {
|
|
endpoint.refcount += 1; // the slot's own reference, dropped on unsubscribe-by-death
|
|
slot.* = .{ .endpoint = endpoint, .owner = t.id };
|
|
return architecture.setSystemCallResult(state, 0);
|
|
}
|
|
}
|
|
failErr(state, ipc.ENOSPC);
|
|
}
|
|
|
|
/// signal_bind(endpoint): nominate where this process's signals arrive — the
|
|
/// IRQ-as-IPC pattern a fourth time (docs/process-lifecycle.md). Replacing a
|
|
/// binding drops the old reference; signals that pended while unbound are
|
|
/// delivered immediately on bind, coalesced into one notification.
|
|
fn systemSignalBind(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
if (t.signal_endpoint) |raw| ipc.dropRef(@ptrCast(@alignCast(raw)));
|
|
endpoint.refcount += 1;
|
|
t.signal_endpoint = @ptrCast(endpoint);
|
|
if (t.pending_signals != 0) {
|
|
ipc.notifyLocked(endpoint, abi.notify_signal_bit | t.pending_signals);
|
|
t.pending_signals = 0;
|
|
}
|
|
architecture.setSystemCallResult(state, 0);
|
|
}
|
|
|
|
/// process_signal(id, signal): post a signal — a one-way, coalescing statement,
|
|
/// never a question (docs/process-lifecycle.md). The authority gate is the
|
|
/// supervision link, like kill; a process may also signal itself. Unbound
|
|
/// targets accumulate the signal in their pending mask.
|
|
fn systemProcessSignal(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const id = architecture.systemCallArg(state, 0);
|
|
const signal = architecture.systemCallArg(state, 1);
|
|
if (id > std.math.maxInt(u32)) return failErr(state, ipc.ESRCH);
|
|
if (signal > 31) return failErr(state, ipc.EBADF); // not a Signal bit position
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
const target = scheduler.taskByIdLocked(@intCast(id)) orelse return failErr(state, ipc.ESRCH);
|
|
if (target.aspace == 0) return failErr(state, ipc.ESRCH);
|
|
if (target.supervisor != t.id and target.id != t.id) return failErr(state, ipc.EPERM);
|
|
target.pending_signals |= @as(u32, 1) << @intCast(signal);
|
|
if (target.signal_endpoint) |raw| {
|
|
const endpoint: *ipc.Endpoint = @ptrCast(@alignCast(raw));
|
|
ipc.notifyLocked(endpoint, abi.notify_signal_bit | target.pending_signals);
|
|
target.pending_signals = 0;
|
|
}
|
|
architecture.setSystemCallResult(state, 0);
|
|
}
|
|
|
|
/// The one-shot timers of timer_bind: the missing timed wait. A service arms a
|
|
/// deadline and keeps serving; the expiry arrives in the same replyWait as
|
|
/// everything else (notify_timer_bit). What stop-sequence escalation, hello
|
|
/// deadlines, and restart backoff are built from — and later, `alarm`.
|
|
const timer_capacity = 16;
|
|
const OneShotTimer = struct { deadline: u64, endpoint: *ipc.Endpoint, owner: u32 };
|
|
var one_shot_timers: [timer_capacity]?OneShotTimer = .{null} ** timer_capacity;
|
|
|
|
/// Sweep expired timers — hung on scheduler.timer_tick_hook, so it runs on every
|
|
/// tick with the big kernel lock held, like the sleeper wake it rides beside.
|
|
fn timerSweepLocked() void {
|
|
const now = architecture.millis();
|
|
for (&one_shot_timers) |*slot| {
|
|
if (slot.*) |timer| {
|
|
if (now >= timer.deadline) {
|
|
ipc.notifyLocked(timer.endpoint, abi.notify_timer_bit);
|
|
ipc.dropRef(timer.endpoint);
|
|
slot.* = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// timer_bind(endpoint, ms): arm a one-shot timer. -ENOSPC when the table is full.
|
|
fn systemTimerBind(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
|
const ms = architecture.systemCallArg(state, 1);
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
for (&one_shot_timers) |*slot| {
|
|
if (slot.* == null) {
|
|
endpoint.refcount += 1;
|
|
slot.* = .{ .deadline = architecture.millis() + ms, .endpoint = endpoint, .owner = t.id };
|
|
return architecture.setSystemCallResult(state, 0);
|
|
}
|
|
}
|
|
failErr(state, ipc.ENOSPC);
|
|
}
|
|
|
|
fn systemProcessExitReason(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const id = architecture.systemCallArg(state, 0);
|
|
if (id > std.math.maxInt(u32)) return failErr(state, ipc.ESRCH);
|
|
const r = exitReasonOf(t.id, @intCast(id));
|
|
architecture.setSystemCallResult(state, @bitCast(r));
|
|
}
|
|
|
|
/// Resolve `(device_id, resource_index)` to a GSI this process is entitled to bind, or null.
|
|
/// The two checks are the whole security story: the device must be *claimed* by the
|
|
/// caller, and the resource must be one of that device's `irq` resources as recorded
|
|
/// by discovery. Neither a raw GSI nor an unclaimed device can get through — which
|
|
/// is why irq_bind takes a resource index and not an interrupt number.
|
|
fn ownedGsi(t: *scheduler.Task, device_id: u64, resource_index: u64) ?u32 {
|
|
const owner = devices_broker.ownerOf(device_id) orelse return null;
|
|
if (owner != t.id) return null;
|
|
const r = devices_broker.resourceOf(device_id, resource_index) orelse return null;
|
|
if (r.kind != @intFromEnum(device_abi.ResourceKind.irq)) return null;
|
|
if (r.start >= irq.maximum_gsi) return null;
|
|
return @intCast(r.start);
|
|
}
|
|
|
|
/// irq_bind(device_id, resource_index, endpoint) -> 0/-1: deliver that device's IRQ to the
|
|
/// endpoint as an asynchronous IPC notification. The driver then blocks in
|
|
/// IPC_ReplyWait and is woken by the ISR; see system/kernel/irq.zig for the cycle.
|
|
fn systemIrqBind(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const gsi = ownedGsi(t, architecture.systemCallArg(state, 0), architecture.systemCallArg(state, 1)) orelse
|
|
return fail(state);
|
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 2)) orelse return fail(state);
|
|
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
irq.bind(gsi, endpoint, t.id) catch return fail(state);
|
|
architecture.setSystemCallResult(state, 0);
|
|
}
|
|
|
|
/// msi_bind(device_id, endpoint_handle) -> address (rax), data (rdx): set up
|
|
/// Message-Signalled Interrupts for a claimed device. The kernel allocates a vector,
|
|
/// binds it to `endpoint`, and hands back the (address, data) the driver programs into
|
|
/// its own MSI capability (found via its ECAM config space, resource 0). Unlike
|
|
/// `irq_bind` there is no GSI, no sharing, and no ack — MSI is edge-triggered. The
|
|
/// claim is the capability. See docs/driver-model.md (M15).
|
|
fn systemMsiBind(state: *architecture.CpuState) void {
|
|
const device_id = architecture.systemCallArg(state, 0);
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const owner = devices_broker.ownerOf(device_id) orelse return fail(state);
|
|
if (owner != t.id) return fail(state); // not claimed by this process
|
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 1)) orelse return failErr(state, ipc.EBADF);
|
|
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
const vector = irq.msiBind(endpoint, t.id) catch return fail(state);
|
|
// x86 MSI: the address routes to the BSP's LAPIC (physical destination, fixed
|
|
// delivery — dest field 0); the data carries the vector.
|
|
architecture.setSystemCallResult(state, abi.msi_address_base);
|
|
architecture.setSystemCallResult2(state, vector);
|
|
}
|
|
|
|
/// irq_ack(device_id, resource_index) -> 0/-1: re-arm a bound IRQ. The ISR left the line
|
|
/// masked (it could not quiet the device — that's this driver's job), so nothing
|
|
/// more arrives until the driver says it has serviced the hardware.
|
|
fn systemIrqAck(state: *architecture.CpuState) void {
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state);
|
|
const gsi = ownedGsi(t, architecture.systemCallArg(state, 0), architecture.systemCallArg(state, 1)) orelse
|
|
return fail(state);
|
|
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
if (irq.ack(gsi)) architecture.setSystemCallResult(state, 0) else fail(state);
|
|
}
|
|
|
|
/// Whether the debug_write stream sits at the start of a line — the last emitted
|
|
/// byte was a newline (true at boot: nothing emitted yet). Guarded by the kernel
|
|
/// lock in `systemDebugWrite`, like the stream it describes.
|
|
var write_at_line_start: bool = true;
|
|
|
|
/// debug_write(ptr, len): copy bytes from user memory into the kernel log.
|
|
/// A bring-up diagnostic — real output goes through the VFS/console later.
|
|
///
|
|
/// The pointer must lie in the user (low) half, so kernel addresses and
|
|
/// non-canonical values fall outside it and the read below can't be steered at
|
|
/// kernel data. Length is checked first so the upper-bound add can't overflow.
|
|
/// Known gap (fine for trusted user code): a pointer into an *unmapped* hole in
|
|
/// the user half passes the check and the read #PFs -> on_fault halts — a
|
|
/// self-DoS, not an isolation break. Fault-recovering copy-in is a later item.
|
|
///
|
|
/// The emit runs under the kernel lock, so a message is atomic on the wire — two
|
|
/// processes writing from different cores can interleave *messages*, never bytes.
|
|
/// The "DANOS-INIT: " marker is emitted only at the start of a line (not per
|
|
/// call), so a process may assemble a line from several writes without the marker
|
|
/// (or, with the lock, another byte) landing in the middle. Cleanly-terminated
|
|
/// lines from concurrent writers stay whole either way.
|
|
fn systemDebugWrite(state: *architecture.CpuState) void {
|
|
const ptr = architecture.systemCallArg(state, 0);
|
|
const len = architecture.systemCallArg(state, 1);
|
|
if (len <= write_buffer.len and ptr < user_half_end and ptr + len <= user_half_end) {
|
|
const source: [*]const u8 = @ptrFromInt(ptr);
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
@memcpy(write_buffer[0..len], source[0..len]); // keep the latest message
|
|
write_len = len;
|
|
write_from_user = architecture.fromUser(state);
|
|
write_count += 1;
|
|
log.write(source[0..len]);
|
|
if (len != 0) write_at_line_start = source[len - 1] == '\n';
|
|
architecture.setSystemCallResult(state, len);
|
|
} else {
|
|
fail(state);
|
|
}
|
|
}
|
|
|
|
/// mmap(len, prot) -> base: grant `len` bytes (rounded up to whole pages) of
|
|
/// fresh, zeroed, writable+NX memory in the caller's mmap arena, and return the
|
|
/// base virtual address. `prot` is accepted but not yet honoured (grants are
|
|
/// always RW+NX; W^X for user code stays with the ELF loader). Failure returns
|
|
/// -1. The user-space allocator (lib `runtime`) carves these pages into malloc blocks.
|
|
fn systemMmap(state: *architecture.CpuState) void {
|
|
const len = architecture.systemCallArg(state, 0);
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0) return fail(state); // not a user process — nothing to map into
|
|
const pages = (len + page_size - 1) / page_size;
|
|
if (pages == 0 or pages > maximum_mmap_pages) return fail(state);
|
|
|
|
if (t.heap_next == 0) t.heap_next = heap_arena_base; // seed the arena lazily
|
|
const base = t.heap_next;
|
|
if (base + pages * page_size > heap_arena_end) return fail(state); // arena exhausted
|
|
|
|
// Reserve all frames up front so a mid-way exhaustion rolls back cleanly
|
|
// (no partially-mapped grant leaks into the address space).
|
|
var frames: [maximum_mmap_pages]u64 = undefined;
|
|
var got: usize = 0;
|
|
while (got < pages) : (got += 1) {
|
|
frames[got] = pmm.alloc() orelse {
|
|
for (frames[0..got]) |f| pmm.free(f);
|
|
return fail(state);
|
|
};
|
|
}
|
|
|
|
for (frames[0..pages], 0..) |frame, i| {
|
|
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frame));
|
|
@memset(destination[0..page_size], 0); // hand out zeroed memory
|
|
architecture.mapUserPageInto(t.aspace, base + i * page_size, frame, true, false); // RW + NX
|
|
}
|
|
t.heap_next = base + pages * page_size;
|
|
architecture.setSystemCallResult(state, base);
|
|
}
|
|
|
|
/// munmap(base, len): release a range previously handed out by `mmap`. Unmaps
|
|
/// each page and frees its frame. The arena is a bump allocator, so the virtual
|
|
/// range is not recycled (the user-space allocator reuses freed *blocks* itself);
|
|
/// this just returns the physical frames to the kernel. Returns 0, or -1 if the
|
|
/// range is not page-aligned or lies outside the arena.
|
|
fn systemMunmap(state: *architecture.CpuState) void {
|
|
const base = architecture.systemCallArg(state, 0);
|
|
const len = architecture.systemCallArg(state, 1);
|
|
const t = scheduler.current();
|
|
if (t.aspace == 0 or base % page_size != 0) return fail(state);
|
|
const pages = (len + page_size - 1) / page_size;
|
|
if (base < heap_arena_base or base + pages * page_size > heap_arena_end) return fail(state);
|
|
|
|
for (0..pages) |i| {
|
|
const va = base + i * page_size;
|
|
if (architecture.translate(t.aspace, va)) |physical| {
|
|
architecture.unmapUserPageInto(t.aspace, va);
|
|
pmm.free(physical);
|
|
}
|
|
}
|
|
architecture.setSystemCallResult(state, 0);
|
|
}
|
|
|
|
/// Reset the recorded system_call evidence before a user-mode run.
|
|
fn resetRecords() void {
|
|
write_len = 0;
|
|
write_from_user = false;
|
|
write_count = 0;
|
|
exit_code = 0;
|
|
}
|
|
|
|
pub const RunError = error{ ProgramTooBig, OutOfMemory };
|
|
|
|
/// Map `blob` at code_virtual with a fresh user stack, drop to ring 3, and return
|
|
/// once the program exits via system_call 0. See the migration caveat in the module
|
|
/// doc. A program that faults instead never returns (on_fault halts the core).
|
|
pub fn run(blob: []const u8) RunError!void {
|
|
if (blob.len > page_size) return error.ProgramTooBig;
|
|
const code_frame = pmm.alloc() orelse return error.OutOfMemory;
|
|
const stack_frame = pmm.alloc() orelse {
|
|
pmm.free(code_frame);
|
|
return error.OutOfMemory;
|
|
};
|
|
|
|
// Fill the code frame through the physmap (supervisor RW): the user-facing
|
|
// mapping is read-only, and this also sidesteps CR0.WP/SMAP. The tail is
|
|
// padded with int3 so a stray jump traps instead of sliding.
|
|
const code: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(code_frame));
|
|
@memcpy(code[0..blob.len], blob);
|
|
@memset(code[blob.len..page_size], 0xCC);
|
|
|
|
architecture.mapUserPage(code_virtual, code_frame, false, true); // RO + X
|
|
architecture.mapUserPage(stack_base_virtual, stack_frame, true, false); // RW + NX (one page; the probe barely stacks)
|
|
resetRecords();
|
|
|
|
architecture.enterUser(scheduler.currentCpuIndex(), code_virtual, stack_base_virtual + page_size);
|
|
|
|
// Back via the exit system_call; the interrupt gate left IF clear.
|
|
architecture.enableInterrupts();
|
|
architecture.unmapPage(code_virtual);
|
|
architecture.unmapPage(stack_base_virtual);
|
|
pmm.free(code_frame);
|
|
pmm.free(stack_frame);
|
|
}
|
|
|
|
// --- user ELF loading (/system/services/init) ------------------------------------------
|
|
|
|
pub const InitError = error{
|
|
BadElf, // malformed/inapplicable image (magic, class, machine, type, bounds)
|
|
BadSegment, // PT_LOAD unaligned, out of the user region, W&X, or overlapping
|
|
BadEntry, // e_entry not inside an executable segment
|
|
BadArguments, // no argv[0], too many entries, or too many bytes for the entry stack
|
|
ProgramTooBig, // more pages than the loader's budget
|
|
OutOfMemory,
|
|
};
|
|
|
|
const maximum_segments = 16;
|
|
const maximum_pages = 256; // 1 MiB loader budget; the user region caps at 2 MiB anyway
|
|
|
|
const Segment = struct {
|
|
vaddr: u64,
|
|
memsz: u64,
|
|
filesz: u64,
|
|
off: u64,
|
|
writable: bool,
|
|
executable: bool,
|
|
|
|
fn pages(self: Segment) u64 {
|
|
return (self.memsz + page_size - 1) / page_size;
|
|
}
|
|
};
|
|
|
|
/// Parse and validate every PT_LOAD before touching memory. Bounds are checked
|
|
/// against the image and the user region; segments must be page-aligned,
|
|
/// non-overlapping, and W^X (R-only is fine — linkers may emit a headers-only
|
|
/// segment, mapped RO+NX).
|
|
fn parseSegments(image: []const u8, segs: *[maximum_segments]Segment) InitError!struct { count: usize, entry: u64 } {
|
|
if (image.len < @sizeOf(elf.Elf64_Ehdr)) return error.BadElf;
|
|
const ehdr = std.mem.bytesToValue(elf.Elf64_Ehdr, image[0..@sizeOf(elf.Elf64_Ehdr)]);
|
|
if (!std.mem.eql(u8, image[0..4], "\x7fELF")) return error.BadElf;
|
|
if (image[elf.EI_CLASS] != elf.ELFCLASS64) return error.BadElf;
|
|
if (ehdr.e_machine != .X86_64) return error.BadElf;
|
|
if (ehdr.e_type != .EXEC) return error.BadElf; // a PIE would need relocation
|
|
if (ehdr.e_phentsize < @sizeOf(elf.Elf64_Phdr)) return error.BadElf;
|
|
if (ehdr.e_phnum > maximum_segments) return error.BadElf;
|
|
const ph_bytes = @as(u64, ehdr.e_phnum) * ehdr.e_phentsize;
|
|
if (ehdr.e_phoff > image.len or ph_bytes > image.len - ehdr.e_phoff) return error.BadElf;
|
|
|
|
var count: usize = 0;
|
|
var total_pages: u64 = 0;
|
|
for (0..ehdr.e_phnum) |i| {
|
|
const off = ehdr.e_phoff + i * ehdr.e_phentsize;
|
|
const phdr = std.mem.bytesToValue(elf.Elf64_Phdr, image[off..][0..@sizeOf(elf.Elf64_Phdr)]);
|
|
if (phdr.p_type != elf.PT_LOAD) continue;
|
|
if (phdr.p_memsz == 0) continue;
|
|
|
|
if (phdr.p_vaddr % page_size != 0) return error.BadSegment;
|
|
if (phdr.p_filesz > phdr.p_memsz) return error.BadSegment;
|
|
if (phdr.p_offset > image.len or phdr.p_filesz > image.len - phdr.p_offset) return error.BadSegment;
|
|
// Inside the user image region, strictly below the stack page.
|
|
if (phdr.p_vaddr < code_virtual) return error.BadSegment;
|
|
if (phdr.p_memsz > stack_virtual - phdr.p_vaddr) return error.BadSegment;
|
|
|
|
const w = phdr.p_flags & elf.PF_W != 0;
|
|
const x = phdr.p_flags & elf.PF_X != 0;
|
|
if (w and x) return error.BadSegment; // W^X, even for init
|
|
|
|
const seg = Segment{
|
|
.vaddr = phdr.p_vaddr,
|
|
.memsz = phdr.p_memsz,
|
|
.filesz = phdr.p_filesz,
|
|
.off = phdr.p_offset,
|
|
.writable = w,
|
|
.executable = x,
|
|
};
|
|
// No overlap with any earlier segment (page-granular, since mapping is).
|
|
for (segs[0..count]) |other| {
|
|
const a_end = seg.vaddr + seg.pages() * page_size;
|
|
const b_end = other.vaddr + other.pages() * page_size;
|
|
if (seg.vaddr < b_end and other.vaddr < a_end) return error.BadSegment;
|
|
}
|
|
total_pages += seg.pages();
|
|
if (total_pages > maximum_pages) return error.ProgramTooBig;
|
|
segs[count] = seg;
|
|
count += 1;
|
|
}
|
|
if (count == 0) return error.BadElf;
|
|
|
|
// The entry point must land inside an executable segment.
|
|
for (segs[0..count]) |seg| {
|
|
if (seg.executable and ehdr.e_entry >= seg.vaddr and ehdr.e_entry < seg.vaddr + seg.memsz)
|
|
return .{ .count = count, .entry = ehdr.e_entry };
|
|
}
|
|
return error.BadEntry;
|
|
}
|
|
|
|
/// Load one page of a segment into address space `aspace`: a fresh frame, zeroed
|
|
/// and filled through the physmap, mapped user-accessible with the segment's W^X.
|
|
/// On a later failure the whole address space is torn down, which frees every
|
|
/// frame mapped into it — so no per-page rollback list is needed here.
|
|
fn loadPageInto(aspace: u64, image: []const u8, seg: Segment, page_index: u64) InitError!void {
|
|
const frame = pmm.alloc() orelse return error.OutOfMemory;
|
|
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frame));
|
|
@memset(destination[0..page_size], 0);
|
|
const page_off = page_index * page_size;
|
|
if (page_off < seg.filesz) {
|
|
const n = @min(page_size, seg.filesz - page_off);
|
|
@memcpy(destination[0..n], image[seg.off + page_off ..][0..n]);
|
|
}
|
|
architecture.mapUserPageInto(aspace, seg.vaddr + page_off, frame, seg.writable, seg.executable);
|
|
}
|
|
|
|
/// Build the System V AMD64 process-entry block at the top of a process's stack
|
|
/// page and return the initial user stack pointer. At the first user instruction,
|
|
/// rsp is 16-byte aligned and points at (addresses growing upward):
|
|
///
|
|
/// argc, argv[0..argc-1], NULL, NULL (empty envp), auxiliary vector, strings
|
|
///
|
|
/// — the layout every C runtime's startup code walks, so danos's own runtime and a
|
|
/// future libc port read arguments identically (docs/sysv.md). `page` is the kernel
|
|
/// (physmap) view of the stack's **top** frame and `page_user_base` that frame's
|
|
/// user address (stack_top_virtual - page_size); the pointers written into it are
|
|
/// user addresses inside that page. The caller has validated the sizes
|
|
/// (`entryStackBytes`), so this cannot overrun.
|
|
fn buildEntryStack(page: [*]u8, page_user_base: u64, argv: []const []const u8) u64 {
|
|
// The strings live at the very top of the page, packed from the end downward.
|
|
var string_offset: usize = page_size;
|
|
var pointers: [maximum_arguments]u64 = undefined;
|
|
var i: usize = argv.len;
|
|
while (i > 0) {
|
|
i -= 1;
|
|
string_offset -= argv[i].len + 1;
|
|
@memcpy(page[string_offset..][0..argv[i].len], argv[i]);
|
|
page[string_offset + argv[i].len] = 0; // NUL-terminated, as C expects
|
|
pointers[i] = page_user_base + string_offset;
|
|
}
|
|
|
|
// The vector sits below the strings: argc, the argv pointers, the argv
|
|
// terminator, an empty envp (terminator only), then the auxiliary vector.
|
|
const word_count = 1 + argv.len + 1 + 1 + 4;
|
|
const vector_offset = (string_offset - word_count * 8) & ~@as(usize, 15); // entry rsp % 16 == 0
|
|
const words: [*]u64 = @ptrCast(@alignCast(page + vector_offset));
|
|
var w: usize = 0;
|
|
words[w] = argv.len; // argc
|
|
w += 1;
|
|
for (pointers[0..argv.len]) |pointer| {
|
|
words[w] = pointer;
|
|
w += 1;
|
|
}
|
|
words[w] = 0; // argv terminator
|
|
words[w + 1] = 0; // envp: no environment yet, just the terminator
|
|
words[w + 2] = auxiliary_vector_page_size;
|
|
words[w + 3] = page_size;
|
|
words[w + 4] = auxiliary_vector_null; // end of the auxiliary vector
|
|
words[w + 5] = 0;
|
|
return page_user_base + vector_offset;
|
|
}
|
|
|
|
/// Bytes the entry block for `argv` occupies at the top of the stack page:
|
|
/// strings (each NUL-terminated), vector words, and the alignment slack.
|
|
fn entryStackBytes(argv: []const []const u8) usize {
|
|
var string_bytes: usize = 0;
|
|
for (argv) |argument| string_bytes += argument.len + 1;
|
|
return string_bytes + (1 + argv.len + 1 + 1 + 4) * 8 + 16;
|
|
}
|
|
|
|
/// Load a user ELF image into a fresh address space and spawn it as a scheduled
|
|
/// ring-3 process at `priority`, entered with `argv` on its stack per the System V
|
|
/// convention (`buildEntryStack`). `argv[0]` is required — it names the process:
|
|
/// the path or initial-ramdisk name it was spawned as. It is also recorded on the
|
|
/// task, so a fault report can say *which* binary died, not just its id.
|
|
/// The kernel-internal spawn (init at boot, tests): supervisor 0, no exit
|
|
/// notification. `spawnProcessSupervised` is the full form.
|
|
pub fn spawnProcess(image: []const u8, priority: u3, argv: []const []const u8) InitError!void {
|
|
_ = try spawnProcessSupervised(image, priority, argv, 0, null);
|
|
}
|
|
|
|
/// `spawnProcess`, recording `supervisor` (the id of the process that asked — the
|
|
/// kill authority) and, if given, `exit_endpoint` to notify when the child ends
|
|
/// (a reference is taken here and dropped when the notification posts).
|
|
/// Returns the child's process id.
|
|
/// Returns immediately — the process runs preemptively on its own page tables
|
|
/// alongside everything else, and its exit is handled by the system_call layer.
|
|
/// The whole build (address space + ELF load + task) runs under the kernel lock so
|
|
/// it appears atomically and can't race pmm/heap on another core.
|
|
pub fn spawnProcessSupervised(image: []const u8, priority: u3, argv: []const []const u8, supervisor: u32, exit_endpoint: ?*ipc.Endpoint) InitError!u32 {
|
|
if (argv.len == 0 or argv.len > maximum_arguments) return error.BadArguments;
|
|
// The entry block must leave most of the page as actual stack.
|
|
if (entryStackBytes(argv) > page_size / 2) return error.BadArguments;
|
|
|
|
var segs: [maximum_segments]Segment = undefined;
|
|
const parsed = try parseSegments(image, &segs);
|
|
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
|
|
const aspace = architecture.createAddressSpace() orelse return error.OutOfMemory;
|
|
errdefer architecture.destroyAddressSpace(aspace);
|
|
|
|
for (segs[0..parsed.count]) |seg| {
|
|
for (0..seg.pages()) |i| try loadPageInto(aspace, image, seg, i);
|
|
}
|
|
|
|
// The stack: `user_stack_pages` zeroed pages below stack_top_virtual, RW + NX.
|
|
// The page below them (`stack_virtual`) stays unmapped as the overflow guard.
|
|
// The entry block goes at the top of the highest page.
|
|
var user_sp: u64 = 0;
|
|
for (0..parameters.user_stack_pages) |i| {
|
|
const stack_frame = pmm.alloc() orelse return error.OutOfMemory;
|
|
const stack_page: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(stack_frame));
|
|
@memset(stack_page[0..page_size], 0); // no stale frame contents leak into user space
|
|
const page_virtual = stack_base_virtual + i * page_size;
|
|
if (i == parameters.user_stack_pages - 1)
|
|
user_sp = buildEntryStack(stack_page, page_virtual, argv);
|
|
architecture.mapUserPageInto(aspace, page_virtual, stack_frame, true, false); // RW + NX
|
|
}
|
|
|
|
const child = scheduler.spawnUserLocked(aspace, parsed.entry, user_sp, priority, argv[0], supervisor, if (exit_endpoint) |endpoint| @ptrCast(endpoint) else null) orelse
|
|
return error.OutOfMemory;
|
|
// The child holds a reference to its exit endpoint from birth to death. Taken
|
|
// only now, after nothing can fail; the lock is still held, so the child
|
|
// cannot run (let alone die) before the reference exists.
|
|
if (exit_endpoint) |endpoint| endpoint.refcount += 1;
|
|
return child;
|
|
}
|
|
|
|
/// clock() -> nanoseconds since boot: a monotonic time source. The kernel already owns
|
|
/// the scheduling timer and computes this for preemption, so surfacing it is pure
|
|
/// mechanism — no policy (wall-clock time, calendars, timezones are a user-space
|
|
/// service layered on top). It lets a driver bound a poll loop by real time instead of
|
|
/// a spin count, and time short delays.
|
|
fn systemClock(state: *architecture.CpuState) void {
|
|
architecture.setSystemCallResult(state, architecture.nanos());
|
|
}
|