Prechádzať zdrojové kódy

sbpf syscall map, common edition, license, authors

Dean 利迪恩 3 týždňov pred
rodič
commit
c03121d641

+ 8 - 3
Cargo.toml

@@ -2,9 +2,9 @@
 name = "sbpf"
 version = "0.1.5"
 authors = ["Dean Little <dean@blueshift.gg>", "Claire Fan <claire@blueshift.gg>"]
-edition = "2021"
+edition = "2024"
 description = "A complete toolchain for building and deploying Solana BPF programs"
-license = "MIT OR Apache-2.0"
+license = "MIT"
 repository = "https://github.com/blueshift-gg/sbpf"
 readme = "README.md"
 keywords = ["solana", "bpf", "blockchain", "assembler"]
@@ -25,11 +25,15 @@ sbpf-assembler = { workspace = true }
 sbpf-disassembler = { workspace = true }
 
 [workspace]
-members = ["crates/assembler", "crates/common", "crates/disassembler"]
+members = ["crates/assembler", "crates/common", "crates/disassembler", "crates/sbpf-syscall-map"]
 exclude = ["examples"]
 
 [workspace.package]
 version = "0.1.5"
+edition = "2021"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/blueshift-gg/sbpf"
+authors = ["Dean Little <dean@blueshift.gg>", "Claire Fan <claire@blueshift.gg>"]
 
 [workspace.dependencies]
 num-derive    = "0.4"
@@ -41,3 +45,4 @@ object = "0.37.3"
 sbpf-assembler = { path = "crates/assembler", version = "0.1.5" }
 sbpf-disassembler = { path = "crates/disassembler", version = "0.1.5" }
 sbpf-common = { path = "crates/common", version = "0.1.5" }
+sbpf-syscall-map = { path = "crates/sbpf-syscall-map", version = "0.1.5" }

+ 4 - 3
crates/assembler/Cargo.toml

@@ -1,10 +1,11 @@
 [package]
 name = "sbpf-assembler"
 version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+authors.workspace = true
 description = "Assembler for SBPF (Solana BPF) assembly language"
-edition = "2024"
-license = "MIT OR Apache-2.0"
-repository = "https://github.com/blueshift-gg/sbpf"
 keywords = ["solana", "bpf", "assembler", "blockchain"]
 categories = ["development-tools", "compilers"]
 

+ 5 - 5
crates/common/Cargo.toml

@@ -1,10 +1,11 @@
 [package]
 name = "sbpf-common"
 version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+authors.workspace = true
 description = "Common types and utilities for SBPF (Solana BPF)"
-edition = "2024"
-license = "MIT OR Apache-2.0"
-repository = "https://github.com/blueshift-gg/sbpf"
 keywords = ["solana", "bpf", "blockchain"]
 categories = ["development-tools"]
 
@@ -16,9 +17,8 @@ name = "sbpf_common"
 num-derive = { workspace = true }
 num-traits = { workspace = true }
 thiserror = { workspace = true }
-phf = "0.13.1"
-phf_macros = "0.13.1"
 serde = { version = "1.0.228", features = ["derive"] }
+sbpf-syscall-map = { workspace = true }
 
 
 [dev-dependencies]

+ 2 - 2
crates/common/src/instruction.rs

@@ -1,6 +1,6 @@
 use crate::errors::SBPFError;
 use crate::opcode::Opcode;
-use crate::syscall::SYSCALLS;
+use crate::syscalls::SYSCALLS;
 
 use core::fmt;
 use core::ops::Range;
