jit/lib.rs
1//! The runtime owns guest process state and calls generated native code through a stable C ABI.
2//!
3//! The guest is an RV64 Linux process. The host is the machine that runs this crate.
4//! The runtime maps the guest ELF file, builds its first user stack, and handles a small
5//! set of Linux system calls. Generated code uses raw pointers to enter and leave this
6//! runtime. Thus, the data layout in this file is also part of the JIT interface.
7
8mod mmu;
9mod remi;
10
11use liil::nnil::{Nnil, NnilInstruction};
12
13use std::collections::{BTreeMap, BTreeSet, VecDeque};
14use std::error::Error;
15use std::fs::{self, File, OpenOptions};
16use std::io::{self, IsTerminal as _, Read as _, Seek as _, SeekFrom, Write as _};
17use std::os::unix::fs::{FileExt as _, MetadataExt as _};
18use std::panic::{AssertUnwindSafe, catch_unwind, set_hook, take_hook};
19use std::path::{Component, Path, PathBuf};
20use std::sync::{
21 Arc,
22 atomic::{AtomicBool, Ordering},
23};
24use std::time::{Duration, Instant};
25
26pub use mmu::{GuestMemory, GuestMemoryAbi, GuestPageAbi, GuestTranslationAbi, PagePerms};
27use mmu::{align_down, align_up};
28use remi::compile_jit_multi;
29pub use remi::{CompiledJit, compile_jit};
30
31/*
32
33 todo v7
34 - performance
35 - timen
36 - alle nnils impl.: riscv-arch testsuite
37 - Z* instructions
38 - syscalls ? exit & recovery
39 - reset
40 - privilged
41 - threads?
42 - mmu? / memory richtig
43
44*/
45
46// todo - barebones
47// These fixed sizes keep the host and generated copies of `GuestState` ABI-compatible.
48// Slots 0..31 hold integer registers, slots 32..63 hold floating-point registers,
49// and slots 64..95 are spare project capacity.
50// The emitter checks each index before it writes generated Rust code.
51const GUEST_REG_COUNT: usize = 96;
52// NNIL uses numbered temporary values. The fixed ABI array has a project maximum of 256.
53// The emitter rejects a temporary index that reaches this limit.
54const GUEST_TMP_COUNT: usize = 256;
55// A small fixed array keeps debugger data inside the C-compatible control structure.
56// The value 64 is a project limit, not a RISC-V or Linux ABI value.
57const DEBUG_SLOTS: usize = 64;
58
59// RISC-V register x0 always reads as zero. Generated code must also ignore writes to it.
60const INTEGER_ZERO_REG: usize = 0;
61// Slots 0 through 31 hold x0 through x31. Thus, slot 32 can hold floating-point f0
62// without a second register array.
63const FP_REG_BASE: usize = 32;
64
65// This emulator selects 4 KiB guest pages. Linux and RISC-V do not require this one size.
66// One project value must be used in masks, mappings, `mmap` offsets, and AT_PAGESZ.
67const PAGE_SIZE: usize = 4096;
68
69// The project gives each guest a deterministic 128 KiB stack. This is an emulator
70// policy value. It is not a limit from the RISC-V ABI.
71const DEFAULT_STACK_SIZE: u64 = 0x20_000;
72// Keep the stack at a stable high guest address so low ELF mappings have much free space.
73// Page alignment avoids a partial top page. The fixed address makes mappings repeatable.
74const DEFAULT_STACK_TOP: u64 = 0x0000_7fff_ffff_0000;
75// Leave 256 bytes of mapped headroom below the stack top before startup data is added.
76// This project reserve has no documented ABI purpose. It is not an unmapped guard page.
77const DEFAULT_STACK_POINTER_OFFSET: u64 = 0x100;
78// Load the ELF interpreter at a fixed 1 GiB guest bias for deterministic separation
79// from normal low-address executable segments.
80const DYNAMIC_LOADER_BIAS: u64 = 0x4000_0000;
81// Start the automatic guest `mmap` cursor at 1.25 GiB, above the fixed loader bias.
82// Successful mappings move this cursor upward.
83const MMAP_BASE: u64 = 0x5000_0000;
84
85// These values are from the architecture-neutral Linux file-control UAPI.
86// Source: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/fcntl.h
87// Bits 0 and 1 encode the access mode. The mask 3 selects only these two bits.
88const O_ACCMODE: u64 = 3;
89// Bit 0 requests write-only access. A clear access field means read-only access.
90const O_WRONLY: u64 = 1;
91// Bit 1 requests read and write access.
92const O_RDWR: u64 = 2;
93// Bits 6, 7, 9, 10, and 19 are independent options. Bitwise AND tests one option,
94// and bitwise OR can test a group of options without changing the other flag bits.
95// O_CREAT creates a missing file. O_EXCL makes creation fail if the file exists.
96// O_TRUNC clears an existing file. O_APPEND puts each write at the file end.
97// O_CLOEXEC asks a future `exec` operation to close this descriptor.
98const O_CREAT: u64 = 0x40;
99const O_EXCL: u64 = 0x80;
100const O_TRUNC: u64 = 0x200;
101const O_APPEND: u64 = 0x400;
102const O_CLOEXEC: u64 = 0x80000;
103
104// Commands 1 and 2 read and write the per-descriptor close-on-exec flag.
105const F_GETFD: u64 = 1;
106const F_SETFD: u64 = 2;
107// Only bit 0 has a defined meaning for these commands. Mask all other argument bits.
108const FD_CLOEXEC: u64 = 1;
109
110// todo -- wird das überhaupt gebraucht
111// siehe https://docs.rs/auxv/latest/auxv/
112// Linux puts these type numbers in the auxiliary vector on the first process stack.
113// The dynamic loader and C library use the values to find loader information without
114// parsing the executable again.
115// Source: https://github.com/torvalds/linux/blob/master/include/uapi/linux/auxvec.h
116// Consumer reference: https://man7.org/linux/man-pages/man3/getauxval.3.html
117// Type 0 ends the vector. Its value field has no meaning.
118const AT_NULL: u64 = 0;
119// Types 3 through 5 give the address, entry size, and count of program headers.
120const AT_PHDR: u64 = 3;
121const AT_PHENT: u64 = 4;
122const AT_PHNUM: u64 = 5;
123// Type 6 tells libc which page size the emulated MMU uses.
124const AT_PAGESZ: u64 = 6;
125// Type 7 is the load base of the ELF interpreter. It is zero for a static process.
126const AT_BASE: u64 = 7;
127// Type 8 carries process-entry flags. This runtime supplies zero.
128const AT_FLAGS: u64 = 8;
129// Type 9 is the executable entry. It is not the interpreter entry.
130const AT_ENTRY: u64 = 9;
131// Types 11 through 14 supply real and effective user and group IDs.
132const AT_UID: u64 = 11;
133const AT_EUID: u64 = 12;
134const AT_GID: u64 = 13;
135const AT_EGID: u64 = 14;
136// Type 16 reports the RISC-V ISA letters that this runtime chooses to advertise.
137const AT_HWCAP: u64 = 16;
138// Type 17 gives the number of `times()` clock ticks in one second.
139const AT_CLKTCK: u64 = 17;
140// Type 23 tells libc if secure execution rules must apply. Zero means normal execution.
141const AT_SECURE: u64 = 23;
142// Type 25 points to exactly 16 seed bytes on the initial stack.
143const AT_RANDOM: u64 = 25;
144// Type 26 is a second architecture capability word. This runtime has no values for it.
145const AT_HWCAP2: u64 = 26;
146// Type 31 points to the pathname used to execute the file.
147// This runtime approximates that pathname with the first argument string.
148const AT_EXECFN: u64 = 31;
149
150// Linux assigns one HWCAP bit to each ISA letter at position `letter - 'A'`.
151// These shifts set A=0, C=2, D=3, F=5, I=8, and M=12. OR combines the six bits.
152// This is a fixed advertised IMAFDC word. It is not derived from decoder coverage;
153// atomics and parts of floating-point support are still incomplete.
154// Source: https://github.com/torvalds/linux/blob/master/arch/riscv/include/uapi/asm/hwcap.h
155const RISCV_HWCAP: u64 = (1 << 0) | (1 << 2) | (1 << 3) | (1 << 5) | (1 << 8) | (1 << 12);
156
157// Linux gives AT_RANDOM a pointer to 16 bytes. Fixed bytes make guest startup and
158// snapshots repeatable. They are not suitable for security or cryptography.
159// Source: https://github.com/torvalds/linux/blob/master/fs/binfmt_elf.c
160const ELF_AUX_RANDOM: [u8; 16] = *b"moin-liebe-leute";
161
162// https://doc.rust-lang.org/rust-by-example/error/multiple_error_types/boxing_errors.html
163// ELF, file, memory, and compiler operations return different error types. A boxed
164// error lets one public result type carry all of them without a large local error enum.
165type JitResult<T> = Result<T, Box<dyn Error>>;
166// Generated libraries expose one C-ABI function. It is unsafe because Rust cannot
167// prove that the raw pointer is valid or that both libraries use the same layout.
168pub(crate) type ExecuteFn = unsafe extern "C" fn(*mut GuestState) -> ExitReason;
169
170// Each auxiliary-vector record contains one 64-bit type and one 64-bit value on RV64.
171type AuxEntry = (u64, u64);
172
173#[repr(u8)]
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175/// Distinguishes fault types so the runtime can report the failed guest operation.
176pub enum AccessKind {
177 // Values 0 through 3 are enum tags shared with generated code. They are not permission bits.
178 // Zero means that no memory fault is pending.
179 /// Reports that no memory access fault is pending.
180 None = 0,
181 // Nonzero values keep read, write, and instruction-fetch failures separate.
182 /// Reports a failed guest data read.
183 Read = 1,
184 /// Reports a failed guest data write.
185 Write = 2,
186 /// Reports a failed guest instruction fetch.
187 Execute = 3,
188}
189
190// C ABI kompatibiliät todo muss das?
191// ExitReason von gamozo!
192#[repr(C)]
193#[derive(Clone, Copy, Debug, PartialEq, Eq)]
194/// Transfers control from generated code to the runtime without unwinding across the ABI.
195pub enum ExitReason {
196 // The order is part of the C ABI that generated libraries copy. Do not reorder it
197 // unless the generated definition changes at the same time.
198 // Implicit discriminants start at zero and increase by one.
199 /// Reports that generated code has not selected a stop reason.
200 None,
201 // The generated block could not find the next guest PC in its dispatch table.
202 /// Returns an uncached computed target to the runtime for compilation or dispatch.
203 IndirectBranch,
204 // These values return detailed MMU failures without a host signal or Rust panic.
205 /// Reports that a guest load did not have a readable mapping.
206 ReadFault,
207 /// Reports that a guest store did not have a writable mapping.
208 WriteFault,
209 /// Reports that instruction fetch did not have an executable mapping.
210 ExecFault,
211 // A host call has no runtime implementation, so the caller can decide what to do.
212 /// Reports an NNIL host call that the runtime does not implement.
213 UnhandledHostCall,
214 // GuestExit is separate from Syscall so normal process exit is easy to identify.
215 /// Reports normal termination through the guest `exit` system call.
216 GuestExit,
217 /// Returns a guest environment call to the runtime system-call handler.
218 Syscall,
219 /// Returns a guest breakpoint instruction to the runtime.
220 Sysbreak,
221 // These stops support the interactive debugger and bounded execution.
222 /// Stops before a translated block at a configured guest address.
223 Breakpoint,
224 /// Stops after another thread sets the shared pause flag.
225 Paused,
226 /// Stops after a bounded run uses its translated-block budget.
227 BlockLimit,
228}
229
230// todo - fuzzer api klären
231#[repr(C)]
232/// Keeps debugger checks optional so normal generated blocks do not call back into the runtime.
233pub struct GuestControl {
234 // A raw pointer avoids an `Arc` in the generated ABI. The runtime owns the `Arc`.
235 pause: *const AtomicBool,
236 // Fixed arrays make the layout stable across the host and generated library.
237 breakpoints: [u64; DEBUG_SLOTS],
238 // Only this sorted prefix contains active breakpoints.
239 breakpoint_count: usize,
240 // `u64::MAX` is the no-skip sentinel. A breakpoint at that address conflicts with it.
241 skip_breakpoint: u64,
242 // Generated blocks reduce this value when bounded execution is active.
243 blocks_left: u64,
244 // Use one byte as an ABI flag. Zero is false and any nonzero value is true.
245 limited: u8,
246 // This array is a ring. `trace_pos` can increase after the array becomes full.
247 trace: [u64; DEBUG_SLOTS],
248 trace_pos: usize,
249}
250
251#[repr(C)]
252#[derive(Clone, Copy)]
253/// Pairs a guest target with native code for indirect dispatch from generated code.
254pub struct JitEntryAbi {
255 // The vector is sorted by this key so generated code can use binary search.
256 /// Gives the guest block-entry address used as the sorted lookup key.
257 pub guest_pc: u64,
258 // This pointer enters the compiled block that starts at `guest_pc`.
259 /// Calls native code that starts at `guest_pc`.
260 pub execute: ExecuteFn,
261}
262
263#[repr(C)]
264#[derive(Clone)]
265/// Uses one shared layout because both the runtime and generated libraries access guest state.
266pub struct GuestState {
267 // Generated code writes the reason before it gives control back to the runtime.
268 /// Tells the runtime why generated code returned.
269 pub exit_reason: ExitReason,
270 // The PC is a guest virtual byte address, not a host function address.
271 /// Gives the current guest virtual instruction address.
272 pub pc: u64,
273 // Registers store raw bits. Slots 0 through 31 are integer x0 through x31.
274 // Floating-point register f0 starts at slot 32.
275 /// Stores raw integer and floating-point register bits in the shared ABI layout.
276 pub regs: [u64; GUEST_REG_COUNT],
277 // NNIL temporaries live only for translated calculations but share block exits.
278 /// Stores NNIL temporary values that must remain available across generated functions.
279 pub tmps: [u64; GUEST_TMP_COUNT],
280 // Generated code uses this MMU view because it cannot borrow `GuestMemory`.
281 /// Points to the MMU view that generated code can use without Rust containers.
282 pub memory: *mut GuestMemoryAbi, // todo siehe txt
283 // These fields identify the guest operation that failed. The runtime can then
284 // report an emulated fault instead of a host segmentation fault.
285 /// Gives the guest address of the last failed memory access.
286 pub fault_addr: u64,
287 /// Identifies the operation that failed at `fault_addr`.
288 pub fault_access: AccessKind,
289 // A null pointer selects fast execution without debugger checks.
290 /// Points to optional debugger controls, or is null for unrestricted execution.
291 pub control: *mut GuestControl,
292 // This borrowed sorted array lets compiled blocks call other compiled blocks.
293 /// Points to sorted cross-artifact dispatch entries.
294 pub jit_entries: *const JitEntryAbi,
295 /// Gives the valid length of `jit_entries`.
296 pub jit_entry_count: usize,
297}
298
299impl Default for GuestState {
300 /// Starts with no active guest operation and no borrowed ABI pointers.
301 /// The runtime installs each pointer only after it owns the target allocation.
302 fn default() -> Self {
303 Self {
304 exit_reason: ExitReason::None,
305 pc: 0,
306 regs: [0; GUEST_REG_COUNT],
307 tmps: [0; GUEST_TMP_COUNT],
308 memory: std::ptr::null_mut(),
309 fault_addr: 0,
310 fault_access: AccessKind::None,
311 control: std::ptr::null_mut(),
312 jit_entries: std::ptr::null(),
313 jit_entry_count: 0,
314 }
315 }
316}
317
318// todo -- refactor nach remu über cli flags setzen lassen
319#[derive(Debug, Clone)]
320/// Keeps process and compiler inputs together so snapshots and cache behavior are reproducible.
321pub struct RemuConfig {
322 // These strings become guest-owned NUL-terminated data on the initial stack.
323 /// Supplies guest argument strings for the initial process stack.
324 pub argv: Vec<String>,
325 /// Supplies guest environment strings for the initial process stack.
326 pub envp: Vec<String>,
327 // Absolute guest paths resolve below `sysroot`. Syscall policy normally treats this tree
328 // as read-only. The openat subset documents one flag-combination limit.
329 /// Selects the read-only host root for absolute guest paths.
330 pub sysroot: Option<PathBuf>,
331 // Relative guest paths resolve below `workdir`. Guest file changes stay here.
332 /// Selects the writable host root for relative guest paths.
333 pub workdir: Option<PathBuf>,
334 // This value goes to rustc as `opt-level`. The runtime accepts only 0, 1, or 2
335 // to limit compile time during lazy translation.
336 /// Selects Rust compiler optimization level 0, 1, or 2 for generated code.
337 pub jit_opt_level: u8,
338 // A disk cache can reuse generated source and machine code across runtime instances.
339 /// Selects an optional directory for reusable generated artifacts.
340 pub jit_cache_dir: Option<PathBuf>,
341 // Raw JIT output uses the custom ELF mapper and the Linux x86-64 memory-map ABI.
342 /// Requests the Linux x86-64 raw loader instead of a shared library.
343 pub raw_jit: bool, // todo - so wie gamozolabs - nur linux64
344}
345
346impl Default for RemuConfig {
347 /// Selects repeatable compiler and cache settings for callers that need no guest I/O roots.
348 fn default() -> Self {
349 Self {
350 argv: Vec::new(),
351 envp: Vec::new(),
352 sysroot: None,
353 workdir: None,
354 jit_opt_level: 2,
355 jit_cache_dir: Some(PathBuf::from("jit-tmp/cache")),
356 raw_jit: false,
357 }
358 }
359}
360
361// todo - nm, readelf
362#[derive(Debug, Clone)]
363/// Keeps file and memory sizes separate because ELF load segments can contain zero-filled BSS.
364pub struct LoadSegment {
365 // ELF virtual addresses are guest addresses. They never point into host memory.
366 /// Gives the first guest virtual address of the segment.
367 pub vaddr: u64,
368 // `mem_size` includes file bytes and the zero-filled tail, such as `.bss`.
369 /// Gives the total mapped size, including the zero-filled tail.
370 pub mem_size: u64,
371 // `file_size` limits the bytes that the loader copies from the ELF file.
372 /// Gives the number of initialized bytes copied from the file.
373 pub file_size: u64,
374 // The MMU enforces the R, W, and X flags after it maps the segment.
375 /// Gives the guest access permissions from the ELF program header.
376 pub perms: PagePerms,
377 // Keep an owned copy because the input byte slice can go out of scope.
378 /// Owns the initialized file bytes after the source ELF buffer is gone.
379 pub bytes: Vec<u8>,
380}
381
382#[derive(Debug, Clone)]
383/// Retains symbol metadata for entry selection and debugger address display.
384pub struct ProgramSymbol {
385 // A copied name makes symbol lookup independent from the ELF string-table lifetime.
386 /// Owns the symbol name from its ELF string table.
387 pub name: String,
388 // This is the guest symbol address for an executable image.
389 /// Gives the symbol guest address.
390 pub value: u64,
391 // A zero size is valid. Some assembler and linker symbols have no object extent.
392 /// Gives the symbol extent, or zero when ELF does not supply one.
393 pub size: u64,
394 // Index zero is ELF `SHN_UNDEF`. Dynamic imports commonly use that index.
395 /// Gives the defining ELF section index; zero means undefined.
396 pub section_index: u16,
397}
398
399impl ProgramSymbol {
400 /// Rejects undefined table entries when the debugger resolves an address.
401 pub fn is_defined(&self) -> bool {
402 self.section_index != 0
403 }
404}
405
406#[derive(Debug, Clone)]
407/// Retains jump-slot locations so dynamic imports can be connected later.
408pub struct ImportedSymbol {
409 // The name lets a later shim select the host service.
410 /// Owns the imported symbol name used to select a future shim.
411 pub name: String,
412 // A jump-slot relocation writes the resolved function address at this GOT address.
413 /// Gives the guest GOT slot that receives the resolved address.
414 pub got_addr: u64,
415}
416
417#[derive(Debug, Clone)]
418/// Stores parsed ELF data in a form that the MMU can map without parsing again.
419pub struct ProgramImage {
420 // ELF type 2 is an executable. Type 3 is a shared object or a PIE executable.
421 /// Gives the ELF object type, such as `ET_EXEC` or `ET_DYN`.
422 pub elf_type: u16,
423 // Start execution here unless PT_INTERP makes the runtime start an interpreter first.
424 /// Gives the main executable entry address before interpreter selection.
425 pub entry_pc: u64,
426 // Program headers, not sections, define the data that Linux maps into a process.
427 /// Owns the loadable segments that define the initial process image.
428 pub segments: Vec<LoadSegment>,
429 // A tree map gives stable lookup and iteration order for debugger output.
430 /// Maps each retained ELF symbol name to its metadata.
431 pub symbols: BTreeMap<String, ProgramSymbol>,
432 /// Lists RISC-V jump-slot relocations for future import connection.
433 pub imports: Vec<ImportedSymbol>,
434 // `PT_INTERP` names the guest dynamic loader, such as `/lib/ld-linux-riscv64-lp64d.so.1`.
435 /// Names the guest dynamic loader from `PT_INTERP`, when present.
436 pub interp: Option<String>,
437 // The initial break follows the highest load segment so the heap does not cover ELF data.
438 /// Gives the page-aligned initial heap end after all load segments.
439 pub brk_base: u64, // initialer heap start
440 // Linux passes these three values in AT_PHDR, AT_PHENT, and AT_PHNUM.
441 /// Gives the mapped program-header address used for `AT_PHDR`.
442 pub elf_phdr_addr: Option<u64>, // todo
443 /// Gives one program-header entry size used for `AT_PHENT`.
444 pub elf_phent_size: u16, // todo
445 /// Gives the program-header count used for `AT_PHNUM`.
446 pub elf_phnum: u16, // todo
447}
448
449impl ProgramImage {
450 /// Builds a synthetic image from caller-supplied segments.
451 pub fn from_segments(entry_pc: u64, mut segments: Vec<LoadSegment>) -> Self {
452 // Sorted segments make memory mapping and free-range behavior deterministic.
453 segments.sort_by_key(|segment| segment.vaddr);
454
455 // todo -- heap
456 // Round every segment end up to 4 KiB because `brk` maps and unmaps complete pages.
457 // Saturating addition limits the unrounded end. `align_up` still assumes that the
458 // rounded result fits in u64.
459 let brk_base = segments
460 .iter()
461 .map(|segment| {
462 align_up(
463 segment.vaddr.saturating_add(segment.mem_size),
464 PAGE_SIZE as u64,
465 )
466 })
467 .max()
468 .unwrap_or(0);
469
470 Self {
471 // Value 2 is ELF `ET_EXEC`. Synthetic images have fixed guest addresses.
472 elf_type: 2,
473 entry_pc,
474 segments,
475 symbols: BTreeMap::new(),
476 imports: Vec::new(),
477 interp: None,
478 brk_base,
479 elf_phdr_addr: None,
480 elf_phent_size: 0,
481 elf_phnum: 0,
482 }
483 }
484
485 // todo !!!
486 /// Accepts only RISC-V ELF64 little-endian input because the current process ABI is RV64.
487 pub fn from_elf_bytes(bytes: &[u8]) -> JitResult<Self> {
488 // The generic ELF ABI assigns machine number 243 to RISC-V.
489 // Source: https://gabi.xinuos.com/elf/a-emachine.html
490 const EM_RISCV: u16 = 243; // unnötig (?)
491
492 // Program-header types tell a process loader what it must map or inspect.
493 // `PT_LOAD` maps bytes. `PT_DYNAMIC` describes dynamic linking. `PT_INTERP`
494 // names that linker. `PT_PHDR` gives the in-memory header-table address.
495 // Source: https://gabi.xinuos.com/elf/07-pheader.html
496 const PT_LOAD: u32 = 1;
497 const PT_INTERP: u32 = 3;
498 const PT_DYNAMIC: u32 = 2;
499 const PT_PHDR: u32 = 6;
500
501 // ELF program-header permissions use bit 0 for execute, bit 1 for write,
502 // and bit 2 for read. This differs from Linux `PROT_*`, which uses R=1,
503 // W=2, and X=4. Each AND test isolates one permission bit and ignores other flags.
504 const PF_X: u32 = 1;
505 const PF_W: u32 = 2;
506 const PF_R: u32 = 4;
507
508 // These section types are the only ones needed for names and jump-slot imports.
509 // `SHT_SYMTAB` is the full link symbol table. `SHT_DYNSYM` is the smaller table
510 // used for dynamic linking. `SHT_RELA` stores relocations with explicit addends.
511 // Source: https://gabi.xinuos.com/elf/03-sheader.html
512 const SHT_RELA: u32 = 4;
513 const SHT_SYMTAB: u32 = 2;
514 const SHT_DYNSYM: u32 = 11;
515
516 // RISC-V relocation type 5 connects one PLT entry to its GOT function slot.
517 // Source: https://riscv-non-isa.github.io/riscv-elf-psabi-doc/#relocations
518 const R_RISCV_JUMP_SLOT: u32 = 5;
519
520 // const ELF_MAGIC: &[u8; 4] = b"\x7fELF";
521 // const ELFCLASS64: u8 = 2;
522 // const ELFDATA2LSB: u8 = 1;
523 /* if bytes.len() < 64 || &bytes[..4] != ELF_MAGIC {
524 return Err(io::Error::other("falsch").into());
525 }
526 if bytes[4] != ELFCLASS64 {
527 return Err(io::Error::other("falsch").into());
528 }
529 if bytes[5] != ELFDATA2LSB {
530 return Err(io::Error::other("falsch").into());
531 } */
532
533 // Check the magic, class, and byte order before fixed-offset reads interpret the input.
534 // This parser does not validate all ELF identity and header-version fields.
535 // Bytes 0..4 are 0x7f and `ELF`. Byte 4 is class 2 for ELF64. Byte 5 is data
536 // encoding 1 for little endian. All later integer reads depend on these values.
537 // Source: https://gabi.xinuos.com/elf/02-eheader.html
538 if bytes.get(..6) != Some(b"\x7fELF\x02\x01") {
539 return Err(io::Error::other("expected a little-endian ELF64 file").into());
540 }
541
542 // The first 16 bytes are `e_ident`. Thus, `e_type` starts at byte 16 and
543 // `e_machine` starts at byte 18 in an ELF64 header.
544 let elf_type = read_u16(bytes, 16)?;
545 let march = read_u16(bytes, 18)?;
546
547 if march != EM_RISCV {
548 return Err(io::Error::other(format!("nicht riscv???? -- sondern: {}", march)).into());
549 }
550
551 // ELF64 aligns the next 64-bit fields to byte 24. `e_entry` is the guest entry.
552 let entry_pc = read_u64(bytes, 24)?;
553
554 // Bytes 32 and 40 hold the program-header and section-header table offsets.
555 // Bytes 54 through 60 give table strides and counts. Fixed reads still require
556 // program entries of at least 56 bytes and section entries of at least 64 bytes,
557 // but this parser does not validate those minima. It also lacks extended counts.
558 let phoff_u64 = read_u64(bytes, 32)?;
559 let phoff = phoff_u64 as usize;
560 let phentsize_u16 = read_u16(bytes, 54)?;
561 let phentsize = phentsize_u16 as usize;
562 let phnum_u16 = read_u16(bytes, 56)?;
563 let phnum = phnum_u16 as usize;
564
565 let shoff = read_u64(bytes, 40)? as usize;
566 let shentsize = read_u16(bytes, 58)? as usize;
567 let shnum = read_u16(bytes, 60)? as usize;
568
569 let mut segments = Vec::new();
570 let mut interp = None;
571 let mut elf_phdr_addr = None;
572
573 // Some ELF files have no `PT_PHDR`. Keep a derived address as a fallback for AT_PHDR.
574 let mut dephdra = None;
575
576 for idx in 0..phnum {
577 // Each index selects one file-defined entry. Checked addition rejects address
578 // wrap. Saturating multiplication cannot wrap before the checked addition.
579 let header = phoff
580 .checked_add(idx.saturating_mul(phentsize))
581 .ok_or_else(|| io::Error::other("program header overflow"))?;
582
583 // An ELF64 program header is 56 bytes. The field offsets are 0 for type,
584 // 4 for flags, 8 for file offset, 16 for virtual address, 32 for file size,
585 // and 40 for memory size. Physical address and alignment are not needed here.
586 let p_type = read_u32(bytes, header)?;
587 let p_flags = read_u32(bytes, header + 4)?;
588 let p_offset_u64 = read_u64(bytes, header + 8)?;
589 let p_offset = p_offset_u64 as usize;
590 let p_vaddr = read_u64(bytes, header + 16)?;
591 let p_filesz = read_u64(bytes, header + 32)?;
592 let p_memsz = read_u64(bytes, header + 40)?;
593
594 // dbg!(header);
595 // dbg!(p_type);
596
597 // todo --
598 match p_type {
599 PT_LOAD => {
600 if dephdra.is_none() {
601 let Some(segment_end) = p_offset_u64.checked_add(p_filesz) else {
602 return Err(io::Error::other("overflow").into());
603 };
604
605 // Derive AT_PHDR when ELF omits PT_PHDR and maps the table start.
606 // This check does not prove that the complete header table is mapped.
607 // Convert a file offset to a guest address with the containing
608 // segment relation `p_vaddr + (e_phoff - p_offset)`.
609 if phoff_u64 >= p_offset_u64 && phoff_u64 < segment_end {
610 let delta = phoff_u64 - p_offset_u64;
611
612 dephdra = Some(
613 p_vaddr
614 .checked_add(delta)
615 .ok_or_else(|| io::Error::other("overflow"))?,
616 );
617 }
618 }
619
620 if p_filesz > p_memsz {
621 // A load segment can add a zero-filled memory tail, but it cannot
622 // place more file bytes in less memory.
623 return Err(io::Error::other("overflow").into());
624 }
625
626 let end = p_offset
627 .checked_add(p_filesz as usize)
628 .ok_or_else(|| io::Error::other("overflow"))?;
629
630 let file_bytes = bytes
631 .get(p_offset..end)
632 .ok_or_else(|| io::Error::other("overflow"))?;
633
634 // Preserve ELF permissions so guest fetches and data accesses fault correctly.
635 // Bitwise AND asks if one ELF flag is set. Bitwise OR adds the matching
636 // internal permission without removing permissions that were added before it.
637 let mut perms = PagePerms::empty();
638
639 if (p_flags & PF_R) != 0 {
640 perms |= PagePerms::READ;
641 }
642 if (p_flags & PF_W) != 0 {
643 perms |= PagePerms::WRITE;
644 }
645 if (p_flags & PF_X) != 0 {
646 perms |= PagePerms::EXEC;
647 }
648
649 segments.push(LoadSegment {
650 vaddr: p_vaddr,
651 mem_size: p_memsz,
652 file_size: p_filesz,
653 perms,
654 bytes: file_bytes.to_vec(),
655 });
656 }
657
658 PT_INTERP => {
659 // ELF requires a trailing NUL byte in this file range.
660 let end = p_offset
661 .checked_add(p_filesz as usize)
662 .ok_or_else(|| io::Error::other("interp overflow"))?;
663
664 let raw = bytes
665 .get(p_offset..end)
666 .ok_or_else(|| io::Error::other("interp overflow"))?;
667
668 // Use bytes before the first NUL. A missing terminator is not rejected here.
669 let path = raw.split(|byte| *byte == 0).next().unwrap_or_default();
670 interp = Some(String::from_utf8_lossy(path).into_owned());
671 }
672
673 // The guest interpreter processes dynamic metadata after it starts.
674 // This parser does no separate work for PT_DYNAMIC and relies on overlapping
675 // PT_LOAD bytes when the ELF file supplies them.
676 PT_DYNAMIC => {}
677 PT_PHDR => {
678 // `p_vaddr` is already the in-memory address that AT_PHDR requires.
679 elf_phdr_addr = Some(p_vaddr);
680 }
681
682 _ => {
683 // dbg!("?");
684 }
685 }
686 }
687
688 let elf_phdr_addr = elf_phdr_addr
689 .or(dephdra)
690 .ok_or_else(|| io::Error::other("elf headers nicht da"))?;
691
692 // Section headers are not required for process mapping. They are parsed only to
693 // support symbol names, entry selection, debugger output, and import discovery.
694 let sections = parse_section_headers(bytes, shoff, shentsize, shnum)?;
695 let mut symbols = BTreeMap::<String, ProgramSymbol>::new();
696 let mut symbol_sections = BTreeMap::<usize, Vec<ProgramSymbol>>::new();
697
698 for (section_idx, section) in sections.iter().enumerate() {
699 if matches!(section.sh_type, SHT_SYMTAB | SHT_DYNSYM) {
700 let parsed = parse_symbol_section(bytes, §ions, section)?;
701
702 // passt noch nicht todo
703 for symbol in &parsed {
704 if symbol.name.is_empty() {
705 continue;
706 }
707
708 // Prefer defined, nonzero symbols because debug and entry lookup need addresses.
709 let replace = match symbols.get(&symbol.name) {
710 Some(existing) => {
711 (symbol.is_defined() && !existing.is_defined())
712 || (symbol.is_defined() && existing.value == 0 && symbol.value != 0)
713 || (!existing.is_defined()
714 && symbol.value != 0
715 && existing.value == 0)
716 }
717 None => true,
718 };
719
720 if replace {
721 symbols.insert(symbol.name.clone(), symbol.clone());
722 }
723 }
724
725 symbol_sections.insert(section_idx, parsed);
726 }
727 }
728
729 let mut imports = Vec::new();
730
731 for section in §ions {
732 if section.sh_type != SHT_RELA || section.sh_entsize == 0 {
733 // A zero entry size cannot be the divisor for a relocation count.
734 continue;
735 }
736
737 let symbol_table_index = section.sh_link as usize;
738 let Some(section_symbols) = symbol_sections.get(&symbol_table_index) else {
739 continue;
740 };
741
742 // ELF64 Rela records are 24 bytes, but this parser checks only for a nonzero
743 // entry size and reads the first 16 bytes. A smaller declared size is not rejected.
744 // Integer division also ignores incomplete trailing bytes. These are parser limits.
745 let count = (section.sh_size / section.sh_entsize) as usize;
746 //let count = 0;
747
748 for idx in 0..count {
749 let rela_offset = section
750 .sh_offset
751 .checked_add((idx as u64).saturating_mul(section.sh_entsize))
752 .ok_or_else(|| io::Error::other("overflow"))?
753 as usize;
754
755 // In an ELF64 `Rela` record, `r_offset` starts at byte 0 and `r_info`
756 // starts at byte 8. The explicit addend at byte 16 is not needed to find imports.
757 let r_offset = read_u64(bytes, rela_offset)?;
758 let r_info = read_u64(bytes, rela_offset + 8)?;
759 // ELF64 packs the relocation type in the low 32 bits. A cast to `u32`
760 // discards the high symbol-index bits and keeps this type field.
761 let r_type = r_info as u32;
762
763 // Only jump slots identify external function calls that the runtime can shim.
764 if r_type != R_RISCV_JUMP_SLOT {
765 continue;
766 }
767
768 // Shift right by 32 to remove the type field and move the symbol-table
769 // index into the low bits. This follows the ELF64 `ELF64_R_SYM` rule.
770 let symbol_idx = (r_info >> 32) as usize;
771
772 let Some(symbol) = section_symbols.get(symbol_idx) else {
773 return Err(io::Error::other("missing symbol ").into());
774 };
775
776 if symbol.name.is_empty() {
777 continue;
778 }
779
780 imports.push(ImportedSymbol {
781 name: symbol.name.clone(),
782 got_addr: r_offset,
783 });
784 }
785 }
786
787 // Sort by GOT address for deterministic grouping. `dedup_by` removes only adjacent
788 // exact duplicates; different names at one address can keep equal pairs apart.
789 imports.sort_by_key(|symbol| symbol.got_addr);
790 imports.dedup_by(|lhs, rhs| lhs.got_addr == rhs.got_addr && lhs.name == rhs.name);
791
792 //dbg!(imports);
793
794 Ok(Self {
795 elf_type,
796 // The first page after the highest memory segment is the initial heap break.
797 // Saturating addition limits the unrounded end. `align_up` assumes that its
798 // rounded result fits in u64.
799 brk_base: segments
800 .iter()
801 .map(|segment| {
802 align_up(
803 segment.vaddr.saturating_add(segment.mem_size),
804 PAGE_SIZE as u64,
805 )
806 })
807 .max()
808 .unwrap_or(0),
809 entry_pc,
810 segments,
811 symbols,
812 imports,
813 interp,
814 elf_phdr_addr: Some(elf_phdr_addr),
815 elf_phent_size: phentsize_u16,
816 elf_phnum: phnum_u16,
817 })
818 }
819
820 // siehe trait
821 /// Limits lifting to file-backed executable bytes so data and BSS are not decoded as code.
822 pub fn exec_region_containing(&self, pc: u64) -> Option<(u64, &[u8])> {
823 self.segments.iter().find_map(|segment| {
824 // Checked addition rejects a malformed segment whose file range wraps.
825 let end = segment.vaddr.checked_add(segment.file_size)?;
826
827 if segment.perms.contains(PagePerms::EXEC) && pc >= segment.vaddr && pc < end {
828 Some((segment.vaddr, segment.bytes.as_slice()))
829 } else {
830 None
831 }
832 })
833 }
834
835 /// Finds one symbol by its exact ELF name for entry and register setup.
836 pub fn find_symbol(&self, name: &str) -> Option<&ProgramSymbol> {
837 self.symbols.get(name)
838 }
839
840 /// Finds the closest defined symbol at or below a mapped guest address.
841 pub fn symbol_at(&self, address: u64) -> Option<&ProgramSymbol> {
842 // Reject unmapped addresses before selecting the nearest preceding symbol.
843 if !self.segments.iter().any(|segment| {
844 address >= segment.vaddr && address < segment.vaddr.saturating_add(segment.mem_size)
845 }) {
846 return None;
847 }
848 // A symbol size can be zero or missing. Use the nearest preceding value so the
849 // debugger can still label addresses inside assembler and linker symbols.
850 self.symbols
851 .values()
852 .filter(|symbol| symbol.is_defined() && symbol.value <= address)
853 .max_by_key(|symbol| symbol.value)
854 }
855
856 /// Borrows sorted jump-slot imports without exposing the owned vector for changes.
857 pub fn imports(&self) -> &[ImportedSymbol] {
858 &self.imports
859 }
860}
861
862struct GuestFile {
863 // The host file supplies storage, but the descriptor number and flags belong to the guest.
864 file: File,
865 // Keep `FD_CLOEXEC` outside the host file because only a guest `exec` would use it.
866 fd_flags: u64,
867}
868
869// todo - fuzz api später - erstmal provisorisch
870struct SnapshotFile {
871 // A cloned handle keeps the open file alive when the active guest closes its descriptor.
872 file: File,
873 // File data is external to guest memory, so save the stream position explicitly.
874 offset: u64,
875 fd_flags: u64,
876}
877
878/// Saves mutable process data while leaving compiled code available for reuse after restore.
879pub struct RuntimeSnapshot {
880 // The baseline includes mappings, bytes, permissions, stack bounds, and heap state.
881 memory: GuestMemory,
882 // Raw ABI pointers are cleared before this value is stored.
883 state: GuestState,
884 // Guest descriptor numbers must remain stable after restore.
885 files: BTreeMap<i32, SnapshotFile>,
886 next_fd: i32,
887 // A restored allocation sequence must start its next search at the same address.
888 mmap_base: u64,
889}
890
891/// Uses a lifting callback so this crate does not depend on a specific guest architecture crate.
892pub struct JitRuntime<L>
893where
894 L: Fn(&GuestMemory, u64) -> JitResult<Nnil>,
895{
896 // Keep parsed executable metadata for symbols, startup data, and debugger queries.
897 program: ProgramImage,
898 // Keep normalized path roots and compiler settings for every later guest operation.
899 process: RemuConfig,
900 // A box gives the MMU owner a stable address while generated code has raw ABI pointers.
901 memory: Box<GuestMemory>,
902
903 // The key is a guest block-entry PC. A tree map keeps keys sorted for ABI dispatch.
904 cache: BTreeMap<u64, CompiledJit>,
905 // This dense mirror has only the C-compatible data that generated code needs.
906 jit_entries: Vec<JitEntryAbi>,
907
908 // Dynamic programs start at the loader entry. `program.entry_pc` stays the main entry.
909 initial_pc: u64,
910 // Zero means a static process. A dynamic process uses the fixed interpreter bias.
911 loader_base: u64,
912 // This is the register and stop-state object shared with native blocks.
913 state: GuestState,
914 // Guest descriptors 0, 1, and 2 map directly to host standard streams. This map starts at 3.
915 files: BTreeMap<i32, GuestFile>,
916 // Monotonic allocation makes the next descriptor stable after snapshot restore.
917 next_fd: i32,
918 // This cursor separates automatic mappings from the growing heap.
919 mmap_base: u64,
920 // Generated source and compiler products go here for inspection and loading.
921 jit_output_dir: PathBuf,
922 // Keep diagnostic work out of the fast path unless the caller requests it.
923 verbose: bool,
924
925 // todo - mehr verbose stats später
926 // todo - fuzz logs?
927 // These counters measure JIT cost but do not affect guest-visible state.
928 compile_count: usize,
929 compile_time: Duration,
930 execute_time: Duration,
931 // Boxes keep raw control pointers stable even when the runtime value moves.
932 control: Box<GuestControl>,
933 // The `Arc` keeps the atomic flag alive while another thread requests a pause.
934 pause: Arc<AtomicBool>,
935 // `None` is used only during construction and while `reset` moves the value out.
936 initial_snapshot: Option<RuntimeSnapshot>,
937
938 // Eight words hold syscall number, PC, and argument registers a0 through a5.
939 last_syscall: Option<[u64; 8]>,
940 // The callback changes guest bytes at one PC into one NNIL region.
941 lift_region: L,
942}
943
944impl<L> JitRuntime<L>
945where
946 L: Fn(&GuestMemory, u64) -> JitResult<Nnil>,
947{
948 /// Creates a runtime with default guest-process settings and the local output directory.
949 pub fn new(program: ProgramImage, lift_region: L) -> JitResult<Self> {
950 // todo - default path
951 Self::init_runtime(
952 program,
953 RemuConfig::default(),
954 Path::new("output").to_path_buf(),
955 lift_region,
956 )
957 }
958
959 /// Maps the program and optional interpreter, then builds Linux process startup state.
960 pub fn init_runtime(
961 program: ProgramImage,
962 mut process: RemuConfig,
963 jit_output_dir: PathBuf,
964 lift_region: L,
965 ) -> JitResult<Self> {
966 // todo - doku schreiben
967 // Reject unsupported settings before files, mappings, or snapshots are created.
968 if process.jit_opt_level > 2 {
969 return Err(io::Error::other("JIT optimization level ist entweder 0, 1 oder 2").into());
970 }
971 // `cfg!` is a compile-time host check. Raw artifacts contain x86-64 Linux details,
972 // so another host cannot safely load them.
973 if process.raw_jit && !cfg!(all(target_os = "linux", target_arch = "x86_64")) {
974 return Err(io::Error::other("--raw-jit läuft nur auf Linux x86-64!!").into());
975 }
976 if let Some(cache_dir) = &process.jit_cache_dir {
977 fs::create_dir_all(cache_dir)?;
978 }
979
980 // Map the main executable before the interpreter so overlap checks include it.
981 let mut memory = Box::new(GuestMemory::from_program(&program)?);
982
983 // Canonical roots make later path checks reliable.
984 if let Some(sysroot) = &process.sysroot {
985 process.sysroot = Some(fs::canonicalize(sysroot).map_err(|err| {
986 io::Error::other(format!(
987 "cannot open sysroot in {}: {err}",
988 sysroot.display()
989 ))
990 })?);
991 }
992
993 // todo - workdir wirklich gebraucht?
994 // Use the host current directory only when the caller gives no writable guest root.
995 let workdir = process.workdir.clone().unwrap_or(std::env::current_dir()?);
996 let workdir = fs::canonicalize(&workdir).map_err(|err| {
997 io::Error::other(format!(
998 "cannot open workdir in {}: {err}",
999 workdir.display()
1000 ))
1001 })?;
1002 if !workdir.is_dir() {
1003 return Err(io::Error::other("workdir is not a directory").into());
1004 }
1005 process.workdir = Some(workdir);
1006
1007 // dbg!(process);
1008
1009 let mut loader_base = 0;
1010 let mut initial_pc = program.entry_pc;
1011
1012 if let Some(interp) = &program.interp {
1013 // `PT_INTERP` makes this a dynamic process. The named file is a guest absolute
1014 // path, so it must come from the configured sysroot and not the host root.
1015 let sysroot = process.sysroot.as_ref().ok_or_else(|| {
1016 io::Error::other(format!(
1017 "WICHTIG - dynamic ELF requires --sysroot containing {interp}"
1018 ))
1019 })?;
1020
1021 let loader_path = resolve_guest_path(sysroot, interp)?;
1022 let loader_bytes = fs::read(&loader_path).map_err(|err| {
1023 io::Error::other(format!(
1024 "cannot read guest interpreter {}: {err}",
1025 loader_path.display()
1026 ))
1027 })?;
1028 // Apply the same ELF checks to the loader because it will execute as guest code.
1029 let loader = ProgramImage::from_elf_bytes(&loader_bytes)?;
1030
1031 // if loader.elf_type != 3 {
1032 // return Err(io::Error::other("ist nich ET_DYN").into());
1033 // }
1034
1035 // A fixed bias keeps loader addresses stable across repeat fuzzing runs.
1036 loader_base = DYNAMIC_LOADER_BIAS;
1037
1038 // Check the loader pages first to avoid a partially mapped interpreter.
1039 // This arithmetic assumes that each biased segment end and rounded end fit in u64.
1040 for segment in &loader.segments {
1041 if segment.mem_size == 0 {
1042 continue;
1043 }
1044
1045 // Segment addresses and sizes need not be page-aligned. Check every host-style
1046 // mapping page from the rounded-down start to the rounded-up end.
1047 let start = align_down(loader_base + segment.vaddr, PAGE_SIZE as u64);
1048 let end = align_up(
1049 loader_base + segment.vaddr + segment.mem_size,
1050 PAGE_SIZE as u64,
1051 );
1052
1053 if !memory.range_is_free(start, end - start) {
1054 return Err(io::Error::other("??").into());
1055 }
1056 }
1057
1058 // Add the bias to every loader segment and to its entry PC. The main executable
1059 // retains its original addresses for AT_ENTRY and AT_PHDR.
1060 memory.map_program_at(&loader, loader_base)?;
1061 initial_pc = loader_base
1062 .checked_add(loader.entry_pc)
1063 .ok_or_else(|| io::Error::other("interpreter entry error"))?;
1064 }
1065
1066 // `Arc` uses shared ownership. The debugger can retain a clone without borrowing
1067 // the complete runtime.
1068 let mut state = GuestState::default();
1069 let pause = Arc::new(AtomicBool::new(false));
1070
1071 // dbg!(state);
1072
1073 // todo -- hier meckert clippy
1074 // Initialize raw pointers only after their boxed owners have stable addresses.
1075 state.memory = memory.as_abi_mut();
1076 // `regs[2]` is RISC-V x2 (`sp`). Use the same 256-byte top reserve as startup setup.
1077 state.regs[2] = memory.stack_top - DEFAULT_STACK_POINTER_OFFSET;
1078
1079 let mut runtime = Self {
1080 program,
1081 process,
1082 memory,
1083 cache: BTreeMap::new(),
1084 jit_entries: Vec::new(),
1085 initial_pc,
1086 loader_base,
1087 state,
1088 files: BTreeMap::new(),
1089 // Linux processes conventionally start with 0, 1, and 2 as standard streams.
1090 // This runtime keeps those descriptors fixed instead of permitting close and reuse.
1091 next_fd: 3,
1092 mmap_base: MMAP_BASE,
1093 jit_output_dir,
1094 verbose: false,
1095
1096 compile_count: 0,
1097 compile_time: Duration::ZERO,
1098 execute_time: Duration::ZERO,
1099
1100 // todo -- anders machen
1101 control: Box::new(GuestControl {
1102 pause: Arc::as_ptr(&pause),
1103 breakpoints: [0; DEBUG_SLOTS],
1104 breakpoint_count: 0,
1105 // Use the no-skip sentinel until execution stops at a breakpoint.
1106 skip_breakpoint: u64::MAX,
1107 blocks_left: 0,
1108 // Zero is false in the generated C-compatible ABI.
1109 limited: 0,
1110 trace: [0; DEBUG_SLOTS],
1111 trace_pos: 0,
1112 }),
1113 pause,
1114 initial_snapshot: None,
1115 last_syscall: None,
1116 lift_region,
1117 };
1118
1119 //runtime.initialize_shim_data()?;
1120 //runtime.install_shims()?;
1121
1122 runtime.initialize_process_state()?;
1123
1124 runtime.state.pc = runtime.initial_pc;
1125 runtime.state.memory = runtime.memory.as_abi_mut();
1126 // The initial snapshot restores guest memory and file-handle state during reset.
1127 // It does not restore external file contents that the guest or another process changes.
1128 runtime.initial_snapshot = Some(runtime.snapshot()?);
1129
1130 Ok(runtime)
1131 }
1132
1133 /// Returns the number of guest entry PCs that have native code.
1134 pub fn cache_len(&self) -> usize {
1135 self.cache.len()
1136 }
1137
1138 /// Borrows parsed program data without giving permission to change runtime mappings.
1139 pub fn program(&self) -> &ProgramImage {
1140 &self.program
1141 }
1142
1143 /// Borrows guest registers and stop data for inspection.
1144 pub fn state(&self) -> &GuestState {
1145 &self.state
1146 }
1147
1148 /// Gives controlled callers direct access to guest registers and the PC.
1149 pub fn state_mut(&mut self) -> &mut GuestState {
1150 &mut self.state
1151 }
1152
1153 /// Borrows guest memory for reads and mapping inspection.
1154 pub fn memory(&self) -> &GuestMemory {
1155 &self.memory
1156 }
1157
1158 /// Borrows guest memory for changes and first refreshes its generated-code view.
1159 pub fn memory_mut(&mut self) -> &mut GuestMemory {
1160 // Refresh the ABI first because a previous mapping change can make its pointers stale.
1161 self.state.memory = self.memory.as_abi_mut();
1162 &mut self.memory
1163 }
1164
1165 /// Changes both reset entry and current execution entry to keep them consistent.
1166 pub fn set_entry_pc(&mut self, pc: u64) {
1167 self.initial_pc = pc;
1168 self.state.pc = pc;
1169 }
1170
1171 /// Enables compiler, dispatch, and syscall diagnostics for manual investigation.
1172 pub fn set_verbose(&mut self, verbose: bool) {
1173 self.verbose = verbose;
1174 }
1175
1176 /// Exposes the normalized roots and JIT options that this runtime uses.
1177 pub fn process_config(&self) -> &RemuConfig {
1178 &self.process
1179 }
1180
1181 /// Clones the shared pause handle so another thread can stop generated code safely.
1182 pub fn pause_flag(&self) -> Arc<AtomicBool> {
1183 Arc::clone(&self.pause)
1184 }
1185
1186 /// Adds one guest block-entry address to the generated-code breakpoint table.
1187 pub fn add_breakpoint(&mut self, address: u64) -> Result<(), &'static str> {
1188 // The fixed generated-code ABI has DEBUG_SLOTS entries. This method does not report
1189 // a full table, so callers must not add more than DEBUG_SLOTS different addresses.
1190 let mut values = self.control.breakpoints[..self.control.breakpoint_count].to_vec();
1191
1192 if values.contains(&address) {
1193 return Ok(());
1194 }
1195
1196 // Generated code uses binary search, so the shared breakpoint array must stay sorted.
1197 values.push(address);
1198 values.sort_unstable();
1199
1200 self.control.breakpoint_count = values.len();
1201 self.control.breakpoints[..values.len()].copy_from_slice(&values);
1202 Ok(())
1203 }
1204
1205 /// Removes all copies of an address and keeps the active prefix compact.
1206 pub fn remove_breakpoint(&mut self, address: u64) {
1207 let mut values = self.control.breakpoints[..self.control.breakpoint_count].to_vec();
1208 values.retain(|value| *value != address);
1209 self.control.breakpoint_count = values.len();
1210 self.control.breakpoints[..values.len()].copy_from_slice(&values);
1211 }
1212
1213 /// Hides all saved array values by setting the active count to zero.
1214 pub fn clear_breakpoints(&mut self) {
1215 self.control.breakpoint_count = 0;
1216 }
1217
1218 /// Returns only the active sorted prefix, not unused fixed-array slots.
1219 pub fn breakpoints(&self) -> &[u64] {
1220 &self.control.breakpoints[..self.control.breakpoint_count]
1221 }
1222
1223 /// Returns the retained block-entry trace in execution order for debugger output.
1224 pub fn trace(&self) -> Vec<u64> {
1225 // Reorder the ring buffer so callers always receive entries from oldest to newest.
1226 // `min(DEBUG_SLOTS)` limits output to the array capacity. Subtraction finds the oldest
1227 // retained sequence number. Modulo DEBUG_SLOTS maps each sequence number to its slot.
1228 let count = self.control.trace_pos.min(DEBUG_SLOTS);
1229 let start = self.control.trace_pos.saturating_sub(count);
1230 (start..self.control.trace_pos)
1231 .map(|index| self.control.trace[index % DEBUG_SLOTS])
1232 .collect()
1233 }
1234
1235 /// Returns the last syscall register record for debugger and fuzzer diagnostics.
1236 pub fn last_syscall(&self) -> Option<[u64; 8]> {
1237 self.last_syscall
1238 }
1239
1240 /// Runs the architecture callback without compiling or changing the JIT cache.
1241 pub fn lift_at(&self, pc: u64) -> JitResult<Nnil> {
1242 (self.lift_region)(&self.memory, pc)
1243 }
1244
1245 /// Captures guest-visible mutable state so one runtime can replay from this point.
1246 pub fn snapshot(&mut self) -> JitResult<RuntimeSnapshot> {
1247 let mut files = BTreeMap::new();
1248 for (&fd, file) in &mut self.files {
1249 // `try_clone` keeps the same underlying open file available in the snapshot.
1250 // Save the current offset because the guest can change it with read or seek.
1251 files.insert(
1252 fd,
1253 SnapshotFile {
1254 file: file.file.try_clone()?,
1255 offset: file.file.stream_position()?,
1256 fd_flags: file.fd_flags,
1257 },
1258 );
1259 }
1260
1261 // Raw ABI pointers belong to this runtime and cannot be copied into a saved snapshot.
1262 let mut state = self.state.clone();
1263
1264 state.memory = std::ptr::null_mut();
1265 state.control = std::ptr::null_mut();
1266 state.jit_entries = std::ptr::null();
1267 state.jit_entry_count = 0;
1268
1269 // Start dirty tracking after the full baseline copy for fast later restores.
1270 let memory = (*self.memory).clone();
1271 self.memory.clear_dirty();
1272
1273 // dbg!(state);
1274
1275 Ok(RuntimeSnapshot {
1276 memory,
1277 state,
1278 files,
1279 next_fd: self.next_fd,
1280 mmap_base: self.mmap_base,
1281 })
1282 }
1283
1284 // todo - optimize
1285 // todo - dirty bit so wie gamozo
1286 /// Restores process data but keeps compiled native blocks for fast replay.
1287 pub fn restore(&mut self, snapshot: &RuntimeSnapshot) -> JitResult<()> {
1288 let mut files = BTreeMap::new();
1289
1290 // Clone file descriptions and restore offsets so guest I/O resumes at the same position.
1291 for (&fd, saved) in &snapshot.files {
1292 let mut file = saved.file.try_clone()?;
1293 file.seek(SeekFrom::Start(saved.offset))?;
1294 files.insert(
1295 fd,
1296 GuestFile {
1297 file,
1298 fd_flags: saved.fd_flags,
1299 },
1300 );
1301 }
1302
1303 // Use dirty pages when layouts match; otherwise rebuild all memory and ABI pointers.
1304 if !self.memory.restore_dirty_from(&snapshot.memory) {
1305 *self.memory = snapshot.memory.clone();
1306 }
1307
1308 self.state = snapshot.state.clone();
1309 self.state.memory = self.memory.as_abi_mut();
1310
1311 // Do not resume old debugger limits or pointers after a restore.
1312 self.state.control = std::ptr::null_mut();
1313 self.sync_jit_entries();
1314 self.files = files;
1315 self.next_fd = snapshot.next_fd;
1316
1317 // self.sync_jit_entries();
1318
1319 self.mmap_base = snapshot.mmap_base;
1320 self.last_syscall = None;
1321 Ok(())
1322 }
1323
1324 // todo -- siehe restore
1325 /// Restores the snapshot that was made after initial process setup.
1326 pub fn reset(&mut self) -> JitResult<()> {
1327 // Rust cannot mutably borrow `self` for `restore` while it also borrows a field
1328 // from `self`. `take` moves the snapshot out, then the code puts it back.
1329 let snapshot = self
1330 .initial_snapshot
1331 .take()
1332 .ok_or_else(|| io::Error::other(" snapshot missing"))?;
1333
1334 // Put the snapshot back even when restore reports an error, so a later reset can retry.
1335 let result = self.restore(&snapshot);
1336 self.initial_snapshot = Some(snapshot);
1337 result
1338 }
1339
1340 /*
1341 // todo - brauchen wir die alle hier wirklich?
1342
1343 let nnil = (self.lift_region)(&self.program, self.state.pc)?;
1344 let compiled = compile_jit(&nnil, &self.jit_output_dir)?;
1345
1346 if self.verbose {
1347 println!(
1348 "jit-cache-miss: neu compiled für pc=0x{:x}: {} {}",
1349 self.state.pc,
1350 compiled.source_path().display(),
1351 compiled.library_path().display(),
1352 );
1353 }
1354
1355 self.cache.insert(self.state.pc, compiled);
1356 } else {
1357 if self.verbose {
1358 println!(
1359 "jit-cache-hit für pc=0x{:x}",
1360 self.state.pc,
1361 );
1362 } */
1363
1364 /// Compiles a linear walk from the entry to reduce lazy JIT stops during startup.
1365 pub fn precompile(&mut self) -> JitResult<usize> {
1366 let start_pc = self.initial_pc;
1367
1368 let segment_end = self.memory.executable_end(start_pc).ok_or_else(|| {
1369 io::Error::other(format!("entry pc 0x{start_pc:x} is not executable"))
1370 })?;
1371
1372 // https://ratatui.rs/recipes/apps/panic-hooks/
1373
1374 // Hide expected decoder panics while the best-effort walk probes unknown code.
1375 // Rust panic hooks are process-global. A concurrent panic can also be hidden, and
1376 // restoring this saved hook can replace a hook that another thread installed.
1377 let old_panic_hook = take_hook();
1378 set_hook(Box::new(|_| {}));
1379
1380 let mut pc = start_pc;
1381 let mut regions = Vec::new();
1382
1383 // Walk linearly to get broad startup coverage without requiring a complete control-flow graph.
1384 while pc < segment_end {
1385 // The decoder can panic on unsupported bytes. Catch that panic because this
1386 // optional walk must not terminate the emulator. The callback only borrows memory.
1387 let attempt = catch_unwind(AssertUnwindSafe(|| (self.lift_region)(&self.memory, pc)));
1388
1389 match attempt {
1390 Ok(Ok(nnil)) => {
1391 let next_pc = nnil.pc();
1392
1393 // dbg!(nnil);
1394
1395 // Every lifted region must advance. This check prevents an infinite walk.
1396 if next_pc <= pc {
1397 if self.verbose {
1398 println!(
1399 "aot-stop: entry walk kein progress bei pc=0x{:x} next=0x{:x}",
1400 pc, next_pc
1401 );
1402 }
1403 break;
1404 }
1405
1406 regions.push(nnil);
1407
1408 if next_pc >= segment_end {
1409 break;
1410 }
1411
1412 pc = next_pc;
1413 }
1414
1415 Ok(Err(err)) => {
1416 if self.verbose {
1417 println!("aot-skip: entry walk stopped at pc=0x{:x}: {}", pc, err);
1418 }
1419 break;
1420 }
1421
1422 Err(_) => {
1423 if self.verbose {
1424 println!("aot-skip: entry walk panic pc=0x{:x}", pc);
1425 }
1426 break;
1427 }
1428 }
1429 }
1430
1431 // Restore the process-wide hook so later real panics remain visible.
1432 set_hook(old_panic_hook);
1433
1434 if regions.is_empty() {
1435 return Ok(0);
1436 }
1437
1438 // One bundle reduces rustc start cost for all regions found by the walk.
1439 let compiled_regions = match compile_jit_multi(
1440 ®ions,
1441 &self.jit_output_dir,
1442 "generated-aot",
1443 self.process.jit_opt_level,
1444 self.process.jit_cache_dir.as_deref(),
1445 self.process.raw_jit,
1446 ) {
1447
1448 Ok(compiled_regions) => compiled_regions,
1449 Err(err) => {
1450 if self.verbose {
1451 println!("aot-bundle-stop: failed to compile start batch: {}", err);
1452 }
1453 return Ok(0);
1454 }
1455 };
1456
1457 // dbg!(compiled_regions);
1458
1459 if self.verbose
1460 && let Some((_, first)) = compiled_regions.first()
1461 {
1462 println!(
1463 "aot-bundle: compiled {} regions into {} {}",
1464 compiled_regions.len(),
1465 first.source_path().display(),
1466 first.library_path().display(),
1467 );
1468 }
1469
1470 let compiled_count = compiled_regions.len();
1471
1472 // Each returned handle shares the loaded bundle but has its own guest entry function.
1473 for (entry_pc, compiled) in compiled_regions {
1474 self.cache.insert(entry_pc, compiled);
1475 }
1476 self.sync_jit_entries();
1477
1478 Ok(compiled_count)
1479 }
1480
1481 // todo -- schnitstelle nach "außen"
1482 // todo -- hashing
1483 // todo -- resetting
1484 /// Runs until the guest exits, faults, makes an unsupported call, or requests a stop.
1485 pub fn run(&mut self) -> JitResult<ExitReason> {
1486 // A null control pointer removes debugger checks from an unrestricted run.
1487 self.state.control = std::ptr::null_mut();
1488 self.run_inner()
1489 }
1490
1491 /// Runs at most `limit` translated blocks so a debugger can step or stay responsive.
1492 pub fn run_for_blocks(&mut self, limit: u64) -> JitResult<ExitReason> {
1493 // One block is one translated basic block, not one RISC-V instruction.
1494 self.control.blocks_left = limit;
1495
1496 // One is true in this byte-sized ABI flag, so generated blocks use the limit.
1497 self.control.limited = 1;
1498
1499 // Skip the current breakpoint once so continue and step can make progress.
1500 self.control.skip_breakpoint = if self.state.exit_reason == ExitReason::Breakpoint {
1501 self.state.pc
1502 } else {
1503 u64::MAX
1504 };
1505
1506 self.state.control = &mut *self.control;
1507 let result = self.run_inner();
1508
1509 // dbg!(self.state);
1510
1511 self.state.control = std::ptr::null_mut();
1512 self.control.limited = 0;
1513 // The pause flag is only a stop request. It does not protect other memory, so relaxed
1514 // atomic ordering is sufficient and avoids an unnecessary synchronization barrier.
1515 self.pause.store(false, Ordering::Relaxed);
1516 result
1517 }
1518
1519 fn run_inner(&mut self) -> JitResult<ExitReason> {
1520 // PC zero is the runtime's not-started sentinel. Replace it with the selected entry.
1521 // This convention prevents this path from starting or resuming a real block at address zero.
1522 if self.state.pc == 0 {
1523 self.state.pc = self.initial_pc;
1524 }
1525
1526 // Each native exit returns here for lazy compilation, syscalls, or user control.
1527 loop {
1528 self.state.memory = self.memory.as_abi_mut();
1529 self.state.exit_reason = ExitReason::None;
1530
1531 // Remove details from the previous exit. A caller must not mistake an old
1532 // fault address for the cause of a later syscall or debugger stop.
1533 self.state.fault_addr = 0;
1534 self.state.fault_access = AccessKind::None;
1535
1536 if self.verbose {
1537 println!("neuer pc=0x{:x}", self.state.pc)
1538 }
1539
1540 // todo - fault memory läuft noch nicht ganz
1541 if !self.memory.can_execute(self.state.pc) {
1542 self.state.exit_reason = ExitReason::ExecFault;
1543 self.state.fault_addr = self.state.pc;
1544 self.state.fault_access = AccessKind::Execute;
1545
1546
1547 return Ok(self.finished(ExitReason::ExecFault));
1548 }
1549
1550 // Compile only on first entry so unused guest regions do not pay JIT cost.
1551 if !self.cache.contains_key(&self.state.pc) {
1552 let _ = self.compile_region(self.state.pc)?;
1553 } else if self.verbose {
1554 println!("jit-cache-hit für pc=0x{:x}", self.state.pc);
1555 }
1556
1557 // todo -- raw_jit siehe oben
1558 let compiled = self
1559 .cache
1560 .get(&self.state.pc)
1561 .ok_or_else(|| io::Error::other("cache fehler"))?;
1562
1563 // Calling native code is unsafe because the library receives a raw pointer.
1564 // `CompiledJit` keeps the library loaded for the full call.
1565 let exit_reason = if self.verbose {
1566 let started = Instant::now();
1567 let reason = unsafe { compiled.run(&mut self.state) };
1568 self.execute_time += started.elapsed();
1569 reason
1570 } else {
1571 unsafe { compiled.run(&mut self.state) }
1572 };
1573
1574 // Internal dispatch exits resume automatically. Caller-visible stops return to the caller.
1575 match exit_reason {
1576 ExitReason::IndirectBranch => continue,
1577 ExitReason::Syscall => {
1578 if self.verbose {
1579 self.log_syscall();
1580 }
1581 if self.handle_syscall() {
1582 continue;
1583 }
1584 if self.state.exit_reason == ExitReason::GuestExit {
1585 return Ok(self.finished(ExitReason::GuestExit));
1586 }
1587 return Ok(self.finished(ExitReason::Syscall));
1588 }
1589 other => return Ok(self.finished(other)),
1590 }
1591 }
1592 }
1593
1594 // todo - schöne logs für benchmarks
1595 fn finished(&self, reason: ExitReason) -> ExitReason {
1596 if self.verbose {
1597 println!(
1598 "jit stats: {} rustc calls, {:.2?} compiling, {:.2?} executing",
1599 self.compile_count, self.compile_time, self.execute_time
1600 );
1601 }
1602 reason
1603 }
1604
1605 // todo -- refactor vereinfachen
1606 // Return `(compiled, next_pc)` so callers can distinguish a cache hit and continue a walk.
1607 fn compile_region(&mut self, pc: u64) -> JitResult<(bool, u64)> {
1608 if self.cache.contains_key(&pc) {
1609 /* if self.verbose {
1610 println!(
1611 "jit-cache-hit: für pc=0x{:x}",
1612 pc,
1613 );
1614 } */
1615 return Ok((false, pc));
1616 }
1617
1618 // A bounded batch improves cross-region branches without making one source file unlimited.
1619 // The limit of 128 is a project tradeoff. It gives rustc more cross-block context
1620 // and caps region count and source size for one cache miss. It cannot bound rustc time.
1621 let regions = self.collect_lazy_batch(pc, 128)?;
1622 // Collection reports an error if it cannot lift the required first region.
1623 // Thus, index zero is the requested block and is safe to use as the fallback.
1624 let next_pc = regions[0].pc();
1625
1626 // Use the simpler single-entry artifact path when no useful neighbor was found.
1627 if regions.len() == 1 {
1628 let started = Instant::now();
1629
1630 let compiled = compile_jit(
1631 ®ions[0],
1632 &self.jit_output_dir,
1633 self.process.jit_opt_level,
1634 self.process.jit_cache_dir.as_deref(),
1635 self.process.raw_jit,
1636 )?;
1637
1638 if compiled.compiled_now() {
1639 self.compile_count += 1;
1640 self.compile_time += started.elapsed();
1641 }
1642
1643 if self.verbose {
1644 println!(
1645 "jit-cache-{} für pc=0x{:x}: {} {}",
1646 if compiled.compiled_now() {
1647 "miss: compiled"
1648 } else {
1649 "disk-hit!"
1650 },
1651 pc,
1652 compiled.source_path().display(),
1653 compiled.library_path().display(),
1654 );
1655 }
1656
1657 self.cache.insert(pc, compiled);
1658 self.sync_jit_entries();
1659
1660 return Ok((true, next_pc));
1661 }
1662
1663 let started = Instant::now();
1664
1665 // The result keeps compiled handles with one shared-artifact cache flag because
1666 // statistics and diagnostics need both parts.
1667 let (compiled_regions, compiled_now) = match compile_jit_multi(
1668 ®ions,
1669 &self.jit_output_dir,
1670 "generated-batch",
1671 self.process.jit_opt_level,
1672 self.process.jit_cache_dir.as_deref(),
1673 self.process.raw_jit,
1674 ) {
1675 Ok(compiled_regions) => {
1676 // All entries come from one artifact. Its first handle therefore reports
1677 // whether rustc built that common artifact now.
1678 let compiled_now = compiled_regions
1679 .first()
1680 .is_some_and(|(_, compiled)| compiled.compiled_now());
1681
1682 if compiled_now {
1683 self.compile_count += 1;
1684 self.compile_time += started.elapsed();
1685 }
1686 (compiled_regions, compiled_now)
1687 }
1688
1689 Err(err) => {
1690 // Keep execution available when one neighbor makes the batch invalid.
1691 self.compile_count += 1;
1692 self.compile_time += started.elapsed();
1693 if self.verbose {
1694 println!(
1695 "jit-batch-stop: fallback to single region bei pc=0x{:x}: {}",
1696 pc, err
1697 );
1698 }
1699
1700 let started = Instant::now();
1701
1702 /*
1703 * self.sync_jit_entries();
1704 */
1705
1706 let compiled = compile_jit(
1707 ®ions[0],
1708 &self.jit_output_dir,
1709 self.process.jit_opt_level,
1710 self.process.jit_cache_dir.as_deref(),
1711 self.process.raw_jit,
1712 )?;
1713
1714 if compiled.compiled_now() {
1715 self.compile_count += 1;
1716 self.compile_time += started.elapsed();
1717 }
1718
1719 if self.verbose {
1720 println!(
1721 "jit-cache-{} für pc=0x{:x}: {} {}",
1722 if compiled.compiled_now() {
1723 "miss: compiled"
1724 } else {
1725 "disk-hit !"
1726 },
1727 pc,
1728 compiled.source_path().display(),
1729 compiled.library_path().display(),
1730 );
1731 }
1732
1733 // dbg!(self.cache);
1734
1735 self.cache.insert(pc, compiled);
1736 self.sync_jit_entries();
1737 return Ok((true, next_pc));
1738 }
1739 };
1740
1741 // dbg!(compiled_regions);
1742
1743 if self.verbose
1744 && let Some((_, first)) = compiled_regions.first()
1745 {
1746 println!(
1747 "jit-cache-batch-{}: {} regions ab pc=0x{:x}: {} {}",
1748 if compiled_now { "compiled" } else { "disk-hit !" },
1749 compiled_regions.len(),
1750 pc,
1751 first.source_path().display(),
1752 first.library_path().display(),
1753 );
1754 }
1755
1756 for (entry_pc, compiled) in compiled_regions {
1757 self.cache.insert(entry_pc, compiled);
1758 }
1759 self.sync_jit_entries();
1760
1761 Ok((true, next_pc))
1762 }
1763
1764 // todo -- code dupliction
1765 // Collect direct control-flow neighbors so one rustc call can link their tail calls.
1766 fn collect_lazy_batch(&self, start_pc: u64, max_regions: usize) -> JitResult<Vec<Nnil>> {
1767 self.memory.executable_end(start_pc).ok_or_else(|| {
1768 io::Error::other(format!("entry pc 0x{start_pc:x} is not executable! ende"))
1769 })?;
1770
1771 // Breadth-first order favors nearby direct successors and gives stable bundle content.
1772 let mut queue = VecDeque::from([start_pc]);
1773 let mut seen = BTreeSet::new();
1774 let mut regions = Vec::new();
1775
1776 // Stop when the queue is empty or the project batch limit is reached. Thus, one
1777 // cache miss cannot grow the compilation bundle without a limit.
1778 while let Some(pc) = queue.pop_front()
1779 && regions.len() < max_regions
1780 {
1781 // `insert` returns false for a duplicate. Short-circuit OR then skips that PC
1782 // without a second tree lookup. Non-executable targets are also unsafe to lift.
1783 if !seen.insert(pc) || !self.memory.can_execute(pc) {
1784 continue;
1785 }
1786
1787 // todo -- refactor vereinfachen
1788
1789 // A bad optional successor must not prevent compilation of the required start region.
1790 // The hook is process-global. Concurrent panics can be hidden, and restoring the
1791 // saved value can replace a hook that another thread installed.
1792 let old_panic_hook = take_hook();
1793 set_hook(Box::new(|_| {}));
1794 let lifted = catch_unwind(AssertUnwindSafe(|| (self.lift_region)(&self.memory, pc)));
1795 set_hook(old_panic_hook);
1796
1797 // Failure at an optional neighbor is harmless. Failure at the requested start
1798 // address must be reported because execution cannot make progress without it.
1799 let nnil = match lifted {
1800 Ok(Ok(nnil)) => nnil,
1801 Ok(Err(_)) if pc != start_pc || !regions.is_empty() => continue,
1802 Ok(Err(err)) => return Err(err),
1803 Err(payload) => {
1804 if pc != start_pc || !regions.is_empty() {
1805 continue;
1806 }
1807
1808 // Rust panic payloads are usually `&str` or `String`. Downcast without
1809 // taking ownership so the diagnostic can cover both common forms.
1810 let message = payload
1811 .downcast_ref::<&str>()
1812 .copied()
1813 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
1814 .unwrap_or("unknown panic");
1815
1816 // Show four executable bytes for context. This can include a second compressed
1817 // instruction. If four bytes are unavailable, show the first 16-bit parcel.
1818 let raw = self
1819 .memory
1820 .fetch_u32(pc)
1821 .map(|value| format!("0x{value:08x}"))
1822 .or_else(|| {
1823 self.memory
1824 .fetch_u16(pc)
1825 .map(|value| format!("0x{value:04x}"))
1826 })
1827 .unwrap_or_else(|| "unreadable".to_string());
1828
1829 return Err(io::Error::other(format!(
1830 "lifter panic bei guest pc 0x{pc:x}, instruction {raw}: {message}"
1831 ))
1832 .into());
1833 }
1834 };
1835
1836 let next_pc = nnil.pc();
1837
1838 if next_pc <= pc {
1839 return Err(io::Error::other(format!(
1840 "compiling pc 0x{pc:x} kein progress (next pc 0x{next_pc:x})"
1841 ))
1842 .into());
1843 }
1844
1845 // Queue only static targets because indirect targets are unknown until runtime.
1846 for successor in direct_successors(&nnil) {
1847 if !seen.contains(&successor) {
1848 queue.push_back(successor);
1849 }
1850 }
1851
1852 if !self.cache.contains_key(&pc) {
1853 regions.push(nnil);
1854 }
1855 }
1856
1857 Ok(regions)
1858 }
1859
1860 fn log_syscall(&self) {
1861 // The RV64 Linux ABI puts the syscall number in x17 (`a7`). Arguments use
1862 // x10 through x15 (`a0` through `a5`), and x2 is the stack pointer.
1863 // Source: https://man7.org/linux/man-pages/man2/syscall.2.html
1864 let nr = self.state.regs[17];
1865
1866 println!(
1867 "achtung syscall {} - pc=0x{:x} a0=0x{:x} a1=0x{:x} a2=0x{:x} a3=0x{:x} a4=0x{:x} a5=0x{:x} sp=0x{:x}",
1868 nr,
1869 self.state.pc,
1870 self.state.regs[10],
1871 self.state.regs[11],
1872 self.state.regs[12],
1873 self.state.regs[13],
1874 self.state.regs[14],
1875 self.state.regs[15],
1876 self.state.regs[2],
1877 );
1878 }
1879
1880 // todo - fuzz api
1881 // todo -- refactor, eigene datei
1882 fn handle_syscall(&mut self) -> bool {
1883 // Keep the entry registers before a handler changes a0 into the return value.
1884 let nr = self.state.regs[17];
1885 self.last_syscall = Some([
1886 nr,
1887 self.state.pc,
1888 self.state.regs[10],
1889 self.state.regs[11],
1890 self.state.regs[12],
1891 self.state.regs[13],
1892 self.state.regs[14],
1893 self.state.regs[15],
1894 ]);
1895
1896 // dbg!(self.state);
1897
1898 // Handlers use positive Linux errno numbers internally. The values used below are:
1899 // 2=ENOENT, 5=EIO, 9=EBADF, 12=ENOMEM, 13=EACCES, 14=EFAULT,
1900 // 17=EEXIST, 22=EINVAL, 25=ENOTTY, 30=EROFS, and 38=ENOSYS.
1901 // Sources:
1902 // https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h
1903 // https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno.h
1904
1905 // https://gpages.juszkiewicz.com.pl/syscalls-table/syscalls.html
1906 // Emulate only the process services needed by current user-mode guests.
1907 // RV64 uses the architecture-neutral Linux syscall table for these numbers.
1908 // Source: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h
1909 // 17=getcwd, 25=fcntl, 29=ioctl, 35=unlinkat, 48=faccessat,
1910 // 56=openat, 57=close, 62=lseek, 63=read, 64=write, 66=writev,
1911 // 67=pread64, 78=readlinkat, 79=newfstatat, and 80=fstat.
1912 let result = match nr {
1913 17 => self.sys_getcwd(),
1914 25 => self.sys_fcntl(),
1915 // No guest ioctl is available. Error 25 is `ENOTTY`, which tells libc that
1916 // this descriptor does not support the requested device operation.
1917 29 => Err(25),
1918 35 => self.sys_unlinkat(),
1919 // Number 439 is `faccessat2`. Routing it to the old path check ignores its fourth
1920 // flags argument. This is an ABI limitation of the current subset.
1921 48 | 439 => self.sys_faccessat(),
1922 56 => self.sys_openat(),
1923 57 => self.sys_close(),
1924 62 => self.sys_lseek(),
1925 63 => self.sys_read(),
1926 64 => self.sys_write(),
1927 66 => self.sys_writev(),
1928 67 => self.sys_pread64(),
1929 78 => self.sys_readlinkat(),
1930 79 => self.sys_newfstatat(),
1931 80 => self.sys_fstat(),
1932 // 93 is `exit` and 94 is `exit_group`. This runtime has one guest thread,
1933 // so both calls stop the complete guest process.
1934 93 | 94 => {
1935 self.state.exit_reason = ExitReason::GuestExit;
1936 return false;
1937 }
1938 // 96 is `set_tid_address`. Return virtual thread ID 1 because there is one thread.
1939 // The runtime does not record the x10 clear-child-TID pointer or clear it at exit.
1940 96 => Ok(1),
1941 // Calls 99 and 233 are set_robust_list and madvise; this model ignores both.
1942 // Call 259 is riscv_flush_icache. Returning success without invalidating translated
1943 // blocks is a limitation and can leave stale code after a guest modifies instructions.
1944 99 | 233 | 259 => Ok(0),
1945 // 113=clock_gettime, 123=sched_getaffinity, 160=uname, and 163=getrlimit.
1946 113 => self.sys_clock_gettime(),
1947 123 => self.sys_sched_getaffinity(),
1948 160 => self.sys_uname(),
1949 163 => self.sys_getrlimit(),
1950 // 172=getpid and 178=gettid. Both identify the only virtual task as 1.
1951 172 | 178 => Ok(1),
1952 // 174..177 are getuid, geteuid, getgid, and getegid. A stable unprivileged
1953 // identity matches the IDs in auxiliary-vector and synthetic stat data.
1954 174..=177 => Ok(1000),
1955 // 214=brk, 215=munmap, 222=mmap, and 226=mprotect.
1956 214 => self.sys_brk(),
1957 215 => self.sys_munmap(),
1958 222 => self.sys_mmap(),
1959 226 => self.sys_mprotect(),
1960 // 258 is `riscv_hwprobe`. Error 38 is `ENOSYS`, so libc can use an older
1961 // capability path when detailed CPU data is not available.
1962 // Source: https://github.com/torvalds/linux/blob/master/arch/riscv/kernel/syscalls/syscall.tbl
1963 258 => Err(38),
1964 // 261=prlimit64 and 278=getrandom.
1965 261 => self.sys_prlimit64(),
1966 278 => self.sys_getrandom(),
1967 // 293 is `rseq`. Return `ENOSYS` because this runtime has no guest thread scheduler.
1968 293 => Err(38),
1969 // Return control to the caller when host policy does not define this syscall.
1970 _ => return false,
1971 };
1972
1973 // Linux returns success values or negative errno values in x10 (`a0`).
1974 self.state.regs[10] = result.unwrap_or_else(guest_errno);
1975 self.state.exit_reason = ExitReason::None;
1976 true
1977 }
1978
1979 fn sys_openat(&mut self) -> Result<u64, i32> {
1980 // `openat` receives dirfd in x10, path in x11, flags in x12, and mode in x13.
1981 // This subset uses host creation defaults, so it does not read the mode argument.
1982 // x10 is signed because Linux uses -100 for `AT_FDCWD`. Casting the saved u64
1983 // preserves its two's-complement bit pattern as an i64.
1984 let dirfd = self.state.regs[10] as i64;
1985 // One 4 KiB page is a project limit for path scanning. It bounds work when a guest
1986 // gives an address with no NUL terminator.
1987 let path = self.read_guest_string(self.state.regs[11], 4096)?;
1988 let flags = self.state.regs[12];
1989
1990 // Supporting only AT_FDCWD avoids implementing guest directory-FD state and keeps
1991 // every lookup relative to the configured process roots.
1992 // Source for -100 (`AT_FDCWD`):
1993 // https://github.com/torvalds/linux/blob/master/include/uapi/linux/fcntl.h
1994 if dirfd != -100 {
1995 // Error 22 is `EINVAL`: this emulator does not accept another dirfd form.
1996 return Err(22);
1997 }
1998
1999 // AND with 3 removes all option bits and keeps the two access-mode bits.
2000 let access = flags & O_ACCMODE;
2001
2002 if access > O_RDWR {
2003 // Access value 3 is reserved, so it is not a valid read or write mode.
2004 return Err(22);
2005 }
2006 // Detect normal write access and the listed file-changing options.
2007 let writable = access != 0 || flags & (O_CREAT | O_TRUNC | O_APPEND) != 0;
2008 // This test omits O_EXCL. O_EXCL without O_CREAT can still reach Rust `create_new`
2009 // below, so this subset does not fully enforce the read-only sysroot for that form.
2010 // Absolute paths are intended to stay in the read-only sysroot.
2011 if path.starts_with('/') && writable {
2012 // Error 30 is `EROFS`. This presents the sysroot as a read-only guest file system.
2013 return Err(30);
2014 }
2015
2016 let path = self.resolve_process_path(&path, flags & O_CREAT != 0)?;
2017 let mut options = OpenOptions::new();
2018
2019
2020 // Translate only the supported Linux flags to host `OpenOptions`. Read-only is
2021 // encoded as zero, so `access != O_WRONLY` enables reads for modes 0 and 2.
2022 // Unsupported option bits are ignored instead of rejected. This is an ABI limitation.
2023 options
2024 .read(access != O_WRONLY)
2025 .write(access != 0)
2026 .create(flags & O_CREAT != 0)
2027 .create_new(flags & O_EXCL != 0)
2028 .truncate(flags & O_TRUNC != 0)
2029 .append(flags & O_APPEND != 0);
2030
2031 let file = options.open(path).map_err(host_errno)?;
2032 // Allocate monotonically instead of reusing a closed number. This keeps descriptor
2033 // assignment deterministic across snapshots and avoids a free-list data structure.
2034 let fd = self.next_fd;
2035 self.next_fd += 1;
2036
2037 self.files.insert(
2038 fd,
2039 GuestFile {
2040 file,
2041 // AND tests only Linux bit 19. Store the guest flag even though this runtime
2042 // does not currently replace its process image with guest `exec`.
2043 fd_flags: if flags & O_CLOEXEC != 0 {
2044 FD_CLOEXEC
2045 } else {
2046 0
2047 },
2048 },
2049 );
2050
2051 // dbg!(self.files);
2052
2053 Ok(fd as u64)
2054 }
2055
2056 fn sys_unlinkat(&mut self) -> Result<u64, i32> {
2057 // `unlinkat` receives dirfd in x10, path in x11, and option flags in x12.
2058 // Support only `AT_FDCWD` with flags zero. In particular, flag 0x200
2059 // (`AT_REMOVEDIR`) is not implemented because this function removes files only.
2060 if self.state.regs[10] as i64 != -100 || self.state.regs[12] != 0 {
2061 return Err(22);
2062 }
2063 let path = self.read_guest_string(self.state.regs[11], 4096)?;
2064 // Never remove files from the shared sysroot.
2065 if path.starts_with('/') {
2066 return Err(30);
2067 }
2068 let path = self.resolve_process_path(&path, false)?;
2069 fs::remove_file(path).map_err(host_errno)?;
2070 Ok(0)
2071 }
2072
2073 fn sys_fcntl(&mut self) -> Result<u64, i32> {
2074 // `fcntl` receives the descriptor in x10, command in x11, and command value in x12.
2075 let fd = self.state.regs[10] as i32;
2076 let command = self.state.regs[11];
2077 let argument = self.state.regs[12];
2078 // Error 9 is `EBADF`. Standard streams have no stored guest flag record here.
2079 let file = self.files.get_mut(&fd).ok_or(9)?;
2080
2081 match command {
2082 F_GETFD => Ok(file.fd_flags),
2083 F_SETFD => {
2084 // Keep only bit 0 because Linux defines no other descriptor flag here.
2085 file.fd_flags = argument & FD_CLOEXEC;
2086 Ok(0)
2087 }
2088 _ => Err(22),
2089 }
2090 }
2091
2092 fn sys_close(&mut self) -> Result<u64, i32> {
2093 // `close` receives its descriptor in x10.
2094 let fd = self.state.regs[10] as i32;
2095 // Treat standard streams as always present because the runtime does not own their handles.
2096 if (0..=2).contains(&fd) || self.files.remove(&fd).is_some() {
2097 Ok(0)
2098 } else {
2099 // Error 9 is `EBADF`: the guest number is not open.
2100 Err(9)
2101 }
2102 }
2103
2104 fn sys_read(&mut self) -> Result<u64, i32> {
2105 // `read` receives the descriptor in x10, guest buffer in x11, and byte count in x12.
2106 let fd = self.state.regs[10] as i32;
2107 let addr = self.state.regs[11];
2108 let size = usize::try_from(self.state.regs[12]).map_err(|_| 22)?;
2109 // Read into host-owned memory first. This prevents the host I/O API from receiving
2110 // a raw guest pointer and lets the MMU validate the later copy.
2111 let mut bytes = vec![0; size];
2112
2113 // let size = 256;
2114
2115 // Descriptor 0 is the conventional standard input number.
2116 let count = if fd == 0 {
2117 io::stdin().read(&mut bytes).map_err(host_errno)?
2118 } else {
2119 let file = self.files.get_mut(&fd).ok_or(9)?;
2120 file.file.read(&mut bytes).map_err(host_errno)?
2121 };
2122
2123 // Copy only bytes returned by the host so unread buffer space stays unchanged.
2124 if !self.memory.store_bytes(addr, &bytes[..count]) {
2125 // Error 14 is `EFAULT`: the output guest range is not writable.
2126 return Err(14);
2127 }
2128 Ok(count as u64)
2129 }
2130
2131 fn sys_write(&mut self) -> Result<u64, i32> {
2132 // `write` receives the descriptor in x10, guest buffer in x11, and byte count in x12.
2133 let fd = self.state.regs[10] as i32;
2134 let size = usize::try_from(self.state.regs[12]).map_err(|_| 22)?;
2135 // Copy from guest memory before host I/O so invalid guest pages return `EFAULT`
2136 // instead of causing a host memory fault.
2137 let bytes = self
2138 .memory
2139 .load_bytes(self.state.regs[11], size)
2140 .ok_or(14)?;
2141
2142 let count = self.write_fd(fd, &bytes)?;
2143 Ok(count as u64)
2144 }
2145
2146 fn sys_writev(&mut self) -> Result<u64, i32> {
2147 // `writev` receives the descriptor in x10, iovec address in x11, and count in x12.
2148 let fd = self.state.regs[10] as i32;
2149 let iov = self.state.regs[11];
2150 let count = usize::try_from(self.state.regs[12]).map_err(|_| 22)?;
2151 // Bound allocation and guest-memory work for invalid vector counts.
2152 // Linux defines `UIO_MAXIOV` as 1024, so larger arrays are invalid.
2153 // Source: https://github.com/torvalds/linux/blob/master/include/uapi/linux/uio.h
2154 if count > 1024 {
2155 return Err(22);
2156 }
2157
2158 let mut bytes = Vec::new();
2159 for index in 0..count {
2160 // RV64 `struct iovec` contains an 8-byte pointer followed by an 8-byte size.
2161 // Thus, each record is 16 bytes and record `index` starts at `iov + index * 16`.
2162 let entry = self
2163 .memory
2164 .load_bytes(iov + (index * 16) as u64, 16)
2165 .ok_or(14)?;
2166 // The first byte ranges have known length because `load_bytes` returned 16 bytes.
2167 // Convert each field from guest little-endian order to a host integer.
2168 let addr = u64::from_le_bytes(entry[..8].try_into().unwrap());
2169 let size = u64::from_le_bytes(entry[8..].try_into().unwrap());
2170 let size = usize::try_from(size).map_err(|_| 22)?;
2171 bytes.extend(self.memory.load_bytes(addr, size).ok_or(14)?);
2172 }
2173
2174 Ok(self.write_fd(fd, &bytes)? as u64)
2175 }
2176
2177 // todo
2178 fn write_fd(&mut self, fd: i32, bytes: &[u8]) -> Result<usize, i32> {
2179 // Mark terminal output so emulator diagnostics stay distinct from guest text.
2180 match fd {
2181 // Descriptors 1 and 2 conventionally start as standard output and error.
2182 // This runtime keeps those meanings fixed instead of permitting close and reuse.
2183 1 => write_marked_output(
2184 io::stdout().lock(),
2185 bytes,
2186 "stdout",
2187 // ANSI sequence 1;36 selects bright cyan on a compatible terminal.
2188 "\x1b[1;36m", // juckt
2189 io::stdout().is_terminal(),
2190 ),
2191 2 => write_marked_output(
2192 io::stderr().lock(),
2193 bytes,
2194 "stderr",
2195 // ANSI sequence 1;31 selects bright red for error output.
2196 "\x1b[1;31m",
2197 io::stderr().is_terminal(),
2198 ),
2199 _ => self
2200 .files
2201 .get_mut(&fd)
2202 .ok_or(9)?
2203 .file
2204 .write(bytes)
2205 .map_err(host_errno),
2206 }
2207 }
2208
2209 fn sys_pread64(&mut self) -> Result<u64, i32> {
2210 // `pread64` receives fd, buffer, count, and file offset in x10 through x13.
2211 let fd = self.state.regs[10] as i32;
2212 let addr = self.state.regs[11];
2213 let size = usize::try_from(self.state.regs[12]).map_err(|_| 22)?;
2214 let offset = self.state.regs[13];
2215 let file = self.files.get(&fd).ok_or(9)?;
2216
2217 let mut bytes = vec![0; size];
2218
2219 // Positional reads must not change the file offset saved in runtime snapshots.
2220 let count = file.file.read_at(&mut bytes, offset).map_err(host_errno)?;
2221
2222 if !self.memory.store_bytes(addr, &bytes[..count]) {
2223 return Err(14);
2224 }
2225 Ok(count as u64)
2226 }
2227
2228 fn sys_lseek(&mut self) -> Result<u64, i32> {
2229 // `lseek` receives fd in x10, signed offset in x11, and origin in x12.
2230 let fd = self.state.regs[10] as i32;
2231 let offset = self.state.regs[11] as i64;
2232 let whence = self.state.regs[12];
2233 let file = self.files.get_mut(&fd).ok_or(9)?;
2234 // Linux values 0, 1, and 2 are `SEEK_SET`, `SEEK_CUR`, and `SEEK_END`.
2235 // A start-relative position cannot be negative, but current and end offsets can be.
2236 // Source: https://github.com/torvalds/linux/blob/master/include/uapi/linux/fs.h
2237 let position = match whence {
2238 0 if offset >= 0 => SeekFrom::Start(offset as u64),
2239 0 => return Err(22),
2240 1 => SeekFrom::Current(offset),
2241 2 => SeekFrom::End(offset),
2242 _ => return Err(22),
2243 };
2244
2245 file.file.seek(position).map_err(host_errno)
2246 }
2247
2248 fn sys_faccessat(&self) -> Result<u64, i32> {
2249 // `faccessat` receives dirfd in x10, path in x11, and access mode in x12.
2250 // This subset checks path existence below the selected root. It does not evaluate
2251 // the requested access-mode argument against guest credentials.
2252 if self.state.regs[10] as i64 != -100 {
2253 return Err(22);
2254 }
2255 let path = self.read_guest_string(self.state.regs[11], 4096)?;
2256 self.resolve_process_path(&path, false).map(|_| 0)
2257 }
2258
2259 fn sys_fstat(&mut self) -> Result<u64, i32> {
2260 // `fstat` receives the descriptor in x10 and the guest `stat` address in x11.
2261 let fd = self.state.regs[10] as i32;
2262 // Standard streams have no owned `File`, so write a synthetic character-device record.
2263 let metadata = if (0..=2).contains(&fd) {
2264 None
2265 } else {
2266 Some(
2267 self.files
2268 .get(&fd)
2269 .ok_or(9)?
2270 .file
2271 .metadata()
2272 .map_err(host_errno)?,
2273 )
2274 };
2275 self.write_guest_stat(self.state.regs[11], metadata.as_ref())
2276 }
2277
2278 fn sys_newfstatat(&mut self) -> Result<u64, i32> {
2279 // `newfstatat` receives dirfd, path, output address, and flags in x10 through x13.
2280 // Allow only bit 8, Linux `AT_SYMLINK_NOFOLLOW` (0x100). `flags & !0x100`
2281 // clears that supported bit and detects if any unsupported bit remains.
2282 // Source: https://github.com/torvalds/linux/blob/master/include/uapi/linux/fcntl.h
2283 if self.state.regs[10] as i64 != -100 || self.state.regs[13] & !0x100 != 0 {
2284 return Err(22);
2285 }
2286 let path = self.read_guest_string(self.state.regs[11], 4096)?;
2287 let path = self.resolve_process_path(&path, false)?;
2288 // `symlink_metadata` normally describes the link itself, but path resolution above
2289 // already canonicalizes and follows the final link. NOFOLLOW is not fully implemented.
2290 let metadata = if self.state.regs[13] & 0x100 != 0 {
2291 fs::symlink_metadata(path)
2292 } else {
2293 fs::metadata(path)
2294 }
2295 .map_err(host_errno)?;
2296
2297 self.write_guest_stat(self.state.regs[12], Some(&metadata))
2298
2299 // dbg!(self.state);
2300 }
2301
2302 fn sys_readlinkat(&mut self) -> Result<u64, i32> {
2303 // `readlinkat` receives dirfd, path, output address, and size in x10 through x13.
2304 // This current shortcut ignores dirfd and path. It returns the configured argv[0].
2305 // let path = self.read_guest_string(self.state.regs[11], 4096)?;
2306
2307 // if path != "/proc/self/exe" {
2308 // return Err(2);
2309 // }
2310
2311 let size = usize::try_from(self.state.regs[13]).map_err(|_| 22)?;
2312 // Report argv[0] so `/proc/self/exe` style guest queries see their guest executable name.
2313 let bytes = self
2314 .process
2315 .argv
2316 .first()
2317 .map(String::as_bytes)
2318 .unwrap_or_default();
2319
2320 // `readlinkat` does not append a NUL byte. Copy at most the supplied buffer size
2321 // and return the exact number of bytes that were copied.
2322 let count = size.min(bytes.len());
2323
2324 if !self
2325 .memory
2326 .store_bytes(self.state.regs[12], &bytes[..count])
2327 {
2328 return Err(14);
2329 }
2330 Ok(count as u64)
2331 }
2332
2333 fn sys_getcwd(&mut self) -> Result<u64, i32> {
2334 // `getcwd` receives the output address in x10 and buffer size in x11.
2335 let addr = self.state.regs[10];
2336 // The virtual process always starts at guest root. Two bytes are needed for `/`
2337 // and its C-string NUL terminator.
2338 if self.state.regs[11] < 2 || !self.memory.store_bytes(addr, b"/\0") {
2339 return Err(14);
2340 }
2341 // The raw Linux syscall should return length 2, including NUL. This implementation
2342 // returns the buffer address and also uses EFAULT for an undersized buffer.
2343 Ok(addr)
2344 }
2345
2346 fn sys_brk(&mut self) -> Result<u64, i32> {
2347 // `brk` receives the requested first address after the heap in x10.
2348 let requested = self.state.regs[10];
2349
2350 if requested == 0 {
2351 // Linux permits a zero request to query the current program break.
2352 return Ok(self.memory.brk);
2353 }
2354
2355 // Keep the heap below the current automatic mmap cursor. Successful mmap calls can
2356 // move this boundary upward, so it is not a fixed heap limit.
2357 if requested >= self.mmap_base {
2358 // Linux `brk` reports the unchanged break when it cannot grant the request.
2359 return Ok(self.memory.brk);
2360 }
2361
2362 let old = self.memory.brk;
2363
2364 if requested > old {
2365 // The break can end within a page. Map only complete new pages between the
2366 // old and requested page-rounded ends, with normal heap read/write permission.
2367 let start = align_up(old, PAGE_SIZE as u64);
2368 let end = align_up(requested, PAGE_SIZE as u64);
2369 if end > start {
2370 self.memory
2371 .map_zeroed(start, end - start, PagePerms::READ | PagePerms::WRITE)
2372 .map_err(|_| 12)?;
2373 }
2374 } else {
2375 // Keep the page that contains the new break. Only pages wholly above it can go.
2376 let start = align_up(requested, PAGE_SIZE as u64);
2377 let end = align_up(old, PAGE_SIZE as u64);
2378 if end > start {
2379 self.memory.unmap(start, end - start).map_err(|_| 22)?;
2380 // Removed heap pages can contain code, so their compiled entries are unsafe to keep.
2381 self.invalidate_cache(start, end - start);
2382 }
2383 }
2384
2385 self.memory.brk = requested;
2386 Ok(requested)
2387 }
2388
2389 fn sys_mmap(&mut self) -> Result<u64, i32> {
2390 // `mmap` receives address, length, protection, flags, fd, and offset in x10 through x15.
2391 let hint = self.state.regs[10];
2392 // Linux mappings cover complete pages, so round a nonzero byte length up to 4 KiB.
2393 // `align_up` assumes that the rounded guest length fits in u64.
2394 let size = align_up(self.state.regs[11], PAGE_SIZE as u64);
2395 let prot = self.state.regs[12];
2396 let flags = self.state.regs[13];
2397 // Linux flag 0x10 is `MAP_FIXED`: the address is a requirement, not a hint.
2398 let fixed = flags & 0x10 != 0;
2399
2400
2401 // Linux flag 0x20 is `MAP_ANONYMOUS`: no file supplies the initial bytes.
2402 // Source: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/mman-common.h
2403 let anonymous = flags & 0x20 != 0;
2404 // Only MAP_FIXED and MAP_ANONYMOUS affect this implementation. It does not implement
2405 // or validate MAP_PRIVATE, MAP_SHARED, or other Linux mmap flags.
2406
2407 // Protection bits 0, 1, and 2 mean read, write, and execute. Decimal 7 is the
2408 // mask `0b111`. AND with its complement detects all unsupported protection bits.
2409 if size == 0 || prot & !7 != 0 {
2410 return Err(22);
2411 }
2412
2413 // Honor a free hint, but use the deterministic mmap cursor when it overlaps a mapping.
2414 let base = if fixed {
2415 // A fixed mapping must start at a page boundary because it can replace pages.
2416 if !hint.is_multiple_of(PAGE_SIZE as u64) {
2417 return Err(22);
2418 }
2419 hint
2420 } else if hint != 0
2421 && self
2422 .memory
2423 .range_is_free(align_down(hint, PAGE_SIZE as u64), size)
2424 {
2425 align_down(hint, PAGE_SIZE as u64)
2426 } else {
2427 self.memory
2428 .find_free_range(self.mmap_base, size)
2429 .ok_or(12)?
2430 };
2431
2432 let bytes = if anonymous {
2433 // An empty source tells the MMU to zero-fill the complete new mapping.
2434 Vec::new()
2435 } else {
2436 let fd = self.state.regs[14] as i32;
2437 let offset = self.state.regs[15];
2438 // Linux file mappings require the byte offset to be a multiple of page size.
2439 if !offset.is_multiple_of(PAGE_SIZE as u64) {
2440 return Err(22);
2441 }
2442 let file = self.files.get(&fd).ok_or(9)?;
2443 let mut bytes = vec![0; size as usize];
2444 let count = file.file.read_at(&mut bytes, offset).map_err(host_errno)?;
2445 // Keep only bytes that the host file supplied. The MMU zero-fills the tail.
2446 bytes.truncate(count);
2447 bytes
2448 };
2449
2450 self.memory
2451 .replace_mapping(base, size, page_perms(prot), &bytes)
2452 .map_err(|_| 12)?;
2453 // Replacement can change executable bytes or permissions at an existing guest PC.
2454 self.invalidate_cache(base, size);
2455 // A successful replacement validated that the mapping end fits in u64.
2456 self.mmap_base = self.mmap_base.max(base + size);
2457
2458 // dbg!(self.memory);
2459
2460 Ok(base)
2461 }
2462
2463 fn sys_mprotect(&mut self) -> Result<u64, i32> {
2464 // `mprotect` receives address, length, and protection in x10 through x12.
2465 let base = self.state.regs[10];
2466 let size = self.state.regs[11];
2467 let prot = self.state.regs[12];
2468 // As in `mmap`, reject every protection bit outside the low R/W/X mask.
2469 if prot & !7 != 0 || !self.memory.protect(base, size, page_perms(prot)) {
2470 return Err(22);
2471 }
2472 Ok(0)
2473 }
2474
2475 fn sys_munmap(&mut self) -> Result<u64, i32> {
2476 // `munmap` receives address in x10 and byte length in x11.
2477 let base = self.state.regs[10];
2478 let size = self.state.regs[11];
2479 self.memory.unmap(base, size).map_err(|_| 22)?;
2480 self.invalidate_cache(base, size);
2481 Ok(0)
2482 }
2483
2484 fn sys_getrandom(&mut self) -> Result<u64, i32> {
2485 // `getrandom` receives output address in x10, byte count in x11, and flags in x12.
2486 // This deterministic subset ignores all flag values, including invalid Linux flags.
2487 let size = usize::try_from(self.state.regs[11]).map_err(|_| 22)?;
2488 // Deterministic bytes make replay and fuzzing results reproducible.
2489 // They are not random and are not safe for cryptographic or security use.
2490 // The odd multiplier 31 visits each byte value once for every 256 indices.
2491 // The offset 17 avoids an all-zero prefix. Wrapping keeps arithmetic modulo 256.
2492 let bytes = (0..size)
2493 .map(|idx| (idx as u8).wrapping_mul(31).wrapping_add(17))
2494 .collect::<Vec<_>>();
2495 if !self.memory.store_bytes(self.state.regs[10], &bytes) {
2496 return Err(14);
2497 }
2498
2499 Ok(size as u64)
2500 }
2501
2502 fn sys_clock_gettime(&mut self) -> Result<u64, i32> {
2503 // `clock_gettime` receives the clock ID in x10 and output address in x11.
2504 // This deterministic clock accepts and ignores every clock ID. Linux does not do this;
2505 // it is an ABI limitation of the emulator.
2506 // A fixed time removes host timing from deterministic guest runs.
2507 // RV64 `timespec` has one 8-byte seconds field and one 8-byte nanoseconds field.
2508 // Sixteen zero bytes therefore represent time zero without host-layout casts.
2509 if !self.memory.store_bytes(self.state.regs[11], &[0; 16]) {
2510 return Err(14);
2511 }
2512 Ok(0)
2513 }
2514
2515 fn sys_sched_getaffinity(&mut self) -> Result<u64, i32> {
2516 // `sched_getaffinity` receives pid, mask size, and mask address in x10 through x12.
2517 // This single-task model does not need the pid to select another guest task.
2518 let size = usize::try_from(self.state.regs[11]).map_err(|_| 22)?;
2519 // Expose one virtual CPU so guests do not create host-dependent worker counts.
2520 // Bit 0 in the byte selects virtual CPU 0. Return 1 because one mask byte was written.
2521 // Raw Linux returns its kernel affinity-mask byte size, which need not be one.
2522 // Returning one is an emulator policy and ABI limitation.
2523 if size == 0 || !self.memory.store_bytes(self.state.regs[12], &[1]) {
2524 return Err(14);
2525 }
2526 Ok(1)
2527 }
2528
2529 fn sys_uname(&mut self) -> Result<u64, i32> {
2530 // `uname` receives the guest `utsname` output address in x10.
2531 // Fixed identity data prevents host details from changing guest behavior.
2532 // Linux `new_utsname` has six arrays of 65 bytes. Each array permits 64 text
2533 // bytes plus one NUL. The arrays are system, node, release, version, machine,
2534 // and domain name.
2535 // Source: https://github.com/torvalds/linux/blob/master/include/uapi/linux/utsname.h
2536 let mut uts = [0u8; 65 * 6];
2537 for (index, value) in ["Linux", "remu", "6.0.0", "#1", "riscv64", ""]
2538 .iter()
2539 .enumerate()
2540 {
2541 // Multiply by one complete field width to keep text in its own NUL-filled array.
2542 let start = index * 65;
2543 uts[start..start + value.len()].copy_from_slice(value.as_bytes());
2544 }
2545 if !self.memory.store_bytes(self.state.regs[10], &uts) {
2546 return Err(14);
2547 }
2548 Ok(0)
2549 }
2550
2551 fn sys_getrlimit(&mut self) -> Result<u64, i32> {
2552 // `getrlimit` receives the resource in x10 and output address in x11.
2553 self.write_guest_rlimit(self.state.regs[11], self.state.regs[10])
2554 }
2555
2556 fn sys_prlimit64(&mut self) -> Result<u64, i32> {
2557 // `prlimit64` receives pid, resource, new-limit pointer, and old-limit pointer
2558 // in x10 through x13. This one-process model ignores pid.
2559 // A non-null x12 is a request to set a new limit. Reject it because emulator
2560 // policy limits must stay consistent with mapped memory and descriptor handling.
2561 if self.state.regs[12] != 0 {
2562 return Err(22);
2563 }
2564 // A non-null x13 requests the old limit. x11 contains the resource number.
2565 if self.state.regs[13] != 0 {
2566 self.write_guest_rlimit(self.state.regs[13], self.state.regs[11])?;
2567 }
2568 Ok(0)
2569 }
2570
2571 fn read_guest_string(&self, addr: u64, max: usize) -> Result<String, i32> {
2572 // Linux path arguments are NUL-terminated byte strings in guest memory. This runtime
2573 // chooses a Rust `String`, so it accepts only UTF-8 guest paths.
2574 // Path handlers pass 4096 as an emulator work limit because it is one selected guest
2575 // page. This value is not a universal Linux pathname limit.
2576 let mut bytes = Vec::new();
2577 let mut current = addr;
2578
2579 // Read per page so a terminator before an unmapped next page still succeeds.
2580 while bytes.len() < max {
2581 // A 4 KiB page is a power of two. AND with 4095 (`PAGE_SIZE - 1`) keeps
2582 // the low 12 address bits, which are the offset inside the current page.
2583 // Subtract that offset to get the byte count up to the next page boundary.
2584 let page_left = PAGE_SIZE - (current as usize & (PAGE_SIZE - 1));
2585 let count = page_left.min(max - bytes.len());
2586 let chunk = self.memory.load_bytes(current, count).ok_or(14)?;
2587 if let Some(end) = chunk.iter().position(|byte| *byte == 0) {
2588 // Exclude the NUL marker. Error 22 (`EINVAL`) reports a non-UTF-8 path.
2589 bytes.extend_from_slice(&chunk[..end]);
2590 return String::from_utf8(bytes).map_err(|_| 22);
2591 }
2592 bytes.extend_from_slice(&chunk);
2593 current = current.checked_add(count as u64).ok_or(14)?;
2594 }
2595
2596 // No NUL occurred within the policy limit, so the path argument is invalid.
2597 Err(22)
2598 }
2599
2600 // todo - egal
2601 fn write_guest_stat(&mut self, addr: u64, metadata: Option<&fs::Metadata>) -> Result<u64, i32> {
2602 // Build the RV64 Linux byte layout explicitly because the host Rust type has another ABI.
2603 // The generic 64-bit Linux `struct stat` is 128 bytes. It has explicit padding at
2604 // bytes 40..48 and 60..64, and unused bytes at 120..128. Starting with zero fills
2605 // these ABI gaps and prevents host data from leaking to the guest.
2606 // Source: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/stat.h
2607 let mut stat = [0u8; 128];
2608 if let Some(metadata) = metadata {
2609 // Bytes 0 and 8 hold 64-bit device and inode numbers.
2610 put_u64(&mut stat, 0, metadata.dev());
2611 put_u64(&mut stat, 8, metadata.ino());
2612 // Bytes 16 through 28 hold four 32-bit values: mode, link count, user ID,
2613 // and group ID. These widths are part of the guest ABI, not host Rust types.
2614 put_u32(&mut stat, 16, metadata.mode());
2615 put_u32(&mut stat, 20, metadata.nlink() as u32);
2616 put_u32(&mut stat, 24, metadata.uid());
2617 put_u32(&mut stat, 28, metadata.gid());
2618 // Byte 32 holds the special-file device. Byte 40 is the first padding slot.
2619 put_u64(&mut stat, 32, metadata.rdev());
2620 // File size starts at byte 48. Preferred I/O block size is a 32-bit value at 56.
2621 put_u64(&mut stat, 48, metadata.size());
2622 put_u32(&mut stat, 56, metadata.blksize() as u32);
2623 // Byte 60 is padding. The 64-bit count of allocated 512-byte blocks starts at 64.
2624 put_u64(&mut stat, 64, metadata.blocks());
2625 // Three timestamp pairs follow. Each pair has 8-byte seconds and 8-byte
2626 // nanoseconds fields, at offsets 72, 88, and 104.
2627 put_u64(&mut stat, 72, metadata.atime() as u64);
2628 put_u64(&mut stat, 80, metadata.atime_nsec() as u64);
2629 put_u64(&mut stat, 88, metadata.mtime() as u64);
2630 put_u64(&mut stat, 96, metadata.mtime_nsec() as u64);
2631 put_u64(&mut stat, 104, metadata.ctime() as u64);
2632 put_u64(&mut stat, 112, metadata.ctime_nsec() as u64);
2633 } else {
2634 // Octal 020000 is `S_IFCHR`, and 0666 gives read/write permission to all users.
2635 // This makes a standard stream look like a character device instead of a file.
2636 put_u32(&mut stat, 16, 0o020666);
2637 // A synthetic stream has one link and the same unprivileged ID 1000 as auxv.
2638 put_u32(&mut stat, 20, 1);
2639 put_u32(&mut stat, 24, 1000);
2640 put_u32(&mut stat, 28, 1000);
2641 // Report 4 KiB as the preferred I/O block size to match guest memory pages.
2642 put_u32(&mut stat, 56, PAGE_SIZE as u32);
2643 }
2644 if !self.memory.store_bytes(addr, &stat) {
2645 return Err(14);
2646 }
2647 Ok(0)
2648 }
2649
2650 fn resolve_process_path(&self, guest_path: &str, create: bool) -> Result<PathBuf, i32> {
2651 let path = Path::new(guest_path);
2652 // Absolute guest paths use the sysroot; relative paths use the writable work directory.
2653 let root = if path.is_absolute() {
2654 // Error 2 is `ENOENT`. Without a sysroot, no absolute guest tree exists.
2655 self.process.sysroot.as_ref().ok_or(2)?
2656 } else {
2657 self.process.workdir.as_ref().ok_or(2)?
2658 };
2659
2660 resolve_beneath(root, path, create).map_err(host_errno)
2661 }
2662
2663 fn write_guest_rlimit(&mut self, addr: u64, resource: u64) -> Result<u64, i32> {
2664 // Linux resource 3 is `RLIMIT_STACK`, and 7 is `RLIMIT_NOFILE`. The project
2665 // exposes its 128 KiB mapped stack and reports 1024 descriptors. It does not enforce
2666 // that descriptor limit. It also reports infinity for unknown resources, although
2667 // Linux normally returns EINVAL. Infinity has an all-ones unsigned representation.
2668 // Source: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/resource.h
2669 let value = match resource {
2670 3 => DEFAULT_STACK_SIZE,
2671 7 => 1024,
2672 _ => u64::MAX,
2673 };
2674 // RV64 `struct rlimit` has two 8-byte words. Write the same value as the soft
2675 // current limit and the hard maximum limit.
2676 if !self.memory.store_le(addr, 8, value) || !self.memory.store_le(addr + 8, 8, value) {
2677 return Err(14);
2678 }
2679 Ok(0)
2680 }
2681
2682 fn invalidate_cache(&mut self, base: u64, size: u64) {
2683 // Native code is tied to guest addresses whose mappings can be replaced or removed.
2684 // Saturating addition treats an overflowing range as one that reaches the highest
2685 // guest address. It cannot wrap and retain low-address entries by mistake.
2686 let end = base.saturating_add(size);
2687 self.cache.retain(|pc, _| *pc < base || *pc >= end);
2688 self.sync_jit_entries();
2689 }
2690
2691 fn sync_jit_entries(&mut self) {
2692 // Rebuild after each cache change because vector growth can move the raw ABI array.
2693 self.jit_entries = self
2694 .cache
2695 .iter()
2696 .map(|(&guest_pc, compiled)| JitEntryAbi {
2697 guest_pc,
2698 execute: compiled.entry(),
2699 })
2700 .collect();
2701 // `as_ptr` borrows vector storage without moving entries. Generated code must use
2702 // the paired count and must not keep the pointer after the cache changes.
2703 self.state.jit_entries = self.jit_entries.as_ptr();
2704 self.state.jit_entry_count = self.jit_entries.len();
2705
2706 // dbg!(self.state.jit_entries);
2707 }
2708
2709 fn initialize_process_state(&mut self) -> JitResult<()> {
2710 // RISC-V x2 is `sp`. Begin below the exact mapping top so startup data can grow down.
2711 self.state.regs[2] = self.memory.stack_top - DEFAULT_STACK_POINTER_OFFSET;
2712
2713 // Use the linker-defined global pointer because RISC-V code can address data through it.
2714 self.state.regs[3] = self
2715 .program
2716 .find_symbol("__global_pointer$")
2717 .map(|symbol| symbol.value)
2718 .unwrap_or(0);
2719
2720 // RISC-V x4 is `tp`, the thread pointer. Zero is valid until a guest dynamic loader
2721 // sets up thread-local storage. This runtime does not create a guest thread itself.
2722 self.state.regs[4] = 0;
2723
2724 self.build_elf_entry_state()?;
2725 self.state.pc = self.initial_pc;
2726 Ok(())
2727 }
2728
2729 // todo - quatsch
2730 fn build_elf_entry_state(&mut self) -> JitResult<()> {
2731 // Linux starts a process with strings at high stack addresses and an entry table
2732 // below them. The table contains argc, argv, envp, and auxiliary-vector records.
2733 // Source: https://github.com/torvalds/linux/blob/master/fs/binfmt_elf.c
2734 let mut sp = self.memory.stack_top - DEFAULT_STACK_POINTER_OFFSET;
2735
2736 // Clone these vectors to avoid a Rust borrow conflict: the stack-writing methods
2737 // need mutable access to the runtime while configuration is stored in the same value.
2738 let argv_values = self.process.argv.clone();
2739 let envp_values = self.process.envp.clone();
2740
2741 // Store strings first so the final entry table can contain stable guest pointers.
2742 let argv = self.push_guest_strings(&mut sp, &argv_values)?;
2743 let envp = self.push_guest_strings(&mut sp, &envp_values)?;
2744
2745 // Keep the 16-byte AT_RANDOM object aligned to 16 bytes. Linux defines the object
2746 // size as 16, and aligned placement also preserves the final RISC-V stack alignment.
2747 let random_addr = self.push_guest_stack_bytes(&mut sp, &ELF_AUX_RANDOM, 16)?;
2748
2749 let argc = argv.len() as u64;
2750 // Linux AT_EXECFN points at the executed pathname. This runtime uses argv[0] as an approximation.
2751 // Use null when no argument exists.
2752 let execfn_ptr = argv.first().copied().unwrap_or(0);
2753
2754 // todo -- siehe oben struct auxv
2755 let auxv = self.build_elf_auxv(random_addr, execfn_ptr)?;
2756
2757 // RV64 uses 8-byte words. The table has one argc word, one word per pointer,
2758 // one null word after each pointer list, and two words per auxiliary entry.
2759 let table_size = 8
2760 + ((argv.len() + 1) as u64 * 8)
2761 + ((envp.len() + 1) as u64 * 8)
2762 + (auxv.len() as u64 * 16);
2763
2764 // Linux requires an aligned initial stack for the process entry ABI.
2765 // The RISC-V psABI requires x2 to be aligned to 128 bits, which is 16 bytes.
2766 // Source: https://riscv-non-isa.github.io/riscv-elf-psabi-doc/#integer-calling-convention
2767 let table_addr = self.reserve_guest_stack(&mut sp, table_size, 16)?;
2768 self.write_elf_entry_table(table_addr, argc, &argv, &envp, &auxv)?;
2769
2770 // x1 is the return-address register. Process entry has no RISC-V caller, so zero is
2771 // the no-caller sentinel. A guest can still execute address zero if it is mapped.
2772 self.state.regs[1] = 0;
2773 self.state.regs[2] = table_addr;
2774 // `_start` reads argc and pointer lists from x2. Clear a0 through a2 so startup
2775 // cannot depend on old values from a reused runtime state.
2776 self.state.regs[10] = 0;
2777 self.state.regs[11] = 0;
2778 self.state.regs[12] = 0;
2779
2780 Ok(())
2781 }
2782
2783 // todo ??
2784 fn build_elf_auxv(&self, random_addr: u64, execfn_ptr: u64) -> JitResult<Vec<AuxEntry>> {
2785 // The main executable must expose its program headers because the dynamic loader
2786 // uses them before normal application code starts.
2787 let phdr_addr = self
2788 .program
2789 .elf_phdr_addr
2790 .ok_or_else(|| io::Error::other("missing ELF program header address"))?;
2791
2792 // Keep the records explicit. This makes guest-visible process policy independent
2793 // from the host process and gives the dynamic loader a stable environment.
2794 Ok(vec![
2795 (AT_PHDR, phdr_addr),
2796 (AT_PHENT, self.program.elf_phent_size as u64),
2797 (AT_PHNUM, self.program.elf_phnum as u64),
2798 (AT_PAGESZ, PAGE_SIZE as u64),
2799 // Zero means no interpreter. Otherwise this is the fixed loader bias.
2800 (AT_BASE, self.loader_base),
2801 (AT_FLAGS, 0),
2802 (AT_ENTRY, self.program.entry_pc),
2803 // ID 1000 models one unprivileged user and agrees with identity syscalls.
2804 (AT_UID, 1000),
2805 (AT_EUID, 1000),
2806 (AT_GID, 1000),
2807 (AT_EGID, 1000),
2808 (AT_HWCAP, RISCV_HWCAP),
2809 // Advertise the fixed policy value of 100 clock ticks per second.
2810 (AT_CLKTCK, 100),
2811 (AT_SECURE, 0),
2812 (AT_RANDOM, random_addr),
2813 (AT_HWCAP2, 0),
2814 (AT_EXECFN, execfn_ptr),
2815 // The final zero type is mandatory because consumers scan until AT_NULL.
2816 (AT_NULL, 0),
2817 ])
2818 }
2819
2820 fn write_elf_entry_table(
2821 &mut self,
2822 table_addr: u64,
2823 argc: u64,
2824 argv: &[u64],
2825 envp: &[u64],
2826 auxv: &[AuxEntry],
2827 ) -> JitResult<()> {
2828 // `c` is a guest byte address. Each write advances it by one RV64 word.
2829 let mut c = table_addr;
2830
2831 self.write_guest_u64(c, argc, "argc")?;
2832 c += 8;
2833
2834 // The table order is argc, argv pointers, null, envp pointers, null, then auxv.
2835 self.write_u64_s(&mut c, argv, "argv entry")?;
2836 self.write_guest_u64(c, 0, "argv terminator")?;
2837 c += 8;
2838
2839 self.write_u64_s(&mut c, envp, "envp entry")?;
2840 self.write_guest_u64(c, 0, "envp terminator")?;
2841 c += 8;
2842
2843 // Write each auxiliary entry as its type word followed by its value word, as the ABI requires.
2844 for &(kind, value) in auxv {
2845 self.write_guest_u64(c, kind, "auxv type")?;
2846 c += 8;
2847 self.write_guest_u64(c, value, "auxv value")?;
2848 c += 8;
2849 }
2850
2851 Ok(())
2852 }
2853
2854 fn write_u64_s(&mut self, cursor: &mut u64, values: &[u64], what: &str) -> JitResult<()> {
2855 for &value in values {
2856 self.write_guest_u64(*cursor, value, what)?;
2857 // Advance by 8 because every pointer and integer word is 64 bits on RV64.
2858 *cursor += 8;
2859 }
2860
2861 Ok(())
2862 }
2863
2864 fn write_guest_u64(&mut self, addr: u64, value: u64, what: &str) -> JitResult<()> {
2865 // Store exactly 8 bytes in guest little-endian order. Do not copy the host memory
2866 // representation because host byte order is not part of the guest ABI.
2867 if !self.memory.store_le(addr, 8, value) {
2868 return Err(io::Error::other(format!("failed to write guest {what}")).into());
2869 }
2870
2871 Ok(())
2872 }
2873
2874 // fn invalidate_cache(&mut self, base: u64, size: u64) {
2875 // let end = base.saturating_add(size);
2876 // self.cache.retain(|pc, _| *pc < base || *pc >= end);
2877 // self.sync_jit_entries();
2878 // }
2879
2880 fn push_guest_strings(&mut self, sp: &mut u64, values: &[String]) -> JitResult<Vec<u64>> {
2881 let mut addresses = Vec::with_capacity(values.len());
2882
2883 // Push in reverse because the stack grows down, then restore logical argument order.
2884 for value in values.iter().rev() {
2885 // Linux C strings need one zero byte after their text.
2886 let mut bytes = value.as_bytes().to_vec();
2887 bytes.push(0);
2888 let addr = self.push_guest_stack_bytes(sp, &bytes, 1)?;
2889 addresses.push(addr);
2890 }
2891
2892 addresses.reverse();
2893 Ok(addresses)
2894 }
2895
2896 fn push_guest_stack_bytes(&mut self, sp: &mut u64, bytes: &[u8], align: u64) -> JitResult<u64> {
2897 // Reserve before writing because a downward-growing stack places new data below `sp`.
2898 let addr = self.reserve_guest_stack(sp, bytes.len() as u64, align)?;
2899
2900 if !self.memory.store_bytes(addr, bytes) {
2901 return Err(io::Error::other("failed to write guest stack bytes").into());
2902 }
2903 Ok(addr)
2904 }
2905
2906 fn reserve_guest_stack(&mut self, sp: &mut u64, size: u64, align: u64) -> JitResult<u64> {
2907 // Checked subtraction rejects a request that would wrap below guest address zero.
2908 let next = sp
2909 .checked_sub(size)
2910 .ok_or_else(|| io::Error::other("guest stack underflow"))?;
2911
2912 // Alignment can add padding below the object. `max(1)` prevents a zero alignment.
2913 // `align_down` requires a power of two. Current callers supply only 1 or 16.
2914 let aligned = align_down(next, align.max(1));
2915
2916 // The stack mapping starts 128 KiB below its fixed top. Reject data outside it.
2917 if aligned < DEFAULT_STACK_TOP - DEFAULT_STACK_SIZE {
2918 return Err(io::Error::other("overflow!").into());
2919 }
2920
2921 *sp = aligned;
2922 Ok(aligned)
2923 }
2924}
2925
2926// todo - quatsch
2927// Finds known block-entry PCs so lazy compilation can include direct neighbors.
2928fn direct_successors(nnil: &Nnil) -> Vec<u64> {
2929 let mut successors = Vec::new();
2930
2931 // Use only explicit targets so lazy batching never guesses an indirect destination.
2932 // NNIL regions end with their control-flow operation. Earlier instructions cannot
2933 // select the next region, so only the final instruction is relevant here.
2934 match nnil.insns().last() {
2935 Some(
2936 NnilInstruction::Beq { ttgt, ftgt, .. }
2937 | NnilInstruction::Bne { ttgt, ftgt, .. }
2938 | NnilInstruction::Blt { ttgt, ftgt, .. }
2939 | NnilInstruction::Bgt { ttgt, ftgt, .. },
2940 ) => {
2941 // Conditional branches have one false label and one true label. Both can be
2942 // the next block, so resolve both labels to guest addresses.
2943 if let Some(address) = nnil.label_address(*ftgt) {
2944 successors.push(address.0);
2945 }
2946 if let Some(address) = nnil.label_address(*ttgt) {
2947 successors.push(address.0);
2948 }
2949 }
2950
2951 Some(NnilInstruction::Jr { dst } | NnilInstruction::Jmp { dst }) => {
2952 // `dst` is an NNIL instruction handle. It is a static successor only when that
2953 // instruction is a nonnegative immediate guest address.
2954 if let Some(NnilInstruction::Imm { imm }) = nnil.insns().get(dst.0 as usize)
2955 && *imm >= 0
2956 {
2957 successors.push(*imm as u64);
2958 }
2959 }
2960
2961
2962 Some(NnilInstruction::Syscall {} | NnilInstruction::Sysbreak {}) => {
2963 // Generated code records the post-region PC before either stop. A later resume
2964 // starts there; a syscall can also resume inside the runtime.
2965 successors.push(nnil.pc());
2966 }
2967 _ => {}
2968 }
2969
2970 successors
2971}
2972
2973fn resolve_guest_path(sysroot: &Path, guest_path: &str) -> io::Result<PathBuf> {
2974 // Check ELF interpreter paths against the configured sysroot.
2975 if !guest_path.starts_with('/') {
2976 return Err(io::Error::other("guest path is not absolute"));
2977 }
2978
2979 resolve_beneath(sysroot, Path::new(guest_path), false)
2980}
2981
2982// todo - macht fuzzer schon?
2983// Converts a guest path to a host path and checks its current canonical target against `root`.
2984fn resolve_beneath(root: &Path, path: &Path, create: bool) -> io::Result<PathBuf> {
2985 let mut relative = PathBuf::new();
2986
2987 // Reject parent components before canonicalization to make escape attempts explicit.
2988 for component in path.components() {
2989 match component {
2990 // Ignore the guest root marker so `PathBuf::join` cannot replace the host root.
2991 // A current-directory marker does not change the location.
2992 Component::RootDir | Component::CurDir => {}
2993 // Normal components are the only guest-controlled text added below the root.
2994 Component::Normal(value) => relative.push(value),
2995 // `..` can leave the root. A platform prefix can select another host volume.
2996 Component::ParentDir | Component::Prefix(_) => {
2997 return Err(io::Error::new(
2998 io::ErrorKind::PermissionDenied,
2999 "guest path escapes its root",
3000 ));
3001 }
3002 }
3003 }
3004
3005 let candidate = root.join(relative);
3006
3007 // Canonicalization detects symbolic links that leave the allowed root at check time.
3008 // A later host-side link change can race the file operation, so this is not a strong sandbox boundary.
3009 match fs::canonicalize(&candidate) {
3010 // `Path::starts_with` compares path components, not an unsafe text prefix.
3011 Ok(path) if path.starts_with(root) => Ok(path),
3012 Ok(_) => Err(io::Error::new(
3013 io::ErrorKind::PermissionDenied,
3014 "guest path escapes its root",
3015 )),
3016
3017 Err(err) if create && err.kind() == io::ErrorKind::NotFound => {
3018 // A new file has no canonical path, so validate its existing parent instead.
3019 let parent = candidate
3020 .parent()
3021 .ok_or_else(|| io::Error::other("guest path has no parent"))?;
3022 let parent = fs::canonicalize(parent)?;
3023 if !parent.starts_with(root) {
3024 return Err(io::Error::new(
3025 io::ErrorKind::PermissionDenied,
3026 "guest path escapes its root",
3027 ));
3028 }
3029 let name = candidate
3030 .file_name()
3031 .ok_or_else(|| io::Error::other("guest path has no file name"))?;
3032 Ok(parent.join(name))
3033 }
3034 Err(err) => Err(err),
3035 }
3036}
3037
3038fn host_errno(err: io::Error) -> i32 {
3039 // Preserve native Unix error numbers when the host supplied one. These are guest Linux
3040 // values only on a Linux host. Another Unix host needs an explicit errno translation.
3041 if let Some(errno) = err.raw_os_error() {
3042 return errno;
3043 }
3044
3045 // Use Linux generic errno values when Rust has only a portable error category:
3046 // 2=ENOENT, 13=EACCES, 17=EEXIST, and 22=EINVAL. Error 5, EIO, is the project fallback.
3047 // Source: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h
3048 match err.kind() {
3049 io::ErrorKind::NotFound => 2,
3050 io::ErrorKind::PermissionDenied => 13,
3051 io::ErrorKind::AlreadyExists => 17,
3052 io::ErrorKind::InvalidInput | io::ErrorKind::InvalidData => 22,
3053 _ => 5,
3054 }
3055}
3056
3057fn guest_errno(errno: i32) -> u64 {
3058 // Linux reports syscall errors as negative values in the unsigned guest register.
3059 // The final cast keeps the two's-complement bit pattern that RV64 sees in x10.
3060 (-(errno as i64)) as u64
3061}
3062
3063// todo - code dup
3064fn write_marked_output(
3065 mut output: impl io::Write,
3066 bytes: &[u8],
3067 name: &str,
3068 color: &str,
3069 colored: bool,
3070) -> Result<usize, i32> {
3071
3072 // Add color only for terminals so redirected guest output has no escape sequences.
3073 let (color, reset) = if colored {
3074 // ANSI sequence 0 clears color and intensity after the marker.
3075 (color, "\x1b[0m")
3076 } else {
3077 ("", "")
3078 };
3079
3080 writeln!(output, "\n{color}----- guest {name} -----{reset}").map_err(host_errno)?;
3081 // `write` can complete only a prefix. Return that actual count as the guest result.
3082 let count = output.write(bytes).map_err(host_errno)?;
3083 // Add a display newline only when the written prefix has none. This newline is part
3084 // of the host marker and is not included in the guest byte count.
3085 if bytes
3086 .get(..count)
3087 .is_none_or(|written| !written.ends_with(b"\n"))
3088 {
3089 writeln!(output).map_err(host_errno)?;
3090 }
3091
3092 writeln!(output, "{color}----- end guest {name} -----{reset}").map_err(host_errno)?;
3093 // Flush now so guest output appears before later emulator diagnostics.
3094 output.flush().map_err(host_errno)?;
3095
3096 Ok(count)
3097}
3098
3099fn page_perms(prot: u64) -> PagePerms {
3100 // Linux `PROT_READ`, `PROT_WRITE`, and `PROT_EXEC` use the same low three bits as
3101 // `PagePerms`. Callers reject higher bits before this narrowing cast.
3102 PagePerms::from_bits(prot as u8)
3103}
3104
3105fn put_u32(bytes: &mut [u8], offset: usize, value: u32) {
3106 // A 32-bit field occupies four bytes. Explicit little-endian order builds RV64 data
3107 // correctly even if the host layout or alignment is different.
3108 // The caller must supply an in-bounds fixed ABI offset, or slicing will panic.
3109 bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
3110}
3111
3112fn put_u64(bytes: &mut [u8], offset: usize, value: u64) {
3113 // A 64-bit field occupies eight bytes. The caller must supply an in-bounds fixed ABI
3114 // offset, or slicing will panic.
3115 bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
3116}
3117
3118// todo
3119#[derive(Debug, Clone, Copy)]
3120// Store only section fields that later symbol and relocation code reads.
3121struct ElfSectionHeader {
3122 // The type selects symbol, string, relocation, or other section semantics.
3123 sh_type: u32,
3124 // Offset and size select this section's byte range in the ELF file.
3125 sh_offset: u64,
3126 sh_size: u64,
3127 // Its meaning depends on the section type. For SYMTAB or DYNSYM it selects the name
3128 // string table. For RELA it selects the related symbol table.
3129 sh_link: u32,
3130 // A nonzero entry size converts the section byte size to a record count.
3131 sh_entsize: u64,
3132}
3133
3134// todo -- gamozo macht readelf, nm
3135// Reads the ELF64 section table for optional symbol and import metadata.
3136fn parse_section_headers(
3137 bytes: &[u8],
3138 shoff: usize,
3139 shentsize: usize,
3140 shnum: usize,
3141) -> JitResult<Vec<ElfSectionHeader>> {
3142 if shnum == 0 {
3143 // An ELF can execute without section headers because program headers define mapping.
3144 // ELF also uses zero for extended numbering. This parser treats zero as no sections
3145 // and does not support that extended form.
3146 return Ok(Vec::new());
3147 }
3148
3149 let mut sections = Vec::with_capacity(shnum);
3150
3151 // Retain only fields needed for symbol and relocation parsing.
3152 for idx in 0..shnum {
3153 // Multiply the file-defined entry size by the index. Saturation prevents a low wrap
3154 // in that product, and checked addition protects the table base addition.
3155 let header = shoff
3156 .checked_add(idx.saturating_mul(shentsize))
3157 .ok_or_else(|| io::Error::other("section header overflow"))?;
3158
3159 // Check the declared entry range. The final `header + shentsize` is unchecked, and
3160 // this parser does not require the 64 bytes that its fixed ELF64 field reads need.
3161 let _ = bytes
3162 .get(header..header + shentsize)
3163 .ok_or_else(|| io::Error::other(""))?;
3164
3165 // ELF64 section fields start at byte offsets 4, 24, 32, 40, and 56.
3166 // Source: https://gabi.xinuos.com/elf/03-sheader.html
3167 sections.push(ElfSectionHeader {
3168 sh_type: read_u32(bytes, header + 4)?,
3169 sh_offset: read_u64(bytes, header + 24)?,
3170 sh_size: read_u64(bytes, header + 32)?,
3171 sh_link: read_u32(bytes, header + 40)?,
3172 sh_entsize: read_u64(bytes, header + 56)?,
3173 });
3174 }
3175
3176 Ok(sections)
3177}
3178
3179fn parse_symbol_section(
3180 bytes: &[u8],
3181 sections: &[ElfSectionHeader],
3182 section: &ElfSectionHeader,
3183) -> JitResult<Vec<ProgramSymbol>> {
3184 if section.sh_entsize == 0 {
3185 // A zero divisor cannot define records, so treat the malformed table as empty.
3186 return Ok(Vec::new());
3187 }
3188
3189 // ELF links each symbol table to the string table that owns its names.
3190 let strtab = sections
3191 .get(section.sh_link as usize)
3192 .ok_or_else(|| io::Error::other(""))?;
3193
3194 let strtab_bytes = read_elf_range(bytes, strtab.sh_offset, strtab.sh_size, "string table")?;
3195 // Integer division silently ignores an incomplete trailing record. This is a parser limit.
3196 // ELF64 symbols need 24 bytes, but this parser does not reject a smaller nonzero entry size.
3197 let count = (section.sh_size / section.sh_entsize) as usize;
3198
3199 let mut symbols = Vec::with_capacity(count);
3200
3201 for idx in 0..count {
3202 // Select entry `idx` inside the symbol-table section without unchecked addition.
3203 let sym_offset = section
3204 .sh_offset
3205 .checked_add((idx as u64).saturating_mul(section.sh_entsize))
3206 .ok_or_else(|| io::Error::other("symbol table overflow"))?
3207 as usize;
3208
3209 // An ELF64 symbol record is 24 bytes. `st_name` is at 0, `st_shndx` at 6,
3210 // `st_value` at 8, and `st_size` at 16. Bytes 4 and 5 hold binding/type and
3211 // visibility, which current debugger lookup does not need.
3212 // Source: https://gabi.xinuos.com/elf/05-symtab.html
3213 let name_offset = read_u32(bytes, sym_offset)? as usize;
3214 let section_index = read_u16(bytes, sym_offset + 6)?;
3215
3216 let value = read_u64(bytes, sym_offset + 8)?;
3217 let size = read_u64(bytes, sym_offset + 16)?;
3218 let name = read_elf_string(strtab_bytes, name_offset);
3219
3220 symbols.push(ProgramSymbol {
3221 name,
3222 value,
3223 size,
3224 section_index,
3225 });
3226 }
3227
3228 // dbg!(symbols);
3229 Ok(symbols)
3230}
3231
3232// todo - code dup
3233// Reads one 16-bit ELF integer from an in-bounds slice.
3234fn read_u16(bytes: &[u8], offset: usize) -> io::Result<u16> {
3235 // The caller must ensure that `offset + 2` does not overflow usize.
3236 let raw = bytes
3237 .get(offset..offset + 2)
3238 .ok_or_else(|| io::Error::other("short ELF read (u16)"))?;
3239 // The slice check guarantees exactly two bytes, so this conversion cannot fail.
3240 // The ELF identity check already established little-endian byte order.
3241 Ok(u16::from_le_bytes(raw.try_into().unwrap()))
3242}
3243
3244// Reads one 32-bit ELF integer from an in-bounds slice.
3245fn read_u32(bytes: &[u8], offset: usize) -> io::Result<u32> {
3246 // The caller must ensure that `offset + 4` does not overflow usize.
3247 let raw = bytes
3248 .get(offset..offset + 4)
3249 .ok_or_else(|| io::Error::other("short ELF read (u32)"))?;
3250
3251 // The four-byte slice makes `try_into` safe to unwrap.
3252 Ok(u32::from_le_bytes(raw.try_into().unwrap()))
3253}
3254
3255// Reads one 64-bit ELF integer from an in-bounds slice.
3256fn read_u64(bytes: &[u8], offset: usize) -> io::Result<u64> {
3257 // The caller must ensure that `offset + 8` does not overflow usize.
3258 let raw = bytes
3259 .get(offset..offset + 8)
3260 .ok_or_else(|| io::Error::other("short ELF read (u64)"))?;
3261
3262 // The eight-byte slice makes `try_into` safe to unwrap.
3263 Ok(u64::from_le_bytes(raw.try_into().unwrap()))
3264}
3265
3266fn read_elf_range<'a>(bytes: &'a [u8], offset: u64, size: u64, what: &str) -> io::Result<&'a [u8]> {
3267 // Check every conversion and addition because ELF offsets are untrusted input.
3268 let start =
3269 usize::try_from(offset).map_err(|_| io::Error::other(format!("{what} offset overflow")))?;
3270
3271 let len =
3272 usize::try_from(size).map_err(|_| io::Error::other(format!("{what} size overflow")))?;
3273
3274 let end = start
3275 .checked_add(len)
3276 .ok_or_else(|| io::Error::other(format!("{what} range overflow")))?;
3277
3278 bytes
3279 .get(start..end)
3280 .ok_or_else(|| io::Error::other(format!("{what} range overflow")))
3281}
3282
3283fn read_elf_string(bytes: &[u8], offset: usize) -> String {
3284 // An invalid string-table offset is not allowed to index the host slice.
3285 let Some(rest) = bytes.get(offset..) else {
3286 return String::new();
3287 };
3288 // Stop at NUL because following bytes belong to other strings in the table.
3289 // If no NUL remains, this parser accepts all remaining bytes. That is a malformed-ELF limit.
3290 let end = rest
3291 .iter()
3292 .position(|byte| *byte == 0)
3293 .unwrap_or(rest.len());
3294 // ELF names are bytes. Lossy conversion preserves usable diagnostics for an invalid
3295 // UTF-8 name and returns owned text that does not borrow the ELF input.
3296 String::from_utf8_lossy(&rest[..end]).into_owned()
3297}
3298
3299/*
3300
3301fn read_elf_parse(bytes: &[u8], offset: usize) -> String {
3302 let Some(rest) = bytes.get(offset..) else {
3303 return String::new();
3304
3305}
3306*/