summaryrefslogtreecommitdiff
path: root/plugins/workflow_objc/src/metadata
diff options
context:
space:
mode:
authorMark Rowe <mark@vector35.com>2025-08-11 10:42:07 -0700
committerMark Rowe <mark@vector35.com>2025-08-27 19:15:49 -0700
commit2303f75b080f6dd0c9a5c669a71f64ce830f5650 (patch)
treea49d80ea0a8131a50c16f85e767722679ff08c85 /plugins/workflow_objc/src/metadata
parentb302d7ba796f41b1102ee61feed8b8e212299997 (diff)
Rewrite Obj-C workflow in Rust
This is functionally equivalent to the previous workflow_objc, with the following changes: 1. It mutates the `core.function.metaAnalysis` workflow rather than registering a new named workflow. The activities now all check for the presence of the Objective-C metadata added by `ObjCProcessor` to determine whether they should do work, rather than relying on `MachoView` to override the function workflow when Objective-C metadata is present. This fixes https://github.com/Vector35/binaryninja-api/issues/6779. 2. The auto-inlining of `objc_msgSend` selector stub functions is performed in a separate activity from the processing of `objc_msgSend` call sites. The selector stub inlining activity is configured so that it does not run in `DSCView` as the shared cache needs different behavior for stub functions more generally that `SharedCacheWorkflow` already provides. 3. The way that types like `id` and `SEL` are referenced is fixed so that they show up as `id` rather than `objc_struct*`. This also replaces the Objective-C portion of the shared cache's workflow, and incorporates several bug fixes that had been applied to it but not the standalone Objective-C workflow.
Diffstat (limited to 'plugins/workflow_objc/src/metadata')
-rw-r--r--plugins/workflow_objc/src/metadata/global_state.rs207
-rw-r--r--plugins/workflow_objc/src/metadata/mod.rs5
-rw-r--r--plugins/workflow_objc/src/metadata/selector.rs52
3 files changed, 264 insertions, 0 deletions
diff --git a/plugins/workflow_objc/src/metadata/global_state.rs b/plugins/workflow_objc/src/metadata/global_state.rs
new file mode 100644
index 00000000..61104451
--- /dev/null
+++ b/plugins/workflow_objc/src/metadata/global_state.rs
@@ -0,0 +1,207 @@
+use binaryninja::{
+ binary_view::{BinaryView, BinaryViewBase, BinaryViewExt},
+ file_metadata::FileMetadata,
+ metadata::Metadata,
+ rc::Ref,
+ settings::{QueryOptions, Settings},
+ ObjectDestructor,
+};
+use dashmap::DashMap;
+use once_cell::sync::Lazy;
+use std::{
+ collections::{HashMap, HashSet},
+ ops::Range,
+ sync::{Arc, RwLock},
+};
+
+pub struct AnalysisInfo {
+ pub image_base: u64,
+ pub objc_stubs: Option<Range<u64>>,
+ pub should_rewrite_to_direct_calls: bool,
+ selector_impls: RwLock<SelectorImplsState>,
+}
+
+enum SelectorImplsState {
+ NotLoaded,
+ Loaded(Option<SelectorImplementations>),
+}
+
+struct SelectorImplementations {
+ sel_ref_to_impl: HashMap<u64, Vec<u64>>,
+ sel_to_impl: HashMap<u64, Vec<u64>>,
+}
+
+static VIEW_INFOS: Lazy<DashMap<usize, Arc<AnalysisInfo>>> = Lazy::new(DashMap::new);
+static IGNORED_VIEWS: Lazy<DashMap<usize, bool>> = Lazy::new(DashMap::new);
+
+struct ObjectLifetimeObserver;
+
+impl ObjectDestructor for ObjectLifetimeObserver {
+ fn destruct_file_metadata(&self, metadata: &FileMetadata) {
+ let id = metadata.session_id();
+ VIEW_INFOS.remove(&id);
+ IGNORED_VIEWS.remove(&id);
+ }
+}
+
+static SUPPORTED_ARCHS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
+ let mut m = HashSet::new();
+ m.insert("aarch64");
+ m.insert("x86_64");
+ m.insert("armv7");
+ m.insert("thumb2");
+ m
+});
+
+fn is_supported_arch(bv: &BinaryView) -> bool {
+ let arch_name = bv
+ .default_arch()
+ .map(|arch| arch.name())
+ .unwrap_or_default();
+ SUPPORTED_ARCHS.contains(&arch_name as &str)
+}
+
+pub struct GlobalState;
+
+impl GlobalState {
+ pub fn register_cleanup() {
+ let observer = Box::leak(Box::new(ObjectLifetimeObserver));
+ observer.register();
+ }
+
+ fn id(bv: &BinaryView) -> usize {
+ bv.file().session_id()
+ }
+
+ pub fn analysis_info(bv: &BinaryView) -> Option<Arc<AnalysisInfo>> {
+ let id = Self::id(bv);
+
+ if let Some(info) = VIEW_INFOS.get(&id) {
+ if bv.start() == info.image_base {
+ return Some(info.clone());
+ }
+ }
+
+ let info = Arc::new(AnalysisInfo::from_view(bv)?);
+ VIEW_INFOS.insert(id, info.clone());
+ Some(info)
+ }
+
+ pub fn should_ignore_view(bv: &BinaryView) -> bool {
+ if let Some(ignore) = IGNORED_VIEWS.get(&Self::id(bv)) {
+ return *ignore;
+ }
+
+ let ignore = !(is_supported_arch(bv) && AnalysisInfo::has_metadata(bv));
+ IGNORED_VIEWS.insert(Self::id(bv), ignore);
+ ignore
+ }
+}
+
+impl AnalysisInfo {
+ fn from_view(bv: &BinaryView) -> Option<Self> {
+ let should_rewrite_to_direct_calls = Settings::new().get_bool_with_opts(
+ "analysis.objectiveC.resolveDynamicDispatch",
+ &mut QueryOptions::new_with_view(bv),
+ );
+ let info = AnalysisInfo {
+ image_base: bv.start(),
+ objc_stubs: bv
+ .section_by_name("__objc_stubs")
+ .map(|section| section.start()..section.end()),
+ should_rewrite_to_direct_calls,
+ selector_impls: RwLock::new(SelectorImplsState::NotLoaded),
+ };
+ if !Self::has_metadata(bv) {
+ return None;
+ }
+ Some(info)
+ }
+
+ fn has_metadata(bv: &BinaryView) -> bool {
+ bv.query_metadata("Objective-C").is_some()
+ }
+
+ pub fn get_selector_impl(&self, bv: &BinaryView, selector_addr: u64) -> Option<u64> {
+ let get = |impls: &SelectorImplementations| {
+ impls
+ .sel_ref_to_impl
+ .get(&selector_addr)
+ .or_else(|| impls.sel_to_impl.get(&selector_addr))
+ .and_then(|v| v.first().copied())
+ .filter(|&addr| addr != 0)
+ };
+
+ let cache = self.selector_impls.read().unwrap();
+ match &*cache {
+ SelectorImplsState::Loaded(Some(impls)) => return get(impls),
+ SelectorImplsState::Loaded(None) => return None,
+ SelectorImplsState::NotLoaded => {}
+ }
+ drop(cache);
+
+ let mut cache = self.selector_impls.write().unwrap();
+ if let SelectorImplsState::NotLoaded = &*cache {
+ *cache = SelectorImplsState::Loaded(self.load_selector_impls(bv));
+ }
+
+ if let SelectorImplsState::Loaded(Some(impls)) = &*cache {
+ get(impls)
+ } else {
+ None
+ }
+ }
+
+ fn load_selector_impls(&self, bv: &BinaryView) -> Option<SelectorImplementations> {
+ let Some(Ok(meta)) = bv.get_metadata::<HashMap<String, Ref<Metadata>>>("Objective-C")
+ else {
+ return None;
+ };
+ let version_meta = meta.get("version")?;
+ if version_meta.get_unsigned_integer()? != 1 {
+ log::error!(
+ "workflow_objc: Unexpected Objective-C metadata version. Expected 1, got {}.",
+ version_meta.get_unsigned_integer()?
+ );
+ return None;
+ }
+
+ let mut sel_ref_to_impl = HashMap::new();
+ if let Some(sel_ref_to_impl_meta) = meta.get("selRefImplementations") {
+ if let Some(map) = Self::parse_selector_impls(sel_ref_to_impl_meta) {
+ sel_ref_to_impl = map;
+ }
+ }
+
+ let mut sel_to_impl = HashMap::new();
+ if let Some(sel_to_impl_meta) = meta.get("selImplementations") {
+ if let Some(map) = Self::parse_selector_impls(sel_to_impl_meta) {
+ sel_to_impl = map;
+ }
+ }
+
+ Some(SelectorImplementations {
+ sel_ref_to_impl,
+ sel_to_impl,
+ })
+ }
+
+ fn parse_selector_impls(meta: &Metadata) -> Option<HashMap<u64, Vec<u64>>> {
+ let array = meta.get_array()?;
+ let mut result = HashMap::new();
+ for item in &array {
+ let item = item.get_array()?;
+ if item.len() != 2 {
+ log::warn!(
+ "Expected selector implementation metadata to have 2 items, found {}",
+ item.len()
+ );
+ return None;
+ }
+ let selector = item.get(0).get_unsigned_integer()?;
+ let impls_meta = item.get(1).get_unsigned_integer_list()?;
+ result.insert(selector, impls_meta);
+ }
+ Some(result)
+ }
+}
diff --git a/plugins/workflow_objc/src/metadata/mod.rs b/plugins/workflow_objc/src/metadata/mod.rs
new file mode 100644
index 00000000..58926eb2
--- /dev/null
+++ b/plugins/workflow_objc/src/metadata/mod.rs
@@ -0,0 +1,5 @@
+mod global_state;
+mod selector;
+
+pub use global_state::GlobalState;
+pub use selector::Selector;
diff --git a/plugins/workflow_objc/src/metadata/selector.rs b/plugins/workflow_objc/src/metadata/selector.rs
new file mode 100644
index 00000000..a48be14d
--- /dev/null
+++ b/plugins/workflow_objc/src/metadata/selector.rs
@@ -0,0 +1,52 @@
+use crate::Error;
+use binaryninja::binary_view::{BinaryView, BinaryViewBase as _, BinaryViewExt};
+
+pub struct Selector {
+ pub name: String,
+ pub addr: u64,
+}
+
+impl Selector {
+ pub fn from_address(bv: &BinaryView, addr: u64) -> Result<Self, Error> {
+ let name = if bv.offset_valid(addr) {
+ // Read the selector name from the binary view
+ read_cstring(bv, addr, 500)
+ } else {
+ // Look for the `sel_` symbols that ObjCProcessor adds to represent selectors
+ // whose backing regions have not yet been loaded into the view.
+ bv.symbol_by_address(addr)
+ .and_then(|sym| sym.raw_name().to_str().ok().map(|name| name.to_owned()))
+ .filter(|name| name.starts_with("sel_"))
+ .map(|name| name["sel_".len()..].to_string())
+ }
+ .ok_or(Error::InvalidSelector { address: addr })?;
+ Ok(Selector { name, addr })
+ }
+
+ pub fn argument_labels(&self) -> Vec<String> {
+ if !self.name.contains(':') {
+ return vec![];
+ }
+
+ self.name
+ .split(':')
+ .filter(|s| !s.is_empty())
+ .map(|s| s.to_string())
+ .collect()
+ }
+}
+
+// Read a null-terminated string from the view
+fn read_cstring(bv: &BinaryView, address: u64, max_len: usize) -> Option<String> {
+ let mut buffer = vec![0u8; max_len];
+ let bytes_read = bv.read(&mut buffer, address);
+ if bytes_read == 0 {
+ return None;
+ }
+
+ // Find the null terminator
+ let null_pos = buffer.iter().position(|&b| b == 0).unwrap_or(bytes_read);
+ buffer.truncate(null_pos);
+
+ String::from_utf8(buffer).ok()
+}