Skip to content
Open
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
72 changes: 66 additions & 6 deletions .github/scripts/check-libziskos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ set -euo pipefail
FAIL=0
TARGET=riscv64ima-zisk-zkvm-elf

cargo +zisk build -p ziskos-staticlib --release \
# Unoptimised, no LTO, no debug info.
# opt-level=0 keeps every crate as a separate ELF object with intact symbol
# tables; LTO would merge everything into a single bitcode module where
# per-object undefined references are no longer visible to nm, which would
# break the global-allocator check below. Debug symbols are suppressed to
# keep the artifact small and the nm output clean.
cargo +zisk build -p ziskos-staticlib \
--target "$TARGET" \
--config 'profile.release.lto="fat"'
--config 'profile.dev.debug=false'

LIBZISKOS=$(find target -name "libziskos_staticlib.a" -path "*$TARGET*" | head -1)
LIBZISKOS="target/$TARGET/debug/libziskos_staticlib.a"
if [ -z "$LIBZISKOS" ]; then
echo "FAIL: libziskos_staticlib.a not found"
exit 1
Expand All @@ -25,8 +31,9 @@ else
echo "OK: no std object files bundled"
fi

# Use llvm-nm from the zisk toolchain sysroot to avoid LLVM version mismatches
# when reading bitcode objects produced by lto="fat".
# Use llvm-nm from the zisk toolchain sysroot for consistent symbol output.
# Standard nm also works for regular ELF objects (no fat LTO bitcode here),
# but llvm-nm is already available and produces identical results.
rustup component add llvm-tools --toolchain zisk 2>/dev/null || true
LLVM_NM=$(rustup run zisk sh -c 'find "$(rustc --print sysroot)" -name "llvm-nm" -type f | head -1')
if [[ -z "$LLVM_NM" ]]; then
Expand Down Expand Up @@ -75,8 +82,61 @@ for SYM in "${REQUIRED_SYMBOLS[@]}"; do
fi
done

# ── No global-allocator usage in accelerator code ─────────────────────────────
# Accelerator functions must allocate exclusively through the scratch arena
# (BumpScratch), never through the global allocator.
#
# The check looks for "U __rustc::__rust_alloc" — an undefined reference to the
# global allocator entry point — in object files that belong to the ziskos crates.
#
# Why this catches all global-allocator use:
# The alloc-crate functions that lead to __rust_alloc (Global::allocate,
# alloc::alloc::alloc, exchange_malloc for Box, …) are all marked #[inline].
# The Rust compiler emits #[inline] functions into every crate that uses them
# as LLVM "available_externally" definitions — even at opt-level=0 — so their
# bodies, which reference __rust_alloc, appear in the caller's object file as
# undefined (U) symbols. Any ziskos object that uses the global allocator
# through any path (Vec<T, Global>, Box, String, Arc, …) therefore carries
# "U __rustc::__rust_alloc" in its symbol table.
#
# Symbol name:
# The raw mangled form (_RNvCsd..._7___rustc12___rust_alloc) embeds a
# build-specific crate hash, so -C (demangle) is required before matching.
#
# Filter:
# llvm-nm -A -C output format:
# <archive_path>:<member_name>: <addr> <type> <demangled_symbol>
# awk -F: isolates field $2 (the member filename). Members whose name starts
# with "ziskos" are the project crates; alloc, core, proofman_verifier, serde,
# num_bigint, and other dependencies that legitimately call __rust_alloc are
# excluded.
#
# Known limitation — codegen-units=1:
# Building with codegen-units=1 collapses the entire ziskos crate into a
# single object file. That object both defines "T __rustc::__rust_alloc"
# (the #[global_allocator] wrapper in alloc/bump.rs) and contains all
# accelerator code. Any "U __rustc::__rust_alloc" that a violation would
# produce resolves internally to the definition in the same CGU and therefore
# does not appear as "U" — the check is completely blind to violations.
#
# With codegen-units ≥ 2 (Rust's dev-profile default is 256) the allocator
# definition lands in its own object, separate from the accelerator CGUs, so
# violations remain visible as "U" symbols. Do not add
# --config 'profile.dev.codegen-units=1' to this build.
RUST_ALLOC_DIRECT=$("$LLVM_NM" -C -A "$LIBZISKOS" 2>/dev/null | \
grep " U __rustc::__rust_alloc" | \
awk -F: '$2 ~ /^ziskos/' || true)

if [ -n "$RUST_ALLOC_DIRECT" ]; then
echo "FAIL: __rustc::__rust_alloc referenced from ziskos object files:"
echo "$RUST_ALLOC_DIRECT"
FAIL=1
else
echo "OK: no __rustc::__rust_alloc references from ziskos object files"
fi

if [ "$FAIL" -eq 0 ]; then
echo "OK: libziskos_staticlib.a passes all checks"
else
exit "$FAIL"
fi
fi
6 changes: 6 additions & 0 deletions ziskos/entrypoint/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![cfg_attr(zisk_guest, no_std)]
#![cfg_attr(zisk_guest, feature(allocator_api))]
#![allow(unexpected_cfgs)]
#![allow(unused_imports)]

