Add a `system_spawn(name)` system call: the kernel loads a binary bundled in the initial-ramdisk, by name, as a fresh ring-3 process. It's the mechanism a user-space supervisor needs — discovery and policy stay in user space, the kernel only spawns. The kernel already holds the initial-ramdisk image from the boot handoff; it now stashes it (process.setInitialRamdisk) so the handler can resolve names, bounds-checks the name into the user half like debug_write, and returns -1 for an unknown name or a load failure. Ungated for now (any process may spawn any bundled binary); a spawn capability belongs here once the model grows one. The device manager stops logging "would spawn it" and calls runtime.system.spawn on its matched driver. On QEMU it discovers the HPET, matches `hpet`, and spawns it — and the driver comes all the way up (claims the timer, maps its MMIO, binds and services its IRQ, prints "hpet: ok"). The device-manager test now keys on that final marker: since only the manager is spawned, `hpet: ok` appearing proves the whole discover -> match -> system_spawn -> driver-up chain end to end. Transitional: the kernel still auto-spawns the whole initial-ramdisk at boot, so a real boot briefly double-spawns hpet (the second claim fails harmlessly). Increment 3 removes that redundancy so the manager is the sole owner of driver spawning. Suite 36/36 plus host tests.
623 lines
30 KiB
Zig
623 lines
30 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 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 page sits above.
|
|
pub const code_virtual: u64 = 0x0000_7000_0000_0000;
|
|
pub const stack_virtual: u64 = 0x0000_7000_0020_0000;
|
|
|
|
/// 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);
|
|
|
|
/// 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;
|
|
|
|
// 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`.
|
|
pub fn init() void {
|
|
architecture.setSystemCallHandler(system_call);
|
|
}
|
|
|
|
/// 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 {
|
|
switch (@as(SystemCall, @enumFromInt(architecture.systemCallNumber(state)))) {
|
|
.exit => {
|
|
exit_code = architecture.systemCallArg(state, 0);
|
|
// A scheduled process drops its endpoint references, frees its address
|
|
// space, and reschedules; a borrowed test thread unwinds back to the
|
|
// kernel that entered it.
|
|
if (scheduler.currentIsUserProcess()) {
|
|
// Unbind before closeHandles: dropping the last reference destroys the
|
|
// Endpoint, and a still-bound GSI would have an ISR call
|
|
// notifyFromIsr on freed memory the next time the device fired.
|
|
// unbindAll also leaves the line masked, so a dead driver's device
|
|
// goes quiet rather than storming.
|
|
releaseIrqs(scheduler.current());
|
|
ipc.closeHandles(scheduler.current());
|
|
scheduler.exitUser();
|
|
} 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_endpoint => systemCreateEndpoint(state),
|
|
.ipc_register => systemIpcRegister(state),
|
|
.ipc_lookup => systemIpcLookup(state),
|
|
.ipc_call => systemIpcCall(state),
|
|
.ipc_reply_wait => systemIpcReplyWait(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),
|
|
_ => fail(state),
|
|
}
|
|
}
|
|
|
|
/// Return `-errno` in the system_call result register.
|
|
fn failErr(state: *architecture.CpuState, errno: i64) void {
|
|
architecture.setSystemCallResult(state, @bitCast(-errno));
|
|
}
|
|
|
|
/// create_endpoint() -> handle: allocate an endpoint and install it in the
|
|
/// caller's handle table.
|
|
fn systemCreateEndpoint(state: *architecture.CpuState) void {
|
|
const endpoint = ipc.createEndpoint() 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);
|
|
const r = ipc.call(endpoint, architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), architecture.systemCallArg(state, 3), architecture.systemCallArg(state, 4));
|
|
architecture.setSystemCallResult(state, @bitCast(r));
|
|
}
|
|
|
|
/// 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;
|
|
const r = ipc.replyWait(endpoint, architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), architecture.systemCallArg(state, 3), architecture.systemCallArg(state, 4), &badge);
|
|
architecture.setSystemCallResult(state, @bitCast(r));
|
|
architecture.setSystemCallResult2(state, badge);
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
/// 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) -> 0 on success, -1 on failure. Load the binary
|
|
/// bundled in the initial-ramdisk under `name` as a fresh ring-3 process. 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.
|
|
///
|
|
/// Ungated for now — any process may spawn any bundled binary. A capability (only a
|
|
/// supervisor holds the right to spawn) belongs here once the model grows one; see
|
|
/// docs/driver-model.md. The name is 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);
|
|
if (len == 0 or len > 64 or ptr >= user_half_end or ptr + len > user_half_end) return fail(state);
|
|
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 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;
|
|
spawnProcess(item.blob, 4) catch return fail(state);
|
|
architecture.setSystemCallResult(state, 0);
|
|
return;
|
|
}
|
|
fail(state); // no bundled binary by that name
|
|
}
|
|
|
|
/// Drop every IRQ binding `t` made. Called on exit, before the handle table is closed
|
|
/// (which is what frees the endpoints an ISR would otherwise notify into).
|
|
fn releaseIrqs(t: *scheduler.Task) void {
|
|
const flags = sync.enter();
|
|
defer sync.leave(flags);
|
|
irq.releaseOwner(t.id);
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// 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.
|
|
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);
|
|
@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("DANOS-INIT: ");
|
|
log.write(source[0..len]);
|
|
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_virtual, stack_frame, true, false); // RW + NX
|
|
resetRecords();
|
|
|
|
architecture.enterUser(scheduler.currentCpuIndex(), code_virtual, stack_virtual + page_size);
|
|
|
|
// Back via the exit system_call; the interrupt gate left IF clear.
|
|
architecture.enableInterrupts();
|
|
architecture.unmapPage(code_virtual);
|
|
architecture.unmapPage(stack_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
|
|
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);
|
|
}
|
|
|
|
/// Load a user ELF image into a fresh address space and spawn it as a scheduled
|
|
/// ring-3 process at `priority`. 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 spawnProcess(image: []const u8, priority: u3) InitError!void {
|
|
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);
|
|
}
|
|
const stack_frame = pmm.alloc() orelse return error.OutOfMemory;
|
|
architecture.mapUserPageInto(aspace, stack_virtual, stack_frame, true, false); // RW + NX
|
|
|
|
if (!scheduler.spawnUserLocked(aspace, parsed.entry, stack_virtual + page_size, priority))
|
|
return error.OutOfMemory;
|
|
}
|