Skip to main content

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}