ipc_call and ipc_reply_wait grow a `send_cap` argument (r9) and a `received_cap` return (r8): an endpoint travels alongside a message, installed into the receiver's handle table. The transfer is a share, not a move — the endpoint's refcount is bumped and the sender keeps its handle. If the receiver's table is full the call fails -ENOSPC and the message is NOT delivered (a half-delivered capability is worse than a failed send); a bad handle fails -EBADF. Both directions carry a cap: a client's call hands one to the server (seen in the server's replyWait), and the server's reply hands one back (seen in the client's call return). This is the "open" primitive the driver model was blocked on: a bus driver mints a per-device endpoint and hands it to a class driver, giving it a private channel to one device without the 8-slot global name registry. Kernel: shareCapability in ipc-synchronous.zig at both copy points; new setSystemCallResult3 (r8, saved/restored by the syscall stub); Task gains ipc_send_cap / ipc_received_cap. Runtime: callCap + Reply, replyWait gains send_cap and Received.cap; plain call/replyWait delegate with no_cap. New abi.no_cap. New ipc-cap test (two kernel tasks exercise both directions, each verifying the endpoint it received is the same object shared, refcount bumped to 2). No class driver consumes callCap yet — it lands with the first one. Suite 37/37 plus host tests.
67 lines
4.0 KiB
Zig
67 lines
4.0 KiB
Zig
//! The **kernel ↔ user** ABI: the core contract every user program speaks to the
|
|
//! kernel — the system_call numbers, `mmap` protection flags, the page size those
|
|
//! calls work in, and the IPC name-registry ids and notification bit. Shared by the
|
|
//! kernel dispatcher (system/kernel/process.zig) and the user runtime library
|
|
//! (library/runtime/), so the two can never drift.
|
|
//!
|
|
//! This is the *core* ABI; the device half — `DeviceDescriptor` and friends, which
|
|
//! also cross this boundary — lives with the device sub-project as [[device-abi]]
|
|
//! (system/devices/device-abi.zig). The loader↔kernel handoff is [[boot-handoff]].
|
|
|
|
/// Page size every `mmap`/`munmap` grant and the boot memory map are measured in.
|
|
/// 4 KiB on every architecture danos targets so far. Part of the ABI because user
|
|
/// code aligns to it (grants are page-granular) and the kernel guarantees it.
|
|
pub const page_size = 4096;
|
|
|
|
/// The kernel system_call numbers — the single source of truth shared by the kernel
|
|
/// dispatcher (system/kernel/process.zig) and the user runtime library, so the two
|
|
/// can never drift. The set is deliberately microkernel-minimal: file/device I/O
|
|
/// is not here — it lives in user-space servers reached through the IPC calls.
|
|
/// The table grows one milestone at a time; see docs/syscall.md.
|
|
pub const SystemCall = enum(u64) {
|
|
exit = 0, // exit(code): end the calling process
|
|
yield = 1, // yield(): give up the rest of this quantum
|
|
debug_write = 2, // debug_write(ptr, len): raw bytes to the kernel log (bring-up only)
|
|
sleep = 3, // sleep(ms): block the caller for ms milliseconds
|
|
mmap = 4, // mmap(len, prot) -> base: grant zeroed, page-aligned user pages
|
|
munmap = 5, // munmap(base, len): release pages from a prior mmap
|
|
create_endpoint = 6, // create_endpoint() -> handle: a new IPC endpoint
|
|
ipc_register = 7, // ipc_register(service_id, handle): publish an endpoint by well-known id
|
|
ipc_lookup = 8, // ipc_lookup(service_id) -> handle: find a published endpoint
|
|
ipc_call = 9, // ipc_call(h, message, len, reply, cap) -> reply_len: send + block for reply
|
|
ipc_reply_wait = 10, // ipc_reply_wait(h, reply, len, receive, cap) -> receive_len (+badge in rdx)
|
|
device_enumerate = 11, // device_enumerate(buffer, maximum) -> count: snapshot the device table
|
|
device_claim = 12, // device_claim(id) -> ok: take exclusive ownership of a device
|
|
mmio_map = 13, // mmio_map(id, resource_index) -> vaddr: map a claimed device's MMIO into this AS
|
|
irq_bind = 14, // irq_bind(id, resource_index, endpoint): deliver a device IRQ as an IPC notification
|
|
irq_ack = 15, // irq_ack(id, resource_index): re-arm a bound IRQ after servicing it
|
|
device_register = 16, // device_register(parent_id, descriptor) -> id: publish a child of a device you claimed
|
|
system_spawn = 17, // system_spawn(name_ptr, name_len) -> 0: start a named initial-ramdisk binary as a new ring-3 process
|
|
_,
|
|
};
|
|
|
|
/// Set in the badge returned by `ipc_reply_wait` when what arrived is an
|
|
/// **asynchronous notification** (today: a device interrupt bound with `irq_bind`)
|
|
/// rather than a message from a client. There is no payload and no reply owed; the
|
|
/// low bits carry the source, a GSI. Shared so the kernel's ISR and the driver's
|
|
/// event loop can't disagree about which bit means "the hardware spoke".
|
|
pub const notify_badge_bit: u64 = 1 << 63;
|
|
|
|
/// Well-known IPC service ids for the bootstrap name registry (create_endpoint +
|
|
/// ipc_register/ipc_lookup). Small integers, so no string interning is needed
|
|
/// during bring-up. The VFS server registers under `vfs`; clients look it up.
|
|
pub const ServiceId = enum(u32) {
|
|
vfs = 1,
|
|
_,
|
|
};
|
|
|
|
/// Protection flags for `mmap` (matching the usual C bit values).
|
|
pub const prot_read: u64 = 1;
|
|
pub const prot_write: u64 = 2;
|
|
pub const prot_exec: u64 = 4;
|
|
|
|
/// `send_cap` / `received_cap` sentinel meaning "no capability" on the `ipc_call` /
|
|
/// `ipc_reply_wait` cap-passing path (M13). `~0`, like `no_parent` — a real handle is
|
|
/// a small index, so it can never collide.
|
|
pub const no_cap: u64 = ~@as(u64, 0);
|