M13: IPC capability passing

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.
This commit is contained in:
Daniel Samson
2026-07-10 19:23:19 +01:00
parent 8eb4210251
commit a581712b09
12 changed files with 243 additions and 53 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o
## Directory structure ## Directory structure
| Path | Description | | Path | Description |
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| |------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| / | Primary hierarchy root and root directory of the entire file system hierarchy. | | / | Primary hierarchy root and root directory of the entire file system hierarchy. |
| /bin | Essential command binaries that need to be available in single-user mode, including to bring up the system or repair it, for all users (e.g., cat, ls, cp). | | /bin | Essential command binaries that need to be available in single-user mode, including to bring up the system or repair it, for all users (e.g., cat, ls, cp). |
| /boot | Boot loader files (e.g., EFI, initial-ramdisk.img ). | | /boot | Boot loader files (e.g., EFI, initial-ramdisk.img ). |
@@ -29,7 +29,7 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o
POSIX specifies the long format of the ls command to represent the Unix file type as the first letter for an entry. POSIX specifies the long format of the ls command to represent the Unix file type as the first letter for an entry.
| type | symbol | Description | | type | symbol | Description |
|-------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| regular | - | An ordinary file holding an uninterpreted byte stream. Reads and writes are positional, and the file grows on demand (e.g., a binary in /bin, a config file in /etc). | | regular | - | An ordinary file holding an uninterpreted byte stream. Reads and writes are positional, and the file grows on demand (e.g., a binary in /bin, a config file in /etc). |
| directory | d | A container mapping names to other files. It may only be modified through directory operations, never written to directly. | | directory | d | A container mapping names to other files. It may only be modified through directory operations, never written to directly. |
| symbolic link | l | A file whose contents are a path that is resolved in its place. The target need not exist, and may cross mount points. | | symbolic link | l | A file whose contents are a path that is resolved in its place. The target need not exist, and may cross mount points. |
+11 -1
View File
@@ -132,6 +132,12 @@ If a class driver needs `mmio`, it has become an HCD and should be one.
- **M11** — `irq_bind` / `irq_ack`. IRQ delivered as an IPC notification; mask before - **M11** — `irq_bind` / `irq_ack`. IRQ delivered as an IPC notification; mask before
EOI; `irq_ack` is the unmask. EOI; `irq_ack` is the unmask.
- **M12** — `parent` in `DeviceDesc`, `device_register` with resource containment. - **M12** — `parent` in `DeviceDesc`, `device_register` with resource containment.
- **M13** — capability passing. `ipc_call` / `ipc_reply_wait` grew a `send_cap` argument
and a `received_cap` return (r8): an endpoint travels with a message, installed into
the receiver's handle table (shared, refcount-bumped — a copy, not a move). A full
table fails `-ENOSPC` and does not half-deliver. This is the "open" primitive — a bus
driver mints a per-device endpoint and hands it to a class driver. The runtime exposes
`callCap` and `replyWait(..., send_cap)`; no class driver consumes it yet.
- **`system_spawn`** — a user-space supervisor starts a driver: `system_spawn(name)` - **`system_spawn`** — a user-space supervisor starts a driver: `system_spawn(name)`
loads a binary bundled in the initial-ramdisk as a fresh ring-3 process. This is what loads a binary bundled in the initial-ramdisk as a fresh ring-3 process. This is what
turned the device manager from "log the match" into "run the driver": the kernel now turned the device manager from "log the match" into "run the driver": the kernel now
@@ -146,7 +152,11 @@ HCDs and class drivers do not work yet. Here is exactly why, and exactly what wo
# Proposed ABI # Proposed ABI
## M13 — capability passing, for class drivers ## M13 — capability passing, for class drivers ✅ done
*Implemented as described below (see "What exists today"). The signatures landed
verbatim: `send_cap` in r9, `received_cap` returned in r8, `-ENOSPC` on a full receiver
table with no delivery. The rest of this section is the original design note.*
**The blocker.** A class driver has to reach *its* device. Today the only way to find **The blocker.** A class driver has to reach *its* device. Today the only way to find
an endpoint is the name registry: `ipc_register(service_id, h)` / `ipc_lookup(id)`, an endpoint is the name registry: `ipc_register(service_id, h)` / `ipc_lookup(id)`,
+47 -14
View File
@@ -43,11 +43,40 @@ pub fn lookup(id: abi.ServiceId) ?Handle {
pub const CallError = error{Failed}; pub const CallError = error{Failed};
/// The result of a capability-passing `callCap`: the reply length, and the handle of
/// an endpoint the server sent back (e.g. a per-device channel), or null.
pub const Reply = struct {
len: usize,
cap: ?Handle,
};
/// Send `message` to endpoint `h` and block until the server replies into `reply`,
/// optionally handing the server a capability (`send_cap`) and receiving one back.
/// This is the class-driver "open" primitive: call a bus with `send_cap = null`, get a
/// private per-device endpoint back in `.cap`. Two return values (reply length in rax,
/// received handle in r8) need a hand-written stub — r8 is read-write (in: reply
/// capacity, arg #4; out: the received handle).
pub fn callCap(h: Handle, message: []const u8, reply: []u8, send_cap: ?Handle) CallError!Reply {
var rax: usize = undefined;
var r8: usize = reply.len; // in: reply capacity (arg #4); out: received capability handle
asm volatile ("syscall"
: [rax] "={rax}" (rax),
[r8] "+{r8}" (r8),
: [n] "{rax}" (@intFromEnum(abi.SystemCall.ipc_call)),
[a0] "{rdi}" (h),
[a1] "{rsi}" (@intFromPtr(message.ptr)),
[a2] "{rdx}" (message.len),
[a3] "{r10}" (@intFromPtr(reply.ptr)),
[a5] "{r9}" (send_cap orelse abi.no_cap),
: .{ .rcx = true, .r11 = true, .memory = true });
if (failed(rax)) return error.Failed;
return .{ .len = rax, .cap = if (r8 == abi.no_cap) null else r8 };
}
/// Send `message` to endpoint `h` and block until the server replies into `reply`. /// Send `message` to endpoint `h` and block until the server replies into `reply`.
/// Returns the reply length. /// Returns the reply length. The common case: no capability passed either way.
pub fn call(h: Handle, message: []const u8, reply: []u8) CallError!usize { pub fn call(h: Handle, message: []const u8, reply: []u8) CallError!usize {
const r = sc.systemCall5(.ipc_call, h, @intFromPtr(message.ptr), message.len, @intFromPtr(reply.ptr), reply.len); return (try callCap(h, message, reply, null)).len;
return if (failed(r)) error.Failed else r;
} }
/// Set in `Received.badge` when what arrived is an asynchronous notification — a /// Set in `Received.badge` when what arrived is an asynchronous notification — a
@@ -55,11 +84,12 @@ pub fn call(h: Handle, message: []const u8, reply: []u8) CallError!usize {
/// GSI. See `isNotification`. /// GSI. See `isNotification`.
pub const notify_badge_bit: u64 = abi.notify_badge_bit; pub const notify_badge_bit: u64 = abi.notify_badge_bit;
/// The result of a `replyWait`: the request length and the sender's badge (a /// The result of a `replyWait`: the request length, the sender's badge (a task id, or
/// task id, or an IRQ notification if the high bit is set). /// an IRQ notification if the high bit is set), and any capability the request carried.
pub const Received = struct { pub const Received = struct {
len: usize, len: usize,
badge: u64, badge: u64,
cap: ?Handle,
/// True if this wake-up was a device interrupt, not a client request. A driver's /// True if this wake-up was a device interrupt, not a client request. A driver's
/// event loop branches on this; there is no reply owed on the notification path. /// event loop branches on this; there is no reply owed on the notification path.
@@ -73,22 +103,25 @@ pub const Received = struct {
} }
}; };
/// Server side of IPC_ReplyWait: deliver `reply` to the client last received (if /// Server side of IPC_ReplyWait: deliver `reply` to the client last received (if any,
/// any), then block until the next request arrives in `receive`. Returns its length /// optionally handing it `send_cap`), then block until the next request arrives in
/// and the sender badge. This system_call returns two values — the length in rax and /// `receive`. Returns its length, the sender badge, and any capability the request
/// the badge in rdx — so it needs a hand-written stub: rdx is a read-write /// carried (in `.cap`). Three return values — length in rax, badge in rdx, received
/// operand (input = reply length, arg #3; output = badge). /// handle in r8 — so it needs a hand-written stub: rdx is read-write (in: reply length,
pub fn replyWait(h: Handle, reply: []const u8, receive: []u8) Received { /// arg #3; out: badge) and r8 is read-write (in: receive capacity, arg #4; out: handle).
pub fn replyWait(h: Handle, reply: []const u8, receive: []u8, send_cap: ?Handle) Received {
var rax: usize = undefined; var rax: usize = undefined;
var rdx: usize = reply.len; // in: reply_len (arg #3 -> rdx); out: badge var rdx: usize = reply.len; // in: reply_len (arg #3); out: badge
var r8: usize = receive.len; // in: receive capacity (arg #4); out: received capability handle
asm volatile ("syscall" asm volatile ("syscall"
: [rax] "={rax}" (rax), : [rax] "={rax}" (rax),
[rdx] "+{rdx}" (rdx), [rdx] "+{rdx}" (rdx),
[r8] "+{r8}" (r8),
: [n] "{rax}" (@intFromEnum(abi.SystemCall.ipc_reply_wait)), : [n] "{rax}" (@intFromEnum(abi.SystemCall.ipc_reply_wait)),
[a0] "{rdi}" (h), [a0] "{rdi}" (h),
[a1] "{rsi}" (@intFromPtr(reply.ptr)), [a1] "{rsi}" (@intFromPtr(reply.ptr)),
[a3] "{r10}" (@intFromPtr(receive.ptr)), [a3] "{r10}" (@intFromPtr(receive.ptr)),
[a4] "{r8}" (receive.len), [a5] "{r9}" (send_cap orelse abi.no_cap),
: .{ .rcx = true, .r11 = true, .memory = true }); : .{ .rcx = true, .r11 = true, .memory = true });
return .{ .len = rax, .badge = rdx }; return .{ .len = rax, .badge = rdx, .cap = if (r8 == abi.no_cap) null else r8 };
} }
+5
View File
@@ -59,3 +59,8 @@ pub const ServiceId = enum(u32) {
pub const prot_read: u64 = 1; pub const prot_read: u64 = 1;
pub const prot_write: u64 = 2; pub const prot_write: u64 = 2;
pub const prot_exec: u64 = 4; 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);
+1 -1
View File
@@ -152,7 +152,7 @@ pub fn main() void {
while (count < target_ticks) { while (count < target_ticks) {
// Blocked here. The task is `.blocked` and off every scheduler queue; the // Blocked here. The task is `.blocked` and off every scheduler queue; the
// next line runs only because the HPET raised its line. // next line runs only because the HPET raised its line.
const r = ipc.replyWait(endpoint, &.{}, &receive); const r = ipc.replyWait(endpoint, &.{}, &receive, null);
if (!r.isNotification()) continue; // a client request, not our IRQ if (!r.isNotification()) continue; // a client request, not our IRQ
// Quiet the device: write 1 to timer 0's status bit. Until this lands, the // Quiet the device: write 1 to timer 0's status bit. Until this lands, the
@@ -87,6 +87,14 @@ pub fn setSystemCallResult2(state: *CpuState, value: u64) void {
state.rdx = value; state.rdx = value;
} }
/// Write a *third* system_call return value (r8 here). r8 is an input argument
/// register (arg #4), but the syscall/int-0x80 stubs push and pop it around the
/// dispatch, so a value written into the frame is restored to the user on return.
/// Used by the IPC cap-passing calls to hand back the received capability handle.
pub fn setSystemCallResult3(state: *CpuState, value: u64) void {
state.r8 = value;
}
/// Bring up the serial port (the kernel's machine-readable log). No dependencies, /// Bring up the serial port (the kernel's machine-readable log). No dependencies,
/// so it can be the very first thing called. /// so it can be the very first thing called.
pub fn serialInit() void { pub fn serialInit() void {
+52 -8
View File
@@ -157,10 +157,28 @@ pub fn copyFromUser(user_as: u64, user_va: u64, destination: []u8) bool {
// --- the two IPC operations ------------------------------------------------- // --- the two IPC operations -------------------------------------------------
/// Share the capability named by handle `cap` in `from`'s table into `to`'s table,
/// bumping the endpoint's refcount (the sender keeps its handle — this is a copy, not
/// a move). Returns the handle it landed at in `to` (>= 0), or `-EBADF` if `cap` names
/// no live handle, or `-ENOSPC` if `to`'s table is full. Callers only invoke this when
/// `cap != no_cap`. Used by both IPC directions to carry an endpoint with a message.
fn shareCapability(from: *Task, to: *Task, cap: u64) i64 {
const endpoint = resolveHandle(from, cap) orelse return -EBADF;
endpoint.refcount += 1;
const handle = installHandle(to, endpoint);
if (handle < 0) {
dropRef(endpoint); // undo the bump; the receiver had no room
return -ENOSPC;
}
return handle;
}
/// Client side of IPC_Call: send `[message_ptr, message_len)` to `endpoint` and block until a /// Client side of IPC_Call: send `[message_ptr, message_len)` to `endpoint` and block until a
/// server replies into `[reply_ptr, reply_cap)`. Returns the reply length, or a /// server replies into `[reply_ptr, reply_cap)`. Returns the reply length, or a
/// negative errno. Runs as the current task. /// negative errno. `send_cap` (a handle, or `no_cap`) is an endpoint transferred to the
pub fn call(endpoint: *Endpoint, message_ptr: u64, message_len: u64, reply_ptr: u64, reply_cap: u64) i64 { /// server with the request; `out_received_cap` receives the handle of an endpoint the
/// server sent back in its reply, or `no_cap`. Runs as the current task.
pub fn call(endpoint: *Endpoint, message_ptr: u64, message_len: u64, reply_ptr: u64, reply_cap: u64, send_cap: u64, out_received_cap: *u64) i64 {
if (message_len > MESSAGE_MAXIMUM or reply_cap > MESSAGE_MAXIMUM) return -E2BIG; if (message_len > MESSAGE_MAXIMUM or reply_cap > MESSAGE_MAXIMUM) return -E2BIG;
const flags = sync.enter(); const flags = sync.enter();
defer sync.leave(flags); defer sync.leave(flags);
@@ -170,12 +188,15 @@ pub fn call(endpoint: *Endpoint, message_ptr: u64, message_len: u64, reply_ptr:
me.ipc_send_len = message_len; me.ipc_send_len = message_len;
me.ipc_reply_ptr = reply_ptr; me.ipc_reply_ptr = reply_ptr;
me.ipc_reply_cap = reply_cap; me.ipc_reply_cap = reply_cap;
me.ipc_send_cap = send_cap;
me.ipc_received_cap = abi.no_cap;
me.ipc_status = 0; me.ipc_status = 0;
enqueueSender(endpoint, me); // join the FIFO, then... enqueueSender(endpoint, me); // join the FIFO, then...
scheduler.wakeLocked(&endpoint.receive_wait_queue); // ...wake a waiting server (no-op if none) scheduler.wakeLocked(&endpoint.receive_wait_queue); // ...wake a waiting server (no-op if none)
scheduler.blockCurrentLocked(); // block until the reply readies us again scheduler.blockCurrentLocked(); // block until the reply readies us again
out_received_cap.* = me.ipc_received_cap; // a capability the replier sent back, or no_cap
return me.ipc_status; // reply length or -errno, written by the replier return me.ipc_status; // reply length or -errno, written by the replier
} }
@@ -185,21 +206,33 @@ pub fn call(endpoint: *Endpoint, message_ptr: u64, message_len: u64, reply_ptr:
/// to `out_badge` and returns the request length, or a negative errno. A pending /// to `out_badge` and returns the request length, or a negative errno. A pending
/// notification is delivered ahead of client requests (length 0, badge with /// notification is delivered ahead of client requests (length 0, badge with
/// `notify_badge_bit` set, no reply owed). /// `notify_badge_bit` set, no reply owed).
pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_ptr: u64, receive_cap: u64, out_badge: *u64) i64 { pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_ptr: u64, receive_cap: u64, send_cap: u64, out_badge: *u64, out_received_cap: *u64) i64 {
if (reply_len > MESSAGE_MAXIMUM or receive_cap > MESSAGE_MAXIMUM) return -E2BIG; if (reply_len > MESSAGE_MAXIMUM or receive_cap > MESSAGE_MAXIMUM) return -E2BIG;
const flags = sync.enter(); const flags = sync.enter();
defer sync.leave(flags); defer sync.leave(flags);
const me = scheduler.current(); const me = scheduler.current();
out_received_cap.* = abi.no_cap; // no capability received unless a request delivers one
// (1) Reply to the client we're still holding, if any. // (1) Reply to the client we're still holding, if any — carrying `send_cap` to it.
if (me.ipc_client) |client| { if (me.ipc_client) |client| {
me.ipc_client = null; me.ipc_client = null;
const n = @min(reply_len, client.ipc_reply_cap); const n = @min(reply_len, client.ipc_reply_cap);
if (copyAcross(me.aspace, reply_ptr, client.aspace, client.ipc_reply_ptr, n)) { client.ipc_received_cap = abi.no_cap;
client.ipc_status = @intCast(n); if (!copyAcross(me.aspace, reply_ptr, client.aspace, client.ipc_reply_ptr, n)) {
} else {
client.ipc_status = -EFAULT; client.ipc_status = -EFAULT;
} else if (send_cap != abi.no_cap) {
// Transfer the reply's capability into the client. A failure fails the
// client's `call` rather than delivering a reply without its promised cap.
const shared = shareCapability(me, client, send_cap);
if (shared < 0) {
client.ipc_status = shared; // -EBADF (bad handle) or -ENOSPC (client table full)
} else {
client.ipc_received_cap = @intCast(shared);
client.ipc_status = @intCast(n);
}
} else {
client.ipc_status = @intCast(n);
} }
scheduler.readyLocked(client); // its `call` now returns scheduler.readyLocked(client); // its `call` now returns
} }
@@ -208,7 +241,7 @@ pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_pt
while (true) { while (true) {
if (popNotify(endpoint)) |badge| { if (popNotify(endpoint)) |badge| {
out_badge.* = badge | notify_badge_bit; out_badge.* = badge | notify_badge_bit;
return 0; // notification: no payload, no reply owed return 0; // notification: no payload, no reply owed, no cap
} }
if (dequeueSender(endpoint)) |caller| { if (dequeueSender(endpoint)) |caller| {
const n = @min(caller.ipc_send_len, receive_cap); const n = @min(caller.ipc_send_len, receive_cap);
@@ -217,6 +250,17 @@ pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_pt
scheduler.readyLocked(caller); scheduler.readyLocked(caller);
continue; continue;
} }
// Install the capability the caller sent, if any, into my table. A failure
// fails the caller's `call` and does not deliver — no half-delivered cap.
if (caller.ipc_send_cap != abi.no_cap) {
const shared = shareCapability(caller, me, caller.ipc_send_cap);
if (shared < 0) {
caller.ipc_status = shared; // -EBADF or -ENOSPC
scheduler.readyLocked(caller);
continue;
}
out_received_cap.* = @intCast(shared);
}
me.ipc_client = caller; // remember who to reply to me.ipc_client = caller; // remember who to reply to
out_badge.* = caller.id; out_badge.* = caller.id;
return @intCast(n); return @intCast(n);
+6 -2
View File
@@ -197,8 +197,10 @@ fn systemIpcLookup(state: *architecture.CpuState) void {
/// stack, so it survives the block and receives the result on resume. /// stack, so it survives the block and receives the result on resume.
fn systemIpcCall(state: *architecture.CpuState) void { fn systemIpcCall(state: *architecture.CpuState) void {
const endpoint = ipc.resolveHandle(scheduler.current(), architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF); 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)); 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.setSystemCallResult(state, @bitCast(r));
architecture.setSystemCallResult3(state, received_cap);
} }
/// ipc_reply_wait(handle, reply_ptr, reply_len, receive_ptr, receive_cap) -> receive_len, /// ipc_reply_wait(handle, reply_ptr, reply_len, receive_ptr, receive_cap) -> receive_len,
@@ -206,9 +208,11 @@ fn systemIpcCall(state: *architecture.CpuState) void {
fn systemIpcReplyWait(state: *architecture.CpuState) void { fn systemIpcReplyWait(state: *architecture.CpuState) void {
const endpoint = ipc.resolveHandle(scheduler.current(), architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF); const endpoint = ipc.resolveHandle(scheduler.current(), architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
var badge: u64 = 0; 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); 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.setSystemCallResult(state, @bitCast(r));
architecture.setSystemCallResult2(state, badge); architecture.setSystemCallResult2(state, badge);
architecture.setSystemCallResult3(state, received_cap);
} }
/// device_enumerate(buffer, maximum) -> total: snapshot the device table into the caller's /// device_enumerate(buffer, maximum) -> total: snapshot the device table into the caller's
+2
View File
@@ -66,6 +66,8 @@ pub const Task = struct {
ipc_reply_ptr: u64 = 0, // client: reply buffer (vaddr) ipc_reply_ptr: u64 = 0, // client: reply buffer (vaddr)
ipc_reply_cap: u64 = 0, ipc_reply_cap: u64 = 0,
ipc_status: i64 = 0, // client: reply length / -errno, written by the replier ipc_status: i64 = 0, // client: reply length / -errno, written by the replier
ipc_send_cap: u64 = ~@as(u64, 0), // handle to transfer with this message (abi.no_cap = none)
ipc_received_cap: u64 = ~@as(u64, 0), // client: handle the reply's transferred cap landed at (abi.no_cap = none)
next: ?*Task = null, // ready-queue link (also the endpoint sender-FIFO link) next: ?*Task = null, // ready-queue link (also the endpoint sender-FIFO link)
}; };
+81 -2
View File
@@ -82,6 +82,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
ipcTest(); ipcTest();
} else if (eql(case, "ipc-call")) { } else if (eql(case, "ipc-call")) {
ipcCallTest(); ipcCallTest();
} else if (eql(case, "ipc-cap")) {
capabilityTest();
} else if (eql(case, "smp")) { } else if (eql(case, "smp")) {
smpTest(); smpTest();
} else if (eql(case, "affinity")) { } else if (eql(case, "affinity")) {
@@ -808,9 +810,10 @@ fn ipcServer() void {
var reply_buffer: [8]u8 = undefined; var reply_buffer: [8]u8 = undefined;
var reply_len: u64 = 0; var reply_len: u64 = 0;
var badge: u64 = 0; var badge: u64 = 0;
var received_cap: u64 = abi.no_cap;
while (true) { while (true) {
var receive: [8]u8 = undefined; var receive: [8]u8 = undefined;
const n = ipcsync.replyWait(ipc_endpoint, @intFromPtr(&reply_buffer), reply_len, @intFromPtr(&receive), receive.len, &badge); const n = ipcsync.replyWait(ipc_endpoint, @intFromPtr(&reply_buffer), reply_len, @intFromPtr(&receive), receive.len, abi.no_cap, &badge, &received_cap);
if (n < 0) scheduler.exit(); if (n < 0) scheduler.exit();
const v = std.mem.readInt(u64, receive[0..8], .little); const v = std.mem.readInt(u64, receive[0..8], .little);
std.mem.writeInt(u64, reply_buffer[0..8], v + 1, .little); std.mem.writeInt(u64, reply_buffer[0..8], v + 1, .little);
@@ -826,7 +829,8 @@ fn ipcClient() void {
var message: [8]u8 = undefined; var message: [8]u8 = undefined;
std.mem.writeInt(u64, message[0..8], i, .little); std.mem.writeInt(u64, message[0..8], i, .little);
var reply: [8]u8 = undefined; var reply: [8]u8 = undefined;
const n = ipcsync.call(ipc_endpoint, @intFromPtr(&message), 8, @intFromPtr(&reply), reply.len); var received_cap: u64 = abi.no_cap;
const n = ipcsync.call(ipc_endpoint, @intFromPtr(&message), 8, @intFromPtr(&reply), reply.len, abi.no_cap, &received_cap);
if (n != 8 or std.mem.readInt(u64, reply[0..8], .little) != i + 1) ok = false; if (n != 8 or std.mem.readInt(u64, reply[0..8], .little) != i + 1) ok = false;
} }
ipc_replies_ok = ok; ipc_replies_ok = ok;
@@ -856,6 +860,81 @@ fn ipcCallTest() void {
result(); result();
} }
// --- IPC capability passing (M13) -------------------------------------------
var cap_endpoint: *ipcsync.Endpoint = undefined;
var cap_ep_x: *ipcsync.Endpoint = undefined; // client mints, sends to the server
var cap_ep_y: *ipcsync.Endpoint = undefined; // server mints, sends back to the client
var cap_server_got_x: bool = false;
var cap_client_got_y: bool = false;
var cap_done: bool = false;
/// Server half of the "open" pattern: receive one request carrying a capability,
/// verify it, then reply handing back a capability of its own.
fn capServer() void {
const me = scheduler.current();
cap_ep_y = ipcsync.createEndpoint().?;
const h_y = ipcsync.installHandle(me, cap_ep_y); // the handle to send back in the reply
var reply_buffer: [8]u8 = .{0} ** 8;
var receive: [8]u8 = undefined;
var badge: u64 = 0;
var received: u64 = abi.no_cap;
// Phase 1: no reply owed yet — receive the client's request, which carries ep_x.
_ = ipcsync.replyWait(cap_endpoint, @intFromPtr(&reply_buffer), 0, @intFromPtr(&receive), receive.len, abi.no_cap, &badge, &received);
cap_server_got_x = received != abi.no_cap and
ipcsync.resolveHandle(me, received) == cap_ep_x and
cap_ep_x.refcount == 2; // shared (client's handle + this one), not moved
// Phase 2: reply to the held client, handing it ep_y; then block for a next
// request that never comes (so this replyWait does not return).
_ = ipcsync.replyWait(cap_endpoint, @intFromPtr(&reply_buffer), 8, @intFromPtr(&receive), receive.len, @intCast(h_y), &badge, &received);
scheduler.exit();
}
/// Client half of "open": mint a capability, send it in a call, receive one back.
fn capClient() void {
const me = scheduler.current();
cap_ep_x = ipcsync.createEndpoint().?;
const h_x = ipcsync.installHandle(me, cap_ep_x);
var message: [8]u8 = .{0} ** 8;
var reply: [8]u8 = undefined;
var received: u64 = abi.no_cap;
_ = ipcsync.call(cap_endpoint, @intFromPtr(&message), 8, @intFromPtr(&reply), reply.len, @intCast(h_x), &received);
cap_client_got_y = received != abi.no_cap and
ipcsync.resolveHandle(me, received) == cap_ep_y and
cap_ep_y.refcount == 2;
cap_done = true;
scheduler.exit();
}
/// IPC capability passing: a client hands the server an endpoint in a `call`, and the
/// server hands one back in its reply — the primitive that lets a bus driver give a
/// class driver a private channel to one device (M13). Two kernel tasks (no user ELF);
/// each verifies the endpoint it received is the *same* object the peer sent (resolves
/// equal) and was *shared*, not moved (refcount bumped to 2).
fn capabilityTest() void {
log("DANOS-TEST-BEGIN: ipc-cap\n", .{});
cap_endpoint = ipcsync.createEndpoint().?;
cap_server_got_x = false;
cap_client_got_y = false;
cap_done = false;
scheduler.spawn(capServer, 5);
scheduler.spawn(capClient, 5);
const done: *volatile bool = &cap_done;
var spins: u64 = 0;
while (!done.* and spins < 100_000_000) : (spins += 1) scheduler.yield();
check("capability test completed", cap_done);
check("server received the client's endpoint (same object, shared not moved)", cap_server_got_x);
check("client received the server's endpoint back (same object, shared not moved)", cap_client_got_y);
result();
}
var proc_worker_run: bool = true; var proc_worker_run: bool = true;
var proc_worker_ran: bool = false; var proc_worker_ran: bool = false;
+1 -1
View File
@@ -128,7 +128,7 @@ pub fn main() void {
var reply_len: usize = 0; var reply_len: usize = 0;
var receive: [protocol.message_maximum]u8 = undefined; var receive: [protocol.message_maximum]u8 = undefined;
while (true) { while (true) {
const got = runtime.ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive); const got = runtime.ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
// Ignore notifications (none expected here); handle a request. // Ignore notifications (none expected here); handle a request.
reply_len = handle(receive[0..got.len], &reply_buffer); reply_len = handle(receive[0..got.len], &reply_buffer);
} }
+5
View File
@@ -125,6 +125,11 @@ CASES = [
{"name": "ipc-call", {"name": "ipc-call",
"expect": r"DANOS-TEST-RESULT: PASS", "expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
# IPC capability passing (M13): a client hands the server an endpoint in a call,
# the server hands one back in its reply; each is verified same-object + shared.
{"name": "ipc-cap",
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# Parallelism: needs more than one core, so this case boots with -smp 4. # Parallelism: needs more than one core, so this case boots with -smp 4.
{"name": "smp", {"name": "smp",
"smp": 4, "smp": 4,