@@ -173,7 +173,7 @@ impl Instruction {
             }
 
             Opcode::Call => {
-                if SYSCALLS.get(&(imm as u32)).is_some() {
+                if SYSCALLS.get(imm as u32).is_some() {
                     if reg != 0 || off != 0 {
                         return Err(SBPFError::BytecodeError {
                             error: format!(

+ 2 - 1
crates/common/src/lib.rs

@@ -1,4 +1,5 @@
 pub mod errors;
 pub mod instruction;
 pub mod opcode;
-pub mod syscall;
+pub mod syscalls;
+pub mod syscalls_map;

+ 0 - 35
crates/common/src/syscall.rs

@@ -1,35 +0,0 @@
-use phf::Map;
-use phf_macros::phf_map;
-
-pub static SYSCALLS: Map<u32, &'static str> = phf_map! {
-    0xb6fc1a11u32 => "abort",
-    0x686093bbu32 => "sol_panic_",
-    0x207559bdu32 => "sol_log_",
-    0x5c2a3178u32 => "sol_log_64_",
-    0x52ba5096u32 => "sol_log_compute_units_",
-    0x7ef088cau32 => "sol_log_pubkey",
-    0x9377323cu32 => "sol_create_program_address",
-    0x48504a38u32 => "sol_try_find_program_address",
-    0x11f49d86u32 => "sol_sha256",
-    0xd7793abbu32 => "sol_keccak256",
-    0x17e40350u32 => "sol_secp256k1_recover",
-    0x174c5122u32 => "sol_blake3",
-    0xaa2607cau32 => "sol_curve_validate_point",
-    0xdd1c41a6u32 => "sol_curve_group_op",
-    0xd56b5fe9u32 => "sol_get_clock_sysvar",
-    0x23a29a61u32 => "sol_get_epoch_schedule_sysvar",
-    0x3b97b73cu32 => "sol_get_fees_sysvar",
-    0xbf7188f6u32 => "sol_get_rent_sysvar",
-    0x717cc4a3u32 => "sol_memcpy_",
-    0x434371f8u32 => "sol_memmove_",
-    0x5fdcde31u32 => "sol_memcmp_",
-    0x3770fb22u32 => "sol_memset_",
-    0xa22b9c85u32 => "sol_invoke_signed_c",
-    0xd7449092u32 => "sol_invoke_signed_rust",
-    0x83f00e8fu32 => "sol_alloc_free_",
-    0xa226d3ebu32 => "sol_set_return_data",
-    0x5d2245e4u32 => "sol_get_return_data",
-    0x7317b434u32 => "sol_log_data",
-    0xadb8efc8u32 => "sol_get_processed_sibling_instruction",
-    0x85532d94u32 => "sol_get_stack_height",
-};

+ 36 - 0
crates/common/src/syscalls.rs

@@ -0,0 +1,36 @@
+use crate::syscalls_map::{compute_syscall_entries_const, SyscallMap};
+
+pub const REGISTERED_SYSCALLS: &[&str] = &[
+    "abort",
+    "sol_panic_",
+    "sol_log_",
+    "sol_log_64_",
+    "sol_log_compute_units_",
+    "sol_log_pubkey",
+    "sol_create_program_address",
+    "sol_try_find_program_address",
+    "sol_sha256",
+    "sol_keccak256",
+    "sol_secp256k1_recover",
+    "sol_blake3",
+    "sol_curve_validate_point",
+    "sol_curve_group_op",
+    "sol_get_clock_sysvar",
+    "sol_get_epoch_schedule_sysvar",
+    "sol_get_fees_sysvar",
+    "sol_get_rent_sysvar",
+    "sol_memcpy_",
+    "sol_memmove_",
+    "sol_memcmp_",
+    "sol_memset_",
+    "sol_invoke_signed_c",
+    "sol_invoke_signed_rust",
+    "sol_alloc_free_",
+    "sol_set_return_data",
+    "sol_get_return_data",
+    "sol_log_data",
+    "sol_get_processed_sibling_instruction",
+    "sol_get_stack_height",
+];
+
+pub static SYSCALLS: SyscallMap<'static> = SyscallMap::from_entries(&compute_syscall_entries_const::<{REGISTERED_SYSCALLS.len()}>(REGISTERED_SYSCALLS));

+ 399 - 0
crates/common/src/syscalls_map.rs

@@ -0,0 +1,399 @@
+// Simple const hashmap implementation using binary search on sorted array
+// Supports both static (compile-time) and dynamic (runtime) syscall lists via lifetimes
+pub struct SyscallMap<'a> {
+    entries: &'a [(u32, &'a str)],
+}
+
+impl<'a> SyscallMap<'a> {
+    /// Create from a pre-sorted slice of (hash, name) pairs
+    /// Works for both static and dynamic lifetimes
+    pub const fn from_entries(entries: &'a [(u32, &'a str)]) -> Self {
+        // Check for hash conflicts
+        let mut i = 0;
+        while i < entries.len() - 1 {
+            if entries[i].0 == entries[i + 1].0 {
+                panic!("Hash conflict detected between syscalls");
+            }
+            i += 1;
+        }
+
+        Self { entries }
+    }
+
+    pub const fn get(&self, hash: u32) -> Option<&'a str> {
+        // Binary search in const context
+        let mut left = 0;
+        let mut right = self.entries.len();
+
+        while left < right {
+            let mid = (left + right) / 2;
+            if self.entries[mid].0 == hash {
+                return Some(self.entries[mid].1);
+            } else if self.entries[mid].0 < hash {
+                left = mid + 1;
+            } else {
+                right = mid;
+            }
+        }
+        None
+    }
+
+    pub const fn len(&self) -> usize {
+        self.entries.len()
+    }
+
+    pub const fn is_empty(&self) -> bool {
+        self.entries.is_empty()
+    }
+}
+
+/// Runtime-mutable syscall map that owns its data
+/// This allows for dynamic updates at runtime
+pub struct DynamicSyscallMap {
+    // Store entries as (hash, name) pairs, kept sorted by hash
+    entries: Vec<(u32, String)>,
+}
+
+impl DynamicSyscallMap {
+    /// Create a new dynamic syscall map from owned strings
+    pub fn new(syscalls: Vec<String>) -> Result<Self, String> {
+        let mut entries: Vec<(u32, String)> = syscalls
+            .into_iter()
+            .map(|name| (murmur3_32(&name), name))
+            .collect();
+
+        entries.sort_by_key(|(hash, _)| *hash);
+
+        // Check for conflicts
+        for i in 0..entries.len().saturating_sub(1) {
+            if entries[i].0 == entries[i + 1].0 {
+                return Err(format!(
+                    "Hash conflict detected between syscalls '{}' and '{}'",
+                    entries[i].1, entries[i + 1].1
+                ));
+            }
+        }
+
+        Ok(Self { entries })
+    }
+
+    /// Create from string slices (convenience method)
+    pub fn from_names(names: &[&str]) -> Result<Self, String> {
+        Self::new(names.iter().map(|&s| s.to_string()).collect())
+    }
+
+    /// Look up a syscall by hash
+    pub fn get(&self, hash: u32) -> Option<&str> {
+        match self.entries.binary_search_by_key(&hash, |(h, _)| *h) {
+            Ok(idx) => Some(&self.entries[idx].1),
+            Err(_) => None,
+        }
+    }
+
+    /// Add a new syscall at runtime
+    pub fn add(&mut self, name: String) -> Result<(), String> {
+        let hash = murmur3_32(&name);
+
+        // Check if it already exists or would conflict
+        match self.entries.binary_search_by_key(&hash, |(h, _)| *h) {
+            Ok(_) => {
+                return Err(format!(
+                    "Hash conflict: '{}' conflicts with existing syscall",
+                    name
+                ));
+            }
+            Err(pos) => {
+                self.entries.insert(pos, (hash, name));
+                Ok(())
+            }
+        }
+    }
+
+    pub fn len(&self) -> usize {
+        self.entries.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.entries.is_empty()
+    }
+}
+
+/// Convert a static SyscallMap to a dynamic one
+impl<'a> From<&SyscallMap<'a>> for DynamicSyscallMap {
+    fn from(static_map: &SyscallMap<'a>) -> Self {
+        let entries = static_map.entries
+            .iter()
+            .map(|(hash, name)| (*hash, name.to_string()))
+            .collect();
+
+        Self { entries }
+    }
+}
+
+/// Helper function for compile-time syscall map creation
+/// Computes hashes and sorts entries at compile time
+pub const fn compute_syscall_entries_const<'a, const N: usize>(
+    syscalls: &'a [&'a str],
+) -> [(u32, &'a str); N] {
+    let mut entries: [(u32, &str); N] = [(0, ""); N];
+    let mut i = 0;
+    while i < N {
+        entries[i] = (murmur3_32(syscalls[i]), syscalls[i]);
+        i += 1;
+    }
+
+    // Sort the entries at compile time using bubble sort
+    let mut i = 0;
+    while i < N {
+        let mut j = 0;
+        while j < N - i - 1 {
+            if entries[j].0 > entries[j + 1].0 {
+                let temp = entries[j];
+                entries[j] = entries[j + 1];
+                entries[j + 1] = temp;
+            }
+            j += 1;
+        }
+        i += 1;
+    }
+
+    entries
+}
+
+/// Runtime helper for dynamic syscall lists
+/// Computes hashes and sorts entries, borrowing from the input
+///
+/// The caller must own the string data (e.g., Vec<String>) and pass references.
+/// This function returns references to those owned strings.
+pub fn compute_syscall_entries<'a, T: AsRef<str>>(
+    syscalls: &'a [T],
+) -> Vec<(u32, &'a str)> {
+    let mut entries: Vec<(u32, &'a str)> = syscalls
+        .iter()
+        .map(|name| (murmur3_32(name.as_ref()), name.as_ref()))
+        .collect();
+
+    entries.sort_by_key(|(hash, _)| *hash);
+
+    // Check for conflicts
+    for i in 0..entries.len().saturating_sub(1) {
+        if entries[i].0 == entries[i + 1].0 {
+            panic!(
+                "Hash conflict detected between syscalls '{}' and '{}'",
+                entries[i].1, entries[i + 1].1
+            );
+        }
+    }
+
+    entries
+}
+
+pub const fn murmur3_32(buf: &str) -> u32 {
+    const fn pre_mix(buf: [u8; 4]) -> u32 {
+        u32::from_le_bytes(buf)
+            .wrapping_mul(0xcc9e2d51)
+            .rotate_left(15)
+            .wrapping_mul(0x1b873593)
+    }
+
+    let mut hash = 0;
+    let mut i = 0;
+    let buf = buf.as_bytes();
+
+    while i < buf.len() / 4 {
+        let buf = [buf[i * 4], buf[i * 4 + 1], buf[i * 4 + 2], buf[i * 4 + 3]];
+        hash ^= pre_mix(buf);
+        hash = hash.rotate_left(13);
+        hash = hash.wrapping_mul(5).wrapping_add(0xe6546b64);
+
+        i += 1;
+    }
+
+    match buf.len() % 4 {
+        0 => {}
+        1 => {
+            hash = hash ^ pre_mix([buf[i * 4], 0, 0, 0]);
+        }
+        2 => {
+            hash = hash ^ pre_mix([buf[i * 4], buf[i * 4 + 1], 0, 0]);
+        }
+        3 => {
+            hash = hash ^ pre_mix([buf[i * 4], buf[i * 4 + 1], buf[i * 4 + 2], 0]);
+        }
+        _ => { /* unreachable!() */ }
+    }
+
+    hash = hash ^ buf.len() as u32;
+    hash = hash ^ (hash.wrapping_shr(16));
+    hash = hash.wrapping_mul(0x85ebca6b);
+    hash = hash ^ (hash.wrapping_shr(13));
+    hash = hash.wrapping_mul(0xc2b2ae35);
+    hash = hash ^ (hash.wrapping_shr(16));
+
+    hash
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::syscalls::{REGISTERED_SYSCALLS, SYSCALLS};
+
+    use super::*;
+
+    #[test]
+    fn test_syscall_lookup() {
+        // Test that all syscalls can be found
+        for &name in REGISTERED_SYSCALLS.iter() {
+            let hash = murmur3_32(name);
+            assert_eq!(SYSCALLS.get(hash), Some(name), "Failed to find syscall: {}", name);
+        }
+    }
+
+    #[test]
+    fn test_const_evaluation() {
+        // Verify const evaluation works at compile time
+        const ABORT_HASH: u32 = murmur3_32("abort");
+        const SOL_LOG_HASH: u32 = murmur3_32("sol_log_");
+
+        // Verify the hashes are computed correctly and can look up syscalls
+        assert_eq!(SYSCALLS.get(ABORT_HASH), Some("abort"));
+        assert_eq!(SYSCALLS.get(SOL_LOG_HASH), Some("sol_log_"));
+    }
+
+    #[test]
+    fn test_nonexistent_syscall() {
+        // Test that non-existent syscalls return None
+        assert_eq!(SYSCALLS.get(0xDEADBEEF), None);
+    }
+
+    #[test]
+    fn test_dynamic_syscalls() {
+        // Example: Create a dynamic syscall map with owned strings
+        // The caller owns the strings (e.g., from user input, config file, etc.)
+        let owned_syscalls: Vec<String> = vec![
+            String::from("my_custom_syscall"),
+            String::from("another_syscall"),
+        ];
+
+        // Compute entries - they borrow from owned_syscalls
+        let entries = compute_syscall_entries(&owned_syscalls);
+
+        // Create the map - it borrows from entries
+        let map = SyscallMap::from_entries(&entries);
+
+        // Verify lookups work
+        let hash1 = murmur3_32("my_custom_syscall");
+        let hash2 = murmur3_32("another_syscall");
+
+        assert_eq!(map.get(hash1), Some("my_custom_syscall"));
+        assert_eq!(map.get(hash2), Some("another_syscall"));
+
+        // The lifetimes ensure owned_syscalls outlives both entries and map
+    }
+
+    #[test]
+    fn test_dynamic_syscalls_with_str_slices() {
+        // Also works with &str slices
+        let syscalls: Vec<&str> = vec!["syscall_a", "syscall_b", "syscall_c"];
+
+        let entries = compute_syscall_entries(&syscalls);
+        let map = SyscallMap::from_entries(&entries);
+
+        assert_eq!(map.get(murmur3_32("syscall_a")), Some("syscall_a"));
+        assert_eq!(map.get(murmur3_32("syscall_b")), Some("syscall_b"));
+        assert_eq!(map.get(murmur3_32("syscall_c")), Some("syscall_c"));
+    }
+
+    #[test]
+    fn test_static_custom_map() {
+        // Example: Create a static custom syscall map at compile time
+        const CUSTOM_SYSCALLS: &[&str; 2] = &["test1", "test2"];
+        const CUSTOM_ENTRIES: &[(u32, &str); 2] = &compute_syscall_entries_const(CUSTOM_SYSCALLS);
+        const CUSTOM_MAP: SyscallMap<'static> = SyscallMap::from_entries(CUSTOM_ENTRIES);
+
+        assert_eq!(CUSTOM_MAP.get(murmur3_32("test1")), Some("test1"));
+        assert_eq!(CUSTOM_MAP.get(murmur3_32("test2")), Some("test2"));
+    }
+
+    #[test]
+    fn test_dynamic_mutable_map() {
+        // Example: Create a fully dynamic, mutable syscall map
+        let mut map = DynamicSyscallMap::from_names(&["initial_syscall"]).unwrap();
+
+        // Initial lookup works
+        assert_eq!(
+            map.get(murmur3_32("initial_syscall")),
+            Some("initial_syscall")
+        );
+
+        // Add new syscalls at runtime
+        map.add("runtime_syscall_1".to_string()).unwrap();
+        map.add("runtime_syscall_2".to_string()).unwrap();
+
+        // All lookups work
+        assert_eq!(
+            map.get(murmur3_32("initial_syscall")),
+            Some("initial_syscall")
+        );
+        assert_eq!(
+            map.get(murmur3_32("runtime_syscall_1")),
+            Some("runtime_syscall_1")
+        );
+        assert_eq!(
+            map.get(murmur3_32("runtime_syscall_2")),
+            Some("runtime_syscall_2")
+        );
+
+        // Non-existent syscall returns None
+        assert_eq!(map.get(0xDEADBEEF), None);
+
+        // Verify count
+        assert_eq!(map.len(), 3);
+    }
+
+    #[test]
+    fn test_dynamic_map_with_owned_strings() {
+        // Create from owned strings directly
+        let syscalls = vec![
+            String::from("custom_1"),
+            String::from("custom_2"),
+            String::from("custom_3"),
+        ];
+
+        let mut map = DynamicSyscallMap::new(syscalls).unwrap();
+
+        assert_eq!(map.get(murmur3_32("custom_1")), Some("custom_1"));
+        assert_eq!(map.get(murmur3_32("custom_2")), Some("custom_2"));
+        assert_eq!(map.get(murmur3_32("custom_3")), Some("custom_3"));
+
+        // Add more at runtime
+        map.add("custom_4".to_string()).unwrap();
+        assert_eq!(map.get(murmur3_32("custom_4")), Some("custom_4"));
+    }
+
+    #[test]
+    fn test_convert_static_to_dynamic() {
+        // Start with the static syscall map
+        let dynamic = DynamicSyscallMap::from(&SYSCALLS);
+
+        // Verify all static syscalls are present
+        for &name in REGISTERED_SYSCALLS.iter() {
+            let hash = murmur3_32(name);
+            assert_eq!(dynamic.get(hash), Some(name), "Failed to find syscall: {}", name);
+        }
+
+        // Verify we can add new syscalls to it
+        let mut dynamic_mut = dynamic;
+        dynamic_mut.add("my_custom_syscall".to_string()).unwrap();
+
+        assert_eq!(
+            dynamic_mut.get(murmur3_32("my_custom_syscall")),
+            Some("my_custom_syscall")
+        );
+
+        // Original static syscalls still work
+        assert_eq!(dynamic_mut.get(murmur3_32("abort")), Some("abort"));
+
+        // Count should be original + 1
+        assert_eq!(dynamic_mut.len(), REGISTERED_SYSCALLS.len() + 1);
+    }
+}

+ 4 - 3
crates/disassembler/Cargo.toml

@@ -1,10 +1,11 @@
 [package]
 name = "sbpf-disassembler"
 version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+authors.workspace = true
 description = "Disassembler for SBPF (Solana BPF) bytecode"
-edition = "2024"
-license = "MIT OR Apache-2.0"
-repository = "https://github.com/blueshift-gg/sbpf"
 keywords = ["solana", "bpf", "disassembler", "blockchain"]
 categories = ["development-tools"]
 

+ 16 - 0
crates/sbpf-syscall-map/Cargo.toml

@@ -0,0 +1,16 @@
+[package]
+name = "sbpf-syscall-map"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+authors.workspace = true
+description = "Compile-time and runtime syscall mapping for SBPF"
+keywords = ["solana", "bpf", "syscall"]
+categories = ["development-tools"]
+
+[dependencies]
+
+[lib]
+name = "syscall_map"
+path = "src/lib.rs"

+ 402 - 0
crates/sbpf-syscall-map/src/lib.rs

@@ -0,0 +1,402 @@
+// Simple const hashmap implementation using binary search on sorted array
+// Supports both static (compile-time) and dynamic (runtime) syscall lists via lifetimes
+pub struct SyscallMap<'a> {
+    entries: &'a [(u32, &'a str)],
+}
+
+impl<'a> SyscallMap<'a> {
+    /// Create from a pre-sorted slice of (hash, name) pairs
+    /// Works for both static and dynamic lifetimes
+    pub const fn from_entries(entries: &'a [(u32, &'a str)]) -> Self {
+        // Check for hash conflicts
+        let mut i = 0;
+        while i < entries.len() - 1 {
+            if entries[i].0 == entries[i + 1].0 {
+                panic!("Hash conflict detected between syscalls");
+            }
+            i += 1;
+        }
+
+        Self { entries }
+    }
+
+    pub const fn get(&self, hash: u32) -> Option<&'a str> {
+        // Binary search in const context
+        let mut left = 0;
+        let mut right = self.entries.len();
+
+        while left < right {
+            let mid = (left + right) / 2;
+            if self.entries[mid].0 == hash {
+                return Some(self.entries[mid].1);
+            } else if self.entries[mid].0 < hash {
+                left = mid + 1;
+            } else {
+                right = mid;
+            }
+        }
+        None
+    }
+
+    pub const fn len(&self) -> usize {
+        self.entries.len()
+    }
+
+    pub const fn is_empty(&self) -> bool {
+        self.entries.is_empty()
+    }
+}
+
+/// Runtime-mutable syscall map that owns its data
+/// This allows for dynamic updates at runtime
+pub struct DynamicSyscallMap {
+    // Store entries as (hash, name) pairs, kept sorted by hash
+    entries: Vec<(u32, String)>,
+}
+
+impl DynamicSyscallMap {
+    /// Create a new dynamic syscall map from owned strings
+    pub fn new(syscalls: Vec<String>) -> Result<Self, String> {
+        let mut entries: Vec<(u32, String)> = syscalls
+            .into_iter()
+            .map(|name| (murmur3_32(&name), name))
+            .collect();
+
+        entries.sort_by_key(|(hash, _)| *hash);
+
+        // Check for conflicts
+        for i in 0..entries.len().saturating_sub(1) {
+            if entries[i].0 == entries[i + 1].0 {
+                return Err(format!(
+                    "Hash conflict detected between syscalls '{}' and '{}'",
+                    entries[i].1, entries[i + 1].1
+                ));
+            }
+        }
+
+        Ok(Self { entries })
+    }
+
+    /// Create from string slices (convenience method)
+    pub fn from_names(names: &[&str]) -> Result<Self, String> {
+        Self::new(names.iter().map(|&s| s.to_string()).collect())
+    }
+
+    /// Look up a syscall by hash
+    pub fn get(&self, hash: u32) -> Option<&str> {
+        match self.entries.binary_search_by_key(&hash, |(h, _)| *h) {
+            Ok(idx) => Some(&self.entries[idx].1),
+            Err(_) => None,
+        }
+    }
+
+    /// Add a new syscall at runtime
+    pub fn add(&mut self, name: String) -> Result<(), String> {
+        let hash = murmur3_32(&name);
+
+        // Check if it already exists or would conflict
+        match self.entries.binary_search_by_key(&hash, |(h, _)| *h) {
+            Ok(_) => {
+                return Err(format!(
+                    "Hash conflict: '{}' conflicts with existing syscall",
+                    name
+                ));
+            }
+            Err(pos) => {
+                self.entries.insert(pos, (hash, name));
+                Ok(())
+            }
+        }
+    }
+
+    pub fn len(&self) -> usize {
+        self.entries.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.entries.is_empty()
+    }
+}
+
+/// Convert a static SyscallMap to a dynamic one
+impl<'a> From<&SyscallMap<'a>> for DynamicSyscallMap {
+    fn from(static_map: &SyscallMap<'a>) -> Self {
+        let entries = static_map.entries
+            .iter()
+            .map(|(hash, name)| (*hash, name.to_string()))
+            .collect();
+
+        Self { entries }
+    }
+}
+
+/// Helper function for compile-time syscall map creation
+/// Computes hashes and sorts entries at compile time
+pub const fn compute_syscall_entries_const<'a, const N: usize>(
+    syscalls: &'a [&'a str; N],
+) -> [(u32, &'a str); N] {
+    let mut entries: [(u32, &str); N] = [(0, ""); N];
+    let mut i = 0;
+    while i < N {
+        entries[i] = (murmur3_32(syscalls[i]), syscalls[i]);
+        i += 1;
+    }
+
+    // Sort the entries at compile time using bubble sort
+    let mut i = 0;
+    while i < N {
+        let mut j = 0;
+        while j < N - i - 1 {
+            if entries[j].0 > entries[j + 1].0 {
+                let temp = entries[j];
+                entries[j] = entries[j + 1];
+                entries[j + 1] = temp;
+            }
+            j += 1;
+        }
+        i += 1;
+    }
+
+    entries
+}
+
+/// Runtime helper for dynamic syscall lists
+/// Computes hashes and sorts entries, borrowing from the input
+///
+/// The caller must own the string data (e.g., Vec<String>) and pass references.
+/// This function returns references to those owned strings.
+pub fn compute_syscall_entries<'a, T: AsRef<str>>(
+    syscalls: &'a [T],
+) -> Vec<(u32, &'a str)> {
+    let mut entries: Vec<(u32, &'a str)> = syscalls
+        .iter()
+        .map(|name| (murmur3_32(name.as_ref()), name.as_ref()))
+        .collect();
+
+    entries.sort_by_key(|(hash, _)| *hash);
+
+    // Check for conflicts
+    for i in 0..entries.len().saturating_sub(1) {
+        if entries[i].0 == entries[i + 1].0 {
+            panic!(
+                "Hash conflict detected between syscalls '{}' and '{}'",
+                entries[i].1, entries[i + 1].1
+            );
+        }
+    }
+
+    entries
+}
+
+pub const fn murmur3_32(buf: &str) -> u32 {
+    const fn pre_mix(buf: [u8; 4]) -> u32 {
+        u32::from_le_bytes(buf)
+            .wrapping_mul(0xcc9e2d51)
+            .rotate_left(15)
+            .wrapping_mul(0x1b873593)
+    }
+
+    let mut hash = 0;
+    let mut i = 0;
+    let buf = buf.as_bytes();
+
+    while i < buf.len() / 4 {
+        let buf = [buf[i * 4], buf[i * 4 + 1], buf[i * 4 + 2], buf[i * 4 + 3]];
+        hash ^= pre_mix(buf);
+        hash = hash.rotate_left(13);
+        hash = hash.wrapping_mul(5).wrapping_add(0xe6546b64);
+
+        i += 1;
+    }
+
+    match buf.len() % 4 {
+        0 => {}
+        1 => {
+            hash = hash ^ pre_mix([buf[i * 4], 0, 0, 0]);
+        }
+        2 => {
+            hash = hash ^ pre_mix([buf[i * 4], buf[i * 4 + 1], 0, 0]);
+        }
+        3 => {
+            hash = hash ^ pre_mix([buf[i * 4], buf[i * 4 + 1], buf[i * 4 + 2], 0]);
+        }
+        _ => { /* unreachable!() */ }
+    }
+
+    hash = hash ^ buf.len() as u32;
+    hash = hash ^ (hash.wrapping_shr(16));
+    hash = hash.wrapping_mul(0x85ebca6b);
+    hash = hash ^ (hash.wrapping_shr(13));
+    hash = hash.wrapping_mul(0xc2b2ae35);
+    hash = hash ^ (hash.wrapping_shr(16));
+
+    hash
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_const_evaluation() {
+        // Verify const evaluation works at compile time
+        const ABORT_HASH: u32 = murmur3_32("abort");
+        const SOL_LOG_HASH: u32 = murmur3_32("sol_log_");
+
+        // Create a test map
+        const TEST_SYSCALLS: &[&str; 2] = &["abort", "sol_log_"];
+        const TEST_ENTRIES: &[(u32, &str); 2] = &compute_syscall_entries_const(TEST_SYSCALLS);
+        const TEST_MAP: SyscallMap<'static> = SyscallMap::from_entries(TEST_ENTRIES);
+
+        // Verify the hashes are computed correctly and can look up syscalls
+        assert_eq!(TEST_MAP.get(ABORT_HASH), Some("abort"));
+        assert_eq!(TEST_MAP.get(SOL_LOG_HASH), Some("sol_log_"));
+    }
+
+    #[test]
+    fn test_nonexistent_syscall() {
+        // Test that non-existent syscalls return None
+        const TEST_SYSCALLS: &[&str; 1] = &["test"];
+        const TEST_ENTRIES: &[(u32, &str); 1] = &compute_syscall_entries_const(TEST_SYSCALLS);
+        const TEST_MAP: SyscallMap<'static> = SyscallMap::from_entries(TEST_ENTRIES);
+
+        assert_eq!(TEST_MAP.get(0xDEADBEEF), None);
+    }
+
+    #[test]
+    fn test_dynamic_syscalls() {
+        // Example: Create a dynamic syscall map with owned strings
+        // The caller owns the strings (e.g., from user input, config file, etc.)
+        let owned_syscalls: Vec<String> = vec![
+            String::from("my_custom_syscall"),
+            String::from("another_syscall"),
+        ];
+
+        // Compute entries - they borrow from owned_syscalls
+        let entries = compute_syscall_entries(&owned_syscalls);
+
+        // Create the map - it borrows from entries
+        let map = SyscallMap::from_entries(&entries);
+
+        // Verify lookups work
+        let hash1 = murmur3_32("my_custom_syscall");
+        let hash2 = murmur3_32("another_syscall");
+
+        assert_eq!(map.get(hash1), Some("my_custom_syscall"));
+        assert_eq!(map.get(hash2), Some("another_syscall"));
+
+        // The lifetimes ensure owned_syscalls outlives both entries and map
+    }
+
+    #[test]
+    fn test_dynamic_syscalls_with_str_slices() {
+        // Also works with &str slices
+        let syscalls: Vec<&str> = vec!["syscall_a", "syscall_b", "syscall_c"];
+
+        let entries = compute_syscall_entries(&syscalls);
+        let map = SyscallMap::from_entries(&entries);
+
+        assert_eq!(map.get(murmur3_32("syscall_a")), Some("syscall_a"));
+        assert_eq!(map.get(murmur3_32("syscall_b")), Some("syscall_b"));
+        assert_eq!(map.get(murmur3_32("syscall_c")), Some("syscall_c"));
+    }
+
+    #[test]
+    fn test_static_custom_map() {
+        // Example: Create a static custom syscall map at compile time
+        const CUSTOM_SYSCALLS: &[&str; 2] = &["test1", "test2"];
+        const CUSTOM_ENTRIES: &[(u32, &str); 2] = &compute_syscall_entries_const(CUSTOM_SYSCALLS);
+        const CUSTOM_MAP: SyscallMap<'static> = SyscallMap::from_entries(CUSTOM_ENTRIES);
+
+        assert_eq!(CUSTOM_MAP.get(murmur3_32("test1")), Some("test1"));
+        assert_eq!(CUSTOM_MAP.get(murmur3_32("test2")), Some("test2"));
+    }
+
+    #[test]
+    fn test_dynamic_mutable_map() {
+        // Example: Create a fully dynamic, mutable syscall map
+        let mut map = DynamicSyscallMap::from_names(&["initial_syscall"]).unwrap();
+
+        // Initial lookup works
+        assert_eq!(
+            map.get(murmur3_32("initial_syscall")),
+            Some("initial_syscall")
+        );
+
+        // Add new syscalls at runtime
+        map.add("runtime_syscall_1".to_string()).unwrap();
+        map.add("runtime_syscall_2".to_string()).unwrap();
+
+        // All lookups work
+        assert_eq!(
+            map.get(murmur3_32("initial_syscall")),
+            Some("initial_syscall")
+        );
+        assert_eq!(
+            map.get(murmur3_32("runtime_syscall_1")),
+            Some("runtime_syscall_1")
+        );
+        assert_eq!(
+            map.get(murmur3_32("runtime_syscall_2")),
+            Some("runtime_syscall_2")
+        );
+
+        // Non-existent syscall returns None
+        assert_eq!(map.get(0xDEADBEEF), None);
+
+        // Verify count
+        assert_eq!(map.len(), 3);
+    }
+
+    #[test]
+    fn test_dynamic_map_with_owned_strings() {
+        // Create from owned strings directly
+        let syscalls = vec![
+            String::from("custom_1"),
+            String::from("custom_2"),
+            String::from("custom_3"),
+        ];
+
+        let mut map = DynamicSyscallMap::new(syscalls).unwrap();
+
+        assert_eq!(map.get(murmur3_32("custom_1")), Some("custom_1"));
+        assert_eq!(map.get(murmur3_32("custom_2")), Some("custom_2"));
+        assert_eq!(map.get(murmur3_32("custom_3")), Some("custom_3"));
+
+        // Add more at runtime
+        map.add("custom_4".to_string()).unwrap();
+        assert_eq!(map.get(murmur3_32("custom_4")), Some("custom_4"));
+    }
+
+    #[test]
+    fn test_convert_static_to_dynamic() {
+        // Create a test static map
+        const TEST_SYSCALLS: &[&str; 3] = &["abort", "sol_log_", "sol_panic_"];
+        const TEST_ENTRIES: &[(u32, &str); 3] = &compute_syscall_entries_const(TEST_SYSCALLS);
+        const TEST_MAP: SyscallMap<'static> = SyscallMap::from_entries(TEST_ENTRIES);
+
+        // Convert to dynamic
+        let dynamic = DynamicSyscallMap::from(&TEST_MAP);
+
+        // Verify all static syscalls are present
+        for &name in TEST_SYSCALLS.iter() {
+            let hash = murmur3_32(name);
+            assert_eq!(dynamic.get(hash), Some(name), "Failed to find syscall: {}", name);
+        }
+
+        // Verify we can add new syscalls to it
+        let mut dynamic_mut = dynamic;
+        dynamic_mut.add("my_custom_syscall".to_string()).unwrap();
+
+        assert_eq!(
+            dynamic_mut.get(murmur3_32("my_custom_syscall")),
+            Some("my_custom_syscall")
+        );
+
+        // Original static syscalls still work
+        assert_eq!(dynamic_mut.get(murmur3_32("abort")), Some("abort"));
+
+        // Count should be original + 1
+        assert_eq!(dynamic_mut.len(), TEST_SYSCALLS.len() + 1);
+    }
+}

+ 3 - 3
tests/utils.rs

@@ -8,7 +8,7 @@ impl EnvVarGuard {
     pub fn new<K: Into<String>, V: Into<String>>(key: K, value: V) -> Self {
         let key = key.into();
         let original = std::env::var(&key).ok();
-        std::env::set_var(&key, value.into());
+        unsafe { std::env::set_var(&key, value.into()) };
         Self { key, original }
     }
 }
@@ -16,9 +16,9 @@ impl EnvVarGuard {
 impl Drop for EnvVarGuard {
     fn drop(&mut self) {
         if let Some(ref val) = self.original {
-            std::env::set_var(&self.key, val);
+            unsafe { std::env::set_var(&self.key, val) };
         } else {
-            std::env::remove_var(&self.key);
+            unsafe { std::env::remove_var(&self.key) };
         }
     }
 }