I'm trying to assemble and call code at runtime in an interpreter project written in Rust. I'm using the assembler crate for this. I'd like to have extern "C" fn wrappers around important runtime functionality that JIT'ed code can call directly. To minimise complexity in the generated code segments, I'd like to keep the interpreter state as a global variable.
However, the program keeps crashing whenever I try to access and/or modify any global state. A simple println!("hello world") crashes with a general protection fault, seemingly when stdio::_print() accesses stdout. A simpler example with a static mut variable dumps core as well. Interestingly, while the former crashes under Valgrind with a neat stack trace, the latter example runs without fail, actually passing the assertions that hint that the program works as expected. Note that the static variable does not need to be mutable and reading it is enough to cause a crash.
I'm quite ignorant of the mmap details the assembler crate uses underneath, but I wasn't able to find any hint of why my approach crashes. Any guidance would be greatly appreciated.
I've created a Repl.it repl with an MRE.
use anyhow::Result;
use assembler::*;
use assembler::mnemonic_parameter_types::{registers::*, immediates::*};
const CHUNK_LENGTH: usize = 4096;
const LABEL_COUNT: usize = 64;
static mut X: u32 = 0;
#[no_mangle]
unsafe extern "C" fn foo() {
// printing here will lead to a coredump,
// Valgrind will provide more insight (general protection fault)
// println!("hello world");
// modifying a global variable instead will also dump
// core but will run without fail in Valgrind
X += 1
}
fn main() -> Result<()> {
let mut memory_map = ExecutableAnonymousMemoryMap::new(CHUNK_LENGTH, true, true)?;
let mut instr_stream = memory_map.instruction_stream(&InstructionStreamHints {
number_of_labels: LABEL_COUNT,
..Default::default()
});
let f = instr_stream.nullary_function_pointer::<i64>();
instr_stream.call_function(foo as unsafe extern "C" fn());
instr_stream.mov_Register64Bit_Immediate64Bit(Register64Bit::RAX, Immediate64Bit(0x123456789abcdef0));
instr_stream.ret();
instr_stream.finish();
assert_eq!(unsafe { f() }, 0x123456789abcdef0);
assert_eq!(unsafe { X }, 1);
Ok(())
}
Here are the Valgrind outputs for both cases.
Global variable:
==4186== Memcheck, a memory error detector
==4186== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==4186== Using Valgrind-3.16.1 and LibVEX; rerun with -h for copyright info
==4186== Command: target/debug/jit-ffi-fault-mre
==4186==
==4186==
==4186== HEAP SUMMARY:
==4186== in use at exit: 0 bytes in 0 blocks
==4186== total heap usage: 14 allocs, 14 frees, 199,277 bytes allocated
==4186==
==4186== All heap blocks were freed -- no leaks are possible
==4186==
==4186== For lists of detected and suppressed errors, rerun with: -s
==4186== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
println!:
==4341== Memcheck, a memory error detector
==4341== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==4341== Using Valgrind-3.16.1 and LibVEX; rerun with -h for copyright info
==4341== Command: target/debug/jit-ffi-fault-mre
==4341==
==4341==
==4341== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==4341== General Protection Fault
==4341== at 0x13FB7A: std::io::stdio::_print (stdio.rs:1028)
==4341== by 0x1174B0: foo (main.rs:15)
==4341== by 0x4E58004: ???
==4341== by 0x11A1BC: jit_ffi_fault_mre::main (main.rs:35)
==4341== by 0x113FEA: core::ops::function::FnOnce::call_once (function.rs:248)
==4341== by 0x11497D: std::sys_common::backtrace::__rust_begin_short_backtrace (backtrace.rs:122)
==4341== by 0x114E70: std::rt::lang_start::{{closure}} (rt.rs:145)
==4341== by 0x13CA95: call_once<(), (dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe)> (function.rs:280)
==4341== by 0x13CA95: do_call<&(dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe), i32> (panicking.rs:492)
==4341== by 0x13CA95: try<i32, &(dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe)> (panicking.rs:456)
==4341== by 0x13CA95: catch_unwind<&(dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe), i32> (panic.rs:137)
==4341== by 0x13CA95: {closure#2} (rt.rs:128)
==4341== by 0x13CA95: do_call<std::rt::lang_start_internal::{closure_env#2}, isize> (panicking.rs:492)
==4341== by 0x13CA95: try<isize, std::rt::lang_start_internal::{closure_env#2}> (panicking.rs:456)
==4341== by 0x13CA95: catch_unwind<std::rt::lang_start_internal::{closure_env#2}, isize> (panic.rs:137)
==4341== by 0x13CA95: std::rt::lang_start_internal (rt.rs:128)
==4341== by 0x114E3F: std::rt::lang_start (rt.rs:144)
==4341== by 0x11A37B: main (in /home/runner/UnsightlyAwfulPhases/jit-ffi-fault-mre/target/debug/jit-ffi-fault-mre)
==4341==
==4341== HEAP SUMMARY:
==4341== in use at exit: 85 bytes in 3 blocks
==4341== total heap usage: 14 allocs, 11 frees, 199,277 bytes allocated
==4341==
==4341== LEAK SUMMARY:
==4341== definitely lost: 0 bytes in 0 blocks
==4341== indirectly lost: 0 bytes in 0 blocks
==4341== possibly lost: 0 bytes in 0 blocks
==4341== still reachable: 85 bytes in 3 blocks
==4341== suppressed: 0 bytes in 0 blocks
==4341== Rerun with --leak-check=full to see details of leaked memory
==4341==
==4341== For lists of detected and suppressed errors, rerun with: -s
==4341== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
/tmp/nix-shell-4318-0/rc: line 1: 4341 Segmentation fault (core dumped) valgrind target/debug/jit-ffi-fault-mre