Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ pub enum KoreProviderError {
ReadOnly = 13,
Backpressure = 14,
Timeout = 15,
/// A DMA window an earlier client lent has not retired from the device and
/// the requester-wide retirement failed: no new client window until a retry
/// succeeds. Existing lanes keep serving.
Retiring = 16,
}

/// Scoped generational name. Copying its bits does not duplicate native ownership.
Expand Down
1 change: 1 addition & 0 deletions crates/ffi/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ fn provider_class(error: dag::ProviderError) -> KoreProviderError {
R::ReadOnly => C::ReadOnly,
R::Backpressure => C::Backpressure,
R::Timeout => C::Timeout,
R::Retiring => C::Retiring,
}
}

Expand Down
32 changes: 22 additions & 10 deletions crates/lua/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,16 @@
//! remote list — or as pointers into a frame the owner keeps alive across
//! the whole phase (the eval's script).
//!
//! Resolution order (`SEARCH`): the task's own tree, then `libs/`; a source
//! module `<name>.lua` wins over a native `<name>.so`. Native modules are
//! store objects stamped `EXECUTABLE_IMAGE` that `kore-late` checks, places
//! in the JIT arena, and links against this executable's exported C API.
//! Resolution: a source module `<name>.lua` from `SOURCE_SEARCH` — the
//! task's own tree, then `libs/` — wins over a native `<name>.so`, which
//! comes from the READ-only `libs/` view alone. Source runs inside the VM
//! with the VM's authority; native code runs with the task's whole
//! authority, so it is never taken from a tree the task can write (nor can
//! a squatting `<name>.so` there shadow the library). `kore-late` holds
//! that rule: it resolves the library edge itself, refuses a member not
//! stamped `EXECUTABLE_IMAGE` (a stamp only the install authority can
//! make), checks it, places it in the JIT arena, and links it against this
//! executable's exported C API.

use alloc::vec::Vec;
use core::cell::UnsafeCell;
Expand Down Expand Up @@ -81,9 +87,10 @@ use crate::{Answer, Extension, OP_EMBEDDER_BASE, Policy};
/// engine sees it. Larger sources belong in the store as several modules.
pub const MAX_SOURCE_MODULE_BYTES: usize = 64 * 1024;

/// Where `require` looks, in order: the task's own tree (its cwd view),
/// then the READ-only `libs/` view every task carries.
const SEARCH: &[&[u8]] = &[b"", b"libs"];
/// Where `require` looks for a *source* module, in order: the task's own
/// tree (its cwd view), then the READ-only `libs/` view every task carries.
/// A native module takes no search list (`Loader::load_native`).
const SOURCE_SEARCH: &[&[u8]] = &[b"", kore_late::LIBS_VIEW.as_bytes()];

/// The prefix a native module's opener carries: `luaopen_<name>`.
const OPENER_PREFIX: &[u8] = b"luaopen_";
Expand Down Expand Up @@ -1112,7 +1119,9 @@ enum Reply {
}

