summaryrefslogtreecommitdiff
path: root/rust
diff options
context:
space:
mode:
authorLukBukkit <luk.bukkit@gmail.com>2025-04-24 16:20:32 +0000
committerMason Reed <mason@vector35.com>2025-10-07 16:47:33 -0400
commit64633f61f7b9e03be9437b5f4896bbe122f7a7a2 (patch)
tree8fafed1395a8d2e29e5a6b8b7f7d7b8a2af2731a /rust
parente6e4ebea5d0ea7842766c7e686e0dfd45cb54bb8 (diff)
[Rust] Implement custom data renderer API
Also adds an example plugin and misc rust fixes / documentation. This is a continuation of https://github.com/Vector35/binaryninja-api/pull/6721 Co-authored-by: rbran <lgit@rubens.io>
Diffstat (limited to 'rust')
-rw-r--r--rust/plugin_examples/data_renderer/Cargo.toml13
-rw-r--r--rust/plugin_examples/data_renderer/README.md63
-rw-r--r--rust/plugin_examples/data_renderer/build.rs25
-rw-r--r--rust/plugin_examples/data_renderer/src/lib.rs111
-rw-r--r--rust/src/data_renderer.rs242
-rw-r--r--rust/src/disassembly.rs16
-rw-r--r--rust/src/lib.rs1
-rw-r--r--rust/tests/data_renderer.rs122
8 files changed, 591 insertions, 2 deletions
diff --git a/rust/plugin_examples/data_renderer/Cargo.toml b/rust/plugin_examples/data_renderer/Cargo.toml
new file mode 100644
index 00000000..7ac32dfd
--- /dev/null
+++ b/rust/plugin_examples/data_renderer/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "example_data_renderer"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+binaryninjacore-sys = { path = "../../binaryninjacore-sys" }
+binaryninja = { path = "../.." }
+uuid = "1.18.1"
+log = "0.4.27"
diff --git a/rust/plugin_examples/data_renderer/README.md b/rust/plugin_examples/data_renderer/README.md
new file mode 100644
index 00000000..794b93b9
--- /dev/null
+++ b/rust/plugin_examples/data_renderer/README.md
@@ -0,0 +1,63 @@
+# Data Renderer Example
+
+This example implements a simple data renderer for the Mach-O load command LC_UUID.
+You can try the renderer by loading the `/bin/cat` binary from macOS.
+
+We're implementing a functionality similar to the one described in the Python data renderer blog post:
+https://binary.ninja/2024/04/08/customizing-data-display.html.
+
+## Building
+
+```sh
+# Build from the root directory (binaryninja-api)
+cargo build --manifest-path rust/plugin_examples/data_renderer/Cargo.toml
+# Link binary on macOS
+ln -sf $PWD/target/debug/libexample_data_renderer.dylib ~/Library/Application\ Support/Binary\ Ninja/plugins
+```
+
+## Result
+
+The following Mach-O load command be will be transformed from
+
+```c
+struct uuid __macho_load_command_[10] =
+{
+ enum load_command_type_t cmd = LC_UUID
+ uint32_t cmdsize = 0x18
+ uint8_t uuid[0x10] =
+ {
+ [0x0] = 0x74
+ [0x1] = 0xa0
+ [0x2] = 0x3a
+ [0x3] = 0xbd
+ [0x4] = 0x1e
+ [0x5] = 0x19
+ [0x6] = 0x32
+ [0x7] = 0x67
+ [0x8] = 0x9a
+ [0x9] = 0xdc
+ [0xa] = 0x42
+ [0xb] = 0x99
+ [0xc] = 0x4e
+ [0xd] = 0x26
+ [0xe] = 0xa2
+ [0xf] = 0xb7
+ }
+}
+```
+
+into the following representation
+
+```c
+struct uuid __macho_load_command_[10] =
+{
+ enum load_command_type_t cmd = LC_UUID
+ uint32_t cmdsize = 0x18
+ uint8_t uuid[0x10] = UUID("74a03abd-1e19-3267-9adc-42994e26a2b7")
+}
+```
+
+You can compare the shown UUID with the output of otool:
+```sh
+otool -arch all -l /bin/cat
+``` \ No newline at end of file
diff --git a/rust/plugin_examples/data_renderer/build.rs b/rust/plugin_examples/data_renderer/build.rs
new file mode 100644
index 00000000..9006f16a
--- /dev/null
+++ b/rust/plugin_examples/data_renderer/build.rs
@@ -0,0 +1,25 @@
+fn main() {
+ let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH")
+ .expect("DEP_BINARYNINJACORE_PATH not specified");
+
+ println!("cargo::rustc-link-lib=dylib=binaryninjacore");
+ println!("cargo::rustc-link-search={}", link_path.to_str().unwrap());
+
+ #[cfg(target_os = "linux")]
+ {
+ println!(
+ "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}",
+ link_path.to_string_lossy()
+ );
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME not set");
+ let lib_name = crate_name.replace('-', "_");
+ println!(
+ "cargo::rustc-link-arg=-Wl,-install_name,@rpath/lib{}.dylib",
+ lib_name
+ );
+ }
+}
diff --git a/rust/plugin_examples/data_renderer/src/lib.rs b/rust/plugin_examples/data_renderer/src/lib.rs
new file mode 100644
index 00000000..178402af
--- /dev/null
+++ b/rust/plugin_examples/data_renderer/src/lib.rs
@@ -0,0 +1,111 @@
+use binaryninja::binary_view::{BinaryView, BinaryViewBase};
+use binaryninja::data_renderer::{
+ register_data_renderer, CustomDataRenderer, RegistrationType, TypeContext,
+};
+use binaryninja::disassembly::{
+ DisassemblyTextLine, InstructionTextToken, InstructionTextTokenKind,
+};
+use binaryninja::types::{Type, TypeClass};
+use uuid::Uuid;
+
+struct UuidDataRenderer {}
+
+impl CustomDataRenderer for UuidDataRenderer {
+ const REGISTRATION_TYPE: RegistrationType = RegistrationType::Specific;
+
+ fn is_valid_for_data(
+ &self,
+ _view: &BinaryView,
+ _addr: u64,
+ type_: &Type,
+ types: &[TypeContext],
+ ) -> bool {
+ // We only want to render arrays with a size of 16 elements
+ if type_.type_class() != TypeClass::ArrayTypeClass {
+ return false;
+ }
+ if type_.count() != 0x10 {
+ return false;
+ }
+
+ // The array elements must be of the type uint8_t
+ let Some(element_type_conf) = type_.element_type() else {
+ return false;
+ };
+ let element_type = element_type_conf.contents;
+ if element_type.type_class() != TypeClass::IntegerTypeClass {
+ return false;
+ }
+ if element_type.width() != 1 {
+ return false;
+ }
+
+ // The array should be embedded in a named type reference with the id macho:["uuid"]
+ for type_ctx in types {
+ if type_ctx.ty().type_class() != TypeClass::NamedTypeReferenceClass {
+ continue;
+ }
+
+ let Some(name_ref) = type_ctx.ty().get_named_type_reference() else {
+ continue;
+ };
+
+ if name_ref.id() == "macho:[\"uuid\"]" {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ fn lines_for_data(
+ &self,
+ view: &BinaryView,
+ addr: u64,
+ _type_: &Type,
+ prefix: Vec<InstructionTextToken>,
+ _width: usize,
+ _types_ctx: &[TypeContext],
+ _language: &str,
+ ) -> Vec<DisassemblyTextLine> {
+ let mut tokens = prefix.clone();
+
+ let mut buf = [0u8; 0x10];
+ let bytes_read = view.read(&mut buf, addr);
+
+ // Make sure that we've read all UUID bytes and convert them to token
+ if bytes_read == 0x10 {
+ tokens.extend([
+ InstructionTextToken::new("UUID(\"", InstructionTextTokenKind::Text),
+ InstructionTextToken::new(
+ Uuid::from_bytes(buf).to_string(),
+ InstructionTextTokenKind::String { value: 0 },
+ ),
+ InstructionTextToken::new("\")", InstructionTextTokenKind::Text),
+ ]);
+ } else {
+ tokens.push(InstructionTextToken::new(
+ "error: cannot read 0x10 bytes",
+ InstructionTextTokenKind::Annotation,
+ ));
+ }
+
+ vec![DisassemblyTextLine::new_with_addr(tokens, addr)]
+ }
+}
+
+/// # Safety
+/// This function is called from Binary Ninja once to initialize the plugin.
+#[allow(non_snake_case)]
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn CorePluginInit() -> bool {
+ // Initialize logging
+ binaryninja::logger::Logger::new("UUID Data Renderer")
+ .with_level(log::LevelFilter::Debug)
+ .init();
+
+ // Register data renderer
+ register_data_renderer(UuidDataRenderer {});
+
+ true
+}
diff --git a/rust/src/data_renderer.rs b/rust/src/data_renderer.rs
new file mode 100644
index 00000000..6fa19b59
--- /dev/null
+++ b/rust/src/data_renderer.rs
@@ -0,0 +1,242 @@
+use binaryninjacore_sys::*;
+use core::ffi;
+use ffi::c_void;
+use std::fmt::Debug;
+use std::ptr::NonNull;
+
+use crate::binary_view::BinaryView;
+use crate::disassembly::{DisassemblyTextLine, InstructionTextToken};
+use crate::rc::Array;
+use crate::string::BnString;
+use crate::types::Type;
+
+/// Registers a custom data renderer, this allows you to customize the representation of data variables.
+pub fn register_data_renderer<C: CustomDataRenderer>(
+ custom: C,
+) -> (&'static mut C, CoreDataRenderer) {
+ let renderer = Box::leak(Box::new(custom));
+ let mut callbacks = BNCustomDataRenderer {
+ context: renderer as *mut _ as *mut c_void,
+ freeObject: Some(cb_free_object::<C>),
+ isValidForData: Some(cb_is_valid_for_data::<C>),
+ getLinesForData: Some(cb_get_lines_for_data::<C>),
+ freeLines: Some(cb_free_lines),
+ };
+ let result = unsafe { BNCreateDataRenderer(&mut callbacks) };
+ let core = unsafe { CoreDataRenderer::from_raw(NonNull::new(result).unwrap()) };
+ let container = DataRendererContainer::get();
+ match C::REGISTRATION_TYPE {
+ RegistrationType::Generic => container.register_data_renderer(&core),
+ RegistrationType::Specific => container.register_specific_data_renderer(&core),
+ }
+ (renderer, core)
+}
+
+/// Renders the data at the given address using the registered data renderers, returning associated lines.
+pub fn render_lines_for_data(
+ view: &BinaryView,
+ addr: u64,
+ type_: &Type,
+ prefix: Vec<InstructionTextToken>,
+ width: usize,
+ types_ctx: &[TypeContext],
+ language: Option<&str>,
+) -> Vec<DisassemblyTextLine> {
+ let bn_prefix: Vec<BNInstructionTextToken> = prefix
+ .into_iter()
+ .map(InstructionTextToken::into_raw)
+ .collect();
+ let bn_language = BnString::from(language.unwrap_or(""));
+
+ let mut count: usize = 0;
+ let lines_ptr = unsafe {
+ BNRenderLinesForData(
+ view.handle,
+ addr,
+ type_.handle,
+ bn_prefix.as_ptr(),
+ bn_prefix.len(),
+ width,
+ &mut count as *mut usize,
+ types_ctx.as_ptr() as *mut BNTypeContext,
+ types_ctx.len(),
+ bn_language.as_ptr(),
+ )
+ };
+
+ for token in bn_prefix {
+ InstructionTextToken::free_raw(token);
+ }
+
+ let lines_arr: Array<DisassemblyTextLine> = unsafe { Array::new(lines_ptr, count, ()) };
+ lines_arr.to_vec()
+}
+
+#[derive(Clone, Copy)]
+struct DataRendererContainer {
+ pub(crate) handle: *mut BNDataRendererContainer,
+}
+
+impl DataRendererContainer {
+ pub fn get() -> Self {
+ Self {
+ handle: unsafe { BNGetDataRendererContainer() },
+ }
+ }
+
+ pub fn register_data_renderer(&self, renderer: &CoreDataRenderer) {
+ unsafe { BNRegisterGenericDataRenderer(self.handle, renderer.handle.as_ptr()) };
+ }
+
+ pub fn register_specific_data_renderer(&self, renderer: &CoreDataRenderer) {
+ unsafe { BNRegisterTypeSpecificDataRenderer(self.handle, renderer.handle.as_ptr()) };
+ }
+}
+
+/// Used by [`CustomDataRenderer`] to determine the priority of the renderer relative to other registered renderers.
+pub enum RegistrationType {
+ Generic,
+ /// This data renderer wants to run before any generic data renderers.
+ ///
+ /// Use this if you want to take priority over rendering of specific types.
+ Specific,
+}
+
+pub trait CustomDataRenderer: Sized + Sync + Send + 'static {
+ /// The registration type for the renderer really only determines the priority for the renderer.
+ ///
+ /// If you are overriding the behavior of a specific type, you should use [`RegistrationType::Specific`].
+ const REGISTRATION_TYPE: RegistrationType;
+
+ fn is_valid_for_data(
+ &self,
+ view: &BinaryView,
+ addr: u64,
+ type_: &Type,
+ types: &[TypeContext],
+ ) -> bool;
+
+ fn lines_for_data(
+ &self,
+ view: &BinaryView,
+ addr: u64,
+ type_: &Type,
+ prefix: Vec<InstructionTextToken>,
+ width: usize,
+ types_ctx: &[TypeContext],
+ language: &str,
+ ) -> Vec<DisassemblyTextLine>;
+}
+
+pub struct CoreDataRenderer {
+ pub(crate) handle: NonNull<BNDataRenderer>,
+}
+
+impl CoreDataRenderer {
+ pub(crate) unsafe fn from_raw(handle: NonNull<BNDataRenderer>) -> CoreDataRenderer {
+ Self { handle }
+ }
+}
+
+/// Data renderers are recursive, so we keep track of observed types.
+///
+/// This can be used to influence the rendering of structure fields and related nested types.
+#[repr(transparent)]
+pub struct TypeContext {
+ handle: BNTypeContext,
+}
+
+impl TypeContext {
+ /// The [`Type`] in the context.
+ pub fn ty(&self) -> &Type {
+ // SAFETY Type and `*mut BNType` are transparent, and the type is expected to be valid for the lifetime of the context.
+ unsafe { core::mem::transmute::<&*mut BNType, &Type>(&self.handle.type_) }
+ }
+
+ /// The offset with which the type is associated.
+ ///
+ /// The offset in many cases refers to a structure byte offset.
+ pub fn offset(&self) -> usize {
+ self.handle.offset
+ }
+}
+
+impl Debug for TypeContext {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("TypeContext")
+ .field("ty", &self.ty())
+ .field("offset", &self.offset())
+ .finish()
+ }
+}
+
+unsafe extern "C" fn cb_free_object<C: CustomDataRenderer>(ctxt: *mut c_void) {
+ let _ = Box::from_raw(ctxt as *mut C);
+}
+
+unsafe extern "C" fn cb_is_valid_for_data<C: CustomDataRenderer>(
+ ctxt: *mut c_void,
+ view: *mut BNBinaryView,
+ addr: u64,
+ type_: *mut BNType,
+ type_ctx: *mut BNTypeContext,
+ ctx_count: usize,
+) -> bool {
+ let ctxt = ctxt as *mut C;
+ // SAFETY BNTypeContext and TypeContext are transparent
+ let types = core::slice::from_raw_parts(type_ctx as *mut TypeContext, ctx_count);
+ (*ctxt).is_valid_for_data(
+ &BinaryView::from_raw(view),
+ addr,
+ &Type::from_raw(type_),
+ types,
+ )
+}
+
+unsafe extern "C" fn cb_get_lines_for_data<C: CustomDataRenderer>(
+ ctxt: *mut c_void,
+ view: *mut BNBinaryView,
+ addr: u64,
+ type_: *mut BNType,
+ prefix: *const BNInstructionTextToken,
+ prefix_count: usize,
+ width: usize,
+ count: *mut usize,
+ type_ctx: *mut BNTypeContext,
+ ctx_count: usize,
+ language: *const ffi::c_char,
+) -> *mut BNDisassemblyTextLine {
+ let ctxt = ctxt as *mut C;
+ // SAFETY BNTypeContext and TypeContext are transparent
+ let types = core::slice::from_raw_parts(type_ctx as *mut TypeContext, ctx_count);
+ let prefix = core::slice::from_raw_parts(prefix, prefix_count)
+ .iter()
+ .map(InstructionTextToken::from_raw)
+ .collect::<Vec<_>>();
+ let result = (*ctxt).lines_for_data(
+ &BinaryView::from_raw(view),
+ addr,
+ &Type::from_raw(type_),
+ prefix,
+ width,
+ types,
+ ffi::CStr::from_ptr(language).to_str().unwrap(),
+ );
+ let result: Box<[BNDisassemblyTextLine]> = result
+ .into_iter()
+ .map(DisassemblyTextLine::into_raw)
+ .collect();
+ *count = result.len();
+ Box::leak(result).as_mut_ptr()
+}
+
+unsafe extern "C" fn cb_free_lines(
+ _ctx: *mut c_void,
+ lines: *mut BNDisassemblyTextLine,
+ count: usize,
+) {
+ let lines = Box::from_raw(core::slice::from_raw_parts_mut(lines, count));
+ for line in lines {
+ let _ = DisassemblyTextLine::from_raw(&line);
+ }
+}
diff --git a/rust/src/disassembly.rs b/rust/src/disassembly.rs
index ed9b0854..cd5a860d 100644
--- a/rust/src/disassembly.rs
+++ b/rust/src/disassembly.rs
@@ -142,6 +142,14 @@ impl DisassemblyTextLine {
..Default::default()
}
}
+
+ pub fn new_with_addr(tokens: Vec<InstructionTextToken>, addr: u64) -> Self {
+ Self {
+ address: addr,
+ tokens,
+ ..Default::default()
+ }
+ }
}
impl From<&str> for DisassemblyTextLine {
@@ -308,6 +316,10 @@ impl InstructionTextToken {
}
}
+ /// Construct a new token **without** an associated address.
+ ///
+ /// You most likely want to call [`InstructionTextToken::new_with_address`], while also adjusting
+ /// the [`InstructionTextToken::expr_index`] field where applicable.
pub fn new(text: impl Into<String>, kind: InstructionTextTokenKind) -> Self {
Self {
address: 0,
@@ -493,13 +505,13 @@ pub enum InstructionTextTokenKind {
hash: Option<u64>,
},
CodeSymbol {
- // TODO: Value of what?
+ // Target address of the symbol
value: u64,
// TODO: Size of what?
size: usize, // TODO: Operand?
},
DataSymbol {
- // TODO: Value of what?
+ // Target address of the symbol
value: u64,
// TODO: Size of what?
size: usize, // TODO: Operand?
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 9d34f6bc..c9b0bb0a 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -39,6 +39,7 @@ pub mod component;
pub mod confidence;
pub mod custom_binary_view;
pub mod data_buffer;
+pub mod data_renderer;
pub mod database;
pub mod debuginfo;
pub mod demangle;
diff --git a/rust/tests/data_renderer.rs b/rust/tests/data_renderer.rs
new file mode 100644
index 00000000..fdf39697
--- /dev/null
+++ b/rust/tests/data_renderer.rs
@@ -0,0 +1,122 @@
+use binaryninja::binary_view::{BinaryView, BinaryViewExt};
+use binaryninja::data_renderer::{
+ register_data_renderer, render_lines_for_data, CustomDataRenderer, RegistrationType,
+ TypeContext,
+};
+use binaryninja::disassembly::{
+ DisassemblyTextLine, InstructionTextToken, InstructionTextTokenKind,
+};
+use binaryninja::headless::Session;
+use binaryninja::types::Type;
+use std::path::PathBuf;
+
+struct StructRenderer {}
+impl CustomDataRenderer for StructRenderer {
+ const REGISTRATION_TYPE: RegistrationType = RegistrationType::Specific;
+
+ fn is_valid_for_data(
+ &self,
+ _view: &BinaryView,
+ _addr: u64,
+ type_: &Type,
+ _types: &[TypeContext],
+ ) -> bool {
+ type_.get_structure().is_some()
+ }
+
+ fn lines_for_data(
+ &self,
+ _view: &BinaryView,
+ addr: u64,
+ type_: &Type,
+ _prefix: Vec<InstructionTextToken>,
+ width: usize,
+ _types_ctx: &[TypeContext],
+ _language: &str,
+ ) -> Vec<DisassemblyTextLine> {
+ let name = type_.registered_name().map(|name| name.name().to_string());
+ let Some(type_) = type_.get_structure() else {
+ unreachable!();
+ };
+
+ let mut output = vec![
+ DisassemblyTextLine::new(vec![InstructionTextToken::new(
+ format!(
+ "Struct{}{} width {} or {width} {addr}",
+ name.as_ref().map(|_| " ").unwrap_or(""),
+ name.as_ref().map(String::as_str).unwrap_or(""),
+ type_.width()
+ ),
+ InstructionTextTokenKind::Comment { target: addr },
+ )]),
+ DisassemblyTextLine::new(vec![InstructionTextToken::new(
+ "{",
+ InstructionTextTokenKind::Text,
+ )]),
+ ];
+ let members = type_.members();
+ let offset_size =
+ usize::try_from(members.last().map(|last| last.offset.ilog(16)).unwrap_or(0) + 3)
+ .unwrap();
+ for member in members {
+ let line = [
+ InstructionTextToken::new(
+ format!("{:#0width$x}", member.offset, width = offset_size),
+ InstructionTextTokenKind::StructOffset {
+ offset: member.offset,
+ type_names: vec![member.name.clone()],
+ },
+ ),
+ InstructionTextToken::new("|", InstructionTextTokenKind::Text),
+ InstructionTextToken::new(
+ member.name.clone(),
+ InstructionTextTokenKind::FieldName {
+ offset: member.offset,
+ type_names: vec![member.name.clone()],
+ },
+ ),
+ InstructionTextToken::new(",", InstructionTextTokenKind::Text),
+ ];
+ output.push(DisassemblyTextLine::new(line.to_vec()));
+ }
+ output.push(DisassemblyTextLine::new(vec![InstructionTextToken::new(
+ "}",
+ InstructionTextTokenKind::Text,
+ )]));
+ output
+ }
+}
+
+#[test]
+fn test_data_renderer_basic() {
+ let _session = Session::new().expect("Failed to initialize session");
+ let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap();
+ let view = binaryninja::load(out_dir.join("atox.obj")).expect("Failed to create view");
+ let _ = register_data_renderer(StructRenderer {});
+
+ // This will use all available data renderers, so we are also verifying that our custom renderer is being used.
+ let lines = render_lines_for_data(
+ &view,
+ 0x362e9,
+ &view.type_by_name("_ABC").unwrap(),
+ vec![],
+ 100,
+ &[],
+ None,
+ );
+
+ // TODO: This is not really checking all possible issues that could occur with round-tripping.
+ // TODO: But it is a good start to just make sure it visually is what we expect.
+ let lines_str = lines.iter().map(ToString::to_string).collect::<Vec<_>>();
+ assert_eq!(
+ lines_str,
+ vec![
+ "Struct _ABC width 12 or 100 221929",
+ "{",
+ "0x0|abcA,",
+ "0x4|abcB,",
+ "0x8|abcC,",
+ "}"
+ ]
+ )
+}