//! 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 (`/sbin/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 system = @import("system"); 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 log = @import("log.zig"); const page_size = system.page_size; const SystemCall = system.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 system_call surface, dispatched on the saved system_call number (`system.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), _ => 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(system.DeviceDescriptor); const cap = @min(maximum, (user_half_end - buffer_ptr) / sz); // clamp to the user half const out: [*]system.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(system.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: system.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); } /// 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(system.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(system.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(system.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 (/sbin/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(system.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; }