Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
cwe_checker
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
fact-depend
cwe_checker
Commits
242c5325
Unverified
Commit
242c5325
authored
Apr 28, 2022
by
Enkelmann
Committed by
GitHub
Apr 28, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement new check for CWE-415 and CWE-416 (#318)
parent
26f9844d
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
781 additions
and
35 deletions
+781
-35
main.rs
src/caller/src/main.rs
+1
-1
mod.rs
src/cwe_checker_lib/src/analysis/pointer_inference/mod.rs
+49
-5
checkers.rs
src/cwe_checker_lib/src/checkers.rs
+1
-0
mod.rs
src/cwe_checker_lib/src/checkers/cwe_119/context/mod.rs
+10
-10
tests.rs
src/cwe_checker_lib/src/checkers/cwe_119/context/tests.rs
+1
-8
mod.rs
src/cwe_checker_lib/src/checkers/cwe_119/mod.rs
+4
-9
context.rs
src/cwe_checker_lib/src/checkers/cwe_416/context.rs
+350
-0
mod.rs
src/cwe_checker_lib/src/checkers/cwe_416/mod.rs
+84
-0
state.rs
src/cwe_checker_lib/src/checkers/cwe_416/state.rs
+278
-0
lib.rs
src/cwe_checker_lib/src/lib.rs
+1
-0
lib.rs
test/src/lib.rs
+2
-2
No files found.
src/caller/src/main.rs
View file @
242c5325
...
@@ -178,7 +178,7 @@ fn run_with_ghidra(args: &CmdlineArgs) {
...
@@ -178,7 +178,7 @@ fn run_with_ghidra(args: &CmdlineArgs) {
let
modules_depending_on_string_abstraction
=
BTreeSet
::
from_iter
([
"CWE78"
]);
let
modules_depending_on_string_abstraction
=
BTreeSet
::
from_iter
([
"CWE78"
]);
let
modules_depending_on_pointer_inference
=
let
modules_depending_on_pointer_inference
=
BTreeSet
::
from_iter
([
"CWE119"
,
"CWE134"
,
"CWE476"
,
"Memory"
]);
BTreeSet
::
from_iter
([
"CWE119"
,
"CWE134"
,
"CWE4
16"
,
"CWE4
76"
,
"Memory"
]);
let
string_abstraction_needed
=
modules
let
string_abstraction_needed
=
modules
.iter
()
.iter
()
...
...
src/cwe_checker_lib/src/analysis/pointer_inference/mod.rs
View file @
242c5325
...
@@ -29,7 +29,7 @@
...
@@ -29,7 +29,7 @@
use
super
::
fixpoint
::
Computation
;
use
super
::
fixpoint
::
Computation
;
use
super
::
forward_interprocedural_fixpoint
::
GeneralizedContext
;
use
super
::
forward_interprocedural_fixpoint
::
GeneralizedContext
;
use
super
::
interprocedural_fixpoint_generic
::
NodeValue
;
use
super
::
interprocedural_fixpoint_generic
::
NodeValue
;
use
crate
::
abstract_domain
::{
DataDomain
,
IntervalDomain
,
SizedDomain
};
use
crate
::
abstract_domain
::{
AbstractIdentifier
,
DataDomain
,
IntervalDomain
,
SizedDomain
};
use
crate
::
analysis
::
forward_interprocedural_fixpoint
::
Context
as
_
;
use
crate
::
analysis
::
forward_interprocedural_fixpoint
::
Context
as
_
;
use
crate
::
analysis
::
graph
::{
Graph
,
Node
};
use
crate
::
analysis
::
graph
::{
Graph
,
Node
};
use
crate
::
intermediate_representation
::
*
;
use
crate
::
intermediate_representation
::
*
;
...
@@ -37,7 +37,7 @@ use crate::prelude::*;
...
@@ -37,7 +37,7 @@ use crate::prelude::*;
use
crate
::
utils
::
log
::
*
;
use
crate
::
utils
::
log
::
*
;
use
petgraph
::
graph
::
NodeIndex
;
use
petgraph
::
graph
::
NodeIndex
;
use
petgraph
::
visit
::
IntoNodeReferences
;
use
petgraph
::
visit
::
IntoNodeReferences
;
use
std
::
collections
::
HashMap
;
use
std
::
collections
::
{
BTreeMap
,
HashMap
}
;
mod
context
;
mod
context
;
pub
mod
object
;
pub
mod
object
;
...
@@ -96,6 +96,9 @@ pub struct PointerInference<'a> {
...
@@ -96,6 +96,9 @@ pub struct PointerInference<'a> {
/// Maps certain TIDs like the TIDs of [`Jmp`] instructions to the pointer inference state at that TID.
/// Maps certain TIDs like the TIDs of [`Jmp`] instructions to the pointer inference state at that TID.
/// The map will be filled after the fixpoint computation finished.
/// The map will be filled after the fixpoint computation finished.
states_at_tids
:
HashMap
<
Tid
,
State
>
,
states_at_tids
:
HashMap
<
Tid
,
State
>
,
/// Maps the TIDs of call instructions to a map mapping callee IDs to the corresponding value in the caller.
/// The map will be filled after the fixpoint computation finished.
id_renaming_maps_at_calls
:
HashMap
<
Tid
,
BTreeMap
<
AbstractIdentifier
,
Data
>>
,
}
}
impl
<
'a
>
PointerInference
<
'a
>
{
impl
<
'a
>
PointerInference
<
'a
>
{
...
@@ -145,6 +148,7 @@ impl<'a> PointerInference<'a> {
...
@@ -145,6 +148,7 @@ impl<'a> PointerInference<'a> {
values_at_defs
:
HashMap
::
new
(),
values_at_defs
:
HashMap
::
new
(),
addresses_at_defs
:
HashMap
::
new
(),
addresses_at_defs
:
HashMap
::
new
(),
states_at_tids
:
HashMap
::
new
(),
states_at_tids
:
HashMap
::
new
(),
id_renaming_maps_at_calls
:
HashMap
::
new
(),
}
}
}
}
...
@@ -256,12 +260,12 @@ impl<'a> PointerInference<'a> {
...
@@ -256,12 +260,12 @@ impl<'a> PointerInference<'a> {
let
context
=
self
.computation
.get_context
()
.get_context
();
let
context
=
self
.computation
.get_context
()
.get_context
();
let
graph
=
self
.computation
.get_graph
();
let
graph
=
self
.computation
.get_graph
();
for
node
in
graph
.node_indices
()
{
for
node
in
graph
.node_indices
()
{
match
graph
[
node
]
{
Node
::
BlkStart
(
blk
,
_sub
)
=>
{
let
node_state
=
match
self
.computation
.get_node_value
(
node
)
{
let
node_state
=
match
self
.computation
.get_node_value
(
node
)
{
Some
(
NodeValue
::
Value
(
value
))
=>
value
,
Some
(
NodeValue
::
Value
(
value
))
=>
value
,
_
=>
continue
,
_
=>
continue
,
};
};
match
graph
[
node
]
{
Node
::
BlkStart
(
blk
,
_sub
)
=>
{
let
mut
state
=
node_state
.clone
();
let
mut
state
=
node_state
.clone
();
for
def
in
&
blk
.term.defs
{
for
def
in
&
blk
.term.defs
{
match
&
def
.term
{
match
&
def
.term
{
...
@@ -291,12 +295,40 @@ impl<'a> PointerInference<'a> {
...
@@ -291,12 +295,40 @@ impl<'a> PointerInference<'a> {
}
}
}
}
Node
::
BlkEnd
(
blk
,
_sub
)
=>
{
Node
::
BlkEnd
(
blk
,
_sub
)
=>
{
let
node_state
=
match
self
.computation
.get_node_value
(
node
)
{
Some
(
NodeValue
::
Value
(
value
))
=>
value
,
_
=>
continue
,
};
for
jmp
in
&
blk
.term.jmps
{
for
jmp
in
&
blk
.term.jmps
{
self
.states_at_tids
self
.states_at_tids
.insert
(
jmp
.tid
.clone
(),
node_state
.clone
());
.insert
(
jmp
.tid
.clone
(),
node_state
.clone
());
}
}
}
}
Node
::
CallSource
{
..
}
|
Node
::
CallReturn
{
..
}
=>
(),
Node
::
CallSource
{
..
}
=>
(),
Node
::
CallReturn
{
call
:
(
caller_blk
,
_caller_sub
),
return_
:
_
,
}
=>
{
let
call_tid
=
match
caller_blk
.term.jmps
.get
(
0
)
{
Some
(
call
)
=>
&
call
.tid
,
_
=>
continue
,
};
let
(
state_before_call
,
state_before_return
)
=
match
self
.computation
.get_node_value
(
node
)
{
Some
(
NodeValue
::
CallFlowCombinator
{
call_stub
:
Some
(
state_before_call
),
interprocedural_flow
:
Some
(
state_before_return
),
})
=>
(
state_before_call
,
state_before_return
),
_
=>
continue
,
};
let
id_to_data_map
=
context
.create_callee_id_to_caller_data_map
(
state_before_call
,
state_before_return
,
call_tid
,
);
self
.id_renaming_maps_at_calls
.insert
(
call_tid
.clone
(),
id_to_data_map
);
}
}
}
}
}
}
}
...
@@ -307,6 +339,18 @@ impl<'a> PointerInference<'a> {
...
@@ -307,6 +339,18 @@ impl<'a> PointerInference<'a> {
self
.states_at_tids
.get
(
jmp_tid
)
self
.states_at_tids
.get
(
jmp_tid
)
}
}
/// Get the mapping from callee IDs to caller values for the given call.
/// This function only yields results after the fixpoint has been computed.
///
/// Note that the maps may contain mappings from callee IDs to temporary caller IDs that get instantly removed from the caller
/// since they are not referenced in any caller object.
pub
fn
get_id_renaming_map_at_call_tid
(
&
self
,
call_tid
:
&
Tid
,
)
->
Option
<&
BTreeMap
<
AbstractIdentifier
,
Data
>>
{
self
.id_renaming_maps_at_calls
.get
(
call_tid
)
}
/// Print information on dead ends in the control flow graph for debugging purposes.
/// Print information on dead ends in the control flow graph for debugging purposes.
/// Ignore returns where there is no known caller stack id.
/// Ignore returns where there is no known caller stack id.
#[allow(dead_code)]
#[allow(dead_code)]
...
...
src/cwe_checker_lib/src/checkers.rs
View file @
242c5325
...
@@ -12,6 +12,7 @@ pub mod cwe_215;
...
@@ -12,6 +12,7 @@ pub mod cwe_215;
pub
mod
cwe_243
;
pub
mod
cwe_243
;
pub
mod
cwe_332
;
pub
mod
cwe_332
;
pub
mod
cwe_367
;
pub
mod
cwe_367
;
pub
mod
cwe_416
;
pub
mod
cwe_426
;
pub
mod
cwe_426
;
pub
mod
cwe_467
;
pub
mod
cwe_467
;
pub
mod
cwe_476
;
pub
mod
cwe_476
;
...
...
src/cwe_checker_lib/src/checkers/cwe_119/context/mod.rs
View file @
242c5325
...
@@ -39,19 +39,19 @@ pub struct Context<'a> {
...
@@ -39,19 +39,19 @@ pub struct Context<'a> {
impl
<
'a
>
Context
<
'a
>
{
impl
<
'a
>
Context
<
'a
>
{
/// Create a new context object.
/// Create a new context object.
pub
fn
new
(
pub
fn
new
<
'b
>
(
project
:
&
'a
Project
,
analysis_results
:
&
'b
AnalysisResults
<
'a
>
,
graph
:
&
'a
Graph
<
'a
>
,
pointer_inference
:
&
'a
PointerInference
<
'a
>
,
function_signatures
:
&
'a
BTreeMap
<
Tid
,
FunctionSignature
>
,
analysis_results
:
&
AnalysisResults
,
log_collector
:
crossbeam_channel
::
Sender
<
LogThreadMsg
>
,
log_collector
:
crossbeam_channel
::
Sender
<
LogThreadMsg
>
,
)
->
Self
{
)
->
Context
<
'a
>
where
'a
:
'b
,
{
let
project
=
analysis_results
.project
;
Context
{
Context
{
project
,
project
,
graph
,
graph
:
analysis_results
.control_flow_graph
,
pointer_inference
,
pointer_inference
:
analysis_results
.pointer_inference
.unwrap
()
,
function_signatures
,
function_signatures
:
analysis_results
.function_signatures
.unwrap
()
,
callee_to_callsites_map
:
compute_callee_to_call_sites_map
(
project
),
callee_to_callsites_map
:
compute_callee_to_call_sites_map
(
project
),
param_replacement_map
:
compute_param_replacement_map
(
analysis_results
),
param_replacement_map
:
compute_param_replacement_map
(
analysis_results
),
malloc_tid_to_object_size_map
:
compute_size_values_of_malloc_calls
(
analysis_results
),
malloc_tid_to_object_size_map
:
compute_size_values_of_malloc_calls
(
analysis_results
),
...
...
src/cwe_checker_lib/src/checkers/cwe_119/context/tests.rs
View file @
242c5325
...
@@ -18,14 +18,7 @@ impl<'a> Context<'a> {
...
@@ -18,14 +18,7 @@ impl<'a> Context<'a> {
let
analysis_results
=
Box
::
leak
(
analysis_results
);
let
analysis_results
=
Box
::
leak
(
analysis_results
);
let
(
log_collector
,
_
)
=
crossbeam_channel
::
unbounded
();
let
(
log_collector
,
_
)
=
crossbeam_channel
::
unbounded
();
Context
::
new
(
Context
::
new
(
analysis_results
,
log_collector
)
analysis_results
.project
,
analysis_results
.control_flow_graph
,
analysis_results
.pointer_inference
.unwrap
(),
analysis_results
.function_signatures
.unwrap
(),
analysis_results
,
log_collector
,
)
}
}
}
}
...
...
src/cwe_checker_lib/src/checkers/cwe_119/mod.rs
View file @
242c5325
...
@@ -65,14 +65,7 @@ pub fn check_cwe(
...
@@ -65,14 +65,7 @@ pub fn check_cwe(
)
->
(
Vec
<
LogMessage
>
,
Vec
<
CweWarning
>
)
{
)
->
(
Vec
<
LogMessage
>
,
Vec
<
CweWarning
>
)
{
let
log_thread
=
LogThread
::
spawn
(
LogThread
::
collect_and_deduplicate
);
let
log_thread
=
LogThread
::
spawn
(
LogThread
::
collect_and_deduplicate
);
let
context
=
Context
::
new
(
let
context
=
Context
::
new
(
analysis_results
,
log_thread
.get_msg_sender
());
analysis_results
.project
,
analysis_results
.control_flow_graph
,
analysis_results
.pointer_inference
.unwrap
(),
analysis_results
.function_signatures
.unwrap
(),
analysis_results
,
log_thread
.get_msg_sender
(),
);
let
mut
fixpoint_computation
=
let
mut
fixpoint_computation
=
crate
::
analysis
::
forward_interprocedural_fixpoint
::
create_computation
(
context
,
None
);
crate
::
analysis
::
forward_interprocedural_fixpoint
::
create_computation
(
context
,
None
);
...
@@ -91,5 +84,7 @@ pub fn check_cwe(
...
@@ -91,5 +84,7 @@ pub fn check_cwe(
fixpoint_computation
.compute_with_max_steps
(
100
);
fixpoint_computation
.compute_with_max_steps
(
100
);
log_thread
.collect
()
let
(
logs
,
mut
cwe_warnings
)
=
log_thread
.collect
();
cwe_warnings
.sort
();
(
logs
,
cwe_warnings
)
}
}
src/cwe_checker_lib/src/checkers/cwe_416/context.rs
0 → 100644
View file @
242c5325
use
super
::
State
;
use
super
::
CWE_MODULE
;
use
crate
::
abstract_domain
::
AbstractDomain
;
use
crate
::
analysis
::
function_signature
::
FunctionSignature
;
use
crate
::
analysis
::
graph
::
Graph
;
use
crate
::
analysis
::
pointer_inference
::
PointerInference
;
use
crate
::
analysis
::
vsa_results
::
VsaResult
;
use
crate
::
intermediate_representation
::
*
;
use
crate
::
prelude
::
*
;
use
crate
::
utils
::
log
::
CweWarning
;
use
crate
::
utils
::
log
::
LogMessage
;
use
crate
::
utils
::
log
::
LogThreadMsg
;
use
std
::
collections
::
BTreeMap
;
/// The context struct for the fixpoint algorithm that contains references to the analysis results
/// of other analyses used in this analysis.
pub
struct
Context
<
'a
>
{
/// A pointer to the project struct.
pub
project
:
&
'a
Project
,
/// A pointer to the control flow graph.
pub
graph
:
&
'a
Graph
<
'a
>
,
/// A pointer to the results of the pointer inference analysis.
pub
pointer_inference
:
&
'a
PointerInference
<
'a
>
,
/// A pointer to the computed function signatures for all internal functions.
pub
function_signatures
:
&
'a
BTreeMap
<
Tid
,
FunctionSignature
>
,
/// A sender channel that can be used to collect logs in the corresponding logging thread.
pub
log_collector
:
crossbeam_channel
::
Sender
<
LogThreadMsg
>
,
/// Generic function arguments assumed for calls to functions where the real number of parameters are unknown.
generic_function_parameter
:
Vec
<
Arg
>
,
}
impl
<
'a
>
Context
<
'a
>
{
/// Generate a new context struct from the given analysis results and a channel for gathering log messages and CWE warnings.
pub
fn
new
<
'b
>
(
analysis_results
:
&
'b
AnalysisResults
<
'a
>
,
log_collector
:
crossbeam_channel
::
Sender
<
LogThreadMsg
>
,
)
->
Context
<
'a
>
where
'a
:
'b
,
{
let
generic_function_parameter
:
Vec
<
_
>
=
if
let
Some
(
cconv
)
=
analysis_results
.project
.get_standard_calling_convention
()
{
cconv
.integer_parameter_register
.iter
()
.map
(|
reg
|
Arg
::
from_var
(
reg
.clone
(),
None
))
.collect
()
}
else
{
Vec
::
new
()
};
Context
{
project
:
analysis_results
.project
,
graph
:
analysis_results
.control_flow_graph
,
pointer_inference
:
analysis_results
.pointer_inference
.unwrap
(),
function_signatures
:
analysis_results
.function_signatures
.unwrap
(),
log_collector
,
generic_function_parameter
,
}
}
/// For the given call parameters of the given call check for possible Use-After-Free bugs
/// and return the possible causes for such bugs.
fn
collect_cwe_warnings_of_call_params
<
'b
>
(
&
self
,
state
:
&
mut
State
,
call_tid
:
&
Tid
,
call_params
:
impl
IntoIterator
<
Item
=
&
'b
Arg
>
,
)
->
Option
<
Vec
<
String
>>
{
let
mut
warnings
=
Vec
::
new
();
for
arg
in
call_params
{
if
let
Some
(
arg_value
)
=
self
.pointer_inference
.eval_parameter_arg_at_call
(
call_tid
,
arg
)
{
if
let
Some
(
mut
warning_causes
)
=
state
.check_address_for_use_after_free
(
&
arg_value
)
{
warnings
.append
(
&
mut
warning_causes
);
}
}
}
if
!
warnings
.is_empty
()
{
Some
(
warnings
)
}
else
{
None
}
}
/// Check the parameters of an internal function call for dangling pointers and report CWE warnings accordingly.
fn
check_internal_call_params_for_use_after_free
(
&
self
,
state
:
&
mut
State
,
callee_sub_tid
:
&
Tid
,
call_tid
:
&
Tid
,
)
{
let
function_signature
=
match
self
.function_signatures
.get
(
callee_sub_tid
)
{
Some
(
fn_sig
)
=>
fn_sig
,
None
=>
return
,
};
let
mut
warnings
=
Vec
::
new
();
for
(
arg
,
access_pattern
)
in
&
function_signature
.parameters
{
if
access_pattern
.is_dereferenced
()
{
if
let
Some
(
arg_value
)
=
self
.pointer_inference
.eval_parameter_arg_at_call
(
call_tid
,
arg
)
{
if
let
Some
(
mut
warning_causes
)
=
state
.check_address_for_use_after_free
(
&
arg_value
)
{
warnings
.append
(
&
mut
warning_causes
);
}
}
}
}
let
callee_sub_name
=
&
self
.project.program.term.subs
[
callee_sub_tid
]
.term.name
;
if
!
warnings
.is_empty
()
{
let
cwe_warning
=
CweWarning
{
name
:
"CWE416"
.to_string
(),
version
:
CWE_MODULE
.version
.to_string
(),
addresses
:
vec!
[
call_tid
.address
.clone
()],
tids
:
vec!
[
format!
(
"{}"
,
call_tid
)],
symbols
:
Vec
::
new
(),
other
:
vec!
[
warnings
],
description
:
format!
(
"(Use After Free) Call to {} at {} may access dangling pointers through its parameters"
,
callee_sub_name
,
call_tid
.address
),
};
self
.log_collector
.send
(
cwe_warning
.into
())
.unwrap
();
}
}
/// Handle a call to `free` by marking the corresponding memory object IDs as dangling and detecting possible double frees.
fn
handle_call_to_free
(
&
self
,
state
:
&
mut
State
,
call_tid
:
&
Tid
,
free_symbol
:
&
ExternSymbol
)
{
if
free_symbol
.parameters
.is_empty
()
{
let
error_msg
=
LogMessage
::
new_error
(
"free symbol without parameter encountered."
)
.location
(
call_tid
.clone
())
.source
(
CWE_MODULE
.name
);
self
.log_collector
.send
(
error_msg
.into
())
.unwrap
();
return
;
}
if
let
Some
(
param
)
=
self
.pointer_inference
.eval_parameter_arg_at_call
(
call_tid
,
&
free_symbol
.parameters
[
0
])
{
if
let
Some
(
pi_state
)
=
self
.pointer_inference
.get_state_at_jmp_tid
(
call_tid
)
{
if
let
Some
(
warning_causes
)
=
state
.handle_param_of_free_call
(
call_tid
,
&
param
,
pi_state
)
{
let
cwe_warning
=
CweWarning
{
name
:
"CWE415"
.to_string
(),
version
:
CWE_MODULE
.version
.to_string
(),
addresses
:
vec!
[
call_tid
.address
.clone
()],
tids
:
vec!
[
format!
(
"{}"
,
call_tid
)],
symbols
:
Vec
::
new
(),
other
:
vec!
[
warning_causes
],
description
:
format!
(
"(Double Free) Object may have been freed before at {}"
,
call_tid
.address
),
};
self
.log_collector
.send
(
cwe_warning
.into
())
.unwrap
();
}
}
}
}
}
impl
<
'a
>
crate
::
analysis
::
forward_interprocedural_fixpoint
::
Context
<
'a
>
for
Context
<
'a
>
{
type
Value
=
State
;
/// Get a reference to the control flow graph.
fn
get_graph
(
&
self
)
->
&
Graph
<
'a
>
{
self
.graph
}
/// Merge two node states.
fn
merge
(
&
self
,
state1
:
&
State
,
state2
:
&
State
)
->
State
{
state1
.merge
(
state2
)
}
/// Check whether the `def` may access already freed memory.
/// If yes, generate a CWE warning and mark the corresponding object IDs as already flagged.
fn
update_def
(
&
self
,
state
:
&
State
,
def
:
&
Term
<
Def
>
)
->
Option
<
State
>
{
let
mut
state
=
state
.clone
();
if
let
Some
(
address
)
=
self
.pointer_inference
.eval_address_at_def
(
&
def
.tid
)
{
if
let
Some
(
warning_causes
)
=
state
.check_address_for_use_after_free
(
&
address
)
{
let
cwe_warning
=
CweWarning
{
name
:
"CWE416"
.to_string
(),
version
:
CWE_MODULE
.version
.to_string
(),
addresses
:
vec!
[
def
.tid.address
.clone
()],
tids
:
vec!
[
format!
(
"{}"
,
def
.tid
)],
symbols
:
Vec
::
new
(),
other
:
vec!
[
warning_causes
],
description
:
format!
(
"(Use After Free) Access through a dangling pointer at {}"
,
def
.tid.address
),
};
self
.log_collector
.send
(
cwe_warning
.into
())
.unwrap
();
}
}
Some
(
state
)
}
/// Just returns the unmodified state.
fn
update_jump
(
&
self
,
state
:
&
State
,
_jump
:
&
Term
<
Jmp
>
,
_untaken_conditional
:
Option
<&
Term
<
Jmp
>>
,
_target
:
&
Term
<
Blk
>
,
)
->
Option
<
State
>
{
Some
(
state
.clone
())
}
/// Check whether any call parameters are dangling pointers and generate CWE warnings accordingly.
/// Always returns `None` since the analysis is a bottom-up analysis (i.e. no information flows from caller to callee).
fn
update_call
(
&
self
,
state
:
&
State
,
call
:
&
Term
<
Jmp
>
,
target
:
&
crate
::
analysis
::
graph
::
Node
,
_calling_convention
:
&
Option
<
String
>
,
)
->
Option
<
State
>
{
use
crate
::
analysis
::
graph
::
Node
;
let
sub
=
match
*
target
{
Node
::
BlkStart
(
_
,
sub
)
=>
sub
,
_
=>
return
None
,
};
let
mut
state
=
state
.clone
();
self
.check_internal_call_params_for_use_after_free
(
&
mut
state
,
&
sub
.tid
,
&
call
.tid
);
// No information flows from caller to callee, so we return `None` regardless.
None
}
/// Collect the IDs of objects freed in the callee and mark the corresponding objects in the caller as freed.
/// Also check the call parameters for Use-After-Frees.
fn
update_return
(
&
self
,
state
:
Option
<&
State
>
,
state_before_call
:
Option
<&
State
>
,
call
:
&
Term
<
Jmp
>
,
_return_term
:
&
Term
<
Jmp
>
,
_calling_convention
:
&
Option
<
String
>
,
)
->
Option
<
State
>
{
let
(
state_before_return
,
state_before_call
)
=
match
(
state
,
state_before_call
)
{
(
Some
(
state_before_return
),
Some
(
state_before_call
))
=>
{
(
state_before_return
,
state_before_call
)
}
_
=>
return
None
,
};
let
id_replacement_map
=
match
self
.pointer_inference
.get_id_renaming_map_at_call_tid
(
&
call
.tid
)
{
Some
(
map
)
=>
map
,
None
=>
return
None
,
};
let
pi_state_before_call
=
match
self
.pointer_inference
.get_state_at_jmp_tid
(
&
call
.tid
)
{
Some
(
pi_state
)
=>
pi_state
,
None
=>
return
None
,
};
let
mut
state_after_return
=
state_before_call
.clone
();
// Check for Use-After-Frees through function parameters.
// FIXME: This is actually done twice, since the `update_call` method uses the same check.
// But to remove the check there we would have to know the callee function TID here
// even in the case when the call does not actually return at all.
self
.check_internal_call_params_for_use_after_free
(
&
mut
state_after_return
,
&
state_before_return
.current_fn_tid
,
&
call
.tid
,
);
// Add object IDs of objects that may have been freed in the callee.
state_after_return
.collect_freed_objects_from_called_function
(
state_before_return
,
id_replacement_map
,
&
call
.tid
,
pi_state_before_call
,
);
Some
(
state_after_return
)
}
/// Handle extern symbols by checking for Use-After-Frees in the call parameters.
/// Also handle calls to `free` by marking the corresponding object ID as dangling.
fn
update_call_stub
(
&
self
,
state
:
&
State
,
call
:
&
Term
<
Jmp
>
)
->
Option
<
State
>
{
let
mut
state
=
state
.clone
();
if
let
Some
(
extern_symbol
)
=
match
&
call
.term
{
Jmp
::
Call
{
target
,
..
}
=>
self
.project.program.term.extern_symbols
.get
(
target
),
_
=>
None
,
}
{
match
extern_symbol
.name
.as_str
()
{
"free"
=>
self
.handle_call_to_free
(
&
mut
state
,
&
call
.tid
,
extern_symbol
),
extern_symbol_name
=>
{
if
let
Some
(
warnings
)
=
self
.collect_cwe_warnings_of_call_params
(
&
mut
state
,
&
call
.tid
,
&
extern_symbol
.parameters
,
)
{
let
cwe_warning
=
CweWarning
{
name
:
"CWE416"
.to_string
(),
version
:
CWE_MODULE
.version
.to_string
(),
addresses
:
vec!
[
call
.tid.address
.clone
()],
tids
:
vec!
[
format!
(
"{}"
,
call
.tid
)],
symbols
:
Vec
::
new
(),
other
:
vec!
[
warnings
],
description
:
format!
(
"(Use After Free) Call to {} at {} may access dangling pointers through its parameters"
,
extern_symbol_name
,
call
.tid.address
),
};
self
.log_collector
.send
(
cwe_warning
.into
())
.unwrap
();
}
}
}
}
else
if
let
Some
(
warnings
)
=
self
.collect_cwe_warnings_of_call_params
(
&
mut
state
,
&
call
.tid
,
&
self
.generic_function_parameter
,
)
{
let
cwe_warning
=
CweWarning
{
name
:
"CWE416"
.to_string
(),
version
:
CWE_MODULE
.version
.to_string
(),
addresses
:
vec!
[
call
.tid.address
.clone
()],
tids
:
vec!
[
format!
(
"{}"
,
call
.tid
)],
symbols
:
Vec
::
new
(),
other
:
vec!
[
warnings
],
description
:
format!
(
"(Use After Free) Call at {} may access dangling pointers through its parameters"
,
call
.tid.address
),
};
self
.log_collector
.send
(
cwe_warning
.into
())
.unwrap
();
}
Some
(
state
)
}
/// Just returns the unmodified state
fn
specialize_conditional
(
&
self
,
state
:
&
State
,
_condition
:
&
Expression
,
_block_before_condition
:
&
Term
<
Blk
>
,
_is_true
:
bool
,
)
->
Option
<
State
>
{
Some
(
state
.clone
())
}
}
src/cwe_checker_lib/src/checkers/cwe_416/mod.rs
0 → 100644
View file @
242c5325
//! This module implements a check for CWE-415: Double Free and CWE-416: Use After Free.
//!
//! If a program tries to reference memory objects or other resources after they have been freed
//! it can lead to crashes, unexpected behaviour or even arbitrary code execution.
//! The same is true if the program tries to free the same resource more than once
//! as this can lead to another unrelated resource being freed instead.
//!
//! See <https://cwe.mitre.org/data/definitions/415.html> and <https://cwe.mitre.org/data/definitions/416.html> for detailed descriptions.
//!
//! ## How the check works
//!
//! Using an interprocedural, bottom-up dataflow analysis
//! based on the results of the [Pointer Inference analysis](`crate::analysis::pointer_inference`)
//! the check keeps track of memory objects that have already been freed.
//! If a pointer to an already freed object is used to access memory or provided as a parameter to another function
//! then a CWE warning is generated.
//! To prevent duplicate CWE warnings with the same root cause
//! the check also keeps track of objects for which a CWE warning was already generated.
//!
//! ## False Positives
//!
//! - Since the analysis is not path-sensitive, infeasible paths may lead to false positives.
//! - Any analysis imprecision of the pointer inference analysis
//! that leads to assuming that a pointer can target more memory objects that it actually can target
//! may lead to false positive CWE warnings in this check.
//!
//! ## False Negatives
//!
//! - Arrays of memory objects are not tracked by this analysis as we currently cannot distinguish different array elements in the analysis.
//! Subsequently, CWEs corresponding to arrays of memory objects are not detected.
//! - Memory objects not tracked by the Pointer Inference analysis or pointer targets missed by the Pointer Inference
//! may lead to missed CWEs in this check.
//! - The analysis currently only tracks pointers to objects that were freed by a call to `free`.
//! If a memory object is freed by another external function then this may lead to false negatives in this check.
use
crate
::
prelude
::
*
;
use
crate
::
utils
::
log
::
CweWarning
;
use
crate
::
utils
::
log
::
LogMessage
;
use
crate
::
utils
::
log
::
LogThread
;
use
crate
::
CweModule
;
/// The module name and version
pub
static
CWE_MODULE
:
CweModule
=
CweModule
{
name
:
"CWE416"
,
version
:
"0.3"
,
run
:
check_cwe
,
};
mod
context
;
use
context
::
Context
;
mod
state
;
use
state
::
State
;
/// Run the check for CWE-416: Use After Free.
///
/// This function prepares the bottom-up fixpoint computation
/// by initializing the state at the start of each function with the empty state (i.e. no dangling objects known)
/// and then executing the fixpoint algorithm.
/// Returns collected log messages and CWE warnings.
pub
fn
check_cwe
(
analysis_results
:
&
AnalysisResults
,
_config
:
&
serde_json
::
Value
,
)
->
(
Vec
<
LogMessage
>
,
Vec
<
CweWarning
>
)
{
let
log_thread
=
LogThread
::
spawn
(
LogThread
::
collect_and_deduplicate
);
let
context
=
Context
::
new
(
analysis_results
,
log_thread
.get_msg_sender
());
let
mut
fixpoint_computation
=
crate
::
analysis
::
forward_interprocedural_fixpoint
::
create_computation
(
context
,
None
);
for
(
sub_tid
,
entry_node_of_sub
)
in
crate
::
analysis
::
graph
::
get_entry_nodes_of_subs
(
analysis_results
.control_flow_graph
)
{
let
fn_start_state
=
State
::
new
(
sub_tid
);
fixpoint_computation
.set_node_value
(
entry_node_of_sub
,
crate
::
analysis
::
interprocedural_fixpoint_generic
::
NodeValue
::
Value
(
fn_start_state
),
);
}
fixpoint_computation
.compute_with_max_steps
(
100
);
let
(
logs
,
mut
cwe_warnings
)
=
log_thread
.collect
();
cwe_warnings
.sort
();
(
logs
,
cwe_warnings
)
}
src/cwe_checker_lib/src/checkers/cwe_416/state.rs
0 → 100644
View file @
242c5325
use
crate
::
analysis
::
pointer_inference
::
State
as
PiState
;
use
crate
::{
abstract_domain
::{
AbstractDomain
,
AbstractIdentifier
,
DomainMap
,
UnionMergeStrategy
},
analysis
::
pointer_inference
::
Data
,
prelude
::
*
,
};
use
std
::
collections
::
BTreeMap
;
/// The state of a memory object for which at least one possible call to a `free`-like function was detected.
#[derive(Serialize,
Deserialize,
Debug,
PartialEq,
Eq,
Clone)]
enum
ObjectState
{
/// The object is already freed, i.e. pointers to it are dangling.
/// The associated TID denotes the point in time when the object was freed.
Dangling
(
Tid
),
/// The object is already freed and a use-after-free CWE message for it was already generated.
/// This object state is used to prevent duplicate CWE warnings with the same root cause.
AlreadyFlagged
,
}
impl
AbstractDomain
for
ObjectState
{
/// Merge two object states.
/// If both object states are dangling then use the source TID of `self` in the result.
fn
merge
(
&
self
,
other
:
&
Self
)
->
Self
{
match
(
self
,
other
)
{
(
ObjectState
::
AlreadyFlagged
,
_
)
|
(
_
,
ObjectState
::
AlreadyFlagged
)
=>
{
ObjectState
::
AlreadyFlagged
}
(
ObjectState
::
Dangling
(
tid
),
ObjectState
::
Dangling
(
_
))
=>
{
ObjectState
::
Dangling
(
tid
.clone
())
}
}
}
/// The `Top` element for object states is a dangling pointer.
fn
is_top
(
&
self
)
->
bool
{
matches!
(
self
,
ObjectState
::
Dangling
(
_
))
}
}
/// The `State` currently only keeps track of the list of TIDs of memory object that may have been freed already
/// together with the corresponding object states.
#[derive(Serialize,
Deserialize,
Debug,
PartialEq,
Eq,
Clone)]
pub
struct
State
{
pub
current_fn_tid
:
Tid
,
dangling_objects
:
DomainMap
<
AbstractIdentifier
,
ObjectState
,
UnionMergeStrategy
>
,
}
impl
State
{
/// Create a new, empty state, i.e. a state without any object marked as already freed.
pub
fn
new
(
current_fn_tid
:
Tid
)
->
State
{
State
{
current_fn_tid
,
dangling_objects
:
BTreeMap
::
new
()
.into
(),
}
}
/// Check the given address on whether it may point to already freed memory.
/// For each possible dangling pointer target a string describing the root cause is returnen.
/// The object states of corresponding memory objects are set to [`ObjectState::AlreadyFlagged`]
/// to prevent reporting duplicate CWE messages with the same root cause.
pub
fn
check_address_for_use_after_free
(
&
mut
self
,
address
:
&
Data
)
->
Option
<
Vec
<
String
>>
{
let
mut
free_ids_of_dangling_pointers
=
Vec
::
new
();
for
id
in
address
.get_relative_values
()
.keys
()
{
if
let
Some
(
ObjectState
::
Dangling
(
free_id
))
=
self
.dangling_objects
.get
(
id
)
{
free_ids_of_dangling_pointers
.push
(
format!
(
"Accessed ID {} may have been already freed at {}"
,
id
,
free_id
));
self
.dangling_objects
.insert
(
id
.clone
(),
ObjectState
::
AlreadyFlagged
);
}
}
if
free_ids_of_dangling_pointers
.is_empty
()
{
None
}
else
{
Some
(
free_ids_of_dangling_pointers
)
}
}
/// All TIDs that the given `param` may point to are marked as freed, i.e. pointers to them are dangling.
/// For each ID that was already marked as dangling return a string describing the root cause of a possible double free bug.
pub
fn
handle_param_of_free_call
(
&
mut
self
,
call_tid
:
&
Tid
,
param
:
&
Data
,
pi_state
:
&
PiState
,
)
->
Option
<
Vec
<
String
>>
{
// FIXME: This function could also generate debug log messages whenever nonsensical information is detected.
// E.g. stack frame IDs or non-zero ID offsets can be indicators of other bugs.
let
mut
warnings
=
Vec
::
new
();
for
id
in
param
.get_relative_values
()
.keys
()
{
if
pi_state
.memory
.is_unique_object
(
id
)
.ok
()
==
Some
(
false
)
{
// FIXME: We cannot distinguish different objects represented by the same ID.
// So to avoid producing lots of false positive warnings
// we ignore these cases by not marking these IDs as freed.
continue
;
}
if
let
Some
(
ObjectState
::
Dangling
(
old_free_id
))
=
self
.dangling_objects
.insert
(
id
.clone
(),
ObjectState
::
Dangling
(
call_tid
.clone
()))
{
warnings
.push
(
format!
(
"Object {} may have been freed before at {}."
,
id
,
old_free_id
));
}
}
if
!
warnings
.is_empty
()
{
Some
(
warnings
)
}
else
{
None
}
}
/// Add objects that were freed in the callee of a function call to the list of dangling pointers of `self`.
/// May return a list of warnings if cases of possible double frees are detected,
/// i.e. if an already freed object may also have been freed in the callee.
pub
fn
collect_freed_objects_from_called_function
(
&
mut
self
,
state_before_return
:
&
State
,
id_replacement_map
:
&
BTreeMap
<
AbstractIdentifier
,
Data
>
,
call_tid
:
&
Tid
,
pi_state
:
&
PiState
,
)
{
for
(
callee_id
,
callee_object_state
)
in
state_before_return
.dangling_objects
.iter
()
{
if
let
Some
(
caller_value
)
=
id_replacement_map
.get
(
callee_id
)
{
for
caller_id
in
caller_value
.get_relative_values
()
.keys
()
{
if
pi_state
.memory
.is_unique_object
(
caller_id
)
.ok
()
!=
Some
(
false
)
{
// FIXME: We cannot distinguish different objects represented by the same ID.
// So to avoid producing lots of false positive warnings we ignore these cases.
match
(
callee_object_state
,
self
.dangling_objects
.get
(
caller_id
))
{
// Case 1: The dangling object is unknown to the caller, so we add it.
(
ObjectState
::
Dangling
(
_
),
None
)
|
(
ObjectState
::
AlreadyFlagged
,
None
)
=>
{
self
.dangling_objects
.insert
(
caller_id
.clone
(),
ObjectState
::
Dangling
(
call_tid
.clone
()),
);
}
// Case 2: The dangling object is already known to the caller.
// If this were a case of Use-After-Free, then this should have been flagged when checking the call parameters.
// Thus we can simply leave the object state as it is.
(
_
,
Some
(
ObjectState
::
Dangling
(
_
)))
|
(
_
,
Some
(
&
ObjectState
::
AlreadyFlagged
))
=>
(),
}
}
}
}
}
}
}
impl
AbstractDomain
for
State
{
/// Merge two states.
fn
merge
(
&
self
,
other
:
&
Self
)
->
Self
{
State
{
current_fn_tid
:
self
.current_fn_tid
.clone
(),
dangling_objects
:
self
.dangling_objects
.merge
(
&
other
.dangling_objects
),
}
}
/// Always returns false. The state has no logical `Top` element.
fn
is_top
(
&
self
)
->
bool
{
false
}
}
#[cfg(test)]
pub
mod
tests
{
use
crate
::
intermediate_representation
::
Variable
;
use
super
::
*
;
#[test]
fn
test_check_address_for_use_after_free
()
{
let
mut
state
=
State
::
new
(
Tid
::
new
(
"current_fn"
));
state
.dangling_objects
.insert
(
AbstractIdentifier
::
mock
(
"obj_id"
,
"RAX"
,
8
),
ObjectState
::
Dangling
(
Tid
::
new
(
"free_call"
)),
);
state
.dangling_objects
.insert
(
AbstractIdentifier
::
mock
(
"flagged_obj_id"
,
"RAX"
,
8
),
ObjectState
::
AlreadyFlagged
,
);
let
address
=
Data
::
mock_from_target_map
(
BTreeMap
::
from
([
(
AbstractIdentifier
::
mock
(
"obj_id"
,
"RAX"
,
8
),
Bitvector
::
from_i64
(
0
)
.into
(),
),
(
AbstractIdentifier
::
mock
(
"flagged_obj_id"
,
"RAX"
,
8
),
Bitvector
::
from_i64
(
0
)
.into
(),
),
]));
// Check that one warning is generated for the dangling pointer
// and that afterwards all corresponding IDs are marked as already flagged.
assert_eq!
(
state
.check_address_for_use_after_free
(
&
address
)
.unwrap
()
.len
(),
1
);
assert_eq!
(
*
state
.dangling_objects
.get
(
&
AbstractIdentifier
::
mock
(
"obj_id"
,
"RAX"
,
8
))
.unwrap
(),
ObjectState
::
AlreadyFlagged
);
assert_eq!
(
*
state
.dangling_objects
.get
(
&
AbstractIdentifier
::
mock
(
"flagged_obj_id"
,
"RAX"
,
8
))
.unwrap
(),
ObjectState
::
AlreadyFlagged
);
}
#[test]
fn
test_handle_param_of_free_call
()
{
let
mut
state
=
State
::
new
(
Tid
::
new
(
"current_fn"
));
let
param
=
Data
::
from_target
(
AbstractIdentifier
::
mock
(
"obj_id"
,
"RAX"
,
8
),
Bitvector
::
from_i64
(
0
)
.into
(),
);
let
pi_state
=
PiState
::
new
(
&
Variable
::
mock
(
"RSP"
,
8
),
Tid
::
new
(
"call"
));
// Check that the parameter is correctly marked as freed in the state.
assert
!
(
state
.handle_param_of_free_call
(
&
Tid
::
new
(
"free_call"
),
&
param
,
&
pi_state
)
.is_none
());
assert_eq!
(
*
state
.dangling_objects
.get
(
&
AbstractIdentifier
::
mock
(
"obj_id"
,
"RAX"
,
8
))
.unwrap
(),
ObjectState
::
Dangling
(
Tid
::
new
(
"free_call"
))
);
// Check that a second free operation yields a double free warning.
assert
!
(
state
.handle_param_of_free_call
(
&
Tid
::
new
(
"free_call"
),
&
param
,
&
pi_state
)
.is_some
());
}
#[test]
fn
test_collect_freed_objects_from_called_function
()
{
let
mut
state
=
State
::
new
(
Tid
::
new
(
"current_fn"
));
let
mut
state_before_return
=
State
::
new
(
Tid
::
new
(
"callee_fn_tid"
));
state_before_return
.dangling_objects
.insert
(
AbstractIdentifier
::
mock
(
"callee_obj_tid"
,
"RAX"
,
8
),
ObjectState
::
Dangling
(
Tid
::
new
(
"free_tid"
)),
);
let
pi_state
=
PiState
::
new
(
&
Variable
::
mock
(
"RSP"
,
8
),
Tid
::
new
(
"call"
));
let
id_replacement_map
=
BTreeMap
::
from
([(
AbstractIdentifier
::
mock
(
"callee_obj_tid"
,
"RAX"
,
8
),
Data
::
from_target
(
AbstractIdentifier
::
mock
(
"caller_tid"
,
"RBX"
,
8
),
Bitvector
::
from_i64
(
42
)
.into
(),
),
)]);
// Check that the callee object ID is correctly translated to a caller object ID
state
.collect_freed_objects_from_called_function
(
&
state_before_return
,
&
id_replacement_map
,
&
Tid
::
new
(
"call_tid"
),
&
pi_state
,
);
assert_eq!
(
state
.dangling_objects
.len
(),
1
);
assert_eq!
(
state
.dangling_objects
.get
(
&
AbstractIdentifier
::
mock
(
"caller_tid"
,
"RBX"
,
8
))
.unwrap
(),
&
ObjectState
::
Dangling
(
Tid
::
new
(
"call_tid"
))
);
}
}
src/cwe_checker_lib/src/lib.rs
View file @
242c5325
...
@@ -123,6 +123,7 @@ pub fn get_modules() -> Vec<&'static CweModule> {
...
@@ -123,6 +123,7 @@ pub fn get_modules() -> Vec<&'static CweModule> {
&
crate
::
checkers
::
cwe_243
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_243
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_332
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_332
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_367
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_367
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_416
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_426
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_426
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_467
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_467
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_476
::
CWE_MODULE
,
&
crate
::
checkers
::
cwe_476
::
CWE_MODULE
,
...
...
test/src/lib.rs
View file @
242c5325
...
@@ -423,7 +423,7 @@ mod tests {
...
@@ -423,7 +423,7 @@ mod tests {
#[ignore]
#[ignore]
fn
cwe_415
()
{
fn
cwe_415
()
{
let
mut
error_log
=
Vec
::
new
();
let
mut
error_log
=
Vec
::
new
();
let
mut
tests
=
all_test_cases
(
"cwe_415"
,
"
Memory
"
);
let
mut
tests
=
all_test_cases
(
"cwe_415"
,
"
CWE416
"
);
mark_architecture_skipped
(
&
mut
tests
,
"ppc64"
);
// Ghidra generates mangled function names here for some reason.
mark_architecture_skipped
(
&
mut
tests
,
"ppc64"
);
// Ghidra generates mangled function names here for some reason.
mark_architecture_skipped
(
&
mut
tests
,
"ppc64le"
);
// Ghidra generates mangled function names here for some reason.
mark_architecture_skipped
(
&
mut
tests
,
"ppc64le"
);
// Ghidra generates mangled function names here for some reason.
...
@@ -451,7 +451,7 @@ mod tests {
...
@@ -451,7 +451,7 @@ mod tests {
#[ignore]
#[ignore]
fn
cwe_416
()
{
fn
cwe_416
()
{
let
mut
error_log
=
Vec
::
new
();
let
mut
error_log
=
Vec
::
new
();
let
mut
tests
=
all_test_cases
(
"cwe_416"
,
"
Memory
"
);
let
mut
tests
=
all_test_cases
(
"cwe_416"
,
"
CWE416
"
);
mark_architecture_skipped
(
&
mut
tests
,
"ppc64"
);
// Ghidra generates mangled function names here for some reason.
mark_architecture_skipped
(
&
mut
tests
,
"ppc64"
);
// Ghidra generates mangled function names here for some reason.
mark_architecture_skipped
(
&
mut
tests
,
"ppc64le"
);
// Ghidra generates mangled function names here for some reason.
mark_architecture_skipped
(
&
mut
tests
,
"ppc64le"
);
// Ghidra generates mangled function names here for some reason.
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment