Skip to main content

async_ebpf/
program.rs

1//! This module contains the core logic for eBPF program execution. A lot of
2//! `unsafe` code is used - be careful when making changes here.
3
4use std::{
5  any::{Any, TypeId},
6  cell::{Cell, RefCell},
7  collections::HashMap,
8  ffi::CStr,
9  marker::PhantomData,
10  mem::ManuallyDrop,
11  ops::{Deref, DerefMut},
12  pin::Pin,
13  ptr::NonNull,
14  rc::Rc,
15  sync::{
16    atomic::{compiler_fence, AtomicBool, AtomicU64, Ordering},
17    Arc, Once,
18  },
19  task::{Context, Poll},
20  thread::ThreadId,
21  time::{Duration, Instant},
22};
23
24use futures::{task::noop_waker_ref, Future, FutureExt};
25use memmap2::{MmapOptions, MmapRaw};
26use parking_lot::{Condvar, Mutex};
27use rand::prelude::SliceRandom;
28
29use crate::{
30  coroutine::{
31    stack::{DefaultStack, Stack},
32    Coroutine, CoroutineResult, ScopedCoroutine, Yielder,
33  },
34  error::{Error, RuntimeError},
35  function_analysis::{analyze_functions, FunctionLayout},
36  helpers::Helper,
37  linker::{link_elf, validate_local_call_graph},
38  pointer_cage::PointerCage,
39  region_analysis::PointerSignature,
40  util::nonnull_bytes_overlap,
41};
42
43const NATIVE_STACK_SIZE: usize = 16384;
44const SHADOW_STACK_SIZE: usize = 32768;
45const MAX_CALLDATA_SIZE: usize = 512;
46const MAX_MUTABLE_DEREF_REGIONS: usize = 4;
47const MAX_IMMUTABLE_DEREF_REGIONS: usize = 16;
48
49#[cfg(all(
50  target_arch = "x86_64",
51  any(target_os = "linux", target_os = "openbsd")
52))]
53std::arch::global_asm!(
54  r#"
55.global async_ebpf_entry_trampoline
56.type async_ebpf_entry_trampoline,@function
57async_ebpf_entry_trampoline:
58    mov r10, rdi
59    mov rax, [rsp + 8]
60    push rbp
61    push rbx
62    push r12
63    push r13
64    push r14
65    push r15
66    mov r15, rcx
67    add r15, r8
68    mov r11, rcx
69    mov rdi, rsi
70    mov rsi, rdx
71    mov rdx, r11
72    mov rcx, r8
73    mov r8, r9
74    mov r11, rdi
75    sub rsp, 8
76    mov rbp, rsp
77    sub rsp, 32
78    mov [rbp - 8], rax
79    call r10
80    mov rsp, rbp
81    add rsp, 8
82    pop r15
83    pop r14
84    pop r13
85    pop r12
86    pop rbx
87    pop rbp
88    ret
89.size async_ebpf_entry_trampoline, . - async_ebpf_entry_trampoline
90"#
91);
92
93#[cfg(all(
94  target_arch = "aarch64",
95  any(target_os = "linux", target_os = "openbsd")
96))]
97std::arch::global_asm!(
98  r#"
99.global async_ebpf_entry_trampoline
100.type async_ebpf_entry_trampoline,%function
101async_ebpf_entry_trampoline:
102    bti c
103    mov x17, x0
104    sub sp, sp, #16
105    stp x29, x30, [sp]
106    sub sp, sp, #64
107    stp x19, x20, [sp, #0]
108    stp x21, x22, [sp, #16]
109    stp x23, x24, [sp, #32]
110    stp x25, x26, [sp, #48]
111    mov x29, sp
112    add x23, x3, x4
113    mov x0, x1
114    mov x1, x2
115    mov x2, x3
116    mov x3, x4
117    mov x4, x5
118    sub sp, sp, #16
119    str x6, [x29, #-8]
120    mov x26, x0
121    blr x17
122    mov x0, x5
123    mov sp, x29
124    ldp x19, x20, [sp, #0]
125    ldp x21, x22, [sp, #16]
126    ldp x23, x24, [sp, #32]
127    ldp x25, x26, [sp, #48]
128    add sp, sp, #64
129    ldp x29, x30, [sp]
130    add sp, sp, #16
131    ret
132.size async_ebpf_entry_trampoline, . - async_ebpf_entry_trampoline
133"#
134);
135
136extern "C" {
137  fn async_ebpf_entry_trampoline(
138    target: usize,
139    ctx: usize,
140    mem_len: usize,
141    stack: usize,
142    stack_len: usize,
143    reserved: usize,
144    memory: usize,
145  ) -> u64;
146}
147
148/// Per-invocation storage for helper state during a program run.
149pub struct InvokeScope {
150  data: HashMap<TypeId, Box<dyn Any + Send>>,
151}
152
153impl InvokeScope {
154  /// Gets or creates typed data scoped to this invocation.
155  pub fn data_mut<T: Default + Send + 'static>(&mut self) -> &mut T {
156    let ty = TypeId::of::<T>();
157    self
158      .data
159      .entry(ty)
160      .or_insert_with(|| Box::new(T::default()))
161      .downcast_mut()
162      .expect("InvokeScope::data_mut: downcast failed")
163  }
164}
165
166/// Context passed to helpers while a program is executing.
167pub struct HelperScope<'a, 'b> {
168  /// The program being executed.
169  pub program: &'a Program,
170  /// Mutable per-invocation data for helpers.
171  pub invoke: RefCell<&'a mut InvokeScope>,
172  resources: RefCell<&'a mut [&'b mut dyn Any]>,
173  memory: &'a JitMemory,
174  mutable_dereferenced_regions: [Cell<Option<NonNull<[u8]>>>; MAX_MUTABLE_DEREF_REGIONS],
175  immutable_dereferenced_regions: [Cell<Option<NonNull<[u8]>>>; MAX_IMMUTABLE_DEREF_REGIONS],
176  can_post_task: bool,
177}
178
179/// A validated mutable view into user memory.
180pub struct MutableUserMemory<'a, 'b, 'c> {
181  _scope: &'c HelperScope<'a, 'b>,
182  region: NonNull<[u8]>,
183}
184
185impl<'a, 'b, 'c> Deref for MutableUserMemory<'a, 'b, 'c> {
186  type Target = [u8];
187
188  fn deref(&self) -> &Self::Target {
189    unsafe { self.region.as_ref() }
190  }
191}
192
193impl<'a, 'b, 'c> DerefMut for MutableUserMemory<'a, 'b, 'c> {
194  fn deref_mut(&mut self) -> &mut Self::Target {
195    unsafe { self.region.as_mut() }
196  }
197}
198
199impl<'a, 'b> HelperScope<'a, 'b> {
200  /// Posts an async task to be run between timeslices.
201  pub fn post_task(
202    &self,
203    task: impl Future<Output = impl FnOnce(&HelperScope) -> Result<u64, ()> + 'static> + 'static,
204  ) {
205    if !self.can_post_task {
206      panic!("HelperScope::post_task() called in a context where posting task is not allowed");
207    }
208
209    PENDING_ASYNC_TASK.with(|x| {
210      let mut x = x.borrow_mut();
211      if x.is_some() {
212        panic!("post_task called while another task is pending");
213      }
214      *x = Some(async move { Box::new(task.await) as AsyncTaskOutput }.boxed_local());
215    });
216  }
217
218  /// Calls `callback` with a mutable resource of type `T`, if present.
219  pub fn with_resource_mut<'c, T: 'static, R>(
220    &'c self,
221    callback: impl FnOnce(Result<&mut T, ()>) -> R,
222  ) -> R {
223    let mut resources = self.resources.borrow_mut();
224    let Some(res) = resources
225      .iter_mut()
226      .filter_map(|x| x.downcast_mut::<T>())
227      .next()
228    else {
229      tracing::warn!(resource_type = ?TypeId::of::<T>(), "resource not found");
230      return callback(Err(()));
231    };
232
233    callback(Ok(res))
234  }
235
236  /// Validates and returns an immutable view into user memory.
237  pub fn user_memory(&self, ptr: u64, size: u64) -> Result<&[u8], ()> {
238    let Some(region) = self.memory.safe_deref_for_read(ptr as usize, size as usize) else {
239      tracing::warn!(ptr, size, "invalid read");
240      return Err(());
241    };
242
243    if size != 0 {
244      // The region must not overlap with any previously dereferenced mutable regions
245      if self
246        .mutable_dereferenced_regions
247        .iter()
248        .filter_map(|x| x.get())
249        .any(|x| nonnull_bytes_overlap(x, region))
250      {
251        tracing::warn!(ptr, size, "read overlapped with previous write");
252        return Err(());
253      }
254
255      // Find a slot to record this dereference
256      let Some(slot) = self
257        .immutable_dereferenced_regions
258        .iter()
259        .find(|x| x.get().is_none())
260      else {
261        tracing::warn!(ptr, size, "too many reads");
262        return Err(());
263      };
264      slot.set(Some(region));
265    }
266
267    Ok(unsafe { region.as_ref() })
268  }
269
270  /// Validates and returns a mutable view into user memory.
271  pub fn user_memory_mut<'c>(
272    &'c self,
273    ptr: u64,
274    size: u64,
275  ) -> Result<MutableUserMemory<'a, 'b, 'c>, ()> {
276    let Some(region) = self
277      .memory
278      .safe_deref_for_write(ptr as usize, size as usize)
279    else {
280      tracing::warn!(ptr, size, "invalid write");
281      return Err(());
282    };
283
284    if size != 0 {
285      // The region must not overlap with any other previously dereferenced mutable or immutable regions
286      if self
287        .mutable_dereferenced_regions
288        .iter()
289        .chain(self.immutable_dereferenced_regions.iter())
290        .filter_map(|x| x.get())
291        .any(|x| nonnull_bytes_overlap(x, region))
292      {
293        tracing::warn!(ptr, size, "write overlapped with previous read/write");
294        return Err(());
295      }
296
297      // Find a slot to record this dereference
298      let Some(slot) = self
299        .mutable_dereferenced_regions
300        .iter()
301        .find(|x| x.get().is_none())
302      else {
303        tracing::warn!(ptr, size, "too many writes");
304        return Err(());
305      };
306      slot.set(Some(region));
307    }
308
309    Ok(MutableUserMemory {
310      _scope: self,
311      region,
312    })
313  }
314}
315
316#[derive(Copy, Clone)]
317struct AssumeSend<T>(T);
318unsafe impl<T> Send for AssumeSend<T> {}
319
320struct ExecContext {
321  native_stack: DefaultStack,
322  guest_stack: Box<[u8; SHADOW_STACK_SIZE]>,
323}
324
325impl ExecContext {
326  fn new() -> Self {
327    Self {
328      native_stack: DefaultStack::new(NATIVE_STACK_SIZE)
329        .expect("failed to initialize native stack"),
330      guest_stack: Box::new([0u8; SHADOW_STACK_SIZE]),
331    }
332  }
333}
334
335#[repr(C)]
336struct JitMemory {
337  stack_guest_bottom: usize,
338  stack_guest_top: usize,
339  stack_native_base: usize,
340  data_guest_bottom: usize,
341  data_guest_top: usize,
342  data_native_base: usize,
343}
344
345impl JitMemory {
346  fn checked_region(
347    guest: usize,
348    size: usize,
349    guest_bottom: usize,
350    guest_top: usize,
351    native_base: usize,
352  ) -> Option<NonNull<[u8]>> {
353    if size == 0 {
354      return Some(NonNull::slice_from_raw_parts(NonNull::dangling(), 0));
355    }
356
357    let end = guest.checked_add(size)?;
358    if guest < guest_bottom || end > guest_top {
359      return None;
360    }
361    let native = native_base.checked_add(guest - guest_bottom)? as *mut u8;
362    unsafe {
363      Some(NonNull::new_unchecked(std::ptr::slice_from_raw_parts_mut(
364        native, size,
365      )))
366    }
367  }
368
369  fn safe_deref_for_write(&self, guest: usize, size: usize) -> Option<NonNull<[u8]>> {
370    Self::checked_region(
371      guest,
372      size,
373      self.stack_guest_bottom,
374      self.stack_guest_top,
375      self.stack_native_base,
376    )
377  }
378
379  fn safe_deref_for_read(&self, guest: usize, size: usize) -> Option<NonNull<[u8]>> {
380    Self::checked_region(
381      guest,
382      size,
383      self.stack_guest_bottom,
384      self.stack_guest_top,
385      self.stack_native_base,
386    )
387    .or_else(|| {
388      Self::checked_region(
389        guest,
390        size,
391        self.data_guest_bottom,
392        self.data_guest_top,
393        self.data_native_base,
394      )
395    })
396  }
397}
398
399/// A pending async task spawned by a helper.
400pub type PendingAsyncTask = Pin<Box<dyn Future<Output = AsyncTaskOutput>>>;
401/// The callback produced by a helper async task when it resumes.
402pub type AsyncTaskOutput = Box<dyn FnOnce(&HelperScope) -> Result<u64, ()>>;
403
404static NEXT_PROGRAM_ID: AtomicU64 = AtomicU64::new(1);
405
406#[derive(Copy, Clone, Debug)]
407enum PreemptionState {
408  Inactive,
409  Armed(usize),
410  Shutdown,
411}
412
413type PreemptionStateSignal = (Mutex<PreemptionState>, Condvar);
414
415thread_local! {
416  static RUST_TID: ThreadId = std::thread::current().id();
417  static SIGUSR1_COUNTER: Cell< u64> = Cell::new(0);
418  static ACTIVE_JIT_CODE_ZONE: ActiveJitCodeZone = ActiveJitCodeZone::default();
419  static EXEC_CONTEXT_POOL: RefCell<Vec<ExecContext>> = Default::default();
420  static PENDING_ASYNC_TASK: RefCell<Option<PendingAsyncTask>> = RefCell::new(None);
421  static PREEMPTION_STATE: Arc<PreemptionStateSignal> = Arc::new((Mutex::new(PreemptionState::Inactive), Condvar::new()));
422  static LOADING_PROGRAM_LOADER: Cell<*const ProgramLoader> = const { Cell::new(std::ptr::null()) };
423  static ACTIVE_PROGRAM: Cell<*const Program> = const { Cell::new(std::ptr::null()) };
424}
425
426struct BorrowedExecContext {
427  ctx: ManuallyDrop<ExecContext>,
428}
429
430impl BorrowedExecContext {
431  fn new() -> Self {
432    let mut me = Self {
433      ctx: ManuallyDrop::new(
434        EXEC_CONTEXT_POOL.with(|x| x.borrow_mut().pop().unwrap_or_else(ExecContext::new)),
435      ),
436    };
437    me.ctx.guest_stack.fill(0x8e);
438    me
439  }
440}
441
442impl Drop for BorrowedExecContext {
443  fn drop(&mut self) {
444    let ctx = unsafe { ManuallyDrop::take(&mut self.ctx) };
445    EXEC_CONTEXT_POOL.with(|x| x.borrow_mut().push(ctx));
446  }
447}
448
449#[derive(Default)]
450struct ActiveJitCodeZone {
451  valid: AtomicBool,
452  code_range: Cell<(usize, usize)>,
453  pointer_cage_protected_range: Cell<(usize, usize)>,
454  yielder: Cell<Option<NonNull<Yielder<u64, Dispatch>>>>,
455}
456
457/// Hooks for observing program execution events.
458pub trait ProgramEventListener: Send + Sync + 'static {
459  /// Called after an async preemption is triggered.
460  fn did_async_preempt(&self, _scope: &HelperScope) {}
461  /// Called after yielding back to the async runtime.
462  fn did_yield(&self) {}
463  /// Called after throttling a program's execution.
464  fn did_throttle(&self, _scope: &HelperScope) -> Option<Pin<Box<dyn Future<Output = ()>>>> {
465    None
466  }
467}
468
469/// No-op event listener implementation.
470pub struct DummyProgramEventListener;
471impl ProgramEventListener for DummyProgramEventListener {}
472
473/// Default limit for the total JIT-compiled native code size of one program.
474pub const DEFAULT_CODE_SIZE_LIMIT: usize = 1 << 20;
475
476/// Prepares helper tables and loads eBPF programs.
477pub struct ProgramLoader {
478  helpers_inverse: HashMap<&'static str, i32>,
479  event_listener: Arc<dyn ProgramEventListener>,
480  helper_id_xor: u16,
481  helpers: Arc<Vec<(u16, &'static str, Helper)>>,
482  code_size_limit: usize,
483  require_static_regions: bool,
484}
485
486/// A loaded program that is not yet pinned to a thread.
487pub struct UnboundProgram {
488  id: u64,
489  _code_mem: MmapRaw,
490  code_base: usize,
491  code_size: usize,
492  page_size: usize,
493  code_arena: RefCell<CodeArena>,
494  cage: PointerCage,
495  helper_id_xor: u16,
496  helpers: Arc<Vec<(u16, &'static str, Helper)>>,
497  event_listener: Arc<dyn ProgramEventListener>,
498  require_static_regions: bool,
499  entrypoints: HashMap<String, usize>,
500  sections: RefCell<Vec<Section>>,
501  resolvers: RefCell<HashMap<u32, ResolverInfo>>,
502  next_resolver_id: Cell<u32>,
503}
504
505/// A program pinned to a specific thread and ready to execute.
506pub struct Program {
507  unbound: UnboundProgram,
508  data: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
509  t: ThreadEnv,
510}
511
512#[derive(Copy, Clone)]
513struct Entrypoint {
514  code_ptr: usize,
515}
516
517struct CodeArena {
518  used: usize,
519}
520
521struct Section {
522  vm: Vm,
523  code_vaddr: usize,
524  code_len: usize,
525  layout: FunctionLayout,
526  functions: Vec<FunctionState>,
527}
528
529#[derive(Default)]
530struct FunctionState {
531  compiled: HashMap<PointerSignature, FunctionCompilation>,
532  #[cfg(test)]
533  compile_attempts: usize,
534}
535
536#[derive(Clone)]
537enum FunctionCompilation {
538  Succeeded(Entrypoint),
539  Failed(RuntimeError),
540}
541
542impl FunctionCompilation {
543  fn result(&self) -> Result<Entrypoint, RuntimeError> {
544    match self {
545      Self::Succeeded(entrypoint) => Ok(*entrypoint),
546      Self::Failed(err) => Err(err.clone()),
547    }
548  }
549
550  fn entrypoint(&self) -> Option<Entrypoint> {
551    match self {
552      Self::Succeeded(entrypoint) => Some(*entrypoint),
553      Self::Failed(_) => None,
554    }
555  }
556}
557
558#[derive(Clone, Copy)]
559struct ResolverInfo {
560  section_index: usize,
561  function_index: usize,
562  signature: PointerSignature,
563}
564
565/// Time limits used to yield or throttle execution.
566#[derive(Clone, Debug)]
567pub struct TimesliceConfig {
568  /// Maximum runtime before yielding to the async scheduler.
569  pub max_run_time_before_yield: Duration,
570  /// Maximum runtime before a throttle sleep is forced.
571  pub max_run_time_before_throttle: Duration,
572  /// Duration of the throttle sleep once triggered.
573  pub throttle_duration: Duration,
574}
575
576/// Async runtime integration for yielding and sleeping.
577pub trait Timeslicer {
578  /// Sleep for the provided duration.
579  fn sleep(&self, duration: Duration) -> impl Future<Output = ()>;
580  /// Yield to the async scheduler.
581  fn yield_now(&self) -> impl Future<Output = ()>;
582}
583
584/// Global runtime environment for signal handlers.
585#[derive(Copy, Clone)]
586pub struct GlobalEnv(());
587
588/// Per-thread runtime environment for preemption handling.
589#[derive(Copy, Clone)]
590pub struct ThreadEnv {
591  _not_send_sync: std::marker::PhantomData<*const ()>,
592}
593
594#[cfg(target_os = "linux")]
595type NativeThread = (libc::pid_t, libc::pid_t);
596#[cfg(target_os = "openbsd")]
597type NativeThread = libc::pthread_t;
598
599#[cfg(target_os = "linux")]
600unsafe fn current_native_thread() -> NativeThread {
601  (libc::getpid(), libc::gettid())
602}
603
604#[cfg(target_os = "openbsd")]
605unsafe fn current_native_thread() -> NativeThread {
606  libc::pthread_self()
607}
608
609#[cfg(target_os = "linux")]
610unsafe fn signal_native_thread(thread: NativeThread, signal: i32) -> i32 {
611  libc::syscall(libc::SYS_tgkill, thread.0, thread.1, signal) as i32
612}
613
614#[cfg(target_os = "openbsd")]
615unsafe fn signal_native_thread(thread: NativeThread, signal: i32) -> i32 {
616  libc::pthread_kill(thread, signal)
617}
618
619impl GlobalEnv {
620  /// Initializes global state and installs signal handlers.
621  ///
622  /// # Safety
623  /// Must be called in a process that can install SIGUSR1/SIGSEGV handlers.
624  pub unsafe fn new() -> Self {
625    static INIT: Once = Once::new();
626
627    // SIGUSR1 must be blocked during exception handling
628    // Otherwise it seems that Linux gives up and throws an uncatchable SI_KERNEL SIGSEGV:
629    //
630    // [pid 517110] tgkill(517109, 517112, SIGUSR1 <unfinished ...>
631    // [pid 517112] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_ACCERR, si_addr=0x793789be227e} ---
632    // [pid 517109] write(15, "\1\0\0\0\0\0\0\0", 8 <unfinished ...>
633    // [pid 517110] <... tgkill resumed>)      = 0
634    // [pid 517109] <... write resumed>)       = 8
635    // [pid 517112] --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_TKILL, si_pid=517109, si_uid=1000} ---
636    // [pid 517109] recvfrom(57,  <unfinished ...>
637    // [pid 517110] futex(0x79378abff5a8, FUTEX_WAIT_PRIVATE, 1, {tv_sec=0, tv_nsec=5999909} <unfinished ...>
638    // [pid 517109] <... recvfrom resumed>"GET /write_rodata HTTP/1.1\r\nHost"..., 8192, 0, NULL, NULL) = 61
639    // [pid 517112] --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=NULL} ---
640
641    INIT.call_once(|| {
642      let sa_mask = get_blocked_sigset();
643
644      for (sig, handler) in [
645        (libc::SIGUSR1, sigusr1_handler as *const () as usize),
646        (libc::SIGSEGV, sigsegv_handler as *const () as usize),
647      ] {
648        let mut act: libc::sigaction = std::mem::zeroed();
649        act.sa_sigaction = handler;
650        act.sa_flags = libc::SA_SIGINFO;
651        act.sa_mask = sa_mask;
652        if libc::sigaction(sig, &act, std::ptr::null_mut()) != 0 {
653          panic!("failed to setup handler for signal {}", sig);
654        }
655      }
656    });
657
658    Self(())
659  }
660
661  /// Initializes per-thread state and starts the async preemption watcher.
662  pub fn init_thread(self, async_preemption_interval: Duration) -> ThreadEnv {
663    // Signal handlers must never trigger lazy TLS initialization. In
664    // particular, OpenBSD aborts if a signal recursively enters the runtime's
665    // TLS initialization path. Initialize every slot touched by SIGUSR1 before
666    // the watcher can send its first signal.
667    RUST_TID.with(|_| {});
668    SIGUSR1_COUNTER.with(|_| {});
669    ACTIVE_JIT_CODE_ZONE.with(|_| {});
670
671    struct DeferDrop(Arc<PreemptionStateSignal>);
672    impl Drop for DeferDrop {
673      fn drop(&mut self) {
674        let x = &self.0;
675        *x.0.lock() = PreemptionState::Shutdown;
676        x.1.notify_one();
677      }
678    }
679
680    thread_local! {
681      static WATCHER: RefCell<Option<DeferDrop>> = RefCell::new(None);
682    }
683
684    if WATCHER.with(|x| x.borrow().is_some()) {
685      return ThreadEnv {
686        _not_send_sync: PhantomData,
687      };
688    }
689
690    let preemption_state = PREEMPTION_STATE.with(|x| x.clone());
691
692    unsafe {
693      let target_thread = current_native_thread();
694      let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(0);
695
696      std::thread::Builder::new()
697        .name("preempt-watcher".to_string())
698        .spawn(move || {
699          let mut state = preemption_state.0.lock();
700          let _ = ready_tx.send(());
701          loop {
702            match *state {
703              PreemptionState::Shutdown => break,
704              PreemptionState::Inactive => {
705                preemption_state.1.wait(&mut state);
706              }
707              PreemptionState::Armed(_) => {
708                let timeout = preemption_state.1.wait_while_for(
709                  &mut state,
710                  |x| matches!(x, PreemptionState::Armed(_)),
711                  async_preemption_interval,
712                );
713                if timeout.timed_out() {
714                  match *state {
715                    PreemptionState::Armed(0) => {
716                      *state = PreemptionState::Inactive;
717                    }
718                    PreemptionState::Armed(_) => {
719                      if signal_native_thread(target_thread, libc::SIGUSR1) != 0 {
720                        break;
721                      }
722                    }
723                    PreemptionState::Inactive => {}
724                    PreemptionState::Shutdown => break,
725                  }
726                }
727              }
728            }
729          }
730        })
731        .expect("failed to spawn preemption watcher");
732      ready_rx
733        .recv()
734        .expect("preemption watcher stopped during startup");
735
736      WATCHER.with(|x| {
737        x.borrow_mut()
738          .replace(DeferDrop(PREEMPTION_STATE.with(|x| x.clone())));
739      });
740
741      ThreadEnv {
742        _not_send_sync: PhantomData,
743      }
744    }
745  }
746}
747
748impl UnboundProgram {
749  /// Pins the program to the current thread using a prepared `ThreadEnv`.
750  pub fn pin_to_current_thread(self, t: ThreadEnv) -> Program {
751    Program {
752      unbound: self,
753      data: RefCell::new(HashMap::new()),
754      t,
755    }
756  }
757}
758
759pub struct PreemptionEnabled(());
760
761impl PreemptionEnabled {
762  pub fn new(_: ThreadEnv) -> Self {
763    PREEMPTION_STATE.with(|x| {
764      let mut notify = false;
765      {
766        let mut st = x.0.lock();
767        let next = match *st {
768          PreemptionState::Inactive => {
769            notify = true;
770            PreemptionState::Armed(1)
771          }
772          PreemptionState::Armed(n) => PreemptionState::Armed(n + 1),
773          PreemptionState::Shutdown => unreachable!(),
774        };
775        *st = next;
776      }
777
778      if notify {
779        x.1.notify_one();
780      }
781    });
782    Self(())
783  }
784}
785
786impl Drop for PreemptionEnabled {
787  fn drop(&mut self) {
788    PREEMPTION_STATE.with(|x| {
789      let mut st = x.0.lock();
790      let next = match *st {
791        PreemptionState::Armed(1) => PreemptionState::Armed(0),
792        PreemptionState::Armed(n) => {
793          assert!(n > 1);
794          PreemptionState::Armed(n - 1)
795        }
796        PreemptionState::Inactive | PreemptionState::Shutdown => unreachable!(),
797      };
798      *st = next;
799    });
800  }
801}
802
803impl Program {
804  /// Returns the unique program identifier.
805  pub fn id(&self) -> u64 {
806    self.unbound.id
807  }
808
809  pub fn thread_env(&self) -> ThreadEnv {
810    self.t
811  }
812
813  /// Gets or creates shared typed data for this program instance.
814  pub fn data<T: Default + 'static>(&self) -> Rc<T> {
815    let mut data = self.data.borrow_mut();
816    let entry = data.entry(TypeId::of::<T>());
817    let entry = entry.or_insert_with(|| Rc::new(T::default()));
818    entry.clone().downcast().unwrap()
819  }
820
821  pub fn has_section(&self, name: &str) -> bool {
822    self.unbound.entrypoints.contains_key(name)
823  }
824
825  #[cfg(test)]
826  pub(crate) fn compiled_function_count_for_tests(&self) -> usize {
827    self
828      .unbound
829      .sections
830      .borrow()
831      .iter()
832      .map(|section| {
833        section
834          .functions
835          .iter()
836          .map(|function| {
837            function
838              .compiled
839              .values()
840              .filter(|compilation| compilation.entrypoint().is_some())
841              .count()
842          })
843          .sum::<usize>()
844      })
845      .sum()
846  }
847
848  #[cfg(test)]
849  pub(crate) fn failed_function_count_for_tests(&self) -> usize {
850    self
851      .unbound
852      .sections
853      .borrow()
854      .iter()
855      .map(|section| {
856        section
857          .functions
858          .iter()
859          .map(|function| {
860            function
861              .compiled
862              .values()
863              .filter(|compilation| compilation.entrypoint().is_none())
864              .count()
865          })
866          .sum::<usize>()
867      })
868      .sum()
869  }
870
871  #[cfg(test)]
872  pub(crate) fn function_compile_attempt_count_for_tests(&self) -> usize {
873    self
874      .unbound
875      .sections
876      .borrow()
877      .iter()
878      .map(|section| {
879        section
880          .functions
881          .iter()
882          .map(|function| function.compile_attempts)
883          .sum::<usize>()
884      })
885      .sum()
886  }
887
888  #[cfg(test)]
889  pub(crate) fn code_arena_used_for_tests(&self) -> usize {
890    self.unbound.code_arena.borrow().used
891  }
892
893  /// Number of successfully compiled variants per source function, flattened
894  /// across sections. One entry per function in the layout; a value > 1 means
895  /// that callee was specialized for multiple incoming pointer signatures.
896  #[cfg(test)]
897  pub(crate) fn function_variant_counts_for_tests(&self) -> Vec<usize> {
898    self
899      .unbound
900      .sections
901      .borrow()
902      .iter()
903      .flat_map(|section| {
904        section.functions.iter().map(|function| {
905          function
906            .compiled
907            .values()
908            .filter(|compilation| compilation.entrypoint().is_some())
909            .count()
910        })
911      })
912      .collect()
913  }
914
915  fn compile_entrypoint(&self, section_index: usize) -> Result<Entrypoint, RuntimeError> {
916    self.compile_function(section_index, 0, PointerSignature::entry())
917  }
918
919  fn compile_resolver(&self, resolver_id: u32) -> Result<Entrypoint, RuntimeError> {
920    let Some(info) = self.unbound.resolvers.borrow().get(&resolver_id).copied() else {
921      return Err(RuntimeError::InvalidArgument(
922        "local call resolver not found",
923      ));
924    };
925    self.compile_function(info.section_index, info.function_index, info.signature)
926  }
927
928  fn cached_resolver_target(&self, resolver_id: u32) -> Option<usize> {
929    let info = self.unbound.resolvers.borrow().get(&resolver_id).copied()?;
930    let sections = self.unbound.sections.borrow();
931    let section = sections.get(info.section_index)?;
932    section
933      .functions
934      .get(info.function_index)?
935      .compiled
936      .get(&info.signature)
937      .and_then(|compilation| compilation.entrypoint())
938      .map(|entrypoint| entrypoint.code_ptr)
939  }
940
941  fn protect_code_pages(&self, executable_len: usize) -> Result<(), RuntimeError> {
942    let executable_len =
943      (executable_len + self.unbound.page_size - 1) & !(self.unbound.page_size - 1);
944    unsafe {
945      if executable_len > 0
946        && libc::mprotect(
947          self.unbound.code_base as *mut _,
948          executable_len,
949          libc::PROT_READ | libc::PROT_EXEC,
950        ) != 0
951      {
952        return Err(RuntimeError::PlatformError(
953          "failed to protect executable code",
954        ));
955      }
956      if executable_len < self.unbound.code_size
957        && libc::mprotect(
958          (self.unbound.code_base + executable_len) as *mut _,
959          self.unbound.code_size - executable_len,
960          libc::PROT_NONE,
961        ) != 0
962      {
963        return Err(RuntimeError::PlatformError("failed to protect unused code"));
964      }
965    }
966    Ok(())
967  }
968
969  fn compile_function(
970    &self,
971    section_index: usize,
972    function_index: usize,
973    signature: PointerSignature,
974  ) -> Result<Entrypoint, RuntimeError> {
975    {
976      let sections = self.unbound.sections.borrow();
977      let Some(section) = sections.get(section_index) else {
978        return Err(RuntimeError::InvalidArgument("section not found"));
979      };
980      let Some(function) = section.functions.get(function_index) else {
981        return Err(RuntimeError::InvalidArgument("function not found"));
982      };
983      if let Some(compilation) = function.compiled.get(&signature) {
984        return compilation.result();
985      }
986    }
987
988    let mut sections = self.unbound.sections.borrow_mut();
989    let section = sections
990      .get_mut(section_index)
991      .ok_or(RuntimeError::InvalidArgument("section not found"))?;
992    if function_index >= section.functions.len() {
993      return Err(RuntimeError::InvalidArgument("function not found"));
994    }
995    if let Some(compilation) = section.functions[function_index].compiled.get(&signature) {
996      return compilation.result();
997    }
998    #[cfg(test)]
999    {
1000      section.functions[function_index].compile_attempts += 1;
1001    }
1002    let function = section.layout.functions[function_index].clone();
1003    let code = self
1004      .unbound
1005      .cage
1006      .safe_deref_for_read(section.code_vaddr, section.code_len)
1007      .unwrap();
1008    let code_bytes = unsafe { std::slice::from_raw_parts(code.as_ptr() as *const u8, code.len()) };
1009    let region_analysis = crate::region_analysis::analyze_function(
1010      code_bytes,
1011      function.start_pc,
1012      function.end_pc,
1013      signature,
1014      self.unbound.cage.data_bottom() as u64,
1015      self.unbound.cage.data_top() as u64,
1016    );
1017    if self.unbound.require_static_regions && !region_analysis.unresolved.is_empty() {
1018      let err = RuntimeError::InvalidArgumentOwned(format!(
1019        "static region analysis failed in function [{}, {}): {} memory access(es) could not be \
1020         routed to a single region (instruction slots {:?})",
1021        function.start_pc,
1022        function.end_pc,
1023        region_analysis.unresolved.len(),
1024        region_analysis.unresolved,
1025      ));
1026      section.functions[function_index]
1027        .compiled
1028        .insert(signature, FunctionCompilation::Failed(err.clone()));
1029      return Err(err);
1030    }
1031
1032    // Allocate resolver ids and build their metadata locally. Nothing is
1033    // committed to `next_resolver_id` or the shared `resolvers` map until the
1034    // function has been fully compiled and protected, so a failed compilation
1035    // does not leak resolver ids or orphan map entries.
1036    let mut resolver_ids = vec![0u32; code_bytes.len() / 8];
1037    let mut pending_resolvers: Vec<(u32, ResolverInfo)> = Vec::new();
1038    let mut next_resolver_id = self.unbound.next_resolver_id.get();
1039    for (&call_pc, &callee_signature) in &region_analysis.call_signatures {
1040      let target_pc = local_call_target(code_bytes, call_pc);
1041      let callee_index = section.layout.pc_to_func[target_pc];
1042      let resolver_id = next_resolver_id;
1043      let Some(advanced) = resolver_id.checked_add(1) else {
1044        let err = RuntimeError::InvalidArgument("too many local call resolvers");
1045        section.functions[function_index]
1046          .compiled
1047          .insert(signature, FunctionCompilation::Failed(err.clone()));
1048        return Err(err);
1049      };
1050      next_resolver_id = advanced;
1051      resolver_ids[call_pc] = resolver_id;
1052      pending_resolvers.push((
1053        resolver_id,
1054        ResolverInfo {
1055          section_index,
1056          function_index: callee_index,
1057          signature: callee_signature,
1058        },
1059      ));
1060    }
1061
1062    let mut arena = self.unbound.code_arena.borrow_mut();
1063    if arena.used >= self.unbound.code_size {
1064      let err = RuntimeError::InvalidArgument("no space left for jit compilation");
1065      section.functions[function_index]
1066        .compiled
1067        .insert(signature, FunctionCompilation::Failed(err.clone()));
1068      return Err(err);
1069    }
1070
1071    unsafe {
1072      if libc::mprotect(
1073        self.unbound.code_base as *mut _,
1074        self.unbound.code_size,
1075        libc::PROT_READ | libc::PROT_WRITE,
1076      ) != 0
1077      {
1078        let err = RuntimeError::PlatformError("failed to make code writable");
1079        section.functions[function_index]
1080          .compiled
1081          .insert(signature, FunctionCompilation::Failed(err.clone()));
1082        return Err(err);
1083      }
1084    }
1085
1086    let code_ptr = self.unbound.code_base + arena.used;
1087    let mut written_len = self.unbound.code_size - arena.used;
1088    let mut errmsg_ptr = std::ptr::null_mut();
1089
1090    let ret = unsafe {
1091      crate::ubpf::ubpf_set_region_hints(
1092        section.vm.0.as_ptr(),
1093        region_analysis.hints.as_ptr(),
1094        region_analysis.hints.len(),
1095      );
1096      crate::ubpf::ubpf_set_lazy_local_call_resolver(
1097        section.vm.0.as_ptr(),
1098        Some(tls_local_call_resolver),
1099        resolver_ids.as_ptr(),
1100        resolver_ids.len(),
1101      );
1102      let ret = crate::ubpf::ubpf_translate_function_ex(
1103        section.vm.0.as_ptr(),
1104        code_ptr as *mut u8,
1105        &mut written_len,
1106        &mut errmsg_ptr,
1107        crate::ubpf::JitMode_ExtendedJitMode,
1108        function.start_pc as u32,
1109        function.end_pc as u32,
1110      );
1111      crate::ubpf::ubpf_set_region_hints(section.vm.0.as_ptr(), std::ptr::null(), 0);
1112      crate::ubpf::ubpf_set_lazy_local_call_resolver(
1113        section.vm.0.as_ptr(),
1114        None,
1115        std::ptr::null(),
1116        0,
1117      );
1118      ret
1119    };
1120
1121    if ret != 0 {
1122      let errmsg = unsafe {
1123        if errmsg_ptr.is_null() {
1124          "".to_string()
1125        } else {
1126          CStr::from_ptr(errmsg_ptr).to_string_lossy().into_owned()
1127        }
1128      };
1129      if !errmsg_ptr.is_null() {
1130        unsafe { libc::free(errmsg_ptr as _) };
1131      }
1132      let _ = self.protect_code_pages(arena.used);
1133      let err =
1134        RuntimeError::InvalidArgumentOwned(format!("ubpf: code translation failed: {errmsg}"));
1135      section.functions[function_index]
1136        .compiled
1137        .insert(signature, FunctionCompilation::Failed(err.clone()));
1138      return Err(err);
1139    }
1140    if !errmsg_ptr.is_null() {
1141      unsafe { libc::free(errmsg_ptr as _) };
1142    }
1143
1144    unsafe {
1145      crate::ubpf::ubpf_clear_instruction_cache(code_ptr as *mut u8, written_len);
1146    }
1147
1148    // Restore W^X protection covering the newly emitted function before
1149    // advancing the arena. If protection cannot be restored the function is not
1150    // executable, so leave `arena.used` unchanged (reclaiming the space, which
1151    // the next compilation overwrites after making the region writable again)
1152    // and do not register its resolvers.
1153    let new_used = arena.used + written_len;
1154    if let Err(err) = self.protect_code_pages(new_used) {
1155      section.functions[function_index]
1156        .compiled
1157        .insert(signature, FunctionCompilation::Failed(err.clone()));
1158      return Err(err);
1159    }
1160    arena.used = new_used;
1161
1162    // Compilation succeeded: commit the resolver ids and metadata.
1163    self.unbound.next_resolver_id.set(next_resolver_id);
1164    {
1165      let mut resolvers = self.unbound.resolvers.borrow_mut();
1166      for (resolver_id, info) in pending_resolvers {
1167        resolvers.insert(resolver_id, info);
1168      }
1169    }
1170
1171    let entrypoint = Entrypoint { code_ptr };
1172    section.functions[function_index]
1173      .compiled
1174      .insert(signature, FunctionCompilation::Succeeded(entrypoint));
1175    tracing::debug!(
1176      section_index,
1177      function_index,
1178      start_pc = function.start_pc,
1179      end_pc = function.end_pc,
1180      native_code_addr = ?(code_ptr as *const u8),
1181      native_code_size = written_len,
1182      "jit compiled function"
1183    );
1184    Ok(entrypoint)
1185  }
1186
1187  /// Runs the program entrypoint with the provided resources and calldata.
1188  pub async fn run(
1189    &self,
1190    timeslice: &TimesliceConfig,
1191    timeslicer: &impl Timeslicer,
1192    entrypoint: &str,
1193    resources: &mut [&mut dyn Any],
1194    calldata: &[u8],
1195    preemption: &PreemptionEnabled,
1196  ) -> Result<i64, Error> {
1197    self
1198      ._run(
1199        timeslice, timeslicer, entrypoint, resources, calldata, preemption,
1200      )
1201      .await
1202      .map_err(Error)
1203  }
1204
1205  async fn _run(
1206    &self,
1207    timeslice: &TimesliceConfig,
1208    timeslicer: &impl Timeslicer,
1209    entrypoint: &str,
1210    resources: &mut [&mut dyn Any],
1211    calldata: &[u8],
1212    _: &PreemptionEnabled,
1213  ) -> Result<i64, RuntimeError> {
1214    let Some(section_index) = self.unbound.entrypoints.get(entrypoint).copied() else {
1215      return Err(RuntimeError::InvalidArgument("entrypoint not found"));
1216    };
1217    let entrypoint = self.compile_entrypoint(section_index)?;
1218    struct CoDropper<'a, Input, Yield, Return, DefaultStack: Stack>(
1219      ScopedCoroutine<'a, Input, Yield, Return, DefaultStack>,
1220    );
1221    impl<'a, Input, Yield, Return, DefaultStack: Stack> Drop
1222      for CoDropper<'a, Input, Yield, Return, DefaultStack>
1223    {
1224      fn drop(&mut self) {
1225        // Prevent the coroutine library from attempting to unwind the stack of the coroutine
1226        // and run destructors, because this stack might be running a signal handler and
1227        // it's not allowed to unwind from there.
1228        //
1229        // SAFETY: The coroutine stack only contains stack frames for JIT-compiled code and
1230        // carefully chosen Rust code that do not hold Droppable values, so it's safe to
1231        // skip destructors.
1232        unsafe {
1233          self.0.force_reset();
1234        }
1235      }
1236    }
1237
1238    let mut ectx = BorrowedExecContext::new();
1239
1240    if calldata.len() > MAX_CALLDATA_SIZE {
1241      return Err(RuntimeError::InvalidArgument("calldata too large"));
1242    }
1243    ectx.ctx.guest_stack[SHADOW_STACK_SIZE - calldata.len()..].copy_from_slice(calldata);
1244    let calldata_len = calldata.len();
1245
1246    let program_ret: u64 = {
1247      let guest_stack_top = self.unbound.cage.stack_top();
1248      let guest_stack_bottom = self.unbound.cage.stack_bottom();
1249      let ctx = &mut *ectx.ctx;
1250      let memory = JitMemory {
1251        stack_guest_bottom: guest_stack_bottom,
1252        stack_guest_top: guest_stack_top,
1253        stack_native_base: ctx.guest_stack.as_mut_ptr() as usize,
1254        data_guest_bottom: self.unbound.cage.data_bottom(),
1255        data_guest_top: self.unbound.cage.data_top(),
1256        data_native_base: self.unbound.cage.data_native_base(),
1257      };
1258      let memory_ptr = &memory as *const JitMemory as usize;
1259
1260      let mut co = AssumeSend(CoDropper(Coroutine::with_stack(
1261        &mut ctx.native_stack,
1262        move |yielder, _input| unsafe {
1263          ACTIVE_JIT_CODE_ZONE.with(|x| {
1264            x.yielder.set(NonNull::new(yielder as *const _ as *mut _));
1265          });
1266          let calldata_start = guest_stack_top - calldata_len;
1267          let stack_top = calldata_start & !0x7;
1268          let stack_len = stack_top - guest_stack_bottom;
1269          async_ebpf_entry_trampoline(
1270            entrypoint.code_ptr,
1271            calldata_start,
1272            calldata_start,
1273            guest_stack_bottom,
1274            stack_len,
1275            0,
1276            memory_ptr,
1277          )
1278        },
1279      )));
1280
1281      let mut last_yield_time: Option<Instant> = None;
1282      let mut last_throttle_time: Option<Instant> = None;
1283      let mut yielder: Option<AssumeSend<NonNull<Yielder<u64, Dispatch>>>> = None;
1284      let mut resume_input: u64 = 0;
1285      let mut did_throttle = false;
1286      let mut rust_tid_sigusr1_counter = (RUST_TID.with(|x| *x), SIGUSR1_COUNTER.with(|x| x.get()));
1287      let mut prev_async_task_output: Option<(&'static str, AsyncTaskOutput)> = None;
1288      let mut invoke_scope = InvokeScope {
1289        data: HashMap::new(),
1290      };
1291
1292      loop {
1293        ACTIVE_JIT_CODE_ZONE.with(|x| {
1294          x.code_range.set((
1295            self.unbound.code_base,
1296            self.unbound.code_base + self.unbound.code_size,
1297          ));
1298          x.yielder.set(yielder.map(|x| x.0));
1299          x.pointer_cage_protected_range.set((0, 4096));
1300          compiler_fence(Ordering::Release);
1301          x.valid.store(true, Ordering::Relaxed);
1302        });
1303        ACTIVE_PROGRAM.with(|x| x.set(self as *const _));
1304
1305        // If the previous iteration wants to write back to machine state
1306        if let Some((helper_name, prev_async_task_output)) = prev_async_task_output.take() {
1307          resume_input = prev_async_task_output(&HelperScope {
1308            program: self,
1309            invoke: RefCell::new(&mut invoke_scope),
1310            resources: RefCell::new(resources),
1311            memory: &memory,
1312            mutable_dereferenced_regions: unsafe { std::mem::zeroed() },
1313            immutable_dereferenced_regions: unsafe { std::mem::zeroed() },
1314            can_post_task: false,
1315          })
1316          .map_err(|_| RuntimeError::AsyncHelperError(helper_name))?;
1317        }
1318
1319        let ret = co.0 .0.resume(resume_input);
1320        ACTIVE_PROGRAM.with(|x| x.set(std::ptr::null()));
1321        ACTIVE_JIT_CODE_ZONE.with(|x| {
1322          x.valid.store(false, Ordering::Relaxed);
1323          compiler_fence(Ordering::Release);
1324          yielder = x.yielder.get().map(AssumeSend);
1325        });
1326
1327        let dispatch: Dispatch = match ret {
1328          CoroutineResult::Return(x) => break x,
1329          CoroutineResult::Yield(x) => x,
1330        };
1331
1332        // restore signal mask of current thread
1333        if dispatch.memory_access_error.is_some() || dispatch.async_preemption {
1334          unsafe {
1335            let unblock = get_blocked_sigset();
1336            libc::sigprocmask(libc::SIG_UNBLOCK, &unblock, std::ptr::null_mut());
1337          }
1338        }
1339
1340        if let Some(si_addr) = dispatch.memory_access_error {
1341          let vaddr = if si_addr >= memory.stack_native_base
1342            && si_addr < memory.stack_native_base + SHADOW_STACK_SIZE
1343          {
1344            memory.stack_guest_bottom + (si_addr - memory.stack_native_base)
1345          } else if si_addr >= memory.data_native_base
1346            && si_addr
1347              < memory.data_native_base + (memory.data_guest_top - memory.data_guest_bottom)
1348          {
1349            memory.data_guest_bottom + (si_addr - memory.data_native_base)
1350          } else {
1351            0
1352          };
1353          return Err(RuntimeError::MemoryFault(vaddr));
1354        }
1355
1356        if let Some(resolver_id) = dispatch.lazy_local_call {
1357          resume_input = self.compile_resolver(resolver_id)?.code_ptr as u64;
1358          continue;
1359        }
1360
1361        // Clear pending task if something else has set it
1362        PENDING_ASYNC_TASK.with(|x| x.borrow_mut().take());
1363        let mut helper_name: &'static str = "";
1364
1365        let mut helper_scope = HelperScope {
1366          program: self,
1367          invoke: RefCell::new(&mut invoke_scope),
1368          resources: RefCell::new(resources),
1369          memory: &memory,
1370          mutable_dereferenced_regions: unsafe { std::mem::zeroed() },
1371          immutable_dereferenced_regions: unsafe { std::mem::zeroed() },
1372          can_post_task: false,
1373        };
1374
1375        if dispatch.async_preemption {
1376          self
1377            .unbound
1378            .event_listener
1379            .did_async_preempt(&mut helper_scope);
1380        } else {
1381          // validator should ensure all helper indexes are present in the table
1382          let Some((_, got_helper_name, helper)) = self
1383            .unbound
1384            .helpers
1385            .get(
1386              ((dispatch.index & 0xffff) as u16 ^ self.unbound.helper_id_xor).wrapping_sub(1)
1387                as usize,
1388            )
1389            .copied()
1390          else {
1391            panic!("unknown helper index: {}", dispatch.index);
1392          };
1393          helper_name = got_helper_name;
1394
1395          helper_scope.can_post_task = true;
1396          resume_input = helper(
1397            &mut helper_scope,
1398            dispatch.arg1,
1399            dispatch.arg2,
1400            dispatch.arg3,
1401            dispatch.arg4,
1402            dispatch.arg5,
1403          )
1404          .map_err(|()| RuntimeError::HelperError(helper_name))?;
1405          helper_scope.can_post_task = false;
1406        }
1407
1408        let pending_async_task = PENDING_ASYNC_TASK.with(|x| x.borrow_mut().take());
1409
1410        // Fast path: do not read timestamp if no thread migration or async preemption happened
1411        let new_rust_tid_sigusr1_counter =
1412          (RUST_TID.with(|x| *x), SIGUSR1_COUNTER.with(|x| x.get()));
1413        if new_rust_tid_sigusr1_counter == rust_tid_sigusr1_counter && pending_async_task.is_none()
1414        {
1415          continue;
1416        }
1417
1418        rust_tid_sigusr1_counter = new_rust_tid_sigusr1_counter;
1419
1420        let now = Instant::now();
1421        let last_throttle = last_throttle_time.get_or_insert(now);
1422        let last_yield = last_yield_time.get_or_insert(now);
1423        let should_throttle = now > *last_throttle
1424          && now.duration_since(*last_throttle) >= timeslice.max_run_time_before_throttle;
1425        let should_yield = now > *last_yield
1426          && now.duration_since(*last_yield) >= timeslice.max_run_time_before_yield;
1427        if should_throttle || should_yield || pending_async_task.is_some() {
1428          // we are now free to give up control of current thread to other async tasks
1429
1430          if should_throttle {
1431            if !did_throttle {
1432              did_throttle = true;
1433              tracing::warn!("throttling program");
1434            }
1435            timeslicer.sleep(timeslice.throttle_duration).await;
1436            let now = Instant::now();
1437            last_throttle_time = Some(now);
1438            last_yield_time = Some(now);
1439            let task = self.unbound.event_listener.did_throttle(&mut helper_scope);
1440            if let Some(task) = task {
1441              task.await;
1442            }
1443          } else if should_yield {
1444            timeslicer.yield_now().await;
1445            let now = Instant::now();
1446            last_yield_time = Some(now);
1447            self.unbound.event_listener.did_yield();
1448          }
1449
1450          // Now we have released all exclusive resources and can safely execute the async task.
1451          if let Some(mut pending_async_task) = pending_async_task {
1452            // Fast path: helpers that merely compute a value (the common case)
1453            // complete on the first poll without ever suspending. Poll once and,
1454            // if it's ready, skip the `Instant::now()` reads used for run-budget
1455            // compensation below. This matters because `clock_gettime` can cost
1456            // ~200ns on virtualized hosts, and this path runs on every async
1457            // helper invocation.
1458            let output =
1459              match pending_async_task.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
1460                Poll::Ready(output) => output,
1461                Poll::Pending => {
1462                  // The task genuinely suspended; account for the wall-clock time
1463                  // it took so it isn't charged against the program's run budget.
1464                  let async_start = Instant::now();
1465                  let output = pending_async_task.await;
1466                  let async_dur = async_start.elapsed();
1467                  if let Some(last_throttle_time) = &mut last_throttle_time {
1468                    *last_throttle_time += async_dur;
1469                  }
1470                  if let Some(last_yield_time) = &mut last_yield_time {
1471                    *last_yield_time += async_dur;
1472                  }
1473                  output
1474                }
1475              };
1476            prev_async_task_output = Some((helper_name, output));
1477          }
1478        }
1479      }
1480    };
1481
1482    Ok(program_ret as i64)
1483  }
1484}
1485
1486struct Vm(NonNull<crate::ubpf::ubpf_vm>);
1487unsafe impl Send for Vm {}
1488
1489impl Vm {
1490  fn new(cage: &PointerCage) -> Self {
1491    let vm = NonNull::new(unsafe { crate::ubpf::ubpf_create() }).expect("failed to create ubpf_vm");
1492    unsafe {
1493      crate::ubpf::ubpf_toggle_bounds_check(vm.as_ptr(), false);
1494      crate::ubpf::ubpf_set_jit_pointer_mask_and_offset(vm.as_ptr(), cage.mask(), cage.offset());
1495    }
1496    Self(vm)
1497  }
1498}
1499
1500impl Drop for Vm {
1501  fn drop(&mut self) {
1502    unsafe {
1503      crate::ubpf::ubpf_destroy(self.0.as_ptr());
1504    }
1505  }
1506}
1507
1508struct LoaderValidationScope {
1509  previous: *const ProgramLoader,
1510}
1511
1512impl LoaderValidationScope {
1513  fn new(loader: &ProgramLoader) -> Self {
1514    let previous = LOADING_PROGRAM_LOADER.with(|x| {
1515      let previous = x.get();
1516      x.set(loader as *const _);
1517      previous
1518    });
1519    Self { previous }
1520  }
1521}
1522
1523impl Drop for LoaderValidationScope {
1524  fn drop(&mut self) {
1525    LOADING_PROGRAM_LOADER.with(|x| x.set(self.previous));
1526  }
1527}
1528
1529fn local_call_target(code: &[u8], pc: usize) -> usize {
1530  let offset = pc * 8;
1531  let imm = i32::from_le_bytes([
1532    code[offset + 4],
1533    code[offset + 5],
1534    code[offset + 6],
1535    code[offset + 7],
1536  ]);
1537  (pc as i64 + imm as i64 + 1) as usize
1538}
1539
1540impl ProgramLoader {
1541  /// Creates a new `ProgramLoader` to load eBPF code.
1542  pub fn new(
1543    rng: &mut impl rand::Rng,
1544    event_listener: Arc<dyn ProgramEventListener>,
1545    raw_helpers: &[&[(&'static str, Helper)]],
1546  ) -> Self {
1547    let helper_id_xor = rng.gen::<u16>();
1548    let mut helpers_inverse: HashMap<&'static str, i32> = HashMap::new();
1549    // Collect first to a HashMap then to a Vec to deduplicate
1550    let mut shuffled_helpers = raw_helpers
1551      .iter()
1552      .flat_map(|x| x.iter().copied())
1553      .collect::<HashMap<_, _>>()
1554      .into_iter()
1555      .collect::<Vec<_>>();
1556    shuffled_helpers.shuffle(rng);
1557    let mut helpers: Vec<(u16, &'static str, Helper)> = Vec::with_capacity(shuffled_helpers.len());
1558
1559    assert!(shuffled_helpers.len() <= 65535);
1560
1561    for (i, (name, helper)) in shuffled_helpers.into_iter().enumerate() {
1562      let entropy = rng.gen::<u16>() & 0x7fff;
1563      helpers.push((entropy, name, helper));
1564      helpers_inverse.insert(
1565        name,
1566        (((entropy as usize) << 16) | ((i + 1) ^ (helper_id_xor as usize))) as i32,
1567      );
1568    }
1569
1570    tracing::info!(?helpers_inverse, "generated helper table");
1571    Self {
1572      helper_id_xor,
1573      helpers: Arc::new(helpers),
1574      helpers_inverse,
1575      event_listener,
1576      code_size_limit: DEFAULT_CODE_SIZE_LIMIT,
1577      require_static_regions: false,
1578    }
1579  }
1580
1581  /// Requires every guest memory access to be statically routable to a single
1582  /// region (stack or read-only data). When enabled, compilation fails if the
1583  /// region analysis cannot classify any load, store, or atomic — i.e. no
1584  /// access falls back to the dual-region runtime probe. Memory accesses
1585  /// reached only through unmodeled control flow (e.g. an argument pointer in a
1586  /// local function) count as unresolved and are rejected.
1587  ///
1588  /// Because functions are JIT-compiled lazily and per pointer-signature
1589  /// specialization, this check runs when each function variant is first
1590  /// compiled — at the start of [`Program::run`], not at load time. The error
1591  /// is therefore surfaced through `Program::run`. A function variant that is
1592  /// never reached is never compiled and never checked; the guarantee is "every
1593  /// executed access is statically routable", scoped to the variants actually
1594  /// invoked, rather than a whole-section load-time guarantee. (Soundness does
1595  /// not depend on this flag: unclassified accesses still get the dual-region
1596  /// runtime probe, so the flag only tightens strictness.)
1597  pub fn require_static_region_analysis(mut self, require: bool) -> Self {
1598    self.require_static_regions = require;
1599    self
1600  }
1601
1602  /// Sets the maximum total size of JIT-compiled native code per program.
1603  ///
1604  /// Must be a non-zero multiple of 64 KiB so the code region stays
1605  /// page-aligned on all supported page sizes.
1606  ///
1607  /// Note for arm64: conditional branches and literal loads emitted by the
1608  /// JIT have a ±1 MiB range, and a section's helper calls reference a
1609  /// literal pool at the end of that section's code. Any single ELF section
1610  /// whose jitted code exceeds ~1 MiB is therefore rejected at load time
1611  /// ("ubpf: code translation failed") regardless of this limit; raising it
1612  /// past 1 MiB only adds room for more or larger sections within that
1613  /// per-section ceiling. x86-64 has no such per-section constraint.
1614  pub fn with_code_size_limit(mut self, limit: usize) -> Self {
1615    assert!(
1616      limit > 0 && limit % (64 * 1024) == 0,
1617      "code size limit must be a non-zero multiple of 64 KiB"
1618    );
1619    assert!(
1620      limit <= u32::MAX as usize,
1621      "code size limit must fit in u32"
1622    );
1623    self.code_size_limit = limit;
1624    self
1625  }
1626
1627  /// Loads an ELF image into a new `UnboundProgram`.
1628  pub fn load(&self, rng: &mut impl rand::Rng, elf: &[u8]) -> Result<UnboundProgram, Error> {
1629    self._load(rng, elf).map_err(Error)
1630  }
1631
1632  fn _load(&self, rng: &mut impl rand::Rng, elf: &[u8]) -> Result<UnboundProgram, RuntimeError> {
1633    let start_time = Instant::now();
1634    let cage = PointerCage::new(rng, SHADOW_STACK_SIZE, elf.len())?;
1635
1636    let code_sections = {
1637      let mut data = cage
1638        .safe_deref_for_read(cage.data_bottom(), elf.len())
1639        .unwrap();
1640      let data = unsafe { data.as_mut() };
1641      data.copy_from_slice(elf);
1642
1643      link_elf(data, cage.data_bottom(), &self.helpers_inverse).map_err(RuntimeError::Linker)?
1644    };
1645    cage.freeze_data();
1646
1647    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
1648    if page_size < 0 {
1649      return Err(RuntimeError::PlatformError("failed to get page size"));
1650    }
1651    let page_size = page_size as usize;
1652
1653    let guard_size_before = rng.gen_range(16..128) * page_size;
1654    let guard_size_after = rng.gen_range(16..128) * page_size;
1655
1656    let code_len_allocated = self.code_size_limit;
1657    let code_mem = MmapRaw::from(
1658      MmapOptions::new()
1659        .len(code_len_allocated + guard_size_before + guard_size_after)
1660        .map_anon()
1661        .map_err(|_| RuntimeError::PlatformError("failed to allocate code memory"))?,
1662    );
1663    let code_base = code_mem.as_ptr() as usize + guard_size_before;
1664
1665    unsafe {
1666      if libc::mprotect(
1667        code_mem.as_mut_ptr() as *mut _,
1668        guard_size_before,
1669        libc::PROT_NONE,
1670      ) != 0
1671        || libc::mprotect(
1672          code_mem
1673            .as_mut_ptr()
1674            .offset((guard_size_before + code_len_allocated) as isize) as *mut _,
1675          guard_size_after,
1676          libc::PROT_NONE,
1677        ) != 0
1678        || libc::mprotect(
1679          code_mem.as_mut_ptr().offset(guard_size_before as isize) as *mut _,
1680          code_len_allocated,
1681          libc::PROT_NONE,
1682        ) != 0
1683      {
1684        return Err(RuntimeError::PlatformError("failed to protect code memory"));
1685      }
1686    }
1687
1688    let mut entrypoints = HashMap::new();
1689    let mut sections = Vec::new();
1690    let resolvers = HashMap::new();
1691    let next_resolver_id = 1u32;
1692
1693    for (section_name, code_vaddr_size) in code_sections {
1694      let vm = Vm::new(&cage);
1695      unsafe {
1696        if crate::ubpf::ubpf_register_external_dispatcher(
1697          vm.0.as_ptr(),
1698          Some(tls_dispatcher),
1699          Some(std_validator),
1700        ) != 0
1701        {
1702          return Err(RuntimeError::PlatformError(
1703            "ubpf: failed to register external dispatcher",
1704          ));
1705        }
1706      }
1707
1708      let mut errmsg_ptr = std::ptr::null_mut();
1709      let code = cage
1710        .safe_deref_for_read(code_vaddr_size.0, code_vaddr_size.1)
1711        .unwrap();
1712      let code_bytes =
1713        unsafe { std::slice::from_raw_parts(code.as_ptr() as *const u8, code.len()) };
1714      validate_local_call_graph(code_bytes).map_err(|err| {
1715        RuntimeError::InvalidArgumentOwned(format!(
1716          "local call graph validation failed in {section_name}: {err}"
1717        ))
1718      })?;
1719      let layout = analyze_functions(code_bytes).map_err(|err| {
1720        RuntimeError::InvalidArgumentOwned(format!(
1721          "local function analysis failed in {section_name}: {err}"
1722        ))
1723      })?;
1724      let ret = unsafe {
1725        let validation_scope = LoaderValidationScope::new(self);
1726        let ret = crate::ubpf::ubpf_load(
1727          vm.0.as_ptr(),
1728          code.as_ptr() as *const _,
1729          code.len() as u32,
1730          &mut errmsg_ptr,
1731        );
1732        drop(validation_scope);
1733        ret
1734      };
1735      if ret != 0 {
1736        let errmsg = unsafe {
1737          if errmsg_ptr.is_null() {
1738            "".to_string()
1739          } else {
1740            CStr::from_ptr(errmsg_ptr).to_string_lossy().into_owned()
1741          }
1742        };
1743        if !errmsg_ptr.is_null() {
1744          unsafe { libc::free(errmsg_ptr as _) };
1745        }
1746        tracing::error!(section_name, error = errmsg, "failed to load code");
1747        return Err(RuntimeError::InvalidArgumentOwned(format!(
1748          "ubpf: code load failed: {errmsg}"
1749        )));
1750      }
1751
1752      let section_index = sections.len();
1753
1754      entrypoints.insert(section_name, section_index);
1755      let functions = (0..layout.functions.len())
1756        .map(|_| FunctionState::default())
1757        .collect();
1758      sections.push(Section {
1759        vm,
1760        code_vaddr: code_vaddr_size.0,
1761        code_len: code_vaddr_size.1,
1762        layout,
1763        functions,
1764      });
1765    }
1766
1767    tracing::info!(
1768      elf_size = elf.len(),
1769      native_code_addr = ?code_mem.as_ptr(),
1770      native_code_size_limit = code_len_allocated,
1771      guard_size_before,
1772      guard_size_after,
1773      duration = ?start_time.elapsed(),
1774      cage_ptr = ?cage.region().as_ptr(),
1775      cage_mapped_size = cage.region().len(),
1776      "loaded program for lazy jit"
1777    );
1778
1779    Ok(UnboundProgram {
1780      id: NEXT_PROGRAM_ID.fetch_add(1, Ordering::Relaxed),
1781      _code_mem: code_mem,
1782      code_base,
1783      code_size: code_len_allocated,
1784      page_size,
1785      code_arena: RefCell::new(CodeArena { used: 0 }),
1786      cage,
1787      helper_id_xor: self.helper_id_xor,
1788      helpers: self.helpers.clone(),
1789      event_listener: self.event_listener.clone(),
1790      require_static_regions: self.require_static_regions,
1791      entrypoints,
1792      sections: RefCell::new(sections),
1793      resolvers: RefCell::new(resolvers),
1794      next_resolver_id: Cell::new(next_resolver_id),
1795    })
1796  }
1797}
1798
1799#[derive(Default)]
1800struct Dispatch {
1801  async_preemption: bool,
1802  memory_access_error: Option<usize>,
1803  lazy_local_call: Option<u32>,
1804
1805  index: u32,
1806  arg1: u64,
1807  arg2: u64,
1808  arg3: u64,
1809  arg4: u64,
1810  arg5: u64,
1811}
1812
1813unsafe extern "C" fn tls_dispatcher(
1814  arg1: u64,
1815  arg2: u64,
1816  arg3: u64,
1817  arg4: u64,
1818  arg5: u64,
1819  index: std::os::raw::c_uint,
1820  _cookie: *mut std::os::raw::c_void,
1821) -> u64 {
1822  let yielder = ACTIVE_JIT_CODE_ZONE
1823    .with(|x| x.yielder.get())
1824    .expect("no yielder");
1825  let yielder = yielder.as_ref();
1826  let ret = yielder.suspend(Dispatch {
1827    async_preemption: false,
1828    memory_access_error: None,
1829    lazy_local_call: None,
1830    index,
1831    arg1,
1832    arg2,
1833    arg3,
1834    arg4,
1835    arg5,
1836  });
1837  ret
1838}
1839
1840unsafe extern "C" fn tls_local_call_resolver(resolver_id: std::os::raw::c_uint) -> u64 {
1841  let program = ACTIVE_PROGRAM.with(|x| x.get());
1842  if !program.is_null() {
1843    if let Some(ptr) = (*program).cached_resolver_target(resolver_id) {
1844      return ptr as u64;
1845    }
1846  }
1847
1848  let yielder = ACTIVE_JIT_CODE_ZONE
1849    .with(|x| x.yielder.get())
1850    .expect("no yielder");
1851  let yielder = yielder.as_ref();
1852  yielder.suspend(Dispatch {
1853    lazy_local_call: Some(resolver_id),
1854    ..Default::default()
1855  })
1856}
1857
1858unsafe extern "C" fn std_validator(
1859  index: std::os::raw::c_uint,
1860  _vm: *const crate::ubpf::ubpf_vm,
1861) -> bool {
1862  let loader = LOADING_PROGRAM_LOADER.with(|x| x.get());
1863  if loader.is_null() {
1864    return false;
1865  }
1866  let loader = &*loader;
1867  let entropy = (index >> 16) & 0xffff;
1868  let index = (((index & 0xffff) as u16) ^ loader.helper_id_xor).wrapping_sub(1);
1869  loader.helpers.get(index as usize).map(|x| x.0) == Some(entropy as u16)
1870}
1871
1872#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
1873unsafe fn program_counter(uctx: *mut libc::ucontext_t) -> usize {
1874  (*uctx).uc_mcontext.gregs[libc::REG_RIP as usize] as usize
1875}
1876
1877#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
1878unsafe fn program_counter(uctx: *mut libc::ucontext_t) -> usize {
1879  (*uctx).uc_mcontext.pc as usize
1880}
1881
1882#[cfg(all(target_arch = "x86_64", target_os = "openbsd"))]
1883unsafe fn program_counter(uctx: *mut libc::ucontext_t) -> usize {
1884  (*uctx).sc_rip as usize
1885}
1886
1887#[cfg(all(target_arch = "aarch64", target_os = "openbsd"))]
1888unsafe fn program_counter(uctx: *mut libc::ucontext_t) -> usize {
1889  (*uctx).sc_elr as usize
1890}
1891
1892unsafe extern "C" fn sigsegv_handler(
1893  _sig: i32,
1894  siginfo: *mut libc::siginfo_t,
1895  uctx: *mut libc::ucontext_t,
1896) {
1897  let fail = || restore_default_signal_handler(libc::SIGSEGV);
1898
1899  let Some((jit_code_zone, pointer_cage, yielder)) = ACTIVE_JIT_CODE_ZONE.with(|x| {
1900    if x.valid.load(Ordering::Relaxed) {
1901      compiler_fence(Ordering::Acquire);
1902      Some((
1903        x.code_range.get(),
1904        x.pointer_cage_protected_range.get(),
1905        x.yielder.get(),
1906      ))
1907    } else {
1908      None
1909    }
1910  }) else {
1911    return fail();
1912  };
1913
1914  let pc = program_counter(uctx);
1915
1916  if pc < jit_code_zone.0 || pc >= jit_code_zone.1 {
1917    return fail();
1918  }
1919
1920  // SEGV_MAPERR or SEGV_ACCERR.
1921  if (*siginfo).si_code != 1 && (*siginfo).si_code != 2 {
1922    return fail();
1923  }
1924
1925  let si_addr = (*siginfo).si_addr() as usize;
1926  if si_addr < pointer_cage.0 || si_addr >= pointer_cage.1 {
1927    return fail();
1928  }
1929
1930  let yielder = yielder.expect("no yielder").as_ref();
1931  yielder.suspend(Dispatch {
1932    memory_access_error: Some(si_addr),
1933    ..Default::default()
1934  });
1935}
1936
1937unsafe extern "C" fn sigusr1_handler(
1938  _sig: i32,
1939  _siginfo: *mut libc::siginfo_t,
1940  uctx: *mut libc::ucontext_t,
1941) {
1942  SIGUSR1_COUNTER.with(|x| x.set(x.get() + 1));
1943
1944  let Some((jit_code_zone, yielder)) = ACTIVE_JIT_CODE_ZONE.with(|x| {
1945    if x.valid.load(Ordering::Relaxed) {
1946      compiler_fence(Ordering::Acquire);
1947      Some((x.code_range.get(), x.yielder.get()))
1948    } else {
1949      None
1950    }
1951  }) else {
1952    return;
1953  };
1954  let pc = program_counter(uctx);
1955  if pc < jit_code_zone.0 || pc >= jit_code_zone.1 {
1956    return;
1957  }
1958
1959  // A signal can arrive after the active zone is published but before the
1960  // coroutine has installed its yielder on its first resume.
1961  let Some(yielder) = yielder else {
1962    return;
1963  };
1964  yielder.as_ref().suspend(Dispatch {
1965    async_preemption: true,
1966    ..Default::default()
1967  });
1968}
1969
1970unsafe fn restore_default_signal_handler(signum: i32) {
1971  let mut act: libc::sigaction = std::mem::zeroed();
1972  act.sa_sigaction = libc::SIG_DFL;
1973  act.sa_flags = libc::SA_SIGINFO;
1974  if libc::sigaction(signum, &act, std::ptr::null_mut()) != 0 {
1975    libc::abort();
1976  }
1977}
1978
1979fn get_blocked_sigset() -> libc::sigset_t {
1980  unsafe {
1981    let mut s: libc::sigset_t = std::mem::zeroed();
1982    libc::sigaddset(&mut s, libc::SIGUSR1);
1983    libc::sigaddset(&mut s, libc::SIGSEGV);
1984    s
1985  }
1986}