Expand All @@ -12,6 +13,9 @@ mod fcall;
#[cfg(zisk_guest)]
mod alloc;

#[path = "scratch-accelerators.rs"]
pub(crate) mod scratch_accelerators;

// Link the `alloc` crate under an alias to avoid conflict with `mod alloc` above.
// Exposed as `crate::alloc_crate` so submodules can use `use crate::alloc_crate::vec::Vec;`
#[cfg(zisk_guest)]
Expand Down Expand Up @@ -356,6 +360,8 @@ pub mod ziskos {
))]
crate::alloc::init_sys_alloc();

crate::scratch_accelerators::init_scratch();

main()
}
}
Expand Down
149 changes: 149 additions & 0 deletions ziskos/entrypoint/src/scratch-accelerators.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//! Per-call scratch arena for accelerator functions.
//!
//! A fixed 2 MiB static buffer (`SCRATCH_BUF`) lives in `.bss` (zero-
//! initialised, no heap involvement). `BumpScratch::reset` rewinds the bump
//! pointer to the start of that buffer at the beginning of every accelerator
//! call, recycling all prior allocations in bulk.
//!
//! `init_scratch` is a thin delegating wrapper around `reset`; it exists only
//! to preserve the call-site convention in `_zisk_main`.

#[cfg(zisk_guest)]
use core::{
alloc::{AllocError, Allocator, Layout},
ptr::NonNull,
};

#[allow(dead_code)]
/// Size of the per-call scratch arena (2 MiB).
pub const SCRATCH_SIZE: usize = 2 * 1024 * 1024;

/// Backing storage for the scratch arena.
///
/// Declared as `[u64; N]` so that the array has 8-byte alignment without
/// requiring a wrapper type. Lives in `.bss` (zero-initialised at startup).
#[cfg(zisk_guest)]
static mut SCRATCH_BUF: [u64; SCRATCH_SIZE / 8] = [0u64; SCRATCH_SIZE / 8];

/// Current bump pointer into `SCRATCH_BUF`.
#[cfg(zisk_guest)]
static mut SCRATCH_POS: usize = 0;

/// Zero-sized type used as the allocator for scratch-backed `Vec`s.
///
/// On `zisk_guest` this implements `core::alloc::Allocator` (via
/// `allocator_api`) and routes all allocations into the scratch arena.
/// `dealloc` is a no-op; memory is reclaimed in bulk at each accelerator
/// entry by calling `BumpScratch::reset`.
///
/// On host builds the struct still exists so that call sites compile without
/// any `#[cfg]` guards; its `reset` method is a no-op.
#[derive(Copy, Clone)]
#[allow(dead_code)]
pub struct BumpScratch;

/// Reset the scratch arena to the start of the static backing buffer.
///
/// Delegates to `BumpScratch::reset`. Exists to preserve the call-site
/// convention in `_zisk_main`; on non-guest builds this is a no-op.
#[allow(dead_code)]
pub unsafe fn init_scratch() {
BumpScratch::reset();
}

impl BumpScratch {
/// Rewind the arena to the start of `SCRATCH_BUF`, recycling all
/// allocations since the last reset. Must be the very first statement
/// of every public accelerator entry point.
///
/// On non-guest builds this is a no-op.
#[inline(always)]
#[allow(dead_code)]
pub fn reset() {
#[cfg(zisk_guest)]
// SAFETY: single-threaded guest — no concurrent access.
unsafe {
SCRATCH_POS = core::ptr::addr_of_mut!(SCRATCH_BUF) as usize;
}
}
}

// ── Allocator impl (zisk_guest only) ─────────────────────────────────────────

#[cfg(zisk_guest)]
unsafe impl Allocator for BumpScratch {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
// SAFETY: single-threaded guest.
let pos = unsafe { SCRATCH_POS };
let align = layout.align();
// Round up to the required alignment.
let start = pos.wrapping_add(align - 1) & !(align - 1);
let end = start.checked_add(layout.size()).ok_or(AllocError)?;
// Compute the buffer's end address from the static's address.
// No separate SCRATCH_TOP static needed; the compiler/linker resolves
// addr_of_mut!(SCRATCH_BUF) to a link-time constant.
let top = core::ptr::addr_of_mut!(SCRATCH_BUF) as usize + SCRATCH_SIZE;
if end > top {
// Scratch arena exhausted — SCRATCH_SIZE is too small for this call.
return Err(AllocError);
}
unsafe { SCRATCH_POS = end };
// SAFETY: `start` lies within the scratch region.
let ptr = unsafe { NonNull::new_unchecked(start as *mut u8) };
Ok(NonNull::slice_from_raw_parts(ptr, layout.size()))
}

#[inline(always)]
unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
// Bulk-reset on the next accelerator entry; no per-dealloc cost.
}
}