async fn resolve(services: &mut Services<'_, '_>, stem: &[u8], policy: Policy) -> Reply {
let search = Search { prefixes: SEARCH };
let search = Search {
prefixes: SOURCE_SEARCH,
};
let Some(fs) = services.fs().await else {
return Reply::Error(c"filesystem face is unavailable");
};
Expand Down Expand Up @@ -1143,7 +1152,7 @@ async fn resolve(services: &mut Services<'_, '_>, stem: &[u8], policy: Policy) -
let (Some(fs), Some(loader)) = (services.fs.as_mut(), services.loader.as_mut()) else {
return Reply::Error(c"filesystem face is unavailable");
};
let id = match loader.load_native(fs, search, stem).await {
let id = match loader.load_native(fs, stem).await {
Ok(id) => id,
Err(error) => return Reply::Error(describe(error)),
};
Expand All @@ -1165,7 +1174,10 @@ async fn resolve(services: &mut Services<'_, '_>, stem: &[u8], policy: Policy) -
fn describe(error: LoadError) -> &'static CStr {
match error {
LoadError::Name => c"module name is not a store name",
LoadError::NotFound => c"no <name>.lua in the task tree and no libs/<name>.so",
LoadError::NotFound => {
c"no <name>.lua in the task tree or libs/, and no libs/<name>.so (native modules load only from libs/)"
}
LoadError::LibsWritable => c"libs/ is writable by this task; native modules load only from the READ-only library view",
LoadError::NotExecutableImage => c"libs/ member is not stamped EXECUTABLE_IMAGE",
LoadError::TooLarge => c"module exceeds the bounded loader",
LoadError::TooMany => c"module table is full",
Expand Down
22 changes: 15 additions & 7 deletions docs/lua.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,13 +542,21 @@ remember.

## `require`

`require "<name>"` resolves against the task's own tree first, then `libs/`; a
source module `<name>.lua` wins over a native `<name>.so`. Source modules are
bounded at 64 KiB — larger sources belong in the store as several modules.

A native module is a store object stamped `EXECUTABLE_IMAGE` that is checked,
placed in the JIT arena, and linked against this executable's exported C API. It
must export `luaopen_<name>`.
`require "<name>"` looks for a source module `<name>.lua` in the task's own
tree first, then `libs/`; a source module wins over a native `<name>.so`. Source
modules run inside the VM with the VM's authority and are bounded at 64 KiB —
larger sources belong in the store as several modules.

A native module runs with the task's whole authority, so it comes from the
READ-only `libs/` view **alone** — never from a tree the task can write, and a
`<name>.so` squatting there cannot shadow the library. `kore-late` holds that
rule: it enters the `libs` grant directly (not by path, which a `libs` child of
the task's own root would answer first), refuses one the task could write
beneath, and refuses a member unless it is stamped `EXECUTABLE_IMAGE` — a stamp
the store takes, and keeps over later byte changes, only through the install
authority's `BIND_EXECUTABLE`. The member is then checked, placed in the JIT
arena, and linked against this executable's exported C API. It must export
`luaopen_<name>`.

## Errors

Expand Down
6 changes: 6 additions & 0 deletions extern/c/include/kore.h
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ enum KoreProviderError
KORE_PROVIDER_ERROR_READ_ONLY = 13,
KORE_PROVIDER_ERROR_BACKPRESSURE = 14,
KORE_PROVIDER_ERROR_TIMEOUT = 15,
/**
* A DMA window an earlier client lent has not retired from the device and
* the requester-wide retirement failed: no new client window until a retry
* succeeds. Existing lanes keep serving.
*/
KORE_PROVIDER_ERROR_RETIRING = 16,
};
#ifndef __cplusplus
#if __STDC_VERSION__ >= 202311L
Expand Down
10 changes: 6 additions & 4 deletions native/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions native/crates/abi/src/cap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ pub enum ResourceKind {
/// peer window it sponsored before the epoch may be reclaimed.
/// Append-only wire.
DeviceMmu,
/// Boot-discovered memory controller; currently kernel-owned.
MemoryController,
}

/// The stamped capability record as it lives in the shared cap arena — the
Expand Down
6 changes: 6 additions & 0 deletions native/crates/abi/src/guest/aarch64/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! AArch64 guest architecture vocabulary.
//!
//! The kernel's EL2 guest backend is not implemented yet. No register
//! indices are exported until that backend defines its wire conversion;
//! in particular, this module must not inherit x86-64's register numbering.
//! Shared records and semantic role positions remain in the parent module.
74 changes: 74 additions & 0 deletions native/crates/abi/src/guest/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Guest exit/control wire records — the **per-vCPU window vocabulary**
//! (#487/#481), frozen here because kernel and hypervisor are two
//! endpoints of one contract and can never drift on a layout.
//!
//! One page per vCPU carries **two SPSC rings** (`kore-ring`'s frozen
//! `SPSC_ABI = 1` format, one ring per direction):
//!
//! ```text
//! offset 0: exit ring (kernel → hypervisor,
//! capacity 2, GuestExitRecord)
//! offset EXIT_RING_BYTES: control ring (hypervisor → kernel,
//! capacity 4, GuestCtrlRecord)
//! ```
//!
//! The exit capacity is **2 by proof, not by tuning** (#487 §5): the
//! producer is the vCPU run loop, which parks after every publish, so at
//! most one ordinary exit plus one terminal record can ever be
//! outstanding. `Full` on the exit ring is therefore a protocol
//! violation and the kernel's response is the fail-closed poisoned-window
//! retire — never a drop, never a wait. The control capacity (4) is the
//! largest sane batch (`WriteState + Inject + Resume` and headroom); the
//! run loop drains it to empty at its top-of-loop safe point, so `Full`
//! there is ordinary backpressure on an awake producer.
//!
//! Records are **bytes and indices only** — no capability ever rides a
//! ring (the standing doctrine). Every field is any-bit-valid on read:
//! the peer is untrusted, and a record must decode from arbitrary bytes
//! (unknown kinds refuse at the consumer, they do not trap).
//!
//! The register image rides as a fixed `[u64; GUEST_STATE_QWORDS]` in
//! the **executing architecture's own frozen wire order** — the kernel's
//! per-arch `state_to_wire`/`state_from_wire` seam uses the shared architecture
//! layout selected by [`arch`] so the instruction decoder cannot drift from it.
//! The record itself stays architecture-agnostic: the protocol cites
//! registers exclusively through the semantic roles in [`state_index`],
//! which every architecture's wire order is required to satisfy. The
//! array is the contract, never a field list.
//!
//! ## The decode-assist tail (#765)
//!
//! A stage-2 fault names an *address*, not an instruction. Servicing it
//! as MMIO needs the trapped instruction's width, direction and register
//! — facts the hardware hands over in three different shapes: AMD stores
//! the raw instruction bytes in the VMCB (decode assists), Intel stores
//! nothing and the kernel fetches the bytes through the guest's own page
//! tables, and Arm / RISC-V report the access **already decoded**
//! (`ESR_EL2.ISS` / `htinst`). An exit record therefore carries whichever
//! shape the producing architecture has, tagged by [`GuestExitRecord::assist`]
//! (the [`assist`] roster): raw bytes in `insn`/`insn_len`, or a decoded
//! access in `access`/`data`. The consumer's one job is to lift that into
//! a decoded access — running an ISA decoder only when handed bytes — so
//! an architecture whose hardware decodes for it never needs one. No form
//! is mandatory: [`assist::NONE`] is an honest "no help", and the consumer
//! decides fail-closed.

// Keep architecture selection here, as in the kernel's platform::arch.
// Consumers use guest::arch; an x86 register name cannot leak into a
// build for another ISA through an unconditional module.
#[cfg(target_arch = "aarch64")]
pub mod aarch64;
#[cfg(target_arch = "riscv64")]
pub mod riscv64;
#[cfg(target_arch = "x86_64")]
pub mod x86_64;

#[cfg(target_arch = "aarch64")]
pub use aarch64 as arch;
#[cfg(target_arch = "riscv64")]
pub use riscv64 as arch;
#[cfg(target_arch = "x86_64")]
pub use x86_64 as arch;

mod wire;
pub use wire::*;
6 changes: 6 additions & 0 deletions native/crates/abi/src/guest/riscv64/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! RISC-V guest architecture vocabulary.
//!
//! The kernel's H-extension guest backend is not implemented yet. No register
//! indices are exported until that backend defines its wire conversion;
//! in particular, this module must not inherit x86-64's register numbering.
//! Shared records and semantic role positions remain in the parent module.
Loading
Loading