subsoil/lib.rs
1//! Guest-side runtime support library for JAVM chain Images.
2//!
3//! Provides compiler builtins (memset, memcpy, memcmp), a panic
4//! handler, a default trap `_start` (so guests link without
5//! defining one themselves), and the `map_args` runtime helper
6//! that moves the kernel-allocated args DATA cap from bare-Frame
7//! slot 4 into the guest's main-frame CapTable and maps it into
8//! the guest address space.
9//!
10//! Guests declare their entry points via the
11//! `#[subsoil::endpoint(N)]` attribute (from `subsoil-derive`).
12//! Each annotation emits a per-endpoint trampoline that calls the
13//! user fn and halts; the kernel enters trampolines via
14//! `endpoints[N].entry_pc`. `_start` (PC=0) is never an intended
15//! entry — it traps via `unimp` if ever reached.
16//!
17//! All freestanding-only symbols are gated behind `cfg(target_os =
18//! "none")` — on host this crate is empty. Services force-link it via
19//! `use subsoil as _;`.
20
21#![no_std]
22
23pub use subsoil_derive::endpoint;
24
25/// Descriptor written into the `.subsoil.endpoints` ELF section by
26/// the [`endpoint`] attribute macro. The JAVM transpiler reads this
27/// section at link time and uses each entry to populate the chain
28/// Image's `endpoints: BTreeMap<u8, EndpointDef>` field.
29///
30/// Layout is `#[repr(C)]` so the transpiler can decode the section
31/// as a flat array of fixed-size records. On RISC-V64 the function
32/// pointer occupies 8 bytes, followed by 8 bytes of metadata, for a
33/// total stride of 16 bytes.
34#[repr(C)]
35pub struct EndpointDescriptor {
36 /// RISC-V address of the endpoint function. The transpiler maps
37 /// this to a PVM PC via its instruction-mapping table.
38 pub fn_ptr: fn(u64) -> u64,
39 /// Endpoint index (key in the chain Image's `endpoints` map).
40 pub index: u8,
41 /// Caller-supplied register-arg count (per Image::EndpointDef).
42 pub arg_registers: u8,
43 /// Caller-supplied arg-cnode size (per Image::EndpointDef).
44 pub arg_cnode_size: u8,
45 /// Reserved for alignment / future expansion.
46 pub _pad: [u8; 5],
47}
48
49// -- Compiler builtins (freestanding targets only) ----------------------------
50
51#[cfg(target_os = "none")]
52mod builtins {
53 #[unsafe(no_mangle)]
54 pub unsafe extern "C" fn memset(dst: *mut u8, val: i32, n: usize) -> *mut u8 {
55 let mut i = 0;
56 while i < n {
57 unsafe { *dst.add(i) = val as u8 };
58 i += 1;
59 }
60 dst
61 }
62
63 #[unsafe(no_mangle)]
64 pub unsafe extern "C" fn memcpy(dst: *mut u8, src: *const u8, n: usize) -> *mut u8 {
65 let mut i = 0;
66 while i < n {
67 unsafe { *dst.add(i) = *src.add(i) };
68 i += 1;
69 }
70 dst
71 }
72
73 #[unsafe(no_mangle)]
74 pub unsafe extern "C" fn memcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 {
75 let mut i = 0;
76 while i < n {
77 let a = unsafe { *s1.add(i) };
78 let b = unsafe { *s2.add(i) };
79 if a != b {
80 return a as i32 - b as i32;
81 }
82 i += 1;
83 }
84 0
85 }
86}
87
88// -- Panic handler (freestanding targets only) --------------------------------
89
90#[cfg(target_os = "none")]
91#[panic_handler]
92fn panic(_: &core::panic::PanicInfo) -> ! {
93 unsafe {
94 core::arch::asm!("li a0, 0xDEAD", "unimp", options(noreturn));
95 }
96}
97
98// -- Default `_start` ---------------------------------------------------------
99//
100// The linker picks `_start` as the default ELF entry symbol. Guests
101// never expect PC=0 to be entered at runtime — the kernel always
102// enters via `endpoints[N].entry_pc` (a trampoline emitted by
103// `#[subsoil::endpoint(N)]`). This default `_start` exists only to
104// satisfy the linker and traps loudly if ever reached.
105#[cfg(target_env = "javm")]
106core::arch::global_asm!(
107 ".section .text._start, \"ax\", @progbits",
108 ".global _start",
109 "_start:",
110 " unimp",
111);
112
113// -- Args helper --------------------------------------------------------------
114
115/// Map the kernel-supplied args DATA cap into guest address space and
116/// return a slice over its bytes. See [`InvocationKernel::set_args`]
117/// for the kernel side: the kernel allocates a fresh DATA cap, writes
118/// the host's bytes into its backing pages, and places it at
119/// bare-Frame slot 4. The guest is responsible for the MOVE +
120/// MGMT_MAP steps because the kernel rule "MGMT_MAP only on caps
121/// held in a VM's persistent Frame" requires the cap to live in the
122/// active VM's main frame before mapping.
123///
124/// Steps:
125/// 1. MGMT_MOVE: bare-Frame[4] → main-frame[`ARGS_SLOT`].
126/// 2. MGMT_MAP: main-frame[`ARGS_SLOT`] @ `args_base_page`, RW, all
127/// pages.
128/// 3. Return `&[u8]` of length `args_len` at the mapped byte address.
129///
130/// `args_base_page` is chosen as `(_end + JAVM_HEAP_BYTES + 4095) /
131/// 4096` — one page past the program's heap top. `_end` is the LLD
132/// linker-emitted symbol pointing to the end of `.bss`;
133/// `JAVM_HEAP_BYTES` matches the transpiler's hard-coded
134/// `heap_pages = 16` (`javm-transpiler::linker::heap_pages`). If the
135/// transpiler's heap size ever changes, this constant must be
136/// updated in lockstep.
137///
138/// Returns `&[]` if `args_len == 0` (no MOVE/MAP performed). On
139/// freestanding targets only — host expansion is a stub.
140///
141/// # Safety
142/// Issues `ecall`s that mutate kernel state. Must be called at most
143/// once per invocation; subsequent calls would attempt to MOVE from
144/// an already-empty bare-Frame slot 4 and fail with `RESULT_WHAT`.
145/// Cache the returned slice if multiple readers need the bytes.
146#[cfg(all(target_env = "javm", target_os = "none"))]
147pub fn map_args(args_len: u64) -> &'static [u8] {
148 if args_len == 0 {
149 return &[];
150 }
151
152 // Heap size assumed by the transpiler (`javm-transpiler::linker.rs:
153 // heap_pages = 16`). Args must live above the heap top.
154 const JAVM_HEAP_BYTES: u64 = 16 * 4096;
155 const ARGS_SLOT: u64 = 69;
156
157 unsafe extern "C" {
158 static _end: u8;
159 }
160 let end_addr = (&raw const _end) as u64;
161 let args_base_addr = (end_addr + JAVM_HEAP_BYTES).next_multiple_of(4096);
162 let args_base_page = args_base_addr / 4096;
163 let page_count = args_len.div_ceil(4096);
164
165 // Cap-ref encoding (see `javm_legacy::kernel::resolve_cap_ref`):
166 // - direct slot N in the active VM: `N` (8 bits)
167 // - slot N in the bare Frame: `N << 8` (cross slot 0
168 // of active VM to bare Frame, then access slot N)
169 let bare_frame_slot_4: u64 = 4 << 8; // = 0x400
170 let main_frame_args: u64 = ARGS_SLOT;
171
172 // MGMT_MOVE: subject = bare-Frame[4], object = main-frame[ARGS_SLOT].
173 // φ[11] = MGMT_MOVE = 6
174 // φ[12] = (subject << 32) | object
175 let move_refs: u64 = (bare_frame_slot_4 << 32) | main_frame_args;
176 unsafe {
177 core::arch::asm!(
178 // CSR 0x800 marker: tells the transpiler the next ecall
179 // is a PVM ecall (management op), not ecalli (CALL cap).
180 "csrw 0x800, zero",
181 "ecall",
182 in("a4") 6u64, // φ[11] = MGMT_MOVE
183 in("a5") move_refs, // φ[12] = (subject << 32) | object
184 // Kernel may overwrite a0..a5 with result code; mark all clobbered.
185 lateout("a0") _, lateout("a1") _, lateout("a2") _,
186 lateout("a3") _, lateout("a4") _, lateout("a5") _,
187 );
188 }
189
190 // MGMT_MAP: subject = main-frame[ARGS_SLOT], all pages, RW.
191 // φ[7] = base_offset = args_base_page
192 // φ[8] = page_offset = 0
193 // φ[9] = page_count
194 // φ[10] = access = 1 (RW)
195 // φ[11] = MGMT_MAP = 2
196 // φ[12] = (subject << 32) | 0
197 let map_refs: u64 = main_frame_args << 32;
198 unsafe {
199 core::arch::asm!(
200 "csrw 0x800, zero",
201 "ecall",
202 in("a0") args_base_page,
203 in("a1") 0u64,
204 in("a2") page_count,
205 in("a3") 1u64,
206 in("a4") 2u64,
207 in("a5") map_refs,
208 lateout("a0") _, lateout("a1") _, lateout("a2") _,
209 lateout("a3") _, lateout("a4") _, lateout("a5") _,
210 );
211 }
212
213 // SAFETY: kernel mapped `page_count` pages of args bytes at
214 // `args_base_addr` with RW access. The slice lives for the rest
215 // of the invocation (until the kernel tears down the window).
216 unsafe { core::slice::from_raw_parts(args_base_addr as *const u8, args_len as usize) }
217}
218
219/// Host-side stub. Always returns an empty slice; meaningful only on
220/// the JAVM freestanding target.
221#[cfg(not(all(target_env = "javm", target_os = "none")))]
222pub fn map_args(_args_len: u64) -> &'static [u8] {
223 &[]
224}