diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index cb56a7e..081bcc4 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -1,11 +1,11 @@ name: C# Build, Test and Publish on: push: - branches: [ master, refactor-reloaded3-compliance ] + branches: [ master, refactor-reloaded3-compliance, support-android-and-bsd ] tags: - '*' pull_request: - branches: [ master, refactor-reloaded3-compliance ] + branches: [ master, refactor-reloaded3-compliance, support-android-and-bsd ] workflow_dispatch: jobs: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0d441d2..20ef45d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,11 +2,11 @@ name: Rust Build, Test & Publish on: push: - branches: [ main, master, crab ] + branches: [ main, master, crab, support-android-and-bsd ] tags: - '*' pull_request: - branches: [ main, master, crab ] + branches: [ main, master, crab, support-android-and-bsd ] workflow_dispatch: jobs: diff --git a/README.md b/README.md index ac28cee..7aa43c8 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ With the following properties: - ***Large Address Aware:*** On Windows, the library can correctly leverage all 4GB in 32-bit processes. - ***Cross Platform***: Supports Windows, OSX and Linux. +Note: Rust/C port also work with FreeBSD (untested), and has partial [(limited) Android support](https://github.com/Reloaded-Project/Reloaded.Memory.Buffers/issues/3). + ## Wiki & Documentation [For full documentation, please see the Wiki](https://reloaded-project.github.io/Reloaded.Memory.Buffers/). diff --git a/docs/index.md b/docs/index.md index 0be5fb1..f242aee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -46,6 +46,8 @@ With the following properties: - ***Large Address Aware:*** On Windows, the library can correctly leverage all 4GB in 32-bit processes. - ***Cross Platform***: Supports Windows, OSX and Linux. +Note: Rust/C port also works with FreeBSD (untested), and has partial [(limited) Android support](https://github.com/Reloaded-Project/Reloaded.Memory.Buffers/issues/3). + ## Example Use Cases !!! tip "These are just examples." @@ -161,11 +163,12 @@ With the following properties: free_get_buffer_result(result); ``` +!!! note "Use `append_code` instead of `append_bytes` if you need to add executable code. (Currently unavailable in C# port)" + ### Allocate Memory !!! info "Allows you to temporarily allocate memory within a specific address range and size constraints." - === "C#" ```csharp @@ -215,6 +218,32 @@ With the following properties: !!! note "You can specify another process with `TargetProcess = someProcess` in `BufferAllocatorSettings`, but this is only supported on Windows." +### Overwriting Allocated Instructions + +!!! info "On non-x86 architectures, some extra actions may be needed when overwriting executable code allocated with `append_code`." + +!!! note "This involves clearing instruction cache, and abiding by Write XOR Execute restrictions." + +=== "Rust" + + ```rust + Self::overwrite_allocated_code(address, size, |addr, size| { + // Do stuff with executable code + }); + ``` + +=== "C/C++" + + ```cpp + void do_stuff_with_executable_code(char* addr, size_t size) { + // Modify executable code in buffer + } + + overwrite_allocated_code(address, size, do_stuff_with_executable_code); + ``` + +!!! warning "Not currently available in C# version. Submit an issue request or PR if you need this." + ## Community Feedback If you have questions/bug reports/etc. feel free to [Open an Issue](https://github.com/Reloaded-Project/Reloaded.Memory.Buffers/issues/new). diff --git a/src-rust/Cargo.lock b/src-rust/Cargo.lock index 434c8d5..fa92e2b 100644 --- a/src-rust/Cargo.lock +++ b/src-rust/Cargo.lock @@ -101,6 +101,18 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + [[package]] name = "cast" version = "0.3.0" @@ -171,6 +183,25 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "clf" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fdb46ea404b842f573950b923aded875227defcb556011fcab4ac9bcd214c49" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "cpp_demangle" version = "0.4.2" @@ -246,7 +277,7 @@ dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.9.0", "scopeguard", ] @@ -295,11 +326,23 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "errno" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -393,7 +436,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.25", ] [[package]] @@ -467,6 +510,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.2" @@ -605,6 +654,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.5.0" @@ -620,6 +678,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -638,6 +705,23 @@ dependencies = [ "adler", ] +[[package]] +name = "mmap-rs-with-map-from-existing" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1034526ca11b67736c66254c4e0345b6b322f3c377c0cdc31f1cb4252524167" +dependencies = [ + "bitflags 1.3.2", + "combine", + "libc", + "mach2", + "nix", + "sysctl", + "thiserror", + "widestring", + "windows", +] + [[package]] name = "nix" version = "0.26.2" @@ -647,6 +731,8 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", + "memoffset 0.7.1", + "pin-utils", "static_assertions", ] @@ -771,9 +857,9 @@ dependencies = [ [[package]] name = "pprof" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b90f8560ad8bd57b207b8293bc5226e48e89039a6e590c12a297d91b84c7e60" +checksum = "ef5c97c51bd34c7e742402e216abdeb44d415fbe6ae41d56b114723e953711cb" dependencies = [ "backtrace", "cfg-if", @@ -906,15 +992,17 @@ checksum = "4bf2521270932c3c7bed1a59151222bd7643c79310f2916f01925e1e16255698" [[package]] name = "reloaded-memory-buffers" -version = "3.1.3" +version = "3.2.0" dependencies = [ + "clf", "criterion", "dirs", "errno", "lazy_static", "libc", "mach", - "memoffset", + "memoffset 0.9.0", + "mmap-rs-with-map-from-existing", "pprof", "rstest", "windows", @@ -954,7 +1042,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn", + "syn 2.0.25", "unicode-ident", ] @@ -1044,7 +1132,7 @@ checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.25", ] [[package]] @@ -1114,6 +1202,17 @@ dependencies = [ "symbolic-common", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.25" @@ -1125,6 +1224,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysctl" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed66d6a2ccbd656659289bc90767895b7abbdec897a0fc6031aca3ed1cb51d3e" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "enum-as-inner", + "libc", + "thiserror", + "walkdir", +] + [[package]] name = "tempfile" version = "3.6.0" @@ -1156,7 +1269,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.25", ] [[package]] @@ -1224,7 +1337,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.25", "wasm-bindgen-shared", ] @@ -1246,7 +1359,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.25", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1267,6 +1380,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "winapi" version = "0.3.9" diff --git a/src-rust/Cargo.toml b/src-rust/Cargo.toml index 7ef2b61..85cf6d5 100644 --- a/src-rust/Cargo.toml +++ b/src-rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reloaded-memory-buffers" -version = "3.1.3" +version = "3.2.0" edition = "2021" authors = [ "sewer56" ] description = "Shared, Concurrent, Permanent Memory Allocator tied to Process Lifetime" @@ -17,7 +17,7 @@ crate-type = ["cdylib", "staticlib"] memoffset = "0.9.0" lazy_static = "1.4.0" dirs = "5.0.1" -errno = "0.3.1" +errno = "0.3.3" [dev-dependencies] rstest = "0.18.1" @@ -26,12 +26,16 @@ criterion = "0.5.1" [target.'cfg(unix)'.dependencies] libc = "0.2.146" -[target.'cfg(unix)'.dev-dependencies] -pprof = { version = "0.12", features = ["flamegraph", "criterion"] } +[target.'cfg(all(unix, not(target_os = "android")))'.dev-dependencies] +pprof = { version = "0.13", features = ["flamegraph", "criterion"] } [target.'cfg(target_os = "macos")'.dependencies] mach = "0.3.2" +[target.'cfg(not(target_os = "windows"))'.dependencies] +mmap-rs-with-map-from-existing = "0.6.0" +clf = "0.1.7" + [target.'cfg(target_os = "windows")'.dependencies.windows] version = "0.48.0" features = [ diff --git a/src-rust/benches/my_benchmark.rs b/src-rust/benches/my_benchmark.rs index 87cde66..edca65a 100644 --- a/src-rust/benches/my_benchmark.rs +++ b/src-rust/benches/my_benchmark.rs @@ -1,3 +1,5 @@ +#![cfg(not(target_os = "android"))] + use criterion::{criterion_group, criterion_main, Criterion}; #[cfg(not(target_os = "windows"))] diff --git a/src-rust/src/buffers.rs b/src-rust/src/buffers.rs index 9370080..de9ca5e 100644 --- a/src-rust/src/buffers.rs +++ b/src-rust/src/buffers.rs @@ -4,7 +4,12 @@ use crate::structs::errors::{BufferAllocationError, BufferSearchError, ItemAlloc use crate::structs::internal::LocatorHeader; use crate::structs::params::{BufferAllocatorSettings, BufferSearchSettings}; use crate::structs::{PrivateAllocation, SafeLocatorItem}; +use crate::utilities::disable_write_xor_execute::{ + disable_write_xor_execute, restore_write_xor_execute, +}; +use crate::utilities::icache_clear::clear_instruction_cache; use crate::utilities::mathematics::round_up; +use core::u8; use std::ptr::NonNull; pub struct Buffers {} @@ -112,6 +117,38 @@ impl Buffers { ) -> Result { unsafe { Self::get_buffer_recursive(settings, LocatorHeaderFinder::find()) } } + + /// Call this method in order to safely be able to overwrite existing code that was + /// allocated by the library inside one of its buffers. (e.g. Hooking/detours code.) + /// + /// This callback handles various edge cases, (such as flushing caches), and flipping page permissions + /// on relevant platforms. + /// + /// # Parameters + /// + /// * `address` - The address of the code your callback will overwrite. + /// * `size` - The size of the code your callback will overwrite. + /// * `callback` - Your method to overwrite the code. + /// + /// # Safety + /// + /// Only use this with addresses allocated inside a Reloaded.Memory.Buffers buffer. + /// Usage with any other memory is undefined behaviour. + /// + /// # Remarks + /// + /// This function can be skipped on some combinations (e.g. Windows/Linux/macOS x86/x64). But + /// should not be skipped on non-x86 architectures. + pub fn overwrite_allocated_code( + address: *const u8, + size: usize, + callback: fn(*const u8, usize), + ) { + disable_write_xor_execute(address, size); + callback(address, size); + restore_write_xor_execute(address, size); + clear_instruction_cache(address as *mut u8, address.wrapping_add(size)); + } } impl Buffers { @@ -181,7 +218,6 @@ mod tests { assert!(result.is_ok()); let item = result.unwrap(); - assert!(!item.base_address.as_ptr().is_null()); assert!(item.size >= settings.size as usize); } @@ -195,7 +231,6 @@ mod tests { assert!(result.is_ok()); let item = result.unwrap(); - assert!(!item.base_address.as_ptr().is_null()); assert!(item.size >= settings.size as usize); } @@ -220,7 +255,7 @@ mod tests { #[cfg(target_arch = "x86_64")] #[test] - fn memory_is_executable() { + fn memory_is_executable_x64() { let settings = BufferSearchSettings { min_address: (CACHED.max_address / 2), max_address: CACHED.max_address, @@ -238,7 +273,40 @@ mod tests { unsafe { let code_ptr = (*item.item.get()).base_address.value as *mut u8; - item.append_bytes(&code); + item.append_code(&code); + + // Cast the buffer to a function pointer and execute it. + let func: extern "C" fn() -> u64 = std::mem::transmute(code_ptr); + + // If the memory is executable, this will return 0x1234567812345678. + let result = func(); + assert_eq!(result, 0x1234567812345678); + } + } + + #[cfg(target_arch = "aarch64")] + #[test] + fn memory_is_executable_aarch64() { + let settings = BufferSearchSettings { + min_address: (CACHED.max_address / 2), + max_address: CACHED.max_address, + size: 4096, + }; + + let item = Buffers::get_buffer(&settings).unwrap(); + + // Prepare a simple piece of x86_64 code: `mov rax, 0x1234567812345678; ret` + let code = [ + 0x00, 0xCF, 0x8A, 0xD2, // movz x0, #0x5678, LSL #0 + 0x80, 0x46, 0xA2, 0xF2, // movk x0, #0x1234, LSL #16 + 0x00, 0xCF, 0xCA, 0xF2, // movk x0, #0x5678, LSL #32 + 0x80, 0x46, 0xE2, 0xF2, // movk x0, #0x1234, LSL #48 + 0xC0, 0x03, 0x5F, 0xD6, // ret + ]; + + unsafe { + let code_ptr = (*item.item.get()).base_address.value as *mut u8; + item.append_code(&code); // Cast the buffer to a function pointer and execute it. let func: extern "C" fn() -> u64 = std::mem::transmute(code_ptr); diff --git a/src-rust/src/c/buffers_c_buffers.rs b/src-rust/src/c/buffers_c_buffers.rs index b4f0187..2a67e93 100644 --- a/src-rust/src/c/buffers_c_buffers.rs +++ b/src-rust/src/c/buffers_c_buffers.rs @@ -11,6 +11,10 @@ use crate::{ params::{BufferAllocatorSettings, BufferSearchSettings}, PrivateAllocation, }, + utilities::{ + disable_write_xor_execute::{disable_write_xor_execute, restore_write_xor_execute}, + icache_clear::clear_instruction_cache, + }, }; use super::{ @@ -259,6 +263,49 @@ pub extern "C" fn bufferallocatorsettings_from_proximity( BufferAllocatorSettings::from_proximity(proximity, target, size) } +/// Clears the instruction cache for the specified range. +/// +/// # Arguments +/// +/// * `start` - The start address of the range to clear. +/// * `end` - The end address of the range to clear. +#[no_mangle] +pub extern "C" fn utilities_clear_instruction_cache(start: *mut u8, end: *mut u8) { + clear_instruction_cache(start, end); +} + +/// Call this method in order to safely be able to overwrite existing code that was +/// allocated by the library inside one of its buffers. (e.g. Hooking/detours code.) +/// +/// This callback handles various edge cases, (such as flushing caches), and flipping page permissions +/// on relevant platforms. +/// +/// # Parameters +/// +/// * `address` - The address of the code your callback will overwrite. +/// * `size` - The size of the code your callback will overwrite. +/// * `callback` - Your method to overwrite the code. +/// +/// # Safety +/// +/// Only use this with addresses allocated inside a Reloaded.Memory.Buffers buffer. +/// Usage with any other memory is undefined behaviour. +/// +/// # Remarks +/// +/// This function can be skipped on some combinations (e.g. Windows/Linux/macOS x86/x64). But +/// should not be skipped on non-x86 architectures. +pub extern "C" fn overwrite_allocated_code( + address: *const u8, + size: usize, + callback: extern "C" fn(*const u8, usize), +) { + disable_write_xor_execute(address, size); + callback(address, size); + restore_write_xor_execute(address, size); + clear_instruction_cache(address as *mut u8, address.wrapping_add(size)); +} + /// Returns all exported functions inside a struct. #[no_mangle] pub extern "C" fn get_functions() -> BuffersFunctions { @@ -283,6 +330,8 @@ pub extern "C" fn get_functions() -> BuffersFunctions { locatoritem_unlock, locatoritem_can_use, locatoritem_append_bytes, + utilities_clear_instruction_cache, + overwrite_allocated_code, } } @@ -313,7 +362,6 @@ mod tests { let result = buffers_allocate_private_memory(&mut settings); assert!(result.is_ok); - assert!(!result.ok.base_address.as_ptr().is_null()); assert!(result.ok.size >= settings.size as usize); free_allocation_result(result); } @@ -327,7 +375,6 @@ mod tests { let result = buffers_allocate_private_memory(&mut settings); assert!(result.is_ok); - assert!(!result.ok.base_address.as_ptr().is_null()); assert!(result.ok.size >= settings.size as usize); free_allocation_result(result); } diff --git a/src-rust/src/c/buffers_fnptr.rs b/src-rust/src/c/buffers_fnptr.rs index ec3fc7e..dc708a7 100644 --- a/src-rust/src/c/buffers_fnptr.rs +++ b/src-rust/src/c/buffers_fnptr.rs @@ -186,4 +186,36 @@ pub struct BuffersFunctions { /// There is no error thrown if size is insufficient. pub locatoritem_append_bytes: unsafe extern "C" fn(item: *mut LocatorItem, data: *const u8, data_len: usize) -> usize, + + /// Clears the instruction cache for the specified range. + /// + /// # Arguments + /// + /// * `start` - The start address of the range to clear. + /// * `end` - The end address of the range to clear. + pub utilities_clear_instruction_cache: unsafe extern "C" fn(start: *mut u8, end: *mut u8), + + /// Call this method in order to safely be able to overwrite existing code that was + /// allocated by the library inside one of its buffers. (e.g. Hooking/detours code.) + /// + /// This callback handles various edge cases, (such as flushing caches), and flipping page permissions + /// on relevant platforms. + /// + /// # Parameters + /// + /// * `address` - The address of the code your callback will overwrite. + /// * `size` - The size of the code your callback will overwrite. + /// * `callback` - Your method to overwrite the code. + /// + /// # Safety + /// + /// Only use this with addresses allocated inside a Reloaded.Memory.Buffers buffer. + /// Usage with any other memory is undefined behaviour. + /// + /// # Remarks + /// + /// This function can be skipped on some combinations (e.g. Windows/Linux/macOS x86/x64). But + /// should not be skipped on non-x86 architectures. + pub overwrite_allocated_code: + extern "C" fn(address: *const u8, size: usize, callback: extern "C" fn(*const u8, usize)), } diff --git a/src-rust/src/internal/buffer_allocator.rs b/src-rust/src/internal/buffer_allocator.rs index 392c1a9..f2c492b 100644 --- a/src-rust/src/internal/buffer_allocator.rs +++ b/src-rust/src/internal/buffer_allocator.rs @@ -19,6 +19,10 @@ pub fn allocate( #[cfg(target_os = "macos")] return crate::internal::buffer_allocator_osx::allocate_osx(settings); + + // Fallback for non-hot-path OSes. + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + crate::internal::buffer_allocator_mmap_rs::allocate_mmap_rs(settings) } pub unsafe fn get_possible_buffer_addresses( @@ -328,7 +332,7 @@ mod tests { #[cfg(target_os = "windows")] free_windows(item); - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(unix)] free_libc(item); } @@ -340,7 +344,7 @@ mod tests { assert!(success); } - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(unix)] fn free_libc(item: LocatorItem) { unsafe { libc::munmap(item.base_address.value as *mut c_void, item.size as usize); diff --git a/src-rust/src/internal/buffer_allocator_linux.rs b/src-rust/src/internal/buffer_allocator_linux.rs index 090adca..b626040 100644 --- a/src-rust/src/internal/buffer_allocator_linux.rs +++ b/src-rust/src/internal/buffer_allocator_linux.rs @@ -1,9 +1,12 @@ -use crate::internal::buffer_allocator::get_possible_buffer_addresses; use crate::structs::errors::BufferAllocationError; use crate::structs::internal::LocatorItem; use crate::structs::params::BufferAllocatorSettings; use crate::utilities::cached::CACHED; -use crate::utilities::linux_map_parser::{get_free_regions_from_process_id, MemoryMapEntry}; +use crate::utilities::linux_map_parser::get_free_regions_from_process_id; +use crate::{ + internal::buffer_allocator::get_possible_buffer_addresses, + utilities::map_parser_utilities::MemoryMapEntry, +}; use libc::{ mmap, munmap, MAP_ANONYMOUS, MAP_FIXED_NOREPLACE, MAP_PRIVATE, PROT_EXEC, PROT_READ, PROT_WRITE, }; diff --git a/src-rust/src/internal/buffer_allocator_mmap_rs.rs b/src-rust/src/internal/buffer_allocator_mmap_rs.rs new file mode 100644 index 0000000..7963c0c --- /dev/null +++ b/src-rust/src/internal/buffer_allocator_mmap_rs.rs @@ -0,0 +1,100 @@ +use crate::structs::errors::BufferAllocationError; +use crate::structs::internal::LocatorItem; +use crate::structs::params::BufferAllocatorSettings; +use crate::utilities::cached::CACHED; +use crate::utilities::map_parser_utilities::get_free_regions; +use crate::{ + internal::buffer_allocator::get_possible_buffer_addresses, + utilities::map_parser_utilities::MemoryMapEntry, +}; +use core::cmp::min; +use core::mem; +use mmap_rs_with_map_from_existing::{MemoryAreas, MmapOptions, UnsafeMmapFlags}; + +// Implementation // +pub fn allocate_mmap_rs( + settings: &BufferAllocatorSettings, +) -> Result { + for _ in 0..settings.retry_count { + let maps = MemoryAreas::open(None).map_err(|_x| BufferAllocationError { + settings: *settings, + text: "Failed to Query Memory Pages via mmap-rs. Probably unsupported or lacking permissions.", + })?; + + let mapped_regions: Vec = maps + .filter(|x| x.is_ok()) + .map( + |x: Result< + mmap_rs_with_map_from_existing::MemoryArea, + mmap_rs_with_map_from_existing::Error, + >| unsafe { + let area = x.unwrap_unchecked(); + MemoryMapEntry::new(area.start(), area.end()) + }, + ) + .collect(); + + let free_regions = get_free_regions(&mapped_regions); + + for region in free_regions { + if region.start_address > settings.max_address { + break; + } + + unsafe { + match try_allocate_buffer(®ion, settings) { + Ok(item) => return Ok(item), + Err(_) => continue, + } + } + } + } + + Err(BufferAllocationError::new( + *settings, + "Failed to allocate buffer on Linux", + )) +} + +unsafe fn try_allocate_buffer( + entry: &MemoryMapEntry, + settings: &BufferAllocatorSettings, +) -> Result { + let buffer: &mut [usize; 4] = &mut [0; 4]; + + for addr in get_possible_buffer_addresses( + settings.min_address, + settings.max_address, + entry.start_address, + entry.end_address, + settings.size as usize, + CACHED.get_allocation_granularity() as usize, + buffer, + ) { + let mmapoptions = MmapOptions::new(settings.size as usize) + .map_err(|_x| "Failed to create mmap options")? + .with_address(*addr) + .with_unsafe_flags(UnsafeMmapFlags::MAP_FIXED) + .with_unsafe_flags(UnsafeMmapFlags::JIT); + + let map: Result< + mmap_rs_with_map_from_existing::MmapMut, + mmap_rs_with_map_from_existing::Error, + > = unsafe { mmapoptions.map_exec_mut() }; + if map.is_err() { + continue; + } + + let mapped = map.unwrap(); + let mapped_addr = mapped.start(); + + if mapped.start() != *addr { + continue; // dropped + } + + mem::forget(mapped); + return Ok(LocatorItem::new(mapped_addr, settings.size)); + } + + Err("Failed to allocate buffer") +} diff --git a/src-rust/src/internal/buffer_allocator_osx.rs b/src-rust/src/internal/buffer_allocator_osx.rs index 4357809..43a1962 100644 --- a/src-rust/src/internal/buffer_allocator_osx.rs +++ b/src-rust/src/internal/buffer_allocator_osx.rs @@ -132,13 +132,25 @@ fn try_allocate_buffer( continue; } + // TODO: M1 W^X + // M1 macOS has strict W^X enforcement where pages are not allowed to be writeable + // and executable at the same time. Therefore, we have to work around this by allocating as RW + // and temporarily changing it on every write. + + // This is not safe, but later we'll get a better workaround going. + #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] + const PROT: vm_prot_t = VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE; + + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + const PROT: vm_prot_t = VM_PROT_READ | VM_PROT_WRITE; + kr = unsafe { mach_vm_protect( self_task, allocated, settings.size as mach_vm_size_t, 0, - VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE, + PROT, ) }; diff --git a/src-rust/src/internal/locator_header_finder.rs b/src-rust/src/internal/locator_header_finder.rs index e4ff011..120f082 100644 --- a/src-rust/src/internal/locator_header_finder.rs +++ b/src-rust/src/internal/locator_header_finder.rs @@ -5,11 +5,11 @@ use lazy_static::lazy_static; use std::ptr::null_mut; use std::sync::Mutex; -#[cfg(any(target_os = "linux", target_os = "macos"))] +#[cfg(unix)] use { super::memory_mapped_file_unix::BASE_DIR, - crate::internal::memory_mapped_file_unix::UnixMemoryMappedFile, libc::kill, std::fs, - std::path::Path, + crate::internal::memory_mapped_file_unix::UnixMemoryMappedFile, errno::errno, libc::kill, + std::fs, std::path::Path, }; #[cfg(target_os = "windows")] @@ -39,31 +39,12 @@ impl LocatorHeaderFinder { // Lock initial acquisiton. This is so we don't create two buffers at once. let _unused = GLOBAL_LOCK.lock().unwrap(); - let mmf = LocatorHeaderFinder::open_or_create_memory_mapped_file(); - // If the MMF previously existed, we need to read the real address from - // the header, then close our mapping. - if mmf.already_existed() { - let header_addr = (*mmf).data() as *mut LocatorHeader; - LOCATOR_HEADER_ADDRESS = (*header_addr).this_address.value; + #[cfg(target_os = "android")] + return init_locatorheader_memorymappedfiles_unsupported(); - #[cfg(test)] - LocatorHeaderFinder::set_last_find_reason(FindReason::PreviouslyExisted); - - return unsafe { LOCATOR_HEADER_ADDRESS }; - } - - // Otherwise, we got a new MMF going, keep it alive forever. - #[cfg(any(target_os = "linux", target_os = "macos"))] - LocatorHeaderFinder::cleanup(); - - LOCATOR_HEADER_ADDRESS = mmf.data().cast(); - (*LOCATOR_HEADER_ADDRESS).initialize(mmf.length()); - MMF = Some(mmf); - - #[cfg(test)] - LocatorHeaderFinder::set_last_find_reason(FindReason::Created); - LOCATOR_HEADER_ADDRESS + #[cfg(not(target_os = "android"))] + return init_locatorheader_standard(); // OSes with unsupported Memory Mapped Files } fn open_or_create_memory_mapped_file() -> Box { @@ -77,14 +58,11 @@ impl LocatorHeaderFinder { CACHED.allocation_granularity as usize, )); - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(unix)] return Box::new(UnixMemoryMappedFile::new( &name, CACHED.allocation_granularity as usize, )); - - #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] - panic!("This platform is not supported! Only Windows/Linux/macOS are supported."); } #[cfg(test)] @@ -98,7 +76,7 @@ impl LocatorHeaderFinder { LAST_FIND_REASON = reason; } - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(unix)] fn cleanup() { LocatorHeaderFinder::cleanup_posix(BASE_DIR, |path| { if let Err(err) = fs::remove_file(path) { @@ -107,7 +85,7 @@ impl LocatorHeaderFinder { }); } - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(unix)] fn cleanup_posix(mmf_directory: &str, mut delete_file: T) where T: FnMut(&Path), @@ -138,18 +116,63 @@ impl LocatorHeaderFinder { } } - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(unix)] fn is_process_running(pid: i32) -> bool { unsafe { - #[cfg(target_os = "macos")] - return kill(pid, 0) == 0 || *libc::__error() == libc::EPERM; - - #[cfg(target_os = "linux")] - return kill(pid, 0) == 0 || *libc::__errno_location() == libc::EPERM; + #[cfg(unix)] + return kill(pid, 0) == 0 || errno().0 == libc::EPERM; } } } +unsafe fn init_locatorheader_standard() -> *mut LocatorHeader { + let mmf = LocatorHeaderFinder::open_or_create_memory_mapped_file(); + + // If the MMF previously existed, we need to read the real address from + // the header, then close our mapping. + if mmf.already_existed() { + let header_addr = (*mmf).data() as *mut LocatorHeader; + LOCATOR_HEADER_ADDRESS = (*header_addr).this_address.value; + + #[cfg(test)] + LocatorHeaderFinder::set_last_find_reason(FindReason::PreviouslyExisted); + + return unsafe { LOCATOR_HEADER_ADDRESS }; + } + + // Otherwise, we got a new MMF going, keep it alive forever. + #[cfg(unix)] + LocatorHeaderFinder::cleanup(); + + LOCATOR_HEADER_ADDRESS = mmf.data().cast(); + (*LOCATOR_HEADER_ADDRESS).initialize(mmf.length()); + MMF = Some(mmf); + + #[cfg(test)] + LocatorHeaderFinder::set_last_find_reason(FindReason::Created); + LOCATOR_HEADER_ADDRESS +} + +#[cfg(target_os = "android")] +unsafe fn init_locatorheader_memorymappedfiles_unsupported() -> *mut LocatorHeader { + use core::mem; + use mmap_rs_with_map_from_existing::MmapOptions; + + let mmap = MmapOptions::new(MmapOptions::allocation_granularity()) + .unwrap() + .map_mut() + .unwrap(); + + LOCATOR_HEADER_ADDRESS = mmap.start() as *mut LocatorHeader; + (*LOCATOR_HEADER_ADDRESS).initialize(mmap.size()); + + mem::forget(mmap); + + #[cfg(test)] + LocatorHeaderFinder::set_last_find_reason(FindReason::Created); + LOCATOR_HEADER_ADDRESS +} + #[cfg(test)] #[derive(Debug, PartialEq, Copy, Clone)] pub(crate) enum FindReason { @@ -168,6 +191,7 @@ mod tests { use crate::utilities::cached::CACHED; #[test] + #[cfg(not(target_os = "android"))] fn find_should_return_address_when_previously_exists() { unsafe { LocatorHeaderFinder::reset(); diff --git a/src-rust/src/internal/memory_mapped_file_unix.rs b/src-rust/src/internal/memory_mapped_file_unix.rs index 45f1f9e..5782f02 100644 --- a/src-rust/src/internal/memory_mapped_file_unix.rs +++ b/src-rust/src/internal/memory_mapped_file_unix.rs @@ -1,7 +1,7 @@ use errno::errno; use libc::{ - c_int, close, ftruncate, mmap, munmap, open, MAP_SHARED, O_CREAT, O_RDWR, PROT_EXEC, PROT_READ, + c_int, close, ftruncate, mmap, munmap, open, MAP_SHARED, O_CREAT, O_RDWR, PROT_READ, PROT_WRITE, S_IRWXU, }; use std::ffi::{c_void, CString}; @@ -12,8 +12,12 @@ use libc::c_uint; use crate::internal::memory_mapped_file::MemoryMappedFile; +#[cfg(not(target_os = "android"))] pub const BASE_DIR: &str = "/tmp/.reloaded/memory.buffers"; +#[cfg(target_os = "android")] // needs storage permission, no idea if it will even allow it though +pub const BASE_DIR: &str = "/sdcard/.reloaded/memory.buffers"; + pub struct UnixMemoryMappedFile { pub file_descriptor: i32, pub already_existed: bool, @@ -38,10 +42,10 @@ impl UnixMemoryMappedFile { let dir = Path::new(new_name.as_str()).parent().unwrap(); std::fs::create_dir_all(dir).unwrap(); - #[cfg(target_os = "linux")] - Self::open_linux(file_name, &mut file_descriptor); + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + Self::open_unix(file_name, &mut file_descriptor); - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "ios"))] Self::open_macos(file_name, &mut file_descriptor); if file_descriptor == -1 { @@ -59,7 +63,7 @@ impl UnixMemoryMappedFile { mmap( std::ptr::null_mut::(), length, - PROT_READ | PROT_WRITE | PROT_EXEC, + PROT_READ | PROT_WRITE, MAP_SHARED, file_descriptor, 0, @@ -80,13 +84,14 @@ impl UnixMemoryMappedFile { } } - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "ios"))] fn open_macos(file_name: CString, x: &mut c_int) { unsafe { *x = open(file_name.as_ptr(), O_RDWR | O_CREAT, S_IRWXU as c_uint) } } - #[cfg(target_os = "linux")] - fn open_linux(file_name: CString, x: &mut c_int) { + #[cfg(unix)] + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + fn open_unix(file_name: CString, x: &mut c_int) { unsafe { *x = open(file_name.as_ptr(), O_RDWR | O_CREAT, S_IRWXU) } } } @@ -116,10 +121,12 @@ impl MemoryMappedFile for UnixMemoryMappedFile { #[cfg(test)] mod tests { - use super::*; - use crate::utilities::cached::CACHED; + + #[cfg(not(target_os = "android"))] + use {super::*, crate::utilities::cached::CACHED}; #[test] + #[cfg(not(target_os = "android"))] fn test_memory_mapped_file_creation() { // Let's create a memory mapped file with a specific size. let file_name = format!( @@ -138,6 +145,7 @@ mod tests { } #[test] + #[cfg(not(target_os = "android"))] fn test_memory_mapped_file_data() { let file_name = format!( "/test_memory_mapped_file_data PID {}", diff --git a/src-rust/src/lib.rs b/src-rust/src/lib.rs index 66be5b1..07fddca 100644 --- a/src-rust/src/lib.rs +++ b/src-rust/src/lib.rs @@ -105,9 +105,12 @@ pub(crate) mod internal { #[cfg(target_os = "windows")] pub mod buffer_allocator_windows; + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + pub mod buffer_allocator_mmap_rs; + pub mod memory_mapped_file; - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(unix)] pub mod memory_mapped_file_unix; #[cfg(target_os = "windows")] @@ -118,11 +121,16 @@ pub(crate) mod utilities { pub mod address_range; pub mod cached; + pub mod icache_clear; + pub mod map_parser_utilities; pub mod mathematics; pub mod wrappers; #[cfg(target_os = "linux")] pub mod linux_map_parser; + + // Internal, disables W^X for internal buffers. + pub(crate) mod disable_write_xor_execute; } /// Provides a C interface to the library. diff --git a/src-rust/src/structs/internal/locator_header.rs b/src-rust/src/structs/internal/locator_header.rs index ac38808..22af0d1 100644 --- a/src-rust/src/structs/internal/locator_header.rs +++ b/src-rust/src/structs/internal/locator_header.rs @@ -61,13 +61,32 @@ impl LocatorHeader { /// /// * `length` - Number of bytes available. pub(crate) fn initialize(&mut self, length: usize) { + self.set_default_values(); + let remaining_bytes = (length - LENGTH) as u32; + + // We allocate to allocation_granularity, however, under some platforms (*cough* M1 macOS) + // W^X policy is enforced, in which case, we cannot allocate executable memory here, + // as the header would also be affected. + + // We will use the remaining space for more headers on these affected platforms, and + // on non-W^X platforms, we will use it for buffers. + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + Self::initialize_remaining_space_as_headers(self as *mut LocatorHeader, remaining_bytes); + + #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] + self.initialize_remaining_space_as_buffers(remaining_bytes); + } + + fn set_default_values(&mut self) { self.this_address = Unaligned::new(self as *mut LocatorHeader); self.next_locator_ptr = Unaligned::new(std::ptr::null_mut()); self.is_locked = AtomicI32::new(0); self.flags = 0; + self.num_items = 0; + } + fn initialize_remaining_space_as_buffers(&mut self, mut remaining_bytes: u32) { let mut num_items = 0u8; - let mut remaining_bytes = (length - LENGTH) as u32; unsafe { let buffer_address = (self.this_address.value as *mut u8).add(LENGTH); let mut current_item = self.get_first_item(); @@ -84,6 +103,20 @@ impl LocatorHeader { self.num_items = num_items; } + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + fn initialize_remaining_space_as_headers(header: *mut LocatorHeader, mut remaining_bytes: u32) { + unsafe { + let mut current_header = header; + while remaining_bytes >= LENGTH as u32 { + let next_header = (current_header as *mut u8).add(LENGTH) as *mut LocatorHeader; + (*next_header).set_default_values(); + (*current_header).next_locator_ptr = Unaligned::new(next_header); + current_header = next_header; + remaining_bytes -= LENGTH as u32; + } + } + } + /// Returns the version represented by the first 3 bits of `flags`. #[allow(dead_code)] pub fn version(&self) -> u8 { diff --git a/src-rust/src/structs/internal/locator_item.rs b/src-rust/src/structs/internal/locator_item.rs index b20db4d..a87f020 100644 --- a/src-rust/src/structs/internal/locator_item.rs +++ b/src-rust/src/structs/internal/locator_item.rs @@ -1,5 +1,10 @@ -use crate::utilities::mathematics::add_with_overflow_cap; +use crate::utilities::disable_write_xor_execute::{ + disable_write_xor_execute, restore_write_xor_execute, +}; +use crate::utilities::icache_clear::clear_instruction_cache; +use crate::utilities::mathematics::add_with_overflow_cap; use crate::utilities::wrappers::Unaligned; +use core::mem::size_of; use std::sync::atomic::{AtomicI32, Ordering}; use std::thread; @@ -107,6 +112,39 @@ impl LocatorItem { start_available_address >= min_address && end_available_address <= max_address } + /// Appends the code to this buffer. + /// + /// # Arguments + /// + /// * `data` - The data to append to the item. + /// + /// # Returns + /// + /// The address of the written data. + /// + /// # Remarks + /// + /// It is the caller's responsibility to ensure there is sufficient space in the buffer. + /// When returning buffers from the library, the library will ensure there's at least the requested amount of space; + /// so if the total size of your data falls under that space, you are good. + /// + /// # Safety + /// + /// This function is safe provided that the caller ensures that the buffer is large enough to hold the data. + /// There is no error thrown if size is insufficient. + pub unsafe fn append_code(&mut self, data: &[u8]) -> usize { + disable_write_xor_execute(self.base_address.value as *const u8, data.len()); + let address = self.base_address.value + self.position as usize; + let data_len = data.len(); + + std::ptr::copy_nonoverlapping(data.as_ptr(), address as *mut u8, data_len); + self.position += data_len as u32; + + restore_write_xor_execute(self.base_address.value as *const u8, data.len()); + clear_instruction_cache(address as *mut u8, (address + data_len) as *mut u8); + address + } + /// Appends the data to this buffer. /// /// # Arguments @@ -128,12 +166,14 @@ impl LocatorItem { /// This function is safe provided that the caller ensures that the buffer is large enough to hold the data. /// There is no error thrown if size is insufficient. pub unsafe fn append_bytes(&mut self, data: &[u8]) -> usize { + disable_write_xor_execute(self.base_address.value as *const u8, data.len()); let address = self.base_address.value + self.position as usize; let data_len = data.len(); std::ptr::copy_nonoverlapping(data.as_ptr(), address as *mut u8, data_len); self.position += data_len as u32; + restore_write_xor_execute(self.base_address.value as *const u8, data.len()); address } @@ -160,9 +200,11 @@ impl LocatorItem { where T: Copy, { + disable_write_xor_execute(self.base_address.value as *const u8, size_of::()); let address = (self.base_address.value + self.position as usize) as *mut T; *address = data; - self.position += std::mem::size_of::() as u32; + self.position += size_of::() as u32; + restore_write_xor_execute(self.base_address.value as *const u8, size_of::()); address as usize } } diff --git a/src-rust/src/structs/private_allocation.rs b/src-rust/src/structs/private_allocation.rs index bd8d184..51d2fb0 100644 --- a/src-rust/src/structs/private_allocation.rs +++ b/src-rust/src/structs/private_allocation.rs @@ -1,26 +1,21 @@ -#[cfg(any(target_os = "windows", target_os = "linux"))] use std::ffi::c_void; - use std::ptr::NonNull; use crate::utilities::cached::CACHED; -#[cfg(target_os = "windows")] -use windows::Win32::System::Memory::{VirtualFree, VirtualFreeEx, MEM_RELEASE}; - -#[cfg(target_os = "macos")] -use mach::kern_return::KERN_SUCCESS; -#[cfg(target_os = "macos")] -use mach::traps::mach_task_self; - -#[cfg(target_os = "macos")] -use mach::vm::mach_vm_deallocate; +#[cfg(target_os = "windows")] +use { + crate::internal::buffer_allocator_windows::ProcessHandle, + windows::Win32::System::Memory::{VirtualFree, VirtualFreeEx, MEM_RELEASE}, +}; #[cfg(target_os = "macos")] -use mach::vm_types::{mach_vm_address_t, mach_vm_size_t}; - -#[cfg(target_os = "windows")] -use crate::internal::buffer_allocator_windows::ProcessHandle; +use { + mach::kern_return::KERN_SUCCESS, + mach::traps::mach_task_self, + mach::vm::mach_vm_deallocate, + mach::vm_types::{mach_vm_address_t, mach_vm_size_t}, +}; /// Provides information about a recently made allocation. /// @@ -142,7 +137,7 @@ impl PrivateAllocation { /// Frees the allocated memory when the `PrivateAllocation` instance is dropped. #[cfg(target_os = "linux")] - pub(crate) fn drop_linux(&mut self) { + pub(crate) fn drop_unix(&mut self) { unsafe { if self._this_process_id == CACHED.this_process_id { let result = libc::munmap(self.base_address.as_ptr() as *mut c_void, self.size); @@ -154,6 +149,21 @@ impl PrivateAllocation { }; } } + + /// Frees the allocated memory when the `PrivateAllocation` instance is dropped. + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + pub(crate) fn drop_mmap_rs(&mut self) { + use mmap_rs_with_map_from_existing::MmapOptions; + let _map = unsafe { + MmapOptions::new(self.size) + .unwrap() + .with_address(self.base_address.as_ptr() as usize) + .map_from_existing() + .unwrap() + }; + + // map will be dropped after being mapped from existing + } } impl Drop for PrivateAllocation { @@ -162,11 +172,15 @@ impl Drop for PrivateAllocation { #[cfg(target_os = "windows")] return PrivateAllocation::drop_windows(self); - #[cfg(target_os = "linux")] - return PrivateAllocation::drop_linux(self); + #[cfg(target_os = "linux")] // linux & co. + return PrivateAllocation::drop_unix(self); #[cfg(target_os = "macos")] return PrivateAllocation::drop_macos(self); + + // non-hot-path-os + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + return PrivateAllocation::drop_mmap_rs(self); } } diff --git a/src-rust/src/structs/safe_locator_item.rs b/src-rust/src/structs/safe_locator_item.rs index d5e8163..45e29d0 100644 --- a/src-rust/src/structs/safe_locator_item.rs +++ b/src-rust/src/structs/safe_locator_item.rs @@ -13,6 +13,30 @@ pub struct SafeLocatorItem { } impl SafeLocatorItem { + /// Appends the code to this buffer. + /// This is same as [`append_bytes`] but automatically clears the instruction cache on given CPU. + /// + /// It is the caller's responsibility to ensure there is sufficient space in the buffer. + /// When returning buffers from the library, the library will ensure there's at least + /// the requested amount of space; so if the total size of your data falls under that + /// space, you are good. + /// + /// # Arguments + /// + /// * `data` - The data to append to the item. + /// + /// # Returns + /// + /// The address of the written data. + /// + /// # Safety + /// + /// This function is safe provided that the caller ensures that the buffer is large enough to hold the data. + /// There is no error thrown if size is insufficient. + pub unsafe fn append_code(&self, data: &[u8]) -> usize { + (*self.item.get()).append_code(data) + } + /// Appends the data to this buffer. /// /// It is the caller's responsibility to ensure there is sufficient space in the buffer. diff --git a/src-rust/src/utilities/cached.rs b/src-rust/src/utilities/cached.rs index a4df621..0737ccd 100644 --- a/src-rust/src/utilities/cached.rs +++ b/src-rust/src/utilities/cached.rs @@ -3,15 +3,6 @@ use std::process; #[cfg(target_os = "windows")] use windows::Win32::System::SystemInformation::{GetSystemInfo, SYSTEM_INFO}; -#[cfg(not(target_os = "windows"))] -use libc; - -#[cfg(target_os = "linux")] -const SC_PAGESIZE: i32 = 30; // from `man 3 sysconf` - -#[cfg(target_os = "macos")] -const SC_PAGESIZE: i32 = 29; // from `man 3 sysconf` - lazy_static! { pub static ref CACHED: Cached = Cached::new(); } @@ -37,16 +28,13 @@ impl Cached { &mut page_size, ); - #[cfg(any(target_os = "linux", target_os = "macos"))] - Self::get_address_and_allocation_granularity_posix( + #[cfg(not(target_os = "windows"))] + Self::get_address_and_allocation_granularity_mmap_rs( &mut allocation_granularity, &mut max_address, &mut page_size, ); - #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] - panic!("Platform not supported"); - Cached { max_address, allocation_granularity, @@ -72,25 +60,35 @@ impl Cached { } #[allow(overflowing_literals)] - #[cfg(any(target_os = "linux", target_os = "macos"))] - fn get_address_and_allocation_granularity_posix( + #[cfg(not(target_os = "windows"))] + fn get_address_and_allocation_granularity_mmap_rs( allocation_granularity: &mut i32, max_address: &mut usize, page_size: &mut i32, ) { - // Note: On POSIX, applications are aware of full address space by default. - // Technically a chunk of address space is reserved for kernel, however for our use case that's not a concern. - // Note 2: There is no API on Linux (or OSX) to get max address; so we'll restrict to signed 48-bits on x64 for now. + // Note: This is a fallback mechanism dependent on mmap-rs. + use mmap_rs_with_map_from_existing::MmapOptions; if cfg!(target_pointer_width = "32") { *max_address = 0xFFFF_FFFF; } else if cfg!(target_pointer_width = "64") { - *max_address = 0x7FFFFFFFFFFF; + *max_address = 0x7FFFFFFFFFFF; // no max-address API, so restricted to Linux level } - *allocation_granularity = unsafe { libc::sysconf(SC_PAGESIZE) as i32 }; + #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] + { + *page_size = MmapOptions::page_size() as i32; + } + + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + { + // Apple lies about page size in libc on M1 says it's 4096 instead of 16384 + *page_size = 16384; + } - *page_size = *allocation_granularity; + // Blame Apple + *allocation_granularity = + std::cmp::max(MmapOptions::allocation_granularity() as i32, *page_size); } pub fn get_allocation_granularity(&self) -> i32 { diff --git a/src-rust/src/utilities/disable_write_xor_execute.rs b/src-rust/src/utilities/disable_write_xor_execute.rs new file mode 100644 index 0000000..0829da6 --- /dev/null +++ b/src-rust/src/utilities/disable_write_xor_execute.rs @@ -0,0 +1,66 @@ +// An utility to disable write xor execute protection on a memory region. +// This method contains the code to disable W^X on platforms where it's enforced. + +#[cfg(target_os = "macos")] +use { + libc::mach_task_self, mach::vm::mach_vm_protect, mach::vm_prot::VM_PROT_EXECUTE, + mach::vm_prot::VM_PROT_READ, mach::vm_prot::VM_PROT_WRITE, mach::vm_types::mach_vm_size_t, +}; + +/// Temporarily disables write XOR execute protection with an OS specialized +/// API call (if available). +/// +/// # Parameters +/// +/// - `address`: The address of the memory to disable write XOR execute protection for. +/// - `size`: The size of the memory to disable write XOR execute protection for. +/// +/// # Returns +/// +/// - `usize`: The old memory protection (if needed for call to [`self::restore_write_xor_execute`]). +/// +/// # Remarks +/// +/// This is not currently used on any platform, but is intended for environments +/// which enforce write XOR execute, such as M1 macs. +/// +/// The idea is that you use memory which is read_write_execute (MAP_JIT if mmap), +/// then disable W^X for the current thread. Then we write the code, and re-enable W^X. +#[allow(unused_variables)] +pub(crate) fn disable_write_xor_execute(address: *const u8, size: usize) { + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + unsafe { + mach_vm_protect( + mach_task_self(), + address as u64, + size as mach_vm_size_t, + 0, + VM_PROT_READ | VM_PROT_WRITE, + ); + } +} + +/// Restores write XOR execute protection. +/// +/// # Parameters +/// +/// - `address`: The address of the memory to disable write XOR execute protection for. +/// - `size`: The size of the memory to disable write XOR execute protection for. +/// - `protection`: The protection returned in the result of the call to [`self::disable_write_xor_execute`]. +/// +/// # Returns +/// +/// Success or error. +#[allow(unused_variables)] +pub(crate) fn restore_write_xor_execute(address: *const u8, size: usize) { + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + unsafe { + mach_vm_protect( + mach_task_self(), + address as u64, + size as mach_vm_size_t, + 0, + VM_PROT_READ | VM_PROT_EXECUTE, + ); + } +} diff --git a/src-rust/src/utilities/icache_clear.rs b/src-rust/src/utilities/icache_clear.rs new file mode 100644 index 0000000..8fce10b --- /dev/null +++ b/src-rust/src/utilities/icache_clear.rs @@ -0,0 +1,43 @@ +/// Clears the instruction cache for the specified range. +/// +/// # Arguments +/// +/// * `start` - The start address of the range to clear. +/// * `end` - The end address of the range to clear. +/// +/// # Remarks +/// +/// This function is provided by LLVM. It might not work in non-LLVM backends. +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +#[cfg(not(target_os = "windows"))] +pub fn clear_instruction_cache(start: *const u8, end: *const u8) { + clf::cache_line_flush_with_ptr(start, end); +} + +/// Clears the instruction cache for the specified range. +/// +/// # Arguments +/// +/// * `start` - The start address of the range to clear. +/// * `end` - The end address of the range to clear. +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +#[cfg(target_os = "windows")] // MSVC fix +pub fn clear_instruction_cache(start: *const u8, end: *const u8) { + use windows::Win32::System::{ + Diagnostics::Debug::FlushInstructionCache, Threading::GetCurrentProcess, + }; + + unsafe { + FlushInstructionCache( + GetCurrentProcess(), + Some(start as *const std::ffi::c_void), + end as usize - start as usize, + ); + } +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +pub fn clear_instruction_cache(_start: *const u8, _end: *const u8) { + // x86 & x86_64 have unified data and instruction cache, thus flushing is not needed. + // Therefore it is a no-op +} diff --git a/src-rust/src/utilities/linux_map_parser.rs b/src-rust/src/utilities/linux_map_parser.rs index 1128792..cbc00b1 100644 --- a/src-rust/src/utilities/linux_map_parser.rs +++ b/src-rust/src/utilities/linux_map_parser.rs @@ -1,23 +1,7 @@ -use crate::utilities; use std::fs::read_to_string; use std::io; -#[derive(Debug)] -pub struct MemoryMapEntry { - pub(crate) start_address: usize, - pub(crate) end_address: usize, -} - -/// This struct represents an entry in the memory map, -/// which is a region in the process's virtual memory space. -impl MemoryMapEntry { - fn new(start_address: usize, end_address: usize) -> MemoryMapEntry { - MemoryMapEntry { - start_address, - end_address, - } - } -} +use super::map_parser_utilities::{get_free_regions, MemoryMapEntry}; /// Parses the contents of the /proc/{id}/maps file and returns a vector of memory mapping entries. /// @@ -97,38 +81,6 @@ fn parse_memory_map_entry(line: &str) -> Result Vec { - let mut last_end_address: usize = 0; - let mut free_regions = Vec::with_capacity(regions.len() + 2); // +2 for start and finish - - for entry in regions.iter() { - if entry.start_address > last_end_address { - free_regions.push(MemoryMapEntry { - start_address: last_end_address, - end_address: entry.start_address - 1, - }); - } - - last_end_address = entry.end_address; - } - - // After the last region, up to the end of memory - if last_end_address < utilities::cached::CACHED.max_address { - free_regions.push(MemoryMapEntry { - start_address: last_end_address, - end_address: utilities::cached::CACHED.max_address, - }); - } - - free_regions -} - /// Returns all free regions based on the found regions. /// /// # Arguments @@ -143,45 +95,6 @@ pub fn get_free_regions_from_process_id(process_id: i32) -> Vec mod tests { use super::*; - #[test] - fn get_free_regions_with_no_gap() { - let regions = vec![ - MemoryMapEntry::new(0, 10), - MemoryMapEntry::new(10, 20), - MemoryMapEntry::new(20, usize::MAX), - ]; - let free_regions = get_free_regions(®ions); - assert_eq!(free_regions.len(), 0); - } - - #[test] - fn get_free_regions_single_gap() { - let regions = vec![ - MemoryMapEntry::new(0, 10), - MemoryMapEntry::new(10, 20), - MemoryMapEntry::new(30, usize::MAX), - ]; - let free_regions = get_free_regions(®ions); - assert_eq!(free_regions.len(), 1); - assert_eq!(free_regions[0].start_address, 20); - assert_eq!(free_regions[0].end_address, 29); - } - - #[test] - fn get_free_regions_multiple_gaps() { - let regions = vec![ - MemoryMapEntry::new(0, 10), - MemoryMapEntry::new(20, 30), - MemoryMapEntry::new(40, usize::MAX), - ]; - let free_regions = get_free_regions(®ions); - assert_eq!(free_regions.len(), 2); - assert_eq!(free_regions[0].start_address, 10); - assert_eq!(free_regions[0].end_address, 19); - assert_eq!(free_regions[1].start_address, 30); - assert_eq!(free_regions[1].end_address, 39); - } - #[cfg(target_pointer_width = "64")] #[test] fn parse_memory_map_entry_valid_line() { diff --git a/src-rust/src/utilities/map_parser_utilities.rs b/src-rust/src/utilities/map_parser_utilities.rs new file mode 100644 index 0000000..4ac43a7 --- /dev/null +++ b/src-rust/src/utilities/map_parser_utilities.rs @@ -0,0 +1,110 @@ +use super::cached::CACHED; + +// Generic structure to use for custom parsers. +#[derive(Debug)] +pub struct MemoryMapEntry { + pub start_address: usize, + pub end_address: usize, +} + +/// This struct represents an entry in the memory map, +/// which is a region in the process's virtual memory space. +impl MemoryMapEntry { + pub fn new(start_address: usize, end_address: usize) -> MemoryMapEntry { + MemoryMapEntry { + start_address, + end_address, + } + } +} + +// Trait to use for external types. +pub trait MemoryMapEntryTrait { + fn start_address(&self) -> usize; + fn end_address(&self) -> usize; +} + +impl MemoryMapEntryTrait for MemoryMapEntry { + fn start_address(&self) -> usize { + self.start_address + } + + fn end_address(&self) -> usize { + self.end_address + } +} + +/// Returns all free regions based on the found regions. +/// +/// # Arguments +/// +/// * `regions` - A slice of MemoryMapEntry that contains the regions. +pub fn get_free_regions(regions: &[T]) -> Vec { + let mut last_end_address: usize = 0; + let mut free_regions = Vec::with_capacity(regions.len() + 2); // +2 for start and finish + + for entry in regions.iter() { + if entry.start_address() > last_end_address { + free_regions.push(MemoryMapEntry { + start_address: last_end_address, + end_address: entry.start_address() - 1, + }); + } + + last_end_address = entry.end_address(); + } + + // After the last region, up to the end of memory + if last_end_address < CACHED.max_address { + free_regions.push(MemoryMapEntry { + start_address: last_end_address, + end_address: CACHED.max_address, + }); + } + + free_regions +} + +#[cfg(test)] +mod tests { + use crate::utilities::map_parser_utilities::{get_free_regions, MemoryMapEntry}; + + #[test] + fn get_free_regions_with_no_gap() { + let regions = vec![ + MemoryMapEntry::new(0, 10), + MemoryMapEntry::new(10, 20), + MemoryMapEntry::new(20, usize::MAX), + ]; + let free_regions = get_free_regions(®ions); + assert_eq!(free_regions.len(), 0); + } + + #[test] + fn get_free_regions_single_gap() { + let regions = vec![ + MemoryMapEntry::new(0, 10), + MemoryMapEntry::new(10, 20), + MemoryMapEntry::new(30, usize::MAX), + ]; + let free_regions = get_free_regions(®ions); + assert_eq!(free_regions.len(), 1); + assert_eq!(free_regions[0].start_address, 20); + assert_eq!(free_regions[0].end_address, 29); + } + + #[test] + fn get_free_regions_multiple_gaps() { + let regions = vec![ + MemoryMapEntry::new(0, 10), + MemoryMapEntry::new(20, 30), + MemoryMapEntry::new(40, usize::MAX), + ]; + let free_regions = get_free_regions(®ions); + assert_eq!(free_regions.len(), 2); + assert_eq!(free_regions[0].start_address, 10); + assert_eq!(free_regions[0].end_address, 19); + assert_eq!(free_regions[1].start_address, 30); + assert_eq!(free_regions[1].end_address, 39); + } +}