summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMason Reed <mason@vector35.com>2025-01-27 16:56:15 -0500
committerMason Reed <mason@vector35.com>2025-01-27 17:52:29 -0500
commit01a224425d2a90e660bbfffe8d1cbc6b93da24bd (patch)
tree4be2aa6c3154b4fb34e61815956dcd357c754e2c
parent9c9f5085e010059c813d2285ddff79b542c76834 (diff)
Fix some rust race conditions and comment some likely suspects
FYI The binary view saving stuff can deadlock right now, so we document that now :/
-rw-r--r--Cargo.lock76
-rw-r--r--plugins/warp/src/plugin.rs4
-rw-r--r--rust/Cargo.toml3
-rw-r--r--rust/src/binary_view.rs16
-rw-r--r--rust/src/collaboration/remote.rs8
-rw-r--r--rust/src/headless.rs36
-rw-r--r--rust/src/platform.rs4
-rw-r--r--rust/src/type_container.rs10
-rw-r--r--rust/tests/binary_view.rs5
-rw-r--r--rust/tests/collaboration.rs22
-rw-r--r--rust/tests/platform.rs1
11 files changed, 153 insertions, 32 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 3a287568..3a30b0ba 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -358,6 +358,7 @@ dependencies = [
"log",
"rayon",
"rstest",
+ "serial_test",
"tempfile",
"thiserror 2.0.11",
]
@@ -1134,12 +1135,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
+ "futures-sink",
]
[[package]]
@@ -1149,6 +1166,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1179,6 +1207,12 @@ dependencies = [
]
[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1196,9 +1230,11 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
+ "futures-channel",
"futures-core",
"futures-io",
"futures-macro",
+ "futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
@@ -2457,6 +2493,15 @@ dependencies = [
]
[[package]]
+name = "scc"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28e1c91382686d21b5ac7959341fcb9780fa7c03773646995a87c950fa7be640"
+dependencies = [
+ "sdd",
+]
+
+[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2495,6 +2540,12 @@ dependencies = [
]
[[package]]
+name = "sdd"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9"
+
+[[package]]
name = "semver"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2544,6 +2595,31 @@ dependencies = [
]
[[package]]
+name = "serial_test"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
+dependencies = [
+ "futures",
+ "log",
+ "once_cell",
+ "parking_lot",
+ "scc",
+ "serial_test_derive",
+]
+
+[[package]]
+name = "serial_test_derive"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/plugins/warp/src/plugin.rs b/plugins/warp/src/plugin.rs
index 86c4bc7b..b84173f7 100644
--- a/plugins/warp/src/plugin.rs
+++ b/plugins/warp/src/plugin.rs
@@ -39,6 +39,10 @@ fn get_warp_tag_type(view: &BinaryView) -> Ref<TagType> {
// TODO: Rename to markup_function or something.
pub fn on_matched_function(function: &Function, matched: &WarpFunction) {
let view = function.view();
+ // TODO: Using user symbols here is problematic
+ // TODO: For one they queue up a bunch of main thread actions
+ // TODO: Secondly by queueing up those main thread actions if you attempt to save the file
+ // TODO: Before the undo actions are done completing
view.define_user_symbol(&to_bn_symbol_at_address(
&view,
&matched.symbol,
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 0a17730c..29170b6d 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -20,4 +20,5 @@ thiserror = "2.0"
[dev-dependencies]
rstest = "0.24"
-tempfile = "3.15" \ No newline at end of file
+tempfile = "3.15"
+serial_test = "3.2" \ No newline at end of file
diff --git a/rust/src/binary_view.rs b/rust/src/binary_view.rs
index b508fa8b..43e913d5 100644
--- a/rust/src/binary_view.rs
+++ b/rust/src/binary_view.rs
@@ -1580,7 +1580,7 @@ pub trait BinaryViewExt: BinaryViewBase {
let type_container_ptr =
NonNull::new(unsafe { BNGetAnalysisUserTypeContainer(self.as_ref().handle) });
// NOTE: I have no idea how this isn't a UAF, see the note in `TypeContainer::from_raw`
- unsafe { TypeContainer::from_raw(type_container_ptr.unwrap()) }
+ unsafe { TypeContainer::from_raw(type_container_ptr.unwrap()) }.clone()
}
/// Type container for auto types in the Binary View.
@@ -1878,12 +1878,26 @@ impl BinaryView {
}
/// Save the original binary file to the provided `file_path` along with any modifications.
+ ///
+ /// WARNING: Currently there is a possibility to deadlock if the analysis has queued up a main thread action
+ /// that tries to take the [`FileMetadata`] lock of the current view, and is executed while we
+ /// are executing in this function.
+ ///
+ /// To avoid the above issue use [`crate::main_thread::execute_on_main_thread_and_wait`] to verify there
+ /// are no queued up main thread actions.
pub fn save_to_path(&self, file_path: impl AsRef<Path>) -> bool {
let file = file_path.as_ref().into_bytes_with_nul();
unsafe { BNSaveToFilename(self.handle, file.as_ptr() as *mut _) }
}
/// Save the original binary file to the provided [`FileAccessor`] along with any modifications.
+ ///
+ /// WARNING: Currently there is a possibility to deadlock if the analysis has queued up a main thread action
+ /// that tries to take the [`FileMetadata`] lock of the current view, and is executed while we
+ /// are executing in this function.
+ ///
+ /// To avoid the above issue use [`crate::main_thread::execute_on_main_thread_and_wait`] to verify there
+ /// are no queued up main thread actions.
pub fn save_to_accessor(&self, file: &mut FileAccessor) -> bool {
unsafe { BNSaveToFile(self.handle, &mut file.api_object) }
}
diff --git a/rust/src/collaboration/remote.rs b/rust/src/collaboration/remote.rs
index 382d1ca2..21f148e9 100644
--- a/rust/src/collaboration/remote.rs
+++ b/rust/src/collaboration/remote.rs
@@ -192,6 +192,10 @@ impl Remote {
/// Connects to the Remote, loading metadata and optionally acquiring a token.
///
/// Use [Remote::connect_with_opts] if you cannot otherwise automatically connect using enterprise.
+ ///
+ /// WARNING: This is currently **not** thread safe, if you try and connect/disconnect to a remote on
+ /// multiple threads you will be subject to race conditions. To avoid this wrap the [`Remote`] in
+ /// a synchronization primitive, and pass that to your threads. Or don't try and connect on multiple threads.
pub fn connect(&self) -> Result<(), ()> {
// TODO: implement SecretsProvider
if self.is_enterprise()? && enterprise::is_server_authenticated() {
@@ -234,6 +238,10 @@ impl Remote {
}
/// Disconnects from the remote.
+ ///
+ /// WARNING: This is currently **not** thread safe, if you try and connect/disconnect to a remote on
+ /// multiple threads you will be subject to race conditions. To avoid this wrap the [`Remote`] in
+ /// a synchronization primitive, and pass that to your threads. Or don't try and connect on multiple threads.
pub fn disconnect(&self) -> Result<(), ()> {
let success = unsafe { BNRemoteDisconnect(self.handle.as_ptr()) };
success.then_some(()).ok_or(())
diff --git a/rust/src/headless.rs b/rust/src/headless.rs
index 2642b2b0..78c04a96 100644
--- a/rust/src/headless.rs
+++ b/rust/src/headless.rs
@@ -148,26 +148,26 @@ impl Default for InitializationOptions {
/// This initializes the core with the given [`InitializationOptions`].
pub fn init_with_opts(options: InitializationOptions) -> Result<(), InitializationError> {
// If we are the main thread that means there is no main thread, we should register a main thread handler.
- if options.register_main_thread_handler
- && is_main_thread()
- && MAIN_THREAD_HANDLE.lock().unwrap().is_none()
- {
- let (sender, receiver) = std::sync::mpsc::channel();
- let main_thread = HeadlessMainThreadSender::new(sender);
+ if options.register_main_thread_handler && is_main_thread() {
+ let mut main_thread_handle = MAIN_THREAD_HANDLE.lock().unwrap();
+ if main_thread_handle.is_none() {
+ let (sender, receiver) = std::sync::mpsc::channel();
+ let main_thread = HeadlessMainThreadSender::new(sender);
- // This thread will act as our main thread.
- let main_thread_handle = std::thread::Builder::new()
- .name("HeadlessMainThread".to_string())
- .spawn(move || {
- // We must register the main thread within said thread.
- main_thread.register();
- while let Ok(action) = receiver.recv() {
- action.execute();
- }
- })?;
+ // This thread will act as our main thread.
+ let join_handle = std::thread::Builder::new()
+ .name("HeadlessMainThread".to_string())
+ .spawn(move || {
+ // We must register the main thread within said thread.
+ main_thread.register();
+ while let Ok(action) = receiver.recv() {
+ action.execute();
+ }
+ })?;
- // Set the static MAIN_THREAD_HANDLER so that we can close the thread on shutdown.
- *MAIN_THREAD_HANDLE.lock().unwrap() = Some(main_thread_handle);
+ // Set the static MAIN_THREAD_HANDLER so that we can close the thread on shutdown.
+ *main_thread_handle = Some(join_handle);
+ }
}
match crate::product().as_str() {
diff --git a/rust/src/platform.rs b/rust/src/platform.rs
index 62c61297..e732152c 100644
--- a/rust/src/platform.rs
+++ b/rust/src/platform.rs
@@ -90,7 +90,7 @@ impl Platform {
if res.is_null() {
None
} else {
- Some(Ref::new(Self { handle: res }))
+ Some(Self::ref_from_raw(res))
}
}
}
@@ -176,7 +176,7 @@ impl Platform {
// NOTE: I have no idea how this isn't a UAF, see the note in `TypeContainer::from_raw`
// TODO: We are cloning here for platforms but we dont need to do this for [BinaryViewExt::type_container]
// TODO: Why does this require that we, construct a TypeContainer, duplicate the type container, then drop the original.
- unsafe { TypeContainer::from_raw(type_container_ptr.unwrap()).clone() }
+ unsafe { TypeContainer::from_raw(type_container_ptr.unwrap()) }
}
pub fn get_type_libraries_by_name<T: BnStrCompatible>(&self, name: T) -> Array<TypeLibrary> {
diff --git a/rust/src/type_container.rs b/rust/src/type_container.rs
index f5689acf..947c6912 100644
--- a/rust/src/type_container.rs
+++ b/rust/src/type_container.rs
@@ -36,11 +36,10 @@ impl TypeContainer {
// NOTE: but this is how the C++ and Python bindings operate so i guess its fine?
// TODO: I really dont get how some of the usage of the TypeContainer doesnt free the underlying container.
// TODO: So for now we always duplicate the type container
- // let cloned_ptr = NonNull::new(BNDuplicateTypeContainer(handle.as_ptr()));
- // Self {
- // handle: cloned_ptr.unwrap(),
- // }
- Self { handle }
+ let cloned_ptr = NonNull::new(BNDuplicateTypeContainer(handle.as_ptr()));
+ Self {
+ handle: cloned_ptr.unwrap(),
+ }
}
/// Get an id string for the Type Container. This will be unique within a given
@@ -417,6 +416,7 @@ impl Debug for TypeContainer {
.field("name", &self.name())
.field("container_type", &self.container_type())
.field("is_mutable", &self.is_mutable())
+ .field("type_names", &self.type_names().unwrap().to_vec())
.finish()
}
}
diff --git a/rust/tests/binary_view.rs b/rust/tests/binary_view.rs
index 65f6b45b..0ee10d87 100644
--- a/rust/tests/binary_view.rs
+++ b/rust/tests/binary_view.rs
@@ -3,6 +3,7 @@ use binaryninja::headless::Session;
use binaryninja::symbol::{SymbolBuilder, SymbolType};
use rstest::*;
use std::path::PathBuf;
+use binaryninja::main_thread::execute_on_main_thread_and_wait;
#[fixture]
#[once]
@@ -31,6 +32,10 @@ fn test_binary_saving(_session: &Session) {
// Verify that we modified the binary
let modified_contents = view.read_vec(0x1560, 4);
assert_eq!(modified_contents, [0xff, 0xff, 0xff, 0xff]);
+
+ // HACK: To prevent us from deadlocking in save_to_path we wait for all main thread actions to finish.
+ execute_on_main_thread_and_wait(|| {});
+
// Save the modified file
assert!(view.save_to_path(out_dir.join("atox.obj.new")));
// Verify that the file exists and is modified.
diff --git a/rust/tests/collaboration.rs b/rust/tests/collaboration.rs
index 56b357e9..a8c0d52c 100644
--- a/rust/tests/collaboration.rs
+++ b/rust/tests/collaboration.rs
@@ -6,6 +6,7 @@ use binaryninja::headless::Session;
use binaryninja::symbol::{SymbolBuilder, SymbolType};
use rstest::*;
use std::path::PathBuf;
+use serial_test::serial;
#[fixture]
#[once]
@@ -14,13 +15,21 @@ fn session() -> Session {
}
// TODO: This cannot run in CI, as headless does not have collaboration, we should gate this.
+// TODO: Why cant we create_project for the same project name? why does that fail.
-fn temp_project_scope<T: Fn(&RemoteProject)>(remote: &Remote, cb: T) {
+// TODO: Remote connection / disconnection is NOT thread safe, the core needs to lock on each.
+// TODO: Because of this we run these tests serially, this isnt _really_ an issue for real code, as
+// TODO: Real code shouldnt be trying to connect to the same remote on multiple threads.
+
+fn temp_project_scope<T: Fn(&RemoteProject)>(remote: &Remote, project_name: &str, cb: T) {
if !remote.is_connected() {
- remote.connect().expect("Failed to connect to remote");
+ // TODO: Because connecting is not thread safe we wont check the error here, this is because we might already
+ // TODO: be connecting in some other thread and will error out on this thread. But we _probably_ will
+ // TODO: have connected by the time this errors out. Maybe?
+ let _ = remote.connect();
}
let project = remote
- .create_project("Test Project", "Test project for test purposes")
+ .create_project(project_name, "Test project for test purposes")
.expect("Failed to create project");
project.open().expect("Failed to open project");
assert!(project.is_open(), "Project was not opened");
@@ -48,6 +57,7 @@ fn temp_project_scope<T: Fn(&RemoteProject)>(remote: &Remote, cb: T) {
}
#[rstest]
+#[serial]
fn test_connection(_session: &Session) {
if !has_collaboration_support() {
eprintln!("No collaboration support, skipping test...");
@@ -63,6 +73,7 @@ fn test_connection(_session: &Session) {
}
#[rstest]
+#[serial]
fn test_project_creation(_session: &Session) {
if !has_collaboration_support() {
eprintln!("No collaboration support, skipping test...");
@@ -70,7 +81,7 @@ fn test_project_creation(_session: &Session) {
}
let remotes = binaryninja::collaboration::known_remotes();
let remote = remotes.iter().next().expect("No known remotes!");
- temp_project_scope(&remote, |project| {
+ temp_project_scope(&remote, "test_creation", |project| {
// Create the file than verify it by opening and checking contents.
let created_file = project
.create_file(
@@ -143,6 +154,7 @@ fn test_project_creation(_session: &Session) {
}
#[rstest]
+#[serial]
fn test_project_sync(_session: &Session) {
if !has_collaboration_support() {
eprintln!("No collaboration support, skipping test...");
@@ -150,7 +162,7 @@ fn test_project_sync(_session: &Session) {
}
let remotes = binaryninja::collaboration::known_remotes();
let remote = remotes.iter().next().expect("No known remotes!");
- temp_project_scope(&remote, |project| {
+ temp_project_scope(&remote, "test_sync", |project| {
// Open a view so that we can upload it.
let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap();
let view = binaryninja::load(out_dir.join("atox.obj")).expect("Failed to create view");
diff --git a/rust/tests/platform.rs b/rust/tests/platform.rs
index c6c861fb..530f97b0 100644
--- a/rust/tests/platform.rs
+++ b/rust/tests/platform.rs
@@ -15,6 +15,7 @@ fn test_platform_lifetime(_session: &Session) {
let platform_1 = Platform::by_name("windows-x86_64").expect("windows-x86_64 exists");
let platform_types_1 = platform_1.types();
assert_eq!(platform_types_0.len(), platform_types_1.len());
+ assert_ne!(platform_types_1.len(), 0);
}
#[rstest]