Skip to content

implement rust plugin command #6583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub mod main_thread;
pub mod medium_level_il;
pub mod metadata;
pub mod platform;
pub mod plugin_command;
pub mod progress;
pub mod project;
pub mod rc;
Expand Down
261 changes: 261 additions & 0 deletions rust/src/plugin_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
use core::ffi;
use std::ffi::CStr;

use binaryninjacore_sys::*;

use crate::architecture::CoreArchitecture;
use crate::binary_view::BinaryViewExt;
use crate::high_level_il::HighLevelILFunction;
use crate::low_level_il::function::{Finalized, LowLevelILFunction, NonSSA, RegularNonSSA};
use crate::medium_level_il::MediumLevelILFunction;
use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner};
use crate::string::BnStrCompatible;
use crate::{BinaryView, Function};

type PluginCommandType = BNPluginCommandType;

#[repr(transparent)]
#[derive(Debug)]
pub struct PluginCommand<T> {
handle: BNPluginCommand,
_kind: core::marker::PhantomData<T>,
}

impl<T> PluginCommand<T> {
pub fn type_(&self) -> PluginCommandType {
self.handle.type_
}
pub fn name(&self) -> &str {
unsafe { CStr::from_ptr(self.handle.name) }
.to_str()
.unwrap()
}
pub fn description(&self) -> &str {
unsafe { CStr::from_ptr(self.handle.description) }
.to_str()
.unwrap()
}
}

impl<T> CoreArrayProvider for PluginCommand<T> {
type Raw = BNPluginCommand;
type Context = ();
type Wrapped<'a>
= &'a PluginCommand<T>
where
T: 'a;
}

unsafe impl<T> CoreArrayProviderInner for PluginCommand<T> {
unsafe fn free(raw: *mut Self::Raw, _count: usize, _context: &Self::Context) {
BNFreePluginCommandList(raw)
}

unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> {
// SAFETY BNPluginCommand and PluginCommand are transparent
core::mem::transmute::<&'a BNPluginCommand, &'a PluginCommand<T>>(raw)
}
}

macro_rules! define_enum {
(
$bv_name:ident,
$($get_all:ident, $register_name:ident, $enum_name:ident, $trait_name:ident {
$fun_run:ident :: $fun_is_valid:ident(
$(
$arg_name:ident:
$raw_arg_type:ty:
$arg_type:ty =
$rust2ffi:expr =>
$ffi2rust:expr
),* $(,)?
) $(,)?
}),* $(,)?
) => {
impl PluginCommand<PluginCommandAll> {
pub fn valid_plugin_commands() -> Array<Self> {
let mut count = 0;
let array = unsafe { BNGetAllPluginCommands(&mut count) };
unsafe { Array::new(array, count, ()) }
}
pub fn execution(&self) -> PluginCommandAll {
match self.type_() {
$(PluginCommandType::$enum_name => {
PluginCommandAll::$enum_name($enum_name {
context: self.handle.context,
is_valid: self.handle.$fun_is_valid,
run: self.handle.$fun_run.unwrap(),
})
}),*
}
}
}

pub enum PluginCommandAll {
$($enum_name($enum_name)),*
}

$(
pub struct $enum_name {
context: *mut ffi::c_void,
is_valid: Option<unsafe extern "C" fn (
ctxt: *mut ffi::c_void,
$bv_name: *mut BNBinaryView,
$($raw_arg_type),*
) -> bool>,
run: unsafe extern "C" fn (
ctxt: *mut ffi::c_void,
$bv_name: *mut BNBinaryView,
$($raw_arg_type),*
),
}

impl $enum_name {
pub fn is_valid(
&mut self,
$bv_name: &BinaryView,
$($arg_name: $arg_type),*
) -> bool {
// TODO I'm assuming is_valid be null means it's always valid
let Some(fun) = self.is_valid else {
return true
};
unsafe{ fun(self.context, $bv_name.handle, $($rust2ffi),*) }
}

pub fn run(
&mut self,
$bv_name: &BinaryView,
$($arg_name: $arg_type),*
) {
unsafe{ (self.run)(self.context, $bv_name.handle, $($rust2ffi),*) }
}
}
)*

$(
pub trait $trait_name: Send + Sync + 'static {
fn is_valid(
&mut self,
$bv_name: &BinaryView,
$($arg_name: $arg_type),*
) -> bool;
fn run(
&mut self,
$bv_name: &BinaryView,
$($arg_name: $arg_type),*
);
fn register(self, name: impl BnStrCompatible, description: impl BnStrCompatible) where Self: Sized {
unsafe extern "C" fn ffi_action<T: $trait_name>(
ctxt: *mut ffi::c_void,
$bv_name: *mut BNBinaryView,
$($arg_name: $raw_arg_type),*
) {
let slf = ctxt as *mut T;
(*slf).run(&BinaryView::from_raw($bv_name), $($ffi2rust),*)
}
unsafe extern "C" fn ffi_is_valid<T: $trait_name>(
ctxt: *mut ffi::c_void,
$bv_name: *mut BNBinaryView,
$($arg_name: $raw_arg_type),*
) -> bool {
let slf = ctxt as *mut T;
(*slf).is_valid(&BinaryView::from_raw($bv_name), $($ffi2rust),*)
}
let name = name.into_bytes_with_nul();
let description = description.into_bytes_with_nul();
unsafe{ $register_name(
name.as_ref().as_ptr() as *const ffi::c_char,
description.as_ref().as_ptr() as *const ffi::c_char,
Some(ffi_action::<Self>),
Some(ffi_is_valid::<Self>),
Box::leak(Box::new(self)) as *mut Self as *mut ffi::c_void,
) }
}
}

impl PluginCommand<$enum_name> {
pub fn valid_plugin_commands(view: &BinaryView, $($arg_name: $arg_type),*) -> Array<Self> {
let mut count = 0;
let array = unsafe { $get_all(view.handle, $($rust2ffi, )* &mut count) };
unsafe { Array::new(array, count, ()) }
}

pub fn execution(&self) -> $enum_name {
assert_eq!(self.type_(), PluginCommandType::$enum_name);
$enum_name {
context: self.handle.context,
is_valid: self.handle.$fun_is_valid,
run: self.handle.$fun_run.unwrap(),
}
}
}
)*
};
}

