use std::fmt;
use std::ptr::NonNull;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ExecutableMemoryError {
ZeroSize,
AllocationFailed,
ProtectionChangeFailed,
Unsupported,
}
impl fmt::Display for ExecutableMemoryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ZeroSize => f.write_str("executable memory request was zero bytes"),
Self::AllocationFailed => {
f.write_str("operating system rejected the executable memory allocation")
}
Self::ProtectionChangeFailed => {
f.write_str("operating system rejected the W^X protection change")
}
Self::Unsupported => {
f.write_str("executable memory is not supported on this build target")
}
}
}
}
impl std::error::Error for ExecutableMemoryError {}
pub struct ExecutableMemory {
ptr: NonNull<u8>,
len: usize,
}
unsafe impl Send for ExecutableMemory {}
unsafe impl Sync for ExecutableMemory {}
impl fmt::Debug for ExecutableMemory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExecutableMemory")
.field("len", &self.len)
.finish()
}
}
impl ExecutableMemory {
pub fn new(code: &[u8]) -> Result<Self, ExecutableMemoryError> {
if code.is_empty() {
return Err(ExecutableMemoryError::ZeroSize);
}
imp::allocate(code).map(|(ptr, len)| Self { ptr, len })
}
#[inline]
pub fn as_ptr(&self) -> *const u8 {
self.ptr.as_ptr()
}
#[inline]
pub fn as_mut_ptr(&self) -> *mut u8 {
self.ptr.as_ptr()
}
#[inline]
pub fn len(&self) -> usize {
self.len
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn as_bytes(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}
}
impl Drop for ExecutableMemory {
fn drop(&mut self) {
unsafe { imp::release(self.ptr, self.len) };
}
}
#[cfg(unix)]
mod imp {
use super::ExecutableMemoryError;
use std::ptr::NonNull;
pub(super) fn allocate(code: &[u8]) -> Result<(NonNull<u8>, usize), ExecutableMemoryError> {
let len = code.len();
let mem = unsafe {
libc::mmap(
std::ptr::null_mut(),
len,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
if mem == libc::MAP_FAILED {
return Err(ExecutableMemoryError::AllocationFailed);
}
unsafe {
std::ptr::copy_nonoverlapping(code.as_ptr(), mem.cast::<u8>(), len);
}
let rc = unsafe { libc::mprotect(mem, len, libc::PROT_READ | libc::PROT_EXEC) };
if rc != 0 {
unsafe {
libc::munmap(mem, len);
}
return Err(ExecutableMemoryError::ProtectionChangeFailed);
}
let ptr = unsafe { NonNull::new_unchecked(mem.cast::<u8>()) };
Ok((ptr, len))
}
pub(super) unsafe fn release(ptr: NonNull<u8>, len: usize) {
unsafe {
libc::munmap(ptr.as_ptr().cast(), len);
}
}
}
#[cfg(all(target_arch = "x86_64", windows))]
mod imp {
use super::ExecutableMemoryError;
use std::ptr::NonNull;
use windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache;
use windows_sys::Win32::System::Memory::{
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_EXECUTE_READ, PAGE_READWRITE, VirtualAlloc,
VirtualFree, VirtualProtect,
};
use windows_sys::Win32::System::Threading::GetCurrentProcess;
pub(super) fn allocate(code: &[u8]) -> Result<(NonNull<u8>, usize), ExecutableMemoryError> {
let len = code.len();
let mem = unsafe {
VirtualAlloc(
std::ptr::null(),
len,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
)
};
if mem.is_null() {
return Err(ExecutableMemoryError::AllocationFailed);
}
unsafe {
std::ptr::copy_nonoverlapping(code.as_ptr(), mem.cast::<u8>(), len);
}
let mut old_protect: u32 = 0;
let ok = unsafe { VirtualProtect(mem, len, PAGE_EXECUTE_READ, &mut old_protect) };
if ok == 0 {
unsafe {
VirtualFree(mem, 0, MEM_RELEASE);
}
return Err(ExecutableMemoryError::ProtectionChangeFailed);
}
unsafe {
FlushInstructionCache(GetCurrentProcess(), mem, len);
}
let ptr = unsafe { NonNull::new_unchecked(mem.cast::<u8>()) };
Ok((ptr, len))
}
pub(super) unsafe fn release(ptr: NonNull<u8>, _len: usize) {
unsafe {
VirtualFree(ptr.as_ptr().cast(), 0, MEM_RELEASE);
}
}
}
#[cfg(not(any(unix, all(target_arch = "x86_64", windows))))]
mod imp {
use super::ExecutableMemoryError;
use std::ptr::NonNull;
pub(super) fn allocate(_code: &[u8]) -> Result<(NonNull<u8>, usize), ExecutableMemoryError> {
Err(ExecutableMemoryError::Unsupported)
}
pub(super) unsafe fn release(_ptr: NonNull<u8>, _len: usize) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_buffer_is_rejected() {
assert_eq!(
ExecutableMemory::new(&[]).unwrap_err(),
ExecutableMemoryError::ZeroSize
);
}
#[cfg(not(any(unix, all(target_arch = "x86_64", windows))))]
#[test]
fn unsupported_targets_report_unsupported() {
let buf = [0u8; 4];
assert_eq!(
ExecutableMemory::new(&buf).unwrap_err(),
ExecutableMemoryError::Unsupported
);
}
#[cfg(any(unix, all(target_arch = "x86_64", windows)))]
#[test]
fn allocates_and_exposes_code_bytes() {
let payload: [u8; 4] = [0xC3, 0x90, 0x90, 0x90];
let mem = ExecutableMemory::new(&payload).expect("allocation must succeed");
assert_eq!(mem.len(), payload.len());
assert!(!mem.is_empty());
assert_eq!(mem.as_bytes(), &payload);
assert!(!mem.as_ptr().is_null());
}
#[cfg(all(target_arch = "x86_64", any(unix, windows)))]
#[test]
fn executes_tiny_x86_64_blob() {
let code: [u8; 6] = [0xB8, 0x2A, 0x00, 0x00, 0x00, 0xC3];
let mem = ExecutableMemory::new(&code).expect("allocation must succeed");
let result = unsafe {
let f: extern "C" fn() -> u32 = std::mem::transmute(mem.as_ptr());
f()
};
assert_eq!(result, 42);
}
}