Skip to content

Commit

Permalink
libafl_qemu: Add RISCV support (AFLplusplus#2367)
Browse files Browse the repository at this point in the history
* libafl_qemu: Add RISCV support

Adds the following targets (as features):
- riscv32
- riscv64

Added `RISCVCPU` and `CPURISCVState` to the bindings allow list.

Added riscv.rs to the arch module, with all necessary functions and
registers implemented and mapped.
The registers are the same as the ones found in qemus gdbstub xml found
after a build.

Additionally we added all syscall numbers for riscv 64 bit (already
supported by the `syscall_numbers` crate) and also added the missing
ones for riscv 32 bit. We compared both lists and their differences /
equalities with a simple python script and generated a list of the
missing ones, to be complete.
We might PR those to the `syscall_numbers` crate later on.

---------

Co-authored-by: Romain Malmain <[email protected]>
  • Loading branch information
saibotk and rmalmain authored Oct 30, 2024
1 parent 6eb2daf commit 83c87ac
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 7 deletions.
2 changes: 2 additions & 0 deletions libafl_qemu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ mips = [
] # build qemu for mips (el, use with the 'be' feature of mips be)
ppc = ["libafl_qemu_sys/ppc"] # build qemu for powerpc
hexagon = ["libafl_qemu_sys/hexagon"] # build qemu for hexagon
riscv32 = ["libafl_qemu_sys/riscv32"] # build qemu for riscv 32bit
riscv64 = ["libafl_qemu_sys/riscv64"] # build qemu for riscv 64bit

## Big Endian mode
be = ["libafl_qemu_sys/be"]
Expand Down
8 changes: 6 additions & 2 deletions libafl_qemu/build_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void __libafl_qemu_testfile() {}
pub fn build() {
// Note: Unique features are checked in libafl_qemu_sys
println!(
r#"cargo::rustc-check-cfg=cfg(cpu_target, values("arm", "aarch64", "hexagon", "i386", "mips", "ppc", "x86_64"))"#
r#"cargo::rustc-check-cfg=cfg(cpu_target, values("arm", "aarch64", "hexagon", "i386", "mips", "ppc", "riscv32", "riscv64", "x86_64"))"#
);

let emulation_mode = if cfg!(feature = "usermode") {
Expand Down Expand Up @@ -92,14 +92,18 @@ pub fn build() {
"mips".to_string()
} else if cfg!(feature = "ppc") {
"ppc".to_string()
} else if cfg!(feature = "riscv32") {
"riscv32".to_string()
} else if cfg!(feature = "riscv64") {
"riscv64".to_string()
} else if cfg!(feature = "hexagon") {
"hexagon".to_string()
} else {
env::var("CPU_TARGET").unwrap_or_else(|_| "x86_64".to_string())
};
println!("cargo:rerun-if-env-changed=CPU_TARGET");
println!("cargo:rustc-cfg=cpu_target=\"{cpu_target}\"");
println!("cargo::rustc-check-cfg=cfg(cpu_target, values(\"x86_64\", \"arm\", \"aarch64\", \"i386\", \"mips\", \"ppc\", \"hexagon\"))");
println!("cargo::rustc-check-cfg=cfg(cpu_target, values(\"x86_64\", \"arm\", \"aarch64\", \"i386\", \"mips\", \"ppc\", \"hexagon\", \"riscv32\", \"riscv64\"))");

let cross_cc = if cfg!(feature = "usermode") && (qemu_asan || qemu_asan_guest) {
// TODO try to autodetect a cross compiler with the arch name (e.g. aarch64-linux-gnu-gcc)
Expand Down
4 changes: 4 additions & 0 deletions libafl_qemu/libafl_qemu_build/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ pub fn generate(
bindings
.allowlist_type("ARMCPU")
.allowlist_type("ARMv7MState")
} else if cpu_target == "riscv32" || cpu_target == "riscv64" {
bindings
.allowlist_type("RISCVCPU")
.allowlist_type("CPURISCVState")
} else {
bindings
};
Expand Down
2 changes: 1 addition & 1 deletion libafl_qemu/libafl_qemu_build/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::cargo_add_rpath;

pub const QEMU_URL: &str = "https://github.com/AFLplusplus/qemu-libafl-bridge";
pub const QEMU_DIRNAME: &str = "qemu-libafl-bridge";
pub const QEMU_REVISION: &str = "805b14ffc44999952562e8f219d81c21a4fa50b9";
pub const QEMU_REVISION: &str = "c3c9c2128566ff325aa1a2bdcedde717f7d86e2c";

#[allow(clippy::module_name_repetitions)]
pub struct BuildResult {
Expand Down
1 change: 1 addition & 0 deletions libafl_qemu/libafl_qemu_build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ fn qemu_bindgen_clang_args(
let target_arch_dir = match cpu_target {
"x86_64" => format!("-I{}/target/i386", qemu_dir.display()),
"aarch64" => format!("-I{}/target/arm", qemu_dir.display()),
"riscv32" | "riscv64" => format!("-I{}/target/riscv", qemu_dir.display()),
_ => format!("-I{}/target/{cpu_target}", qemu_dir.display()),
};

Expand Down
2 changes: 2 additions & 0 deletions libafl_qemu/libafl_qemu_sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ aarch64 = [] # build qemu for aarch64
mips = [] # build qemu for mips (el, use with the 'be' feature of mips be)
ppc = [] # build qemu for powerpc
hexagon = [] # build qemu for hexagon
riscv32 = [] # build qemu for riscv 32bit
riscv64 = [] # build qemu for riscv 64bit

be = []

Expand Down
14 changes: 10 additions & 4 deletions libafl_qemu/libafl_qemu_sys/build_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ pub fn build() {

// Make sure we have at most one architecutre feature set
// Else, we default to `x86_64` - having a default makes CI easier :)
assert_unique_feature!("arm", "aarch64", "i386", "x86_64", "mips", "ppc", "hexagon");
assert_unique_feature!(
"arm", "aarch64", "i386", "x86_64", "mips", "ppc", "hexagon", "riscv32", "riscv64"
);

// Make sure that we don't have BE set for any architecture other than arm and mips
// Sure aarch64 may support BE, but its not in common usage and we don't
// need it yet and so haven't tested it
assert_unique_feature!("be", "aarch64", "i386", "x86_64", "hexagon");
assert_unique_feature!("be", "aarch64", "i386", "x86_64", "hexagon", "riscv32", "riscv64");

let cpu_target = if cfg!(feature = "x86_64") {
"x86_64".to_string()
Expand All @@ -60,20 +62,24 @@ pub fn build() {
"mips".to_string()
} else if cfg!(feature = "ppc") {
"ppc".to_string()
} else if cfg!(feature = "riscv32") {
"riscv32".to_string()
} else if cfg!(feature = "riscv64") {
"riscv64".to_string()
} else if cfg!(feature = "hexagon") {
"hexagon".to_string()
} else {
env::var("CPU_TARGET").unwrap_or_else(|_| {
println!(
"cargo:warning=No architecture feature enabled or CPU_TARGET env specified for libafl_qemu, supported: arm, aarch64, hexagon, i386, mips, ppc, x86_64 - defaulting to x86_64"
"cargo:warning=No architecture feature enabled or CPU_TARGET env specified for libafl_qemu, supported: arm, aarch64, hexagon, i386, mips, ppc, riscv32, riscv64, x86_64 - defaulting to x86_64"
);
"x86_64".to_string()
})
};
println!("cargo:rerun-if-env-changed=CPU_TARGET");
println!("cargo:rerun-if-env-changed=LIBAFL_QEMU_GEN_STUBS");
println!("cargo:rustc-cfg=cpu_target=\"{cpu_target}\"");
println!("cargo::rustc-check-cfg=cfg(cpu_target, values(\"x86_64\", \"arm\", \"aarch64\", \"i386\", \"mips\", \"ppc\", \"hexagon\"))");
println!("cargo::rustc-check-cfg=cfg(cpu_target, values(\"x86_64\", \"arm\", \"aarch64\", \"i386\", \"mips\", \"ppc\", \"hexagon\", \"riscv32\", \"riscv64\"))");

let jobs = env::var("NUM_JOBS")
.ok()
Expand Down
5 changes: 5 additions & 0 deletions libafl_qemu/src/arch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ pub use ppc::*;
pub mod hexagon;
#[cfg(cpu_target = "hexagon")]
pub use hexagon::*;

#[cfg(any(cpu_target = "riscv32", cpu_target = "riscv64"))]
pub mod riscv;
#[cfg(any(cpu_target = "riscv32", cpu_target = "riscv64"))]
pub use riscv::*;
159 changes: 159 additions & 0 deletions libafl_qemu/src/arch/riscv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use core::ffi::c_long;
use std::sync::OnceLock;

use capstone::arch::BuildsCapstone;
use enum_map::{enum_map, EnumMap};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use strum_macros::EnumIter;
#[cfg(feature = "riscv32")]
pub use syscall_numbers::riscv32::*;
#[cfg(feature = "riscv64")]
pub use syscall_numbers::riscv64::*;

// QEMU specific
#[allow(non_upper_case_globals)]
pub const SYS_syscalls: c_long = 447;
#[allow(non_upper_case_globals)]
pub const SYS_riscv_flush_icache: c_long = SYS_arch_specific_syscall + 15;
#[allow(non_upper_case_globals)]
pub const SYS_riscv_hwprobe: c_long = SYS_arch_specific_syscall + 14;

use crate::{sync_exit::ExitArgs, CallingConvention, QemuRWError, QemuRWErrorKind};

#[derive(IntoPrimitive, TryFromPrimitive, Debug, Clone, Copy, EnumIter)]
#[repr(i32)]
pub enum Regs {
Zero = 0, // x0: Hardwired zero
Ra = 1, // x1: Return address
Sp = 2, // x2: Stack pointer
Gp = 3, // x3: Global pointer
Tp = 4, // x4: Thread pointer
T0 = 5, // x5: Temporary register
T1 = 6, // x6: Temporary register
T2 = 7, // x7: Temporary register
FP = 8, // x8: Saved register / frame pointer
S1 = 9, // x9: Saved register
A0 = 10, // x10: Function argument / return value
A1 = 11, // x11: Function argument / return value
A2 = 12, // x12: Function argument
A3 = 13, // x13: Function argument
A4 = 14, // x14: Function argument
A5 = 15, // x15: Function argument
A6 = 16, // x16: Function argument
A7 = 17, // x17: Function argument
S2 = 18, // x18: Saved register
S3 = 19, // x19: Saved register
S4 = 20, // x20: Saved register
S5 = 21, // x21: Saved register
S6 = 22, // x22: Saved register
S7 = 23, // x23: Saved register
S8 = 24, // x24: Saved register
S9 = 25, // x25: Saved register
S10 = 26, // x26: Saved register
S11 = 27, // x27: Saved register
T3 = 28, // x28: Temporary register
T4 = 29, // x29: Temporary register
T5 = 30, // x30: Temporary register
T6 = 31, // x31: Temporary register
Pc = 32, // Program Counter (code pointer not actual register)
}

static EXIT_ARCH_REGS: OnceLock<EnumMap<ExitArgs, Regs>> = OnceLock::new();

pub fn get_exit_arch_regs() -> &'static EnumMap<ExitArgs, Regs> {
EXIT_ARCH_REGS.get_or_init(|| {
enum_map! {
ExitArgs::Ret => Regs::A0,
ExitArgs::Cmd => Regs::A0,
ExitArgs::Arg1 => Regs::A1,
ExitArgs::Arg2 => Regs::A2,
ExitArgs::Arg3 => Regs::A3,
ExitArgs::Arg4 => Regs::A4,
ExitArgs::Arg5 => Regs::A5,
ExitArgs::Arg6 => Regs::A6,
}
})
}

#[cfg(not(feature = "riscv64"))]
pub type GuestReg = u32;
#[cfg(feature = "riscv64")]
pub type GuestReg = u64;

/// Return a RISCV ArchCapstoneBuilder
pub fn capstone() -> capstone::arch::riscv::ArchCapstoneBuilder {
#[cfg(not(feature = "riscv64"))]
return capstone::Capstone::new()
.riscv()
.mode(capstone::arch::riscv::ArchMode::RiscV32);
#[cfg(feature = "riscv64")]
return capstone::Capstone::new()
.riscv()
.mode(capstone::arch::riscv::ArchMode::RiscV64);
}

impl crate::ArchExtras for crate::CPU {
fn read_return_address<T>(&self) -> Result<T, QemuRWError>
where
T: From<GuestReg>,
{
self.read_reg(Regs::Ra)
}

fn write_return_address<T>(&self, val: T) -> Result<(), QemuRWError>
where
T: Into<GuestReg>,
{
self.write_reg(Regs::Ra, val)
}

fn read_function_argument<T>(&self, conv: CallingConvention, idx: u8) -> Result<T, QemuRWError>
where
T: From<GuestReg>,
{
QemuRWError::check_conv(QemuRWErrorKind::Read, CallingConvention::Cdecl, conv)?;

// Note that 64 bit values may be passed in two registers (and are even-odd eg. A0, A2 and A3 where A1 is empty), then this mapping is off.
// Note: This does not consider the floating point registers.
// See https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf
let reg_id = match idx {
0 => Regs::A0, // argument / return value
1 => Regs::A1, // argument / return value
2 => Regs::A2, // argument value
3 => Regs::A3, // argument value
4 => Regs::A4, // argument value
5 => Regs::A5, // argument value
6 => Regs::A6, // argument value
7 => Regs::A7, // argument value
r => {
return Err(QemuRWError::new_argument_error(
QemuRWErrorKind::Read,
i32::from(r),
))
}
};

self.read_reg(reg_id)
}

fn write_function_argument<T>(
&self,
conv: CallingConvention,
idx: i32,
val: T,
) -> Result<(), QemuRWError>
where
T: Into<GuestReg>,
{
QemuRWError::check_conv(QemuRWErrorKind::Write, CallingConvention::Cdecl, conv)?;

let val: GuestReg = val.into();
match idx {
0 => self.write_reg(Regs::A0, val), // argument / return value
1 => self.write_reg(Regs::A1, val), // argument / return value
r => Err(QemuRWError::new_argument_error(QemuRWErrorKind::Write, r)),
}
}
}
4 changes: 4 additions & 0 deletions libafl_sugar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ mips = ["libafl_qemu/mips"]
ppc = ["libafl_qemu/ppc"]
## build qemu for hexagon
hexagon = ["libafl_qemu/hexagon"]
## build qemu for riscv 32bit
riscv32 = ["libafl_qemu/riscv32"]
## build qemu for riscv 64bit
riscv64 = ["libafl_qemu/riscv64"]

[build-dependencies]
pyo3-build-config = { version = "0.22.3", optional = true }
Expand Down

0 comments on commit 83c87ac

Please sign in to comment.