define_enum! {
view,
BNGetValidPluginCommands, BNRegisterPluginCommand, DefaultPluginCommand, CustomDefaultPluginCommand {
defaultCommand::defaultIsValid(),
},
BNGetValidPluginCommandsForAddress, BNRegisterPluginCommandForAddress, AddressPluginCommand, CustomAddressPluginCommand {
addressCommand::addressIsValid(
addr: u64: u64 = addr => addr,
),
},
BNGetValidPluginCommandsForRange, BNRegisterPluginCommandForRange, RangePluginCommand, CustomRangePluginCommand {
rangeCommand::rangeIsValid(
addr: u64: u64 = addr => addr,
len: u64: u64 = len => len,
),
},
BNGetValidPluginCommandsForFunction, BNRegisterPluginCommandForFunction, FunctionPluginCommand, CustomFunctionPluginCommand {
functionCommand::functionIsValid(
func: *mut BNFunction: &Function = func.handle => &Function::from_raw(func),
),
},
BNGetValidPluginCommandsForLowLevelILFunction, BNRegisterPluginCommandForLowLevelILFunction, LowLevelILFunctionPluginCommand, CustomLowLevelILFunctionPluginCommand {
lowLevelILFunctionCommand::lowLevelILFunctionIsValid(
// TODO I don't know what Generics should be used here
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RegularLowLevelILFunction

Need to document this more... but generally use that unless you are emitting LLIL or need SSA version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RegularLowLevelILFunction<CoreArchitecture> with BinaryView::default_arch?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the LLIL function in rust needs to stop trying to track the arch at construction, i.e. do what C++ does

Ref<Architecture> LowLevelILFunction::GetArchitecture() const
{
Ref<Function> func = GetFunction();
if (!func)
return nullptr;
return func->GetArchitecture();
}

IDK the implication of this, but otherwise we need to manually retrieve the underlying function object to get the architecture.

You might have ran into this with your PR #5318

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely so that we get the generic A in the arch signature here:

pub(crate) fn arch(&self) -> &A {
self.arch_handle.borrow()
}

So we don't have another layer of indirection though the core to get at our architecture from LLIL.

Copy link
Member

@emesare emesare Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling the default_arch obviously is not the correct move here, but you can leave it for now and add a TODO, the underlying issue is we need a way to construct the LLIL function with the function objects underlying core architecture, this is easy to "solve" with a new function that calls

pub fn function(&self) -> Ref<Function> {
unsafe {
let func = BNGetLowLevelILOwnerFunction(self.handle);
Function::ref_from_raw(func)
}
}
to get the function architecture

And then sets the arch handle for the LLIL function:

pub(crate) arch_handle: A::Handle,

Copy link
Member

@emesare emesare Apr 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the right answer is a new new function that will lookup the CoreArchitecture. I think keeping that generic around is the right move.

EDIT: Didn't see the commit right below, ignore this message lol

func: *mut BNLowLevelILFunction: &LowLevelILFunction<CoreArchitecture, Finalized, NonSSA<RegularNonSSA>> =
func.handle => &LowLevelILFunction::from_raw(
BinaryView::from_raw(view).default_arch().unwrap(),
func,
),
),
},
BNGetValidPluginCommandsForLowLevelILInstruction, BNRegisterPluginCommandForLowLevelILInstruction, LowLevelILInstructionPluginCommand, CustomLowLevelILInstructionPluginCommand {
lowLevelILInstructionCommand::lowLevelILInstructionIsValid(
func: *mut BNLowLevelILFunction: &LowLevelILFunction<CoreArchitecture, Finalized, NonSSA<RegularNonSSA>> =
func.handle => &LowLevelILFunction::from_raw(
BinaryView::from_raw(view).default_arch().unwrap(),
func,
),
instr: usize: usize = instr => instr,
),
},
BNGetValidPluginCommandsForMediumLevelILFunction, BNRegisterPluginCommandForMediumLevelILFunction, MediumLevelILFunctionPluginCommand, CustomMediumLevelILFunctionPluginCommand {
mediumLevelILFunctionCommand::mediumLevelILFunctionIsValid(
func: *mut BNMediumLevelILFunction: &MediumLevelILFunction = func.handle => &MediumLevelILFunction::from_raw(func),
),
},
BNGetValidPluginCommandsForMediumLevelILInstruction, BNRegisterPluginCommandForMediumLevelILInstruction, MediumLevelILInstructionPluginCommand, CustomMediumLevelILInstructionPluginCommand {
mediumLevelILInstructionCommand::mediumLevelILInstructionIsValid(
func: *mut BNMediumLevelILFunction: &MediumLevelILFunction = func.handle => &MediumLevelILFunction::from_raw(func),
instr: usize: usize = instr => instr,
),
},
BNGetValidPluginCommandsForHighLevelILFunction, BNRegisterPluginCommandForHighLevelILFunction, HighLevelILFunctionPluginCommand, CustomHighLevelILFunctionPluginCommand {
// TODO I don't know if the value is full_ast or not
highLevelILFunctionCommand::highLevelILFunctionIsValid(
func: *mut BNHighLevelILFunction: &HighLevelILFunction = func.handle => &HighLevelILFunction{full_ast: false, handle: func},
),
},
BNGetValidPluginCommandsForHighLevelILInstruction, BNRegisterPluginCommandForHighLevelILInstruction, HighLevelILInstructionPluginCommand, CustomHighLevelILInstructionPluginCommand {
highLevelILInstructionCommand::highLevelILInstructionIsValid(
func: *mut BNHighLevelILFunction: &HighLevelILFunction = func.handle => &HighLevelILFunction{full_ast: false, handle: func},
instr: usize: usize = instr => instr,
),
},
}
51 changes: 51 additions & 0 deletions rust/tests/plugin_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use binaryninja::binary_view::BinaryView;
use binaryninja::headless::Session;
use binaryninja::plugin_command::{CustomDefaultPluginCommand, PluginCommand, PluginCommandAll};

#[test]
fn test_custom_plugin_command() {
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 counter = Arc::new(Mutex::new(0));
struct MyCommand {
counter: Arc<Mutex<usize>>,
}
impl CustomDefaultPluginCommand for MyCommand {
fn is_valid(&mut self, _view: &BinaryView) -> bool {
true
}

fn run(&mut self, _view: &BinaryView) {
let mut counter = self.counter.lock().unwrap();
*counter += 1;
}
}
const PLUGIN_NAME: &str = "MyTestCommand1";
MyCommand {
counter: Arc::clone(&counter),
}
.register(
PLUGIN_NAME,
"Test for the plugin command custom implementation",
);

let all_plugins = PluginCommand::<PluginCommandAll>::valid_plugin_commands();
let my_core_plugin = all_plugins
.iter()
.find(|plugin| plugin.name() == PLUGIN_NAME)
.unwrap();
match my_core_plugin.execution() {
PluginCommandAll::DefaultPluginCommand(mut exe) => {
assert!(exe.is_valid(&view));
exe.run(&view);
}
_ => unreachable!(),
}

let counter = *counter.lock().unwrap();
assert_eq!(counter, 1);
}
Loading