Skip to content

Commit 7d38336

Browse files
committed
[WARP] Improve matcher speed and add fast fail for pathological cases
Improves the speed by moving chunks of functions into worker threads, because of how the functions and possible functions are gathered we have many locations to insert fast fails, which is also partially addressed by this commit (see `maximum_possible_functions`).
1 parent 08a5db4 commit 7d38336

File tree

2 files changed

+80
-22
lines changed

2 files changed

+80
-22
lines changed

plugins/warp/src/matcher.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ impl Matcher {
4747
return None;
4848
}
4949

50+
// The number of possible functions is too high, skip.
51+
// This can happen if the function is extremely common, in cases like that we are already unlikely to match.
52+
// It is unfortunate that we have to do this, but it is the best we can do. In the future we
53+
// may find a way to chunk up the possible functions and only match on a subset of them.
54+
if self
55+
.settings
56+
.maximum_possible_functions
57+
.is_some_and(|max| max < matched_functions.len() as u64)
58+
{
59+
return None;
60+
}
61+
5062
// If we have a single possible match than that must be our function.
5163
// We must also not be a trivial function, as those will likely be artifacts of an incomplete dataset
5264
if matched_functions.len() == 1 && !is_function_trivial {
@@ -296,6 +308,10 @@ pub struct MatcherSettings {
296308
///
297309
/// This is set to [MatcherSettings::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_DEFAULT] by default.
298310
pub trivial_function_adjacent_allowed: bool,
311+
/// The maximum number of WARP functions that can be used to match a Binary Ninja function.
312+
///
313+
/// This is set to [MatcherSettings::MAXIMUM_POSSIBLE_FUNCTIONS_DEFAULT] by default.
314+
pub maximum_possible_functions: Option<u64>,
299315
}
300316

301317
impl MatcherSettings {
@@ -311,6 +327,9 @@ impl MatcherSettings {
311327
pub const TRIVIAL_FUNCTION_ADJACENT_ALLOWED_DEFAULT: bool = false;
312328
pub const TRIVIAL_FUNCTION_ADJACENT_ALLOWED_SETTING: &'static str =
313329
"analysis.warp.trivialFunctionAdjacentAllowed";
330+
pub const MAXIMUM_POSSIBLE_FUNCTIONS_SETTING: &'static str =
331+
"analysis.warp.maximumPossibleFunctions";
332+
pub const MAXIMUM_POSSIBLE_FUNCTIONS_DEFAULT: u64 = 1000;
314333

315334
/// Populates the [MatcherSettings] to the current Binary Ninja settings instance.
316335
///
@@ -378,6 +397,18 @@ impl MatcherSettings {
378397
Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_SETTING,
379398
&trivial_function_adjacent_allowed_props.to_string(),
380399
);
400+
401+
let maximum_possible_functions_props = json!({
402+
"title" : "Maximum Possible Functions",
403+
"type" : "number",
404+
"default" : Self::MAXIMUM_POSSIBLE_FUNCTIONS_DEFAULT,
405+
"description" : "When matching any function that has a list of possible functions greater than this number will be skipped. A value of 0 will disable this check.",
406+
"ignore" : []
407+
});
408+
bn_settings.register_setting_json(
409+
Self::MAXIMUM_POSSIBLE_FUNCTIONS_SETTING,
410+
&maximum_possible_functions_props.to_string(),
411+
);
381412
}
382413

383414
/// Retrieve matcher settings from [`BNSettings`].
@@ -407,6 +438,14 @@ impl MatcherSettings {
407438
settings.trivial_function_adjacent_allowed = bn_settings
408439
.get_bool_with_opts(Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_SETTING, query_opts);
409440
}
441+
if bn_settings.contains(Self::MAXIMUM_POSSIBLE_FUNCTIONS_SETTING) {
442+
match bn_settings
443+
.get_integer_with_opts(Self::MAXIMUM_POSSIBLE_FUNCTIONS_SETTING, query_opts)
444+
{
445+
0 => settings.maximum_possible_functions = None,
446+
len => settings.maximum_possible_functions = Some(len),
447+
}
448+
}
410449
settings
411450
}
412451
}
@@ -420,6 +459,7 @@ impl Default for MatcherSettings {
420459
minimum_matched_constraints: MatcherSettings::MINIMUM_MATCHED_CONSTRAINTS_DEFAULT,
421460
trivial_function_adjacent_allowed:
422461
MatcherSettings::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_DEFAULT,
462+
maximum_possible_functions: Some(MatcherSettings::MAXIMUM_POSSIBLE_FUNCTIONS_DEFAULT),
423463
}
424464
}
425465
}

plugins/warp/src/plugin/workflow.rs

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use binaryninja::command::Command;
1313
use binaryninja::settings::{QueryOptions, Settings};
1414
use binaryninja::workflow::{activity, Activity, AnalysisContext, Workflow, WorkflowBuilder};
1515
use itertools::Itertools;
16+
use rayon::iter::IntoParallelIterator;
17+
use rayon::iter::ParallelIterator;
1618
use std::collections::HashMap;
1719
use std::time::Instant;
1820
use warp::r#type::class::function::{Location, RegisterLocation, StackLocation};
@@ -112,31 +114,47 @@ pub fn run_matcher(view: &BinaryView) {
112114
.sources_with_function_guids(target, guids)
113115
.unwrap_or_default();
114116

115-
for (guid, sources) in &function_guid_with_sources {
116-
let matched_functions: Vec<Function> = sources
117-
.iter()
118-
.flat_map(|source| {
119-
container
120-
.functions_with_guid(target, source, guid)
121-
.unwrap_or_default()
122-
})
123-
.collect();
117+
function_guid_with_sources
118+
.into_par_iter()
119+
.for_each(|(guid, sources)| {
120+
let matched_functions: Vec<Function> = sources
121+
.iter()
122+
.flat_map(|source| {
123+
container
124+
.functions_with_guid(target, source, &guid)
125+
.unwrap_or_default()
126+
})
127+
.collect();
124128

125-
let functions = functions_by_target_and_guid
126-
.get(&(*guid, target.clone()))
127-
.expect("Function guid not found");
128-
129-
for function in functions {
130-
// Match on all the possible functions
131-
if let Some(matched_function) =
132-
matcher.match_function_from_constraints(function, &matched_functions)
129+
// NOTE: See the comment in `match_function_from_constraints` about this fast fail.
130+
if matcher
131+
.settings
132+
.maximum_possible_functions
133+
.is_some_and(|max| max < matched_functions.len() as u64)
133134
{
134-
// We were able to find a match, add it to the match cache and then mark the function
135-
// as requiring updates; this is so that we know about it in the applier activity.
136-
insert_cached_function_match(function, Some(matched_function.clone()));
135+
log::warn!(
136+
"Skipping {}, too many possible functions: {}",
137+
guid,
138+
matched_functions.len()
139+
);
140+
return;
137141
}
138-
}
139-
}
142+
143+
let functions = functions_by_target_and_guid
144+
.get(&(guid, target.clone()))
145+
.expect("Function guid not found");
146+
147+
for function in functions {
148+
// Match on all the possible functions
149+
if let Some(matched_function) =
150+
matcher.match_function_from_constraints(function, &matched_functions)
151+
{
152+
// We were able to find a match, add it to the match cache and then mark the function
153+
// as requiring updates; this is so that we know about it in the applier activity.
154+
insert_cached_function_match(function, Some(matched_function.clone()));
155+
}
156+
}
157+
});
140158
}
141159
});
142160

0 commit comments

Comments
 (0)