nub_arch_x86_abi/lib.rs
1//! Wire format for the host ↔ guest "run this PVM program" RPC.
2//!
3//! The host pre-publishes each `Cap` it wants the guest to see via
4//! the [`FN_ID_NUB_PUT_CAP`] RPC (rkyv-archived `javm_cap::Cap`
5//! payload; see the `state_cache` module in `nub-arch-x86` for the
6//! guest-side heap-resident directory it lands in), then ships a
7//! fixed-size
8//! [`InvokePacket`] referencing the published `Cap::Instance` by
9//! hash on every call. The invoke packet is `#[repr(C)]` bytes (no
10//! codec); the response is rkyv-archived ([`InvocationResult`]).
11
12#![cfg_attr(not(feature = "std"), no_std)]
13
14extern crate alloc;
15
16use core::sync::atomic::{AtomicU32, AtomicU64};
17
18/// Maximum fixed execution lanes the guest runtime can address. The production
19/// default vCPU pool is capped lower, but host configuration must not exceed
20/// this ABI-visible lane table size.
21pub const MAX_EXECUTION_LANES: usize = 64;
22
23/// `fn_id` for the `nub_heap_stats` diagnostic. Payload is empty;
24/// response is 32 bytes packing four LE u64s (allocated_bytes,
25/// allocation_count, fragment_count, available_bytes).
26pub const FN_ID_NUB_HEAP_STATS: u32 = 2;
27
28/// `fn_id` for the cache-based RPC. Payload is a
29/// [`InvokePacket`] (host-side `#[repr(C)]` bytes, no rkyv); the
30/// guest dereferences cache VAs by `instance_hash` lookup, runs the
31/// JIT, and replies with rkyv-archived [`InvocationResult`].
32pub const FN_ID_NUB_INVOKE_CACHED: u32 = 3;
33
34/// `fn_id` for the heap-resident cap directory `put_cap` RPC.
35///
36/// Payload: rkyv-archived `javm_cap::Cap`. Guest validates and
37/// materialises via [`rkyv::access`] + [`rkyv::deserialize`], computes
38/// the cap's content hash, inserts into the guest-resident `CACHE`
39/// (a resident `CacheDirectory<FixedState, CachedCap>` holding
40/// `HashMap<CapHash, Arc<CachedCap>>` in talc heap), and replies with the
41/// rkyv-archived [`CapHash`] (raw
42/// 32 bytes). The host's `MultiUseSandbox::put_cap` propagates a
43/// `CapHasRefError` from `javm_cap` if any slot still holds a Ref.
44pub const FN_ID_NUB_PUT_CAP: u32 = 4;
45
46/// `fn_id` for the boot-info-read diagnostic RPC. Empty payload; the
47/// guest replies with the raw bytes of its [`BootInfo`] struct. The
48/// host can also obtain the same data by reading the kernel's
49/// `.boot_info` ELF section directly — this RPC exists as a
50/// belt-and-braces fallback in case the ELF symbol lookup misses.
51pub const FN_ID_NUB_GET_BOOT_INFO: u32 = 5;
52
53/// `fn_id` for the bench-only "evict the entire JIT compile cache"
54/// RPC. Empty payload; empty response. Used by `javm-bench` to force
55/// each criterion iteration to pay the recompile cost (otherwise the
56/// JIT cache turns the loop into pure warm-cache execute, which isn't
57/// what we want to measure for PolkaVM-shaped workloads).
58pub const FN_ID_NUB_EVICT_JIT_ALL: u32 = 6;
59/// `fn_id` for a long-lived per-vCPU invoke worker. Payload is a little-endian
60/// `u32` lane index. The function does not use the legacy rkyv response ring;
61/// it polls that lane's [`ParallelInvokeSlot`] in scratch memory, runs invokes
62/// with `run_top_on_lane`, and writes results back into the same slot.
63pub const FN_ID_NUB_INVOKE_WORKER: u32 = 7;
64
65/// Boot-time info published by the guest at a known location (linker
66/// section `.boot_info`). The host reads it after the sandbox boots
67/// to learn the VA of the guest's cap directory, then dereferences
68/// the directory directly from host code (the kernel half is mapped
69/// at the same VA via the shallow-PML4-copy mechanism, so a
70/// directory-VA pointer is valid both in guest kernel mode and via
71/// the host's mmap shadow of the kernel image).
72///
73/// `magic` is checked first as a sanity guard against reading a
74/// stale or wrong-binary boot region. `directory_va` is the address
75/// of the guest's resident cap directory.
76/// `directory_type_id` lets future protocol upgrades reject a
77/// mismatched layout (today: opaque sentinel matching
78/// `CacheDirectory<FixedState, CachedCap>` — bumped when any field is added or
79/// its type changes).
80#[repr(C)]
81#[derive(Clone, Copy, Debug)]
82pub struct BootInfo {
83 /// `BootInfo::MAGIC` ("JAR_BOOT" in ASCII, little-endian). Host
84 /// reader rejects a region whose first 8 bytes don't match.
85 pub magic: u64,
86 /// VA of the cap directory's inner `HashMap` (NOT the wrapping
87 /// `Mutex`). Resolved by `nub-arch-x86` at boot via
88 /// `init_directory_va`.
89 pub directory_va: u64,
90 /// Hash of the directory's type signature. Bumped when the wire
91 /// layout of the directory changes. Today: opaque sentinel, the
92 /// host just compares for equality.
93 pub directory_type_id: u64,
94 /// Base of the per-process GUEST_VA reservation. Mirrors the
95 /// host-side constant; reproduced here so the host can sanity-
96 /// check the guest agrees on the layout.
97 pub guest_va_base: u64,
98 /// Reserved space for future fields. Zero-initialised; host
99 /// readers should not depend on the contents.
100 pub _reserved: [u64; 12],
101}
102
103impl BootInfo {
104 /// Constant numeric guard. The hex digits spell "JAR_BOOT" when
105 /// interpreted as ASCII bytes in big-endian order
106 /// (`0x4A 0x41 0x52 0x5F 0x42 0x4F 0x4F 0x54`). Stored as the
107 /// numeric u64 with that big-endian interpretation — read+compare
108 /// is a single u64 load.
109 pub const MAGIC: u64 = 0x4A41_525F_424F_4F54;
110}
111
112/// 32-byte Cap::Instance identity hash. Matches
113/// `javm_cap::CapHash` byte-wise (kept as a local alias here so
114/// `nub-arch-x86-abi` stays free of the javm-cap dependency, which
115/// pulls in `alloc::collections` etc.).
116pub type CapHash = [u8; 32];
117
118/// Fixed-layout invocation packet. Sent as raw `#[repr(C)]` bytes via
119/// the existing rkyv `Request` envelope (its `payload` field). The
120/// guest reads the bytes directly with `core::ptr::read_unaligned`.
121///
122/// `instance_hash` keys the cap to invoke (a published `Cap::Instance`).
123/// `endpoint_idx` selects the entry within `ImageCap.endpoints`.
124/// `args` overlay φ[7..=10] on top of the endpoint's `initial_regs`.
125#[repr(C)]
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub struct InvokePacket {
128 pub instance_hash: CapHash,
129 pub endpoint_idx: u32,
130 pub _pad: u32,
131 pub args: [u64; 4],
132 pub initial_gas: u64,
133}
134
135impl InvokePacket {
136 /// Size of the packet in bytes — what the host writes to the
137 /// `Request.payload` and what the guest reads back.
138 pub const SIZE: usize = core::mem::size_of::<Self>();
139
140 /// Cast the packet to its raw bytes.
141 pub fn as_bytes(&self) -> &[u8] {
142 unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::SIZE) }
143 }
144
145 /// Parse a packet from raw bytes (length-checked).
146 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
147 if bytes.len() != Self::SIZE {
148 return None;
149 }
150 Some(unsafe { core::ptr::read_unaligned(bytes.as_ptr() as *const Self) })
151 }
152}
153
154/// Bytes of the running Instance's scratchpad (`slot[0]`) region surfaced at the
155/// top-level HALT — a fixed-size **head** of the returned DataCap's effective
156/// content. The guest writes its result into the scratchpad-mapped memory
157/// region during the run (CoW into the cap); at top HALT the engine reads the
158/// region's effective bytes back out here, so the host observes the full,
159/// uncompressed result without a separate data-flow event.
160///
161/// V1 surfaces a fixed-size window (enough for the fuzz differential's 13-slot
162/// register signature: 13 × 8 = 104 ≤ 128). The full variable-length DataCap
163/// return is deferred to the YieldMarker/YieldCatcher kernel design — see
164/// `kernel-assisted-instances.md`. Zero-filled when the Instance maps no
165/// scratchpad region (every non-fuzz path today).
166pub const SCRATCHPAD_HEAD_LEN: usize = 128;
167
168/// Invocation result. Both backends produce this shape on completion;
169/// rkyv-archived on the wire from the cached path's response.
170#[repr(C)]
171#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize)]
172#[rkyv(derive(Debug, PartialEq, Eq))]
173pub struct InvocationResult {
174 pub exit_reason: u32,
175 pub exit_arg: u32,
176 pub return_value: u64,
177 pub gas_remaining: u64,
178 /// Effective bytes of the running Instance's scratchpad (`slot[0]`) region
179 /// head at top HALT (see [`SCRATCHPAD_HEAD_LEN`]). Zero when no scratchpad
180 /// region is mapped.
181 pub scratchpad_head: [u8; SCRATCHPAD_HEAD_LEN],
182}
183
184pub const PARALLEL_INVOKE_SLOT_BYTES: usize = 512;
185
186pub const PARALLEL_INVOKE_STATUS_EMPTY: u32 = 0;
187pub const PARALLEL_INVOKE_STATUS_READY: u32 = 1;
188pub const PARALLEL_INVOKE_STATUS_RUNNING: u32 = 2;
189pub const PARALLEL_INVOKE_STATUS_DONE: u32 = 3;
190pub const PARALLEL_INVOKE_STATUS_STOP: u32 = 4;
191pub const PARALLEL_INVOKE_STATUS_STARTING: u32 = 5;
192pub const PARALLEL_INVOKE_STATUS_EVICT_JIT_READY: u32 = 6;
193
194/// One host<->guest invoke slot. Slots are addressed by lane index at
195/// `parallel_slot_base + lane * PARALLEL_INVOKE_SLOT_BYTES`.
196///
197/// Synchronization protocol:
198/// - host writes `job_id` and `packet`, then stores `READY` with release;
199/// - guest CASes `READY -> RUNNING`, runs the invoke, writes `result`, then
200/// stores `DONE` with release;
201/// - host reads `DONE` with acquire, copies `result`, then stores `EMPTY`.
202///
203/// Bench-only control commands, such as `EVICT_JIT_READY`, use the same
204/// `RUNNING -> DONE -> EMPTY` completion protocol after the host reserves all
205/// lanes.
206#[repr(C, align(64))]
207pub struct ParallelInvokeSlot {
208 pub status: AtomicU32,
209 pub _pad0: u32,
210 pub job_id: AtomicU64,
211 pub packet: InvokePacket,
212 pub result: InvocationResult,
213}
214
215const _: () = assert!(core::mem::size_of::<ParallelInvokeSlot>() <= PARALLEL_INVOKE_SLOT_BYTES);
216const _: () = assert!(core::mem::align_of::<ParallelInvokeSlot>() <= 64);