// ── ScratchVec and constructors ───────────────────────────────────────────────

/// A `Vec<T>` backed by the per-call scratch arena on `zisk_guest` targets,
/// or by the standard global allocator on host targets.
///
/// The full `Vec` API (push, len, deref-to-slice, …) is available in both
/// cases.
#[cfg(zisk_guest)]
pub type ScratchVec<T> = crate::alloc_crate::vec::Vec<T, BumpScratch>;

#[cfg(not(zisk_guest))]
pub type ScratchVec<T> = std::vec::Vec<T>;

/// Create a `ScratchVec<T>` pre-allocated for `capacity` elements (length = 0).
///
/// On `zisk_guest` this draws from the scratch arena.
/// On host targets this calls `Vec::with_capacity`.
#[inline(always)]
pub fn new_scratch_vec<T>(capacity: usize) -> ScratchVec<T> {
#[cfg(zisk_guest)]
{
crate::alloc_crate::vec::Vec::with_capacity_in(capacity, BumpScratch)
}
#[cfg(not(zisk_guest))]
{
std::vec::Vec::with_capacity(capacity)
}
}

/// Create a `ScratchVec<T>` of `len` elements all initialised to `value`.
///
/// Equivalent to `vec![value; len]` but backed by the scratch arena on guest.
#[inline(always)]
pub fn new_scratch_vec_filled<T: Clone>(len: usize, value: T) -> ScratchVec<T> {
let mut v = new_scratch_vec(len);
v.resize(len, value);
v
}

/// Copy a slice into a new `ScratchVec<T>`.
///
/// Equivalent to `slice.to_vec()` but backed by the scratch arena on guest.
#[inline(always)]
pub fn scratch_vec_from_slice<T: Copy>(slice: &[T]) -> ScratchVec<T> {
let mut v = new_scratch_vec(slice.len());
v.extend_from_slice(slice);
v
}
4 changes: 2 additions & 2 deletions ziskos/entrypoint/src/syscalls/point.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
//! Shared data structures for elliptic curve points used by curve syscalls.

/// An affine elliptic curve point with two 256-bit coordinates `(x, y)`.
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct SyscallPoint256 {
pub x: [u64; 4],
pub y: [u64; 4],
}

/// An affine elliptic curve point with two 384-bit coordinates `(x, y)`.
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct SyscallPoint384 {
pub x: [u64; 6],
Expand Down
26 changes: 13 additions & 13 deletions ziskos/entrypoint/src/zisklib/lib/bigint/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ use core::{
fmt::{self, Debug, Display},
};

#[cfg(zisk_guest)]
use crate::alloc_extern::vec;
#[cfg(zisk_guest)]
use crate::alloc_extern::vec::Vec;

use crate::scratch_accelerators::{new_scratch_vec_filled, ScratchVec};

/// A 256-bit unsigned integer stored as four little-endian 64-bit limbs.
#[repr(transparent)]
#[derive(Clone, Copy)]
Expand Down Expand Up @@ -196,10 +196,10 @@ impl Default for ShortScratch {

/// Scratch space for the remainder step of long-divisor division verification.
pub struct RemLongScratch {
pub quo: Vec<u64>, // quotient
pub rem: Vec<u64>, // remainder
pub q_b: Vec<U256>, // q * b
pub q_b_r: Vec<U256>, // q * b + r
pub quo: ScratchVec<u64>, // quotient
pub rem: ScratchVec<u64>, // remainder
pub q_b: ScratchVec<U256>, // q * b
pub q_b_r: ScratchVec<U256>, // q * b + r
}

impl RemLongScratch {
Expand All @@ -208,23 +208,23 @@ impl RemLongScratch {
let max_rem = len_m * 4;
let max_prod = 2 * len_m;
Self {
quo: vec![0u64; max_quo],
rem: vec![0u64; max_rem],
q_b: vec![U256::ZERO; max_prod],
q_b_r: vec![U256::ZERO; max_prod],
quo: new_scratch_vec_filled(max_quo, 0u64),
rem: new_scratch_vec_filled(max_rem, 0u64),
q_b: new_scratch_vec_filled(max_prod, U256::ZERO),
q_b_r: new_scratch_vec_filled(max_prod, U256::ZERO),
}
}
}

/// Combined scratch space for long-divisor division and multiplication verification.
pub struct LongScratch {
pub rem: RemLongScratch, // for rem_long verification
pub mul: Vec<U256>, // result of mul_long or square_long
pub rem: RemLongScratch, // for rem_long verification
pub mul: ScratchVec<U256>, // result of mul_long or square_long
}

impl LongScratch {
pub fn new(len_m: usize) -> Self {
let max_mul = 2 * len_m;
Self { rem: RemLongScratch::new(len_m), mul: vec![U256::ZERO; max_mul] }
Self { rem: RemLongScratch::new(len_m), mul: new_scratch_vec_filled(max_mul, U256::ZERO) }
}
}
Loading